rpc_getblockstats.py raw
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()
198