Skip to content

Commit b1329b7

Browse files
committed
Merge bitcoin/bitcoin#26499: wallet: Abandon descendants of orphaned coinbases
b0fa598 test: Check that orphaned coinbase unconf spend is still abandoned (Andrew Chow) 9addbd7 wallet: Automatically abandon orphaned coinbases and their children (Andrew Chow) Pull request description: When a block is reorged out of the main chain, any descendants of the coinbase will no longer be valid. Currently they are only marked as inactive, which means that our balance calculations will still include them. In order to be excluded from the balance calculation, they need to either be abandoned or conflicted. This PR goes with the abandoned method. Note that even when they are included in balance calculations, coin selection will not select outputs belonging to these transactions because they are not in the mempool. Fixes #14148 ACKs for top commit: furszy: ACK b0fa598 with a not-blocking nit. aureleoules: reACK b0fa598 ishaanam: ACK b0fa598 Tree-SHA512: 68f12e7aa8df392d8817dc6ac0becce8dbe83837bfa538f47027e6730e5b2e1b1a090cfcea2dc598398fdb66090e02d321d799f087020d7e1badcf96e598c3ac
2 parents 37fea41 + b0fa598 commit b1329b7

File tree

3 files changed

+57
-14
lines changed

3 files changed

+57
-14
lines changed

src/wallet/transaction.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ class CWalletTx
293293

294294
bool isAbandoned() const { return state<TxStateInactive>() && state<TxStateInactive>()->abandoned; }
295295
bool isConflicted() const { return state<TxStateConflicted>(); }
296+
bool isInactive() const { return state<TxStateInactive>(); }
296297
bool isUnconfirmed() const { return !isAbandoned() && !isConflicted() && !isConfirmed(); }
297298
bool isConfirmed() const { return state<TxStateConfirmed>(); }
298299
const uint256& GetHash() const { return tx->GetHash(); }

src/wallet/wallet.cpp

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,33 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const
10651065
}
10661066
}
10671067

1068+
// Mark inactive coinbase transactions and their descendants as abandoned
1069+
if (wtx.IsCoinBase() && wtx.isInactive()) {
1070+
std::vector<CWalletTx*> txs{&wtx};
1071+
1072+
TxStateInactive inactive_state = TxStateInactive{/*abandoned=*/true};
1073+
1074+
while (!txs.empty()) {
1075+
CWalletTx* desc_tx = txs.back();
1076+
txs.pop_back();
1077+
desc_tx->m_state = inactive_state;
1078+
// Break caches since we have changed the state
1079+
desc_tx->MarkDirty();
1080+
batch.WriteTx(*desc_tx);
1081+
MarkInputsDirty(desc_tx->tx);
1082+
for (unsigned int i = 0; i < desc_tx->tx->vout.size(); ++i) {
1083+
COutPoint outpoint(desc_tx->GetHash(), i);
1084+
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(outpoint);
1085+
for (TxSpends::const_iterator it = range.first; it != range.second; ++it) {
1086+
const auto wit = mapWallet.find(it->second);
1087+
if (wit != mapWallet.end()) {
1088+
txs.push_back(&wit->second);
1089+
}
1090+
}
1091+
}
1092+
}
1093+
}
1094+
10681095
//// debug print
10691096
WalletLogPrintf("AddToWallet %s %s%s\n", hash.ToString(), (fInsertedNew ? "new" : ""), (fUpdated ? "update" : ""));
10701097

@@ -1274,7 +1301,11 @@ bool CWallet::AbandonTransaction(const uint256& hashTx)
12741301
wtx.MarkDirty();
12751302
batch.WriteTx(wtx);
12761303
NotifyTransactionChanged(wtx.GetHash(), CT_UPDATED);
1277-
// Iterate over all its outputs, and mark transactions in the wallet that spend them abandoned too
1304+
// Iterate over all its outputs, and mark transactions in the wallet that spend them abandoned too.
1305+
// States are not permanent, so these transactions can become unabandoned if they are re-added to the
1306+
// mempool, or confirmed in a block, or conflicted.
1307+
// Note: If the reorged coinbase is re-added to the main chain, the descendants that have not had their
1308+
// states change will remain abandoned and will require manual broadcast if the user wants them.
12781309
for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
12791310
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(COutPoint(now, i));
12801311
for (TxSpends::const_iterator iter = range.first; iter != range.second; ++iter) {

test/functional/wallet_orphanedreward.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,40 @@ def run_test(self):
3434
# the existing balance and the block reward.
3535
self.generate(self.nodes[0], 150)
3636
assert_equal(self.nodes[1].getbalance(), 10 + 25)
37+
pre_reorg_conf_bals = self.nodes[1].getbalances()
3738
txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 30)
39+
orig_chain_tip = self.nodes[0].getbestblockhash()
40+
self.sync_mempools()
3841

3942
# Orphan the block reward and make sure that the original coins
4043
# from the wallet can still be spent.
4144
self.nodes[0].invalidateblock(blk)
42-
self.generate(self.nodes[0], 152)
43-
# Without the following abandontransaction call, the coins are
44-
# not considered available yet.
45-
assert_equal(self.nodes[1].getbalances()["mine"], {
46-
"trusted": 0,
47-
"untrusted_pending": 0,
48-
"immature": 0,
49-
})
50-
# The following abandontransaction is necessary to make the later
51-
# lines succeed, and probably should not be needed; see
52-
# https://github.com/bitcoin/bitcoin/issues/14148.
53-
self.nodes[1].abandontransaction(txid)
45+
blocks = self.generate(self.nodes[0], 152)
46+
conflict_block = blocks[0]
47+
# We expect the descendants of orphaned rewards to no longer be considered
5448
assert_equal(self.nodes[1].getbalances()["mine"], {
5549
"trusted": 10,
5650
"untrusted_pending": 0,
5751
"immature": 0,
5852
})
59-
self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 9)
53+
# And the unconfirmed tx to be abandoned
54+
assert_equal(self.nodes[1].gettransaction(txid)["details"][0]["abandoned"], True)
55+
56+
# The abandoning should persist through reloading
57+
self.nodes[1].unloadwallet(self.default_wallet_name)
58+
self.nodes[1].loadwallet(self.default_wallet_name)
59+
assert_equal(self.nodes[1].gettransaction(txid)["details"][0]["abandoned"], True)
60+
61+
# If the orphaned reward is reorged back into the main chain, any unconfirmed
62+
# descendant txs at the time of the original reorg remain abandoned.
63+
self.nodes[0].invalidateblock(conflict_block)
64+
self.nodes[0].reconsiderblock(blk)
65+
assert_equal(self.nodes[0].getbestblockhash(), orig_chain_tip)
66+
self.generate(self.nodes[0], 3)
67+
68+
assert_equal(self.nodes[1].getbalances(), pre_reorg_conf_bals)
69+
assert_equal(self.nodes[1].gettransaction(txid)["details"][0]["abandoned"], True)
70+
6071

6172
if __name__ == '__main__':
6273
OrphanedBlockRewardTest().main()

0 commit comments

Comments
 (0)