Skip to content

Commit 57d611e

Browse files
committed
Merge bitcoin/bitcoin#31757: wallet: fix crash on double block disconnection
11f8ab1 test: wallet, coverage for crash on dup block disconnection during unclean shutdown (Martin Zumsande) 9ef429b wallet: fix crash on double block disconnection (furszy) Pull request description: The wallet crashes if it processes the same block disconnection event twice in a row due to an incompatible coinbase transaction state. This happens because `disconnectBlock` provides `TxStateInactive` without the "abandoned" flag for coinbase transactions to `SyncTransaction`, while `AddToWallet()` internally modifies it to retain the abandoned state. The crash flow is as follows: 1) On the first disconnection, the transaction state transitions from "confirmed" to "inactive," bypassing the state equality check since the provided state differs. Then, `AddToWallet` internally updates the state to "inactive + abandoned" 2) On the second disconnection, as we provide only the "inactive" state to `SyncTransaction()`, the state equality assertion fails and crashes the wallet. Reviewers Note: The crash can easily be replicated by cherry-picking the test commit in master. ACKs for top commit: mzumsande: Code Review ACK 11f8ab1 rkrux: ACK 11f8ab1 pinheadmz: ACK 11f8ab1 Tree-SHA512: 971069bca562f0afb06c34a2516842d01b5cbc2b18ed851392aa3caa3bb7488f4a84a5d017ea334e6361261d3c44aa597cc67a1d4fa16781f85e081f3d1f8771
2 parents 199d47d + 11f8ab1 commit 57d611e

File tree

2 files changed

+57
-2
lines changed

2 files changed

+57
-2
lines changed

src/wallet/wallet.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,8 +1548,11 @@ void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
15481548

15491549
int disconnect_height = block.height;
15501550

1551-
for (const CTransactionRef& ptx : Assert(block.data)->vtx) {
1552-
SyncTransaction(ptx, TxStateInactive{});
1551+
for (size_t index = 0; index < block.data->vtx.size(); index++) {
1552+
const CTransactionRef& ptx = Assert(block.data)->vtx[index];
1553+
// Coinbase transactions are not only inactive but also abandoned,
1554+
// meaning they should never be relayed standalone via the p2p protocol.
1555+
SyncTransaction(ptx, TxStateInactive{/*abandoned=*/index == 0});
15531556

15541557
for (const CTxIn& tx_in : ptx->vin) {
15551558
// No other wallet transactions conflicted with this transaction

test/functional/wallet_reorgsrestore.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from test_framework.test_framework import BitcoinTestFramework
2020
from test_framework.util import (
2121
assert_equal,
22+
assert_greater_than,
2223
assert_raises_rpc_error
2324
)
2425

@@ -90,6 +91,54 @@ def test_coinbase_automatic_abandon_during_startup(self):
9091
# Verify the coinbase descendant was also marked as abandoned
9192
assert_equal(wallet0.gettransaction(descendant_tx_id)['details'][0]['abandoned'], True)
9293

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)
93142

94143
def run_test(self):
95144
# Send a tx from which to conflict outputs later
@@ -163,6 +212,9 @@ def run_test(self):
163212
# Verify we mark coinbase txs, and their descendants, as abandoned during startup
164213
self.test_coinbase_automatic_abandon_during_startup()
165214

215+
# Verify reorg behavior during an unclean shutdown
216+
self.test_reorg_handling_during_unclean_shutdown()
217+
166218

167219
if __name__ == '__main__':
168220
ReorgsRestoreTest(__file__).main()

0 commit comments

Comments
 (0)