Skip to main content

ethrex_storage/
layering.rs

1//! # Trie layering: in-memory diff-layers and deep-reorg overlay
2//!
3//! This module implements ethrex's two-tier in-memory trie cache that sits
4//! between block execution and RocksDB. It is the read/write path for all
5//! trie-node and flat-KV accesses during block execution and fork-choice
6//! updates.
7//!
8//! ## Architecture overview
9//!
10//! ```text
11//! ┌──────────────────────────────────────────────────────────┐
12//! │  Block N+2  ──► Block N+1  ──► Block N (cache edge D)   │  TrieLayerCache
13//! │  (newest layer)              (oldest cached layer)       │  (forward diff-layers)
14//! └───────────────────────────┬──────────────────────────────┘
15//!                             │ miss
16//!                             ▼
17//! ┌──────────────────────────────────────────────────────────┐
18//! │  Overlay: reverse-diff [D..pivot+1] on the OLD chain     │  (installed only during
19//! │  exposes the virtual state at `pivot` without touching   │   deep reorgs; None in
20//! │  the on-disk trie.                                       │   steady state)
21//! └───────────────────────────┬──────────────────────────────┘
22//!                             │ miss (or no overlay)
23//!                             ▼
24//! ┌──────────────────────────────────────────────────────────┐
25//! │  RocksDB on-disk state  (account/storage trie+flat KV)   │
26//! └──────────────────────────────────────────────────────────┘
27//! ```
28//!
29//! ## `TrieLayer` — one block's diff
30//!
31//! Each [`TrieLayer`] stores the trie-node writes produced by executing one
32//! block (in regular sync) or one batch of ~1024 blocks (full sync / batch
33//! mode). Layers are linked via a `parent` state-root field to form a
34//! singly-linked chain from newest to oldest.
35//!
36//! ## `TrieLayerCache` — the forward cache
37//!
38//! [`TrieLayerCache`] is a `HashMap<state_root, Arc<TrieLayer>>` with a
39//! bloom filter for fast miss detection. When the chain reaches
40//! `commit_threshold` layers the oldest eligible layer is flushed to
41//! RocksDB and removed from the map. Two thresholds are used:
42//! - **128** — regular block-by-block execution.
43//! - **4** — full sync / batch mode (one layer ≈ 1 GB of state diffs).
44//!
45//! ## `Overlay` — the deep-reorg bridge
46//!
47//! When a fork-choice update targets a head whose ancestor state was flushed
48//! past the layer-cache edge `D`, ethrex builds an [`Overlay`] by replaying
49//! the [`STATE_HISTORY`](crate::api::tables::STATE_HISTORY) journal entries
50//! for blocks `[D, D-1, ..., pivot+1]` in descending order.  The overlay
51//! holds the accumulated reverse-diff, exposing the virtual state at `pivot`
52//! without mutating RocksDB.
53//!
54//! ## `TrieWrapper::get` — the read cascade
55//!
56//! [`TrieWrapper`] is the [`ethrex_trie::TrieDB`] implementation used during
57//! block execution. Its `get` method follows a strict priority order:
58//!
59//! 1. **Layer cache** — forward layers on the new chain (keyed by state-root
60//!    chain from the executing block back to the oldest in-memory layer).
61//! 2. **Overlay** — if installed, the reverse-diff that reconstructs the
62//!    pivot state. A layer hit pre-empts the overlay; an overlay hit pre-empts
63//!    disk. `Some(None)` from the overlay means the key was absent at the
64//!    pivot (caller must treat as missing, not fall through to disk, because
65//!    disk still holds the old chain's value).
66//! 3. **Disk** — RocksDB, queried only when both cache and overlay miss.
67//!
68//! ## Cache swap on deep reorg
69//!
70//! [`Store::install_overlay_for_reorg`](crate::store::Store::install_overlay_for_reorg)
71//! atomically replaces the layer cache with a fresh empty cache that has the
72//! newly built overlay pre-installed. Side-chain blocks `[pivot+1 .. new_head]`
73//! are then executed via the normal `add_block` path; each block's reads
74//! cascade through the overlay and each commit adds a new forward layer.
75//! On the first commit the reconciliation step folds the overlay entries and
76//! the new layer together into a single atomic RocksDB write batch, then
77//! clears the overlay.
78
79use ethrex_common::{H256, types::BlockNumber};
80use fastbloom::AtomicBloomFilter;
81use rayon::prelude::*;
82use rustc_hash::{FxBuildHasher, FxHashMap};
83use std::{
84    fmt,
85    sync::{Arc, RwLock},
86};
87
88use ethrex_trie::{Nibbles, TrieDB, TrieError};
89
90use crate::{
91    api::{StorageBackend, tables::STATE_HISTORY},
92    error::StoreError,
93    journal::{JournalDecodeError, JournalEntry},
94    trie::classify_trie_key,
95};
96
97const BLOOM_SIZE: usize = 1_000_000;
98const FALSE_POSITIVE_RATE: f64 = 0.02;
99
100#[derive(Debug, Clone)]
101struct TrieLayer {
102    nodes: FxHashMap<Vec<u8>, Vec<u8>>,
103    parent: H256,
104    id: usize,
105    /// Number of the block whose post-state this layer represents. Used by the
106    /// journal write path so a commit can record the entry under the correct
107    /// block number (not the in-flight block whose insertion triggered the commit).
108    block_number: BlockNumber,
109    /// Hash of the block whose post-state this layer represents.
110    block_hash: H256,
111}
112
113/// In-memory cache of trie diff-layers, one per block (or batch in full sync), forming a
114/// newest->oldest chain via `parent` down to the on-disk state.
115///
116/// Disk commits are gated on a canonical safe-commit root (`safe_commit_root`): a layer is
117/// flushed only when that root is canonical and deep enough that it lands on the ancestor
118/// walk. `H256::zero()` means "no safe commit point yet", so nothing is flushed and the
119/// on-disk genesis state is never pruned. A global bloom filter short-circuits lookups for
120/// keys absent from every layer.
121#[derive(Clone)]
122pub struct TrieLayerCache {
123    /// Monotonically increasing ID for layers, starting at 1.
124    /// TODO: this implementation panics on overflow
125    last_id: usize,
126    /// Number of layers after which we should commit to the database.
127    pub(crate) commit_threshold: usize,
128    layers: FxHashMap<H256, Arc<TrieLayer>>,
129    /// Global bloom filter that tracks all keys across all layers.
130    ///
131    /// Used to avoid looking up all layers when the given path doesn't exist in any
132    /// layer, thus going directly to the database.
133    bloom: AtomicBloomFilter<FxBuildHasher>,
134    /// Optional in-memory overlay bridging on-disk state at the cache edge `D` to the
135    /// virtual state at a deep-reorg pivot. When installed, reads that miss the layer
136    /// chain consult the overlay before falling through to disk. `None` in steady state.
137    overlay: Option<Arc<Overlay>>,
138    /// The canonical safe-commit state root, computed by the Store after each forkchoice update.
139    ///
140    /// `H256::zero()` means "no safe commit point yet". Read by
141    /// [`get_commitable`](Self::get_commitable) to gate disk commits.
142    pub(crate) safe_commit_root: Arc<RwLock<H256>>,
143}
144
145impl fmt::Debug for TrieLayerCache {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        let safe_commit = match self.safe_commit_root.read() {
148            Ok(guard) => format!("{:?}", *guard),
149            Err(_) => "<poisoned>".to_string(),
150        };
151        f.debug_struct("TrieLayerCache")
152            .field("last_id", &self.last_id)
153            .field("commit_threshold", &self.commit_threshold)
154            .field("layers", &self.layers)
155            .field("bloom", &"AtomicBloomFilter")
156            .field("overlay", &self.overlay)
157            .field("safe_commit_root", &safe_commit)
158            .finish()
159    }
160}
161
162impl Default for TrieLayerCache {
163    fn default() -> Self {
164        Self {
165            bloom: Self::create_filter(BLOOM_SIZE),
166            last_id: 0,
167            layers: Default::default(),
168            // TODO (issue #6345): this is coupled with DB_COMMIT_THRESHOLD in store.rs — unify them.
169            commit_threshold: 128,
170            overlay: None,
171            safe_commit_root: Arc::new(RwLock::new(H256::zero())),
172        }
173    }
174}
175
176impl TrieLayerCache {
177    /// Creates a new cache with the given commit threshold and a shared safe-commit-root cell.
178    ///
179    /// The `safe_commit_root` Arc is shared with [`Store`](crate::Store) so that the Store
180    /// can update the cell without replacing the cache Arc.
181    /// `H256::zero()` in the cell means "no safe commit point yet".
182    pub fn new_with_safe_commit(
183        commit_threshold: usize,
184        safe_commit_root: Arc<RwLock<H256>>,
185    ) -> Self {
186        Self {
187            bloom: Self::create_filter(BLOOM_SIZE),
188            last_id: 0,
189            layers: Default::default(),
190            commit_threshold,
191            overlay: None,
192            safe_commit_root,
193        }
194    }
195
196    /// Installs an overlay on this cache. Subsequent reads that miss the layer chain
197    /// will consult the overlay before falling through to disk. Replaces any
198    /// previously-installed overlay.
199    pub fn set_overlay(&mut self, overlay: Arc<Overlay>) {
200        self.overlay = Some(overlay);
201    }
202
203    /// Removes any installed overlay. Idempotent.
204    pub fn clear_overlay(&mut self) {
205        self.overlay = None;
206    }
207
208    /// Returns a reference to the installed overlay, if any.
209    pub fn overlay(&self) -> Option<&Arc<Overlay>> {
210        self.overlay.as_ref()
211    }
212
213    /// Whether a reader at `state_root` should consult the installed overlay.
214    ///
215    /// The overlay reconstructs the pivot's state ([`Overlay::serves_root`]); the
216    /// new-chain layers built on top of it during replay live in `self.layers`. Only
217    /// those "consuming" roots may see overlay values ; every other reader (an
218    /// eth_call/getProof at the old cache-edge `D`, or any unrelated historical root)
219    /// must fall through to disk, which still holds that root's canonical state while
220    /// the overlay is alive. Returns `false` when no overlay is installed.
221    pub fn overlay_serves(&self, state_root: H256) -> bool {
222        self.overlay
223            .as_ref()
224            .is_some_and(|o| state_root == o.serves_root() || self.layers.contains_key(&state_root))
225    }
226
227    /// Looks up `key` in the installed overlay. Three-state return:
228    /// - `None` ; no overlay installed, or overlay does not contain the key. Caller
229    ///   should fall through to disk.
230    /// - `Some(None)` ; overlay says the key did not exist at the pivot. Caller
231    ///   should treat as missing without consulting disk (disk still holds the OLD
232    ///   chain's value).
233    /// - `Some(Some(v))` ; overlay says the key had value `v` at the pivot. Caller
234    ///   should return `v` without consulting disk.
235    ///
236    /// CF is determined by the key's length, matching `BackendTrieDB::table_for_key`.
237    pub fn lookup_overlay(&self, key: &[u8]) -> Option<Option<Vec<u8>>> {
238        let overlay = self.overlay.as_ref()?;
239        let cf = OverlayCf::classify_by_key_length(key.len());
240        overlay.lookup(cf, key)
241    }
242
243    /// Returns true if a layer with the given `state_root` is present in the cache.
244    /// Used by callers (engine API, deep-reorg orchestrator) to decide whether a
245    /// parent state is reachable through forward execution or requires overlay
246    /// construction.
247    pub fn contains(&self, state_root: H256) -> bool {
248        self.layers.contains_key(&state_root)
249    }
250
251    /// Returns this cache's commit threshold. Used by the deep-reorg path so a
252    /// freshly-constructed replacement cache inherits the same threshold.
253    pub fn commit_threshold(&self) -> usize {
254        self.commit_threshold
255    }
256
257    fn create_filter(expected_items: usize) -> AtomicBloomFilter<FxBuildHasher> {
258        AtomicBloomFilter::with_false_pos(FALSE_POSITIVE_RATE)
259            .hasher(FxBuildHasher)
260            .expected_items(expected_items.max(BLOOM_SIZE))
261    }
262
263    /// Looks up a trie node `key` starting from the layer identified by `state_root`,
264    /// walking the parent chain toward older layers.
265    ///
266    /// Returns `Some(value)` from the first (newest) layer that contains the key, or `None`
267    /// if no layer has it. A bloom filter is checked first to skip the walk entirely when the
268    /// key is guaranteed absent from all layers (callers then fall through to the on-disk trie).
269    pub fn get(&self, state_root: H256, key: &[u8]) -> Option<Vec<u8>> {
270        // Fast check to know if any layer may contain the given key.
271        // We can only be certain it doesn't exist, but if it returns true it may or may not exist (false positive).
272        if !self.bloom.contains(key) {
273            // TrieWrapper goes to db when returning None.
274            return None;
275        }
276
277        let mut current_state_root = state_root;
278
279        while let Some(layer) = self.layers.get(&current_state_root) {
280            if let Some(value) = layer.nodes.get(key) {
281                return Some(value.clone());
282            }
283            current_state_root = layer.parent;
284            if current_state_root == state_root {
285                // TODO: check if this is possible in practice
286                // This can't happen in L1, due to system contracts irreversibly modifying state
287                // at each block.
288                // On L2, if no transactions are included in a block, the state root remains the same,
289                // but we handle that case in put_batch. It may happen, however, if someone modifies
290                // state with a privileged tx and later reverts it (since it doesn't update nonce).
291                panic!("State cycle found");
292            }
293        }
294        None
295    }
296
297    /// Determines whether a disk commit should happen by checking whether the safe-commit root
298    /// appears on the ancestor chain starting from `parent_state_root`.
299    ///
300    /// Returns `Some(safe_commit_root)` when the root is found on the ancestor walk; `None`
301    /// when the cell is zero, poisoned, or the root is not on the walk. The bounded-walk
302    /// cycle guard caps the walk at `layers.len()` steps to ensure termination.
303    pub fn get_commitable(&self, parent_state_root: H256) -> Option<H256> {
304        // (a) Read the safe-commit root; a poisoned lock is treated as "not ready".
305        let safe_root = *self.safe_commit_root.read().ok()?;
306        // (b) Zero means no safe commit point yet; commit nothing.
307        if safe_root.is_zero() {
308            return None;
309        }
310        // (c) The executed parent IS the safe-commit root. Still requires a layer: a
311        // canonical root need not have one (`put_batch` skips blocks whose state root equals
312        // their parent's, which on L2 is every empty block). Branch (d) gets this for free
313        // by walking `layers`.
314        if parent_state_root == safe_root {
315            return self.has_layer(safe_root).then_some(safe_root);
316        }
317        // (d) Walk the layer parent-chain from parent_state_root looking for safe_root.
318        let mut current = parent_state_root;
319        let mut steps = 0usize;
320        let max_steps = self.layers.len();
321        while let Some(layer) = self.layers.get(&current) {
322            if current == safe_root {
323                return Some(safe_root);
324            }
325            let next = layer.parent;
326            // Cycle guard: if walking would return to the walk start, stop.
327            if next == parent_state_root {
328                return None;
329            }
330            steps += 1;
331            // Bounded-walk safeguard: a mid-chain cycle (e.g. B→C→B) would not be
332            // caught by the start-of-walk guard above. Capping at layers.len() steps
333            // ensures the loop always terminates.
334            if steps > max_steps {
335                return None;
336            }
337            current = next;
338        }
339        // (e) Reached chain bottom (root not in layers / already on disk) without matching safe_root.
340        None
341    }
342
343    /// Depth-only commit gate for single-canonical-chain re-execution (full sync, block
344    /// import, startup state regeneration).
345    ///
346    /// Walks the parent chain from `state_root`, counting layers, and returns the state root of
347    /// the layer that is `threshold` layers deep — committing purely by depth, ignoring the
348    /// canonical [`safe_commit_root`](Self::safe_commit_root) cell.
349    ///
350    /// Used only where the node extends a single canonical chain (these paths never execute
351    /// competing forks), so the non-canonical-commit hazard that the canonical gate guards
352    /// against cannot occur. The canonical gate keys on the `head - 128` safe-commit root, which
353    /// these paths never advance (nothing is canonicalized until a later forkchoice update), so
354    /// it would never flush during re-execution; this depth gate bounds memory instead.
355    pub(crate) fn get_commitable_by_depth(
356        &self,
357        mut state_root: H256,
358        threshold: usize,
359    ) -> Option<H256> {
360        let mut counter = 0;
361        while let Some(layer) = self.layers.get(&state_root) {
362            counter += 1;
363            if counter >= threshold {
364                return Some(state_root);
365            }
366            state_root = layer.parent;
367        }
368        None
369    }
370
371    /// Inserts a new diff-layer into the cache, keyed by `state_root` and pointing to `parent`.
372    ///
373    /// In regular sync each call adds one block's trie diffs. In full sync (batch mode), each
374    /// call adds diffs for an entire batch of ~1024 blocks.
375    ///
376    /// No-ops if `parent == state_root` (empty block with no state change), or if `state_root`
377    /// is already present (duplicate insertion guard).
378    pub fn put_batch(
379        &mut self,
380        parent: H256,
381        state_root: H256,
382        block_number: BlockNumber,
383        block_hash: H256,
384        key_values: Vec<(Nibbles, Vec<u8>)>,
385    ) {
386        if parent == state_root && key_values.is_empty() {
387            return;
388        } else if parent == state_root {
389            // L1 always changes the state root (system contracts run even on empty blocks), so
390            // this should not happen there. L2 can legitimately keep the same root on empty blocks
391            // because it has no system contract calls.
392            tracing::trace!("parent == state_root but key_values not empty");
393            return;
394        }
395        if self.layers.contains_key(&state_root) {
396            tracing::warn!("tried to insert a state_root that's already inserted");
397            return;
398        }
399
400        // Add keys to the global bloom filter
401        for (p, _) in &key_values {
402            self.bloom.insert(p.as_ref());
403        }
404
405        let nodes: FxHashMap<Vec<u8>, Vec<u8>> = key_values
406            .into_iter()
407            .map(|(path, value)| (path.into_vec(), value))
408            .collect();
409
410        self.last_id += 1;
411        let entry = TrieLayer {
412            nodes,
413            parent,
414            id: self.last_id,
415            block_number,
416            block_hash,
417        };
418        self.layers.insert(state_root, Arc::new(entry));
419    }
420
421    /// Rebuilds the global bloom filter from scratch using all keys across all remaining layers.
422    ///
423    /// Called after [`commit`](Self::commit) removes layers, since the old filter may contain
424    /// keys from the removed layers (producing unnecessary false positives).
425    pub fn rebuild_bloom(&mut self) {
426        // Pre-compute total keys for optimal filter sizing
427        let total_keys: usize = self.layers.values().map(|layer| layer.nodes.len()).sum();
428
429        let filter = Self::create_filter(total_keys.max(BLOOM_SIZE));
430
431        // Parallel insertion - AtomicBloomFilter allows concurrent insert via &self
432        self.layers.par_iter().for_each(|(_, layer)| {
433            for path in layer.nodes.keys() {
434                filter.insert(path);
435            }
436        });
437
438        self.bloom = filter;
439    }
440
441    /// Whether `state_root` has a diff layer. Not every canonical root does: [`Self::put_batch`]
442    /// skips blocks whose state root equals their parent's, and flushed roots are pruned. Callers
443    /// holding a root from outside the cache must check this before treating it as committable.
444    pub fn has_layer(&self, state_root: H256) -> bool {
445        self.layers.contains_key(&state_root)
446    }
447
448    /// Number of diff layers held in memory. Diagnostic only: distinguishes a pruned root from
449    /// an empty cache.
450    pub fn layer_count(&self) -> usize {
451        self.layers.len()
452    }
453
454    /// Returns the root of the oldest layer in the layer chain containing `root` — i.e.
455    /// walks parents until reaching a layer whose parent is not itself a layer. Returns
456    /// `root` unchanged if it has no layer.
457    ///
458    /// Used to force single-layer commits while a deep-reorg overlay is installed: the
459    /// The reconciliation in `commit_to_disk` is defined for exactly one layer at
460    /// the pivot tip `T`, and a multi-layer sweep would journal upper layers' pre-images
461    /// against the old-chain disk state instead of the new-chain/bridge state.
462    pub(crate) fn bottom_layer_root(&self, root: H256) -> H256 {
463        let mut current = root;
464        while let Some(layer) = self.layers.get(&current) {
465            if !self.layers.contains_key(&layer.parent) {
466                break;
467            }
468            current = layer.parent;
469        }
470        current
471    }
472
473    /// Removes the layer at `state_root` and all its ancestors from the cache, returning
474    /// one [`CommittedLayer`] per removed layer in oldest-first order (suitable for
475    /// sequential disk write and per-block journaling).
476    ///
477    /// `state_root` must be a key in `self.layers` (as returned by
478    /// [`get_commitable`](Self::get_commitable) /
479    /// [`get_commitable_with_threshold`](Self::get_commitable_with_threshold)).
480    /// If it isn't, the walk exits immediately and returns `None`.
481    ///
482    /// After removal, any orphaned layers (older than the committed ones) are pruned, and
483    /// the bloom filter is rebuilt to remove stale entries.
484    ///
485    /// Normal block-by-block sync commits exactly one layer per call. Multi-layer commits
486    /// are legitimate for the forkchoice-driven flush of an accumulated backlog (e.g. block
487    /// import, which executes many blocks and then advances the safe-commit root once): each
488    /// committed layer keeps its own block identity and `parent_state_root`, so the caller can
489    /// write one journal entry per block rather than merging diffs across blocks.
490    pub fn commit(&mut self, state_root: H256) -> Option<Vec<CommittedLayer>> {
491        let mut layers_to_commit = vec![];
492        let mut current_state_root = state_root;
493        while let Some(layer) = self.layers.remove(&current_state_root) {
494            let layer = Arc::unwrap_or_clone(layer);
495            current_state_root = layer.parent;
496            layers_to_commit.push(layer);
497        }
498        // `layers_to_commit` is built by walking parent links from `state_root`,
499        // so `.first()` is the newest layer (the one at `state_root` itself).
500        let top_layer_id = layers_to_commit.first()?.id;
501        // older layers are useless
502        self.layers.retain(|_, item| item.id > top_layer_id);
503        self.rebuild_bloom(); // layers removed, rebuild global bloom filter.
504        // Oldest-first: apply/journal in block order so per-block reverse diffs are
505        // correct and newer writes overwrite older ones on disk.
506        let committed = layers_to_commit
507            .into_iter()
508            .rev()
509            .map(|layer| CommittedLayer {
510                block_number: layer.block_number,
511                block_hash: layer.block_hash,
512                parent_state_root: layer.parent,
513                nodes: layer.nodes.into_iter().collect(),
514            })
515            .collect();
516        Some(committed)
517    }
518}
519
520/// One committed layer produced by [`TrieLayerCache::commit`]: a single block's identity plus
521/// the trie/flat-KV node diffs it wrote. Returned oldest-first so callers apply them in block
522/// order and journal each block separately.
523///
524/// `parent_state_root` is the state we'd return to on rollback (this block's pre-state).
525#[derive(Debug)]
526pub struct CommittedLayer {
527    /// Block number of the committed block.
528    pub block_number: BlockNumber,
529    /// Block hash of the committed block.
530    pub block_hash: H256,
531    /// Pre-state root of the committed block (the state to return to on rollback).
532    pub parent_state_root: H256,
533    /// Merged trie node updates in oldest-first order, ready for a sequential disk write.
534    pub nodes: Vec<(Vec<u8>, Vec<u8>)>,
535}
536
537/// [`TrieDB`] adapter that checks in-memory diff-layers ([`TrieLayerCache`]) first,
538/// falling back to the on-disk trie only for keys not found in any layer.
539///
540/// Used by the EVM during block execution: reads see the latest uncommitted state without
541/// waiting for a disk flush.
542pub struct TrieWrapper {
543    /// State root of the executing block; used as the starting point for the layer-chain walk.
544    pub state_root: H256,
545    /// Shared reference to the layer cache. Multiple `TrieWrapper` instances (per account/storage
546    /// trie) share the same cache within a single block execution context.
547    pub inner: Arc<TrieLayerCache>,
548    /// The underlying on-disk trie, consulted only when both the layer cache and the overlay miss.
549    pub db: Box<dyn TrieDB>,
550    /// Pre-computed prefix nibbles for storage tries.
551    /// For state tries this is None; for storage tries this is
552    /// `Nibbles::from_bytes(address.as_bytes()).append_new(17)`.
553    prefix_nibbles: Option<Nibbles>,
554}
555
556impl TrieWrapper {
557    /// Constructs a `TrieWrapper`. `prefix` is `Some(account_hash)` for storage tries;
558    /// pass `None` for the state trie.
559    pub fn new(
560        state_root: H256,
561        inner: Arc<TrieLayerCache>,
562        db: Box<dyn TrieDB>,
563        prefix: Option<H256>,
564    ) -> Self {
565        let prefix_nibbles = prefix.map(|p| Nibbles::from_bytes(p.as_bytes()).append_new(17));
566        Self {
567            state_root,
568            inner,
569            db,
570            prefix_nibbles,
571        }
572    }
573}
574
575/// Prepends an account address prefix (with an invalid nibble `17` as separator) to a
576/// trie path, distinguishing storage trie entries from state trie entries in the flat
577/// key-value namespace. Returns the path unchanged if `prefix` is `None` (state trie).
578pub fn apply_prefix(prefix: Option<H256>, path: Nibbles) -> Nibbles {
579    match prefix {
580        Some(prefix) => Nibbles::from_bytes(prefix.as_bytes())
581            .append_new(17)
582            .concat(&path),
583        None => path,
584    }
585}
586
587impl TrieDB for TrieWrapper {
588    fn flatkeyvalue_computed(&self, key: Nibbles) -> bool {
589        // While a deep-reorg overlay serves this root, flat-KV leaf reads must not
590        // trust disk: journal entries written while the FKV generator was running
591        // are permanently missing pre-images for keys past the generator frontier,
592        // and disk flat-KV may hold the generator's value for the chain
593        // being reorged away. Force the trie-node read path instead — trie nodes
594        // are always journaled, so the overlay reconstructs them completely.
595        if self.inner.overlay_serves(self.state_root) {
596            return false;
597        }
598        // NOTE: we apply the prefix here, since the underlying TrieDB should
599        // always be for the state trie.
600        let key = match &self.prefix_nibbles {
601            Some(prefix) => prefix.concat(&key),
602            None => key,
603        };
604        self.db.flatkeyvalue_computed(key)
605    }
606
607    fn get(&self, key: Nibbles) -> Result<Option<Vec<u8>>, TrieError> {
608        let key = match &self.prefix_nibbles {
609            Some(prefix) => prefix.concat(&key),
610            None => key,
611        };
612        // Read cascade: forward layer cache (new-chain layers above the pivot) ->
613        // overlay (reverse-diff bridge to disk during deep reorgs, if installed) ->
614        // on-disk state. A layer-cache hit pre-empts the overlay because a
615        // side-chain write at this key supersedes the pivot value the overlay holds.
616        // An overlay hit pre-empts disk because disk still reflects the OLD chain's
617        // edge `D`, not the pivot.
618        if let Some(value) = self.inner.get(self.state_root, key.as_ref()) {
619            return Ok(Some(value));
620        }
621        // Overlay gate: the overlay reconstructs the pivot's state and the new-chain
622        // layers built on top of it. Only a reader at a consuming root (the pivot or one
623        // of those layer roots) may see it; an eth_call/getProof at the old cache-edge
624        // `D` or an unrelated historical root must fall through to disk, which is
625        // unchanged during the overlay window. See [`TrieLayerCache::overlay_serves`].
626        if self.inner.overlay_serves(self.state_root)
627            && let Some(overlay_result) = self.inner.lookup_overlay(key.as_ref())
628        {
629            return Ok(overlay_result);
630        }
631        self.db.get(key)
632    }
633
634    fn put_batch(&self, _key_values: Vec<(Nibbles, Vec<u8>)>) -> Result<(), TrieError> {
635        // TODO: Get rid of this.
636        unimplemented!("This function should not be called");
637    }
638}
639
640// ===========================================================================
641// Overlay ; in-memory aggregated reverse-diff used during deep reorgs.
642// ===========================================================================
643
644/// Identifier of which on-disk column family an [`Overlay`] entry targets.
645#[derive(Debug, Clone, Copy, PartialEq, Eq)]
646pub enum OverlayCf {
647    /// Non-leaf nodes of the account/state trie (`ACCOUNT_TRIE_NODES` CF, key length < 65).
648    AccountTrie,
649    /// Non-leaf nodes of storage tries (`STORAGE_TRIE_NODES` CF, key length 66-130).
650    StorageTrie,
651    /// Leaf entries of the account flat-KV table (`ACCOUNT_FLATKEYVALUE` CF, key length 65).
652    AccountFlat,
653    /// Leaf entries of the storage flat-KV table (`STORAGE_FLATKEYVALUE` CF, key length 131).
654    StorageFlat,
655}
656
657impl OverlayCf {
658    /// Classifies an on-disk key into its CF based on length, matching the rule in
659    /// `BackendTrieDB::table_for_key` / `classify_trie_key`:
660    /// - `len == 65` -> `AccountFlat` (account leaf)
661    /// - `len == 131` -> `StorageFlat` (storage leaf, includes 32-byte account prefix)
662    /// - `len < 65` -> `AccountTrie` (non-leaf state-trie node)
663    /// - otherwise -> `StorageTrie` (non-leaf storage-trie node)
664    pub fn classify_by_key_length(len: usize) -> Self {
665        let (is_leaf, is_account) = classify_trie_key(len);
666        match (is_leaf, is_account) {
667            (true, true) => OverlayCf::AccountFlat,
668            (true, false) => OverlayCf::StorageFlat,
669            (false, true) => OverlayCf::AccountTrie,
670            (false, false) => OverlayCf::StorageTrie,
671        }
672    }
673}
674
675/// Errors produced while constructing an [`Overlay`] from the on-disk journal.
676#[derive(Debug, thiserror::Error)]
677pub enum OverlayError {
678    #[error("missing journal entry for block {0}")]
679    MissingEntry(BlockNumber),
680    #[error(
681        "journal block_hash mismatch at block {block_number}: expected {expected:?}, found {found:?}"
682    )]
683    HashMismatch {
684        block_number: BlockNumber,
685        expected: H256,
686        found: H256,
687    },
688    #[error("invalid overlay range: from_block ({from_block}) < to_block ({to_block})")]
689    InvalidRange {
690        from_block: BlockNumber,
691        to_block: BlockNumber,
692    },
693    #[error("journal decode error: {0}")]
694    Decode(#[from] JournalDecodeError),
695    #[error("storage error: {0}")]
696    Store(#[from] StoreError),
697}
698
699/// In-memory aggregated reverse-diff bridging the on-disk state at the cache edge `D`
700/// to the virtual state at a deep-reorg pivot `T-1`.
701///
702/// Built once per deep reorg by replaying [`STATE_HISTORY`] entries for blocks
703/// `D, D-1, ..., T` in descending order. Subsequent state reads during side-chain
704/// execution cascade as: new layer cache -> overlay -> on-disk state. On-disk state
705/// is NOT mutated while the overlay is alive; disk stays at `D` until the first
706/// new-chain commit folds the overlay and the new layer together into a single
707/// atomic write (the reconciliation step in `commit_to_disk`).
708pub struct Overlay {
709    account_trie: FxHashMap<Vec<u8>, Option<Vec<u8>>>,
710    storage_trie: FxHashMap<Vec<u8>, Option<Vec<u8>>>,
711    account_flat: FxHashMap<Vec<u8>, Option<Vec<u8>>>,
712    storage_flat: FxHashMap<Vec<u8>, Option<Vec<u8>>>,
713    /// Bloom filter shared across all four CFs. A miss here lets readers skip the
714    /// overlay lookup and fall through to disk without touching any map.
715    bloom: AtomicBloomFilter<FxBuildHasher>,
716    /// Highest block number covered by the overlay (= cache edge `D` at install time).
717    from_block: BlockNumber,
718    /// Lowest block number covered by the overlay (= `pivot + 1`).
719    to_block: BlockNumber,
720    /// State root the overlay reconstructs: the state as of `to_block - 1` (the pivot).
721    /// Captured from the `parent_state_root` of the journal entry at `to_block`. Used by
722    /// the read cascade to gate overlay consultation to the pivot root (and, transitively,
723    /// the new-chain layer roots built on top of it) so unrelated readers of the shared
724    /// cache do not get pivot values in place of the on-disk canonical state.
725    /// `H256::zero()` for a default/empty overlay (never a real reconstructed root).
726    serves_root: H256,
727}
728
729impl fmt::Debug for Overlay {
730    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
731        f.debug_struct("Overlay")
732            .field("account_trie_len", &self.account_trie.len())
733            .field("storage_trie_len", &self.storage_trie.len())
734            .field("account_flat_len", &self.account_flat.len())
735            .field("storage_flat_len", &self.storage_flat.len())
736            .field("from_block", &self.from_block)
737            .field("to_block", &self.to_block)
738            .finish()
739    }
740}
741
742impl Default for Overlay {
743    fn default() -> Self {
744        Self {
745            account_trie: FxHashMap::default(),
746            storage_trie: FxHashMap::default(),
747            account_flat: FxHashMap::default(),
748            storage_flat: FxHashMap::default(),
749            bloom: AtomicBloomFilter::with_false_pos(FALSE_POSITIVE_RATE)
750                .hasher(FxBuildHasher)
751                .expected_items(Self::BLOOM_INITIAL_CAPACITY),
752            from_block: 0,
753            to_block: 0,
754            serves_root: H256::zero(),
755        }
756    }
757}
758
759impl Overlay {
760    /// Expected-items hint used to size the bloom filter at construction time.
761    /// Sized for typical reorg depths (tens to low-hundreds of blocks); the filter
762    /// will still function past this count, just with a higher false-positive rate.
763    const BLOOM_INITIAL_CAPACITY: usize = 64 * 1024;
764
765    /// Builds an overlay by replaying journal entries for blocks `[to_block, from_block]`
766    /// (inclusive both ends) in descending order. Each loaded entry's `block_hash` is
767    /// verified against `expected_hash(n)`; a mismatch aborts with
768    /// [`OverlayError::HashMismatch`].
769    ///
770    /// `expected_hash` is a callback that maps a height to the hash of the canonical
771    /// block at that height on the chain being unwound. Returning `None` skips
772    /// verification at that height (useful for tests).
773    ///
774    /// Within a single key, the OLDEST recorded `prev` value wins ; later inserts
775    /// during the descending walk overwrite earlier ones, so the value at `to_block - 1`
776    /// (whatever the oldest in-range journal entry recorded as the pre-image) is what
777    /// remains after the walk.
778    pub fn from_journal(
779        backend: &dyn StorageBackend,
780        from_block: BlockNumber,
781        to_block: BlockNumber,
782        expected_hash: impl Fn(BlockNumber) -> Option<H256>,
783    ) -> Result<Self, OverlayError> {
784        // Hard guard (not debug-only): swapped arguments would underflow `n -= 1`
785        // below in release builds and loop indefinitely.
786        if from_block < to_block {
787            return Err(OverlayError::InvalidRange {
788                from_block,
789                to_block,
790            });
791        }
792        let mut overlay = Overlay {
793            from_block,
794            to_block,
795            ..Default::default()
796        };
797
798        // SAFETY: `StorageReadView` does not guarantee snapshot isolation on RocksDB.
799        // The only writer to STATE_HISTORY is `forkchoice_update_inner` (finality
800        // pruning); a concurrent FCU `delete_range` between two `.get()` calls below
801        // could cause a spurious `MissingEntry`. This is prevented by the
802        // reorg-in-progress guard: `Blockchain::enter_reorg` holds
803        // `Store::set_journal_pruning_paused(true)` for the whole apply pass, so
804        // pruning is deferred while the overlay is constructed (it catches up on
805        // the next finality advance after the pass).
806        let read = backend.begin_read()?;
807        let mut n = from_block;
808        loop {
809            let bytes = read
810                .get(STATE_HISTORY, &n.to_be_bytes())?
811                .ok_or(OverlayError::MissingEntry(n))?;
812            let entry = JournalEntry::decode(&bytes)?;
813            if let Some(expected) = expected_hash(n)
814                && entry.block_hash != expected
815            {
816                return Err(OverlayError::HashMismatch {
817                    block_number: n,
818                    expected,
819                    found: entry.block_hash,
820                });
821            }
822            // The entry at `to_block` unwinds `to_block -> to_block - 1`, so its
823            // `parent_state_root` is the state root the overlay reconstructs (the pivot).
824            if n == to_block {
825                overlay.serves_root = entry.parent_state_root;
826            }
827            overlay.absorb(entry);
828            if n == to_block {
829                break;
830            }
831            n -= 1;
832        }
833        Ok(overlay)
834    }
835
836    /// Absorbs one journal entry into the overlay. Later inserts overwrite earlier
837    /// ones ; combined with a descending walk in [`Self::from_journal`], this makes
838    /// the OLDEST in-range entry's `prev` value win, which is the correct value at
839    /// the pivot.
840    fn absorb(&mut self, entry: JournalEntry) {
841        for (k, v) in entry.account_trie_diff {
842            self.bloom.insert(&k);
843            self.account_trie.insert(k, v);
844        }
845        for (k, v) in entry.storage_trie_diff {
846            self.bloom.insert(&k);
847            self.storage_trie.insert(k, v);
848        }
849        for (k, v) in entry.account_flat_diff {
850            self.bloom.insert(&k);
851            self.account_flat.insert(k, v);
852        }
853        for (k, v) in entry.storage_flat_diff {
854            self.bloom.insert(&k);
855            self.storage_flat.insert(k, v);
856        }
857    }
858
859    /// Looks up `key` in the overlay's `cf` slot. Three-state return:
860    /// - `None` ; key not in overlay (caller falls through to disk).
861    /// - `Some(None)` ; key was overwritten and previously didn't exist on disk
862    ///   (caller treats as absent ; a rollback would delete it).
863    /// - `Some(Some(v))` ; key was overwritten and previously had value `v` on disk
864    ///   (caller treats as `v` ; a rollback would restore it).
865    pub fn lookup(&self, cf: OverlayCf, key: &[u8]) -> Option<Option<Vec<u8>>> {
866        if !self.bloom.contains(key) {
867            return None;
868        }
869        let map = match cf {
870            OverlayCf::AccountTrie => &self.account_trie,
871            OverlayCf::StorageTrie => &self.storage_trie,
872            OverlayCf::AccountFlat => &self.account_flat,
873            OverlayCf::StorageFlat => &self.storage_flat,
874        };
875        map.get(key).cloned()
876    }
877
878    /// Total number of overlay entries across all four CFs.
879    pub fn len(&self) -> usize {
880        self.account_trie.len()
881            + self.storage_trie.len()
882            + self.account_flat.len()
883            + self.storage_flat.len()
884    }
885
886    /// Whether the overlay holds any entries.
887    #[allow(dead_code)]
888    pub fn is_empty(&self) -> bool {
889        self.len() == 0
890    }
891
892    /// Approximate byte size of the overlay's key+value data. O(N) over entries;
893    /// intended for one-shot install-time metric emission, NOT per-lookup.
894    pub fn byte_size(&self) -> usize {
895        [
896            &self.account_trie,
897            &self.storage_trie,
898            &self.account_flat,
899            &self.storage_flat,
900        ]
901        .iter()
902        .flat_map(|map| map.iter())
903        .map(|(k, v)| k.len() + v.as_ref().map_or(0, |v| v.len()))
904        .sum()
905    }
906
907    /// Highest block number covered by the overlay (= cache edge `D` at install time).
908    #[allow(
909        clippy::wrong_self_convention,
910        reason = "field accessor: name matches struct field"
911    )]
912    pub fn from_block(&self) -> BlockNumber {
913        self.from_block
914    }
915
916    /// State root the overlay reconstructs (the pivot's state, as of `to_block - 1`).
917    /// The read cascade consults the overlay only for this root and the new-chain
918    /// layer roots derived from it; see [`TrieWrapper::get`].
919    pub fn serves_root(&self) -> H256 {
920        self.serves_root
921    }
922
923    /// Lowest block number covered by the overlay (= `pivot + 1`).
924    pub fn to_block(&self) -> BlockNumber {
925        self.to_block
926    }
927
928    /// Iterates every overlay entry across the four CFs as `(cf, key, value)`. Used
929    /// by the reconciliation step in `commit_to_disk` to fold overlay-only entries
930    /// into the first new-chain commit.
931    pub fn iter_all_entries(
932        &self,
933    ) -> impl Iterator<Item = (OverlayCf, &Vec<u8>, &Option<Vec<u8>>)> {
934        self.account_trie
935            .iter()
936            .map(|(k, v)| (OverlayCf::AccountTrie, k, v))
937            .chain(
938                self.storage_trie
939                    .iter()
940                    .map(|(k, v)| (OverlayCf::StorageTrie, k, v)),
941            )
942            .chain(
943                self.account_flat
944                    .iter()
945                    .map(|(k, v)| (OverlayCf::AccountFlat, k, v)),
946            )
947            .chain(
948                self.storage_flat
949                    .iter()
950                    .map(|(k, v)| (OverlayCf::StorageFlat, k, v)),
951            )
952    }
953}
954
955#[cfg(test)]
956mod overlay_tests {
957    use super::*;
958    use crate::backend::in_memory::InMemoryBackend;
959    use crate::journal::FlatDiff;
960
961    fn h(b: u8) -> H256 {
962        H256::repeat_byte(b)
963    }
964
965    /// Seeds N journal entries directly into STATE_HISTORY so tests can drive overlay
966    /// construction without going through the full block-execution path.
967    fn seed(backend: &Arc<dyn StorageBackend>, per_block: &[(BlockNumber, H256, FlatDiff)]) {
968        let mut tx = backend.begin_write().unwrap();
969        for (n, block_hash, diff) in per_block {
970            let entry = JournalEntry {
971                block_hash: *block_hash,
972                parent_state_root: H256::zero(),
973                account_trie_diff: diff.clone(),
974                storage_trie_diff: vec![],
975                account_flat_diff: vec![],
976                storage_flat_diff: vec![],
977            };
978            tx.put(STATE_HISTORY, &n.to_be_bytes(), &entry.encode())
979                .unwrap();
980        }
981        tx.commit().unwrap();
982    }
983
984    #[test]
985    fn from_journal_loads_descending_range() {
986        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
987        seed(
988            &backend,
989            &[
990                (3, h(0x03), vec![(vec![0xa], Some(vec![0x33]))]),
991                (4, h(0x04), vec![(vec![0xb], Some(vec![0x44]))]),
992                (5, h(0x05), vec![(vec![0xc], Some(vec![0x55]))]),
993            ],
994        );
995        let overlay =
996            Overlay::from_journal(backend.as_ref(), 5, 3, |n| Some(H256::repeat_byte(n as u8)))
997                .unwrap();
998        assert_eq!(overlay.len(), 3);
999        assert_eq!(overlay.from_block(), 5);
1000        assert_eq!(overlay.to_block(), 3);
1001        assert_eq!(
1002            overlay.lookup(OverlayCf::AccountTrie, &[0xa]),
1003            Some(Some(vec![0x33]))
1004        );
1005        assert_eq!(
1006            overlay.lookup(OverlayCf::AccountTrie, &[0xb]),
1007            Some(Some(vec![0x44]))
1008        );
1009        assert_eq!(
1010            overlay.lookup(OverlayCf::AccountTrie, &[0xc]),
1011            Some(Some(vec![0x55]))
1012        );
1013    }
1014
1015    /// Block 3 (oldest) recorded K=X. Block 5 (newest) recorded K=Y4. After
1016    /// descending walk, the overlay must expose K=X ; the value at the pivot
1017    /// (= to_block - 1 = 2).
1018    #[test]
1019    fn older_entry_wins_when_key_repeats() {
1020        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
1021        seed(
1022            &backend,
1023            &[
1024                (3, h(0x03), vec![(vec![0xaa], Some(b"X".to_vec()))]),
1025                (4, h(0x04), vec![(vec![0xaa], Some(b"Y3".to_vec()))]),
1026                (5, h(0x05), vec![(vec![0xaa], Some(b"Y4".to_vec()))]),
1027            ],
1028        );
1029        let overlay =
1030            Overlay::from_journal(backend.as_ref(), 5, 3, |n| Some(H256::repeat_byte(n as u8)))
1031                .unwrap();
1032        assert_eq!(
1033            overlay.lookup(OverlayCf::AccountTrie, &[0xaa]),
1034            Some(Some(b"X".to_vec())),
1035            "oldest reverse-diff value should win after descending walk"
1036        );
1037    }
1038
1039    #[test]
1040    fn absent_key_passes_through_bloom() {
1041        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
1042        seed(
1043            &backend,
1044            &[(3, h(0x03), vec![(vec![0xaa], Some(vec![0x11]))])],
1045        );
1046        let overlay =
1047            Overlay::from_journal(backend.as_ref(), 3, 3, |n| Some(H256::repeat_byte(n as u8)))
1048                .unwrap();
1049        assert_eq!(overlay.lookup(OverlayCf::AccountTrie, &[0xff]), None);
1050    }
1051
1052    #[test]
1053    fn hash_mismatch_aborts() {
1054        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
1055        seed(&backend, &[(7, h(0x07), vec![(vec![0xaa], None)])]);
1056        // Caller supplies the WRONG expected hash for height 7.
1057        let err = Overlay::from_journal(backend.as_ref(), 7, 7, |_| Some(h(0xff))).unwrap_err();
1058        match err {
1059            OverlayError::HashMismatch { block_number, .. } => assert_eq!(block_number, 7),
1060            other => panic!("expected HashMismatch, got {other:?}"),
1061        }
1062    }
1063
1064    #[test]
1065    fn missing_entry_aborts() {
1066        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
1067        // Seed only block 5; ask for [5, 3] ; blocks 4 and 3 are missing.
1068        seed(&backend, &[(5, h(0x05), vec![])]);
1069        let err = Overlay::from_journal(backend.as_ref(), 5, 3, |_| None).unwrap_err();
1070        match err {
1071            OverlayError::MissingEntry(n) => assert_eq!(n, 4),
1072            other => panic!("expected MissingEntry, got {other:?}"),
1073        }
1074    }
1075
1076    #[test]
1077    fn skip_verification_when_callback_returns_none() {
1078        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
1079        seed(&backend, &[(7, h(0xab), vec![(vec![0x01], None)])]);
1080        let overlay = Overlay::from_journal(backend.as_ref(), 7, 7, |_| None).unwrap();
1081        assert_eq!(overlay.lookup(OverlayCf::AccountTrie, &[0x01]), Some(None));
1082    }
1083
1084    #[test]
1085    fn classify_by_key_length_matches_backend_table_routing() {
1086        // Spot-check the boundaries. These must agree with `classify_trie_key`
1087        // (account leaf at 65, storage leaf at 131, anything else routed by length).
1088        assert_eq!(OverlayCf::classify_by_key_length(0), OverlayCf::AccountTrie);
1089        assert_eq!(
1090            OverlayCf::classify_by_key_length(64),
1091            OverlayCf::AccountTrie
1092        );
1093        assert_eq!(
1094            OverlayCf::classify_by_key_length(65),
1095            OverlayCf::AccountFlat
1096        );
1097        assert_eq!(
1098            OverlayCf::classify_by_key_length(66),
1099            OverlayCf::StorageTrie
1100        );
1101        assert_eq!(
1102            OverlayCf::classify_by_key_length(130),
1103            OverlayCf::StorageTrie
1104        );
1105        assert_eq!(
1106            OverlayCf::classify_by_key_length(131),
1107            OverlayCf::StorageFlat
1108        );
1109        assert_eq!(
1110            OverlayCf::classify_by_key_length(132),
1111            OverlayCf::StorageTrie
1112        );
1113    }
1114
1115    /// `serves_root` is the reconstructed pivot root, taken from the `parent_state_root`
1116    /// of the entry at `to_block` (the deepest in-range entry) ; NOT the `from_block`
1117    /// entry. Proves the capture picks the right end of the descending walk.
1118    #[test]
1119    fn from_journal_captures_serves_root_from_to_block_entry() {
1120        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
1121        let pivot = h(0x77);
1122        let mut tx = backend.begin_write().unwrap();
1123        for (n, psr) in [(3u64, pivot), (4, h(0x44)), (5, h(0x55))] {
1124            let entry = JournalEntry {
1125                block_hash: h(n as u8),
1126                parent_state_root: psr,
1127                account_trie_diff: vec![(vec![n as u8], Some(vec![n as u8]))],
1128                storage_trie_diff: vec![],
1129                account_flat_diff: vec![],
1130                storage_flat_diff: vec![],
1131            };
1132            tx.put(STATE_HISTORY, &n.to_be_bytes(), &entry.encode())
1133                .unwrap();
1134        }
1135        tx.commit().unwrap();
1136        // Range [to_block=3, from_block=5]; serves_root must be entry-3's parent root.
1137        let overlay = Overlay::from_journal(backend.as_ref(), 5, 3, |_| None).unwrap();
1138        assert_eq!(overlay.serves_root(), pivot);
1139    }
1140
1141    /// The overlay must only be consulted by readers at a "consuming" root ; the pivot
1142    /// (`serves_root`) or a new-chain layer root present in the cache. Unrelated roots
1143    /// (old-chain edge `D`, historical/RPC reads) must fall through to disk. Regression
1144    /// for the review finding "Overlay Applies Across Roots".
1145    #[test]
1146    fn overlay_serves_only_consuming_roots() {
1147        let pivot = h(0xaa);
1148        let new_chain = h(0xbb);
1149        let unrelated = h(0xcc);
1150        let parent = h(0xa9);
1151
1152        let mut cache =
1153            TrieLayerCache::new_with_safe_commit(128, Arc::new(RwLock::new(H256::zero())));
1154
1155        // No overlay installed -> never serves.
1156        assert!(!cache.overlay_serves(pivot));
1157
1158        // Register a new-chain layer at `new_chain` (a replay commit).
1159        cache.put_batch(
1160            parent,
1161            new_chain,
1162            1,
1163            h(0xb1),
1164            vec![(Nibbles::from_bytes(&[0x01]), vec![0x02])],
1165        );
1166
1167        // Install an overlay reconstructing the pivot state.
1168        let overlay = Overlay {
1169            serves_root: pivot,
1170            from_block: 5,
1171            to_block: 3,
1172            ..Default::default()
1173        };
1174        cache.set_overlay(Arc::new(overlay));
1175
1176        assert!(
1177            cache.overlay_serves(pivot),
1178            "pivot root must consume the overlay"
1179        );
1180        assert!(
1181            cache.overlay_serves(new_chain),
1182            "new-chain layer root must consume the overlay"
1183        );
1184        assert!(
1185            !cache.overlay_serves(unrelated),
1186            "unrelated root must NOT see the overlay (would leak pivot state over disk)"
1187        );
1188
1189        // Clearing the overlay disables consumption for every root.
1190        cache.clear_overlay();
1191        assert!(!cache.overlay_serves(pivot));
1192        assert!(!cache.overlay_serves(new_chain));
1193    }
1194
1195    /// While an overlay serves the read's state root, `flatkeyvalue_computed`
1196    /// must return false so `Trie::get` walks the (always journaled) trie nodes
1197    /// instead of trusting disk flat-KV, which may hold the generator's stale,
1198    /// unjournaled values. Roots the overlay does not serve must be unaffected.
1199    #[test]
1200    fn flatkeyvalue_computed_is_disabled_while_overlay_serves() {
1201        struct AlwaysComputedDb;
1202        impl TrieDB for AlwaysComputedDb {
1203            fn flatkeyvalue_computed(&self, _key: Nibbles) -> bool {
1204                true
1205            }
1206            fn get(&self, _key: Nibbles) -> Result<Option<Vec<u8>>, TrieError> {
1207                Ok(None)
1208            }
1209            fn put_batch(&self, _key_values: Vec<(Nibbles, Vec<u8>)>) -> Result<(), TrieError> {
1210                unimplemented!()
1211            }
1212        }
1213
1214        let pivot = h(0xaa);
1215        let unrelated = h(0xcc);
1216        let key = Nibbles::from_bytes(&[0x01; 32]);
1217
1218        let mut cache =
1219            TrieLayerCache::new_with_safe_commit(128, Arc::new(RwLock::new(H256::zero())));
1220        cache.set_overlay(Arc::new(Overlay {
1221            serves_root: pivot,
1222            ..Default::default()
1223        }));
1224        let cache = Arc::new(cache);
1225
1226        let served = TrieWrapper::new(pivot, cache.clone(), Box::new(AlwaysComputedDb), None);
1227        assert!(
1228            !served.flatkeyvalue_computed(key.clone()),
1229            "served root must not trust disk flat-KV"
1230        );
1231
1232        let unserved = TrieWrapper::new(unrelated, cache.clone(), Box::new(AlwaysComputedDb), None);
1233        assert!(
1234            unserved.flatkeyvalue_computed(key.clone()),
1235            "unserved root must keep the flat-KV fast path"
1236        );
1237
1238        // Same for storage tries (prefixed wrapper).
1239        let served_storage =
1240            TrieWrapper::new(pivot, cache, Box::new(AlwaysComputedDb), Some(h(0x01)));
1241        assert!(
1242            !served_storage.flatkeyvalue_computed(key),
1243            "served storage root must not trust disk flat-KV"
1244        );
1245    }
1246
1247    /// `lookup_overlay` is the entry point from the read cascade. It must short-circuit
1248    /// to `None` when no overlay is installed, regardless of key length.
1249    #[test]
1250    fn overlay_lookup_returns_none_when_no_overlay_installed() {
1251        let cache = TrieLayerCache::new_with_safe_commit(1, Arc::new(RwLock::new(H256::zero())));
1252        for key_len in [4usize, 65, 67, 131] {
1253            let key = vec![0xab; key_len];
1254            assert_eq!(
1255                cache.lookup_overlay(&key),
1256                None,
1257                "no overlay installed -> outer None at length {key_len}"
1258            );
1259        }
1260    }
1261
1262    /// Installs an overlay with one entry per CF (each at the canonical length) and
1263    /// confirms `lookup_overlay` routes to the right map.
1264    #[test]
1265    fn overlay_lookup_classifies_cf_by_key_length() {
1266        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
1267        let entry = JournalEntry {
1268            block_hash: h(0x01),
1269            parent_state_root: H256::zero(),
1270            account_trie_diff: vec![(vec![0x10; 4], Some(b"acct-trie".to_vec()))],
1271            storage_trie_diff: vec![(vec![0x20; 67], Some(b"stor-trie".to_vec()))],
1272            account_flat_diff: vec![(vec![0x30; 65], Some(b"acct-flat".to_vec()))],
1273            storage_flat_diff: vec![(vec![0x40; 131], None)],
1274        };
1275        let mut tx = backend.begin_write().unwrap();
1276        tx.put(STATE_HISTORY, &1u64.to_be_bytes(), &entry.encode())
1277            .unwrap();
1278        tx.commit().unwrap();
1279        let overlay = Overlay::from_journal(backend.as_ref(), 1, 1, |_| None).unwrap();
1280
1281        let mut cache =
1282            TrieLayerCache::new_with_safe_commit(1, Arc::new(RwLock::new(H256::zero())));
1283        cache.set_overlay(Arc::new(overlay));
1284
1285        assert_eq!(
1286            cache.lookup_overlay(&[0x10; 4]),
1287            Some(Some(b"acct-trie".to_vec()))
1288        );
1289        assert_eq!(
1290            cache.lookup_overlay(&[0x20; 67]),
1291            Some(Some(b"stor-trie".to_vec()))
1292        );
1293        assert_eq!(
1294            cache.lookup_overlay(&[0x30; 65]),
1295            Some(Some(b"acct-flat".to_vec()))
1296        );
1297        assert_eq!(
1298            cache.lookup_overlay(&[0x40; 131]),
1299            Some(None),
1300            "overlay with None means key was absent at pivot"
1301        );
1302        // Same length but different bytes ; bloom miss.
1303        assert_eq!(cache.lookup_overlay(&[0xee; 4]), None);
1304    }
1305
1306    #[test]
1307    fn set_and_clear_overlay_round_trips() {
1308        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
1309        seed(&backend, &[(1, h(0x01), vec![(vec![0xaa], None)])]);
1310        let overlay = Overlay::from_journal(backend.as_ref(), 1, 1, |_| None).unwrap();
1311
1312        let mut cache =
1313            TrieLayerCache::new_with_safe_commit(1, Arc::new(RwLock::new(H256::zero())));
1314        assert!(cache.overlay().is_none());
1315        cache.set_overlay(Arc::new(overlay));
1316        assert!(cache.overlay().is_some());
1317        cache.clear_overlay();
1318        assert!(cache.overlay().is_none());
1319        // Idempotent.
1320        cache.clear_overlay();
1321        assert!(cache.overlay().is_none());
1322    }
1323
1324    /// `from_block == to_block == 0` is a legitimate single-block-at-genesis case
1325    /// and must not underflow the descending loop.
1326    #[test]
1327    fn single_entry_at_genesis() {
1328        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
1329        seed(
1330            &backend,
1331            &[(0, h(0x00), vec![(vec![0xaa], Some(vec![0x11]))])],
1332        );
1333        let overlay = Overlay::from_journal(backend.as_ref(), 0, 0, |_| None).unwrap();
1334        assert_eq!(overlay.len(), 1);
1335        assert_eq!(overlay.from_block(), 0);
1336        assert_eq!(overlay.to_block(), 0);
1337        assert_eq!(
1338            overlay.lookup(OverlayCf::AccountTrie, &[0xaa]),
1339            Some(Some(vec![0x11]))
1340        );
1341    }
1342
1343    /// Swapped `from_block < to_block` must be a hard error (not a debug-only
1344    /// assert) so a caller mistake fires in release too. Pin the variant to guard
1345    /// against future error-text changes.
1346    #[test]
1347    fn swapped_args_returns_error() {
1348        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
1349        let err = Overlay::from_journal(backend.as_ref(), 3, 5, |_| None).unwrap_err();
1350        match err {
1351            OverlayError::InvalidRange {
1352                from_block,
1353                to_block,
1354            } => {
1355                assert_eq!(from_block, 3);
1356                assert_eq!(to_block, 5);
1357            }
1358            other => panic!("expected InvalidRange, got {other:?}"),
1359        }
1360    }
1361
1362    /// `Some(Some(vec![]))` (an empty-but-present pre-image) must round-trip
1363    /// through `absorb`/`lookup` without being confused with `Some(None)`
1364    /// (absent at pivot). The journal codec handles this correctly; this test
1365    /// guards a future codec change.
1366    #[test]
1367    fn empty_but_present_round_trips() {
1368        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
1369        seed(&backend, &[(1, h(0x01), vec![(vec![0xaa], Some(vec![]))])]);
1370        let overlay = Overlay::from_journal(backend.as_ref(), 1, 1, |_| None).unwrap();
1371        assert_eq!(
1372            overlay.lookup(OverlayCf::AccountTrie, &[0xaa]),
1373            Some(Some(vec![])),
1374            "empty-but-present value must NOT degrade to Some(None)"
1375        );
1376    }
1377
1378    #[test]
1379    fn iter_all_entries_visits_each_cf() {
1380        // Sanity check for the reconciliation path: every CF an entry was inserted
1381        // into must show up in iter_all_entries, with the right CF tag.
1382        let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::open().unwrap());
1383        let entry = JournalEntry {
1384            block_hash: h(0x01),
1385            parent_state_root: H256::zero(),
1386            account_trie_diff: vec![(vec![0x10; 4], Some(b"at".to_vec()))],
1387            storage_trie_diff: vec![(vec![0x20; 67], Some(b"st".to_vec()))],
1388            account_flat_diff: vec![(vec![0x30; 65], None)],
1389            storage_flat_diff: vec![(vec![0x40; 131], Some(b"sf".to_vec()))],
1390        };
1391        let mut tx = backend.begin_write().unwrap();
1392        tx.put(STATE_HISTORY, &1u64.to_be_bytes(), &entry.encode())
1393            .unwrap();
1394        tx.commit().unwrap();
1395        let overlay = Overlay::from_journal(backend.as_ref(), 1, 1, |_| None).unwrap();
1396
1397        let mut cfs: Vec<OverlayCf> = overlay.iter_all_entries().map(|(cf, _, _)| cf).collect();
1398        cfs.sort_by_key(|cf| match cf {
1399            OverlayCf::AccountTrie => 0,
1400            OverlayCf::StorageTrie => 1,
1401            OverlayCf::AccountFlat => 2,
1402            OverlayCf::StorageFlat => 3,
1403        });
1404        assert_eq!(
1405            cfs,
1406            vec![
1407                OverlayCf::AccountTrie,
1408                OverlayCf::StorageTrie,
1409                OverlayCf::AccountFlat,
1410                OverlayCf::StorageFlat,
1411            ]
1412        );
1413        assert_eq!(overlay.len(), 4);
1414        assert!(!overlay.is_empty());
1415    }
1416}
1417
1418#[cfg(test)]
1419mod tests {
1420    use std::collections::HashSet;
1421    use std::sync::{Arc, RwLock};
1422
1423    use ethrex_common::H256;
1424    use ethrex_trie::Nibbles;
1425
1426    use super::TrieLayerCache;
1427
1428    /// Build an `H256` from a single byte, all other bytes zero.
1429    fn h256(b: u8) -> H256 {
1430        let mut bytes = [0u8; 32];
1431        bytes[31] = b;
1432        H256(bytes)
1433    }
1434
1435    /// A dummy trie key, distinct per layer so `put_batch`'s empty-block guard does not skip it.
1436    fn key(b: u8) -> Nibbles {
1437        Nibbles::from_bytes(&[b; 32])
1438    }
1439
1440    /// Build a linear chain of N layers on top of an on-disk floor (`H256::zero()`),
1441    /// returning the roots in order `[root_1, ..., root_n]` with `root_{i+1}.parent == root_i`.
1442    fn build_chain(cache: &mut TrieLayerCache, n: u8) -> Vec<H256> {
1443        let mut roots = Vec::with_capacity(n as usize);
1444        let mut parent = H256::zero();
1445        for i in 1..=n {
1446            let root = h256(i);
1447            cache.put_batch(parent, root, i as u64, root, vec![(key(i), vec![i])]);
1448            roots.push(root);
1449            parent = root;
1450        }
1451        roots
1452    }
1453
1454    fn cache_with_cell(threshold: usize, root: H256) -> (TrieLayerCache, Arc<RwLock<H256>>) {
1455        let cell = Arc::new(RwLock::new(root));
1456        let cache = TrieLayerCache::new_with_safe_commit(threshold, Arc::clone(&cell));
1457        (cache, cell)
1458    }
1459
1460    /// (a) Zero cell means "no safe commit point yet" -> get_commitable returns None.
1461    #[test]
1462    fn zero_cell_yields_none() {
1463        let (mut cache, _cell) = cache_with_cell(4, H256::zero());
1464        let roots = build_chain(&mut cache, 5);
1465        assert_eq!(cache.get_commitable(*roots.last().unwrap()), None);
1466    }
1467
1468    /// (b) Safe root on the parent walk -> get_commitable returns Some(safe_root).
1469    #[test]
1470    fn safe_root_on_walk_yields_some() {
1471        let safe = h256(2);
1472        let (mut cache, _cell) = cache_with_cell(4, safe);
1473        let roots = build_chain(&mut cache, 4);
1474        // Walk from L4: L4 -> L3 -> L2 (== safe) -> Some(L2).
1475        assert_eq!(cache.get_commitable(roots[3]), Some(safe));
1476    }
1477
1478    /// (c) parent_state_root == safe root -> immediate Some, no walk needed.
1479    #[test]
1480    fn parent_equals_safe_root_yields_some() {
1481        let roots = {
1482            let (mut cache, _cell) = cache_with_cell(4, H256::zero());
1483            build_chain(&mut cache, 3)
1484        };
1485        let l3 = roots[2];
1486        let (mut cache, _cell) = cache_with_cell(4, l3);
1487        build_chain(&mut cache, 3);
1488        assert_eq!(cache.get_commitable(l3), Some(l3));
1489    }
1490
1491    /// (c2) Regression: `parent_state_root == safe_root` but that root has no layer, the steady
1492    /// state on L2 where empty blocks keep their parent's root. Branch (c) used to match on the
1493    /// root value alone and hand `commit_to_disk` something it could not commit.
1494    #[test]
1495    fn parent_equals_safe_root_without_layer_yields_none() {
1496        let orphan = h256(7);
1497        let (mut cache, _cell) = cache_with_cell(4, orphan);
1498        // An empty block: parent == state_root, so `put_batch` inserts nothing.
1499        cache.put_batch(orphan, orphan, 7, orphan, vec![]);
1500        assert!(
1501            !cache.has_layer(orphan),
1502            "empty-block root must not create a layer"
1503        );
1504        assert_eq!(
1505            cache.get_commitable(orphan),
1506            None,
1507            "a safe root with no layer is not committable, even when it equals the parent"
1508        );
1509    }
1510
1511    /// (c3) A flushed root is pruned by `commit`, so it must not be offered again.
1512    #[test]
1513    fn already_committed_safe_root_yields_none() {
1514        let safe = h256(2);
1515        let (mut cache, _cell) = cache_with_cell(4, safe);
1516        build_chain(&mut cache, 3);
1517        assert_eq!(cache.get_commitable(safe), Some(safe));
1518        cache.commit(safe).expect("first commit flushes the layer");
1519        assert!(!cache.has_layer(safe), "commit prunes the flushed layer");
1520        assert_eq!(
1521            cache.get_commitable(safe),
1522            None,
1523            "re-committing an already-flushed root must not be offered again"
1524        );
1525    }
1526
1527    /// (d) Safe root not an ancestor (never inserted as a layer) -> None, regardless of depth.
1528    #[test]
1529    fn safe_root_not_ancestor_yields_none() {
1530        let (mut cache, _cell) = cache_with_cell(4, h256(99));
1531        // 6 layers (> threshold) so an old depth-only path would have fired.
1532        let roots = build_chain(&mut cache, 6);
1533        assert_eq!(cache.get_commitable(*roots.last().unwrap()), None);
1534    }
1535
1536    /// (e) Cycle guard: a B -> C -> B link must terminate and return None.
1537    #[test]
1538    fn cycle_guard_terminates() {
1539        let b = h256(20);
1540        let c = h256(21);
1541        let (mut cache, _cell) = cache_with_cell(4, h256(99));
1542        // Insert C (parent B) then B (parent C); neither key pre-exists, so both insert,
1543        // forming the cycle B <-> C. The safe root (h256(99)) is absent on purpose.
1544        cache.put_batch(b, c, 21, c, vec![(key(21), vec![21])]);
1545        cache.put_batch(c, b, 20, b, vec![(key(20), vec![20])]);
1546        // Walking from C must terminate (start-of-walk + bounded-walk guards) and yield None.
1547        assert_eq!(cache.get_commitable(c), None);
1548    }
1549
1550    /// (f) commit(safe_root) removes the safe layer and all older ones, retaining layers above it.
1551    #[test]
1552    fn commit_retains_layers_above_safe_root() {
1553        let safe = h256(2);
1554        let (mut cache, _cell) = cache_with_cell(4, safe);
1555        let roots = build_chain(&mut cache, 4);
1556        let (l3, l4) = (roots[2], roots[3]);
1557        assert_eq!(cache.get_commitable(roots[3]), Some(safe));
1558
1559        cache.commit(safe);
1560        let remaining: HashSet<H256> = cache.layers.keys().copied().collect();
1561        let expected: HashSet<H256> = [l3, l4].into_iter().collect();
1562        assert_eq!(
1563            remaining, expected,
1564            "commit(safe) must retain only the layers above it"
1565        );
1566    }
1567
1568    /// (g) Memory bound: after building > threshold layers and committing at a safe root that
1569    /// keeps the chain bounded, the retained layer count stays <= commit_threshold + 1.
1570    #[test]
1571    fn memory_bound_after_commit() {
1572        let threshold = 4usize;
1573        // Build threshold + 3 = 7 layers; set the safe root `threshold` below the tip.
1574        let n = (threshold + 3) as u8;
1575        let safe = h256(n - threshold as u8); // h256(3): leaves layers 4..=7 above it
1576        let (mut cache, _cell) = cache_with_cell(threshold, safe);
1577        let roots = build_chain(&mut cache, n);
1578        let tip = *roots.last().unwrap();
1579
1580        let commitable = cache.get_commitable(tip).expect("safe root on the walk");
1581        cache.commit(commitable);
1582        assert!(
1583            cache.layers.len() <= threshold + 1,
1584            "retained layers ({}) must stay within commit_threshold + 1 ({})",
1585            cache.layers.len(),
1586            threshold + 1
1587        );
1588    }
1589
1590    /// Why live block-by-block execution must NOT use the depth gate: with nothing canonicalized
1591    /// (safe_commit cell ZERO), the depth gate would flush a non-canonical layer and prune genesis
1592    /// -> the "post-state for block 0 absent" wedge. The canonical gate commits nothing instead.
1593    ///
1594    /// Wedge simulation: non-canonical newPayload layers pile up but nothing is canonicalized,
1595    /// so the safe_commit cell stays ZERO and never advances.
1596    #[test]
1597    fn live_canonical_gate_holds_while_depth_gate_would_commit() {
1598        // threshold = 4, safe_commit cell = ZERO (nothing canonicalized).
1599        let (mut cache, _cell) = cache_with_cell(4, H256::zero());
1600        // Linear chain L1 <- L2 <- L3 <- L4 <- L5 (distinct keys so guards pass).
1601        let roots = build_chain(&mut cache, 5);
1602        let l5 = *roots.last().unwrap();
1603
1604        // Depth gate: commits a layer at depth 4 -> would prune genesis on the path-keyed disk
1605        // root if used in live mode. This is why live mode uses the canonical gate below.
1606        assert!(
1607            cache.get_commitable_by_depth(l5, 4).is_some(),
1608            "depth-only gate commits at depth 4 regardless of canonicality"
1609        );
1610
1611        // Canonical gate (live mode): safe_commit cell is zero, so nothing is committed and
1612        // genesis is preserved.
1613        assert_eq!(
1614            cache.get_commitable(l5),
1615            None,
1616            "canonical gate must commit nothing while safe_commit is zero (the wedge fix)"
1617        );
1618    }
1619
1620    /// Batch execution (full sync / import) must still flush even when no FCU has advanced the
1621    /// safe-commit cell. The canonical gate stays parked at zero (import does not FCU until the
1622    /// end; full sync's `head - 128` root never lands on a ~1024-block batch boundary), so batch
1623    /// mode commits by depth instead -> memory stays bounded and state is durable across restart.
1624    #[test]
1625    fn batch_depth_gate_flushes_without_safe_commit() {
1626        // safe_commit cell = ZERO, as during bulk import before the terminal FCU.
1627        let (mut cache, _cell) = cache_with_cell(4, H256::zero());
1628        // Five batch layers stacked (each stands in for ~1024 blocks in real batch mode).
1629        let roots = build_chain(&mut cache, 5);
1630        let tip = *roots.last().unwrap();
1631
1632        // Canonical gate would never flush here -> unbounded memory (the regression iovoid flagged).
1633        assert_eq!(
1634            cache.get_commitable(tip),
1635            None,
1636            "canonical gate never flushes batch layers while safe_commit is zero"
1637        );
1638
1639        // Depth gate (batch mode) flushes the layer BATCH_COMMIT_THRESHOLD deep: root_2 sits 4
1640        // layers below the tip (tip=root_5).
1641        assert_eq!(
1642            cache.get_commitable_by_depth(tip, 4),
1643            Some(roots[1]),
1644            "batch depth gate must flush the layer 4 deep, bounding memory"
1645        );
1646    }
1647}