Bug 56
Mining RPCs report an internal bug, or abort, if the node shuts down mid-call
- Status
- Open, fix under review
- Symptom
- Assertion failure; worst effect seen: assertion failure
- Where seen
- Bitcoin Core functional tests
- Trigger
- Shutdown
- First reported
- 2026-01-12 (first report)
- Fixed
- Not yet
- In released binaries
- Yes (v30.0-v31.1)
- Defect ever on master
- Yes
Summary
The getblocktemplate and generateblock RPCs (and the generateBlocks helper behind generatetoaddress) fail a CHECK_NONFATAL(block_template) check when the node starts shutting down while they are running. Since 2025, Mining::createNewBlock() returns null when the node shuts down while it waits, so that IPC mining clients never receive a template built on a missing chain tip. These three RPC call sites still assert that it cannot return null. In release builds the caller gets "Internal bug detected: block_template" instead of "Shutting down"; in builds that abort on failed assumptions (such as the Antithesis runs that found it) the node aborts. Released binaries v30.0 through v31.1 have the bug. The proposed fix is bitcoin#36047, not yet merged.
What happened
- @dergoegge reported
rpc/mining.cpp:875 ... getblocktemplate ... Assertion 'block_template' failedfrom Antithesis runs, once withgetblocktemplatecalled during shutdown and once during startup, with both debug logs attached (bitcoin#34262, 2026-01-12). The log line has theassertion_failformat thatCHECK_NONFATALuses when built to abort, so the Antithesis node aborted. - @chriszeng1010 reproduced it by hand on regtest: a long-poll
getblocktemplatein one terminal andbitcoin-cli stopin another returned "Internal bug detected: block_template" in about half the attempts and the correct error -9 (shutting down) in the rest. He walked through the race and showed that the startup case is the same path, reached through a shutdown that begins during init.
Root cause
5315278e7c7f in bitcoin#31785 ("Have createNewBlock() wait for tip, make rpc handle shutdown during long poll and wait methods") made createNewBlock() wait for a tip and return nullptr if shutdown interrupts the wait, "instead of the last tip", so that IPC clients never see a template built on a missing tip. It updated the long-poll and wait paths of the RPCs, but left the three CHECK_NONFATAL(block_template) checks in src/rpc/mining.cpp (in generateBlocks, generateblock and getblocktemplate), which had been an unreachable invariant since bitcoin#30440 ("Have createNewBlock return BlockTemplate interface") added them.
In the long-poll case the sequence is:
Interrupt()(src/init.cpp) notifiesm_tip_block_cvbefore it callsInterruptRPC().- The long-poll loop in
getblocktemplatewakes with no new tip, breaks out, and passes itsIsRPCRunning()check because RPC has not been interrupted yet. - It calls
miner.createNewBlock(), which seesm_interruptset and returns null, andCHECK_NONFATAL(block_template)fails.
Fix
No fix is merged yet. bitcoin#36047 ("rpc: handle createNewBlock() returning nullptr during shutdown", by @chriszeng1010, open) replaces the three checks in its fix commit b1fa37ea8681 (+6/-3). getblocktemplate and generateblock throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, "Shutting down"), matching the existing long-poll path, and generateBlocks stops its loop and returns the blocks mined so far. The commit message mentions only the first two. A new long-poll shutdown case in mining_getblocktemplate_longpoll.py fails only some of the time on a build without the fix, since the race is timing-dependent.
Two earlier PRs that tried to fix this issue, bitcoin#34357 and bitcoin#35184, were closed because, as a comment on the issue put it, they did not fix the problem.
Next steps
- Review bitcoin#36047 by @chriszeng1010. It replaces the three
CHECK_NONFATAL(block_template)checks with a "Shutting down" error ingetblocktemplateandgenerateblock, and an early return ingenerateBlocks. It has no reviews yet, and it needs a rebase since 2026-09-24, becausegetblocktemplateon master now waits throughBlockTemplateManager. - Ask for the fix commit message to cover the
generateBlockschange too; it names only the other two call sites. - The new long-poll case in
mining_getblocktemplate_longpoll.pyfails only about half the time without the fix, because the race depends on timing. A reviewer could look for a way to make it fail every time.
Timeline
- 2024-08: the
CHECK_NONFATAL(block_template)checks added in bitcoin#30440, unreachable at the time. - 2025-04-14: introduced when bitcoin#31785 was merged, making
createNewBlock()return null on shutdown. - 2026-01-12: bitcoin#34262 reported by @dergoegge (Antithesis).
- 2026-08-18: reproduced by hand by @chriszeng1010.
- 2026-08-20: bitcoin#36047 opened; still open. As of 2026-09-24 the three checks are unchanged on master (
getblocktemplatenow waits throughBlockTemplateManager, but the checks are the same), andInterrupt()still wakes tip waiters before interrupting RPC. - Releases: v30.0 through v31.1 have the bug.
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 A contract changed and a caller was left behind: A function's contract changed under its callers
- Prevented by a review checklist item, the type system
- Earliest detector code review (existed then)
- Diagnosis moderate
- Fix design easy
- Debugging aids logging, a reproduction script
- Rust partly
Would Rust have prevented it? Partly. Changing the return from Box<BlockTemplate> to Option<Box<BlockTemplate>> would fail to compile at every unvisited caller, which is the type-driven audit the lesson asks for; but the C++ type was already a nullable unique_ptr, and a port that started from Option with expect() at those three sites would panic the same way. More on Rust
Could it have been prevented?
Yes. bitcoin#31785 changed the
contract of createNewBlock() from "always returns a template" to "returns null if
shutdown interrupts the wait", for the benefit of IPC clients, and updated the long-poll
and wait paths it had in mind. Three other callers in src/rpc/mining.cpp still carried
CHECK_NONFATAL(block_template), added in
bitcoin#30440 when null was
impossible. The checklist item is mechanical: when a function gains a new return value,
visit every caller, and treat an existing assertion on the old contract as a caller
that needs the visit most, since it was written to document exactly the assumption
being removed. A return type that cannot be ignored (an std::expected carrying the
shutdown reason, or a distinct "interrupted" result) would have turned the missed
callers into compile errors; a nullable pointer that used to be non-null does not.
Could it have been found sooner?
By review of the PR that changed the contract, since grep createNewBlock finds the
three sites. At runtime the window is small: Interrupt() wakes m_tip_block_cv
before it calls InterruptRPC(), so a long-poll getblocktemplate has to wake, pass
IsRPCRunning(), and call createNewBlock() in that gap. No functional test stopped
the node during a long poll, and the fix's new test fails only some of the time on an
unfixed build. Antithesis, which explores shutdown windows systematically, found it
nine months after the merge; on a release build the effect is a misleading "Internal
bug detected" RPC reply, so no user was likely to report it.
What made it hard to debug?
The Antithesis logs named the failing check, but explaining why the RPC's own
shutdown guard did not fire took a reading of the order of operations in
Interrupt(), and two earlier fix PRs
(bitcoin#34357,
bitcoin#35184) were closed as not
fixing the problem before chriszeng1010
reproduced it by hand
and walked through the race.
The by-hand reproduction (long-poll in one terminal, stop in another, about one
failure in two) is what made the fix reviewable. The severity was also easy to misread:
release builds only return a wrong error, but builds that abort on failed
CHECK_NONFATAL, including the Antithesis ones, abort the node.
Lesson
When you widen what a function can return, the assertions in its callers are the list of places that believed the old contract; update them in the same change, or use a return type that makes ignoring the new case a compile error.
Notes
- @maflcko called the fix PR "basically a small style cleanup". For release builds that is close: the node keeps running and the caller gets a misleading internal-bug error instead of the right one. It understates the effect on builds that abort on failed
CHECK_NONFATAL, where the node aborts. - The failing calls are in-process RPCs, not IPC calls. The bug belongs to the IPC work because the null-on-shutdown contract of
createNewBlock()exists for IPC mining clients.
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 | Shutdown (shutdown) |
| Phase | shutdown |
| Side | server |
| Component | bitcoin-rpc |
| Root cause kind | assumption-violated |
| Fix kind | local-fix |
| Detection | antithesis |
| Needs a peer that is not libmultiprocess C++ | no |
| Needed a sanitizer to see | no |
| Defect ever on master | yes |
| Released | binary |
| Confidence | medium |