Skip to content

Commit c8e3978

Browse files
committed
Merge bitcoin/bitcoin#27307: wallet: track mempool conflicts with wallet transactions
5952292 wallet, rpc: show mempool conflicts in `gettransaction` result (ishaanam) 54e07ee wallet: track mempool conflicts (ishaanam) d64922b wallet refactor: use CWalletTx member functions to determine tx state (ishaanam) ffe5ff1 scripted-diff: wallet: s/TxStateConflicted/TxStateBlockConflicted (ishaanam) 180973a test: Add tests for wallet mempool conflicts (ishaanam) Pull request description: The `mempool_conflicts` variable is added to `CWalletTx`, it is a set of txids of txs in the mempool conflicting with the wallet tx or a wallet tx's parent. This PR only changes how mempool-conflicted txs are dealt with in memory. `IsSpent` now returns false for an output being spent by a mempool conflicted transaction where it previously returned true. A txid is added to `mempool_conflicts` during `transactionAddedToMempool`. A txid is removed from `mempool_conflicts` during `transactionRemovedFromMempool`. This PR also adds a `mempoolconflicts` field to the `gettransaction` wallet RPC result. Builds on #27145 Second attempt at #18600 ACKs for top commit: achow101: ACK 5952292 ryanofsky: Code review ACK 5952292. Just small suggested changes since last review furszy: ACK 5952292 Tree-SHA512: 615779606723dbb6c2e302681d8e58ae2052ffee52d721ee0389746ddbbcf4b4c4afacf01ddf42b6405bc6f883520524186a955bf6b628fe9b3ae54cffc56a29
2 parents 7a12cbe + 5952292 commit c8e3978

10 files changed

+383
-34
lines changed

src/wallet/interfaces.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ WalletTxStatus MakeWalletTxStatus(const CWallet& wallet, const CWalletTx& wtx)
9292
WalletTxStatus result;
9393
result.block_height =
9494
wtx.state<TxStateConfirmed>() ? wtx.state<TxStateConfirmed>()->confirmed_block_height :
95-
wtx.state<TxStateConflicted>() ? wtx.state<TxStateConflicted>()->conflicting_block_height :
95+
wtx.state<TxStateBlockConflicted>() ? wtx.state<TxStateBlockConflicted>()->conflicting_block_height :
9696
std::numeric_limits<int>::max();
9797
result.blocks_to_maturity = wallet.GetTxBlocksToMaturity(wtx);
9898
result.depth_in_main_chain = wallet.GetTxDepthInMainChain(wtx);
@@ -101,7 +101,7 @@ WalletTxStatus MakeWalletTxStatus(const CWallet& wallet, const CWalletTx& wtx)
101101
result.is_trusted = CachedTxIsTrusted(wallet, wtx);
102102
result.is_abandoned = wtx.isAbandoned();
103103
result.is_coinbase = wtx.IsCoinBase();
104-
result.is_in_main_chain = wallet.IsTxInMainChain(wtx);
104+
result.is_in_main_chain = wtx.isConfirmed();
105105
return result;
106106
}
107107

src/wallet/receive.cpp

+3-4
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ CAmount CachedTxGetImmatureCredit(const CWallet& wallet, const CWalletTx& wtx, c
149149
{
150150
AssertLockHeld(wallet.cs_wallet);
151151

152-
if (wallet.IsTxImmatureCoinBase(wtx) && wallet.IsTxInMainChain(wtx)) {
152+
if (wallet.IsTxImmatureCoinBase(wtx) && wtx.isConfirmed()) {
153153
return GetCachableAmount(wallet, wtx, CWalletTx::IMMATURE_CREDIT, filter);
154154
}
155155

@@ -256,9 +256,8 @@ bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminef
256256
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents)
257257
{
258258
AssertLockHeld(wallet.cs_wallet);
259-
int nDepth = wallet.GetTxDepthInMainChain(wtx);
260-
if (nDepth >= 1) return true;
261-
if (nDepth < 0) return false;
259+
if (wtx.isConfirmed()) return true;
260+
if (wtx.isBlockConflicted()) return false;
262261
// using wtx's cached debit
263262
if (!wallet.m_spend_zero_conf_change || !CachedTxIsFromMe(wallet, wtx, ISMINE_ALL)) return false;
264263

src/wallet/rpc/transactions.cpp

+8
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ static void WalletTxToJSON(const CWallet& wallet, const CWalletTx& wtx, UniValue
4040
for (const uint256& conflict : wallet.GetTxConflicts(wtx))
4141
conflicts.push_back(conflict.GetHex());
4242
entry.pushKV("walletconflicts", conflicts);
43+
UniValue mempool_conflicts(UniValue::VARR);
44+
for (const Txid& mempool_conflict : wtx.mempool_conflicts)
45+
mempool_conflicts.push_back(mempool_conflict.GetHex());
46+
entry.pushKV("mempoolconflicts", mempool_conflicts);
4347
entry.pushKV("time", wtx.GetTxTime());
4448
entry.pushKV("timereceived", int64_t{wtx.nTimeReceived});
4549

@@ -417,6 +421,10 @@ static std::vector<RPCResult> TransactionDescriptionString()
417421
}},
418422
{RPCResult::Type::STR_HEX, "replaced_by_txid", /*optional=*/true, "Only if 'category' is 'send'. The txid if this tx was replaced."},
419423
{RPCResult::Type::STR_HEX, "replaces_txid", /*optional=*/true, "Only if 'category' is 'send'. The txid if this tx replaces another."},
424+
{RPCResult::Type::ARR, "mempoolconflicts", "Transactions that directly conflict with either this transaction or an ancestor transaction",
425+
{
426+
{RPCResult::Type::STR_HEX, "txid", "The transaction id."},
427+
}},
420428
{RPCResult::Type::STR, "to", /*optional=*/true, "If a comment to is associated with the transaction."},
421429
{RPCResult::Type::NUM_TIME, "time", "The transaction time expressed in " + UNIX_EPOCH_TIME + "."},
422430
{RPCResult::Type::NUM_TIME, "timereceived", "The time received expressed in " + UNIX_EPOCH_TIME + "."},

src/wallet/transaction.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ void CWalletTx::updateState(interfaces::Chain& chain)
4545
};
4646
if (auto* conf = state<TxStateConfirmed>()) {
4747
lookup_block(conf->confirmed_block_hash, conf->confirmed_block_height, m_state);
48-
} else if (auto* conf = state<TxStateConflicted>()) {
48+
} else if (auto* conf = state<TxStateBlockConflicted>()) {
4949
lookup_block(conf->conflicting_block_hash, conf->conflicting_block_height, m_state);
5050
}
5151
}

src/wallet/transaction.h

+18-9
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ struct TxStateInMempool {
4343
};
4444

4545
//! State of rejected transaction that conflicts with a confirmed block.
46-
struct TxStateConflicted {
46+
struct TxStateBlockConflicted {
4747
uint256 conflicting_block_hash;
4848
int conflicting_block_height;
4949

50-
explicit TxStateConflicted(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); }
50+
explicit TxStateBlockConflicted(const uint256& block_hash, int height) : conflicting_block_hash(block_hash), conflicting_block_height(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
@@ -75,7 +75,7 @@ struct TxStateUnrecognized {
7575
};
7676

7777
//! All possible CWalletTx states
78-
using TxState = std::variant<TxStateConfirmed, TxStateInMempool, TxStateConflicted, TxStateInactive, TxStateUnrecognized>;
78+
using TxState = std::variant<TxStateConfirmed, TxStateInMempool, TxStateBlockConflicted, TxStateInactive, TxStateUnrecognized>;
7979

8080
//! Subset of states transaction sync logic is implemented to handle.
8181
using SyncTxState = std::variant<TxStateConfirmed, TxStateInMempool, TxStateInactive>;
@@ -90,7 +90,7 @@ static inline TxState TxStateInterpretSerialized(TxStateUnrecognized data)
9090
} else if (data.index >= 0) {
9191
return TxStateConfirmed{data.block_hash, /*height=*/-1, data.index};
9292
} else if (data.index == -1) {
93-
return TxStateConflicted{data.block_hash, /*height=*/-1};
93+
return TxStateBlockConflicted{data.block_hash, /*height=*/-1};
9494
}
9595
return data;
9696
}
@@ -102,7 +102,7 @@ static inline uint256 TxStateSerializedBlockHash(const TxState& state)
102102
[](const TxStateInactive& inactive) { return inactive.abandoned ? uint256::ONE : uint256::ZERO; },
103103
[](const TxStateInMempool& in_mempool) { return uint256::ZERO; },
104104
[](const TxStateConfirmed& confirmed) { return confirmed.confirmed_block_hash; },
105-
[](const TxStateConflicted& conflicted) { return conflicted.conflicting_block_hash; },
105+
[](const TxStateBlockConflicted& conflicted) { return conflicted.conflicting_block_hash; },
106106
[](const TxStateUnrecognized& unrecognized) { return unrecognized.block_hash; }
107107
}, state);
108108
}
@@ -114,7 +114,7 @@ static inline int TxStateSerializedIndex(const TxState& state)
114114
[](const TxStateInactive& inactive) { return inactive.abandoned ? -1 : 0; },
115115
[](const TxStateInMempool& in_mempool) { return 0; },
116116
[](const TxStateConfirmed& confirmed) { return confirmed.position_in_block; },
117-
[](const TxStateConflicted& conflicted) { return -1; },
117+
[](const TxStateBlockConflicted& conflicted) { return -1; },
118118
[](const TxStateUnrecognized& unrecognized) { return unrecognized.index; }
119119
}, state);
120120
}
@@ -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; }
338-
bool isConflicted() const { return state<TxStateConflicted>(); }
346+
bool isMempoolConflicted() const { return !mempool_conflicts.empty(); }
347+
bool isBlockConflicted() const { return state<TxStateBlockConflicted>(); }
339348
bool isInactive() const { return state<TxStateInactive>(); }
340-
bool isUnconfirmed() const { return !isAbandoned() && !isConflicted() && !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

+43-11
Original file line numberDiff line numberDiff line change
@@ -752,8 +752,8 @@ bool CWallet::IsSpent(const COutPoint& outpoint) const
752752
const uint256& wtxid = it->second;
753753
const auto mit = mapWallet.find(wtxid);
754754
if (mit != mapWallet.end()) {
755-
int depth = GetTxDepthInMainChain(mit->second);
756-
if (depth > 0 || (depth == 0 && !mit->second.isAbandoned()))
755+
const auto& wtx = mit->second;
756+
if (!wtx.isAbandoned() && !wtx.isBlockConflicted() && !wtx.isMempoolConflicted())
757757
return true; // Spent
758758
}
759759
}
@@ -1197,7 +1197,7 @@ bool CWallet::LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx
11971197
auto it = mapWallet.find(txin.prevout.hash);
11981198
if (it != mapWallet.end()) {
11991199
CWalletTx& prevtx = it->second;
1200-
if (auto* prev = prevtx.state<TxStateConflicted>()) {
1200+
if (auto* prev = prevtx.state<TxStateBlockConflicted>()) {
12011201
MarkConflicted(prev->conflicting_block_hash, prev->conflicting_block_height, wtx.GetHash());
12021202
}
12031203
}
@@ -1309,7 +1309,7 @@ bool CWallet::AbandonTransaction(const uint256& hashTx)
13091309
assert(!wtx.isConfirmed());
13101310
assert(!wtx.InMempool());
13111311
// If already conflicted or abandoned, no need to set abandoned
1312-
if (!wtx.isConflicted() && !wtx.isAbandoned()) {
1312+
if (!wtx.isBlockConflicted() && !wtx.isAbandoned()) {
13131313
wtx.m_state = TxStateInactive{/*abandoned=*/true};
13141314
return TxUpdate::NOTIFY_CHANGED;
13151315
}
@@ -1346,7 +1346,7 @@ void CWallet::MarkConflicted(const uint256& hashBlock, int conflicting_height, c
13461346
if (conflictconfirms < GetTxDepthInMainChain(wtx)) {
13471347
// Block is 'more conflicted' than current confirm; update.
13481348
// Mark transaction as conflicted with this block.
1349-
wtx.m_state = TxStateConflicted{hashBlock, conflicting_height};
1349+
wtx.m_state = TxStateBlockConflicted{hashBlock, conflicting_height};
13501350
return TxUpdate::CHANGED;
13511351
}
13521352
return TxUpdate::UNCHANGED;
@@ -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)
@@ -1506,11 +1538,11 @@ void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
15061538
for (TxSpends::const_iterator _it = range.first; _it != range.second; ++_it) {
15071539
CWalletTx& wtx = mapWallet.find(_it->second)->second;
15081540

1509-
if (!wtx.isConflicted()) continue;
1541+
if (!wtx.isBlockConflicted()) continue;
15101542

15111543
auto try_updating_state = [&](CWalletTx& tx) {
1512-
if (!tx.isConflicted()) return TxUpdate::UNCHANGED;
1513-
if (tx.state<TxStateConflicted>()->conflicting_block_height >= disconnect_height) {
1544+
if (!tx.isBlockConflicted()) return TxUpdate::UNCHANGED;
1545+
if (tx.state<TxStateBlockConflicted>()->conflicting_block_height >= disconnect_height) {
15141546
tx.m_state = TxStateInactive{};
15151547
return TxUpdate::CHANGED;
15161548
}
@@ -2787,7 +2819,7 @@ unsigned int CWallet::ComputeTimeSmart(const CWalletTx& wtx, bool rescanning_old
27872819
std::optional<uint256> block_hash;
27882820
if (auto* conf = wtx.state<TxStateConfirmed>()) {
27892821
block_hash = conf->confirmed_block_hash;
2790-
} else if (auto* conf = wtx.state<TxStateConflicted>()) {
2822+
} else if (auto* conf = wtx.state<TxStateBlockConflicted>()) {
27912823
block_hash = conf->conflicting_block_hash;
27922824
}
27932825

@@ -3377,7 +3409,7 @@ int CWallet::GetTxDepthInMainChain(const CWalletTx& wtx) const
33773409
if (auto* conf = wtx.state<TxStateConfirmed>()) {
33783410
assert(conf->confirmed_block_height >= 0);
33793411
return GetLastBlockHeight() - conf->confirmed_block_height + 1;
3380-
} else if (auto* conf = wtx.state<TxStateConflicted>()) {
3412+
} else if (auto* conf = wtx.state<TxStateBlockConflicted>()) {
33813413
assert(conf->conflicting_block_height >= 0);
33823414
return -1 * (GetLastBlockHeight() - conf->conflicting_block_height + 1);
33833415
} else {

src/wallet/wallet.h

+1-5
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);
@@ -518,11 +519,6 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
518519
* referenced in transaction, and might cause assert failures.
519520
*/
520521
int GetTxDepthInMainChain(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
521-
bool IsTxInMainChain(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet)
522-
{
523-
AssertLockHeld(cs_wallet);
524-
return GetTxDepthInMainChain(wtx) > 0;
525-
}
526522

527523
/**
528524
* @return number of blocks to maturity for this transaction:

test/functional/wallet_abandonconflict.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,11 @@ def run_test(self):
231231
balance = newbalance
232232

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

test/functional/wallet_basic.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ def run_test(self):
681681
"category": baz["category"],
682682
"vout": baz["vout"]}
683683
expected_fields = frozenset({'amount', 'bip125-replaceable', 'confirmations', 'details', 'fee',
684-
'hex', 'lastprocessedblock', 'time', 'timereceived', 'trusted', 'txid', 'wtxid', 'walletconflicts'})
684+
'hex', 'lastprocessedblock', 'time', 'timereceived', 'trusted', 'txid', 'wtxid', 'walletconflicts', 'mempoolconflicts'})
685685
verbose_field = "decoded"
686686
expected_verbose_fields = expected_fields | {verbose_field}
687687

0 commit comments

Comments
 (0)