Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wallet: track mempool conflicts with wallet transactions #27307

Merged
merged 5 commits into from Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/wallet/interfaces.cpp
Expand Up @@ -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 :
ishaanam marked this conversation as resolved.
Show resolved Hide resolved
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);
Expand All @@ -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();
ishaanam marked this conversation as resolved.
Show resolved Hide resolved
return result;
}

Expand Down
7 changes: 3 additions & 4 deletions src/wallet/receive.cpp
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;

Expand Down
8 changes: 8 additions & 0 deletions src/wallet/rpc/transactions.cpp
Expand Up @@ -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});

Expand Down Expand Up @@ -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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR just so you don't have to re-touch it but it would be good to describe the difference between walletconflicts and mempoolconflicts inside the help text. walletconflicts description is quite vague.

{
{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 + "."},
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/transaction.cpp
Expand Up @@ -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);
}
}
Expand Down
27 changes: 18 additions & 9 deletions src/wallet/transaction.h
Expand Up @@ -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
Expand All @@ -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>;
Expand All @@ -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;
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -258,6 +258,14 @@ class CWalletTx
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;
ishaanam marked this conversation as resolved.
Show resolved Hide resolved

template<typename Stream>
void Serialize(Stream& s) const
{
Expand Down Expand Up @@ -335,9 +343,10 @@ class CWalletTx
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(); }
Expand Down
54 changes: 43 additions & 11 deletions src/wallet/wallet.cpp
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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());
}
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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));
Expand Down Expand Up @@ -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) {
ryanofsky marked this conversation as resolved.
Show resolved Hide resolved
// For each wallet transaction spending this prevout..
for (auto range = mapTxSpends.equal_range(tx_in.prevout); range.first != range.second; range.first++) {
ishaanam marked this conversation as resolved.
Show resolved Hide resolved
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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -2725,7 +2757,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;
}

Expand Down Expand Up @@ -3315,7 +3347,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 {
Expand Down
6 changes: 1 addition & 5 deletions src/wallet/wallet.h
Expand Up @@ -364,6 +364,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati

/** 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);
Expand Down Expand Up @@ -515,11 +516,6 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
* 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:
Expand Down
6 changes: 5 additions & 1 deletion test/functional/wallet_abandonconflict.py
Expand Up @@ -232,7 +232,11 @@ def run_test(self):
balance = newbalance

# Invalidate the block with the double spend. B & C's 10 BTC outputs should no longer be available
self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash())
blk = self.nodes[0].getbestblockhash()
# mine 10 blocks so that when the blk is invalidated, the transactions are not
# returned to the mempool
self.generate(self.nodes[1], 10)
ishaanam marked this conversation as resolved.
Show resolved Hide resolved
self.nodes[0].invalidateblock(blk)
assert_equal(alice.gettransaction(txAB1)["confirmations"], 0)
newbalance = alice.getbalance()
assert_equal(newbalance, balance - Decimal("20"))
Expand Down
2 changes: 1 addition & 1 deletion test/functional/wallet_basic.py
Expand Up @@ -679,7 +679,7 @@ def run_test(self):
"category": baz["category"],
"vout": baz["vout"]}
expected_fields = frozenset({'amount', 'bip125-replaceable', 'confirmations', 'details', 'fee',
'hex', 'lastprocessedblock', 'time', 'timereceived', 'trusted', 'txid', 'wtxid', 'walletconflicts'})
'hex', 'lastprocessedblock', 'time', 'timereceived', 'trusted', 'txid', 'wtxid', 'walletconflicts', 'mempoolconflicts'})
verbose_field = "decoded"
expected_verbose_fields = expected_fields | {verbose_field}

Expand Down