From 0b96b9c600e0dd946fd4d0e827e7f7cbef7a571a Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 18 Feb 2026 17:25:33 +0100 Subject: [PATCH] Minimize mempool lock, sync txo spender index only when and if needed We sync txospenderindex after we've checked the mempool for spending transaction, and only if search is not limited to the mempool and no spending transactions have been found for some of the provided outpoints. This should minimize the chance of having a block containing a spending transaction that is no longer in the mempool but has not been indexed yet. --- src/rpc/mempool.cpp | 63 +++++++++++++-------- test/functional/rpc_gettxspendingprevout.py | 4 ++ 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp index 68d6d4d5751..2fec90b2b49 100644 --- a/src/rpc/mempool.cpp +++ b/src/rpc/mempool.cpp @@ -31,6 +31,7 @@ #include #include +#include #include #include @@ -826,11 +827,15 @@ static RPCHelpMan gettxspendingprevout() {"return_spending_tx", UniValueType(UniValue::VBOOL)}, }, /*fAllowNull=*/true, /*fStrict=*/true); - const bool txospenderindex_ready{g_txospenderindex && g_txospenderindex->BlockUntilSyncedToCurrentChain()}; - const bool mempool_only{options.exists("mempool_only") ? options["mempool_only"].get_bool() : !txospenderindex_ready}; + const bool mempool_only{options.exists("mempool_only") ? options["mempool_only"].get_bool() : !g_txospenderindex}; const bool return_spending_tx{options.exists("return_spending_tx") ? options["return_spending_tx"].get_bool() : false}; - std::vector prevouts; + struct Entry { + const COutPoint prevout; + const UniValue& input; + UniValue output; + }; + std::vector prevouts; prevouts.reserve(output_params.size()); for (unsigned int idx = 0; idx < output_params.size(); idx++) { @@ -847,33 +852,45 @@ static RPCHelpMan gettxspendingprevout() if (nOutput < 0) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout cannot be negative"); } - - prevouts.emplace_back(txid, nOutput); + prevouts.emplace_back(COutPoint{txid, uint32_t(nOutput)}, o, UniValue{}); } - const CTxMemPool& mempool = EnsureAnyMemPool(request.context); - LOCK(mempool.cs); - - UniValue result{UniValue::VARR}; - - for (const COutPoint& prevout : prevouts) { - UniValue o(UniValue::VOBJ); - o.pushKV("txid", prevout.hash.ToString()); - o.pushKV("vout", prevout.n); - - const CTransaction* spendingTx = mempool.GetConflictTx(prevout); - if (spendingTx != nullptr) { - o.pushKV("spendingtxid", spendingTx->GetHash().ToString()); - if (return_spending_tx) { - o.pushKV("spendingtx", EncodeHexTx(*spendingTx)); + // search the mempool first + bool missing_from_mempool{false}; + { + const CTxMemPool& mempool = EnsureAnyMemPool(request.context); + LOCK(mempool.cs); + for (auto& entry : prevouts) { + const CTransaction* spendingTx = mempool.GetConflictTx(entry.prevout); + if (spendingTx != nullptr) { + UniValue o{entry.input}; + o.pushKV("spendingtxid", spendingTx->GetHash().ToString()); + if (return_spending_tx) { + o.pushKV("spendingtx", EncodeHexTx(*spendingTx)); + } + entry.output = std::move(o); + } else { + missing_from_mempool = true; } - } else if (mempool_only) { + } + } + // if search is not limited to the mempool and no spender was found for an outpoint, search the txospenderindex + // we call g_txospenderindex->BlockUntilSyncedToCurrentChain() only if g_txospenderindex is going to be used + UniValue result{UniValue::VARR}; + bool txospenderindex_ready{mempool_only || !missing_from_mempool || (g_txospenderindex && g_txospenderindex->BlockUntilSyncedToCurrentChain())}; + for (auto& entry : prevouts) { + if (!entry.output.isNull()) { + result.push_back(std::move(entry.output)); + continue; + } + UniValue o{entry.input}; + if (mempool_only) { // do nothing, caller has selected to only query the mempool } else if (!txospenderindex_ready) { - throw JSONRPCError(RPC_MISC_ERROR, strprintf("No spending tx for the outpoint %s:%d in mempool, and txospenderindex is unavailable.", prevout.hash.GetHex(), prevout.n)); + throw JSONRPCError(RPC_MISC_ERROR, strprintf("No spending tx for the outpoint %s:%d in mempool, and txospenderindex is unavailable.", entry.prevout.hash.GetHex(), entry.prevout.n)); } else { // no spending tx in mempool, query txospender index - const auto spender{g_txospenderindex->FindSpender(prevout)}; + const auto spender{g_txospenderindex->FindSpender(entry.prevout)}; if (!spender) { throw JSONRPCError(RPC_MISC_ERROR, spender.error()); } diff --git a/test/functional/rpc_gettxspendingprevout.py b/test/functional/rpc_gettxspendingprevout.py index 42efba37f85..05697dd87fd 100755 --- a/test/functional/rpc_gettxspendingprevout.py +++ b/test/functional/rpc_gettxspendingprevout.py @@ -116,6 +116,10 @@ class GetTxSpendingPrevoutTest(BitcoinTestFramework): result = self.nodes[2].gettxspendingprevout([{ 'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ], return_spending_tx=True) assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1}]) + # spending transaction is not found if we only search the mempool + result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ], return_spending_tx=True, mempool_only=True) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1}]) + self.log.info("Check that our txospenderindex is updated when a reorg replaces a spending transaction") confirmed_utxo = self.wallet.get_utxo(mark_as_spent = False) tx1 = create_tx(utxos_to_spend=[confirmed_utxo], num_outputs=1)