Skip to content

Commit 5a6dd7c

Browse files
committed
Merge bitcoin#34661: ipc mining: Prevent `Assertion m_node.chainman' failed`` errors on early startup
bbc8f1e ipc mining: Prevent ``Assertion `m_node.chainman' failed`` errors on early startup (Ryan Ofsky) a7cabf9 init refactor: Only initialize node.notifications one time (Ryan Ofsky) c8e332c init refactor: Remove node.init accesss in AppInitInterfaces (Ryan Ofsky) Pull request description: This fixes ``Assertion `m_node.chainman' failed`` errors first reported bitcoin#33994 (comment) when IPC mining methods are called before ChainstateManager is loaded. The fix works by making the `Init.makeMining` method wait until chainstate data is loaded. It's probably the simplest possible fix but other alternatives like moving the wait to `Mining.createNewBlock` were discussed in the thread bitcoin#34661 (comment) and could be implemented later without changes to clients. ACKs for top commit: Sjors: utACK bbc8f1e ismaelsadeeq: ACK bbc8f1e achow101: ACK bbc8f1e Tree-SHA512: 3e2e4e28ccff364b2303efd06ce337a229c28609076638500acb29559f716a15ad99409c6970ce9ad91776d53e3f9d959f1bbbd144ea9a4a2fb578ddbf2da267
2 parents ceff677 + bbc8f1e commit 5a6dd7c

8 files changed

Lines changed: 84 additions & 15 deletions

File tree

src/init.cpp

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,8 +1206,10 @@ bool AppInitLockDirectories()
12061206

12071207
bool AppInitInterfaces(NodeContext& node)
12081208
{
1209-
node.chain = node.init->makeChain();
1210-
node.mining = node.init->makeMining();
1209+
node.chain = interfaces::MakeChain(node);
1210+
// Specify wait_loaded=false so internal mining interface can be initialized
1211+
// on early startup and does not need to be tied to chainstate loading.
1212+
node.mining = interfaces::MakeMining(node, /*wait_loaded=*/false);
12111213
return true;
12121214
}
12131215

@@ -1304,16 +1306,12 @@ static ChainstateLoadResult InitAndLoadChainstate(
13041306
const ArgsManager& args)
13051307
{
13061308
// This function may be called twice, so any dirty state must be reset.
1307-
node.notifications.reset(); // Drop state, such as a cached tip block
1309+
node.notifications->setChainstateLoaded(false); // Drop state, such as a cached tip block
13081310
node.mempool.reset();
13091311
node.chainman.reset(); // Drop state, such as an initialized m_block_tree_db
13101312

13111313
const CChainParams& chainparams = Params();
13121314

1313-
Assert(!node.notifications); // Was reset above
1314-
node.notifications = std::make_unique<KernelNotifications>(Assert(node.shutdown_request), node.exit_status, *Assert(node.warnings));
1315-
ReadNotificationArgs(args, *node.notifications);
1316-
13171315
CTxMemPool::Options mempool_opts{
13181316
.check_ratio = chainparams.DefaultConsistencyChecks() ? 1 : 0,
13191317
.signals = node.validation_signals.get(),
@@ -1414,6 +1412,7 @@ static ChainstateLoadResult InitAndLoadChainstate(
14141412
std::tie(status, error) = catch_exceptions([&] { return VerifyLoadedChainstate(chainman, options); });
14151413
if (status == node::ChainstateLoadStatus::SUCCESS) {
14161414
LogInfo("Block index and chainstate loaded");
1415+
node.notifications->setChainstateLoaded(true);
14171416
}
14181417
}
14191418
return {status, error};
@@ -1486,6 +1485,13 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
14861485
node.validation_signals = std::make_unique<ValidationSignals>(std::make_unique<SerialTaskRunner>(scheduler));
14871486
auto& validation_signals = *node.validation_signals;
14881487

1488+
// Create KernelNotifications object. Important to do this early before
1489+
// calling ipc->listenAddress() below so makeMining and other IPC methods
1490+
// can use this.
1491+
assert(!node.notifications);
1492+
node.notifications = std::make_unique<KernelNotifications>(Assert(node.shutdown_request), node.exit_status, *Assert(node.warnings));
1493+
ReadNotificationArgs(args, *node.notifications);
1494+
14891495
// Create client interfaces for wallets that are supposed to be loaded
14901496
// according to -wallet and -disablewallet options. This only constructs
14911497
// the interfaces, it doesn't load wallet data. Wallets actually get loaded

src/interfaces/mining.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,11 @@ class Mining
160160
};
161161

162162
//! Return implementation of Mining interface.
163-
std::unique_ptr<Mining> MakeMining(node::NodeContext& node);
163+
//!
164+
//! @param[in] wait_loaded waits for chainstate data to be loaded before
165+
//! returning. Used to prevent external clients from
166+
//! being able to crash the node during startup.
167+
std::unique_ptr<Mining> MakeMining(node::NodeContext& node, bool wait_loaded=true);
164168

165169
} // namespace interfaces
166170

src/node/interfaces.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1017,5 +1017,17 @@ class MinerImpl : public Mining
10171017
namespace interfaces {
10181018
std::unique_ptr<Node> MakeNode(node::NodeContext& context) { return std::make_unique<node::NodeImpl>(context); }
10191019
std::unique_ptr<Chain> MakeChain(node::NodeContext& context) { return std::make_unique<node::ChainImpl>(context); }
1020-
std::unique_ptr<Mining> MakeMining(node::NodeContext& context) { return std::make_unique<node::MinerImpl>(context); }
1020+
std::unique_ptr<Mining> MakeMining(node::NodeContext& context, bool wait_loaded)
1021+
{
1022+
if (wait_loaded) {
1023+
node::KernelNotifications& kernel_notifications(*Assert(context.notifications));
1024+
util::SignalInterrupt& interrupt(*Assert(context.shutdown_signal));
1025+
WAIT_LOCK(kernel_notifications.m_tip_block_mutex, lock);
1026+
kernel_notifications.m_tip_block_cv.wait(lock, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) {
1027+
return kernel_notifications.m_state.chainstate_loaded || interrupt;
1028+
});
1029+
if (interrupt) return nullptr;
1030+
}
1031+
return std::make_unique<node::MinerImpl>(context);
1032+
}
10211033
} // namespace interfaces

src/node/kernel_notifications.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ kernel::InterruptResult KernelNotifications::blockTip(SynchronizationState state
5353
{
5454
LOCK(m_tip_block_mutex);
5555
Assume(index.GetBlockHash() != uint256::ZERO);
56-
m_tip_block = index.GetBlockHash();
56+
m_state.tip_block = index.GetBlockHash();
5757
m_tip_block_cv.notify_all();
5858
}
5959

@@ -103,7 +103,7 @@ void KernelNotifications::fatalError(const bilingual_str& message)
103103
std::optional<uint256> KernelNotifications::TipBlock()
104104
{
105105
AssertLockHeld(m_tip_block_mutex);
106-
return m_tip_block;
106+
return m_state.tip_block;
107107
};
108108

109109

src/node/kernel_notifications.h

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ namespace node {
2929
class Warnings;
3030
static constexpr int DEFAULT_STOPATHEIGHT{0};
3131

32+
//! State tracked by the KernelNotifications interface meant to be used by
33+
//! mining code, index code, RPCs, and other code sitting above the validation
34+
//! layer.
35+
//!
36+
//! Currently just tracks the chain tip, but could be used to hold other
37+
//! information in the future, like the last flushed block, pruning
38+
//! information, etc.
39+
struct KernelState {
40+
bool chainstate_loaded{false};
41+
std::optional<uint256> tip_block;
42+
};
43+
3244
class KernelNotifications : public kernel::Notifications
3345
{
3446
public:
@@ -49,13 +61,21 @@ class KernelNotifications : public kernel::Notifications
4961

5062
void fatalError(const bilingual_str& message) override;
5163

64+
void setChainstateLoaded(bool chainstate_loaded) EXCLUSIVE_LOCKS_REQUIRED(!m_tip_block_mutex) {
65+
LOCK(m_tip_block_mutex);
66+
if (!chainstate_loaded) m_state = {};
67+
m_state.chainstate_loaded = chainstate_loaded;
68+
m_tip_block_cv.notify_all();
69+
}
70+
5271
//! Block height after which blockTip notification will return Interrupted{}, if >0.
5372
int m_stop_at_height{DEFAULT_STOPATHEIGHT};
5473
//! Useful for tests, can be set to false to avoid shutdown on fatal error.
5574
bool m_shutdown_on_fatal_error{true};
5675

5776
Mutex m_tip_block_mutex;
5877
std::condition_variable m_tip_block_cv GUARDED_BY(m_tip_block_mutex);
78+
KernelState m_state GUARDED_BY(m_tip_block_mutex);
5979
//! The block for which the last blockTip notification was received.
6080
//! It's first set when the tip is connected during node initialization.
6181
//! Might be unset during an early shutdown.
@@ -65,8 +85,6 @@ class KernelNotifications : public kernel::Notifications
6585
const std::function<bool()>& m_shutdown_request;
6686
std::atomic<int>& m_exit_status;
6787
node::Warnings& m_warnings;
68-
69-
std::optional<uint256> m_tip_block GUARDED_BY(m_tip_block_mutex);
7088
};
7189

7290
void ReadNotificationArgs(const ArgsManager& args, KernelNotifications& notifications);

src/test/miner_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ struct MinerTestingSetup : public TestingSetup {
6868
}
6969
std::unique_ptr<Mining> MakeMining()
7070
{
71-
return interfaces::MakeMining(m_node);
71+
return interfaces::MakeMining(m_node, /*wait_loaded=*/false);
7272
}
7373
};
7474
} // namespace miner_tests

src/test/testnet4_miner_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ namespace testnet4_miner_tests {
2222
struct Testnet4MinerTestingSetup : public Testnet4Setup {
2323
std::unique_ptr<Mining> MakeMining()
2424
{
25-
return interfaces::MakeMining(m_node);
25+
return interfaces::MakeMining(m_node, /*wait_loaded=*/false);
2626
}
2727
};
2828
} // namespace testnet4_miner_tests

test/functional/interface_ipc_mining.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,34 @@ async def wait_for_tip():
147147

148148
asyncio.run(capnp.run(async_routine()))
149149

150+
def run_early_startup_test(self):
151+
"""Make sure mining.createNewBlock safely returns on early startup as
152+
soon as mining interface is available """
153+
self.log.info("Running Mining interface early startup test")
154+
155+
node = self.nodes[0]
156+
self.stop_node(node.index)
157+
node.start()
158+
159+
async def async_routine():
160+
while True:
161+
try:
162+
ctx, mining = await self.make_mining_ctx()
163+
break
164+
except (ConnectionRefusedError, FileNotFoundError):
165+
# Poll quickly to connect as soon as socket becomes
166+
# available but without using a lot of CPU
167+
await asyncio.sleep(0.005)
168+
169+
opts = self.capnp_modules['mining'].BlockCreateOptions()
170+
await mining.createNewBlock(ctx, opts)
171+
172+
asyncio.run(capnp.run(async_routine()))
173+
174+
# Reconnect nodes so next tests are happy
175+
node.wait_for_rpc_connection()
176+
self.connect_nodes(1, 0)
177+
150178
def run_block_template_test(self):
151179
"""Test BlockTemplate interface methods."""
152180
self.log.info("Running BlockTemplate interface test")
@@ -374,6 +402,7 @@ def run_test(self):
374402
self.miniwallet = MiniWallet(self.nodes[0])
375403
self.default_block_create_options = self.capnp_modules['mining'].BlockCreateOptions()
376404
self.run_mining_interface_test()
405+
self.run_early_startup_test()
377406
self.run_block_template_test()
378407
self.run_coinbase_and_submission_test()
379408
self.run_ipc_option_override_test()

0 commit comments

Comments
 (0)