Skip to main content

reth_storage_overlay/
changeset_cache.rs

1//! Trie changeset caching utilities.
2//!
3//! This module provides functionality to compute trie changesets for a given block,
4//! which represent the old trie node values before the block was processed.
5//!
6//! It also provides an efficient in-memory cache for these changesets, which is essential for:
7//! - **Reorg support**: Quickly access changesets to revert blocks during chain reorganizations
8//! - **Memory efficiency**: Explicit eviction releases persisted changesets
9
10use crate::{database_state_frontiers, OverlayManager, OverlayStateProvider};
11use alloy_eips::BlockNumHash;
12use alloy_primitives::{map::B256Map, BlockNumber, B256};
13use parking_lot::RwLock;
14use reth_metrics::{
15    metrics::{Counter, Gauge},
16    Metrics,
17};
18use reth_primitives_traits::{AlloyBlockHeader, FastInstant as Instant, NodePrimitives};
19use reth_storage_api::{
20    BlockNumReader, ChangeSetReader, DBProvider, PruneCheckpointReader, StageCheckpointReader,
21    StorageChangeSetReader, StorageSettingsCache,
22};
23use reth_storage_errors::provider::{ProviderError, ProviderResult};
24use reth_trie::trie_cursor::{InMemoryTrieCursorFactory, TrieCursor, TrieCursorFactory};
25use reth_trie_common::updates::{StorageTrieUpdatesSorted, TrieUpdatesSorted};
26use reth_trie_db::{DatabaseTrieCursorFactory, TrieTableAdapter};
27use std::{
28    collections::{BTreeMap, HashMap},
29    ops::RangeInclusive,
30    sync::Arc,
31};
32use tracing::{debug, warn};
33
34#[cfg(test)]
35use reth_trie::{changesets::compute_trie_changesets, HashedPostStateSorted, TrieInputSorted};
36#[cfg(test)]
37use reth_trie_db::{DatabaseHashedCursorFactory, DatabaseHashedPostState, DatabaseStateRoot};
38
39/// Computes block trie updates using the changeset cache.
40///
41/// # Algorithm
42///
43/// For block N:
44/// 1. Get cumulative trie reverts from block N+1 to db tip using the cache
45/// 2. Create an overlay cursor factory with these reverts (representing trie state after block N)
46/// 3. Walk through account trie changesets for block N
47/// 4. For each changed path, look up the current value using the overlay cursor
48/// 5. Walk through storage trie changesets for block N
49/// 6. For each changed path, look up the current value using the overlay cursor
50/// 7. Return the collected trie updates
51///
52/// # Arguments
53///
54/// * `provider` - Database provider for accessing changesets and block data
55/// * `block_number` - Block number to compute trie updates for
56///
57/// # Returns
58///
59/// Trie updates representing the state of trie nodes after the block was processed
60///
61/// # Errors
62///
63/// Returns error if:
64/// - Block number exceeds database tip
65/// - Database access fails
66/// - Cache retrieval fails
67pub(crate) fn compute_block_trie_updates<N, Provider>(
68    overlay_manager: &OverlayManager<N>,
69    provider: &Provider,
70    block_number: BlockNumber,
71) -> ProviderResult<TrieUpdatesSorted>
72where
73    N: NodePrimitives,
74    Provider: DBProvider
75        + ChangeSetReader
76        + StorageChangeSetReader
77        + PruneCheckpointReader
78        + StageCheckpointReader
79        + BlockNumReader
80        + StorageSettingsCache,
81{
82    reth_trie_db::with_adapter!(provider, |A| {
83        compute_block_trie_updates_inner::<_, _, A>(overlay_manager, provider, block_number)
84    })
85}
86
87fn compute_block_trie_updates_inner<N, Provider, A>(
88    overlay_manager: &OverlayManager<N>,
89    provider: &Provider,
90    block_number: BlockNumber,
91) -> ProviderResult<TrieUpdatesSorted>
92where
93    N: NodePrimitives,
94    Provider: DBProvider
95        + ChangeSetReader
96        + StorageChangeSetReader
97        + PruneCheckpointReader
98        + StageCheckpointReader
99        + BlockNumReader
100        + StorageSettingsCache,
101    A: TrieTableAdapter,
102{
103    let tx = provider.tx_ref();
104    let cache = overlay_manager.changeset_cache();
105    let (partial_state_trie, finish) = database_state_frontiers(provider)?;
106
107    // Step 1: Get the trie changesets for the target block from cache
108    let changesets = cache.get_or_compute(
109        overlay_manager,
110        provider,
111        block_number,
112        partial_state_trie,
113        finish,
114    )?;
115
116    // Step 2: Get the trie reverts for the state after the target block using the cache
117    let reverts = cache.get_or_compute_range(
118        overlay_manager,
119        provider,
120        (block_number + 1)..=finish.number,
121        partial_state_trie,
122        finish,
123        true,
124    )?;
125
126    // Step 3: Create an InMemoryTrieCursorFactory with the reverts
127    // This gives us the trie state as it was after the target block was processed
128    let db_cursor_factory = DatabaseTrieCursorFactory::<_, A>::new(tx);
129    let cursor_factory = InMemoryTrieCursorFactory::new(db_cursor_factory, &reverts);
130
131    // Step 4: Collect all account trie nodes that changed in the target block
132    let account_nodes_ref = changesets.account_nodes_ref();
133    let mut account_nodes = Vec::with_capacity(account_nodes_ref.len());
134    let mut account_cursor = cursor_factory.account_trie_cursor()?;
135
136    // Iterate over the account nodes from the changesets
137    for (nibbles, _old_node) in account_nodes_ref {
138        // Look up the current value of this trie node using the overlay cursor
139        let node_value = account_cursor.seek_exact(*nibbles)?.map(|(_, node)| node);
140        account_nodes.push((*nibbles, node_value));
141    }
142
143    // Step 5: Collect all storage trie nodes that changed in the target block
144    let mut storage_tries = B256Map::default();
145
146    // Iterate over the storage tries from the changesets
147    for (hashed_address, storage_changeset) in changesets.storage_tries_ref() {
148        let mut storage_cursor = cursor_factory.storage_trie_cursor(*hashed_address)?;
149        let storage_nodes_ref = storage_changeset.storage_nodes_ref();
150        let mut storage_nodes = Vec::with_capacity(storage_nodes_ref.len());
151
152        // Iterate over the storage nodes for this account
153        for (nibbles, _old_node) in storage_nodes_ref {
154            // Look up the current value of this storage trie node
155            let node_value = storage_cursor.seek_exact(*nibbles)?.map(|(_, node)| node);
156            storage_nodes.push((*nibbles, node_value));
157        }
158
159        storage_tries.insert(*hashed_address, StorageTrieUpdatesSorted { storage_nodes });
160    }
161
162    Ok(TrieUpdatesSorted::new(account_nodes, storage_tries))
163}
164
165/// Thread-safe changeset cache.
166///
167/// The `RwLock` enables concurrent reads while ensuring exclusive access for writes.
168#[derive(Debug)]
169pub(crate) struct ChangesetCache {
170    inner: RwLock<ChangesetCacheInner>,
171}
172
173impl Default for ChangesetCache {
174    fn default() -> Self {
175        Self::new()
176    }
177}
178
179impl ChangesetCache {
180    /// Creates a new cache.
181    ///
182    /// The cache has no capacity limit and relies on explicit eviction
183    /// via the `evict()` method to manage memory usage.
184    pub(crate) fn new() -> Self {
185        Self { inner: RwLock::new(ChangesetCacheInner::new()) }
186    }
187
188    /// Evicts changesets for blocks below the given block number.
189    ///
190    /// This should be called after blocks are persisted to the database to free
191    /// memory for changesets that are no longer needed in the cache.
192    ///
193    /// # Arguments
194    ///
195    /// * `up_to_block` - Evict blocks with number < this value. Blocks with number >= this value
196    ///   are retained.
197    pub(crate) fn evict(&self, up_to_block: BlockNumber) {
198        self.inner.write().evict(up_to_block)
199    }
200
201    /// Gets changesets from cache, or computes them on-the-fly if missing.
202    ///
203    /// This is the primary API for retrieving changesets. It checks the cache first, then falls
204    /// back to computing from database state if missing.
205    ///
206    /// # Arguments
207    ///
208    /// * `block_number` - Block number (for cache insertion and logging)
209    /// * `provider` - Database provider for DB access
210    ///
211    /// # Returns
212    ///
213    /// Changesets for the block, either from cache or computed on-the-fly.
214    pub(crate) fn get_or_compute<N, P>(
215        &self,
216        overlay_manager: &OverlayManager<N>,
217        provider: &P,
218        block_number: BlockNumber,
219        partial_state_trie: BlockNumHash,
220        finish: BlockNumHash,
221    ) -> ProviderResult<Arc<TrieUpdatesSorted>>
222    where
223        N: NodePrimitives,
224        P: DBProvider
225            + ChangeSetReader
226            + StorageChangeSetReader
227            + StageCheckpointReader
228            + PruneCheckpointReader
229            + BlockNumReader
230            + StorageSettingsCache,
231    {
232        self.get_or_compute_range(
233            overlay_manager,
234            provider,
235            block_number..=block_number,
236            partial_state_trie,
237            finish,
238            true,
239        )
240    }
241
242    /// Gets or computes trie reverts for a range of blocks.
243    ///
244    /// If all blocks in the range are cached, this method retrieves and accumulates those
245    /// per-block trie changesets (reverts) in reverse order (newest to oldest), so that older
246    /// values take precedence when there are conflicts.
247    ///
248    /// If any block is missing from cache, this falls back to one aggregate database computation
249    /// for the whole range. The aggregate result restores the trie to the state before the range
250    /// and is inserted into the range cache when `write_reverts_to_cache` is set.
251    ///
252    /// # Arguments
253    ///
254    /// * `provider` - Database provider for DB access and block lookups
255    /// * `range` - Block range to accumulate reverts for (inclusive)
256    /// * `write_reverts_to_cache` - Whether to retain the accumulated reverts. Callers serving
257    ///   ranges an untrusted peer chooses pass `false`, since entries stay until their start block
258    ///   is evicted.
259    ///
260    /// # Returns
261    ///
262    /// Accumulated trie reverts for all blocks in the specified range
263    ///
264    /// # Errors
265    ///
266    /// Returns error if:
267    /// - Any block in the range is beyond the database tip
268    /// - Database access fails
269    /// - Block hash lookup fails
270    /// - Changeset computation fails
271    pub(crate) fn get_or_compute_range<N, P>(
272        &self,
273        overlay_manager: &OverlayManager<N>,
274        provider: &P,
275        range: RangeInclusive<BlockNumber>,
276        partial_state_trie: BlockNumHash,
277        finish: BlockNumHash,
278        write_reverts_to_cache: bool,
279    ) -> ProviderResult<Arc<TrieUpdatesSorted>>
280    where
281        N: NodePrimitives,
282        P: DBProvider
283            + ChangeSetReader
284            + StorageChangeSetReader
285            + StageCheckpointReader
286            + PruneCheckpointReader
287            + BlockNumReader
288            + StorageSettingsCache,
289    {
290        let start_block = *range.start();
291        let end_block = *range.end();
292        let timer = Instant::now();
293
294        if end_block > finish.number {
295            return Err(ProviderError::InsufficientChangesets {
296                requested: end_block,
297                available: 0..=finish.number,
298            });
299        }
300
301        debug!(
302            target: "trie::changeset_cache",
303            start_block,
304            end_block,
305            ?partial_state_trie,
306            ?finish,
307            "Starting get_or_compute_range"
308        );
309
310        if start_block > end_block {
311            debug!(
312                target: "trie::changeset_cache",
313                start_block,
314                end_block,
315                "Empty changeset range requested"
316            );
317            return Ok(Arc::new(TrieUpdatesSorted::default()))
318        }
319
320        let end_block_hash = provider.block_hash(end_block)?.ok_or_else(|| {
321            ProviderError::other(std::io::Error::new(
322                std::io::ErrorKind::NotFound,
323                format!("block hash not found for block number {}", end_block),
324            ))
325        })?;
326        let range_key = ChangesetRangeKey::new(start_block, end_block, end_block_hash);
327
328        if let Some(accumulated_reverts) = self.inner.read().get(&range_key) {
329            let elapsed = timer.elapsed();
330
331            debug!(
332                target: "trie::changeset_cache",
333                ?elapsed,
334                start_block,
335                end_block,
336                ?end_block_hash,
337                num_blocks = end_block.saturating_sub(start_block).saturating_add(1),
338                "Changeset cache HIT for block range"
339            );
340
341            return Ok(accumulated_reverts)
342        }
343
344        let mut cached_reverts =
345            Vec::with_capacity(end_block.saturating_sub(start_block).saturating_add(1) as usize);
346        let mut all_cached = true;
347
348        for block_number in range.rev() {
349            // Get the block hash for this block number
350            let block_hash = if block_number == end_block {
351                end_block_hash
352            } else {
353                provider.block_hash(block_number)?.ok_or_else(|| {
354                    ProviderError::other(std::io::Error::new(
355                        std::io::ErrorKind::NotFound,
356                        format!("block hash not found for block number {}", block_number),
357                    ))
358                })?
359            };
360
361            debug!(
362                target: "trie::changeset_cache",
363                block_number,
364                ?block_hash,
365                "Looked up block hash for block number in range"
366            );
367
368            let block_key = ChangesetRangeKey::single(block_number, block_hash);
369            if let Some(changesets) = self.inner.read().get(&block_key) {
370                cached_reverts.push(changesets);
371            } else {
372                all_cached = false;
373                break
374            }
375        }
376
377        if all_cached {
378            // `merge_slice` gives precedence to earlier items, so pass reverts oldest-to-newest.
379            cached_reverts.reverse();
380            let accumulated_reverts = Arc::new(TrieUpdatesSorted::merge_slice(&cached_reverts));
381            let elapsed = timer.elapsed();
382
383            let num_account_nodes = accumulated_reverts.account_nodes_ref().len();
384            let num_storage_tries = accumulated_reverts.storage_tries_ref().len();
385
386            debug!(
387                target: "trie::changeset_cache",
388                ?elapsed,
389                start_block,
390                end_block,
391                num_blocks = end_block.saturating_sub(start_block).saturating_add(1),
392                num_account_nodes,
393                num_storage_tries,
394                "Finished accumulating cached trie reverts for block range"
395            );
396
397            if write_reverts_to_cache {
398                self.inner.write().insert(range_key, Arc::clone(&accumulated_reverts));
399            }
400            return Ok(accumulated_reverts)
401        }
402
403        warn!(
404            target: "trie::changeset_cache",
405            start_block,
406            end_block,
407            "Changeset cache MISS in range, falling back to aggregate DB-based computation"
408        );
409
410        let overlay = overlay_manager
411            .overlay_builder(finish.hash)
412            .with_no_reverts()
413            .build_state_trie_overlay_at_frontiers(provider, partial_state_trie, finish, true)?;
414
415        // Forward updates are a best-effort optimization to speed up compute_range_trie_changesets,
416        // missing blocks do not affect correctness.
417        let mut forward_updates = overlay_manager
418            .parent_chain(finish.hash)
419            .take_while(|block| block.recovered_block().number() >= start_block)
420            .map(|block| (block.recovered_block().number(), block.trie_updates()))
421            .collect::<Vec<_>>();
422        forward_updates.reverse();
423
424        let state_trie_provider = OverlayStateProvider::<&P, N>::new_with_state_trie(
425            provider,
426            overlay,
427            provider.cached_storage_settings().is_v2(),
428        );
429
430        let accumulated_reverts = Arc::new(reth_trie_db::compute_range_trie_changesets(
431            provider,
432            &state_trie_provider,
433            &forward_updates,
434            start_block..=end_block,
435            finish.number,
436        )?);
437
438        let elapsed = timer.elapsed();
439
440        let num_account_nodes = accumulated_reverts.account_nodes_ref().len();
441        let num_storage_tries = accumulated_reverts.storage_tries_ref().len();
442
443        debug!(
444            target: "trie::changeset_cache",
445            ?elapsed,
446            start_block,
447            end_block,
448            ?end_block_hash,
449            num_blocks = end_block.saturating_sub(start_block).saturating_add(1),
450            num_account_nodes,
451            num_storage_tries,
452            "Finished accumulating trie reverts for block range"
453        );
454
455        if write_reverts_to_cache {
456            self.inner.write().insert(range_key, Arc::clone(&accumulated_reverts));
457        }
458
459        Ok(accumulated_reverts)
460    }
461}
462
463/// Cache key for one contiguous range of canonical trie changesets.
464///
465/// The end block hash disambiguates canonical rewrites where the same block numbers later refer to
466/// a different chain. For a single block, `start_block == end_block` and `end_block_hash` is that
467/// block's hash.
468#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
469struct ChangesetRangeKey {
470    start_block: BlockNumber,
471    end_block: BlockNumber,
472    end_block_hash: B256,
473}
474
475impl ChangesetRangeKey {
476    const fn new(start_block: BlockNumber, end_block: BlockNumber, end_block_hash: B256) -> Self {
477        Self { start_block, end_block, end_block_hash }
478    }
479
480    const fn single(block_number: BlockNumber, block_hash: B256) -> Self {
481        Self::new(block_number, block_number, block_hash)
482    }
483}
484
485/// In-memory cache for trie changesets with explicit eviction policy.
486///
487/// Holds changesets for blocks or block ranges that have been validated but not yet persisted.
488/// Keyed by canonical block range. Eviction is controlled
489/// explicitly by the engine API tree handler when persistence completes.
490///
491/// ## Eviction Policy
492///
493/// Unlike traditional caches with automatic eviction, this cache requires explicit
494/// eviction calls. The engine API tree handler calls `evict(block_number)` after
495/// blocks are persisted to the database, ensuring changesets remain available
496/// until their corresponding blocks are safely on disk.
497///
498/// ## Metrics
499///
500/// The cache maintains several metrics for observability:
501/// - `hits`: Number of successful cache lookups
502/// - `misses`: Number of failed cache lookups
503/// - `evictions`: Number of blocks evicted
504/// - `size`: Current number of cached blocks
505#[derive(Debug)]
506struct ChangesetCacheInner {
507    /// Cache entries keyed by inclusive block range plus the range's canonical end hash.
508    entries: HashMap<ChangesetRangeKey, Arc<TrieUpdatesSorted>>,
509
510    /// Range start block to cache keys mapping for eviction.
511    range_starts: BTreeMap<BlockNumber, Vec<ChangesetRangeKey>>,
512
513    /// Metrics for monitoring cache behavior
514    metrics: ChangesetCacheMetrics,
515}
516
517/// Metrics for the changeset cache.
518///
519/// These metrics provide visibility into cache performance and help identify
520/// potential issues like high miss rates.
521#[derive(Metrics, Clone)]
522#[metrics(scope = "trie.changeset_cache")]
523struct ChangesetCacheMetrics {
524    /// Cache hit counter
525    hits: Counter,
526
527    /// Cache miss counter
528    misses: Counter,
529
530    /// Eviction counter
531    evictions: Counter,
532
533    /// Current cache size (number of entries)
534    size: Gauge,
535}
536
537impl Default for ChangesetCacheInner {
538    fn default() -> Self {
539        Self::new()
540    }
541}
542
543impl ChangesetCacheInner {
544    /// Creates a new empty changeset cache.
545    ///
546    /// The cache has no capacity limit and relies on explicit eviction
547    /// via the `evict()` method to manage memory usage.
548    fn new() -> Self {
549        Self { entries: HashMap::new(), range_starts: BTreeMap::new(), metrics: Default::default() }
550    }
551
552    fn get(&self, key: &ChangesetRangeKey) -> Option<Arc<TrieUpdatesSorted>> {
553        match self.entries.get(key) {
554            Some(changesets) => {
555                self.metrics.hits.increment(1);
556                Some(Arc::clone(changesets))
557            }
558            None => {
559                self.metrics.misses.increment(1);
560                None
561            }
562        }
563    }
564
565    fn insert(&mut self, key: ChangesetRangeKey, changesets: Arc<TrieUpdatesSorted>) {
566        debug!(
567            target: "trie::changeset_cache",
568            ?key,
569            cache_size_before = self.entries.len(),
570            "Inserting changeset into cache"
571        );
572
573        let is_new_entry = self.entries.insert(key, changesets).is_none();
574
575        if is_new_entry {
576            self.range_starts.entry(key.start_block).or_default().push(key);
577        }
578
579        // Update size metric
580        self.metrics.size.set(self.entries.len() as f64);
581
582        debug!(
583            target: "trie::changeset_cache",
584            ?key,
585            cache_size_after = self.entries.len(),
586            "Changeset inserted into cache"
587        );
588    }
589
590    fn evict(&mut self, up_to_block: BlockNumber) {
591        debug!(
592            target: "trie::changeset_cache",
593            up_to_block,
594            cache_size_before = self.entries.len(),
595            "Starting cache eviction"
596        );
597
598        // Find all block numbers that should be evicted (< up_to_block)
599        let range_starts_to_evict: Vec<u64> =
600            self.range_starts.range(..up_to_block).map(|(num, _)| *num).collect();
601
602        // Remove entries for each block number below threshold
603        let mut evicted_count = 0;
604
605        for start_block in &range_starts_to_evict {
606            if let Some(keys) = self.range_starts.remove(start_block) {
607                debug!(
608                    target: "trie::changeset_cache",
609                    start_block,
610                    num_ranges = keys.len(),
611                    "Evicting ranges from cache"
612                );
613                for key in keys {
614                    if self.entries.remove(&key).is_some() {
615                        evicted_count += 1;
616                    }
617                }
618            }
619        }
620
621        debug!(
622            target: "trie::changeset_cache",
623            up_to_block,
624            evicted_count,
625            cache_size_after = self.entries.len(),
626            "Finished cache eviction"
627        );
628
629        // Update metrics if we evicted anything
630        if evicted_count > 0 {
631            self.metrics.evictions.increment(evicted_count as u64);
632            self.metrics.size.set(self.entries.len() as f64);
633        }
634    }
635}
636
637#[cfg(test)]
638mod tests {
639    use super::*;
640    use crate::StateTrieOverlay;
641    use alloy_consensus::Header;
642    use alloy_primitives::{
643        keccak256,
644        map::{B256Map, HashMap},
645        Address, U256,
646    };
647    use reth_chain_state::{test_utils::TestBlockBuilder, ExecutedBlock};
648    use reth_db::{
649        models::{AccountBeforeTx, BlockNumberAddress},
650        tables,
651        transaction::DbTxMut,
652    };
653    use reth_primitives_traits::{Account, StorageEntry};
654    use reth_provider::{
655        test_utils::create_test_provider_factory, BlockWriter, StaticFileProviderFactory,
656        StaticFileSegment, StaticFileWriter,
657    };
658    use reth_stages_types::{FinishCheckpoint, StageCheckpoint, StageId};
659    use reth_storage_api::{StageCheckpointWriter, TrieWriter};
660    use reth_trie::{BranchNodeCompact, Nibbles, SortedTrieData, StateRoot};
661
662    // Helper function to create empty TrieUpdatesSorted for testing
663    fn create_test_changesets() -> Arc<TrieUpdatesSorted> {
664        Arc::new(TrieUpdatesSorted::new(vec![], B256Map::default()))
665    }
666
667    fn empty_overlay() -> StateTrieOverlay {
668        StateTrieOverlay::new(TrieInputSorted::default())
669    }
670
671    fn insert_test_changesets(
672        cache: &mut ChangesetCacheInner,
673        block_hash: B256,
674        block_number: BlockNumber,
675        changesets: Arc<TrieUpdatesSorted>,
676    ) {
677        cache.insert(ChangesetRangeKey::single(block_number, block_hash), changesets);
678    }
679
680    fn get_test_changesets(
681        cache: &ChangesetCacheInner,
682        block_hash: B256,
683        block_number: BlockNumber,
684    ) -> Option<Arc<TrieUpdatesSorted>> {
685        cache.get(&ChangesetRangeKey::single(block_number, block_hash))
686    }
687
688    fn test_account(balance: u64) -> Account {
689        Account { balance: U256::from(balance), ..Default::default() }
690    }
691
692    fn test_storage(slot: u64, value: u64) -> StorageEntry {
693        StorageEntry { key: B256::from(U256::from(slot)), value: U256::from(value) }
694    }
695
696    fn seed_headers(
697        factory: &impl StaticFileProviderFactory<
698            Primitives: reth_primitives_traits::NodePrimitives<BlockHeader = Header>,
699        >,
700        end_block: BlockNumber,
701    ) {
702        let static_file_provider = factory.static_file_provider();
703        let mut header_writer =
704            static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
705        for block_number in 0..=end_block {
706            let header = Header { number: block_number, ..Default::default() };
707            header_writer
708                .append_header(&header, &B256::with_last_byte(block_number as u8))
709                .unwrap();
710        }
711        header_writer.commit().unwrap();
712    }
713
714    fn legacy_compute_range_trie_changesets<Provider>(
715        provider: &Provider,
716        range: RangeInclusive<BlockNumber>,
717    ) -> TrieUpdatesSorted
718    where
719        Provider: DBProvider
720            + ChangeSetReader
721            + StorageChangeSetReader
722            + BlockNumReader
723            + StorageSettingsCache,
724    {
725        let mut accumulated_reverts = TrieUpdatesSorted::default();
726        for block_number in range.rev() {
727            let changesets = legacy_compute_block_trie_changesets(provider, block_number);
728            accumulated_reverts.extend_ref_and_sort(&changesets);
729        }
730        accumulated_reverts
731    }
732
733    fn legacy_compute_block_trie_changesets<Provider>(
734        provider: &Provider,
735        block_number: BlockNumber,
736    ) -> TrieUpdatesSorted
737    where
738        Provider: DBProvider
739            + ChangeSetReader
740            + StorageChangeSetReader
741            + BlockNumReader
742            + StorageSettingsCache,
743    {
744        reth_trie_db::with_adapter!(provider, |A| {
745            legacy_compute_block_trie_changesets_inner::<_, A>(provider, block_number)
746        })
747    }
748
749    fn legacy_compute_block_trie_changesets_inner<Provider, A>(
750        provider: &Provider,
751        block_number: BlockNumber,
752    ) -> TrieUpdatesSorted
753    where
754        Provider: DBProvider
755            + ChangeSetReader
756            + StorageChangeSetReader
757            + BlockNumReader
758            + StorageSettingsCache,
759        A: TrieTableAdapter,
760    {
761        let individual_state_revert =
762            HashedPostStateSorted::from_reverts(provider, block_number..=block_number).unwrap();
763        let cumulative_state_revert =
764            HashedPostStateSorted::from_reverts(provider, (block_number + 1)..).unwrap();
765
766        let mut cumulative_state_revert_prev = cumulative_state_revert.clone();
767        cumulative_state_revert_prev.extend_ref_and_sort(&individual_state_revert);
768
769        type DbStateRoot<'a, TX, A> =
770            StateRoot<DatabaseTrieCursorFactory<&'a TX, A>, DatabaseHashedCursorFactory<&'a TX>>;
771
772        let input_prev = TrieInputSorted::new(
773            Arc::default(),
774            Arc::new(cumulative_state_revert_prev.clone()),
775            cumulative_state_revert_prev.construct_prefix_sets(),
776        );
777        let cumulative_trie_updates_prev =
778            DbStateRoot::<_, A>::overlay_root_from_nodes_with_updates(
779                provider.tx_ref(),
780                input_prev,
781            )
782            .unwrap()
783            .1
784            .into_sorted();
785
786        let input = TrieInputSorted::new(
787            Arc::new(cumulative_trie_updates_prev.clone()),
788            Arc::new(cumulative_state_revert),
789            individual_state_revert.construct_prefix_sets(),
790        );
791        let trie_updates =
792            DbStateRoot::<_, A>::overlay_root_from_nodes_with_updates(provider.tx_ref(), input)
793                .unwrap()
794                .1
795                .into_sorted();
796
797        let db_cursor_factory = DatabaseTrieCursorFactory::<_, A>::new(provider.tx_ref());
798        let state_provider_factory =
799            InMemoryTrieCursorFactory::new(db_cursor_factory, &cumulative_trie_updates_prev);
800
801        compute_trie_changesets(&state_provider_factory, &trie_updates).unwrap()
802    }
803
804    fn seed_tip_trie_tables<Provider, A>(provider: &Provider)
805    where
806        Provider: DBProvider + TrieWriter,
807        A: TrieTableAdapter,
808    {
809        type DbStateRoot<'a, TX, A> =
810            StateRoot<DatabaseTrieCursorFactory<&'a TX, A>, DatabaseHashedCursorFactory<&'a TX>>;
811
812        let (_, trie_updates) =
813            DbStateRoot::<_, A>::from_tx(provider.tx_ref()).root_with_updates().unwrap();
814        provider.write_trie_updates(trie_updates).unwrap();
815    }
816
817    #[test]
818    fn consumers_restore_trie_values_masked_by_deferred_blocks() {
819        let manager = OverlayManager::<reth_ethereum_primitives::EthPrimitives>::default();
820        let factory = create_test_provider_factory();
821        let blocks = TestBlockBuilder::eth().get_executed_blocks(0..11).collect::<Vec<_>>();
822        let provider = factory.provider_rw().unwrap();
823        for block in &blocks {
824            provider.insert_block(block.recovered_block()).unwrap();
825        }
826        provider
827            .save_stage_checkpoint(
828                StageId::Finish,
829                StageCheckpoint::new(10)
830                    .with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: Some(5) }),
831            )
832            .unwrap();
833        let path = Nibbles::from_nibbles([1]);
834        let updates = |mask| {
835            Arc::new(TrieUpdatesSorted::new(
836                vec![(path, Some(BranchNodeCompact::new(mask, 0, 0, vec![], None)))],
837                B256Map::default(),
838            ))
839        };
840        let block4 = updates(1);
841        let block9 = updates(2);
842        // Block 9 masks block 4's write, so the persisted trie does not contain the block-5 value.
843        let masked = TrieUpdatesSorted::disjointed_merge_batch(&[&block4], &[&block9]);
844        assert!(masked.is_empty());
845        provider.write_trie_updates_sorted(&masked).unwrap();
846        provider.commit().unwrap();
847        let provider = factory.provider_rw().unwrap();
848        for (start, end, before) in [
849            (4, 4, Arc::new(TrieUpdatesSorted::new(vec![(path, None)], B256Map::default()))),
850            (5, 10, Arc::clone(&block4)),
851            (5, 5, Arc::default()),
852        ] {
853            manager.changeset_cache().inner.write().insert(
854                ChangesetRangeKey::new(start, end, blocks[end as usize].recovered_block().hash()),
855                before,
856            );
857        }
858        let (frontier, finish) = database_state_frontiers(&*provider).unwrap();
859        let overlay = manager
860            .overlay_builder(blocks[4].recovered_block().hash())
861            .build_state_trie_overlay_at_frontiers(&*provider, frontier, finish, true)
862            .unwrap();
863        assert_eq!(overlay.input().nodes, block4);
864        assert_eq!(manager.compute_block_trie_updates(&*provider, 4).unwrap(), *block4);
865
866        // With only per-block entries left, the reverts are merged again on every build, and a
867        // builder that opted out keeps the merged range out of the cache.
868        manager.evict_cached_changesets(11);
869        for number in 5..=10 {
870            insert_test_changesets(
871                &mut manager.changeset_cache().inner.write(),
872                blocks[number as usize].recovered_block().hash(),
873                number,
874                Arc::default(),
875            );
876        }
877        let cached = || manager.changeset_cache().inner.read().entries.len();
878        let builder = manager.overlay_builder(blocks[4].recovered_block().hash());
879        builder
880            .clone()
881            .without_trie_revert_caching()
882            .build_state_trie_overlay_at_frontiers(&*provider, frontier, finish, true)
883            .unwrap();
884        assert_eq!(cached(), 6);
885        builder.build_state_trie_overlay_at_frontiers(&*provider, frontier, finish, true).unwrap();
886        assert_eq!(cached(), 7);
887    }
888
889    #[test]
890    fn calculated_reverts_preserve_deletions_across_frontiers() {
891        let factory = create_test_provider_factory();
892        let blocks = TestBlockBuilder::eth().get_executed_blocks(0..4).collect::<Vec<_>>();
893        let address = Address::with_last_byte(1);
894        let hashed_address = keccak256(address);
895        let removed = Nibbles::from_nibbles([1]);
896        let replaced = Nibbles::from_nibbles([2]);
897        let node = BranchNodeCompact::new(0b0001, 0, 0, vec![], None);
898        let storage_updates = |storage_nodes| {
899            TrieUpdatesSorted::new(
900                vec![],
901                B256Map::from_iter([(hashed_address, StorageTrieUpdatesSorted { storage_nodes })]),
902            )
903        };
904        let finish_updates =
905            Arc::new(storage_updates(vec![(removed, None), (replaced, Some(node.clone()))]));
906        let provider_rw = factory.provider_rw().unwrap();
907        for block in &blocks {
908            provider_rw.insert_block(block.recovered_block()).unwrap();
909        }
910        provider_rw
911            .write_trie_updates_sorted(&storage_updates(vec![(removed, Some(node.clone()))]))
912            .unwrap();
913        // Reverting the account's creation deletes every storage node visible at Finish.
914        provider_rw
915            .tx_ref()
916            .put::<tables::AccountChangeSets>(1, AccountBeforeTx { address, info: None })
917            .unwrap();
918        provider_rw
919            .save_stage_checkpoint(
920                StageId::Finish,
921                StageCheckpoint::new(2)
922                    .with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: Some(1) }),
923            )
924            .unwrap();
925        provider_rw.commit().unwrap();
926
927        let manager = OverlayManager::<reth_ethereum_primitives::EthPrimitives>::default();
928        for (block, updates) in [
929            (&blocks[1], Arc::new(storage_updates(vec![(removed, Some(node))]))),
930            (&blocks[2], Arc::clone(&finish_updates)),
931            (&blocks[3], Arc::default()),
932        ] {
933            manager.insert_block(ExecutedBlock::new(
934                Arc::clone(&block.recovered_block),
935                Arc::clone(&block.execution_output),
936                SortedTrieData::new(Arc::default(), updates),
937            ));
938        }
939        let before = factory.provider().unwrap();
940        let cache = manager.changeset_cache();
941        let (frontier, finish) = database_state_frontiers(&before).unwrap();
942        let result =
943            cache.get_or_compute_range(&manager, &before, 1..=2, frontier, finish, true).unwrap();
944        assert_eq!(
945            result.storage_tries_ref()[&hashed_address].storage_nodes_ref(),
946            &[(removed, None), (replaced, None)],
947        );
948
949        let provider_rw = factory.provider_rw().unwrap();
950        provider_rw.write_trie_updates_sorted(&finish_updates).unwrap();
951        provider_rw.save_stage_checkpoint(StageId::Finish, StageCheckpoint::new(3)).unwrap();
952        provider_rw.commit().unwrap();
953        let after = factory.provider().unwrap();
954        let (frontier, finish) = database_state_frontiers(&after).unwrap();
955        let advanced =
956            cache.get_or_compute_range(&manager, &after, 1..=2, frontier, finish, true).unwrap();
957        assert!(Arc::ptr_eq(&advanced, &result));
958
959        // A cold calculation at the newer frontier must also serve the older reader.
960        cache.evict(2);
961        let advanced =
962            cache.get_or_compute_range(&manager, &after, 1..=2, frontier, finish, true).unwrap();
963        assert_eq!(advanced, result);
964
965        // An older reader must still receive the deletion after the frontier advances.
966        let cached = manager.get_or_compute_cached_changesets_range(&before, 1..=2).unwrap();
967        assert!(Arc::ptr_eq(&cached, &advanced));
968    }
969
970    #[test]
971    fn calculated_historical_range_excludes_tail_reverts() {
972        let factory = create_test_provider_factory();
973        let provider = factory.provider_rw().unwrap();
974        let address = Address::with_last_byte(1);
975        let hashed_address = keccak256(address);
976        let path = Nibbles::from_nibbles([1]);
977        let base_nodes = Arc::new(TrieUpdatesSorted::new(
978            vec![],
979            B256Map::from_iter([(
980                hashed_address,
981                StorageTrieUpdatesSorted {
982                    storage_nodes: vec![(
983                        path,
984                        Some(BranchNodeCompact::new(0b0001, 0, 0, vec![], None)),
985                    )],
986                },
987            )]),
988        ));
989        // Rewinding block 3 deletes a node introduced by the input overlay.
990        provider
991            .tx_ref()
992            .put::<tables::AccountChangeSets>(3, AccountBeforeTx { address, info: None })
993            .unwrap();
994        let state_trie_provider =
995            OverlayStateProvider::<&_, reth_ethereum_primitives::EthPrimitives>::new_with_state_trie(
996                &*provider,
997                StateTrieOverlay::new(TrieInputSorted::new(
998                    Arc::clone(&base_nodes),
999                    Arc::default(),
1000                    Default::default(),
1001                )),
1002                provider.cached_storage_settings().is_v2(),
1003            );
1004
1005        let end = 2;
1006        for range in [1..=end, end + 1..=end] {
1007            let result = reth_trie_db::compute_range_trie_changesets(
1008                &*provider,
1009                &state_trie_provider,
1010                &[(3, Arc::clone(&base_nodes))],
1011                range,
1012                3,
1013            )
1014            .unwrap();
1015            assert!(result.is_empty());
1016        }
1017    }
1018
1019    #[test]
1020    fn persisted_range_retains_transient_nodes_without_executed_blocks() {
1021        let factory = create_test_provider_factory();
1022        seed_headers(&factory, 2);
1023        let provider = factory.provider_rw().unwrap();
1024        provider.save_stage_checkpoint(StageId::Finish, StageCheckpoint::new(2)).unwrap();
1025        let address = Address::with_last_byte(1);
1026        let hashed_address = keccak256(address);
1027        provider
1028            .tx_ref()
1029            .put::<tables::AccountChangeSets>(1, AccountBeforeTx { address, info: None })
1030            .unwrap();
1031        provider
1032            .tx_ref()
1033            .put::<tables::AccountChangeSets>(
1034                2,
1035                AccountBeforeTx { address, info: Some(test_account(1)) },
1036            )
1037            .unwrap();
1038        // The account and its storage exist only at block 1. Enough slots to branch below the first
1039        // nibble ensure the trie stores non-root nodes.
1040        for slot in 0..257 {
1041            provider
1042                .tx_ref()
1043                .put::<tables::StorageChangeSets>(
1044                    BlockNumberAddress((1, address)),
1045                    test_storage(slot, 0),
1046                )
1047                .unwrap();
1048            provider
1049                .tx_ref()
1050                .put::<tables::StorageChangeSets>(
1051                    BlockNumberAddress((2, address)),
1052                    test_storage(slot, 1),
1053                )
1054                .unwrap();
1055        }
1056        let state_trie_provider =
1057            OverlayStateProvider::<&_, reth_ethereum_primitives::EthPrimitives>::new_with_state_trie(
1058                &*provider,
1059                empty_overlay(),
1060                provider.cached_storage_settings().is_v2(),
1061            );
1062        let result = reth_trie_db::compute_range_trie_changesets(
1063            &*provider,
1064            &state_trie_provider,
1065            &[],
1066            1..=2,
1067            2,
1068        )
1069        .unwrap();
1070        let nodes = result.storage_tries_ref()[&hashed_address].storage_nodes_ref();
1071        assert!(!nodes.is_empty());
1072        assert!(nodes.iter().all(|(_, node)| node.is_none()));
1073
1074        let first = reth_trie_db::compute_range_trie_changesets(
1075            &*provider,
1076            &state_trie_provider,
1077            &[],
1078            1..=1,
1079            2,
1080        )
1081        .unwrap();
1082        let second = reth_trie_db::compute_range_trie_changesets(
1083            &*provider,
1084            &state_trie_provider,
1085            &[],
1086            2..=2,
1087            2,
1088        )
1089        .unwrap();
1090        let mut merged = second.clone();
1091        merged.extend_ref_and_sort(&first);
1092        assert_eq!(result, merged);
1093        let forward = [(1, Arc::new(second.clone())), (2, Arc::new(first.clone()))];
1094        assert_eq!(
1095            reth_trie_db::compute_range_trie_changesets(
1096                &*provider,
1097                &state_trie_provider,
1098                &forward,
1099                1..=2,
1100                2
1101            )
1102            .unwrap(),
1103            result,
1104        );
1105        let manager = OverlayManager::<reth_ethereum_primitives::EthPrimitives>::default();
1106        assert_eq!(manager.compute_block_trie_updates(&*provider, 1).unwrap(), second);
1107        assert_eq!(manager.compute_block_trie_updates(&*provider, 2).unwrap(), first);
1108
1109        // Recompute block 1 against its own tip instead of a later tip. Its changeset is identical.
1110        let at_first = OverlayStateProvider::<&_, reth_ethereum_primitives::EthPrimitives>::new_with_state_trie(
1111            &*provider,
1112            StateTrieOverlay::new(TrieInputSorted::new(
1113                Arc::new(second),
1114                Arc::new(HashedPostStateSorted::from_reverts(&*provider, 2..=2).unwrap()),
1115                Default::default(),
1116            )),
1117            provider.cached_storage_settings().is_v2(),
1118        );
1119        let at_first =
1120            reth_trie_db::compute_range_trie_changesets(&*provider, &at_first, &[], 1..=1, 1)
1121                .unwrap();
1122        assert_eq!(first, at_first);
1123    }
1124
1125    #[test]
1126    fn cached_range_merge_keeps_oldest_revert_values() {
1127        let factory = create_test_provider_factory();
1128        seed_headers(&factory, 2);
1129
1130        let provider = factory.provider_rw().unwrap();
1131        provider.save_stage_checkpoint(StageId::Finish, StageCheckpoint::new(2)).unwrap();
1132
1133        let cache = ChangesetCache::new();
1134        let path = Nibbles::from_nibbles([0x1, 0x2]);
1135        let older_node = BranchNodeCompact::new(0b0001, 0, 0, vec![], None);
1136        let newer_node = BranchNodeCompact::new(0b0010, 0, 0, vec![], None);
1137
1138        {
1139            let mut cache = cache.inner.write();
1140            insert_test_changesets(
1141                &mut cache,
1142                B256::with_last_byte(1),
1143                1,
1144                Arc::new(TrieUpdatesSorted::new(
1145                    vec![(path, Some(older_node.clone()))],
1146                    B256Map::default(),
1147                )),
1148            );
1149            insert_test_changesets(
1150                &mut cache,
1151                B256::with_last_byte(2),
1152                2,
1153                Arc::new(TrieUpdatesSorted::new(
1154                    vec![(path, Some(newer_node))],
1155                    B256Map::default(),
1156                )),
1157            );
1158        }
1159
1160        let overlay_manager = OverlayManager::<reth_ethereum_primitives::EthPrimitives>::default();
1161        let (partial_state_trie, finish) = database_state_frontiers(&*provider).unwrap();
1162        let accumulated = cache
1163            .get_or_compute_range(
1164                &overlay_manager,
1165                &*provider,
1166                1..=2,
1167                partial_state_trie,
1168                finish,
1169                true,
1170            )
1171            .unwrap();
1172        assert_eq!(accumulated.account_nodes_ref(), &[(path, Some(older_node))]);
1173    }
1174
1175    #[test]
1176    fn aggregate_range_reverts_to_pre_range_state() {
1177        let factory = create_test_provider_factory();
1178        seed_headers(&factory, 3);
1179
1180        let provider = factory.provider_rw().unwrap();
1181        let address = Address::with_last_byte(1);
1182        let hashed_address = keccak256(address);
1183        let slot1 = B256::with_last_byte(1);
1184        let slot2 = B256::with_last_byte(2);
1185        let account1 = test_account(10);
1186        let account2 = test_account(20);
1187        let account3 = test_account(30);
1188
1189        provider.tx_ref().put::<tables::HashedAccounts>(hashed_address, account3).unwrap();
1190        provider
1191            .tx_ref()
1192            .put::<tables::HashedStorages>(
1193                hashed_address,
1194                StorageEntry { key: keccak256(slot1), value: U256::from(25) },
1195            )
1196            .unwrap();
1197        provider
1198            .tx_ref()
1199            .put::<tables::HashedStorages>(
1200                hashed_address,
1201                StorageEntry { key: keccak256(slot2), value: U256::from(20) },
1202            )
1203            .unwrap();
1204
1205        provider
1206            .tx_ref()
1207            .put::<tables::AccountChangeSets>(1, AccountBeforeTx { address, info: None })
1208            .unwrap();
1209        provider
1210            .tx_ref()
1211            .put::<tables::AccountChangeSets>(2, AccountBeforeTx { address, info: Some(account1) })
1212            .unwrap();
1213        provider
1214            .tx_ref()
1215            .put::<tables::AccountChangeSets>(3, AccountBeforeTx { address, info: Some(account2) })
1216            .unwrap();
1217
1218        provider
1219            .tx_ref()
1220            .put::<tables::StorageChangeSets>(BlockNumberAddress((1, address)), test_storage(1, 0))
1221            .unwrap();
1222        provider
1223            .tx_ref()
1224            .put::<tables::StorageChangeSets>(BlockNumberAddress((1, address)), test_storage(2, 0))
1225            .unwrap();
1226        provider
1227            .tx_ref()
1228            .put::<tables::StorageChangeSets>(
1229                BlockNumberAddress((2, address)),
1230                StorageEntry { key: slot1, value: U256::from(10) },
1231            )
1232            .unwrap();
1233        provider
1234            .tx_ref()
1235            .put::<tables::StorageChangeSets>(
1236                BlockNumberAddress((3, address)),
1237                StorageEntry { key: slot1, value: U256::from(15) },
1238            )
1239            .unwrap();
1240
1241        provider.save_stage_checkpoint(StageId::Finish, StageCheckpoint::new(3)).unwrap();
1242        reth_trie_db::with_adapter!(provider, |A| seed_tip_trie_tables::<_, A>(&*provider));
1243
1244        let overlay = empty_overlay();
1245        let state_trie_provider =
1246            OverlayStateProvider::<&_, reth_ethereum_primitives::EthPrimitives>::new_with_state_trie(
1247                &*provider,
1248                overlay,
1249                provider.cached_storage_settings().is_v2(),
1250            );
1251        let actual = reth_trie_db::compute_range_trie_changesets(
1252            &*provider,
1253            &state_trie_provider,
1254            &[],
1255            1..=3,
1256            3,
1257        )
1258        .unwrap();
1259        assert!(actual.storage_tries_ref().get(&hashed_address).is_none());
1260
1261        let cache = ChangesetCache::new();
1262        let overlay_manager = OverlayManager::<reth_ethereum_primitives::EthPrimitives>::default();
1263        let (partial_state_trie, finish) = database_state_frontiers(&*provider).unwrap();
1264        let uncached = cache
1265            .get_or_compute_range(
1266                &overlay_manager,
1267                &*provider,
1268                1..=3,
1269                partial_state_trie,
1270                finish,
1271                false,
1272            )
1273            .unwrap();
1274        assert_eq!(*uncached, actual);
1275        assert!(cache.inner.read().entries.is_empty());
1276
1277        let from_cache_api = cache
1278            .get_or_compute_range(
1279                &overlay_manager,
1280                &*provider,
1281                1..=3,
1282                partial_state_trie,
1283                finish,
1284                true,
1285            )
1286            .unwrap();
1287        assert_eq!(*from_cache_api, actual);
1288        assert_eq!(cache.inner.read().entries.len(), 1);
1289
1290        let block_changesets = cache
1291            .get_or_compute(&overlay_manager, &*provider, 2, partial_state_trie, finish)
1292            .unwrap();
1293        assert_eq!(*block_changesets, legacy_compute_block_trie_changesets(&*provider, 2));
1294        assert_eq!(cache.inner.read().entries.len(), 2);
1295    }
1296
1297    #[test]
1298    fn aggregate_range_matches_legacy_per_block_merge_with_storage_wipe() {
1299        let factory = create_test_provider_factory();
1300        seed_headers(&factory, 3);
1301
1302        let provider = factory.provider_rw().unwrap();
1303        let address = Address::with_last_byte(1);
1304        let slot1 = B256::with_last_byte(1);
1305        let slot2 = B256::with_last_byte(2);
1306        let account1 = test_account(10);
1307        let account2 = test_account(20);
1308
1309        provider
1310            .tx_ref()
1311            .put::<tables::AccountChangeSets>(1, AccountBeforeTx { address, info: None })
1312            .unwrap();
1313        provider
1314            .tx_ref()
1315            .put::<tables::AccountChangeSets>(2, AccountBeforeTx { address, info: Some(account1) })
1316            .unwrap();
1317        provider
1318            .tx_ref()
1319            .put::<tables::AccountChangeSets>(3, AccountBeforeTx { address, info: Some(account2) })
1320            .unwrap();
1321
1322        provider
1323            .tx_ref()
1324            .put::<tables::StorageChangeSets>(BlockNumberAddress((1, address)), test_storage(1, 0))
1325            .unwrap();
1326        provider
1327            .tx_ref()
1328            .put::<tables::StorageChangeSets>(BlockNumberAddress((1, address)), test_storage(2, 0))
1329            .unwrap();
1330        provider
1331            .tx_ref()
1332            .put::<tables::StorageChangeSets>(
1333                BlockNumberAddress((2, address)),
1334                StorageEntry { key: slot1, value: U256::from(10) },
1335            )
1336            .unwrap();
1337        provider
1338            .tx_ref()
1339            .put::<tables::StorageChangeSets>(
1340                BlockNumberAddress((3, address)),
1341                StorageEntry { key: slot1, value: U256::from(15) },
1342            )
1343            .unwrap();
1344        provider
1345            .tx_ref()
1346            .put::<tables::StorageChangeSets>(
1347                BlockNumberAddress((3, address)),
1348                StorageEntry { key: slot2, value: U256::from(20) },
1349            )
1350            .unwrap();
1351
1352        provider.save_stage_checkpoint(StageId::Finish, StageCheckpoint::new(3)).unwrap();
1353        reth_trie_db::with_adapter!(provider, |A| seed_tip_trie_tables::<_, A>(&*provider));
1354
1355        let expected = legacy_compute_range_trie_changesets(&*provider, 2..=3);
1356        let overlay = empty_overlay();
1357        let state_trie_provider =
1358            OverlayStateProvider::<&_, reth_ethereum_primitives::EthPrimitives>::new_with_state_trie(
1359                &*provider,
1360                overlay,
1361                provider.cached_storage_settings().is_v2(),
1362            );
1363        let actual = reth_trie_db::compute_range_trie_changesets(
1364            &*provider,
1365            &state_trie_provider,
1366            &[],
1367            2..=3,
1368            3,
1369        )
1370        .unwrap();
1371        assert_eq!(actual, expected);
1372    }
1373
1374    #[test]
1375    fn test_eviction_when_explicitly_called() {
1376        let mut cache = ChangesetCacheInner::new();
1377
1378        // Insert 15 blocks (0-14)
1379        let mut hashes = Vec::new();
1380        for i in 0..15 {
1381            let hash = B256::random();
1382            insert_test_changesets(&mut cache, hash, i, create_test_changesets());
1383            hashes.push((i, hash));
1384        }
1385
1386        // All blocks should be present (no automatic eviction)
1387        assert_eq!(cache.entries.len(), 15);
1388
1389        // Explicitly evict blocks < 4
1390        cache.evict(4);
1391
1392        // Blocks 0-3 should be evicted
1393        assert_eq!(cache.entries.len(), 11); // blocks 4-14 = 11 blocks
1394
1395        // Verify blocks 0-3 are evicted
1396        for i in 0..4 {
1397            assert!(
1398                get_test_changesets(&cache, hashes[i as usize].1, i).is_none(),
1399                "Block {} should be evicted",
1400                i
1401            );
1402        }
1403
1404        // Verify blocks 4-14 are still present
1405        for i in 4..15 {
1406            assert!(
1407                get_test_changesets(&cache, hashes[i as usize].1, i).is_some(),
1408                "Block {} should be present",
1409                i
1410            );
1411        }
1412    }
1413
1414    #[test]
1415    fn test_eviction_with_persistence_watermark() {
1416        let mut cache = ChangesetCacheInner::new();
1417
1418        // Insert blocks 100-165
1419        let mut hashes = HashMap::new();
1420        for i in 100..=165 {
1421            let hash = B256::random();
1422            insert_test_changesets(&mut cache, hash, i, create_test_changesets());
1423            hashes.insert(i, hash);
1424        }
1425
1426        // All blocks should be present (no automatic eviction)
1427        assert_eq!(cache.entries.len(), 66);
1428
1429        // Simulate persistence up to block 164, with 64-block retention window
1430        // Eviction threshold = 164 - 64 = 100
1431        cache.evict(100);
1432
1433        // Blocks 100-165 should remain (66 blocks)
1434        assert_eq!(cache.entries.len(), 66);
1435
1436        // Simulate persistence up to block 165
1437        // Eviction threshold = 165 - 64 = 101
1438        cache.evict(101);
1439
1440        // Blocks 101-165 should remain (65 blocks)
1441        assert_eq!(cache.entries.len(), 65);
1442        assert!(get_test_changesets(&cache, hashes[&100], 100).is_none());
1443        assert!(get_test_changesets(&cache, hashes[&101], 101).is_some());
1444    }
1445
1446    #[test]
1447    fn test_out_of_order_inserts_with_explicit_eviction() {
1448        let mut cache = ChangesetCacheInner::new();
1449
1450        // Insert blocks in random order
1451        let hash_10 = B256::random();
1452        insert_test_changesets(&mut cache, hash_10, 10, create_test_changesets());
1453
1454        let hash_5 = B256::random();
1455        insert_test_changesets(&mut cache, hash_5, 5, create_test_changesets());
1456
1457        let hash_15 = B256::random();
1458        insert_test_changesets(&mut cache, hash_15, 15, create_test_changesets());
1459
1460        let hash_3 = B256::random();
1461        insert_test_changesets(&mut cache, hash_3, 3, create_test_changesets());
1462
1463        // All blocks should be present (no automatic eviction)
1464        assert_eq!(cache.entries.len(), 4);
1465
1466        // Explicitly evict blocks < 5
1467        cache.evict(5);
1468
1469        assert!(get_test_changesets(&cache, hash_3, 3).is_none(), "Block 3 should be evicted");
1470        assert!(get_test_changesets(&cache, hash_5, 5).is_some(), "Block 5 should be present");
1471        assert!(get_test_changesets(&cache, hash_10, 10).is_some(), "Block 10 should be present");
1472        assert!(get_test_changesets(&cache, hash_15, 15).is_some(), "Block 15 should be present");
1473    }
1474
1475    #[test]
1476    fn test_ranges_with_same_numbers_and_different_end_hashes_are_distinct() {
1477        let mut cache = ChangesetCacheInner::new();
1478        let path = Nibbles::from_nibbles_unchecked([0x01]);
1479        let hash_a = B256::with_last_byte(1);
1480        let hash_b = B256::with_last_byte(2);
1481        let key_a = ChangesetRangeKey::new(10, 20, hash_a);
1482        let key_b = ChangesetRangeKey::new(10, 20, hash_b);
1483        let changesets_a = Arc::new(TrieUpdatesSorted::new(
1484            vec![(path, Some(BranchNodeCompact::new(0b0001, 0, 0, vec![], None)))],
1485            B256Map::default(),
1486        ));
1487        let changesets_b = Arc::new(TrieUpdatesSorted::new(
1488            vec![(path, Some(BranchNodeCompact::new(0b0010, 0, 0, vec![], None)))],
1489            B256Map::default(),
1490        ));
1491
1492        cache.insert(key_a, Arc::clone(&changesets_a));
1493        cache.insert(key_b, Arc::clone(&changesets_b));
1494
1495        assert_eq!(cache.entries.len(), 2);
1496        assert_eq!(
1497            cache.get(&key_a).unwrap().account_nodes_ref(),
1498            changesets_a.account_nodes_ref()
1499        );
1500        assert_eq!(
1501            cache.get(&key_b).unwrap().account_nodes_ref(),
1502            changesets_b.account_nodes_ref()
1503        );
1504
1505        cache.evict(11);
1506        assert!(cache.get(&key_a).is_none());
1507        assert!(cache.get(&key_b).is_none());
1508    }
1509
1510    #[test]
1511    fn test_eviction_removes_all_side_chains() {
1512        let mut cache = ChangesetCacheInner::new();
1513
1514        // Insert multiple blocks at same height (side chains)
1515        let hash_10a = B256::random();
1516        let hash_10b = B256::random();
1517        let hash_10c = B256::random();
1518        insert_test_changesets(&mut cache, hash_10a, 10, create_test_changesets());
1519        insert_test_changesets(&mut cache, hash_10b, 10, create_test_changesets());
1520        insert_test_changesets(&mut cache, hash_10c, 10, create_test_changesets());
1521
1522        let hash_20 = B256::random();
1523        insert_test_changesets(&mut cache, hash_20, 20, create_test_changesets());
1524
1525        assert_eq!(cache.entries.len(), 4);
1526
1527        // Evict blocks < 15 - should remove all three side chains at height 10
1528        cache.evict(15);
1529
1530        assert_eq!(cache.entries.len(), 1);
1531        assert!(get_test_changesets(&cache, hash_10a, 10).is_none());
1532        assert!(get_test_changesets(&cache, hash_10b, 10).is_none());
1533        assert!(get_test_changesets(&cache, hash_10c, 10).is_none());
1534        assert!(get_test_changesets(&cache, hash_20, 20).is_some());
1535    }
1536}