Skip to main content

reth_storage_overlay/
builder.rs

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