diff options
Diffstat (limited to 'test/functional/wallet_reorgsrestore.py')
-rwxr-xr-x | test/functional/wallet_reorgsrestore.py | 116 |
1 files changed, 116 insertions, 0 deletions
diff --git a/test/functional/wallet_reorgsrestore.py b/test/functional/wallet_reorgsrestore.py index 77cf34898b..9c69c33680 100755 --- a/test/functional/wallet_reorgsrestore.py +++ b/test/functional/wallet_reorgsrestore.py @@ -19,6 +19,8 @@ import shutil from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_greater_than, + assert_raises_rpc_error ) class ReorgsRestoreTest(BitcoinTestFramework): @@ -31,6 +33,113 @@ class ReorgsRestoreTest(BitcoinTestFramework): def skip_test_if_missing_module(self): self.skip_if_no_wallet() + def test_coinbase_automatic_abandon_during_startup(self): + ########################################################################################################## + # Verify the wallet marks coinbase transactions, and their descendants, as abandoned during startup when # + # the block is no longer part of the best chain. # + ########################################################################################################## + self.log.info("Test automatic coinbase abandonment during startup") + # Test setup: Sync nodes for the coming test, ensuring both are at the same block, then disconnect them to + # generate two competing chains. After disconnection, verify no other peer connection exists. + self.connect_nodes(1, 0) + self.sync_blocks(self.nodes[:2]) + self.disconnect_nodes(1, 0) + assert all(len(node.getpeerinfo()) == 0 for node in self.nodes[:2]) + + # Create a new block in node0, coinbase going to wallet0 + self.nodes[0].createwallet(wallet_name="w0", load_on_startup=True) + wallet0 = self.nodes[0].get_wallet_rpc("w0") + self.generatetoaddress(self.nodes[0], 1, wallet0.getnewaddress(), sync_fun=self.no_op) + node0_coinbase_tx_hash = wallet0.getblock(wallet0.getbestblockhash(), verbose=1)['tx'][0] + + # Mine 100 blocks on top to mature the coinbase and create a descendant + self.generate(self.nodes[0], 101, sync_fun=self.no_op) + # Make descendant, send-to-self + descendant_tx_id = wallet0.sendtoaddress(wallet0.getnewaddress(), 1) + + # Verify balance + wallet0.syncwithvalidationinterfacequeue() + assert(wallet0.getbalances()['mine']['trusted'] > 0) + + # Now create a fork in node1. This will be used to replace node0's chain later. + self.nodes[1].createwallet(wallet_name="w1", load_on_startup=True) + wallet1 = self.nodes[1].get_wallet_rpc("w1") + self.generatetoaddress(self.nodes[1], 1, wallet1.getnewaddress(), sync_fun=self.no_op) + wallet1.syncwithvalidationinterfacequeue() + + # Verify both nodes are on a different chain + block0_best_hash, block1_best_hash = wallet0.getbestblockhash(), wallet1.getbestblockhash() + assert(block0_best_hash != block1_best_hash) + + # Stop both nodes and replace node0 chain entirely for the node1 chain + self.stop_nodes() + for path in ["chainstate", "blocks"]: + shutil.rmtree(self.nodes[0].chain_path / path) + shutil.copytree(self.nodes[1].chain_path / path, self.nodes[0].chain_path / path) + + # Start node0 and verify that now it has node1 chain and no info about its previous best block + self.start_node(0) + wallet0 = self.nodes[0].get_wallet_rpc("w0") + assert_equal(wallet0.getbestblockhash(), block1_best_hash) + assert_raises_rpc_error(-5, "Block not found", wallet0.getblock, block0_best_hash) + + # Verify the coinbase tx was marked as abandoned and balance correctly computed + tx_info = wallet0.gettransaction(node0_coinbase_tx_hash)['details'][0] + assert_equal(tx_info['abandoned'], True) + assert_equal(tx_info['category'], 'orphan') + assert(wallet0.getbalances()['mine']['trusted'] == 0) + # Verify the coinbase descendant was also marked as abandoned + assert_equal(wallet0.gettransaction(descendant_tx_id)['details'][0]['abandoned'], True) + + def test_reorg_handling_during_unclean_shutdown(self): + self.log.info("Test that wallet doesn't crash due to a duplicate block disconnection event after an unclean shutdown") + node = self.nodes[0] + # Receive coinbase reward on a new wallet + node.createwallet(wallet_name="reorg_crash", load_on_startup=True) + wallet = node.get_wallet_rpc("reorg_crash") + self.generatetoaddress(node, 1, wallet.getnewaddress(), sync_fun=self.no_op) + + # Restart to ensure node and wallet are flushed + self.restart_node(0) + wallet = node.get_wallet_rpc("reorg_crash") + assert_greater_than(wallet.getwalletinfo()['immature_balance'], 0) + + # Disconnect tip and sync wallet state + tip = wallet.getbestblockhash() + wallet.invalidateblock(tip) + wallet.syncwithvalidationinterfacequeue() + + # Tip was disconnected, ensure coinbase has been abandoned + assert_equal(wallet.getwalletinfo()['immature_balance'], 0) + coinbase_tx_id = wallet.getblock(tip, verbose=1)["tx"][0] + assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], True) + + # Abort process abruptly to mimic an unclean shutdown (no chain state flush to disk) + node.kill_process() + + # Restart the node and confirm that it has not persisted the last chain state changes to disk + self.start_node(0) + assert_equal(node.getbestblockhash(), tip) + + # Due to an existing bug, the wallet incorrectly keeps the transaction in an abandoned state, even though that's + # no longer the case (after the unclean shutdown, the node's chain returned to the pre-invalidation tip). + # This issue blocks any future spending and results in an incorrect balance display. + wallet = node.get_wallet_rpc("reorg_crash") + assert_equal(wallet.getwalletinfo()['immature_balance'], 0) # FIXME: #31824. + + # Previously, a bug caused the node to crash if two block disconnection events occurred consecutively. + # Ensure this is no longer the case by simulating a new reorg. + node.invalidateblock(tip) + assert(node.getbestblockhash() != tip) + # Ensure wallet state is consistent now + assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], True) + assert_equal(wallet.getwalletinfo()['immature_balance'], 0) + + # And finally, verify the state if the block ends up being into the best chain again + node.reconsiderblock(tip) + assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], False) + assert_greater_than(wallet.getwalletinfo()['immature_balance'], 0) + def run_test(self): # Send a tx from which to conflict outputs later txid_conflict_from = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10")) @@ -100,5 +209,12 @@ class ReorgsRestoreTest(BitcoinTestFramework): assert_equal(conflicted_after_reorg["confirmations"], 1) assert conflicting["blockhash"] != conflicted_after_reorg["blockhash"] + # Verify we mark coinbase txs, and their descendants, as abandoned during startup + self.test_coinbase_automatic_abandon_during_startup() + + # Verify reorg behavior during an unclean shutdown + self.test_reorg_handling_during_unclean_shutdown() + + if __name__ == '__main__': ReorgsRestoreTest(__file__).main() |