Skip to content

rpc: fix race condition in gettxoutsetinfo#34451

Open
w0xlt wants to merge 2 commits intobitcoin:masterfrom
w0xlt:i_34263
Open

rpc: fix race condition in gettxoutsetinfo#34451
w0xlt wants to merge 2 commits intobitcoin:masterfrom
w0xlt:i_34263

Conversation

@w0xlt
Copy link
Contributor

@w0xlt w0xlt commented Jan 29, 2026

Fixes #34263

A CHECK_NONFATAL assertion failure could occur in gettxoutsetinfo when a new block was connected between capturing pindex and validating it in GetUTXOStats().

The fix removes the early pindex capture since ComputeUTXOStats() independently fetches the current best block under lock. The response now uses stats.hashBlock and stats.nHeight (the actual computed values) instead of the potentially stale pindex.

@DrahtBot
Copy link
Contributor

DrahtBot commented Jan 29, 2026

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK fjahr, sedited

If your review is incorrectly listed, please copy-paste <!--meta-tag:bot-skip--> into the comment that the bot should ignore.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #34654 ([RFC] BlockMap and CChain Concurrency Improvement by alexanderwiederin)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

LLM Linter (✨ experimental)

Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

  • ComputeUTXOStats(view, stats, nullptr, interruption_point, std::move(pcursor)) in src/kernel/coinstats.cpp

2026-03-11 00:56:26

@fjahr
Copy link
Contributor

fjahr commented Jan 29, 2026

Concept ACK

Do you have a hint on how to reproduce the failure?

Comment on lines -972 to -976
// If the coinstats index isn't requested or is otherwise not usable, the
// pindex should either be null or equal to the view's best block. This is
// because without the coinstats index we can only get coinstats about the
// best block.
CHECK_NONFATAL(!pindex || pindex->GetBlockHash() == view->GetBestBlock());
Copy link
Member

Choose a reason for hiding this comment

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

Why remove this? Now that pindex is null, it should pass...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed it because it seemed redundant given the current gettxoutsetinfo flow (normally pindex is nullptr unless querying a specific block, which already requires the index).
However, it may be better to keep it to prevent future misuse of the function with a stale pindex.

Reintroduced in 0672e1c

@luke-jr
Copy link
Member

luke-jr commented Feb 2, 2026

I believe there is still a race, just not properly handled with this PR.

ComputeUTXOStats sets nHeight and hashBlock without any lock held, and before/separately from acquiring the view's cursor.

@w0xlt
Copy link
Contributor Author

w0xlt commented Feb 3, 2026

Do you have a hint on how to reproduce the failure?

To make a test that reliably reproduces the race condition detected by the Antithesis tool, we would need a way to force CoinsDB()->GetBestBlock() to change between the initial capture of pindex and the subsequent GetUTXOStats() call. There is currently no deterministic “pause point” in the code to do this.

Given that, the PR instead removes the early capture of pindex, eliminating this scenario.

ComputeUTXOStats sets nHeight and hashBlock without holding any lock, and before (or separately from) acquiring the view’s cursor.

Done in 8305f29. Thanks.

@w0xlt
Copy link
Contributor Author

w0xlt commented Feb 3, 2026

CI error is unrelated.

@maflcko
Copy link
Member

maflcko commented Feb 4, 2026

There is currently no deterministic “pause point” in the code to do this.

It would be possible to add an UninterruptibleSleep(100ms); (or so) in the right place to make this deterministic? Having such a patch would make testing easier.

@w0xlt
Copy link
Contributor Author

w0xlt commented Feb 6, 2026

@maflcko @fjahr

The below commit adds a test that fails on master with the following AssertionError:
gettxoutsetinfo failed: Internal bug detected: !pindex || pindex->GetBlockHash() == view->GetBestBlock()
The error appears in the logs on master, but the test passes on this PR.
w0xlt@beca478

Not sure if it should be added here because since it introduces test code in gettxoutsetinfo.

Copy link
Contributor

@sedited sedited left a comment

Choose a reason for hiding this comment

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

Concept ACK

{
LOCK(::cs_main);
pindex = blockman.LookupBlockIndex(view->GetBestBlock());
pcursor = view->Cursor();
Copy link
Contributor

Choose a reason for hiding this comment

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

It doesn't really make a difference, since we are holding cs_main here, but might it be a bit clearer, if we'd read GetBestBlock from the cursor?

diff --git a/src/kernel/coinstats.cpp b/src/kernel/coinstats.cpp
index e1d3fff5ea..c9049ea7a3 100644
--- a/src/kernel/coinstats.cpp
+++ b/src/kernel/coinstats.cpp
@@ -151,7 +151,3 @@ std::optional<CCoinsStats> ComputeUTXOStats(CoinStatsHashType hash_type, CCoinsV
-    std::unique_ptr<CCoinsViewCursor> pcursor;
-    CBlockIndex* pindex;
-    {
-        LOCK(::cs_main);
-        pindex = blockman.LookupBlockIndex(view->GetBestBlock());
-        pcursor = view->Cursor();
-    }
+    std::unique_ptr<CCoinsViewCursor> pcursor = view->Cursor();
+    CBlockIndex* pindex = WITH_LOCK(::cs_main, return blockman.LookupBlockIndex(pcursor->GetBestBlock()));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! Adopted the pcursor->GetBestBlock() approach - it does make the intent clearer since pindex is explicitly tied to the cursor's snapshot.

One thing to note: cs_main is still held during Cursor() because internally CCoinsViewDB::Cursor() calls GetBestBlock() and NewIterator() as two separate operations.

So if I understand correctly, without the lock, a concurrent FlushStateToDisk could slip between them, causing the cursor's hashBlock to be from a different state than its iterator data - the same kind of mismatch this PR is fixing. I changed the diff to:

diff --git a/src/kernel/coinstats.cpp b/src/kernel/coinstats.cpp
index e1d3fff5ea..79da8ea4d4 100644
--- a/src/kernel/coinstats.cpp
+++ b/src/kernel/coinstats.cpp
@@ -152,8 +152,8 @@ std::optional<CCoinsStats> ComputeUTXOStats(CoinStatsHashType hash_type, CCoinsV
     CBlockIndex* pindex;
     {
         LOCK(::cs_main);
-        pindex = blockman.LookupBlockIndex(view->GetBestBlock());
         pcursor = view->Cursor();
+        pindex = blockman.LookupBlockIndex(pcursor->GetBestBlock());
     }
     CCoinsStats stats{Assert(pindex)->nHeight, pindex->GetBlockHash()};

w0xlt added 2 commits March 10, 2026 17:49
Fix an assertion failure in gettxoutsetinfo (issue bitcoin#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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rpc: Assertion '!pindex || pindex->GetBlockHash() == view->GetBestBlock()' failed during gettxoutsetinfo

6 participants