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    providers::OverlayStateProviderFactory, BlockExecutionOutput, BlockReader,
73    DatabaseProviderFactory, DatabaseProviderROFactory, HashedPostStateProvider, ProviderError,
74    StateProviderFactory, StateReader, StateRootProvider,
75};
76use reth_storage_overlay::OverlayManager;
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
823        + BlockReader<Header = N::BlockHeader>
824        + StateProviderFactory
825        + StateReader
826        + Clone
827        + 'static,
828    OverlayStateProviderFactory<P, N>: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
829        + Clone
830        + Send
831        + Sync
832        + 'static,
833    Evm: ConfigureEvm<Primitives = N> + 'static,
834{
835    fn prepare(
836        &self,
837        mut ctx: StateRootJobContext<'_, N, P, Evm>,
838    ) -> ProviderResult<PreparedStateRootJob<N>> {
839        if ctx.config.skip_state_root() {
840            return Ok(PreparedStateRootJob::new(Box::new(SkippedStateRootJob {}), None))
841        }
842
843        if !ctx.config.use_state_root_task() {
844            return Ok(PreparedStateRootJob::new(
845                Box::new(SynchronousStateRootJob { provider_builder: ctx.provider_builder }),
846                None,
847            ))
848        }
849
850        let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
851        let StateRootJobContext {
852            executor,
853            overlay_manager,
854            env,
855            parent_header,
856            provider_builder,
857            overlay_factory,
858            config,
859            parallel_bal_execution,
860            state: _,
861        } = ctx;
862
863        let preserved_sparse_trie = overlay_manager.take_sparse_trie();
864        let overlay_factory = if let Some(anchor_hash) = preserved_sparse_trie
865            .as_ref()
866            .filter(|trie| trie.state_root() == env.parent_state_root)
867            .map(|trie| trie.anchor_hash())
868        {
869            overlay_factory.with_skip_overlay_for_reused_sparse_trie(anchor_hash)
870        } else {
871            overlay_factory
872        };
873
874        let mut handle = self.spawn_state_root(
875            executor,
876            overlay_manager,
877            overlay_factory.clone(),
878            StateRootTaskOptions {
879                parent_header: parent_header.clone(),
880                preserved_sparse_trie,
881                transaction_count: Some(env.transaction_count),
882                config,
883                pending_sparse_trie_prune_blocks,
884            },
885        );
886
887        // The execution mode decides who finishes the update stream: the execution hook on
888        // the serial path, the BAL streamer on the parallel path. Both come from one slot in
889        // the handle, so only one of them can exist.
890        let (hashed_update_stream, execution_hook): (
891            Option<StateRootUpdateStream>,
892            Option<StateRootUpdateHook>,
893        ) = match parallel_bal_execution {
894            true => (Some(handle.take_hashed_update_stream()), None),
895            false => (None, Some(handle.take_execution_hook())),
896        };
897        let hint_stream = handle.take_hint_stream();
898
899        let hashed_state_rx = Some(handle.take_hashed_state_rx());
900
901        let mut prepared = PreparedStateRootJob::new(
902            Box::new(SparseTrieStateRootJob {
903                handle,
904                provider_builder,
905                overlay_factory,
906                executor: executor.clone(),
907                timeout: config.state_root_task_timeout(),
908                compare_trie_updates: config.always_compare_trie_updates(),
909                metrics: BlockValidationMetrics::default(),
910            }),
911            hashed_state_rx,
912        )
913        .with_hint_stream(hint_stream);
914        if let Some(hook) = execution_hook {
915            prepared = prepared.with_execution_hook(hook);
916        }
917        if let Some(stream) = hashed_update_stream {
918            prepared = prepared.with_hashed_update_stream(stream);
919        }
920        Ok(prepared)
921    }
922
923    fn prepare_payload_builder(
924        &self,
925        mut ctx: PayloadStateRootJobContext<'_, N, P>,
926    ) -> ProviderResult<Option<PayloadStateRootHandle>> {
927        // Sharing the engine state-root task with the payload builder is opt-in, and needs a
928        // host that can run the task pipeline at all.
929        if !ctx.config.share_sparse_trie_with_payload_builder() ||
930            ctx.config.skip_state_root() ||
931            !ctx.config.has_enough_parallelism()
932        {
933            return Ok(None)
934        }
935
936        let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
937        let parent_state_root = ctx.parent_state_root();
938        let parent_header = SealedHeader::new(ctx.parent_header().clone(), ctx.parent_hash());
939        let preserved_sparse_trie = ctx.overlay_manager.take_sparse_trie();
940        let overlay_factory = if let Some(anchor_hash) = preserved_sparse_trie
941            .as_ref()
942            .filter(|trie| trie.state_root() == parent_state_root)
943            .map(|trie| trie.anchor_hash())
944        {
945            ctx.overlay_factory.clone().with_skip_overlay_for_reused_sparse_trie(anchor_hash)
946        } else {
947            ctx.overlay_factory.clone()
948        };
949        Ok(Some(
950            self.spawn_state_root(
951                ctx.executor,
952                ctx.overlay_manager,
953                overlay_factory,
954                StateRootTaskOptions {
955                    parent_header,
956                    preserved_sparse_trie,
957                    // Tx count unknown at FCU time (block built incrementally): full proof workers.
958                    transaction_count: None,
959                    config: ctx.config,
960                    pending_sparse_trie_prune_blocks,
961                },
962            )
963            .into_payload_state_root_handle(),
964        ))
965    }
966}
967
968#[derive(Debug)]
969struct SkippedStateRootJob {}
970
971impl<N: NodePrimitives> StateRootJob<N> for SkippedStateRootJob {
972    fn name(&self) -> &'static str {
973        "skipped"
974    }
975
976    fn finish(
977        &mut self,
978        block: &RecoveredBlock<N::Block>,
979        _output: Arc<BlockExecutionOutput<N::Receipt>>,
980        _hashed_state: &LazyHashedPostState,
981    ) -> ProviderResult<StateRootJobOutcome> {
982        Ok(StateRootJobOutcome::new(block.header().state_root(), Arc::new(TrieUpdates::default())))
983    }
984}
985
986#[derive(Debug)]
987struct SynchronousStateRootJob<N: NodePrimitives, P> {
988    provider_builder: StateProviderBuilder<N, P>,
989}
990
991impl<N, P> StateRootJob<N> for SynchronousStateRootJob<N, P>
992where
993    N: NodePrimitives,
994    P: BlockReader + StateProviderFactory + StateReader + Clone + Send + Sync + 'static,
995{
996    fn name(&self) -> &'static str {
997        "synchronous"
998    }
999
1000    fn finish(
1001        &mut self,
1002        _block: &RecoveredBlock<N::Block>,
1003        _output: Arc<BlockExecutionOutput<N::Receipt>>,
1004        hashed_state: &LazyHashedPostState,
1005    ) -> ProviderResult<StateRootJobOutcome> {
1006        let provider = self.provider_builder.clone().build()?;
1007        let (state_root, trie_updates) =
1008            provider.state_root_with_updates(hashed_state.get().as_ref().clone())?;
1009        Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates)))
1010    }
1011}
1012
1013#[derive(Debug)]
1014struct SparseTrieStateRootJob<N: NodePrimitives, P> {
1015    handle: StateRootHandle,
1016    provider_builder: StateProviderBuilder<N, P>,
1017    overlay_factory: OverlayStateProviderFactory<P, N>,
1018    executor: reth_tasks::Runtime,
1019    timeout: Option<Duration>,
1020    compare_trie_updates: bool,
1021    metrics: BlockValidationMetrics,
1022}
1023
1024impl<N, P> SparseTrieStateRootJob<N, P>
1025where
1026    N: NodePrimitives,
1027    P: StateProviderFactory + Clone + Send + Sync + 'static,
1028    P: BlockReader + StateReader,
1029    OverlayStateProviderFactory<P, N>: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
1030        + Clone
1031        + Send
1032        + Sync
1033        + 'static,
1034{
1035    fn serial_fallback(
1036        executor: &reth_tasks::Runtime,
1037        provider_builder: StateProviderBuilder<N, P>,
1038        output: Arc<BlockExecutionOutput<N::Receipt>>,
1039    ) -> ProviderResult<SerialFallbackRx> {
1040        let provider = provider_builder.build()?;
1041        let (fallback_tx, fallback_rx) = mpsc::channel();
1042        executor.spawn_blocking_named("serial-root", move || {
1043            let result = (|| {
1044                let hashed_state = Arc::new(provider.hashed_post_state(&output.state));
1045                let (root, updates) =
1046                    provider.state_root_with_updates(hashed_state.as_ref().clone())?;
1047                Ok((root, updates, hashed_state))
1048            })();
1049            let _ = fallback_tx.send(result);
1050        });
1051
1052        Ok(fallback_rx)
1053    }
1054
1055    /// Recomputes the state root serially from the execution output.
1056    ///
1057    /// Used when the state-root task failed or produced a wrong root, so the recomputed hashed
1058    /// post state is returned in the outcome for validation to re-check against.
1059    fn compute_serial(
1060        &self,
1061        output: &BlockExecutionOutput<N::Receipt>,
1062    ) -> ProviderResult<StateRootJobOutcome> {
1063        let provider = self.provider_builder.clone().build()?;
1064        let hashed_state = Arc::new(provider.hashed_post_state(&output.state));
1065        let (state_root, trie_updates) =
1066            provider.state_root_with_updates(hashed_state.as_ref().clone())?;
1067        self.metrics.state_root_task_fallback_success_total.increment(1);
1068        Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1069            .with_hashed_state(Some(hashed_state)))
1070    }
1071
1072    /// Converts a task outcome into a job outcome, recomputing serially when the task returned
1073    /// a root that does not match the block header. A state-root-task bug then costs latency
1074    /// instead of marking a valid block invalid; if the serial root also mismatches, validation
1075    /// rejects the block.
1076    fn verified_sparse_outcome(
1077        &self,
1078        block: &RecoveredBlock<N::Block>,
1079        output: &BlockExecutionOutput<N::Receipt>,
1080        outcome: StateRootComputeOutcome,
1081    ) -> ProviderResult<StateRootJobOutcome> {
1082        let outcome = self.sparse_outcome(block, output, outcome);
1083        if outcome.state_root == block.header().state_root() {
1084            return Ok(outcome)
1085        }
1086        warn!(
1087            target: "engine::tree::state_root_strategy",
1088            state_root = ?outcome.state_root,
1089            block_state_root = ?block.header().state_root(),
1090            "State root task returned incorrect state root, recomputing serially"
1091        );
1092        self.compute_serial(output)
1093    }
1094
1095    fn sparse_outcome(
1096        &self,
1097        _block: &RecoveredBlock<N::Block>,
1098        output: &BlockExecutionOutput<N::Receipt>,
1099        outcome: StateRootComputeOutcome,
1100    ) -> StateRootJobOutcome {
1101        let StateRootComputeOutcome {
1102            state_root,
1103            trie_updates,
1104            hashed_state: _hashed_state,
1105            #[cfg(feature = "trie-debug")]
1106            debug_recorders,
1107        } = outcome;
1108
1109        if self.compare_trie_updates {
1110            let _has_diff = compare_trie_updates_with_serial(
1111                self.provider_builder.clone(),
1112                self.overlay_factory.clone(),
1113                output,
1114                trie_updates.as_ref().clone(),
1115            );
1116            #[cfg(feature = "trie-debug")]
1117            if _has_diff {
1118                write_trie_debug_recorders(_block.header().number(), &debug_recorders);
1119            }
1120        }
1121
1122        #[cfg(feature = "trie-debug")]
1123        if state_root != _block.header().state_root() {
1124            write_trie_debug_recorders(_block.header().number(), &debug_recorders);
1125        }
1126
1127        StateRootJobOutcome::new(state_root, trie_updates)
1128    }
1129}
1130
1131impl<N, P> StateRootJob<N> for SparseTrieStateRootJob<N, P>
1132where
1133    N: NodePrimitives,
1134    P: BlockReader + StateProviderFactory + StateReader + Clone + Send + Sync + 'static,
1135    OverlayStateProviderFactory<P, N>: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
1136        + Clone
1137        + Send
1138        + Sync
1139        + 'static,
1140{
1141    fn name(&self) -> &'static str {
1142        "sparse-trie"
1143    }
1144
1145    fn finish(
1146        &mut self,
1147        block: &RecoveredBlock<N::Block>,
1148        output: Arc<BlockExecutionOutput<N::Receipt>>,
1149        _hashed_state: &LazyHashedPostState,
1150    ) -> ProviderResult<StateRootJobOutcome> {
1151        if self.timeout.is_none() {
1152            return match self.handle.state_root() {
1153                Ok(outcome) => self.verified_sparse_outcome(block, &output, outcome),
1154                Err(err) => {
1155                    debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1156                    self.compute_serial(&output)
1157                }
1158            }
1159        }
1160
1161        let timeout = self.timeout.expect("checked above");
1162        let task_rx = self.handle.take_state_root_rx();
1163        let fallback_rx = match task_rx.recv_timeout(timeout) {
1164            Ok(Ok(outcome)) => return self.verified_sparse_outcome(block, &output, outcome),
1165            Ok(Err(err)) => {
1166                debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1167                Self::serial_fallback(
1168                    &self.executor,
1169                    self.provider_builder.clone(),
1170                    output.clone(),
1171                )?
1172            }
1173            Err(RecvTimeoutError::Timeout) => {
1174                warn!(target: "engine::tree::state_root_strategy", ?timeout, "State root task timed out, racing serial fallback");
1175                self.metrics.state_root_task_timeout_total.increment(1);
1176                Self::serial_fallback(
1177                    &self.executor,
1178                    self.provider_builder.clone(),
1179                    output.clone(),
1180                )?
1181            }
1182            Err(RecvTimeoutError::Disconnected) => {
1183                debug!(target: "engine::tree::state_root_strategy", "State root task dropped, falling back to serial root");
1184                Self::serial_fallback(
1185                    &self.executor,
1186                    self.provider_builder.clone(),
1187                    output.clone(),
1188                )?
1189            }
1190        };
1191
1192        loop {
1193            if let Ok(Ok(outcome)) = task_rx.try_recv() {
1194                let outcome = self.sparse_outcome(block, &output, outcome);
1195                if outcome.state_root == block.header().state_root() {
1196                    return Ok(outcome)
1197                }
1198                // A wrong task root falls through to the serial fallback already racing below.
1199                warn!(
1200                    target: "engine::tree::state_root_strategy",
1201                    state_root = ?outcome.state_root,
1202                    block_state_root = ?block.header().state_root(),
1203                    "State root task returned incorrect state root, using serial fallback"
1204                );
1205            }
1206
1207            match fallback_rx.try_recv() {
1208                Ok(Ok((state_root, trie_updates, hashed_state))) => {
1209                    self.metrics.state_root_task_fallback_success_total.increment(1);
1210                    return Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1211                        .with_hashed_state(Some(hashed_state)))
1212                }
1213                Ok(Err(err)) => return Err(err),
1214                Err(mpsc::TryRecvError::Empty) => {}
1215                Err(mpsc::TryRecvError::Disconnected) => {
1216                    return Err(ProviderError::other(std::io::Error::other(
1217                        "serial state root fallback task dropped",
1218                    )))
1219                }
1220            }
1221
1222            std::thread::sleep(Duration::from_millis(1));
1223        }
1224    }
1225}
1226
1227fn compare_trie_updates_with_serial<N, P>(
1228    state_provider_builder: StateProviderBuilder<N, P>,
1229    overlay_factory: OverlayStateProviderFactory<P, N>,
1230    output: &BlockExecutionOutput<N::Receipt>,
1231    task_trie_updates: TrieUpdates,
1232) -> bool
1233where
1234    N: NodePrimitives,
1235    P: BlockReader + StateProviderFactory + StateReader + Clone,
1236    OverlayStateProviderFactory<P, N>:
1237        DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>,
1238{
1239    debug!(target: "engine::tree::state_root_strategy", "Comparing trie updates with serial computation");
1240
1241    match state_provider_builder.build().and_then(|provider| {
1242        let hashed_state = provider.hashed_post_state(&output.state);
1243        provider.state_root_with_updates(hashed_state)
1244    }) {
1245        Ok((serial_root, serial_trie_updates)) => {
1246            debug!(
1247                target: "engine::tree::state_root_strategy",
1248                ?serial_root,
1249                "Serial state root computation finished for comparison"
1250            );
1251
1252            match overlay_factory.database_provider_ro() {
1253                Ok(provider) => match super::trie_updates::compare_trie_updates(
1254                    &provider,
1255                    task_trie_updates,
1256                    serial_trie_updates,
1257                ) {
1258                    Ok(has_diff) => return has_diff,
1259                    Err(err) => {
1260                        warn!(
1261                            target: "engine::tree::state_root_strategy",
1262                            %err,
1263                            "Error comparing trie updates"
1264                        );
1265                        return true;
1266                    }
1267                },
1268                Err(err) => {
1269                    warn!(
1270                        target: "engine::tree::state_root_strategy",
1271                        %err,
1272                        "Failed to get database provider for trie update comparison"
1273                    );
1274                }
1275            }
1276        }
1277        Err(err) => {
1278            warn!(
1279                target: "engine::tree::state_root_strategy",
1280                %err,
1281                "Failed to compute serial state root for comparison"
1282            );
1283        }
1284    }
1285    false
1286}
1287
1288/// Writes trie debug recorders to a JSON file for the given block number.
1289///
1290/// The file is written to the current working directory as `trie_debug_block_{block_number}.json`.
1291#[cfg(feature = "trie-debug")]
1292fn write_trie_debug_recorders(block_number: u64, recorders: &[(Option<B256>, TrieDebugRecorder)]) {
1293    let path = format!("trie_debug_block_{block_number}.json");
1294    match serde_json::to_string_pretty(recorders) {
1295        Ok(json) => match std::fs::write(&path, json) {
1296            Ok(()) => {
1297                warn!(
1298                    target: "engine::tree::state_root_strategy",
1299                    %path,
1300                    "Wrote trie debug recorders to file"
1301                );
1302            }
1303            Err(err) => {
1304                warn!(
1305                    target: "engine::tree::state_root_strategy",
1306                    %err,
1307                    %path,
1308                    "Failed to write trie debug recorders"
1309                );
1310            }
1311        },
1312        Err(err) => {
1313            warn!(
1314                target: "engine::tree::state_root_strategy",
1315                %err,
1316                "Failed to serialize trie debug recorders"
1317            );
1318        }
1319    }
1320}
1321
1322#[cfg(test)]
1323mod tests {
1324    use super::*;
1325    use alloy_consensus::constants::KECCAK_EMPTY;
1326    use alloy_primitives::{map::HashMap, Address, U256};
1327    use rand::Rng;
1328    use reth_chain_state::test_utils::TestBlockBuilder;
1329    use reth_chainspec::ChainSpec;
1330    use reth_db_common::init::init_genesis;
1331    use reth_ethereum_primitives::EthPrimitives;
1332    use reth_evm::OnStateHook;
1333    use reth_evm_ethereum::EthEvmConfig;
1334    use reth_primitives_traits::{Account, StorageEntry};
1335    use reth_provider::{
1336        providers::{BlockchainProvider, OverlayStateProviderFactory},
1337        test_utils::create_test_provider_factory_with_chain_spec,
1338        HashingWriter,
1339    };
1340    use reth_storage_overlay::OverlayManager;
1341    use reth_testing_utils::generators;
1342    use reth_trie::test_utils::state_root;
1343    use revm::state::{AccountInfo, AccountStatus, EvmState, EvmStorageSlot, TransactionId};
1344
1345    #[test]
1346    fn sparse_trie_prune_before_uses_requested_range() {
1347        let new_epoch = TrieNodeEpoch::new(10);
1348        assert_eq!(sparse_trie_prune_before::<EthPrimitives>(None, new_epoch), None);
1349        assert_eq!(
1350            sparse_trie_prune_before::<EthPrimitives>(Some(&[]), new_epoch),
1351            Some(new_epoch)
1352        );
1353
1354        let mut blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(7..10).collect();
1355        blocks.reverse();
1356
1357        assert_eq!(sparse_trie_prune_before(Some(&blocks), new_epoch), Some(TrieNodeEpoch::new(7)));
1358    }
1359
1360    #[test]
1361    fn published_sparse_trie_anchor_advances_to_prune_anchor() {
1362        let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..5).collect();
1363        let reused_anchor_hash = blocks[0].recovered_block().hash();
1364        let expected_prune_anchor = blocks[1].recovered_block().hash();
1365        let prune_blocks: Vec<_> = blocks.into_iter().skip(2).rev().collect();
1366
1367        assert_eq!(
1368            published_sparse_trie_anchor_hash(reused_anchor_hash, true, Some(&prune_blocks)),
1369            expected_prune_anchor
1370        );
1371    }
1372
1373    #[test]
1374    fn published_sparse_trie_anchor_does_not_move_backwards_when_anchor_is_in_prune_range() {
1375        let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..5).collect();
1376        let reused_anchor_hash = blocks[2].recovered_block().hash();
1377        let mut prune_blocks = blocks;
1378        prune_blocks.reverse();
1379        let prune_anchor = prune_blocks.last().unwrap().recovered_block().parent_hash();
1380
1381        assert_ne!(reused_anchor_hash, prune_anchor);
1382        assert_eq!(
1383            published_sparse_trie_anchor_hash(reused_anchor_hash, true, Some(&prune_blocks)),
1384            reused_anchor_hash
1385        );
1386    }
1387
1388    #[test]
1389    fn published_sparse_trie_anchor_keeps_parent_for_fresh_trie() {
1390        let mut blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..3).collect();
1391        blocks.reverse();
1392        let parent_hash = B256::with_last_byte(0xaa);
1393
1394        assert_eq!(
1395            published_sparse_trie_anchor_hash(parent_hash, false, Some(&blocks)),
1396            parent_hash
1397        );
1398    }
1399
1400    fn create_mock_state_updates(num_accounts: usize, updates_per_account: usize) -> Vec<EvmState> {
1401        let mut rng = generators::rng();
1402        let all_addresses: Vec<Address> = (0..num_accounts).map(|_| rng.random()).collect();
1403        let mut updates = Vec::with_capacity(updates_per_account);
1404
1405        for _ in 0..updates_per_account {
1406            let num_accounts_in_update = rng.random_range(1..=num_accounts);
1407            let mut state_update = EvmState::default();
1408
1409            for &address in &all_addresses[0..num_accounts_in_update] {
1410                let mut storage = HashMap::default();
1411                if rng.random_bool(0.7) {
1412                    for _ in 0..rng.random_range(1..10) {
1413                        let slot = U256::from(rng.random::<u64>());
1414                        storage.insert(
1415                            slot,
1416                            EvmStorageSlot::new_changed(
1417                                U256::ZERO,
1418                                U256::from(rng.random::<u64>()),
1419                                TransactionId::ZERO,
1420                            ),
1421                        );
1422                    }
1423                }
1424
1425                let mut account = revm::state::Account::default();
1426                account.info = AccountInfo {
1427                    balance: U256::from(rng.random::<u64>()),
1428                    nonce: rng.random::<u64>(),
1429                    code_hash: KECCAK_EMPTY,
1430                    code: Some(Default::default()),
1431                    account_id: None,
1432                };
1433                account.storage = storage;
1434                account.status = AccountStatus::Touched;
1435                account.transaction_id = TransactionId::ZERO;
1436                state_update.insert(address, account);
1437            }
1438
1439            updates.push(state_update);
1440        }
1441
1442        updates
1443    }
1444
1445    #[test]
1446    fn state_root_task_matches_serial_root() {
1447        reth_tracing::init_test_tracing();
1448
1449        let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1450        let genesis_hash = init_genesis(&factory).unwrap();
1451        let state_updates = create_mock_state_updates(10, 10);
1452        let mut accumulated_state: HashMap<Address, (Account, HashMap<B256, U256>)> =
1453            HashMap::default();
1454
1455        {
1456            let provider_rw = factory.provider_rw().expect("failed to get provider");
1457            for update in &state_updates {
1458                let account_updates = update.iter().map(|(address, account)| {
1459                    (*address, Some(Account::from_revm_account(account)))
1460                });
1461                provider_rw
1462                    .insert_account_for_hashing(account_updates)
1463                    .expect("failed to insert accounts");
1464
1465                let storage_updates = update.iter().map(|(address, account)| {
1466                    let storage_entries = account.storage.iter().map(|(slot, value)| {
1467                        StorageEntry { key: B256::from(*slot), value: value.present_value }
1468                    });
1469                    (*address, storage_entries)
1470                });
1471                provider_rw
1472                    .insert_storage_for_hashing(storage_updates)
1473                    .expect("failed to insert storage");
1474            }
1475            provider_rw.commit().expect("failed to commit changes");
1476        }
1477
1478        for update in &state_updates {
1479            for (address, account) in update {
1480                let storage: HashMap<B256, U256> = account
1481                    .storage
1482                    .iter()
1483                    .map(|(key, value)| (B256::from(*key), value.present_value))
1484                    .collect();
1485                let entry = accumulated_state.entry(*address).or_default();
1486                entry.0 = Account::from_revm_account(account);
1487                entry.1.extend(storage);
1488            }
1489        }
1490
1491        let provider_factory = BlockchainProvider::new(factory).unwrap();
1492        let env: ExecutionEnv<EthEvmConfig> = ExecutionEnv::test_default();
1493        let runtime = reth_tasks::Runtime::test();
1494        let overlay_manager = OverlayManager::<EthPrimitives>::default();
1495        let mut state_root_handle = DefaultStateRootStrategy::default().spawn_state_root(
1496            &runtime,
1497            &overlay_manager,
1498            OverlayStateProviderFactory::new(
1499                provider_factory,
1500                overlay_manager.overlay_builder(genesis_hash),
1501            ),
1502            StateRootTaskOptions {
1503                parent_header: SealedHeader::new(Default::default(), genesis_hash),
1504                preserved_sparse_trie: None,
1505                transaction_count: Some(env.transaction_count),
1506                config: &TreeConfig::default(),
1507                pending_sparse_trie_prune_blocks: None,
1508            },
1509        );
1510
1511        let mut state_hook = state_root_handle.take_execution_hook();
1512        for update in state_updates {
1513            state_hook.on_state(update);
1514        }
1515        drop(state_hook);
1516
1517        let root_from_task = state_root_handle.state_root().expect("task failed").state_root;
1518        let root_from_regular = state_root(accumulated_state);
1519        assert_eq!(root_from_task, root_from_regular);
1520    }
1521}