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 `StateUpdate`s 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;
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        PayloadStateRootHandle, StateAccessHint, StateRootComputeOutcome, StateRootHandle,
83        StateRootHintStream, StateRootMessage, StateRootTaskCancelGuard, StateRootUpdateHook,
84        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 const fn take_execution_hook(&mut self) -> Option<StateRootUpdateHook> {
381        self.execution_hook.take()
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    /// Publish the returned pending trie with [`publish_sparse_trie`] only after accepting the
530    /// matching block root. Dropping that receiver discards the reusable trie.
531    #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
532    pub fn spawn_state_root<N, F>(
533        &self,
534        executor: &reth_tasks::Runtime,
535        multiproof_provider_factory: OverlayStateProviderFactory<F, N>,
536        options: StateRootTaskOptions<'_, N>,
537    ) -> (StateRootHandle, mpsc::Receiver<PendingSparseTrie>)
538    where
539        N: NodePrimitives,
540        F: DatabaseProviderFactory + Clone + 'static,
541        F::Provider: BlockNumReader
542            + PruneCheckpointReader
543            + StageCheckpointReader
544            + ChangeSetReader
545            + StorageChangeSetReader
546            + StorageSettingsCache
547            + 'static,
548    {
549        let StateRootTaskOptions {
550            parent_header,
551            preserved_sparse_trie,
552            transaction_count,
553            gas_used,
554            config,
555            pending_sparse_trie_prune_blocks,
556        } = options;
557        let (updates_tx, from_multi_proof) = crossbeam_channel::unbounded();
558        let (cancel_guard, cancel_rx) = StateRootTaskCancelGuard::channel();
559
560        let worker_counts = ProofWorkerCounts::new(
561            Self::proof_worker_count(
562                executor.proof_storage_worker_pool().num_threads(),
563                executor.proof_storage_worker_threads_override(),
564                transaction_count,
565                gas_used,
566            ),
567            Self::proof_worker_count(
568                executor.proof_account_worker_pool().num_threads(),
569                executor.proof_account_worker_threads_override(),
570                transaction_count,
571                gas_used,
572            ),
573        );
574        let (state_root_tx, state_root_rx) = mpsc::channel();
575        let (pending_trie_tx, pending_trie_rx) = mpsc::channel();
576        let (hashed_state_tx, hashed_state_rx) = mpsc::channel();
577        let parent_state_root = parent_header.state_root();
578
579        self.spawn_sparse_trie_task(
580            executor,
581            multiproof_provider_factory,
582            state_root_tx,
583            hashed_state_tx,
584            from_multi_proof,
585            cancel_rx,
586            SparseTrieTaskOptions {
587                parent_header,
588                preserved_sparse_trie,
589                pending_trie_tx,
590                chunk_size: config.multiproof_chunk_size(),
591                worker_counts,
592                #[cfg(feature = "trie-debug")]
593                proof_jitter: config.proof_jitter(),
594                pending_sparse_trie_prune_blocks: if config.disable_sparse_trie_cache_pruning() {
595                    None
596                } else {
597                    pending_sparse_trie_prune_blocks
598                },
599            },
600        );
601
602        (
603            StateRootHandle::new(
604                parent_state_root,
605                updates_tx,
606                cancel_guard,
607                state_root_rx,
608                hashed_state_rx,
609            ),
610            pending_trie_rx,
611        )
612    }
613
614    /// Spawns the sparse-trie task and hands off its pending trie for later publication.
615    #[expect(clippy::too_many_arguments)]
616    fn spawn_sparse_trie_task<N, F>(
617        &self,
618        executor: &reth_tasks::Runtime,
619        multiproof_provider_factory: OverlayStateProviderFactory<F, N>,
620        state_root_tx: mpsc::Sender<Result<StateRootComputeOutcome, StateRootTaskError>>,
621        hashed_state_tx: mpsc::Sender<Arc<HashedPostState>>,
622        from_multi_proof: CrossbeamReceiver<StateRootMessage>,
623        cancel_rx: CrossbeamReceiver<()>,
624        options: SparseTrieTaskOptions<N>,
625    ) where
626        N: NodePrimitives,
627        F: DatabaseProviderFactory + Clone + 'static,
628        F::Provider: BlockNumReader
629            + PruneCheckpointReader
630            + StageCheckpointReader
631            + ChangeSetReader
632            + StorageChangeSetReader
633            + StorageSettingsCache
634            + 'static,
635    {
636        let SparseTrieTaskOptions {
637            parent_header,
638            preserved_sparse_trie,
639            pending_trie_tx,
640            chunk_size,
641            worker_counts,
642            #[cfg(feature = "trie-debug")]
643            proof_jitter,
644            pending_sparse_trie_prune_blocks,
645        } = options;
646        let trie_metrics = self.metrics.clone();
647        let worker_metrics = self.proof_worker_metrics.clone();
648        let executor = executor.clone();
649
650        let parent_span = Span::current();
651        executor.clone().spawn_blocking_named("sparse-trie", move || {
652            reth_tasks::once!(increase_thread_priority);
653
654            let parent_hash = parent_header.hash();
655            let parent_state_root = parent_header.state_root();
656            let new_epoch = TrieNodeEpoch::new(parent_header.number().saturating_add(1));
657
658            let _enter = debug_span!(
659                target: "engine::tree::payload_processor",
660                parent: &parent_span,
661                "sparse_trie_task"
662            )
663            .entered();
664
665            let new_sparse_state_trie = || {
666                debug!(
667                    target: "engine::tree::payload_processor",
668                    "Creating new sparse trie - no preserved trie available"
669                );
670                let default_trie =
671                    RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
672                SparseStateTrie::default()
673                    .with_accounts_trie(default_trie.clone())
674                    .with_default_storage_trie(default_trie)
675                    .with_updates(true)
676            };
677
678            let mut sparse_trie_anchor_hash = parent_hash;
679            let mut reused_preserved_sparse_trie = false;
680            let mut known_storage_roots = Default::default();
681            let sparse_state_trie = match preserved_sparse_trie {
682                Some(preserved) => {
683                    let start = Instant::now();
684                    let preserved_anchor_hash = preserved.anchor_hash();
685                    let preserved = preserved.into_trie_for(parent_hash);
686                    trie_metrics
687                        .sparse_trie_cache_wait_duration_histogram
688                        .record(start.elapsed().as_secs_f64());
689
690                    match preserved {
691                        Ok(Some(SparseTrieWithStorageRoots { trie, storage_roots })) => {
692                            known_storage_roots = storage_roots;
693                            sparse_trie_anchor_hash = preserved_anchor_hash;
694                            reused_preserved_sparse_trie = true;
695                            trie
696                        }
697                        Ok(None) => new_sparse_state_trie(),
698                        Err(err) => {
699                            let _ = state_root_tx.send(Err(StateRootTaskError::Other(err.into())));
700                            return;
701                        }
702                    }
703                }
704                None => new_sparse_state_trie(),
705            };
706            let prune_target = sparse_trie_prune_target(
707                sparse_trie_anchor_hash,
708                reused_preserved_sparse_trie,
709                parent_header.num_hash(),
710                pending_sparse_trie_prune_blocks.as_deref(),
711            );
712            let factory = if reused_preserved_sparse_trie {
713                multiproof_provider_factory.with_skip_overlay_for_reused_sparse_trie(
714                    sparse_trie_anchor_hash,
715                    known_storage_roots,
716                )
717            } else {
718                multiproof_provider_factory
719            };
720            let task_ctx = ProofTaskCtx::new(factory);
721            #[cfg(feature = "trie-debug")]
722            let task_ctx = task_ctx.with_proof_jitter(proof_jitter);
723            let proof_worker_handle =
724                ProofWorkerHandle::new(&executor, task_ctx, worker_counts, worker_metrics);
725            let mut task = SparseTrieCacheTask::new_with_trie(
726                &executor,
727                from_multi_proof,
728                cancel_rx,
729                hashed_state_tx,
730                proof_worker_handle,
731                trie_metrics.clone(),
732                sparse_state_trie,
733                parent_state_root,
734                new_epoch,
735                chunk_size,
736            );
737
738            let result = task.run();
739            // Send the pending handle before the result so acceptance never waits for pruning.
740            let trie_completer = if let Ok(outcome) = &result {
741                let preserved_anchor_hash =
742                    prune_target.map_or(sparse_trie_anchor_hash, |(_, anchor_hash)| anchor_hash);
743                let (tx, rx) = mpsc::channel();
744                let pending = PendingSparseTrie {
745                    state_root: outcome.state_root,
746                    anchor_hash: preserved_anchor_hash,
747                    trie: rx,
748                };
749                pending_trie_tx.send(pending).is_ok().then_some(tx)
750            } else {
751                None
752            };
753
754            if state_root_tx.send(result).is_err() {
755                debug!(
756                    target: "engine::tree::payload_processor",
757                    "State root receiver dropped"
758                );
759            }
760            drop(pending_trie_tx);
761
762            let _enter =
763                debug_span!(target: "engine::tree::payload_processor", "preserve").entered();
764            let (trie_to_drop, deferred) = if let Some(completer) = trie_completer {
765                // Complete every handed-off handle, even if the root receiver was abandoned:
766                // a builder may have accepted the matching root through another strategy.
767                let start = Instant::now();
768                let (mut trie, deferred) = task.into_trie_for_reuse();
769                let storage_roots = trie.storage_roots(new_epoch);
770                if let Some((prune_before, _)) = prune_target {
771                    let prune_start = Instant::now();
772                    trie.prune(prune_before);
773                    trie_metrics
774                        .sparse_trie_prune_duration_histogram
775                        .record(prune_start.elapsed().as_secs_f64());
776                }
777                trie_metrics
778                    .into_trie_for_reuse_duration_histogram
779                    .record(start.elapsed().as_secs_f64());
780                trie_metrics
781                    .sparse_trie_retained_storage_tries
782                    .set(trie.retained_storage_tries_count() as f64);
783                (
784                    completer
785                        .send(SparseTrieWithStorageRoots { trie, storage_roots })
786                        .err()
787                        .map(|err| err.0.trie),
788                    deferred,
789                )
790            } else {
791                debug!(
792                    target: "engine::tree::payload_processor",
793                    "State root task has no reusable trie to publish, dropping trie"
794                );
795                let (trie, deferred) = task.into_cleared_trie();
796                (Some(trie), deferred)
797            };
798            if let Some(trie) = trie_to_drop {
799                executor.spawn_drop(trie);
800            }
801            executor.spawn_drop(deferred);
802        });
803    }
804}
805
806/// A computed trie whose finalization and pruning may still be running.
807#[derive(Debug)]
808pub struct PendingSparseTrie {
809    state_root: B256,
810    anchor_hash: B256,
811    trie: mpsc::Receiver<SparseTrieWithStorageRoots>,
812}
813
814/// Publishes a matching completed root for reuse after its block has been accepted.
815///
816/// Does nothing if the producer has not handed off a trie or its root differs.
817pub fn publish_sparse_trie<N: NodePrimitives>(
818    pending_trie_rx: &mpsc::Receiver<PendingSparseTrie>,
819    overlay_manager: &OverlayManager<N>,
820    block_hash: B256,
821    state_root: B256,
822) {
823    // Validation has already received the root. A builder using another strategy may finish
824    // before the sparse task; it must not wait for that task just to populate the cache.
825    if let Ok(pending) = pending_trie_rx.try_recv() &&
826        pending.state_root == state_root
827    {
828        overlay_manager.store_sparse_trie(PreservedSparseTrie::pending(
829            pending.trie,
830            block_hash,
831            pending.anchor_hash,
832        ));
833    }
834}
835
836struct SparseTrieTaskOptions<N: NodePrimitives> {
837    parent_header: SealedHeader<N::BlockHeader>,
838    preserved_sparse_trie: Option<PreservedSparseTrie>,
839    pending_trie_tx: mpsc::Sender<PendingSparseTrie>,
840    chunk_size: usize,
841    worker_counts: ProofWorkerCounts,
842    #[cfg(feature = "trie-debug")]
843    proof_jitter: Option<Duration>,
844    /// `None` disables pruning. `Some(Vec::new())` prunes nodes older than the current block.
845    pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
846}
847
848/// Inputs for starting one sparse-trie task and its proof workers.
849///
850/// Complete the returned handle's authoritative update stream before awaiting its result.
851#[derive(Debug)]
852pub struct StateRootTaskOptions<'a, N: NodePrimitives> {
853    /// Accepted parent whose post-state the proof workers must read.
854    pub parent_header: SealedHeader<N::BlockHeader>,
855    /// Previously accepted trie, or `None` for a cold start.
856    pub preserved_sparse_trie: Option<PreservedSparseTrie>,
857    /// Transaction count hint for proof-worker sizing, or `None` if unknown.
858    pub transaction_count: Option<usize>,
859    /// Gas the block claims to use, or `None` if unknown.
860    pub gas_used: Option<u64>,
861    /// Task and proof batching configuration.
862    pub config: &'a TreeConfig,
863    /// In-memory parent chain, newest first, when pruning is requested.
864    ///
865    /// `None` disables pruning; an empty chain means the parent is durable.
866    pub pending_sparse_trie_prune_blocks: Option<Vec<ExecutedBlock<N>>>,
867}
868
869fn sparse_trie_prune_target<N: NodePrimitives>(
870    current_anchor: B256,
871    reused_preserved_sparse_trie: bool,
872    parent: alloy_eips::BlockNumHash,
873    pending_blocks: Option<&[ExecutedBlock<N>]>,
874) -> Option<(TrieNodeEpoch, B256)> {
875    let blocks = pending_blocks?;
876    let Some(oldest) = blocks.last() else {
877        // Pruning the now-durable parent moves the proof source forward with it.
878        return Some((TrieNodeEpoch::new(parent.number.saturating_add(1)), parent.hash))
879    };
880    let anchor = if !reused_preserved_sparse_trie ||
881        blocks.iter().any(|block| block.recovered_block().hash() == current_anchor)
882    {
883        current_anchor
884    } else {
885        oldest.recovered_block().parent_hash()
886    };
887    Some((TrieNodeEpoch::new(oldest.recovered_block().number()), anchor))
888}
889
890impl<N, P, Evm> StateRootStrategy<N, P, Evm> for DefaultStateRootStrategy
891where
892    N: NodePrimitives,
893    P: DatabaseProviderFactory + Clone + 'static,
894    P::Provider: BlockNumReader
895        + PruneCheckpointReader
896        + StageCheckpointReader
897        + ChangeSetReader
898        + StorageChangeSetReader
899        + StorageSettingsCache
900        + 'static,
901    Evm: ConfigureEvm<Primitives = N> + 'static,
902{
903    fn prepare(
904        &self,
905        mut ctx: StateRootJobContext<'_, N, P, Evm>,
906    ) -> ProviderResult<PreparedStateRootJob<N>> {
907        if ctx.config.skip_state_root() {
908            return Ok(PreparedStateRootJob::new(Box::new(SkippedStateRootJob {}), None))
909        }
910
911        if !ctx.config.use_state_root_task() {
912            return Ok(PreparedStateRootJob::new(
913                Box::new(SynchronousStateRootJob {
914                    state_provider_factory: ctx.state_provider_factory,
915                }),
916                None,
917            ))
918        }
919
920        let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
921        let StateRootJobContext {
922            executor,
923            overlay_manager,
924            env,
925            parent_header,
926            state_provider_factory,
927            config,
928            parallel_bal_execution,
929            state: _,
930        } = ctx;
931
932        let preserved_sparse_trie = overlay_manager.take_sparse_trie();
933        let (mut handle, pending_trie_rx) = self.spawn_state_root(
934            executor,
935            state_provider_factory.clone(),
936            StateRootTaskOptions {
937                parent_header: parent_header.clone(),
938                preserved_sparse_trie,
939                transaction_count: Some(env.transaction_count),
940                gas_used: Some(env.gas_used),
941                config,
942                pending_sparse_trie_prune_blocks,
943            },
944        );
945
946        // The execution mode decides who finishes the update stream: the execution hook on
947        // the serial path, the BAL streamer on the parallel path. Both come from one slot in
948        // the handle, so only one of them can exist.
949        let (hashed_update_stream, execution_hook): (
950            Option<StateRootUpdateStream>,
951            Option<StateRootUpdateHook>,
952        ) = match parallel_bal_execution {
953            true => (Some(handle.take_hashed_update_stream()), None),
954            false => (None, Some(handle.take_execution_hook())),
955        };
956        let hint_stream = handle.take_hint_stream();
957
958        let hashed_state_rx = Some(handle.take_hashed_state_rx());
959
960        let mut prepared = PreparedStateRootJob::new(
961            Box::new(SparseTrieStateRootJob {
962                pending_trie_rx,
963                overlay_manager: overlay_manager.clone(),
964                handle,
965                state_provider_factory,
966                executor: executor.clone(),
967                timeout: config.state_root_task_timeout(),
968                metrics: BlockValidationMetrics::default(),
969            }),
970            hashed_state_rx,
971        )
972        .with_hint_stream(hint_stream);
973        if let Some(hook) = execution_hook {
974            prepared = prepared.with_execution_hook(hook);
975        }
976        if let Some(stream) = hashed_update_stream {
977            prepared = prepared.with_hashed_update_stream(stream);
978        }
979        Ok(prepared)
980    }
981
982    fn prepare_payload_builder(
983        &self,
984        mut ctx: PayloadStateRootJobContext<'_, N, P>,
985    ) -> ProviderResult<Option<PayloadStateRootHandle>> {
986        // Sharing the engine state-root task with the payload builder is opt-in, and needs a
987        // host that can run the task pipeline at all.
988        if !ctx.config.share_sparse_trie_with_payload_builder() ||
989            ctx.config.skip_state_root() ||
990            !ctx.config.has_enough_parallelism()
991        {
992            return Ok(None)
993        }
994
995        let pending_sparse_trie_prune_blocks = ctx.take_sparse_trie_prune_blocks();
996        let parent_header = SealedHeader::new(ctx.parent_header().clone(), ctx.parent_hash());
997        let preserved_sparse_trie = ctx.overlay_manager.take_sparse_trie();
998        let (handle, pending_trie_rx) = self.spawn_state_root(
999            ctx.executor,
1000            ctx.state_provider_factory.clone(),
1001            StateRootTaskOptions {
1002                parent_header,
1003                preserved_sparse_trie,
1004                // Block built incrementally, so neither the tx count nor the gas is known at FCU
1005                // time: the payload builder uses half of each proof pool.
1006                transaction_count: None,
1007                gas_used: None,
1008                config: ctx.config,
1009                pending_sparse_trie_prune_blocks,
1010            },
1011        );
1012        let overlay_manager = ctx.overlay_manager.clone();
1013        Ok(Some(handle.into_payload_state_root_handle().with_on_payload_built(
1014            move |block_hash, state_root| {
1015                publish_sparse_trie(&pending_trie_rx, &overlay_manager, block_hash, state_root);
1016            },
1017        )))
1018    }
1019}
1020
1021#[derive(Debug)]
1022struct SkippedStateRootJob {}
1023
1024impl<N: NodePrimitives> StateRootJob<N> for SkippedStateRootJob {
1025    fn name(&self) -> &'static str {
1026        "skipped"
1027    }
1028
1029    fn finish(
1030        &mut self,
1031        block: &RecoveredBlock<N::Block>,
1032        _output: Arc<BlockExecutionOutput<N::Receipt>>,
1033        _hashed_state: &LazyHashedPostState,
1034    ) -> ProviderResult<StateRootJobOutcome> {
1035        Ok(StateRootJobOutcome::new(block.header().state_root(), Arc::new(TrieUpdates::default())))
1036    }
1037}
1038
1039#[derive(Debug)]
1040struct SynchronousStateRootJob<N: NodePrimitives, P> {
1041    state_provider_factory: OverlayStateProviderFactory<P, N>,
1042}
1043
1044impl<N, P> StateRootJob<N> for SynchronousStateRootJob<N, P>
1045where
1046    N: NodePrimitives,
1047    P: DatabaseProviderFactory + Clone + 'static,
1048    P::Provider: BlockNumReader
1049        + PruneCheckpointReader
1050        + StageCheckpointReader
1051        + ChangeSetReader
1052        + StorageChangeSetReader
1053        + StorageSettingsCache
1054        + 'static,
1055{
1056    fn name(&self) -> &'static str {
1057        "synchronous"
1058    }
1059
1060    fn finish(
1061        &mut self,
1062        _block: &RecoveredBlock<N::Block>,
1063        _output: Arc<BlockExecutionOutput<N::Receipt>>,
1064        hashed_state: &LazyHashedPostState,
1065    ) -> ProviderResult<StateRootJobOutcome> {
1066        let provider = self.state_provider_factory.database_provider_ro()?;
1067        let (state_root, trie_updates) =
1068            provider.state_root_with_updates(hashed_state.get().as_ref().clone())?;
1069        Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates)))
1070    }
1071}
1072
1073#[derive(Debug)]
1074struct SparseTrieStateRootJob<N: NodePrimitives, P> {
1075    pending_trie_rx: mpsc::Receiver<PendingSparseTrie>,
1076    overlay_manager: OverlayManager<N>,
1077    handle: StateRootHandle,
1078    state_provider_factory: OverlayStateProviderFactory<P, N>,
1079    executor: reth_tasks::Runtime,
1080    timeout: Option<Duration>,
1081    metrics: BlockValidationMetrics,
1082}
1083
1084impl<N, P> SparseTrieStateRootJob<N, P>
1085where
1086    N: NodePrimitives,
1087    P: DatabaseProviderFactory + Clone + 'static,
1088    P::Provider: BlockNumReader
1089        + PruneCheckpointReader
1090        + StageCheckpointReader
1091        + ChangeSetReader
1092        + StorageChangeSetReader
1093        + StorageSettingsCache
1094        + 'static,
1095{
1096    fn serial_fallback(
1097        &self,
1098        output: Arc<BlockExecutionOutput<N::Receipt>>,
1099    ) -> ProviderResult<SerialFallbackRx> {
1100        let provider = self.state_provider_factory.database_provider_ro()?;
1101        let (fallback_tx, fallback_rx) = mpsc::channel();
1102        self.executor.spawn_blocking_named("serial-root", move || {
1103            let result = (|| {
1104                let hashed_state = Arc::new(provider.hashed_post_state(&output.state)?);
1105                let (root, updates) =
1106                    provider.state_root_with_updates(hashed_state.as_ref().clone())?;
1107                Ok((root, updates, hashed_state))
1108            })();
1109            let _ = fallback_tx.send(result);
1110        });
1111
1112        Ok(fallback_rx)
1113    }
1114
1115    /// Recomputes the state root serially from the execution output.
1116    ///
1117    /// Used when the state-root task failed or produced a wrong root, so the recomputed hashed
1118    /// post state is returned in the outcome for validation to re-check against.
1119    fn compute_serial(
1120        &self,
1121        output: &BlockExecutionOutput<N::Receipt>,
1122    ) -> ProviderResult<StateRootJobOutcome> {
1123        let provider = self.state_provider_factory.database_provider_ro()?;
1124        let hashed_state = Arc::new(provider.hashed_post_state(&output.state)?);
1125        let (state_root, trie_updates) =
1126            provider.state_root_with_updates(hashed_state.as_ref().clone())?;
1127        self.metrics.state_root_task_fallback_success_total.increment(1);
1128        Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1129            .with_hashed_state(Some(hashed_state)))
1130    }
1131
1132    /// Converts a task outcome into a job outcome, recomputing serially when the task returned
1133    /// a root that does not match the block header. A state-root-task bug then costs latency
1134    /// instead of marking a valid block invalid; if the serial root also mismatches, validation
1135    /// rejects the block.
1136    fn verified_sparse_outcome(
1137        &self,
1138        block: &RecoveredBlock<N::Block>,
1139        output: &BlockExecutionOutput<N::Receipt>,
1140        outcome: StateRootComputeOutcome,
1141    ) -> ProviderResult<StateRootJobOutcome> {
1142        let outcome = StateRootJobOutcome::new(outcome.state_root, outcome.trie_updates);
1143        if outcome.state_root == block.header().state_root() {
1144            publish_sparse_trie(
1145                &self.pending_trie_rx,
1146                &self.overlay_manager,
1147                block.hash(),
1148                outcome.state_root,
1149            );
1150            return Ok(outcome)
1151        }
1152        warn!(
1153            target: "engine::tree::state_root_strategy",
1154            state_root = ?outcome.state_root,
1155            block_state_root = ?block.header().state_root(),
1156            "State root task returned incorrect state root, recomputing serially"
1157        );
1158        self.compute_serial(output)
1159    }
1160}
1161
1162impl<N, P> StateRootJob<N> for SparseTrieStateRootJob<N, P>
1163where
1164    N: NodePrimitives,
1165    P: DatabaseProviderFactory + Clone + 'static,
1166    P::Provider: BlockNumReader
1167        + PruneCheckpointReader
1168        + StageCheckpointReader
1169        + ChangeSetReader
1170        + StorageChangeSetReader
1171        + StorageSettingsCache
1172        + 'static,
1173{
1174    fn name(&self) -> &'static str {
1175        "sparse-trie"
1176    }
1177
1178    fn finish(
1179        &mut self,
1180        block: &RecoveredBlock<N::Block>,
1181        output: Arc<BlockExecutionOutput<N::Receipt>>,
1182        _hashed_state: &LazyHashedPostState,
1183    ) -> ProviderResult<StateRootJobOutcome> {
1184        if self.timeout.is_none() {
1185            return match self.handle.state_root() {
1186                Ok(outcome) => self.verified_sparse_outcome(block, &output, outcome),
1187                Err(err) => {
1188                    debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1189                    self.compute_serial(&output)
1190                }
1191            }
1192        }
1193
1194        let timeout = self.timeout.expect("checked above");
1195        let task_rx = self.handle.take_state_root_rx();
1196        let fallback_rx = match task_rx.recv_timeout(timeout) {
1197            Ok(Ok(outcome)) => return self.verified_sparse_outcome(block, &output, outcome),
1198            Ok(Err(err)) => {
1199                debug!(target: "engine::tree::state_root_strategy", %err, "State root task failed, falling back to serial root");
1200                self.serial_fallback(output)?
1201            }
1202            Err(RecvTimeoutError::Timeout) => {
1203                warn!(target: "engine::tree::state_root_strategy", ?timeout, "State root task timed out, racing serial fallback");
1204                self.metrics.state_root_task_timeout_total.increment(1);
1205                self.serial_fallback(output)?
1206            }
1207            Err(RecvTimeoutError::Disconnected) => {
1208                debug!(target: "engine::tree::state_root_strategy", "State root task dropped, falling back to serial root");
1209                self.serial_fallback(output)?
1210            }
1211        };
1212
1213        loop {
1214            if let Ok(Ok(outcome)) = task_rx.try_recv() {
1215                let outcome = StateRootJobOutcome::new(outcome.state_root, outcome.trie_updates);
1216                if outcome.state_root == block.header().state_root() {
1217                    publish_sparse_trie(
1218                        &self.pending_trie_rx,
1219                        &self.overlay_manager,
1220                        block.hash(),
1221                        outcome.state_root,
1222                    );
1223                    return Ok(outcome)
1224                }
1225                // A wrong task root falls through to the serial fallback already racing below.
1226                warn!(
1227                    target: "engine::tree::state_root_strategy",
1228                    state_root = ?outcome.state_root,
1229                    block_state_root = ?block.header().state_root(),
1230                    "State root task returned incorrect state root, using serial fallback"
1231                );
1232            }
1233
1234            match fallback_rx.try_recv() {
1235                Ok(Ok((state_root, trie_updates, hashed_state))) => {
1236                    self.metrics.state_root_task_fallback_success_total.increment(1);
1237                    return Ok(StateRootJobOutcome::new(state_root, Arc::new(trie_updates))
1238                        .with_hashed_state(Some(hashed_state)))
1239                }
1240                Ok(Err(err)) => return Err(err),
1241                Err(mpsc::TryRecvError::Empty) => {}
1242                Err(mpsc::TryRecvError::Disconnected) => {
1243                    return Err(ProviderError::other(std::io::Error::other(
1244                        "serial state root fallback task dropped",
1245                    )))
1246                }
1247            }
1248
1249            std::thread::sleep(Duration::from_millis(1));
1250        }
1251    }
1252}
1253
1254#[cfg(test)]
1255mod tests {
1256    use super::*;
1257    use alloy_consensus::constants::KECCAK_EMPTY;
1258    use alloy_primitives::{map::HashMap, Address, U256};
1259    use evm2::evm::AccountInfo;
1260    use rand::Rng;
1261    use reth_chain_state::test_utils::TestBlockBuilder;
1262    use reth_chainspec::ChainSpec;
1263    use reth_db_common::init::init_genesis;
1264    use reth_ethereum_primitives::EthPrimitives;
1265    use reth_evm::OnStateHook;
1266    use reth_evm_ethereum::EthEvmConfig;
1267    use reth_execution_types::{AccountUpdate, StateUpdate};
1268    use reth_primitives_traits::{Account, StorageEntry};
1269    use reth_provider::{
1270        providers::BlockchainProvider, test_utils::create_test_provider_factory_with_chain_spec,
1271        HashingWriter,
1272    };
1273    use reth_storage_overlay::{OverlayManager, OverlayStateProviderFactory};
1274    use reth_testing_utils::generators;
1275    use reth_trie::test_utils::state_root;
1276    use reth_trie_common::bundle_state::StorageSlot;
1277
1278    #[test]
1279    fn proof_worker_count_scales_with_block_gas() {
1280        let pool_threads = 64;
1281        let count = |transaction_count, gas_used| {
1282            DefaultStateRootStrategy::proof_worker_count(
1283                pool_threads,
1284                None,
1285                transaction_count,
1286                gas_used,
1287            )
1288        };
1289
1290        // Small blocks use a quarter of the pool, whatever their gas.
1291        assert_eq!(count(Some(30), Some(1_000_000)), 16);
1292        assert_eq!(count(Some(30), Some(300_000_000)), 16);
1293
1294        // Gas heavy blocks use the full pool.
1295        assert_eq!(count(Some(1_000), Some(100_000_000)), 64);
1296        assert_eq!(count(Some(1_000), Some(300_000_000)), 64);
1297
1298        // Everything else, including a block whose size is not known yet, uses half the pool.
1299        assert_eq!(count(Some(31), Some(99_999_999)), 32);
1300        assert_eq!(count(Some(1_000), Some(15_000_000)), 32);
1301        assert_eq!(count(None, None), 32);
1302
1303        // An explicitly configured count is used verbatim for every block.
1304        for (transaction_count, gas_used) in
1305            [(Some(30), Some(1_000_000)), (Some(1_000), Some(300_000_000)), (None, None)]
1306        {
1307            assert_eq!(
1308                DefaultStateRootStrategy::proof_worker_count(
1309                    48,
1310                    Some(48),
1311                    transaction_count,
1312                    gas_used
1313                ),
1314                48
1315            );
1316        }
1317
1318        // Pools smaller than two threads, and a pinned count of zero, still get a worker.
1319        assert_eq!(
1320            DefaultStateRootStrategy::proof_worker_count(1, None, Some(1), Some(1_000_000)),
1321            1
1322        );
1323        assert_eq!(DefaultStateRootStrategy::proof_worker_count(1, None, None, None), 1);
1324        assert_eq!(DefaultStateRootStrategy::proof_worker_count(1, Some(0), None, None), 1);
1325    }
1326
1327    #[test]
1328    fn finish_publishes_only_accepted_sparse_trie() {
1329        let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1330        let genesis_hash = init_genesis(&factory).unwrap();
1331        let provider = BlockchainProvider::new(factory).unwrap();
1332        let runtime = reth_tasks::Runtime::test();
1333
1334        for timeout in [None, Some(Duration::from_secs(1))] {
1335            for (finish, matching_root) in [(false, true), (true, true), (true, false)] {
1336                let overlay_manager = OverlayManager::<EthPrimitives>::default();
1337                let block = RecoveredBlock::new_unhashed(
1338                    reth_ethereum_primitives::Block {
1339                        header: alloy_consensus::Header {
1340                            state_root: B256::with_last_byte(1),
1341                            ..Default::default()
1342                        },
1343                        ..Default::default()
1344                    },
1345                    Vec::new(),
1346                );
1347                let (completer, trie) = mpsc::channel();
1348                let (pending_trie_tx, pending_trie_rx) = mpsc::channel();
1349                pending_trie_tx
1350                    .send(PendingSparseTrie {
1351                        state_root: if matching_root { block.state_root } else { B256::ZERO },
1352                        anchor_hash: genesis_hash,
1353                        trie,
1354                    })
1355                    .unwrap();
1356                let (result_tx, result_rx) = mpsc::channel();
1357                result_tx
1358                    .send(Ok(StateRootComputeOutcome {
1359                        state_root: if matching_root { block.state_root } else { B256::ZERO },
1360                        trie_updates: Default::default(),
1361                        hashed_state: Default::default(),
1362                    }))
1363                    .unwrap();
1364                let mut job = SparseTrieStateRootJob {
1365                    pending_trie_rx,
1366                    overlay_manager: overlay_manager.clone(),
1367                    handle: StateRootHandle::new(
1368                        B256::ZERO,
1369                        crossbeam_channel::unbounded().0,
1370                        StateRootTaskCancelGuard::channel().0,
1371                        result_rx,
1372                        mpsc::channel().1,
1373                    ),
1374                    state_provider_factory: OverlayStateProviderFactory::new(
1375                        provider.clone(),
1376                        overlay_manager.overlay_builder(genesis_hash),
1377                    ),
1378                    executor: runtime.clone(),
1379                    timeout,
1380                    metrics: Default::default(),
1381                };
1382                assert!(overlay_manager.take_sparse_trie().is_none());
1383                if finish {
1384                    job.finish(
1385                        &block,
1386                        Arc::new(BlockExecutionOutput::default()),
1387                        &LazyHashedPostState::ready(Default::default()),
1388                    )
1389                    .unwrap();
1390                }
1391                drop(job);
1392                let preserved = overlay_manager.take_sparse_trie();
1393                assert_eq!(preserved.is_some(), finish && matching_root);
1394                // Publication must finish before trie finalization does.
1395                assert_eq!(
1396                    completer.send(SparseTrieWithStorageRoots::default()).is_ok(),
1397                    finish && matching_root
1398                );
1399                if let Some(preserved) = preserved {
1400                    assert!(preserved.into_trie_for(block.hash()).unwrap().is_some());
1401                }
1402            }
1403        }
1404    }
1405
1406    #[test]
1407    fn failed_parent_trie_closes_handoff_without_publishing() {
1408        let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1409        let genesis_hash = init_genesis(&factory).unwrap();
1410        let runtime = reth_tasks::Runtime::test();
1411        {
1412            let (completer, trie) = mpsc::channel();
1413            let preserved = PreservedSparseTrie::pending(trie, genesis_hash, B256::ZERO);
1414            drop(completer);
1415            let (mut handle, pending_trie_rx) = DefaultStateRootStrategy::default()
1416                .spawn_state_root(
1417                    &runtime,
1418                    OverlayStateProviderFactory::new(
1419                        factory,
1420                        OverlayManager::<EthPrimitives>::default().overlay_builder(genesis_hash),
1421                    ),
1422                    StateRootTaskOptions::<EthPrimitives> {
1423                        parent_header: SealedHeader::new(Default::default(), genesis_hash),
1424                        preserved_sparse_trie: Some(preserved),
1425                        transaction_count: None,
1426                        gas_used: None,
1427                        config: &TreeConfig::default(),
1428                        pending_sparse_trie_prune_blocks: None,
1429                    },
1430                );
1431            assert!(handle.state_root().is_err());
1432            assert!(matches!(
1433                pending_trie_rx.recv_timeout(Duration::from_secs(5)),
1434                Err(RecvTimeoutError::Disconnected)
1435            ));
1436        }
1437        for name in ["sparse-trie", "storage-workers", "account-workers"] {
1438            runtime.spawn_blocking_named(name, || {}).get();
1439        }
1440    }
1441
1442    #[test]
1443    fn publication_does_not_wait_for_worker() {
1444        let (pending_trie_tx, pending_trie_rx) = mpsc::channel();
1445        let overlay_manager = OverlayManager::<EthPrimitives>::default();
1446        let (done_tx, done_rx) = mpsc::channel();
1447        let worker = std::thread::spawn(move || {
1448            publish_sparse_trie(&pending_trie_rx, &overlay_manager, B256::ZERO, B256::ZERO);
1449            assert!(overlay_manager.take_sparse_trie().is_none());
1450            done_tx.send(()).unwrap();
1451        });
1452        let result = done_rx.recv_timeout(Duration::from_secs(5));
1453        drop(pending_trie_tx);
1454        worker.join().unwrap();
1455        result.expect("publication should not wait for the root task");
1456    }
1457
1458    #[test]
1459    fn dropped_root_receiver_still_completes_pending_trie() {
1460        let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1461        let genesis_hash = init_genesis(&factory).unwrap();
1462        let runtime = reth_tasks::Runtime::test();
1463        let (mut handle, pending_trie_rx) = DefaultStateRootStrategy::default().spawn_state_root(
1464            &runtime,
1465            OverlayStateProviderFactory::new(
1466                factory,
1467                OverlayManager::<EthPrimitives>::default().overlay_builder(genesis_hash),
1468            ),
1469            StateRootTaskOptions::<EthPrimitives> {
1470                parent_header: SealedHeader::new(Default::default(), genesis_hash),
1471                preserved_sparse_trie: None,
1472                transaction_count: None,
1473                gas_used: None,
1474                config: &TreeConfig::default(),
1475                pending_sparse_trie_prune_blocks: None,
1476            },
1477        );
1478        drop(handle.take_state_root_rx());
1479        drop(handle.take_execution_hook());
1480        let pending = pending_trie_rx.recv_timeout(Duration::from_secs(5)).unwrap();
1481        assert_eq!(pending.anchor_hash, genesis_hash);
1482        pending
1483            .trie
1484            .recv_timeout(Duration::from_secs(5))
1485            .expect("handed-off trie must be completed");
1486        drop(handle);
1487        for name in ["sparse-trie", "trie-hashing", "storage-workers", "account-workers", "drop"] {
1488            runtime.spawn_blocking_named(name, || {}).get();
1489        }
1490    }
1491
1492    #[test]
1493    fn sparse_trie_prune_target_uses_requested_range() {
1494        let parent = alloy_eips::BlockNumHash { hash: B256::with_last_byte(9), number: 9 };
1495        assert_eq!(
1496            sparse_trie_prune_target::<EthPrimitives>(B256::ZERO, false, parent, None),
1497            None
1498        );
1499        assert_eq!(
1500            sparse_trie_prune_target::<EthPrimitives>(B256::ZERO, false, parent, Some(&[])),
1501            Some((TrieNodeEpoch::new(10), parent.hash))
1502        );
1503
1504        let mut blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(7..10).collect();
1505        blocks.reverse();
1506
1507        let anchor = blocks[0].recovered_block().hash();
1508        assert_eq!(
1509            sparse_trie_prune_target(anchor, true, parent, Some(&blocks)),
1510            Some((TrieNodeEpoch::new(7), anchor))
1511        );
1512    }
1513
1514    #[test]
1515    fn sparse_trie_prune_target_advances_to_prune_anchor() {
1516        let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..5).collect();
1517        let reused_anchor_hash = blocks[0].recovered_block().hash();
1518        let expected_prune_anchor = blocks[1].recovered_block().hash();
1519        let parent = blocks[3].recovered_block().num_hash();
1520        let prune_blocks: Vec<_> = blocks.into_iter().skip(2).rev().collect();
1521
1522        assert_eq!(
1523            sparse_trie_prune_target(reused_anchor_hash, true, parent, Some(&prune_blocks),),
1524            Some((TrieNodeEpoch::new(3), expected_prune_anchor))
1525        );
1526    }
1527
1528    #[test]
1529    fn sparse_trie_prune_target_keeps_parent_for_fresh_trie() {
1530        let mut blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..3).collect();
1531        blocks.reverse();
1532        let parent_hash = B256::with_last_byte(0xaa);
1533
1534        assert_eq!(
1535            sparse_trie_prune_target(
1536                parent_hash,
1537                false,
1538                blocks[0].recovered_block().num_hash(),
1539                Some(&blocks),
1540            ),
1541            Some((TrieNodeEpoch::new(1), parent_hash))
1542        );
1543    }
1544
1545    #[test]
1546    fn sparse_trie_prune_target_advances_reused_anchor_when_parent_becomes_durable() {
1547        let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..4).collect();
1548        let old_anchor = blocks[1].recovered_block().hash();
1549        let parent = blocks[2].recovered_block().num_hash();
1550
1551        assert_eq!(
1552            sparse_trie_prune_target::<EthPrimitives>(old_anchor, true, parent, Some(&[])),
1553            Some((TrieNodeEpoch::new(4), parent.hash))
1554        );
1555    }
1556
1557    fn create_mock_state_updates(
1558        num_accounts: usize,
1559        updates_per_account: usize,
1560    ) -> Vec<StateUpdate> {
1561        let mut rng = generators::rng();
1562        let all_addresses: Vec<Address> = (0..num_accounts).map(|_| rng.random()).collect();
1563        let mut updates = Vec::with_capacity(updates_per_account);
1564
1565        for _ in 0..updates_per_account {
1566            let num_accounts_in_update = rng.random_range(1..=num_accounts);
1567            let mut state_update = StateUpdate::default();
1568
1569            for &address in &all_addresses[0..num_accounts_in_update] {
1570                let mut storage = HashMap::<_, _>::default();
1571                if rng.random_bool(0.7) {
1572                    for _ in 0..rng.random_range(1..10) {
1573                        let slot = U256::from(rng.random::<u64>());
1574                        storage.insert(
1575                            slot,
1576                            StorageSlot::new_changed(U256::ZERO, U256::from(rng.random::<u64>())),
1577                        );
1578                    }
1579                }
1580
1581                let info = AccountInfo {
1582                    balance: U256::from(rng.random::<u64>()),
1583                    nonce: rng.random::<u64>(),
1584                    code_hash: KECCAK_EMPTY,
1585                    code: Some(Default::default()),
1586                    ..Default::default()
1587                };
1588                state_update.accounts.push(AccountUpdate {
1589                    address,
1590                    original: Some(AccountInfo::default()),
1591                    current: Some(info),
1592                    created: false,
1593                    wiped: false,
1594                    storage: storage.into_iter().collect(),
1595                });
1596            }
1597
1598            updates.push(state_update);
1599        }
1600
1601        updates
1602    }
1603
1604    #[test]
1605    fn payload_builder_trie_matches_serial_root_and_can_be_reused() {
1606        reth_tracing::init_test_tracing();
1607
1608        let factory = create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
1609        let genesis_hash = init_genesis(&factory).unwrap();
1610        let state_updates = create_mock_state_updates(10, 10);
1611        let mut accumulated_state: HashMap<Address, (Account, HashMap<B256, U256>)> =
1612            HashMap::default();
1613
1614        {
1615            let provider_rw = factory.provider_rw().expect("failed to get provider");
1616            for update in &state_updates {
1617                let account_updates = update.accounts.iter().map(|account| {
1618                    (
1619                        account.address,
1620                        account.current.as_ref().map(reth_primitives_traits::Account::from),
1621                    )
1622                });
1623                provider_rw
1624                    .insert_account_for_hashing(account_updates)
1625                    .expect("failed to insert accounts");
1626
1627                let storage_updates = update.accounts.iter().map(|account| {
1628                    let storage_entries = account.storage.iter().map(|(slot, value)| {
1629                        StorageEntry { key: B256::from(*slot), value: value.present_value }
1630                    });
1631                    (account.address, storage_entries)
1632                });
1633                provider_rw
1634                    .insert_storage_for_hashing(storage_updates)
1635                    .expect("failed to insert storage");
1636            }
1637            provider_rw.commit().expect("failed to commit changes");
1638        }
1639
1640        for update in &state_updates {
1641            for account in &update.accounts {
1642                let storage: HashMap<B256, U256> = account
1643                    .storage
1644                    .iter()
1645                    .map(|(key, value)| (B256::from(*key), value.present_value))
1646                    .collect();
1647                let entry = accumulated_state.entry(account.address).or_default();
1648                entry.0 = reth_primitives_traits::Account::from(account.current.as_ref().unwrap());
1649                entry.1.extend(storage);
1650            }
1651        }
1652
1653        let provider_factory = BlockchainProvider::new(factory).unwrap();
1654        let runtime = reth_tasks::Runtime::test();
1655        let overlay_manager = OverlayManager::<EthPrimitives>::default();
1656        let config = TreeConfig::default()
1657            .with_has_enough_parallelism(true)
1658            .with_share_sparse_trie_with_payload_builder(true);
1659        let mut state = EngineApiTreeState::new(
1660            10,
1661            10,
1662            config.invalid_header_hit_eviction_threshold(),
1663            alloy_eips::BlockNumHash::new(0, genesis_hash),
1664            crate::engine::EngineApiKind::Ethereum,
1665            overlay_manager.clone(),
1666        );
1667        let mut root_from_regular = state_root(accumulated_state.clone());
1668        let mut preserved: Option<PreservedSparseTrie> = None;
1669        for (number, (reuse, return_payload, matching_root)) in [
1670            (false, false, true),
1671            (false, true, false),
1672            (false, true, true),
1673            (true, true, true),
1674            (true, false, true),
1675        ]
1676        .into_iter()
1677        .enumerate()
1678        {
1679            assert_eq!(preserved.is_some(), reuse);
1680            let parent_hash =
1681                preserved.as_ref().map_or(genesis_hash, PreservedSparseTrie::block_hash);
1682            let block_hash = B256::with_last_byte(number as u8 + 1);
1683            if let Some(preserved) = preserved.take() {
1684                overlay_manager.store_sparse_trie(preserved);
1685            }
1686            let parent_header = alloy_consensus::Header {
1687                number: number as u64,
1688                state_root: if reuse { root_from_regular } else { B256::ZERO },
1689                ..Default::default()
1690            };
1691            let mut state_root_handle = <DefaultStateRootStrategy as StateRootStrategy<
1692                EthPrimitives,
1693                _,
1694                EthEvmConfig,
1695            >>::prepare_payload_builder(
1696                &DefaultStateRootStrategy::default(),
1697                PayloadStateRootJobContext::new(
1698                    &runtime,
1699                    &overlay_manager,
1700                    parent_hash,
1701                    &parent_header,
1702                    1,
1703                    &mut state,
1704                    OverlayStateProviderFactory::new(
1705                        provider_factory.clone(),
1706                        overlay_manager.overlay_builder(genesis_hash),
1707                    ),
1708                    &config,
1709                ),
1710            )
1711            .unwrap()
1712            .unwrap();
1713            let on_payload_built = state_root_handle.take_on_payload_built().unwrap();
1714            let mut state_hook = state_root_handle.take_state_hook();
1715            if reuse {
1716                let mut update = state_updates.last().unwrap().clone();
1717                for account in &mut update.accounts {
1718                    let info = account.current.as_mut().unwrap();
1719                    info.nonce += number as u64;
1720                    accumulated_state.get_mut(&account.address).unwrap().0 =
1721                        reth_primitives_traits::Account::from(&*info);
1722                }
1723                state_hook.on_state(update);
1724                root_from_regular = state_root(accumulated_state.clone());
1725            } else {
1726                for update in &state_updates {
1727                    state_hook.on_state(update.clone());
1728                }
1729            }
1730            drop(state_hook);
1731
1732            let root_from_task = state_root_handle.state_root().expect("task failed").state_root;
1733            assert_eq!(root_from_task, root_from_regular);
1734            assert!(overlay_manager.take_sparse_trie().is_none());
1735            drop(state_root_handle);
1736            if return_payload {
1737                on_payload_built(
1738                    block_hash,
1739                    if matching_root { root_from_task } else { B256::ZERO },
1740                );
1741            } else {
1742                drop(on_payload_built);
1743            }
1744            preserved = overlay_manager.take_sparse_trie();
1745            assert_eq!(preserved.is_some(), return_payload && matching_root);
1746            if let Some(preserved) = &preserved {
1747                assert_eq!(preserved.block_hash(), block_hash);
1748                assert_eq!(preserved.anchor_hash(), genesis_hash);
1749            }
1750        }
1751        for name in ["sparse-trie", "trie-hashing", "storage-workers", "account-workers", "drop"] {
1752            runtime.spawn_blocking_named(name, || {}).get();
1753        }
1754    }
1755}