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