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.
This commit is contained in:
sstone 2026-02-18 17:25:33 +01:00
parent 3d82ec5bdd
commit 0b96b9c600
No known key found for this signature in database
GPG Key ID: E04E48E72C205463
2 changed files with 44 additions and 23 deletions

View File

@ -31,6 +31,7 @@
#include <util/time.h>
#include <util/vector.h>
#include <map>
#include <string_view>
#include <utility>
@ -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<COutPoint> prevouts;
struct Entry {
const COutPoint prevout;
const UniValue& input;
UniValue output;
};
std::vector<Entry> 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());
}

View File

@ -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)