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