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