Now that this accepts nullopt, I don’t think there is a reason to have a vague “age limit” option instead of clearer “max age” option. Would suggest dropping TimeIntervalElapsed function and age_limit variable everywhere replacing them with max_age returning templates <= than that age, so the API is easier to understand and code is simpler to reason about:
0--- a/src/node/miner.cpp
1+++ b/src/node/miner.cpp
2@@ -368,7 +368,7 @@ BlockTemplateRef BlockTemplateCache::createBlockTemplateInternal(const BlockAsse
3
4 BlockTemplateRef BlockTemplateCache::getCachedTemplate(
5 const BlockAssembler::Options& options,
6- MillisecondsDouble template_age_limit)
7+ MillisecondsDouble template_max_age)
8 {
9 AssertLockHeld(m_mutex);
10 auto it = std::find_if(m_block_templates.begin(), m_block_templates.end(),
11@@ -376,7 +376,9 @@ BlockTemplateRef BlockTemplateCache::getCachedTemplate(
12 if (it == m_block_templates.end()) {
13 return nullptr;
14 }
15- if (TimeIntervalElapsed(it->second->m_creation_time, template_age_limit)) {
16+ const auto now = MockableSteadyClock::now();
17+ // Erase template if it is too old, or if the clock moved backwards (should never happen except in tests).
18+ if (now - it->second->m_creation_time > template_max_age || now < it->second->m_creation_time) {
19 m_block_templates.erase(it);
20 return nullptr;
21 }
22@@ -385,9 +387,9 @@ BlockTemplateRef BlockTemplateCache::getCachedTemplate(
23
24 BlockTemplateRef BlockTemplateCache::GetBlockTemplate(
25 const BlockAssembler::Options& options,
26- std::optional<MillisecondsDouble> template_age_limit)
27+ std::optional<MillisecondsDouble> template_max_age)
28 {
29- if (!template_age_limit.has_value()) {
30+ if (!template_max_age.has_value()) {
31 return WITH_LOCK(::cs_main, return createBlockTemplateInternal(options));
32 }
33
34@@ -398,13 +400,13 @@ BlockTemplateRef BlockTemplateCache::GetBlockTemplate(
35 // the cache key and accept cache misses in those cases.
36 {
37 LOCK(m_mutex);
38- auto cached = getCachedTemplate(options, *template_age_limit);
39+ auto cached = getCachedTemplate(options, *template_max_age);
40 if (cached) return cached;
41 }
42
43 LOCK2(::cs_main, m_mutex);
44 // Search again - another thread might have added a template while we released m_mutex
45- auto cached = getCachedTemplate(options, *template_age_limit);
46+ auto cached = getCachedTemplate(options, *template_max_age);
47 if (cached) return cached;
48 auto block_template = createBlockTemplateInternal(options);
49 m_block_templates.emplace_back(options, block_template);
50--- a/src/node/miner.h
51+++ b/src/node/miner.h
52@@ -46,14 +46,6 @@ class KernelNotifications;
53 static const bool DEFAULT_PRINT_MODIFIED_FEE = false;
54 static constexpr size_t DEFAULT_BLOCK_TEMPLATE_CACHE_LIMIT{10};
55
56-// Return true if current time is greater or equal to `prev_time + time_interval`, or if
57-// `prev_time` is greater than the current time (indicating clock moved backward; only possible in test).
58-static inline bool TimeIntervalElapsed(const MockableSteadyClock::time_point& prev_time, MillisecondsDouble time_interval)
59-{
60- const auto now = MockableSteadyClock::now();
61- return now < prev_time || MillisecondsDouble{now - prev_time} >= time_interval;
62-}
63-
64 struct CBlockTemplate
65 {
66 CBlock block;
67@@ -152,15 +144,15 @@ private:
68 * template is stored per set of options, and the cache has a fixed maximum size.
69 *
70 * When requesting a block template:
71- * - If template_age_limit is std::nullopt, a new block template is always
72+ * - If template_max_age is std::nullopt, a new block template is always
73 * created and returned, and nothing is cached.
74 *
75 * - Otherwise, the cache is checked for a template with matching options.
76- * If a cached template exists and its age (elapsed time since creation) is strictly
77- * less than template_age_limit, it is reused.
78+ * If a cached template exists and its age (elapsed time since creation)
79+ * doesn't exceed template_max_age, it is reused.
80 *
81- * - If a matching template exists but its age has reached or exceeded the
82- * limit, it is removed and replaced with a newly created template.
83+ * - If a cached template exists but is too old, it is removed and replaced
84+ with a newly created template.
85 *
86 * - If no matching template exists, a new template is created and cached.
87 *
88@@ -184,12 +176,12 @@ class BlockTemplateCache : public CValidationInterface
89 /**
90 * Search for a cached template matching the provided options.
91 *
92- * - If found and within the age limit, return it.
93+ * - If found and not too old, return it.
94 * - If found but too old, evict it and return nullptr.
95 * - If not found, return nullptr.
96 */
97 BlockTemplateRef getCachedTemplate(const BlockAssembler::Options& options,
98- MillisecondsDouble template_age_limit)
99+ MillisecondsDouble template_max_age)
100 EXCLUSIVE_LOCKS_REQUIRED(m_mutex);
101 /**
102 * Create a new block template.
103@@ -211,12 +203,12 @@ public:
104 * Cache lookup requires an exact match on all block creation options.
105 *
106 * [@param](/bitcoin-bitcoin/contributor/param/) options Block assembly options to match
107- * [@param](/bitcoin-bitcoin/contributor/param/) template_age_limit Maximum age for cached templates in milliseconds.
108+ * [@param](/bitcoin-bitcoin/contributor/param/) template_max_age Maximum age for cached templates in milliseconds.
109 * If nullopt, bypasses cache entirely (doesn't insert into, read or delete from cache).
110 * [@return](/bitcoin-bitcoin/contributor/return/) A shared pointer to the block template
111 */
112 BlockTemplateRef GetBlockTemplate(const BlockAssembler::Options& options,
113- std::optional<MillisecondsDouble> template_age_limit = std::nullopt)
114+ std::optional<MillisecondsDouble> template_max_age = std::nullopt)
115 EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
116
117 /** Test-only: verify cache invariants */
118--- a/src/test/fuzz/blocktemplatecache.cpp
119+++ b/src/test/fuzz/blocktemplatecache.cpp
120@@ -73,17 +73,17 @@ FUZZ_TARGET(block_template_cache, .init = initialize_block_template_cache)
121 auto prev_time = tmpl->m_creation_time;
122 auto CheckCache = [&](bool expect_hit,
123 const node::BlockAssembler::Options& opts,
124- std::optional<MillisecondsDouble> age_limit = std::nullopt,
125+ std::optional<MillisecondsDouble> max_age = std::nullopt,
126 std::chrono::milliseconds advance = 1ms) {
127 steady_clock += advance;
128- auto t = template_cache->GetBlockTemplate(opts, age_limit);
129+ auto t = template_cache->GetBlockTemplate(opts, max_age);
130 assert(t);
131
132 if (expect_hit) {
133 assert(t->m_creation_time == prev_time);
134 } else {
135 assert(t->m_creation_time > prev_time);
136- if (age_limit) prev_time = t->m_creation_time;
137+ if (max_age) prev_time = t->m_creation_time;
138 }
139 };
140 LIMITED_WHILE(fuzzed_data_provider.remaining_bytes(), 10)
141@@ -91,13 +91,13 @@ FUZZ_TARGET(block_template_cache, .init = initialize_block_template_cache)
142 // template age-based behavior
143 const auto advance_ms =
144 std::chrono::milliseconds{fuzzed_data_provider.ConsumeIntegralInRange<int>(1, 100000)};
145- const auto template_age_limit_ms =
146+ const auto template_max_age_ms =
147 std::chrono::milliseconds{fuzzed_data_provider.ConsumeIntegralInRange<int>(0, 100000)};
148- const auto age_limit = MillisecondsDouble{template_age_limit_ms};
149+ const auto max_age = MillisecondsDouble{template_max_age_ms};
150
151- // Expected hit iff block template age limit exceeds the elapsed time.
152- const bool expect_hit = advance_ms < template_age_limit_ms;
153- CheckCache(expect_hit, base_options, age_limit, advance_ms);
154+ // Expected hit iff block template isn't older than max_age.
155+ const bool expect_hit = advance_ms <= template_max_age_ms;
156+ CheckCache(expect_hit, base_options, max_age, advance_ms);
157
158 // historical chainstate role should not clear cache
159 kernel::ChainstateRole background_role{true, true};
160--- a/src/test/miner_tests.cpp
161+++ b/src/test/miner_tests.cpp
162@@ -893,10 +893,10 @@ BOOST_AUTO_TEST_CASE(blocktemplate_cache)
163 auto prev_time = block_template->m_creation_time;
164 auto check_cache = [&](bool expect_hit,
165 const BlockAssembler::Options& opts,
166- std::optional<MillisecondsDouble> template_age_limit = std::nullopt,
167+ std::optional<MillisecondsDouble> template_max_age = std::nullopt,
168 std::chrono::milliseconds advance = 1ms) {
169 steady_clock += advance;
170- const auto tmpl = template_cache->GetBlockTemplate(opts, template_age_limit);
171+ const auto tmpl = template_cache->GetBlockTemplate(opts, template_max_age);
172 BOOST_CHECK(tmpl != nullptr);
173 if (expect_hit) {
174 BOOST_CHECK(tmpl->m_creation_time == prev_time);
175@@ -912,12 +912,15 @@ BOOST_AUTO_TEST_CASE(blocktemplate_cache)
176 // 0ms limit creates new template (nothing can be <= 0ms old iff time advances)
177 check_cache(/*expect_hit=*/false, default_options, MillisecondsDouble{0});
178
179- // Hit if age < limit
180+ // Hit if age < max_age
181 check_cache(/*expect_hit=*/true, default_options, 2ms);
182
183- // Boundary: age == limit => miss (TimeIntervalElapsed uses >=)
184+ // Miss if age > max_age
185 check_cache(/*expect_hit=*/false, default_options, MillisecondsDouble{1});
186
187+ // Hit if age == max_age
188+ check_cache(/*expect_hit=*/true, default_options, 1ms);
189+
190 // BlockConnected signal with background chainstate role should not invalidate cache
191 kernel::ChainstateRole bg_role{true, true};
192 auto chain_tip = WITH_LOCK(cs_main, return m_node.chainman->ActiveChain().Tip());