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