Skip to content

Commit 4cbfb6a

Browse files
committed
Tests: Test new getblockstats RPC
Includes commit from Anthony Towns @ajtowns: Tests: Save and load block and corresponding expected statistics
1 parent 35e77a0 commit 4cbfb6a

File tree

2 files changed

+181
-0
lines changed

2 files changed

+181
-0
lines changed

test/functional/rpc_getblockstats.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2017-2017 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+
from test_framework.test_framework import BitcoinTestFramework
10+
from test_framework.util import (
11+
assert_equal,
12+
assert_raises_rpc_error,
13+
)
14+
import json
15+
import os
16+
import time
17+
18+
TESTSDIR = os.path.dirname(os.path.realpath(__file__))
19+
20+
class GetblockstatsTest(BitcoinTestFramework):
21+
22+
start_height = 101
23+
max_stat_pos = 2
24+
STATS_NEED_TXINDEX = [
25+
'avgfee',
26+
'avgfeerate',
27+
'maxfee',
28+
'maxfeerate',
29+
'medianfee',
30+
'medianfeerate',
31+
'minfee',
32+
'minfeerate',
33+
'totalfee',
34+
'utxo_size_inc',
35+
]
36+
37+
def add_options(self, parser):
38+
parser.add_option('--gen-test-data', dest='gen_test_data',
39+
default=False, action='store_true',
40+
help='Generate test data')
41+
parser.add_option('--test-data', dest='test_data',
42+
default='data/rpc_getblockstats.json',
43+
action='store', metavar='FILE',
44+
help='Test data file')
45+
46+
def set_test_params(self):
47+
self.num_nodes = 2
48+
self.extra_args = [['-txindex'], ['-paytxfee=0.003']]
49+
self.setup_clean_chain = True
50+
51+
def get_stats(self):
52+
return [self.nodes[0].getblockstats(hash_or_height=self.start_height + i) for i in range(self.max_stat_pos+1)]
53+
54+
def generate_test_data(self, filename):
55+
mocktime = time.time()
56+
self.nodes[0].generate(101)
57+
58+
self.nodes[0].sendtoaddress(address=self.nodes[1].getnewaddress(), amount=10, subtractfeefromamount=True)
59+
self.nodes[0].generate(1)
60+
self.sync_all()
61+
62+
self.nodes[0].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=10, subtractfeefromamount=True)
63+
self.nodes[0].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=10, subtractfeefromamount=False)
64+
self.nodes[1].sendtoaddress(address=self.nodes[0].getnewaddress(), amount=1, subtractfeefromamount=True)
65+
self.sync_all()
66+
self.nodes[0].generate(1)
67+
68+
self.expected_stats = self.get_stats()
69+
70+
blocks = []
71+
tip = self.nodes[0].getbestblockhash()
72+
blockhash = None
73+
height = 0
74+
while tip != blockhash:
75+
blockhash = self.nodes[0].getblockhash(height)
76+
blocks.append(self.nodes[0].getblock(blockhash, 0))
77+
height += 1
78+
79+
to_dump = {
80+
'blocks': blocks,
81+
'mocktime': int(mocktime),
82+
'stats': self.expected_stats,
83+
}
84+
with open(filename, 'w') as f:
85+
json.dump(to_dump, f, sort_keys=True, indent=2)
86+
87+
def load_test_data(self, filename):
88+
with open(filename, 'r') as f:
89+
d = json.load(f)
90+
blocks = d['blocks']
91+
mocktime = d['mocktime']
92+
self.expected_stats = d['stats']
93+
94+
# Set the timestamps from the file so that the nodes can get out of Initial Block Download
95+
self.nodes[0].setmocktime(mocktime)
96+
self.nodes[1].setmocktime(mocktime)
97+
98+
for b in blocks:
99+
self.nodes[0].submitblock(b)
100+
101+
def run_test(self):
102+
test_data = os.path.join(TESTSDIR, self.options.test_data)
103+
if self.options.gen_test_data:
104+
self.generate_test_data(test_data)
105+
else:
106+
self.load_test_data(test_data)
107+
108+
self.sync_all()
109+
stats = self.get_stats()
110+
expected_stats_noindex = []
111+
for stat_row in stats:
112+
expected_stats_noindex.append({k: v for k, v in stat_row.items() if k not in self.STATS_NEED_TXINDEX})
113+
114+
# Make sure all valid statistics are included but nothing else is
115+
expected_keys = self.expected_stats[0].keys()
116+
assert_equal(set(stats[0].keys()), set(expected_keys))
117+
118+
assert_equal(stats[0]['height'], self.start_height)
119+
assert_equal(stats[self.max_stat_pos]['height'], self.start_height + self.max_stat_pos)
120+
121+
for i in range(self.max_stat_pos+1):
122+
self.log.info('Checking block %d\n' % (i))
123+
assert_equal(stats[i], self.expected_stats[i])
124+
125+
# Check selecting block by hash too
126+
blockhash = self.expected_stats[i]['blockhash']
127+
stats_by_hash = self.nodes[0].getblockstats(hash_or_height=blockhash)
128+
assert_equal(stats_by_hash, self.expected_stats[i])
129+
130+
# Check with the node that has no txindex
131+
stats_no_txindex = self.nodes[1].getblockstats(hash_or_height=blockhash, stats=list(expected_stats_noindex[i].keys()))
132+
assert_equal(stats_no_txindex, expected_stats_noindex[i])
133+
134+
# Make sure each stat can be queried on its own
135+
for stat in expected_keys:
136+
for i in range(self.max_stat_pos+1):
137+
result = self.nodes[0].getblockstats(hash_or_height=self.start_height + i, stats=[stat])
138+
assert_equal(list(result.keys()), [stat])
139+
if result[stat] != self.expected_stats[i][stat]:
140+
self.log.info('result[%s] (%d) failed, %r != %r' % (
141+
stat, i, result[stat], self.expected_stats[i][stat]))
142+
assert_equal(result[stat], self.expected_stats[i][stat])
143+
144+
# Make sure only the selected statistics are included (more than one)
145+
some_stats = {'minfee', 'maxfee'}
146+
stats = self.nodes[0].getblockstats(hash_or_height=1, stats=list(some_stats))
147+
assert_equal(set(stats.keys()), some_stats)
148+
149+
# Test invalid parameters raise the proper json exceptions
150+
tip = self.start_height + self.max_stat_pos
151+
assert_raises_rpc_error(-8, 'Target block height %d after current tip %d' % (tip+1, tip),
152+
self.nodes[0].getblockstats, hash_or_height=tip+1)
153+
assert_raises_rpc_error(-8, 'Target block height %d is negative' % (-1),
154+
self.nodes[0].getblockstats, hash_or_height=-1)
155+
156+
# Make sure not valid stats aren't allowed
157+
inv_sel_stat = 'asdfghjkl'
158+
inv_stats = [
159+
[inv_sel_stat],
160+
['minfee' , inv_sel_stat],
161+
[inv_sel_stat, 'minfee'],
162+
['minfee', inv_sel_stat, 'maxfee'],
163+
]
164+
for inv_stat in inv_stats:
165+
assert_raises_rpc_error(-8, 'Invalid selected statistic %s' % inv_sel_stat,
166+
self.nodes[0].getblockstats, hash_or_height=1, stats=inv_stat)
167+
168+
# Make sure we aren't always returning inv_sel_stat as the culprit stat
169+
assert_raises_rpc_error(-8, 'Invalid selected statistic aaa%s' % inv_sel_stat,
170+
self.nodes[0].getblockstats, hash_or_height=1, stats=['minfee' , 'aaa%s' % inv_sel_stat])
171+
172+
assert_raises_rpc_error(-8, 'One or more of the selected stats requires -txindex enabled',
173+
self.nodes[1].getblockstats, hash_or_height=self.start_height + self.max_stat_pos)
174+
175+
# Mainchain's genesis block shouldn't be found on regtest
176+
assert_raises_rpc_error(-5, 'Block not found', self.nodes[0].getblockstats,
177+
hash_or_height='000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f')
178+
179+
if __name__ == '__main__':
180+
GetblockstatsTest().main()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
'wallet_resendwallettransactions.py',
136136
'wallet_fallbackfee.py',
137137
'feature_minchainwork.py',
138+
'rpc_getblockstats.py',
138139
'p2p_fingerprint.py',
139140
'feature_uacomment.py',
140141
'p2p_unrequested_blocks.py',

0 commit comments

Comments
 (0)