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, trie_cursor::TrieCursorFactory, updates::TrieUpdates,
77 HashedPostState,
78};
79use reth_trie_parallel::proof_task::{ProofResultMessage, ProofTaskCtx, ProofWorkerHandle};
80pub use reth_trie_parallel::{
81 error::StateRootTaskError,
82 state_root_task::{
83 evm_state_to_hashed_post_state, PayloadStateRootHandle, StateAccessHint,
84 StateRootComputeOutcome, StateRootHandle, StateRootHintStream, StateRootMessage,
85 StateRootSink, StateRootTaskCancelGuard, StateRootUpdateHook, StateRootUpdateStream,
86 },
87};
88use reth_trie_sparse::{
89 ArenaParallelSparseTrie, RevealableSparseTrie, SparseStateTrie, TrieNodeEpoch,
90};
91use std::{
92 fmt,
93 sync::{
94 mpsc::{self, RecvTimeoutError},
95 Arc,
96 },
97 time::Duration,
98};
99use tracing::{debug, debug_span, instrument, warn, Span};
100
101pub type LazyHashedPostState = reth_tasks::LazyHandle<Arc<HashedPostState>>;
103
104pub trait StateRootStrategy<N, P, Evm>: Send + Sync
106where
107 N: NodePrimitives,
108 Evm: ConfigureEvm<Primitives = N>,
109{
110 fn prepare(
115 &self,
116 ctx: StateRootJobContext<'_, N, P, Evm>,
117 ) -> ProviderResult<PreparedStateRootJob<N>>;
118
119 fn prepare_payload_builder(
126 &self,
127 _ctx: PayloadStateRootJobContext<'_, N, P>,
128 ) -> ProviderResult<Option<PayloadStateRootHandle>> {
129 Ok(None)
130 }
131}
132
133pub struct PayloadStateRootJobContext<'a, N, P>
135where
136 N: NodePrimitives,
137{
138 executor: &'a reth_tasks::Runtime,
139 overlay_manager: &'a OverlayManager<N>,
140 parent_hash: B256,
141 parent_header: &'a N::BlockHeader,
142 timestamp: u64,
143 state: &'a mut EngineApiTreeState<N>,
144 state_provider_factory: OverlayStateProviderFactory<P, N>,
145 config: &'a TreeConfig,
146}
147
148impl<N, P> fmt::Debug for PayloadStateRootJobContext<'_, N, P>
149where
150 N: NodePrimitives,
151{
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 f.debug_struct("PayloadStateRootJobContext")
154 .field("parent_hash", &self.parent_hash)
155 .field("parent_state_root", &self.parent_state_root())
156 .field("timestamp", &self.timestamp)
157 .field("pending_sparse_trie_prune", &self.state.pending_sparse_trie_prune())
158 .finish_non_exhaustive()
159 }
160}
161
162impl<'a, N, P> PayloadStateRootJobContext<'a, N, P>
163where
164 N: NodePrimitives,
165{
166 #[expect(clippy::too_many_arguments)]
168 pub(crate) const fn new(
169 executor: &'a reth_tasks::Runtime,
170 overlay_manager: &'a OverlayManager<N>,
171 parent_hash: B256,
172 parent_header: &'a N::BlockHeader,
173 timestamp: u64,
174 state: &'a mut EngineApiTreeState<N>,
175 state_provider_factory: OverlayStateProviderFactory<P, N>,
176 config: &'a TreeConfig,
177 ) -> Self {
178 Self {
179 executor,
180 overlay_manager,
181 parent_hash,
182 parent_header,
183 timestamp,
184 state,
185 state_provider_factory,
186 config,
187 }
188 }
189
190 pub const fn parent_hash(&self) -> B256 {
192 self.parent_hash
193 }
194
195 pub const fn parent_header(&self) -> &N::BlockHeader {
200 self.parent_header
201 }
202
203 pub fn parent_state_root(&self) -> B256 {
205 self.parent_header.state_root()
206 }
207
208 pub const fn timestamp(&self) -> u64 {
212 self.timestamp
213 }
214
215 pub const fn executor(&self) -> &reth_tasks::Runtime {
217 self.executor
218 }
219
220 pub fn take_sparse_trie_prune_blocks(&mut self) -> Option<Vec<ExecutedBlock<N>>> {
225 self.state.take_sparse_trie_prune_blocks(self.parent_hash)
226 }
227}
228
229pub struct StateRootJobContext<'a, N, P, Evm>
231where
232 N: NodePrimitives,
233 Evm: ConfigureEvm<Primitives = N>,
234{
235 executor: &'a reth_tasks::Runtime,
236 overlay_manager: &'a OverlayManager<N>,
237 env: &'a ExecutionEnv<Evm>,
238 parent_header: &'a SealedHeader<N::BlockHeader>,
239 state_provider_factory: OverlayStateProviderFactory<P, N>,
240 config: &'a TreeConfig,
241 parallel_bal_execution: bool,
242 state: &'a mut EngineApiTreeState<N>,
243}
244
245impl<N, P, Evm> fmt::Debug for StateRootJobContext<'_, N, P, Evm>
246where
247 N: NodePrimitives,
248 Evm: ConfigureEvm<Primitives = N>,
249{
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 f.debug_struct("StateRootJobContext")
252 .field("parallel_bal_execution", &self.parallel_bal_execution)
253 .field("has_pending_sparse_trie_prune", &self.state.pending_sparse_trie_prune())
254 .finish_non_exhaustive()
255 }
256}
257
258impl<'a, N, P, Evm> StateRootJobContext<'a, N, P, Evm>
259where
260 N: NodePrimitives,
261 Evm: ConfigureEvm<Primitives = N>,
262{
263 #[expect(clippy::too_many_arguments)]
265 pub(crate) const fn new(
266 executor: &'a reth_tasks::Runtime,
267 overlay_manager: &'a OverlayManager<N>,
268 env: &'a ExecutionEnv<Evm>,
269 parent_header: &'a SealedHeader<N::BlockHeader>,
270 state_provider_factory: OverlayStateProviderFactory<P, N>,
271 config: &'a TreeConfig,
272 parallel_bal_execution: bool,
273 state: &'a mut EngineApiTreeState<N>,
274 ) -> Self {
275 Self {
276 executor,
277 overlay_manager,
278 env,
279 parent_header,
280 state_provider_factory,
281 config,
282 parallel_bal_execution,
283 state,
284 }
285 }
286
287 pub const fn env(&self) -> &ExecutionEnv<Evm> {
289 self.env
290 }
291
292 pub const fn parent_header(&self) -> &SealedHeader<N::BlockHeader> {
294 self.parent_header
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 take_sparse_trie_prune_blocks(&mut self) -> Option<Vec<ExecutedBlock<N>>> {
312 self.state.take_sparse_trie_prune_blocks(self.env.parent_hash)
313 }
314}
315
316pub struct PreparedStateRootJob<N: NodePrimitives> {
323 job: Box<dyn StateRootJob<N>>,
324 execution_hook: Option<StateRootUpdateHook>,
325 hint_stream: Option<StateRootHintStream>,
326 hashed_update_stream: Option<StateRootUpdateStream>,
327 hashed_state_rx: Option<mpsc::Receiver<Arc<HashedPostState>>>,
328}
329
330impl<N: NodePrimitives> fmt::Debug for PreparedStateRootJob<N> {
331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332 f.debug_struct("PreparedStateRootJob")
333 .field("name", &self.job.name())
334 .field("has_execution_hook", &self.execution_hook.is_some())
335 .field("has_hint_stream", &self.hint_stream.is_some())
336 .field("has_hashed_update_stream", &self.hashed_update_stream.is_some())
337 .field("has_hashed_state_rx", &self.hashed_state_rx.is_some())
338 .finish()
339 }
340}
341
342impl<N: NodePrimitives> PreparedStateRootJob<N> {
343 pub const fn new(
345 job: Box<dyn StateRootJob<N>>,
346 hashed_state_rx: Option<mpsc::Receiver<Arc<HashedPostState>>>,
347 ) -> Self {
348 Self {
349 job,
350 execution_hook: None,
351 hint_stream: None,
352 hashed_update_stream: None,
353 hashed_state_rx,
354 }
355 }
356
357 pub fn with_execution_hook(mut self, hook: StateRootUpdateHook) -> Self {
359 self.execution_hook = Some(hook);
360 self
361 }
362
363 pub fn with_hint_stream(mut self, hint_stream: StateRootHintStream) -> Self {
365 self.hint_stream = Some(hint_stream);
366 self
367 }
368
369 pub fn with_hashed_update_stream(mut self, stream: StateRootUpdateStream) -> Self {
371 self.hashed_update_stream = Some(stream);
372 self
373 }
374
375 pub fn name(&self) -> &'static str {
377 self.job.name()
378 }
379
380 pub fn take_execution_hook(&mut self) -> Option<Box<dyn OnStateHook + 'static>> {
382 self.execution_hook.take().map(|hook| Box::new(hook) as Box<dyn OnStateHook + 'static>)
383 }
384
385 pub const fn take_hint_stream(&mut self) -> Option<StateRootHintStream> {
387 self.hint_stream.take()
388 }
389
390 pub const fn take_hashed_update_stream(&mut self) -> Option<StateRootUpdateStream> {
392 self.hashed_update_stream.take()
393 }
394
395 pub const fn take_hashed_state_rx(&mut self) -> Option<mpsc::Receiver<Arc<HashedPostState>>> {
401 self.hashed_state_rx.take()
402 }
403
404 pub fn finish(
406 &mut self,
407 block: &RecoveredBlock<N::Block>,
408 output: Arc<BlockExecutionOutput<N::Receipt>>,
409 hashed_state: &LazyHashedPostState,
410 ) -> ProviderResult<StateRootJobOutcome> {
411 self.job.finish(block, output, hashed_state)
412 }
413}
414
415pub trait StateRootJob<N: NodePrimitives>: Send {
417 fn name(&self) -> &'static str;
419
420 fn finish(
424 &mut self,
425 block: &RecoveredBlock<N::Block>,
426 output: Arc<BlockExecutionOutput<N::Receipt>>,
427 hashed_state: &LazyHashedPostState,
428 ) -> ProviderResult<StateRootJobOutcome>;
429}
430
431#[derive(Debug)]
433pub struct StateRootJobOutcome {
434 pub state_root: B256,
436 pub trie_updates: Arc<TrieUpdates>,
438 pub hashed_state: Option<Arc<HashedPostState>>,
443}
444
445impl StateRootJobOutcome {
446 pub const fn new(state_root: B256, trie_updates: Arc<TrieUpdates>) -> Self {
448 Self { state_root, trie_updates, hashed_state: None }
449 }
450
451 pub fn with_hashed_state(mut self, hashed_state: Option<Arc<HashedPostState>>) -> Self {
453 self.hashed_state = hashed_state;
454 self
455 }
456}
457
458type SerialFallbackRx = mpsc::Receiver<ProviderResult<(B256, TrieUpdates, Arc<HashedPostState>)>>;
461
462#[derive(Default)]
470pub struct DefaultStateRootStrategy {
471 metrics: SparseTrieTaskMetrics,
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 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
491 fn spawn_state_root<N, F>(
492 &self,
493 executor: &reth_tasks::Runtime,
494 overlay_manager: &OverlayManager<N>,
495 multiproof_provider_factory: F,
496 options: StateRootTaskOptions<'_, N>,
497 ) -> StateRootHandle
498 where
499 N: NodePrimitives,
500 F: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
501 + Clone
502 + Send
503 + Sync
504 + 'static,
505 {
506 let StateRootTaskOptions {
507 parent_header,
508 preserved_sparse_trie,
509 transaction_count,
510 config,
511 pending_sparse_trie_prune_blocks,
512 } = options;
513 let (updates_tx, from_multi_proof) = crossbeam_channel::unbounded();
514 let (cancel_guard, cancel_rx) = StateRootTaskCancelGuard::channel();
515 let (proof_result_tx, proof_result_rx) =
516 crossbeam_channel::unbounded::<ProofResultMessage>();
517
518 let task_ctx = ProofTaskCtx::new(multiproof_provider_factory);
519 #[cfg(feature = "trie-debug")]
520 let task_ctx = task_ctx.with_proof_jitter(config.proof_jitter());
521 let halve_workers = transaction_count
522 .is_some_and(|count| count <= Self::SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD);
523 let proof_handle =
524 ProofWorkerHandle::new(executor, task_ctx, halve_workers, proof_result_tx.clone());
525
526 let (state_root_tx, state_root_rx) = mpsc::channel();
527 let (hashed_state_tx, hashed_state_rx) = mpsc::channel();
528 let parent_state_root = parent_header.state_root();
529
530 self.spawn_sparse_trie_task(
531 executor,
532 overlay_manager,
533 proof_handle,
534 proof_result_tx,
535 proof_result_rx,
536 state_root_tx,
537 hashed_state_tx,
538 from_multi_proof,
539 cancel_rx,
540 SparseTrieTaskOptions {
541 parent_header,
542 preserved_sparse_trie,
543 chunk_size: config.multiproof_chunk_size(),
544 pending_sparse_trie_prune_blocks: if config.disable_sparse_trie_cache_pruning() {
545 None
546 } else {
547 pending_sparse_trie_prune_blocks
548 },
549 },
550 );
551
552 StateRootHandle::new(
553 parent_state_root,
554 updates_tx,
555 cancel_guard,
556 state_root_rx,
557 hashed_state_rx,
558 )
559 }
560
561 #[expect(clippy::too_many_arguments)]
563 fn spawn_sparse_trie_task<N: NodePrimitives>(
564 &self,
565 executor: &reth_tasks::Runtime,
566 overlay_manager: &OverlayManager<N>,
567 proof_worker_handle: ProofWorkerHandle,
568 proof_result_tx: CrossbeamSender<ProofResultMessage>,
569 proof_result_rx: CrossbeamReceiver<ProofResultMessage>,
570 state_root_tx: mpsc::Sender<Result<StateRootComputeOutcome, StateRootTaskError>>,
571 hashed_state_tx: mpsc::Sender<Arc<HashedPostState>>,
572 from_multi_proof: CrossbeamReceiver<StateRootMessage>,
573 cancel_rx: CrossbeamReceiver<()>,
574 options: SparseTrieTaskOptions<N>,
575 ) {
576 let SparseTrieTaskOptions {
577 parent_header,
578 preserved_sparse_trie,
579 chunk_size,
580 pending_sparse_trie_prune_blocks,
581 } = options;
582 let overlay_manager = overlay_manager.clone();
583 let trie_metrics = self.metrics.clone();
584 let executor = executor.clone();
585
586 let parent_span = Span::current();
587 executor.clone().spawn_blocking_named("sparse-trie", move || {
588 reth_tasks::once!(increase_thread_priority);
589
590 let parent_hash = parent_header.hash();
591 let parent_state_root = parent_header.state_root();
592 let new_epoch = TrieNodeEpoch::new(parent_header.number().saturating_add(1));
593 let prune_before =
594 sparse_trie_prune_before(pending_sparse_trie_prune_blocks.as_deref(), new_epoch);
595
596 let _enter = debug_span!(
597 target: "engine::tree::payload_processor",
598 parent: parent_span,
599 "sparse_trie_task"
600 )
601 .entered();
602
603 let new_sparse_state_trie = || {
604 debug!(
605 target: "engine::tree::payload_processor",
606 "Creating new sparse trie - no preserved trie available"
607 );
608 let default_trie =
609 RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
610 SparseStateTrie::default()
611 .with_accounts_trie(default_trie.clone())
612 .with_default_storage_trie(default_trie)
613 .with_updates(true)
614 };
615
616 let mut sparse_trie_anchor_hash = parent_hash;
617 let mut reused_preserved_sparse_trie = false;
618 let sparse_state_trie = match preserved_sparse_trie {
619 Some(preserved) => {
620 let start = Instant::now();
621 let preserved_anchor_hash = preserved.anchor_hash();
622 let preserved = preserved.into_trie_for(parent_state_root);
623 trie_metrics
624 .sparse_trie_cache_wait_duration_histogram
625 .record(start.elapsed().as_secs_f64());
626
627 match preserved {
628 Ok(Some(trie)) => {
629 sparse_trie_anchor_hash = preserved_anchor_hash;
630 reused_preserved_sparse_trie = true;
631 trie
632 }
633 Ok(None) => new_sparse_state_trie(),
634 Err(err) => {
635 let _ =
636 state_root_tx.send(Err(StateRootTaskError::Other(err.to_string())));
637 return;
638 }
639 }
640 }
641 None => new_sparse_state_trie(),
642 };
643 let mut task = SparseTrieCacheTask::new_with_trie(
644 &executor,
645 from_multi_proof,
646 cancel_rx,
647 hashed_state_tx,
648 proof_worker_handle,
649 proof_result_tx,
650 proof_result_rx,
651 trie_metrics.clone(),
652 sparse_state_trie,
653 parent_state_root,
654 new_epoch,
655 chunk_size,
656 );
657
658 let result = task.run();
659 let task_result = result.as_ref().ok().cloned();
660
661 let pending_trie = if let Some(result) = &task_result {
664 let preserved_anchor_hash = published_sparse_trie_anchor_hash(
665 sparse_trie_anchor_hash,
666 reused_preserved_sparse_trie,
667 pending_sparse_trie_prune_blocks.as_deref(),
668 );
669 let (preserved, completer) =
670 PreservedSparseTrie::pending(result.state_root, preserved_anchor_hash);
671 overlay_manager.store_sparse_trie(preserved);
672 Some(completer)
673 } else {
674 overlay_manager.clear_sparse_trie();
675 None
676 };
677
678 if state_root_tx.send(result).is_err() {
679 debug!(
685 target: "engine::tree::payload_processor",
686 "State root receiver dropped, dropping trie"
687 );
688 let (trie, deferred) = task.into_cleared_trie();
689 overlay_manager.clear_sparse_trie();
690 executor.spawn_drop(trie);
691 executor.spawn_drop(deferred);
692 return;
693 }
694
695 let _enter =
696 debug_span!(target: "engine::tree::payload_processor", "preserve").entered();
697 let mut trie_to_drop = None;
698 let deferred = if task_result.is_some() {
699 let pending_trie =
700 pending_trie.expect("pending trie is created for successful task result");
701 let start = Instant::now();
702 let (mut trie, deferred) = task.into_trie_for_reuse();
703 if let Some(prune_before) = prune_before {
704 let prune_start = Instant::now();
705 trie.prune(prune_before);
706 trie_metrics
707 .sparse_trie_prune_duration_histogram
708 .record(prune_start.elapsed().as_secs_f64());
709 }
710 trie_metrics
711 .into_trie_for_reuse_duration_histogram
712 .record(start.elapsed().as_secs_f64());
713 trie_metrics
714 .sparse_trie_retained_storage_tries
715 .set(trie.retained_storage_tries_count() as f64);
716 if let Err(trie) = pending_trie.complete(trie) {
717 trie_to_drop = Some(trie);
718 }
719 deferred
720 } else {
721 debug!(
722 target: "engine::tree::payload_processor",
723 "State root computation failed, dropping trie"
724 );
725 let (trie, deferred) = task.into_cleared_trie();
726 trie_to_drop = Some(trie);
727 deferred
728 };
729 if let Some(trie) = trie_to_drop {
730 executor.spawn_drop(trie);
731 }
732 executor.spawn_drop(deferred);
733 });
734 }
735}
736
737struct SparseTrieTaskOptions<N: NodePrimitives> {
738 parent_header: SealedHeader<N::BlockHeader>,
739 preserved_sparse_trie: Option<PreservedSparseTrie>,
740 chunk_size: usize,
741 pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
743}
744
745struct StateRootTaskOptions<'a, N: NodePrimitives> {
746 parent_header: SealedHeader<N::BlockHeader>,
747 preserved_sparse_trie: Option<PreservedSparseTrie>,
748 transaction_count: Option<usize>,
749 config: &'a TreeConfig,
750 pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
751}
752
753fn sparse_trie_prune_before<N: NodePrimitives>(
754 pending_sparse_trie_prune_blocks: Option<&[ExecutedBlock<N>]>,
755 new_epoch: TrieNodeEpoch,
756) -> Option<TrieNodeEpoch> {
757 match pending_sparse_trie_prune_blocks {
760 None => None,
761 Some([]) => Some(new_epoch),
762 Some([.., oldest]) => Some(TrieNodeEpoch::new(oldest.recovered_block().number())),
763 }
764}
765
766fn published_sparse_trie_anchor_hash<N: NodePrimitives>(
767 sparse_trie_anchor_hash: B256,
768 reused_preserved_sparse_trie: bool,
769 pending_sparse_trie_prune_blocks: Option<&[ExecutedBlock<N>]>,
770) -> B256 {
771 if !reused_preserved_sparse_trie {
772 return sparse_trie_anchor_hash
773 }
774
775 let Some(prune_blocks) = pending_sparse_trie_prune_blocks else {
776 return sparse_trie_anchor_hash
777 };
778 let Some(oldest_prune_block) = prune_blocks.last() else { return sparse_trie_anchor_hash };
779
780 if prune_blocks.iter().any(|block| block.recovered_block().hash() == sparse_trie_anchor_hash) {
786 return sparse_trie_anchor_hash
787 }
788
789 oldest_prune_block.recovered_block().parent_hash()
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.state_root() == env.parent_state_root)
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 = self.spawn_state_root(
854 executor,
855 overlay_manager,
856 proof_state_provider_factory,
857 StateRootTaskOptions {
858 parent_header: parent_header.clone(),
859 preserved_sparse_trie,
860 transaction_count: Some(env.transaction_count),
861 config,
862 pending_sparse_trie_prune_blocks,
863 },
864 );
865
866 let (hashed_update_stream, execution_hook): (
870 Option<StateRootUpdateStream>,
871 Option<StateRootUpdateHook>,
872 ) = match parallel_bal_execution {
873 true => (Some(handle.take_hashed_update_stream()), None),
874 false => (None, Some(handle.take_execution_hook())),
875 };
876 let hint_stream = handle.take_hint_stream();
877
878 let hashed_state_rx = Some(handle.take_hashed_state_rx());
879
880 let mut prepared = PreparedStateRootJob::new(
881 Box::new(SparseTrieStateRootJob {
882 handle,
883 state_provider_factory,
884 executor: executor.clone(),
885 timeout: config.state_root_task_timeout(),
886 compare_trie_updates: config.always_compare_trie_updates(),
887 metrics: BlockValidationMetrics::default(),
888 }),
889 hashed_state_rx,
890 )
891 .with_hint_stream(hint_stream);
892 if let Some(hook) = execution_hook {
893 prepared = prepared.with_execution_hook(hook);
894 }
895 if let Some(stream) = hashed_update_stream {
896 prepared = prepared.with_hashed_update_stream(stream);
897 }
898 Ok(prepared)
899 }
900
901 fn prepare_payload_builder(
902 &self,
903 mut ctx: PayloadStateRootJobContext<'_, N, P>,
904 ) -> ProviderResult<Option<PayloadStateRootHandle>> {
905 if !ctx.config.share_sparse_trie_with_payload_builder() ||
908 ctx.config.skip_state_root() ||
909 !ctx.config.has_enough_parallelism()
910 {
911 return Ok(None)
912 }
913
914 let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
915 let parent_state_root = ctx.parent_state_root();
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.state_root() == parent_state_root)
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 Ok(Some(
928 self.spawn_state_root(
929 ctx.executor,
930 ctx.overlay_manager,
931 proof_state_provider_factory,
932 StateRootTaskOptions {
933 parent_header,
934 preserved_sparse_trie,
935 transaction_count: None,
937 config: ctx.config,
938 pending_sparse_trie_prune_blocks,
939 },
940 )
941 .into_payload_state_root_handle(),
942 ))
943 }
944}
945
946#[derive(Debug)]
947struct SkippedStateRootJob {}
948
949impl<N: NodePrimitives> StateRootJob<N> for SkippedStateRootJob {
950 fn name(&self) -> &'static str {
951 "skipped"
952 }
953
954 fn finish(
955 &mut self,
956 block: &RecoveredBlock<N::Block>,
957 _output: Arc<BlockExecutionOutput<N::Receipt>>,
958 _hashed_state: &LazyHashedPostState,
959 ) -> ProviderResult<StateRootJobOutcome> {
960 Ok(StateRootJobOutcome::new(block.header().state_root(), Arc::new(TrieUpdates::default())))
961 }
962}
963
964#[derive(Debug)]
965struct SynchronousStateRootJob<N: NodePrimitives, P> {
966 state_provider_factory: OverlayStateProviderFactory<P, N>,
967}
968
969impl<N, P> StateRootJob<N> for SynchronousStateRootJob<N, P>
970where
971 N: NodePrimitives,
972 P: DatabaseProviderFactory + Clone + 'static,
973 P::Provider: BlockNumReader
974 + PruneCheckpointReader
975 + StageCheckpointReader
976 + ChangeSetReader
977 + StorageChangeSetReader
978 + StorageSettingsCache
979 + 'static,
980 OverlayStateProviderFactory<P, N>: DatabaseProviderROFactory<Provider: StateRootProvider>,
981{
982 fn name(&self) -> &'static str {
983 "synchronous"
984 }
985
986 fn finish(
987 &mut self,
988 _block: &RecoveredBlock<N::Block>,
989 _output: Arc<BlockExecutionOutput<N::Receipt>>,
990 hashed_state: &LazyHashedPostState,
991 ) -> ProviderResult<StateRootJobOutcome> {
992 let provider = self.state_provider_factory.database_provider_ro()?;
993 let (state_root, trie_updates) =
994 provider.state_root_with_updates(hashed_state.get().as_ref().clone())?;
995 Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates)))
996 }
997}
998
999#[derive(Debug)]
1000struct SparseTrieStateRootJob<N: NodePrimitives, P> {
1001 handle: StateRootHandle,
1002 state_provider_factory: OverlayStateProviderFactory<P, N>,
1003 executor: reth_tasks::Runtime,
1004 timeout: Option<Duration>,
1005 compare_trie_updates: bool,
1006 metrics: BlockValidationMetrics,
1007}
1008
1009impl<N, P> SparseTrieStateRootJob<N, P>
1010where
1011 N: NodePrimitives,
1012 P: DatabaseProviderFactory + Clone + 'static,
1013 P::Provider: BlockNumReader
1014 + PruneCheckpointReader
1015 + StageCheckpointReader
1016 + ChangeSetReader
1017 + StorageChangeSetReader
1018 + StorageSettingsCache
1019 + 'static,
1020 OverlayStateProviderFactory<P, N>: DatabaseProviderROFactory<
1021 Provider: TrieCursorFactory
1022 + HashedCursorFactory
1023 + HashedPostStateProvider
1024 + StateRootProvider
1025 + Send,
1026 > + Clone
1027 + 'static,
1028{
1029 fn serial_fallback(
1030 executor: &reth_tasks::Runtime,
1031 state_provider_factory: OverlayStateProviderFactory<P, N>,
1032 output: Arc<BlockExecutionOutput<N::Receipt>>,
1033 ) -> ProviderResult<SerialFallbackRx> {
1034 let provider = state_provider_factory.database_provider_ro()?;
1035 let (fallback_tx, fallback_rx) = mpsc::channel();
1036 executor.spawn_blocking_named("serial-root", move || {
1037 let result = (|| {
1038 let hashed_state = Arc::new(provider.hashed_post_state(&output.state)?);
1039 let (root, updates) =
1040 provider.state_root_with_updates(hashed_state.as_ref().clone())?;
1041 Ok((root, updates, hashed_state))
1042 })();
1043 let _ = fallback_tx.send(result);
1044 });
1045
1046 Ok(fallback_rx)
1047 }
1048
1049 fn compute_serial(
1054 &self,
1055 output: &BlockExecutionOutput<N::Receipt>,
1056 ) -> ProviderResult<StateRootJobOutcome> {
1057 let provider = self.state_provider_factory.database_provider_ro()?;
1058 let hashed_state = Arc::new(provider.hashed_post_state(&output.state)?);
1059 let (state_root, trie_updates) =
1060 provider.state_root_with_updates(hashed_state.as_ref().clone())?;
1061 self.metrics.state_root_task_fallback_success_total.increment(1);
1062 Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1063 .with_hashed_state(Some(hashed_state)))
1064 }
1065
1066 fn verified_sparse_outcome(
1071 &self,
1072 block: &RecoveredBlock<N::Block>,
1073 output: &BlockExecutionOutput<N::Receipt>,
1074 outcome: StateRootComputeOutcome,
1075 ) -> ProviderResult<StateRootJobOutcome> {
1076 let outcome = self.sparse_outcome(block, output, outcome);
1077 if outcome.state_root == block.header().state_root() {
1078 return Ok(outcome)
1079 }
1080 warn!(
1081 target: "engine::tree::state_root_strategy",
1082 state_root = ?outcome.state_root,
1083 block_state_root = ?block.header().state_root(),
1084 "State root task returned incorrect state root, recomputing serially"
1085 );
1086 self.compute_serial(output)
1087 }
1088
1089 fn sparse_outcome(
1090 &self,
1091 _block: &RecoveredBlock<N::Block>,
1092 output: &BlockExecutionOutput<N::Receipt>,
1093 outcome: StateRootComputeOutcome,
1094 ) -> StateRootJobOutcome {
1095 let StateRootComputeOutcome { state_root, trie_updates, hashed_state: _hashed_state } =
1096 outcome;
1097
1098 if self.compare_trie_updates {
1099 compare_trie_updates_with_serial(
1100 self.state_provider_factory.clone(),
1101 output,
1102 trie_updates.as_ref().clone(),
1103 );
1104 }
1105
1106 StateRootJobOutcome::new(state_root, trie_updates)
1107 }
1108}
1109
1110impl<N, P> StateRootJob<N> for SparseTrieStateRootJob<N, P>
1111where
1112 N: NodePrimitives,
1113 P: DatabaseProviderFactory + Clone + 'static,
1114 P::Provider: BlockNumReader
1115 + PruneCheckpointReader
1116 + StageCheckpointReader
1117 + ChangeSetReader
1118 + StorageChangeSetReader
1119 + StorageSettingsCache
1120 + 'static,
1121 OverlayStateProviderFactory<P, N>: DatabaseProviderROFactory<
1122 Provider: TrieCursorFactory
1123 + HashedCursorFactory
1124 + HashedPostStateProvider
1125 + StateRootProvider
1126 + Send,
1127 > + Clone
1128 + 'static,
1129{
1130 fn name(&self) -> &'static str {
1131 "sparse-trie"
1132 }
1133
1134 fn finish(
1135 &mut self,
1136 block: &RecoveredBlock<N::Block>,
1137 output: Arc<BlockExecutionOutput<N::Receipt>>,
1138 _hashed_state: &LazyHashedPostState,
1139 ) -> ProviderResult<StateRootJobOutcome> {
1140 if self.timeout.is_none() {
1141 return match self.handle.state_root() {
1142 Ok(outcome) => self.verified_sparse_outcome(block, &output, outcome),
1143 Err(err) => {
1144 debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1145 self.compute_serial(&output)
1146 }
1147 }
1148 }
1149
1150 let timeout = self.timeout.expect("checked above");
1151 let task_rx = self.handle.take_state_root_rx();
1152 let fallback_rx = match task_rx.recv_timeout(timeout) {
1153 Ok(Ok(outcome)) => return self.verified_sparse_outcome(block, &output, outcome),
1154 Ok(Err(err)) => {
1155 debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1156 Self::serial_fallback(
1157 &self.executor,
1158 self.state_provider_factory.clone(),
1159 output.clone(),
1160 )?
1161 }
1162 Err(RecvTimeoutError::Timeout) => {
1163 warn!(target: "engine::tree::state_root_strategy", ?timeout, "State root task timed out, racing serial fallback");
1164 self.metrics.state_root_task_timeout_total.increment(1);
1165 Self::serial_fallback(
1166 &self.executor,
1167 self.state_provider_factory.clone(),
1168 output.clone(),
1169 )?
1170 }
1171 Err(RecvTimeoutError::Disconnected) => {
1172 debug!(target: "engine::tree::state_root_strategy", "State root task dropped, falling back to serial root");
1173 Self::serial_fallback(
1174 &self.executor,
1175 self.state_provider_factory.clone(),
1176 output.clone(),
1177 )?
1178 }
1179 };
1180
1181 loop {
1182 if let Ok(Ok(outcome)) = task_rx.try_recv() {
1183 let outcome = self.sparse_outcome(block, &output, outcome);
1184 if outcome.state_root == block.header().state_root() {
1185 return Ok(outcome)
1186 }
1187 warn!(
1189 target: "engine::tree::state_root_strategy",
1190 state_root = ?outcome.state_root,
1191 block_state_root = ?block.header().state_root(),
1192 "State root task returned incorrect state root, using serial fallback"
1193 );
1194 }
1195
1196 match fallback_rx.try_recv() {
1197 Ok(Ok((state_root, trie_updates, hashed_state))) => {
1198 self.metrics.state_root_task_fallback_success_total.increment(1);
1199 return Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1200 .with_hashed_state(Some(hashed_state)))
1201 }
1202 Ok(Err(err)) => return Err(err),
1203 Err(mpsc::TryRecvError::Empty) => {}
1204 Err(mpsc::TryRecvError::Disconnected) => {
1205 return Err(ProviderError::other(std::io::Error::other(
1206 "serial state root fallback task dropped",
1207 )))
1208 }
1209 }
1210
1211 std::thread::sleep(Duration::from_millis(1));
1212 }
1213 }
1214}
1215
1216fn compare_trie_updates_with_serial<N, P>(
1217 state_provider_factory: OverlayStateProviderFactory<P, N>,
1218 output: &BlockExecutionOutput<N::Receipt>,
1219 task_trie_updates: TrieUpdates,
1220) -> bool
1221where
1222 N: NodePrimitives,
1223 P: DatabaseProviderFactory,
1224 P::Provider: BlockNumReader
1225 + PruneCheckpointReader
1226 + StageCheckpointReader
1227 + ChangeSetReader
1228 + StorageChangeSetReader
1229 + StorageSettingsCache
1230 + 'static,
1231 OverlayStateProviderFactory<P, N>: DatabaseProviderROFactory<
1232 Provider: TrieCursorFactory
1233 + HashedCursorFactory
1234 + HashedPostStateProvider
1235 + StateRootProvider,
1236 >,
1237{
1238 debug!(target: "engine::tree::state_root_strategy", "Comparing trie updates with serial computation");
1239
1240 match state_provider_factory.database_provider_ro().and_then(|provider| {
1241 let hashed_state = provider.hashed_post_state(&output.state)?;
1242 provider.state_root_with_updates(hashed_state)
1243 }) {
1244 Ok((serial_root, serial_trie_updates)) => {
1245 debug!(
1246 target: "engine::tree::state_root_strategy",
1247 ?serial_root,
1248 "Serial state root computation finished for comparison"
1249 );
1250
1251 match state_provider_factory.database_provider_ro() {
1252 Ok(provider) => match super::trie_updates::compare_trie_updates(
1253 &provider,
1254 task_trie_updates,
1255 serial_trie_updates,
1256 ) {
1257 Ok(has_diff) => return has_diff,
1258 Err(err) => {
1259 warn!(
1260 target: "engine::tree::state_root_strategy",
1261 %err,
1262 "Error comparing trie updates"
1263 );
1264 return true;
1265 }
1266 },
1267 Err(err) => {
1268 warn!(
1269 target: "engine::tree::state_root_strategy",
1270 %err,
1271 "Failed to get database provider for trie update comparison"
1272 );
1273 }
1274 }
1275 }
1276 Err(err) => {
1277 warn!(
1278 target: "engine::tree::state_root_strategy",
1279 %err,
1280 "Failed to compute serial state root for comparison"
1281 );
1282 }
1283 }
1284 false
1285}
1286
1287#[cfg(test)]
1288mod tests {
1289 use super::*;
1290 use alloy_consensus::constants::KECCAK_EMPTY;
1291 use alloy_primitives::{map::HashMap, Address, U256};
1292 use rand::Rng;
1293 use reth_chain_state::test_utils::TestBlockBuilder;
1294 use reth_chainspec::ChainSpec;
1295 use reth_db_common::init::init_genesis;
1296 use reth_ethereum_primitives::EthPrimitives;
1297 use reth_evm::OnStateHook;
1298 use reth_evm_ethereum::EthEvmConfig;
1299 use reth_primitives_traits::{Account, StorageEntry};
1300 use reth_provider::{
1301 providers::BlockchainProvider, test_utils::create_test_provider_factory_with_chain_spec,
1302 HashingWriter,
1303 };
1304 use reth_storage_overlay::{OverlayManager, OverlayStateProviderFactory};
1305 use reth_testing_utils::generators;
1306 use reth_trie::test_utils::state_root;
1307 use revm::state::{AccountInfo, AccountStatus, EvmState, EvmStorageSlot, TransactionId};
1308
1309 #[test]
1310 fn sparse_trie_prune_before_uses_requested_range() {
1311 let new_epoch = TrieNodeEpoch::new(10);
1312 assert_eq!(sparse_trie_prune_before::<EthPrimitives>(None, new_epoch), None);
1313 assert_eq!(
1314 sparse_trie_prune_before::<EthPrimitives>(Some(&[]), new_epoch),
1315 Some(new_epoch)
1316 );
1317
1318 let mut blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(7..10).collect();
1319 blocks.reverse();
1320
1321 assert_eq!(sparse_trie_prune_before(Some(&blocks), new_epoch), Some(TrieNodeEpoch::new(7)));
1322 }
1323
1324 #[test]
1325 fn published_sparse_trie_anchor_advances_to_prune_anchor() {
1326 let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..5).collect();
1327 let reused_anchor_hash = blocks[0].recovered_block().hash();
1328 let expected_prune_anchor = blocks[1].recovered_block().hash();
1329 let prune_blocks: Vec<_> = blocks.into_iter().skip(2).rev().collect();
1330
1331 assert_eq!(
1332 published_sparse_trie_anchor_hash(reused_anchor_hash, true, Some(&prune_blocks)),
1333 expected_prune_anchor
1334 );
1335 }
1336
1337 #[test]
1338 fn published_sparse_trie_anchor_does_not_move_backwards_when_anchor_is_in_prune_range() {
1339 let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..5).collect();
1340 let reused_anchor_hash = blocks[2].recovered_block().hash();
1341 let mut prune_blocks = blocks;
1342 prune_blocks.reverse();
1343 let prune_anchor = prune_blocks.last().unwrap().recovered_block().parent_hash();
1344
1345 assert_ne!(reused_anchor_hash, prune_anchor);
1346 assert_eq!(
1347 published_sparse_trie_anchor_hash(reused_anchor_hash, true, Some(&prune_blocks)),
1348 reused_anchor_hash
1349 );
1350 }
1351
1352 #[test]
1353 fn published_sparse_trie_anchor_keeps_parent_for_fresh_trie() {
1354 let mut blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..3).collect();
1355 blocks.reverse();
1356 let parent_hash = B256::with_last_byte(0xaa);
1357
1358 assert_eq!(
1359 published_sparse_trie_anchor_hash(parent_hash, false, Some(&blocks)),
1360 parent_hash
1361 );
1362 }
1363
1364 fn create_mock_state_updates(num_accounts: usize, updates_per_account: usize) -> Vec<EvmState> {
1365 let mut rng = generators::rng();
1366 let all_addresses: Vec<Address> = (0..num_accounts).map(|_| rng.random()).collect();
1367 let mut updates = Vec::with_capacity(updates_per_account);
1368
1369 for _ in 0..updates_per_account {
1370 let num_accounts_in_update = rng.random_range(1..=num_accounts);
1371 let mut state_update = EvmState::default();
1372
1373 for &address in &all_addresses[0..num_accounts_in_update] {
1374 let mut storage = HashMap::default();
1375 if rng.random_bool(0.7) {
1376 for _ in 0..rng.random_range(1..10) {
1377 let slot = U256::from(rng.random::<u64>());
1378 storage.insert(
1379 slot,
1380 EvmStorageSlot::new_changed(
1381 U256::ZERO,
1382 U256::from(rng.random::<u64>()),
1383 TransactionId::ZERO,
1384 ),
1385 );
1386 }
1387 }
1388
1389 let mut account = revm::state::Account::default();
1390 account.info = AccountInfo {
1391 balance: U256::from(rng.random::<u64>()),
1392 nonce: rng.random::<u64>(),
1393 code_hash: KECCAK_EMPTY,
1394 code: Some(Default::default()),
1395 account_id: None,
1396 };
1397 account.storage = storage;
1398 account.status = AccountStatus::Touched;
1399 account.transaction_id = TransactionId::ZERO;
1400 state_update.insert(address, account);
1401 }
1402
1403 updates.push(state_update);
1404 }
1405
1406 updates
1407 }
1408
1409 #[test]
1410 fn state_root_task_matches_serial_root() {
1411 reth_tracing::init_test_tracing();
1412
1413 let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1414 let genesis_hash = init_genesis(&factory).unwrap();
1415 let state_updates = create_mock_state_updates(10, 10);
1416 let mut accumulated_state: HashMap<Address, (Account, HashMap<B256, U256>)> =
1417 HashMap::default();
1418
1419 {
1420 let provider_rw = factory.provider_rw().expect("failed to get provider");
1421 for update in &state_updates {
1422 let account_updates = update.iter().map(|(address, account)| {
1423 (*address, Some(Account::from_revm_account(account)))
1424 });
1425 provider_rw
1426 .insert_account_for_hashing(account_updates)
1427 .expect("failed to insert accounts");
1428
1429 let storage_updates = update.iter().map(|(address, account)| {
1430 let storage_entries = account.storage.iter().map(|(slot, value)| {
1431 StorageEntry { key: B256::from(*slot), value: value.present_value }
1432 });
1433 (*address, storage_entries)
1434 });
1435 provider_rw
1436 .insert_storage_for_hashing(storage_updates)
1437 .expect("failed to insert storage");
1438 }
1439 provider_rw.commit().expect("failed to commit changes");
1440 }
1441
1442 for update in &state_updates {
1443 for (address, account) in update {
1444 let storage: HashMap<B256, U256> = account
1445 .storage
1446 .iter()
1447 .map(|(key, value)| (B256::from(*key), value.present_value))
1448 .collect();
1449 let entry = accumulated_state.entry(*address).or_default();
1450 entry.0 = Account::from_revm_account(account);
1451 entry.1.extend(storage);
1452 }
1453 }
1454
1455 let provider_factory = BlockchainProvider::new(factory).unwrap();
1456 let env: ExecutionEnv<EthEvmConfig> = ExecutionEnv::test_default();
1457 let runtime = reth_tasks::Runtime::test();
1458 let overlay_manager = OverlayManager::<EthPrimitives>::default();
1459 let mut state_root_handle = DefaultStateRootStrategy::default().spawn_state_root(
1460 &runtime,
1461 &overlay_manager,
1462 OverlayStateProviderFactory::new(
1463 provider_factory,
1464 overlay_manager.overlay_builder(genesis_hash),
1465 ),
1466 StateRootTaskOptions {
1467 parent_header: SealedHeader::new(Default::default(), genesis_hash),
1468 preserved_sparse_trie: None,
1469 transaction_count: Some(env.transaction_count),
1470 config: &TreeConfig::default(),
1471 pending_sparse_trie_prune_blocks: None,
1472 },
1473 );
1474
1475 let mut state_hook = state_root_handle.take_execution_hook();
1476 for update in state_updates {
1477 state_hook.on_state(update);
1478 }
1479 drop(state_hook);
1480
1481 let root_from_task = state_root_handle.state_root().expect("task failed").state_root;
1482 let root_from_regular = state_root(accumulated_state);
1483 assert_eq!(root_from_task, root_from_regular);
1484 }
1485}