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;
62use reth_chain_state::{ExecutedBlock, PreservedSparseTrie, SparseTrieWithStorageRoots};
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::{updates::TrieUpdates, HashedPostState};
76use reth_trie_parallel::proof_task::{
77    metrics::ProofWorkerIdleMetrics, ProofTaskCtx, ProofWorkerCounts, ProofWorkerHandle,
78};
79pub use reth_trie_parallel::{
80    error::StateRootTaskError,
81    state_root_task::{
82        evm_state_to_hashed_post_state, PayloadStateRootHandle, StateAccessHint,
83        StateRootComputeOutcome, StateRootHandle, StateRootHintStream, StateRootMessage,
84        StateRootTaskCancelGuard, StateRootUpdateHook, StateRootUpdateStream,
85    },
86};
87use reth_trie_sparse::{
88    ArenaParallelSparseTrie, RevealableSparseTrie, SparseStateTrie, TrieNodeEpoch,
89};
90use std::{
91    fmt,
92    sync::{
93        mpsc::{self, RecvTimeoutError},
94        Arc,
95    },
96    time::Duration,
97};
98use tracing::{debug, debug_span, instrument, warn, Span};
99
100/// Handle to a [`HashedPostState`] computed on a background thread.
101pub type LazyHashedPostState = reth_tasks::LazyHandle<Arc<HashedPostState>>;
102
103/// Strategy used by engine-tree validation to prepare per-block state-root work.
104pub trait StateRootStrategy<N, P, Evm>: Send + Sync
105where
106    N: NodePrimitives,
107    Evm: ConfigureEvm<Primitives = N>,
108{
109    /// Prepares a per-block state-root job before execution starts.
110    ///
111    /// A custom strategy that maintains a reusable sparse trie is responsible for consuming the
112    /// pending prune request from the context when it starts the corresponding job.
113    fn prepare(
114        &self,
115        ctx: StateRootJobContext<'_, N, P, Evm>,
116    ) -> ProviderResult<PreparedStateRootJob<N>>;
117
118    /// Prepares the optional payload-builder state-root handle used for FCU-triggered block
119    /// building.
120    ///
121    /// This is consulted on every FCU that carries payload attributes. Returning `None` means the
122    /// payload builder computes the state root itself; the stock builders fall back to a
123    /// synchronous MPT state root. The default implementation returns `None`.
124    fn prepare_payload_builder(
125        &self,
126        _ctx: PayloadStateRootJobContext<'_, N, P>,
127    ) -> ProviderResult<Option<PayloadStateRootHandle>> {
128        Ok(None)
129    }
130}
131
132/// Data available while preparing one payload-builder state-root handle.
133pub struct PayloadStateRootJobContext<'a, N, P>
134where
135    N: NodePrimitives,
136{
137    executor: &'a reth_tasks::Runtime,
138    overlay_manager: &'a OverlayManager<N>,
139    parent_hash: B256,
140    parent_header: &'a N::BlockHeader,
141    timestamp: u64,
142    state: &'a mut EngineApiTreeState<N>,
143    state_provider_factory: OverlayStateProviderFactory<P, N>,
144    config: &'a TreeConfig,
145}
146
147impl<N, P> fmt::Debug for PayloadStateRootJobContext<'_, N, P>
148where
149    N: NodePrimitives,
150{
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        f.debug_struct("PayloadStateRootJobContext")
153            .field("parent_hash", &self.parent_hash)
154            .field("parent_state_root", &self.parent_state_root())
155            .field("timestamp", &self.timestamp)
156            .field("pending_sparse_trie_prune", &self.state.pending_sparse_trie_prune())
157            .finish_non_exhaustive()
158    }
159}
160
161impl<'a, N, P> PayloadStateRootJobContext<'a, N, P>
162where
163    N: NodePrimitives,
164{
165    /// Creates a payload-builder state-root job context.
166    #[expect(clippy::too_many_arguments)]
167    pub(crate) const fn new(
168        executor: &'a reth_tasks::Runtime,
169        overlay_manager: &'a OverlayManager<N>,
170        parent_hash: B256,
171        parent_header: &'a N::BlockHeader,
172        timestamp: u64,
173        state: &'a mut EngineApiTreeState<N>,
174        state_provider_factory: OverlayStateProviderFactory<P, N>,
175        config: &'a TreeConfig,
176    ) -> Self {
177        Self {
178            executor,
179            overlay_manager,
180            parent_hash,
181            parent_header,
182            timestamp,
183            state,
184            state_provider_factory,
185            config,
186        }
187    }
188
189    /// Returns the parent block hash for the payload being built.
190    pub const fn parent_hash(&self) -> B256 {
191        self.parent_hash
192    }
193
194    /// Returns the parent block header for the payload being built.
195    ///
196    /// This is the chain's concrete header type, so chain-specific strategies can read
197    /// chain-specific fields, and number-activated forks can dispatch on the parent number.
198    pub const fn parent_header(&self) -> &N::BlockHeader {
199        self.parent_header
200    }
201
202    /// Returns the parent state root for the payload being built.
203    pub fn parent_state_root(&self) -> B256 {
204        self.parent_header.state_root()
205    }
206
207    /// Returns the timestamp of the payload being built, taken from the payload attributes.
208    ///
209    /// Strategies that switch behavior at a fork activation can dispatch on this value.
210    pub const fn timestamp(&self) -> u64 {
211        self.timestamp
212    }
213
214    /// Returns the task runtime used by state-root work.
215    pub const fn executor(&self) -> &reth_tasks::Runtime {
216        self.executor
217    }
218
219    /// Consumes the pending sparse trie prune request as in-memory parent-chain blocks, if any.
220    ///
221    /// Custom strategies that maintain a reusable sparse trie should call this when starting the
222    /// corresponding job. Strategies that do not use the request should leave it pending.
223    pub fn take_sparse_trie_prune_blocks(&mut self) -> Option<Vec<ExecutedBlock<N>>> {
224        self.state.take_sparse_trie_prune_blocks(self.parent_hash)
225    }
226}
227
228/// Data available while preparing one state-root job.
229pub struct StateRootJobContext<'a, N, P, Evm>
230where
231    N: NodePrimitives,
232    Evm: ConfigureEvm<Primitives = N>,
233{
234    executor: &'a reth_tasks::Runtime,
235    overlay_manager: &'a OverlayManager<N>,
236    env: &'a ExecutionEnv<Evm>,
237    parent_header: &'a SealedHeader<N::BlockHeader>,
238    state_provider_factory: OverlayStateProviderFactory<P, N>,
239    config: &'a TreeConfig,
240    parallel_bal_execution: bool,
241    state: &'a mut EngineApiTreeState<N>,
242}
243
244impl<N, P, Evm> fmt::Debug for StateRootJobContext<'_, N, P, Evm>
245where
246    N: NodePrimitives,
247    Evm: ConfigureEvm<Primitives = N>,
248{
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        f.debug_struct("StateRootJobContext")
251            .field("parallel_bal_execution", &self.parallel_bal_execution)
252            .field("has_pending_sparse_trie_prune", &self.state.pending_sparse_trie_prune())
253            .finish_non_exhaustive()
254    }
255}
256
257impl<'a, N, P, Evm> StateRootJobContext<'a, N, P, Evm>
258where
259    N: NodePrimitives,
260    Evm: ConfigureEvm<Primitives = N>,
261{
262    /// Creates a new state-root job context.
263    #[expect(clippy::too_many_arguments)]
264    pub(crate) const fn new(
265        executor: &'a reth_tasks::Runtime,
266        overlay_manager: &'a OverlayManager<N>,
267        env: &'a ExecutionEnv<Evm>,
268        parent_header: &'a SealedHeader<N::BlockHeader>,
269        state_provider_factory: OverlayStateProviderFactory<P, N>,
270        config: &'a TreeConfig,
271        parallel_bal_execution: bool,
272        state: &'a mut EngineApiTreeState<N>,
273    ) -> Self {
274        Self {
275            executor,
276            overlay_manager,
277            env,
278            parent_header,
279            state_provider_factory,
280            config,
281            parallel_bal_execution,
282            state,
283        }
284    }
285
286    /// Returns the execution environment for the block.
287    pub const fn env(&self) -> &ExecutionEnv<Evm> {
288        self.env
289    }
290
291    /// Returns the sealed parent block header.
292    pub const fn parent_header(&self) -> &SealedHeader<N::BlockHeader> {
293        self.parent_header
294    }
295
296    /// Returns the task runtime used by state-root work.
297    pub const fn executor(&self) -> &reth_tasks::Runtime {
298        self.executor
299    }
300
301    /// Returns true when validation will use the parallel BAL execution path.
302    pub const fn parallel_bal_execution(&self) -> bool {
303        self.parallel_bal_execution
304    }
305
306    /// Consumes the pending sparse trie prune request as in-memory parent-chain blocks, if any.
307    ///
308    /// Custom strategies that maintain a reusable sparse trie should call this when starting the
309    /// corresponding job. Strategies that do not use the request should leave it pending.
310    pub fn take_sparse_trie_prune_blocks(&mut self) -> Option<Vec<ExecutedBlock<N>>> {
311        self.state.take_sparse_trie_prune_blocks(self.env.parent_hash)
312    }
313}
314
315/// Prepared per-block state-root work and its update-stream capabilities.
316///
317/// The capabilities are populated by the strategy's `prepare` according to the execution
318/// mode: the execution hook on the serial path, the hashed update stream on the parallel BAL
319/// path, never both. Each capability is taken once by the code that produces its messages
320/// and is not retained here, so the task's update channel closes when the producers are done.
321pub struct PreparedStateRootJob<N: NodePrimitives> {
322    job: Box<dyn StateRootJob<N>>,
323    execution_hook: Option<StateRootUpdateHook>,
324    hint_stream: Option<StateRootHintStream>,
325    hashed_update_stream: Option<StateRootUpdateStream>,
326    hashed_state_rx: Option<mpsc::Receiver<Arc<HashedPostState>>>,
327}
328
329impl<N: NodePrimitives> fmt::Debug for PreparedStateRootJob<N> {
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        f.debug_struct("PreparedStateRootJob")
332            .field("name", &self.job.name())
333            .field("has_execution_hook", &self.execution_hook.is_some())
334            .field("has_hint_stream", &self.hint_stream.is_some())
335            .field("has_hashed_update_stream", &self.hashed_update_stream.is_some())
336            .field("has_hashed_state_rx", &self.hashed_state_rx.is_some())
337            .finish()
338    }
339}
340
341impl<N: NodePrimitives> PreparedStateRootJob<N> {
342    /// Creates a prepared state-root job without update-stream capabilities.
343    pub const fn new(
344        job: Box<dyn StateRootJob<N>>,
345        hashed_state_rx: Option<mpsc::Receiver<Arc<HashedPostState>>>,
346    ) -> Self {
347        Self {
348            job,
349            execution_hook: None,
350            hint_stream: None,
351            hashed_update_stream: None,
352            hashed_state_rx,
353        }
354    }
355
356    /// Attaches the execution hook capability (serial execution path).
357    pub fn with_execution_hook(mut self, hook: StateRootUpdateHook) -> Self {
358        self.execution_hook = Some(hook);
359        self
360    }
361
362    /// Attaches the hint stream capability.
363    pub fn with_hint_stream(mut self, hint_stream: StateRootHintStream) -> Self {
364        self.hint_stream = Some(hint_stream);
365        self
366    }
367
368    /// Attaches the hashed update stream capability (parallel BAL path).
369    pub fn with_hashed_update_stream(mut self, stream: StateRootUpdateStream) -> Self {
370        self.hashed_update_stream = Some(stream);
371        self
372    }
373
374    /// Returns the job name used in logs.
375    pub fn name(&self) -> &'static str {
376        self.job.name()
377    }
378
379    /// Takes the execution hook, present only when the job wants normal execution updates.
380    pub fn take_execution_hook(&mut self) -> Option<Box<dyn OnStateHook + 'static>> {
381        self.execution_hook.take().map(|hook| Box::new(hook) as Box<dyn OnStateHook + 'static>)
382    }
383
384    /// Takes the hint stream for transaction prewarming.
385    pub const fn take_hint_stream(&mut self) -> Option<StateRootHintStream> {
386        self.hint_stream.take()
387    }
388
389    /// Takes the hashed update stream, present only on the parallel BAL path.
390    pub const fn take_hashed_update_stream(&mut self) -> Option<StateRootUpdateStream> {
391        self.hashed_update_stream.take()
392    }
393
394    /// Takes the optional hashed-state receiver produced by the job.
395    ///
396    /// The sender behind a returned receiver must either deliver one value or be dropped;
397    /// validation blocks on it while hashing the post state, so a job that keeps the sender
398    /// alive without sending stalls block validation.
399    pub const fn take_hashed_state_rx(&mut self) -> Option<mpsc::Receiver<Arc<HashedPostState>>> {
400        self.hashed_state_rx.take()
401    }
402
403    /// Completes the job after execution.
404    pub fn finish(
405        &mut self,
406        block: &RecoveredBlock<N::Block>,
407        output: Arc<BlockExecutionOutput<N::Receipt>>,
408        hashed_state: &LazyHashedPostState,
409    ) -> ProviderResult<StateRootJobOutcome> {
410        self.job.finish(block, output, hashed_state)
411    }
412}
413
414/// Per-block state-root job prepared before execution and finished after execution.
415pub trait StateRootJob<N: NodePrimitives>: Send {
416    /// Human-readable strategy name used in logs.
417    fn name(&self) -> &'static str;
418
419    /// Completes the job after execution.
420    ///
421    /// Called at most once per prepared job; implementations may panic if called again.
422    fn finish(
423        &mut self,
424        block: &RecoveredBlock<N::Block>,
425        output: Arc<BlockExecutionOutput<N::Receipt>>,
426        hashed_state: &LazyHashedPostState,
427    ) -> ProviderResult<StateRootJobOutcome>;
428}
429
430/// Outcome of a per-block state-root job.
431#[derive(Debug)]
432pub struct StateRootJobOutcome {
433    /// Computed state root.
434    pub state_root: B256,
435    /// Trie updates associated with the computed state root.
436    pub trie_updates: Arc<TrieUpdates>,
437    /// Hashed post state recomputed by a fallback path.
438    ///
439    /// When set, the root was not derived from the streamed updates, so validation replaces its
440    /// streaming-derived hashed post state with this one and re-runs hashed-state checks.
441    pub hashed_state: Option<Arc<HashedPostState>>,
442}
443
444impl StateRootJobOutcome {
445    /// Creates a state-root job outcome.
446    pub const fn new(state_root: B256, trie_updates: Arc<TrieUpdates>) -> Self {
447        Self { state_root, trie_updates, hashed_state: None }
448    }
449
450    /// Sets the hashed post state recomputed by a fallback path.
451    pub fn with_hashed_state(mut self, hashed_state: Option<Arc<HashedPostState>>) -> Self {
452        self.hashed_state = hashed_state;
453        self
454    }
455}
456
457/// Receiver for the raced serial state-root fallback: root, trie updates, and the hashed
458/// post state the fallback recomputed.
459type SerialFallbackRx = mpsc::Receiver<ProviderResult<(B256, TrieUpdates, Arc<HashedPostState>)>>;
460
461/// Default state-root strategy used by engine-tree validation.
462///
463/// Covers the built-in modes: the sparse-trie state-root task, plus the skipped and
464/// synchronous modes selected by [`TreeConfig`].
465///
466/// Custom strategies can hold this type and delegate to it for blocks where they want the
467/// default behavior.
468#[derive(Default)]
469pub struct DefaultStateRootStrategy {
470    metrics: SparseTrieTaskMetrics,
471    proof_worker_metrics: ProofWorkerIdleMetrics,
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 at or below which a block gets a quarter of the proof worker pool, since
482    /// fewer transactions produce fewer state changes and most workers would be idle overhead.
483    const SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD: usize = 30;
484
485    /// Gas used at or above which a block gets the full proof worker pool.
486    ///
487    /// More workers help drain the proof queue on large blocks while storage reads are blocked.
488    const LARGE_BLOCK_PROOF_WORKER_GAS_THRESHOLD: u64 = 100_000_000;
489
490    /// Returns how many workers to spawn for the block being validated from one kind of proof
491    /// worker pool, given its capacity.
492    ///
493    /// Explicit counts take precedence. Otherwise, small transaction counts use a quarter of
494    /// the capacity, large gas usage uses all of it, and unknown or regular blocks use half.
495    /// The transaction threshold is checked first, regardless of gas usage.
496    const fn proof_worker_count(
497        pool_threads: usize,
498        configured_threads: Option<usize>,
499        transaction_count: Option<usize>,
500        gas_used: Option<u64>,
501    ) -> usize {
502        let count = if let Some(configured_threads) = configured_threads {
503            configured_threads
504        } else {
505            match (transaction_count, gas_used) {
506                (Some(count), _) if count <= Self::SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD => {
507                    pool_threads / 4
508                }
509                (_, Some(gas)) if gas >= Self::LARGE_BLOCK_PROOF_WORKER_GAS_THRESHOLD => {
510                    pool_threads
511                }
512                _ => pool_threads / 2,
513            }
514        };
515
516        // Pools can be smaller than two threads, and a pinned count can be zero; a block without
517        // workers stalls the proofs.
518        if count == 0 {
519            1
520        } else {
521            count
522        }
523    }
524
525    /// Spawns the default state-root computation pipeline.
526    ///
527    /// The authoritative update capability taken from the returned handle must be dropped or
528    /// explicitly finished after execution so the task observes the end of the update stream.
529    #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
530    fn spawn_state_root<N, F>(
531        &self,
532        executor: &reth_tasks::Runtime,
533        multiproof_provider_factory: OverlayStateProviderFactory<F, N>,
534        options: StateRootTaskOptions<'_, N>,
535    ) -> (StateRootHandle, mpsc::Receiver<PendingSparseTrie>)
536    where
537        N: NodePrimitives,
538        F: DatabaseProviderFactory + Clone + 'static,
539        F::Provider: BlockNumReader
540            + PruneCheckpointReader
541            + StageCheckpointReader
542            + ChangeSetReader
543            + StorageChangeSetReader
544            + StorageSettingsCache
545            + 'static,
546    {
547        let StateRootTaskOptions {
548            parent_header,
549            preserved_sparse_trie,
550            transaction_count,
551            gas_used,
552            config,
553            pending_sparse_trie_prune_blocks,
554        } = options;
555        let (updates_tx, from_multi_proof) = crossbeam_channel::unbounded();
556        let (cancel_guard, cancel_rx) = StateRootTaskCancelGuard::channel();
557
558        let worker_counts = ProofWorkerCounts::new(
559            Self::proof_worker_count(
560                executor.proof_storage_worker_pool().num_threads(),
561                executor.proof_storage_worker_threads_override(),
562                transaction_count,
563                gas_used,
564            ),
565            Self::proof_worker_count(
566                executor.proof_account_worker_pool().num_threads(),
567                executor.proof_account_worker_threads_override(),
568                transaction_count,
569                gas_used,
570            ),
571        );
572        let (state_root_tx, state_root_rx) = mpsc::channel();
573        let (pending_trie_tx, pending_trie_rx) = mpsc::channel();
574        let (hashed_state_tx, hashed_state_rx) = mpsc::channel();
575        let parent_state_root = parent_header.state_root();
576
577        self.spawn_sparse_trie_task(
578            executor,
579            multiproof_provider_factory,
580            state_root_tx,
581            hashed_state_tx,
582            from_multi_proof,
583            cancel_rx,
584            SparseTrieTaskOptions {
585                parent_header,
586                preserved_sparse_trie,
587                pending_trie_tx,
588                chunk_size: config.multiproof_chunk_size(),
589                worker_counts,
590                #[cfg(feature = "trie-debug")]
591                proof_jitter: config.proof_jitter(),
592                pending_sparse_trie_prune_blocks: if config.disable_sparse_trie_cache_pruning() {
593                    None
594                } else {
595                    pending_sparse_trie_prune_blocks
596                },
597            },
598        );
599
600        (
601            StateRootHandle::new(
602                parent_state_root,
603                updates_tx,
604                cancel_guard,
605                state_root_rx,
606                hashed_state_rx,
607            ),
608            pending_trie_rx,
609        )
610    }
611
612    /// Spawns the sparse-trie task and hands off its pending trie for later publication.
613    #[expect(clippy::too_many_arguments)]
614    fn spawn_sparse_trie_task<N, F>(
615        &self,
616        executor: &reth_tasks::Runtime,
617        multiproof_provider_factory: OverlayStateProviderFactory<F, N>,
618        state_root_tx: mpsc::Sender<Result<StateRootComputeOutcome, StateRootTaskError>>,
619        hashed_state_tx: mpsc::Sender<Arc<HashedPostState>>,
620        from_multi_proof: CrossbeamReceiver<StateRootMessage>,
621        cancel_rx: CrossbeamReceiver<()>,
622        options: SparseTrieTaskOptions<N>,
623    ) where
624        N: NodePrimitives,
625        F: DatabaseProviderFactory + Clone + 'static,
626        F::Provider: BlockNumReader
627            + PruneCheckpointReader
628            + StageCheckpointReader
629            + ChangeSetReader
630            + StorageChangeSetReader
631            + StorageSettingsCache
632            + 'static,
633    {
634        let SparseTrieTaskOptions {
635            parent_header,
636            preserved_sparse_trie,
637            pending_trie_tx,
638            chunk_size,
639            worker_counts,
640            #[cfg(feature = "trie-debug")]
641            proof_jitter,
642            pending_sparse_trie_prune_blocks,
643        } = options;
644        let trie_metrics = self.metrics.clone();
645        let worker_metrics = self.proof_worker_metrics.clone();
646        let executor = executor.clone();
647
648        let parent_span = Span::current();
649        executor.clone().spawn_blocking_named("sparse-trie", move || {
650            reth_tasks::once!(increase_thread_priority);
651
652            let parent_hash = parent_header.hash();
653            let parent_state_root = parent_header.state_root();
654            let new_epoch = TrieNodeEpoch::new(parent_header.number().saturating_add(1));
655
656            let _enter = debug_span!(
657                target: "engine::tree::payload_processor",
658                parent: &parent_span,
659                "sparse_trie_task"
660            )
661            .entered();
662
663            let new_sparse_state_trie = || {
664                debug!(
665                    target: "engine::tree::payload_processor",
666                    "Creating new sparse trie - no preserved trie available"
667                );
668                let default_trie =
669                    RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
670                SparseStateTrie::default()
671                    .with_accounts_trie(default_trie.clone())
672                    .with_default_storage_trie(default_trie)
673                    .with_updates(true)
674            };
675
676            let mut sparse_trie_anchor_hash = parent_hash;
677            let mut reused_preserved_sparse_trie = false;
678            let mut known_storage_roots = Default::default();
679            let sparse_state_trie = match preserved_sparse_trie {
680                Some(preserved) => {
681                    let start = Instant::now();
682                    let preserved_anchor_hash = preserved.anchor_hash();
683                    let preserved = preserved.into_trie_for(parent_hash);
684                    trie_metrics
685                        .sparse_trie_cache_wait_duration_histogram
686                        .record(start.elapsed().as_secs_f64());
687
688                    match preserved {
689                        Ok(Some(SparseTrieWithStorageRoots { trie, storage_roots })) => {
690                            known_storage_roots = storage_roots;
691                            sparse_trie_anchor_hash = preserved_anchor_hash;
692                            reused_preserved_sparse_trie = true;
693                            trie
694                        }
695                        Ok(None) => new_sparse_state_trie(),
696                        Err(err) => {
697                            let _ = state_root_tx.send(Err(StateRootTaskError::Other(err.into())));
698                            return;
699                        }
700                    }
701                }
702                None => new_sparse_state_trie(),
703            };
704            let prune_target = sparse_trie_prune_target(
705                sparse_trie_anchor_hash,
706                reused_preserved_sparse_trie,
707                parent_header.num_hash(),
708                pending_sparse_trie_prune_blocks.as_deref(),
709            );
710            let factory = if reused_preserved_sparse_trie {
711                multiproof_provider_factory.with_skip_overlay_for_reused_sparse_trie(
712                    sparse_trie_anchor_hash,
713                    known_storage_roots,
714                )
715            } else {
716                multiproof_provider_factory
717            };
718            let task_ctx = ProofTaskCtx::new(factory);
719            #[cfg(feature = "trie-debug")]
720            let task_ctx = task_ctx.with_proof_jitter(proof_jitter);
721            let proof_worker_handle =
722                ProofWorkerHandle::new(&executor, task_ctx, worker_counts, worker_metrics);
723            let mut task = SparseTrieCacheTask::new_with_trie(
724                &executor,
725                from_multi_proof,
726                cancel_rx,
727                hashed_state_tx,
728                proof_worker_handle,
729                trie_metrics.clone(),
730                sparse_state_trie,
731                parent_state_root,
732                new_epoch,
733                chunk_size,
734            );
735
736            let result = task.run();
737            // Send the pending handle before the result so acceptance never waits for pruning.
738            let trie_completer = if let Ok(outcome) = &result {
739                let preserved_anchor_hash =
740                    prune_target.map_or(sparse_trie_anchor_hash, |(_, anchor_hash)| anchor_hash);
741                let (tx, rx) = mpsc::channel();
742                let pending = PendingSparseTrie {
743                    state_root: outcome.state_root,
744                    anchor_hash: preserved_anchor_hash,
745                    trie: rx,
746                };
747                pending_trie_tx.send(pending).is_ok().then_some(tx)
748            } else {
749                None
750            };
751
752            if state_root_tx.send(result).is_err() {
753                debug!(
754                    target: "engine::tree::payload_processor",
755                    "State root receiver dropped"
756                );
757            }
758            drop(pending_trie_tx);
759
760            let _enter =
761                debug_span!(target: "engine::tree::payload_processor", "preserve").entered();
762            let (trie_to_drop, deferred) = if let Some(completer) = trie_completer {
763                // Complete every handed-off handle, even if the root receiver was abandoned:
764                // a builder may have accepted the matching root through another strategy.
765                let start = Instant::now();
766                let (mut trie, deferred) = task.into_trie_for_reuse();
767                let storage_roots = trie.storage_roots(new_epoch);
768                if let Some((prune_before, _)) = prune_target {
769                    let prune_start = Instant::now();
770                    trie.prune(prune_before);
771                    trie_metrics
772                        .sparse_trie_prune_duration_histogram
773                        .record(prune_start.elapsed().as_secs_f64());
774                }
775                trie_metrics
776                    .into_trie_for_reuse_duration_histogram
777                    .record(start.elapsed().as_secs_f64());
778                trie_metrics
779                    .sparse_trie_retained_storage_tries
780                    .set(trie.retained_storage_tries_count() as f64);
781                (
782                    completer
783                        .send(SparseTrieWithStorageRoots { trie, storage_roots })
784                        .err()
785                        .map(|err| err.0.trie),
786                    deferred,
787                )
788            } else {
789                debug!(
790                    target: "engine::tree::payload_processor",
791                    "State root task has no reusable trie to publish, dropping trie"
792                );
793                let (trie, deferred) = task.into_cleared_trie();
794                (Some(trie), deferred)
795            };
796            if let Some(trie) = trie_to_drop {
797                executor.spawn_drop(trie);
798            }
799            executor.spawn_drop(deferred);
800        });
801    }
802}
803
804/// A computed trie whose finalization and pruning may still be running.
805#[derive(Debug)]
806struct PendingSparseTrie {
807    state_root: B256,
808    anchor_hash: B256,
809    trie: mpsc::Receiver<SparseTrieWithStorageRoots>,
810}
811
812fn publish_sparse_trie<N: NodePrimitives>(
813    pending_trie_rx: &mpsc::Receiver<PendingSparseTrie>,
814    overlay_manager: &OverlayManager<N>,
815    block_hash: B256,
816    state_root: B256,
817) {
818    // Validation has already received the root. A builder using another strategy may finish
819    // before the sparse task; it must not wait for that task just to populate the cache.
820    if let Ok(pending) = pending_trie_rx.try_recv() &&
821        pending.state_root == state_root
822    {
823        overlay_manager.store_sparse_trie(PreservedSparseTrie::pending(
824            pending.trie,
825            block_hash,
826            pending.anchor_hash,
827        ));
828    }
829}
830
831struct SparseTrieTaskOptions<N: NodePrimitives> {
832    parent_header: SealedHeader<N::BlockHeader>,
833    preserved_sparse_trie: Option<PreservedSparseTrie>,
834    pending_trie_tx: mpsc::Sender<PendingSparseTrie>,
835    chunk_size: usize,
836    worker_counts: ProofWorkerCounts,
837    #[cfg(feature = "trie-debug")]
838    proof_jitter: Option<Duration>,
839    /// `None` disables pruning. `Some(Vec::new())` prunes nodes older than the current block.
840    pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
841}
842
843struct StateRootTaskOptions<'a, N: NodePrimitives> {
844    parent_header: SealedHeader<N::BlockHeader>,
845    preserved_sparse_trie: Option<PreservedSparseTrie>,
846    transaction_count: Option<usize>,
847    /// Gas the block claims to use, taken from the payload before execution.
848    gas_used: Option<u64>,
849    config: &'a TreeConfig,
850    pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
851}
852
853fn sparse_trie_prune_target<N: NodePrimitives>(
854    current_anchor: B256,
855    reused_preserved_sparse_trie: bool,
856    parent: alloy_eips::BlockNumHash,
857    pending_blocks: Option<&[ExecutedBlock<N>]>,
858) -> Option<(TrieNodeEpoch, B256)> {
859    let blocks = pending_blocks?;
860    let Some(oldest) = blocks.last() else {
861        // Pruning the now-durable parent moves the proof source forward with it.
862        return Some((TrieNodeEpoch::new(parent.number.saturating_add(1)), parent.hash))
863    };
864    let anchor = if !reused_preserved_sparse_trie ||
865        blocks.iter().any(|block| block.recovered_block().hash() == current_anchor)
866    {
867        current_anchor
868    } else {
869        oldest.recovered_block().parent_hash()
870    };
871    Some((TrieNodeEpoch::new(oldest.recovered_block().number()), anchor))
872}
873
874impl<N, P, Evm> StateRootStrategy<N, P, Evm> for DefaultStateRootStrategy
875where
876    N: NodePrimitives,
877    P: DatabaseProviderFactory + Clone + 'static,
878    P::Provider: BlockNumReader
879        + PruneCheckpointReader
880        + StageCheckpointReader
881        + ChangeSetReader
882        + StorageChangeSetReader
883        + StorageSettingsCache
884        + 'static,
885    Evm: ConfigureEvm<Primitives = N> + 'static,
886{
887    fn prepare(
888        &self,
889        mut ctx: StateRootJobContext<'_, N, P, Evm>,
890    ) -> ProviderResult<PreparedStateRootJob<N>> {
891        if ctx.config.skip_state_root() {
892            return Ok(PreparedStateRootJob::new(Box::new(SkippedStateRootJob {}), None))
893        }
894
895        if !ctx.config.use_state_root_task() {
896            return Ok(PreparedStateRootJob::new(
897                Box::new(SynchronousStateRootJob {
898                    state_provider_factory: ctx.state_provider_factory,
899                }),
900                None,
901            ))
902        }
903
904        let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
905        let StateRootJobContext {
906            executor,
907            overlay_manager,
908            env,
909            parent_header,
910            state_provider_factory,
911            config,
912            parallel_bal_execution,
913            state: _,
914        } = ctx;
915
916        let preserved_sparse_trie = overlay_manager.take_sparse_trie();
917        let (mut handle, pending_trie_rx) = self.spawn_state_root(
918            executor,
919            state_provider_factory.clone(),
920            StateRootTaskOptions {
921                parent_header: parent_header.clone(),
922                preserved_sparse_trie,
923                transaction_count: Some(env.transaction_count),
924                gas_used: Some(env.gas_used),
925                config,
926                pending_sparse_trie_prune_blocks,
927            },
928        );
929
930        // The execution mode decides who finishes the update stream: the execution hook on
931        // the serial path, the BAL streamer on the parallel path. Both come from one slot in
932        // the handle, so only one of them can exist.
933        let (hashed_update_stream, execution_hook): (
934            Option<StateRootUpdateStream>,
935            Option<StateRootUpdateHook>,
936        ) = match parallel_bal_execution {
937            true => (Some(handle.take_hashed_update_stream()), None),
938            false => (None, Some(handle.take_execution_hook())),
939        };
940        let hint_stream = handle.take_hint_stream();
941
942        let hashed_state_rx = Some(handle.take_hashed_state_rx());
943
944        let mut prepared = PreparedStateRootJob::new(
945            Box::new(SparseTrieStateRootJob {
946                pending_trie_rx,
947                overlay_manager: overlay_manager.clone(),
948                handle,
949                state_provider_factory,
950                executor: executor.clone(),
951                timeout: config.state_root_task_timeout(),
952                compare_trie_updates: config.always_compare_trie_updates(),
953                metrics: BlockValidationMetrics::default(),
954            }),
955            hashed_state_rx,
956        )
957        .with_hint_stream(hint_stream);
958        if let Some(hook) = execution_hook {
959            prepared = prepared.with_execution_hook(hook);
960        }
961        if let Some(stream) = hashed_update_stream {
962            prepared = prepared.with_hashed_update_stream(stream);
963        }
964        Ok(prepared)
965    }
966
967    fn prepare_payload_builder(
968        &self,
969        mut ctx: PayloadStateRootJobContext<'_, N, P>,
970    ) -> ProviderResult<Option<PayloadStateRootHandle>> {
971        // Sharing the engine state-root task with the payload builder is opt-in, and needs a
972        // host that can run the task pipeline at all.
973        if !ctx.config.share_sparse_trie_with_payload_builder() ||
974            ctx.config.skip_state_root() ||
975            !ctx.config.has_enough_parallelism()
976        {
977            return Ok(None)
978        }
979
980        let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
981        let parent_header = SealedHeader::new(ctx.parent_header().clone(), ctx.parent_hash());
982        let preserved_sparse_trie = ctx.overlay_manager.take_sparse_trie();
983        let (handle, pending_trie_rx) = self.spawn_state_root(
984            ctx.executor,
985            ctx.state_provider_factory.clone(),
986            StateRootTaskOptions {
987                parent_header,
988                preserved_sparse_trie,
989                // Block built incrementally, so neither the tx count nor the gas is known at FCU
990                // time: the payload builder uses half of each proof pool.
991                transaction_count: None,
992                gas_used: None,
993                config: ctx.config,
994                pending_sparse_trie_prune_blocks,
995            },
996        );
997        let overlay_manager = ctx.overlay_manager.clone();
998        Ok(Some(handle.into_payload_state_root_handle().with_on_payload_built(
999            move |block_hash, state_root| {
1000                publish_sparse_trie(&pending_trie_rx, &overlay_manager, block_hash, state_root);
1001            },
1002        )))
1003    }
1004}
1005
1006#[derive(Debug)]
1007struct SkippedStateRootJob {}
1008
1009impl<N: NodePrimitives> StateRootJob<N> for SkippedStateRootJob {
1010    fn name(&self) -> &'static str {
1011        "skipped"
1012    }
1013
1014    fn finish(
1015        &mut self,
1016        block: &RecoveredBlock<N::Block>,
1017        _output: Arc<BlockExecutionOutput<N::Receipt>>,
1018        _hashed_state: &LazyHashedPostState,
1019    ) -> ProviderResult<StateRootJobOutcome> {
1020        Ok(StateRootJobOutcome::new(block.header().state_root(), Arc::new(TrieUpdates::default())))
1021    }
1022}
1023
1024#[derive(Debug)]
1025struct SynchronousStateRootJob<N: NodePrimitives, P> {
1026    state_provider_factory: OverlayStateProviderFactory<P, N>,
1027}
1028
1029impl<N, P> StateRootJob<N> for SynchronousStateRootJob<N, P>
1030where
1031    N: NodePrimitives,
1032    P: DatabaseProviderFactory + Clone + 'static,
1033    P::Provider: BlockNumReader
1034        + PruneCheckpointReader
1035        + StageCheckpointReader
1036        + ChangeSetReader
1037        + StorageChangeSetReader
1038        + StorageSettingsCache
1039        + 'static,
1040{
1041    fn name(&self) -> &'static str {
1042        "synchronous"
1043    }
1044
1045    fn finish(
1046        &mut self,
1047        _block: &RecoveredBlock<N::Block>,
1048        _output: Arc<BlockExecutionOutput<N::Receipt>>,
1049        hashed_state: &LazyHashedPostState,
1050    ) -> ProviderResult<StateRootJobOutcome> {
1051        let provider = self.state_provider_factory.database_provider_ro()?;
1052        let (state_root, trie_updates) =
1053            provider.state_root_with_updates(hashed_state.get().as_ref().clone())?;
1054        Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates)))
1055    }
1056}
1057
1058#[derive(Debug)]
1059struct SparseTrieStateRootJob<N: NodePrimitives, P> {
1060    pending_trie_rx: mpsc::Receiver<PendingSparseTrie>,
1061    overlay_manager: OverlayManager<N>,
1062    handle: StateRootHandle,
1063    state_provider_factory: OverlayStateProviderFactory<P, N>,
1064    executor: reth_tasks::Runtime,
1065    timeout: Option<Duration>,
1066    compare_trie_updates: bool,
1067    metrics: BlockValidationMetrics,
1068}
1069
1070impl<N, P> SparseTrieStateRootJob<N, P>
1071where
1072    N: NodePrimitives,
1073    P: DatabaseProviderFactory + Clone + 'static,
1074    P::Provider: BlockNumReader
1075        + PruneCheckpointReader
1076        + StageCheckpointReader
1077        + ChangeSetReader
1078        + StorageChangeSetReader
1079        + StorageSettingsCache
1080        + 'static,
1081{
1082    fn serial_fallback(
1083        &self,
1084        output: Arc<BlockExecutionOutput<N::Receipt>>,
1085    ) -> ProviderResult<SerialFallbackRx> {
1086        let provider = self.state_provider_factory.database_provider_ro()?;
1087        let (fallback_tx, fallback_rx) = mpsc::channel();
1088        self.executor.spawn_blocking_named("serial-root", move || {
1089            let result = (|| {
1090                let hashed_state = Arc::new(provider.hashed_post_state(&output.state)?);
1091                let (root, updates) =
1092                    provider.state_root_with_updates(hashed_state.as_ref().clone())?;
1093                Ok((root, updates, hashed_state))
1094            })();
1095            let _ = fallback_tx.send(result);
1096        });
1097
1098        Ok(fallback_rx)
1099    }
1100
1101    /// Recomputes the state root serially from the execution output.
1102    ///
1103    /// Used when the state-root task failed or produced a wrong root, so the recomputed hashed
1104    /// post state is returned in the outcome for validation to re-check against.
1105    fn compute_serial(
1106        &self,
1107        output: &BlockExecutionOutput<N::Receipt>,
1108    ) -> ProviderResult<StateRootJobOutcome> {
1109        let provider = self.state_provider_factory.database_provider_ro()?;
1110        let hashed_state = Arc::new(provider.hashed_post_state(&output.state)?);
1111        let (state_root, trie_updates) =
1112            provider.state_root_with_updates(hashed_state.as_ref().clone())?;
1113        self.metrics.state_root_task_fallback_success_total.increment(1);
1114        Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1115            .with_hashed_state(Some(hashed_state)))
1116    }
1117
1118    /// Converts a task outcome into a job outcome, recomputing serially when the task returned
1119    /// a root that does not match the block header. A state-root-task bug then costs latency
1120    /// instead of marking a valid block invalid; if the serial root also mismatches, validation
1121    /// rejects the block.
1122    fn verified_sparse_outcome(
1123        &self,
1124        block: &RecoveredBlock<N::Block>,
1125        output: &BlockExecutionOutput<N::Receipt>,
1126        outcome: StateRootComputeOutcome,
1127    ) -> ProviderResult<StateRootJobOutcome> {
1128        let outcome = self.sparse_outcome(output, outcome);
1129        if outcome.state_root == block.header().state_root() {
1130            publish_sparse_trie(
1131                &self.pending_trie_rx,
1132                &self.overlay_manager,
1133                block.hash(),
1134                outcome.state_root,
1135            );
1136            return Ok(outcome)
1137        }
1138        warn!(
1139            target: "engine::tree::state_root_strategy",
1140            state_root = ?outcome.state_root,
1141            block_state_root = ?block.header().state_root(),
1142            "State root task returned incorrect state root, recomputing serially"
1143        );
1144        self.compute_serial(output)
1145    }
1146
1147    fn sparse_outcome(
1148        &self,
1149        output: &BlockExecutionOutput<N::Receipt>,
1150        outcome: StateRootComputeOutcome,
1151    ) -> StateRootJobOutcome {
1152        let StateRootComputeOutcome { state_root, trie_updates, hashed_state: _hashed_state } =
1153            outcome;
1154
1155        if self.compare_trie_updates {
1156            compare_trie_updates_with_serial(
1157                self.state_provider_factory.clone(),
1158                output,
1159                trie_updates.as_ref().clone(),
1160            );
1161        }
1162
1163        StateRootJobOutcome::new(state_root, trie_updates)
1164    }
1165}
1166
1167impl<N, P> StateRootJob<N> for SparseTrieStateRootJob<N, P>
1168where
1169    N: NodePrimitives,
1170    P: DatabaseProviderFactory + Clone + 'static,
1171    P::Provider: BlockNumReader
1172        + PruneCheckpointReader
1173        + StageCheckpointReader
1174        + ChangeSetReader
1175        + StorageChangeSetReader
1176        + StorageSettingsCache
1177        + 'static,
1178{
1179    fn name(&self) -> &'static str {
1180        "sparse-trie"
1181    }
1182
1183    fn finish(
1184        &mut self,
1185        block: &RecoveredBlock<N::Block>,
1186        output: Arc<BlockExecutionOutput<N::Receipt>>,
1187        _hashed_state: &LazyHashedPostState,
1188    ) -> ProviderResult<StateRootJobOutcome> {
1189        if self.timeout.is_none() {
1190            return match self.handle.state_root() {
1191                Ok(outcome) => self.verified_sparse_outcome(block, &output, outcome),
1192                Err(err) => {
1193                    debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1194                    self.compute_serial(&output)
1195                }
1196            }
1197        }
1198
1199        let timeout = self.timeout.expect("checked above");
1200        let task_rx = self.handle.take_state_root_rx();
1201        let fallback_rx = match task_rx.recv_timeout(timeout) {
1202            Ok(Ok(outcome)) => return self.verified_sparse_outcome(block, &output, outcome),
1203            Ok(Err(err)) => {
1204                debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1205                self.serial_fallback(output.clone())?
1206            }
1207            Err(RecvTimeoutError::Timeout) => {
1208                warn!(target: "engine::tree::state_root_strategy", ?timeout, "State root task timed out, racing serial fallback");
1209                self.metrics.state_root_task_timeout_total.increment(1);
1210                self.serial_fallback(output.clone())?
1211            }
1212            Err(RecvTimeoutError::Disconnected) => {
1213                debug!(target: "engine::tree::state_root_strategy", "State root task dropped, falling back to serial root");
1214                self.serial_fallback(output.clone())?
1215            }
1216        };
1217
1218        loop {
1219            if let Ok(Ok(outcome)) = task_rx.try_recv() {
1220                let outcome = self.sparse_outcome(&output, outcome);
1221                if outcome.state_root == block.header().state_root() {
1222                    publish_sparse_trie(
1223                        &self.pending_trie_rx,
1224                        &self.overlay_manager,
1225                        block.hash(),
1226                        outcome.state_root,
1227                    );
1228                    return Ok(outcome)
1229                }
1230                // A wrong task root falls through to the serial fallback already racing below.
1231                warn!(
1232                    target: "engine::tree::state_root_strategy",
1233                    state_root = ?outcome.state_root,
1234                    block_state_root = ?block.header().state_root(),
1235                    "State root task returned incorrect state root, using serial fallback"
1236                );
1237            }
1238
1239            match fallback_rx.try_recv() {
1240                Ok(Ok((state_root, trie_updates, hashed_state))) => {
1241                    self.metrics.state_root_task_fallback_success_total.increment(1);
1242                    return Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1243                        .with_hashed_state(Some(hashed_state)))
1244                }
1245                Ok(Err(err)) => return Err(err),
1246                Err(mpsc::TryRecvError::Empty) => {}
1247                Err(mpsc::TryRecvError::Disconnected) => {
1248                    return Err(ProviderError::other(std::io::Error::other(
1249                        "serial state root fallback task dropped",
1250                    )))
1251                }
1252            }
1253
1254            std::thread::sleep(Duration::from_millis(1));
1255        }
1256    }
1257}
1258
1259fn compare_trie_updates_with_serial<N, P>(
1260    state_provider_factory: OverlayStateProviderFactory<P, N>,
1261    output: &BlockExecutionOutput<N::Receipt>,
1262    task_trie_updates: TrieUpdates,
1263) -> bool
1264where
1265    N: NodePrimitives,
1266    P: DatabaseProviderFactory,
1267    P::Provider: BlockNumReader
1268        + PruneCheckpointReader
1269        + StageCheckpointReader
1270        + ChangeSetReader
1271        + StorageChangeSetReader
1272        + StorageSettingsCache
1273        + 'static,
1274{
1275    debug!(target: "engine::tree::state_root_strategy", "Comparing trie updates with serial computation");
1276
1277    match state_provider_factory.database_provider_ro().and_then(|provider| {
1278        let hashed_state = provider.hashed_post_state(&output.state)?;
1279        provider.state_root_with_updates(hashed_state)
1280    }) {
1281        Ok((serial_root, serial_trie_updates)) => {
1282            debug!(
1283                target: "engine::tree::state_root_strategy",
1284                ?serial_root,
1285                "Serial state root computation finished for comparison"
1286            );
1287
1288            match state_provider_factory.database_provider_ro() {
1289                Ok(provider) => match super::trie_updates::compare_trie_updates(
1290                    &provider,
1291                    task_trie_updates,
1292                    serial_trie_updates,
1293                ) {
1294                    Ok(has_diff) => return has_diff,
1295                    Err(err) => {
1296                        warn!(
1297                            target: "engine::tree::state_root_strategy",
1298                            %err,
1299                            "Error comparing trie updates"
1300                        );
1301                        return true;
1302                    }
1303                },
1304                Err(err) => {
1305                    warn!(
1306                        target: "engine::tree::state_root_strategy",
1307                        %err,
1308                        "Failed to get database provider for trie update comparison"
1309                    );
1310                }
1311            }
1312        }
1313        Err(err) => {
1314            warn!(
1315                target: "engine::tree::state_root_strategy",
1316                %err,
1317                "Failed to compute serial state root for comparison"
1318            );
1319        }
1320    }
1321    false
1322}
1323
1324#[cfg(test)]
1325mod tests {
1326    use super::*;
1327    use alloy_consensus::constants::KECCAK_EMPTY;
1328    use alloy_primitives::{map::HashMap, Address, U256};
1329    use rand::Rng;
1330    use reth_chain_state::test_utils::TestBlockBuilder;
1331    use reth_chainspec::ChainSpec;
1332    use reth_db_common::init::init_genesis;
1333    use reth_ethereum_primitives::EthPrimitives;
1334    use reth_evm::OnStateHook;
1335    use reth_evm_ethereum::EthEvmConfig;
1336    use reth_primitives_traits::{Account, StorageEntry};
1337    use reth_provider::{
1338        providers::BlockchainProvider, test_utils::create_test_provider_factory_with_chain_spec,
1339        HashingWriter,
1340    };
1341    use reth_storage_overlay::{OverlayManager, OverlayStateProviderFactory};
1342    use reth_testing_utils::generators;
1343    use reth_trie::test_utils::state_root;
1344    use revm::state::{AccountInfo, AccountStatus, EvmState, EvmStorageSlot, TransactionId};
1345
1346    #[test]
1347    fn proof_worker_count_scales_with_block_gas() {
1348        let pool_threads = 64;
1349        let count = |transaction_count, gas_used| {
1350            DefaultStateRootStrategy::proof_worker_count(
1351                pool_threads,
1352                None,
1353                transaction_count,
1354                gas_used,
1355            )
1356        };
1357
1358        // Small blocks use a quarter of the pool, whatever their gas.
1359        assert_eq!(count(Some(30), Some(1_000_000)), 16);
1360        assert_eq!(count(Some(30), Some(300_000_000)), 16);
1361
1362        // Gas heavy blocks use the full pool.
1363        assert_eq!(count(Some(1_000), Some(100_000_000)), 64);
1364        assert_eq!(count(Some(1_000), Some(300_000_000)), 64);
1365
1366        // Everything else, including a block whose size is not known yet, uses half the pool.
1367        assert_eq!(count(Some(31), Some(99_999_999)), 32);
1368        assert_eq!(count(Some(1_000), Some(15_000_000)), 32);
1369        assert_eq!(count(None, None), 32);
1370
1371        // An explicitly configured count is used verbatim for every block.
1372        for (transaction_count, gas_used) in
1373            [(Some(30), Some(1_000_000)), (Some(1_000), Some(300_000_000)), (None, None)]
1374        {
1375            assert_eq!(
1376                DefaultStateRootStrategy::proof_worker_count(
1377                    48,
1378                    Some(48),
1379                    transaction_count,
1380                    gas_used
1381                ),
1382                48
1383            );
1384        }
1385
1386        // Pools smaller than two threads, and a pinned count of zero, still get a worker.
1387        assert_eq!(
1388            DefaultStateRootStrategy::proof_worker_count(1, None, Some(1), Some(1_000_000)),
1389            1
1390        );
1391        assert_eq!(DefaultStateRootStrategy::proof_worker_count(1, None, None, None), 1);
1392        assert_eq!(DefaultStateRootStrategy::proof_worker_count(1, Some(0), None, None), 1);
1393    }
1394
1395    #[test]
1396    fn finish_publishes_only_accepted_sparse_trie() {
1397        let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1398        let genesis_hash = init_genesis(&factory).unwrap();
1399        let provider = BlockchainProvider::new(factory).unwrap();
1400        let runtime = reth_tasks::Runtime::test();
1401
1402        for timeout in [None, Some(Duration::from_secs(1))] {
1403            for (finish, matching_root) in [(false, true), (true, true), (true, false)] {
1404                let overlay_manager = OverlayManager::<EthPrimitives>::default();
1405                let block = RecoveredBlock::new_unhashed(
1406                    reth_ethereum_primitives::Block {
1407                        header: alloy_consensus::Header {
1408                            state_root: B256::with_last_byte(1),
1409                            ..Default::default()
1410                        },
1411                        ..Default::default()
1412                    },
1413                    Vec::new(),
1414                );
1415                let (completer, trie) = mpsc::channel();
1416                let (pending_trie_tx, pending_trie_rx) = mpsc::channel();
1417                pending_trie_tx
1418                    .send(PendingSparseTrie {
1419                        state_root: if matching_root { block.state_root } else { B256::ZERO },
1420                        anchor_hash: genesis_hash,
1421                        trie,
1422                    })
1423                    .unwrap();
1424                let (result_tx, result_rx) = mpsc::channel();
1425                result_tx
1426                    .send(Ok(StateRootComputeOutcome {
1427                        state_root: if matching_root { block.state_root } else { B256::ZERO },
1428                        trie_updates: Default::default(),
1429                        hashed_state: Default::default(),
1430                    }))
1431                    .unwrap();
1432                let mut job = SparseTrieStateRootJob {
1433                    pending_trie_rx,
1434                    overlay_manager: overlay_manager.clone(),
1435                    handle: StateRootHandle::new(
1436                        B256::ZERO,
1437                        crossbeam_channel::unbounded().0,
1438                        StateRootTaskCancelGuard::channel().0,
1439                        result_rx,
1440                        mpsc::channel().1,
1441                    ),
1442                    state_provider_factory: OverlayStateProviderFactory::new(
1443                        provider.clone(),
1444                        overlay_manager.overlay_builder(genesis_hash),
1445                    ),
1446                    executor: runtime.clone(),
1447                    timeout,
1448                    compare_trie_updates: false,
1449                    metrics: Default::default(),
1450                };
1451                assert!(overlay_manager.take_sparse_trie().is_none());
1452                if finish {
1453                    job.finish(
1454                        &block,
1455                        Arc::new(BlockExecutionOutput::default()),
1456                        &LazyHashedPostState::ready(Default::default()),
1457                    )
1458                    .unwrap();
1459                }
1460                drop(job);
1461                let preserved = overlay_manager.take_sparse_trie();
1462                assert_eq!(preserved.is_some(), finish && matching_root);
1463                // Publication must finish before trie finalization does.
1464                assert_eq!(
1465                    completer.send(SparseTrieWithStorageRoots::default()).is_ok(),
1466                    finish && matching_root
1467                );
1468                if let Some(preserved) = preserved {
1469                    assert!(preserved.into_trie_for(block.hash()).unwrap().is_some());
1470                }
1471            }
1472        }
1473    }
1474
1475    #[test]
1476    fn failed_parent_trie_closes_handoff_without_publishing() {
1477        let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1478        let genesis_hash = init_genesis(&factory).unwrap();
1479        let runtime = reth_tasks::Runtime::test();
1480        {
1481            let (completer, trie) = mpsc::channel();
1482            let preserved = PreservedSparseTrie::pending(trie, genesis_hash, B256::ZERO);
1483            drop(completer);
1484            let (mut handle, pending_trie_rx) = DefaultStateRootStrategy::default()
1485                .spawn_state_root(
1486                    &runtime,
1487                    OverlayStateProviderFactory::new(
1488                        factory,
1489                        OverlayManager::<EthPrimitives>::default().overlay_builder(genesis_hash),
1490                    ),
1491                    StateRootTaskOptions::<EthPrimitives> {
1492                        parent_header: SealedHeader::new(Default::default(), genesis_hash),
1493                        preserved_sparse_trie: Some(preserved),
1494                        transaction_count: None,
1495                        gas_used: None,
1496                        config: &TreeConfig::default(),
1497                        pending_sparse_trie_prune_blocks: None,
1498                    },
1499                );
1500            assert!(handle.state_root().is_err());
1501            assert!(matches!(
1502                pending_trie_rx.recv_timeout(Duration::from_secs(5)),
1503                Err(RecvTimeoutError::Disconnected)
1504            ));
1505        }
1506        for name in ["sparse-trie", "storage-workers", "account-workers"] {
1507            runtime.spawn_blocking_named(name, || {}).get();
1508        }
1509    }
1510
1511    #[test]
1512    fn publication_does_not_wait_for_worker() {
1513        let (pending_trie_tx, pending_trie_rx) = mpsc::channel();
1514        let overlay_manager = OverlayManager::<EthPrimitives>::default();
1515        let (done_tx, done_rx) = mpsc::channel();
1516        let worker = std::thread::spawn(move || {
1517            publish_sparse_trie(&pending_trie_rx, &overlay_manager, B256::ZERO, B256::ZERO);
1518            assert!(overlay_manager.take_sparse_trie().is_none());
1519            done_tx.send(()).unwrap();
1520        });
1521        let result = done_rx.recv_timeout(Duration::from_secs(5));
1522        drop(pending_trie_tx);
1523        worker.join().unwrap();
1524        result.expect("publication should not wait for the root task");
1525    }
1526
1527    #[test]
1528    fn dropped_root_receiver_still_completes_pending_trie() {
1529        let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1530        let genesis_hash = init_genesis(&factory).unwrap();
1531        let runtime = reth_tasks::Runtime::test();
1532        let (mut handle, pending_trie_rx) = DefaultStateRootStrategy::default().spawn_state_root(
1533            &runtime,
1534            OverlayStateProviderFactory::new(
1535                factory,
1536                OverlayManager::<EthPrimitives>::default().overlay_builder(genesis_hash),
1537            ),
1538            StateRootTaskOptions::<EthPrimitives> {
1539                parent_header: SealedHeader::new(Default::default(), genesis_hash),
1540                preserved_sparse_trie: None,
1541                transaction_count: None,
1542                gas_used: None,
1543                config: &TreeConfig::default(),
1544                pending_sparse_trie_prune_blocks: None,
1545            },
1546        );
1547        drop(handle.take_state_root_rx());
1548        drop(handle.take_execution_hook());
1549        let pending = pending_trie_rx.recv_timeout(Duration::from_secs(5)).unwrap();
1550        assert_eq!(pending.anchor_hash, genesis_hash);
1551        pending
1552            .trie
1553            .recv_timeout(Duration::from_secs(5))
1554            .expect("handed-off trie must be completed");
1555        drop(handle);
1556        for name in ["sparse-trie", "trie-hashing", "storage-workers", "account-workers", "drop"] {
1557            runtime.spawn_blocking_named(name, || {}).get();
1558        }
1559    }
1560
1561    #[test]
1562    fn sparse_trie_prune_target_uses_requested_range() {
1563        let parent = alloy_eips::BlockNumHash { hash: B256::with_last_byte(9), number: 9 };
1564        assert_eq!(
1565            sparse_trie_prune_target::<EthPrimitives>(B256::ZERO, false, parent, None),
1566            None
1567        );
1568        assert_eq!(
1569            sparse_trie_prune_target::<EthPrimitives>(B256::ZERO, false, parent, Some(&[])),
1570            Some((TrieNodeEpoch::new(10), parent.hash))
1571        );
1572
1573        let mut blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(7..10).collect();
1574        blocks.reverse();
1575
1576        let anchor = blocks[0].recovered_block().hash();
1577        assert_eq!(
1578            sparse_trie_prune_target(anchor, true, parent, Some(&blocks)),
1579            Some((TrieNodeEpoch::new(7), anchor))
1580        );
1581    }
1582
1583    #[test]
1584    fn sparse_trie_prune_target_advances_to_prune_anchor() {
1585        let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..5).collect();
1586        let reused_anchor_hash = blocks[0].recovered_block().hash();
1587        let expected_prune_anchor = blocks[1].recovered_block().hash();
1588        let parent = blocks[3].recovered_block().num_hash();
1589        let prune_blocks: Vec<_> = blocks.into_iter().skip(2).rev().collect();
1590
1591        assert_eq!(
1592            sparse_trie_prune_target(reused_anchor_hash, true, parent, Some(&prune_blocks),),
1593            Some((TrieNodeEpoch::new(3), expected_prune_anchor))
1594        );
1595    }
1596
1597    #[test]
1598    fn sparse_trie_prune_target_keeps_parent_for_fresh_trie() {
1599        let mut blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..3).collect();
1600        blocks.reverse();
1601        let parent_hash = B256::with_last_byte(0xaa);
1602
1603        assert_eq!(
1604            sparse_trie_prune_target(
1605                parent_hash,
1606                false,
1607                blocks[0].recovered_block().num_hash(),
1608                Some(&blocks),
1609            ),
1610            Some((TrieNodeEpoch::new(1), parent_hash))
1611        );
1612    }
1613
1614    #[test]
1615    fn sparse_trie_prune_target_advances_reused_anchor_when_parent_becomes_durable() {
1616        let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..4).collect();
1617        let old_anchor = blocks[1].recovered_block().hash();
1618        let parent = blocks[2].recovered_block().num_hash();
1619
1620        assert_eq!(
1621            sparse_trie_prune_target::<EthPrimitives>(old_anchor, true, parent, Some(&[])),
1622            Some((TrieNodeEpoch::new(4), parent.hash))
1623        );
1624    }
1625
1626    fn create_mock_state_updates(num_accounts: usize, updates_per_account: usize) -> Vec<EvmState> {
1627        let mut rng = generators::rng();
1628        let all_addresses: Vec<Address> = (0..num_accounts).map(|_| rng.random()).collect();
1629        let mut updates = Vec::with_capacity(updates_per_account);
1630
1631        for _ in 0..updates_per_account {
1632            let num_accounts_in_update = rng.random_range(1..=num_accounts);
1633            let mut state_update = EvmState::default();
1634
1635            for &address in &all_addresses[0..num_accounts_in_update] {
1636                let mut storage = HashMap::default();
1637                if rng.random_bool(0.7) {
1638                    for _ in 0..rng.random_range(1..10) {
1639                        let slot = U256::from(rng.random::<u64>());
1640                        storage.insert(
1641                            slot,
1642                            EvmStorageSlot::new_changed(
1643                                U256::ZERO,
1644                                U256::from(rng.random::<u64>()),
1645                                TransactionId::ZERO,
1646                            ),
1647                        );
1648                    }
1649                }
1650
1651                let mut account = revm::state::Account::default();
1652                account.info = AccountInfo {
1653                    balance: U256::from(rng.random::<u64>()),
1654                    nonce: rng.random::<u64>(),
1655                    code_hash: KECCAK_EMPTY,
1656                    code: Some(Default::default()),
1657                    ..Default::default()
1658                };
1659                account.storage = storage;
1660                account.status = AccountStatus::Touched;
1661                account.transaction_id = TransactionId::ZERO;
1662                state_update.insert(address, account);
1663            }
1664
1665            updates.push(state_update);
1666        }
1667
1668        updates
1669    }
1670
1671    #[test]
1672    fn payload_builder_trie_matches_serial_root_and_can_be_reused() {
1673        reth_tracing::init_test_tracing();
1674
1675        let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1676        let genesis_hash = init_genesis(&factory).unwrap();
1677        let state_updates = create_mock_state_updates(10, 10);
1678        let mut accumulated_state: HashMap<Address, (Account, HashMap<B256, U256>)> =
1679            HashMap::default();
1680
1681        {
1682            let provider_rw = factory.provider_rw().expect("failed to get provider");
1683            for update in &state_updates {
1684                let account_updates = update.iter().map(|(address, account)| {
1685                    (*address, Some(Account::from_revm_account(account)))
1686                });
1687                provider_rw
1688                    .insert_account_for_hashing(account_updates)
1689                    .expect("failed to insert accounts");
1690
1691                let storage_updates = update.iter().map(|(address, account)| {
1692                    let storage_entries = account.storage.iter().map(|(slot, value)| {
1693                        StorageEntry { key: B256::from(*slot), value: value.present_value }
1694                    });
1695                    (*address, storage_entries)
1696                });
1697                provider_rw
1698                    .insert_storage_for_hashing(storage_updates)
1699                    .expect("failed to insert storage");
1700            }
1701            provider_rw.commit().expect("failed to commit changes");
1702        }
1703
1704        for update in &state_updates {
1705            for (address, account) in update {
1706                let storage: HashMap<B256, U256> = account
1707                    .storage
1708                    .iter()
1709                    .map(|(key, value)| (B256::from(*key), value.present_value))
1710                    .collect();
1711                let entry = accumulated_state.entry(*address).or_default();
1712                entry.0 = Account::from_revm_account(account);
1713                entry.1.extend(storage);
1714            }
1715        }
1716
1717        let provider_factory = BlockchainProvider::new(factory).unwrap();
1718        let runtime = reth_tasks::Runtime::test();
1719        let overlay_manager = OverlayManager::<EthPrimitives>::default();
1720        let config = TreeConfig::default()
1721            .with_has_enough_parallelism(true)
1722            .with_share_sparse_trie_with_payload_builder(true);
1723        let mut state = EngineApiTreeState::new(
1724            10,
1725            10,
1726            config.invalid_header_hit_eviction_threshold(),
1727            alloy_eips::BlockNumHash::new(0, genesis_hash),
1728            crate::engine::EngineApiKind::Ethereum,
1729            overlay_manager.clone(),
1730        );
1731        let mut root_from_regular = state_root(accumulated_state.clone());
1732        let mut preserved: Option<PreservedSparseTrie> = None;
1733        for (number, (reuse, return_payload, matching_root)) in [
1734            (false, false, true),
1735            (false, true, false),
1736            (false, true, true),
1737            (true, true, true),
1738            (true, false, true),
1739        ]
1740        .into_iter()
1741        .enumerate()
1742        {
1743            assert_eq!(preserved.is_some(), reuse);
1744            let parent_hash =
1745                preserved.as_ref().map_or(genesis_hash, PreservedSparseTrie::block_hash);
1746            let block_hash = B256::with_last_byte(number as u8 + 1);
1747            if let Some(preserved) = preserved.take() {
1748                overlay_manager.store_sparse_trie(preserved);
1749            }
1750            let parent_header = alloy_consensus::Header {
1751                number: number as u64,
1752                state_root: if reuse { root_from_regular } else { B256::ZERO },
1753                ..Default::default()
1754            };
1755            let mut state_root_handle = <DefaultStateRootStrategy as StateRootStrategy<
1756                EthPrimitives,
1757                _,
1758                EthEvmConfig,
1759            >>::prepare_payload_builder(
1760                &DefaultStateRootStrategy::default(),
1761                PayloadStateRootJobContext::new(
1762                    &runtime,
1763                    &overlay_manager,
1764                    parent_hash,
1765                    &parent_header,
1766                    1,
1767                    &mut state,
1768                    OverlayStateProviderFactory::new(
1769                        provider_factory.clone(),
1770                        overlay_manager.overlay_builder(genesis_hash),
1771                    ),
1772                    &config,
1773                ),
1774            )
1775            .unwrap()
1776            .unwrap();
1777            let on_payload_built = state_root_handle.take_on_payload_built().unwrap();
1778            let mut state_hook = state_root_handle.take_state_hook();
1779            if reuse {
1780                let mut update = state_updates.last().unwrap().clone();
1781                for (address, account) in &mut update {
1782                    account.info.nonce += number as u64;
1783                    accumulated_state.get_mut(address).unwrap().0 =
1784                        Account::from_revm_account(account);
1785                }
1786                state_hook.on_state(update);
1787                root_from_regular = state_root(accumulated_state.clone());
1788            } else {
1789                for update in &state_updates {
1790                    state_hook.on_state(update.clone());
1791                }
1792            }
1793            drop(state_hook);
1794
1795            let root_from_task = state_root_handle.state_root().expect("task failed").state_root;
1796            assert_eq!(root_from_task, root_from_regular);
1797            assert!(overlay_manager.take_sparse_trie().is_none());
1798            drop(state_root_handle);
1799            if return_payload {
1800                on_payload_built(
1801                    block_hash,
1802                    if matching_root { root_from_task } else { B256::ZERO },
1803                );
1804            } else {
1805                drop(on_payload_built);
1806            }
1807            preserved = overlay_manager.take_sparse_trie();
1808            assert_eq!(preserved.is_some(), return_payload && matching_root);
1809            if let Some(preserved) = &preserved {
1810                assert_eq!(preserved.block_hash(), block_hash);
1811                assert_eq!(preserved.anchor_hash(), genesis_hash);
1812            }
1813        }
1814        for name in ["sparse-trie", "trie-hashing", "storage-workers", "account-workers", "drop"] {
1815            runtime.spawn_blocking_named(name, || {}).get();
1816        }
1817    }
1818}