Skip to main content

reth_engine_tree/tree/state_root_strategy/
mod.rs

1//! State-root strategies for engine-tree block validation.
2//!
3//! A [`StateRootStrategy`] is installed once per node, via
4//! `BasicEngineValidator::with_state_root_strategy`, and consulted for every block that engine
5//! validation executes. For each block the strategy prepares a [`StateRootJob`] before execution
6//! starts, and validation finishes the job after execution to obtain the state root that is
7//! checked against the block header. On every FCU that carries payload attributes, the strategy
8//! is also asked through [`StateRootStrategy::prepare_payload_builder`] for an optional
9//! [`PayloadStateRootHandle`] that the payload builder uses while building a block.
10//!
11//! # Job lifecycle
12//!
13//! 1. [`StateRootStrategy::prepare`] runs before block execution. The job can spawn background work
14//!    here and can expose hooks that observe execution.
15//! 2. Execution runs. Jobs that observe execution receive updates through their hooks.
16//! 3. [`StateRootJob::finish`] runs after execution and returns the [`StateRootJobOutcome`]. It
17//!    must produce a result even if no execution updates were observed, since the full
18//!    [`BlockExecutionOutput`] is passed to it.
19//!
20//! Dropping a prepared job without calling `finish` aborts it. Implementations must treat
21//! channel disconnects from dropped hooks as cancellation and must not leak background work.
22//!
23//! # Stream delivery contract
24//!
25//! A prepared job exposes update-stream capabilities over its sink. `prepare` installs exactly
26//! one authoritative capability per block, matching the execution mode:
27//!
28//! - On the parallel BAL execution path, prewarm converts the block access list and delivers
29//!   pre-hashed updates through the hashed update stream, terminated by
30//!   [`StateRootUpdateStream::finish`].
31//! - On the serial execution path, per-transaction `EvmState` updates arrive through the execution
32//!   hook, terminated when the hook is dropped after execution.
33//!
34//! Which path runs depends on runtime conditions (BAL present, caching and prewarming enabled),
35//! so a sink must handle both. Access hints from prewarming are best-effort: they may be
36//! missing, duplicated, or stale, and must not be treated as state updates.
37//!
38//! # Custom strategies
39//!
40//! Custom implementations can hold a [`DefaultStateRootStrategy`] and forward calls to it for
41//! blocks where the default behavior is wanted, for example before a fork activates. See
42//! `examples/custom-state-root` for the wiring.
43//!
44//! Returning empty trie updates in the outcome means the trie tables are no longer maintained:
45//! `eth_getProof` and anything else that reads the stored trie will not work for new blocks.
46//! Returning no changed paths opts the block out of sparse-trie cache pruning.
47
48mod sparse_trie;
49
50use self::sparse_trie::{SparseTrieCacheTask, SparseTrieTaskMetrics};
51use crate::tree::{
52    metrics::BlockValidationMetrics, EngineApiTreeState, ExecutionEnv, StateProviderBuilder,
53    TreeConfig,
54};
55use alloy_primitives::B256;
56use crossbeam_channel::Receiver as CrossbeamReceiver;
57use reth_chain_state::{ExecutedBlock, PreservedSparseTrie, StateTrieOverlayManager};
58use reth_errors::ProviderResult;
59use reth_evm::{ConfigureEvm, OnStateHook};
60use reth_primitives_traits::{
61    AlloyBlockHeader, FastInstant as Instant, NodePrimitives, RecoveredBlock,
62};
63use reth_provider::{
64    providers::OverlayStateProviderFactory, BlockExecutionOutput, BlockReader,
65    DatabaseProviderFactory, DatabaseProviderROFactory, HashedPostStateProvider, ProviderError,
66    StateProviderFactory, StateReader, StateRootProvider,
67};
68use reth_tasks::utils::increase_thread_priority;
69use reth_trie::{
70    hashed_cursor::HashedCursorFactory, prefix_set::TriePrefixSetsMut,
71    trie_cursor::TrieCursorFactory, updates::TrieUpdates, HashedPostState,
72};
73use reth_trie_parallel::proof_task::{ProofTaskCtx, ProofWorkerHandle};
74pub use reth_trie_parallel::{
75    error::StateRootTaskError,
76    state_root_task::{
77        evm_state_to_hashed_post_state, PayloadStateRootHandle, StateAccessHint,
78        StateRootComputeOutcome, StateRootHandle, StateRootHintStream, StateRootMessage,
79        StateRootSink, StateRootTaskCancelGuard, StateRootUpdateHook, StateRootUpdateStream,
80    },
81};
82#[cfg(feature = "trie-debug")]
83use reth_trie_sparse::debug_recorder::TrieDebugRecorder;
84use reth_trie_sparse::{ArenaParallelSparseTrie, RevealableSparseTrie, SparseStateTrie};
85use std::{
86    fmt,
87    sync::{
88        mpsc::{self, RecvTimeoutError},
89        Arc,
90    },
91    time::Duration,
92};
93use tracing::{debug, debug_span, instrument, warn, Span};
94
95/// Handle to a [`HashedPostState`] computed on a background thread.
96pub type LazyHashedPostState = reth_tasks::LazyHandle<Arc<HashedPostState>>;
97
98/// Strategy used by engine-tree validation to prepare per-block state-root work.
99pub trait StateRootStrategy<N, P, Evm>: Send + Sync
100where
101    N: NodePrimitives,
102    Evm: ConfigureEvm<Primitives = N>,
103{
104    /// Prepares a per-block state-root job before execution starts.
105    ///
106    /// A custom strategy that maintains a reusable sparse trie is responsible for consuming the
107    /// pending prune request from the context when it starts the corresponding job.
108    fn prepare(
109        &self,
110        ctx: StateRootJobContext<'_, N, P, Evm>,
111    ) -> ProviderResult<PreparedStateRootJob<N>>;
112
113    /// Prepares the optional payload-builder state-root handle used for FCU-triggered block
114    /// building.
115    ///
116    /// This is consulted on every FCU that carries payload attributes. Returning `None` means the
117    /// payload builder computes the state root itself; the stock builders fall back to a
118    /// synchronous MPT state root. The default implementation returns `None`.
119    fn prepare_payload_builder(
120        &self,
121        _ctx: PayloadStateRootJobContext<'_, N, P>,
122    ) -> ProviderResult<Option<PayloadStateRootHandle>> {
123        Ok(None)
124    }
125}
126
127/// Data available while preparing one payload-builder state-root handle.
128pub struct PayloadStateRootJobContext<'a, N, P>
129where
130    N: NodePrimitives,
131{
132    executor: &'a reth_tasks::Runtime,
133    state_trie_overlays: &'a StateTrieOverlayManager<N>,
134    parent_hash: B256,
135    parent_header: &'a N::BlockHeader,
136    timestamp: u64,
137    state: &'a mut EngineApiTreeState<N>,
138    provider_builder: StateProviderBuilder<N, P>,
139    overlay_factory: OverlayStateProviderFactory<P, N>,
140    config: &'a TreeConfig,
141}
142
143impl<N, P> fmt::Debug for PayloadStateRootJobContext<'_, N, P>
144where
145    N: NodePrimitives,
146{
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        f.debug_struct("PayloadStateRootJobContext")
149            .field("parent_hash", &self.parent_hash)
150            .field("parent_state_root", &self.parent_state_root())
151            .field("timestamp", &self.timestamp)
152            .field("pending_sparse_trie_prune", &self.state.pending_sparse_trie_prune())
153            .finish_non_exhaustive()
154    }
155}
156
157impl<'a, N, P> PayloadStateRootJobContext<'a, N, P>
158where
159    N: NodePrimitives,
160{
161    /// Creates a payload-builder state-root job context.
162    #[expect(clippy::too_many_arguments)]
163    pub(crate) const fn new(
164        executor: &'a reth_tasks::Runtime,
165        state_trie_overlays: &'a StateTrieOverlayManager<N>,
166        parent_hash: B256,
167        parent_header: &'a N::BlockHeader,
168        timestamp: u64,
169        state: &'a mut EngineApiTreeState<N>,
170        provider_builder: StateProviderBuilder<N, P>,
171        overlay_factory: OverlayStateProviderFactory<P, N>,
172        config: &'a TreeConfig,
173    ) -> Self {
174        Self {
175            executor,
176            state_trie_overlays,
177            parent_hash,
178            parent_header,
179            timestamp,
180            state,
181            provider_builder,
182            overlay_factory,
183            config,
184        }
185    }
186
187    /// Returns the parent block hash for the payload being built.
188    pub const fn parent_hash(&self) -> B256 {
189        self.parent_hash
190    }
191
192    /// Returns the parent block header for the payload being built.
193    ///
194    /// This is the chain's concrete header type, so chain-specific strategies can read
195    /// chain-specific fields, and number-activated forks can dispatch on the parent number.
196    pub const fn parent_header(&self) -> &N::BlockHeader {
197        self.parent_header
198    }
199
200    /// Returns the parent state root for the payload being built.
201    pub fn parent_state_root(&self) -> B256 {
202        self.parent_header.state_root()
203    }
204
205    /// Returns the timestamp of the payload being built, taken from the payload attributes.
206    ///
207    /// Strategies that switch behavior at a fork activation can dispatch on this value.
208    pub const fn timestamp(&self) -> u64 {
209        self.timestamp
210    }
211
212    /// Returns the task runtime used by state-root work.
213    pub const fn executor(&self) -> &reth_tasks::Runtime {
214        self.executor
215    }
216
217    /// Returns a clone of the state provider builder.
218    pub fn provider_builder(&self) -> StateProviderBuilder<N, P>
219    where
220        P: Clone,
221    {
222        self.provider_builder.clone()
223    }
224
225    /// Consumes the pending sparse trie prune request as in-memory parent-chain blocks, if any.
226    ///
227    /// Custom strategies that maintain a reusable sparse trie should call this when starting the
228    /// corresponding job. Strategies that do not use the request should leave it pending.
229    pub fn take_sparse_trie_prune_blocks(&mut self) -> Option<Vec<ExecutedBlock<N>>> {
230        self.state.take_sparse_trie_prune_blocks(self.parent_hash)
231    }
232}
233
234/// Data available while preparing one state-root job.
235pub struct StateRootJobContext<'a, N, P, Evm>
236where
237    N: NodePrimitives,
238    Evm: ConfigureEvm<Primitives = N>,
239{
240    executor: &'a reth_tasks::Runtime,
241    state_trie_overlays: &'a StateTrieOverlayManager<N>,
242    env: &'a ExecutionEnv<Evm>,
243    provider_builder: StateProviderBuilder<N, P>,
244    overlay_factory: OverlayStateProviderFactory<P, N>,
245    config: &'a TreeConfig,
246    parallel_bal_execution: bool,
247    state: &'a mut EngineApiTreeState<N>,
248}
249
250impl<N, P, Evm> fmt::Debug for StateRootJobContext<'_, N, P, Evm>
251where
252    N: NodePrimitives,
253    Evm: ConfigureEvm<Primitives = N>,
254{
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        f.debug_struct("StateRootJobContext")
257            .field("parallel_bal_execution", &self.parallel_bal_execution)
258            .field("has_pending_sparse_trie_prune", &self.state.pending_sparse_trie_prune())
259            .finish_non_exhaustive()
260    }
261}
262
263impl<'a, N, P, Evm> StateRootJobContext<'a, N, P, Evm>
264where
265    N: NodePrimitives,
266    Evm: ConfigureEvm<Primitives = N>,
267{
268    /// Creates a new state-root job context.
269    #[expect(clippy::too_many_arguments)]
270    pub(crate) const fn new(
271        executor: &'a reth_tasks::Runtime,
272        state_trie_overlays: &'a StateTrieOverlayManager<N>,
273        env: &'a ExecutionEnv<Evm>,
274        provider_builder: StateProviderBuilder<N, P>,
275        overlay_factory: OverlayStateProviderFactory<P, N>,
276        config: &'a TreeConfig,
277        parallel_bal_execution: bool,
278        state: &'a mut EngineApiTreeState<N>,
279    ) -> Self {
280        Self {
281            executor,
282            state_trie_overlays,
283            env,
284            provider_builder,
285            overlay_factory,
286            config,
287            parallel_bal_execution,
288            state,
289        }
290    }
291
292    /// Returns the execution environment for the block.
293    pub const fn env(&self) -> &ExecutionEnv<Evm> {
294        self.env
295    }
296
297    /// Returns the task runtime used by state-root work.
298    pub const fn executor(&self) -> &reth_tasks::Runtime {
299        self.executor
300    }
301
302    /// Returns true when validation will use the parallel BAL execution path.
303    pub const fn parallel_bal_execution(&self) -> bool {
304        self.parallel_bal_execution
305    }
306
307    /// Returns a clone of the state provider builder.
308    pub fn provider_builder(&self) -> StateProviderBuilder<N, P>
309    where
310        P: Clone,
311    {
312        self.provider_builder.clone()
313    }
314
315    /// Consumes the pending sparse trie prune request as in-memory parent-chain blocks, if any.
316    ///
317    /// Custom strategies that maintain a reusable sparse trie should call this when starting the
318    /// corresponding job. Strategies that do not use the request should leave it pending.
319    pub fn take_sparse_trie_prune_blocks(&mut self) -> Option<Vec<ExecutedBlock<N>>> {
320        self.state.take_sparse_trie_prune_blocks(self.env.parent_hash)
321    }
322}
323
324/// Prepared per-block state-root work and its update-stream capabilities.
325///
326/// The capabilities are populated by the strategy's `prepare` according to the execution
327/// mode: the execution hook on the serial path, the hashed update stream on the parallel BAL
328/// path, never both. Each capability is taken once by the code that produces its messages
329/// and is not retained here, so the task's update channel closes when the producers are done.
330pub struct PreparedStateRootJob<N: NodePrimitives> {
331    job: Box<dyn StateRootJob<N>>,
332    execution_hook: Option<StateRootUpdateHook>,
333    hint_stream: Option<StateRootHintStream>,
334    hashed_update_stream: Option<StateRootUpdateStream>,
335    hashed_state_rx: Option<mpsc::Receiver<HashedPostState>>,
336}
337
338impl<N: NodePrimitives> fmt::Debug for PreparedStateRootJob<N> {
339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340        f.debug_struct("PreparedStateRootJob")
341            .field("name", &self.job.name())
342            .field("has_execution_hook", &self.execution_hook.is_some())
343            .field("has_hint_stream", &self.hint_stream.is_some())
344            .field("has_hashed_update_stream", &self.hashed_update_stream.is_some())
345            .field("has_hashed_state_rx", &self.hashed_state_rx.is_some())
346            .finish()
347    }
348}
349
350impl<N: NodePrimitives> PreparedStateRootJob<N> {
351    /// Creates a prepared state-root job without update-stream capabilities.
352    pub const fn new(
353        job: Box<dyn StateRootJob<N>>,
354        hashed_state_rx: Option<mpsc::Receiver<HashedPostState>>,
355    ) -> Self {
356        Self {
357            job,
358            execution_hook: None,
359            hint_stream: None,
360            hashed_update_stream: None,
361            hashed_state_rx,
362        }
363    }
364
365    /// Attaches the execution hook capability (serial execution path).
366    pub fn with_execution_hook(mut self, hook: StateRootUpdateHook) -> Self {
367        self.execution_hook = Some(hook);
368        self
369    }
370
371    /// Attaches the hint stream capability.
372    pub fn with_hint_stream(mut self, hint_stream: StateRootHintStream) -> Self {
373        self.hint_stream = Some(hint_stream);
374        self
375    }
376
377    /// Attaches the hashed update stream capability (parallel BAL path).
378    pub fn with_hashed_update_stream(mut self, stream: StateRootUpdateStream) -> Self {
379        self.hashed_update_stream = Some(stream);
380        self
381    }
382
383    /// Returns the job name used in logs.
384    pub fn name(&self) -> &'static str {
385        self.job.name()
386    }
387
388    /// Takes the execution hook, present only when the job wants normal execution updates.
389    pub fn take_execution_hook(&mut self) -> Option<Box<dyn OnStateHook + 'static>> {
390        self.execution_hook.take().map(|hook| Box::new(hook) as Box<dyn OnStateHook + 'static>)
391    }
392
393    /// Takes the hint stream for transaction prewarming.
394    pub const fn take_hint_stream(&mut self) -> Option<StateRootHintStream> {
395        self.hint_stream.take()
396    }
397
398    /// Takes the hashed update stream, present only on the parallel BAL path.
399    pub const fn take_hashed_update_stream(&mut self) -> Option<StateRootUpdateStream> {
400        self.hashed_update_stream.take()
401    }
402
403    /// Takes the optional hashed-state receiver produced by the job.
404    ///
405    /// The sender behind a returned receiver must either deliver one value or be dropped;
406    /// validation blocks on it while hashing the post state, so a job that keeps the sender
407    /// alive without sending stalls block validation.
408    pub const fn take_hashed_state_rx(&mut self) -> Option<mpsc::Receiver<HashedPostState>> {
409        self.hashed_state_rx.take()
410    }
411
412    /// Completes the job after execution.
413    pub fn finish(
414        &mut self,
415        block: &RecoveredBlock<N::Block>,
416        output: Arc<BlockExecutionOutput<N::Receipt>>,
417        hashed_state: &LazyHashedPostState,
418    ) -> ProviderResult<StateRootJobOutcome> {
419        self.job.finish(block, output, hashed_state)
420    }
421}
422
423/// Per-block state-root job prepared before execution and finished after execution.
424pub trait StateRootJob<N: NodePrimitives>: Send {
425    /// Human-readable strategy name used in logs.
426    fn name(&self) -> &'static str;
427
428    /// Completes the job after execution.
429    ///
430    /// Called at most once per prepared job; implementations may panic if called again.
431    fn finish(
432        &mut self,
433        block: &RecoveredBlock<N::Block>,
434        output: Arc<BlockExecutionOutput<N::Receipt>>,
435        hashed_state: &LazyHashedPostState,
436    ) -> ProviderResult<StateRootJobOutcome>;
437}
438
439/// Outcome of a per-block state-root job.
440#[derive(Debug)]
441pub struct StateRootJobOutcome {
442    /// Computed state root.
443    pub state_root: B256,
444    /// Trie updates associated with the computed state root.
445    pub trie_updates: Arc<TrieUpdates>,
446    /// Changed trie node base paths retained while computing the root, if the job tracks them.
447    pub changed_paths: Option<Arc<TriePrefixSetsMut>>,
448    /// Hashed post state recomputed by a fallback path.
449    ///
450    /// When set, the root was not derived from the streamed updates, so validation replaces its
451    /// streaming-derived hashed post state with this one and re-runs hashed-state checks.
452    pub hashed_state: Option<Arc<HashedPostState>>,
453}
454
455impl StateRootJobOutcome {
456    /// Creates a state-root job outcome without changed paths.
457    pub const fn new(state_root: B256, trie_updates: Arc<TrieUpdates>) -> Self {
458        Self { state_root, trie_updates, changed_paths: None, hashed_state: None }
459    }
460
461    /// Sets the changed trie node base paths retained while computing the root.
462    pub fn with_changed_paths(mut self, changed_paths: Option<Arc<TriePrefixSetsMut>>) -> Self {
463        self.changed_paths = changed_paths;
464        self
465    }
466
467    /// Sets the hashed post state recomputed by a fallback path.
468    pub fn with_hashed_state(mut self, hashed_state: Option<Arc<HashedPostState>>) -> Self {
469        self.hashed_state = hashed_state;
470        self
471    }
472}
473
474/// Receiver for the raced serial state-root fallback: root, trie updates, and the hashed
475/// post state the fallback recomputed.
476type SerialFallbackRx = mpsc::Receiver<ProviderResult<(B256, TrieUpdates, Arc<HashedPostState>)>>;
477
478/// Default state-root strategy used by engine-tree validation.
479///
480/// Covers the built-in modes: the sparse-trie state-root task, plus the skipped and
481/// synchronous modes selected by [`TreeConfig`].
482///
483/// Custom strategies can hold this type and delegate to it for blocks where they want the
484/// default behavior.
485#[derive(Default)]
486pub struct DefaultStateRootStrategy {
487    metrics: SparseTrieTaskMetrics,
488}
489
490impl fmt::Debug for DefaultStateRootStrategy {
491    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
492        f.debug_struct("DefaultStateRootStrategy").finish_non_exhaustive()
493    }
494}
495
496impl DefaultStateRootStrategy {
497    /// Transaction count threshold below which proof workers are halved, since fewer transactions
498    /// produce fewer state changes and most workers would be idle overhead.
499    const SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD: usize = 30;
500
501    /// Spawns the default state-root computation pipeline.
502    ///
503    /// The authoritative update capability taken from the returned handle must be dropped or
504    /// explicitly finished after execution so the task observes the end of the update stream.
505    /// An unknown transaction count uses the full proof-worker pool.
506    #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
507    fn spawn_state_root<N, F>(
508        &self,
509        executor: &reth_tasks::Runtime,
510        state_trie_overlays: &StateTrieOverlayManager<N>,
511        multiproof_provider_factory: F,
512        options: StateRootTaskOptions<'_, N>,
513    ) -> StateRootHandle
514    where
515        N: NodePrimitives,
516        F: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
517            + Clone
518            + Send
519            + Sync
520            + 'static,
521    {
522        let StateRootTaskOptions {
523            parent_state_root,
524            transaction_count,
525            config,
526            pending_sparse_trie_prune_blocks,
527        } = options;
528        let (updates_tx, from_multi_proof) = crossbeam_channel::unbounded();
529        let (cancel_guard, cancel_rx) = StateRootTaskCancelGuard::channel();
530
531        let task_ctx = ProofTaskCtx::new(multiproof_provider_factory);
532        #[cfg(feature = "trie-debug")]
533        let task_ctx = task_ctx.with_proof_jitter(config.proof_jitter());
534        let halve_workers = transaction_count
535            .is_some_and(|count| count <= Self::SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD);
536        let proof_handle = ProofWorkerHandle::new(executor, task_ctx, halve_workers);
537
538        let (state_root_tx, state_root_rx) = mpsc::channel();
539        let (hashed_state_tx, hashed_state_rx) = mpsc::channel();
540
541        self.spawn_sparse_trie_task(
542            executor,
543            state_trie_overlays,
544            proof_handle,
545            state_root_tx,
546            hashed_state_tx,
547            from_multi_proof,
548            cancel_rx,
549            SparseTrieTaskOptions {
550                parent_state_root,
551                chunk_size: config.multiproof_chunk_size(),
552                pending_sparse_trie_prune_blocks: if config.disable_sparse_trie_cache_pruning() {
553                    None
554                } else {
555                    pending_sparse_trie_prune_blocks
556                },
557                max_hot_slots: config.sparse_trie_max_hot_slots(),
558                max_hot_accounts: config.sparse_trie_max_hot_accounts(),
559            },
560        );
561
562        StateRootHandle::new(
563            parent_state_root,
564            updates_tx,
565            cancel_guard,
566            state_root_rx,
567            hashed_state_rx,
568        )
569    }
570
571    /// Spawns the sparse-trie task and preserves its trie for the next state-root job.
572    #[expect(clippy::too_many_arguments)]
573    fn spawn_sparse_trie_task<N: NodePrimitives>(
574        &self,
575        executor: &reth_tasks::Runtime,
576        state_trie_overlays: &StateTrieOverlayManager<N>,
577        proof_worker_handle: ProofWorkerHandle,
578        state_root_tx: mpsc::Sender<Result<StateRootComputeOutcome, StateRootTaskError>>,
579        hashed_state_tx: mpsc::Sender<HashedPostState>,
580        from_multi_proof: CrossbeamReceiver<StateRootMessage>,
581        cancel_rx: CrossbeamReceiver<()>,
582        options: SparseTrieTaskOptions<N>,
583    ) {
584        let SparseTrieTaskOptions {
585            parent_state_root,
586            chunk_size,
587            pending_sparse_trie_prune_blocks,
588            max_hot_slots,
589            max_hot_accounts,
590        } = options;
591        let state_trie_overlays = state_trie_overlays.clone();
592        let trie_metrics = self.metrics.clone();
593        let executor = executor.clone();
594
595        let parent_span = Span::current();
596        executor.clone().spawn_blocking_named("sparse-trie", move || {
597            reth_tasks::once!(increase_thread_priority);
598
599            let _enter = debug_span!(
600                target: "engine::tree::payload_processor",
601                parent: parent_span,
602                "sparse_trie_task"
603            )
604            .entered();
605
606            let start = Instant::now();
607            let preserved = state_trie_overlays.take_sparse_trie();
608            trie_metrics
609                .sparse_trie_cache_wait_duration_histogram
610                .record(start.elapsed().as_secs_f64());
611
612            let mut sparse_state_trie = preserved
613                .map(|preserved| preserved.into_trie_for(parent_state_root))
614                .unwrap_or_else(|| {
615                    debug!(
616                        target: "engine::tree::payload_processor",
617                        "Creating new sparse trie - no preserved trie available"
618                    );
619                    let default_trie =
620                        RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
621                    SparseStateTrie::default()
622                        .with_accounts_trie(default_trie.clone())
623                        .with_default_storage_trie(default_trie)
624                        .with_updates(true)
625                });
626            sparse_state_trie.set_changed_paths(true);
627            sparse_state_trie.set_hot_cache_capacities(max_hot_slots, max_hot_accounts);
628
629            let mut task = SparseTrieCacheTask::new_with_trie(
630                &executor,
631                from_multi_proof,
632                cancel_rx,
633                hashed_state_tx,
634                proof_worker_handle,
635                trie_metrics.clone(),
636                sparse_state_trie,
637                parent_state_root,
638                chunk_size,
639            );
640
641            let result = task.run();
642            // Lock before publishing the result. The next block can start as soon as the receiver
643            // wakes and must not observe an empty overlay before this task stores the reusable
644            // trie.
645            let mut guard = state_trie_overlays.lock_sparse_trie();
646            let task_result = result.as_ref().ok().cloned();
647
648            if state_root_tx.send(result).is_err() {
649                debug!(
650                    target: "engine::tree::payload_processor",
651                    "State root receiver dropped, clearing trie"
652                );
653                let (trie, deferred) = task.into_cleared_trie();
654                guard.store(PreservedSparseTrie::cleared(trie));
655                drop(guard);
656                executor.spawn_drop(deferred);
657                return
658            }
659
660            let _enter =
661                debug_span!(target: "engine::tree::payload_processor", "preserve").entered();
662            let deferred = if let Some(result) = task_result {
663                let start = Instant::now();
664                let (mut trie, deferred) = task.into_trie_for_reuse();
665                if let Some(prune_blocks) = pending_sparse_trie_prune_blocks {
666                    let changed_paths = result
667                        .changed_paths
668                        .as_deref()
669                        .expect("sparse trie task always returns changed paths");
670                    if let Some(retained_paths) =
671                        sparse_trie_retained_paths(prune_blocks, changed_paths)
672                    {
673                        trie.prune(max_hot_slots, max_hot_accounts, retained_paths);
674                    }
675                }
676                trie_metrics
677                    .into_trie_for_reuse_duration_histogram
678                    .record(start.elapsed().as_secs_f64());
679                trie_metrics.sparse_trie_retained_memory_bytes.set(trie.memory_size() as f64);
680                trie_metrics
681                    .sparse_trie_retained_storage_tries
682                    .set(trie.retained_storage_tries_count() as f64);
683                guard.store(PreservedSparseTrie::anchored(trie, result.state_root));
684                deferred
685            } else {
686                debug!(
687                    target: "engine::tree::payload_processor",
688                    "State root computation failed, clearing trie"
689                );
690                let (trie, deferred) = task.into_cleared_trie();
691                guard.store(PreservedSparseTrie::cleared(trie));
692                deferred
693            };
694            drop(guard);
695            executor.spawn_drop(deferred);
696        });
697    }
698}
699
700struct SparseTrieTaskOptions<N: NodePrimitives> {
701    parent_state_root: B256,
702    chunk_size: usize,
703    /// `None` disables pruning. `Some(Vec::new())` prunes using only the current block's paths.
704    pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
705    max_hot_slots: usize,
706    max_hot_accounts: usize,
707}
708
709struct StateRootTaskOptions<'a, N: NodePrimitives> {
710    parent_state_root: B256,
711    transaction_count: Option<usize>,
712    config: &'a TreeConfig,
713    pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
714}
715
716fn sparse_trie_retained_paths<N: NodePrimitives>(
717    prune_blocks: Vec<ExecutedBlock<N>>,
718    current_changed_paths: &TriePrefixSetsMut,
719) -> Option<TriePrefixSetsMut> {
720    let mut retained_paths = TriePrefixSetsMut::default();
721    for block in prune_blocks {
722        let trie_data = block.trie_data();
723        let Some(changed_paths) = trie_data.changed_paths.as_deref() else {
724            debug!(
725                target: "engine::tree::payload_processor",
726                block = ?block.recovered_block().num_hash(),
727                "Skipping sparse trie prune because changed paths for in-memory block are unknown"
728            );
729            return None
730        };
731        retained_paths.extend_ref(changed_paths);
732    }
733    retained_paths.extend_ref(current_changed_paths);
734    Some(retained_paths)
735}
736
737impl<N, P, Evm> StateRootStrategy<N, P, Evm> for DefaultStateRootStrategy
738where
739    N: NodePrimitives,
740    P: DatabaseProviderFactory
741        + BlockReader<Header = N::BlockHeader>
742        + StateProviderFactory
743        + StateReader
744        + Clone
745        + 'static,
746    OverlayStateProviderFactory<P, N>: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
747        + Clone
748        + Send
749        + Sync
750        + 'static,
751    Evm: ConfigureEvm<Primitives = N> + 'static,
752{
753    fn prepare(
754        &self,
755        mut ctx: StateRootJobContext<'_, N, P, Evm>,
756    ) -> ProviderResult<PreparedStateRootJob<N>> {
757        if ctx.config.skip_state_root() {
758            return Ok(PreparedStateRootJob::new(Box::new(SkippedStateRootJob {}), None))
759        }
760
761        if !ctx.config.use_state_root_task() {
762            return Ok(PreparedStateRootJob::new(
763                Box::new(SynchronousStateRootJob { provider_builder: ctx.provider_builder }),
764                None,
765            ))
766        }
767
768        let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
769        let StateRootJobContext {
770            executor,
771            state_trie_overlays,
772            env,
773            provider_builder,
774            overlay_factory,
775            config,
776            parallel_bal_execution,
777            state: _,
778        } = ctx;
779
780        let mut handle = self.spawn_state_root(
781            executor,
782            state_trie_overlays,
783            overlay_factory.clone(),
784            StateRootTaskOptions {
785                parent_state_root: env.parent_state_root,
786                transaction_count: Some(env.transaction_count),
787                config,
788                pending_sparse_trie_prune_blocks,
789            },
790        );
791
792        // The execution mode decides who finishes the update stream: the execution hook on
793        // the serial path, the BAL streamer on the parallel path. Both come from one slot in
794        // the handle, so only one of them can exist.
795        let (hashed_update_stream, execution_hook): (
796            Option<StateRootUpdateStream>,
797            Option<StateRootUpdateHook>,
798        ) = match parallel_bal_execution {
799            true => (Some(handle.take_hashed_update_stream()), None),
800            false => (None, Some(handle.take_execution_hook())),
801        };
802        let hint_stream = handle.take_hint_stream();
803
804        let hashed_state_rx = Some(handle.take_hashed_state_rx());
805
806        let mut prepared = PreparedStateRootJob::new(
807            Box::new(SparseTrieStateRootJob {
808                handle,
809                provider_builder,
810                overlay_factory,
811                executor: executor.clone(),
812                timeout: config.state_root_task_timeout(),
813                compare_trie_updates: config.always_compare_trie_updates(),
814                metrics: BlockValidationMetrics::default(),
815            }),
816            hashed_state_rx,
817        )
818        .with_hint_stream(hint_stream);
819        if let Some(hook) = execution_hook {
820            prepared = prepared.with_execution_hook(hook);
821        }
822        if let Some(stream) = hashed_update_stream {
823            prepared = prepared.with_hashed_update_stream(stream);
824        }
825        Ok(prepared)
826    }
827
828    fn prepare_payload_builder(
829        &self,
830        mut ctx: PayloadStateRootJobContext<'_, N, P>,
831    ) -> ProviderResult<Option<PayloadStateRootHandle>> {
832        // Sharing the engine state-root task with the payload builder is opt-in, and needs a
833        // host that can run the task pipeline at all.
834        if !ctx.config.share_sparse_trie_with_payload_builder() ||
835            ctx.config.skip_state_root() ||
836            !ctx.config.has_enough_parallelism()
837        {
838            return Ok(None)
839        }
840
841        let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
842        Ok(Some(
843            self.spawn_state_root(
844                ctx.executor,
845                ctx.state_trie_overlays,
846                ctx.overlay_factory.clone(),
847                StateRootTaskOptions {
848                    parent_state_root: ctx.parent_state_root(),
849                    // Tx count unknown at FCU time (block built incrementally): full proof workers.
850                    transaction_count: None,
851                    config: ctx.config,
852                    pending_sparse_trie_prune_blocks,
853                },
854            )
855            .into_payload_state_root_handle(),
856        ))
857    }
858}
859
860#[derive(Debug)]
861struct SkippedStateRootJob {}
862
863impl<N: NodePrimitives> StateRootJob<N> for SkippedStateRootJob {
864    fn name(&self) -> &'static str {
865        "skipped"
866    }
867
868    fn finish(
869        &mut self,
870        block: &RecoveredBlock<N::Block>,
871        _output: Arc<BlockExecutionOutput<N::Receipt>>,
872        _hashed_state: &LazyHashedPostState,
873    ) -> ProviderResult<StateRootJobOutcome> {
874        Ok(StateRootJobOutcome::new(block.header().state_root(), Arc::new(TrieUpdates::default())))
875    }
876}
877
878#[derive(Debug)]
879struct SynchronousStateRootJob<N: NodePrimitives, P> {
880    provider_builder: StateProviderBuilder<N, P>,
881}
882
883impl<N, P> StateRootJob<N> for SynchronousStateRootJob<N, P>
884where
885    N: NodePrimitives,
886    P: BlockReader + StateProviderFactory + StateReader + Clone + Send + Sync + 'static,
887{
888    fn name(&self) -> &'static str {
889        "synchronous"
890    }
891
892    fn finish(
893        &mut self,
894        _block: &RecoveredBlock<N::Block>,
895        _output: Arc<BlockExecutionOutput<N::Receipt>>,
896        hashed_state: &LazyHashedPostState,
897    ) -> ProviderResult<StateRootJobOutcome> {
898        let provider = self.provider_builder.clone().build()?;
899        let (state_root, trie_updates) =
900            provider.state_root_with_updates(hashed_state.get().as_ref().clone())?;
901        Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates)))
902    }
903}
904
905#[derive(Debug)]
906struct SparseTrieStateRootJob<N: NodePrimitives, P> {
907    handle: StateRootHandle,
908    provider_builder: StateProviderBuilder<N, P>,
909    overlay_factory: OverlayStateProviderFactory<P, N>,
910    executor: reth_tasks::Runtime,
911    timeout: Option<Duration>,
912    compare_trie_updates: bool,
913    metrics: BlockValidationMetrics,
914}
915
916impl<N, P> SparseTrieStateRootJob<N, P>
917where
918    N: NodePrimitives,
919    P: StateProviderFactory + Clone + Send + Sync + 'static,
920    P: BlockReader + StateReader,
921    OverlayStateProviderFactory<P, N>: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
922        + Clone
923        + Send
924        + Sync
925        + 'static,
926{
927    fn serial_fallback(
928        executor: &reth_tasks::Runtime,
929        provider_builder: StateProviderBuilder<N, P>,
930        output: Arc<BlockExecutionOutput<N::Receipt>>,
931    ) -> ProviderResult<SerialFallbackRx> {
932        let provider = provider_builder.build()?;
933        let (fallback_tx, fallback_rx) = mpsc::channel();
934        executor.spawn_blocking_named("serial-root", move || {
935            let result = (|| {
936                let hashed_state = Arc::new(provider.hashed_post_state(&output.state));
937                let (root, updates) =
938                    provider.state_root_with_updates(hashed_state.as_ref().clone())?;
939                Ok((root, updates, hashed_state))
940            })();
941            let _ = fallback_tx.send(result);
942        });
943
944        Ok(fallback_rx)
945    }
946
947    /// Recomputes the state root serially from the execution output.
948    ///
949    /// Used when the state-root task failed or produced a wrong root, so the recomputed hashed
950    /// post state is returned in the outcome for validation to re-check against.
951    fn compute_serial(
952        &self,
953        output: &BlockExecutionOutput<N::Receipt>,
954    ) -> ProviderResult<StateRootJobOutcome> {
955        let provider = self.provider_builder.clone().build()?;
956        let hashed_state = Arc::new(provider.hashed_post_state(&output.state));
957        let (state_root, trie_updates) =
958            provider.state_root_with_updates(hashed_state.as_ref().clone())?;
959        self.metrics.state_root_task_fallback_success_total.increment(1);
960        Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
961            .with_hashed_state(Some(hashed_state)))
962    }
963
964    /// Converts a task outcome into a job outcome, recomputing serially when the task returned
965    /// a root that does not match the block header. A state-root-task bug then costs latency
966    /// instead of marking a valid block invalid; if the serial root also mismatches, validation
967    /// rejects the block.
968    fn verified_sparse_outcome(
969        &self,
970        block: &RecoveredBlock<N::Block>,
971        output: &BlockExecutionOutput<N::Receipt>,
972        outcome: StateRootComputeOutcome,
973    ) -> ProviderResult<StateRootJobOutcome> {
974        let outcome = self.sparse_outcome(block, output, outcome);
975        if outcome.state_root == block.header().state_root() {
976            return Ok(outcome)
977        }
978        warn!(
979            target: "engine::tree::state_root_strategy",
980            state_root = ?outcome.state_root,
981            block_state_root = ?block.header().state_root(),
982            "State root task returned incorrect state root, recomputing serially"
983        );
984        self.compute_serial(output)
985    }
986
987    fn sparse_outcome(
988        &self,
989        _block: &RecoveredBlock<N::Block>,
990        output: &BlockExecutionOutput<N::Receipt>,
991        outcome: StateRootComputeOutcome,
992    ) -> StateRootJobOutcome {
993        let StateRootComputeOutcome {
994            state_root,
995            trie_updates,
996            changed_paths,
997            #[cfg(feature = "trie-debug")]
998            debug_recorders,
999        } = outcome;
1000
1001        if self.compare_trie_updates {
1002            let _has_diff = compare_trie_updates_with_serial(
1003                self.provider_builder.clone(),
1004                self.overlay_factory.clone(),
1005                output,
1006                trie_updates.as_ref().clone(),
1007            );
1008            #[cfg(feature = "trie-debug")]
1009            if _has_diff {
1010                write_trie_debug_recorders(_block.header().number(), &debug_recorders);
1011            }
1012        }
1013
1014        #[cfg(feature = "trie-debug")]
1015        if state_root != _block.header().state_root() {
1016            write_trie_debug_recorders(_block.header().number(), &debug_recorders);
1017        }
1018
1019        StateRootJobOutcome::new(state_root, trie_updates).with_changed_paths(changed_paths)
1020    }
1021}
1022
1023impl<N, P> StateRootJob<N> for SparseTrieStateRootJob<N, P>
1024where
1025    N: NodePrimitives,
1026    P: BlockReader + StateProviderFactory + StateReader + Clone + Send + Sync + 'static,
1027    OverlayStateProviderFactory<P, N>: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
1028        + Clone
1029        + Send
1030        + Sync
1031        + 'static,
1032{
1033    fn name(&self) -> &'static str {
1034        "sparse-trie"
1035    }
1036
1037    fn finish(
1038        &mut self,
1039        block: &RecoveredBlock<N::Block>,
1040        output: Arc<BlockExecutionOutput<N::Receipt>>,
1041        _hashed_state: &LazyHashedPostState,
1042    ) -> ProviderResult<StateRootJobOutcome> {
1043        if self.timeout.is_none() {
1044            return match self.handle.state_root() {
1045                Ok(outcome) => self.verified_sparse_outcome(block, &output, outcome),
1046                Err(err) => {
1047                    debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1048                    self.compute_serial(&output)
1049                }
1050            }
1051        }
1052
1053        let timeout = self.timeout.expect("checked above");
1054        let task_rx = self.handle.take_state_root_rx();
1055        let fallback_rx = match task_rx.recv_timeout(timeout) {
1056            Ok(Ok(outcome)) => return self.verified_sparse_outcome(block, &output, outcome),
1057            Ok(Err(err)) => {
1058                debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1059                Self::serial_fallback(
1060                    &self.executor,
1061                    self.provider_builder.clone(),
1062                    output.clone(),
1063                )?
1064            }
1065            Err(RecvTimeoutError::Timeout) => {
1066                warn!(target: "engine::tree::state_root_strategy", ?timeout, "State root task timed out, racing serial fallback");
1067                self.metrics.state_root_task_timeout_total.increment(1);
1068                Self::serial_fallback(
1069                    &self.executor,
1070                    self.provider_builder.clone(),
1071                    output.clone(),
1072                )?
1073            }
1074            Err(RecvTimeoutError::Disconnected) => {
1075                debug!(target: "engine::tree::state_root_strategy", "State root task dropped, falling back to serial root");
1076                Self::serial_fallback(
1077                    &self.executor,
1078                    self.provider_builder.clone(),
1079                    output.clone(),
1080                )?
1081            }
1082        };
1083
1084        loop {
1085            if let Ok(Ok(outcome)) = task_rx.try_recv() {
1086                let outcome = self.sparse_outcome(block, &output, outcome);
1087                if outcome.state_root == block.header().state_root() {
1088                    return Ok(outcome)
1089                }
1090                // A wrong task root falls through to the serial fallback already racing below.
1091                warn!(
1092                    target: "engine::tree::state_root_strategy",
1093                    state_root = ?outcome.state_root,
1094                    block_state_root = ?block.header().state_root(),
1095                    "State root task returned incorrect state root, using serial fallback"
1096                );
1097            }
1098
1099            match fallback_rx.try_recv() {
1100                Ok(Ok((state_root, trie_updates, hashed_state))) => {
1101                    self.metrics.state_root_task_fallback_success_total.increment(1);
1102                    return Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1103                        .with_hashed_state(Some(hashed_state)))
1104                }
1105                Ok(Err(err)) => return Err(err),
1106                Err(mpsc::TryRecvError::Empty) => {}
1107                Err(mpsc::TryRecvError::Disconnected) => {
1108                    return Err(ProviderError::other(std::io::Error::other(
1109                        "serial state root fallback task dropped",
1110                    )))
1111                }
1112            }
1113
1114            std::thread::sleep(Duration::from_millis(1));
1115        }
1116    }
1117}
1118
1119fn compare_trie_updates_with_serial<N, P>(
1120    state_provider_builder: StateProviderBuilder<N, P>,
1121    overlay_factory: OverlayStateProviderFactory<P, N>,
1122    output: &BlockExecutionOutput<N::Receipt>,
1123    task_trie_updates: TrieUpdates,
1124) -> bool
1125where
1126    N: NodePrimitives,
1127    P: BlockReader + StateProviderFactory + StateReader + Clone,
1128    OverlayStateProviderFactory<P, N>:
1129        DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>,
1130{
1131    debug!(target: "engine::tree::state_root_strategy", "Comparing trie updates with serial computation");
1132
1133    match state_provider_builder.build().and_then(|provider| {
1134        let hashed_state = provider.hashed_post_state(&output.state);
1135        provider.state_root_with_updates(hashed_state)
1136    }) {
1137        Ok((serial_root, serial_trie_updates)) => {
1138            debug!(
1139                target: "engine::tree::state_root_strategy",
1140                ?serial_root,
1141                "Serial state root computation finished for comparison"
1142            );
1143
1144            match overlay_factory.database_provider_ro() {
1145                Ok(provider) => match super::trie_updates::compare_trie_updates(
1146                    &provider,
1147                    task_trie_updates,
1148                    serial_trie_updates,
1149                ) {
1150                    Ok(has_diff) => return has_diff,
1151                    Err(err) => {
1152                        warn!(
1153                            target: "engine::tree::state_root_strategy",
1154                            %err,
1155                            "Error comparing trie updates"
1156                        );
1157                        return true;
1158                    }
1159                },
1160                Err(err) => {
1161                    warn!(
1162                        target: "engine::tree::state_root_strategy",
1163                        %err,
1164                        "Failed to get database provider for trie update comparison"
1165                    );
1166                }
1167            }
1168        }
1169        Err(err) => {
1170            warn!(
1171                target: "engine::tree::state_root_strategy",
1172                %err,
1173                "Failed to compute serial state root for comparison"
1174            );
1175        }
1176    }
1177    false
1178}
1179
1180/// Writes trie debug recorders to a JSON file for the given block number.
1181///
1182/// The file is written to the current working directory as `trie_debug_block_{block_number}.json`.
1183#[cfg(feature = "trie-debug")]
1184fn write_trie_debug_recorders(block_number: u64, recorders: &[(Option<B256>, TrieDebugRecorder)]) {
1185    let path = format!("trie_debug_block_{block_number}.json");
1186    match serde_json::to_string_pretty(recorders) {
1187        Ok(json) => match std::fs::write(&path, json) {
1188            Ok(()) => {
1189                warn!(
1190                    target: "engine::tree::state_root_strategy",
1191                    %path,
1192                    "Wrote trie debug recorders to file"
1193                );
1194            }
1195            Err(err) => {
1196                warn!(
1197                    target: "engine::tree::state_root_strategy",
1198                    %err,
1199                    %path,
1200                    "Failed to write trie debug recorders"
1201                );
1202            }
1203        },
1204        Err(err) => {
1205            warn!(
1206                target: "engine::tree::state_root_strategy",
1207                %err,
1208                "Failed to serialize trie debug recorders"
1209            );
1210        }
1211    }
1212}
1213
1214#[cfg(test)]
1215mod tests {
1216    use super::*;
1217    use alloy_consensus::constants::KECCAK_EMPTY;
1218    use alloy_primitives::{map::HashMap, Address, U256};
1219    use rand::Rng;
1220    use reth_chain_state::{test_utils::TestBlockBuilder, StateTrieOverlayManager};
1221    use reth_chainspec::ChainSpec;
1222    use reth_db_common::init::init_genesis;
1223    use reth_ethereum_primitives::EthPrimitives;
1224    use reth_evm::OnStateHook;
1225    use reth_evm_ethereum::EthEvmConfig;
1226    use reth_primitives_traits::{Account, StorageEntry};
1227    use reth_provider::{
1228        providers::{BlockchainProvider, OverlayBuilder, OverlayStateProviderFactory},
1229        test_utils::create_test_provider_factory_with_chain_spec,
1230        HashingWriter,
1231    };
1232    use reth_testing_utils::generators;
1233    use reth_trie::{
1234        prefix_set::{PrefixSetMut, TriePrefixSetsMut},
1235        test_utils::state_root,
1236        LazyTrieData,
1237    };
1238    use reth_trie_common::Nibbles;
1239    use reth_trie_db::ChangesetCache;
1240    use revm::state::{AccountInfo, AccountStatus, EvmState, EvmStorageSlot, TransactionId};
1241
1242    fn with_changed_paths(
1243        block: ExecutedBlock<EthPrimitives>,
1244        changed_paths: TriePrefixSetsMut,
1245    ) -> ExecutedBlock<EthPrimitives> {
1246        let mut trie_data = block.trie_data();
1247        trie_data.changed_paths = Some(Arc::new(changed_paths));
1248        ExecutedBlock::with_deferred_trie_data(
1249            block.recovered_block,
1250            block.execution_output,
1251            LazyTrieData::ready(trie_data),
1252        )
1253    }
1254
1255    fn trie_changed_paths(account_path: u8, storage_path: u8) -> TriePrefixSetsMut {
1256        TriePrefixSetsMut {
1257            account_prefix_set: PrefixSetMut::from([Nibbles::from_nibbles([account_path])]),
1258            storage_prefix_sets: HashMap::from_iter([(
1259                B256::with_last_byte(account_path),
1260                PrefixSetMut::from([Nibbles::from_nibbles([storage_path])]),
1261            )]),
1262            destroyed_accounts: Default::default(),
1263        }
1264    }
1265
1266    #[test]
1267    fn sparse_trie_retained_paths_merges_prune_blocks_with_current_block() {
1268        let blocks: Vec<_> = TestBlockBuilder::eth()
1269            .get_executed_blocks(1..3)
1270            .zip([trie_changed_paths(0x01, 0x02), trie_changed_paths(0x03, 0x04)])
1271            .map(|(block, changed_paths)| with_changed_paths(block, changed_paths))
1272            .collect();
1273        let current_changed_paths = trie_changed_paths(0x05, 0x06);
1274
1275        let retained_paths =
1276            sparse_trie_retained_paths(blocks, &current_changed_paths).unwrap().freeze();
1277
1278        assert_eq!(retained_paths.account_prefix_set.len(), 3);
1279        assert_eq!(retained_paths.storage_prefix_sets.len(), 3);
1280    }
1281
1282    #[test]
1283    fn sparse_trie_retained_paths_skips_prune_when_changed_paths_missing() {
1284        let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..2).collect();
1285        let current_changed_paths = trie_changed_paths(0x01, 0x02);
1286
1287        assert!(sparse_trie_retained_paths(blocks, &current_changed_paths).is_none());
1288    }
1289
1290    fn create_mock_state_updates(num_accounts: usize, updates_per_account: usize) -> Vec<EvmState> {
1291        let mut rng = generators::rng();
1292        let all_addresses: Vec<Address> = (0..num_accounts).map(|_| rng.random()).collect();
1293        let mut updates = Vec::with_capacity(updates_per_account);
1294
1295        for _ in 0..updates_per_account {
1296            let num_accounts_in_update = rng.random_range(1..=num_accounts);
1297            let mut state_update = EvmState::default();
1298
1299            for &address in &all_addresses[0..num_accounts_in_update] {
1300                let mut storage = HashMap::default();
1301                if rng.random_bool(0.7) {
1302                    for _ in 0..rng.random_range(1..10) {
1303                        let slot = U256::from(rng.random::<u64>());
1304                        storage.insert(
1305                            slot,
1306                            EvmStorageSlot::new_changed(
1307                                U256::ZERO,
1308                                U256::from(rng.random::<u64>()),
1309                                TransactionId::ZERO,
1310                            ),
1311                        );
1312                    }
1313                }
1314
1315                let mut account = revm::state::Account::default();
1316                account.info = AccountInfo {
1317                    balance: U256::from(rng.random::<u64>()),
1318                    nonce: rng.random::<u64>(),
1319                    code_hash: KECCAK_EMPTY,
1320                    code: Some(Default::default()),
1321                    account_id: None,
1322                };
1323                account.storage = storage;
1324                account.status = AccountStatus::Touched;
1325                account.transaction_id = TransactionId::ZERO;
1326                state_update.insert(address, account);
1327            }
1328
1329            updates.push(state_update);
1330        }
1331
1332        updates
1333    }
1334
1335    #[test]
1336    fn state_root_task_matches_serial_root() {
1337        reth_tracing::init_test_tracing();
1338
1339        let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1340        let genesis_hash = init_genesis(&factory).unwrap();
1341        let state_updates = create_mock_state_updates(10, 10);
1342        let mut accumulated_state: HashMap<Address, (Account, HashMap<B256, U256>)> =
1343            HashMap::default();
1344
1345        {
1346            let provider_rw = factory.provider_rw().expect("failed to get provider");
1347            for update in &state_updates {
1348                let account_updates = update.iter().map(|(address, account)| {
1349                    (*address, Some(Account::from_revm_account(account)))
1350                });
1351                provider_rw
1352                    .insert_account_for_hashing(account_updates)
1353                    .expect("failed to insert accounts");
1354
1355                let storage_updates = update.iter().map(|(address, account)| {
1356                    let storage_entries = account.storage.iter().map(|(slot, value)| {
1357                        StorageEntry { key: B256::from(*slot), value: value.present_value }
1358                    });
1359                    (*address, storage_entries)
1360                });
1361                provider_rw
1362                    .insert_storage_for_hashing(storage_updates)
1363                    .expect("failed to insert storage");
1364            }
1365            provider_rw.commit().expect("failed to commit changes");
1366        }
1367
1368        for update in &state_updates {
1369            for (address, account) in update {
1370                let storage: HashMap<B256, U256> = account
1371                    .storage
1372                    .iter()
1373                    .map(|(key, value)| (B256::from(*key), value.present_value))
1374                    .collect();
1375                let entry = accumulated_state.entry(*address).or_default();
1376                entry.0 = Account::from_revm_account(account);
1377                entry.1.extend(storage);
1378            }
1379        }
1380
1381        let provider_factory = BlockchainProvider::new(factory).unwrap();
1382        let env: ExecutionEnv<EthEvmConfig> = ExecutionEnv::test_default();
1383        let runtime = reth_tasks::Runtime::test();
1384        let state_trie_overlays = StateTrieOverlayManager::<EthPrimitives>::default();
1385        let mut state_root_handle = DefaultStateRootStrategy::default().spawn_state_root(
1386            &runtime,
1387            &state_trie_overlays,
1388            OverlayStateProviderFactory::new(
1389                provider_factory,
1390                OverlayBuilder::<EthPrimitives>::new(genesis_hash, ChangesetCache::new()),
1391            ),
1392            StateRootTaskOptions {
1393                parent_state_root: env.parent_state_root,
1394                transaction_count: Some(env.transaction_count),
1395                config: &TreeConfig::default(),
1396                pending_sparse_trie_prune_blocks: None,
1397            },
1398        );
1399
1400        let mut state_hook = state_root_handle.take_execution_hook();
1401        for update in state_updates {
1402            state_hook.on_state(update);
1403        }
1404        drop(state_hook);
1405
1406        let root_from_task = state_root_handle.state_root().expect("task failed").state_root;
1407        let root_from_regular = state_root(accumulated_state);
1408        assert_eq!(root_from_task, root_from_regular);
1409    }
1410}