wallet: mark unconfirmed v3 siblings as mempool conflicts
What changed, and why it matters
This Bitcoin Core wallet patch fixes how the wallet tracks conflicting transactions for a new type of transaction called TRUC (v3). TRUC rules allow only one unconfirmed child of a parent in the mempool at a time. The wallet now correctly marks other wallet transactions that spend from the same parent as 'mempool conflicts' when one TRUC child enters or leaves the mempool. Without this, the wallet could misreport whether these sibling transactions are spendable or likely to confirm, potentially leading users to make decisions based on stale or incorrect transaction state.
Treat as a wallet correctness/bug-fix patch with limited security impact. Users relying on TRUC/v3 transactions should upgrade to ensure accurate wallet state. No immediate emergency response is indicated; standard patch deployment is appropriate.
Security signals we found
Wallet state desynchronization with mempool policy
Incorrect transaction conflict tracking for TRUC/v3 transactions
Potential stale balance or spendable-UTXO reporting
No cryptographic or consensus vulnerability in the diff
Evidence from the diff
The commit adds CWallet::UpdateTrucSiblingConflicts(), which iterates over all outputs of a parent wallet transaction and uses mapTxSpends to find sibling transactions spending those outputs. When a TRUC/v3 child is added to or removed from the mempool, it updates each sibling’s mempool_conflicts set accordingly via RecursiveUpdateTxState(). This addresses a wallet state-tracking gap: under TRUC policy, only one unconfirmed child per parent may exist in the mempool, so siblings that do not double-spend the same UTXO are still mutually exclusive with the in-mempool child. Previously the wallet did not record this conflict, which could affect balance/availability calculations and user-visible transaction status.
Changed components
src/wallet/wallet.cppsrc/wallet/wallet.hBitcoin Core wallet mempool conflict trackingInspect captured patch +24 / −0
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index 0814d672..2db55273 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -1234,6 +1234,23 @@ bool CWallet::TransactionCanBeAbandoned(const Txid& hashTx) const
return wtx && !wtx->isAbandoned() && GetTxDepthInMainChain(*wtx) == 0 && !wtx->InMempool();
}
+void CWallet::UpdateTrucSiblingConflicts(const CWalletTx& parent_wtx, const Txid& child_txid, bool add_conflict) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet)
+{
+ // Find all other txs in our wallet that spend utxos from this parent
+ // so that we can mark them as mempool-conflicted by this new tx.
+ for (long unsigned int i = 0; i < parent_wtx.tx->vout.size(); i++) {
+ for (auto range = mapTxSpends.equal_range(COutPoint(parent_wtx.tx->GetHash(), i)); range.first != range.second; range.first++) {
+ const Txid& sibling_txid = range.first->second;
+ // Skip the child_tx itself
+ if (sibling_txid == child_txid) continue;
+ RecursiveUpdateTxState(/*batch=*/nullptr, sibling_txid, [&child_txid, add_conflict](CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) {
+ return add_conflict ? (wtx.mempool_conflicts.insert(child_txid).second ? TxUpdate::CHANGED : TxUpdate::UNCHANGED)
+ : (wtx.mempool_conflicts.erase(child_txid) ? TxUpdate::CHANGED : TxUpdate::UNCHANGED);
+ });
+ }
+ }
+}
+
void CWallet::MarkInputsDirty(const CTransactionRef& tx)
{
for (const CTxIn& txin : tx->vin) {
@@ -1402,6 +1419,9 @@ void CWallet::transactionAddedToMempool(const CTransactionRef& tx) {
CWalletTx& parent_wtx = parent_it->second;
if (parent_wtx.isUnconfirmed()) {
parent_wtx.truc_child_in_mempool = tx->GetHash();
+ // Even though these siblings do not spend the same utxos, they can't
+ // be present in the mempool at the same time because of TRUC policy rules
+ UpdateTrucSiblingConflicts(parent_wtx, txid, /*add_conflict=*/true);
}
}
}
@@ -1470,6 +1490,7 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe
CWalletTx& parent_wtx = parent_it->second;
if (parent_wtx.truc_child_in_mempool == tx->GetHash()) {
parent_wtx.truc_child_in_mempool = std::nullopt;
+ UpdateTrucSiblingConflicts(parent_wtx, txid, /*add_conflict=*/false);
}
}
}
diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h
index 5ef21025..baeb8943 100644
--- a/src/wallet/wallet.h
+++ b/src/wallet/wallet.h
@@ -453,6 +453,9 @@ private:
// Update last block processed in memory only
void SetLastBlockProcessedInMem(int block_height, uint256 block_hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
+ //! Update mempool conflicts for TRUC sibling transactions
+ void UpdateTrucSiblingConflicts(const CWalletTx& parent_wtx, const Txid& child_txid, bool add_conflict) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
+
public:
/**
* Main wallet lock.
Why this scored 33/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.