feature_coinstatsindex.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2020-2022 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 """Test coinstatsindex across nodes. 6 7 Test that the values returned by gettxoutsetinfo are consistent 8 between a node running the coinstatsindex and a node without 9 the index. 10 """ 11 12 from decimal import Decimal 13 14 from test_framework.blocktools import ( 15 COINBASE_MATURITY, 16 create_block, 17 create_coinbase, 18 ) 19 from test_framework.messages import ( 20 COIN, 21 CTxOut, 22 ) 23 from test_framework.script import ( 24 CScript, 25 OP_FALSE, 26 OP_RETURN, 27 ) 28 from test_framework.test_framework import BitcoinTestFramework 29 from test_framework.util import ( 30 assert_equal, 31 assert_raises_rpc_error, 32 ) 33 from test_framework.wallet import ( 34 MiniWallet, 35 getnewdestination, 36 ) 37 38 39 class CoinStatsIndexTest(BitcoinTestFramework): 40 def set_test_params(self): 41 self.setup_clean_chain = True 42 self.num_nodes = 2 43 self.supports_cli = False 44 self.extra_args = [ 45 [], 46 ["-coinstatsindex"] 47 ] 48 49 def run_test(self): 50 self.wallet = MiniWallet(self.nodes[0]) 51 self._test_coin_stats_index() 52 self._test_use_index_option() 53 self._test_reorg_index() 54 self._test_index_rejects_hash_serialized() 55 self._test_init_index_after_reorg() 56 57 def block_sanity_check(self, block_info): 58 block_subsidy = 50 59 assert_equal( 60 block_info['prevout_spent'] + block_subsidy, 61 block_info['new_outputs_ex_coinbase'] + block_info['coinbase'] + block_info['unspendable'] 62 ) 63 64 def sync_index_node(self): 65 self.wait_until(lambda: self.nodes[1].getindexinfo()['coinstatsindex']['synced'] is True) 66 67 def _test_coin_stats_index(self): 68 node = self.nodes[0] 69 index_node = self.nodes[1] 70 # Both none and muhash options allow the usage of the index 71 index_hash_options = ['none', 'muhash'] 72 73 # Generate a normal transaction and mine it 74 self.generate(self.wallet, COINBASE_MATURITY + 1) 75 self.wallet.send_self_transfer(from_node=node) 76 self.generate(node, 1) 77 78 self.log.info("Test that gettxoutsetinfo() output is consistent with or without coinstatsindex option") 79 res0 = node.gettxoutsetinfo('none') 80 81 # The fields 'disk_size' and 'transactions' do not exist on the index 82 del res0['disk_size'], res0['transactions'] 83 84 for hash_option in index_hash_options: 85 res1 = index_node.gettxoutsetinfo(hash_option) 86 # The fields 'block_info' and 'total_unspendable_amount' only exist on the index 87 del res1['block_info'], res1['total_unspendable_amount'] 88 res1.pop('muhash', None) 89 90 # Everything left should be the same 91 assert_equal(res1, res0) 92 93 self.log.info("Test that gettxoutsetinfo() can get fetch data on specific heights with index") 94 95 # Generate a new tip 96 self.generate(node, 5) 97 98 for hash_option in index_hash_options: 99 # Fetch old stats by height 100 res2 = index_node.gettxoutsetinfo(hash_option, 102) 101 del res2['block_info'], res2['total_unspendable_amount'] 102 res2.pop('muhash', None) 103 assert_equal(res0, res2) 104 105 # Fetch old stats by hash 106 res3 = index_node.gettxoutsetinfo(hash_option, res0['bestblock']) 107 del res3['block_info'], res3['total_unspendable_amount'] 108 res3.pop('muhash', None) 109 assert_equal(res0, res3) 110 111 # It does not work without coinstatsindex 112 assert_raises_rpc_error(-8, "Querying specific block heights requires coinstatsindex", node.gettxoutsetinfo, hash_option, 102) 113 114 self.log.info("Test gettxoutsetinfo() with index and verbose flag") 115 116 for hash_option in index_hash_options: 117 # Genesis block is unspendable 118 res4 = index_node.gettxoutsetinfo(hash_option, 0) 119 assert_equal(res4['total_unspendable_amount'], 50) 120 assert_equal(res4['block_info'], { 121 'unspendable': 50, 122 'prevout_spent': 0, 123 'new_outputs_ex_coinbase': 0, 124 'coinbase': 0, 125 'unspendables': { 126 'genesis_block': 50, 127 'bip30': 0, 128 'scripts': 0, 129 'unclaimed_rewards': 0 130 } 131 }) 132 self.block_sanity_check(res4['block_info']) 133 134 # Test an older block height that included a normal tx 135 res5 = index_node.gettxoutsetinfo(hash_option, 102) 136 assert_equal(res5['total_unspendable_amount'], 50) 137 assert_equal(res5['block_info'], { 138 'unspendable': 0, 139 'prevout_spent': 50, 140 'new_outputs_ex_coinbase': Decimal('49.99968800'), 141 'coinbase': Decimal('50.00031200'), 142 'unspendables': { 143 'genesis_block': 0, 144 'bip30': 0, 145 'scripts': 0, 146 'unclaimed_rewards': 0, 147 } 148 }) 149 self.block_sanity_check(res5['block_info']) 150 151 # Generate and send a normal tx with two outputs 152 tx1 = self.wallet.send_to( 153 from_node=node, 154 scriptPubKey=self.wallet.get_scriptPubKey(), 155 amount=21 * COIN, 156 ) 157 158 # Find the right position of the 21 BTC output 159 tx1_out_21 = self.wallet.get_utxo(txid=tx1["txid"], vout=tx1["sent_vout"]) 160 161 # Generate and send another tx with an OP_RETURN output (which is unspendable) 162 tx2 = self.wallet.create_self_transfer(utxo_to_spend=tx1_out_21)['tx'] 163 tx2_val = '20.99' 164 tx2.vout = [CTxOut(int(Decimal(tx2_val) * COIN), CScript([OP_RETURN] + [OP_FALSE] * 30))] 165 tx2_hex = tx2.serialize().hex() 166 self.nodes[0].sendrawtransaction(tx2_hex, 0, tx2_val) 167 168 # Include both txs in a block 169 self.generate(self.nodes[0], 1) 170 171 for hash_option in index_hash_options: 172 # Check all amounts were registered correctly 173 res6 = index_node.gettxoutsetinfo(hash_option, 108) 174 assert_equal(res6['total_unspendable_amount'], Decimal('70.99000000')) 175 assert_equal(res6['block_info'], { 176 'unspendable': Decimal('20.99000000'), 177 'prevout_spent': 71, 178 'new_outputs_ex_coinbase': Decimal('49.99999000'), 179 'coinbase': Decimal('50.01001000'), 180 'unspendables': { 181 'genesis_block': 0, 182 'bip30': 0, 183 'scripts': Decimal('20.99000000'), 184 'unclaimed_rewards': 0, 185 } 186 }) 187 self.block_sanity_check(res6['block_info']) 188 189 # Create a coinbase that does not claim full subsidy and also 190 # has two outputs 191 cb = create_coinbase(109, nValue=35) 192 cb.vout.append(CTxOut(5 * COIN, CScript([OP_FALSE]))) 193 cb.rehash() 194 195 # Generate a block that includes previous coinbase 196 tip = self.nodes[0].getbestblockhash() 197 block_time = self.nodes[0].getblock(tip)['time'] + 1 198 block = create_block(int(tip, 16), cb, block_time) 199 block.solve() 200 self.nodes[0].submitblock(block.serialize().hex()) 201 self.sync_all() 202 203 for hash_option in index_hash_options: 204 res7 = index_node.gettxoutsetinfo(hash_option, 109) 205 assert_equal(res7['total_unspendable_amount'], Decimal('80.99000000')) 206 assert_equal(res7['block_info'], { 207 'unspendable': 10, 208 'prevout_spent': 0, 209 'new_outputs_ex_coinbase': 0, 210 'coinbase': 40, 211 'unspendables': { 212 'genesis_block': 0, 213 'bip30': 0, 214 'scripts': 0, 215 'unclaimed_rewards': 10 216 } 217 }) 218 self.block_sanity_check(res7['block_info']) 219 220 self.log.info("Test that the index is robust across restarts") 221 222 res8 = index_node.gettxoutsetinfo('muhash') 223 self.restart_node(1, extra_args=self.extra_args[1]) 224 res9 = index_node.gettxoutsetinfo('muhash') 225 assert_equal(res8, res9) 226 227 self.generate(index_node, 1, sync_fun=self.no_op) 228 res10 = index_node.gettxoutsetinfo('muhash') 229 assert res8['txouts'] < res10['txouts'] 230 231 self.log.info("Test that the index works with -reindex") 232 233 self.restart_node(1, extra_args=["-coinstatsindex", "-reindex"]) 234 self.sync_index_node() 235 res11 = index_node.gettxoutsetinfo('muhash') 236 assert_equal(res11, res10) 237 238 self.log.info("Test that the index works with -reindex-chainstate") 239 240 self.restart_node(1, extra_args=["-coinstatsindex", "-reindex-chainstate"]) 241 self.sync_index_node() 242 res12 = index_node.gettxoutsetinfo('muhash') 243 assert_equal(res12, res10) 244 245 def _test_use_index_option(self): 246 self.log.info("Test use_index option for nodes running the index") 247 248 self.connect_nodes(0, 1) 249 self.nodes[0].waitforblockheight(110) 250 res = self.nodes[0].gettxoutsetinfo('muhash') 251 option_res = self.nodes[1].gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False) 252 del res['disk_size'], option_res['disk_size'] 253 assert_equal(res, option_res) 254 255 def _test_reorg_index(self): 256 self.log.info("Test that index can handle reorgs") 257 258 # Generate two block, let the index catch up, then invalidate the blocks 259 index_node = self.nodes[1] 260 reorg_blocks = self.generatetoaddress(index_node, 2, getnewdestination()[2]) 261 reorg_block = reorg_blocks[1] 262 self.sync_index_node() 263 res_invalid = index_node.gettxoutsetinfo('muhash') 264 index_node.invalidateblock(reorg_blocks[0]) 265 assert_equal(index_node.gettxoutsetinfo('muhash')['height'], 110) 266 267 # Add two new blocks 268 block = self.generate(index_node, 2, sync_fun=self.no_op)[1] 269 res = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False) 270 271 # Test that the result of the reorged block is not returned for its old block height 272 res2 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=112) 273 assert_equal(res["bestblock"], block) 274 assert_equal(res["muhash"], res2["muhash"]) 275 assert res["muhash"] != res_invalid["muhash"] 276 277 # Test that requesting reorged out block by hash is still returning correct results 278 res_invalid2 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=reorg_block) 279 assert_equal(res_invalid2["muhash"], res_invalid["muhash"]) 280 assert res["muhash"] != res_invalid2["muhash"] 281 282 # Add another block, so we don't depend on reconsiderblock remembering which 283 # blocks were touched by invalidateblock 284 self.generate(index_node, 1) 285 286 # Ensure that removing and re-adding blocks yields consistent results 287 block = index_node.getblockhash(99) 288 index_node.invalidateblock(block) 289 index_node.reconsiderblock(block) 290 res3 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=112) 291 assert_equal(res2, res3) 292 293 def _test_index_rejects_hash_serialized(self): 294 self.log.info("Test that the rpc raises if the legacy hash is passed with the index") 295 296 msg = "hash_serialized_3 hash type cannot be queried for a specific block" 297 assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_3', hash_or_height=111) 298 299 for use_index in {True, False, None}: 300 assert_raises_rpc_error(-8, msg, self.nodes[1].gettxoutsetinfo, hash_type='hash_serialized_3', hash_or_height=111, use_index=use_index) 301 302 def _test_init_index_after_reorg(self): 303 self.log.info("Test a reorg while the index is deactivated") 304 index_node = self.nodes[1] 305 block = self.nodes[0].getbestblockhash() 306 self.generate(index_node, 2, sync_fun=self.no_op) 307 self.sync_index_node() 308 309 # Restart without index 310 self.restart_node(1, extra_args=[]) 311 self.connect_nodes(0, 1) 312 index_node.invalidateblock(block) 313 self.generatetoaddress(index_node, 5, getnewdestination()[2]) 314 res = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=False) 315 316 # Restart with index that still has its best block on the old chain 317 self.restart_node(1, extra_args=self.extra_args[1]) 318 self.sync_index_node() 319 res1 = index_node.gettxoutsetinfo(hash_type='muhash', hash_or_height=None, use_index=True) 320 assert_equal(res["muhash"], res1["muhash"]) 321 322 323 if __name__ == '__main__': 324 CoinStatsIndexTest().main()