BLURT is under heavy spam attack right now

When viewing the stats on https://blocktivity.info/ its seems that BLURT has almost reached the same activity as HIVE:

blocktivity stats

The blockchain operation counts are correct this time, so I wrote a python script to count the different operation types.

import sys
from datetime import datetime, timedelta
from prettytable import PrettyTable
import argparse
from timeit import default_timer as timer
import logging
from beem.blockchain import Blockchain
from beem.block import Block
from beem import Hive, Blurt, Steem
from beem.utils import parse_time
from beem.nodelist import NodeList

log = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)


def parse_args(args=None):
    d = 'Show op type stats for either hive, blurt or steem.'
    parser = argparse.ArgumentParser(description=d)
    parser.add_argument('blockchain', type=str, nargs='?',
                        default=sys.stdin,
                        help='Blockchain (hive, blurt or steem)')
    return parser.parse_args(args)


def main(args=None):
    
    args = parse_args(args)
    blockchain = args.blockchain
    
    nodelist = NodeList()
    nodelist.update_nodes(weights={"block": 1})
    
    if blockchain == "hive" or blockchain is None:
        max_batch_size = 50
        threading = False
        thread_num = 16
        block_debug = 1000
        
        nodes = nodelist.get_hive_nodes()
        blk_inst = Hive(node=nodes, num_retries=3, num_retries_call=3, timeout=30)
    elif blockchain == "blurt":
        max_batch_size = None
        threading = False
        thread_num = 8
        block_debug = 20
        nodes = ["https://rpc.blurt.buzz/", "https://api.blurt.blog", "https://rpc.blurtworld.com", "https://rpc.blurtworld.com"]
        blk_inst = Blurt(node=nodes, num_retries=3, num_retries_call=3, timeout=30)
    elif blockchain == "steem":
        max_batch_size = 50
        threading = False
        thread_num = 16
        block_debug = 1000
        nodes = nodelist.get_steem_nodes()
        blk_inst = Steem(node=nodes, num_retries=3, num_retries_call=3, timeout=30)
    else:
        raise Exception("Wrong parameter, can be hive, blurt or steem")
    print(blk_inst)
    block_count = 0
    total_ops = 0
    total_trx = 0
    duration_s = 60 * 60 * 1
    blocksperday = int(duration_s / 3)
    
    blockchain = Blockchain(blockchain_instance=blk_inst, )
    current_block_num = blockchain.get_current_block_num()
    last_block_id = current_block_num - blocksperday

    last_block = Block(last_block_id, blockchain_instance=blk_inst)

    stopTime = last_block.time() + timedelta(seconds=duration_s)

    start = timer()
    op_stats = {}
    for entry in blockchain.blocks(start=last_block_id, max_batch_size=max_batch_size, threading=threading, thread_num=thread_num):
        if "block" in entry:
            block_time = parse_time(entry["block"]["timestamp"])
        else:
            block_time = entry["timestamp"]
        if block_time > stopTime:
            break
        block_count += 1
        if "block" in entry:
            trxs = entry["block"]["transactions"]
        else:
            trxs = entry["transactions"]
        for tx in trxs:
            total_trx += 1
            for op in tx["operations"]:
                if "_operation" in op["type"]:
                    op_type = op["type"][:-10]
                else:
                    op_type = op["type"]
                if op_type in op_stats:
                    op_stats[op_type] += 1
                else:
                    op_stats[op_type] = 1
                total_ops += 1

        ops_per_day = total_ops / block_count * blocksperday
        if block_count % (block_debug) == 0:
            print("%d blocks remaining... estimated ops per day: %.1f" % (blocksperday - block_count, ops_per_day))

    duration = timer() - start    
    t = PrettyTable(["Type", "Count", "percentage"])
    t.align = "l"
    op_list = []
    for o in op_stats:
        op_list.append({"type": o, "n": op_stats[o], "perc": op_stats[o] / total_ops * 100})
    op_list_sorted = sorted(op_list, key=lambda x: x['n'], reverse=True)
    for op in op_list_sorted:
        t.add_row([op["type"], op["n"], "%.2f %%" % op["perc"]])
    print(t)
if __name__ == '__main__':
    sys.exit(main())

You need to install beem (pip3 install beem) and can then start the script (after stored it as blockstats.py):

python3 blockstats.py hive
python3 blockstats.py blurt

As the API speed of BLURT is limited, I limit the time period to the last hour.
(It can be adapted at line 62).

Results

Hive

+-------------------------+-------+------------+
| Type                    | Count | percentage |
+-------------------------+-------+------------+
| custom_json             | 22413 | 58.24 %    |
| vote                    | 13786 | 35.82 %    |
| comment                 | 716   | 1.86 %     |
| transfer                | 554   | 1.44 %     |
| claim_reward_balance    | 398   | 1.03 %     |
| comment_options         | 209   | 0.54 %     |
| limit_order_create      | 86    | 0.22 %     |
| claim_account           | 84    | 0.22 %     |
| feed_publish            | 76    | 0.20 %     |
| limit_order_cancel      | 57    | 0.15 %     |
| delegate_vesting_shares | 27    | 0.07 %     |
| create_claimed_account  | 11    | 0.03 %     |
| transfer_to_vesting     | 10    | 0.03 %     |
| update_proposal_votes   | 10    | 0.03 %     |
| convert                 | 9     | 0.02 %     |
| account_update          | 8     | 0.02 %     |
| account_update2         | 8     | 0.02 %     |
| delete_comment          | 6     | 0.02 %     |
| witness_set_properties  | 6     | 0.02 %     |
| account_witness_vote    | 5     | 0.01 %     |
| withdraw_vesting        | 4     | 0.01 %     |
| transfer_to_savings     | 1     | 0.00 %     |
| transfer_from_savings   | 1     | 0.00 %     |
+-------------------------+-------+------------+

BLURT

+------------------------+-------+------------+
| Type                   | Count | percentage |
+------------------------+-------+------------+
| witness_set_properties | 37589 | 99.88 %    |
| claim_reward_balance   | 16    | 0.04 %     |
| vote                   | 14    | 0.04 %     |
| comment                | 9     | 0.02 %     |
| comment_options        | 4     | 0.01 %     |
| transfer               | 1     | 0.00 %     |
| custom_json            | 1     | 0.00 %     |
+------------------------+-------+------------+

Spam attack on BLURT

This issue indicates that the broadcasted witness_set_properties operations are indeed an attack to the BLURT blockchain:
https://gitlab.com/blurt/blurt/-/issues/89
image.png

Transaction fee on BLURT

image.png
The bandwidth has increased from 0.01 BLURT/KByte(as far as I remember) to 0.250 BLURT/KByte. As the transaction fee has to be paid in front for every broadcasted operation from the users wallet, this 25x increase is painful.

Blurt price

The BLURT price has reacted to the spam attack and is almost 1 Sat/BLURT:
image.png

Conclusion

99.88 % of all operations on the BLURT blockchain are spam witness_set_properties operations. 9 posts/comments have been written in the last hour and 14 votes have been cast. This is not much in comparison to 716 posts / comments and 13786 votes on Hive. This indicates that the attack works and blurt user have almost stopped using the chain. This is also indicated by the price movement.

The strange thing is that the attacker seems not to pay a bandwidth fee for their attack:
image.png

https://blocks.blurtwallet.com/#/@gbemyktquwtpfizr
image.png
As it can be seen that the blurt wallet is almost empty, it seems to me that the attacker have found a way to cast their spam ops for free.

I found the problem:
https://gitlab.com/blurt/blurt/-/blob/dev/libraries/plugins/rc/include/blurt/plugins/rc/resource_sizes.hpp :
witness_set_properties has no
blurt::plugins::rc::state_object_size_info properties, which means that broadcasting a witness_set_properties op seems not to costs any blurt fee.

H2
H3
H4
3 columns
2 columns
1 column
78 Comments
Ecency