mining: move getblocktemplate cache into BlockTemplateManager #36356

pull w0xlt wants to merge 3 commits into bitcoin:master from w0xlt:block-template-manager-gbt-cache changing 4 files +137 −32
  1. w0xlt commented at 8:24 AM on September 27, 2026: contributor

    Follow-up to #35675. This addresses the getblocktemplate part of the "Remove remaining global states" TODO in #33758, as suggested in #35675#pullrequestreview-5299625960.

    getblocktemplate reuses its last template until the tip changes, or until the mempool changes and the template is more than 5 seconds old. That cache currently lives in four function-local statics, which:

    • live as long as the process, so the cached CBlockIndex* can outlive its ChainstateManager (as in unit tests);
    • rely on cs_main being held, which nothing checks;
    • can't be unit tested.

    This PR moves the cache into BlockTemplateManager, in three commits:

    1. rpc: refactors the statics in place into a CachedBlockTemplate struct and a GetNewOrCachedBlockTemplate() helper, GUARDED_BY(::cs_main). The template's hashPrevBlock replaces the cached CBlockIndex*, and the 5 seconds becomes GBT_TEMPLATE_REBUILD_INTERVAL.
    2. refactor: moves them into BlockTemplateManager. It's easiest to review with git show --color-moved=dimmed-zebra --color-moved-ws=allow-indentation-change.
    3. test: adds a unit test for each rebuild rule and for the retry after a failed build.

    No behavior change is intended, and only getblocktemplate uses the cache. It uses cs_main rather than its own mutex, since the RPC already holds it and building a template takes it anyway.

  2. DrahtBot added the label Mining on Sep 27, 2026
  3. DrahtBot commented at 8:25 AM on September 27, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    Type Reviewers
    Concept ACK ismaelsadeeq

    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:

    • #36469 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36469.svg"></sub> (refactor: deduplicate block validation state catcher by enirox001)
    • #36451 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36451.svg"></sub> (refactor: Use NotNull for CreateNewBlock and pimpls by maflcko)
    • #35671 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35671.svg"></sub> (mining: add TxCollection to bandwidth-efficiently validate external block templates by Sjors)
    • #35569 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35569.svg"></sub> (Encapsulation for CTransaction by purpleKarrot)
    • #33922 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/33922.svg"></sub> (mining: add getMemoryLoad() and track template non-mempool memory footprint by Sjors)

    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. Sjors commented at 10:08 AM on October 7, 2026: member

    I plan to review this.

  5. ismaelsadeeq commented at 10:38 AM on October 8, 2026: member

    Concept ACK

    I think we can implement the broader cache as attempted here #33421, mempool estimator dependency on ChainStateManager will be gone, and the ipc createnewblock, waitnext and getblocktemplate RPC can use it too. That way we have shared code for all three?

    This has a bit of an advantage when the 7 sec for the mempool fee rate estimator elapses and no mempool change, we wont rebuilt; same for the waitnext too. We can later extend to use actual top block inflow to decide not just mempool transaction list number change, that may not affect the top block.

    <details> <summary>see POC</summary>

    diff --git a/src/init.cpp b/src/init.cpp
    index 3632a210a4..a7ab529a52 100644
    --- a/src/init.cpp
    +++ b/src/init.cpp
    @@ -1956,7 +1956,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
                 return InitError(strprintf(_("acceptstalefeeestimates is not supported on %s chain."), chainparams.GetChainTypeString()));
             }
             MaybeMigrateLegacyFeeEstimates(args);
    -        node.fee_estimator_man = std::make_unique<FeeRateEstimatorManager>(BlockPolicyFeeEstPath(args), read_stale_estimates, MempoolPolicyEstimatorPath(args), *Assert(node.mempool), chainman);
    +        node.fee_estimator_man = std::make_unique<FeeRateEstimatorManager>(BlockPolicyFeeEstPath(args), read_stale_estimates, MempoolPolicyEstimatorPath(args), *Assert(node.mempool), *Assert(node.block_template_manager));
    
             // Flush estimates to disk periodically
             FeeRateEstimatorManager* fee_estimator_man = node.fee_estimator_man.get();
    diff --git a/src/node/block_template_manager.cpp b/src/node/block_template_manager.cpp
    index 34f864c99a..d9e2c3d8da 100644
    --- a/src/node/block_template_manager.cpp
    +++ b/src/node/block_template_manager.cpp
    @@ -50,34 +50,84 @@ std::unique_ptr<CBlockTemplate> BlockTemplateManager::CreateNewTemplate(const Bl
         }.CreateNewBlock();
     }
    
    -unsigned int BlockTemplateManager::GetCachedTransactionsUpdated() const
    +static constexpr size_t MAX_CACHED_TEMPLATES{3};
    +
    +unsigned int BlockTemplateManager::GetCachedTransactionsUpdated(const BlockCreateOptions& options) const
     {
    -    AssertLockHeld(::cs_main);
    -    return m_cached_template.transactions_updated;
    +    LOCK(m_mutex);
    +    for (const auto& cache : m_cached_templates) {
    +        if (cache.options == options) return cache.transactions_updated;
    +    }
    +    return 0;
    +}
    +
    +static bool BuildsOnTip(const CachedBlockTemplate& cache, const uint256& tip)
    +{
    +    return cache.block_template && cache.block_template->block.hashPrevBlock == tip;
    +}
    +
    +static bool MempoolChanged(unsigned int transactions_updated, const CTxMemPool& mempool)
    +{
    +    return mempool.GetTransactionsUpdated() != transactions_updated;
    +}
    +
    +static bool TooOld(NodeSeconds time_start, std::optional<std::chrono::seconds> max_age)
    +{
    +    return max_age && Now<NodeSeconds>() - time_start > *max_age;
    +}
    +
    +static bool Stale(const CachedBlockTemplate& cache, const CTxMemPool& mempool, std::optional<std::chrono::seconds> max_age)
    +{
    +    return TooOld(cache.time_start, max_age) && MempoolChanged(cache.transactions_updated, mempool);
     }
    
    -CachedBlockTemplate BlockTemplateManager::RefreshCachedTemplate()
    +static std::vector<CachedBlockTemplate>::iterator FindCached(std::vector<CachedBlockTemplate>& cached_templates, const BlockCreateOptions& options)
    +{
    +    return std::find_if(cached_templates.begin(), cached_templates.end(),
    +                        [&](const CachedBlockTemplate& cached_template) { return cached_template.options == options; });
    +}
    +
    +CachedBlockTemplate BlockTemplateManager::BuildCachedTemplate(const BlockCreateOptions& options)
     {
         AssertLockHeld(::cs_main);
    -    CachedBlockTemplate& cache{m_cached_template};
    +    CachedBlockTemplate cache;
    +    cache.options = options;
    +
    +    // Sample the mempool counter before building, so that a change made
    +    // while CreateNewTemplate runs is seen as a change on the next call
    +    cache.transactions_updated = m_mempool.GetTransactionsUpdated();
    +    cache.time_start = Now<NodeSeconds>();
    +    cache.block_template = CreateNewTemplate(options);
    +    return cache;
    +}
    +
    +CachedBlockTemplate& BlockTemplateManager::StoreCachedTemplate(std::vector<CachedBlockTemplate>::iterator existing, CachedBlockTemplate&& rebuilt)
    +{
    +    AssertLockHeld(m_mutex);
    +    if (existing != m_cached_templates.end()) return *existing = std::move(rebuilt);
    +    if (m_cached_templates.size() >= MAX_CACHED_TEMPLATES) m_cached_templates.erase(m_cached_templates.begin());
    +    return m_cached_templates.emplace_back(std::move(rebuilt));
    +}
    +
    +CachedBlockTemplate BlockTemplateManager::GetBlockTemplate(std::optional<std::chrono::seconds> max_age, const BlockCreateOptions& options)
    +{
    +    // Template assembly reads the chainstate under cs_main; the cache is guarded
    +    // by m_mutex, which ranks below cs_main in the lock order.
    +    LOCK2(::cs_main, m_mutex);
         const uint256 tip{CHECK_NONFATAL(m_chainman.ActiveChain().Tip())->GetBlockHash()};
    -    if (!cache.block_template || cache.block_template->block.hashPrevBlock != tip ||
    -        (m_mempool.GetTransactionsUpdated() != cache.transactions_updated && Now<NodeSeconds>() - cache.time_start > 5s)) {
    -        // Clear the template so future calls make a new block, despite any failures from here on
    -        cache.block_template.reset();
    -
    -        // Sample the mempool counter before building, so that a change made
    -        // while CreateNewTemplate runs is seen as a change on the next call
    -        cache.transactions_updated = m_mempool.GetTransactionsUpdated();
    -        cache.time_start = Now<NodeSeconds>();
    -
    -        // Create new block. Opt-out of cooldown mechanism, because it would add
    -        // a delay to each getblocktemplate call. This differs from typical
    -        // long-lived IPC usage, where the overhead is paid only when creating
    -        // the initial template.
    -        cache.block_template = CreateNewTemplate({});
    +    const auto cached{FindCached(m_cached_templates, options)};
    +    if (cached != m_cached_templates.end() && BuildsOnTip(*cached, tip) && !Stale(*cached, m_mempool, max_age)) {
    +        return *cached;
         }
    -    return cache;
    +    // Build before touching the cache, so a failed build leaves it unchanged.
    +    return StoreCachedTemplate(cached, BuildCachedTemplate(options));
    +}
    +
    +CachedBlockTemplate BlockTemplateManager::RefreshCachedTemplate(const BlockCreateOptions& options)
    +{
    +    LOCK2(::cs_main, m_mutex);
    +    const auto existing{FindCached(m_cached_templates, options)};
    +    return StoreCachedTemplate(existing, BuildCachedTemplate(options));
     }
    
     namespace {
    @@ -219,7 +269,9 @@ std::unique_ptr<CBlockTemplate> BlockTemplateManager::WaitAndCreateNewBlock(
              * We'll also create a new template if the tip changed during this iteration.
              */
             if (wait_options.fee_threshold < MAX_MONEY || tip_changed) {
    -            auto new_tmpl{CreateNewTemplate(create_options)};
    +            // Fetch via the shared cache (rebuilt whenever the mempool changed),
    +            // then copy into an owned, mutable template.
    +            auto new_tmpl{std::make_unique<CBlockTemplate>(*GetBlockTemplate(/*max_age=*/std::chrono::seconds{0}, create_options).block_template)};
    
                 // If the tip changed, return the new template regardless of its fees.
                 if (tip_changed) return new_tmpl;
    diff --git a/src/node/block_template_manager.h b/src/node/block_template_manager.h
    index 3fbd52d7dd..9de3d19d74 100644
    
    ismaelsadeeq in 🌐 sadiq-server in bitcoin on ī‚  pr-36356 [$!?] via ā–³ v3.31.5 via šŸ v3.12.9 via ā„  impure (nix-shell-env) took 1m34s
    āÆ nvim src/node/block_template_manager.cpp
    
    ismaelsadeeq in 🌐 sadiq-server in bitcoin on ī‚  pr-36356 [$!?] via ā–³ v3.31.5 via šŸ v3.12.9 via ā„  impure (nix-shell-env) took 13m36s
    āÆ git diff
    diff --git a/src/init.cpp b/src/init.cpp
    index 3632a210a4..a7ab529a52 100644
    --- a/src/init.cpp
    +++ b/src/init.cpp
    @@ -1956,7 +1956,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
                 return InitError(strprintf(_("acceptstalefeeestimates is not supported on %s chain."), chainparams.GetChainTypeString()));
             }
             MaybeMigrateLegacyFeeEstimates(args);
    -        node.fee_estimator_man = std::make_unique<FeeRateEstimatorManager>(BlockPolicyFeeEstPath(args), read_stale_estimates, MempoolPolicyEstimatorPath(args), *Assert(node.mempool), chainman);
    +        node.fee_estimator_man = std::make_unique<FeeRateEstimatorManager>(BlockPolicyFeeEstPath(args), read_stale_estimates, MempoolPolicyEstimatorPath(args), *Assert(node.mempool), *Assert(node.block_template_manager));
    
             // Flush estimates to disk periodically
             FeeRateEstimatorManager* fee_estimator_man = node.fee_estimator_man.get();
    diff --git a/src/node/block_template_manager.cpp b/src/node/block_template_manager.cpp
    index 34f864c99a..7266ba3e7c 100644
    --- a/src/node/block_template_manager.cpp
    +++ b/src/node/block_template_manager.cpp
    @@ -50,36 +50,75 @@ std::unique_ptr<CBlockTemplate> BlockTemplateManager::CreateNewTemplate(const Bl
         }.CreateNewBlock();
     }
    
    -unsigned int BlockTemplateManager::GetCachedTransactionsUpdated() const
    +static constexpr size_t MAX_CACHED_TEMPLATES{3};
    +
    +unsigned int BlockTemplateManager::GetCachedTransactionsUpdated(const BlockCreateOptions& options) const
     {
    -    AssertLockHeld(::cs_main);
    -    return m_cached_template.transactions_updated;
    +    LOCK(m_mutex);
    +    for (const auto& cache : m_cached_templates) {
    +        if (cache.options == options) return cache.transactions_updated;
    +    }
    +    return 0;
    +}
    +
    +static bool BuildsOnTip(const CachedBlockTemplate& cache, const uint256& tip)
    +{
    +    return cache.block_template && cache.block_template->block.hashPrevBlock == tip;
    +}
    +
    +static bool MempoolChanged(unsigned int transactions_updated, const CTxMemPool& mempool)
    +{
    +    return mempool.GetTransactionsUpdated() != transactions_updated;
     }
    
    -CachedBlockTemplate BlockTemplateManager::RefreshCachedTemplate()
    +static bool TooOld(NodeSeconds time_start, std::optional<std::chrono::seconds> max_age)
    +{
    +    return max_age && Now<NodeSeconds>() - time_start > *max_age;
    +}
    +
    +static bool Stale(const CachedBlockTemplate& cache, const CTxMemPool& mempool, std::optional<std::chrono::seconds> max_age)
    +{
    +    return TooOld(cache.time_start, max_age) && MempoolChanged(cache.transactions_updated, mempool);
    +}
    +
    +static std::vector<CachedBlockTemplate>::iterator FindCached(std::vector<CachedBlockTemplate>& cached_templates, const BlockCreateOptions& options)
    +{
    +    return std::find_if(cached_templates.begin(), cached_templates.end(),
    +                        [&](const CachedBlockTemplate& cached_template) { return cached_template.options == options; });
    +}
    +
    +CachedBlockTemplate BlockTemplateManager::BuildCachedTemplate(const BlockCreateOptions& options)
     {
         AssertLockHeld(::cs_main);
    -    CachedBlockTemplate& cache{m_cached_template};
    -    const uint256 tip{CHECK_NONFATAL(m_chainman.ActiveChain().Tip())->GetBlockHash()};
    -    if (!cache.block_template || cache.block_template->block.hashPrevBlock != tip ||
    -        (m_mempool.GetTransactionsUpdated() != cache.transactions_updated && Now<NodeSeconds>() - cache.time_start > 5s)) {
    -        // Clear the template so future calls make a new block, despite any failures from here on
    -        cache.block_template.reset();
    -
    -        // Sample the mempool counter before building, so that a change made
    -        // while CreateNewTemplate runs is seen as a change on the next call
    -        cache.transactions_updated = m_mempool.GetTransactionsUpdated();
    -        cache.time_start = Now<NodeSeconds>();
    -
    -        // Create new block. Opt-out of cooldown mechanism, because it would add
    -        // a delay to each getblocktemplate call. This differs from typical
    -        // long-lived IPC usage, where the overhead is paid only when creating
    -        // the initial template.
    -        cache.block_template = CreateNewTemplate({});
    -    }
    +    CachedBlockTemplate cache;
    +    cache.options = options;
    +    // Sample the mempool counter before building, so that a change made
    +    // while CreateNewTemplate runs is seen as a change on the next call
    +    cache.transactions_updated = m_mempool.GetTransactionsUpdated();
    +    cache.time_start = Now<NodeSeconds>();
    +    cache.block_template = CreateNewTemplate(options);
         return cache;
     }
    
    +CachedBlockTemplate& BlockTemplateManager::StoreCachedTemplate(std::vector<CachedBlockTemplate>::iterator existing, CachedBlockTemplate&& rebuilt)
    +{
    +    AssertLockHeld(m_mutex);
    +    if (existing != m_cached_templates.end()) return *existing = std::move(rebuilt);
    +    if (m_cached_templates.size() >= MAX_CACHED_TEMPLATES) m_cached_templates.erase(m_cached_templates.begin());
    +    return m_cached_templates.emplace_back(std::move(rebuilt));
    +}
    +
    +CachedBlockTemplate BlockTemplateManager::GetBlockTemplate(std::optional<std::chrono::seconds> max_age, const BlockCreateOptions& options, bool overwrite)
    +{
    +    LOCK2(::cs_main, m_mutex);
    +    const uint256 tip{CHECK_NONFATAL(m_chainman.ActiveChain().Tip())->GetBlockHash()};
    +    const auto cached{FindCached(m_cached_templates, options)};
    +    const bool reusable{cached != m_cached_templates.end() && BuildsOnTip(*cached, tip) && !Stale(*cached, m_mempool, max_age)};
    +    if (reusable && !overwrite) return *cached;
    +    // Build before touching the cache, so a failed build leaves it unchanged.
    +    return StoreCachedTemplate(cached, BuildCachedTemplate(options));
    +}
    +
     namespace {
     class SubmitBlockStateCatcher final : public CValidationInterface
     {
    @@ -219,7 +258,9 @@ std::unique_ptr<CBlockTemplate> BlockTemplateManager::WaitAndCreateNewBlock(
              * We'll also create a new template if the tip changed during this iteration.
              */
             if (wait_options.fee_threshold < MAX_MONEY || tip_changed) {
    -            auto new_tmpl{CreateNewTemplate(create_options)};
    +            // Fetch via the shared cache (rebuilt whenever the mempool changed),
    +            // then copy into an owned, mutable template.
    +            auto new_tmpl{std::make_unique<CBlockTemplate>(*GetBlockTemplate(/*max_age=*/std::chrono::seconds{0}, create_options).block_template)};
    
                 // If the tip changed, return the new template regardless of its fees.
                 if (tip_changed) return new_tmpl;
    diff --git a/src/node/block_template_manager.h b/src/node/block_template_manager.h
    index 3fbd52d7dd..1a6acc218f 100644
    --- a/src/node/block_template_manager.h
    +++ b/src/node/block_template_manager.h
    @@ -7,12 +7,14 @@
    
     #include <kernel/cs_main.h>
     #include <node/mining_types.h>
    +#include <sync.h>
     #include <threadsafety.h>
     #include <util/time.h>
    
     #include <memory>
     #include <optional>
     #include <string>
    +#include <vector>
    
     class CBlock;
     class ChainstateManager;
    @@ -27,8 +29,10 @@ namespace node {
     class KernelNotifications;
     struct CBlockTemplate;
    
    -/** Block template shared by getblocktemplate calls, and the state of its last build attempt. */
    +/** A cached block template, keyed by its build options, and the state of its last build attempt. */
     struct CachedBlockTemplate {
    +    //! Options the template was built with; entries are keyed on these.
    +    BlockCreateOptions options{};
         //! CTxMemPool::GetTransactionsUpdated() sampled before the last build attempt.
         unsigned int transactions_updated{0};
         //! Mockable time of the last build attempt.
    @@ -39,8 +43,8 @@ struct CachedBlockTemplate {
    
     /**
      * Creates block templates, submits solved blocks, and provides tip-waiting
    - * helpers for mining code. Owns the init-time block creation args and the
    - * template cached for getblocktemplate.
    + * helpers for mining code. Owns the init-time block creation args and caches
    + * block templates keyed by their build options.
      */
     class BlockTemplateManager
     {
    @@ -49,7 +53,17 @@ private:
         ChainstateManager& m_chainman;
         KernelNotifications& m_notifications;
         const BlockCreateOptions m_block_create_args;
    -    CachedBlockTemplate m_cached_template GUARDED_BY(::cs_main);
    +    //! Guards the template cache.
    +    mutable Mutex m_mutex;
    +    //! Templates cached per distinct build options, evicted oldest-first (FIFO).
    +    std::vector<CachedBlockTemplate> m_cached_templates GUARDED_BY(m_mutex);
    +
    +    //! Build a fresh template for [@p](/github-metadata-backup-bitcoin-bitcoin/contributor/p/) options. Does not touch the cache.
    +    CachedBlockTemplate BuildCachedTemplate(const BlockCreateOptions& options) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
    +
    +    //! Store [@p](/github-metadata-backup-bitcoin-bitcoin/contributor/p/) rebuilt at [@p](/github-metadata-backup-bitcoin-bitcoin/contributor/p/) existing (end() to insert a new entry, evicting the
    +    //! oldest when full). [@return](/github-metadata-backup-bitcoin-bitcoin/contributor/return/) a reference to the stored entry.
    +    CachedBlockTemplate& StoreCachedTemplate(std::vector<CachedBlockTemplate>::iterator existing, CachedBlockTemplate&& rebuilt) EXCLUSIVE_LOCKS_REQUIRED(m_mutex);
    
     public:
         explicit BlockTemplateManager(CTxMemPool& mempool,
    @@ -63,18 +77,23 @@ public:
         /** Create a fresh block template, applying init-time defaults to any unset options. */
         std::unique_ptr<CBlockTemplate> CreateNewTemplate(const BlockCreateOptions& options);
    
    -    /** [@return](/github-metadata-backup-bitcoin-bitcoin/contributor/return/) CTxMemPool::GetTransactionsUpdated() as sampled before the last
    -     *  getblocktemplate template build attempt, for longpolling. */
    -    unsigned int GetCachedTransactionsUpdated() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
    -
    -    /** Rebuild the cached getblocktemplate template with init-time options if
    -     *  none is cached, if it does not build on the active chain tip, or if the
    -     *  mempool changed and the template is more than 5 seconds old.
    -     * [@return](/github-metadata-backup-bitcoin-bitcoin/contributor/return/) the cached template, rebuilt if needed. Its block_template is
    -     *  never null.
    -     * [@throws](/github-metadata-backup-bitcoin-bitcoin/contributor/throws/) std::runtime_error if building the template fails. The cache is
    -     *  then left without a template, so the next call tries again. */
    -    CachedBlockTemplate RefreshCachedTemplate() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
    +    /** [@return](/github-metadata-backup-bitcoin-bitcoin/contributor/return/) CTxMemPool::GetTransactionsUpdated() sampled before the last build
    +     *  attempt of the template cached for [@p](/github-metadata-backup-bitcoin-bitcoin/contributor/p/) options, or 0 if none is cached. */
    +    unsigned int GetCachedTransactionsUpdated(const BlockCreateOptions& options) const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
    +
    +    /** Return a cached block template built with [@p](/github-metadata-backup-bitcoin-bitcoin/contributor/p/) options, rebuilding it if none
    +     *  is cached for those options or if the cached one does not build on the
    +     *  active chain tip. A template that still builds on the tip is rebuilt once it
    +     *  is older than [@p](/github-metadata-backup-bitcoin-bitcoin/contributor/p/) max_age and the mempool has changed since it was built. A
    +     *  std::nullopt [@p](/github-metadata-backup-bitcoin-bitcoin/contributor/p/) max_age disables age-based rebuilds, reusing the template
    +     *  until the tip changes. Set [@p](/github-metadata-backup-bitcoin-bitcoin/contributor/p/) overwrite to always rebuild and replace the
    +     *  cached entry, ignoring [@p](/github-metadata-backup-bitcoin-bitcoin/contributor/p/) max_age. Templates are cached per distinct
    +     * [@p](/github-metadata-backup-bitcoin-bitcoin/contributor/p/) options (bounded, oldest evicted first); callers passing equal options
    +     *  share an entry.
    +     * [@return](/github-metadata-backup-bitcoin-bitcoin/contributor/return/) the cached template; its block_template is never null.
    +     * [@throws](/github-metadata-backup-bitcoin-bitcoin/contributor/throws/) std::runtime_error if building the template fails, leaving the
    +     *  cache unchanged; the next call retries. */
    +    CachedBlockTemplate GetBlockTemplate(std::optional<std::chrono::seconds> max_age, const BlockCreateOptions& options = {}, bool overwrite = false) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
    
         /** Submit a block via ProcessNewBlock and capture validation state.
          * [@return](/github-metadata-backup-bitcoin-bitcoin/contributor/return/) whether the block was accepted as a new valid block. */
    @@ -126,7 +145,7 @@ public:
             const std::unique_ptr<CBlockTemplate>& block_template,
             const BlockWaitOptions& wait_options,
             const BlockCreateOptions& create_options,
    -        bool& interrupt_wait);
    +        bool& interrupt_wait) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
     };
     } // namespace node
    
    diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp
    index 6ed03b0a69..054ff32071 100644
    --- a/src/node/interfaces.cpp
    +++ b/src/node/interfaces.cpp
    @@ -993,7 +993,10 @@ public:
                 // Also wait during the final catch-up moments after IBD.
                 if (!block_template_manager().CooldownIfHeadersAhead(*maybe_tip, m_interrupt_mining)) return {};
             }
    -        auto new_template = block_template_manager().CreateNewTemplate(options);
    +        // Build a fresh template (also caching it for waitNext) and copy it into an
    +        // owned, mutable instance, since submitSolution mutates it.
    +        const auto cached{block_template_manager().GetBlockTemplate(/*max_age=*/std::nullopt, options, /*overwrite=*/true).block_template};
    +        auto new_template{std::make_unique<CBlockTemplate>(*cached)};
             return std::make_unique<BlockTemplateImpl>(options, std::move(new_template), m_node);
         }
    
    diff --git a/src/node/mining_types.h b/src/node/mining_types.h
    index 6e4eb82aa9..121cf762b6 100644
    --- a/src/node/mining_types.h
    +++ b/src/node/mining_types.h
    @@ -89,6 +89,8 @@ struct BlockCreateOptions {
          * Should only be disabled for tests / benchmarks.
          */
         bool test_block_validity{true};
    +
    +    friend bool operator==(const BlockCreateOptions&, const BlockCreateOptions&) = default;
     };
    
     struct BlockWaitOptions {
    diff --git a/src/policy/fees/estimator_man.cpp b/src/policy/fees/estimator_man.cpp
    index 498e0580fd..a3b4ca82dd 100644
    --- a/src/policy/fees/estimator_man.cpp
    +++ b/src/policy/fees/estimator_man.cpp
    @@ -16,9 +16,9 @@ FeeRateEstimatorManager::FeeRateEstimatorManager(const fs::path& block_policy_pa
                                                      bool read_stale_estimates,
                                                      const fs::path& mempool_estimator_path,
                                                      const CTxMemPool& mempool,
    -                                                 ChainstateManager& chainman)
    +                                                 node::BlockTemplateManager& block_template_manager)
         : m_block_policy_estimator(std::make_unique<CBlockPolicyEstimator>(block_policy_path, read_stale_estimates)),
    -      m_mempool_estimator(std::make_unique<MemPoolFeeRateEstimator>(mempool_estimator_path, mempool, chainman))
    +      m_mempool_estimator(std::make_unique<MemPoolFeeRateEstimator>(mempool_estimator_path, mempool, block_template_manager))
     {
     }
    
    diff --git a/src/policy/fees/estimator_man.h b/src/policy/fees/estimator_man.h
    index 6950414ac1..94169299ea 100644
    --- a/src/policy/fees/estimator_man.h
    +++ b/src/policy/fees/estimator_man.h
    @@ -17,10 +17,13 @@
     #include <memory>
    
     class CFeeRate;
    -class ChainstateManager;
     class CTxMemPool;
     class MemPoolFeeRateEstimator;
    
    +namespace node {
    +class BlockTemplateManager;
    +} // namespace node
    +
     // How often to flush data to disk
     inline constexpr std::chrono::hours FEE_FLUSH_INTERVAL{1};
    
    @@ -35,13 +38,13 @@ public:
          * [@param](/github-metadata-backup-bitcoin-bitcoin/contributor/param/)[in] read_stale_estimates Whether to load stale estimates from disk.
          * [@param](/github-metadata-backup-bitcoin-bitcoin/contributor/param/)[in] mempool_estimator_path Path to the mempool policy estimator data file.
          * [@param](/github-metadata-backup-bitcoin-bitcoin/contributor/param/)[in] mempool              The mempool to use for the mempool fee rate estimator.
    -     * [@param](/github-metadata-backup-bitcoin-bitcoin/contributor/param/)[in] chainman             The chainstate manager.
    +     * [@param](/github-metadata-backup-bitcoin-bitcoin/contributor/param/)[in] block_template_manager The block template manager.
          */
         FeeRateEstimatorManager(const fs::path& block_policy_path,
                                 bool read_stale_estimates,
                                 const fs::path& mempool_estimator_path,
                                 const CTxMemPool& mempool,
    -                            ChainstateManager& chainman);
    +                            node::BlockTemplateManager& block_template_manager);
    
         virtual ~FeeRateEstimatorManager();
    
    diff --git a/src/policy/fees/mempool_estimator.cpp b/src/policy/fees/mempool_estimator.cpp
    index 5f384925fd..560670f3aa 100644
    --- a/src/policy/fees/mempool_estimator.cpp
    +++ b/src/policy/fees/mempool_estimator.cpp
    @@ -4,7 +4,9 @@
    
     #include <policy/fees/mempool_estimator.h>
    
    +#include <interfaces/types.h>
     #include <logging.h>
    +#include <node/block_template_manager.h>
     #include <node/miner.h>
     #include <policy/feerate.h>
     #include <policy/policy.h>
    @@ -79,18 +81,6 @@ void AddMinedBlockStats(std::vector<MinedBlockStats>& mined_blocks, MinedBlockSt
         mined_blocks.push_back(stats);
     }
    
    -struct ActiveTip {
    -    int height;
    -    uint256 hash;
    -};
    -
    -std::optional<ActiveTip> GetActiveTip(const ChainstateManager& chainman)
    -{
    -    LOCK(::cs_main);
    -    const CBlockIndex* tip{chainman.ActiveTip()};
    -    if (!tip) return std::nullopt;
    -    return ActiveTip{tip->nHeight, tip->GetBlockHash()};
    -}
     } // namespace
    
     MemPoolFeeRateEstimator::Percentiles MemPoolFeeRateEstimator::CalculateMaxWeightPercentiles(std::span<const FeePerVSize> chunk_feerates)
    @@ -114,32 +104,6 @@ MemPoolFeeRateEstimator::Percentiles MemPoolFeeRateEstimator::CalculateMaxWeight
         return percentiles;
     }
    
    -bool MemPoolFeeRateEstimatorCache::IsStale() const
    -{
    -    return !m_fee_rate_estimation || (m_last_updated + CACHE_LIFE) < NodeClock::now();
    -}
    -
    -std::optional<MemPoolFeeRateEstimatorCache::FeeRateEstimate>
    -MemPoolFeeRateEstimatorCache::GetCachedEstimate(const uint256& tip_hash) const
    -{
    -    if (IsStale() || tip_hash != m_tip_hash) return std::nullopt;
    -    return m_fee_rate_estimation;
    -}
    -
    -void MemPoolFeeRateEstimatorCache::Update(FeePerVSize conservative, FeePerVSize economical, const uint256& tip_hash)
    -{
    -    m_fee_rate_estimation = {conservative, economical};
    -    m_tip_hash = tip_hash;
    -    m_last_updated = NodeClock::now();
    -}
    -
    -void MemPoolFeeRateEstimatorCache::Clear()
    -{
    -    m_fee_rate_estimation.reset();
    -    m_tip_hash.SetNull();
    -    m_last_updated = {};
    -}
    -
     //! Build the error result for a failed mempool fee rate estimation.
     static util::Unexpected<FeeRateEstimationError> EstimationError(std::string error)
     {
    @@ -162,9 +126,9 @@ static std::optional<std::string_view> MempoolHealthError(MemPoolFeeRateEstimato
    
     MemPoolFeeRateEstimator::MemPoolFeeRateEstimator(fs::path mempool_estimator_file_path,
                                                      const CTxMemPool& mempool,
    -                                                 ChainstateManager& chainman)
    +                                                 node::BlockTemplateManager& block_template_manager)
         : m_mempool(mempool),
    -      m_chainman(chainman),
    +      m_block_template_manager(block_template_manager),
           m_mempool_estimator_file_path(std::move(mempool_estimator_file_path))
     {
         ReadFromDisk();
    @@ -218,7 +182,7 @@ bool MemPoolFeeRateEstimator::Read(AutoFile& file)
             }
             if (!blocks.empty()) {
                 const auto& last_block{blocks.back()};
    -            const std::optional<ActiveTip> active_tip{GetActiveTip(m_chainman)};
    +            const std::optional<interfaces::BlockRef> active_tip{m_block_template_manager.GetTip()};
                 if (!active_tip) {
                     LogWarning("%s: Mined-block stats read end at height %s block %s, but there is no active chain tip; ignoring file",
                                FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY),
    @@ -236,7 +200,6 @@ bool MemPoolFeeRateEstimator::Read(AutoFile& file)
             LOCK(cs);
             m_prev_mined_blocks = std::move(blocks);
             m_mined_blocks_tip_hash = tip_hash;
    -        m_cache.Clear();
         } catch (const std::exception&) {
             LogWarning("%s: Unable to read mined-block stats from stream (non-fatal)",
                        FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY));
    @@ -315,7 +278,6 @@ void MemPoolFeeRateEstimator::MempoolTxsRemovedForBlock(const std::shared_ptr<co
             });
         AddMinedBlockStats(m_prev_mined_blocks, {block_height, removed_weight, block_weight});
         m_mined_blocks_tip_hash = block->GetHash();
    -    m_cache.Clear();
     }
    
     // Require at least one block worth of activity across the window before using
    @@ -368,38 +330,18 @@ util::Expected<FeeRateEstimation, FeeRateEstimationError> MemPoolFeeRateEstimato
         if (auto error{MempoolHealthError(GetMempoolHealth())}) {
             return EstimationError(strprintf("%s: %s", FeeRateEstimatorTypeToString(estimator_type), *error));
         }
    -    // The estimator lock is not held while building a block template, so
    -    // in a rare edge case concurrent callers may duplicate work.
    -    //
    -    // Cached fee rate estimates are tagged with the chain tip they were computed on
    -    // and only served from the cache while that tip is current.
    -    //
    -    // The fee rate estimate returned directly below may still reflect a tip that went
    -    // stale during the call; that is an accepted tradeoff of not holding
    -    // locks across block assembly.
    -    {
    -        const uint256 tip_hash{WITH_LOCK(::cs_main, return Assume(m_chainman.CurrentChainstate().m_chain.Tip())->GetBlockHash())};
    -        LOCK(cs);
    -        const auto cached_estimate = m_cache.GetCachedEstimate(tip_hash);
    -        if (cached_estimate) {
    -            const auto cached_feerate{
    -                conservative ? cached_estimate->m_conservative : cached_estimate->m_economical};
    -            return FeeRateEstimation{estimator_type, cached_feerate, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET};
    -        }
    -    }
    -    node::BlockCreateOptions options;
    -    options.test_block_validity = false;
    -    const auto blocktemplate = WITH_LOCK(::cs_main, return (node::BlockAssembler{m_chainman.CurrentChainstate(), &m_mempool, options}).CreateNewBlock());
    +    const auto blocktemplate{m_block_template_manager.GetBlockTemplate(/*max_age=*/CACHE_LIFE, {.test_block_validity = false}).block_template};
         if (!blocktemplate) return EstimationError(strprintf("%s: Failed to create block template for fee rate estimation", FeeRateEstimatorTypeToString(estimator_type)));
    -    // Sort again because the rounding up when converting from weight to vsize may cause slight misorder.
    -    std::sort(blocktemplate->m_package_feerates.begin(), blocktemplate->m_package_feerates.end(), [](const auto& a, const auto& b) { return ByRatio{a} > ByRatio{b}; });
    -    const auto percentiles = CalculateMaxWeightPercentiles(blocktemplate->m_package_feerates);
    +    // Sort a copy because the rounding up when converting from weight to vsize may
    +    // cause slight misorder, and the cached template is shared and immutable.
    +    std::vector<FeePerVSize> package_feerates{blocktemplate->m_package_feerates};
    +    std::sort(package_feerates.begin(), package_feerates.end(), [](const auto& a, const auto& b) { return ByRatio{a} > ByRatio{b}; });
    +    const auto percentiles = CalculateMaxWeightPercentiles(package_feerates);
         // Fall back to a relayable floor (the higher of the min relay fee and the current
         // mempool min fee) for any percentile the mempool was too sparse to fill.
         const FeePerVSize floor{std::max(m_mempool.m_opts.min_relay_feerate, m_mempool.GetMinFee()).GetFeePerVSize()};
         const FeePerVSize p50{percentiles.p50.IsEmpty() ? floor : percentiles.p50};
         const FeePerVSize p75{percentiles.p75.IsEmpty() ? floor : percentiles.p75};
    -    WITH_LOCK(cs, m_cache.Update(p50, p75, blocktemplate->block.hashPrevBlock));
         LogDebug(BCLog::ESTIMATEFEE, "%s: conservative/economical fee rate: %s/%s %s/kvB",
                  FeeRateEstimatorTypeToString(estimator_type), CFeeRate(p50).GetFeePerK(),
                  CFeeRate(p75).GetFeePerK(), CURRENCY_ATOM);
    diff --git a/src/policy/fees/mempool_estimator.h b/src/policy/fees/mempool_estimator.h
    index 7420227ac3..ccab5a958d 100644
    --- a/src/policy/fees/mempool_estimator.h
    +++ b/src/policy/fees/mempool_estimator.h
    @@ -23,9 +23,12 @@
    
     class CBlock;
     class AutoFile;
    -class ChainstateManager;
     class CTxMemPool;
    
    +namespace node {
    +class BlockTemplateManager;
    +} // namespace node
    +
     struct RemovedMempoolTransactionInfo;
    
     // Fee rate estimate for confirmation target above this is not reliable,
    @@ -47,36 +50,6 @@ struct MinedBlockStats {
         uint64_t m_block_weight{0};
     };
    
    -/**
    - * MemPoolFeeRateEstimatorCache holds a cache of recent fee rate estimates.
    - * A cached fee rate is only provided while it is not older than CACHE_LIFE
    - * and the chain tip has not changed.
    - */
    -class MemPoolFeeRateEstimatorCache
    -{
    -public:
    -    MemPoolFeeRateEstimatorCache() = default;
    -    MemPoolFeeRateEstimatorCache(const MemPoolFeeRateEstimatorCache&) = delete;
    -    MemPoolFeeRateEstimatorCache& operator=(const MemPoolFeeRateEstimatorCache&) = delete;
    -    /** Returns true if the cache is empty or older than CACHE_LIFE. */
    -    bool IsStale() const;
    -    struct FeeRateEstimate {
    -        FeePerVSize m_conservative;
    -        FeePerVSize m_economical;
    -    };
    -    /** Returns cached estimates if not stale and computed on tip_hash, nullopt otherwise. */
    -    std::optional<FeeRateEstimate> GetCachedEstimate(const uint256& tip_hash) const;
    -    /** Update the cache with new estimates computed on tip_hash. */
    -    void Update(FeePerVSize conservative, FeePerVSize economical, const uint256& tip_hash);
    -    /** Clear cached fee rate estimates. */
    -    void Clear();
    -
    -private:
    -    std::optional<FeeRateEstimate> m_fee_rate_estimation;
    -    uint256 m_tip_hash;
    -    NodeClock::time_point m_last_updated{};
    -};
    -
     /**
      * Estimate the fee rate required for a transaction to be included in the next block.
      *
    @@ -95,7 +68,7 @@ public:
    
         MemPoolFeeRateEstimator(fs::path mempool_estimator_file_path,
                                 const CTxMemPool& mempool,
    -                            ChainstateManager& chainman);
    +                            node::BlockTemplateManager& block_template_manager);
         ~MemPoolFeeRateEstimator() = default;
         /**
          * Calculate the 50th and 75th percentile fee rates from block template chunks,
    @@ -148,9 +121,8 @@ private:
         uint256 m_mined_blocks_tip_hash GUARDED_BY(cs);
    
         const CTxMemPool& m_mempool;
    -    ChainstateManager& m_chainman;
    +    node::BlockTemplateManager& m_block_template_manager;
         mutable Mutex cs;
    -    mutable MemPoolFeeRateEstimatorCache m_cache GUARDED_BY(cs);
         const fs::path m_mempool_estimator_file_path;
     };
    
    diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp
    index 8f063eac2b..05d6e159dd 100644
    --- a/src/rpc/mining.cpp
    +++ b/src/rpc/mining.cpp
    @@ -843,7 +843,7 @@ static RPCMethod getblocktemplate()
             {
                 // NOTE: Spec does not specify behaviour for non-string longpollid, but this makes testing easier
                 hashWatchedChain = tip;
    -            nTransactionsUpdatedLastLP = block_template_manager.GetCachedTransactionsUpdated();
    +            nTransactionsUpdatedLastLP = block_template_manager.GetCachedTransactionsUpdated({});
             }
    
             // Release lock while waiting
    @@ -886,7 +886,7 @@ static RPCMethod getblocktemplate()
         }
    
         // Update block
    -    const node::CachedBlockTemplate cached{block_template_manager.RefreshCachedTemplate()};
    +    const node::CachedBlockTemplate cached{block_template_manager.GetBlockTemplate(/*max_age=*/std::chrono::seconds{5})};
         const std::shared_ptr<const node::CBlockTemplate> block_template{CHECK_NONFATAL(cached.block_template)};
         const CBlockIndex* const pindexPrev{CHECK_NONFATAL(chainman.m_blockman.LookupBlockIndex(block_template->block.hashPrevBlock))};
         CBlockHeader block_header{block_template->block};
    diff --git a/src/test/fuzz/policy_estimator_io.cpp b/src/test/fuzz/policy_estimator_io.cpp
    index 2d0edaa2fd..7e571c70a9 100644
    --- a/src/test/fuzz/policy_estimator_io.cpp
    +++ b/src/test/fuzz/policy_estimator_io.cpp
    @@ -41,7 +41,7 @@ FUZZ_TARGET(policy_estimator_io, .init = initialize_policy_estimator_io)
         static MemPoolFeeRateEstimator mempool_feerate_estimator{
             MempoolPolicyEstimatorPath(*g_setup->m_node.args),
             *g_setup->m_node.mempool,
    -        *g_setup->m_node.chainman};
    +        *g_setup->m_node.block_template_manager};
         {
             AutoFile fuzzed_auto_file_mempool_policy{fuzzed_file_provider.open()};
             if (mempool_feerate_estimator.Read(fuzzed_auto_file_mempool_policy)) {
    diff --git a/src/test/mempool_fee_estimator_tests.cpp b/src/test/mempool_fee_estimator_tests.cpp
    index c75c7c7237..9a3b1f98c3 100644
    --- a/src/test/mempool_fee_estimator_tests.cpp
    +++ b/src/test/mempool_fee_estimator_tests.cpp
    @@ -103,34 +103,9 @@ BOOST_AUTO_TEST_CASE(calculate_max_weight_percentiles)
         BOOST_CHECK(ByRatio{percentiles.p50} > ByRatio{percentiles.p75});
     }
    
    -BOOST_AUTO_TEST_CASE(mempool_fee_rate_estimator_cache)
    -{
    -    FakeNodeClock clock{};
    -    MemPoolFeeRateEstimatorCache cache;
    -    const uint256 tip_hash{uint256::ONE};
    -    const uint256 next_tip_hash{uint256{2}};
    -    const FeePerVSize conservative{2, 1};
    -    const FeePerVSize economical{1, 1};
    -
    -    BOOST_CHECK(cache.IsStale());
    -    BOOST_CHECK(!cache.GetCachedEstimate(tip_hash));
    -
    -    cache.Update(conservative, economical, tip_hash);
    -    BOOST_CHECK(!cache.IsStale());
    -    const auto cached{cache.GetCachedEstimate(tip_hash)};
    -    BOOST_REQUIRE(cached);
    -    BOOST_CHECK(cached->m_conservative == conservative);
    -    BOOST_CHECK(cached->m_economical == economical);
    -    BOOST_CHECK(!cache.GetCachedEstimate(next_tip_hash));
    -
    -    clock += CACHE_LIFE + std::chrono::seconds{1};
    -    BOOST_CHECK(cache.IsStale());
    -    BOOST_CHECK(!cache.GetCachedEstimate(tip_hash));
    -}
    -
     BOOST_AUTO_TEST_CASE(MempoolFeeRateEstimator)
     {
    -    auto mempool_estimator = MemPoolFeeRateEstimator(MempoolPolicyEstimatorPath(*m_node.args), *m_node.mempool, *m_node.chainman);
    +    auto mempool_estimator = MemPoolFeeRateEstimator(MempoolPolicyEstimatorPath(*m_node.args), *m_node.mempool, *Assert(m_node.block_template_manager));
         BOOST_CHECK_EQUAL(mempool_estimator.MaximumTarget(), MEMPOOL_FEE_ESTIMATOR_MAX_TARGET);
         // Before the mempool has finished loading, no estimate is available.
         {
    @@ -153,7 +128,7 @@ BOOST_AUTO_TEST_CASE(MempoolFeeRateEstimator)
         }
         {
             MemPoolFeeRateEstimator custom_mempool_estimator{
    -            MempoolPolicyEstimatorPath(*m_node.args), *m_node.mempool, *m_node.chainman};
    +            MempoolPolicyEstimatorPath(*m_node.args), *m_node.mempool, *Assert(m_node.block_template_manager)};
             unsigned int custom_height{100};
             for (size_t block_count{1}; block_count < MEMPOOL_HEALTH_WINDOW_BLOCKS; ++block_count) {
                 AddRemovedBlock(custom_mempool_estimator,
    diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp
    index ad9e8dc3a0..b673526f63 100644
    --- a/src/test/miner_tests.cpp
    +++ b/src/test/miner_tests.cpp
    @@ -990,11 +990,12 @@ BOOST_FIXTURE_TEST_CASE(block_template_manager_cached_template, RegTestingSetup)
         auto& mempool{*Assert(m_node.mempool)};
         FakeNodeClock clock{};
         const auto active_tip{[&] { return WITH_LOCK(::cs_main, return m_node.chainman->ActiveChain().Tip()->GetBlockHash()); }};
    -    const auto refresh{[&] { return WITH_LOCK(::cs_main, return block_template_manager.RefreshCachedTemplate()); }};
    +    constexpr auto max_age{5s};
    +    const auto get{[&] { return block_template_manager.GetBlockTemplate(max_age); }};
    
    -    // The first refresh builds a template on the tip.
    +    // The first call builds a template on the tip.
         const uint256 tip{active_tip()};
    -    const auto first{refresh()};
    +    const auto first{get()};
         BOOST_REQUIRE(first.block_template);
         BOOST_CHECK_EQUAL(first.block_template->block.hashPrevBlock, tip);
         BOOST_CHECK_EQUAL(first.transactions_updated, mempool.GetTransactionsUpdated());
    @@ -1002,45 +1003,45 @@ BOOST_FIXTURE_TEST_CASE(block_template_manager_cached_template, RegTestingSetup)
    
         // Without a mempool change, the template is reused however old it is.
         clock += 1h;
    -    BOOST_CHECK(refresh().block_template == first.block_template);
    +    BOOST_CHECK(get().block_template == first.block_template);
    
    -    // A mempool change rebuilds a template that is more than 5 seconds old.
    +    // A mempool change rebuilds a template that is more than max_age old.
         mempool.AddTransactionsUpdated(1);
    -    const auto second{refresh()};
    +    const auto second{get()};
         BOOST_CHECK(second.block_template != first.block_template);
         BOOST_CHECK_EQUAL(second.transactions_updated, mempool.GetTransactionsUpdated());
         BOOST_CHECK_EQUAL(second.time_start, Now<NodeSeconds>());
    -    BOOST_CHECK_EQUAL(WITH_LOCK(::cs_main, return block_template_manager.GetCachedTransactionsUpdated()), second.transactions_updated);
    +    BOOST_CHECK_EQUAL(block_template_manager.GetCachedTransactionsUpdated({}), second.transactions_updated);
    
    -    // A mempool change does not rebuild a template that is up to 5 seconds old.
    +    // A mempool change does not rebuild a template that is up to max_age old.
         mempool.AddTransactionsUpdated(1);
    -    clock += 5s;
    -    BOOST_CHECK(refresh().block_template == second.block_template);
    +    clock += max_age;
    +    BOOST_CHECK(get().block_template == second.block_template);
         clock += 1s;
    -    const auto third{refresh()};
    +    const auto third{get()};
         BOOST_CHECK(third.block_template != second.block_template);
    
         // A new tip rebuilds the template.
         MineBlock(m_node, {});
         const uint256 new_tip{active_tip()};
         BOOST_REQUIRE(new_tip != tip);
    -    const auto fourth{refresh()};
    +    const auto fourth{get()};
         BOOST_REQUIRE(fourth.block_template);
         BOOST_CHECK(fourth.block_template != third.block_template);
         BOOST_CHECK_EQUAL(fourth.block_template->block.hashPrevBlock, new_tip);
    
    -    // A failed rebuild leaves no template, so the next refresh tries again
    -    // without waiting for the 5 seconds to pass. An orphan in the mempool
    -    // makes TestBlockValidity() reject the block.
    +    // A failed rebuild leaves no template, so the next call tries again without
    +    // waiting for max_age. An orphan in the mempool makes TestBlockValidity()
    +    // reject the block.
         CMutableTransaction orphan;
         orphan.vin.emplace_back(COutPoint{Txid::FromUint256(uint256::ONE), 0});
         orphan.vout.emplace_back(COIN, CScript() << OP_TRUE);
         TryAddToMempool(mempool, TestMemPoolEntryHelper{}.Fee(CENT).FromTx(orphan));
         BOOST_REQUIRE(mempool.exists(orphan.GetHash()));
    -    clock += 6s;
    -    BOOST_CHECK_EXCEPTION(refresh(), std::runtime_error, HasReason("bad-txns-inputs-missingorspent"));
    +    clock += max_age + 1s;
    +    BOOST_CHECK_EXCEPTION(get(), std::runtime_error, HasReason("bad-txns-inputs-missingorspent"));
         WITH_LOCK(mempool.cs, mempool.removeRecursive(CTransaction{orphan}, MemPoolRemovalReason::REPLACED));
    -    const auto fifth{refresh()};
    +    const auto fifth{get()};
         BOOST_REQUIRE(fifth.block_template);
         BOOST_CHECK(fifth.block_template != fourth.block_template);
         BOOST_CHECK_EQUAL(fifth.block_template->block.hashPrevBlock, new_tip);
    diff --git a/src/wallet/test/fuzz/fees.cpp b/src/wallet/test/fuzz/fees.cpp
    index 5b2a225deb..88b0f999a0 100644
    --- a/src/wallet/test/fuzz/fees.cpp
    +++ b/src/wallet/test/fuzz/fees.cpp
    @@ -46,8 +46,8 @@ class FuzzedFeeEstimatorMan : public FeeRateEstimatorManager
         FuzzedDataProvider& fuzzed_data_provider;
    
     public:
    -    FuzzedFeeEstimatorMan(FuzzedDataProvider& provider, const CTxMemPool& mempool, ChainstateManager& chainman)
    -        : FeeRateEstimatorManager(fs::path{}, false, fs::path{}, mempool, chainman), fuzzed_data_provider(provider) {}
    +    FuzzedFeeEstimatorMan(FuzzedDataProvider& provider, const CTxMemPool& mempool, node::BlockTemplateManager& block_template_manager)
    +        : FeeRateEstimatorManager(fs::path{}, false, fs::path{}, mempool, block_template_manager), fuzzed_data_provider(provider) {}
    
         util::Expected<FeeRateEstimation, FeeRateEstimationError> GetFeeRateEstimate(int confTarget, bool conservative) const override
         {
    @@ -89,7 +89,7 @@ FUZZ_TARGET(wallet_fees, .init = initialize_setup)
         node.block_template_manager.reset();
         node.mempool = std::make_unique<CTxMemPool>(mempool_opts, error);
         g_setup->CreateBlockTemplateManager();
    -    std::unique_ptr<FeeRateEstimatorManager> fee_estimator_man = std::make_unique<FuzzedFeeEstimatorMan>(fuzzed_data_provider, *node.mempool, *node.chainman);
    +    std::unique_ptr<FeeRateEstimatorManager> fee_estimator_man = std::make_unique<FuzzedFeeEstimatorMan>(fuzzed_data_provider, *node.mempool, *node.block_template_manager);
         g_setup->SetFeeEstimatorMan(std::move(fee_estimator_man));
         auto target_feerate{CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/1'000'000)}};
         if (target_feerate > node.mempool->m_opts.incremental_relay_feerate &&
    

    </details>

    We may need to extend this to clear all cache on tip change though to free some memory, this diff currently has them till shutdown.

  6. w0xlt commented at 5:40 PM on October 8, 2026: contributor

    @ismaelsadeeq Thanks for the Concept ACK and the POC. I’d prefer to keep this PR focused on moving the existing getblocktemplate cache while preserving its behavior.

    The POC introduces multiple entries keyed by build options, eviction, another mutex, configurable freshness and forced rebuilds, plus changes to IPC and fee estimation. Those bring separate correctness and performance questions, so I think the broader cache would be a better fit for a follow-up.

    This refactor gives that work a place to build on.

  7. ismaelsadeeq commented at 4:38 AM on October 9, 2026: member

    @ismaelsadeeq Thanks for the Concept ACK and the POC. I’d prefer to keep this PR focused on moving the existing getblocktemplate cache while preserving its behaviour.

    The POC introduces multiple entries keyed by build options, eviction, another mutex, configurable freshness and forced rebuilds, plus changes to IPC and fee estimation. Those bring separate correctness and performance questions, so I think the broader cache would be a better fit for a follow-up.

    This refactor gives that work a place to build on.

    Fair, I think it would be nice to have the cache and make this generic. The reason the broader cache wasn't pursued further in #33421 is thin motivation (i.e., no users as of then).

    But with this, we cache anyway, so making it generic is a nice win and usable by not just getblocktemplate rpc. However, I agree this is a start; we can extend it later. It is just a bit more work to have a single template cache design and then extend it to multiple after merging.

    I get that this is simpler to reason about and review, but it would be more ideal if we decide whether we want to do the caching for all or not.

    CC @sedited and @stickies-v since u both weigh in here #33421

  8. in src/node/block_template_manager.h:52 in ccb8e84039 outdated
      48 | @@ -36,6 +49,7 @@ class BlockTemplateManager
      49 |      ChainstateManager& m_chainman;
      50 |      KernelNotifications& m_notifications;
      51 |      const BlockCreateOptions m_block_create_args;
      52 | +    CachedBlockTemplate m_cached_template GUARDED_BY(::cs_main);
    


    ismaelsadeeq commented at 4:39 AM on October 9, 2026:

    The cache is kept forever till shutdown. Should we clear it on a block connection?


    w0xlt commented at 10:54 PM on October 28, 2026:

    Thanks! Master already keeps the last template until the node stops (in the getblocktemplate statics); this PR only moves it. It's a single template, and the next call after a new block replaces it. Clearing it on block connection would need a new notification hook in the manager, so I'd leave that for a follow-up.

  9. in src/node/block_template_manager.h:77 in ccb8e84039
      72 | +     *  mempool changed and the template is more than 5 seconds old.
      73 | +     *  @return the cached template, rebuilt if needed. Its block_template is
      74 | +     *  never null.
      75 | +     *  @throws std::runtime_error if building the template fails. The cache is
      76 | +     *  then left without a template, so the next call tries again. */
      77 | +    CachedBlockTemplate RefreshCachedTemplate() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
    


    ismaelsadeeq commented at 4:40 AM on October 9, 2026:

    Should we pass the max_age so that it is not tied to GBT? Could we also rename it to GetNewOrCacheBlockTemplate or something similar?


    w0xlt commented at 10:57 PM on October 28, 2026:

    Thanks! Renamed it to GetNewOrCachedBlockTemplate().

    I'd keep max_age out of this PR. The cache is still a single entry built with the default options, so another caller with a different max_age would just keep rebuilding the GBT template. It fits better in a follow-up that makes the cache generic, like your POC. I did give the 5 seconds a name, though: GBT_TEMPLATE_REBUILD_INTERVAL.

  10. DrahtBot added the label Needs rebase on Oct 14, 2026
  11. Sjors commented at 9:55 AM on October 28, 2026: member

    I also prefer to keep this simple and focussed.

    I'm not sure if the current two commit split is the easiest to review, because instead of moving code we now have one commit that introduces new code and a second commit that deletes old code.

  12. w0xlt force-pushed on Oct 28, 2026
  13. rpc: gather the getblocktemplate cache state into a struct
    Replace the function-local statics in getblocktemplate with a
    CachedBlockTemplate struct and a GetNewOrCachedBlockTemplate() helper,
    both local to rpc/mining.cpp. The cache is still process-wide, but it is
    now GUARDED_BY(::cs_main), so its synchronization is checked.
    
    GetNewOrCachedBlockTemplate() keeps the existing rules: the template is
    rebuilt when the tip changes, or when the mempool changed and the cached
    template is more than 5 seconds old. A failed rebuild leaves the cached
    template unset so that the next call tries again.
    
    This prepares for moving the cache into BlockTemplateManager in the next
    commit. While the code is still next to the original, it also:
    
    - Drops the static CBlockIndex pointer. pindexPrev is looked up from the
      template's hashPrevBlock, so it cannot outlive the ChainstateManager
      that owns it.
    - Reads the active chain tip inside GetNewOrCachedBlockTemplate() and
      compares it with the template's hashPrevBlock, so the cache cannot
      pair a template with a tip it does not build on. The caller no longer
      needs to re-read the tip after longpolling.
    - Builds the longpollid from hashPrevBlock, so it always names the block
      the template builds on.
    - Replaces the deprecated GetTime() with the equivalent
      Now<NodeSeconds>().
    - Names the 5 second rebuild interval GBT_TEMPLATE_REBUILD_INTERVAL.
    cde2bb8773
  14. refactor: move getblocktemplate cache into BlockTemplateManager
    Move CachedBlockTemplate, GBT_TEMPLATE_REBUILD_INTERVAL and
    GetNewOrCachedBlockTemplate() from rpc/mining.cpp into
    BlockTemplateManager. The file-scope cache becomes a member that is
    still guarded by cs_main, and the interval becomes a public static
    member. The longpolling code reads the cached mempool counter through
    the new GetCachedTransactionsUpdated().
    
    The cache now lives as long as the node context instead of the process.
    
    Apart from the member accesses and declarations, this is a move. It
    can be reviewed with:
    
    git show --color-moved=dimmed-zebra --color-moved-ws=allow-indentation-change
    9414d05949
  15. test: cover the BlockTemplateManager getblocktemplate cache
    Check that GetNewOrCachedBlockTemplate() reuses the cached template
    until the tip changes, or until the mempool changes and the template is
    more than 5 seconds old, and that GetCachedTransactionsUpdated() reports
    the mempool counter sampled for the last build. Also check that a failed
    rebuild leaves the cache without a template, so that the next call
    retries without waiting for the 5 seconds to pass.
    ed3dd1469f
  16. w0xlt force-pushed on Oct 28, 2026
  17. DrahtBot added the label CI failed on Oct 28, 2026
  18. w0xlt commented at 11:02 PM on October 28, 2026: contributor

    Thanks, good point. I've split it into three commits:

    1. The first refactors the cache in place in rpc/mining.cpp, so the diff compares each change directly against master's code.
    2. The second moves it into BlockTemplateManager. It should be easy to check with git show --color-moved=dimmed-zebra --color-moved-ws=allow-indentation-change.
    3. The third adds the unit test.
  19. w0xlt commented at 11:10 PM on October 28, 2026: contributor

    PR description updated.

  20. DrahtBot removed the label Needs rebase on Oct 28, 2026
  21. DrahtBot removed the label CI failed on Oct 28, 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-10-30 09:40 UTC