Bug 94

checkBlock before genesis is connected aborts the node

Status
Open
Symptom
Assertion failure; worst effect seen: assertion failure
Where seen
Bitcoin Core functional tests
Trigger
Node startup
First reported
Reported privately in an independent security review, 2026-09-26
Fixed
Not yet
In released binaries
Yes (v30.0 onward)
Defect ever on master
Yes

Summary

If a mining client calls Mining.checkBlock while bitcoin-node is starting on an empty data directory or reindexing, after the mining interface is available but before the genesis block is connected, the node aborts on an assertion that the chain has a tip. The other mining calls wait for a tip or report that there is none; checkBlock goes straight to a check written for callers that run after startup. It was reported privately in an independent security review, with a functional test; it has not been seen in a running deployment. No fix is merged yet.

What happened

Nothing has been seen in a deployment. An independent security review reported it privately on 2026-09-26. Its test in interface_ipc_mining.py restarts the node with a wiped chain and -loadblock pointing at a named pipe, which holds the block import thread before it connects genesis, checks that getTip returns no tip, and calls checkBlock. The report says the node aborted on that call.

Root cause

MinerImpl::checkBlock in src/node/interfaces.cpp takes the chainstate manager lock and calls TestBlockValidity() (src/validation.cpp), which starts with:

CBlockIndex* tip{Assert(chainstate.m_chain.Tip())};

Assert aborts in every build. Inside the node that precondition always holds, because RPC stays in warmup until the tip exists. Over IPC it does not. Since bitcoin#34661, makeMining waits only until the chainstate is loaded. On an empty data directory, with -reindex, or with -reindex-chainstate, genesis is connected later, by the initial block import thread, after it has read any -loadblock files. A client calling checkBlock in between reaches the Assert.

The other mining calls handle this window: getTip returns no tip, and waitTipChanged and createNewBlock wait for one, since bitcoin#31785 fixed the same problem for them. checkBlock was added afterwards, in bitcoin#31981 (merged 2025-06-19), without a tip check.

In practice the window is short unless -loadblock is used: genesis is connected almost at once on a fresh data directory, and during a reindex as soon as the first block file is read. A client that reconnects as soon as the node restarts and immediately checks a block is the realistic case.

Fix

No fix merged yet. The fix proposed with the report adds one line to checkBlock, under the lock it already holds: if the active chain has no tip, throw an error ("Cannot check a block before the active chain has a tip"), which the client receives as a FAILED exception and can retry after getTip reports a tip. Waiting for genesis, as createNewBlock does, was considered and rejected because a stalled import would leave the call blocked; returning false would wrongly call an unchecked block invalid.

Next steps

Timeline

Lessons

What would have prevented this bug, found it sooner, or made it easier to debug. The patterns page groups these lessons across all bugs.

Pattern Serving before ready, freeing before done: Serving calls before startup finishes

  • Prevented by a design rule, a review checklist item
  • Earliest detector startup test (did not exist then)
  • Diagnosis easy
  • Fix design easy
  • Debugging aids a clear assertion message
  • Rust no

Would Rust have prevented it? No. Rust would model the missing tip as an Option and an unwrap or expect would panic the same way; only a type that cannot be built before a tip exists would rule it out, and that is an API design choice, not a language rule. More on Rust

Could it have been prevented?

Yes. bitcoin#31785 had already found that mining methods can be called over IPC before the chain has a tip and fixed the two methods that existed then. checkBlock, added two months later in bitcoin#31981, called a validation function whose first line asserts a tip. A rule written down with the earlier fix (every method exposed over IPC must handle "no tip yet") or a check at the interface entry, rather than in each method, would have covered it.

Could it have been found sooner?

Yes, by a startup test that calls each mining method while genesis is held back, which the reporter built with a -loadblock named pipe. The functional tests only call mining methods after the node is fully started. Making makeMining wait for the chainstate in bitcoin#34661 was the moment to add such a test, since it defined where the window starts.

What made it hard to debug?

Nothing: the abort message names TestBlockValidity and the tip check. Reproducing it needs control over when genesis is connected, which the named pipe gives.

Lesson

When a service starts accepting calls before it is fully ready, every method needs a defined answer for the not-ready state, and methods added later need the same review; otherwise an internal precondition becomes something any early client can trip.

Classification

The survey's classification of this bug. The explorer charts these fields across all bugs; the method page explains them.

Field Value
Symptom assert
Worst effect observed assert
Seen in functional-test
Scope production
Trigger Node startup (node-startup)
Phase startup
Side server
Component bitcoin-mining
Root cause kind assumption-violated
Fix kind none
Detection review
Needs a peer that is not libmultiprocess C++ no
Needed a sanitizer to see no
Defect ever on master yes
Released binary
Confidence high