ipc mining: Prevent ``Assertion `m_node.chainman' failed`` errors on early startup
What changed, and why it matters
This commit fixes a crash in Bitcoin Core that could happen when external programs talk to the node over the IPC mining interface while the node is still starting up. Before the fix, calling mining methods too early could trigger an internal assertion failure and crash the node. The fix makes external mining clients wait until the node's chain data is fully loaded before they can proceed.
Treat this as a security-hardening fix with denial-of-service relevance. Backport to maintained branches that expose the IPC mining interface. Verify that the new functional test `run_early_startup_test` passes and that no other early-startup IPC interfaces have similar uninitialized-pointer assumptions.
Security signals we found
Denial-of-service vector: unauthenticated or early IPC mining client could crash the node via assertion failure
Race condition between IPC mining interface availability and ChainstateManager initialization
Fix introduces blocking wait on chainstate_loaded condition variable for external mining interface creation
Internal mining interface explicitly opts out of waiting to preserve startup ordering
Evidence from the diff
The patch addresses an assertion failure (Assertionm_node.chainman’ failed) in the IPC mining interface by introducing await_loadedparameter tointerfaces::MakeMining(). External IPC clients now default to waiting on a condition variable untilchainstate_loadedis true or shutdown is signaled, preventing mining method calls beforeChainstateManageris initialized. Internal startup and tests passwait_loaded=false` to retain existing behavior where the interface is created early but not used until ready.
Changed components
src/init.cppsrc/interfaces/mining.hsrc/node/interfaces.cppsrc/test/miner_tests.cppsrc/test/testnet4_miner_tests.cpptest/functional/interface_ipc_mining.pyInspect captured patch +52 / −5
diff --git a/src/init.cpp b/src/init.cpp
index 9d7c76a8..6a6e7a92 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -1207,7 +1207,9 @@ bool AppInitLockDirectories()
bool AppInitInterfaces(NodeContext& node)
{
node.chain = interfaces::MakeChain(node);
- node.mining = interfaces::MakeMining(node);
+ // Specify wait_loaded=false so internal mining interface can be initialized
+ // on early startup and does not need to be tied to chainstate loading.
+ node.mining = interfaces::MakeMining(node, /*wait_loaded=*/false);
return true;
}
diff --git a/src/interfaces/mining.h b/src/interfaces/mining.h
index 72cc0bcb..f4c42e20 100644
--- a/src/interfaces/mining.h
+++ b/src/interfaces/mining.h
@@ -160,7 +160,11 @@ public:
};
//! Return implementation of Mining interface.
-std::unique_ptr<Mining> MakeMining(node::NodeContext& node);
+//!
+//! @param[in] wait_loaded waits for chainstate data to be loaded before
+//! returning. Used to prevent external clients from
+//! being able to crash the node during startup.
+std::unique_ptr<Mining> MakeMining(node::NodeContext& node, bool wait_loaded=true);
} // namespace interfaces
diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp
index afcaeb20..6c61b210 100644
--- a/src/node/interfaces.cpp
+++ b/src/node/interfaces.cpp
@@ -1017,5 +1017,17 @@ public:
namespace interfaces {
std::unique_ptr<Node> MakeNode(node::NodeContext& context) { return std::make_unique<node::NodeImpl>(context); }
std::unique_ptr<Chain> MakeChain(node::NodeContext& context) { return std::make_unique<node::ChainImpl>(context); }
-std::unique_ptr<Mining> MakeMining(node::NodeContext& context) { return std::make_unique<node::MinerImpl>(context); }
+std::unique_ptr<Mining> MakeMining(node::NodeContext& context, bool wait_loaded)
+{
+ if (wait_loaded) {
+ node::KernelNotifications& kernel_notifications(*Assert(context.notifications));
+ util::SignalInterrupt& interrupt(*Assert(context.shutdown_signal));
+ WAIT_LOCK(kernel_notifications.m_tip_block_mutex, lock);
+ kernel_notifications.m_tip_block_cv.wait(lock, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) {
+ return kernel_notifications.m_state.chainstate_loaded || interrupt;
+ });
+ if (interrupt) return nullptr;
+ }
+ return std::make_unique<node::MinerImpl>(context);
+}
} // namespace interfaces
diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp
index 7f05e4e9..0ae8e538 100644
--- a/src/test/miner_tests.cpp
+++ b/src/test/miner_tests.cpp
@@ -67,7 +67,7 @@ struct MinerTestingSetup : public TestingSetup {
}
std::unique_ptr<Mining> MakeMining()
{
- return interfaces::MakeMining(m_node);
+ return interfaces::MakeMining(m_node, /*wait_loaded=*/false);
}
};
} // namespace miner_tests
diff --git a/src/test/testnet4_miner_tests.cpp b/src/test/testnet4_miner_tests.cpp
index 614d2fd6..d0aeebe0 100644
--- a/src/test/testnet4_miner_tests.cpp
+++ b/src/test/testnet4_miner_tests.cpp
@@ -22,7 +22,7 @@ namespace testnet4_miner_tests {
struct Testnet4MinerTestingSetup : public Testnet4Setup {
std::unique_ptr<Mining> MakeMining()
{
- return interfaces::MakeMining(m_node);
+ return interfaces::MakeMining(m_node, /*wait_loaded=*/false);
}
};
} // namespace testnet4_miner_tests
diff --git a/test/functional/interface_ipc_mining.py b/test/functional/interface_ipc_mining.py
index 5095583a..b741beeb 100755
--- a/test/functional/interface_ipc_mining.py
+++ b/test/functional/interface_ipc_mining.py
@@ -147,6 +147,34 @@ class IPCMiningTest(BitcoinTestFramework):
asyncio.run(capnp.run(async_routine()))
+ def run_early_startup_test(self):
+ """Make sure mining.createNewBlock safely returns on early startup as
+ soon as mining interface is available """
+ self.log.info("Running Mining interface early startup test")
+
+ node = self.nodes[0]
+ self.stop_node(node.index)
+ node.start()
+
+ async def async_routine():
+ while True:
+ try:
+ ctx, mining = await self.make_mining_ctx()
+ break
+ except (ConnectionRefusedError, FileNotFoundError):
+ # Poll quickly to connect as soon as socket becomes
+ # available but without using a lot of CPU
+ await asyncio.sleep(0.005)
+
+ opts = self.capnp_modules['mining'].BlockCreateOptions()
+ await mining.createNewBlock(ctx, opts)
+
+ asyncio.run(capnp.run(async_routine()))
+
+ # Reconnect nodes so next tests are happy
+ node.wait_for_rpc_connection()
+ self.connect_nodes(1, 0)
+
def run_block_template_test(self):
"""Test BlockTemplate interface methods."""
self.log.info("Running BlockTemplate interface test")
@@ -374,6 +402,7 @@ class IPCMiningTest(BitcoinTestFramework):
self.miniwallet = MiniWallet(self.nodes[0])
self.default_block_create_options = self.capnp_modules['mining'].BlockCreateOptions()
self.run_mining_interface_test()
+ self.run_early_startup_test()
self.run_block_template_test()
self.run_coinbase_and_submission_test()
self.run_ipc_option_override_test()
Why this scored 57/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.