@@ -3102,20 +3102,18 @@ BOOST_AUTO_TEST_CASE(mn_lists_cache_bounded)
31023102 dmnman.UpdatedBlockTip (tip_index ());
31033103 dmnman.DoMaintenance ();
31043104
3105- // Mine more than the hard cap without running cleanup — mirrors the
3106- // attacker window between blocks when getmnlistd populates the cache.
3107- constexpr size_t n_blocks = CDeterministicMNManager::MAX_CACHE_LISTS + 64 ;
3105+ // Mine past the recency window and diff cap without running cleanup — mirrors
3106+ // the attacker window between blocks when getmnlistd populates the cache.
3107+ constexpr size_t recency_window = CDeterministicMNManager::MAX_CACHE_DIFFS - 64 ;
3108+ constexpr size_t n_blocks = recency_window + (40 * 32 );
31083109 for (size_t i = 0 ; i < n_blocks; ++i) {
31093110 setup.CreateAndProcessBlock ({}, coinbase_pk);
31103111 dmnman.UpdatedBlockTip (tip_index ());
31113112 }
31123113
31133114 // Record the expected list for a spread of historical heights, and for every
3114- // height in the lowest 64 of the range. Eviction is lowest-height-first, so
3115- // after the descending sweep below (which touches MAX_CACHE_LISTS newer
3116- // heights after each of these), the lowest-64 entries are guaranteed to have
3117- // been evicted; re-querying them proves eviction never changes what is
3118- // returned.
3115+ // height in the lowest 64 of the range. Eviction is lowest-height-first in
3116+ // the recent tier; stale heights land in the LRU stale tier instead.
31193117 const CBlockIndex* tip = tip_index ();
31203118 BOOST_REQUIRE (tip != nullptr );
31213119 const int lowest_height = tip->nHeight - static_cast <int >(n_blocks) + 1 ;
@@ -3133,30 +3131,50 @@ BOOST_AUTO_TEST_CASE(mn_lists_cache_bounded)
31333131 }
31343132 BOOST_REQUIRE (expected.size () > 64 );
31353133
3136- // Now exercise GetListForBlock over every distinct historical height — the
3134+ // Exercise GetListForBlock over every distinct historical height — the
31373135 // getmnlistd / BuildSimplifiedMNListDiff path an unauthenticated peer drives.
31383136 for (int h = tip->nHeight ; h >= 0 && h > tip->nHeight - static_cast <int >(n_blocks); --h) {
31393137 const CBlockIndex* pindex = tip->GetAncestor (h);
31403138 BOOST_REQUIRE (pindex != nullptr );
31413139 (void )dmnman.GetListForBlock (pindex);
31423140 }
31433141
3144- // Pre-fix: each ProcessBlock / historical load appends freely → size > MAX.
3145- // Post-fix: insert-time retention + hard cap keep the cache bounded.
3146- // (The diffs cache stays far below its cap here — its bound only bites on
3147- // walks that read stale diffs back from disk — so only the lists cap is
3148- // driven past its limit by this test.)
31493142 const size_t list_cache_size = dmnman.GetListCacheSize ();
3143+ const size_t diff_cache_size = dmnman.GetListDiffsCacheSize ();
3144+ const size_t stale_cache_size = dmnman.GetStaleListCacheSize ();
31503145 BOOST_TEST_MESSAGE (" mnListsCache size after sweep: " << list_cache_size);
3146+ BOOST_TEST_MESSAGE (" mnListDiffsCache size after sweep: " << diff_cache_size);
3147+ BOOST_TEST_MESSAGE (" mnStaleListsCache size after sweep: " << stale_cache_size);
3148+
31513149 BOOST_CHECK_MESSAGE (list_cache_size <= CDeterministicMNManager::MAX_CACHE_LISTS ,
31523150 strprintf (" mnListsCache size %zu exceeds hard cap %zu" , list_cache_size,
31533151 CDeterministicMNManager::MAX_CACHE_LISTS ));
3154- BOOST_CHECK_LE (dmnman.GetListDiffsCacheSize (), CDeterministicMNManager::MAX_CACHE_DIFFS );
3155-
3156- // The cache is pure memoisation: bounding it must not change any result. The
3157- // lowest-64 entries recorded above have certainly been evicted by now, so
3158- // they are recomputed from disk here — the values must still match what the
3159- // cache returned above.
3152+ BOOST_CHECK_LE (diff_cache_size, CDeterministicMNManager::MAX_CACHE_DIFFS );
3153+ // n_blocks ProcessBlock admissions exceed MAX_CACHE_DIFFS; the cap must have trimmed.
3154+ BOOST_CHECK_MESSAGE (diff_cache_size >= CDeterministicMNManager::MAX_CACHE_DIFFS - 64 ,
3155+ strprintf (" mnListDiffsCache size %zu never approached cap %zu" , diff_cache_size,
3156+ CDeterministicMNManager::MAX_CACHE_DIFFS ));
3157+
3158+ // Stale heights from the sweep must have been routed to the LRU stale tier.
3159+ BOOST_CHECK_GT (stale_cache_size, 0U );
3160+ BOOST_CHECK_LE (stale_cache_size, CDeterministicMNManager::MAX_STALE_CACHE_LISTS );
3161+
3162+ // Repeat access to one stale interval must be cache-served, not a full re-walk.
3163+ const int stale_target_height = tip->nHeight - static_cast <int >(recency_window) - 100 ;
3164+ BOOST_REQUIRE (stale_target_height >= 0 );
3165+ const CBlockIndex* stale_pindex = tip->GetAncestor (stale_target_height);
3166+ BOOST_REQUIRE (stale_pindex != nullptr );
3167+ const auto stale_first = dmnman.GetListForBlock (stale_pindex);
3168+ const size_t stale_size_after_first = dmnman.GetStaleListCacheSize ();
3169+ const CBlockIndex* stale_neighbor = tip->GetAncestor (stale_target_height + 16 );
3170+ BOOST_REQUIRE (stale_neighbor != nullptr );
3171+ const auto stale_neighbor_result = dmnman.GetListForBlock (stale_neighbor);
3172+ const auto stale_second = dmnman.GetListForBlock (stale_pindex);
3173+ BOOST_CHECK (stale_first == stale_second);
3174+ BOOST_CHECK (stale_neighbor_result == dmnman.GetListForBlock (stale_neighbor));
3175+ BOOST_CHECK_LE (dmnman.GetStaleListCacheSize (), stale_size_after_first + 1 );
3176+
3177+ // The cache is pure memoisation: bounding it must not change any result.
31603178 for (const auto & [pindex, want] : expected) {
31613179 const auto got = dmnman.GetListForBlock (pindex);
31623180 BOOST_CHECK_MESSAGE (got == want,
0 commit comments