1mod sparse_trie;
49
50use self::sparse_trie::{SparseTrieCacheTask, SparseTrieTaskMetrics};
51use crate::tree::{
52 metrics::BlockValidationMetrics, EngineApiTreeState, ExecutionEnv, StateProviderBuilder,
53 TreeConfig,
54};
55use alloy_primitives::B256;
56use crossbeam_channel::Receiver as CrossbeamReceiver;
57use reth_chain_state::{ExecutedBlock, PreservedSparseTrie, StateTrieOverlayManager};
58use reth_errors::ProviderResult;
59use reth_evm::{ConfigureEvm, OnStateHook};
60use reth_primitives_traits::{
61 AlloyBlockHeader, FastInstant as Instant, NodePrimitives, RecoveredBlock,
62};
63use reth_provider::{
64 providers::OverlayStateProviderFactory, BlockExecutionOutput, BlockReader,
65 DatabaseProviderFactory, DatabaseProviderROFactory, HashedPostStateProvider, ProviderError,
66 StateProviderFactory, StateReader, StateRootProvider,
67};
68use reth_tasks::utils::increase_thread_priority;
69use reth_trie::{
70 hashed_cursor::HashedCursorFactory, prefix_set::TriePrefixSetsMut,
71 trie_cursor::TrieCursorFactory, updates::TrieUpdates, HashedPostState,
72};
73use reth_trie_parallel::proof_task::{ProofTaskCtx, ProofWorkerHandle};
74pub use reth_trie_parallel::{
75 error::StateRootTaskError,
76 state_root_task::{
77 evm_state_to_hashed_post_state, PayloadStateRootHandle, StateAccessHint,
78 StateRootComputeOutcome, StateRootHandle, StateRootHintStream, StateRootMessage,
79 StateRootSink, StateRootTaskCancelGuard, StateRootUpdateHook, StateRootUpdateStream,
80 },
81};
82#[cfg(feature = "trie-debug")]
83use reth_trie_sparse::debug_recorder::TrieDebugRecorder;
84use reth_trie_sparse::{ArenaParallelSparseTrie, RevealableSparseTrie, SparseStateTrie};
85use std::{
86 fmt,
87 sync::{
88 mpsc::{self, RecvTimeoutError},
89 Arc,
90 },
91 time::Duration,
92};
93use tracing::{debug, debug_span, instrument, warn, Span};
94
95pub type LazyHashedPostState = reth_tasks::LazyHandle<Arc<HashedPostState>>;
97
98pub trait StateRootStrategy<N, P, Evm>: Send + Sync
100where
101 N: NodePrimitives,
102 Evm: ConfigureEvm<Primitives = N>,
103{
104 fn prepare(
109 &self,
110 ctx: StateRootJobContext<'_, N, P, Evm>,
111 ) -> ProviderResult<PreparedStateRootJob<N>>;
112
113 fn prepare_payload_builder(
120 &self,
121 _ctx: PayloadStateRootJobContext<'_, N, P>,
122 ) -> ProviderResult<Option<PayloadStateRootHandle>> {
123 Ok(None)
124 }
125}
126
127pub struct PayloadStateRootJobContext<'a, N, P>
129where
130 N: NodePrimitives,
131{
132 executor: &'a reth_tasks::Runtime,
133 state_trie_overlays: &'a StateTrieOverlayManager<N>,
134 parent_hash: B256,
135 parent_header: &'a N::BlockHeader,
136 timestamp: u64,
137 state: &'a mut EngineApiTreeState<N>,
138 provider_builder: StateProviderBuilder<N, P>,
139 overlay_factory: OverlayStateProviderFactory<P, N>,
140 config: &'a TreeConfig,
141}
142
143impl<N, P> fmt::Debug for PayloadStateRootJobContext<'_, N, P>
144where
145 N: NodePrimitives,
146{
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 f.debug_struct("PayloadStateRootJobContext")
149 .field("parent_hash", &self.parent_hash)
150 .field("parent_state_root", &self.parent_state_root())
151 .field("timestamp", &self.timestamp)
152 .field("pending_sparse_trie_prune", &self.state.pending_sparse_trie_prune())
153 .finish_non_exhaustive()
154 }
155}
156
157impl<'a, N, P> PayloadStateRootJobContext<'a, N, P>
158where
159 N: NodePrimitives,
160{
161 #[expect(clippy::too_many_arguments)]
163 pub(crate) const fn new(
164 executor: &'a reth_tasks::Runtime,
165 state_trie_overlays: &'a StateTrieOverlayManager<N>,
166 parent_hash: B256,
167 parent_header: &'a N::BlockHeader,
168 timestamp: u64,
169 state: &'a mut EngineApiTreeState<N>,
170 provider_builder: StateProviderBuilder<N, P>,
171 overlay_factory: OverlayStateProviderFactory<P, N>,
172 config: &'a TreeConfig,
173 ) -> Self {
174 Self {
175 executor,
176 state_trie_overlays,
177 parent_hash,
178 parent_header,
179 timestamp,
180 state,
181 provider_builder,
182 overlay_factory,
183 config,
184 }
185 }
186
187 pub const fn parent_hash(&self) -> B256 {
189 self.parent_hash
190 }
191
192 pub const fn parent_header(&self) -> &N::BlockHeader {
197 self.parent_header
198 }
199
200 pub fn parent_state_root(&self) -> B256 {
202 self.parent_header.state_root()
203 }
204
205 pub const fn timestamp(&self) -> u64 {
209 self.timestamp
210 }
211
212 pub const fn executor(&self) -> &reth_tasks::Runtime {
214 self.executor
215 }
216
217 pub fn provider_builder(&self) -> StateProviderBuilder<N, P>
219 where
220 P: Clone,
221 {
222 self.provider_builder.clone()
223 }
224
225 pub fn take_sparse_trie_prune_blocks(&mut self) -> Option<Vec<ExecutedBlock<N>>> {
230 self.state.take_sparse_trie_prune_blocks(self.parent_hash)
231 }
232}
233
234pub struct StateRootJobContext<'a, N, P, Evm>
236where
237 N: NodePrimitives,
238 Evm: ConfigureEvm<Primitives = N>,
239{
240 executor: &'a reth_tasks::Runtime,
241 state_trie_overlays: &'a StateTrieOverlayManager<N>,
242 env: &'a ExecutionEnv<Evm>,
243 provider_builder: StateProviderBuilder<N, P>,
244 overlay_factory: OverlayStateProviderFactory<P, N>,
245 config: &'a TreeConfig,
246 parallel_bal_execution: bool,
247 state: &'a mut EngineApiTreeState<N>,
248}
249
250impl<N, P, Evm> fmt::Debug for StateRootJobContext<'_, N, P, Evm>
251where
252 N: NodePrimitives,
253 Evm: ConfigureEvm<Primitives = N>,
254{
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 f.debug_struct("StateRootJobContext")
257 .field("parallel_bal_execution", &self.parallel_bal_execution)
258 .field("has_pending_sparse_trie_prune", &self.state.pending_sparse_trie_prune())
259 .finish_non_exhaustive()
260 }
261}
262
263impl<'a, N, P, Evm> StateRootJobContext<'a, N, P, Evm>
264where
265 N: NodePrimitives,
266 Evm: ConfigureEvm<Primitives = N>,
267{
268 #[expect(clippy::too_many_arguments)]
270 pub(crate) const fn new(
271 executor: &'a reth_tasks::Runtime,
272 state_trie_overlays: &'a StateTrieOverlayManager<N>,
273 env: &'a ExecutionEnv<Evm>,
274 provider_builder: StateProviderBuilder<N, P>,
275 overlay_factory: OverlayStateProviderFactory<P, N>,
276 config: &'a TreeConfig,
277 parallel_bal_execution: bool,
278 state: &'a mut EngineApiTreeState<N>,
279 ) -> Self {
280 Self {
281 executor,
282 state_trie_overlays,
283 env,
284 provider_builder,
285 overlay_factory,
286 config,
287 parallel_bal_execution,
288 state,
289 }
290 }
291
292 pub const fn env(&self) -> &ExecutionEnv<Evm> {
294 self.env
295 }
296
297 pub const fn executor(&self) -> &reth_tasks::Runtime {
299 self.executor
300 }
301
302 pub const fn parallel_bal_execution(&self) -> bool {
304 self.parallel_bal_execution
305 }
306
307 pub fn provider_builder(&self) -> StateProviderBuilder<N, P>
309 where
310 P: Clone,
311 {
312 self.provider_builder.clone()
313 }
314
315 pub fn take_sparse_trie_prune_blocks(&mut self) -> Option<Vec<ExecutedBlock<N>>> {
320 self.state.take_sparse_trie_prune_blocks(self.env.parent_hash)
321 }
322}
323
324pub struct PreparedStateRootJob<N: NodePrimitives> {
331 job: Box<dyn StateRootJob<N>>,
332 execution_hook: Option<StateRootUpdateHook>,
333 hint_stream: Option<StateRootHintStream>,
334 hashed_update_stream: Option<StateRootUpdateStream>,
335 hashed_state_rx: Option<mpsc::Receiver<HashedPostState>>,
336}
337
338impl<N: NodePrimitives> fmt::Debug for PreparedStateRootJob<N> {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 f.debug_struct("PreparedStateRootJob")
341 .field("name", &self.job.name())
342 .field("has_execution_hook", &self.execution_hook.is_some())
343 .field("has_hint_stream", &self.hint_stream.is_some())
344 .field("has_hashed_update_stream", &self.hashed_update_stream.is_some())
345 .field("has_hashed_state_rx", &self.hashed_state_rx.is_some())
346 .finish()
347 }
348}
349
350impl<N: NodePrimitives> PreparedStateRootJob<N> {
351 pub const fn new(
353 job: Box<dyn StateRootJob<N>>,
354 hashed_state_rx: Option<mpsc::Receiver<HashedPostState>>,
355 ) -> Self {
356 Self {
357 job,
358 execution_hook: None,
359 hint_stream: None,
360 hashed_update_stream: None,
361 hashed_state_rx,
362 }
363 }
364
365 pub fn with_execution_hook(mut self, hook: StateRootUpdateHook) -> Self {
367 self.execution_hook = Some(hook);
368 self
369 }
370
371 pub fn with_hint_stream(mut self, hint_stream: StateRootHintStream) -> Self {
373 self.hint_stream = Some(hint_stream);
374 self
375 }
376
377 pub fn with_hashed_update_stream(mut self, stream: StateRootUpdateStream) -> Self {
379 self.hashed_update_stream = Some(stream);
380 self
381 }
382
383 pub fn name(&self) -> &'static str {
385 self.job.name()
386 }
387
388 pub fn take_execution_hook(&mut self) -> Option<Box<dyn OnStateHook + 'static>> {
390 self.execution_hook.take().map(|hook| Box::new(hook) as Box<dyn OnStateHook + 'static>)
391 }
392
393 pub const fn take_hint_stream(&mut self) -> Option<StateRootHintStream> {
395 self.hint_stream.take()
396 }
397
398 pub const fn take_hashed_update_stream(&mut self) -> Option<StateRootUpdateStream> {
400 self.hashed_update_stream.take()
401 }
402
403 pub const fn take_hashed_state_rx(&mut self) -> Option<mpsc::Receiver<HashedPostState>> {
409 self.hashed_state_rx.take()
410 }
411
412 pub fn finish(
414 &mut self,
415 block: &RecoveredBlock<N::Block>,
416 output: Arc<BlockExecutionOutput<N::Receipt>>,
417 hashed_state: &LazyHashedPostState,
418 ) -> ProviderResult<StateRootJobOutcome> {
419 self.job.finish(block, output, hashed_state)
420 }
421}
422
423pub trait StateRootJob<N: NodePrimitives>: Send {
425 fn name(&self) -> &'static str;
427
428 fn finish(
432 &mut self,
433 block: &RecoveredBlock<N::Block>,
434 output: Arc<BlockExecutionOutput<N::Receipt>>,
435 hashed_state: &LazyHashedPostState,
436 ) -> ProviderResult<StateRootJobOutcome>;
437}
438
439#[derive(Debug)]
441pub struct StateRootJobOutcome {
442 pub state_root: B256,
444 pub trie_updates: Arc<TrieUpdates>,
446 pub changed_paths: Option<Arc<TriePrefixSetsMut>>,
448 pub hashed_state: Option<Arc<HashedPostState>>,
453}
454
455impl StateRootJobOutcome {
456 pub const fn new(state_root: B256, trie_updates: Arc<TrieUpdates>) -> Self {
458 Self { state_root, trie_updates, changed_paths: None, hashed_state: None }
459 }
460
461 pub fn with_changed_paths(mut self, changed_paths: Option<Arc<TriePrefixSetsMut>>) -> Self {
463 self.changed_paths = changed_paths;
464 self
465 }
466
467 pub fn with_hashed_state(mut self, hashed_state: Option<Arc<HashedPostState>>) -> Self {
469 self.hashed_state = hashed_state;
470 self
471 }
472}
473
474type SerialFallbackRx = mpsc::Receiver<ProviderResult<(B256, TrieUpdates, Arc<HashedPostState>)>>;
477
478#[derive(Default)]
486pub struct DefaultStateRootStrategy {
487 metrics: SparseTrieTaskMetrics,
488}
489
490impl fmt::Debug for DefaultStateRootStrategy {
491 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
492 f.debug_struct("DefaultStateRootStrategy").finish_non_exhaustive()
493 }
494}
495
496impl DefaultStateRootStrategy {
497 const SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD: usize = 30;
500
501 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
507 fn spawn_state_root<N, F>(
508 &self,
509 executor: &reth_tasks::Runtime,
510 state_trie_overlays: &StateTrieOverlayManager<N>,
511 multiproof_provider_factory: F,
512 options: StateRootTaskOptions<'_, N>,
513 ) -> StateRootHandle
514 where
515 N: NodePrimitives,
516 F: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
517 + Clone
518 + Send
519 + Sync
520 + 'static,
521 {
522 let StateRootTaskOptions {
523 parent_state_root,
524 transaction_count,
525 config,
526 pending_sparse_trie_prune_blocks,
527 } = options;
528 let (updates_tx, from_multi_proof) = crossbeam_channel::unbounded();
529 let (cancel_guard, cancel_rx) = StateRootTaskCancelGuard::channel();
530
531 let task_ctx = ProofTaskCtx::new(multiproof_provider_factory);
532 #[cfg(feature = "trie-debug")]
533 let task_ctx = task_ctx.with_proof_jitter(config.proof_jitter());
534 let halve_workers = transaction_count
535 .is_some_and(|count| count <= Self::SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD);
536 let proof_handle = ProofWorkerHandle::new(executor, task_ctx, halve_workers);
537
538 let (state_root_tx, state_root_rx) = mpsc::channel();
539 let (hashed_state_tx, hashed_state_rx) = mpsc::channel();
540
541 self.spawn_sparse_trie_task(
542 executor,
543 state_trie_overlays,
544 proof_handle,
545 state_root_tx,
546 hashed_state_tx,
547 from_multi_proof,
548 cancel_rx,
549 SparseTrieTaskOptions {
550 parent_state_root,
551 chunk_size: config.multiproof_chunk_size(),
552 pending_sparse_trie_prune_blocks: if config.disable_sparse_trie_cache_pruning() {
553 None
554 } else {
555 pending_sparse_trie_prune_blocks
556 },
557 max_hot_slots: config.sparse_trie_max_hot_slots(),
558 max_hot_accounts: config.sparse_trie_max_hot_accounts(),
559 },
560 );
561
562 StateRootHandle::new(
563 parent_state_root,
564 updates_tx,
565 cancel_guard,
566 state_root_rx,
567 hashed_state_rx,
568 )
569 }
570
571 #[expect(clippy::too_many_arguments)]
573 fn spawn_sparse_trie_task<N: NodePrimitives>(
574 &self,
575 executor: &reth_tasks::Runtime,
576 state_trie_overlays: &StateTrieOverlayManager<N>,
577 proof_worker_handle: ProofWorkerHandle,
578 state_root_tx: mpsc::Sender<Result<StateRootComputeOutcome, StateRootTaskError>>,
579 hashed_state_tx: mpsc::Sender<HashedPostState>,
580 from_multi_proof: CrossbeamReceiver<StateRootMessage>,
581 cancel_rx: CrossbeamReceiver<()>,
582 options: SparseTrieTaskOptions<N>,
583 ) {
584 let SparseTrieTaskOptions {
585 parent_state_root,
586 chunk_size,
587 pending_sparse_trie_prune_blocks,
588 max_hot_slots,
589 max_hot_accounts,
590 } = options;
591 let state_trie_overlays = state_trie_overlays.clone();
592 let trie_metrics = self.metrics.clone();
593 let executor = executor.clone();
594
595 let parent_span = Span::current();
596 executor.clone().spawn_blocking_named("sparse-trie", move || {
597 reth_tasks::once!(increase_thread_priority);
598
599 let _enter = debug_span!(
600 target: "engine::tree::payload_processor",
601 parent: parent_span,
602 "sparse_trie_task"
603 )
604 .entered();
605
606 let start = Instant::now();
607 let preserved = state_trie_overlays.take_sparse_trie();
608 trie_metrics
609 .sparse_trie_cache_wait_duration_histogram
610 .record(start.elapsed().as_secs_f64());
611
612 let mut sparse_state_trie = preserved
613 .map(|preserved| preserved.into_trie_for(parent_state_root))
614 .unwrap_or_else(|| {
615 debug!(
616 target: "engine::tree::payload_processor",
617 "Creating new sparse trie - no preserved trie available"
618 );
619 let default_trie =
620 RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
621 SparseStateTrie::default()
622 .with_accounts_trie(default_trie.clone())
623 .with_default_storage_trie(default_trie)
624 .with_updates(true)
625 });
626 sparse_state_trie.set_changed_paths(true);
627 sparse_state_trie.set_hot_cache_capacities(max_hot_slots, max_hot_accounts);
628
629 let mut task = SparseTrieCacheTask::new_with_trie(
630 &executor,
631 from_multi_proof,
632 cancel_rx,
633 hashed_state_tx,
634 proof_worker_handle,
635 trie_metrics.clone(),
636 sparse_state_trie,
637 parent_state_root,
638 chunk_size,
639 );
640
641 let result = task.run();
642 let mut guard = state_trie_overlays.lock_sparse_trie();
646 let task_result = result.as_ref().ok().cloned();
647
648 if state_root_tx.send(result).is_err() {
649 debug!(
650 target: "engine::tree::payload_processor",
651 "State root receiver dropped, clearing trie"
652 );
653 let (trie, deferred) = task.into_cleared_trie();
654 guard.store(PreservedSparseTrie::cleared(trie));
655 drop(guard);
656 executor.spawn_drop(deferred);
657 return
658 }
659
660 let _enter =
661 debug_span!(target: "engine::tree::payload_processor", "preserve").entered();
662 let deferred = if let Some(result) = task_result {
663 let start = Instant::now();
664 let (mut trie, deferred) = task.into_trie_for_reuse();
665 if let Some(prune_blocks) = pending_sparse_trie_prune_blocks {
666 let changed_paths = result
667 .changed_paths
668 .as_deref()
669 .expect("sparse trie task always returns changed paths");
670 if let Some(retained_paths) =
671 sparse_trie_retained_paths(prune_blocks, changed_paths)
672 {
673 trie.prune(max_hot_slots, max_hot_accounts, retained_paths);
674 }
675 }
676 trie_metrics
677 .into_trie_for_reuse_duration_histogram
678 .record(start.elapsed().as_secs_f64());
679 trie_metrics.sparse_trie_retained_memory_bytes.set(trie.memory_size() as f64);
680 trie_metrics
681 .sparse_trie_retained_storage_tries
682 .set(trie.retained_storage_tries_count() as f64);
683 guard.store(PreservedSparseTrie::anchored(trie, result.state_root));
684 deferred
685 } else {
686 debug!(
687 target: "engine::tree::payload_processor",
688 "State root computation failed, clearing trie"
689 );
690 let (trie, deferred) = task.into_cleared_trie();
691 guard.store(PreservedSparseTrie::cleared(trie));
692 deferred
693 };
694 drop(guard);
695 executor.spawn_drop(deferred);
696 });
697 }
698}
699
700struct SparseTrieTaskOptions<N: NodePrimitives> {
701 parent_state_root: B256,
702 chunk_size: usize,
703 pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
705 max_hot_slots: usize,
706 max_hot_accounts: usize,
707}
708
709struct StateRootTaskOptions<'a, N: NodePrimitives> {
710 parent_state_root: B256,
711 transaction_count: Option<usize>,
712 config: &'a TreeConfig,
713 pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
714}
715
716fn sparse_trie_retained_paths<N: NodePrimitives>(
717 prune_blocks: Vec<ExecutedBlock<N>>,
718 current_changed_paths: &TriePrefixSetsMut,
719) -> Option<TriePrefixSetsMut> {
720 let mut retained_paths = TriePrefixSetsMut::default();
721 for block in prune_blocks {
722 let trie_data = block.trie_data();
723 let Some(changed_paths) = trie_data.changed_paths.as_deref() else {
724 debug!(
725 target: "engine::tree::payload_processor",
726 block = ?block.recovered_block().num_hash(),
727 "Skipping sparse trie prune because changed paths for in-memory block are unknown"
728 );
729 return None
730 };
731 retained_paths.extend_ref(changed_paths);
732 }
733 retained_paths.extend_ref(current_changed_paths);
734 Some(retained_paths)
735}
736
737impl<N, P, Evm> StateRootStrategy<N, P, Evm> for DefaultStateRootStrategy
738where
739 N: NodePrimitives,
740 P: DatabaseProviderFactory
741 + BlockReader<Header = N::BlockHeader>
742 + StateProviderFactory
743 + StateReader
744 + Clone
745 + 'static,
746 OverlayStateProviderFactory<P, N>: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
747 + Clone
748 + Send
749 + Sync
750 + 'static,
751 Evm: ConfigureEvm<Primitives = N> + 'static,
752{
753 fn prepare(
754 &self,
755 mut ctx: StateRootJobContext<'_, N, P, Evm>,
756 ) -> ProviderResult<PreparedStateRootJob<N>> {
757 if ctx.config.skip_state_root() {
758 return Ok(PreparedStateRootJob::new(Box::new(SkippedStateRootJob {}), None))
759 }
760
761 if !ctx.config.use_state_root_task() {
762 return Ok(PreparedStateRootJob::new(
763 Box::new(SynchronousStateRootJob { provider_builder: ctx.provider_builder }),
764 None,
765 ))
766 }
767
768 let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
769 let StateRootJobContext {
770 executor,
771 state_trie_overlays,
772 env,
773 provider_builder,
774 overlay_factory,
775 config,
776 parallel_bal_execution,
777 state: _,
778 } = ctx;
779
780 let mut handle = self.spawn_state_root(
781 executor,
782 state_trie_overlays,
783 overlay_factory.clone(),
784 StateRootTaskOptions {
785 parent_state_root: env.parent_state_root,
786 transaction_count: Some(env.transaction_count),
787 config,
788 pending_sparse_trie_prune_blocks,
789 },
790 );
791
792 let (hashed_update_stream, execution_hook): (
796 Option<StateRootUpdateStream>,
797 Option<StateRootUpdateHook>,
798 ) = match parallel_bal_execution {
799 true => (Some(handle.take_hashed_update_stream()), None),
800 false => (None, Some(handle.take_execution_hook())),
801 };
802 let hint_stream = handle.take_hint_stream();
803
804 let hashed_state_rx = Some(handle.take_hashed_state_rx());
805
806 let mut prepared = PreparedStateRootJob::new(
807 Box::new(SparseTrieStateRootJob {
808 handle,
809 provider_builder,
810 overlay_factory,
811 executor: executor.clone(),
812 timeout: config.state_root_task_timeout(),
813 compare_trie_updates: config.always_compare_trie_updates(),
814 metrics: BlockValidationMetrics::default(),
815 }),
816 hashed_state_rx,
817 )
818 .with_hint_stream(hint_stream);
819 if let Some(hook) = execution_hook {
820 prepared = prepared.with_execution_hook(hook);
821 }
822 if let Some(stream) = hashed_update_stream {
823 prepared = prepared.with_hashed_update_stream(stream);
824 }
825 Ok(prepared)
826 }
827
828 fn prepare_payload_builder(
829 &self,
830 mut ctx: PayloadStateRootJobContext<'_, N, P>,
831 ) -> ProviderResult<Option<PayloadStateRootHandle>> {
832 if !ctx.config.share_sparse_trie_with_payload_builder() ||
835 ctx.config.skip_state_root() ||
836 !ctx.config.has_enough_parallelism()
837 {
838 return Ok(None)
839 }
840
841 let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
842 Ok(Some(
843 self.spawn_state_root(
844 ctx.executor,
845 ctx.state_trie_overlays,
846 ctx.overlay_factory.clone(),
847 StateRootTaskOptions {
848 parent_state_root: ctx.parent_state_root(),
849 transaction_count: None,
851 config: ctx.config,
852 pending_sparse_trie_prune_blocks,
853 },
854 )
855 .into_payload_state_root_handle(),
856 ))
857 }
858}
859
860#[derive(Debug)]
861struct SkippedStateRootJob {}
862
863impl<N: NodePrimitives> StateRootJob<N> for SkippedStateRootJob {
864 fn name(&self) -> &'static str {
865 "skipped"
866 }
867
868 fn finish(
869 &mut self,
870 block: &RecoveredBlock<N::Block>,
871 _output: Arc<BlockExecutionOutput<N::Receipt>>,
872 _hashed_state: &LazyHashedPostState,
873 ) -> ProviderResult<StateRootJobOutcome> {
874 Ok(StateRootJobOutcome::new(block.header().state_root(), Arc::new(TrieUpdates::default())))
875 }
876}
877
878#[derive(Debug)]
879struct SynchronousStateRootJob<N: NodePrimitives, P> {
880 provider_builder: StateProviderBuilder<N, P>,
881}
882
883impl<N, P> StateRootJob<N> for SynchronousStateRootJob<N, P>
884where
885 N: NodePrimitives,
886 P: BlockReader + StateProviderFactory + StateReader + Clone + Send + Sync + 'static,
887{
888 fn name(&self) -> &'static str {
889 "synchronous"
890 }
891
892 fn finish(
893 &mut self,
894 _block: &RecoveredBlock<N::Block>,
895 _output: Arc<BlockExecutionOutput<N::Receipt>>,
896 hashed_state: &LazyHashedPostState,
897 ) -> ProviderResult<StateRootJobOutcome> {
898 let provider = self.provider_builder.clone().build()?;
899 let (state_root, trie_updates) =
900 provider.state_root_with_updates(hashed_state.get().as_ref().clone())?;
901 Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates)))
902 }
903}
904
905#[derive(Debug)]
906struct SparseTrieStateRootJob<N: NodePrimitives, P> {
907 handle: StateRootHandle,
908 provider_builder: StateProviderBuilder<N, P>,
909 overlay_factory: OverlayStateProviderFactory<P, N>,
910 executor: reth_tasks::Runtime,
911 timeout: Option<Duration>,
912 compare_trie_updates: bool,
913 metrics: BlockValidationMetrics,
914}
915
916impl<N, P> SparseTrieStateRootJob<N, P>
917where
918 N: NodePrimitives,
919 P: StateProviderFactory + Clone + Send + Sync + 'static,
920 P: BlockReader + StateReader,
921 OverlayStateProviderFactory<P, N>: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
922 + Clone
923 + Send
924 + Sync
925 + 'static,
926{
927 fn serial_fallback(
928 executor: &reth_tasks::Runtime,
929 provider_builder: StateProviderBuilder<N, P>,
930 output: Arc<BlockExecutionOutput<N::Receipt>>,
931 ) -> ProviderResult<SerialFallbackRx> {
932 let provider = provider_builder.build()?;
933 let (fallback_tx, fallback_rx) = mpsc::channel();
934 executor.spawn_blocking_named("serial-root", move || {
935 let result = (|| {
936 let hashed_state = Arc::new(provider.hashed_post_state(&output.state));
937 let (root, updates) =
938 provider.state_root_with_updates(hashed_state.as_ref().clone())?;
939 Ok((root, updates, hashed_state))
940 })();
941 let _ = fallback_tx.send(result);
942 });
943
944 Ok(fallback_rx)
945 }
946
947 fn compute_serial(
952 &self,
953 output: &BlockExecutionOutput<N::Receipt>,
954 ) -> ProviderResult<StateRootJobOutcome> {
955 let provider = self.provider_builder.clone().build()?;
956 let hashed_state = Arc::new(provider.hashed_post_state(&output.state));
957 let (state_root, trie_updates) =
958 provider.state_root_with_updates(hashed_state.as_ref().clone())?;
959 self.metrics.state_root_task_fallback_success_total.increment(1);
960 Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
961 .with_hashed_state(Some(hashed_state)))
962 }
963
964 fn verified_sparse_outcome(
969 &self,
970 block: &RecoveredBlock<N::Block>,
971 output: &BlockExecutionOutput<N::Receipt>,
972 outcome: StateRootComputeOutcome,
973 ) -> ProviderResult<StateRootJobOutcome> {
974 let outcome = self.sparse_outcome(block, output, outcome);
975 if outcome.state_root == block.header().state_root() {
976 return Ok(outcome)
977 }
978 warn!(
979 target: "engine::tree::state_root_strategy",
980 state_root = ?outcome.state_root,
981 block_state_root = ?block.header().state_root(),
982 "State root task returned incorrect state root, recomputing serially"
983 );
984 self.compute_serial(output)
985 }
986
987 fn sparse_outcome(
988 &self,
989 _block: &RecoveredBlock<N::Block>,
990 output: &BlockExecutionOutput<N::Receipt>,
991 outcome: StateRootComputeOutcome,
992 ) -> StateRootJobOutcome {
993 let StateRootComputeOutcome {
994 state_root,
995 trie_updates,
996 changed_paths,
997 #[cfg(feature = "trie-debug")]
998 debug_recorders,
999 } = outcome;
1000
1001 if self.compare_trie_updates {
1002 let _has_diff = compare_trie_updates_with_serial(
1003 self.provider_builder.clone(),
1004 self.overlay_factory.clone(),
1005 output,
1006 trie_updates.as_ref().clone(),
1007 );
1008 #[cfg(feature = "trie-debug")]
1009 if _has_diff {
1010 write_trie_debug_recorders(_block.header().number(), &debug_recorders);
1011 }
1012 }
1013
1014 #[cfg(feature = "trie-debug")]
1015 if state_root != _block.header().state_root() {
1016 write_trie_debug_recorders(_block.header().number(), &debug_recorders);
1017 }
1018
1019 StateRootJobOutcome::new(state_root, trie_updates).with_changed_paths(changed_paths)
1020 }
1021}
1022
1023impl<N, P> StateRootJob<N> for SparseTrieStateRootJob<N, P>
1024where
1025 N: NodePrimitives,
1026 P: BlockReader + StateProviderFactory + StateReader + Clone + Send + Sync + 'static,
1027 OverlayStateProviderFactory<P, N>: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
1028 + Clone
1029 + Send
1030 + Sync
1031 + 'static,
1032{
1033 fn name(&self) -> &'static str {
1034 "sparse-trie"
1035 }
1036
1037 fn finish(
1038 &mut self,
1039 block: &RecoveredBlock<N::Block>,
1040 output: Arc<BlockExecutionOutput<N::Receipt>>,
1041 _hashed_state: &LazyHashedPostState,
1042 ) -> ProviderResult<StateRootJobOutcome> {
1043 if self.timeout.is_none() {
1044 return match self.handle.state_root() {
1045 Ok(outcome) => self.verified_sparse_outcome(block, &output, outcome),
1046 Err(err) => {
1047 debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1048 self.compute_serial(&output)
1049 }
1050 }
1051 }
1052
1053 let timeout = self.timeout.expect("checked above");
1054 let task_rx = self.handle.take_state_root_rx();
1055 let fallback_rx = match task_rx.recv_timeout(timeout) {
1056 Ok(Ok(outcome)) => return self.verified_sparse_outcome(block, &output, outcome),
1057 Ok(Err(err)) => {
1058 debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1059 Self::serial_fallback(
1060 &self.executor,
1061 self.provider_builder.clone(),
1062 output.clone(),
1063 )?
1064 }
1065 Err(RecvTimeoutError::Timeout) => {
1066 warn!(target: "engine::tree::state_root_strategy", ?timeout, "State root task timed out, racing serial fallback");
1067 self.metrics.state_root_task_timeout_total.increment(1);
1068 Self::serial_fallback(
1069 &self.executor,
1070 self.provider_builder.clone(),
1071 output.clone(),
1072 )?
1073 }
1074 Err(RecvTimeoutError::Disconnected) => {
1075 debug!(target: "engine::tree::state_root_strategy", "State root task dropped, falling back to serial root");
1076 Self::serial_fallback(
1077 &self.executor,
1078 self.provider_builder.clone(),
1079 output.clone(),
1080 )?
1081 }
1082 };
1083
1084 loop {
1085 if let Ok(Ok(outcome)) = task_rx.try_recv() {
1086 let outcome = self.sparse_outcome(block, &output, outcome);
1087 if outcome.state_root == block.header().state_root() {
1088 return Ok(outcome)
1089 }
1090 warn!(
1092 target: "engine::tree::state_root_strategy",
1093 state_root = ?outcome.state_root,
1094 block_state_root = ?block.header().state_root(),
1095 "State root task returned incorrect state root, using serial fallback"
1096 );
1097 }
1098
1099 match fallback_rx.try_recv() {
1100 Ok(Ok((state_root, trie_updates, hashed_state))) => {
1101 self.metrics.state_root_task_fallback_success_total.increment(1);
1102 return Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1103 .with_hashed_state(Some(hashed_state)))
1104 }
1105 Ok(Err(err)) => return Err(err),
1106 Err(mpsc::TryRecvError::Empty) => {}
1107 Err(mpsc::TryRecvError::Disconnected) => {
1108 return Err(ProviderError::other(std::io::Error::other(
1109 "serial state root fallback task dropped",
1110 )))
1111 }
1112 }
1113
1114 std::thread::sleep(Duration::from_millis(1));
1115 }
1116 }
1117}
1118
1119fn compare_trie_updates_with_serial<N, P>(
1120 state_provider_builder: StateProviderBuilder<N, P>,
1121 overlay_factory: OverlayStateProviderFactory<P, N>,
1122 output: &BlockExecutionOutput<N::Receipt>,
1123 task_trie_updates: TrieUpdates,
1124) -> bool
1125where
1126 N: NodePrimitives,
1127 P: BlockReader + StateProviderFactory + StateReader + Clone,
1128 OverlayStateProviderFactory<P, N>:
1129 DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>,
1130{
1131 debug!(target: "engine::tree::state_root_strategy", "Comparing trie updates with serial computation");
1132
1133 match state_provider_builder.build().and_then(|provider| {
1134 let hashed_state = provider.hashed_post_state(&output.state);
1135 provider.state_root_with_updates(hashed_state)
1136 }) {
1137 Ok((serial_root, serial_trie_updates)) => {
1138 debug!(
1139 target: "engine::tree::state_root_strategy",
1140 ?serial_root,
1141 "Serial state root computation finished for comparison"
1142 );
1143
1144 match overlay_factory.database_provider_ro() {
1145 Ok(provider) => match super::trie_updates::compare_trie_updates(
1146 &provider,
1147 task_trie_updates,
1148 serial_trie_updates,
1149 ) {
1150 Ok(has_diff) => return has_diff,
1151 Err(err) => {
1152 warn!(
1153 target: "engine::tree::state_root_strategy",
1154 %err,
1155 "Error comparing trie updates"
1156 );
1157 return true;
1158 }
1159 },
1160 Err(err) => {
1161 warn!(
1162 target: "engine::tree::state_root_strategy",
1163 %err,
1164 "Failed to get database provider for trie update comparison"
1165 );
1166 }
1167 }
1168 }
1169 Err(err) => {
1170 warn!(
1171 target: "engine::tree::state_root_strategy",
1172 %err,
1173 "Failed to compute serial state root for comparison"
1174 );
1175 }
1176 }
1177 false
1178}
1179
1180#[cfg(feature = "trie-debug")]
1184fn write_trie_debug_recorders(block_number: u64, recorders: &[(Option<B256>, TrieDebugRecorder)]) {
1185 let path = format!("trie_debug_block_{block_number}.json");
1186 match serde_json::to_string_pretty(recorders) {
1187 Ok(json) => match std::fs::write(&path, json) {
1188 Ok(()) => {
1189 warn!(
1190 target: "engine::tree::state_root_strategy",
1191 %path,
1192 "Wrote trie debug recorders to file"
1193 );
1194 }
1195 Err(err) => {
1196 warn!(
1197 target: "engine::tree::state_root_strategy",
1198 %err,
1199 %path,
1200 "Failed to write trie debug recorders"
1201 );
1202 }
1203 },
1204 Err(err) => {
1205 warn!(
1206 target: "engine::tree::state_root_strategy",
1207 %err,
1208 "Failed to serialize trie debug recorders"
1209 );
1210 }
1211 }
1212}
1213
1214#[cfg(test)]
1215mod tests {
1216 use super::*;
1217 use alloy_consensus::constants::KECCAK_EMPTY;
1218 use alloy_primitives::{map::HashMap, Address, U256};
1219 use rand::Rng;
1220 use reth_chain_state::{test_utils::TestBlockBuilder, StateTrieOverlayManager};
1221 use reth_chainspec::ChainSpec;
1222 use reth_db_common::init::init_genesis;
1223 use reth_ethereum_primitives::EthPrimitives;
1224 use reth_evm::OnStateHook;
1225 use reth_evm_ethereum::EthEvmConfig;
1226 use reth_primitives_traits::{Account, StorageEntry};
1227 use reth_provider::{
1228 providers::{BlockchainProvider, OverlayBuilder, OverlayStateProviderFactory},
1229 test_utils::create_test_provider_factory_with_chain_spec,
1230 HashingWriter,
1231 };
1232 use reth_testing_utils::generators;
1233 use reth_trie::{
1234 prefix_set::{PrefixSetMut, TriePrefixSetsMut},
1235 test_utils::state_root,
1236 LazyTrieData,
1237 };
1238 use reth_trie_common::Nibbles;
1239 use reth_trie_db::ChangesetCache;
1240 use revm::state::{AccountInfo, AccountStatus, EvmState, EvmStorageSlot, TransactionId};
1241
1242 fn with_changed_paths(
1243 block: ExecutedBlock<EthPrimitives>,
1244 changed_paths: TriePrefixSetsMut,
1245 ) -> ExecutedBlock<EthPrimitives> {
1246 let mut trie_data = block.trie_data();
1247 trie_data.changed_paths = Some(Arc::new(changed_paths));
1248 ExecutedBlock::with_deferred_trie_data(
1249 block.recovered_block,
1250 block.execution_output,
1251 LazyTrieData::ready(trie_data),
1252 )
1253 }
1254
1255 fn trie_changed_paths(account_path: u8, storage_path: u8) -> TriePrefixSetsMut {
1256 TriePrefixSetsMut {
1257 account_prefix_set: PrefixSetMut::from([Nibbles::from_nibbles([account_path])]),
1258 storage_prefix_sets: HashMap::from_iter([(
1259 B256::with_last_byte(account_path),
1260 PrefixSetMut::from([Nibbles::from_nibbles([storage_path])]),
1261 )]),
1262 destroyed_accounts: Default::default(),
1263 }
1264 }
1265
1266 #[test]
1267 fn sparse_trie_retained_paths_merges_prune_blocks_with_current_block() {
1268 let blocks: Vec<_> = TestBlockBuilder::eth()
1269 .get_executed_blocks(1..3)
1270 .zip([trie_changed_paths(0x01, 0x02), trie_changed_paths(0x03, 0x04)])
1271 .map(|(block, changed_paths)| with_changed_paths(block, changed_paths))
1272 .collect();
1273 let current_changed_paths = trie_changed_paths(0x05, 0x06);
1274
1275 let retained_paths =
1276 sparse_trie_retained_paths(blocks, ¤t_changed_paths).unwrap().freeze();
1277
1278 assert_eq!(retained_paths.account_prefix_set.len(), 3);
1279 assert_eq!(retained_paths.storage_prefix_sets.len(), 3);
1280 }
1281
1282 #[test]
1283 fn sparse_trie_retained_paths_skips_prune_when_changed_paths_missing() {
1284 let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..2).collect();
1285 let current_changed_paths = trie_changed_paths(0x01, 0x02);
1286
1287 assert!(sparse_trie_retained_paths(blocks, ¤t_changed_paths).is_none());
1288 }
1289
1290 fn create_mock_state_updates(num_accounts: usize, updates_per_account: usize) -> Vec<EvmState> {
1291 let mut rng = generators::rng();
1292 let all_addresses: Vec<Address> = (0..num_accounts).map(|_| rng.random()).collect();
1293 let mut updates = Vec::with_capacity(updates_per_account);
1294
1295 for _ in 0..updates_per_account {
1296 let num_accounts_in_update = rng.random_range(1..=num_accounts);
1297 let mut state_update = EvmState::default();
1298
1299 for &address in &all_addresses[0..num_accounts_in_update] {
1300 let mut storage = HashMap::default();
1301 if rng.random_bool(0.7) {
1302 for _ in 0..rng.random_range(1..10) {
1303 let slot = U256::from(rng.random::<u64>());
1304 storage.insert(
1305 slot,
1306 EvmStorageSlot::new_changed(
1307 U256::ZERO,
1308 U256::from(rng.random::<u64>()),
1309 TransactionId::ZERO,
1310 ),
1311 );
1312 }
1313 }
1314
1315 let mut account = revm::state::Account::default();
1316 account.info = AccountInfo {
1317 balance: U256::from(rng.random::<u64>()),
1318 nonce: rng.random::<u64>(),
1319 code_hash: KECCAK_EMPTY,
1320 code: Some(Default::default()),
1321 account_id: None,
1322 };
1323 account.storage = storage;
1324 account.status = AccountStatus::Touched;
1325 account.transaction_id = TransactionId::ZERO;
1326 state_update.insert(address, account);
1327 }
1328
1329 updates.push(state_update);
1330 }
1331
1332 updates
1333 }
1334
1335 #[test]
1336 fn state_root_task_matches_serial_root() {
1337 reth_tracing::init_test_tracing();
1338
1339 let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1340 let genesis_hash = init_genesis(&factory).unwrap();
1341 let state_updates = create_mock_state_updates(10, 10);
1342 let mut accumulated_state: HashMap<Address, (Account, HashMap<B256, U256>)> =
1343 HashMap::default();
1344
1345 {
1346 let provider_rw = factory.provider_rw().expect("failed to get provider");
1347 for update in &state_updates {
1348 let account_updates = update.iter().map(|(address, account)| {
1349 (*address, Some(Account::from_revm_account(account)))
1350 });
1351 provider_rw
1352 .insert_account_for_hashing(account_updates)
1353 .expect("failed to insert accounts");
1354
1355 let storage_updates = update.iter().map(|(address, account)| {
1356 let storage_entries = account.storage.iter().map(|(slot, value)| {
1357 StorageEntry { key: B256::from(*slot), value: value.present_value }
1358 });
1359 (*address, storage_entries)
1360 });
1361 provider_rw
1362 .insert_storage_for_hashing(storage_updates)
1363 .expect("failed to insert storage");
1364 }
1365 provider_rw.commit().expect("failed to commit changes");
1366 }
1367
1368 for update in &state_updates {
1369 for (address, account) in update {
1370 let storage: HashMap<B256, U256> = account
1371 .storage
1372 .iter()
1373 .map(|(key, value)| (B256::from(*key), value.present_value))
1374 .collect();
1375 let entry = accumulated_state.entry(*address).or_default();
1376 entry.0 = Account::from_revm_account(account);
1377 entry.1.extend(storage);
1378 }
1379 }
1380
1381 let provider_factory = BlockchainProvider::new(factory).unwrap();
1382 let env: ExecutionEnv<EthEvmConfig> = ExecutionEnv::test_default();
1383 let runtime = reth_tasks::Runtime::test();
1384 let state_trie_overlays = StateTrieOverlayManager::<EthPrimitives>::default();
1385 let mut state_root_handle = DefaultStateRootStrategy::default().spawn_state_root(
1386 &runtime,
1387 &state_trie_overlays,
1388 OverlayStateProviderFactory::new(
1389 provider_factory,
1390 OverlayBuilder::<EthPrimitives>::new(genesis_hash, ChangesetCache::new()),
1391 ),
1392 StateRootTaskOptions {
1393 parent_state_root: env.parent_state_root,
1394 transaction_count: Some(env.transaction_count),
1395 config: &TreeConfig::default(),
1396 pending_sparse_trie_prune_blocks: None,
1397 },
1398 );
1399
1400 let mut state_hook = state_root_handle.take_execution_hook();
1401 for update in state_updates {
1402 state_hook.on_state(update);
1403 }
1404 drop(state_hook);
1405
1406 let root_from_task = state_root_handle.state_root().expect("task failed").state_root;
1407 let root_from_regular = state_root(accumulated_state);
1408 assert_eq!(root_from_task, root_from_regular);
1409 }
1410}