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;
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 PayloadStateRootHandle, StateAccessHint, StateRootComputeOutcome, StateRootHandle,
83 StateRootHintStream, StateRootMessage, StateRootTaskCancelGuard, StateRootUpdateHook,
84 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 const fn take_execution_hook(&mut self) -> Option<StateRootUpdateHook> {
381 self.execution_hook.take()
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)]
532 pub fn spawn_state_root<N, F>(
533 &self,
534 executor: &reth_tasks::Runtime,
535 multiproof_provider_factory: OverlayStateProviderFactory<F, N>,
536 options: StateRootTaskOptions<'_, N>,
537 ) -> (StateRootHandle, mpsc::Receiver<PendingSparseTrie>)
538 where
539 N: NodePrimitives,
540 F: DatabaseProviderFactory + Clone + 'static,
541 F::Provider: BlockNumReader
542 + PruneCheckpointReader
543 + StageCheckpointReader
544 + ChangeSetReader
545 + StorageChangeSetReader
546 + StorageSettingsCache
547 + 'static,
548 {
549 let StateRootTaskOptions {
550 parent_header,
551 preserved_sparse_trie,
552 transaction_count,
553 gas_used,
554 config,
555 pending_sparse_trie_prune_blocks,
556 } = options;
557 let (updates_tx, from_multi_proof) = crossbeam_channel::unbounded();
558 let (cancel_guard, cancel_rx) = StateRootTaskCancelGuard::channel();
559
560 let worker_counts = ProofWorkerCounts::new(
561 Self::proof_worker_count(
562 executor.proof_storage_worker_pool().num_threads(),
563 executor.proof_storage_worker_threads_override(),
564 transaction_count,
565 gas_used,
566 ),
567 Self::proof_worker_count(
568 executor.proof_account_worker_pool().num_threads(),
569 executor.proof_account_worker_threads_override(),
570 transaction_count,
571 gas_used,
572 ),
573 );
574 let (state_root_tx, state_root_rx) = mpsc::channel();
575 let (pending_trie_tx, pending_trie_rx) = mpsc::channel();
576 let (hashed_state_tx, hashed_state_rx) = mpsc::channel();
577 let parent_state_root = parent_header.state_root();
578
579 self.spawn_sparse_trie_task(
580 executor,
581 multiproof_provider_factory,
582 state_root_tx,
583 hashed_state_tx,
584 from_multi_proof,
585 cancel_rx,
586 SparseTrieTaskOptions {
587 parent_header,
588 preserved_sparse_trie,
589 pending_trie_tx,
590 chunk_size: config.multiproof_chunk_size(),
591 worker_counts,
592 #[cfg(feature = "trie-debug")]
593 proof_jitter: config.proof_jitter(),
594 pending_sparse_trie_prune_blocks: if config.disable_sparse_trie_cache_pruning() {
595 None
596 } else {
597 pending_sparse_trie_prune_blocks
598 },
599 },
600 );
601
602 (
603 StateRootHandle::new(
604 parent_state_root,
605 updates_tx,
606 cancel_guard,
607 state_root_rx,
608 hashed_state_rx,
609 ),
610 pending_trie_rx,
611 )
612 }
613
614 #[expect(clippy::too_many_arguments)]
616 fn spawn_sparse_trie_task<N, F>(
617 &self,
618 executor: &reth_tasks::Runtime,
619 multiproof_provider_factory: OverlayStateProviderFactory<F, N>,
620 state_root_tx: mpsc::Sender<Result<StateRootComputeOutcome, StateRootTaskError>>,
621 hashed_state_tx: mpsc::Sender<Arc<HashedPostState>>,
622 from_multi_proof: CrossbeamReceiver<StateRootMessage>,
623 cancel_rx: CrossbeamReceiver<()>,
624 options: SparseTrieTaskOptions<N>,
625 ) where
626 N: NodePrimitives,
627 F: DatabaseProviderFactory + Clone + 'static,
628 F::Provider: BlockNumReader
629 + PruneCheckpointReader
630 + StageCheckpointReader
631 + ChangeSetReader
632 + StorageChangeSetReader
633 + StorageSettingsCache
634 + 'static,
635 {
636 let SparseTrieTaskOptions {
637 parent_header,
638 preserved_sparse_trie,
639 pending_trie_tx,
640 chunk_size,
641 worker_counts,
642 #[cfg(feature = "trie-debug")]
643 proof_jitter,
644 pending_sparse_trie_prune_blocks,
645 } = options;
646 let trie_metrics = self.metrics.clone();
647 let worker_metrics = self.proof_worker_metrics.clone();
648 let executor = executor.clone();
649
650 let parent_span = Span::current();
651 executor.clone().spawn_blocking_named("sparse-trie", move || {
652 reth_tasks::once!(increase_thread_priority);
653
654 let parent_hash = parent_header.hash();
655 let parent_state_root = parent_header.state_root();
656 let new_epoch = TrieNodeEpoch::new(parent_header.number().saturating_add(1));
657
658 let _enter = debug_span!(
659 target: "engine::tree::payload_processor",
660 parent: &parent_span,
661 "sparse_trie_task"
662 )
663 .entered();
664
665 let new_sparse_state_trie = || {
666 debug!(
667 target: "engine::tree::payload_processor",
668 "Creating new sparse trie - no preserved trie available"
669 );
670 let default_trie =
671 RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
672 SparseStateTrie::default()
673 .with_accounts_trie(default_trie.clone())
674 .with_default_storage_trie(default_trie)
675 .with_updates(true)
676 };
677
678 let mut sparse_trie_anchor_hash = parent_hash;
679 let mut reused_preserved_sparse_trie = false;
680 let mut known_storage_roots = Default::default();
681 let sparse_state_trie = match preserved_sparse_trie {
682 Some(preserved) => {
683 let start = Instant::now();
684 let preserved_anchor_hash = preserved.anchor_hash();
685 let preserved = preserved.into_trie_for(parent_hash);
686 trie_metrics
687 .sparse_trie_cache_wait_duration_histogram
688 .record(start.elapsed().as_secs_f64());
689
690 match preserved {
691 Ok(Some(SparseTrieWithStorageRoots { trie, storage_roots })) => {
692 known_storage_roots = storage_roots;
693 sparse_trie_anchor_hash = preserved_anchor_hash;
694 reused_preserved_sparse_trie = true;
695 trie
696 }
697 Ok(None) => new_sparse_state_trie(),
698 Err(err) => {
699 let _ = state_root_tx.send(Err(StateRootTaskError::Other(err.into())));
700 return;
701 }
702 }
703 }
704 None => new_sparse_state_trie(),
705 };
706 let prune_target = sparse_trie_prune_target(
707 sparse_trie_anchor_hash,
708 reused_preserved_sparse_trie,
709 parent_header.num_hash(),
710 pending_sparse_trie_prune_blocks.as_deref(),
711 );
712 let factory = if reused_preserved_sparse_trie {
713 multiproof_provider_factory.with_skip_overlay_for_reused_sparse_trie(
714 sparse_trie_anchor_hash,
715 known_storage_roots,
716 )
717 } else {
718 multiproof_provider_factory
719 };
720 let task_ctx = ProofTaskCtx::new(factory);
721 #[cfg(feature = "trie-debug")]
722 let task_ctx = task_ctx.with_proof_jitter(proof_jitter);
723 let proof_worker_handle =
724 ProofWorkerHandle::new(&executor, task_ctx, worker_counts, worker_metrics);
725 let mut task = SparseTrieCacheTask::new_with_trie(
726 &executor,
727 from_multi_proof,
728 cancel_rx,
729 hashed_state_tx,
730 proof_worker_handle,
731 trie_metrics.clone(),
732 sparse_state_trie,
733 parent_state_root,
734 new_epoch,
735 chunk_size,
736 );
737
738 let result = task.run();
739 let trie_completer = if let Ok(outcome) = &result {
741 let preserved_anchor_hash =
742 prune_target.map_or(sparse_trie_anchor_hash, |(_, anchor_hash)| anchor_hash);
743 let (tx, rx) = mpsc::channel();
744 let pending = PendingSparseTrie {
745 state_root: outcome.state_root,
746 anchor_hash: preserved_anchor_hash,
747 trie: rx,
748 };
749 pending_trie_tx.send(pending).is_ok().then_some(tx)
750 } else {
751 None
752 };
753
754 if state_root_tx.send(result).is_err() {
755 debug!(
756 target: "engine::tree::payload_processor",
757 "State root receiver dropped"
758 );
759 }
760 drop(pending_trie_tx);
761
762 let _enter =
763 debug_span!(target: "engine::tree::payload_processor", "preserve").entered();
764 let (trie_to_drop, deferred) = if let Some(completer) = trie_completer {
765 let start = Instant::now();
768 let (mut trie, deferred) = task.into_trie_for_reuse();
769 let storage_roots = trie.storage_roots(new_epoch);
770 if let Some((prune_before, _)) = prune_target {
771 let prune_start = Instant::now();
772 trie.prune(prune_before);
773 trie_metrics
774 .sparse_trie_prune_duration_histogram
775 .record(prune_start.elapsed().as_secs_f64());
776 }
777 trie_metrics
778 .into_trie_for_reuse_duration_histogram
779 .record(start.elapsed().as_secs_f64());
780 trie_metrics
781 .sparse_trie_retained_storage_tries
782 .set(trie.retained_storage_tries_count() as f64);
783 (
784 completer
785 .send(SparseTrieWithStorageRoots { trie, storage_roots })
786 .err()
787 .map(|err| err.0.trie),
788 deferred,
789 )
790 } else {
791 debug!(
792 target: "engine::tree::payload_processor",
793 "State root task has no reusable trie to publish, dropping trie"
794 );
795 let (trie, deferred) = task.into_cleared_trie();
796 (Some(trie), deferred)
797 };
798 if let Some(trie) = trie_to_drop {
799 executor.spawn_drop(trie);
800 }
801 executor.spawn_drop(deferred);
802 });
803 }
804}
805
806#[derive(Debug)]
808pub struct PendingSparseTrie {
809 state_root: B256,
810 anchor_hash: B256,
811 trie: mpsc::Receiver<SparseTrieWithStorageRoots>,
812}
813
814pub fn publish_sparse_trie<N: NodePrimitives>(
818 pending_trie_rx: &mpsc::Receiver<PendingSparseTrie>,
819 overlay_manager: &OverlayManager<N>,
820 block_hash: B256,
821 state_root: B256,
822) {
823 if let Ok(pending) = pending_trie_rx.try_recv() &&
826 pending.state_root == state_root
827 {
828 overlay_manager.store_sparse_trie(PreservedSparseTrie::pending(
829 pending.trie,
830 block_hash,
831 pending.anchor_hash,
832 ));
833 }
834}
835
836struct SparseTrieTaskOptions<N: NodePrimitives> {
837 parent_header: SealedHeader<N::BlockHeader>,
838 preserved_sparse_trie: Option<PreservedSparseTrie>,
839 pending_trie_tx: mpsc::Sender<PendingSparseTrie>,
840 chunk_size: usize,
841 worker_counts: ProofWorkerCounts,
842 #[cfg(feature = "trie-debug")]
843 proof_jitter: Option<Duration>,
844 pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
846}
847
848#[derive(Debug)]
852pub struct StateRootTaskOptions<'a, N: NodePrimitives> {
853 pub parent_header: SealedHeader<N::BlockHeader>,
855 pub preserved_sparse_trie: Option<PreservedSparseTrie>,
857 pub transaction_count: Option<usize>,
859 pub gas_used: Option<u64>,
861 pub config: &'a TreeConfig,
863 pub pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
867}
868
869fn sparse_trie_prune_target<N: NodePrimitives>(
870 current_anchor: B256,
871 reused_preserved_sparse_trie: bool,
872 parent: alloy_eips::BlockNumHash,
873 pending_blocks: Option<&[ExecutedBlock<N>]>,
874) -> Option<(TrieNodeEpoch, B256)> {
875 let blocks = pending_blocks?;
876 let Some(oldest) = blocks.last() else {
877 return Some((TrieNodeEpoch::new(parent.number.saturating_add(1)), parent.hash))
879 };
880 let anchor = if !reused_preserved_sparse_trie ||
881 blocks.iter().any(|block| block.recovered_block().hash() == current_anchor)
882 {
883 current_anchor
884 } else {
885 oldest.recovered_block().parent_hash()
886 };
887 Some((TrieNodeEpoch::new(oldest.recovered_block().number()), anchor))
888}
889
890impl<N, P, Evm> StateRootStrategy<N, P, Evm> for DefaultStateRootStrategy
891where
892 N: NodePrimitives,
893 P: DatabaseProviderFactory + Clone + 'static,
894 P::Provider: BlockNumReader
895 + PruneCheckpointReader
896 + StageCheckpointReader
897 + ChangeSetReader
898 + StorageChangeSetReader
899 + StorageSettingsCache
900 + 'static,
901 Evm: ConfigureEvm<Primitives = N> + 'static,
902{
903 fn prepare(
904 &self,
905 mut ctx: StateRootJobContext<'_, N, P, Evm>,
906 ) -> ProviderResult<PreparedStateRootJob<N>> {
907 if ctx.config.skip_state_root() {
908 return Ok(PreparedStateRootJob::new(Box::new(SkippedStateRootJob {}), None))
909 }
910
911 if !ctx.config.use_state_root_task() {
912 return Ok(PreparedStateRootJob::new(
913 Box::new(SynchronousStateRootJob {
914 state_provider_factory: ctx.state_provider_factory,
915 }),
916 None,
917 ))
918 }
919
920 let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
921 let StateRootJobContext {
922 executor,
923 overlay_manager,
924 env,
925 parent_header,
926 state_provider_factory,
927 config,
928 parallel_bal_execution,
929 state: _,
930 } = ctx;
931
932 let preserved_sparse_trie = overlay_manager.take_sparse_trie();
933 let (mut handle, pending_trie_rx) = self.spawn_state_root(
934 executor,
935 state_provider_factory.clone(),
936 StateRootTaskOptions {
937 parent_header: parent_header.clone(),
938 preserved_sparse_trie,
939 transaction_count: Some(env.transaction_count),
940 gas_used: Some(env.gas_used),
941 config,
942 pending_sparse_trie_prune_blocks,
943 },
944 );
945
946 let (hashed_update_stream, execution_hook): (
950 Option<StateRootUpdateStream>,
951 Option<StateRootUpdateHook>,
952 ) = match parallel_bal_execution {
953 true => (Some(handle.take_hashed_update_stream()), None),
954 false => (None, Some(handle.take_execution_hook())),
955 };
956 let hint_stream = handle.take_hint_stream();
957
958 let hashed_state_rx = Some(handle.take_hashed_state_rx());
959
960 let mut prepared = PreparedStateRootJob::new(
961 Box::new(SparseTrieStateRootJob {
962 pending_trie_rx,
963 overlay_manager: overlay_manager.clone(),
964 handle,
965 state_provider_factory,
966 executor: executor.clone(),
967 timeout: config.state_root_task_timeout(),
968 metrics: BlockValidationMetrics::default(),
969 }),
970 hashed_state_rx,
971 )
972 .with_hint_stream(hint_stream);
973 if let Some(hook) = execution_hook {
974 prepared = prepared.with_execution_hook(hook);
975 }
976 if let Some(stream) = hashed_update_stream {
977 prepared = prepared.with_hashed_update_stream(stream);
978 }
979 Ok(prepared)
980 }
981
982 fn prepare_payload_builder(
983 &self,
984 mut ctx: PayloadStateRootJobContext<'_, N, P>,
985 ) -> ProviderResult<Option<PayloadStateRootHandle>> {
986 if !ctx.config.share_sparse_trie_with_payload_builder() ||
989 ctx.config.skip_state_root() ||
990 !ctx.config.has_enough_parallelism()
991 {
992 return Ok(None)
993 }
994
995 let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
996 let parent_header = SealedHeader::new(ctx.parent_header().clone(), ctx.parent_hash());
997 let preserved_sparse_trie = ctx.overlay_manager.take_sparse_trie();
998 let (handle, pending_trie_rx) = self.spawn_state_root(
999 ctx.executor,
1000 ctx.state_provider_factory.clone(),
1001 StateRootTaskOptions {
1002 parent_header,
1003 preserved_sparse_trie,
1004 transaction_count: None,
1007 gas_used: None,
1008 config: ctx.config,
1009 pending_sparse_trie_prune_blocks,
1010 },
1011 );
1012 let overlay_manager = ctx.overlay_manager.clone();
1013 Ok(Some(handle.into_payload_state_root_handle().with_on_payload_built(
1014 move |block_hash, state_root| {
1015 publish_sparse_trie(&pending_trie_rx, &overlay_manager, block_hash, state_root);
1016 },
1017 )))
1018 }
1019}
1020
1021#[derive(Debug)]
1022struct SkippedStateRootJob {}
1023
1024impl<N: NodePrimitives> StateRootJob<N> for SkippedStateRootJob {
1025 fn name(&self) -> &'static str {
1026 "skipped"
1027 }
1028
1029 fn finish(
1030 &mut self,
1031 block: &RecoveredBlock<N::Block>,
1032 _output: Arc<BlockExecutionOutput<N::Receipt>>,
1033 _hashed_state: &LazyHashedPostState,
1034 ) -> ProviderResult<StateRootJobOutcome> {
1035 Ok(StateRootJobOutcome::new(block.header().state_root(), Arc::new(TrieUpdates::default())))
1036 }
1037}
1038
1039#[derive(Debug)]
1040struct SynchronousStateRootJob<N: NodePrimitives, P> {
1041 state_provider_factory: OverlayStateProviderFactory<P, N>,
1042}
1043
1044impl<N, P> StateRootJob<N> for SynchronousStateRootJob<N, P>
1045where
1046 N: NodePrimitives,
1047 P: DatabaseProviderFactory + Clone + 'static,
1048 P::Provider: BlockNumReader
1049 + PruneCheckpointReader
1050 + StageCheckpointReader
1051 + ChangeSetReader
1052 + StorageChangeSetReader
1053 + StorageSettingsCache
1054 + 'static,
1055{
1056 fn name(&self) -> &'static str {
1057 "synchronous"
1058 }
1059
1060 fn finish(
1061 &mut self,
1062 _block: &RecoveredBlock<N::Block>,
1063 _output: Arc<BlockExecutionOutput<N::Receipt>>,
1064 hashed_state: &LazyHashedPostState,
1065 ) -> ProviderResult<StateRootJobOutcome> {
1066 let provider = self.state_provider_factory.database_provider_ro()?;
1067 let (state_root, trie_updates) =
1068 provider.state_root_with_updates(hashed_state.get().as_ref().clone())?;
1069 Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates)))
1070 }
1071}
1072
1073#[derive(Debug)]
1074struct SparseTrieStateRootJob<N: NodePrimitives, P> {
1075 pending_trie_rx: mpsc::Receiver<PendingSparseTrie>,
1076 overlay_manager: OverlayManager<N>,
1077 handle: StateRootHandle,
1078 state_provider_factory: OverlayStateProviderFactory<P, N>,
1079 executor: reth_tasks::Runtime,
1080 timeout: Option<Duration>,
1081 metrics: BlockValidationMetrics,
1082}
1083
1084impl<N, P> SparseTrieStateRootJob<N, P>
1085where
1086 N: NodePrimitives,
1087 P: DatabaseProviderFactory + Clone + 'static,
1088 P::Provider: BlockNumReader
1089 + PruneCheckpointReader
1090 + StageCheckpointReader
1091 + ChangeSetReader
1092 + StorageChangeSetReader
1093 + StorageSettingsCache
1094 + 'static,
1095{
1096 fn serial_fallback(
1097 &self,
1098 output: Arc<BlockExecutionOutput<N::Receipt>>,
1099 ) -> ProviderResult<SerialFallbackRx> {
1100 let provider = self.state_provider_factory.database_provider_ro()?;
1101 let (fallback_tx, fallback_rx) = mpsc::channel();
1102 self.executor.spawn_blocking_named("serial-root", move || {
1103 let result = (|| {
1104 let hashed_state = Arc::new(provider.hashed_post_state(&output.state)?);
1105 let (root, updates) =
1106 provider.state_root_with_updates(hashed_state.as_ref().clone())?;
1107 Ok((root, updates, hashed_state))
1108 })();
1109 let _ = fallback_tx.send(result);
1110 });
1111
1112 Ok(fallback_rx)
1113 }
1114
1115 fn compute_serial(
1120 &self,
1121 output: &BlockExecutionOutput<N::Receipt>,
1122 ) -> ProviderResult<StateRootJobOutcome> {
1123 let provider = self.state_provider_factory.database_provider_ro()?;
1124 let hashed_state = Arc::new(provider.hashed_post_state(&output.state)?);
1125 let (state_root, trie_updates) =
1126 provider.state_root_with_updates(hashed_state.as_ref().clone())?;
1127 self.metrics.state_root_task_fallback_success_total.increment(1);
1128 Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1129 .with_hashed_state(Some(hashed_state)))
1130 }
1131
1132 fn verified_sparse_outcome(
1137 &self,
1138 block: &RecoveredBlock<N::Block>,
1139 output: &BlockExecutionOutput<N::Receipt>,
1140 outcome: StateRootComputeOutcome,
1141 ) -> ProviderResult<StateRootJobOutcome> {
1142 let outcome = StateRootJobOutcome::new(outcome.state_root, outcome.trie_updates);
1143 if outcome.state_root == block.header().state_root() {
1144 publish_sparse_trie(
1145 &self.pending_trie_rx,
1146 &self.overlay_manager,
1147 block.hash(),
1148 outcome.state_root,
1149 );
1150 return Ok(outcome)
1151 }
1152 warn!(
1153 target: "engine::tree::state_root_strategy",
1154 state_root = ?outcome.state_root,
1155 block_state_root = ?block.header().state_root(),
1156 "State root task returned incorrect state root, recomputing serially"
1157 );
1158 self.compute_serial(output)
1159 }
1160}
1161
1162impl<N, P> StateRootJob<N> for SparseTrieStateRootJob<N, P>
1163where
1164 N: NodePrimitives,
1165 P: DatabaseProviderFactory + Clone + 'static,
1166 P::Provider: BlockNumReader
1167 + PruneCheckpointReader
1168 + StageCheckpointReader
1169 + ChangeSetReader
1170 + StorageChangeSetReader
1171 + StorageSettingsCache
1172 + 'static,
1173{
1174 fn name(&self) -> &'static str {
1175 "sparse-trie"
1176 }
1177
1178 fn finish(
1179 &mut self,
1180 block: &RecoveredBlock<N::Block>,
1181 output: Arc<BlockExecutionOutput<N::Receipt>>,
1182 _hashed_state: &LazyHashedPostState,
1183 ) -> ProviderResult<StateRootJobOutcome> {
1184 if self.timeout.is_none() {
1185 return match self.handle.state_root() {
1186 Ok(outcome) => self.verified_sparse_outcome(block, &output, outcome),
1187 Err(err) => {
1188 debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1189 self.compute_serial(&output)
1190 }
1191 }
1192 }
1193
1194 let timeout = self.timeout.expect("checked above");
1195 let task_rx = self.handle.take_state_root_rx();
1196 let fallback_rx = match task_rx.recv_timeout(timeout) {
1197 Ok(Ok(outcome)) => return self.verified_sparse_outcome(block, &output, outcome),
1198 Ok(Err(err)) => {
1199 debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1200 self.serial_fallback(output)?
1201 }
1202 Err(RecvTimeoutError::Timeout) => {
1203 warn!(target: "engine::tree::state_root_strategy", ?timeout, "State root task timed out, racing serial fallback");
1204 self.metrics.state_root_task_timeout_total.increment(1);
1205 self.serial_fallback(output)?
1206 }
1207 Err(RecvTimeoutError::Disconnected) => {
1208 debug!(target: "engine::tree::state_root_strategy", "State root task dropped, falling back to serial root");
1209 self.serial_fallback(output)?
1210 }
1211 };
1212
1213 loop {
1214 if let Ok(Ok(outcome)) = task_rx.try_recv() {
1215 let outcome = StateRootJobOutcome::new(outcome.state_root, outcome.trie_updates);
1216 if outcome.state_root == block.header().state_root() {
1217 publish_sparse_trie(
1218 &self.pending_trie_rx,
1219 &self.overlay_manager,
1220 block.hash(),
1221 outcome.state_root,
1222 );
1223 return Ok(outcome)
1224 }
1225 warn!(
1227 target: "engine::tree::state_root_strategy",
1228 state_root = ?outcome.state_root,
1229 block_state_root = ?block.header().state_root(),
1230 "State root task returned incorrect state root, using serial fallback"
1231 );
1232 }
1233
1234 match fallback_rx.try_recv() {
1235 Ok(Ok((state_root, trie_updates, hashed_state))) => {
1236 self.metrics.state_root_task_fallback_success_total.increment(1);
1237 return Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1238 .with_hashed_state(Some(hashed_state)))
1239 }
1240 Ok(Err(err)) => return Err(err),
1241 Err(mpsc::TryRecvError::Empty) => {}
1242 Err(mpsc::TryRecvError::Disconnected) => {
1243 return Err(ProviderError::other(std::io::Error::other(
1244 "serial state root fallback task dropped",
1245 )))
1246 }
1247 }
1248
1249 std::thread::sleep(Duration::from_millis(1));
1250 }
1251 }
1252}
1253
1254#[cfg(test)]
1255mod tests {
1256 use super::*;
1257 use alloy_consensus::constants::KECCAK_EMPTY;
1258 use alloy_primitives::{map::HashMap, Address, U256};
1259 use evm2::evm::AccountInfo;
1260 use rand::Rng;
1261 use reth_chain_state::test_utils::TestBlockBuilder;
1262 use reth_chainspec::ChainSpec;
1263 use reth_db_common::init::init_genesis;
1264 use reth_ethereum_primitives::EthPrimitives;
1265 use reth_evm::OnStateHook;
1266 use reth_evm_ethereum::EthEvmConfig;
1267 use reth_execution_types::{AccountUpdate, StateUpdate};
1268 use reth_primitives_traits::{Account, StorageEntry};
1269 use reth_provider::{
1270 providers::BlockchainProvider, test_utils::create_test_provider_factory_with_chain_spec,
1271 HashingWriter,
1272 };
1273 use reth_storage_overlay::{OverlayManager, OverlayStateProviderFactory};
1274 use reth_testing_utils::generators;
1275 use reth_trie::test_utils::state_root;
1276 use reth_trie_common::bundle_state::StorageSlot;
1277
1278 #[test]
1279 fn proof_worker_count_scales_with_block_gas() {
1280 let pool_threads = 64;
1281 let count = |transaction_count, gas_used| {
1282 DefaultStateRootStrategy::proof_worker_count(
1283 pool_threads,
1284 None,
1285 transaction_count,
1286 gas_used,
1287 )
1288 };
1289
1290 assert_eq!(count(Some(30), Some(1_000_000)), 16);
1292 assert_eq!(count(Some(30), Some(300_000_000)), 16);
1293
1294 assert_eq!(count(Some(1_000), Some(100_000_000)), 64);
1296 assert_eq!(count(Some(1_000), Some(300_000_000)), 64);
1297
1298 assert_eq!(count(Some(31), Some(99_999_999)), 32);
1300 assert_eq!(count(Some(1_000), Some(15_000_000)), 32);
1301 assert_eq!(count(None, None), 32);
1302
1303 for (transaction_count, gas_used) in
1305 [(Some(30), Some(1_000_000)), (Some(1_000), Some(300_000_000)), (None, None)]
1306 {
1307 assert_eq!(
1308 DefaultStateRootStrategy::proof_worker_count(
1309 48,
1310 Some(48),
1311 transaction_count,
1312 gas_used
1313 ),
1314 48
1315 );
1316 }
1317
1318 assert_eq!(
1320 DefaultStateRootStrategy::proof_worker_count(1, None, Some(1), Some(1_000_000)),
1321 1
1322 );
1323 assert_eq!(DefaultStateRootStrategy::proof_worker_count(1, None, None, None), 1);
1324 assert_eq!(DefaultStateRootStrategy::proof_worker_count(1, Some(0), None, None), 1);
1325 }
1326
1327 #[test]
1328 fn finish_publishes_only_accepted_sparse_trie() {
1329 let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1330 let genesis_hash = init_genesis(&factory).unwrap();
1331 let provider = BlockchainProvider::new(factory).unwrap();
1332 let runtime = reth_tasks::Runtime::test();
1333
1334 for timeout in [None, Some(Duration::from_secs(1))] {
1335 for (finish, matching_root) in [(false, true), (true, true), (true, false)] {
1336 let overlay_manager = OverlayManager::<EthPrimitives>::default();
1337 let block = RecoveredBlock::new_unhashed(
1338 reth_ethereum_primitives::Block {
1339 header: alloy_consensus::Header {
1340 state_root: B256::with_last_byte(1),
1341 ..Default::default()
1342 },
1343 ..Default::default()
1344 },
1345 Vec::new(),
1346 );
1347 let (completer, trie) = mpsc::channel();
1348 let (pending_trie_tx, pending_trie_rx) = mpsc::channel();
1349 pending_trie_tx
1350 .send(PendingSparseTrie {
1351 state_root: if matching_root { block.state_root } else { B256::ZERO },
1352 anchor_hash: genesis_hash,
1353 trie,
1354 })
1355 .unwrap();
1356 let (result_tx, result_rx) = mpsc::channel();
1357 result_tx
1358 .send(Ok(StateRootComputeOutcome {
1359 state_root: if matching_root { block.state_root } else { B256::ZERO },
1360 trie_updates: Default::default(),
1361 hashed_state: Default::default(),
1362 }))
1363 .unwrap();
1364 let mut job = SparseTrieStateRootJob {
1365 pending_trie_rx,
1366 overlay_manager: overlay_manager.clone(),
1367 handle: StateRootHandle::new(
1368 B256::ZERO,
1369 crossbeam_channel::unbounded().0,
1370 StateRootTaskCancelGuard::channel().0,
1371 result_rx,
1372 mpsc::channel().1,
1373 ),
1374 state_provider_factory: OverlayStateProviderFactory::new(
1375 provider.clone(),
1376 overlay_manager.overlay_builder(genesis_hash),
1377 ),
1378 executor: runtime.clone(),
1379 timeout,
1380 metrics: Default::default(),
1381 };
1382 assert!(overlay_manager.take_sparse_trie().is_none());
1383 if finish {
1384 job.finish(
1385 &block,
1386 Arc::new(BlockExecutionOutput::default()),
1387 &LazyHashedPostState::ready(Default::default()),
1388 )
1389 .unwrap();
1390 }
1391 drop(job);
1392 let preserved = overlay_manager.take_sparse_trie();
1393 assert_eq!(preserved.is_some(), finish && matching_root);
1394 assert_eq!(
1396 completer.send(SparseTrieWithStorageRoots::default()).is_ok(),
1397 finish && matching_root
1398 );
1399 if let Some(preserved) = preserved {
1400 assert!(preserved.into_trie_for(block.hash()).unwrap().is_some());
1401 }
1402 }
1403 }
1404 }
1405
1406 #[test]
1407 fn failed_parent_trie_closes_handoff_without_publishing() {
1408 let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1409 let genesis_hash = init_genesis(&factory).unwrap();
1410 let runtime = reth_tasks::Runtime::test();
1411 {
1412 let (completer, trie) = mpsc::channel();
1413 let preserved = PreservedSparseTrie::pending(trie, genesis_hash, B256::ZERO);
1414 drop(completer);
1415 let (mut handle, pending_trie_rx) = DefaultStateRootStrategy::default()
1416 .spawn_state_root(
1417 &runtime,
1418 OverlayStateProviderFactory::new(
1419 factory,
1420 OverlayManager::<EthPrimitives>::default().overlay_builder(genesis_hash),
1421 ),
1422 StateRootTaskOptions::<EthPrimitives> {
1423 parent_header: SealedHeader::new(Default::default(), genesis_hash),
1424 preserved_sparse_trie: Some(preserved),
1425 transaction_count: None,
1426 gas_used: None,
1427 config: &TreeConfig::default(),
1428 pending_sparse_trie_prune_blocks: None,
1429 },
1430 );
1431 assert!(handle.state_root().is_err());
1432 assert!(matches!(
1433 pending_trie_rx.recv_timeout(Duration::from_secs(5)),
1434 Err(RecvTimeoutError::Disconnected)
1435 ));
1436 }
1437 for name in ["sparse-trie", "storage-workers", "account-workers"] {
1438 runtime.spawn_blocking_named(name, || {}).get();
1439 }
1440 }
1441
1442 #[test]
1443 fn publication_does_not_wait_for_worker() {
1444 let (pending_trie_tx, pending_trie_rx) = mpsc::channel();
1445 let overlay_manager = OverlayManager::<EthPrimitives>::default();
1446 let (done_tx, done_rx) = mpsc::channel();
1447 let worker = std::thread::spawn(move || {
1448 publish_sparse_trie(&pending_trie_rx, &overlay_manager, B256::ZERO, B256::ZERO);
1449 assert!(overlay_manager.take_sparse_trie().is_none());
1450 done_tx.send(()).unwrap();
1451 });
1452 let result = done_rx.recv_timeout(Duration::from_secs(5));
1453 drop(pending_trie_tx);
1454 worker.join().unwrap();
1455 result.expect("publication should not wait for the root task");
1456 }
1457
1458 #[test]
1459 fn dropped_root_receiver_still_completes_pending_trie() {
1460 let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1461 let genesis_hash = init_genesis(&factory).unwrap();
1462 let runtime = reth_tasks::Runtime::test();
1463 let (mut handle, pending_trie_rx) = DefaultStateRootStrategy::default().spawn_state_root(
1464 &runtime,
1465 OverlayStateProviderFactory::new(
1466 factory,
1467 OverlayManager::<EthPrimitives>::default().overlay_builder(genesis_hash),
1468 ),
1469 StateRootTaskOptions::<EthPrimitives> {
1470 parent_header: SealedHeader::new(Default::default(), genesis_hash),
1471 preserved_sparse_trie: None,
1472 transaction_count: None,
1473 gas_used: None,
1474 config: &TreeConfig::default(),
1475 pending_sparse_trie_prune_blocks: None,
1476 },
1477 );
1478 drop(handle.take_state_root_rx());
1479 drop(handle.take_execution_hook());
1480 let pending = pending_trie_rx.recv_timeout(Duration::from_secs(5)).unwrap();
1481 assert_eq!(pending.anchor_hash, genesis_hash);
1482 pending
1483 .trie
1484 .recv_timeout(Duration::from_secs(5))
1485 .expect("handed-off trie must be completed");
1486 drop(handle);
1487 for name in ["sparse-trie", "trie-hashing", "storage-workers", "account-workers", "drop"] {
1488 runtime.spawn_blocking_named(name, || {}).get();
1489 }
1490 }
1491
1492 #[test]
1493 fn sparse_trie_prune_target_uses_requested_range() {
1494 let parent = alloy_eips::BlockNumHash { hash: B256::with_last_byte(9), number: 9 };
1495 assert_eq!(
1496 sparse_trie_prune_target::<EthPrimitives>(B256::ZERO, false, parent, None),
1497 None
1498 );
1499 assert_eq!(
1500 sparse_trie_prune_target::<EthPrimitives>(B256::ZERO, false, parent, Some(&[])),
1501 Some((TrieNodeEpoch::new(10), parent.hash))
1502 );
1503
1504 let mut blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(7..10).collect();
1505 blocks.reverse();
1506
1507 let anchor = blocks[0].recovered_block().hash();
1508 assert_eq!(
1509 sparse_trie_prune_target(anchor, true, parent, Some(&blocks)),
1510 Some((TrieNodeEpoch::new(7), anchor))
1511 );
1512 }
1513
1514 #[test]
1515 fn sparse_trie_prune_target_advances_to_prune_anchor() {
1516 let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..5).collect();
1517 let reused_anchor_hash = blocks[0].recovered_block().hash();
1518 let expected_prune_anchor = blocks[1].recovered_block().hash();
1519 let parent = blocks[3].recovered_block().num_hash();
1520 let prune_blocks: Vec<_> = blocks.into_iter().skip(2).rev().collect();
1521
1522 assert_eq!(
1523 sparse_trie_prune_target(reused_anchor_hash, true, parent, Some(&prune_blocks),),
1524 Some((TrieNodeEpoch::new(3), expected_prune_anchor))
1525 );
1526 }
1527
1528 #[test]
1529 fn sparse_trie_prune_target_keeps_parent_for_fresh_trie() {
1530 let mut blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..3).collect();
1531 blocks.reverse();
1532 let parent_hash = B256::with_last_byte(0xaa);
1533
1534 assert_eq!(
1535 sparse_trie_prune_target(
1536 parent_hash,
1537 false,
1538 blocks[0].recovered_block().num_hash(),
1539 Some(&blocks),
1540 ),
1541 Some((TrieNodeEpoch::new(1), parent_hash))
1542 );
1543 }
1544
1545 #[test]
1546 fn sparse_trie_prune_target_advances_reused_anchor_when_parent_becomes_durable() {
1547 let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..4).collect();
1548 let old_anchor = blocks[1].recovered_block().hash();
1549 let parent = blocks[2].recovered_block().num_hash();
1550
1551 assert_eq!(
1552 sparse_trie_prune_target::<EthPrimitives>(old_anchor, true, parent, Some(&[])),
1553 Some((TrieNodeEpoch::new(4), parent.hash))
1554 );
1555 }
1556
1557 fn create_mock_state_updates(
1558 num_accounts: usize,
1559 updates_per_account: usize,
1560 ) -> Vec<StateUpdate> {
1561 let mut rng = generators::rng();
1562 let all_addresses: Vec<Address> = (0..num_accounts).map(|_| rng.random()).collect();
1563 let mut updates = Vec::with_capacity(updates_per_account);
1564
1565 for _ in 0..updates_per_account {
1566 let num_accounts_in_update = rng.random_range(1..=num_accounts);
1567 let mut state_update = StateUpdate::default();
1568
1569 for &address in &all_addresses[0..num_accounts_in_update] {
1570 let mut storage = HashMap::<_, _>::default();
1571 if rng.random_bool(0.7) {
1572 for _ in 0..rng.random_range(1..10) {
1573 let slot = U256::from(rng.random::<u64>());
1574 storage.insert(
1575 slot,
1576 StorageSlot::new_changed(U256::ZERO, U256::from(rng.random::<u64>())),
1577 );
1578 }
1579 }
1580
1581 let info = AccountInfo {
1582 balance: U256::from(rng.random::<u64>()),
1583 nonce: rng.random::<u64>(),
1584 code_hash: KECCAK_EMPTY,
1585 code: Some(Default::default()),
1586 ..Default::default()
1587 };
1588 state_update.accounts.push(AccountUpdate {
1589 address,
1590 original: Some(AccountInfo::default()),
1591 current: Some(info),
1592 created: false,
1593 wiped: false,
1594 storage: storage.into_iter().collect(),
1595 });
1596 }
1597
1598 updates.push(state_update);
1599 }
1600
1601 updates
1602 }
1603
1604 #[test]
1605 fn payload_builder_trie_matches_serial_root_and_can_be_reused() {
1606 reth_tracing::init_test_tracing();
1607
1608 let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1609 let genesis_hash = init_genesis(&factory).unwrap();
1610 let state_updates = create_mock_state_updates(10, 10);
1611 let mut accumulated_state: HashMap<Address, (Account, HashMap<B256, U256>)> =
1612 HashMap::default();
1613
1614 {
1615 let provider_rw = factory.provider_rw().expect("failed to get provider");
1616 for update in &state_updates {
1617 let account_updates = update.accounts.iter().map(|account| {
1618 (
1619 account.address,
1620 account.current.as_ref().map(reth_primitives_traits::Account::from),
1621 )
1622 });
1623 provider_rw
1624 .insert_account_for_hashing(account_updates)
1625 .expect("failed to insert accounts");
1626
1627 let storage_updates = update.accounts.iter().map(|account| {
1628 let storage_entries = account.storage.iter().map(|(slot, value)| {
1629 StorageEntry { key: B256::from(*slot), value: value.present_value }
1630 });
1631 (account.address, storage_entries)
1632 });
1633 provider_rw
1634 .insert_storage_for_hashing(storage_updates)
1635 .expect("failed to insert storage");
1636 }
1637 provider_rw.commit().expect("failed to commit changes");
1638 }
1639
1640 for update in &state_updates {
1641 for account in &update.accounts {
1642 let storage: HashMap<B256, U256> = account
1643 .storage
1644 .iter()
1645 .map(|(key, value)| (B256::from(*key), value.present_value))
1646 .collect();
1647 let entry = accumulated_state.entry(account.address).or_default();
1648 entry.0 = reth_primitives_traits::Account::from(account.current.as_ref().unwrap());
1649 entry.1.extend(storage);
1650 }
1651 }
1652
1653 let provider_factory = BlockchainProvider::new(factory).unwrap();
1654 let runtime = reth_tasks::Runtime::test();
1655 let overlay_manager = OverlayManager::<EthPrimitives>::default();
1656 let config = TreeConfig::default()
1657 .with_has_enough_parallelism(true)
1658 .with_share_sparse_trie_with_payload_builder(true);
1659 let mut state = EngineApiTreeState::new(
1660 10,
1661 10,
1662 config.invalid_header_hit_eviction_threshold(),
1663 alloy_eips::BlockNumHash::new(0, genesis_hash),
1664 crate::engine::EngineApiKind::Ethereum,
1665 overlay_manager.clone(),
1666 );
1667 let mut root_from_regular = state_root(accumulated_state.clone());
1668 let mut preserved: Option<PreservedSparseTrie> = None;
1669 for (number, (reuse, return_payload, matching_root)) in [
1670 (false, false, true),
1671 (false, true, false),
1672 (false, true, true),
1673 (true, true, true),
1674 (true, false, true),
1675 ]
1676 .into_iter()
1677 .enumerate()
1678 {
1679 assert_eq!(preserved.is_some(), reuse);
1680 let parent_hash =
1681 preserved.as_ref().map_or(genesis_hash, PreservedSparseTrie::block_hash);
1682 let block_hash = B256::with_last_byte(number as u8 + 1);
1683 if let Some(preserved) = preserved.take() {
1684 overlay_manager.store_sparse_trie(preserved);
1685 }
1686 let parent_header = alloy_consensus::Header {
1687 number: number as u64,
1688 state_root: if reuse { root_from_regular } else { B256::ZERO },
1689 ..Default::default()
1690 };
1691 let mut state_root_handle = <DefaultStateRootStrategy as StateRootStrategy<
1692 EthPrimitives,
1693 _,
1694 EthEvmConfig,
1695 >>::prepare_payload_builder(
1696 &DefaultStateRootStrategy::default(),
1697 PayloadStateRootJobContext::new(
1698 &runtime,
1699 &overlay_manager,
1700 parent_hash,
1701 &parent_header,
1702 1,
1703 &mut state,
1704 OverlayStateProviderFactory::new(
1705 provider_factory.clone(),
1706 overlay_manager.overlay_builder(genesis_hash),
1707 ),
1708 &config,
1709 ),
1710 )
1711 .unwrap()
1712 .unwrap();
1713 let on_payload_built = state_root_handle.take_on_payload_built().unwrap();
1714 let mut state_hook = state_root_handle.take_state_hook();
1715 if reuse {
1716 let mut update = state_updates.last().unwrap().clone();
1717 for account in &mut update.accounts {
1718 let info = account.current.as_mut().unwrap();
1719 info.nonce += number as u64;
1720 accumulated_state.get_mut(&account.address).unwrap().0 =
1721 reth_primitives_traits::Account::from(&*info);
1722 }
1723 state_hook.on_state(update);
1724 root_from_regular = state_root(accumulated_state.clone());
1725 } else {
1726 for update in &state_updates {
1727 state_hook.on_state(update.clone());
1728 }
1729 }
1730 drop(state_hook);
1731
1732 let root_from_task = state_root_handle.state_root().expect("task failed").state_root;
1733 assert_eq!(root_from_task, root_from_regular);
1734 assert!(overlay_manager.take_sparse_trie().is_none());
1735 drop(state_root_handle);
1736 if return_payload {
1737 on_payload_built(
1738 block_hash,
1739 if matching_root { root_from_task } else { B256::ZERO },
1740 );
1741 } else {
1742 drop(on_payload_built);
1743 }
1744 preserved = overlay_manager.take_sparse_trie();
1745 assert_eq!(preserved.is_some(), return_payload && matching_root);
1746 if let Some(preserved) = &preserved {
1747 assert_eq!(preserved.block_hash(), block_hash);
1748 assert_eq!(preserved.anchor_hash(), genesis_hash);
1749 }
1750 }
1751 for name in ["sparse-trie", "trie-hashing", "storage-workers", "account-workers", "drop"] {
1752 runtime.spawn_blocking_named(name, || {}).get();
1753 }
1754 }
1755}