1mod sparse_trie;
57
58use self::sparse_trie::{SparseTrieCacheTask, SparseTrieTaskMetrics};
59use crate::tree::{metrics::BlockValidationMetrics, EngineApiTreeState, ExecutionEnv, TreeConfig};
60use alloy_primitives::B256;
61use crossbeam_channel::Receiver as CrossbeamReceiver;
62use reth_chain_state::{ExecutedBlock, PreservedSparseTrie, SparseTrieWithStorageRoots};
63use reth_errors::ProviderResult;
64use reth_evm::{ConfigureEvm, OnStateHook};
65use reth_primitives_traits::{
66 AlloyBlockHeader, FastInstant as Instant, NodePrimitives, RecoveredBlock, SealedHeader,
67};
68use reth_provider::{
69 BlockExecutionOutput, BlockNumReader, ChangeSetReader, DatabaseProviderFactory,
70 DatabaseProviderROFactory, HashedPostStateProvider, ProviderError, PruneCheckpointReader,
71 StageCheckpointReader, StateRootProvider, StorageChangeSetReader, StorageSettingsCache,
72};
73use reth_storage_overlay::{OverlayManager, OverlayStateProviderFactory};
74use reth_tasks::utils::increase_thread_priority;
75use reth_trie::{updates::TrieUpdates, HashedPostState};
76use reth_trie_parallel::proof_task::{
77 metrics::ProofWorkerIdleMetrics, ProofTaskCtx, ProofWorkerCounts, ProofWorkerHandle,
78};
79pub use reth_trie_parallel::{
80 error::StateRootTaskError,
81 state_root_task::{
82 evm_state_to_hashed_post_state, PayloadStateRootHandle, StateAccessHint,
83 StateRootComputeOutcome, StateRootHandle, StateRootHintStream, StateRootMessage,
84 StateRootTaskCancelGuard, StateRootUpdateHook, StateRootUpdateStream,
85 },
86};
87use reth_trie_sparse::{
88 ArenaParallelSparseTrie, RevealableSparseTrie, SparseStateTrie, TrieNodeEpoch,
89};
90use std::{
91 fmt,
92 sync::{
93 mpsc::{self, RecvTimeoutError},
94 Arc,
95 },
96 time::Duration,
97};
98use tracing::{debug, debug_span, instrument, warn, Span};
99
100pub type LazyHashedPostState = reth_tasks::LazyHandle<Arc<HashedPostState>>;
102
103pub trait StateRootStrategy<N, P, Evm>: Send + Sync
105where
106 N: NodePrimitives,
107 Evm: ConfigureEvm<Primitives = N>,
108{
109 fn prepare(
114 &self,
115 ctx: StateRootJobContext<'_, N, P, Evm>,
116 ) -> ProviderResult<PreparedStateRootJob<N>>;
117
118 fn prepare_payload_builder(
125 &self,
126 _ctx: PayloadStateRootJobContext<'_, N, P>,
127 ) -> ProviderResult<Option<PayloadStateRootHandle>> {
128 Ok(None)
129 }
130}
131
132pub struct PayloadStateRootJobContext<'a, N, P>
134where
135 N: NodePrimitives,
136{
137 executor: &'a reth_tasks::Runtime,
138 overlay_manager: &'a OverlayManager<N>,
139 parent_hash: B256,
140 parent_header: &'a N::BlockHeader,
141 timestamp: u64,
142 state: &'a mut EngineApiTreeState<N>,
143 state_provider_factory: OverlayStateProviderFactory<P, N>,
144 config: &'a TreeConfig,
145}
146
147impl<N, P> fmt::Debug for PayloadStateRootJobContext<'_, N, P>
148where
149 N: NodePrimitives,
150{
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 f.debug_struct("PayloadStateRootJobContext")
153 .field("parent_hash", &self.parent_hash)
154 .field("parent_state_root", &self.parent_state_root())
155 .field("timestamp", &self.timestamp)
156 .field("pending_sparse_trie_prune", &self.state.pending_sparse_trie_prune())
157 .finish_non_exhaustive()
158 }
159}
160
161impl<'a, N, P> PayloadStateRootJobContext<'a, N, P>
162where
163 N: NodePrimitives,
164{
165 #[expect(clippy::too_many_arguments)]
167 pub(crate) const fn new(
168 executor: &'a reth_tasks::Runtime,
169 overlay_manager: &'a OverlayManager<N>,
170 parent_hash: B256,
171 parent_header: &'a N::BlockHeader,
172 timestamp: u64,
173 state: &'a mut EngineApiTreeState<N>,
174 state_provider_factory: OverlayStateProviderFactory<P, N>,
175 config: &'a TreeConfig,
176 ) -> Self {
177 Self {
178 executor,
179 overlay_manager,
180 parent_hash,
181 parent_header,
182 timestamp,
183 state,
184 state_provider_factory,
185 config,
186 }
187 }
188
189 pub const fn parent_hash(&self) -> B256 {
191 self.parent_hash
192 }
193
194 pub const fn parent_header(&self) -> &N::BlockHeader {
199 self.parent_header
200 }
201
202 pub fn parent_state_root(&self) -> B256 {
204 self.parent_header.state_root()
205 }
206
207 pub const fn timestamp(&self) -> u64 {
211 self.timestamp
212 }
213
214 pub const fn executor(&self) -> &reth_tasks::Runtime {
216 self.executor
217 }
218
219 pub fn take_sparse_trie_prune_blocks(&mut self) -> Option<Vec<ExecutedBlock<N>>> {
224 self.state.take_sparse_trie_prune_blocks(self.parent_hash)
225 }
226}
227
228pub struct StateRootJobContext<'a, N, P, Evm>
230where
231 N: NodePrimitives,
232 Evm: ConfigureEvm<Primitives = N>,
233{
234 executor: &'a reth_tasks::Runtime,
235 overlay_manager: &'a OverlayManager<N>,
236 env: &'a ExecutionEnv<Evm>,
237 parent_header: &'a SealedHeader<N::BlockHeader>,
238 state_provider_factory: OverlayStateProviderFactory<P, N>,
239 config: &'a TreeConfig,
240 parallel_bal_execution: bool,
241 state: &'a mut EngineApiTreeState<N>,
242}
243
244impl<N, P, Evm> fmt::Debug for StateRootJobContext<'_, N, P, Evm>
245where
246 N: NodePrimitives,
247 Evm: ConfigureEvm<Primitives = N>,
248{
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 f.debug_struct("StateRootJobContext")
251 .field("parallel_bal_execution", &self.parallel_bal_execution)
252 .field("has_pending_sparse_trie_prune", &self.state.pending_sparse_trie_prune())
253 .finish_non_exhaustive()
254 }
255}
256
257impl<'a, N, P, Evm> StateRootJobContext<'a, N, P, Evm>
258where
259 N: NodePrimitives,
260 Evm: ConfigureEvm<Primitives = N>,
261{
262 #[expect(clippy::too_many_arguments)]
264 pub(crate) const fn new(
265 executor: &'a reth_tasks::Runtime,
266 overlay_manager: &'a OverlayManager<N>,
267 env: &'a ExecutionEnv<Evm>,
268 parent_header: &'a SealedHeader<N::BlockHeader>,
269 state_provider_factory: OverlayStateProviderFactory<P, N>,
270 config: &'a TreeConfig,
271 parallel_bal_execution: bool,
272 state: &'a mut EngineApiTreeState<N>,
273 ) -> Self {
274 Self {
275 executor,
276 overlay_manager,
277 env,
278 parent_header,
279 state_provider_factory,
280 config,
281 parallel_bal_execution,
282 state,
283 }
284 }
285
286 pub const fn env(&self) -> &ExecutionEnv<Evm> {
288 self.env
289 }
290
291 pub const fn parent_header(&self) -> &SealedHeader<N::BlockHeader> {
293 self.parent_header
294 }
295
296 pub const fn executor(&self) -> &reth_tasks::Runtime {
298 self.executor
299 }
300
301 pub const fn parallel_bal_execution(&self) -> bool {
303 self.parallel_bal_execution
304 }
305
306 pub fn take_sparse_trie_prune_blocks(&mut self) -> Option<Vec<ExecutedBlock<N>>> {
311 self.state.take_sparse_trie_prune_blocks(self.env.parent_hash)
312 }
313}
314
315pub struct PreparedStateRootJob<N: NodePrimitives> {
322 job: Box<dyn StateRootJob<N>>,
323 execution_hook: Option<StateRootUpdateHook>,
324 hint_stream: Option<StateRootHintStream>,
325 hashed_update_stream: Option<StateRootUpdateStream>,
326 hashed_state_rx: Option<mpsc::Receiver<Arc<HashedPostState>>>,
327}
328
329impl<N: NodePrimitives> fmt::Debug for PreparedStateRootJob<N> {
330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331 f.debug_struct("PreparedStateRootJob")
332 .field("name", &self.job.name())
333 .field("has_execution_hook", &self.execution_hook.is_some())
334 .field("has_hint_stream", &self.hint_stream.is_some())
335 .field("has_hashed_update_stream", &self.hashed_update_stream.is_some())
336 .field("has_hashed_state_rx", &self.hashed_state_rx.is_some())
337 .finish()
338 }
339}
340
341impl<N: NodePrimitives> PreparedStateRootJob<N> {
342 pub const fn new(
344 job: Box<dyn StateRootJob<N>>,
345 hashed_state_rx: Option<mpsc::Receiver<Arc<HashedPostState>>>,
346 ) -> Self {
347 Self {
348 job,
349 execution_hook: None,
350 hint_stream: None,
351 hashed_update_stream: None,
352 hashed_state_rx,
353 }
354 }
355
356 pub fn with_execution_hook(mut self, hook: StateRootUpdateHook) -> Self {
358 self.execution_hook = Some(hook);
359 self
360 }
361
362 pub fn with_hint_stream(mut self, hint_stream: StateRootHintStream) -> Self {
364 self.hint_stream = Some(hint_stream);
365 self
366 }
367
368 pub fn with_hashed_update_stream(mut self, stream: StateRootUpdateStream) -> Self {
370 self.hashed_update_stream = Some(stream);
371 self
372 }
373
374 pub fn name(&self) -> &'static str {
376 self.job.name()
377 }
378
379 pub fn take_execution_hook(&mut self) -> Option<Box<dyn OnStateHook + 'static>> {
381 self.execution_hook.take().map(|hook| Box::new(hook) as Box<dyn OnStateHook + 'static>)
382 }
383
384 pub const fn take_hint_stream(&mut self) -> Option<StateRootHintStream> {
386 self.hint_stream.take()
387 }
388
389 pub const fn take_hashed_update_stream(&mut self) -> Option<StateRootUpdateStream> {
391 self.hashed_update_stream.take()
392 }
393
394 pub const fn take_hashed_state_rx(&mut self) -> Option<mpsc::Receiver<Arc<HashedPostState>>> {
400 self.hashed_state_rx.take()
401 }
402
403 pub fn finish(
405 &mut self,
406 block: &RecoveredBlock<N::Block>,
407 output: Arc<BlockExecutionOutput<N::Receipt>>,
408 hashed_state: &LazyHashedPostState,
409 ) -> ProviderResult<StateRootJobOutcome> {
410 self.job.finish(block, output, hashed_state)
411 }
412}
413
414pub trait StateRootJob<N: NodePrimitives>: Send {
416 fn name(&self) -> &'static str;
418
419 fn finish(
423 &mut self,
424 block: &RecoveredBlock<N::Block>,
425 output: Arc<BlockExecutionOutput<N::Receipt>>,
426 hashed_state: &LazyHashedPostState,
427 ) -> ProviderResult<StateRootJobOutcome>;
428}
429
430#[derive(Debug)]
432pub struct StateRootJobOutcome {
433 pub state_root: B256,
435 pub trie_updates: Arc<TrieUpdates>,
437 pub hashed_state: Option<Arc<HashedPostState>>,
442}
443
444impl StateRootJobOutcome {
445 pub const fn new(state_root: B256, trie_updates: Arc<TrieUpdates>) -> Self {
447 Self { state_root, trie_updates, hashed_state: None }
448 }
449
450 pub fn with_hashed_state(mut self, hashed_state: Option<Arc<HashedPostState>>) -> Self {
452 self.hashed_state = hashed_state;
453 self
454 }
455}
456
457type SerialFallbackRx = mpsc::Receiver<ProviderResult<(B256, TrieUpdates, Arc<HashedPostState>)>>;
460
461#[derive(Default)]
469pub struct DefaultStateRootStrategy {
470 metrics: SparseTrieTaskMetrics,
471 proof_worker_metrics: ProofWorkerIdleMetrics,
472}
473
474impl fmt::Debug for DefaultStateRootStrategy {
475 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
476 f.debug_struct("DefaultStateRootStrategy").finish_non_exhaustive()
477 }
478}
479
480impl DefaultStateRootStrategy {
481 const SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD: usize = 30;
484
485 const LARGE_BLOCK_PROOF_WORKER_GAS_THRESHOLD: u64 = 100_000_000;
489
490 const fn proof_worker_count(
497 pool_threads: usize,
498 configured_threads: Option<usize>,
499 transaction_count: Option<usize>,
500 gas_used: Option<u64>,
501 ) -> usize {
502 let count = if let Some(configured_threads) = configured_threads {
503 configured_threads
504 } else {
505 match (transaction_count, gas_used) {
506 (Some(count), _) if count <= Self::SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD => {
507 pool_threads / 4
508 }
509 (_, Some(gas)) if gas >= Self::LARGE_BLOCK_PROOF_WORKER_GAS_THRESHOLD => {
510 pool_threads
511 }
512 _ => pool_threads / 2,
513 }
514 };
515
516 if count == 0 {
519 1
520 } else {
521 count
522 }
523 }
524
525 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
530 fn spawn_state_root<N, F>(
531 &self,
532 executor: &reth_tasks::Runtime,
533 multiproof_provider_factory: OverlayStateProviderFactory<F, N>,
534 options: StateRootTaskOptions<'_, N>,
535 ) -> (StateRootHandle, mpsc::Receiver<PendingSparseTrie>)
536 where
537 N: NodePrimitives,
538 F: DatabaseProviderFactory + Clone + 'static,
539 F::Provider: BlockNumReader
540 + PruneCheckpointReader
541 + StageCheckpointReader
542 + ChangeSetReader
543 + StorageChangeSetReader
544 + StorageSettingsCache
545 + 'static,
546 {
547 let StateRootTaskOptions {
548 parent_header,
549 preserved_sparse_trie,
550 transaction_count,
551 gas_used,
552 config,
553 pending_sparse_trie_prune_blocks,
554 } = options;
555 let (updates_tx, from_multi_proof) = crossbeam_channel::unbounded();
556 let (cancel_guard, cancel_rx) = StateRootTaskCancelGuard::channel();
557
558 let worker_counts = ProofWorkerCounts::new(
559 Self::proof_worker_count(
560 executor.proof_storage_worker_pool().num_threads(),
561 executor.proof_storage_worker_threads_override(),
562 transaction_count,
563 gas_used,
564 ),
565 Self::proof_worker_count(
566 executor.proof_account_worker_pool().num_threads(),
567 executor.proof_account_worker_threads_override(),
568 transaction_count,
569 gas_used,
570 ),
571 );
572 let (state_root_tx, state_root_rx) = mpsc::channel();
573 let (pending_trie_tx, pending_trie_rx) = mpsc::channel();
574 let (hashed_state_tx, hashed_state_rx) = mpsc::channel();
575 let parent_state_root = parent_header.state_root();
576
577 self.spawn_sparse_trie_task(
578 executor,
579 multiproof_provider_factory,
580 state_root_tx,
581 hashed_state_tx,
582 from_multi_proof,
583 cancel_rx,
584 SparseTrieTaskOptions {
585 parent_header,
586 preserved_sparse_trie,
587 pending_trie_tx,
588 chunk_size: config.multiproof_chunk_size(),
589 worker_counts,
590 #[cfg(feature = "trie-debug")]
591 proof_jitter: config.proof_jitter(),
592 pending_sparse_trie_prune_blocks: if config.disable_sparse_trie_cache_pruning() {
593 None
594 } else {
595 pending_sparse_trie_prune_blocks
596 },
597 },
598 );
599
600 (
601 StateRootHandle::new(
602 parent_state_root,
603 updates_tx,
604 cancel_guard,
605 state_root_rx,
606 hashed_state_rx,
607 ),
608 pending_trie_rx,
609 )
610 }
611
612 #[expect(clippy::too_many_arguments)]
614 fn spawn_sparse_trie_task<N, F>(
615 &self,
616 executor: &reth_tasks::Runtime,
617 multiproof_provider_factory: OverlayStateProviderFactory<F, N>,
618 state_root_tx: mpsc::Sender<Result<StateRootComputeOutcome, StateRootTaskError>>,
619 hashed_state_tx: mpsc::Sender<Arc<HashedPostState>>,
620 from_multi_proof: CrossbeamReceiver<StateRootMessage>,
621 cancel_rx: CrossbeamReceiver<()>,
622 options: SparseTrieTaskOptions<N>,
623 ) where
624 N: NodePrimitives,
625 F: DatabaseProviderFactory + Clone + 'static,
626 F::Provider: BlockNumReader
627 + PruneCheckpointReader
628 + StageCheckpointReader
629 + ChangeSetReader
630 + StorageChangeSetReader
631 + StorageSettingsCache
632 + 'static,
633 {
634 let SparseTrieTaskOptions {
635 parent_header,
636 preserved_sparse_trie,
637 pending_trie_tx,
638 chunk_size,
639 worker_counts,
640 #[cfg(feature = "trie-debug")]
641 proof_jitter,
642 pending_sparse_trie_prune_blocks,
643 } = options;
644 let trie_metrics = self.metrics.clone();
645 let worker_metrics = self.proof_worker_metrics.clone();
646 let executor = executor.clone();
647
648 let parent_span = Span::current();
649 executor.clone().spawn_blocking_named("sparse-trie", move || {
650 reth_tasks::once!(increase_thread_priority);
651
652 let parent_hash = parent_header.hash();
653 let parent_state_root = parent_header.state_root();
654 let new_epoch = TrieNodeEpoch::new(parent_header.number().saturating_add(1));
655
656 let _enter = debug_span!(
657 target: "engine::tree::payload_processor",
658 parent: &parent_span,
659 "sparse_trie_task"
660 )
661 .entered();
662
663 let new_sparse_state_trie = || {
664 debug!(
665 target: "engine::tree::payload_processor",
666 "Creating new sparse trie - no preserved trie available"
667 );
668 let default_trie =
669 RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
670 SparseStateTrie::default()
671 .with_accounts_trie(default_trie.clone())
672 .with_default_storage_trie(default_trie)
673 .with_updates(true)
674 };
675
676 let mut sparse_trie_anchor_hash = parent_hash;
677 let mut reused_preserved_sparse_trie = false;
678 let mut known_storage_roots = Default::default();
679 let sparse_state_trie = match preserved_sparse_trie {
680 Some(preserved) => {
681 let start = Instant::now();
682 let preserved_anchor_hash = preserved.anchor_hash();
683 let preserved = preserved.into_trie_for(parent_hash);
684 trie_metrics
685 .sparse_trie_cache_wait_duration_histogram
686 .record(start.elapsed().as_secs_f64());
687
688 match preserved {
689 Ok(Some(SparseTrieWithStorageRoots { trie, storage_roots })) => {
690 known_storage_roots = storage_roots;
691 sparse_trie_anchor_hash = preserved_anchor_hash;
692 reused_preserved_sparse_trie = true;
693 trie
694 }
695 Ok(None) => new_sparse_state_trie(),
696 Err(err) => {
697 let _ = state_root_tx.send(Err(StateRootTaskError::Other(err.into())));
698 return;
699 }
700 }
701 }
702 None => new_sparse_state_trie(),
703 };
704 let prune_target = sparse_trie_prune_target(
705 sparse_trie_anchor_hash,
706 reused_preserved_sparse_trie,
707 parent_header.num_hash(),
708 pending_sparse_trie_prune_blocks.as_deref(),
709 );
710 let factory = if reused_preserved_sparse_trie {
711 multiproof_provider_factory.with_skip_overlay_for_reused_sparse_trie(
712 sparse_trie_anchor_hash,
713 known_storage_roots,
714 )
715 } else {
716 multiproof_provider_factory
717 };
718 let task_ctx = ProofTaskCtx::new(factory);
719 #[cfg(feature = "trie-debug")]
720 let task_ctx = task_ctx.with_proof_jitter(proof_jitter);
721 let proof_worker_handle =
722 ProofWorkerHandle::new(&executor, task_ctx, worker_counts, worker_metrics);
723 let mut task = SparseTrieCacheTask::new_with_trie(
724 &executor,
725 from_multi_proof,
726 cancel_rx,
727 hashed_state_tx,
728 proof_worker_handle,
729 trie_metrics.clone(),
730 sparse_state_trie,
731 parent_state_root,
732 new_epoch,
733 chunk_size,
734 );
735
736 let result = task.run();
737 let trie_completer = if let Ok(outcome) = &result {
739 let preserved_anchor_hash =
740 prune_target.map_or(sparse_trie_anchor_hash, |(_, anchor_hash)| anchor_hash);
741 let (tx, rx) = mpsc::channel();
742 let pending = PendingSparseTrie {
743 state_root: outcome.state_root,
744 anchor_hash: preserved_anchor_hash,
745 trie: rx,
746 };
747 pending_trie_tx.send(pending).is_ok().then_some(tx)
748 } else {
749 None
750 };
751
752 if state_root_tx.send(result).is_err() {
753 debug!(
754 target: "engine::tree::payload_processor",
755 "State root receiver dropped"
756 );
757 }
758 drop(pending_trie_tx);
759
760 let _enter =
761 debug_span!(target: "engine::tree::payload_processor", "preserve").entered();
762 let (trie_to_drop, deferred) = if let Some(completer) = trie_completer {
763 let start = Instant::now();
766 let (mut trie, deferred) = task.into_trie_for_reuse();
767 let storage_roots = trie.storage_roots(new_epoch);
768 if let Some((prune_before, _)) = prune_target {
769 let prune_start = Instant::now();
770 trie.prune(prune_before);
771 trie_metrics
772 .sparse_trie_prune_duration_histogram
773 .record(prune_start.elapsed().as_secs_f64());
774 }
775 trie_metrics
776 .into_trie_for_reuse_duration_histogram
777 .record(start.elapsed().as_secs_f64());
778 trie_metrics
779 .sparse_trie_retained_storage_tries
780 .set(trie.retained_storage_tries_count() as f64);
781 (
782 completer
783 .send(SparseTrieWithStorageRoots { trie, storage_roots })
784 .err()
785 .map(|err| err.0.trie),
786 deferred,
787 )
788 } else {
789 debug!(
790 target: "engine::tree::payload_processor",
791 "State root task has no reusable trie to publish, dropping trie"
792 );
793 let (trie, deferred) = task.into_cleared_trie();
794 (Some(trie), deferred)
795 };
796 if let Some(trie) = trie_to_drop {
797 executor.spawn_drop(trie);
798 }
799 executor.spawn_drop(deferred);
800 });
801 }
802}
803
804#[derive(Debug)]
806struct PendingSparseTrie {
807 state_root: B256,
808 anchor_hash: B256,
809 trie: mpsc::Receiver<SparseTrieWithStorageRoots>,
810}
811
812fn publish_sparse_trie<N: NodePrimitives>(
813 pending_trie_rx: &mpsc::Receiver<PendingSparseTrie>,
814 overlay_manager: &OverlayManager<N>,
815 block_hash: B256,
816 state_root: B256,
817) {
818 if let Ok(pending) = pending_trie_rx.try_recv() &&
821 pending.state_root == state_root
822 {
823 overlay_manager.store_sparse_trie(PreservedSparseTrie::pending(
824 pending.trie,
825 block_hash,
826 pending.anchor_hash,
827 ));
828 }
829}
830
831struct SparseTrieTaskOptions<N: NodePrimitives> {
832 parent_header: SealedHeader<N::BlockHeader>,
833 preserved_sparse_trie: Option<PreservedSparseTrie>,
834 pending_trie_tx: mpsc::Sender<PendingSparseTrie>,
835 chunk_size: usize,
836 worker_counts: ProofWorkerCounts,
837 #[cfg(feature = "trie-debug")]
838 proof_jitter: Option<Duration>,
839 pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
841}
842
843struct StateRootTaskOptions<'a, N: NodePrimitives> {
844 parent_header: SealedHeader<N::BlockHeader>,
845 preserved_sparse_trie: Option<PreservedSparseTrie>,
846 transaction_count: Option<usize>,
847 gas_used: Option<u64>,
849 config: &'a TreeConfig,
850 pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
851}
852
853fn sparse_trie_prune_target<N: NodePrimitives>(
854 current_anchor: B256,
855 reused_preserved_sparse_trie: bool,
856 parent: alloy_eips::BlockNumHash,
857 pending_blocks: Option<&[ExecutedBlock<N>]>,
858) -> Option<(TrieNodeEpoch, B256)> {
859 let blocks = pending_blocks?;
860 let Some(oldest) = blocks.last() else {
861 return Some((TrieNodeEpoch::new(parent.number.saturating_add(1)), parent.hash))
863 };
864 let anchor = if !reused_preserved_sparse_trie ||
865 blocks.iter().any(|block| block.recovered_block().hash() == current_anchor)
866 {
867 current_anchor
868 } else {
869 oldest.recovered_block().parent_hash()
870 };
871 Some((TrieNodeEpoch::new(oldest.recovered_block().number()), anchor))
872}
873
874impl<N, P, Evm> StateRootStrategy<N, P, Evm> for DefaultStateRootStrategy
875where
876 N: NodePrimitives,
877 P: DatabaseProviderFactory + Clone + 'static,
878 P::Provider: BlockNumReader
879 + PruneCheckpointReader
880 + StageCheckpointReader
881 + ChangeSetReader
882 + StorageChangeSetReader
883 + StorageSettingsCache
884 + 'static,
885 Evm: ConfigureEvm<Primitives = N> + 'static,
886{
887 fn prepare(
888 &self,
889 mut ctx: StateRootJobContext<'_, N, P, Evm>,
890 ) -> ProviderResult<PreparedStateRootJob<N>> {
891 if ctx.config.skip_state_root() {
892 return Ok(PreparedStateRootJob::new(Box::new(SkippedStateRootJob {}), None))
893 }
894
895 if !ctx.config.use_state_root_task() {
896 return Ok(PreparedStateRootJob::new(
897 Box::new(SynchronousStateRootJob {
898 state_provider_factory: ctx.state_provider_factory,
899 }),
900 None,
901 ))
902 }
903
904 let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
905 let StateRootJobContext {
906 executor,
907 overlay_manager,
908 env,
909 parent_header,
910 state_provider_factory,
911 config,
912 parallel_bal_execution,
913 state: _,
914 } = ctx;
915
916 let preserved_sparse_trie = overlay_manager.take_sparse_trie();
917 let (mut handle, pending_trie_rx) = self.spawn_state_root(
918 executor,
919 state_provider_factory.clone(),
920 StateRootTaskOptions {
921 parent_header: parent_header.clone(),
922 preserved_sparse_trie,
923 transaction_count: Some(env.transaction_count),
924 gas_used: Some(env.gas_used),
925 config,
926 pending_sparse_trie_prune_blocks,
927 },
928 );
929
930 let (hashed_update_stream, execution_hook): (
934 Option<StateRootUpdateStream>,
935 Option<StateRootUpdateHook>,
936 ) = match parallel_bal_execution {
937 true => (Some(handle.take_hashed_update_stream()), None),
938 false => (None, Some(handle.take_execution_hook())),
939 };
940 let hint_stream = handle.take_hint_stream();
941
942 let hashed_state_rx = Some(handle.take_hashed_state_rx());
943
944 let mut prepared = PreparedStateRootJob::new(
945 Box::new(SparseTrieStateRootJob {
946 pending_trie_rx,
947 overlay_manager: overlay_manager.clone(),
948 handle,
949 state_provider_factory,
950 executor: executor.clone(),
951 timeout: config.state_root_task_timeout(),
952 compare_trie_updates: config.always_compare_trie_updates(),
953 metrics: BlockValidationMetrics::default(),
954 }),
955 hashed_state_rx,
956 )
957 .with_hint_stream(hint_stream);
958 if let Some(hook) = execution_hook {
959 prepared = prepared.with_execution_hook(hook);
960 }
961 if let Some(stream) = hashed_update_stream {
962 prepared = prepared.with_hashed_update_stream(stream);
963 }
964 Ok(prepared)
965 }
966
967 fn prepare_payload_builder(
968 &self,
969 mut ctx: PayloadStateRootJobContext<'_, N, P>,
970 ) -> ProviderResult<Option<PayloadStateRootHandle>> {
971 if !ctx.config.share_sparse_trie_with_payload_builder() ||
974 ctx.config.skip_state_root() ||
975 !ctx.config.has_enough_parallelism()
976 {
977 return Ok(None)
978 }
979
980 let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
981 let parent_header = SealedHeader::new(ctx.parent_header().clone(), ctx.parent_hash());
982 let preserved_sparse_trie = ctx.overlay_manager.take_sparse_trie();
983 let (handle, pending_trie_rx) = self.spawn_state_root(
984 ctx.executor,
985 ctx.state_provider_factory.clone(),
986 StateRootTaskOptions {
987 parent_header,
988 preserved_sparse_trie,
989 transaction_count: None,
992 gas_used: None,
993 config: ctx.config,
994 pending_sparse_trie_prune_blocks,
995 },
996 );
997 let overlay_manager = ctx.overlay_manager.clone();
998 Ok(Some(handle.into_payload_state_root_handle().with_on_payload_built(
999 move |block_hash, state_root| {
1000 publish_sparse_trie(&pending_trie_rx, &overlay_manager, block_hash, state_root);
1001 },
1002 )))
1003 }
1004}
1005
1006#[derive(Debug)]
1007struct SkippedStateRootJob {}
1008
1009impl<N: NodePrimitives> StateRootJob<N> for SkippedStateRootJob {
1010 fn name(&self) -> &'static str {
1011 "skipped"
1012 }
1013
1014 fn finish(
1015 &mut self,
1016 block: &RecoveredBlock<N::Block>,
1017 _output: Arc<BlockExecutionOutput<N::Receipt>>,
1018 _hashed_state: &LazyHashedPostState,
1019 ) -> ProviderResult<StateRootJobOutcome> {
1020 Ok(StateRootJobOutcome::new(block.header().state_root(), Arc::new(TrieUpdates::default())))
1021 }
1022}
1023
1024#[derive(Debug)]
1025struct SynchronousStateRootJob<N: NodePrimitives, P> {
1026 state_provider_factory: OverlayStateProviderFactory<P, N>,
1027}
1028
1029impl<N, P> StateRootJob<N> for SynchronousStateRootJob<N, P>
1030where
1031 N: NodePrimitives,
1032 P: DatabaseProviderFactory + Clone + 'static,
1033 P::Provider: BlockNumReader
1034 + PruneCheckpointReader
1035 + StageCheckpointReader
1036 + ChangeSetReader
1037 + StorageChangeSetReader
1038 + StorageSettingsCache
1039 + 'static,
1040{
1041 fn name(&self) -> &'static str {
1042 "synchronous"
1043 }
1044
1045 fn finish(
1046 &mut self,
1047 _block: &RecoveredBlock<N::Block>,
1048 _output: Arc<BlockExecutionOutput<N::Receipt>>,
1049 hashed_state: &LazyHashedPostState,
1050 ) -> ProviderResult<StateRootJobOutcome> {
1051 let provider = self.state_provider_factory.database_provider_ro()?;
1052 let (state_root, trie_updates) =
1053 provider.state_root_with_updates(hashed_state.get().as_ref().clone())?;
1054 Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates)))
1055 }
1056}
1057
1058#[derive(Debug)]
1059struct SparseTrieStateRootJob<N: NodePrimitives, P> {
1060 pending_trie_rx: mpsc::Receiver<PendingSparseTrie>,
1061 overlay_manager: OverlayManager<N>,
1062 handle: StateRootHandle,
1063 state_provider_factory: OverlayStateProviderFactory<P, N>,
1064 executor: reth_tasks::Runtime,
1065 timeout: Option<Duration>,
1066 compare_trie_updates: bool,
1067 metrics: BlockValidationMetrics,
1068}
1069
1070impl<N, P> SparseTrieStateRootJob<N, P>
1071where
1072 N: NodePrimitives,
1073 P: DatabaseProviderFactory + Clone + 'static,
1074 P::Provider: BlockNumReader
1075 + PruneCheckpointReader
1076 + StageCheckpointReader
1077 + ChangeSetReader
1078 + StorageChangeSetReader
1079 + StorageSettingsCache
1080 + 'static,
1081{
1082 fn serial_fallback(
1083 &self,
1084 output: Arc<BlockExecutionOutput<N::Receipt>>,
1085 ) -> ProviderResult<SerialFallbackRx> {
1086 let provider = self.state_provider_factory.database_provider_ro()?;
1087 let (fallback_tx, fallback_rx) = mpsc::channel();
1088 self.executor.spawn_blocking_named("serial-root", move || {
1089 let result = (|| {
1090 let hashed_state = Arc::new(provider.hashed_post_state(&output.state)?);
1091 let (root, updates) =
1092 provider.state_root_with_updates(hashed_state.as_ref().clone())?;
1093 Ok((root, updates, hashed_state))
1094 })();
1095 let _ = fallback_tx.send(result);
1096 });
1097
1098 Ok(fallback_rx)
1099 }
1100
1101 fn compute_serial(
1106 &self,
1107 output: &BlockExecutionOutput<N::Receipt>,
1108 ) -> ProviderResult<StateRootJobOutcome> {
1109 let provider = self.state_provider_factory.database_provider_ro()?;
1110 let hashed_state = Arc::new(provider.hashed_post_state(&output.state)?);
1111 let (state_root, trie_updates) =
1112 provider.state_root_with_updates(hashed_state.as_ref().clone())?;
1113 self.metrics.state_root_task_fallback_success_total.increment(1);
1114 Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1115 .with_hashed_state(Some(hashed_state)))
1116 }
1117
1118 fn verified_sparse_outcome(
1123 &self,
1124 block: &RecoveredBlock<N::Block>,
1125 output: &BlockExecutionOutput<N::Receipt>,
1126 outcome: StateRootComputeOutcome,
1127 ) -> ProviderResult<StateRootJobOutcome> {
1128 let outcome = self.sparse_outcome(output, outcome);
1129 if outcome.state_root == block.header().state_root() {
1130 publish_sparse_trie(
1131 &self.pending_trie_rx,
1132 &self.overlay_manager,
1133 block.hash(),
1134 outcome.state_root,
1135 );
1136 return Ok(outcome)
1137 }
1138 warn!(
1139 target: "engine::tree::state_root_strategy",
1140 state_root = ?outcome.state_root,
1141 block_state_root = ?block.header().state_root(),
1142 "State root task returned incorrect state root, recomputing serially"
1143 );
1144 self.compute_serial(output)
1145 }
1146
1147 fn sparse_outcome(
1148 &self,
1149 output: &BlockExecutionOutput<N::Receipt>,
1150 outcome: StateRootComputeOutcome,
1151 ) -> StateRootJobOutcome {
1152 let StateRootComputeOutcome { state_root, trie_updates, hashed_state: _hashed_state } =
1153 outcome;
1154
1155 if self.compare_trie_updates {
1156 compare_trie_updates_with_serial(
1157 self.state_provider_factory.clone(),
1158 output,
1159 trie_updates.as_ref().clone(),
1160 );
1161 }
1162
1163 StateRootJobOutcome::new(state_root, trie_updates)
1164 }
1165}
1166
1167impl<N, P> StateRootJob<N> for SparseTrieStateRootJob<N, P>
1168where
1169 N: NodePrimitives,
1170 P: DatabaseProviderFactory + Clone + 'static,
1171 P::Provider: BlockNumReader
1172 + PruneCheckpointReader
1173 + StageCheckpointReader
1174 + ChangeSetReader
1175 + StorageChangeSetReader
1176 + StorageSettingsCache
1177 + 'static,
1178{
1179 fn name(&self) -> &'static str {
1180 "sparse-trie"
1181 }
1182
1183 fn finish(
1184 &mut self,
1185 block: &RecoveredBlock<N::Block>,
1186 output: Arc<BlockExecutionOutput<N::Receipt>>,
1187 _hashed_state: &LazyHashedPostState,
1188 ) -> ProviderResult<StateRootJobOutcome> {
1189 if self.timeout.is_none() {
1190 return match self.handle.state_root() {
1191 Ok(outcome) => self.verified_sparse_outcome(block, &output, outcome),
1192 Err(err) => {
1193 debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1194 self.compute_serial(&output)
1195 }
1196 }
1197 }
1198
1199 let timeout = self.timeout.expect("checked above");
1200 let task_rx = self.handle.take_state_root_rx();
1201 let fallback_rx = match task_rx.recv_timeout(timeout) {
1202 Ok(Ok(outcome)) => return self.verified_sparse_outcome(block, &output, outcome),
1203 Ok(Err(err)) => {
1204 debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1205 self.serial_fallback(output.clone())?
1206 }
1207 Err(RecvTimeoutError::Timeout) => {
1208 warn!(target: "engine::tree::state_root_strategy", ?timeout, "State root task timed out, racing serial fallback");
1209 self.metrics.state_root_task_timeout_total.increment(1);
1210 self.serial_fallback(output.clone())?
1211 }
1212 Err(RecvTimeoutError::Disconnected) => {
1213 debug!(target: "engine::tree::state_root_strategy", "State root task dropped, falling back to serial root");
1214 self.serial_fallback(output.clone())?
1215 }
1216 };
1217
1218 loop {
1219 if let Ok(Ok(outcome)) = task_rx.try_recv() {
1220 let outcome = self.sparse_outcome(&output, outcome);
1221 if outcome.state_root == block.header().state_root() {
1222 publish_sparse_trie(
1223 &self.pending_trie_rx,
1224 &self.overlay_manager,
1225 block.hash(),
1226 outcome.state_root,
1227 );
1228 return Ok(outcome)
1229 }
1230 warn!(
1232 target: "engine::tree::state_root_strategy",
1233 state_root = ?outcome.state_root,
1234 block_state_root = ?block.header().state_root(),
1235 "State root task returned incorrect state root, using serial fallback"
1236 );
1237 }
1238
1239 match fallback_rx.try_recv() {
1240 Ok(Ok((state_root, trie_updates, hashed_state))) => {
1241 self.metrics.state_root_task_fallback_success_total.increment(1);
1242 return Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1243 .with_hashed_state(Some(hashed_state)))
1244 }
1245 Ok(Err(err)) => return Err(err),
1246 Err(mpsc::TryRecvError::Empty) => {}
1247 Err(mpsc::TryRecvError::Disconnected) => {
1248 return Err(ProviderError::other(std::io::Error::other(
1249 "serial state root fallback task dropped",
1250 )))
1251 }
1252 }
1253
1254 std::thread::sleep(Duration::from_millis(1));
1255 }
1256 }
1257}
1258
1259fn compare_trie_updates_with_serial<N, P>(
1260 state_provider_factory: OverlayStateProviderFactory<P, N>,
1261 output: &BlockExecutionOutput<N::Receipt>,
1262 task_trie_updates: TrieUpdates,
1263) -> bool
1264where
1265 N: NodePrimitives,
1266 P: DatabaseProviderFactory,
1267 P::Provider: BlockNumReader
1268 + PruneCheckpointReader
1269 + StageCheckpointReader
1270 + ChangeSetReader
1271 + StorageChangeSetReader
1272 + StorageSettingsCache
1273 + 'static,
1274{
1275 debug!(target: "engine::tree::state_root_strategy", "Comparing trie updates with serial computation");
1276
1277 match state_provider_factory.database_provider_ro().and_then(|provider| {
1278 let hashed_state = provider.hashed_post_state(&output.state)?;
1279 provider.state_root_with_updates(hashed_state)
1280 }) {
1281 Ok((serial_root, serial_trie_updates)) => {
1282 debug!(
1283 target: "engine::tree::state_root_strategy",
1284 ?serial_root,
1285 "Serial state root computation finished for comparison"
1286 );
1287
1288 match state_provider_factory.database_provider_ro() {
1289 Ok(provider) => match super::trie_updates::compare_trie_updates(
1290 &provider,
1291 task_trie_updates,
1292 serial_trie_updates,
1293 ) {
1294 Ok(has_diff) => return has_diff,
1295 Err(err) => {
1296 warn!(
1297 target: "engine::tree::state_root_strategy",
1298 %err,
1299 "Error comparing trie updates"
1300 );
1301 return true;
1302 }
1303 },
1304 Err(err) => {
1305 warn!(
1306 target: "engine::tree::state_root_strategy",
1307 %err,
1308 "Failed to get database provider for trie update comparison"
1309 );
1310 }
1311 }
1312 }
1313 Err(err) => {
1314 warn!(
1315 target: "engine::tree::state_root_strategy",
1316 %err,
1317 "Failed to compute serial state root for comparison"
1318 );
1319 }
1320 }
1321 false
1322}
1323
1324#[cfg(test)]
1325mod tests {
1326 use super::*;
1327 use alloy_consensus::constants::KECCAK_EMPTY;
1328 use alloy_primitives::{map::HashMap, Address, U256};
1329 use rand::Rng;
1330 use reth_chain_state::test_utils::TestBlockBuilder;
1331 use reth_chainspec::ChainSpec;
1332 use reth_db_common::init::init_genesis;
1333 use reth_ethereum_primitives::EthPrimitives;
1334 use reth_evm::OnStateHook;
1335 use reth_evm_ethereum::EthEvmConfig;
1336 use reth_primitives_traits::{Account, StorageEntry};
1337 use reth_provider::{
1338 providers::BlockchainProvider, test_utils::create_test_provider_factory_with_chain_spec,
1339 HashingWriter,
1340 };
1341 use reth_storage_overlay::{OverlayManager, OverlayStateProviderFactory};
1342 use reth_testing_utils::generators;
1343 use reth_trie::test_utils::state_root;
1344 use revm::state::{AccountInfo, AccountStatus, EvmState, EvmStorageSlot, TransactionId};
1345
1346 #[test]
1347 fn proof_worker_count_scales_with_block_gas() {
1348 let pool_threads = 64;
1349 let count = |transaction_count, gas_used| {
1350 DefaultStateRootStrategy::proof_worker_count(
1351 pool_threads,
1352 None,
1353 transaction_count,
1354 gas_used,
1355 )
1356 };
1357
1358 assert_eq!(count(Some(30), Some(1_000_000)), 16);
1360 assert_eq!(count(Some(30), Some(300_000_000)), 16);
1361
1362 assert_eq!(count(Some(1_000), Some(100_000_000)), 64);
1364 assert_eq!(count(Some(1_000), Some(300_000_000)), 64);
1365
1366 assert_eq!(count(Some(31), Some(99_999_999)), 32);
1368 assert_eq!(count(Some(1_000), Some(15_000_000)), 32);
1369 assert_eq!(count(None, None), 32);
1370
1371 for (transaction_count, gas_used) in
1373 [(Some(30), Some(1_000_000)), (Some(1_000), Some(300_000_000)), (None, None)]
1374 {
1375 assert_eq!(
1376 DefaultStateRootStrategy::proof_worker_count(
1377 48,
1378 Some(48),
1379 transaction_count,
1380 gas_used
1381 ),
1382 48
1383 );
1384 }
1385
1386 assert_eq!(
1388 DefaultStateRootStrategy::proof_worker_count(1, None, Some(1), Some(1_000_000)),
1389 1
1390 );
1391 assert_eq!(DefaultStateRootStrategy::proof_worker_count(1, None, None, None), 1);
1392 assert_eq!(DefaultStateRootStrategy::proof_worker_count(1, Some(0), None, None), 1);
1393 }
1394
1395 #[test]
1396 fn finish_publishes_only_accepted_sparse_trie() {
1397 let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1398 let genesis_hash = init_genesis(&factory).unwrap();
1399 let provider = BlockchainProvider::new(factory).unwrap();
1400 let runtime = reth_tasks::Runtime::test();
1401
1402 for timeout in [None, Some(Duration::from_secs(1))] {
1403 for (finish, matching_root) in [(false, true), (true, true), (true, false)] {
1404 let overlay_manager = OverlayManager::<EthPrimitives>::default();
1405 let block = RecoveredBlock::new_unhashed(
1406 reth_ethereum_primitives::Block {
1407 header: alloy_consensus::Header {
1408 state_root: B256::with_last_byte(1),
1409 ..Default::default()
1410 },
1411 ..Default::default()
1412 },
1413 Vec::new(),
1414 );
1415 let (completer, trie) = mpsc::channel();
1416 let (pending_trie_tx, pending_trie_rx) = mpsc::channel();
1417 pending_trie_tx
1418 .send(PendingSparseTrie {
1419 state_root: if matching_root { block.state_root } else { B256::ZERO },
1420 anchor_hash: genesis_hash,
1421 trie,
1422 })
1423 .unwrap();
1424 let (result_tx, result_rx) = mpsc::channel();
1425 result_tx
1426 .send(Ok(StateRootComputeOutcome {
1427 state_root: if matching_root { block.state_root } else { B256::ZERO },
1428 trie_updates: Default::default(),
1429 hashed_state: Default::default(),
1430 }))
1431 .unwrap();
1432 let mut job = SparseTrieStateRootJob {
1433 pending_trie_rx,
1434 overlay_manager: overlay_manager.clone(),
1435 handle: StateRootHandle::new(
1436 B256::ZERO,
1437 crossbeam_channel::unbounded().0,
1438 StateRootTaskCancelGuard::channel().0,
1439 result_rx,
1440 mpsc::channel().1,
1441 ),
1442 state_provider_factory: OverlayStateProviderFactory::new(
1443 provider.clone(),
1444 overlay_manager.overlay_builder(genesis_hash),
1445 ),
1446 executor: runtime.clone(),
1447 timeout,
1448 compare_trie_updates: false,
1449 metrics: Default::default(),
1450 };
1451 assert!(overlay_manager.take_sparse_trie().is_none());
1452 if finish {
1453 job.finish(
1454 &block,
1455 Arc::new(BlockExecutionOutput::default()),
1456 &LazyHashedPostState::ready(Default::default()),
1457 )
1458 .unwrap();
1459 }
1460 drop(job);
1461 let preserved = overlay_manager.take_sparse_trie();
1462 assert_eq!(preserved.is_some(), finish && matching_root);
1463 assert_eq!(
1465 completer.send(SparseTrieWithStorageRoots::default()).is_ok(),
1466 finish && matching_root
1467 );
1468 if let Some(preserved) = preserved {
1469 assert!(preserved.into_trie_for(block.hash()).unwrap().is_some());
1470 }
1471 }
1472 }
1473 }
1474
1475 #[test]
1476 fn failed_parent_trie_closes_handoff_without_publishing() {
1477 let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1478 let genesis_hash = init_genesis(&factory).unwrap();
1479 let runtime = reth_tasks::Runtime::test();
1480 {
1481 let (completer, trie) = mpsc::channel();
1482 let preserved = PreservedSparseTrie::pending(trie, genesis_hash, B256::ZERO);
1483 drop(completer);
1484 let (mut handle, pending_trie_rx) = DefaultStateRootStrategy::default()
1485 .spawn_state_root(
1486 &runtime,
1487 OverlayStateProviderFactory::new(
1488 factory,
1489 OverlayManager::<EthPrimitives>::default().overlay_builder(genesis_hash),
1490 ),
1491 StateRootTaskOptions::<EthPrimitives> {
1492 parent_header: SealedHeader::new(Default::default(), genesis_hash),
1493 preserved_sparse_trie: Some(preserved),
1494 transaction_count: None,
1495 gas_used: None,
1496 config: &TreeConfig::default(),
1497 pending_sparse_trie_prune_blocks: None,
1498 },
1499 );
1500 assert!(handle.state_root().is_err());
1501 assert!(matches!(
1502 pending_trie_rx.recv_timeout(Duration::from_secs(5)),
1503 Err(RecvTimeoutError::Disconnected)
1504 ));
1505 }
1506 for name in ["sparse-trie", "storage-workers", "account-workers"] {
1507 runtime.spawn_blocking_named(name, || {}).get();
1508 }
1509 }
1510
1511 #[test]
1512 fn publication_does_not_wait_for_worker() {
1513 let (pending_trie_tx, pending_trie_rx) = mpsc::channel();
1514 let overlay_manager = OverlayManager::<EthPrimitives>::default();
1515 let (done_tx, done_rx) = mpsc::channel();
1516 let worker = std::thread::spawn(move || {
1517 publish_sparse_trie(&pending_trie_rx, &overlay_manager, B256::ZERO, B256::ZERO);
1518 assert!(overlay_manager.take_sparse_trie().is_none());
1519 done_tx.send(()).unwrap();
1520 });
1521 let result = done_rx.recv_timeout(Duration::from_secs(5));
1522 drop(pending_trie_tx);
1523 worker.join().unwrap();
1524 result.expect("publication should not wait for the root task");
1525 }
1526
1527 #[test]
1528 fn dropped_root_receiver_still_completes_pending_trie() {
1529 let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1530 let genesis_hash = init_genesis(&factory).unwrap();
1531 let runtime = reth_tasks::Runtime::test();
1532 let (mut handle, pending_trie_rx) = DefaultStateRootStrategy::default().spawn_state_root(
1533 &runtime,
1534 OverlayStateProviderFactory::new(
1535 factory,
1536 OverlayManager::<EthPrimitives>::default().overlay_builder(genesis_hash),
1537 ),
1538 StateRootTaskOptions::<EthPrimitives> {
1539 parent_header: SealedHeader::new(Default::default(), genesis_hash),
1540 preserved_sparse_trie: None,
1541 transaction_count: None,
1542 gas_used: None,
1543 config: &TreeConfig::default(),
1544 pending_sparse_trie_prune_blocks: None,
1545 },
1546 );
1547 drop(handle.take_state_root_rx());
1548 drop(handle.take_execution_hook());
1549 let pending = pending_trie_rx.recv_timeout(Duration::from_secs(5)).unwrap();
1550 assert_eq!(pending.anchor_hash, genesis_hash);
1551 pending
1552 .trie
1553 .recv_timeout(Duration::from_secs(5))
1554 .expect("handed-off trie must be completed");
1555 drop(handle);
1556 for name in ["sparse-trie", "trie-hashing", "storage-workers", "account-workers", "drop"] {
1557 runtime.spawn_blocking_named(name, || {}).get();
1558 }
1559 }
1560
1561 #[test]
1562 fn sparse_trie_prune_target_uses_requested_range() {
1563 let parent = alloy_eips::BlockNumHash { hash: B256::with_last_byte(9), number: 9 };
1564 assert_eq!(
1565 sparse_trie_prune_target::<EthPrimitives>(B256::ZERO, false, parent, None),
1566 None
1567 );
1568 assert_eq!(
1569 sparse_trie_prune_target::<EthPrimitives>(B256::ZERO, false, parent, Some(&[])),
1570 Some((TrieNodeEpoch::new(10), parent.hash))
1571 );
1572
1573 let mut blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(7..10).collect();
1574 blocks.reverse();
1575
1576 let anchor = blocks[0].recovered_block().hash();
1577 assert_eq!(
1578 sparse_trie_prune_target(anchor, true, parent, Some(&blocks)),
1579 Some((TrieNodeEpoch::new(7), anchor))
1580 );
1581 }
1582
1583 #[test]
1584 fn sparse_trie_prune_target_advances_to_prune_anchor() {
1585 let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..5).collect();
1586 let reused_anchor_hash = blocks[0].recovered_block().hash();
1587 let expected_prune_anchor = blocks[1].recovered_block().hash();
1588 let parent = blocks[3].recovered_block().num_hash();
1589 let prune_blocks: Vec<_> = blocks.into_iter().skip(2).rev().collect();
1590
1591 assert_eq!(
1592 sparse_trie_prune_target(reused_anchor_hash, true, parent, Some(&prune_blocks),),
1593 Some((TrieNodeEpoch::new(3), expected_prune_anchor))
1594 );
1595 }
1596
1597 #[test]
1598 fn sparse_trie_prune_target_keeps_parent_for_fresh_trie() {
1599 let mut blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..3).collect();
1600 blocks.reverse();
1601 let parent_hash = B256::with_last_byte(0xaa);
1602
1603 assert_eq!(
1604 sparse_trie_prune_target(
1605 parent_hash,
1606 false,
1607 blocks[0].recovered_block().num_hash(),
1608 Some(&blocks),
1609 ),
1610 Some((TrieNodeEpoch::new(1), parent_hash))
1611 );
1612 }
1613
1614 #[test]
1615 fn sparse_trie_prune_target_advances_reused_anchor_when_parent_becomes_durable() {
1616 let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..4).collect();
1617 let old_anchor = blocks[1].recovered_block().hash();
1618 let parent = blocks[2].recovered_block().num_hash();
1619
1620 assert_eq!(
1621 sparse_trie_prune_target::<EthPrimitives>(old_anchor, true, parent, Some(&[])),
1622 Some((TrieNodeEpoch::new(4), parent.hash))
1623 );
1624 }
1625
1626 fn create_mock_state_updates(num_accounts: usize, updates_per_account: usize) -> Vec<EvmState> {
1627 let mut rng = generators::rng();
1628 let all_addresses: Vec<Address> = (0..num_accounts).map(|_| rng.random()).collect();
1629 let mut updates = Vec::with_capacity(updates_per_account);
1630
1631 for _ in 0..updates_per_account {
1632 let num_accounts_in_update = rng.random_range(1..=num_accounts);
1633 let mut state_update = EvmState::default();
1634
1635 for &address in &all_addresses[0..num_accounts_in_update] {
1636 let mut storage = HashMap::default();
1637 if rng.random_bool(0.7) {
1638 for _ in 0..rng.random_range(1..10) {
1639 let slot = U256::from(rng.random::<u64>());
1640 storage.insert(
1641 slot,
1642 EvmStorageSlot::new_changed(
1643 U256::ZERO,
1644 U256::from(rng.random::<u64>()),
1645 TransactionId::ZERO,
1646 ),
1647 );
1648 }
1649 }
1650
1651 let mut account = revm::state::Account::default();
1652 account.info = AccountInfo {
1653 balance: U256::from(rng.random::<u64>()),
1654 nonce: rng.random::<u64>(),
1655 code_hash: KECCAK_EMPTY,
1656 code: Some(Default::default()),
1657 ..Default::default()
1658 };
1659 account.storage = storage;
1660 account.status = AccountStatus::Touched;
1661 account.transaction_id = TransactionId::ZERO;
1662 state_update.insert(address, account);
1663 }
1664
1665 updates.push(state_update);
1666 }
1667
1668 updates
1669 }
1670
1671 #[test]
1672 fn payload_builder_trie_matches_serial_root_and_can_be_reused() {
1673 reth_tracing::init_test_tracing();
1674
1675 let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1676 let genesis_hash = init_genesis(&factory).unwrap();
1677 let state_updates = create_mock_state_updates(10, 10);
1678 let mut accumulated_state: HashMap<Address, (Account, HashMap<B256, U256>)> =
1679 HashMap::default();
1680
1681 {
1682 let provider_rw = factory.provider_rw().expect("failed to get provider");
1683 for update in &state_updates {
1684 let account_updates = update.iter().map(|(address, account)| {
1685 (*address, Some(Account::from_revm_account(account)))
1686 });
1687 provider_rw
1688 .insert_account_for_hashing(account_updates)
1689 .expect("failed to insert accounts");
1690
1691 let storage_updates = update.iter().map(|(address, account)| {
1692 let storage_entries = account.storage.iter().map(|(slot, value)| {
1693 StorageEntry { key: B256::from(*slot), value: value.present_value }
1694 });
1695 (*address, storage_entries)
1696 });
1697 provider_rw
1698 .insert_storage_for_hashing(storage_updates)
1699 .expect("failed to insert storage");
1700 }
1701 provider_rw.commit().expect("failed to commit changes");
1702 }
1703
1704 for update in &state_updates {
1705 for (address, account) in update {
1706 let storage: HashMap<B256, U256> = account
1707 .storage
1708 .iter()
1709 .map(|(key, value)| (B256::from(*key), value.present_value))
1710 .collect();
1711 let entry = accumulated_state.entry(*address).or_default();
1712 entry.0 = Account::from_revm_account(account);
1713 entry.1.extend(storage);
1714 }
1715 }
1716
1717 let provider_factory = BlockchainProvider::new(factory).unwrap();
1718 let runtime = reth_tasks::Runtime::test();
1719 let overlay_manager = OverlayManager::<EthPrimitives>::default();
1720 let config = TreeConfig::default()
1721 .with_has_enough_parallelism(true)
1722 .with_share_sparse_trie_with_payload_builder(true);
1723 let mut state = EngineApiTreeState::new(
1724 10,
1725 10,
1726 config.invalid_header_hit_eviction_threshold(),
1727 alloy_eips::BlockNumHash::new(0, genesis_hash),
1728 crate::engine::EngineApiKind::Ethereum,
1729 overlay_manager.clone(),
1730 );
1731 let mut root_from_regular = state_root(accumulated_state.clone());
1732 let mut preserved: Option<PreservedSparseTrie> = None;
1733 for (number, (reuse, return_payload, matching_root)) in [
1734 (false, false, true),
1735 (false, true, false),
1736 (false, true, true),
1737 (true, true, true),
1738 (true, false, true),
1739 ]
1740 .into_iter()
1741 .enumerate()
1742 {
1743 assert_eq!(preserved.is_some(), reuse);
1744 let parent_hash =
1745 preserved.as_ref().map_or(genesis_hash, PreservedSparseTrie::block_hash);
1746 let block_hash = B256::with_last_byte(number as u8 + 1);
1747 if let Some(preserved) = preserved.take() {
1748 overlay_manager.store_sparse_trie(preserved);
1749 }
1750 let parent_header = alloy_consensus::Header {
1751 number: number as u64,
1752 state_root: if reuse { root_from_regular } else { B256::ZERO },
1753 ..Default::default()
1754 };
1755 let mut state_root_handle = <DefaultStateRootStrategy as StateRootStrategy<
1756 EthPrimitives,
1757 _,
1758 EthEvmConfig,
1759 >>::prepare_payload_builder(
1760 &DefaultStateRootStrategy::default(),
1761 PayloadStateRootJobContext::new(
1762 &runtime,
1763 &overlay_manager,
1764 parent_hash,
1765 &parent_header,
1766 1,
1767 &mut state,
1768 OverlayStateProviderFactory::new(
1769 provider_factory.clone(),
1770 overlay_manager.overlay_builder(genesis_hash),
1771 ),
1772 &config,
1773 ),
1774 )
1775 .unwrap()
1776 .unwrap();
1777 let on_payload_built = state_root_handle.take_on_payload_built().unwrap();
1778 let mut state_hook = state_root_handle.take_state_hook();
1779 if reuse {
1780 let mut update = state_updates.last().unwrap().clone();
1781 for (address, account) in &mut update {
1782 account.info.nonce += number as u64;
1783 accumulated_state.get_mut(address).unwrap().0 =
1784 Account::from_revm_account(account);
1785 }
1786 state_hook.on_state(update);
1787 root_from_regular = state_root(accumulated_state.clone());
1788 } else {
1789 for update in &state_updates {
1790 state_hook.on_state(update.clone());
1791 }
1792 }
1793 drop(state_hook);
1794
1795 let root_from_task = state_root_handle.state_root().expect("task failed").state_root;
1796 assert_eq!(root_from_task, root_from_regular);
1797 assert!(overlay_manager.take_sparse_trie().is_none());
1798 drop(state_root_handle);
1799 if return_payload {
1800 on_payload_built(
1801 block_hash,
1802 if matching_root { root_from_task } else { B256::ZERO },
1803 );
1804 } else {
1805 drop(on_payload_built);
1806 }
1807 preserved = overlay_manager.take_sparse_trie();
1808 assert_eq!(preserved.is_some(), return_payload && matching_root);
1809 if let Some(preserved) = &preserved {
1810 assert_eq!(preserved.block_hash(), block_hash);
1811 assert_eq!(preserved.anchor_hash(), genesis_hash);
1812 }
1813 }
1814 for name in ["sparse-trie", "trie-hashing", "storage-workers", "account-workers", "drop"] {
1815 runtime.spawn_blocking_named(name, || {}).get();
1816 }
1817 }
1818}