Skip to content

node: add BlockTemplateCache#33421

Open
ismaelsadeeq wants to merge 6 commits intobitcoin:masterfrom
ismaelsadeeq:09-2025-minerman
Open

node: add BlockTemplateCache#33421
ismaelsadeeq wants to merge 6 commits intobitcoin:masterfrom
ismaelsadeeq:09-2025-minerman

Conversation

@ismaelsadeeq
Copy link
Member

@ismaelsadeeq ismaelsadeeq commented Sep 17, 2025

This PR implements the first step of #33758 by adding a cache layer for node block templates.

The cache stores newly created block templates along with their configuration options. When clients request a block template, they specify an optional template age limit in MillisecondsDouble:

  • If template age limit is nullopt, we create a new template and return it without adding it to the cache.
  • If a matching template (block template with identical block creation options) exists in the cache and hasn't exceeded the age limit, the cached version is returned.
  • If no matching template exists or the age limit has been exceeded, a new template is generated, cached, and returned. When evicting stale templates, we assume no duplicates exist since templates are evicted as soon as their age limit is reached or exceeded.

The cache maintains a configurable maximum size (default: 10 templates). When this limit is exceeded, the oldest template is evicted after each insertion.

Cache lookup requires an exact match on ALL BlockAssembler::Options fields. While some fields (e.g., test_block_validity, print_modified_fee, coinbase_output_script) could be ignored for cache reuse, they mainly differ in tests and benchmarks. We keep the logic simple and accept cache misses in those cases.

The cache enforces at most one template per set of options. When searching for a match in getCachedTemplate(), if a template with matching options is found but has exceeded the age limit, it's immediately evicted before returning nullptr. This ensures no duplicate options exist in the cache. A SanityCheck() method verifies this invariant in tests by checking all cached entries for duplicates.

The cache subscribes to the validation interface and clears itself when blocks are connected (to non-historical chainstate to avoid clearing during assumeutxo background validation) or disconnected from the active chain (during reorgs), preventing stale or invalid templates from being served.

GetBlockTemplate() uses double-checked locking: it first checks the cache with only m_mutex held, then if there's a miss, acquires both cs_main and m_mutex to create the template. Before creating, it checks again since another thread might have added a template while locks were released.

This PR removes an unused node/context.h import from miner to prevent a circular dependency when exposing the block template cache to the node context.

@DrahtBot DrahtBot changed the title node: add BlockTemplateCache node: add BlockTemplateCache Sep 17, 2025
@DrahtBot
Copy link
Contributor

DrahtBot commented Sep 17, 2025

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

Code Coverage & Benchmarks

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

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK ryanofsky
Concept ACK Sjors

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

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #34806 (refactor: logging: Various API improvements by ajtowns)
  • #34479 (fuzz: Add and use NodeClockContext by maflcko)
  • #34075 (fees: Introduce Mempool Based Fee Estimation to reduce overestimation by ismaelsadeeq)
  • #33922 (mining: add getMemoryLoad() and track template non-mempool memory footprint by Sjors)
  • #31117 (miner: Reorg Testnet4 minimum difficulty blocks by fjahr)

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.

@ismaelsadeeq
Copy link
Member Author

Forced pushed from dca8200c71bb568ffe4821c68209e07a305db80d to 16d55585aa2d26e9177734927b8446faa72570e7 5db80d..16d5558

Main Changes are

  1. Moved the BlockTemplateCache to src/node/miner.cpp to fix the circular dependency issue
  2. The response now return the block template creation time
  3. We search for match from right to left to return the fresh hit first
  4. Added a fuzz test that test various cache condition and add more coverage to the BlockAssembler Code.

@ismaelsadeeq
Copy link
Member Author

ismaelsadeeq commented Sep 23, 2025

It seems the new fuzz test in 16d5558
caught a minor UndefinedBehaviorSanitizer: unsigned-integer-overflow issue on master.

/home/admin/actions-runner/_work/_temp/src/node/miner.cpp:414:47: runtime error: unsigned integer overflow: 2000 - 4000 cannot be represented in type 'size_t' (aka 'unsigned long')
    #0 0x5b2b2753aee0 in node::BlockAssembler::addPackageTxs(int&, int&) /home/admin/actions-runner/_work/_temp/build/src/./node/miner.cpp:414:47


SUMMARY: UndefinedBehaviorSanitizer: unsigned-integer-overflow /home/admin/actions-runner/_work/_temp/src/node/miner.cpp:414:47 
MS: 5 PersAutoDict-PersAutoDict-EraseBytes-CopyPart-InsertRepeatedBytes- DE: "\000\000\000\000"-"\001\000\000\000\000\000\000\000"-; base unit: 05fe405753166f125559e7c9ac558654f107c7e9
0x1,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x27,0x27,0x27,0x27,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
\001\000\000\000\000\000\000\000''''\000\000\000\000\000\000\000\000\000\000\000\000\000
artifact_prefix='./'; Test unit written to ./crash-52e81f820b02d40ce3132a4d8e27726deb8eb132
Base64: AQAAAAAAAAAnJycnAAAAAAAAAAAAAAAAAA==

https://github.com/bitcoin/bitcoin/actions/runs/17949962995/job/51047356983?pr=33421

This is possible on master when node is started with -blockmaxweight=2000 , -blockreservedweight=2000 or just blockmaxweight=2000 and mining interface createNewBlock pass blockReservedWeight as 2000.

This undefined behaviour is possible after #32355 because it introduced a fixed BLOCK_FULL_ENOUGH_WEIGHT_DELTA with value 4000.
Before #32355 the delta was tied to the options.blockreservedweight so will just be 2000 - 2000 which will result in a valid size_t value.

I'll push a commit with a fix soon.

@glozow
Copy link
Member

glozow commented Sep 24, 2025

It seems the new fuzz test in 16d5558
caught a minor UndefinedBehaviorSanitizer: unsigned-integer-overflow issue on master.

Nice! Sorry for the labeling noise, I meant to label #33475

fanquake added a commit that referenced this pull request Sep 25, 2025
…rflow

b807dfc miner: fix `addPackageTxs` unsigned integer overflow (ismaelsadeeq)

Pull request description:

  This PR fixes an unsigned integer overflow in the `addPackageTxs` method of the `BlockAssembler`.

  The overflow is a rare edge case that might occur on master when a miner reserves 2000 WU and wants to create an block to be empty.

  i.e, by starting with `-blockmaxweight=2000`, `-blockreservedweight=2000`, or just `blockmaxweight=2000`, and then calling the mining interface `createNewBlock` with `blockReservedWeight` set to `2000`.

  Instead of bailing out after going through transactions equivalent to `MAX_CONSECUTIVE_FAILURES`, the loop never breaks until all mempool transactions are visited.

  See #33421 (comment)

  The fix avoids the overflow by using addition instead adding `BLOCK_FULL_ENOUGH_WEIGHT_DELTA` to the block weight and comparing it with `m_options.nBlockMaxWeight`.

  Another alternative that preserves the same structure is to use `static_cast`. See c9530cf.

  This fix can be tested by cherry-picking the commits from #33421 without the static cast fix and running:

  ```bash
  echo "AQAAAAAAA
  AAnJycnAAAAAAAAAAAAAAAAAA" | base64 --decode > miner.crash

  FUZZ=block_template_cache ./build_fuzz/bin/fuzz miner.crash
  ```

  ---

  This is part of a larger inconsistency in how size/weight is represented in the codebase. It may be worth defining a dedicated type for size/weight.

ACKs for top commit:
  glozow:
    nice, utACK b807dfc
  furszy:
    Code ACK b807dfc

Tree-SHA512: c1d2f7e500f9b0624a4c22a146921a1644017065e6c94d0c5027486392321f5de26c61751a24765e025e45b34c535adfd6d0e2ac809dea6846b99f37d13043c9
@DrahtBot
Copy link
Contributor

🚧 At least one of the CI tasks failed.
Task tidy: https://github.com/bitcoin/bitcoin/actions/runs/21867589207/job/63113002515
LLM reason (✨ experimental): clang-tidy failed the job due to a bugprone-argument-comment warning in blocktemplatecache.cpp (mismatched comment argument name).

Hints

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.

* Search for a cached template matching the provided options.
*
* - If found and within the age limit, return it.
* - If found but too old, evict it and return nullptr.
Copy link
Member

@Sjors Sjors Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 4619759 miner: introduce BlockTemplateCache for reusing block templates: this is somewhat surprising behavior in isolation. When you have two clients, one client might want a recent template and the second client allows an older one.

But it makes sense because an evicted template is guaranteed to be replaced.

I tried some alternative approaches, but it was ugly.

Maybe rename to getOrDeleteCachedTemplate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is okay; it is a shared pointer, so evicting here does not affect a client who holds onto an older one.

- This prevents circular dependecy when we introduce
  BlockTemplateCache in miner and want to expose it
  via the node context.
@ismaelsadeeq
Copy link
Member Author

ismaelsadeeq commented Mar 2, 2026

Forced pushed from 1f623ca to e413103
compare diff

C.I failure seems unrelated. I opened #34716 to track it.

Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review ACK e413103. Left many suggestions but all looks good, and I think having this new class should organize template creation code better and be useful for adding more features and improving efficiency

ismaelsadeeq and others added 5 commits March 5, 2026 11:58
Because the CBlockTemplate struct now has a mockable steady clock
variable, all fuzz tests that invoke code paths that create a CBlockTemplate
will now emit a warning if a SteadyClockContext is not explicitly initialized.
Hence, this commit initializes a SteadyClockContext for all affected fuzz tests.
Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review ACK 20fc737. Mainly max_age corner case cleanup since last review and other simplifications. The code seem very simple and clean now. Left a few more suggestions, but definitely feel free to ignore them and avoid another update. This looks very good

static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
g_setup = testing_setup.get();
SetMockTime(WITH_LOCK(g_setup->m_node.chainman->GetMutex(), return g_setup->m_node.chainman->ActiveTip()->Time()));

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "miner: add block creation time point to CBlockTemplate" (9b6475a)

MIght be an unintended whitespace change here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah will do if I need to retouch.

/* Uses steady clock because it is monotonic and unaffected by rare system clock changes.
* It uses the mockable version to allow for testing.
* Used to decide whether a cached template is eligible for reuse based on its age.*/
MockableSteadyClock::time_point m_creation_time;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "miner: add block creation time point to CBlockTemplate" (9b6475a)

Would it make sense to assign a default value like m_creation_time{MockableSteadyClock::now()}?

If not, it may be better to at least initialize with m_creation_time{} so the value will be 0 instead of an indeterminate value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to assign a default value like m_creation_time{MockableSteadyClock::now()}?

If not, it may be better to at least initialize with m_creation_time{} so the value will be 0 instead of an indeterminate value.

Hmm, I'd rather leave it uninitialized for now (no code path instantiates a CBlockTemplate except via CreateNewBlock). A missed initialization should ideally be a detectable bug rather than a silent default value.

So this is a good flag, but I'd fix this by having an explicit constructor that accepts all CBlockTemplate struct variables so that no field can be left unassigned.

Leaving as-is for now; no degradation, it's the same assumption made with CBlock and the other values in the struct.

m_block_templates.erase(it);
return nullptr;
}
return it->block_template;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "miner: introduce BlockTemplateCache for reusing block templates" (c3f106c)

Maybe do return Assert(it->block_template) to be clear this pointer is never expected to be null

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when retouching, I will add the assert.

@DrahtBot
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants