Skip to main content

reth_storage_overlay/
builder.rs

1use crate::OverlayManager;
2use alloy_eips::BlockNumHash;
3use alloy_primitives::{BlockHash, B256};
4use metrics::{Counter, Histogram};
5use reth_chain_state::ExecutedBlock;
6use reth_errors::{ProviderError, ProviderResult};
7use reth_ethereum_primitives::EthPrimitives;
8use reth_metrics::Metrics;
9use reth_primitives_traits::{AlloyBlockHeader, NodePrimitives};
10use reth_prune_types::PruneSegment;
11use reth_stages_types::StageId;
12use reth_storage_api::{
13    BlockNumReader, ChangeSetReader, DBProvider, PruneCheckpointReader, StageCheckpointReader,
14    StorageChangeSetReader, StorageSettingsCache,
15};
16use reth_trie::{updates::TrieUpdatesSorted, HashedPostStateSorted};
17use reth_trie_db::DatabaseHashedPostState;
18use std::{
19    sync::Arc,
20    time::{Duration, Instant},
21};
22use tracing::{debug, debug_span, instrument};
23
24/// Contains the trie and hashed-state data required to initialize an overlay state provider.
25#[derive(Debug, Clone)]
26pub struct Overlay {
27    /// Trie updates overlay.
28    pub trie_updates: Arc<TrieUpdatesSorted>,
29    /// Hashed state overlay.
30    pub hashed_post_state: Arc<HashedPostStateSorted>,
31}
32
33impl Overlay {
34    fn empty() -> Self {
35        Self {
36            trie_updates: Arc::new(TrieUpdatesSorted::default()),
37            hashed_post_state: Arc::new(HashedPostStateSorted::default()),
38        }
39    }
40}
41
42/// Source of data to apply on top of the durable database state.
43#[derive(Debug, Clone)]
44pub enum OverlaySource {
45    /// Immediate overlay with already-computed data.
46    Immediate {
47        /// Trie updates overlay.
48        ///
49        /// This can be non-empty when a caller starts with an explicit `TrieInputSorted`, such
50        /// as historical providers.
51        trie: Arc<TrieUpdatesSorted>,
52        /// Hashed state overlay.
53        state: Arc<HashedPostStateSorted>,
54    },
55    /// Manager-backed overlay for in-memory state.
56    Managed,
57}
58
59/// Builder for calculating trie and hashed-state overlays.
60///
61/// This stores the overlay manager, overlay configuration, and the logic for resolving overlays
62/// and collecting reverts.
63#[derive(Debug, Clone)]
64pub struct OverlayBuilder<N: NodePrimitives = EthPrimitives> {
65    /// Parent hash requested by the caller.
66    parent_hash: B256,
67    /// Optional overlay source.
68    overlay_source: Option<OverlaySource>,
69    /// Manager used for cached changesets and in-memory parent state.
70    overlay_manager: OverlayManager<N>,
71    /// Anchor hash of the reused sparse trie, if this task reused one.
72    reused_sparse_trie_anchor_hash: Option<B256>,
73    /// Whether building the overlay may query revert changesets.
74    no_reverts: bool,
75    /// Metrics for overlay construction.
76    metrics: OverlayBuilderMetrics,
77}
78
79impl<N: NodePrimitives> OverlayBuilder<N> {
80    /// Create a new manager-backed overlay builder.
81    pub(crate) fn new(parent_hash: B256, overlay_manager: OverlayManager<N>) -> Self {
82        Self {
83            parent_hash,
84            overlay_source: Some(OverlaySource::Managed),
85            overlay_manager,
86            reused_sparse_trie_anchor_hash: None,
87            no_reverts: false,
88            metrics: OverlayBuilderMetrics::default(),
89        }
90    }
91
92    /// Set the overlay source.
93    ///
94    /// This overlay will be applied on top of any reverts.
95    pub fn with_overlay_source(mut self, source: Option<OverlaySource>) -> Self {
96        self.overlay_source = source;
97        self
98    }
99
100    /// Skips managed overlay construction when the sparse trie was reused and the DB tip is
101    /// already covered by its anchor-to-parent range.
102    pub const fn with_skip_overlay_for_reused_sparse_trie(mut self, anchor_hash: B256) -> Self {
103        self.reused_sparse_trie_anchor_hash = Some(anchor_hash);
104        self
105    }
106
107    /// Returns an error instead of querying revert changesets when reverts are required.
108    pub const fn with_no_reverts(mut self) -> Self {
109        self.no_reverts = true;
110        self
111    }
112
113    /// Sets an immediate hashed-state and trie-updates overlay.
114    pub fn with_immediate_state_trie_overlay(
115        mut self,
116        state: Arc<HashedPostStateSorted>,
117        trie: Arc<TrieUpdatesSorted>,
118    ) -> Self {
119        self.overlay_source = Some(OverlaySource::Immediate { trie, state });
120        self
121    }
122
123    /// Returns the durable anchor to use for this builder's parent.
124    pub fn anchor_at_parent<Provider>(
125        &self,
126        provider: &Provider,
127    ) -> ProviderResult<AnchorForParent<N>>
128    where
129        Provider: StageCheckpointReader + BlockNumReader + PruneCheckpointReader,
130    {
131        let (partial_state_trie, finish) = database_state_frontiers(provider)?;
132        self.anchor_at_parent_with_frontiers(provider, partial_state_trie, finish)
133    }
134
135    /// Returns the durable anchor to use for this builder's parent using known frontiers.
136    pub fn anchor_at_parent_with_frontiers<Provider>(
137        &self,
138        provider: &Provider,
139        partial_state_trie: BlockNumHash,
140        finish: BlockNumHash,
141    ) -> ProviderResult<AnchorForParent<N>>
142    where
143        Provider: BlockNumReader + PruneCheckpointReader,
144    {
145        match &self.overlay_source {
146            Some(OverlaySource::Managed) => anchor_for_parent_with_frontiers(
147                self.parent_hash,
148                self.overlay_manager.parent_chain(self.parent_hash),
149                partial_state_trie,
150                finish,
151                provider,
152            ),
153            _ => anchor_for_parent_with_frontiers(
154                self.parent_hash,
155                std::iter::empty(),
156                partial_state_trie,
157                finish,
158                provider,
159            ),
160        }
161    }
162
163    /// Builds the effective overlay for the given provider.
164    #[instrument(level = "debug", target = "storage::overlay", skip_all)]
165    pub fn build_overlay<Provider>(&self, provider: &Provider) -> ProviderResult<Overlay>
166    where
167        Provider: StageCheckpointReader
168            + PruneCheckpointReader
169            + ChangeSetReader
170            + StorageChangeSetReader
171            + DBProvider
172            + BlockNumReader
173            + StorageSettingsCache,
174    {
175        let (state_trie_tip_block, finish_tip_block) = database_state_frontiers(provider)?;
176        self.build_overlay_at_frontiers(provider, state_trie_tip_block, finish_tip_block)
177    }
178
179    /// Builds the effective overlay using frontiers already read from the provider.
180    ///
181    /// This is useful for callers that key an overlay cache by the durable frontiers.
182    #[instrument(
183        level = "debug",
184        target = "storage::overlay",
185        skip_all,
186        fields(?state_trie_tip_block, ?finish_tip_block, parent_hash = ?self.parent_hash)
187    )]
188    pub fn build_overlay_at_frontiers<Provider>(
189        &self,
190        provider: &Provider,
191        state_trie_tip_block: BlockNumHash,
192        finish_tip_block: BlockNumHash,
193    ) -> ProviderResult<Overlay>
194    where
195        Provider: ChangeSetReader
196            + StorageChangeSetReader
197            + DBProvider
198            + BlockNumReader
199            + PruneCheckpointReader
200            + StorageSettingsCache,
201    {
202        let retrieve_trie_reverts_duration;
203        let retrieve_hashed_state_reverts_duration;
204        let trie_updates_total_len;
205        let hashed_state_updates_total_len;
206
207        let anchor_for_parent =
208            self.anchor_at_parent_with_frontiers(provider, state_trie_tip_block, finish_tip_block)?;
209
210        // Collect any reverts which are required to bring the DB view back to the anchor hash.
211        let (trie_updates, hashed_post_state) = match anchor_for_parent {
212            AnchorForParent::RevertsRequired { anchor, finish, .. } => {
213                if self.no_reverts {
214                    return Err(ProviderError::other(std::io::Error::other(format!(
215                        "reverts are disabled, but overlay for parent {} requires reverting Finish #{} ({}) to anchor #{} ({})",
216                        self.parent_hash, finish.number, finish.hash, anchor.number, anchor.hash,
217                    ))))
218                }
219
220                let revert_blocks = anchor.number + 1..=finish.number;
221
222                debug!(
223                    target: "storage::overlay",
224                    ?revert_blocks,
225                    ?anchor,
226                    "Collecting trie reverts for overlay state provider"
227                );
228
229                let trie_reverts = {
230                    let _guard = debug_span!(target: "storage::overlay", "retrieving_trie_reverts")
231                        .entered();
232                    let start = Instant::now();
233                    let accumulated_reverts =
234                        self.overlay_manager.get_or_compute_cached_changesets_range_at_frontiers(
235                            provider,
236                            revert_blocks.clone(),
237                            state_trie_tip_block,
238                            finish_tip_block,
239                        )?;
240                    retrieve_trie_reverts_duration = start.elapsed();
241                    accumulated_reverts
242                };
243
244                let mut hashed_state_reverts = {
245                    let _guard =
246                        debug_span!(target: "storage::overlay", "retrieving_hashed_state_reverts")
247                            .entered();
248                    let start = Instant::now();
249                    let res = HashedPostStateSorted::from_reverts(provider, revert_blocks)?;
250                    retrieve_hashed_state_reverts_duration = start.elapsed();
251                    res
252                };
253
254                // Resolve overlays and extend reverts with them. If reverts are empty, use overlays
255                // directly to avoid cloning.
256                let (overlay_trie, overlay_state) = self.resolve_overlays(anchor.hash)?;
257
258                let trie_updates = if trie_reverts.is_empty() {
259                    overlay_trie
260                } else if !overlay_trie.is_empty() {
261                    let mut trie_reverts = (*trie_reverts).clone();
262                    trie_reverts.extend_ref_and_sort(&overlay_trie);
263                    Arc::new(trie_reverts)
264                } else {
265                    trie_reverts
266                };
267
268                let hashed_state_updates = if hashed_state_reverts.is_empty() {
269                    overlay_state
270                } else if !overlay_state.is_empty() {
271                    hashed_state_reverts.extend_ref_and_sort(&overlay_state);
272                    Arc::new(hashed_state_reverts)
273                } else {
274                    Arc::new(hashed_state_reverts)
275                };
276
277                trie_updates_total_len = trie_updates.total_len();
278                hashed_state_updates_total_len = hashed_state_updates.total_len();
279
280                debug!(
281                    target: "storage::overlay",
282                    num_trie_updates = ?trie_updates_total_len,
283                    num_state_updates = ?hashed_state_updates_total_len,
284                    ?anchor,
285                    "Reverted to anchor block",
286                );
287
288                (trie_updates, hashed_state_updates)
289            }
290            AnchorForParent::NoReverts { anchor, .. } => {
291                // If no reverts are needed, use the manager overlay directly unless the reused
292                // sparse trie already covers both durable frontiers through the
293                // requested parent.
294                if self.should_skip_overlay_for_reused_sparse_trie(
295                    state_trie_tip_block.hash,
296                    finish_tip_block.hash,
297                ) {
298                    debug!(
299                        target: "storage::overlay",
300                        parent_hash = %self.parent_hash,
301                        state_trie_tip_hash = %state_trie_tip_block.hash,
302                        finish_tip_hash = %finish_tip_block.hash,
303                        sparse_trie_anchor_hash = ?self.reused_sparse_trie_anchor_hash,
304                        "Skipping overlay construction because reused sparse trie covers durable frontiers to parent"
305                    );
306
307                    self.metrics.sparse_trie_overlay_skips.increment(1);
308
309                    return Ok(Overlay::empty())
310                }
311
312                let (trie_updates, hashed_post_state) = self.resolve_overlays(anchor.hash)?;
313
314                retrieve_trie_reverts_duration = Duration::ZERO;
315                retrieve_hashed_state_reverts_duration = Duration::ZERO;
316                trie_updates_total_len = trie_updates.total_len();
317                hashed_state_updates_total_len = hashed_post_state.total_len();
318
319                debug!(
320                    target: "storage::overlay",
321                    num_trie_updates = trie_updates_total_len,
322                    num_state_updates = hashed_state_updates_total_len,
323                    ?anchor,
324                    "Built overlay directly from durable frontier"
325                );
326
327                (trie_updates, hashed_post_state)
328            }
329        };
330
331        self.metrics
332            .retrieve_trie_reverts_duration
333            .record(retrieve_trie_reverts_duration.as_secs_f64());
334        self.metrics
335            .retrieve_hashed_state_reverts_duration
336            .record(retrieve_hashed_state_reverts_duration.as_secs_f64());
337        self.metrics.trie_updates_size.record(trie_updates_total_len as f64);
338        self.metrics.hashed_state_size.record(hashed_state_updates_total_len as f64);
339
340        Ok(Overlay { trie_updates, hashed_post_state })
341    }
342
343    /// Resolves the effective overlay (trie updates, hashed state).
344    fn resolve_overlays(
345        &self,
346        anchor_hash: BlockHash,
347    ) -> ProviderResult<(Arc<TrieUpdatesSorted>, Arc<HashedPostStateSorted>)> {
348        match &self.overlay_source {
349            Some(OverlaySource::Managed) => {
350                if anchor_hash == self.parent_hash {
351                    Ok((
352                        Arc::new(TrieUpdatesSorted::default()),
353                        Arc::new(HashedPostStateSorted::default()),
354                    ))
355                } else {
356                    self.overlay_manager
357                        .overlay_for_parent(self.parent_hash, anchor_hash)
358                        .map_err(ProviderError::other)
359                }
360            }
361            Some(OverlaySource::Immediate { trie, state }) => {
362                if anchor_hash != self.parent_hash {
363                    return Err(ProviderError::other(std::io::Error::other(format!(
364                        "anchor_hash {anchor_hash} doesn't match OverlayBuilder's configured parent ({})",
365                        self.parent_hash
366                    ))))
367                }
368                Ok((Arc::clone(trie), Arc::clone(state)))
369            }
370            None => Ok((
371                Arc::new(TrieUpdatesSorted::default()),
372                Arc::new(HashedPostStateSorted::default()),
373            )),
374        }
375    }
376
377    /// Returns true if managed overlay resolution can be skipped for this builder.
378    fn should_skip_overlay_for_reused_sparse_trie(
379        &self,
380        state_trie_tip_hash: B256,
381        finish_tip_hash: B256,
382    ) -> bool {
383        let Some(anchor_hash) = self.reused_sparse_trie_anchor_hash else { return false };
384
385        match &self.overlay_source {
386            Some(OverlaySource::Managed) => {
387                self.overlay_manager.contains_hash(
388                    self.parent_hash,
389                    anchor_hash,
390                    state_trie_tip_hash,
391                ) && self.overlay_manager.contains_hash(
392                    self.parent_hash,
393                    anchor_hash,
394                    finish_tip_hash,
395                )
396            }
397            _ => false,
398        }
399    }
400}
401
402/// Returns the highest blocks whose state/trie data and non-state/trie data are durably
403/// available in the database.
404pub fn database_state_frontiers<Provider>(
405    provider: &Provider,
406) -> ProviderResult<(BlockNumHash, BlockNumHash)>
407where
408    Provider: StageCheckpointReader + BlockNumReader,
409{
410    let checkpoint = provider
411        .get_stage_checkpoint(StageId::Finish)?
412        .ok_or_else(|| ProviderError::InsufficientChangesets { requested: 0, available: 0..=0 })?;
413    let state_trie_tip_number = checkpoint
414        .finish_stage_checkpoint()
415        .and_then(|finish| finish.partial_state_trie())
416        .unwrap_or(checkpoint.block_number);
417    let state_trie_tip_hash = provider
418        .convert_number(state_trie_tip_number.into())?
419        .ok_or_else(|| ProviderError::HeaderNotFound(state_trie_tip_number.into()))?;
420    let finish_tip_number = checkpoint.block_number;
421    let finish_tip_hash = provider
422        .convert_number(finish_tip_number.into())?
423        .ok_or_else(|| ProviderError::HeaderNotFound(finish_tip_number.into()))?;
424
425    Ok((
426        BlockNumHash::new(state_trie_tip_number, state_trie_tip_hash),
427        BlockNumHash::new(finish_tip_number, finish_tip_hash),
428    ))
429}
430
431/// Metrics for overlay construction.
432#[derive(Clone, Metrics)]
433#[metrics(scope = "storage.overlay.builder")]
434struct OverlayBuilderMetrics {
435    /// Duration of retrieving trie updates from the database.
436    retrieve_trie_reverts_duration: Histogram,
437    /// Duration of retrieving hashed state from the database.
438    retrieve_hashed_state_reverts_duration: Histogram,
439    /// Size of trie updates (number of entries).
440    trie_updates_size: Histogram,
441    /// Size of hashed state (number of entries).
442    hashed_state_size: Histogram,
443    /// Number of managed overlay creations skipped because the reused sparse trie already covers
444    /// the DB tip to parent range.
445    sparse_trie_overlay_skips: Counter,
446}
447
448fn anchor_for_parent_in<N: NodePrimitives>(
449    parent_hash: B256,
450    mut in_mem_chain: impl Iterator<Item = ExecutedBlock<N>>,
451    preferred_anchor: B256,
452) -> B256 {
453    if parent_hash == preferred_anchor {
454        return parent_hash
455    }
456
457    let mut hash = parent_hash;
458
459    loop {
460        let Some(block) = in_mem_chain.next() else { return hash };
461        let block_parent_hash = block.recovered_block().parent_hash();
462
463        if block_parent_hash == preferred_anchor {
464            return block_parent_hash
465        }
466        hash = block_parent_hash;
467    }
468}
469
470/// Describes whether an overlay must revert the database before using its anchor.
471#[derive(Debug)]
472pub enum AnchorForParent<N: NodePrimitives> {
473    /// The in-memory chain covers the durable frontiers through this anchor.
474    NoReverts {
475        /// Block to anchor the overlay to.
476        anchor: BlockNumHash,
477        /// In-memory blocks from `parent_hash` through, but excluding, `anchor`.
478        overlay: Vec<ExecutedBlock<N>>,
479    },
480    /// The database must be reverted from `finish` to `anchor` first.
481    RevertsRequired {
482        /// Block to anchor the overlay to.
483        anchor: BlockNumHash,
484        /// Current Finish frontier.
485        finish: BlockNumHash,
486        /// In-memory blocks from `parent_hash` through, but excluding, `anchor`.
487        overlay: Vec<ExecutedBlock<N>>,
488    },
489}
490
491/// Returns the anchor block to use for the target parent and a chain of in-memory blocks.
492///
493/// # Arguments
494/// * `parent`: The block whose post-state is being targeted.
495/// * `in_mem_chain`: Yields the in-memory blocks in the chain, starting at `parent_hash`.
496/// * `provider`: Used to resolve the durable frontiers and check changeset availability.
497pub fn anchor_for_parent<N, Provider>(
498    parent_hash: B256,
499    in_mem_chain: impl Iterator<Item = ExecutedBlock<N>>,
500    provider: &Provider,
501) -> ProviderResult<AnchorForParent<N>>
502where
503    N: NodePrimitives,
504    Provider: StageCheckpointReader + BlockNumReader + PruneCheckpointReader,
505{
506    let (partial_state_trie, finish) = database_state_frontiers(provider)?;
507    anchor_for_parent_with_frontiers(
508        parent_hash,
509        in_mem_chain,
510        partial_state_trie,
511        finish,
512        provider,
513    )
514}
515
516/// Returns the anchor block to use for the target parent and a chain of in-memory blocks using
517/// known durable frontiers.
518///
519/// # Arguments
520/// * `parent`: The block whose post-state is being targeted.
521/// * `in_mem_chain`: Yields the in-memory blocks in the chain, starting at `parent_hash`.
522/// * `partial_state_trie`: The durable state/trie frontier.
523/// * `finish`: The durable Finish frontier.
524/// * `provider`: Used to resolve the parent and check changeset availability.
525pub fn anchor_for_parent_with_frontiers<N, Provider>(
526    parent_hash: B256,
527    in_mem_chain: impl Iterator<Item = ExecutedBlock<N>>,
528    partial_state_trie: BlockNumHash,
529    finish: BlockNumHash,
530    provider: &Provider,
531) -> ProviderResult<AnchorForParent<N>>
532where
533    N: NodePrimitives,
534    Provider: BlockNumReader + PruneCheckpointReader,
535{
536    use std::io::Error;
537
538    let persisted_parent = provider
539        .block_number(parent_hash)?
540        .filter(|&parent_number| parent_number <= partial_state_trie.number);
541
542    let mut finish_seen = parent_hash == finish.hash;
543    let (anchor, overlay) = if let Some(parent_number) = persisted_parent {
544        (BlockNumHash::new(parent_number, parent_hash), Vec::new())
545    } else {
546        let mut overlay = Vec::new();
547        let mut in_mem_chain = in_mem_chain.inspect(|block| {
548            finish_seen |= block.recovered_block().hash() == finish.hash;
549            overlay.push(block.clone());
550        });
551
552        let anchor_hash =
553            anchor_for_parent_in(parent_hash, &mut in_mem_chain, partial_state_trie.hash);
554        let anchor = if anchor_hash == partial_state_trie.hash {
555            BlockNumHash::new(partial_state_trie.number, anchor_hash)
556        } else {
557            let anchor_number = provider
558                .convert_hash_or_number(anchor_hash.into())?
559                .ok_or(ProviderError::BlockHashNotFound(anchor_hash))?;
560            BlockNumHash::new(anchor_number, anchor_hash)
561        };
562        (anchor, overlay)
563    };
564
565    finish_seen |= anchor.hash == finish.hash;
566
567    if anchor.number > partial_state_trie.number {
568        return Err(ProviderError::other(Error::other(format!(
569                "overlay anchor #{} ({}) is after partial state trie frontier #{} ({}); missing trie updates for blocks #{}..=#{}",
570                anchor.number,
571                anchor.hash,
572                partial_state_trie.number,
573                partial_state_trie.hash,
574                partial_state_trie.number + 1,
575                anchor.number,
576            ))))
577    }
578
579    // If the Finish block (db tip) was seen in the in-memory chain then we know that anchor is on
580    // the same chain as partial_state_trie as well. Given that anchor <= partial_state_trie, we can
581    // be sure that the in-memory chain is a superset of partial_state_trie+1..finish, and therefore
582    // can be used without reverts.
583    if finish_seen {
584        return Ok(AnchorForParent::NoReverts { anchor, overlay })
585    }
586
587    // Otherwise reverts are required; we check the changesets to make sure they are actually
588    // available before signaling that they are required.
589    let account_history = provider
590        .get_prune_checkpoint(PruneSegment::AccountHistory)?
591        .and_then(|checkpoint| checkpoint.block_number);
592    let storage_history = provider
593        .get_prune_checkpoint(PruneSegment::StorageHistory)?
594        .and_then(|checkpoint| checkpoint.block_number);
595    let lower_bound = account_history.max(storage_history).unwrap_or_default();
596    let available_range = lower_bound..=finish.number;
597    if !available_range.contains(&anchor.number) {
598        return Err(ProviderError::InsufficientChangesets {
599            requested: anchor.number,
600            available: available_range,
601        })
602    }
603
604    Ok(AnchorForParent::RevertsRequired { anchor, finish, overlay })
605}
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610    use alloy_primitives::U256;
611    use reth_chain_state::{test_utils::TestBlockBuilder, ExecutedBlock};
612    use reth_primitives_traits::Account;
613    #[cfg(feature = "partial-persistence")]
614    use reth_provider::{
615        test_utils::{create_test_provider_factory, MockNodeTypesWithDB},
616        BlockWriter, ProviderFactory,
617    };
618    #[cfg(feature = "partial-persistence")]
619    #[cfg(feature = "partial-persistence")]
620    use reth_stages_types::{FinishCheckpoint, StageCheckpoint};
621    #[cfg(feature = "partial-persistence")]
622    use reth_storage_api::StageCheckpointWriter;
623    use reth_trie::{BranchNodeCompact, ComputedTrieData, HashedPostState, HashedStorage, Nibbles};
624
625    fn with_unique_trie_data(
626        block: &ExecutedBlock<EthPrimitives>,
627        id: u8,
628    ) -> ExecutedBlock<EthPrimitives> {
629        let hashed_address = B256::with_last_byte(id);
630        let hashed_slot = B256::with_last_byte(id.saturating_add(32));
631        let hashed_state = HashedPostState::default()
632            .with_accounts([(hashed_address, Some(Account::default()))])
633            .with_storages([(
634                hashed_address,
635                HashedStorage::from_iter([(hashed_slot, U256::from(id))]),
636            )])
637            .into_sorted();
638        let trie_updates = TrieUpdatesSorted::new(
639            vec![(
640                Nibbles::from_nibbles([id]),
641                Some(BranchNodeCompact::new(0, 0, 0, vec![], None)),
642            )],
643            Default::default(),
644        );
645
646        ExecutedBlock::new(
647            Arc::clone(&block.recovered_block),
648            Arc::clone(&block.execution_output),
649            ComputedTrieData::new(Arc::new(hashed_state), Arc::new(trie_updates)),
650        )
651    }
652
653    fn test_blocks() -> Vec<ExecutedBlock<EthPrimitives>> {
654        TestBlockBuilder::eth()
655            .get_executed_blocks(0..5)
656            .enumerate()
657            .map(|(index, block)| with_unique_trie_data(&block, index as u8 + 1))
658            .collect()
659    }
660
661    #[cfg(feature = "partial-persistence")]
662    fn setup_frontiers(
663        state_trie_tip_index: usize,
664        finish_tip_index: usize,
665    ) -> (ProviderFactory<MockNodeTypesWithDB>, Vec<ExecutedBlock<EthPrimitives>>) {
666        let factory = create_test_provider_factory();
667        let blocks = test_blocks();
668        let provider_rw = factory.provider_rw().unwrap();
669        for block in &blocks[..=finish_tip_index] {
670            provider_rw.insert_block(block.recovered_block()).unwrap();
671        }
672        provider_rw
673            .save_stage_checkpoint(
674                StageId::Finish,
675                StageCheckpoint::new(blocks[finish_tip_index].block_number())
676                    .with_finish_stage_checkpoint(FinishCheckpoint {
677                        partial_state_trie: Some(blocks[state_trie_tip_index].block_number()),
678                    }),
679            )
680            .unwrap();
681        provider_rw.commit().unwrap();
682
683        (factory, blocks)
684    }
685
686    #[cfg(feature = "partial-persistence")]
687    fn account_keys(overlay: &Overlay) -> Vec<B256> {
688        overlay.hashed_post_state.accounts.iter().map(|(key, _)| *key).collect()
689    }
690
691    #[cfg(feature = "partial-persistence")]
692    fn account_node_paths(overlay: &Overlay) -> Vec<Nibbles> {
693        overlay.trie_updates.account_nodes_ref().iter().map(|(path, _)| *path).collect()
694    }
695
696    #[cfg(feature = "partial-persistence")]
697    #[test]
698    fn managed_overlay_starts_at_state_trie_frontier() {
699        let (factory, blocks) = setup_frontiers(1, 3);
700        let manager = OverlayManager::default();
701        for block in &blocks[2..=4] {
702            manager.insert_block(block.clone());
703        }
704        let provider = factory.provider().unwrap();
705
706        for (parent_index, expected_ids) in [(3, vec![3, 4]), (4, vec![3, 4, 5])] {
707            let overlay = manager
708                .overlay_builder(blocks[parent_index].recovered_block().hash())
709                .build_overlay(&provider)
710                .unwrap();
711
712            assert_eq!(
713                account_keys(&overlay),
714                expected_ids.iter().copied().map(B256::with_last_byte).collect::<Vec<_>>()
715            );
716            assert_eq!(
717                account_node_paths(&overlay),
718                expected_ids
719                    .iter()
720                    .copied()
721                    .map(|id| Nibbles::from_nibbles([id]))
722                    .collect::<Vec<_>>()
723            );
724        }
725    }
726
727    #[cfg(feature = "partial-persistence")]
728    #[test]
729    fn managed_overlay_skips_when_finish_is_the_anchor() {
730        let (factory, blocks) = setup_frontiers(3, 3);
731        let manager = OverlayManager::default();
732        manager.insert_block(blocks[4].clone());
733        let provider = factory.provider().unwrap();
734
735        let overlay = manager
736            .overlay_builder(blocks[4].recovered_block().hash())
737            .with_skip_overlay_for_reused_sparse_trie(blocks[3].recovered_block().hash())
738            .build_overlay(&provider)
739            .unwrap();
740
741        assert!(overlay.hashed_post_state.is_empty());
742        assert!(overlay.trie_updates.is_empty());
743    }
744
745    #[cfg(feature = "partial-persistence")]
746    #[test]
747    fn no_reverts_errors_when_reverts_are_required() {
748        let (factory, blocks) = setup_frontiers(2, 3);
749        let provider = factory.provider().unwrap();
750
751        let error = OverlayManager::<EthPrimitives>::default()
752            .overlay_builder(blocks[1].recovered_block().hash())
753            .with_no_reverts()
754            .build_overlay(&provider)
755            .unwrap_err();
756
757        assert!(error.to_string().contains("reverts are disabled"));
758    }
759
760    #[cfg(feature = "partial-persistence")]
761    #[test]
762    fn managed_overlay_uses_persisted_parent_even_if_retained() {
763        let (factory, blocks) = setup_frontiers(2, 3);
764        let manager = OverlayManager::default();
765        manager.insert_block(blocks[1].clone());
766        let provider = factory.provider().unwrap();
767        let builder = manager.overlay_builder(blocks[1].recovered_block().hash());
768        match builder.anchor_at_parent(&provider).unwrap() {
769            AnchorForParent::RevertsRequired { anchor, finish, overlay } => {
770                assert_eq!(anchor, blocks[1].recovered_block().num_hash());
771                assert_eq!(finish, blocks[3].recovered_block().num_hash());
772                assert!(overlay.is_empty());
773            }
774            AnchorForParent::NoReverts { .. } => {
775                panic!("persisted parent below Finish must require reverts")
776            }
777        }
778
779        let overlay = builder.build_overlay(&provider).unwrap();
780
781        assert!(overlay.hashed_post_state.is_empty());
782        assert!(overlay.trie_updates.is_empty());
783    }
784
785    #[cfg(feature = "partial-persistence")]
786    #[test]
787    fn overlay_after_state_trie_frontier_requires_managed_coverage() {
788        let (factory, blocks) = setup_frontiers(1, 3);
789        let provider = factory.provider().unwrap();
790        let error = OverlayManager::<EthPrimitives>::default()
791            .overlay_builder(blocks[3].recovered_block().hash())
792            .with_overlay_source(None)
793            .build_overlay(&provider)
794            .unwrap_err();
795
796        assert!(
797            error.to_string().contains("is after partial state trie frontier"),
798            "unexpected error: {error}"
799        );
800    }
801
802    #[cfg(feature = "partial-persistence")]
803    #[test]
804    fn managed_overlay_errors_if_parent_is_not_persisted_or_managed_across_frontiers() {
805        let (factory, blocks) = setup_frontiers(1, 3);
806        let provider = factory.provider().unwrap();
807        let parent_hash = blocks[3].recovered_block().hash();
808        let error = OverlayManager::<EthPrimitives>::default()
809            .overlay_builder(parent_hash)
810            .build_overlay(&provider)
811            .unwrap_err();
812
813        assert!(error.to_string().contains("is after partial state trie frontier"));
814    }
815
816    #[test]
817    fn managed_overlay_skips_manager_for_persisted_parent() {
818        let parent_hash = B256::with_last_byte(1);
819        let builder = OverlayManager::<EthPrimitives>::default().overlay_builder(parent_hash);
820
821        let (trie, state) = builder.resolve_overlays(parent_hash).unwrap();
822        assert!(trie.is_empty());
823        assert!(state.is_empty());
824    }
825
826    #[test]
827    fn managed_overlay_errors_if_parent_is_not_persisted_or_managed() {
828        let parent_hash = B256::with_last_byte(1);
829        let anchor_hash = B256::with_last_byte(2);
830        let builder = OverlayManager::<EthPrimitives>::default().overlay_builder(parent_hash);
831
832        let err = builder.resolve_overlays(anchor_hash).unwrap_err();
833
834        assert!(err.to_string().contains("cannot be anchored"));
835    }
836
837    #[test]
838    fn managed_overlay_skip_requires_both_frontiers() {
839        let parent_hash = B256::with_last_byte(1);
840        let builder = OverlayManager::<EthPrimitives>::default().overlay_builder(parent_hash);
841        assert!(!builder.should_skip_overlay_for_reused_sparse_trie(parent_hash, parent_hash));
842
843        let builder = builder.with_skip_overlay_for_reused_sparse_trie(parent_hash);
844        assert!(builder.should_skip_overlay_for_reused_sparse_trie(parent_hash, parent_hash));
845        assert!(!builder
846            .should_skip_overlay_for_reused_sparse_trie(B256::with_last_byte(3), parent_hash,));
847
848        let blocks = test_blocks();
849        let manager = OverlayManager::default();
850        for block in &blocks[2..=4] {
851            manager.insert_block(block.clone());
852        }
853        let builder = manager
854            .overlay_builder(blocks[4].recovered_block().hash())
855            .with_skip_overlay_for_reused_sparse_trie(blocks[1].recovered_block().hash());
856        assert!(builder.should_skip_overlay_for_reused_sparse_trie(
857            blocks[1].recovered_block().hash(),
858            blocks[3].recovered_block().hash(),
859        ));
860
861        let builder =
862            builder.with_skip_overlay_for_reused_sparse_trie(blocks[2].recovered_block().hash());
863        assert!(!builder.should_skip_overlay_for_reused_sparse_trie(
864            blocks[1].recovered_block().hash(),
865            blocks[3].recovered_block().hash(),
866        ));
867    }
868}