Skip to main content

reth_chain_state/
state_trie_overlay.rs

1//! Flattened state trie overlays for in-memory blocks.
2//!
3//! Payload validation needs a view of the state trie as of an in-memory parent block even when that
4//! parent has not been persisted yet. [`StateTrieOverlayManager`] tracks those in-memory blocks and
5//! builds reusable flattened state trie overlays on demand.
6
7use crate::{EthPrimitives, ExecutedBlock, PreservedSparseTrie, PreservedTrieGuard};
8use alloy_primitives::B256;
9use parking_lot::Mutex;
10use reth_metrics::{
11    metrics::{Counter, Histogram},
12    Metrics,
13};
14use reth_primitives_traits::{
15    dashmap::{mapref::entry::Entry, DashMap},
16    AlloyBlockHeader, FastInstant, NodePrimitives,
17};
18#[cfg(feature = "rayon")]
19use reth_tasks::WorkerPool;
20use reth_trie::{updates::TrieUpdatesSorted, HashedPostStateSorted, TrieInputSorted};
21use std::{
22    fmt,
23    sync::{Arc, OnceLock},
24    time::Instant,
25};
26use tracing::{debug, trace};
27
28/// Manages flattened state trie overlays for in-memory blocks.
29///
30/// The manager owns the in-memory block graph and a cache of flattened state trie overlays keyed by
31/// `(anchor_hash, tip_hash)`.
32#[derive(Clone)]
33pub struct StateTrieOverlayManager<N: NodePrimitives = EthPrimitives> {
34    blocks: Arc<DashMap<B256, ExecutedBlock<N>>>,
35    overlays: Arc<DashMap<OverlayCacheKey, OverlayCacheEntry>>,
36    preserved_sparse_trie: Arc<Mutex<Option<PreservedSparseTrie>>>,
37    #[cfg(feature = "rayon")]
38    worker_pool: Option<Arc<WorkerPool>>,
39    metrics: StateTrieOverlayMetrics,
40}
41
42/// Metrics for state trie overlay management.
43#[derive(Clone, Metrics)]
44#[metrics(scope = "sync.block_validation.state_trie_overlay")]
45struct StateTrieOverlayMetrics {
46    /// Duration of overlay computation in seconds.
47    overlay_computation_duration_seconds: Histogram,
48    /// Number of requests satisfied by an existing overlay cache entry.
49    overlay_cache_reuses: Counter,
50    /// Number of overlay cache entries populated by computing an overlay.
51    overlay_cache_fills: Counter,
52}
53
54impl<N: NodePrimitives> Default for StateTrieOverlayManager<N> {
55    fn default() -> Self {
56        Self {
57            blocks: Default::default(),
58            overlays: Default::default(),
59            preserved_sparse_trie: Default::default(),
60            #[cfg(feature = "rayon")]
61            worker_pool: None,
62            metrics: Default::default(),
63        }
64    }
65}
66
67impl<N: NodePrimitives> std::fmt::Debug for StateTrieOverlayManager<N> {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        f.debug_struct("StateTrieOverlayManager")
70            .field("blocks", &self.blocks.len())
71            .field("overlays", &self.overlays.len())
72            .finish()
73    }
74}
75
76impl<N: NodePrimitives> StateTrieOverlayManager<N> {
77    /// Create a new [`StateTrieOverlayManager`] backed by the given worker pool.
78    #[cfg(feature = "rayon")]
79    pub fn new(worker_pool: Arc<WorkerPool>) -> Self {
80        Self {
81            blocks: Default::default(),
82            overlays: Default::default(),
83            preserved_sparse_trie: Default::default(),
84            worker_pool: Some(worker_pool),
85            metrics: Default::default(),
86        }
87    }
88
89    /// Takes the preserved sparse trie if present, leaving `None` in its place.
90    pub fn take_sparse_trie(&self) -> Option<PreservedSparseTrie> {
91        self.preserved_sparse_trie.lock().take()
92    }
93
94    /// Acquires a guard that blocks taking the trie until dropped.
95    ///
96    /// Use this before sending the state root result to ensure the next block waits for the trie
97    /// to be stored.
98    pub fn lock_sparse_trie(&self) -> PreservedTrieGuard<'_> {
99        PreservedTrieGuard::new(self.preserved_sparse_trie.lock())
100    }
101
102    /// Waits until the sparse trie lock becomes available.
103    ///
104    /// This acquires and immediately releases the lock, ensuring that any ongoing operations
105    /// complete before returning. Returns the time spent waiting for the lock.
106    pub fn wait_for_sparse_trie_availability(&self) -> std::time::Duration {
107        let start = FastInstant::now();
108        let _guard = self.preserved_sparse_trie.lock();
109        let elapsed = start.elapsed();
110        if elapsed.as_millis() > 5 {
111            debug!(
112                target: "engine::tree::payload_processor",
113                blocked_for=?elapsed,
114                "Waited for preserved sparse trie to become available"
115            );
116        }
117        elapsed
118    }
119
120    /// Inserts an executed in-memory block into the state trie overlay manager.
121    #[tracing::instrument(
122        level = "trace",
123        target = "chain_state::state_trie_overlay",
124        skip_all,
125        fields(
126            block_hash = %block.recovered_block().hash(),
127            parent_hash = %block.recovered_block().parent_hash(),
128            duplicate = false,
129        )
130    )]
131    pub fn insert_block(&self, block: ExecutedBlock<N>) {
132        let hash = block.recovered_block().hash();
133        let parent_hash = block.recovered_block().parent_hash();
134        let span = tracing::Span::current();
135
136        // First add the block to the live graph; duplicate inserts do not need cache work.
137        match self.blocks.entry(hash) {
138            Entry::Occupied(_) => {
139                span.record("duplicate", true);
140                debug!(
141                    target: "chain_state::state_trie_overlay",
142                    %hash,
143                    %parent_hash,
144                    "state trie overlay block already inserted"
145                );
146                return
147            }
148            Entry::Vacant(entry) => {
149                entry.insert(block);
150            }
151        }
152
153        // Snapshot matching parent overlays before spawning so DashMap iteration guards are
154        // dropped.
155        let cached_parent_overlays = self
156            .overlays
157            .iter()
158            .filter_map(|entry| {
159                let key = *entry.key();
160                (key.tip_hash == parent_hash && entry.value().is_ready()).then_some(key.anchor_hash)
161            })
162            .collect::<Vec<_>>();
163
164        debug!(
165            target: "chain_state::state_trie_overlay",
166            %hash,
167            %parent_hash,
168            "inserted block into state trie overlay manager"
169        );
170        if cached_parent_overlays.is_empty() {
171            return
172        }
173
174        #[cfg(feature = "rayon")]
175        let Some(worker_pool) = self.worker_pool.clone() else {
176            return
177        };
178
179        #[cfg(not(feature = "rayon"))]
180        let _ = cached_parent_overlays;
181
182        #[cfg(feature = "rayon")]
183        {
184            let parent_span = span;
185            for anchor_hash in cached_parent_overlays {
186                let manager = <Self as Clone>::clone(self);
187                let parent_span = parent_span.clone();
188                worker_pool.spawn(move || {
189                    let _span = tracing::trace_span!(
190                        target: "chain_state::state_trie_overlay",
191                        parent: parent_span,
192                        "precompute_state_trie_overlay",
193                        tip_hash = %hash,
194                        anchor_hash = %anchor_hash,
195                    )
196                    .entered();
197                    let _ = manager.precompute_overlay(hash, anchor_hash);
198                });
199            }
200        }
201    }
202
203    /// Removes blocks from the live block graph and prunes cached overlays that can no longer be
204    /// built from the remaining blocks.
205    #[tracing::instrument(
206        level = "trace",
207        target = "chain_state::state_trie_overlay",
208        skip_all,
209        fields(
210            block_count = tracing::field::Empty,
211            removed_blocks = tracing::field::Empty,
212            pruned_overlays = tracing::field::Empty,
213        )
214    )]
215    pub fn remove_blocks(&self, hashes: impl IntoIterator<Item = B256>) {
216        let span = tracing::Span::current();
217
218        // Remove blocks first, then prune overlays against the remaining block graph.
219        let mut block_count = 0usize;
220        let mut removed_blocks = 0usize;
221        let mut pruned_overlays = 0usize;
222        for hash in hashes {
223            block_count += 1;
224            removed_blocks += self.blocks.remove(&hash).is_some() as usize;
225        }
226        span.record("block_count", block_count);
227        span.record("removed_blocks", removed_blocks);
228
229        if removed_blocks > 0 {
230            let overlays_before = self.overlays.len();
231            let blocks = Arc::clone(&self.blocks);
232            self.overlays.retain(|key, _| {
233                key.tip_hash != key.anchor_hash &&
234                    Self::anchor_for_parent_in(blocks.as_ref(), key.tip_hash, key.anchor_hash) ==
235                        Some(key.anchor_hash)
236            });
237            pruned_overlays = overlays_before.saturating_sub(self.overlays.len());
238            span.record("pruned_overlays", pruned_overlays);
239        }
240        debug!(
241            target: "chain_state::state_trie_overlay",
242            block_count,
243            removed_blocks,
244            pruned_overlays,
245            "removed blocks from state trie overlay manager"
246        );
247    }
248
249    /// Returns the flattened overlay from `anchor_hash` to `parent_hash`.
250    #[tracing::instrument(
251        level = "trace",
252        target = "chain_state::state_trie_overlay",
253        skip_all,
254        fields(tip_hash = %parent_hash, anchor_hash = %anchor_hash)
255    )]
256    pub fn overlay_for_parent(
257        &self,
258        parent_hash: B256,
259        anchor_hash: B256,
260    ) -> Result<(Arc<TrieUpdatesSorted>, Arc<HashedPostStateSorted>), StateTrieOverlayError> {
261        debug!(
262            target: "chain_state::state_trie_overlay",
263            tip_hash = %parent_hash,
264            %anchor_hash,
265            "loading state trie overlay for parent"
266        );
267        let input = self.get_overlay(parent_hash, anchor_hash)?;
268        Ok((Arc::clone(&input.nodes), Arc::clone(&input.state)))
269    }
270
271    #[cfg(feature = "rayon")]
272    fn precompute_overlay(
273        &self,
274        tip_hash: B256,
275        anchor_hash: B256,
276    ) -> Result<(), StateTrieOverlayError> {
277        let _ = self.get_overlay_inner(tip_hash, anchor_hash, OverlayLookupMode::Precompute)?;
278        Ok(())
279    }
280
281    #[tracing::instrument(
282        level = "trace",
283        target = "chain_state::state_trie_overlay",
284        skip_all,
285        fields(
286            tip_hash = %tip_hash,
287            anchor_hash = %anchor_hash,
288            cache_reused = tracing::field::Empty,
289            block_count = tracing::field::Empty,
290            parent_overlay_reused = tracing::field::Empty,
291        )
292    )]
293    fn get_overlay(
294        &self,
295        tip_hash: B256,
296        anchor_hash: B256,
297    ) -> Result<Arc<TrieInputSorted>, StateTrieOverlayError> {
298        self.get_overlay_inner(tip_hash, anchor_hash, OverlayLookupMode::Required)
299            .map(|input| input.expect("required overlay lookups always return an overlay"))
300    }
301
302    fn get_overlay_inner(
303        &self,
304        tip_hash: B256,
305        anchor_hash: B256,
306        mode: OverlayLookupMode,
307    ) -> Result<Option<Arc<TrieInputSorted>>, StateTrieOverlayError> {
308        let key = OverlayCacheKey { anchor_hash, tip_hash };
309        let span = tracing::Span::current();
310
311        if let Some(entry) = self.overlays.get(&key).map(|entry| entry.value().clone()) {
312            return Ok(match entry {
313                OverlayCacheEntry::Ready(input) => {
314                    self.metrics.overlay_cache_reuses.increment(1);
315                    span.record("cache_reused", true);
316                    Some(input)
317                }
318                OverlayCacheEntry::Computing(waiter) => {
319                    span.record("cache_reused", true);
320                    if mode == OverlayLookupMode::Precompute {
321                        None
322                    } else {
323                        self.metrics.overlay_cache_reuses.increment(1);
324                        Some(waiter.wait())
325                    }
326                }
327            })
328        }
329        span.record("cache_reused", false);
330
331        // Resolve the block path and any cached parent overlay before locking the child entry.
332        let mut hash = tip_hash;
333        let mut blocks = Vec::new();
334        loop {
335            let block =
336                self.blocks.get(&hash).ok_or(StateTrieOverlayError { tip_hash, anchor_hash })?;
337            let parent_hash = block.recovered_block().parent_hash();
338            blocks.push(block.clone());
339
340            if parent_hash == anchor_hash {
341                break
342            }
343            hash = parent_hash;
344        }
345        span.record("block_count", blocks.len());
346        let parent_input = blocks.first().and_then(|block| {
347            let parent_hash = block.recovered_block().parent_hash();
348            (parent_hash != anchor_hash)
349                .then(|| {
350                    self.overlays
351                        .get(&OverlayCacheKey { anchor_hash, tip_hash: parent_hash })
352                        .and_then(|entry| entry.value().ready())
353                })
354                .flatten()
355        });
356        span.record("parent_overlay_reused", parent_input.is_some());
357        let compute_input = match parent_input {
358            Some(parent_input) => {
359                ComputeOverlayInput::ExtendCached { block: blocks.swap_remove(0), parent_input }
360            }
361            None => ComputeOverlayInput::MergeBlocks(blocks),
362        };
363
364        enum CacheAction {
365            Ready(Arc<TrieInputSorted>),
366            Wait(Arc<OverlayWaiter>),
367            Compute(Arc<OverlayWaiter>),
368            Skip,
369        }
370
371        let action = match self.overlays.entry(key) {
372            Entry::Occupied(entry) => match entry.get().clone() {
373                OverlayCacheEntry::Ready(input) => {
374                    self.metrics.overlay_cache_reuses.increment(1);
375                    span.record("cache_reused", true);
376                    CacheAction::Ready(input)
377                }
378                OverlayCacheEntry::Computing(waiter) => {
379                    span.record("cache_reused", true);
380                    if mode == OverlayLookupMode::Precompute {
381                        CacheAction::Skip
382                    } else {
383                        self.metrics.overlay_cache_reuses.increment(1);
384                        CacheAction::Wait(waiter)
385                    }
386                }
387            },
388            Entry::Vacant(entry) => {
389                self.metrics.overlay_cache_fills.increment(1);
390                let waiter = Arc::new(OverlayWaiter::new());
391                entry.insert(OverlayCacheEntry::Computing(Arc::clone(&waiter)));
392                CacheAction::Compute(waiter)
393            }
394        };
395
396        match action {
397            CacheAction::Ready(input) => Ok(Some(input)),
398            CacheAction::Wait(waiter) => Ok(Some(waiter.wait())),
399            CacheAction::Skip => Ok(None),
400            CacheAction::Compute(waiter) => {
401                let input = self.compute_overlay(compute_input, anchor_hash, span);
402                waiter.finish(Arc::clone(&input));
403
404                if let Entry::Occupied(mut entry) = self.overlays.entry(key) {
405                    // The entry may have been pruned while the overlay was computing. Only cache
406                    // the result if the map still points at the waiter installed by this task.
407                    let should_publish = match entry.get() {
408                        OverlayCacheEntry::Computing(existing) => Arc::ptr_eq(existing, &waiter),
409                        OverlayCacheEntry::Ready(_) => false,
410                    };
411                    if should_publish {
412                        entry.insert(OverlayCacheEntry::Ready(Arc::clone(&input)));
413                    }
414                }
415
416                Ok(Some(input))
417            }
418        }
419    }
420
421    /// Returns `preferred_anchor` if it is on the parent chain, otherwise the first missing parent.
422    ///
423    /// Returns `None` if `parent_hash` is not `preferred_anchor` and the manager does not contain a
424    /// block for `parent_hash`, meaning there is no in-memory parent chain to inspect.
425    pub fn anchor_for_parent(&self, parent_hash: B256, preferred_anchor: B256) -> Option<B256> {
426        Self::anchor_for_parent_in(self.blocks.as_ref(), parent_hash, preferred_anchor)
427    }
428
429    fn anchor_for_parent_in(
430        blocks: &DashMap<B256, ExecutedBlock<N>>,
431        parent_hash: B256,
432        preferred_anchor: B256,
433    ) -> Option<B256> {
434        if parent_hash == preferred_anchor {
435            return Some(preferred_anchor)
436        }
437
438        let mut hash = parent_hash;
439
440        loop {
441            let block_parent_hash = blocks.get(&hash)?.recovered_block().parent_hash();
442            if block_parent_hash == preferred_anchor {
443                return Some(block_parent_hash)
444            }
445            if !blocks.contains_key(&block_parent_hash) {
446                return Some(block_parent_hash)
447            }
448            hash = block_parent_hash;
449        }
450    }
451
452    fn compute_overlay(
453        &self,
454        compute_input: ComputeOverlayInput<N>,
455        anchor_hash: B256,
456        _span: tracing::Span,
457    ) -> Arc<TrieInputSorted> {
458        #[cfg(feature = "rayon")]
459        {
460            if let Some(worker_pool) = &self.worker_pool {
461                let compute_span = _span;
462                let metrics = self.metrics.clone();
463                return Arc::new(worker_pool.install_fn(move || {
464                    let _guard = compute_span.enter();
465                    compute_overlay(compute_input, anchor_hash, &metrics)
466                }))
467            }
468        }
469
470        Arc::new(compute_overlay(compute_input, anchor_hash, &self.metrics))
471    }
472}
473
474/// Error returned when a state trie overlay cannot be built from the manager's current block set.
475#[derive(Debug)]
476pub struct StateTrieOverlayError {
477    /// Requested in-memory tip hash.
478    tip_hash: B256,
479    /// Requested anchor hash.
480    anchor_hash: B256,
481}
482
483impl fmt::Display for StateTrieOverlayError {
484    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485        write!(
486            f,
487            "state trie overlay for tip {} cannot be anchored to {} with current blocks",
488            self.tip_hash, self.anchor_hash
489        )
490    }
491}
492
493impl std::error::Error for StateTrieOverlayError {}
494
495#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
496struct OverlayCacheKey {
497    anchor_hash: B256,
498    tip_hash: B256,
499}
500
501#[derive(Clone)]
502enum OverlayCacheEntry {
503    Ready(Arc<TrieInputSorted>),
504    Computing(Arc<OverlayWaiter>),
505}
506
507impl OverlayCacheEntry {
508    const fn is_ready(&self) -> bool {
509        matches!(self, Self::Ready(_))
510    }
511
512    fn ready(&self) -> Option<Arc<TrieInputSorted>> {
513        match self {
514            Self::Ready(input) => Some(Arc::clone(input)),
515            Self::Computing(_) => None,
516        }
517    }
518}
519
520#[derive(Clone, Copy, Debug, Eq, PartialEq)]
521enum OverlayLookupMode {
522    Required,
523    Precompute,
524}
525
526struct OverlayWaiter {
527    input: OnceLock<Arc<TrieInputSorted>>,
528}
529
530impl OverlayWaiter {
531    const fn new() -> Self {
532        Self { input: OnceLock::new() }
533    }
534
535    fn wait(&self) -> Arc<TrieInputSorted> {
536        Arc::clone(self.input.wait())
537    }
538
539    fn finish(&self, computed: Arc<TrieInputSorted>) {
540        let _ = self.input.set(computed);
541    }
542}
543
544enum ComputeOverlayInput<N: NodePrimitives> {
545    ExtendCached { block: ExecutedBlock<N>, parent_input: Arc<TrieInputSorted> },
546    MergeBlocks(Vec<ExecutedBlock<N>>),
547}
548
549#[tracing::instrument(
550    level = "trace",
551    target = "chain_state::state_trie_overlay",
552    skip_all,
553    fields(
554        anchor_hash = %anchor_hash,
555        block_count = tracing::field::Empty,
556        parent_overlay = tracing::field::Empty,
557        elapsed_us = tracing::field::Empty,
558    )
559)]
560fn compute_overlay<N: NodePrimitives>(
561    input: ComputeOverlayInput<N>,
562    anchor_hash: B256,
563    metrics: &StateTrieOverlayMetrics,
564) -> TrieInputSorted {
565    let started_at = Instant::now();
566    let block_count = match &input {
567        ComputeOverlayInput::ExtendCached { .. } => 1,
568        ComputeOverlayInput::MergeBlocks(blocks) => blocks.len(),
569    };
570    let parent_overlay = matches!(&input, ComputeOverlayInput::ExtendCached { .. });
571    tracing::Span::current().record("block_count", block_count);
572    tracing::Span::current().record("parent_overlay", parent_overlay);
573
574    let overlay = match input {
575        ComputeOverlayInput::ExtendCached { block, parent_input } => {
576            let trie_data = block.trie_data();
577
578            trace!(
579                target: "chain_state::state_trie_overlay",
580                %anchor_hash,
581                head = %block.recovered_block().hash(),
582                "extending cached parent state trie overlay"
583            );
584
585            let mut overlay = parent_input.as_ref().clone();
586            extend_overlay(
587                &mut overlay,
588                &trie_data.sorted.hashed_state,
589                &trie_data.sorted.trie_updates,
590            );
591            overlay
592        }
593        ComputeOverlayInput::MergeBlocks(blocks) => merge_blocks(blocks),
594    };
595
596    let elapsed = started_at.elapsed();
597    metrics.overlay_computation_duration_seconds.record(elapsed.as_secs_f64());
598    tracing::Span::current().record("elapsed_us", elapsed.as_micros() as u64);
599    debug!(
600        target: "chain_state::state_trie_overlay",
601        %anchor_hash,
602        block_count,
603        parent_overlay,
604        ?elapsed,
605        "computed state trie overlay"
606    );
607
608    overlay
609}
610
611fn merge_blocks<N: NodePrimitives>(blocks: Vec<ExecutedBlock<N>>) -> TrieInputSorted {
612    let trie_data = blocks.iter().map(ExecutedBlock::trie_data).collect::<Vec<_>>();
613
614    #[cfg(feature = "rayon")]
615    let (nodes, state) = rayon::join(
616        || {
617            TrieUpdatesSorted::merge_batch(
618                trie_data.iter().map(|data| Arc::clone(&data.sorted.trie_updates)),
619            )
620        },
621        || {
622            HashedPostStateSorted::merge_batch(
623                trie_data.iter().map(|data| Arc::clone(&data.sorted.hashed_state)),
624            )
625        },
626    );
627
628    #[cfg(not(feature = "rayon"))]
629    let (nodes, state) = (
630        TrieUpdatesSorted::merge_batch(
631            trie_data.iter().map(|data| Arc::clone(&data.sorted.trie_updates)),
632        ),
633        HashedPostStateSorted::merge_batch(
634            trie_data.iter().map(|data| Arc::clone(&data.sorted.hashed_state)),
635        ),
636    );
637
638    TrieInputSorted::new(nodes, state, Default::default())
639}
640
641fn extend_overlay(
642    overlay: &mut TrieInputSorted,
643    hashed_state: &HashedPostStateSorted,
644    trie_updates: &TrieUpdatesSorted,
645) {
646    #[cfg(feature = "rayon")]
647    {
648        rayon::join(
649            || {
650                if !hashed_state.is_empty() {
651                    Arc::make_mut(&mut overlay.state).extend_ref_and_sort(hashed_state);
652                }
653            },
654            || {
655                if !trie_updates.is_empty() {
656                    Arc::make_mut(&mut overlay.nodes).extend_ref_and_sort(trie_updates);
657                }
658            },
659        );
660    }
661
662    #[cfg(not(feature = "rayon"))]
663    {
664        if !hashed_state.is_empty() {
665            Arc::make_mut(&mut overlay.state).extend_ref_and_sort(hashed_state);
666        }
667        if !trie_updates.is_empty() {
668            Arc::make_mut(&mut overlay.nodes).extend_ref_and_sort(trie_updates);
669        }
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676    use crate::{test_utils::TestBlockBuilder, EthPrimitives, ExecutedBlock};
677    use alloy_primitives::U256;
678    use reth_primitives_traits::Account;
679    use reth_trie::{updates::TrieUpdatesSorted, ComputedTrieData, HashedPostState, HashedStorage};
680    #[cfg(feature = "rayon")]
681    use std::time::Instant;
682    use std::{
683        sync::{mpsc, Arc},
684        thread,
685        time::Duration,
686    };
687
688    fn with_unique_state(
689        block: &ExecutedBlock<EthPrimitives>,
690        id: u8,
691    ) -> ExecutedBlock<EthPrimitives> {
692        let hashed_address = B256::with_last_byte(id);
693        let hashed_slot = B256::with_last_byte(id.saturating_add(32));
694        let hashed_state = HashedPostState::default()
695            .with_accounts([(hashed_address, Some(Account::default()))])
696            .with_storages([(
697                hashed_address,
698                HashedStorage::from_iter(false, [(hashed_slot, U256::from(id))]),
699            )])
700            .into_sorted();
701
702        ExecutedBlock::new(
703            Arc::clone(&block.recovered_block),
704            Arc::clone(&block.execution_output),
705            ComputedTrieData::new(Arc::new(hashed_state), Arc::new(TrieUpdatesSorted::default())),
706        )
707    }
708
709    fn test_blocks() -> Vec<ExecutedBlock<EthPrimitives>> {
710        TestBlockBuilder::eth()
711            .get_executed_blocks(1..4)
712            .enumerate()
713            .map(|(index, block)| with_unique_state(&block, index as u8 + 1))
714            .collect()
715    }
716
717    #[test]
718    fn errors_for_unknown_parent() {
719        let manager = StateTrieOverlayManager::<EthPrimitives>::default();
720        let parent = B256::random();
721        let anchor = B256::random();
722
723        let err = manager.overlay_for_parent(parent, anchor).unwrap_err();
724
725        assert_eq!(err.tip_hash, parent);
726        assert_eq!(err.anchor_hash, anchor);
727    }
728
729    #[test]
730    fn builds_managed_overlay_for_inserted_blocks() {
731        let manager = StateTrieOverlayManager::default();
732        let blocks = test_blocks();
733        for block in &blocks {
734            manager.insert_block(block.clone());
735        }
736
737        let anchor_hash = blocks[0].recovered_block().parent_hash();
738
739        let (_, state) =
740            manager.overlay_for_parent(blocks[2].recovered_block().hash(), anchor_hash).unwrap();
741        assert_eq!(state.accounts.len(), 3);
742
743        let short_anchor = blocks[1].recovered_block().hash();
744        let (_, short) =
745            manager.overlay_for_parent(blocks[2].recovered_block().hash(), short_anchor).unwrap();
746        assert_eq!(short.accounts.len(), 1);
747        let (_, cached_short) =
748            manager.overlay_for_parent(blocks[2].recovered_block().hash(), short_anchor).unwrap();
749        assert!(Arc::ptr_eq(&short, &cached_short));
750    }
751
752    #[test]
753    fn returns_anchor_for_in_memory_parent() {
754        let manager = StateTrieOverlayManager::default();
755        let blocks = test_blocks();
756        for block in &blocks {
757            manager.insert_block(block.clone());
758        }
759
760        assert_eq!(
761            manager.anchor_for_parent(blocks[2].recovered_block().hash(), B256::random()),
762            Some(blocks[0].recovered_block().parent_hash())
763        );
764
765        manager.remove_blocks([blocks[0].recovered_block().hash()]);
766        assert_eq!(
767            manager.anchor_for_parent(
768                blocks[2].recovered_block().hash(),
769                blocks[0].recovered_block().hash()
770            ),
771            Some(blocks[0].recovered_block().hash())
772        );
773    }
774
775    #[test]
776    fn prefers_anchor_in_parent_chain() {
777        let manager = StateTrieOverlayManager::default();
778        let blocks = test_blocks();
779        for block in &blocks {
780            manager.insert_block(block.clone());
781        }
782
783        let db_tip_hash = blocks[1].recovered_block().hash();
784        assert_eq!(
785            manager.anchor_for_parent(blocks[2].recovered_block().hash(), db_tip_hash),
786            Some(db_tip_hash)
787        );
788    }
789
790    #[test]
791    fn required_lookup_waits_for_in_progress_overlay() {
792        let manager = StateTrieOverlayManager::<EthPrimitives>::default();
793        let key = OverlayCacheKey {
794            anchor_hash: B256::with_last_byte(1),
795            tip_hash: B256::with_last_byte(2),
796        };
797        let waiter = Arc::new(OverlayWaiter::new());
798        manager.overlays.insert(key, OverlayCacheEntry::Computing(Arc::clone(&waiter)));
799
800        let (tx, rx) = mpsc::channel();
801        thread::spawn(move || {
802            let res =
803                manager.overlay_for_parent(key.tip_hash, key.anchor_hash).map(|(_, state)| state);
804            tx.send(res).unwrap();
805        });
806
807        assert!(matches!(
808            rx.recv_timeout(Duration::from_millis(50)),
809            Err(mpsc::RecvTimeoutError::Timeout)
810        ));
811
812        waiter.finish(Arc::new(TrieInputSorted::default()));
813
814        let state = rx.recv_timeout(Duration::from_secs(1)).unwrap().unwrap();
815        assert!(state.is_empty());
816    }
817
818    #[cfg(feature = "rayon")]
819    #[test]
820    fn precompute_skips_in_progress_overlay() {
821        let manager = StateTrieOverlayManager::<EthPrimitives>::new(Arc::new(WorkerPool::new(
822            1,
823            "test-ovly",
824        )));
825        let key = OverlayCacheKey {
826            anchor_hash: B256::with_last_byte(1),
827            tip_hash: B256::with_last_byte(2),
828        };
829        manager.overlays.insert(key, OverlayCacheEntry::Computing(Arc::new(OverlayWaiter::new())));
830
831        let (tx, rx) = mpsc::channel();
832        thread::spawn(move || {
833            tx.send(manager.precompute_overlay(key.tip_hash, key.anchor_hash)).unwrap();
834        });
835
836        rx.recv_timeout(Duration::from_secs(1)).unwrap().unwrap();
837    }
838
839    #[cfg(feature = "rayon")]
840    #[test]
841    fn insert_block_prepares_child_overlay_from_cached_parent() {
842        let manager = StateTrieOverlayManager::new(Arc::new(WorkerPool::new(2, "test-ovly")));
843        let blocks = test_blocks();
844
845        manager.insert_block(blocks[0].clone());
846
847        let anchor_hash = blocks[0].recovered_block().parent_hash();
848        let parent_hash = blocks[0].recovered_block().hash();
849        manager.overlay_for_parent(parent_hash, anchor_hash).unwrap();
850
851        let child_hash = blocks[1].recovered_block().hash();
852        manager.insert_block(blocks[1].clone());
853
854        let child_key = OverlayCacheKey { anchor_hash, tip_hash: child_hash };
855        let deadline = Instant::now() + Duration::from_secs(5);
856        while !manager.overlays.contains_key(&child_key) {
857            assert!(
858                Instant::now() < deadline,
859                "timed out waiting for optimistically prepared child overlay"
860            );
861            thread::sleep(Duration::from_millis(10));
862        }
863
864        let (_, state) = manager.overlay_for_parent(child_hash, anchor_hash).unwrap();
865        assert_eq!(state.accounts.len(), 2);
866    }
867
868    #[test]
869    fn prunes_cached_overlays_after_removing_blocks() {
870        let manager = StateTrieOverlayManager::default();
871        let blocks = test_blocks();
872        for block in &blocks {
873            manager.insert_block(block.clone());
874        }
875
876        let original_anchor = blocks[0].recovered_block().parent_hash();
877        manager.overlay_for_parent(blocks[2].recovered_block().hash(), original_anchor).unwrap();
878
879        manager.remove_blocks([
880            blocks[0].recovered_block().hash(),
881            blocks[1].recovered_block().hash(),
882        ]);
883
884        let anchor_hash = blocks[1].recovered_block().hash();
885        assert!(manager
886            .overlay_for_parent(blocks[2].recovered_block().hash(), original_anchor)
887            .is_err());
888
889        let (_, state) =
890            manager.overlay_for_parent(blocks[2].recovered_block().hash(), anchor_hash).unwrap();
891        assert_eq!(state.accounts.len(), 1);
892    }
893}