Skip to content

Commit 984434b

Browse files
committed
mining: add interrupt()
Both waitTipChanged() and createNewBlock() can take a long time to return. Add a way for clients to interrupt them. The new m_interrupt is safely accessed with a lock on m_tip_block_mutex, but it has no guard annotation. A more thorough solution is discussed here: bitcoin#34184 (comment)
1 parent fdd1291 commit 984434b

File tree

6 files changed

+58
-16
lines changed

6 files changed

+58
-16
lines changed

src/interfaces/mining.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ class Mining
138138
* @param[in] timeout how long to wait for a new tip (default is forever)
139139
*
140140
* @retval BlockRef hash and height of the current chain tip after this call.
141-
* @retval std::nullopt if the node is shut down.
141+
* @retval std::nullopt if the node is shut down or interrupt() is called.
142142
*/
143143
virtual std::optional<BlockRef> waitTipChanged(uint256 current_tip, MillisecondsDouble timeout = MillisecondsDouble::max()) = 0;
144144

@@ -149,10 +149,15 @@ class Mining
149149
*
150150
* @param[in] options options for creating the block
151151
* @retval BlockTemplate a block template.
152-
* @retval std::nullptr if the node is shut down.
152+
* @retval std::nullptr if the node is shut down or interrupt() is called.
153153
*/
154154
virtual std::unique_ptr<BlockTemplate> createNewBlock(const node::BlockCreateOptions& options = {}) = 0;
155155

156+
/**
157+
* Interrupts createNewBlock and waitTipChanged.
158+
*/
159+
virtual void interrupt() = 0;
160+
156161
/**
157162
* Checks if a given block is valid.
158163
*

src/ipc/capnp/mining.capnp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface Mining $Proxy.wrap("interfaces::Mining") {
1919
waitTipChanged @3 (context :Proxy.Context, currentTip: Data, timeout: Float64) -> (result: Common.BlockRef);
2020
createNewBlock @4 (options: BlockCreateOptions) -> (result: BlockTemplate);
2121
checkBlock @5 (block: Data, options: BlockCheckOptions) -> (reason: Text, debug: Text, result: Bool);
22+
interrupt @6 () -> ();
2223
}
2324

2425
interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") {

src/node/interfaces.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -964,7 +964,7 @@ class MinerImpl : public Mining
964964

965965
std::optional<BlockRef> waitTipChanged(uint256 current_tip, MillisecondsDouble timeout) override
966966
{
967-
return WaitTipChanged(chainman(), notifications(), current_tip, timeout);
967+
return WaitTipChanged(chainman(), notifications(), current_tip, timeout, m_interrupt);
968968
}
969969

970970
std::unique_ptr<BlockTemplate> createNewBlock(const BlockCreateOptions& options) override
@@ -976,13 +976,18 @@ class MinerImpl : public Mining
976976
// Avoid generating a new template immediately after connecting a block while
977977
// our best known header chain still has more work. This prevents a burst of
978978
// block templates during the final catch-up moments after IBD.
979-
if (!CooldownIfHeadersAhead(chainman(), notifications(), *maybe_tip)) return {};
979+
if (!CooldownIfHeadersAhead(chainman(), notifications(), *maybe_tip, m_interrupt)) return {};
980980

981981
BlockAssembler::Options assemble_options{options};
982982
ApplyArgsManOptions(*Assert(m_node.args), assemble_options);
983983
return std::make_unique<BlockTemplateImpl>(assemble_options, BlockAssembler{chainman().ActiveChainstate(), context()->mempool.get(), assemble_options}.CreateNewBlock(), m_node);
984984
}
985985

986+
void interrupt() override
987+
{
988+
InterruptWait(notifications(), m_interrupt);
989+
}
990+
986991
bool checkBlock(const CBlock& block, const node::BlockCheckOptions& options, std::string& reason, std::string& debug) override
987992
{
988993
LOCK(chainman().GetMutex());
@@ -995,6 +1000,8 @@ class MinerImpl : public Mining
9951000
NodeContext* context() override { return &m_node; }
9961001
ChainstateManager& chainman() { return *Assert(m_node.chainman); }
9971002
KernelNotifications& notifications() { return *Assert(m_node.notifications); }
1003+
// Treat as if guarded by notifications().m_tip_block_mutex
1004+
bool m_interrupt{false};
9981005
NodeContext& m_node;
9991006
};
10001007
} // namespace

src/node/miner.cpp

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ std::optional<BlockRef> GetTip(ChainstateManager& chainman)
452452
return BlockRef{tip->GetBlockHash(), tip->nHeight};
453453
}
454454

455-
bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip)
455+
bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip, bool& interrupt)
456456
{
457457
constexpr auto COOLDOWN{1s};
458458
// Use a steady clock so cooldown is independent of mocktime.
@@ -462,9 +462,12 @@ bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& ke
462462
WAIT_LOCK(kernel_notifications.m_tip_block_mutex, lock);
463463
kernel_notifications.m_tip_block_cv.wait_until(lock, cooldown_deadline, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) {
464464
const auto tip_block = kernel_notifications.TipBlock();
465-
return chainman.m_interrupt || (tip_block && *tip_block != last_tip_hash);
465+
return chainman.m_interrupt || interrupt || (tip_block && *tip_block != last_tip_hash);
466466
});
467-
if (chainman.m_interrupt) return false;
467+
if (chainman.m_interrupt || interrupt) {
468+
interrupt = false;
469+
return false;
470+
}
468471

469472
// If the tip changed during the wait, extend the deadline
470473
const auto tip_block = kernel_notifications.TipBlock();
@@ -481,7 +484,7 @@ bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& ke
481484
return true;
482485
}
483486

484-
std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout)
487+
std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout, bool& interrupt)
485488
{
486489
Assume(timeout >= 0ms); // No internal callers should use a negative timeout
487490
if (timeout < 0ms) timeout = 0ms;
@@ -494,16 +497,22 @@ std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifi
494497
// always returns valid tip information when possible and only
495498
// returns null when shutting down, not when timing out.
496499
kernel_notifications.m_tip_block_cv.wait(lock, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) {
497-
return kernel_notifications.TipBlock() || chainman.m_interrupt;
500+
return kernel_notifications.TipBlock() || chainman.m_interrupt || interrupt;
498501
});
499-
if (chainman.m_interrupt) return {};
502+
if (chainman.m_interrupt || interrupt) {
503+
interrupt = false;
504+
return {};
505+
}
500506
// At this point TipBlock is set, so continue to wait until it is
501507
// different then `current_tip` provided by caller.
502508
kernel_notifications.m_tip_block_cv.wait_until(lock, deadline, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) {
503-
return Assume(kernel_notifications.TipBlock()) != current_tip || chainman.m_interrupt;
509+
return Assume(kernel_notifications.TipBlock()) != current_tip || chainman.m_interrupt || interrupt;
504510
});
511+
if (chainman.m_interrupt || interrupt) {
512+
interrupt = false;
513+
return {};
514+
}
505515
}
506-
if (chainman.m_interrupt) return {};
507516

508517
// Must release m_tip_block_mutex before getTip() locks cs_main, to
509518
// avoid deadlocks.

src/node/miner.h

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ void ApplyArgsManOptions(const ArgsManager& gArgs, BlockAssembler::Options& opti
142142
void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t version, uint32_t timestamp, uint32_t nonce);
143143

144144

145-
/* Interrupt the current wait for the next block template. */
145+
/* Interrupt a blocking call. */
146146
void InterruptWait(KernelNotifications& kernel_notifications, bool& interrupt_wait);
147147
/**
148148
* Return a new block template when fees rise to a certain threshold or after a
@@ -160,8 +160,10 @@ std::unique_ptr<CBlockTemplate> WaitAndCreateNewBlock(ChainstateManager& chainma
160160
std::optional<BlockRef> GetTip(ChainstateManager& chainman);
161161

162162
/* Waits for the connected tip to change until timeout has elapsed. During node initialization, this will wait until the tip is connected (regardless of `timeout`).
163-
* Returns the current tip, or nullopt if the node is shutting down. */
164-
std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout);
163+
* Returns the current tip, or nullopt if the node is shutting down or interrupt()
164+
* is called.
165+
*/
166+
std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout, bool& interrupt);
165167

166168
/**
167169
* Wait while the best known header extends the current chain tip AND at least
@@ -175,10 +177,11 @@ std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifi
175177
* once per connected client. Subsequent templates are provided by waitNext().
176178
*
177179
* @param last_tip tip at the start of the cooldown window.
180+
* @param interrupt set to true to interrupt the cooldown.
178181
*
179182
* @returns false if interrupted.
180183
*/
181-
bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip);
184+
bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip, bool& interrupt);
182185
} // namespace node
183186

184187
#endif // BITCOIN_NODE_MINER_H

test/functional/interface_ipc.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,14 @@ async def async_routine():
281281
assert_equal(oldblockref.hash, newblockref.hash)
282282
assert_equal(oldblockref.height, newblockref.height)
283283

284+
self.log.debug("interrupt() should abort waitTipChanged()")
285+
async def wait_for_tip():
286+
long_timeout = 60000.0 # 1 minute
287+
result = (await mining.waitTipChanged(ctx, newblockref.hash, long_timeout)).result
288+
# Unlike a timeout, interrupt() returns an empty BlockRef.
289+
assert_equal(len(result.hash), 0)
290+
await wait_and_do(wait_for_tip(), mining.interrupt())
291+
284292
async with AsyncExitStack() as stack:
285293
opts = self.capnp_modules['mining'].BlockCreateOptions()
286294
opts.useMempool = True
@@ -300,6 +308,15 @@ async def async_routine():
300308
# even without the cooldown, so this can miss regressions but avoids
301309
# spurious failures.
302310
assert_greater_than_or_equal(time.time() - start, 0.9)
311+
312+
self.log.debug("interrupt() should abort createNewBlock() during cooldown")
313+
async def create_block():
314+
result = await mining.createNewBlock(opts)
315+
# interrupt() causes createNewBlock to return nullptr
316+
assert_equal(result._has("result"), False)
317+
318+
await wait_and_do(create_block(), mining.interrupt())
319+
303320
header_only_peer.peer_disconnect()
304321
self.connect_nodes(0, 1)
305322
self.sync_all()

0 commit comments

Comments
 (0)