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#[derive(Debug, Clone)]
25pub struct Overlay {
26 pub trie_updates: Arc<TrieUpdatesSorted>,
28 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#[derive(Debug, Clone)]
43pub enum OverlaySource {
44 Immediate {
46 trie: Arc<TrieUpdatesSorted>,
51 state: Arc<HashedPostStateSorted>,
53 },
54 Managed,
56}
57
58#[derive(Debug, Clone)]
63pub struct OverlayBuilder<N: NodePrimitives = EthPrimitives> {
64 parent_hash: B256,
66 overlay_source: Option<OverlaySource>,
68 overlay_manager: OverlayManager<N>,
70 reused_sparse_trie_anchor_hash: Option<B256>,
72 metrics: OverlayBuilderMetrics,
74}
75
76impl<N: NodePrimitives> OverlayBuilder<N> {
77 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 pub fn with_overlay_source(mut self, source: Option<OverlaySource>) -> Self {
92 self.overlay_source = source;
93 self
94 }
95
96 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 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 #[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 #[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 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 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 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 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 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 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 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 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 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
437pub 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#[derive(Clone, Metrics)]
468#[metrics(scope = "storage.overlay.builder")]
469struct OverlayBuilderMetrics {
470 retrieve_trie_reverts_duration: Histogram,
472 retrieve_hashed_state_reverts_duration: Histogram,
474 trie_updates_size: Histogram,
476 hashed_state_size: Histogram,
478 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}