|
19 | 19 | from test_framework.test_framework import BitcoinTestFramework
|
20 | 20 | from test_framework.util import (
|
21 | 21 | assert_equal,
|
| 22 | + assert_greater_than, |
22 | 23 | assert_raises_rpc_error
|
23 | 24 | )
|
24 | 25 |
|
@@ -90,6 +91,54 @@ def test_coinbase_automatic_abandon_during_startup(self):
|
90 | 91 | # Verify the coinbase descendant was also marked as abandoned
|
91 | 92 | assert_equal(wallet0.gettransaction(descendant_tx_id)['details'][0]['abandoned'], True)
|
92 | 93 |
|
| 94 | + def test_reorg_handling_during_unclean_shutdown(self): |
| 95 | + self.log.info("Test that wallet doesn't crash due to a duplicate block disconnection event after an unclean shutdown") |
| 96 | + node = self.nodes[0] |
| 97 | + # Receive coinbase reward on a new wallet |
| 98 | + node.createwallet(wallet_name="reorg_crash", load_on_startup=True) |
| 99 | + wallet = node.get_wallet_rpc("reorg_crash") |
| 100 | + self.generatetoaddress(node, 1, wallet.getnewaddress(), sync_fun=self.no_op) |
| 101 | + |
| 102 | + # Restart to ensure node and wallet are flushed |
| 103 | + self.restart_node(0) |
| 104 | + wallet = node.get_wallet_rpc("reorg_crash") |
| 105 | + assert_greater_than(wallet.getwalletinfo()['immature_balance'], 0) |
| 106 | + |
| 107 | + # Disconnect tip and sync wallet state |
| 108 | + tip = wallet.getbestblockhash() |
| 109 | + wallet.invalidateblock(tip) |
| 110 | + wallet.syncwithvalidationinterfacequeue() |
| 111 | + |
| 112 | + # Tip was disconnected, ensure coinbase has been abandoned |
| 113 | + assert_equal(wallet.getwalletinfo()['immature_balance'], 0) |
| 114 | + coinbase_tx_id = wallet.getblock(tip, verbose=1)["tx"][0] |
| 115 | + assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], True) |
| 116 | + |
| 117 | + # Abort process abruptly to mimic an unclean shutdown (no chain state flush to disk) |
| 118 | + node.process.kill() |
| 119 | + |
| 120 | + # Restart the node and confirm that it has not persisted the last chain state changes to disk |
| 121 | + self.start_node(0) |
| 122 | + assert_equal(node.getbestblockhash(), tip) |
| 123 | + |
| 124 | + # Due to an existing bug, the wallet incorrectly keeps the transaction in an abandoned state, even though that's |
| 125 | + # no longer the case (after the unclean shutdown, the node's chain returned to the pre-invalidation tip). |
| 126 | + # This issue blocks any future spending and results in an incorrect balance display. |
| 127 | + wallet = node.get_wallet_rpc("reorg_crash") |
| 128 | + assert_equal(wallet.getwalletinfo()['immature_balance'], 0) # FIXME: #31824. |
| 129 | + |
| 130 | + # Previously, a bug caused the node to crash if two block disconnection events occurred consecutively. |
| 131 | + # Ensure this is no longer the case by simulating a new reorg. |
| 132 | + node.invalidateblock(tip) |
| 133 | + assert(node.getbestblockhash() != tip) |
| 134 | + # Ensure wallet state is consistent now |
| 135 | + assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], True) |
| 136 | + assert_equal(wallet.getwalletinfo()['immature_balance'], 0) |
| 137 | + |
| 138 | + # And finally, verify the state if the block ends up being into the best chain again |
| 139 | + node.reconsiderblock(tip) |
| 140 | + assert_equal(wallet.gettransaction(coinbase_tx_id)['details'][0]['abandoned'], False) |
| 141 | + assert_greater_than(wallet.getwalletinfo()['immature_balance'], 0) |
93 | 142 |
|
94 | 143 | def run_test(self):
|
95 | 144 | # Send a tx from which to conflict outputs later
|
@@ -163,6 +212,9 @@ def run_test(self):
|
163 | 212 | # Verify we mark coinbase txs, and their descendants, as abandoned during startup
|
164 | 213 | self.test_coinbase_automatic_abandon_during_startup()
|
165 | 214 |
|
| 215 | + # Verify reorg behavior during an unclean shutdown |
| 216 | + self.test_reorg_handling_during_unclean_shutdown() |
| 217 | + |
166 | 218 |
|
167 | 219 | if __name__ == '__main__':
|
168 | 220 | ReorgsRestoreTest(__file__).main()
|
0 commit comments