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