diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index abbb6c1267d..a24fc908a12 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2398,8 +2398,15 @@ util::Result CWallet::RemoveTxs(WalletBatch& batch, std::vector& txs for (const auto& it : erased_txs) { const Txid hash{it->first}; wtxOrdered.erase(it->second.m_it_wtxOrdered); - for (const auto& txin : it->second.tx->vin) - mapTxSpends.erase(txin.prevout); + for (const auto& txin : it->second.tx->vin) { + auto range = mapTxSpends.equal_range(txin.prevout); + for (auto iter = range.first; iter != range.second; ++iter) { + if (iter->second == hash) { + mapTxSpends.erase(iter); + break; + } + } + } for (unsigned int i = 0; i < it->second.tx->vout.size(); ++i) { m_txos.erase(COutPoint(hash, i)); } diff --git a/test/functional/wallet_importprunedfunds.py b/test/functional/wallet_importprunedfunds.py index 72896052b5f..1948cb9278b 100755 --- a/test/functional/wallet_importprunedfunds.py +++ b/test/functional/wallet_importprunedfunds.py @@ -14,6 +14,7 @@ from test_framework.messages import ( from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_not_equal, assert_raises_rpc_error, wallet_importprivkey, ) @@ -129,6 +130,33 @@ class ImportPrunedFundsTest(BitcoinTestFramework): mb.header.nTime += 1 # modify arbitrary block header field to change block hash assert_raises_rpc_error(-5, "Block not found in chain", w1.importprunedfunds, rawtxn1, mb.serialize().hex()) + self.log.info("Test removeprunedfunds with conflicting transactions") + node = self.nodes[0] + + # Create a transaction + utxo = node.listunspent()[0] + addr = node.getnewaddress() + tx1_id = node.send(outputs=[{addr: 1}], inputs=[utxo])["txid"] + tx1_fee = node.gettransaction(tx1_id)["fee"] + + # Create a conflicting tx with a larger fee (tx1_fee is negative) + output_value = utxo["amount"] + tx1_fee - Decimal("0.00001") + raw_tx2 = node.createrawtransaction(inputs=[utxo], outputs=[{addr: output_value}]) + signed_tx2 = node.signrawtransactionwithwallet(raw_tx2) + tx2_id = node.sendrawtransaction(signed_tx2["hex"]) + assert_not_equal(tx2_id, tx1_id) + + # Both txs should be in the wallet, tx2 replaced tx1 in mempool + assert tx1_id in [tx["txid"] for tx in node.listtransactions()] + assert tx2_id in [tx["txid"] for tx in node.listtransactions()] + + # Remove the replaced tx from wallet + node.removeprunedfunds(tx1_id) + + # The UTXO should still be considered spent (by tx2) + available_utxos = [u["txid"] for u in node.listunspent(minconf=0)] + assert utxo["txid"] not in available_utxos, "UTXO should still be spent by conflicting tx" + if __name__ == '__main__': ImportPrunedFundsTest(__file__).main()