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