diff --git a/test/functional/feature_coinstatsindex.py b/test/functional/feature_coinstatsindex.py index d154e0fc1..a0c9028ea 100755 --- a/test/functional/feature_coinstatsindex.py +++ b/test/functional/feature_coinstatsindex.py @@ -1,341 +1,327 @@ #!/usr/bin/env python3 # Copyright (c) 2020 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test coinstatsindex across nodes. Test that the values returned by gettxoutsetinfo are consistent between a node running the coinstatsindex and a node without the index. """ from decimal import Decimal from test_framework.blocktools import create_block, create_coinbase from test_framework.messages import XEC, CTxOut, ToHex from test_framework.script import OP_FALSE, OP_RETURN, CScript from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error from test_framework.wallet import MiniWallet, getnewdestination class CoinStatsIndexTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.supports_cli = False self.extra_args = [ [ "-automaticunparking=1", ], [ "-coinstatsindex", ], ] def run_test(self): self.wallet = MiniWallet(self.nodes[0]) self._test_coin_stats_index() self._test_use_index_option() self._test_reorg_index() self._test_index_rejects_hash_serialized() def block_sanity_check(self, block_info): block_subsidy = 50_000_000 assert_equal( block_info["prevout_spent"] + block_subsidy, block_info["new_outputs_ex_coinbase"] + block_info["coinbase"] + block_info["unspendable"], ) def _test_coin_stats_index(self): node = self.nodes[0] index_node = self.nodes[1] # Both none and muhash options allow the usage of the index index_hash_options = ["none", "muhash"] # Generate a normal transaction and mine it self.generate(self.wallet, 101) self.wallet.send_self_transfer(from_node=node) self.generate(node, 1) self.log.info( "Test that gettxoutsetinfo() output is consistent with or without" " coinstatsindex option" ) res0 = node.gettxoutsetinfo("none") # The fields 'disk_size' and 'transactions' do not exist on the index del res0["disk_size"], res0["transactions"] for hash_option in index_hash_options: res1 = index_node.gettxoutsetinfo(hash_option) # The fields 'block_info' and 'total_unspendable_amount' only exist # on the index del res1["block_info"], res1["total_unspendable_amount"] res1.pop("muhash", None) # Everything left should be the same assert_equal(res1, res0) self.log.info( "Test that gettxoutsetinfo() can get fetch data on specific " "heights with index" ) # Generate a new tip self.generate(node, 5) for hash_option in index_hash_options: # Fetch old stats by height res2 = index_node.gettxoutsetinfo(hash_option, 102) del res2["block_info"], res2["total_unspendable_amount"] res2.pop("muhash", None) assert_equal(res0, res2) # Fetch old stats by hash res3 = index_node.gettxoutsetinfo(hash_option, res0["bestblock"]) del res3["block_info"], res3["total_unspendable_amount"] res3.pop("muhash", None) assert_equal(res0, res3) # It does not work without coinstatsindex assert_raises_rpc_error( -8, "Querying specific block heights requires coinstatsindex", node.gettxoutsetinfo, hash_option, 102, ) self.log.info("Test gettxoutsetinfo() with index and verbose flag") for hash_option in index_hash_options: # Genesis block is unspendable res4 = index_node.gettxoutsetinfo(hash_option, 0) assert_equal(res4["total_unspendable_amount"], 50_000_000) assert_equal( res4["block_info"], { "unspendable": 50_000_000, "prevout_spent": 0, "new_outputs_ex_coinbase": 0, "coinbase": 0, "unspendables": { "genesis_block": 50_000_000, "bip30": 0, "scripts": 0, "unclaimed_rewards": 0, }, }, ) self.block_sanity_check(res4["block_info"]) # Test an older block height that included a normal tx res5 = index_node.gettxoutsetinfo(hash_option, 102) assert_equal(res5["total_unspendable_amount"], 50_000_000) assert_equal( res5["block_info"], { "unspendable": 0, "prevout_spent": 50_000_000, "new_outputs_ex_coinbase": Decimal("49999700.00"), "coinbase": Decimal("50000300.00"), "unspendables": { "genesis_block": 0, "bip30": 0, "scripts": 0, "unclaimed_rewards": 0, }, }, ) self.block_sanity_check(res5["block_info"]) # Generate and send a normal tx with two outputs tx1_txid, tx1_vout = self.wallet.send_to( from_node=node, scriptPubKey=self.wallet.get_scriptPubKey(), amount=21_000_000 * XEC, ) # Find the right position of the 21 MegXEC output tx1_out_21 = self.wallet.get_utxo(txid=tx1_txid, vout=tx1_vout) # Generate and send another tx with an OP_RETURN output (which is # unspendable) tx2 = self.wallet.create_self_transfer( from_node=self.nodes[0], utxo_to_spend=tx1_out_21 )["tx"] tx2.vout = [ CTxOut(int(20_990_000 * XEC), CScript([OP_RETURN] + [OP_FALSE] * 50)) ] tx2_hex = tx2.serialize().hex() self.nodes[0].sendrawtransaction(tx2_hex) # Include both txs in a block self.generate(self.nodes[0], 1) for hash_option in index_hash_options: # Check all amounts were registered correctly res6 = index_node.gettxoutsetinfo(hash_option, 108) assert_equal(res6["total_unspendable_amount"], Decimal("70990000.00")) assert_equal( res6["block_info"], { "unspendable": Decimal("20990000.00"), "prevout_spent": 71_000_000, "new_outputs_ex_coinbase": Decimal("49999990.00"), "coinbase": Decimal("50010010.00"), "unspendables": { "genesis_block": 0, "bip30": 0, "scripts": Decimal("20990000.00"), "unclaimed_rewards": 0, }, }, ) self.block_sanity_check(res6["block_info"]) # Create a coinbase that does not claim full subsidy and also # has two outputs cb = create_coinbase(109, nValue=35_000_000) cb.vout.append(CTxOut(5_000_000 * XEC, CScript([OP_FALSE]))) cb.rehash() # Generate a block that includes previous coinbase tip = self.nodes[0].getbestblockhash() block_time = self.nodes[0].getblock(tip)["time"] + 1 block = create_block(int(tip, 16), cb, block_time) block.solve() self.nodes[0].submitblock(ToHex(block)) self.sync_all() for hash_option in index_hash_options: res7 = index_node.gettxoutsetinfo(hash_option, 109) assert_equal(res7["total_unspendable_amount"], Decimal("80990000.00")) assert_equal( res7["block_info"], { "unspendable": 10_000_000, "prevout_spent": 0, "new_outputs_ex_coinbase": 0, "coinbase": 40_000_000, "unspendables": { "genesis_block": 0, "bip30": 0, "scripts": 0, "unclaimed_rewards": 10_000_000, }, }, ) self.block_sanity_check(res7["block_info"]) self.log.info("Test that the index is robust across restarts") res8 = index_node.gettxoutsetinfo("muhash") self.restart_node(1, extra_args=self.extra_args[1]) res9 = index_node.gettxoutsetinfo("muhash") assert_equal(res8, res9) self.generate(index_node, 1, sync_fun=self.no_op) res10 = index_node.gettxoutsetinfo("muhash") assert res8["txouts"] < res10["txouts"] def _test_use_index_option(self): self.log.info("Test use_index option for nodes running the index") self.connect_nodes(0, 1) self.nodes[0].waitforblockheight(110) res = self.nodes[0].gettxoutsetinfo("muhash") option_res = self.nodes[1].gettxoutsetinfo( hash_type="muhash", hash_or_height=None, use_index=False ) del res["disk_size"], option_res["disk_size"] assert_equal(res, option_res) def _test_reorg_index(self): self.log.info("Test that index can handle reorgs") # Generate two block, let the index catch up, then invalidate the # blocks index_node = self.nodes[1] reorg_blocks = self.generatetoaddress(index_node, 2, getnewdestination()[2]) reorg_block = reorg_blocks[1] res_invalid = index_node.gettxoutsetinfo("muhash") index_node.invalidateblock(reorg_blocks[0]) assert_equal(index_node.gettxoutsetinfo("muhash")["height"], 110) # Add two new blocks block = self.generate(index_node, 2, sync_fun=self.no_op)[1] res = index_node.gettxoutsetinfo( hash_type="muhash", hash_or_height=None, use_index=False ) # Test that the result of the reorged block is not returned for its old # block height res2 = index_node.gettxoutsetinfo(hash_type="muhash", hash_or_height=112) assert_equal(res["bestblock"], block) assert_equal(res["muhash"], res2["muhash"]) assert res["muhash"] != res_invalid["muhash"] # Test that requesting reorged out block by hash is still returning # correct results res_invalid2 = index_node.gettxoutsetinfo( hash_type="muhash", hash_or_height=reorg_block ) assert_equal(res_invalid2["muhash"], res_invalid["muhash"]) assert res["muhash"] != res_invalid2["muhash"] # Add another block, so we don't depend on reconsiderblock remembering # which blocks were touched by invalidateblock self.generate(index_node, 1) # Ensure that removing and re-adding blocks yields consistent results block = index_node.getblockhash(99) index_node.invalidateblock(block) index_node.reconsiderblock(block) res3 = index_node.gettxoutsetinfo(hash_type="muhash", hash_or_height=112) assert_equal(res2, res3) - self.log.info("Test that a node aware of stale blocks syncs them as well") - node = self.nodes[0] - # Ensure the node is aware of a stale block prior to restart - node.getblock(reorg_block) - - self.restart_node(0, ["-coinstatsindex"]) - assert_raises_rpc_error( - -32603, - "Unable to get data because coinstatsindex is still syncing.", - node.gettxoutsetinfo, - "muhash", - reorg_block, - ) - def _test_index_rejects_hash_serialized(self): self.log.info( "Test that the rpc raises if the legacy hash is passed with the index" ) msg = "hash_serialized hash type cannot be queried for a specific block" assert_raises_rpc_error( -8, msg, self.nodes[1].gettxoutsetinfo, hash_type="hash_serialized", hash_or_height=111, ) for use_index in {True, False, None}: assert_raises_rpc_error( -8, msg, self.nodes[1].gettxoutsetinfo, hash_type="hash_serialized", hash_or_height=111, use_index=use_index, ) if __name__ == "__main__": CoinStatsIndexTest().main()