diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs index b44cfea1..e767321a 100644 --- a/src/bitcoin/mod.rs +++ b/src/bitcoin/mod.rs @@ -269,28 +269,36 @@ impl BitcoinInterface for d::BitcoinD { // If a conflicting transaction was confirmed instead, replace the txid of the // spender for this coin with it and mark it as confirmed. + // If a conflicting transaction which doesn't spend this coin was mined or accepted in + // our local mempool, mark this spend as expired. enum Conflict { // A replacement spending transaction was confirmed. Replaced((bitcoin::Txid, Block)), - // A transaction conflicting with the former spending transaction was confirmed, - // but it doesn't spend this outpoint anymore. + // A transaction conflicting with the former spending transaction was confirmed or + // included in our local mempool. Dropped, } let conflict = res.conflicting_txs.iter().find_map(|txid| { tx_getter.get_transaction(txid).and_then(|tx| { - // FIXME: if a conflict was mined we should somehow wipe the spend_txid of this - // coin. - tx.block.map(|block| { - // Being part of our watchonly wallet isn't enough, as it could be a - // conflicting transaction which spends a different set of coins. Make sure - // it does actually spend this coin. - for txin in tx.tx.input { - if &txin.previous_output == op { - return Conflict::Replaced((*txid, block)); + tx.block + .map(|block| { + // Being part of our watchonly wallet isn't enough, as it could be a + // conflicting transaction which spends a different set of coins. Make sure + // it does actually spend this coin. + for txin in tx.tx.input { + if &txin.previous_output == op { + return Conflict::Replaced((*txid, block)); + } } - } - Conflict::Dropped - }) + Conflict::Dropped + }) + .or_else(|| { + if self.is_in_mempool(txid) { + Some(Conflict::Dropped) + } else { + None + } + }) }) }); match conflict { diff --git a/tests/test_chain.py b/tests/test_chain.py index 493fe75f..42c60788 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -454,7 +454,7 @@ def test_spend_replacement(lianad, bitcoind): # Now RBF the first transaction by the second one. The third coin should be # newly marked as spending, the second one's spend_txid should be updated and - # the first one should not be updated until the second one is mined. + # the first one's spend txid should be dropped. second_txid = sign_and_broadcast_psbt(lianad, second_psbt) wait_for( lambda: all( @@ -462,9 +462,9 @@ def test_spend_replacement(lianad, bitcoind): for c in lianad.rpc.listcoins([], second_outpoints)["coins"] ) ) - assert ( - lianad.rpc.listcoins([], [first_outpoints[0]])["coins"][0]["spend_info"]["txid"] - == first_txid + wait_for( + lambda: lianad.rpc.listcoins([], [first_outpoints[0]])["coins"][0]["spend_info"] + is None ) # Now RBF the second transaction with a send-to-self, just because. @@ -476,11 +476,10 @@ def test_spend_replacement(lianad, bitcoind): ) ) assert ( - lianad.rpc.listcoins([], [first_outpoints[0]])["coins"][0]["spend_info"]["txid"] - == first_txid + lianad.rpc.listcoins([], [first_outpoints[0]])["coins"][0]["spend_info"] is None ) - # Once the RBF gets mined, the first coin's spend txid is wiped. + # Once the RBF is mined, we detect it as confirmed and the first coin is still unspent. bitcoind.generate_block(1, wait_for_mempool=third_txid) wait_for( lambda: all( @@ -488,7 +487,6 @@ def test_spend_replacement(lianad, bitcoind): for c in lianad.rpc.listcoins([], second_outpoints)["coins"] ) ) - wait_for( - lambda: lianad.rpc.listcoins([], [first_outpoints[0]])["coins"][0]["spend_info"] - is None + assert ( + lianad.rpc.listcoins([], [first_outpoints[0]])["coins"][0]["spend_info"] is None )