mining: add cooldown to createNewBlock() immediately after IBD #34184

pull Sjors wants to merge 11 commits into bitcoin:master from Sjors:2025/12/cool-down changing 21 files +301 −162
  1. Sjors commented at 5:17 am on December 31, 2025: member

    Based on #34568, which contains a breaking interface change this PR needs.

    As reported in #33994, connected mining clients will receive a flood of new templates if the node is still going through IBD or catching up on the last 24 hours. This PR fixes that using an optional cooldown mechanism, only applied to createNewBlock().

    First, cooldown waits for IBD. Then, as the tip keeps moving forward, it waits a few seconds to see if the tip updated. If so, it restarts the timer and waits again. The trade-offs for this mechanism are explained below.

    Because this PR changes createNewBlock() from a method that returns quickly to one that can block for minutes, we rely on #34568 to fix a bug in our .capnp definition, adding the missing context to createNewBlock (and checkBlock).

    The second commit then adds an interrupt() method so that clients can cleanly disconnect.


    Rationale

    The cooldown argument is optional, and not used by internal non-IPC code, for two reasons:

    1. The mechanism wreaks havoc on the functional test suite, which would require very careful mock time handling to work around. But that’s pointless, because only IPC clients need it.
    2. It needs to be optional for IPC clients too, because in some situations, like a signet with only one miner, waiting for IBD can mean being stuck forever.

    The reason it’s only applied to createNewBlock() is that this is the first method called by clients; waitNext() is a method on the interface returned by createNewBlock(), at which point the cooldown is done.

    After IBD, we wait N seconds if the header is N blocks ahead of the tip, with a minimum of 3 and a maximum of 20 seconds. The minimum waiting time is short enough that it shouldn’t be annoying or confusing for someone manually starting up a client. While the maximum should be harmless if it happens spuriously (which it shouldn’t).

    If the minimum wait is too short, clients get a burst of templates, as observed in the original issue. We can’t entirely rule this out without a lot of additional complexity (like scanning our own log file for heuristics). This PR should make it a lot less likely, and thanks to the IBD wait also limit it to one day worth of blocks (-maxtipage).

    Some test runs on an M4 MacBook Pro, where I had a node catch up on the last few days worth of blocks:

    As the chart shows, sometimes it takes longer than 3 seconds. But it turns out that in all those cases there were quite a few headers ahead of the tip. It also demonstrates that it’s important to first wait for IBD, because it’s less likely a random tip update takes longer than 20 seconds.

  2. DrahtBot renamed this:
    mining: add cooldown to createNewBlock() immediately after IBD
    mining: add cooldown to createNewBlock() immediately after IBD
    on Dec 31, 2025
  3. DrahtBot added the label Mining on Dec 31, 2025
  4. DrahtBot commented at 5:23 am on December 31, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34184.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK ismaelsadeeq
    Stale ACK bensig, w0xlt, ryanofsky

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34422 (Update libmultiprocess subtree to be more stable with rust IPC client by ryanofsky)
    • #34284 (ipc, test: Add tests for unclean disconnect and thread busy behavior by ryanofsky)
    • #34038 (logging: replace -loglevel with -trace, various API improvements by ajtowns)
    • #33966 (refactor: disentangle miner startup defaults from runtime options by Sjors)
    • #32297 (bitcoin-cli: Add -ipcconnect option by ryanofsky)
    • #29700 (kernel, refactor: return error status on all fatal errors by ryanofsky)
    • #29409 (multiprocess: Add capnp wrapper for Chain interface by ryanofsky)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • std::nullptr -> nullptr [C++ nullptr is not in the std namespace; comment uses “std::nullptr”, which is incorrect and may confuse readers]

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • std::clamp(*remaining, 3, 20) in src/node/miner.cpp

    2026-02-13 09:44:17

  5. Sjors force-pushed on Dec 31, 2025
  6. DrahtBot added the label CI failed on Dec 31, 2025
  7. Sjors force-pushed on Dec 31, 2025
  8. Sjors force-pushed on Dec 31, 2025
  9. Sjors commented at 7:28 am on December 31, 2025: member

    Switched to avoid mock time for the cooldown mechanism (it’s possible, but would require additional refactoring).

    Added a constraint to ignore alternative header branches. This probably doesn’t matter in real life, but it does in tests. E.g. the assume_utxo test “Check importing a snapshot where current chain-tip is not an ancestor of the snapshot block but has less work” creates an alternative chain of blocks despite having better headers (but not their blocks). In real life this implies a giant block witholding attack.

  10. DrahtBot removed the label CI failed on Dec 31, 2025
  11. bensig commented at 0:46 am on January 3, 2026: contributor

    ACK e8b01c43bb05f1a52adf1279280a744111b8dc45

    Tested on macOS - interface_ipc.py passes.

    Code review:

    • Using SteadyClock for the cooldown (independent of mocktime) is the right call
    • BestHeaderAheadOfActiveTip() correctly uses ancestor check to ignore competing branches
    • 1s timeout prevents DoS from header-only attacks while still allowing catch-up during final IBD

    Nice fix for #33994

  12. DrahtBot added the label Needs rebase on Jan 13, 2026
  13. Sjors force-pushed on Jan 22, 2026
  14. Sjors commented at 9:02 am on January 22, 2026: member
    Rebased.
  15. DrahtBot removed the label Needs rebase on Jan 22, 2026
  16. in src/node/miner.cpp:464 in c02c2832f7 outdated
    459+        }
    460+        if (!best_header_ahead) break;
    461+
    462+        WAIT_LOCK(kernel_notifications.m_tip_block_mutex, lock);
    463+        kernel_notifications.m_tip_block_cv.wait_until(lock, cooldown_deadline, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) {
    464+            const auto tip_block = kernel_notifications.TipBlock();
    


    w0xlt commented at 6:43 am on January 27, 2026:

    Runtime assertion for consistency with WaitAndCreateNewBlock pattern.

    0            AssertLockHeld(kernel_notifications.m_tip_block_mutex);
    1            const auto tip_block = kernel_notifications.TipBlock();
    

    Sjors commented at 10:41 am on January 27, 2026:
    There’s other places where we don’t bother with AssertLockHeld for an inline function. It seems a bit overkill.
  17. in src/node/miner.cpp:465 in c02c2832f7
    460+        if (!best_header_ahead) break;
    461+
    462+        WAIT_LOCK(kernel_notifications.m_tip_block_mutex, lock);
    463+        kernel_notifications.m_tip_block_cv.wait_until(lock, cooldown_deadline, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) {
    464+            const auto tip_block = kernel_notifications.TipBlock();
    465+            return tip_block && *tip_block != last_tip_hash;
    


    w0xlt commented at 6:44 am on January 27, 2026:

    This makes the wait wakes immediately on shutdown signal.

    0            return (tip_block && *tip_block != last_tip_hash) || chainman.m_interrupt;
    

    Sjors commented at 10:49 am on January 27, 2026:
    Taken.
  18. w0xlt commented at 6:44 am on January 27, 2026: contributor
    Approach ACK
  19. Sjors force-pushed on Jan 27, 2026
  20. in src/node/miner.h:167 in 9817657d46
    162@@ -163,6 +163,14 @@ std::optional<BlockRef> GetTip(ChainstateManager& chainman);
    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);
    165 
    166+/**
    167+ * Pause block template creation when the best header chain is ahead of the tip.
    


    ryanofsky commented at 2:09 pm on January 28, 2026:

    In commit “mining: createNewBlock() waits if tip is updating” (9817657d463ff1d8502a5edf0641e88b9b1d4853)

    This is a little vague. I think it would be helpful to describe this in terms of observable behavior, maybe:

    0/**
    1 * Wait while the best known header extends the current chain tip
    2 * AND at least one block per second is being added to the tip.
    3 *
    4 * Returns false if interrupted.
    5 */
    

    Might also be good to add the “not safe to keep waiting” info from your commit description, and maybe say something about how we think this is safe to use for createNewBlock since miners should only call it once on startup, but are avoiding it in waitNext.


    Sjors commented at 3:00 pm on January 29, 2026:
    Taken. I moved some text from the commit message to the comment here.
  21. in src/validation.cpp:6313 in 9817657d46
    6309@@ -6310,6 +6310,16 @@ void ChainstateManager::RecalculateBestHeader()
    6310     }
    6311 }
    6312 
    6313+bool ChainstateManager::BestHeaderAheadOfActiveTip() const
    


    ryanofsky commented at 2:10 pm on January 28, 2026:

    In commit “mining: createNewBlock() waits if tip is updating” (9817657d463ff1d8502a5edf0641e88b9b1d4853)

    Maybe s/Active/Current/ here, can also use CurrentChain() below.

  22. in src/node/miner.cpp:460 in 9817657d46
    455+        bool best_header_ahead{false};
    456+        {
    457+            LOCK(::cs_main);
    458+            best_header_ahead = chainman.BestHeaderAheadOfActiveTip();
    459+        }
    460+        if (!best_header_ahead) break;
    


    ryanofsky commented at 2:20 pm on January 28, 2026:

    In commit “mining: createNewBlock() waits if tip is updating” (9817657d463ff1d8502a5edf0641e88b9b1d4853)

    Could collapse lines 454-460 to just:

    0while (WITH_LOCK(::cs_main, return chainman.BestHeaderAheadOfActiveTip())) {
    

    I feel like that would be easier to understand without the bool and break. Or maybe better just lock cs_main inside BestHeaderAheadOfActiveTip so this can be:

    0while (chainman.BestHeaderAheadOfActiveTip()) {
    

    Sjors commented at 3:00 pm on January 29, 2026:
    Nice, taken.
  23. in test/functional/interface_ipc.py:291 in 9817657d46 outdated
    287+                async with destroying((await mining.createNewBlock(opts)).result, ctx):
    288+                    pass
    289+                # Lower-bound only: a heavily loaded CI host might still exceed 0.9s
    290+                # even without the cooldown, so this can miss regressions but avoids
    291+                # spurious failures.
    292+                assert_greater_than_or_equal(time.time() - start, 0.9)
    


    ryanofsky commented at 2:35 pm on January 28, 2026:

    In commit “mining: createNewBlock() waits if tip is updating” (9817657d463ff1d8502a5edf0641e88b9b1d4853)

    Since this check uses a delay, if there is a way you could move this test to a separate test function (not run_mining_test) i think that would be very good.

    I’m concerned about interface_ipc test getting harder to maintain and slower to run, and would like to move towards using separate test methods like #34284, so we can still have fast update/test cycles by being able to run specific tests.


    Sjors commented at 3:00 pm on January 29, 2026:
    I’m looking into splitting the mining ipc tests out entirely. Will leave this line untouched for now, as one second isn’t too bad.

    Sjors commented at 7:09 pm on January 29, 2026:
    See #34452. No preference on merge order, either should be easy to rebase.
  24. ryanofsky approved
  25. ryanofsky commented at 3:06 pm on January 28, 2026: contributor

    Code review ACK 9817657d463ff1d8502a5edf0641e88b9b1d4853. This seems like probably the simplest change that can resolve #33994.

    I guess I have two lingering concerns about it:

    • This can cause the createNewBlock function to block for potentially a very long time when it didn’t before and createNewBlock doesn’t currently have any cancellation option other than shutting down the node. So it could make it hard for IPC clients to disconnect cleanly, and I suspect we may want to follow up here by adding some timeout or cancellation mechanism.

    • I’m not very sure about effectiveness of the cooldown strategy, and wouldn’t be surprised if there were cases where it didn’t work well but it does seem like a good starting point, and pretty conservative.

  26. Sjors force-pushed on Jan 29, 2026
  27. Sjors commented at 3:01 pm on January 29, 2026: member
    I added a commit that allows interrupting both createNewBlock() and waitTipChanged().
  28. in src/node/interfaces.cpp:1003 in b57da688c2 outdated
     999@@ -995,6 +1000,7 @@ class MinerImpl : public Mining
    1000     NodeContext* context() override { return &m_node; }
    1001     ChainstateManager& chainman() { return *Assert(m_node.chainman); }
    1002     KernelNotifications& notifications() { return *Assert(m_node.notifications); }
    1003+    bool m_interrupt{false};
    


    ryanofsky commented at 6:23 pm on January 29, 2026:

    In commit “mining: add interrupt()” (b57da688c21233e852c5afe5c5afafd3cae84e91)

    Would be good to note that notifications().m_tip_block_mutex must be held while accessing this variable because it is accessed from multiple threads. Not sure if it would be possible to add a GUARDED_BY annotation but that could be nice.

    More importantly, we may want to consider moving this variable somewhere else. A side-effect of adding the variable here is that it makes the MinerImpl class have state, which can be bad because now destroying the Mining object while createNewBlock or waitNext calls are in progress can cause segfaults. This could cause the node to crash with existing IPC clients if they destroy Mining objects early, or if they just disconnect during a waitNext call and the Mining object is garbage collected before it returns.

    For these reasons I think it might be good to move the MinerImpl::m_interrupt variable somewhere else like KernelNotifications::m_blocktemplate_request_interrupted. This would be hackier than the current solution but I think worth it to avoid node crashes triggered by IPC.

    (In the longer term a better solution would be to provide an interfaces::Interrupt type that could be passed to waitNext and createBlockTemplate methods as additional method argument. These methods could then attach a std::function<void()> lambda to run whatever code they need when an interrupt is triggered. Clients would then have the ability to cancel individual calls by triggering the Interrupt object. Internally, the cancellation could be implemented either by making a separate RPC call or by using Cap’n Proto’s native allowCancellation mechanism.)


    Sjors commented at 7:40 pm on January 29, 2026:

    I mostly just cloned the pattern we used for waitNext(). Does it have a similar issue?


    Will give it some thought, but if it’s too complicated then I’ll move the commit to a followup PR.


    ryanofsky commented at 9:11 pm on January 29, 2026:

    re: #34184 (review)

    I mostly just cloned the pattern we used for waitNext(). Does it have a similar issue?

    You are right, I misremembered the way interruptWait was implemented and it does follow a similar pattern, and the issue is not as bad as I thought.

    The issue is actually pretty hard to trigger because the Cap’n Proto’s internal reference counting keeps the BlockTemplate object alive by default until the waitNext call returns. So a client would have to explicitly call destroy on the BlockTemplate object in that case or the Mining object in this case for there to be a crash.

    So I think the current implementation of this is ok and unlikely to lead to any accidental crashes.

    I did go ahead and implement my suggestion before realizing the problem was not so bad, so will paste it here for reference, but your current solution seems cleaner and I’d recommend keeping it.

      0--- a/src/node/interfaces.cpp
      1+++ b/src/node/interfaces.cpp
      2@@ -929,7 +929,7 @@ public:
      3 
      4     void interruptWait() override
      5     {
      6-        InterruptWait(notifications(), m_interrupt_wait);
      7+        InterruptWait(notifications(), &m_interrupt_wait);
      8     }
      9 
     10     const BlockAssembler::Options m_assemble_options;
     11@@ -964,7 +964,7 @@ public:
     12 
     13     std::optional<BlockRef> waitTipChanged(uint256 current_tip, MillisecondsDouble timeout) override
     14     {
     15-        return WaitTipChanged(chainman(), notifications(), current_tip, timeout, m_interrupt);
     16+        return WaitTipChanged(chainman(), notifications(), current_tip, timeout);
     17     }
     18 
     19     std::unique_ptr<BlockTemplate> createNewBlock(const BlockCreateOptions& options) override
     20@@ -976,7 +976,7 @@ public:
     21         // Avoid generating a new template immediately after connecting a block while
     22         // our best known header chain still has more work. This prevents a burst of
     23         // block templates during the final catch-up moments after IBD.
     24-        if (!CooldownIfHeadersAhead(chainman(), notifications(), *maybe_tip, m_interrupt)) return {};
     25+        if (!CooldownIfHeadersAhead(chainman(), notifications(), *maybe_tip)) return {};
     26 
     27         BlockAssembler::Options assemble_options{options};
     28         ApplyArgsManOptions(*Assert(m_node.args), assemble_options);
     29@@ -985,7 +985,7 @@ public:
     30 
     31     void interrupt() override
     32     {
     33-        InterruptWait(notifications(), m_interrupt);
     34+        InterruptWait(notifications());
     35     }
     36 
     37     bool checkBlock(const CBlock& block, const node::BlockCheckOptions& options, std::string& reason, std::string& debug) override
     38@@ -1000,7 +1000,6 @@ public:
     39     NodeContext* context() override { return &m_node; }
     40     ChainstateManager& chainman() { return *Assert(m_node.chainman); }
     41     KernelNotifications& notifications() { return *Assert(m_node.notifications); }
     42-    bool m_interrupt{false};
     43     NodeContext& m_node;
     44 };
     45 } // namespace
     46--- a/src/node/kernel_notifications.h
     47+++ b/src/node/kernel_notifications.h
     48@@ -61,6 +61,14 @@ public:
     49     //! Might be unset during an early shutdown.
     50     std::optional<uint256> TipBlock() EXCLUSIVE_LOCKS_REQUIRED(m_tip_block_mutex);
     51 
     52+    //! Whether a createNewBlock() / waitNext() cancellation has been requested.
     53+    //! Having this variable here hacky, but avoids the problem of
     54+    //! createNewBlock not being interruptible during IBD by mining clients, and
     55+    //! avoids memory safety issues that would occur if the variable was
     56+    //! somewhere else. An idea for getting rid of this is described in
     57+    //! [#34184 (review)](/bitcoin-bitcoin/34184/#discussion_r2742950202)
     58+    bool m_blocktemplate_request_interrupted GUARDED_BY(m_tip_block_mutex);
     59+
     60 private:
     61     const std::function<bool()>& m_shutdown_request;
     62     std::atomic<int>& m_exit_status;
     63--- a/src/node/miner.cpp
     64+++ b/src/node/miner.cpp
     65@@ -341,13 +341,27 @@ void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t
     66     block.fChecked = false;
     67 }
     68 
     69-void InterruptWait(KernelNotifications& kernel_notifications, bool& interrupt_wait)
     70+void InterruptWait(KernelNotifications& kernel_notifications, bool* interrupt_wait)
     71 {
     72     LOCK(kernel_notifications.m_tip_block_mutex);
     73-    interrupt_wait = true;
     74+    kernel_notifications.m_blocktemplate_request_interrupted = true;
     75+    if (interrupt_wait) *interrupt_wait = true;
     76     kernel_notifications.m_tip_block_cv.notify_all();
     77 }
     78 
     79+static bool Interrupted(const KernelNotifications& kernel_notifications, const bool* interrupt_wait = nullptr) EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex)
     80+{
     81+    return kernel_notifications.m_blocktemplate_request_interrupted || (interrupt_wait && *interrupt_wait);
     82+}
     83+
     84+static bool ResetInterrupt(KernelNotifications& kernel_notifications, bool* interrupt_wait = nullptr) EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex)
     85+{
     86+    if (!Interrupted(kernel_notifications, interrupt_wait)) return false;
     87+    kernel_notifications.m_blocktemplate_request_interrupted = false;
     88+    if (interrupt_wait) *interrupt_wait = false;
     89+    return true;
     90+}
     91+
     92 std::unique_ptr<CBlockTemplate> WaitAndCreateNewBlock(ChainstateManager& chainman,
     93                                                       KernelNotifications& kernel_notifications,
     94                                                       CTxMemPool* mempool,
     95@@ -379,12 +393,9 @@ std::unique_ptr<CBlockTemplate> WaitAndCreateNewBlock(ChainstateManager& chainma
     96                 // method on BlockTemplate and no template could have been
     97                 // generated before a tip exists.
     98                 tip_changed = Assume(tip_block) && tip_block != block_template->block.hashPrevBlock;
     99-                return tip_changed || chainman.m_interrupt || interrupt_wait;
    100+                return tip_changed || chainman.m_interrupt || Interrupted(kernel_notifications, &interrupt_wait);
    101             });
    102-            if (interrupt_wait) {
    103-                interrupt_wait = false;
    104-                return nullptr;
    105-            }
    106+            if (ResetInterrupt(kernel_notifications, &interrupt_wait)) return nullptr;
    107         }
    108 
    109         if (chainman.m_interrupt) return nullptr;
    110@@ -445,7 +456,7 @@ std::optional<BlockRef> GetTip(ChainstateManager& chainman)
    111     return BlockRef{tip->GetBlockHash(), tip->nHeight};
    112 }
    113 
    114-bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip, bool& interrupt)
    115+bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip)
    116 {
    117     constexpr auto COOLDOWN{1s};
    118     // Use a steady clock so cooldown is independent of mocktime.
    119@@ -455,12 +466,9 @@ bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& ke
    120         WAIT_LOCK(kernel_notifications.m_tip_block_mutex, lock);
    121         kernel_notifications.m_tip_block_cv.wait_until(lock, cooldown_deadline, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) {
    122             const auto tip_block = kernel_notifications.TipBlock();
    123-            return chainman.m_interrupt || interrupt || (tip_block && *tip_block != last_tip_hash);
    124+            return chainman.m_interrupt || Interrupted(kernel_notifications) || (tip_block && *tip_block != last_tip_hash);
    125         });
    126-        if (chainman.m_interrupt || interrupt) {
    127-            interrupt = false;
    128-            return false;
    129-        }
    130+        if (chainman.m_interrupt || ResetInterrupt(kernel_notifications)) return false;
    131 
    132         // If the tip changed during the wait, extend the deadline
    133         const auto tip_block = kernel_notifications.TipBlock();
    134@@ -477,7 +485,7 @@ bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& ke
    135     return true;
    136 }
    137 
    138-std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout, bool& interrupt)
    139+std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout)
    140 {
    141     Assume(timeout >= 0ms); // No internal callers should use a negative timeout
    142     if (timeout < 0ms) timeout = 0ms;
    143@@ -490,21 +498,15 @@ std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifi
    144         // always returns valid tip information when possible and only
    145         // returns null when shutting down, not when timing out.
    146         kernel_notifications.m_tip_block_cv.wait(lock, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) {
    147-            return kernel_notifications.TipBlock() || chainman.m_interrupt || interrupt;
    148+            return kernel_notifications.TipBlock() || chainman.m_interrupt || Interrupted(kernel_notifications);
    149         });
    150-        if (chainman.m_interrupt || interrupt) {
    151-            interrupt = false;
    152-            return {};
    153-        }
    154+        if (chainman.m_interrupt || ResetInterrupt(kernel_notifications)) return {};
    155         // At this point TipBlock is set, so continue to wait until it is
    156         // different then `current_tip` provided by caller.
    157         kernel_notifications.m_tip_block_cv.wait_until(lock, deadline, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) {
    158-            return Assume(kernel_notifications.TipBlock()) != current_tip || chainman.m_interrupt || interrupt;
    159+            return Assume(kernel_notifications.TipBlock()) != current_tip || chainman.m_interrupt || Interrupted(kernel_notifications);
    160         });
    161-    }
    162-    if (chainman.m_interrupt || interrupt) {
    163-        interrupt = false;
    164-        return {};
    165+        if (chainman.m_interrupt || ResetInterrupt(kernel_notifications)) return {};
    166     }
    167 
    168     // Must release m_tip_block_mutex before getTip() locks cs_main, to
    169--- a/src/node/miner.h
    170+++ b/src/node/miner.h
    171@@ -143,7 +143,7 @@ void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t
    172 
    173 
    174 /* Interrupt a blocking call. */
    175-void InterruptWait(KernelNotifications& kernel_notifications, bool& interrupt_wait);
    176+void InterruptWait(KernelNotifications& kernel_notifications, bool* interrupt_wait = nullptr);
    177 /**
    178  * Return a new block template when fees rise to a certain threshold or after a
    179  * new tip; return nullopt if timeout is reached.
    180@@ -163,7 +163,7 @@ std::optional<BlockRef> GetTip(ChainstateManager& chainman);
    181  * Returns the current tip, or nullopt if the node is shutting down or interrupt()
    182  * is called.
    183  */
    184-std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout, bool& interrupt);
    185+std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout);
    186 
    187 /**
    188  * Wait while the best known header extends the current chain tip AND at least
    189@@ -177,11 +177,10 @@ std::optional<BlockRef> WaitTipChanged(ChainstateManager& chainman, KernelNotifi
    190  * once per connected client. Subsequent templates are provided by waitNext().
    191  *
    192  * [@param](/bitcoin-bitcoin/contributor/param/) last_tip tip at the start of the cooldown window.
    193- * [@param](/bitcoin-bitcoin/contributor/param/) interrupt set to true to interrupt the cooldown.
    194  *
    195  * [@returns](/bitcoin-bitcoin/contributor/returns/) false if interrupted.
    196  */
    197-bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip, bool& interrupt);
    198+bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip);
    199 } // namespace node
    200 
    201 #endif // BITCOIN_NODE_MINER_H
    
  29. ryanofsky commented at 7:33 pm on January 29, 2026: contributor
    Code review b57da688c21233e852c5afe5c5afafd3cae84e91. Thanks for the updates! Documentation update and while loop simplification since last review look good. I’m concerned about the new MinerImpl::m_interrupt variable, however, since I can see it leading to segfaults if clients destroy the Mining interface too early (even if they don’t call the interrupt method). So I suggested moving the variable to the KernelNotications class to avoid this.
  30. in src/node/miner.cpp:506 in b57da688c2
    505+            return Assume(kernel_notifications.TipBlock()) != current_tip || chainman.m_interrupt || interrupt;
    506         });
    507     }
    508-    if (chainman.m_interrupt) return {};
    509+    if (chainman.m_interrupt || interrupt) {
    510+        interrupt = false;
    


    ryanofsky commented at 8:39 pm on January 29, 2026:

    In commit “mining: add interrupt()” (b57da688c21233e852c5afe5c5afafd3cae84e91)

    There is a thread safety issue here because interrupt is accessed without m_tip_block_mutex and it could be written by another thread calling the interrupt method. Could be fixed by moving these checks one line up into the block above


    Sjors commented at 11:38 am on January 30, 2026:
    Done, and added a link to your more elaborate solution in the commit message.
  31. ryanofsky commented at 9:15 pm on January 29, 2026: contributor
    Code review b57da688c21233e852c5afe5c5afafd3cae84e91. There’s a small thread safety bug in the second commit (see below), but the previous concern I had about crashes isn’t really an issue, and the current implementation of cancellation here seems pretty good.
  32. Sjors referenced this in commit 1ae88d1688 on Jan 30, 2026
  33. Sjors force-pushed on Jan 30, 2026
  34. ryanofsky commented at 8:03 pm on January 30, 2026: contributor
    Code review ACK 1ae88d1688958fe567a087ec591015ea832aa8f0, just fixing missing lock on the interrupt variable and adding some comments since last review
  35. DrahtBot requested review from w0xlt on Jan 30, 2026
  36. Sjors commented at 9:12 am on January 31, 2026: member
    @plebhash can you take this for a spin?
  37. DrahtBot added the label Needs rebase on Feb 2, 2026
  38. plebhash commented at 1:52 pm on February 3, 2026: none

    @plebhash can you take this for a spin?

    I checked out this branch, compiled it, launched it on testnet4 and hooked SRI Pool via IPC.

    my testnet4 datadir had been off for a few days, so I left it catch up while observing Pool logs.

    there was a flood of NewTemplate + SetNewPrevHash messages, just like the behavior reported on https://github.com/bitcoin/bitcoin/issues/33994

  39. Sjors referenced this in commit 984434bfa0 on Feb 3, 2026
  40. Sjors force-pushed on Feb 3, 2026
  41. Sjors commented at 1:56 pm on February 3, 2026: member
    Rebased for trivial conflict after #32420. @plebhash ok that’s bad news. It seems unlikely that your node needs more than 1 second to connect testnet4 blocks. Can you share the node log in a gist so we can see if there’s any 1+ second gaps that could explain this?
  42. plebhash commented at 2:47 pm on February 3, 2026: none

    @plebhash ok that’s bad news. It seems unlikely that your node needs more than 1 second to connect testnet4 blocks. Can you share the node log in a gist so we can see if there’s any 1+ second gaps that could explain this?

    you should be able to reproduce:

    0git clone https://github.com/stratum-mining/sv2-apps -b v0.2.0
    1cd pool-apps/pool
    2cargo run -- -c config-examples/testnet4/pool-config-bitcoin-core-ipc-example.toml
    

    this is assuming you want to use testnet4 under default datadir… for setup variations:

    • for a different network, there’s also config-examples/{mainnet, signet}
    • for non-default datadir, edit pool-config-bitcoin-core-ipc-example.toml and uncomment data_dir field
  43. DrahtBot removed the label Needs rebase on Feb 3, 2026
  44. Sjors commented at 3:53 pm on February 3, 2026: member
    I was able to reproduce the issue with testnet4.
  45. Sjors marked this as a draft on Feb 3, 2026
  46. ryanofsky commented at 4:40 pm on February 3, 2026: contributor

    From the logs it looks like waitNext calls are churning, not createNewBlock calls, so just adding the cooldown delay to createNewBlock does not help.

    A possible fix might be to add an additional delay to createNewBlock so it waits to be connected to peers before returning, although this could also be fragile.

    Another fix might be to add the 1 block/second cooldown delay to waitNext as well. This seems like it should be ok although there might be a bigger risk waitNext blocking when it shouldn’t. Maybe adjusting the cooldown to delay to block if 2 blocks were connected in last second instead of 1 block (or something like that) might be safer if adding the cooldown to waitNext

  47. Sjors commented at 6:33 pm on February 3, 2026: member

    I was thinking of just increasing the cooldown. As long as it’s only on createNewBlock() that seems safe.

    From my own testing with syncing testnet4 from sratch on an M4 Apple Silicon machine, gaps of 1 second are common. Once I switched to a 3 second cool down, the pool behaved.

    I also tried the 3 second cool down while syncing a mainnet node, but 3 seconds isn’t long enough on my machine. Just picking a bigger number would be annoying in the normal case.

    I tried a slightly fancier heuristic: cooldown is extended by the number of blocks remaining in seconds, capped at 20.

    Combined with also gating behind IsInitialBlockDownload() (which considers -maxtipage) it should work most of the time. In particular this deals with the (fairly common) situation where the node doesn’t get any headers / blocks from its peers for a minute or so after startup. Just checking for connections isn’t enough.

    Will push some code later.

    #29565 would be quite handy for testing this.

    (the experiment caused the node to segfault as I triggered unclean pool shutdowns with timeout, I believe that’s a known issue being worked on)

  48. Sjors commented at 8:00 pm on February 3, 2026: member

    This is exceedingly hard to test / debug, due to a combination of four issues:

    1. getting a node in a “almost done syncing” state is tedious (the trick it to kill -9 the node when it reaches the tip, so it has to start over)
    2. the node will crash if the client ungraciously exists
    3. the sv2 client doesn’t know about the interrupt method yet (since this PR introduces it)
    4. createNewBlock doesn’t have a context argument (known issue) so a second connection quietly fails to establish (in case of a test sscript that tries to connect a second time)

    Once I clean up the code, I’ll publish a gist with some patches to make testing easier without increasing the scope of this PR. @ryanofsky regarding (4), I’m puzzled why interrupt() works at all in the functional test. Shouldn’t the lack context cause the IPC server thread to block, thus never getting the interrupt message?

    0self.log.debug("interrupt() should abort createNewBlock() during cooldown")
    1async def create_block():
    2    result = await mining.createNewBlock(opts)
    3    # interrupt() causes createNewBlock to return nullptr
    4    assert_equal(result._has("result"), False)
    5
    6await wait_and_do(create_block(), mining.interrupt())
    
  49. Sjors commented at 8:32 pm on February 3, 2026: member
    @ismaelsadeeq is going to be so excited if I need to pull in #33936 and make this a breaking IPC change (in which case I’ll also delete the previously deprecated methods, though probably in a followup).
  50. Sjors referenced this in commit 299f15677b on Feb 4, 2026
  51. Sjors force-pushed on Feb 4, 2026
  52. Sjors commented at 4:36 pm on February 4, 2026: member

    Alright, I made some changes…

    First, I added a commit to pass missing context to createNewBlock (and checkBlock). This is a breaking API change, so it bumps the makeMining version and throws a nice error for older clients. That’s tested in the functional test, and I also saw it with the sv2 Rust client. At this point, at least with the change in this PR, I don’t think it’s worth trying to work around this bug, as it’s confusing at best.

    Next, I made cooldown an optional argument. This is because the mechanism ended up breaking the functional test suite, especially waiting for IBD. And in some situations, like a signet with only one miner, waiting for IBD could mean being stuck forever.

    So cooldown waits for IBD and then as the tip keeps moving forward, it waits a bit to see if new blocks come in. But how long to wait?

    I decided on a minimum of 3 seconds. That short enough that it’s not going to annoy someone who manually starts up a client. And at least on my M4 MacBook Pro on mainnet, it works. I’ll share some (not pretty) code to test this later.

    As the chart shows, sometimes it takes longer than 3 seconds. But it turns out that in all those cases there were quite a few headers ahead of the tip. So the cooldown mechanism has another heuristic: wait N seconds if the header is N blocks ahead of the tip, with a minimum of 3 and a maximum of 20.

    I’m sure we could tweak this numbers further and have the node analyse it’s own log files for perfection, but ultimately it’s just an educated guess. If we wait too long, it’s slightly annoying, but 3 - 20 seconds isn’t terrible. If we wait too short, clients get a burst of templates. That can lead to others issues, but I guess in the worst case they can just restart, reconnect and try again a minute later. The code itself seems acceptably simple.

  53. DrahtBot added the label CI failed on Feb 4, 2026
  54. Sjors referenced this in commit 4756af92d2 on Feb 4, 2026
  55. Sjors force-pushed on Feb 4, 2026
  56. Sjors commented at 5:30 pm on February 4, 2026: member

    I moved run_deprecated_mining_test down to avoid running into #34284.

    Links to the (vibe coded) scripts used for the experiment and chart are at the bottom of the PR description.

  57. DrahtBot removed the label CI failed on Feb 4, 2026
  58. Sjors marked this as ready for review on Feb 4, 2026
  59. in src/interfaces/init.h:42 in a47fd4298d
    38@@ -39,6 +39,7 @@ class Init
    39     virtual Ipc* ipc() { return nullptr; }
    40     virtual bool canListenIpc() { return false; }
    41     virtual const char* exeName() { return nullptr; }
    42+    virtual void makeMiningV2() { throw std::runtime_error("Old mining interface (@2) not supported. Please update your client!"); }
    


    ryanofsky commented at 5:07 pm on February 5, 2026:

    In commit “mining: pass missing context to BlockTemplate methods” (c236980ad64aabf6b9bf5119b92a599d8e707d0b)

    I feel like it would be simpler just to stay backwards compatible, instead breaking compatibility with existing IPC clients over this cooldown feature. Should be doable with less code, just:

     0--- a/src/ipc/capnp/mining.capnp
     1+++ b/src/ipc/capnp/mining.capnp
     2@@ -17,8 +17,12 @@ interface Mining $Proxy.wrap("interfaces::Mining") {
     3     isInitialBlockDownload [@1](/bitcoin-bitcoin/contributor/1/) (context :Proxy.Context) -> (result: Bool);
     4     getTip [@2](/bitcoin-bitcoin/contributor/2/) (context :Proxy.Context) -> (result: Common.BlockRef, hasResult: Bool);
     5     waitTipChanged [@3](/bitcoin-bitcoin/contributor/3/) (context :Proxy.Context, currentTip: Data, timeout: Float64) -> (result: Common.BlockRef);
     6-    createNewBlock [@4](/bitcoin-bitcoin/contributor/4/) (options: BlockCreateOptions) -> (result: BlockTemplate);
     7-    checkBlock [@5](/bitcoin-bitcoin/contributor/5/) (block: Data, options: BlockCheckOptions) -> (reason: Text, debug: Text, result: Bool);
     8+    createNewBlock [@6](/bitcoin-bitcoin/contributor/6/) (context :Proxy.Context, options: BlockCreateOptions) -> (result: BlockTemplate);
     9+    checkBlock [@7](/bitcoin-bitcoin/contributor/7/) (context :Proxy.Context, block: Data, options: BlockCheckOptions) -> (reason: Text, debug: Text, result: Bool);
    10+
    11+    # DEPRECATED: older versions of methods which don't take a context argument (so can't run asynchronously).
    12+    createNewBlockOld4 [@4](/bitcoin-bitcoin/contributor/4/) (options: BlockCreateOptions) -> (result: BlockTemplate) $Proxy.name("createNewBlock");
    13+    checkBlockOld5 [@5](/bitcoin-bitcoin/contributor/5/) (block: Data, options: BlockCheckOptions) -> (reason: Text, debug: Text, result: Bool) $Proxy.name("checkBlock");
    14 }
    

    Plus an update to the python test to pass the context.


    Sjors commented at 11:12 am on February 7, 2026:
    Responded in the main thread.
  60. in src/ipc/capnp/init.capnp:25 in a47fd4298d
    18@@ -19,5 +19,8 @@ using Mining = import "mining.capnp";
    19 interface Init $Proxy.wrap("interfaces::Init") {
    20     construct @0 (threadMap: Proxy.ThreadMap) -> (threadMap :Proxy.ThreadMap);
    21     makeEcho @1 (context :Proxy.Context) -> (result :Echo.Echo);
    22-    makeMining @2 (context :Proxy.Context) -> (result :Mining.Mining);
    23+    makeMining @3 (context :Proxy.Context) -> (result :Mining.Mining);
    24+
    25+    # DEPRECATED: no longer supported; server returns an error.
    26+    makeMiningV2 @2 () -> ();
    


    ryanofsky commented at 5:13 pm on February 5, 2026:

    In commit “mining: pass missing context to BlockTemplate methods” (c236980ad64aabf6b9bf5119b92a599d8e707d0b)

    (If not using other suggestion for staying backwards compatible)

    I think I suggested this V2 suffix in another issue, but now I think another suffix like Old2 might be less confusing because V2 sounds like it indicates a newer version of the method.


    Sjors commented at 11:34 am on February 7, 2026:
    Changed to makeMiningDeprecatedV2
  61. in src/node/miner.cpp:460 in a98510d805 outdated
    451@@ -452,6 +452,38 @@ std::optional<BlockRef> GetTip(ChainstateManager& chainman)
    452     return BlockRef{tip->GetBlockHash(), tip->nHeight};
    453 }
    454 
    455+bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip)
    456+{
    457+    uint256 last_tip_hash{last_tip.hash};
    458+
    459+    while (const std::optional<int> remaining = chainman.BlocksAheadOfTip()) {
    460+        const int cooldown_seconds = std::clamp(*remaining, 3, 20);
    


    ryanofsky commented at 5:23 pm on February 5, 2026:

    In commit “mining: add cooldown argument to createNewBlock()” (617eb13ec178154bdfd1a2f41496912d87bf17fc)

    Like this, clever!


    ismaelsadeeq commented at 3:09 pm on February 11, 2026:

    In “mining: add cooldown argument to createNewBlock()” 0d4cf7041af41e6371473f7df93301d7100cea3e

    Three seconds minimum seems a bit much, IMO, should just be 1?


    Sjors commented at 4:30 pm on February 11, 2026:
    Based on my and Plebhash’ testing that’s not enough. There are plenty of multi second gaps even on a fast machine, see the chart in PR description.

    ismaelsadeeq commented at 8:28 am on February 12, 2026:
    It’s heuristics anyway, no strong opinion here.
  62. in src/node/interfaces.cpp:985 in 4756af92d2
    981@@ -982,18 +982,23 @@ class MinerImpl : public Mining
    982             // forever if no block was mined in the past day.
    983             while (chainman().IsInitialBlockDownload()) {
    984                 maybe_tip = waitTipChanged(maybe_tip->hash, MillisecondsDouble{1000});
    985-                if (!maybe_tip) return {};
    986+                if (!maybe_tip || m_interrupt) return {};
    


    ryanofsky commented at 5:42 pm on February 5, 2026:

    In commit “mining: add interrupt()” (299f15677b3d32b878f79945128c977dc8eaedfd)

    Would probably change this to:

    0if (!maybe_tip || chainman().m_interrupt || WITH_LOCK(notifications().m_tip_block_mutex, return m_interrupt)) return {};
    

    since it’s not safe to check this->m_interrupt without the mutex and it is probably good to include chainman().m_interrupt for completeness, even though waitTipChanged recently checked it.


    Sjors commented at 11:34 am on February 7, 2026:

    Good catch, seems I ignored my own source code comment.

    I renamed it to m_interrupt_mining and also renamed the CooldownIfHeadersAhead argument, because it got confusing.

  63. ryanofsky approved
  64. ryanofsky commented at 6:00 pm on February 5, 2026: contributor

    Code review ACK 299f15677b3d32b878f79945128c977dc8eaedfd. Since last review, createNewBlock was fixed (by adding context parameter) to be actually interruptible, and cooldown was made optional and off by default, wait for chainman().IsInitialBlockDownload() was added, and CooldownIfHeadersAhead internal logic was improved to wait longer when there are more blocks waiting to be connected, instead of returning as soon as more than 1 second elapses between connecting blocks.

    All of these changes seem very nice. I left suggestions for keeping backwards compatibility and fixing a missing lock with I think would be good to apply but probably not critical.

  65. DrahtBot added the label Needs rebase on Feb 7, 2026
  66. Sjors force-pushed on Feb 7, 2026
  67. Sjors referenced this in commit e629a8262c on Feb 7, 2026
  68. Sjors commented at 11:35 am on February 7, 2026: member

    Rebased after the test split in #34452.

    Re: #34184 (review)

    I’d like to stick to making this a breaking change, assuming it’s merged before the v31 branch-off. We quite recently effectively bumped the minium requirement from v30 to v30.2 by removing the older releases, so we lost strong backward compatibility anyway. With v31 coming in two months, it seems fine to do a little cleanup.

    I also haven’t seen other breaking changes on the horizon (unless I overlooked them), so I don’t expect this to happen again soon.

  69. DrahtBot removed the label Needs rebase on Feb 7, 2026
  70. ryanofsky approved
  71. ryanofsky commented at 8:44 pm on February 9, 2026: contributor

    Code review ACK e629a8262c083d4d210689961a4bbeb41851c0af. Changes since last review were rebasing to fix test conflicts and renaming the interrupt variable and adding a missing lock around one access.

    I still feel like it would be better to replace the first commit breaking compatibility with the simpler change suggested in #34184 (review) which keeps compatibility.

    But I can understand being in a hurry to break compatibility when dealing with bugs in older clients & the pace of review here.

    So assuming you do want to keep the commit which breaks compatibility (eaa1209ea4462d1221e049891fb588adb5cf60d3), I’d suggest two more changes from my notes about things to fix in the next version bump. One would be to credit @plebhash for first pointing out the missing Context arguments in his comment #33575 (comment). I think he was the first person to notice that. The other would be to fix inconsistent defaults between C++ and Cap’n Proto interfaces, since changing default values also breaks backwards compatibility and the current missing defaults can lead to unnecessary problems and boilerplate in clients. Assigning consistent defaults should be possible with:

     0--- a/src/ipc/capnp/mining.capnp
     1+++ b/src/ipc/capnp/mining.capnp
     2@@ -12,11 +12,15 @@ using Proxy = import "/mp/proxy.capnp";
     3 $Proxy.include("interfaces/mining.h");
     4 $Proxy.includeTypes("ipc/capnp/mining-types.h");
     5 
     6+const maxMoney :Int64 = 2100000000000000;
     7+const maxDouble :Float64 = 1.7976931348623157e308;
     8+const defaultBlockReservedWeight :UInt32 = 8000;
     9+
    10 interface Mining $Proxy.wrap("interfaces::Mining") {
    11     isTestChain [@0](/bitcoin-bitcoin/contributor/0/) (context :Proxy.Context) -> (result: Bool);
    12     isInitialBlockDownload [@1](/bitcoin-bitcoin/contributor/1/) (context :Proxy.Context) -> (result: Bool);
    13     getTip [@2](/bitcoin-bitcoin/contributor/2/) (context :Proxy.Context) -> (result: Common.BlockRef, hasResult: Bool);
    14-    waitTipChanged [@3](/bitcoin-bitcoin/contributor/3/) (context :Proxy.Context, currentTip: Data, timeout: Float64) -> (result: Common.BlockRef);
    15+    waitTipChanged [@3](/bitcoin-bitcoin/contributor/3/) (context :Proxy.Context, currentTip: Data, timeout: Float64 = .maxDouble) -> (result: Common.BlockRef);
    16     createNewBlock [@4](/bitcoin-bitcoin/contributor/4/) (context :Proxy.Context, options: BlockCreateOptions, cooldown: Bool) -> (result: BlockTemplate);
    17     checkBlock [@5](/bitcoin-bitcoin/contributor/5/) (context :Proxy.Context, block: Data, options: BlockCheckOptions) -> (reason: Text, debug: Text, result: Bool);
    18     interrupt [@6](/bitcoin-bitcoin/contributor/6/) () -> ();
    19@@ -39,19 +43,19 @@ interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") {
    20 }
    21 
    22 struct BlockCreateOptions $Proxy.wrap("node::BlockCreateOptions") {
    23-    useMempool [@0](/bitcoin-bitcoin/contributor/0/) :Bool $Proxy.name("use_mempool");
    24-    blockReservedWeight [@1](/bitcoin-bitcoin/contributor/1/) :UInt64 $Proxy.name("block_reserved_weight");
    25-    coinbaseOutputMaxAdditionalSigops [@2](/bitcoin-bitcoin/contributor/2/) :UInt64 $Proxy.name("coinbase_output_max_additional_sigops");
    26+    useMempool [@0](/bitcoin-bitcoin/contributor/0/) :Bool = true $Proxy.name("use_mempool");
    27+    blockReservedWeight [@1](/bitcoin-bitcoin/contributor/1/) :UInt64 = .defaultBlockReservedWeight $Proxy.name("block_reserved_weight");
    28+    coinbaseOutputMaxAdditionalSigops [@2](/bitcoin-bitcoin/contributor/2/) :UInt64 = 400 $Proxy.name("coinbase_output_max_additional_sigops");
    29 }
    30 
    31 struct BlockWaitOptions $Proxy.wrap("node::BlockWaitOptions") {
    32-    timeout [@0](/bitcoin-bitcoin/contributor/0/) : Float64 $Proxy.name("timeout");
    33-    feeThreshold [@1](/bitcoin-bitcoin/contributor/1/) : Int64 $Proxy.name("fee_threshold");
    34+    timeout [@0](/bitcoin-bitcoin/contributor/0/) : Float64 = .maxDouble $Proxy.name("timeout");
    35+    feeThreshold [@1](/bitcoin-bitcoin/contributor/1/) : Int64 = .maxMoney $Proxy.name("fee_threshold");
    36 }
    37 
    38 struct BlockCheckOptions $Proxy.wrap("node::BlockCheckOptions") {
    39-    checkMerkleRoot [@0](/bitcoin-bitcoin/contributor/0/) :Bool $Proxy.name("check_merkle_root");
    40-    checkPow [@1](/bitcoin-bitcoin/contributor/1/) :Bool $Proxy.name("check_pow");
    41+    checkMerkleRoot [@0](/bitcoin-bitcoin/contributor/0/) :Bool = true $Proxy.name("check_merkle_root");
    42+    checkPow [@1](/bitcoin-bitcoin/contributor/1/) :Bool = true $Proxy.name("check_pow");
    43 }
    44 
    45 struct CoinbaseTx $Proxy.wrap("node::CoinbaseTx") {
    

    PR also seems fine it its current form, though and these are just suggestions.

  72. DrahtBot requested review from w0xlt on Feb 9, 2026
  73. Sjors commented at 9:04 pm on February 9, 2026: member

    I’d suggest two more changes

    I’ll amend the commit to give credit if I need to touch it again.

    The additional interface changes are in different methods, so I prefer to do them as a followup PR. I’ll also drop the previously deprected methods there.

  74. in src/interfaces/mining.h:156 in 0d4cf7041a outdated
    154+     *                     signets with only one miner.
    155      * @retval BlockTemplate a block template.
    156      * @retval std::nullptr if the node is shut down.
    157      */
    158-    virtual std::unique_ptr<BlockTemplate> createNewBlock(const node::BlockCreateOptions& options = {}) = 0;
    159+    virtual std::unique_ptr<BlockTemplate> createNewBlock(const node::BlockCreateOptions& options = {}, bool cooldown = false) = 0;
    


    ismaelsadeeq commented at 2:31 pm on February 11, 2026:

    In “mining: add cooldown argument to createNewBlock()” 0d4cf7041af41e6371473f7df93301d7100cea3e

    Shouldn’t it be the other way around, cooldown true? You do not cool down when on a custom setup?


    Sjors commented at 4:17 pm on February 11, 2026:
    The vast majority of code paths is in test code, so this default is easier (less code churn). Since IPC clients (unless they use libmultiprocess) can’t take advantage of the default value anyway, might as well use false.

    ryanofsky commented at 4:19 am on February 12, 2026:

    Since IPC clients (unless they use libmultiprocess) can’t take advantage of the default value anyway, might as well use false.

    I don’t think I have an opinion on what the default should be, but wanted to point out that Cap’n Proto supports setting default values, so IPC clients should be able to take advantage of whatever default is set.

    I opened #34568 just now which sets defaults for other parameters. The only restriction capnproto has on defaults is that you can only set them when fields and parameters are first introduced. One a field exists changing its default is not backwards compatible (because on the wire default values are always 0 and internally it uses some trickery to map 0 to whatever the declared default value is).


    Sjors commented at 11:53 am on February 12, 2026:
    Ok, that’s useful to know. I’ll take a look at this PR. Will hold on to false until that lands though.
  75. in src/interfaces/mining.h:152 in 0d4cf7041a outdated
    149-     *
    150      * @param[in] options options for creating the block
    151+     * @param[in] cooldown wait for tip to be connected and IBD to complete.
    152+     *                     If the best header is ahead of the tip, wait for the
    153+     *                     tip to catch up. Recommended, except for regtest and
    154+     *                     signets with only one miner.
    


    ismaelsadeeq commented at 2:33 pm on February 11, 2026:

    In “mining: add cooldown argument to createNewBlock()” 0d4cf7041af41e6371473f7df93301d7100cea3e

    nit: no need to mention all the custom setups that don’t need cooldown; highlight that we return immediately.


    Sjors commented at 4:19 pm on February 11, 2026:
    Even though the default in our c++ code is false (see above), the recommendation for real clients is true, with these two exceptions.
  76. in src/node/interfaces.cpp:986 in 0d4cf7041a outdated
    983+            // because on regtest and single miner signets this would wait
    984+            // forever if no block was mined in the past day.
    985+            while (chainman().IsInitialBlockDownload()) {
    986+                maybe_tip = waitTipChanged(maybe_tip->hash, MillisecondsDouble{1000});
    987+                if (!maybe_tip) return {};
    988+            }
    


    ismaelsadeeq commented at 2:52 pm on February 11, 2026:

    In “mining: add cooldown argument to createNewBlock()” 0d4cf7041af41e6371473f7df93301d7100cea3e

    Hmm, when the node loses connectivity briefly during IBD, or experiences block stalling due to poor peers or any other reason, this will return empty. Why not have it wait until IBD is done or interrupt during shutdown using a conditional variable (It is safe to block in this case because we are not in the final catch up yet) ?


    Sjors commented at 4:23 pm on February 11, 2026:

    No, we return empty if the node shuts down or the call is interrupted. If there’s a timeout, waitTipChanged returns the current tip, not nullopt.

    This loop just checks once per second if we’re still in IBD.


    ismaelsadeeq commented at 4:54 pm on February 11, 2026:
    That makes sense, thanks for clarifying.
  77. in src/node/miner.cpp:461 in 0d4cf7041a
    456+{
    457+    uint256 last_tip_hash{last_tip.hash};
    458+
    459+    while (const std::optional<int> remaining = chainman.BlocksAheadOfTip()) {
    460+        const int cooldown_seconds = std::clamp(*remaining, 3, 20);
    461+        // Use a steady clock so cooldown is independent of mocktime.
    


    ismaelsadeeq commented at 3:04 pm on February 11, 2026:

    In “mining: add cooldown argument to createNewBlock()” 0d4cf7041af41e6371473f7df93301d7100cea3e

    Why not mockable steady clock? It also moves forward only and will allow for testing some scenarios??


    Sjors commented at 4:34 pm on February 11, 2026:
    In an earlier version of this PR the cooldown would also happen in the test framework by default, so using mock time was a headache. But now it’s gated behind the cooldown argument. I’ll look into using a mockable stead clock if I need to retouch.

    Sjors commented at 4:33 pm on February 12, 2026:
    Done!
  78. in src/node/interfaces.cpp:973 in 0d4cf7041a outdated
    966@@ -967,10 +967,27 @@ class MinerImpl : public Mining
    967         return WaitTipChanged(chainman(), notifications(), current_tip, timeout);
    968     }
    969 
    970-    std::unique_ptr<BlockTemplate> createNewBlock(const BlockCreateOptions& options) override
    971+    std::unique_ptr<BlockTemplate> createNewBlock(const BlockCreateOptions& options, bool cooldown) override
    972     {
    973         // Ensure m_tip_block is set so consumers of BlockTemplate can rely on that.
    974-        if (!waitTipChanged(uint256::ZERO, MillisecondsDouble::max())) return {};
    975+        std::optional<BlockRef> maybe_tip{waitTipChanged(uint256::ZERO, MillisecondsDouble::max())};
    


    ismaelsadeeq commented at 3:13 pm on February 11, 2026:

    In “mining: add cooldown argument to createNewBlock()” 0d4cf7041af41e6371473f7df93301d7100cea3e

    Should this implementation code move to miner?


    Sjors commented at 4:32 pm on February 11, 2026:
    Maybe, but at the moment both cooldown and interrupt are specific to IPC.
  79. ismaelsadeeq commented at 3:25 pm on February 11, 2026: member

    Concept ACK

    I briefly looked at this, you could still mine on a stale tip even with this when your node stalls for a bit for any reason.

    But I get the point, mining on a stale, is far better than waiting forever.

    I have some suggestions below.

  80. rpc refactor: stop using deprecated getCoinbaseCommitment method
    There should be no change in behavior
    
    Co-authored-by: Sjors Provoost <sjors@sprovoost.nl>
    df53a3e5ec
  81. test framework: expand expected_stderr, expected_ret_code options
    Let expected_stderr option passed to wait_until_stopped and is_node_stopped
    helper functions be a regex pattern instead of just a fixed string.
    Let expected_ret_code be list of possible exit codes instead of a single error
    code to handle the case where exit codes vary depending on OS and libc.
    cd2aa85867
  82. ipc test: add workaround to block_reserved_weight exception test
    libmultiprocess currently handles uncaught exceptions from IPC methods badly
    when an `mp.Context` parameter is passed and the IPC call executes on an a
    worker thread, with the uncaught exception leading to a std::terminate call.
    
    https://github.com/bitcoin-core/libmultiprocess/pull/218 was created to fix
    this, but before that change is available, update an IPC test which can trigger
    this behavior to handle it and recover when mp.Context parameters are added in
    the an upcoming commit.
    
    Having this workaround makes the test a little more complicated and less strict
    but reduces dependencies between pending PRs so they don't need to be reviewed
    or merged in a particular order.
    f7ca042b6a
  83. ipc mining: declare constants for default field values
    The default values will be applied in an upcoming commit since changing them is
    not backwards compatible
    31f18f5551
  84. ipc mining: provide default option values (incompatible schema change)
    This change copies default option values from the C++ mining interface to the
    Cap'n Proto interface. Currently, no capnp default values are set, so they are
    implicitly all false or 0, which is inconvenient for the rust and python
    clients and inconsistent with the C++ client.
    
    Warning: This is an intermediate, review-only commit and is not intended to
    stand alone. It violates Cap'n Proto schema-evolution compatibility, so mixed
    client/server builds will silently decode IPC messages inconsistently,
    producing nonsensical requests/responses. A follow-up commit bumps the mining
    interface version and ensures new clients/servers reject older counterparts.
    
    git-bisect-skip: yes
    abd9a2fe10
  85. ipc mining: remove deprecated methods (incompatible schema change)
    This change removes deprecated methods from the ipc mining interface.
    
    Warning: This is an intermediate, review-only commit and is not intended to
    stand alone. It violates Cap'n Proto schema-evolution compatibility, so mixed
    client/server builds will silently decode IPC messages inconsistently,
    producing nonsensical requests/responses. A follow-up commit bumps the mining
    interface version and ensures new clients/servers reject older counterparts.
    
    git-bisect-skip: yes
    e2770e2f6b
  86. ipc mining: pass missing context to BlockTemplate methods (incompatible schema change)
    Adding a context parameter ensures that these methods are run in
    their own thread and don't block other calls. They were missing
    for:
    
    - createNewBlock()
    - checkBlock()
    
    The missing parameters were first pointed out by plebhash in
    https://github.com/bitcoin/bitcoin/issues/33575#issuecomment-3383290115 and
    adding them should prevent possible performance problems and lockups,
    especially with #34184 which can make the createNewBlock method block for a
    long time before returning. It would be straightforward to make this change in
    a backward compatible way
    (https://github.com/bitcoin/bitcoin/pull/34184#discussion_r2770232149) but nice
    to not need to go through the trouble.
    
    Warning: This is an intermediate, review-only commit and is not intended to
    stand alone. It violates Cap'n Proto schema-evolution compatibility, so mixed
    client/server builds will silently decode IPC messages inconsistently,
    producing nonsensical requests/responses. A follow-up commit bumps the mining
    interface version and ensures new clients/servers reject older counterparts.
    
    git-bisect-skip: yes
    
    Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
    3afebda53c
  87. ipc mining: break compatibility with existing clients (version bump)
    This increments the field number of the `Init.makeMining` method and makes the
    old `makeMining` method return an error, so existing IPC mining clients not
    using the latest schema file will get an error and not be able to access the
    Mining interface.
    
    Normally, there shouldn't be a need to break compatibility this way, but the
    mining interface has evolved a lot since it was first introduced, with old
    clients using the original methods less stable and performant than newer
    clients. So now is a good time to introduce a cutoff, drop deprecated methods,
    and stop supporting old clients which can't function as well.
    
    Bumping the field number is also an opportunity to make other improvements that
    would be awkward to implement compatibly, so a few of these were implemented in
    commits immediately preceding this one.
    
    Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
    db05772f2c
  88. DrahtBot added the label Needs rebase on Feb 12, 2026
  89. ryanofsky referenced this in commit e555eb86e9 on Feb 12, 2026
  90. doc: Release notes for mining IPC interface bump 84372191f8
  91. Sjors commented at 1:49 pm on February 12, 2026: member

    Trying to rebase after #33965, but this is trickier than I expected due to the interaction of 418b7995ddfbc88764f1f0ceabf8993808b08bd8 with eaa1209ea4462d1221e049891fb588adb5cf60d3. Apparently libmultiprocess doesn’t propagate the minimum weight exception now that it runs in another thread (because we add @context).

    I’m looking for a workaround that doesn’t involve patching libmultiprocess.

  92. ryanofsky referenced this in commit 6c614ba31a on Feb 12, 2026
  93. ryanofsky referenced this in commit fc60bd4540 on Feb 12, 2026
  94. Sjors referenced this in commit fe297f0976 on Feb 12, 2026
  95. Sjors force-pushed on Feb 12, 2026
  96. Sjors commented at 4:22 pm on February 12, 2026: member

    I ended up including the test workaround ad8e5f93b0db5ceb9b4331f88b54de6ca593fcbf from #34568 (along with the test framework tweaks in 1f1b089a44331feb7262f95a88437e47f52eb743). Added a note in the PR description that the merge order between this and #34568 doesn’t matter.

    I also switched to mockable steady clock in the test, see #34184 (review).

  97. DrahtBot removed the label Needs rebase on Feb 12, 2026
  98. ryanofsky referenced this in commit 0548a95f94 on Feb 12, 2026
  99. ryanofsky approved
  100. ryanofsky commented at 8:53 pm on February 12, 2026: contributor

    Code review ACK fe297f0976c1a2f8e2be3ad598bc287f71a7f8f9. Just rebased due to conflicts with #33965, added workaround to deal with new exception thrown by createNewBlock in that PR, added cooldown=True to python test, and switching from SteadyClock -> MockableSteadyClock since last review.

    I’d like if #34568 could be merged before this PR so this PR could be cleaner and more narrowly focused on implementing the cooldown feature, and so the mining interface number wouldn’t need to be bumped twice (or bumped once with an intermediate period where incompatible schemas could silently break clients).

    I also think it would be good to add cooldown: Bool = true to the .capnp file if we think this is a better default behavior. It should be ok for capnp default value to be different from c++ if updating the c++ value would create too much churn in unit tests.

    Just my thoughts though. PR is already in good shape as-is.

  101. DrahtBot requested review from ismaelsadeeq on Feb 12, 2026
  102. Sjors commented at 7:46 am on February 13, 2026: member

    I’d like if #34568 could be merged before this PR

    That’s fine by me.

    I also think it would be good to add cooldown: Bool = true to the .capnp file if we think this is a better default behavior.

    Good idea. Since it’s a new method, that’s not a breaking change. It does make the diff bigger because I need to opt-out everywhere, but if we merge #34568 that reduces the PR diff.

  103. Sjors referenced this in commit f24385eede on Feb 13, 2026
  104. Sjors force-pushed on Feb 13, 2026
  105. Sjors referenced this in commit 9c54cb116a on Feb 13, 2026
  106. Sjors force-pushed on Feb 13, 2026
  107. DrahtBot added the label CI failed on Feb 13, 2026
  108. DrahtBot commented at 9:32 am on February 13, 2026: contributor

    🚧 At least one of the CI tasks failed. Task macOS native: https://github.com/bitcoin/bitcoin/actions/runs/21979984946/job/63499970158 LLM reason (✨ experimental): testnet4_miner_tests timed out (Timeout) causing the CI to fail.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  109. Sjors commented at 9:34 am on February 13, 2026: member
    I missed one /*cooldown=*/false spot. Also pulled in the latest versions of the two commits from #34568 (just a commit message change).
  110. mining: add cooldown argument to createNewBlock()
    At startup, if the needs to catch up, connected mining clients will
    receive a flood of new templates as new blocks are connected.
    
    Fix this by adding a cooldown argument to createNewBlock(). When set
    to true, block template creation is briefly paused while the best
    header chain is ahead of the tip.
    
    This wait only happens when the best header extends the current tip,
    to ignore competing branches.
    
    Additionally, cooldown waits for isInitialBlockDownload() to latch to
    false, which happens when there is less than a day of blocks left to sync.
    
    When cooldown is false createNewBlock() returns immediately. The argument
    is optional, because many tests are negatively impacted by this
    mechanism, and single miner signets could end up stuck if no block
    was mined for a day.
    
    The getblocktemplate RPC also opts out, because it would add a delay
    to each call.
    
    Fixes #33994
    d92382f09f
  111. 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_mining is safely accessed with a lock on
    m_tip_block_mutex, but it has no guard annotation. A more thorough
    solution is discussed here:
    https://github.com/bitcoin/bitcoin/pull/34184#discussion_r2743566474
    6428ed1839
  112. Sjors commented at 9:41 am on February 13, 2026: member

    Making my life easier by rebasing this on top of #34568, which is in good shape, and marking as draft until that’s merged.

    (I have a release note too, will push on the next update)

  113. Sjors marked this as a draft on Feb 13, 2026
  114. Sjors force-pushed on Feb 13, 2026
  115. DrahtBot removed the label CI failed on Feb 13, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-02-17 06:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me