Skip to main content

reth_engine_tree/tree/state_root_strategy/
mod.rs

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