rpc_getblockstats.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2017-present The Bitcoin Core developers 3 # Distributed under the MIT software license, see the accompanying 4 # file COPYING or http://www.opensource.org/licenses/mit-license.php. 5 6 # 7 # Test getblockstats rpc call 8 # 9 10 from test_framework.blocktools import COINBASE_MATURITY 11 from test_framework.test_framework import BitcoinTestFramework 12 from test_framework.util import ( 13 assert_equal, 14 assert_raises_rpc_error, 15 wallet_importprivkey, 16 ) 17 import json 18 import os 19 20 TESTSDIR = os.path.dirname(os.path.realpath(__file__)) 21 22 class GetblockstatsTest(BitcoinTestFramework): 23 24 start_height = 101 25 max_stat_pos = 2 26 27 def add_options(self, parser): 28 parser.add_argument('--gen-test-data', dest='gen_test_data', 29 default=False, action='store_true', 30 help='Generate test data') 31 parser.add_argument('--test-data', dest='test_data', 32 default='data/rpc_getblockstats.json', 33 action='store', metavar='FILE', 34 help='Test data file') 35 36 def set_test_params(self): 37 self.num_nodes = 1 38 self.setup_clean_chain = True 39 40 def get_stats(self): 41 return [self.nodes[0].getblockstats(hash_or_height=self.start_height + i) for i in range(self.max_stat_pos+1)] 42 43 def generate_test_data(self, filename): 44 mocktime = 1525107225 45 self.nodes[0].setmocktime(mocktime) 46 self.nodes[0].createwallet(wallet_name='test') 47 privkey = self.nodes[0].get_deterministic_priv_key().key 48 wallet_importprivkey(self.nodes[0], privkey, 0) 49 50 self.generate(self.nodes[0], COINBASE_MATURITY + 1) 51 52 address = self.nodes[0].get_deterministic_priv_key().address 53 self.nodes[0].sendtoaddress(address=address, amount=10, subtractfeefromamount=True) 54 self.generate(self.nodes[0], 1) 55 56 self.nodes[0].sendtoaddress(address=address, amount=10, subtractfeefromamount=True) 57 self.nodes[0].sendtoaddress(address=address, amount=10, subtractfeefromamount=False) 58 self.fee_rate=300 59 self.nodes[0].sendtoaddress(address=address, amount=1, subtractfeefromamount=True, fee_rate=self.fee_rate) 60 # Send to OP_RETURN output to test its exclusion from statistics 61 self.nodes[0].send(outputs={"data": "21"}, fee_rate=self.fee_rate) 62 self.sync_all() 63 self.generate(self.nodes[0], 1) 64 65 self.expected_stats = self.get_stats() 66 67 blocks = [] 68 tip = self.nodes[0].getbestblockhash() 69 blockhash = None 70 height = 0 71 while tip != blockhash: 72 blockhash = self.nodes[0].getblockhash(height) 73 blocks.append(self.nodes[0].getblock(blockhash, 0)) 74 height += 1 75 76 to_dump = { 77 'blocks': blocks, 78 'mocktime': int(mocktime), 79 'stats': self.expected_stats, 80 } 81 with open(filename, 'w') as f: 82 json.dump(to_dump, f, sort_keys=True, indent=2) 83 84 def load_test_data(self, filename): 85 with open(filename, 'r') as f: 86 d = json.load(f) 87 blocks = d['blocks'] 88 mocktime = d['mocktime'] 89 self.expected_stats = d['stats'] 90 91 # Set the timestamps from the file so that the nodes can get out of Initial Block Download 92 self.nodes[0].setmocktime(mocktime) 93 self.sync_all() 94 95 for b in blocks: 96 self.nodes[0].submitblock(b) 97 98 99 def run_test(self): 100 test_data = os.path.join(TESTSDIR, self.options.test_data) 101 if self.options.gen_test_data: 102 self.generate_test_data(test_data) 103 else: 104 self.load_test_data(test_data) 105 106 self.sync_all() 107 stats = self.get_stats() 108 109 # Make sure all valid statistics are included but nothing else is 110 expected_keys = self.expected_stats[0].keys() 111 assert_equal(set(stats[0].keys()), set(expected_keys)) 112 113 assert_equal(stats[0]['height'], self.start_height) 114 assert_equal(stats[self.max_stat_pos]['height'], self.start_height + self.max_stat_pos) 115 116 for i in range(self.max_stat_pos+1): 117 self.log.info('Checking block %d' % (i)) 118 assert_equal(stats[i], self.expected_stats[i]) 119 120 # Check selecting block by hash too 121 blockhash = self.expected_stats[i]['blockhash'] 122 stats_by_hash = self.nodes[0].getblockstats(hash_or_height=blockhash) 123 assert_equal(stats_by_hash, self.expected_stats[i]) 124 125 # Make sure each stat can be queried on its own 126 for stat in expected_keys: 127 for i in range(self.max_stat_pos+1): 128 result = self.nodes[0].getblockstats(hash_or_height=self.start_height + i, stats=[stat]) 129 assert_equal(list(result.keys()), [stat]) 130 if result[stat] != self.expected_stats[i][stat]: 131 self.log.info('result[%s] (%d) failed, %r != %r' % ( 132 stat, i, result[stat], self.expected_stats[i][stat])) 133 assert_equal(result[stat], self.expected_stats[i][stat]) 134 135 # Make sure only the selected statistics are included (more than one) 136 some_stats = {'minfee', 'maxfee'} 137 stats = self.nodes[0].getblockstats(hash_or_height=1, stats=list(some_stats)) 138 assert_equal(set(stats.keys()), some_stats) 139 140 # Test invalid parameters raise the proper json exceptions 141 tip = self.start_height + self.max_stat_pos 142 assert_raises_rpc_error(-8, 'Target block height %d after current tip %d' % (tip+1, tip), 143 self.nodes[0].getblockstats, hash_or_height=tip+1) 144 assert_raises_rpc_error(-8, 'Target block height %d is negative' % (-1), 145 self.nodes[0].getblockstats, hash_or_height=-1) 146 147 # Make sure not valid stats aren't allowed 148 inv_sel_stat = 'asdfghjkl' 149 inv_stats = [ 150 [inv_sel_stat], 151 ['minfee', inv_sel_stat], 152 [inv_sel_stat, 'minfee'], 153 ['minfee', inv_sel_stat, 'maxfee'], 154 ] 155 for inv_stat in inv_stats: 156 assert_raises_rpc_error(-8, f"Invalid selected statistic '{inv_sel_stat}'", 157 self.nodes[0].getblockstats, hash_or_height=1, stats=inv_stat) 158 159 # Make sure we aren't always returning inv_sel_stat as the culprit stat 160 assert_raises_rpc_error(-8, f"Invalid selected statistic 'aaa{inv_sel_stat}'", 161 self.nodes[0].getblockstats, hash_or_height=1, stats=['minfee', f'aaa{inv_sel_stat}']) 162 # Mainchain's genesis block shouldn't be found on regtest 163 assert_raises_rpc_error(-5, 'Block not found', self.nodes[0].getblockstats, 164 hash_or_height='000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f') 165 166 # Invalid number of args 167 assert_raises_rpc_error(-1, 'getblockstats hash_or_height ( stats )', self.nodes[0].getblockstats, '00', 1, 2) 168 assert_raises_rpc_error(-1, 'getblockstats hash_or_height ( stats )', self.nodes[0].getblockstats) 169 170 self.log.info('Test block height 0') 171 genesis_stats = self.nodes[0].getblockstats(0) 172 assert_equal(genesis_stats["blockhash"], "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206") 173 assert_equal(genesis_stats["utxo_increase"], 1) 174 assert_equal(genesis_stats["utxo_size_inc"], 116) 175 assert_equal(genesis_stats["utxo_increase_actual"], 0) 176 assert_equal(genesis_stats["utxo_size_inc_actual"], 0) 177 178 self.log.info('Test tip including OP_RETURN') 179 tip_stats = self.nodes[0].getblockstats(tip) 180 assert_equal(tip_stats["utxo_increase"], 6) 181 assert_equal(tip_stats["utxo_size_inc"], 435) 182 assert_equal(tip_stats["utxo_increase_actual"], 4) 183 assert_equal(tip_stats["utxo_size_inc_actual"], 296) 184 185 self.log.info("Test when only header is known") 186 block = self.generateblock(self.nodes[0], output="raw(55)", transactions=[], submit=False) 187 self.nodes[0].submitheader(block["hex"]) 188 assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", lambda: self.nodes[0].getblockstats(block['hash'])) 189 190 self.log.info('Test when block is missing') 191 (self.nodes[0].blocks_path / 'blk00000.dat').rename(self.nodes[0].blocks_path / 'blk00000.dat.backup') 192 assert_raises_rpc_error(-1, 'Block not found on disk', self.nodes[0].getblockstats, hash_or_height=1) 193 (self.nodes[0].blocks_path / 'blk00000.dat.backup').rename(self.nodes[0].blocks_path / 'blk00000.dat') 194 195 196 if __name__ == '__main__': 197 GetblockstatsTest(__file__).main()