mining: add block template manager #35675

pull ismaelsadeeq wants to merge 15 commits into bitcoin:master from ismaelsadeeq:07-2026-block-template-man-skeleton changing 30 files +900 −450
  1. ismaelsadeeq commented at 11:05 AM on July 7, 2026: member

    This PR introduces node::BlockTemplateManager and moves block template creation, submission, and mining wait helpers behind it.

    Motivation

    Instead of keeping template-related state and helper functions spread across NodeContext, miner.cpp, the mining interface, RPC, and tests, the manager now owns the node's init-time mining options and exposes methods needed by callers.

    This also prevents some redundant copies previously done when using the mining interface to create a block template and then retrieve the template data.

    This keeps the IPC Mining interface focused on IPC-facing mining objects, while RPC and tests use the block template manager directly.

    Changes

    • NodeContext no longer stores BlockCreateOptions directly.
    • BlockTemplateManager stores the parsed init-time mining options and applies them to unset per-call options before creating templates.
    • Block submission through the mining interface is routed through BlockTemplateManager::SubmitBlock(), preserving the existing BlockChecked state-capture behavior.
    • Tip lookup, tip waiting, cooldown, and waitNext() template creation helpers are moved from miner helper functions into BlockTemplateManager.
    • In-process RPC and tests create raw CBlockTemplate objects directly through BlockTemplateManager, avoiding cached BlockTemplateImpl objects that hold NodeContext references during shutdown.
    • A fuzz target is added for BlockTemplateManager::CreateNewTemplate() with fuzzed mempool contents and mining options.

    Note: This change is intended to be a pure refactor that preserves behavior.

  2. DrahtBot added the label Mining on Jul 7, 2026
  3. DrahtBot commented at 11:06 AM on July 7, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

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

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Stale ACK Sjors, w0xlt, pablomartin4btc, enirox001

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35847 (test: move more tests to baseindex_tests and run them for all indexes by mzumsande)
    • #35646 (RFC: Separate out runtime errors from BlockValidationState using util::Expected by yuvicc)
    • #35569 (Encapsulation for CTransaction by purpleKarrot)
    • #35482 (fuzz: exercise the transaction-handling path in process_message(s) by HowHsu)
    • #35300 (mining: add precious option to IPC block submission by w0xlt)
    • #34617 (fees: wallet: remove block policy fee estimator internals from wallet by ismaelsadeeq)
    • #34565 (refactor: extract BlockDownloadManager from PeerManagerImpl by w0xlt)
    • #34075 (fees: Introduce Mempool Based Fee Estimation to reduce overestimation by ismaelsadeeq)
    • #32468 (rpc: generateblock to allow multiple outputs by polespinasa)
    • #31117 (miner: Reorg Testnet4 minimum difficulty blocks by fjahr)
    • #30342 (kernel, logging: Pass Logger instances to kernel objects 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.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. ismaelsadeeq commented at 11:06 AM on July 7, 2026: member

    CC @Sjors @pablomartin4btc @w0xlt as discussed in #35581

  5. Sjors commented at 3:49 PM on July 7, 2026: member

    Concept ACK

    I updated https://github.com/Sjors/bitcoin/pull/120 to build on this branch.

    Will review later.

  6. Sjors commented at 4:47 PM on July 7, 2026: member

    Please rebase, so I can make #33922 cleanly based on this (it conflicts with #35129 and #34020).

  7. ismaelsadeeq force-pushed on Jul 8, 2026
  8. ismaelsadeeq commented at 11:37 AM on July 8, 2026: member

    Please rebase, so I can make #33922 cleanly based on this (it conflicts with #35129 and #34020).

    Done.

  9. Sjors commented at 2:11 PM on July 8, 2026: member

    Commit 231fa5ad4f71e652c3c7a52a88f540d47dad29b7 rpc, test: build block templates via BlockTemplateManager is rather large. Maybe have one commit per RPC method, or at least do the critical getblocktemplate in a separate commit.

    After this change, does node.mining still do anything?

    Consider adding the new files to FILES_WITH_ENFORCED_IWYU since their ancestors were enforced too.

  10. in src/node/block_template_manager.h:18 in 1aa695a6f0
      13 | +class CTxMemPool;
      14 | +
      15 | +namespace node {
      16 | +struct CBlockTemplate;
      17 | +
      18 | +/** Creates block templates. */
    


    Sjors commented at 3:23 PM on July 8, 2026:

    In 1aa695a6f08a6824f5577ce8de3e5fe6578daa3b node: introduce block template manager: in this PR the block template manager doesn't really live up to its name. We should clarify what it does, e.g.:

    Creates block templates, submits solved blocks, and provides tip-waiting helpers for mining code. Owns the init-time mining options. Currently stateless — templates it returns are owned by the caller; template tracking is added in followups (see #33758).

  11. in src/test/fuzz/cmpctblock.cpp:116 in 1aa695a6f0
     112 | @@ -112,13 +113,16 @@ void ResetChainmanAndMempool(TestingSetup& setup)
     113 |      SetMockTime(Params().GenesisBlock().Time());
     114 |  
     115 |      bilingual_str error{};
     116 | -    setup.m_node.mempool.reset();
     117 | -    setup.m_node.mempool = std::make_unique<CTxMemPool>(MemPoolOptionsForTest(setup.m_node), error);
     118 | +    auto& node = setup.m_node;
    


    Sjors commented at 3:28 PM on July 8, 2026:

    In 1aa695a6f08a6824f5577ce8de3e5fe6578daa3b node: introduce block template manager: not sure if this alias adds much value, is does add churn.

  12. in src/node/block_template_manager.h:24 in 610d5d704c
      20 | @@ -21,10 +21,15 @@ class BlockTemplateManager
      21 |  private:
      22 |      CTxMemPool& m_mempool;
      23 |      ChainstateManager& m_chainman;
      24 | +    const BlockCreateOptions m_init_block_create_options;
    


    Sjors commented at 3:36 PM on July 8, 2026:

    In 610d5d704c35c8aeda860640b599e9ffa5e99d92 node: move mining_args to block template manager: maybe call this m_block_create_args and then document that they're set during Init.

  13. in src/node/block_template_manager.h:32 in 610d5d704c
      28 | -                                  ChainstateManager& chainman);
      29 | +                                  ChainstateManager& chainman,
      30 | +                                  BlockCreateOptions init_block_create_options = {});
      31 | +
      32 | +    /** @return a copy of the block create options set during node init. */
      33 | +    BlockCreateOptions GetInitBlockCreateOptions() const { return m_init_block_create_options; }
    


    Sjors commented at 3:37 PM on July 8, 2026:

    In 610d5d704c35c8aeda860640b599e9ffa5e99d92 node: move mining_args to block template manager: in line with my above comment, I'd call this BlockCreateArgs() (dropping Get makes the call sites more readable imo, the method is already marked const)

    nit: const BlockCreateOptions&

  14. Sjors commented at 4:15 PM on July 8, 2026: member

    Reviewed up to 00e3b6717204937272f2d81074b429e7c13430a7 miner: move SubmitBlock into BlockTemplateManager. Be careful when rebasing after #34672.

  15. in src/node/interfaces.cpp:945 in 149baad220 outdated
     949 |  
     950 |      const BlockCreateOptions m_create_options;
     951 |  
     952 |      const std::unique_ptr<CBlockTemplate> m_block_template;
     953 |  
     954 |      bool m_interrupt_wait{false};
    


    pablomartin4btc commented at 4:55 PM on July 8, 2026:

    nit: m_interrupt_wait is missing the comment that m_interrupt_mining in MinerImpl has:

        // Treat as if guarded by notifications().m_tip_block_mutex
        bool m_interrupt_wait{false};
    

    I think both fields are used identically — written under the mutex via InterruptWait(), read inside lambda predicates holding the mutex. The missing comment on m_interrupt_wait seems inconsistent.


    pablomartin4btc commented at 9:35 PM on July 29, 2026:

    I think this hasn't been addressed...


    ismaelsadeeq commented at 9:49 AM on July 30, 2026:

    Yeah, it's a nit. I would prefer not to add this comment, so I ignored it. :)


    pablomartin4btc commented at 3:36 PM on July 30, 2026:

    No problem at all, thought it got missed as it was marked as fixed. Thanks!

  16. pablomartin4btc commented at 5:15 PM on July 8, 2026: member

    Concept ACK

    Thanks for taking the suggestion from the previous PR.

    General first pass review at 149baad220.

    This PR is not based on #34803. #35675 targets master directly. IsStale, TemplateSnapshot, m_template_snapshots, and any MempoolUpdated subscription are absent — this is a pure refactor. The fee inflow tracking from #35581 will follow as a separate PR once this (or #34803) merges.

    Key differences from #35581:

    • WaitAndCreateNewBlock has no cheap staleness check — it rebuilds a full template to detect fee increases. The original comment (// The latter check is expensive so we only run it once per second.) is accurate again here.
    • No template_id / tracked-vs-untracked template distinction.
    • rpc/mining.cpp calls BlockTemplateManager::CreateNewTemplate() directly (returning CBlockTemplate). BlockTemplateImpl is only ever instantiated in node/interfaces.cpp (the IPC layer) — both on initial creation via MinerImpl::createNewBlock and on subsequent calls via waitNext — never from the RPC path. This removes the NodeContext-reference-during-shutdown concern that was present in #35581.

    Re Sjors' question about node.mining: it appears to be unused after this PR. On master it was read via EnsureMining() in rpc/server_util.cpp, but that's now replaced by EnsureBlockTemplateManager(). The field could likely be removed.

    I plan to follow up with a more detailed commit-by-commit review. Please let me know if any of my observations above are off.

    Thanks for splitting this out from #35581 — the separation between the pure refactor and the fee-inflow tracking makes both easier to review.

  17. ismaelsadeeq force-pushed on Jul 10, 2026
  18. ismaelsadeeq commented at 4:32 PM on July 10, 2026: member

    Forced pushed from 149baad220 to 82dc581ae8 149baad220...82dc581ae8

    Thanks for the review @Sjors @pablomartin4btc

  19. enirox001 commented at 5:40 AM on July 15, 2026: contributor

    Concept ACK

    Having a dedicated template manager is helpful. In a recent PR, the mining options were also disentangled from startup defaults, which helped clarify and improve consistency. Nice to see this approach at a centralized block template manager

  20. in src/rpc/mining.cpp:502 in 82dc581ae8
     498 | @@ -502,7 +499,7 @@ static RPCMethod getmininginfo()
     499 |      obj.pushKV("target", GetTarget(tip, chainman.GetConsensus().powLimit).GetHex());
     500 |      obj.pushKV("networkhashps",    getnetworkhashps().HandleRequest(request));
     501 |      obj.pushKV("pooledtx", mempool.size());
     502 | -    const auto mining_options{node::FlattenMiningOptions(node.mining_args)};
     503 | +    const auto mining_options{node::FlattenMiningOptions(CHECK_NONFATAL(node.block_template_manager)->BlockCreateArgs())};
    


    pablomartin4btc commented at 3:51 AM on July 16, 2026:

    In 370d6cdb, shouldn't be EnsureBlockTemplateManager(node) instead of CHECK_NONFATAL?

        const auto mining_options{node::FlattenMiningOptions(EnsureBlockTemplateManager(node).BlockCreateArgs())};
    
  21. in src/test/util/setup_common.cpp:331 in 82dc581ae8 outdated
     327 | @@ -328,12 +328,21 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, TestOpts opts)
     328 |          m_node.chainman = std::make_unique<ChainstateManager>(*Assert(m_node.shutdown_signal), chainman_opts, blockman_opts);
     329 |      };
     330 |      m_make_chainman();
     331 | +    CreateBlockTemplateManager();
    


    pablomartin4btc commented at 4:43 AM on July 16, 2026:

    In 9038cc7e, CreateBlockTemplateManager() is being called before chainstate is activated (before LoadVerifyActivateChainstate() runs. The manager is created before ActiveChainstate() is ready — CreateNewBlock() asserts pindexPrev != nullptr and would crash if called at this point).

    Every fuzz reset path added in the same commit gets the order right: reset() → m_make_chainman() → LoadVerifyActivateChainstate()CreateBlockTemplateManager(). The production path in init.cpp also creates the manager only after VerifyLoadedChainstate() succeeds. The constructor is inconsistent with both.

    So CreateBlockTemplateManager() could be removed from ChainTestingSetup's constructor and call it at the end of LoadVerifyActivateChainstate() instead. The four explicit CreateBlockTemplateManager() calls in the fuzz reset paths become redundant and can be removed. Tests that never call LoadVerifyActivateChainstate() have no loaded chainstate and no use for the manager anyway.

  22. in src/node/interfaces.cpp:941 in 82dc581ae8 outdated
     945 |      {
     946 | -        InterruptWait(notifications(), m_interrupt_wait);
     947 | +        block_template_manager().InterruptWait(m_interrupt_wait);
     948 |      }
     949 |  
     950 |      const BlockCreateOptions m_create_options;
    


    pablomartin4btc commented at 5:05 AM on July 16, 2026:

    minor nit (naming clarity only). The behaviour is correct.

        const BlockCreateOptions m_caller_options;
    

    <br> The concern is in `waitNext()` at line 931-932:

      block_template_manager().WaitAndCreateNewBlock(
          m_block_template, options, m_create_options, m_interrupt_wait);
      if (new_template) return std::make_unique<BlockTemplateImpl>(m_create_options, ...);
    

    A reader seeing m_create_options passed there might think "these are the fully resolved options that were used to build the template" — but they're not, they're the raw caller options that will get merged again inside CreateNewTemplate(). Perhaps m_caller_options makes that clear.


    ismaelsadeeq commented at 1:25 PM on July 16, 2026:

    I think I did not touch that line in this PR, and we flatten in master as well, so making it explicit like u suggested will be nice but beyond the scope of this PR.

  23. in src/test/fuzz/block_template_manager.cpp:1 in 82dc581ae8 outdated


    pablomartin4btc commented at 5:14 AM on July 16, 2026:

    tiny nit: In 82dc581a — fuzz file is named blocktemplatemanager.cpp but FUZZ_TARGET(block_template_manager). Every other fuzz target uses the same name for both (e.g. block_index.cppFUZZ_TARGET(block_index)). Shouldn't be named block_template_manager.cpp?

  24. in src/node/block_template_manager.cpp:216 in 82dc581ae8 outdated
     211 | +        const auto cooldown_deadline{MockableSteadyClock::now() + std::chrono::seconds{cooldown_seconds}};
     212 | +
     213 | +        {
     214 | +            WAIT_LOCK(m_notifications.m_tip_block_mutex, lock);
     215 | +            m_notifications.m_tip_block_cv.wait_until(lock, cooldown_deadline, [&]() EXCLUSIVE_LOCKS_REQUIRED(m_notifications.m_tip_block_mutex) {
     216 | +                const auto tip_block = m_notifications.TipBlock();
    


    pablomartin4btc commented at 5:17 AM on July 16, 2026:

    In b520d18b, AssertLockHeld() isn't missing as in WaitAndCreateNewBlock()?

                    AssertLockHeld(m_notifications.m_tip_block_mutex);
                    const auto tip_block = m_notifications.TipBlock();
    
  25. pablomartin4btc commented at 5:19 AM on July 16, 2026: member

    ACK 82dc581ae80a5ab39b39d44cac1daef8c6232ba8

    Reviewed commit by commit. Clean refactor overall — the consolidation of block creation, submission, and tip-waiting into BlockTemplateManager makes the mining boundary explicit and enables the removal of node.mining from NodeContext (b8ae3fc6).

    Left some comments.

  26. DrahtBot requested review from Sjors on Jul 16, 2026
  27. DrahtBot requested review from enirox001 on Jul 16, 2026
  28. ismaelsadeeq force-pushed on Jul 16, 2026
  29. ismaelsadeeq commented at 1:25 PM on July 16, 2026: member

    Forced pushed 82dc581ae80a5ab39b39d44cac1daef8c6232ba8 to cd83dfd9a6a6ab8404519d775dd10a7aa4067795 https://github.com/bitcoin/bitcoin/compare/82dc581ae80a5ab39b39d44cac1daef8c6232ba8..cd83dfd9a6a6ab8404519d775dd10a7aa4067795

  30. in src/init.cpp:1439 in 4765381815
    1435 | @@ -1434,6 +1436,7 @@ static ChainstateLoadResult InitAndLoadChainstate(
    1436 |          std::tie(status, error) = catch_exceptions([&] { return VerifyLoadedChainstate(chainman, options); });
    1437 |          if (status == node::ChainstateLoadStatus::SUCCESS) {
    1438 |              LogInfo("Block index and chainstate loaded");
    1439 | +            node.block_template_manager = std::make_unique<node::BlockTemplateManager>(*node.mempool, chainman);
    


    Sjors commented at 1:39 PM on July 16, 2026:

    In 47653818150745f452e01d204a240a45162e4b85 node: introduce BlockTemplateManager: maybe document that the order is important here:

    must be set before setChainstateLoaded(true), which unblocks MakeMining waiters that assume it is non-null


    enirox001 commented at 7:51 PM on July 16, 2026:

    In 4765381815 "node: introduce BlockTemplateManager"

    nit:

    I see a pattern in the code where a null assertion is used to check if it has been initialized before initializing a node object, such as node.mempool, node.chainman, node.scheduler. It seems this happens because these can be initialized in a different places, so when you try to initialize again, you should check whether it has already been initialized.

    I think there might be no use for this for the block template manager. As it is only ever initialized here, I think to keep things consistent would be good to assert before initializing as well

    index 73281cab8a..6199b120af 100644
    --- a/src/init.cpp
    +++ b/src/init.cpp
    @@ -1431,6 +1431,7 @@ static ChainstateLoadResult InitAndLoadChainstate(
                 LogInfo("Block index and chainstate loaded");
                 auto mining_args{node::ReadMiningArgs(args)};
                 Assert(mining_args); // no error can happen, already checked in AppIni
    tParameterInteraction
    +            assert(!node.block_template_manager);
                 node.block_template_manager = std::make_unique<node::BlockTemplateMana
    ger>(*node.mempool, chainman, *node.notifications, std::move(*mining_args));
                 node.notifications->setChainstateLoaded(true);
             }
    
  31. in src/node/block_template_manager.h:59 in 9a0be305a2
      50 | @@ -40,6 +51,46 @@ class BlockTemplateManager
      51 |  
      52 |      /** Submit a block via ProcessNewBlock and capture validation state. */
      53 |      bool SubmitBlock(const std::shared_ptr<const CBlock>& block, bool* new_block, std::string& reason, std::string& debug);
      54 | +
      55 | +    /** @return the active chain tip, or nullopt if none exists. */
      56 | +    std::optional<interfaces::BlockRef> GetTip();
      57 | +
      58 | +    /** Wait for the tip to differ from @p current_tip or timeout.
      59 | +     *  Waits indefinitely during startup for a non-null tip. */
    


    Sjors commented at 2:18 PM on July 16, 2026:

    In 9a0be305a2c4a9dd50e4b51520189c6173939715 node: move tip and wait helpers into BlockTemplateManager: maybe ~example~ expand this an the above comment a bit:

    diff --git a/src/node/block_template_manager.h b/src/node/block_template_manager.h
    index 3386bbf65a..02fadaf75c 100644
    --- a/src/node/block_template_manager.h
    +++ b/src/node/block_template_manager.h
    @@ -50,15 +50,18 @@ public:
         std::unique_ptr<CBlockTemplate> CreateNewTemplate(const BlockCreateOptions& options);
    
         /** Submit a block via ProcessNewBlock and capture validation state. */
         bool SubmitBlock(const std::shared_ptr<const CBlock>& block, bool* new_block, std::string& reason, std::string& debug);
    
    -    /** [@return](/bitcoin-bitcoin/contributor/return/) the active chain tip, or nullopt if none exists. */
    +    /** Locks cs_main.
    +     * [@return](/bitcoin-bitcoin/contributor/return/) the active chain tip, or nullopt if none exists. */
         std::optional<interfaces::BlockRef> GetTip();
    
         /** Wait for the tip to differ from [@p](/bitcoin-bitcoin/contributor/p/) current_tip or timeout.
    -     *  Waits indefinitely during startup for a non-null tip. */
    +     *  Waits indefinitely during startup for a non-null tip.
    +     * [@return](/bitcoin-bitcoin/contributor/return/) the current tip, or nullopt if the node is shutting down or
    +     *  interrupt is set (not when the timeout is reached). */
         std::optional<interfaces::BlockRef> WaitTipChanged(const uint256& current_tip, MillisecondsDouble& timeout, bool& interrupt);
    
         /**
          * Wait while the best known header extends the current chain tip AND at
          * least one block is being added to the tip every 3 seconds. If the tip is
    
  32. in src/rpc/mining.cpp:797 in c6f05147f9 outdated
     793 | @@ -794,13 +794,13 @@ static RPCMethod getblocktemplate()
     794 |      if (strMode != "template")
     795 |          throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid mode");
     796 |  
     797 | -    if (!miner.isTestChain()) {
     798 | +    if (!chainman.GetParams().IsTestChain()) {
    


    Sjors commented at 2:38 PM on July 16, 2026:

    In c6f05147f9024c02ea7e3a07eddbd57720695c86 rpc: build getblocktemplate via BlockTemplateManager: perhaps worth a separate commit that stops routing getblocktemplate internals through the Mining interface.

    It was originally done for test coverage, but interface_ipc_mining.py covers all (?) these methods now.

  33. in src/rpc/mining.cpp:909 in c6f05147f9 outdated
     905 | @@ -906,15 +906,15 @@ static RPCMethod getblocktemplate()
     906 |          // a delay to each getblocktemplate call. This differs from typical
     907 |          // long-lived IPC usage, where the overhead is paid only when creating
     908 |          // the initial template.
     909 | -        block_template = miner.createNewBlock({}, /*cooldown=*/false);
     910 | +        block_template = block_template_manager.CreateNewTemplate({});
    


    Sjors commented at 2:40 PM on July 16, 2026:

    In c6f05147f9024c02ea7e3a07eddbd57720695c86 rpc: build getblocktemplate via BlockTemplateManager: the basically does what #32547 wanted?

    It's a more important change than the others in this commit, hence probably worth separating.


    ismaelsadeeq commented at 2:53 PM on July 16, 2026:

    Yes, I added that in the PR description.

  34. Sjors commented at 2:48 PM on July 16, 2026: member

    Code review cd83dfd9a6a6ab8404519d775dd10a7aa4067795.

    Update: maybe widen the range of fuzzed block_reserved_weight, block_max_weight, etc?

    Maybe useful extra fuzz coverage...:

    <details><summary>Claude suggestion</summary>

    diff --git a/src/test/fuzz/block_template_manager.cpp b/src/test/fuzz/block_template_manager.cpp
    index 658ddc3b16..af8fb95a79 100644
    --- a/src/test/fuzz/block_template_manager.cpp
    +++ b/src/test/fuzz/block_template_manager.cpp
    @@ -6,8 +6,10 @@
     #include <consensus/consensus.h>
     #include <consensus/validation.h>
    +#include <interfaces/types.h>
     #include <kernel/mempool_entry.h>
     #include <node/block_template_manager.h>
     #include <node/miner.h>
     #include <node/mining_args.h>
    +#include <node/mining_types.h>
     #include <policy/feerate.h>
     #include <policy/policy.h>
    @@ -21,8 +23,12 @@
     #include <test/util/txmempool.h>
     #include <txmempool.h>
    +#include <uint256.h>
    +#include <util/time.h>
    
     #include <cassert>
    +#include <chrono>
     #include <cstddef>
     #include <cstdint>
    +#include <numeric>
     #include <optional>
    
    @@ -123,4 +129,64 @@ FUZZ_TARGET(block_template_manager, .init = initialize_block_template_manager)
                 assert(fee >= 0);
             if (!resolved.use_mempool) assert(block.vtx.size() == 1);
    +
    +        // Exercise the tip-wait helpers. Every wait below uses a zero
    +        // timeout, so each predicate is checked exactly once and no call
    +        // blocks: the tip cannot change during this test.
    +        const uint256 tip_hash{block.hashPrevBlock};
    +        {
    +            // WaitTipChanged() is what getblocktemplate's longpoll blocks on.
    +            // A current_tip equal to the actual tip times out and returns the
    +            // unchanged tip; any other value returns immediately. An interrupt
    +            // wins over both and is reset by the call.
    +            const bool interrupted{fuzzed_data_provider.ConsumeBool()};
    +            bool interrupt{interrupted};
    +            const uint256 current_tip{fuzzed_data_provider.ConsumeBool() ? tip_hash : ConsumeUInt256(fuzzed_data_provider)};
    +            MillisecondsDouble timeout{0};
    +            const std::optional<interfaces::BlockRef> wait_tip{block_template_manager.WaitTipChanged(current_tip, timeout, interrupt)};
    +            assert(!interrupt);
    +            if (interrupted) {
    +                assert(!wait_tip);
    +            } else {
    +                assert(wait_tip && wait_tip->hash == tip_hash);
    +            }
    +        }
    +        {
    +            // WaitAndCreateNewBlock() backs the Mining interface's waitNext(),
    +            // the IPC template-update mechanism (unrelated to the RPC
    +            // longpoll). Its wait loop runs exactly one iteration on a zero
    +            // timeout. Fresh transactions may raise the next template's fees,
    +            // and mock time more than 20 minutes past the tip forces a
    +            // template through the test-network min-difficulty path.
    +            if (fuzzed_data_provider.ConsumeBool()) {
    +                PopulateRandTransactionsToMempool(fuzzed_data_provider, mempool, fuzzed_data_provider.ConsumeIntegralInRange<int>(0, DEFAULT_BLOCK_MAX_WEIGHT));
    +            }
    +            const std::chrono::seconds mock_offset{fuzzed_data_provider.ConsumeIntegralInRange<int64_t>(0, 30 * 60)};
    +            SetMockTime(WITH_LOCK(node.chainman->GetMutex(),
    +                                  return node.chainman->ActiveTip()->Time()) + mock_offset);
    +            const bool min_difficulty_window{mock_offset > std::chrono::minutes{20}};
    +            node::BlockWaitOptions wait_options;
    +            wait_options.timeout = MillisecondsDouble{0};
    +            if (fuzzed_data_provider.ConsumeBool()) {
    +                wait_options.fee_threshold = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, MAX_MONEY);
    +            }
    +            const bool interrupted{fuzzed_data_provider.ConsumeBool()};
    +            bool interrupt{interrupted};
    +            const auto next_template{block_template_manager.WaitAndCreateNewBlock(block_template, wait_options, options, interrupt)};
    +            assert(!interrupt);
    +            if (interrupted) {
    +                assert(!next_template);
    +            } else if (min_difficulty_window) {
    +                // A new template is returned regardless of its fees.
    +                assert(next_template && next_template->block.hashPrevBlock == tip_hash);
    +            } else if (next_template) {
    +                // Without a tip change the only way out before the deadline is
    +                // the fee threshold.
    +                assert(next_template->block.hashPrevBlock == tip_hash);
    +                const CAmount old_fees{std::accumulate(block_template->vTxFees.begin(), block_template->vTxFees.end(), CAmount{0})};
    +                const CAmount new_fees{std::accumulate(next_template->vTxFees.begin(), next_template->vTxFees.end(), CAmount{0})};
    +                assert(wait_options.fee_threshold < MAX_MONEY);
    +                assert(new_fees >= old_fees + wait_options.fee_threshold);
    +            }
    +        }
         }
     }
    

    </details>

  35. in src/test/util/setup_common.cpp:337 in 4765381815
     328 | @@ -328,12 +329,19 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, TestOpts opts)
     329 |          m_node.chainman = std::make_unique<ChainstateManager>(*Assert(m_node.shutdown_signal), chainman_opts, blockman_opts);
     330 |      };
     331 |      m_make_chainman();
     332 | +    CreateBlockTemplateManager();
     333 | +}
     334 | +
     335 | +void ChainTestingSetup::CreateBlockTemplateManager()
     336 | +{
     337 | +    m_node.block_template_manager = std::make_unique<node::BlockTemplateManager>(*m_node.mempool, *m_node.chainman);
    


    enirox001 commented at 8:38 PM on July 16, 2026:

    In 4765381815 "node: introduce BlockTemplateManager"

    nit: asserting that it is non initialized here could be nice as well

    index eecd0a60cf..6282a77928 100644
    --- a/src/test/util/setup_common.cpp
    +++ b/src/test/util/setup_common.cpp
    @@ -335,6 +335,7 @@ void ChainTestingSetup::CreateBlockTemplateManager()
     {
         auto mining_args{node::ReadMiningArgs(*Assert(m_node.args))};
         Assert(mining_args);
    +    Assert(!m_node.block_template_manager);
         m_node.block_template_manager = std::make_unique<node::BlockTemplateManager>(*
    m_node.mempool, *m_node.chainman, *Assert(m_node.notifications), std::move(*mining_
    args));
     }
    
  36. in src/node/block_template_manager.cpp:32 in b1c0f2f682 outdated
      27 | @@ -23,4 +28,60 @@ std::unique_ptr<CBlockTemplate> BlockTemplateManager::CreateNewTemplate(const Bl
      28 |      return BlockAssembler{m_chainman.ActiveChainstate(), &m_mempool, options}.CreateNewBlock();
      29 |  }
      30 |  
      31 | +namespace {
      32 | +class SubmitBlockStateCatcher final : public CValidationInterface
    


    enirox001 commented at 12:10 PM on July 17, 2026:

    In https://github.com/bitcoin/bitcoin/pull/35675/changes/b1c0f2f682414ee728d27a1739bd4d56130a4532 "miner: move SubmitBlock into BlockTemplateManager"

    I have some gripes with this. The SubmitBlockStateCatcher is duplicated in different places. This moved one in the template manager, another in the rpc/mining.cpp as submitblock_StateCatcher and BlockValidationStateCatcher in util/mining.cpp. I think they more or less do the same thing. This may not be a good fit for this PR, but we could move this to mining_types.h and use it where it applies. Maybe in a separate PR


    Sjors commented at 9:01 AM on July 20, 2026:

    See #34672


    ismaelsadeeq commented at 11:45 AM on July 20, 2026:

    Doing this is beyond the scope of this PR, I think.

  37. ismaelsadeeq force-pushed on Jul 17, 2026
  38. ismaelsadeeq commented at 4:08 PM on July 17, 2026: member

    Forced pushed from cd83dfd9a6a6ab8404519d775dd10a7aa4067795 to 1881848e0c99e1e55f9739d2a097bfdad6e3873c cd83dfd9a6...1881848e0c

  39. in src/node/block_template_manager.cpp:119 in 52b09a4538 outdated
     114 | +    interrupt_wait = true;
     115 | +    m_notifications.m_tip_block_cv.notify_all();
     116 | +}
     117 | +
     118 | +std::unique_ptr<CBlockTemplate> BlockTemplateManager::WaitAndCreateNewBlock(
     119 | +    const std::unique_ptr<CBlockTemplate>& block_template,
    


    enirox001 commented at 1:17 PM on July 18, 2026:

    In 52b09a4538 "node: move tip and wait helpers into BlockTemplateManager"

    Would it be cleaner for WaitAndCreateNewBlock() to accept const CBlockTemplate&? I don't think the method depends on ownership. It also assumes the template is non-null, so i think an object reference would retain the intended behaviour without using the unique_ptr


    ismaelsadeeq commented at 11:46 AM on July 20, 2026:

    This is a move-only commit and aim to reserve call site arguments, changing the parameter types is beyond the scope of this PR.

  40. in src/node/block_template_manager.cpp:44 in c6faba7d8e
      40 | @@ -41,7 +41,11 @@ BlockTemplateManager::BlockTemplateManager(CTxMemPool& mempool, ChainstateManage
      41 |  
      42 |  std::unique_ptr<CBlockTemplate> BlockTemplateManager::CreateNewTemplate(const BlockCreateOptions& options)
      43 |  {
      44 | -    return BlockAssembler{m_chainman.ActiveChainstate(), &m_mempool, options}.CreateNewBlock();
    


    enirox001 commented at 1:46 PM on July 18, 2026:

    In c6faba7d8e: "interfaces create block template via BlockTemplateManager"

    non-blocking nit

    While checking for places where MergeMiningOptions is still used, I saw the miner_tests using it. It appears to use the BlockAssembler for what CreateNewTemplate does, and with the changes in this commit, this is no longer necessary. I think this can be implemented as

    index 2a7a0ff9eb..ee5417a4de 100644
    --- a/src/test/miner_tests.cpp
    +++ b/src/test/miner_tests.cpp
    @@ -219,11 +219,7 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
    
         // Test the inclusion of package feerates in the block template and ensure they are sequential.
         // Can't use the Mining interface because it needs access to m_package_feerates.
    -    const auto block_package_feerates = BlockAssembler{
    -        m_node.chainman->ActiveChainstate(),
    -        &tx_mempool,
    -        MergeMiningOptions(options, Assert(m_node.block_template_manager)->BlockCreateArgs()),
    -    }.CreateNewBlock()->m_package_feerates;
    +    const auto block_package_feerates = Assert(m_node.block_template_manager)->CreateNewTemplate(options)->m_package_feerates;
         BOOST_CHECK(block_package_feerates.size() == 2);
    
         // parent_tx and high_fee_tx are added to the block as a package.
    
  41. in src/node/context.h:32 in e19d9f67a5 outdated
      28 | @@ -29,7 +29,6 @@ class TorController;
      29 |  namespace interfaces {
      30 |  class Chain;
      31 |  class ChainClient;
      32 | -class Mining;
    


    enirox001 commented at 3:08 PM on July 18, 2026:

    In https://github.com/bitcoin/bitcoin/pull/35675/changes/e19d9f67a56af76105ee7524feb63fed59fa5558: "node: remove NodeContext::mining and EnsureMining"

    Minign has been removed from NodeContext header, but the import is still in context.cpp. THis should be removed

    index 05e6039aac..550ada748a 100644
    --- a/src/node/context.cpp
    +++ b/src/node/context.cpp
    @@ -7,7 +7,6 @@
     #include <addrman.h>
     #include <banman.h>
     #include <interfaces/chain.h>
    -#include <interfaces/mining.h>
     #include <kernel/context.h>
     #include <key.h>
     #include <net.h>
    
  42. in src/test/fuzz/block_template_manager.cpp:158 in 1881848e0c outdated
     153 | +        }
     154 | +    }
     155 | +}
     156 | +} // namespace
     157 | +
     158 | +FUZZ_TARGET(block_template_manager, .init = initialize_block_template_manager)
    


    enirox001 commented at 3:37 PM on July 18, 2026:

    In 1881848e0c: "test: fuzz BlockTemplateManager"

    The fuzz target looks good, but i do not see where this exercises the non-default BlockCreateArgs default arguments. I think this prevents it from exercising the merging cases.

    I think perhpas we could construct a local BlockTemplateManager with the init-time options for each iteration, this would cover the options behavior

    codex suggested this

    index 297713d620..332a3a3ba1 100644
    --- a/src/test/fuzz/block_template_manager.cpp
    +++ b/src/test/fuzz/block_template_manager.cpp
    @@ -159,8 +159,9 @@ FUZZ_TARGET(block_template_manager, .init = initialize_block_template_manager)
     {
         FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
         const auto& node = g_setup->m_node;
    -    auto& block_template_manager = *Assert(node.block_template_manager);
         auto& mempool = *Assert(node.mempool);
    +    auto& chainman = *Assert(node.chainman);
    +    auto& notifications = *Assert(node.notifications);
         SeedRandomStateForTest(SeedRand::ZEROS);
         SetMockTime(WITH_LOCK(node.chainman->GetMutex(),
                               return node.chainman->ActiveTip()->Time()));
    @@ -179,6 +180,26 @@ FUZZ_TARGET(block_template_manager, .init = initialize_block_template_manager)
         }
         LIMITED_WHILE(fuzzed_data_provider.remaining_bytes() > 0, 10)
         {
    +        BlockCreateOptions init_options;
    +        if (fuzzed_data_provider.ConsumeBool()) {
    +            const CAmount fee_amount = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, COIN);
    +            const int32_t fee_size = fuzzed_data_provider.ConsumeIntegralInRange<int32_t>(1, MAX_STANDARD_TX_WEIGHT / WITNESS_SCALE_FACTOR);
    +            init_options.block_min_fee_rate = CFeeRate(fee_amount, fee_size);
    +        }
    +        if (fuzzed_data_provider.ConsumeBool()) {
    +            init_options.print_modified_fee = fuzzed_data_provider.ConsumeBool();
    +        }
    +        if (fuzzed_data_provider.ConsumeBool()) {
    +            init_options.block_reserved_weight = fuzzed_data_provider.ConsumeIntegralInRange<uint64_t>(
    +                MINIMUM_BLOCK_RESERVED_WEIGHT, MAX_BLOCK_WEIGHT);
    +        }
    +        if (fuzzed_data_provider.ConsumeBool()) {
    +            init_options.block_max_weight = fuzzed_data_provider.ConsumeIntegralInRange<uint64_t>(
    +                MINIMUM_BLOCK_RESERVED_WEIGHT, MAX_BLOCK_WEIGHT);
    +        }
    +        // Match ReadMiningArgs(), which rejects invalid init-time options.
    +        if (!node::CheckMiningOptions(init_options, /*use_argnames=*/false)) continue;
    +
             BlockCreateOptions options;
             options.test_block_validity = use_valid_transactions;
             if (fuzzed_data_provider.ConsumeBool()) {
    @@ -207,9 +228,14 @@ FUZZ_TARGET(block_template_manager, .init = initialize_block_template_manager)
             if (!use_valid_transactions && fuzzed_data_provider.ConsumeBool()) {
                 options.coinbase_output_script = ConsumeScript(fuzzed_data_provider);
             }
    +        const auto resolved{node::FlattenMiningOptions(node::MergeMiningOptions(options, init_options))};
    +        // Per-call options must also result in a valid effective configuration.
    +        if (!node::CheckMiningOptions(resolved, /*use_argnames=*/false)) continue;
    +
    +        node::BlockTemplateManager block_template_manager{
    +            mempool, chainman, notifications, std::move(init_options)};
             auto block_template = block_template_manager.CreateNewTemplate(options);
             assert(block_template);
    -        const auto resolved{node::FlattenMiningOptions(node::MergeMiningOptions(options, block_template_manager.BlockCreateArgs()))};
             const CBlock& block{block_template->block};
             // Coinbase is first; the per-tx vectors exclude it and track the block.
             assert(!block.vtx.empty() && block.vtx[0]->IsCoinBase());
    
  43. enirox001 commented at 3:41 PM on July 18, 2026: contributor

    Code Review 1881848e0c

    Most changes here are refactors (move), it preserves the code paths that it moves into the block template manager and also the fuzz test coverage is good as well.

    Left some questions and suggestions, mostly non blocking

  44. ismaelsadeeq force-pushed on Jul 20, 2026
  45. ismaelsadeeq commented at 11:50 AM on July 20, 2026: member

    Forced pushed from 1881848e0c99e1e55f9739d2a097bfdad6e3873c to e7f0008848cf39fee548178850eff3e5abb771c9 1881848e0c...e7f0008848

    • #35675 (review) Used the same pattern as mempool and chainman but moved the assert to the same location just below them.
    • #35675 (review) Fixed.
    • #35675 (review) Fixed, note: you use the wrong commit hash and title.
    • #35675 (review) Fixed, thanks.
    • #35675 (review) The aim is to cover block assembler and the manager, using custom block create args won't add much because merely having a custom block create option gives us the same hit. But I use the diff as inspiration to extend the harness to support non-standard options and add further fuzz, so that we ensure incorrect options are checked as well, and when it throws due to that, we expect a match btw the check we do manually and the internal check done by the block assembler.
  46. Sjors commented at 12:25 PM on July 20, 2026: member

    CI is unhappy :-( @ismaelsadeeq I find it easier to follow inline comments if you (also) reply there instead of (only) in the main thread.

  47. ismaelsadeeq force-pushed on Jul 20, 2026
  48. DrahtBot added the label CI failed on Jul 20, 2026
  49. DrahtBot commented at 12:28 PM on July 20, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task tidy: https://github.com/bitcoin/bitcoin/actions/runs/29739534882/job/88342813021</sub> <sub>LLM reason (✨ experimental): CI failed because clang-tidy reported an error (misc-unused-using-decls) for an unused using node::BlockAssembler; in test/miner_tests.cpp, treated as warnings-as-errors.</sub>

    <details><summary>Hints</summary>

    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.

    </details>

  50. ismaelsadeeq commented at 12:30 PM on July 20, 2026: member

    CI is unhappy :-(

    Fixed it's a stale using declaration in the unit test, after the recent push. Fixed.

    @ismaelsadeeq I find it easier to follow inline comments if you (also) reply there instead of (only) in the main thread.

    Noted, but github u.i makes the PR review much harder after just a few comments; that's why I usually batch them with a link (I guess you are okay with a few clicks to expand :)).

  51. DrahtBot removed the label CI failed on Jul 20, 2026
  52. Sjors commented at 1:42 PM on July 20, 2026: member

    @ismaelsadeeq yes, I expand them all anyway

  53. in src/rpc/mining.cpp:909 in 4872a56210 outdated
     905 | @@ -907,15 +906,15 @@ static RPCMethod getblocktemplate()
     906 |          // a delay to each getblocktemplate call. This differs from typical
     907 |          // long-lived IPC usage, where the overhead is paid only when creating
     908 |          // the initial template.
     909 | -        block_template = miner.createNewBlock({}, /*cooldown=*/false);
     910 | +        block_template = block_template_manager.CreateNewTemplate({});
    


    Sjors commented at 2:20 PM on July 20, 2026:

    In 4872a562104fb81c6aad2a528408f492bd5f5306 rpc: build getblocktemplate via BlockTemplateManager: there's now an opportunity to avoid copies, like #32547 tried to do:

    diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp
    index a86e5dc54e..20c6ed928a 100644
    --- a/src/rpc/mining.cpp
    +++ b/src/rpc/mining.cpp
    @@ -911,5 +911,5 @@ static RPCMethod getblocktemplate()
         }
         CHECK_NONFATAL(pindexPrev);
    -    CBlock block{block_template->block};
    +    CBlock& block{block_template->block};
    
         // Update nTime
    @@ -924,6 +924,6 @@ static RPCMethod getblocktemplate()
         UniValue transactions(UniValue::VARR);
         std::map<Txid, int64_t> setTxIndex;
    -    std::vector<CAmount> tx_fees{block_template->vTxFees};
    -    std::vector<int64_t> tx_sigops{block_template->vTxSigOpsCost};
    +    const std::vector<CAmount>& tx_fees{block_template->vTxFees};
    +    const std::vector<int64_t>& tx_sigops{block_template->vTxSigOpsCost};
    
         int i = 0;
    @@ -1053,5 +1053,5 @@ static RPCMethod getblocktemplate()
         }
    
    -    if (auto coinbase{block_template->m_coinbase_tx}; coinbase.required_outputs.size() > 0) {
    +    if (const auto& coinbase{block_template->m_coinbase_tx}; coinbase.required_outputs.size() > 0) {
             CHECK_NONFATAL(coinbase.required_outputs.size() == 1); // Only one output is currently expected
             result.pushKV("default_witness_commitment", HexStr(coinbase.required_outputs[0].scriptPubKey));
    

    This can be made robust against using multiple clients (which we don't really support), per Claude:

    The versionbits signalling loop only ever clears bits based on the requesting client's rules, and nothing re-sets them until the template regenerates — so with in-place mutation, callers with differing rules within one template's lifetime can see a version shaped by an earlier caller. Copying just the header (CBlockHeader block_header{block}; and pointing UpdateTime/nVersion/curtime/bits/target at it) would make each response independent of call history at the cost of an 80-byte copy per call.


    ismaelsadeeq commented at 3:40 PM on July 20, 2026:

    Good idea, done in d36b3cd0e2...c2dde7de2c

  54. Sjors approved
  55. Sjors commented at 2:21 PM on July 20, 2026: member

    ACK d36b3cd0e2f0ccee652c5f34d6379901b02e8abf

  56. DrahtBot requested review from pablomartin4btc on Jul 20, 2026
  57. DrahtBot requested review from enirox001 on Jul 20, 2026
  58. ismaelsadeeq force-pushed on Jul 20, 2026
  59. DrahtBot added the label Needs rebase on Jul 23, 2026
  60. ismaelsadeeq force-pushed on Jul 23, 2026
  61. DrahtBot added the label CI failed on Jul 23, 2026
  62. ismaelsadeeq force-pushed on Jul 23, 2026
  63. DrahtBot removed the label Needs rebase on Jul 23, 2026
  64. ismaelsadeeq commented at 3:00 PM on July 23, 2026: member

    Rebased C. I failure in https://github.com/bitcoin/bitcoin/actions/runs/30013677040/job/89227984564?pr=35675 seems unrelated, it is a private broadcast test failure.

  65. in src/test/fuzz/block_template_manager.cpp:65 in c5864dbae4
      60 | +    g_setup = testing_setup.get();
      61 | +    SetMockTime(WITH_LOCK(g_setup->m_node.chainman->GetMutex(),
      62 | +                          return g_setup->m_node.chainman->ActiveTip()->Time()));
      63 | +    for (int i{0}; i < 2 * COINBASE_MATURITY; ++i) {
      64 | +        COutPoint prevout{MineBlock(g_setup->m_node, {
      65 | +                                                         .coinbase_output_script = P2WSH_OP_TRUE,
    


    Sjors commented at 5:18 PM on July 23, 2026:

    In c5864dbae4e525162fd075c2d986e1553459eaab test: fuzz BlockTemplateManager nit: indentation seems a bit excessive.


    ismaelsadeeq commented at 1:09 PM on July 24, 2026:

    fixed.

  66. Sjors commented at 5:20 PM on July 23, 2026: member

    Code review c5864dbae4e525162fd075c2d986e1553459eaab

    I think your rebase after #34672 reverted some of the changes from that PR.

    Other changes since my last review:

    • added 5284840786f7c440b11f2add76d03a06beb5f38a and 8faf339323d8e3dc1cd148ec1a08c973b4ab7d09 after my suggestion in #35675 (review)
  67. w0xlt commented at 10:38 PM on July 23, 2026: contributor

    LGTM, but the current code is really reverting #34672.

    <details>

    <summary>Suggestion:</summary>

    diff --git a/src/node/block_template_manager.cpp b/src/node/block_template_manager.cpp
    index 376e030a91..a4d2d03ddf 100644
    --- a/src/node/block_template_manager.cpp
    +++ b/src/node/block_template_manager.cpp
    @@ -71,7 +71,7 @@ protected:
     };
     } // namespace
     
    -bool BlockTemplateManager::SubmitBlock(const std::shared_ptr<const CBlock>& block, bool* new_block, std::string& reason, std::string& debug)
    +bool BlockTemplateManager::SubmitBlock(const std::shared_ptr<const CBlock>& block, std::string& reason, std::string& debug)
     {
         reason.clear();
         debug.clear();
    @@ -84,10 +84,13 @@ bool BlockTemplateManager::SubmitBlock(const std::shared_ptr<const CBlock>& bloc
         // results.
         auto sc = std::make_shared<SubmitBlockStateCatcher>(block->GetHash());
         CHECK_NONFATAL(m_chainman.m_options.signals)->RegisterSharedValidationInterface(sc);
    -    bool accepted = m_chainman.ProcessNewBlock(block, /*force_processing=*/true, /*min_pow_checked=*/true, /*new_block=*/new_block);
    +    bool new_block;
    +    bool accepted = m_chainman.ProcessNewBlock(block, /*force_processing=*/true, /*min_pow_checked=*/true, /*new_block=*/&new_block);
    +    // No queue drain is needed. The BlockChecked notification used above is
    +    // emitted synchronously by ProcessNewBlock, unlike most validation signals.
         CHECK_NONFATAL(m_chainman.m_options.signals)->UnregisterSharedValidationInterface(sc);
     
    -    if (new_block && !*new_block && accepted) {
    +    if (!new_block && accepted) {
             reason = "duplicate";
         } else if (!accepted && (!sc->m_found || sc->m_state.IsValid())) {
             // ProcessNewBlock can fail without a validation result, for example
    @@ -96,17 +99,16 @@ bool BlockTemplateManager::SubmitBlock(const std::shared_ptr<const CBlock>& bloc
             // inconclusive.
             reason = "inconclusive";
         } else if (!sc->m_found) {
    -        // A block can be accepted and stored without being connected, for
    -        // example if it does not have more work than the current tip. In that
    -        // case no BlockChecked callback is emitted, so the validation result is
    -        // inconclusive. Mining::submitBlock treats this as an error for mining
    -        // clients, but it does not mean the block is invalid.
    +        // The block was accepted but not connected, for example if it does not
    +        // have more work than the current tip.
             reason = "inconclusive";
         } else if (!sc->m_state.IsValid()) {
             reason = sc->m_state.GetRejectReason();
             debug = sc->m_state.GetDebugMessage();
         }
    -    return accepted;
    +    const bool result{accepted && new_block && reason.empty()};
    +    CHECK_NONFATAL(result == reason.empty());
    +    return result;
     }
     
     std::optional<BlockRef> BlockTemplateManager::GetTip()
    diff --git a/src/node/block_template_manager.h b/src/node/block_template_manager.h
    index 27a339ac7d..142665e8ad 100644
    --- a/src/node/block_template_manager.h
    +++ b/src/node/block_template_manager.h
    @@ -49,8 +49,9 @@ public:
         /** Create a fresh block template, applying init-time defaults to any unset options. */
         std::unique_ptr<CBlockTemplate> CreateNewTemplate(const BlockCreateOptions& options);
     
    -    /** Submit a block via ProcessNewBlock and capture validation state. */
    -    bool SubmitBlock(const std::shared_ptr<const CBlock>& block, bool* new_block, std::string& reason, std::string& debug);
    +    /** Submit a block via ProcessNewBlock and capture validation state.
    +     * [@return](/bitcoin-bitcoin/contributor/return/) whether the block was accepted as a new valid block. */
    +    bool SubmitBlock(const std::shared_ptr<const CBlock>& block, std::string& reason, std::string& debug);
     
         /** Locks cs_main.
          * [@return](/bitcoin-bitcoin/contributor/return/) the active chain tip, or nullopt if none exists. */
    diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp
    index f6a15aabc9..c56fd23a25 100644
    --- a/src/node/interfaces.cpp
    +++ b/src/node/interfaces.cpp
    @@ -920,9 +920,7 @@ public:
         {
             if (!coinbase) return false;
             AddMerkleRootAndCoinbase(m_block_template->block, std::move(coinbase), version, timestamp, nonce);
    -        bool new_block;
    -        const bool accepted = block_template_manager().SubmitBlock(std::make_shared<const CBlock>(m_block_template->block), &new_block, reason, debug);
    -        return accepted && new_block && reason.empty();
    +        return block_template_manager().SubmitBlock(std::make_shared<const CBlock>(m_block_template->block), reason, debug);
         }
     
         std::unique_ptr<BlockTemplate> waitNext(BlockWaitOptions options) override
    @@ -1013,13 +1011,7 @@ public:
     
         bool submitBlock(const CBlock& block_in, std::string& reason, std::string& debug) override
         {
    -        auto block = std::make_shared<const CBlock>(block_in);
    -        bool new_block;
    -        const bool accepted = block_template_manager().SubmitBlock(block, &new_block, reason, debug);
    -        // ProcessNewBlock() can accept and store a block before it is checked
    -        // for validity. Treat duplicates as errors for mining clients, and only
    -        // return success when validation completed without setting a reason.
    -        return accepted && new_block && reason.empty();
    +        return block_template_manager().SubmitBlock(std::make_shared<const CBlock>(block_in), reason, debug);
         }
     
         std::vector<CTransactionRef> getTransactionsByTxID(const std::vector<Txid>& txids) override
    diff --git a/src/test/fuzz/block_template_manager.cpp b/src/test/fuzz/block_template_manager.cpp
    index 84a11001e8..4dc3e0c853 100644
    --- a/src/test/fuzz/block_template_manager.cpp
    +++ b/src/test/fuzz/block_template_manager.cpp
    @@ -319,11 +319,10 @@ FUZZ_TARGET(block_template_manager, .init = initialize_block_template_manager)
             if (fuzzed_data_provider.ConsumeBool()) {
                 // An unsolved header can pass the regtest target by chance.
                 if (!CheckProofOfWork(block.GetHash(), block.nBits, node.chainman->GetConsensus())) {
    -                bool new_block{false};
                     std::string reason;
                     std::string debug;
    -                assert(!block_template_manager.SubmitBlock(std::make_shared<const CBlock>(block), &new_block, reason, debug));
    -                assert(!new_block && reason == "high-hash");
    +                assert(!block_template_manager.SubmitBlock(std::make_shared<const CBlock>(block), reason, debug));
    +                assert(reason == "high-hash");
                     assert(debug == "proof of work failed");
                 }
             }
    

    </details>

  68. ismaelsadeeq force-pushed on Jul 24, 2026
  69. ismaelsadeeq commented at 1:12 PM on July 24, 2026: member

    LGTM, but the current code is really reverting #34672.

    Taken, thanks.

  70. Sjors commented at 1:19 PM on July 24, 2026: member

    utACK a87d81548be53ed0af747adf5b8fd22e0d6b8289

  71. DrahtBot removed the label CI failed on Jul 24, 2026
  72. w0xlt commented at 2:12 PM on July 28, 2026: contributor

    It looks like src/wallet/test/fuzz/fees.cpp needs the same reset/recreate handling as the other fuzz targets.

    diff --git a/src/wallet/test/fuzz/fees.cpp b/src/wallet/test/fuzz/fees.cpp
    index 36c1f56b0a..2899051179 100644
    --- a/src/wallet/test/fuzz/fees.cpp
    +++ b/src/wallet/test/fuzz/fees.cpp
    @@ -2,6 +2,7 @@
     // Distributed under the MIT software license, see the accompanying
     // file COPYING or http://www.opensource.org/licenses/mit-license.php.
     
    +#include <node/block_template_manager.h>
     #include <test/fuzz/FuzzedDataProvider.h>
     #include <test/fuzz/fuzz.h>
     #include <test/fuzz/util.h>
    @@ -73,7 +74,9 @@ FUZZ_TARGET(wallet_fees, .init = initialize_setup)
             .min_relay_feerate = CFeeRate{ConsumeMoney(fuzzed_data_provider, 1'000'000)},
             .dust_relay_feerate = CFeeRate{ConsumeMoney(fuzzed_data_provider, 1'000'000)}
         };
    +    node.block_template_manager.reset();
         node.mempool = std::make_unique<CTxMemPool>(mempool_opts, error);
    +    g_setup->CreateBlockTemplateManager();
         std::unique_ptr<CBlockPolicyEstimator> fee_estimator = std::make_unique<FuzzedBlockPolicyEstimator>(fuzzed_data_provider);
         g_setup->SetFeeEstimator(std::move(fee_estimator));
         auto target_feerate{CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/1'000'000)}};
    
  73. ismaelsadeeq force-pushed on Jul 29, 2026
  74. ismaelsadeeq commented at 8:57 AM on July 29, 2026: member

    a87d81548b...ba196b7f99

    #35675#pullrequestreview-4798195437 Fixed @w0xlt , thanks

  75. DrahtBot added the label CI failed on Jul 29, 2026
  76. DrahtBot commented at 9:40 AM on July 29, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task test ancestor commits: https://github.com/bitcoin/bitcoin/actions/runs/30437535248/job/90528703747</sub> <sub>LLM reason (✨ experimental): CI failed due to a C++ compilation error in the fuzz target: BlockTemplateManager is an incomplete type used with std::unique_ptr (invalid sizeof on forward declaration).</sub>

    <details><summary>Hints</summary>

    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.

    </details>

  77. ismaelsadeeq force-pushed on Jul 29, 2026
  78. ismaelsadeeq commented at 11:23 AM on July 29, 2026: member

    C.I failure seems unrelated.

  79. DrahtBot removed the label CI failed on Jul 29, 2026
  80. w0xlt commented at 10:28 PM on July 29, 2026: contributor

    ACK dad787680358adeba552802c152343a7d9ddfbd0

    That is the code in ChainTestingSetup::CreateBlockTemplateManager I was referring to earlier:

    m_node.block_template_manager = std::make_unique<node::BlockTemplateManager>(*m_node.mempool, *m_node.chainman, *Assert(m_node.notifications), std::move(*mining_args));
    

    Although unused here, replacing node.mempool would leave BlockTemplateManager holding a dangling reference in the previous code.

  81. DrahtBot requested review from Sjors on Jul 29, 2026
  82. in src/rpc/mining.cpp:502 in dad7876803 outdated
     498 | @@ -502,7 +499,7 @@ static RPCMethod getmininginfo()
     499 |      obj.pushKV("target", GetTarget(tip, chainman.GetConsensus().powLimit).GetHex());
     500 |      obj.pushKV("networkhashps",    getnetworkhashps().HandleRequest(request));
     501 |      obj.pushKV("pooledtx", mempool.size());
     502 | -    const auto mining_options{node::FlattenMiningOptions(node.mining_args)};
     503 | +    const auto mining_options{node::FlattenMiningOptions(EnsureBlockTemplateManager(node).BlockCreateArgs())};
    


    pablomartin4btc commented at 3:57 AM on July 30, 2026:

    getmininginfo now calls EnsureBlockTemplateManager() which throws if called before chainstate is loaded (RPC server doesn't accept connections before chainstate is loaded — unreachable in practice), whereas previously it silently returned zero/ default-initialized mining args. I think this is stricter and more correct.

  83. in src/test/fuzz/block_template_manager.cpp:320 in dad7876803 outdated
     315 | +            assert(block_template_manager.CooldownIfHeadersAhead(*block_template_manager.GetTip(), interrupt));
     316 | +            assert(interrupt == interrupted);
     317 | +        }
     318 | +        if (fuzzed_data_provider.ConsumeBool()) {
     319 | +            // An unsolved header can pass the regtest target by chance.
     320 | +            if (!CheckProofOfWork(block.GetHash(), block.nBits, node.chainman->GetConsensus())) {
    


    pablomartin4btc commented at 4:07 AM on July 30, 2026:

    nit (not blocking): If PoW passes by chance, it skips entirely. So the reason == "duplicate", reason == "inconclusive", and result == true paths are never exercised. Perhaps this path can be added:

      // Test SubmitBlock success/ duplicate path with a solved block
      if (use_valid_transactions && fuzzed_data_provider.ConsumeBool()) {
          CBlock solved = block_template->block;
          while (!CheckProofOfWork(solved.GetHash(), solved.nBits, node.chainman->GetConsensus()))
              ++solved.nNonce;
          std::string reason, debug;
          block_template_manager.SubmitBlock(std::make_shared<const CBlock>(solved), reason, debug);
          assert(reason != "high-hash"); // PoW is valid so never a pow failure
      }
    
  84. pablomartin4btc commented at 4:13 AM on July 30, 2026: member

    ACK dad787680358

    Notable changes since my previous ACK at 82dc581a:

    • @Sjors' copy-avoidance suggestions landed in two dedicated commits and a #34672 rebase regression was caught and fixed;
    • @w0xlt caught a missing reset/ recreate in wallet/test/fuzz/fees.cpp;
    • @enirox001's fuzz coverage suggestions were taken and the harness extended.

    Left a couple of comments/ nits below.

  85. DrahtBot added the label Needs rebase on Aug 4, 2026
  86. node: introduce BlockTemplateManager
    Add BlockTemplateManager, a wrapper around
    BlockAssembler::CreateNewBlock(), and store it in NodeContext.
    
    Wire it into node init and test setup so it is reset before its
    mempool/chainman dependencies, and update tests and fuzz setups that
    rebuild chainman or mempool. Add a unit test that verifies a block
    template can be created through the manager.
    3459ac4f24
  87. node: move mining_args to BlockTemplateManager
    Pass the parsed mining args to BlockTemplateManager at construction
    and expose them via BlockCreateArgs(), so the manager owns the
    init-time block create options instead of NodeContext.
    c140c649c6
  88. miner: move SubmitBlock into BlockTemplateManager
    Move SubmitBlockStateCatcher and SubmitBlock from miner.cpp into
    BlockTemplateManager as a member function. This groups block submission
    with block creation in the same class. The function uses m_chainman
    directly instead of taking it as a parameter.
    75c92a5ae9
  89. node: move tip and wait helpers into BlockTemplateManager
    Move the mining tip lookup and block-template waiting helpers (GetTip,
    WaitTipChanged, WaitAndCreateNewBlock, InterruptWait,
    CooldownIfHeadersAhead) into BlockTemplateManager so the manager owns
    the template waiting flow. The manager now takes KernelNotifications
    at construction.
    88bc1e84cc
  90. interfaces: create block template via BlockTemplateManager
    Route the Mining interface's createNewBlock() through
    BlockTemplateManager::CreateNewTemplate() instead of constructing a
    BlockAssembler directly. Merging the init-time defaults into unset
    options now happens inside CreateNewTemplate(), so every caller gets
    them applied.
    18e882ba3c
  91. rpc: route getblocktemplate internals through node
    Add EnsureBlockTemplateManager() and use it in getblocktemplate for tip
    lookup and longpoll waiting, while using ChainstateManager directly for
    test-chain and IBD checks.
    238926ecc3
  92. rpc: build getblocktemplate via BlockTemplateManager b7857bd7ec
  93. rpc: do not copy template data in getblocktemplate
    The transactions, fees and sigops costs, and the coinbase outputs
    are only read, so reference them from the template instead of copying
    them out of it.
    56f6a62b51
  94. rpc: only copy the header in getblocktemplate
    The block copy exists solely to apply header adjustments (time, nonce,
    version bits) that must not mutate the cached template. Now that the
    transactions are read directly from the template, copy only the header
    instead of the entire block.
    a05716da55
  95. rpc: build generation templates via BlockTemplateManager
    generateblock is no longer safe for fuzzing: it previously threw at
    EnsureMining (node.mining is never set in the fuzz setup), but with
    EnsureBlockTemplateManager it now executes, mining and submitting a
    real block. Submission mutates the chain state shared across fuzz
    iterations and writes the block to disk.
    9f231d03e2
  96. rpc: wait for tips via BlockTemplateManager
    The waitforblock, waitforblockheight and waitfornewblock RPCs are no
    longer safe for fuzzing: they previously threw at EnsureMining
    (node.mining is never set in the fuzz setup), but with
    EnsureBlockTemplateManager they now really wait. The tip never changes
    during fuzzing and nothing interrupts the wait, so a call without a
    timeout blocks forever.
    36c1e0db2a
  97. test: create templates via BlockTemplateManager 45fbadf67b
  98. node: remove NodeContext::mining and EnsureMining fdb4d982e7
  99. ci: enforce iwyu for block template manager 3ea3f5e8b4
  100. test: fuzz BlockTemplateManager af294d84f5
  101. ismaelsadeeq force-pushed on Aug 5, 2026
  102. ismaelsadeeq commented at 12:46 PM on August 5, 2026: member

    Rebased to fix the merge conflict in iwyu. @marcofleon, friendly review ping on the fuzz harness added here.

  103. DrahtBot removed the label Needs rebase on Aug 5, 2026
  104. DrahtBot added the label Needs rebase on Aug 7, 2026
  105. DrahtBot commented at 9:25 AM on August 7, 2026: contributor

    <!--cf906140f33d8803c4a75a2196329ecb-->

    🐙 This pull request conflicts with the target branch and needs rebase.


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-08-11 09:51 UTC

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