Skip to content

Commit cd1af85

Browse files
committed
Merge bitcoin/bitcoin#34358: wallet: fix removeprunedfunds bug with conflicting transactions
1f60ca3 wallet: fix removeprunedfunds bug with conflicting transactions (Martin Zumsande) Pull request description: `removeprunedfunds` removes all entries from `mapTxSpends` for the inputs of the pruned tx. However, this is incorrect, because there could be multiple entries from conflicting transactions (that shouldn't be removed as well). This could lead to the wallet creating invalid transactions, trying to double spend utxos. The bug persists when the conflicting tx was mined, because the wallet trusts its internal accounting instead of calling `AddToSpends` again. The added test should fail on master. ACKs for top commit: achow101: ACK 1f60ca3 fjahr: tACK 1f60ca3 furszy: utACK 1f60ca3 vasild: ACK 1f60ca3 Tree-SHA512: 3cc9ed547530fd53e25721177b76ab2e1eae16ce2c0e63fc01b20fdbf8bd02655dae51167ad56f9dec748d34c61ce65d38f993370820601f8257c73b876a3347
2 parents 2dd5e7b + 1f60ca3 commit cd1af85

File tree

2 files changed

+37
-2
lines changed

2 files changed

+37
-2
lines changed

src/wallet/wallet.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2413,8 +2413,15 @@ util::Result<void> CWallet::RemoveTxs(WalletBatch& batch, std::vector<Txid>& txs
24132413
for (const auto& it : erased_txs) {
24142414
const Txid hash{it->first};
24152415
wtxOrdered.erase(it->second.m_it_wtxOrdered);
2416-
for (const auto& txin : it->second.tx->vin)
2417-
mapTxSpends.erase(txin.prevout);
2416+
for (const auto& txin : it->second.tx->vin) {
2417+
auto range = mapTxSpends.equal_range(txin.prevout);
2418+
for (auto iter = range.first; iter != range.second; ++iter) {
2419+
if (iter->second == hash) {
2420+
mapTxSpends.erase(iter);
2421+
break;
2422+
}
2423+
}
2424+
}
24182425
for (unsigned int i = 0; i < it->second.tx->vout.size(); ++i) {
24192426
m_txos.erase(COutPoint(hash, i));
24202427
}

test/functional/wallet_importprunedfunds.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from test_framework.test_framework import BitcoinTestFramework
1515
from test_framework.util import (
1616
assert_equal,
17+
assert_not_equal,
1718
assert_raises_rpc_error,
1819
wallet_importprivkey,
1920
)
@@ -129,6 +130,33 @@ def run_test(self):
129130
mb.header.nTime += 1 # modify arbitrary block header field to change block hash
130131
assert_raises_rpc_error(-5, "Block not found in chain", w1.importprunedfunds, rawtxn1, mb.serialize().hex())
131132

133+
self.log.info("Test removeprunedfunds with conflicting transactions")
134+
node = self.nodes[0]
135+
136+
# Create a transaction
137+
utxo = node.listunspent()[0]
138+
addr = node.getnewaddress()
139+
tx1_id = node.send(outputs=[{addr: 1}], inputs=[utxo])["txid"]
140+
tx1_fee = node.gettransaction(tx1_id)["fee"]
141+
142+
# Create a conflicting tx with a larger fee (tx1_fee is negative)
143+
output_value = utxo["amount"] + tx1_fee - Decimal("0.00001")
144+
raw_tx2 = node.createrawtransaction(inputs=[utxo], outputs=[{addr: output_value}])
145+
signed_tx2 = node.signrawtransactionwithwallet(raw_tx2)
146+
tx2_id = node.sendrawtransaction(signed_tx2["hex"])
147+
assert_not_equal(tx2_id, tx1_id)
148+
149+
# Both txs should be in the wallet, tx2 replaced tx1 in mempool
150+
assert tx1_id in [tx["txid"] for tx in node.listtransactions()]
151+
assert tx2_id in [tx["txid"] for tx in node.listtransactions()]
152+
153+
# Remove the replaced tx from wallet
154+
node.removeprunedfunds(tx1_id)
155+
156+
# The UTXO should still be considered spent (by tx2)
157+
available_utxos = [u["txid"] for u in node.listunspent(minconf=0)]
158+
assert utxo["txid"] not in available_utxos, "UTXO should still be spent by conflicting tx"
159+
132160

133161
if __name__ == '__main__':
134162
ImportPrunedFundsTest(__file__).main()

0 commit comments

Comments
 (0)