From 5e77072fa60110a00d2bff31798d58b6c10bd3da Mon Sep 17 00:00:00 2001 From: w0xlt <94266259+w0xlt@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:57:32 -0800 Subject: [PATCH 1/2] rpc: fix race condition in gettxoutsetinfo Fix an assertion failure in gettxoutsetinfo (issue #34263) caused by capturing the best block before releasing cs_main, then checking it against a potentially newer best block in GetUTXOStats(). Remove the early pindex capture since ComputeUTXOStats() independently fetches the current best block under lock. Use stats.hashBlock and stats.nHeight (the actual computed values) instead of the potentially stale pindex when building the response. --- src/rpc/blockchain.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index d97b7c6c15c..c631a93674a 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1080,7 +1080,6 @@ static RPCHelpMan gettxoutsetinfo() LOCK(::cs_main); coins_view = &active_chainstate.CoinsDB(); blockman = &active_chainstate.m_blockman; - pindex = blockman->LookupBlockIndex(coins_view->GetBestBlock()); } if (!request.params[1].isNull()) { @@ -1104,7 +1103,7 @@ static RPCHelpMan gettxoutsetinfo() // If a specific block was requested and the index has already synced past that height, we can return the // data already even though the index is not fully synced yet. - if (pindex->nHeight > summary.best_block_height) { + if (pindex && pindex->nHeight > summary.best_block_height) { throw JSONRPCError(RPC_INTERNAL_ERROR, strprintf("Unable to get data because coinstatsindex is still syncing. Current height: %d", summary.best_block_height)); } } @@ -1130,8 +1129,9 @@ static RPCHelpMan gettxoutsetinfo() ret.pushKV("disk_size", stats.nDiskSize); } else { CCoinsStats prev_stats{}; - if (pindex->nHeight > 0) { - const std::optional maybe_prev_stats = GetUTXOStats(coins_view, *blockman, hash_type, node.rpc_interruption_point, pindex->pprev, index_requested); + if (stats.nHeight > 0) { + const CBlockIndex& block_index = *CHECK_NONFATAL(WITH_LOCK(::cs_main, return blockman->LookupBlockIndex(stats.hashBlock))); + const std::optional maybe_prev_stats = GetUTXOStats(coins_view, *blockman, hash_type, node.rpc_interruption_point, block_index.pprev, index_requested); if (!maybe_prev_stats) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set"); } From f3bf63ec4f028cf9ee0820226f44fcbe26d358c9 Mon Sep 17 00:00:00 2001 From: w0xlt <94266259+w0xlt@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:18:35 -0800 Subject: [PATCH 2/2] kernel: acquire coinstats cursor and block info atomically Acquire the cursor and block index under the same cs_main lock to eliminate a potential race where a new block could be connected between capturing the block info and acquiring the cursor, causing the reported stats to reference a different block than the one being iterated. --- src/kernel/coinstats.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/kernel/coinstats.cpp b/src/kernel/coinstats.cpp index d287ec4be6e..49b51d64f30 100644 --- a/src/kernel/coinstats.cpp +++ b/src/kernel/coinstats.cpp @@ -109,9 +109,8 @@ static void ApplyStats(CCoinsStats& stats, const std::map& outpu //! Calculate statistics about the unspent transaction output set template -static bool ComputeUTXOStats(CCoinsView* view, CCoinsStats& stats, T hash_obj, const std::function& interruption_point) +static bool ComputeUTXOStats(CCoinsView* view, CCoinsStats& stats, T hash_obj, const std::function& interruption_point, std::unique_ptr pcursor) { - std::unique_ptr pcursor(view->Cursor()); assert(pcursor); Txid prevkey; @@ -149,21 +148,27 @@ static bool ComputeUTXOStats(CCoinsView* view, CCoinsStats& stats, T hash_obj, c std::optional ComputeUTXOStats(CoinStatsHashType hash_type, CCoinsView* view, node::BlockManager& blockman, const std::function& interruption_point) { - CBlockIndex* pindex = WITH_LOCK(::cs_main, return blockman.LookupBlockIndex(view->GetBestBlock())); + std::unique_ptr pcursor; + CBlockIndex* pindex; + { + LOCK(::cs_main); + pcursor = view->Cursor(); + pindex = blockman.LookupBlockIndex(pcursor->GetBestBlock()); + } CCoinsStats stats{Assert(pindex)->nHeight, pindex->GetBlockHash()}; bool success = [&]() -> bool { switch (hash_type) { case(CoinStatsHashType::HASH_SERIALIZED): { HashWriter ss{}; - return ComputeUTXOStats(view, stats, ss, interruption_point); + return ComputeUTXOStats(view, stats, ss, interruption_point, std::move(pcursor)); } case(CoinStatsHashType::MUHASH): { MuHash3072 muhash; - return ComputeUTXOStats(view, stats, muhash, interruption_point); + return ComputeUTXOStats(view, stats, muhash, interruption_point, std::move(pcursor)); } case(CoinStatsHashType::NONE): { - return ComputeUTXOStats(view, stats, nullptr, interruption_point); + return ComputeUTXOStats(view, stats, nullptr, interruption_point, std::move(pcursor)); } } // no default case, so the compiler can warn about missing cases assert(false);