Skip to content

Commit dc3a714

Browse files
committed
Merge bitcoin/bitcoin#31794: wallet: abandon orphan coinbase txs, and their descendants, during startup
e4dd5a3 test: wallet, abandon coinbase txs and their descendants during startup (furszy) 474139a wallet: abandon inactive coinbase tx and their descendants during startup (furszy) Pull request description: Since #26499, we mark coinbase transactions and their descendants as abandoned when a reorg arises through the "block disconnection" signal handler. However, this does not cover all scenarios; external wallets could contain coinbase transactions from blocks the node has not seen yet, or the user could have replaced the chain with an earlier or different version (one without the coinbase chain). This affects balance calculation as well as mempool rebroadcast (descendants shouldn't be relayed). Fix this by marking orphaned coinbase transactions and their descendants as abandoned during wallet startup. ACKs for top commit: achow101: ACK e4dd5a3 rkrux: tACK e4dd5a3 mzumsande: Code Review ACK e4dd5a3 Tree-SHA512: 461a43de7a6f5a580f2e6e3b56ec9bc92239cd45e850a2ff594ab5488dcd4a507f68fbbf550a33d7173b2add0de80de1e1b3841e1dfab0c95b284212d8ced08a
2 parents 06757af + e4dd5a3 commit dc3a714

File tree

4 files changed

+81
-5
lines changed

4 files changed

+81
-5
lines changed

src/wallet/wallet.cpp

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,12 +1315,15 @@ void CWallet::MarkInputsDirty(const CTransactionRef& tx)
13151315
bool CWallet::AbandonTransaction(const uint256& hashTx)
13161316
{
13171317
LOCK(cs_wallet);
1318-
1319-
// Can't mark abandoned if confirmed or in mempool
13201318
auto it = mapWallet.find(hashTx);
13211319
assert(it != mapWallet.end());
1322-
const CWalletTx& origtx = it->second;
1323-
if (GetTxDepthInMainChain(origtx) != 0 || origtx.InMempool()) {
1320+
return AbandonTransaction(it->second);
1321+
}
1322+
1323+
bool CWallet::AbandonTransaction(CWalletTx& tx)
1324+
{
1325+
// Can't mark abandoned if confirmed or in mempool
1326+
if (GetTxDepthInMainChain(tx) != 0 || tx.InMempool()) {
13241327
return false;
13251328
}
13261329

@@ -1342,7 +1345,7 @@ bool CWallet::AbandonTransaction(const uint256& hashTx)
13421345
// Note: If the reorged coinbase is re-added to the main chain, the descendants that have not had their
13431346
// states change will remain abandoned and will require manual broadcast if the user wants them.
13441347

1345-
RecursiveUpdateTxState(hashTx, try_updating_state);
1348+
RecursiveUpdateTxState(tx.GetHash(), try_updating_state);
13461349

13471350
return true;
13481351
}

src/wallet/wallet.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
871871

872872
/* Mark a transaction (and it in-wallet descendants) as abandoned so its inputs may be respent. */
873873
bool AbandonTransaction(const uint256& hashTx);
874+
bool AbandonTransaction(CWalletTx& tx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
874875

875876
/** Mark a transaction as replaced by another transaction. */
876877
bool MarkReplaced(const uint256& originalHash, const uint256& newHash);

src/wallet/walletdb.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,14 @@ static DBErrors LoadTxRecords(CWallet* pwallet, DatabaseBatch& batch, std::vecto
11151115
});
11161116
result = std::max(result, order_pos_res.m_result);
11171117

1118+
// After loading all tx records, abandon any coinbase that is no longer in the active chain.
1119+
// This could happen during an external wallet load, or if the user replaced the chain data.
1120+
for (auto& [id, wtx] : pwallet->mapWallet) {
1121+
if (wtx.IsCoinBase() && wtx.isInactive()) {
1122+
pwallet->AbandonTransaction(wtx);
1123+
}
1124+
}
1125+
11181126
return result;
11191127
}
11201128

test/functional/wallet_reorgsrestore.py

Lines changed: 64 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_raises_rpc_error
2223
)
2324

2425
class ReorgsRestoreTest(BitcoinTestFramework):
@@ -31,6 +32,65 @@ def set_test_params(self):
3132
def skip_test_if_missing_module(self):
3233
self.skip_if_no_wallet()
3334

35+
def test_coinbase_automatic_abandon_during_startup(self):
36+
##########################################################################################################
37+
# Verify the wallet marks coinbase transactions, and their descendants, as abandoned during startup when #
38+
# the block is no longer part of the best chain. #
39+
##########################################################################################################
40+
self.log.info("Test automatic coinbase abandonment during startup")
41+
# Test setup: Sync nodes for the coming test, ensuring both are at the same block, then disconnect them to
42+
# generate two competing chains. After disconnection, verify no other peer connection exists.
43+
self.connect_nodes(1, 0)
44+
self.sync_blocks(self.nodes[:2])
45+
self.disconnect_nodes(1, 0)
46+
assert all(len(node.getpeerinfo()) == 0 for node in self.nodes[:2])
47+
48+
# Create a new block in node0, coinbase going to wallet0
49+
self.nodes[0].createwallet(wallet_name="w0", load_on_startup=True)
50+
wallet0 = self.nodes[0].get_wallet_rpc("w0")
51+
self.generatetoaddress(self.nodes[0], 1, wallet0.getnewaddress(), sync_fun=self.no_op)
52+
node0_coinbase_tx_hash = wallet0.getblock(wallet0.getbestblockhash(), verbose=1)['tx'][0]
53+
54+
# Mine 100 blocks on top to mature the coinbase and create a descendant
55+
self.generate(self.nodes[0], 101, sync_fun=self.no_op)
56+
# Make descendant, send-to-self
57+
descendant_tx_id = wallet0.sendtoaddress(wallet0.getnewaddress(), 1)
58+
59+
# Verify balance
60+
wallet0.syncwithvalidationinterfacequeue()
61+
assert(wallet0.getbalances()['mine']['trusted'] > 0)
62+
63+
# Now create a fork in node1. This will be used to replace node0's chain later.
64+
self.nodes[1].createwallet(wallet_name="w1", load_on_startup=True)
65+
wallet1 = self.nodes[1].get_wallet_rpc("w1")
66+
self.generatetoaddress(self.nodes[1], 1, wallet1.getnewaddress(), sync_fun=self.no_op)
67+
wallet1.syncwithvalidationinterfacequeue()
68+
69+
# Verify both nodes are on a different chain
70+
block0_best_hash, block1_best_hash = wallet0.getbestblockhash(), wallet1.getbestblockhash()
71+
assert(block0_best_hash != block1_best_hash)
72+
73+
# Stop both nodes and replace node0 chain entirely for the node1 chain
74+
self.stop_nodes()
75+
for path in ["chainstate", "blocks"]:
76+
shutil.rmtree(self.nodes[0].chain_path / path)
77+
shutil.copytree(self.nodes[1].chain_path / path, self.nodes[0].chain_path / path)
78+
79+
# Start node0 and verify that now it has node1 chain and no info about its previous best block
80+
self.start_node(0)
81+
wallet0 = self.nodes[0].get_wallet_rpc("w0")
82+
assert_equal(wallet0.getbestblockhash(), block1_best_hash)
83+
assert_raises_rpc_error(-5, "Block not found", wallet0.getblock, block0_best_hash)
84+
85+
# Verify the coinbase tx was marked as abandoned and balance correctly computed
86+
tx_info = wallet0.gettransaction(node0_coinbase_tx_hash)['details'][0]
87+
assert_equal(tx_info['abandoned'], True)
88+
assert_equal(tx_info['category'], 'orphan')
89+
assert(wallet0.getbalances()['mine']['trusted'] == 0)
90+
# Verify the coinbase descendant was also marked as abandoned
91+
assert_equal(wallet0.gettransaction(descendant_tx_id)['details'][0]['abandoned'], True)
92+
93+
3494
def run_test(self):
3595
# Send a tx from which to conflict outputs later
3696
txid_conflict_from = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
@@ -100,5 +160,9 @@ def run_test(self):
100160
assert_equal(conflicted_after_reorg["confirmations"], 1)
101161
assert conflicting["blockhash"] != conflicted_after_reorg["blockhash"]
102162

163+
# Verify we mark coinbase txs, and their descendants, as abandoned during startup
164+
self.test_coinbase_automatic_abandon_during_startup()
165+
166+
103167
if __name__ == '__main__':
104168
ReorgsRestoreTest(__file__).main()

0 commit comments

Comments
 (0)