Skip to content

Commit 54e07ee

Browse files
committed
wallet: track mempool conflicts
Behavior changes are: - if a tx has a mempool conflict, the wallet will not attempt to rebroadcast it - if a txo is spent by a mempool-conflicted tx, that txo is no longer considered spent
1 parent d64922b commit 54e07ee

File tree

5 files changed

+57
-11
lines changed

5 files changed

+57
-11
lines changed

src/wallet/transaction.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ struct TxStateBlockConflicted {
4848
int conflicting_block_height;
4949

5050
explicit TxStateBlockConflicted(const uint256& block_hash, int height) : conflicting_block_hash(block_hash), conflicting_block_height(height) {}
51-
std::string toString() const { return strprintf("Conflicted (block=%s, height=%i)", conflicting_block_hash.ToString(), conflicting_block_height); }
51+
std::string toString() const { return strprintf("BlockConflicted (block=%s, height=%i)", conflicting_block_hash.ToString(), conflicting_block_height); }
5252
};
5353

5454
//! State of transaction not confirmed or conflicting with a known block and
@@ -258,6 +258,14 @@ class CWalletTx
258258
CTransactionRef tx;
259259
TxState m_state;
260260

261+
// Set of mempool transactions that conflict
262+
// directly with the transaction, or that conflict
263+
// with an ancestor transaction. This set will be
264+
// empty if state is InMempool or Confirmed, but
265+
// can be nonempty if state is Inactive or
266+
// BlockConflicted.
267+
std::set<Txid> mempool_conflicts;
268+
261269
template<typename Stream>
262270
void Serialize(Stream& s) const
263271
{
@@ -335,9 +343,10 @@ class CWalletTx
335343
void updateState(interfaces::Chain& chain);
336344

337345
bool isAbandoned() const { return state<TxStateInactive>() && state<TxStateInactive>()->abandoned; }
346+
bool isMempoolConflicted() const { return !mempool_conflicts.empty(); }
338347
bool isBlockConflicted() const { return state<TxStateBlockConflicted>(); }
339348
bool isInactive() const { return state<TxStateInactive>(); }
340-
bool isUnconfirmed() const { return !isAbandoned() && !isBlockConflicted() && !isConfirmed(); }
349+
bool isUnconfirmed() const { return !isAbandoned() && !isBlockConflicted() && !isMempoolConflicted() && !isConfirmed(); }
341350
bool isConfirmed() const { return state<TxStateConfirmed>(); }
342351
const Txid& GetHash() const LIFETIMEBOUND { return tx->GetHash(); }
343352
const Wtxid& GetWitnessHash() const LIFETIMEBOUND { return tx->GetWitnessHash(); }

src/wallet/wallet.cpp

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ bool CWallet::IsSpent(const COutPoint& outpoint) const
753753
const auto mit = mapWallet.find(wtxid);
754754
if (mit != mapWallet.end()) {
755755
const auto& wtx = mit->second;
756-
if (!wtx.isAbandoned() && !wtx.isBlockConflicted())
756+
if (!wtx.isAbandoned() && !wtx.isBlockConflicted() && !wtx.isMempoolConflicted())
757757
return true; // Spent
758758
}
759759
}
@@ -1360,7 +1360,10 @@ void CWallet::MarkConflicted(const uint256& hashBlock, int conflicting_height, c
13601360
void CWallet::RecursiveUpdateTxState(const uint256& tx_hash, const TryUpdatingStateFn& try_updating_state) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) {
13611361
// Do not flush the wallet here for performance reasons
13621362
WalletBatch batch(GetDatabase(), false);
1363+
RecursiveUpdateTxState(&batch, tx_hash, try_updating_state);
1364+
}
13631365

1366+
void CWallet::RecursiveUpdateTxState(WalletBatch* batch, const uint256& tx_hash, const TryUpdatingStateFn& try_updating_state) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) {
13641367
std::set<uint256> todo;
13651368
std::set<uint256> done;
13661369

@@ -1377,7 +1380,7 @@ void CWallet::RecursiveUpdateTxState(const uint256& tx_hash, const TryUpdatingSt
13771380
TxUpdate update_state = try_updating_state(wtx);
13781381
if (update_state != TxUpdate::UNCHANGED) {
13791382
wtx.MarkDirty();
1380-
batch.WriteTx(wtx);
1383+
if (batch) batch->WriteTx(wtx);
13811384
// Iterate over all its outputs, and update those tx states as well (if applicable)
13821385
for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
13831386
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(COutPoint(Txid::FromUint256(now), i));
@@ -1418,6 +1421,20 @@ void CWallet::transactionAddedToMempool(const CTransactionRef& tx) {
14181421
if (it != mapWallet.end()) {
14191422
RefreshMempoolStatus(it->second, chain());
14201423
}
1424+
1425+
const Txid& txid = tx->GetHash();
1426+
1427+
for (const CTxIn& tx_in : tx->vin) {
1428+
// For each wallet transaction spending this prevout..
1429+
for (auto range = mapTxSpends.equal_range(tx_in.prevout); range.first != range.second; range.first++) {
1430+
const uint256& spent_id = range.first->second;
1431+
// Skip the recently added tx
1432+
if (spent_id == txid) continue;
1433+
RecursiveUpdateTxState(/*batch=*/nullptr, spent_id, [&txid](CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) {
1434+
return wtx.mempool_conflicts.insert(txid).second ? TxUpdate::CHANGED : TxUpdate::UNCHANGED;
1435+
});
1436+
}
1437+
}
14211438
}
14221439

14231440
void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason) {
@@ -1455,6 +1472,21 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe
14551472
// https://github.com/bitcoin-core/bitcoin-devwiki/wiki/Wallet-Transaction-Conflict-Tracking
14561473
SyncTransaction(tx, TxStateInactive{});
14571474
}
1475+
1476+
const Txid& txid = tx->GetHash();
1477+
1478+
for (const CTxIn& tx_in : tx->vin) {
1479+
// Iterate over all wallet transactions spending txin.prev
1480+
// and recursively mark them as no longer conflicting with
1481+
// txid
1482+
for (auto range = mapTxSpends.equal_range(tx_in.prevout); range.first != range.second; range.first++) {
1483+
const uint256& spent_id = range.first->second;
1484+
1485+
RecursiveUpdateTxState(/*batch=*/nullptr, spent_id, [&txid](CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) {
1486+
return wtx.mempool_conflicts.erase(txid) ? TxUpdate::CHANGED : TxUpdate::UNCHANGED;
1487+
});
1488+
}
1489+
}
14581490
}
14591491

14601492
void CWallet::blockConnected(ChainstateRole role, const interfaces::BlockInfo& block)

src/wallet/wallet.h

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

365365
/** Mark a transaction (and its in-wallet descendants) as a particular tx state. */
366366
void RecursiveUpdateTxState(const uint256& tx_hash, const TryUpdatingStateFn& try_updating_state) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
367+
void RecursiveUpdateTxState(WalletBatch* batch, const uint256& tx_hash, const TryUpdatingStateFn& try_updating_state) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
367368

368369
/** Mark a transaction's inputs dirty, thus forcing the outputs to be recomputed */
369370
void MarkInputsDirty(const CTransactionRef& tx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

test/functional/wallet_abandonconflict.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,11 @@ def run_test(self):
232232
balance = newbalance
233233

234234
# Invalidate the block with the double spend. B & C's 10 BTC outputs should no longer be available
235-
self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash())
235+
blk = self.nodes[0].getbestblockhash()
236+
# mine 10 blocks so that when the blk is invalidated, the transactions are not
237+
# returned to the mempool
238+
self.generate(self.nodes[1], 10)
239+
self.nodes[0].invalidateblock(blk)
236240
assert_equal(alice.gettransaction(txAB1)["confirmations"], 0)
237241
newbalance = alice.getbalance()
238242
assert_equal(newbalance, balance - Decimal("20"))

test/functional/wallet_conflicts.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,9 @@ def test_mempool_conflict(self):
174174
# broadcast tx2, replaces tx1 in mempool
175175
tx2_txid = alice.sendrawtransaction(tx2)
176176

177-
# Check that unspent[0] is still not available because the wallet does not know that the tx spending it has a mempool conflicted
178-
assert_equal(alice.listunspent(), [])
179-
assert_equal(alice.getbalance(), 0)
177+
# Check that unspent[0] is now available because the transaction spending it has been replaced in the mempool
178+
assert_equal(alice.listunspent(), [unspents[0]])
179+
assert_equal(alice.getbalance(), 25)
180180

181181
self.log.info("Test scenario where a mempool conflict is removed")
182182

@@ -262,8 +262,8 @@ def test_mempool_and_block_conflicts(self):
262262
assert tx2_txid in bob.getrawmempool()
263263
assert tx1_conflict_txid in bob.getrawmempool()
264264

265-
# check that the tx2 unspent is still not available because the wallet does not know that the tx spending it has a mempool conflict
266-
assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
265+
# check that tx3 is now conflicted, so the output from tx2 can now be spent
266+
assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("24.99990000"))
267267

268268
# we will be disconnecting this block in the future
269269
alice.sendrawtransaction(tx2_conflict)
@@ -293,7 +293,7 @@ def test_mempool_and_block_conflicts(self):
293293
assert_equal(bob.gettransaction(tx3_txid)["confirmations"], 0)
294294

295295
bob.sendrawtransaction(raw_tx2)
296-
assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0)
296+
assert_equal(bob.getbalances()["mine"]["untrusted_pending"], Decimal("24.99990000"))
297297

298298
# create a conflict to previous tx (also spends unspents[2]), but don't broadcast, sends funds back to alice
299299
raw_tx = alice.createrawtransaction(inputs=[unspents[2]], outputs=[{alice.getnewaddress() : 24.99}])

0 commit comments

Comments
 (0)