aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRyan Ofsky <ryan@ofsky.org>2024-03-27 12:41:00 -0400
committerRyan Ofsky <ryan@ofsky.org>2024-03-27 12:45:08 -0400
commitc8e3978114716bb8fb10695b9d187652f3ab4926 (patch)
treed72112498ea472284adbc1bd43f3b26d94c62ef3 /src
parent7a12cbed99cabac90e6c198c8ddbf37fff87abe7 (diff)
parent5952292133d6cc889f51ae771f2e0557311e1efe (diff)
downloadbitcoin-c8e3978114716bb8fb10695b9d187652f3ab4926.tar.xz
Merge bitcoin/bitcoin#27307: wallet: track mempool conflicts with wallet transactions
5952292133d6cc889f51ae771f2e0557311e1efe wallet, rpc: show mempool conflicts in `gettransaction` result (ishaanam) 54e07ee22ff16fc68583ade0d2b8ffffc81d444a wallet: track mempool conflicts (ishaanam) d64922b5903e5ffc8d2ce0e6761f99f173b60800 wallet refactor: use CWalletTx member functions to determine tx state (ishaanam) ffe5ff1fb622a8da11b66289e1b778e45e449811 scripted-diff: wallet: s/TxStateConflicted/TxStateBlockConflicted (ishaanam) 180973a94180f9849bf7cb0dab7c9177a942efb8 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 5952292133d6cc889f51ae771f2e0557311e1efe ryanofsky: Code review ACK 5952292133d6cc889f51ae771f2e0557311e1efe. Just small suggested changes since last review furszy: ACK 59522921 Tree-SHA512: 615779606723dbb6c2e302681d8e58ae2052ffee52d721ee0389746ddbbcf4b4c4afacf01ddf42b6405bc6f883520524186a955bf6b628fe9b3ae54cffc56a29
Diffstat (limited to 'src')
-rw-r--r--src/wallet/interfaces.cpp4
-rw-r--r--src/wallet/receive.cpp7
-rw-r--r--src/wallet/rpc/transactions.cpp8
-rw-r--r--src/wallet/transaction.cpp2
-rw-r--r--src/wallet/transaction.h27
-rw-r--r--src/wallet/wallet.cpp54
-rw-r--r--src/wallet/wallet.h6
7 files changed, 76 insertions, 32 deletions
diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp
index d15273dfc9..d33e6f3873 100644
--- a/src/wallet/interfaces.cpp
+++ b/src/wallet/interfaces.cpp
@@ -92,7 +92,7 @@ WalletTxStatus MakeWalletTxStatus(const CWallet& wallet, const CWalletTx& wtx)
WalletTxStatus result;
result.block_height =
wtx.state<TxStateConfirmed>() ? wtx.state<TxStateConfirmed>()->confirmed_block_height :
- wtx.state<TxStateConflicted>() ? wtx.state<TxStateConflicted>()->conflicting_block_height :
+ wtx.state<TxStateBlockConflicted>() ? wtx.state<TxStateBlockConflicted>()->conflicting_block_height :
std::numeric_limits<int>::max();
result.blocks_to_maturity = wallet.GetTxBlocksToMaturity(wtx);
result.depth_in_main_chain = wallet.GetTxDepthInMainChain(wtx);
@@ -101,7 +101,7 @@ WalletTxStatus MakeWalletTxStatus(const CWallet& wallet, const CWalletTx& wtx)
result.is_trusted = CachedTxIsTrusted(wallet, wtx);
result.is_abandoned = wtx.isAbandoned();
result.is_coinbase = wtx.IsCoinBase();
- result.is_in_main_chain = wallet.IsTxInMainChain(wtx);
+ result.is_in_main_chain = wtx.isConfirmed();
return result;
}
diff --git a/src/wallet/receive.cpp b/src/wallet/receive.cpp
index b9d8d9abc9..ea3ffff549 100644
--- a/src/wallet/receive.cpp
+++ b/src/wallet/receive.cpp
@@ -149,7 +149,7 @@ CAmount CachedTxGetImmatureCredit(const CWallet& wallet, const CWalletTx& wtx, c
{
AssertLockHeld(wallet.cs_wallet);
- if (wallet.IsTxImmatureCoinBase(wtx) && wallet.IsTxInMainChain(wtx)) {
+ if (wallet.IsTxImmatureCoinBase(wtx) && wtx.isConfirmed()) {
return GetCachableAmount(wallet, wtx, CWalletTx::IMMATURE_CREDIT, filter);
}
@@ -256,9 +256,8 @@ bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminef
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents)
{
AssertLockHeld(wallet.cs_wallet);
- int nDepth = wallet.GetTxDepthInMainChain(wtx);
- if (nDepth >= 1) return true;
- if (nDepth < 0) return false;
+ if (wtx.isConfirmed()) return true;
+ if (wtx.isBlockConflicted()) return false;
// using wtx's cached debit
if (!wallet.m_spend_zero_conf_change || !CachedTxIsFromMe(wallet, wtx, ISMINE_ALL)) return false;
diff --git a/src/wallet/rpc/transactions.cpp b/src/wallet/rpc/transactions.cpp
index e6c021d426..05b340995d 100644
--- a/src/wallet/rpc/transactions.cpp
+++ b/src/wallet/rpc/transactions.cpp
@@ -40,6 +40,10 @@ static void WalletTxToJSON(const CWallet& wallet, const CWalletTx& wtx, UniValue
for (const uint256& conflict : wallet.GetTxConflicts(wtx))
conflicts.push_back(conflict.GetHex());
entry.pushKV("walletconflicts", conflicts);
+ UniValue mempool_conflicts(UniValue::VARR);
+ for (const Txid& mempool_conflict : wtx.mempool_conflicts)
+ mempool_conflicts.push_back(mempool_conflict.GetHex());
+ entry.pushKV("mempoolconflicts", mempool_conflicts);
entry.pushKV("time", wtx.GetTxTime());
entry.pushKV("timereceived", int64_t{wtx.nTimeReceived});
@@ -417,6 +421,10 @@ static std::vector<RPCResult> TransactionDescriptionString()
}},
{RPCResult::Type::STR_HEX, "replaced_by_txid", /*optional=*/true, "Only if 'category' is 'send'. The txid if this tx was replaced."},
{RPCResult::Type::STR_HEX, "replaces_txid", /*optional=*/true, "Only if 'category' is 'send'. The txid if this tx replaces another."},
+ {RPCResult::Type::ARR, "mempoolconflicts", "Transactions that directly conflict with either this transaction or an ancestor transaction",
+ {
+ {RPCResult::Type::STR_HEX, "txid", "The transaction id."},
+ }},
{RPCResult::Type::STR, "to", /*optional=*/true, "If a comment to is associated with the transaction."},
{RPCResult::Type::NUM_TIME, "time", "The transaction time expressed in " + UNIX_EPOCH_TIME + "."},
{RPCResult::Type::NUM_TIME, "timereceived", "The time received expressed in " + UNIX_EPOCH_TIME + "."},
diff --git a/src/wallet/transaction.cpp b/src/wallet/transaction.cpp
index 6777257e53..561880482f 100644
--- a/src/wallet/transaction.cpp
+++ b/src/wallet/transaction.cpp
@@ -45,7 +45,7 @@ void CWalletTx::updateState(interfaces::Chain& chain)
};
if (auto* conf = state<TxStateConfirmed>()) {
lookup_block(conf->confirmed_block_hash, conf->confirmed_block_height, m_state);
- } else if (auto* conf = state<TxStateConflicted>()) {
+ } else if (auto* conf = state<TxStateBlockConflicted>()) {
lookup_block(conf->conflicting_block_hash, conf->conflicting_block_height, m_state);
}
}
diff --git a/src/wallet/transaction.h b/src/wallet/transaction.h
index ddeb931112..9c27574103 100644
--- a/src/wallet/transaction.h
+++ b/src/wallet/transaction.h
@@ -43,12 +43,12 @@ struct TxStateInMempool {
};
//! State of rejected transaction that conflicts with a confirmed block.
-struct TxStateConflicted {
+struct TxStateBlockConflicted {
uint256 conflicting_block_hash;
int conflicting_block_height;
- explicit TxStateConflicted(const uint256& block_hash, int height) : conflicting_block_hash(block_hash), conflicting_block_height(height) {}
- std::string toString() const { return strprintf("Conflicted (block=%s, height=%i)", conflicting_block_hash.ToString(), conflicting_block_height); }
+ explicit TxStateBlockConflicted(const uint256& block_hash, int height) : conflicting_block_hash(block_hash), conflicting_block_height(height) {}
+ std::string toString() const { return strprintf("BlockConflicted (block=%s, height=%i)", conflicting_block_hash.ToString(), conflicting_block_height); }
};
//! State of transaction not confirmed or conflicting with a known block and
@@ -75,7 +75,7 @@ struct TxStateUnrecognized {
};
//! All possible CWalletTx states
-using TxState = std::variant<TxStateConfirmed, TxStateInMempool, TxStateConflicted, TxStateInactive, TxStateUnrecognized>;
+using TxState = std::variant<TxStateConfirmed, TxStateInMempool, TxStateBlockConflicted, TxStateInactive, TxStateUnrecognized>;
//! Subset of states transaction sync logic is implemented to handle.
using SyncTxState = std::variant<TxStateConfirmed, TxStateInMempool, TxStateInactive>;
@@ -90,7 +90,7 @@ static inline TxState TxStateInterpretSerialized(TxStateUnrecognized data)
} else if (data.index >= 0) {
return TxStateConfirmed{data.block_hash, /*height=*/-1, data.index};
} else if (data.index == -1) {
- return TxStateConflicted{data.block_hash, /*height=*/-1};
+ return TxStateBlockConflicted{data.block_hash, /*height=*/-1};
}
return data;
}
@@ -102,7 +102,7 @@ static inline uint256 TxStateSerializedBlockHash(const TxState& state)
[](const TxStateInactive& inactive) { return inactive.abandoned ? uint256::ONE : uint256::ZERO; },
[](const TxStateInMempool& in_mempool) { return uint256::ZERO; },
[](const TxStateConfirmed& confirmed) { return confirmed.confirmed_block_hash; },
- [](const TxStateConflicted& conflicted) { return conflicted.conflicting_block_hash; },
+ [](const TxStateBlockConflicted& conflicted) { return conflicted.conflicting_block_hash; },
[](const TxStateUnrecognized& unrecognized) { return unrecognized.block_hash; }
}, state);
}
@@ -114,7 +114,7 @@ static inline int TxStateSerializedIndex(const TxState& state)
[](const TxStateInactive& inactive) { return inactive.abandoned ? -1 : 0; },
[](const TxStateInMempool& in_mempool) { return 0; },
[](const TxStateConfirmed& confirmed) { return confirmed.position_in_block; },
- [](const TxStateConflicted& conflicted) { return -1; },
+ [](const TxStateBlockConflicted& conflicted) { return -1; },
[](const TxStateUnrecognized& unrecognized) { return unrecognized.index; }
}, state);
}
@@ -258,6 +258,14 @@ public:
CTransactionRef tx;
TxState m_state;
+ // Set of mempool transactions that conflict
+ // directly with the transaction, or that conflict
+ // with an ancestor transaction. This set will be
+ // empty if state is InMempool or Confirmed, but
+ // can be nonempty if state is Inactive or
+ // BlockConflicted.
+ std::set<Txid> mempool_conflicts;
+
template<typename Stream>
void Serialize(Stream& s) const
{
@@ -335,9 +343,10 @@ public:
void updateState(interfaces::Chain& chain);
bool isAbandoned() const { return state<TxStateInactive>() && state<TxStateInactive>()->abandoned; }
- bool isConflicted() const { return state<TxStateConflicted>(); }
+ bool isMempoolConflicted() const { return !mempool_conflicts.empty(); }
+ bool isBlockConflicted() const { return state<TxStateBlockConflicted>(); }
bool isInactive() const { return state<TxStateInactive>(); }
- bool isUnconfirmed() const { return !isAbandoned() && !isConflicted() && !isConfirmed(); }
+ bool isUnconfirmed() const { return !isAbandoned() && !isBlockConflicted() && !isMempoolConflicted() && !isConfirmed(); }
bool isConfirmed() const { return state<TxStateConfirmed>(); }
const Txid& GetHash() const LIFETIMEBOUND { return tx->GetHash(); }
const Wtxid& GetWitnessHash() const LIFETIMEBOUND { return tx->GetWitnessHash(); }
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index 9c15c2a827..591c5eca6e 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -752,8 +752,8 @@ bool CWallet::IsSpent(const COutPoint& outpoint) const
const uint256& wtxid = it->second;
const auto mit = mapWallet.find(wtxid);
if (mit != mapWallet.end()) {
- int depth = GetTxDepthInMainChain(mit->second);
- if (depth > 0 || (depth == 0 && !mit->second.isAbandoned()))
+ const auto& wtx = mit->second;
+ if (!wtx.isAbandoned() && !wtx.isBlockConflicted() && !wtx.isMempoolConflicted())
return true; // Spent
}
}
@@ -1197,7 +1197,7 @@ bool CWallet::LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx
auto it = mapWallet.find(txin.prevout.hash);
if (it != mapWallet.end()) {
CWalletTx& prevtx = it->second;
- if (auto* prev = prevtx.state<TxStateConflicted>()) {
+ if (auto* prev = prevtx.state<TxStateBlockConflicted>()) {
MarkConflicted(prev->conflicting_block_hash, prev->conflicting_block_height, wtx.GetHash());
}
}
@@ -1309,7 +1309,7 @@ bool CWallet::AbandonTransaction(const uint256& hashTx)
assert(!wtx.isConfirmed());
assert(!wtx.InMempool());
// If already conflicted or abandoned, no need to set abandoned
- if (!wtx.isConflicted() && !wtx.isAbandoned()) {
+ if (!wtx.isBlockConflicted() && !wtx.isAbandoned()) {
wtx.m_state = TxStateInactive{/*abandoned=*/true};
return TxUpdate::NOTIFY_CHANGED;
}
@@ -1346,7 +1346,7 @@ void CWallet::MarkConflicted(const uint256& hashBlock, int conflicting_height, c
if (conflictconfirms < GetTxDepthInMainChain(wtx)) {
// Block is 'more conflicted' than current confirm; update.
// Mark transaction as conflicted with this block.
- wtx.m_state = TxStateConflicted{hashBlock, conflicting_height};
+ wtx.m_state = TxStateBlockConflicted{hashBlock, conflicting_height};
return TxUpdate::CHANGED;
}
return TxUpdate::UNCHANGED;
@@ -1360,7 +1360,10 @@ void CWallet::MarkConflicted(const uint256& hashBlock, int conflicting_height, c
void CWallet::RecursiveUpdateTxState(const uint256& tx_hash, const TryUpdatingStateFn& try_updating_state) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) {
// Do not flush the wallet here for performance reasons
WalletBatch batch(GetDatabase(), false);
+ RecursiveUpdateTxState(&batch, tx_hash, try_updating_state);
+}
+void CWallet::RecursiveUpdateTxState(WalletBatch* batch, const uint256& tx_hash, const TryUpdatingStateFn& try_updating_state) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) {
std::set<uint256> todo;
std::set<uint256> done;
@@ -1377,7 +1380,7 @@ void CWallet::RecursiveUpdateTxState(const uint256& tx_hash, const TryUpdatingSt
TxUpdate update_state = try_updating_state(wtx);
if (update_state != TxUpdate::UNCHANGED) {
wtx.MarkDirty();
- batch.WriteTx(wtx);
+ if (batch) batch->WriteTx(wtx);
// Iterate over all its outputs, and update those tx states as well (if applicable)
for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
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) {
if (it != mapWallet.end()) {
RefreshMempoolStatus(it->second, chain());
}
+
+ const Txid& txid = tx->GetHash();
+
+ for (const CTxIn& tx_in : tx->vin) {
+ // For each wallet transaction spending this prevout..
+ for (auto range = mapTxSpends.equal_range(tx_in.prevout); range.first != range.second; range.first++) {
+ const uint256& spent_id = range.first->second;
+ // Skip the recently added tx
+ if (spent_id == txid) continue;
+ RecursiveUpdateTxState(/*batch=*/nullptr, spent_id, [&txid](CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) {
+ return wtx.mempool_conflicts.insert(txid).second ? TxUpdate::CHANGED : TxUpdate::UNCHANGED;
+ });
+ }
+ }
}
void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason) {
@@ -1455,6 +1472,21 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe
// https://github.com/bitcoin-core/bitcoin-devwiki/wiki/Wallet-Transaction-Conflict-Tracking
SyncTransaction(tx, TxStateInactive{});
}
+
+ const Txid& txid = tx->GetHash();
+
+ for (const CTxIn& tx_in : tx->vin) {
+ // Iterate over all wallet transactions spending txin.prev
+ // and recursively mark them as no longer conflicting with
+ // txid
+ for (auto range = mapTxSpends.equal_range(tx_in.prevout); range.first != range.second; range.first++) {
+ const uint256& spent_id = range.first->second;
+
+ RecursiveUpdateTxState(/*batch=*/nullptr, spent_id, [&txid](CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) {
+ return wtx.mempool_conflicts.erase(txid) ? TxUpdate::CHANGED : TxUpdate::UNCHANGED;
+ });
+ }
+ }
}
void CWallet::blockConnected(ChainstateRole role, const interfaces::BlockInfo& block)
@@ -1506,11 +1538,11 @@ void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
for (TxSpends::const_iterator _it = range.first; _it != range.second; ++_it) {
CWalletTx& wtx = mapWallet.find(_it->second)->second;
- if (!wtx.isConflicted()) continue;
+ if (!wtx.isBlockConflicted()) continue;
auto try_updating_state = [&](CWalletTx& tx) {
- if (!tx.isConflicted()) return TxUpdate::UNCHANGED;
- if (tx.state<TxStateConflicted>()->conflicting_block_height >= disconnect_height) {
+ if (!tx.isBlockConflicted()) return TxUpdate::UNCHANGED;
+ if (tx.state<TxStateBlockConflicted>()->conflicting_block_height >= disconnect_height) {
tx.m_state = TxStateInactive{};
return TxUpdate::CHANGED;
}
@@ -2787,7 +2819,7 @@ unsigned int CWallet::ComputeTimeSmart(const CWalletTx& wtx, bool rescanning_old
std::optional<uint256> block_hash;
if (auto* conf = wtx.state<TxStateConfirmed>()) {
block_hash = conf->confirmed_block_hash;
- } else if (auto* conf = wtx.state<TxStateConflicted>()) {
+ } else if (auto* conf = wtx.state<TxStateBlockConflicted>()) {
block_hash = conf->conflicting_block_hash;
}
@@ -3377,7 +3409,7 @@ int CWallet::GetTxDepthInMainChain(const CWalletTx& wtx) const
if (auto* conf = wtx.state<TxStateConfirmed>()) {
assert(conf->confirmed_block_height >= 0);
return GetLastBlockHeight() - conf->confirmed_block_height + 1;
- } else if (auto* conf = wtx.state<TxStateConflicted>()) {
+ } else if (auto* conf = wtx.state<TxStateBlockConflicted>()) {
assert(conf->conflicting_block_height >= 0);
return -1 * (GetLastBlockHeight() - conf->conflicting_block_height + 1);
} else {
diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h
index 8b0ee22276..fdfe0a8b65 100644
--- a/src/wallet/wallet.h
+++ b/src/wallet/wallet.h
@@ -364,6 +364,7 @@ private:
/** Mark a transaction (and its in-wallet descendants) as a particular tx state. */
void RecursiveUpdateTxState(const uint256& tx_hash, const TryUpdatingStateFn& try_updating_state) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
+ void RecursiveUpdateTxState(WalletBatch* batch, const uint256& tx_hash, const TryUpdatingStateFn& try_updating_state) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
/** Mark a transaction's inputs dirty, thus forcing the outputs to be recomputed */
void MarkInputsDirty(const CTransactionRef& tx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
@@ -518,11 +519,6 @@ public:
* referenced in transaction, and might cause assert failures.
*/
int GetTxDepthInMainChain(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
- bool IsTxInMainChain(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet)
- {
- AssertLockHeld(cs_wallet);
- return GetTxDepthInMainChain(wtx) > 0;
- }
/**
* @return number of blocks to maturity for this transaction: