Skip to main content

reth_engine_tree/tree/state_root_strategy/
sparse_trie.rs

1//! Sparse Trie task related functionality.
2
3use std::{
4    any::Any,
5    panic::{self, AssertUnwindSafe},
6    sync::Arc,
7};
8
9use super::{StateRootComputeOutcome, StateRootMessage};
10use alloy_primitives::{
11    map::{hash_map::Entry, B256Map},
12    B256,
13};
14use alloy_rlp::{Decodable, Encodable};
15use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
16use metrics::{Gauge, Histogram};
17use reth_metrics::Metrics;
18use reth_primitives_traits::{Account, FastInstant as Instant};
19use reth_tasks::Runtime;
20use reth_trie::{
21    updates::TrieUpdates, DecodedMultiProofV2, HashedPostState, TrieAccount, EMPTY_ROOT_HASH,
22    TRIE_ACCOUNT_RLP_MAX_SIZE,
23};
24use reth_trie_common::{MultiProofTargetsV2, ProofTrieNodeV2, ProofV2Target, ProofV2TargetParent};
25use reth_trie_parallel::{
26    error::StateRootTaskError,
27    proof_task::{ProofWorkerHandle, ProofWorkerResult},
28};
29use reth_trie_sparse::{
30    errors::{
31        SparseStateTrieErrorKind, SparseStateTrieResult, SparseTrieErrorKind, SparseTrieResult,
32    },
33    ArenaParallelSparseTrie, DeferredDrops, LeafUpdate, RevealableSparseTrie, SparseStateTrie,
34    SparseTrie, TrieNodeEpoch,
35};
36use tracing::{debug, debug_span, error, instrument, trace_span};
37
38/// Computes a block's state root by applying streamed updates to an in-memory [`SparseStateTrie`].
39/// Updates that reach a blinded node stay pending while the task fetches the missing proof. The
40/// account trie stays on this task's thread; storage tries can be moved into independent rayon
41/// jobs.
42///
43/// # Message flow
44///
45/// The task coordinates three incoming streams:
46///
47/// 1. **Execution and prewarming:** [`Self::run_hashing_task`] receives [`StateRootMessage`]s,
48///    hashes raw state updates, and forwards [`SparseTrieTaskMessage`]s to [`Self::updates`] in
49///    receive order. `HashedState` supplies changed values; `PrefetchProofs` supplies
50///    [`LeafUpdate::Touched`] hints that must not overwrite changed values. The task buffers these
51///    leaf updates before applying them in batches.
52/// 2. **Proof workers:** blinded paths become [`MultiProofTargetsV2`] chunks, which
53///    [`ProofWorkerHandle::dispatch`] splits into independent account and per-address storage jobs.
54///    The task coalesces the [`ProofWorkerResult`]s returned through
55///    [`ProofWorkerHandle::results`], reveals account proofs locally, and queues storage proofs on
56///    the corresponding address' [`StorageTrieState`].
57/// 3. **Storage jobs:** [`Self::spawn_storage_jobs`] moves each [`StorageTrieWork`] into a rayon
58///    closure. A pass reveals queued proofs, applies unblocked leaves, and hashes the storage trie
59///    once its pending leaves are drained. It sends [`StorageJobMessage::Done`] via
60///    [`Self::storage_done_tx`] to [`Self::storage_done_rx`], returning ownership of the payload
61///    along with newly discovered proof targets, cache counters, and any error. Small or
62///    target-only passes run inline and apply the same output directly.
63///
64/// [`Self::run`] selects between these streams and cancellation. After handling a message,
65/// [`Self::make_progress`] collects queued job returns, applies buffered updates, dispatches proof
66/// requests and storage jobs, and promotes account updates whose storage roots are available.
67/// Proof workers fetch nodes; storage jobs reveal and modify the owned storage trie. Neither
68/// mutates the account trie.
69///
70/// # Storage ownership
71///
72/// On first use, an address' storage trie moves out of [`SparseStateTrie`] into an idle entry.
73/// There is at most one job per address, and no shared mutable access to its trie:
74///
75/// ```text
76///                    dispatch: move StorageTrieWork to rayon
77///   Idle --------------------------------------------------------> InFlight
78///   (task owns trie)                                               (job owns trie)
79///        ^                                                              |
80///        +--------------------------------------------------------------+
81///                    Done: restore payload and merge buffered arrivals
82/// ```
83///
84/// In `Idle`, new proofs and leaf updates make the payload ready for another pass. An inline pass
85/// keeps it in `Idle`. In `InFlight`, the task holds only [`InFlightStorage`] buffers for new
86/// arrivals; the job owns the trie, earlier pending leaves, proofs, and fetched-target cache.
87/// When `Done` arrives, newer changed values and deletions override older pending values, while
88/// touch hints preserve them. Buffered work can then trigger another pass.
89///
90/// `Idle` describes ownership, not completion: a payload can still have leaves blocked on a proof.
91/// Blocked leaves alone do not trigger another pass; new updates or that address' own proof nodes
92/// make it ready again. Account updates wait while their storage trie is in flight or still has
93/// pending leaves, so they cannot use an incomplete storage root.
94///
95/// # Task lifecycle
96///
97/// ```text
98///   Streaming -- FinishedStateUpdates --> Draining -- no pending trie work --> Finalize
99///       |                                    |
100///       +------------- cancellation ---------+--> Canceled
101/// ```
102///
103/// The finish marker publishes the accumulated [`HashedPostState`] through
104/// [`Self::final_hashed_state_tx`] and ends consumption of the update stream. Draining continues
105/// to receive proof results and storage-job returns; late prefetch hints are ignored. Closing
106/// the update channel before the marker is an error, while closing it afterward is harmless.
107///
108/// Finalization waits for all leaf updates, account promotions, and checked-out storage tries.
109/// It restores the storage tries to [`SparseStateTrie`], computes the final root and trie updates,
110/// and returns [`StateRootComputeOutcome`]. The caller publishes that result before consuming
111/// the task with [`Self::into_trie_for_reuse`], which also releases the retained job buffers.
112///
113/// Dropping the consumer's cancel guard makes [`Self::run`] return
114/// [`StateRootTaskError::Canceled`]. Teardown through [`Self::into_cleared_trie`] disconnects the
115/// storage return channel, so late jobs drop their payloads when sending fails. Proof and trie
116/// errors fail the calculation; [`StorageJobMessage::Panicked`] resumes the worker's panic on
117/// this task's thread. During draining, pending updates with no remaining source of progress
118/// produce [`StateRootTaskError::Stalled`].
119pub(super) struct SparseTrieCacheTask<A = ArenaParallelSparseTrie, S = ArenaParallelSparseTrie> {
120    /// Receives updates from execution and prewarming.
121    updates: CrossbeamReceiver<SparseTrieTaskMessage>,
122    /// Fires (by disconnecting) when the consumer drops its cancel guard, meaning nobody is
123    /// waiting for the result anymore. This is the teardown path for a task whose pending
124    /// work never drains, since the updates channel closing is a normal end of stream.
125    cancel_rx: CrossbeamReceiver<()>,
126    /// Sender half for the channel to send final hashed state to.
127    final_hashed_state_tx: Option<std::sync::mpsc::Sender<Arc<HashedPostState>>>,
128    /// `SparseStateTrie` used for computing the state root.
129    trie: SparseStateTrie<A, S>,
130    /// The parent block's state root.
131    parent_state_root: B256,
132    /// The new epoch assigned to nodes modified by this task.
133    new_epoch: TrieNodeEpoch,
134    /// Handle to the proof worker pools (storage and account).
135    proof_worker_handle: ProofWorkerHandle,
136
137    /// The size of proof targets chunk to spawn in one calculation.
138    /// If None, chunking is disabled and all targets are processed in a single proof.
139    chunk_size: usize,
140    /// If this number is exceeded and chunking is enabled, then this will override whether or not
141    /// there are any active workers and force chunking across workers. This is to prevent tasks
142    /// which are very long from hitting a single worker.
143    max_targets_for_chunking: usize,
144
145    /// Account trie updates.
146    account_updates: B256Map<LeafUpdate>,
147
148    /// Account updates that are buffered but were not yet applied to the trie.
149    new_account_updates: B256Map<LeafUpdate>,
150    /// Storage updates that are buffered but were not yet applied to the trie.
151    new_storage_updates: B256Map<B256Map<LeafUpdate>>,
152    /// Account updates that are blocked by storage root calculation or account reveal.
153    ///
154    /// Those are being moved into `account_updates` once storage roots
155    /// are revealed and/or calculated.
156    ///
157    /// Invariant: for each entry in `pending_account_updates` account must either be already
158    /// revealed in the trie or have an entry in `account_updates`.
159    ///
160    /// Values can be either of:
161    ///   - None: account had a storage update and is awaiting storage root calculation and/or
162    ///     account node reveal to complete.
163    ///   - Some(_): account was changed/destroyed and is awaiting storage root calculation/reveal
164    ///     to complete.
165    pending_account_updates: B256Map<Option<Option<Account>>>,
166    /// Cache of account proof targets that were already fetched/requested from the proof workers.
167    /// Account to the broadest requested parent context (an unknown parent sorts before every
168    /// known parent).
169    fetched_account_targets: B256Map<ProofV2TargetParent>,
170    /// Reusable buffer for RLP encoding of accounts.
171    account_rlp_buf: Vec<u8>,
172    /// Whether the last state update has been received.
173    finished_state_updates: bool,
174    /// Accumulated account leaf update cache hits.
175    account_cache_hits: u64,
176    /// Accumulated account leaf update cache misses.
177    account_cache_misses: u64,
178    /// Accumulated storage leaf update cache hits.
179    storage_cache_hits: u64,
180    /// Accumulated storage leaf update cache misses.
181    storage_cache_misses: u64,
182    /// Pending proof targets queued for dispatch to proof workers.
183    pending_targets: PendingTargets,
184    /// Proof jobs queued on the workers whose results have not been received yet.
185    in_flight_proof_jobs: usize,
186    /// Everything the task knows about a storage trie touched by this block: the trie itself, its
187    /// queued leaf updates, the proof nodes waiting to be revealed into it and its proof target
188    /// cache. See [`StorageTrieState`].
189    ///
190    /// Invariant: an address is either listed here or has its trie in the state trie's storage
191    /// trie map, never both. Everything that reads a storage trie goes through this map until
192    /// [`Self::return_storage_tries`] hands them all back.
193    storage: B256Map<StorageTrieState<S>>,
194    /// Storage job buffers kept alive after returning their tries, until the result is sent.
195    storage_to_drop: B256Map<StorageTrieState<S>>,
196    /// Number of storage tries currently owned by a job running off this thread.
197    storage_in_flight: usize,
198    /// Sender handed to storage jobs. Kept alive by the task so the receiver never disconnects.
199    storage_done_tx: CrossbeamSender<StorageJobMessage<S>>,
200    /// Receives storage tries coming back from jobs spawned by this task.
201    storage_done_rx: CrossbeamReceiver<StorageJobMessage<S>>,
202    /// Number of pending execution/prewarming updates received but not yet passed to
203    /// `update_leaves`.
204    pending_updates: usize,
205    /// Whether the first buffered leaf batch has been applied.
206    initial_updates_applied: bool,
207    /// Combined final hashed state.
208    ///
209    /// Sparse trie task observes and hashes all state updates, allowing it to cheaply construct a
210    /// final [`HashedPostState`] and share it with main engine thread without requiring any extra
211    /// hashing work.
212    final_hashed_state: HashedPostState,
213
214    /// Metrics for the sparse trie.
215    metrics: SparseTrieTaskMetrics,
216}
217
218impl<A, S> SparseTrieCacheTask<A, S>
219where
220    A: SparseTrie + Default,
221    S: SparseTrie + Default + Clone + 'static,
222{
223    /// Creates a new sparse trie, pre-populating with an existing [`SparseStateTrie`].
224    #[expect(clippy::too_many_arguments)]
225    pub(super) fn new_with_trie(
226        executor: &Runtime,
227        updates: CrossbeamReceiver<StateRootMessage>,
228        cancel_rx: CrossbeamReceiver<()>,
229        final_hashed_state_tx: std::sync::mpsc::Sender<Arc<HashedPostState>>,
230        proof_worker_handle: ProofWorkerHandle,
231        metrics: SparseTrieTaskMetrics,
232        trie: SparseStateTrie<A, S>,
233        parent_state_root: B256,
234        new_epoch: TrieNodeEpoch,
235        chunk_size: usize,
236    ) -> Self {
237        let (hashed_state_tx, hashed_state_rx) = crossbeam_channel::unbounded();
238        let (storage_done_tx, storage_done_rx) = crossbeam_channel::unbounded();
239
240        let parent_span = tracing::Span::current();
241        let hashing_metrics = metrics.clone();
242        executor.spawn_blocking_named("trie-hashing", move || {
243            let _span = trace_span!(parent: &parent_span, "run_hashing_task").entered();
244            Self::run_hashing_task(updates, hashed_state_tx, hashing_metrics)
245        });
246
247        Self {
248            updates: hashed_state_rx,
249            cancel_rx,
250            proof_worker_handle,
251            final_hashed_state_tx: Some(final_hashed_state_tx),
252            trie,
253            parent_state_root,
254            new_epoch,
255            chunk_size,
256            max_targets_for_chunking: DEFAULT_MAX_TARGETS_FOR_CHUNKING,
257            account_updates: Default::default(),
258            new_account_updates: Default::default(),
259            new_storage_updates: Default::default(),
260            pending_account_updates: Default::default(),
261            fetched_account_targets: Default::default(),
262            account_rlp_buf: Vec::with_capacity(TRIE_ACCOUNT_RLP_MAX_SIZE),
263            finished_state_updates: Default::default(),
264            account_cache_hits: 0,
265            account_cache_misses: 0,
266            storage_cache_hits: 0,
267            storage_cache_misses: 0,
268            pending_targets: Default::default(),
269            in_flight_proof_jobs: 0,
270            storage: Default::default(),
271            storage_to_drop: Default::default(),
272            storage_in_flight: 0,
273            storage_done_tx,
274            storage_done_rx,
275            pending_updates: Default::default(),
276            initial_updates_applied: false,
277            final_hashed_state: Default::default(),
278            metrics,
279        }
280    }
281
282    /// Runs the hashing task that drains updates from the channel and converts them to
283    /// `HashedPostState` in parallel, until it forwards the finish marker or the channel closes.
284    fn run_hashing_task(
285        updates: CrossbeamReceiver<StateRootMessage>,
286        hashed_state_tx: CrossbeamSender<SparseTrieTaskMessage>,
287        metrics: SparseTrieTaskMetrics,
288    ) {
289        let mut total_idle_time = std::time::Duration::ZERO;
290        let mut idle_start = Instant::now();
291
292        while let Ok(message) = updates.recv() {
293            total_idle_time += idle_start.elapsed();
294
295            let finished = matches!(message, StateRootMessage::FinishedStateUpdates);
296            let msg = match message {
297                StateRootMessage::PrefetchProofs(targets) => {
298                    SparseTrieTaskMessage::PrefetchProofs(targets)
299                }
300                StateRootMessage::StateUpdate(state) => {
301                    let _span = trace_span!(target: "engine::tree::payload_processor::sparse_trie", "hashing_state_update").entered();
302                    let hashed = reth_execution_types::state_update_to_hashed_post_state(&state);
303                    SparseTrieTaskMessage::HashedState(hashed)
304                }
305                StateRootMessage::FinishedStateUpdates => {
306                    SparseTrieTaskMessage::FinishedStateUpdates
307                }
308                StateRootMessage::HashedStateUpdate(state) => {
309                    SparseTrieTaskMessage::HashedState(state)
310                }
311            };
312            // Only hints can follow the finish marker, and the task ignores them. Returning here
313            // frees the shared hashing thread for the next block's task while a hint sender, such
314            // as a prewarm task queued behind earlier blocks, is still alive.
315            if hashed_state_tx.send(msg).is_err() || finished {
316                break;
317            }
318
319            idle_start = Instant::now();
320        }
321
322        metrics.hashing_task_idle_time_seconds.record(total_idle_time.as_secs_f64());
323    }
324
325    /// Returns the trie for reuse in the next payload built on top of this one.
326    ///
327    /// Should be called after the state root result has been sent.
328    pub(super) fn into_trie_for_reuse(self) -> (SparseStateTrie<A, S>, DeferredDrops) {
329        debug_assert!(
330            self.storage.is_empty(),
331            "storage tries must be back before the trie is preserved"
332        );
333        let Self { mut trie, .. } = self;
334        let deferred = trie.take_deferred_drops();
335        (trie, deferred)
336    }
337
338    /// Clears the trie, discarding all state.
339    ///
340    /// Use this when the payload was invalid or cancelled - we don't want to preserve
341    /// potentially invalid trie state, but we keep the allocations for reuse.
342    pub(super) fn into_cleared_trie(self) -> (SparseStateTrie<A, S>, DeferredDrops) {
343        let Self { mut trie, storage_done_rx, storage, .. } = self;
344        // Disconnect first so tries still out with a job are dropped by that job instead of here.
345        drop(storage_done_rx);
346        for (address, state) in storage {
347            if let StorageTrieState::Idle(work) = state {
348                trie.insert_storage_trie(address, work.trie);
349            }
350        }
351        trie.clear();
352        let deferred = trie.take_deferred_drops();
353        (trie, deferred)
354    }
355
356    /// Runs the sparse trie task to completion.
357    ///
358    /// This waits for new incoming [`SparseTrieTaskMessage`]s, applies updates
359    /// to the trie and schedules proof fetching when needed.
360    ///
361    /// After the finish marker, drains the remaining proof and storage work until all trie updates
362    /// are applied and all checked-out storage tries have returned. See the [task
363    /// lifecycle](Self#task-lifecycle).
364    #[instrument(
365        name = "SparseTrieCacheTask::run",
366        level = "debug",
367        target = "engine::tree::payload_processor::sparse_trie",
368        skip_all
369    )]
370    pub(super) fn run(&mut self) -> Result<StateRootComputeOutcome, StateRootTaskError> {
371        let now = Instant::now();
372
373        let mut total_idle_time = std::time::Duration::ZERO;
374        let mut idle_start = Instant::now();
375        let mut done = false;
376        let mut finalized_hashed_state = None;
377
378        // Streaming phase: updates are still arriving. Ends when the finish marker is
379        // processed. Only producers hold update senders, so the channel closing before the
380        // marker means they died without finishing the stream.
381        while !self.finished_state_updates {
382            let mut t = Instant::now();
383            crossbeam_channel::select_biased! {
384                recv(self.updates) -> message => {
385                    let wake = Instant::now();
386                    total_idle_time += wake.duration_since(idle_start);
387                    self.metrics
388                        .sparse_trie_channel_wait_duration_histogram
389                        .record(wake.duration_since(t));
390
391                    let update = message.map_err(|_| StateRootTaskError::Other(
392                        "updates channel disconnected before state root calculation".into(),
393                    ))?;
394                    if let Some(hashed_state) = self.on_message(update) {
395                        finalized_hashed_state = Some(hashed_state);
396                    }
397                    self.pending_updates += 1;
398                }
399                recv(self.proof_worker_handle.results()) -> message => {
400                    let wake = Instant::now();
401                    total_idle_time += wake.duration_since(idle_start);
402                    self.metrics
403                        .sparse_trie_channel_wait_duration_histogram
404                        .record(wake.duration_since(t));
405                    t = wake;
406
407                    let Ok(result) = message else {
408                        unreachable!("proof worker results never disconnect while the handle is alive")
409                    };
410                    self.on_proof_worker_result(result, &mut t)?;
411                },
412                recv(self.storage_done_rx) -> message => {
413                    let wake = Instant::now();
414                    total_idle_time += wake.duration_since(idle_start);
415                    self.metrics
416                        .sparse_trie_channel_wait_duration_histogram
417                        .record(wake.duration_since(t));
418
419                    let Ok(returned) = message else {
420                        unreachable!("we own the sender half")
421                    };
422                    self.on_storage_job_message(returned)?;
423                },
424                recv(self.cancel_rx) -> _ => return Err(StateRootTaskError::Canceled),
425            }
426
427            done = self.make_progress()?;
428            idle_start = Instant::now();
429        }
430
431        // Draining phase: the marker is the last message read from the updates channel, so
432        // after it only proof results, storage-job returns, and cancellation can occur. The
433        // update channel closing when producers drop their senders is not observed here, and late
434        // hints are ignored: with all updates known, prefetching has nothing left to help.
435        while !done {
436            let mut t = Instant::now();
437            crossbeam_channel::select_biased! {
438                recv(self.proof_worker_handle.results()) -> message => {
439                    let wake = Instant::now();
440                    total_idle_time += wake.duration_since(idle_start);
441                    self.metrics
442                        .sparse_trie_channel_wait_duration_histogram
443                        .record(wake.duration_since(t));
444                    t = wake;
445
446                    let Ok(result) = message else {
447                        unreachable!("proof worker results never disconnect while the handle is alive")
448                    };
449                    self.on_proof_worker_result(result, &mut t)?;
450                },
451                recv(self.storage_done_rx) -> message => {
452                    let wake = Instant::now();
453                    total_idle_time += wake.duration_since(idle_start);
454                    self.metrics
455                        .sparse_trie_channel_wait_duration_histogram
456                        .record(wake.duration_since(t));
457
458                    let Ok(returned) = message else {
459                        unreachable!("we own the sender half")
460                    };
461                    self.on_storage_job_message(returned)?;
462                },
463                recv(self.cancel_rx) -> _ => return Err(StateRootTaskError::Canceled),
464            }
465
466            done = self.make_progress()?;
467            idle_start = Instant::now();
468        }
469
470        debug_assert_eq!(
471            self.storage_in_flight, 0,
472            "completion must wait for every checked out storage trie"
473        );
474        self.metrics.sparse_trie_idle_time_seconds.record(total_idle_time.as_secs_f64());
475
476        debug!(target: "engine::root", "All proofs processed, ending calculation");
477
478        let start = Instant::now();
479        self.return_storage_tries();
480        let (state_root, trie_updates) = match self.trie.root_with_updates(self.new_epoch) {
481            Ok(result) => result,
482            Err(err)
483                if matches!(
484                    err.kind(),
485                    SparseStateTrieErrorKind::Sparse(SparseTrieErrorKind::Blind)
486                ) =>
487            {
488                // A still-blind account trie means this block never changed state, so preserve
489                // the cached parent root instead of fetching and revealing
490                // the unchanged root node.
491                (self.parent_state_root, TrieUpdates::default())
492            }
493            Err(err) => return Err(StateRootTaskError::Other(err.into())),
494        };
495
496        let end = Instant::now();
497        self.metrics.sparse_trie_final_update_duration_histogram.record(end.duration_since(start));
498        self.metrics.sparse_trie_total_duration_histogram.record(end.duration_since(now));
499
500        self.metrics.sparse_trie_account_cache_hits.record(self.account_cache_hits as f64);
501        self.metrics.sparse_trie_account_cache_misses.record(self.account_cache_misses as f64);
502        self.metrics.sparse_trie_storage_cache_hits.record(self.storage_cache_hits as f64);
503        self.metrics.sparse_trie_storage_cache_misses.record(self.storage_cache_misses as f64);
504        self.account_cache_hits = 0;
505        self.account_cache_misses = 0;
506        self.storage_cache_hits = 0;
507        self.storage_cache_misses = 0;
508
509        Ok(StateRootComputeOutcome {
510            state_root,
511            trie_updates: Arc::new(trie_updates),
512            hashed_state: finalized_hashed_state
513                .expect("finished state updates publish the hashed post state"),
514        })
515    }
516
517    /// Handles a received proof worker result: coalesces everything already queued, reveals the
518    /// proof in the trie, and records timing metrics.
519    fn on_proof_worker_result(
520        &mut self,
521        result: ProofWorkerResult,
522        t: &mut Instant,
523    ) -> Result<(), StateRootTaskError> {
524        let mut proofs = self.complete_proof_job(result)?;
525        while let Ok(next) = self.proof_worker_handle.results().try_recv() {
526            proofs.extend(self.complete_proof_job(next)?);
527        }
528
529        let phase_end = Instant::now();
530        self.metrics
531            .sparse_trie_proof_coalesce_duration_histogram
532            .record(phase_end.duration_since(*t));
533        *t = phase_end;
534
535        self.on_proofs(proofs)?;
536        self.metrics.sparse_trie_reveal_multiproof_duration_histogram.record(t.elapsed());
537        Ok(())
538    }
539
540    /// Applies buffered updates to the trie and dispatches proof targets.
541    ///
542    /// Messages queued after the finish marker are best-effort hints and are not actionable.
543    /// Returns `true` once the finish marker was received and all pending trie work is done.
544    fn make_progress(&mut self) -> Result<bool, StateRootTaskError> {
545        // Absorb a whole burst of returns before the scans below, so one batch of storage jobs
546        // costs one promotion pass rather than one per trie.
547        self.drain_returned_storage_tries()?;
548
549        let updates_queued = !self.finished_state_updates && !self.updates.is_empty();
550
551        if !updates_queued && self.proof_worker_handle.results().is_empty() {
552            // If we don't have any pending messages, we can spend some time on computing
553            // storage roots and promoting account updates.
554            self.dispatch_pending_targets()?;
555            let t = Instant::now();
556            self.process_new_updates()?;
557            self.promote_pending_account_updates()?;
558            self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
559
560            if self.finished_state_updates && !self.has_pending_sparse_trie_updates() {
561                return Ok(true);
562            }
563
564            self.dispatch_pending_targets()?;
565            self.ensure_not_stalled(updates_queued)?;
566
567            // If there's still no pending updates spend some time pre-computing the account
568            // trie upper hashes
569            if self.proof_worker_handle.results().is_empty() {
570                self.trie.calculate_subtries(self.new_epoch);
571            }
572        } else if !updates_queued {
573            // If we don't have any pending updates, apply them to the trie,
574            let t = Instant::now();
575            self.process_new_updates()?;
576            self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
577            self.dispatch_pending_targets()?;
578        } else if !self.initial_updates_applied && self.pending_updates >= INITIAL_UPDATE_BATCH_SIZE
579        {
580            // Start proof fetching before a continuously arriving state stream drains. Later
581            // batches retain the usual coalescing policy to avoid repeatedly sorting small maps.
582            let t = Instant::now();
583            self.process_new_updates()?;
584            self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
585            self.dispatch_pending_targets()?;
586        } else if self.pending_targets.len() > self.chunk_size {
587            // Make sure to dispatch targets if we've accumulated a lot of them.
588            self.dispatch_pending_targets()?;
589        }
590        Ok(false)
591    }
592
593    /// Processes a [`SparseTrieTaskMessage`] from the hashing task.
594    fn on_message(&mut self, message: SparseTrieTaskMessage) -> Option<Arc<HashedPostState>> {
595        match message {
596            SparseTrieTaskMessage::PrefetchProofs(targets) => {
597                self.on_prewarm_targets(targets);
598                None
599            }
600            SparseTrieTaskMessage::HashedState(hashed_state) => {
601                self.on_hashed_state_update(hashed_state);
602                None
603            }
604            SparseTrieTaskMessage::FinishedStateUpdates => {
605                let hashed_state = Arc::new(core::mem::take(&mut self.final_hashed_state));
606                let _ = self.final_hashed_state_tx.take().unwrap().send(Arc::clone(&hashed_state));
607                self.finished_state_updates = true;
608                Some(hashed_state)
609            }
610        }
611    }
612
613    #[instrument(
614        level = "trace",
615        target = "engine::tree::payload_processor::sparse_trie",
616        skip_all
617    )]
618    fn on_prewarm_targets(&mut self, targets: MultiProofTargetsV2) {
619        for target in targets.account_targets {
620            // Only touch accounts that are not yet present in the updates set.
621            self.new_account_updates.entry(target.key()).or_insert(LeafUpdate::Touched);
622        }
623
624        for (address, slots) in targets.storage_targets {
625            if !slots.is_empty() {
626                // Look up outer map once per address instead of once per slot.
627                let new_updates = self.new_storage_updates.entry(address).or_default();
628                for slot in slots {
629                    // Only touch storages that are not yet present in the updates set.
630                    new_updates.entry(slot.key()).or_insert(LeafUpdate::Touched);
631                }
632            }
633
634            // Touch corresponding account leaf to make sure its revealed in accounts trie for
635            // storage root update.
636            self.new_account_updates.entry(address).or_insert(LeafUpdate::Touched);
637        }
638    }
639
640    /// Processes a hashed state update and encodes all state changes as trie updates.
641    #[instrument(
642        level = "trace",
643        target = "engine::tree::payload_processor::sparse_trie",
644        skip_all
645    )]
646    #[allow(clippy::clone_on_copy)]
647    fn on_hashed_state_update(&mut self, hashed_state_update: HashedPostState) {
648        for (&address, storage) in &hashed_state_update.storages {
649            if !storage.storage.is_empty() {
650                // Look up the outer map once per address instead of once per slot.
651                let new_updates = self.new_storage_updates.entry(address).or_default();
652
653                for (&slot, &value) in &storage.storage {
654                    let encoded = if value.is_zero() {
655                        Vec::new()
656                    } else {
657                        alloy_rlp::encode_fixed_size(&value).to_vec()
658                    };
659                    new_updates.insert(slot, LeafUpdate::Changed(encoded));
660                }
661            }
662
663            // Make sure account is tracked in `account_updates` so that it is revealed in accounts
664            // trie for storage root update.
665            self.new_account_updates.entry(address).or_insert(LeafUpdate::Touched);
666
667            // Make sure account is tracked in `pending_account_updates` so that once storage root
668            // is computed, it will be updated in the accounts trie.
669            self.pending_account_updates.entry(address).or_insert(None);
670        }
671
672        for (&address, account) in &hashed_state_update.accounts {
673            // Track account as touched.
674            //
675            // This might overwrite an existing update, which is fine, because storage root from it
676            // is already tracked in the trie and can be easily fetched again.
677            self.new_account_updates.insert(address, LeafUpdate::Touched);
678
679            // Track account in `pending_account_updates` so that once storage root is computed,
680            // it will be updated in the accounts trie.
681            self.pending_account_updates.insert(address, Some(account.clone()));
682        }
683
684        self.final_hashed_state.extend(hashed_state_update);
685    }
686
687    fn on_proofs(&mut self, proofs: DecodedMultiProofV2) -> Result<(), StateRootTaskError> {
688        self.reveal_proof_result(proofs).map_err(|e| StateRootTaskError::Other(e.into()))
689    }
690
691    /// Reveals a proof batch.
692    ///
693    /// The account trie is revealed here because it can never be checked out. Storage proof nodes
694    /// are queued on their address' entry and revealed by the job that runs for it next.
695    fn reveal_proof_result(&mut self, proofs: DecodedMultiProofV2) -> SparseStateTrieResult<()> {
696        let DecodedMultiProofV2 { account_proofs, storage_proofs } = proofs;
697
698        self.queue_storage_proofs(storage_proofs);
699        // Get the workers going before spending this thread on the account trie.
700        self.run_ready_storage_work()?;
701
702        self.trie.reveal_account_proof_nodes(account_proofs)
703    }
704
705    /// Queues storage proof nodes on the entry for the address they belong to.
706    fn queue_storage_proofs(&mut self, storage_proofs: B256Map<Vec<ProofTrieNodeV2>>) {
707        let mut revealed_nodes = 0;
708        for (address, mut nodes) in storage_proofs {
709            if nodes.is_empty() {
710                continue;
711            }
712            revealed_nodes += nodes.len();
713
714            match self.storage_trie_state_mut(address) {
715                StorageTrieState::Idle(work) => work.queue_proofs(&mut nodes),
716                StorageTrieState::InFlight(in_flight) => in_flight.proofs.append(&mut nodes),
717            }
718        }
719        self.trie.record_revealed_storage_nodes(revealed_nodes);
720    }
721
722    /// Returns the storage trie state for `address`, checking its storage trie out of the state
723    /// trie the first time the address is touched by this block.
724    fn storage_trie_state_mut(&mut self, address: B256) -> &mut StorageTrieState<S> {
725        let Self { storage, trie, .. } = self;
726        storage.entry(address).or_insert_with(|| {
727            StorageTrieState::Idle(Box::new(StorageTrieWork::new(
728                trie.take_or_create_storage_trie(&address),
729            )))
730        })
731    }
732
733    /// Accounts for a received proof job result and returns its proofs.
734    fn complete_proof_job(
735        &mut self,
736        result: ProofWorkerResult,
737    ) -> Result<DecodedMultiProofV2, StateRootTaskError> {
738        let proofs = result?;
739        debug_assert!(
740            self.in_flight_proof_jobs > 0,
741            "received proof result without an in-flight proof job"
742        );
743        // Saturates so that a broken invariant in release builds still lets the stall check fire.
744        self.in_flight_proof_jobs = self.in_flight_proof_jobs.saturating_sub(1);
745        Ok(proofs)
746    }
747
748    /// Applies buffered input and runs ready storage work, including work buffered while a job
749    /// was in flight even if no new input messages were received.
750    fn process_new_updates(&mut self) -> SparseTrieResult<()> {
751        self.apply_new_updates()?;
752        self.run_ready_storage_work()
753    }
754
755    /// Applies new account updates and queues new storage updates on their address' entry.
756    fn apply_new_updates(&mut self) -> SparseTrieResult<()> {
757        if self.pending_updates == 0 {
758            return Ok(());
759        }
760
761        let _span = debug_span!("process_new_updates").entered();
762        self.pending_updates = 0;
763        self.initial_updates_applied = true;
764
765        // Queue the new storage updates on their entries; the jobs apply them to the tries.
766        let Self { storage, trie, new_storage_updates, .. } = self;
767        for (address, new) in new_storage_updates.drain() {
768            if new.is_empty() {
769                continue;
770            }
771
772            let state = storage.entry(address).or_insert_with(|| {
773                StorageTrieState::Idle(Box::new(StorageTrieWork::new(
774                    trie.take_or_create_storage_trie(&address),
775                )))
776            });
777            match state {
778                StorageTrieState::Idle(work) => work.queue_updates(new),
779                StorageTrieState::InFlight(in_flight) => {
780                    merge_leaf_updates(&mut in_flight.updates, new)
781                }
782            }
783        }
784
785        // Apply the new account updates to the accounts trie, which is never checked out.
786        self.process_account_leaf_updates(true)?;
787
788        for (address, new) in self.new_account_updates.drain() {
789            match self.account_updates.entry(address) {
790                Entry::Occupied(mut entry) => {
791                    if new.is_changed() {
792                        entry.insert(new);
793                    }
794                }
795                Entry::Vacant(entry) => {
796                    entry.insert(new);
797                }
798            }
799        }
800
801        Ok(())
802    }
803
804    /// Runs a pass over every idle storage trie that has something to do: proof nodes to reveal,
805    /// leaf updates that were not tried against the current state of the trie yet.
806    ///
807    /// Each pass owns its address' payload for its whole duration, so the passes are independent
808    /// of each other and of this thread. Most of them are handed to the rayon pool; the ones that
809    /// would cost more in handoff than in work stay here, see [`StorageTrieWork::is_target_only`]
810    /// and [`INLINE_STORAGE_WORK_UNITS`].
811    #[instrument(
812        level = "trace",
813        target = "engine::tree::payload_processor::sparse_trie",
814        skip_all
815    )]
816    fn run_ready_storage_work(&mut self) -> SparseTrieResult<()> {
817        let mut ready = Vec::new();
818        let mut job_units = 0;
819        for (address, state) in &self.storage {
820            let StorageTrieState::Idle(work) = state else { continue };
821            if !work.has_work() {
822                continue;
823            }
824            if !work.is_target_only() {
825                job_units += work.proofs.len() + work.pending.len();
826            }
827            ready.push(*address);
828        }
829        if ready.is_empty() {
830            return Ok(())
831        }
832
833        let inline_round = job_units <= INLINE_STORAGE_WORK_UNITS;
834        let new_epoch = self.new_epoch;
835        let retain_updates = self.trie.retains_updates();
836        let started = Instant::now();
837        let mut jobs = Vec::new();
838        for address in ready {
839            let state = self.storage.get_mut(&address).expect("entry was just seen");
840            let StorageTrieState::Idle(work) = state else {
841                unreachable!("an idle trie is only checked out from here")
842            };
843
844            if inline_round || work.is_target_only() {
845                let output = work.run(new_epoch, retain_updates);
846                self.apply_storage_output(address, output)?;
847                continue;
848            }
849
850            let StorageTrieState::Idle(work) = core::mem::replace(
851                state,
852                StorageTrieState::InFlight(Box::new(InFlightStorage::new(started))),
853            ) else {
854                unreachable!("just matched")
855            };
856            self.storage_in_flight += 1;
857            jobs.push(StorageTrieJob { address, work });
858        }
859
860        self.spawn_storage_jobs(jobs);
861
862        Ok(())
863    }
864
865    /// Records what a storage pass produced: the proof targets it discovered and its cache
866    /// counters. Returns the error the pass stopped at, if any.
867    fn apply_storage_output(
868        &mut self,
869        address: B256,
870        output: StorageWorkOutput,
871    ) -> SparseTrieResult<()> {
872        let StorageWorkOutput { targets, cache_hits, cache_misses, result } = output;
873
874        self.storage_cache_hits += cache_hits;
875        self.storage_cache_misses += cache_misses;
876        if !targets.is_empty() {
877            self.pending_targets.extend_storage_targets(&address, targets);
878        }
879
880        result
881    }
882
883    /// Invokes `update_leaves` for the accounts trie and collects any new targets.
884    ///
885    /// Returns whether any updates were drained (applied to the trie).
886    #[instrument(
887        level = "trace",
888        target = "engine::tree::payload_processor::sparse_trie",
889        skip_all
890    )]
891    fn process_account_leaf_updates(&mut self, new: bool) -> SparseTrieResult<bool> {
892        let account_updates =
893            if new { &mut self.new_account_updates } else { &mut self.account_updates };
894
895        let updates_len_before = account_updates.len();
896
897        self.trie.trie_mut().update_leaves(account_updates, |target, parent| {
898            match self.fetched_account_targets.entry(target) {
899                Entry::Occupied(mut entry) => {
900                    if parent < *entry.get() {
901                        entry.insert(parent);
902                        self.pending_targets
903                            .push_account_target(ProofV2Target::new(target).with_parent(parent));
904                    }
905                }
906                Entry::Vacant(entry) => {
907                    entry.insert(parent);
908                    self.pending_targets
909                        .push_account_target(ProofV2Target::new(target).with_parent(parent));
910                }
911            }
912        })?;
913
914        let updates_len_after = account_updates.len();
915        self.account_cache_hits += (updates_len_before - updates_len_after) as u64;
916        self.account_cache_misses += updates_len_after as u64;
917
918        Ok(updates_len_after < updates_len_before)
919    }
920
921    /// Hands checked out storage tries to the rayon pool in chunks.
922    ///
923    /// Chunking amortizes the spawn cost over a batch, while each trie is still sent back on its
924    /// own so promotion does not wait for the rest of the chunk.
925    fn spawn_storage_jobs(&self, mut jobs: Vec<StorageTrieJob<S>>) {
926        if jobs.is_empty() {
927            return;
928        }
929
930        let parent_span = debug_span!("spawn_storage_jobs", n = jobs.len());
931        let chunk_len = storage_job_chunk_len(jobs.len());
932        let new_epoch = self.new_epoch;
933        let retain_updates = self.trie.retains_updates();
934        while !jobs.is_empty() {
935            let chunk = jobs.split_off(jobs.len().saturating_sub(chunk_len));
936            let storage_done_tx = self.storage_done_tx.clone();
937            let parent_span = parent_span.clone();
938            rayon::spawn(move || {
939                let _enter = debug_span!(
940                    target: "engine::tree::payload_processor::sparse_trie",
941                    parent: &parent_span,
942                    "storage_jobs",
943                    n = chunk.len(),
944                )
945                .entered();
946                for job in chunk {
947                    let address = job.address;
948                    let message = match panic::catch_unwind(AssertUnwindSafe(|| {
949                        job.run(new_epoch, retain_updates)
950                    })) {
951                        Ok(done) => StorageJobMessage::Done(done),
952                        Err(payload) => StorageJobMessage::Panicked { address, payload },
953                    };
954                    if storage_done_tx.send(message).is_err() {
955                        // Nobody is waiting for the result anymore, drop the rest here.
956                        return;
957                    }
958                }
959            });
960        }
961    }
962
963    /// Handles a message from a storage job: takes a finished payload back, or resumes a panic
964    /// that happened on the job thread here, where it fails this task like an inline panic would.
965    fn on_storage_job_message(&mut self, message: StorageJobMessage<S>) -> SparseTrieResult<()> {
966        match message {
967            StorageJobMessage::Done(done) => self.on_storage_trie_returned(done),
968            StorageJobMessage::Panicked { address, payload } => {
969                if self.storage.remove(&address).is_some() {
970                    self.storage_in_flight -= 1;
971                }
972                panic::resume_unwind(payload)
973            }
974        }
975    }
976
977    /// Puts a payload back into its entry once its job finished, folding in everything that
978    /// arrived for the address while it was gone.
979    fn on_storage_trie_returned(&mut self, done: StorageTrieJobDone<S>) -> SparseTrieResult<()> {
980        let StorageTrieJobDone { address, work, output } = done;
981
982        let Entry::Occupied(mut entry) = self.storage.entry(address) else {
983            unreachable!("a returned payload was checked out of its entry")
984        };
985        let StorageTrieState::InFlight(in_flight) = entry.insert(StorageTrieState::Idle(work))
986        else {
987            unreachable!("a checked out address stays in flight until its job returns")
988        };
989        let started = in_flight.started;
990        let StorageTrieState::Idle(work) = entry.into_mut() else { unreachable!("just inserted") };
991        work.take_buffered(*in_flight);
992        self.storage_in_flight -= 1;
993        self.metrics.sparse_trie_storage_job_duration_histogram.record(started.elapsed());
994
995        self.apply_storage_output(address, output)
996    }
997
998    /// Takes back every payload whose job has already finished, without blocking.
999    fn drain_returned_storage_tries(&mut self) -> SparseTrieResult<()> {
1000        while let Ok(message) = self.storage_done_rx.try_recv() {
1001            self.on_storage_job_message(message)?;
1002        }
1003
1004        Ok(())
1005    }
1006
1007    /// Puts every storage trie back into the sparse state trie.
1008    ///
1009    /// The final root and everything after it - trie updates, preservation, pruning - reads the
1010    /// storage tries through [`SparseStateTrie`], so no trie may remain in the task's storage map.
1011    fn return_storage_tries(&mut self) {
1012        let mut storage = core::mem::take(&mut self.storage);
1013        for (address, state) in &mut storage {
1014            let StorageTrieState::Idle(work) = state else {
1015                unreachable!("completion must wait for every storage job")
1016            };
1017            self.trie.insert_storage_trie(*address, core::mem::take(&mut work.trie));
1018        }
1019        // Keep the maps and proof buffers until the result has been published.
1020        self.storage_to_drop = storage;
1021    }
1022
1023    /// Promotes the pending account updates whose storage root is already available into proper
1024    /// leaf updates for the accounts trie.
1025    ///
1026    /// Accounts whose trie is still checked out stay pending and are promoted by a later call,
1027    /// once the payload has come back.
1028    #[instrument(
1029        level = "trace",
1030        target = "engine::tree::payload_processor::sparse_trie",
1031        skip_all
1032    )]
1033    fn promote_pending_account_updates(&mut self) -> SparseTrieResult<()> {
1034        self.process_account_leaf_updates(false)?;
1035
1036        if self.pending_account_updates.is_empty() {
1037            return Ok(());
1038        }
1039
1040        let new_epoch = self.new_epoch;
1041        loop {
1042            let span = trace_span!("promote_updates", promoted = tracing::field::Empty).entered();
1043            // Now handle pending account updates that can be upgraded to a proper update.
1044            let account_rlp_buf = &mut self.account_rlp_buf;
1045            let mut num_promoted = 0;
1046            self.pending_account_updates.retain(|addr, account| {
1047                let updated_storage = match self.storage.get_mut(addr) {
1048                    // The payload is out with a job, so the root is not readable yet.
1049                    Some(StorageTrieState::InFlight(_)) => return true,
1050                    Some(StorageTrieState::Idle(work)) if work.updated => Some(work),
1051                    _ => None,
1052                };
1053
1054                if let Some(work) = updated_storage {
1055                    if !work.pending.is_empty() {
1056                        // If account has pending storage updates, it is still pending.
1057                        return true;
1058                    } else if let Some(account) = account.take() {
1059                        let storage_root = work.trie.root(new_epoch).expect("updates are drained, storage trie should be revealed by now");
1060                        let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
1061                        self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
1062                        num_promoted += 1;
1063                        return false;
1064                    }
1065                }
1066
1067                // Get the current account state either from the trie or from latest account update.
1068                let trie_account = match self.account_updates.get(addr) {
1069                    Some(LeafUpdate::Changed(encoded)) => {
1070                        Some(encoded).filter(|encoded| !encoded.is_empty())
1071                    }
1072                    // Needs to be revealed first
1073                    Some(LeafUpdate::Touched) => return true,
1074                    None => self.trie.get_account_value(addr),
1075                };
1076
1077                let trie_account = trie_account.map(|value| TrieAccount::decode(&mut &value[..]).expect("invalid account RLP"));
1078
1079                let (account, storage_root) = if let Some(account) = account.take() {
1080                    // If account is Some(_) here it means it didn't have any storage updates
1081                    // and we can fetch the storage root directly from the account trie.
1082                    //
1083                    // If it did have storage updates, we would've had processed it above when iterating over storage tries.
1084                    let storage_root = trie_account.map(|account| account.storage_root).unwrap_or(EMPTY_ROOT_HASH);
1085
1086                    (account, storage_root)
1087                } else {
1088                    // An address with no entry was not touched by this block, so its trie, if any,
1089                    // is still in the state trie with the root it was preserved with.
1090                    let storage_root = match self.storage.get_mut(addr) {
1091                        Some(StorageTrieState::Idle(work)) => work.trie.root(new_epoch),
1092                        Some(StorageTrieState::InFlight(_)) => unreachable!("returned above"),
1093                        None => self.trie.storage_root(addr, new_epoch),
1094                    };
1095                    (trie_account.map(Into::into), storage_root.expect("account had storage updates that were applied to its trie, storage root must be revealed by now"))
1096                };
1097
1098                let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
1099                self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
1100                num_promoted += 1;
1101
1102                false
1103            });
1104            span.record("promoted", num_promoted);
1105            drop(span);
1106
1107            // Only exit when no new updates are processed.
1108            //
1109            // We need to keep iterating if any updates are being drained because that might
1110            // indicate that more pending account updates can be promoted.
1111            if num_promoted == 0 || !self.process_account_leaf_updates(false)? {
1112                break
1113            }
1114        }
1115
1116        Ok(())
1117    }
1118
1119    fn dispatch_pending_targets(&mut self) -> Result<(), StateRootTaskError> {
1120        if self.pending_targets.is_empty() {
1121            return Ok(())
1122        }
1123
1124        let _span = trace_span!("dispatch_pending_targets").entered();
1125        let (targets, chunking_length) = self.pending_targets.take();
1126        let mut dispatch_error = None;
1127        dispatch_with_chunking(
1128            targets,
1129            chunking_length,
1130            self.chunk_size,
1131            self.max_targets_for_chunking,
1132            self.proof_worker_handle.has_multiple_idle_account_workers(),
1133            self.proof_worker_handle.has_multiple_idle_storage_workers(),
1134            MultiProofTargetsV2::chunks,
1135            |proof_targets| {
1136                if dispatch_error.is_some() {
1137                    return;
1138                }
1139
1140                // A chunk becomes several jobs, and an address whose slots span chunks gets one
1141                // storage job per chunk. Every job returns its own result.
1142                match self.proof_worker_handle.dispatch(proof_targets) {
1143                    Ok(jobs) => self.in_flight_proof_jobs += jobs,
1144                    Err(e) => {
1145                        error!("failed to dispatch proofs: {e}");
1146                        dispatch_error = Some(StateRootTaskError::Other(Box::new(e)));
1147                    }
1148                }
1149            },
1150        );
1151
1152        if let Some(error) = dispatch_error {
1153            return Err(error)
1154        }
1155
1156        Ok(())
1157    }
1158
1159    fn has_pending_sparse_trie_updates(&self) -> bool {
1160        !self.account_updates.is_empty() ||
1161            !self.pending_account_updates.is_empty() ||
1162            self.storage.values().any(|state| state.is_pending())
1163    }
1164
1165    /// Returns whether any idle storage trie has a pass to run, which is progress that has not been
1166    /// made yet rather than a stall.
1167    fn has_ready_storage_work(&self) -> bool {
1168        self.storage.values().any(|state| match state {
1169            StorageTrieState::Idle(work) => work.has_work(),
1170            StorageTrieState::InFlight(_) => false,
1171        })
1172    }
1173
1174    /// Errors when pending trie updates remain but nothing can deliver them: no update
1175    /// messages are queued, no proof targets are queued or in flight, no proof results
1176    /// are waiting, and no storage pass is running or waiting to run.
1177    ///
1178    /// `updates_queued` is passed in instead of reading `self.updates` directly, because in
1179    /// the draining phase the updates channel is not read anymore and may hold ignored late
1180    /// hints that must not mask a stall.
1181    fn ensure_not_stalled(&self, updates_queued: bool) -> Result<(), StateRootTaskError> {
1182        if self.finished_state_updates &&
1183            !updates_queued &&
1184            self.pending_updates == 0 &&
1185            self.pending_targets.is_empty() &&
1186            self.in_flight_proof_jobs == 0 &&
1187            self.proof_worker_handle.results().is_empty() &&
1188            self.storage_in_flight == 0 &&
1189            !self.has_ready_storage_work() &&
1190            self.has_pending_sparse_trie_updates()
1191        {
1192            const MAX_STALLED_PROOF_TARGETS_TO_LOG: usize = 5;
1193
1194            let mut account_targets = self
1195                .account_updates
1196                .keys()
1197                .map(|target| (*target, self.fetched_account_targets.get(target).copied()))
1198                .collect::<Vec<_>>();
1199            account_targets.sort_unstable();
1200            let account_targets_truncated =
1201                account_targets.len().saturating_sub(MAX_STALLED_PROOF_TARGETS_TO_LOG);
1202            account_targets.truncate(MAX_STALLED_PROOF_TARGETS_TO_LOG);
1203
1204            let mut storage_targets = self
1205                .storage
1206                .iter()
1207                .filter_map(|(address, state)| match state {
1208                    StorageTrieState::Idle(work) => Some((address, work)),
1209                    StorageTrieState::InFlight(_) => None,
1210                })
1211                .flat_map(|(address, work)| {
1212                    work.pending
1213                        .keys()
1214                        .map(move |target| (*address, *target, work.fetched.get(target).copied()))
1215                })
1216                .collect::<Vec<_>>();
1217            storage_targets.sort_unstable();
1218            let storage_targets_truncated =
1219                storage_targets.len().saturating_sub(MAX_STALLED_PROOF_TARGETS_TO_LOG);
1220            storage_targets.truncate(MAX_STALLED_PROOF_TARGETS_TO_LOG);
1221
1222            error!(
1223                ?account_targets,
1224                account_targets_truncated,
1225                ?storage_targets,
1226                storage_targets_truncated,
1227                "sparse trie task stalled: pending updates remain but no proof targets are queued or in flight"
1228            );
1229
1230            return Err(StateRootTaskError::Stalled)
1231        }
1232
1233        Ok(())
1234    }
1235}
1236
1237/// State of one address' storage trie in the sparse trie task.
1238enum StorageTrieState<S> {
1239    /// Nothing is running for this address, so its payload can be read and handed to a job.
1240    Idle(Box<StorageTrieWork<S>>),
1241    /// A job owns the payload. Everything arriving for the address meanwhile is buffered here
1242    /// and folded back in when the job returns.
1243    InFlight(Box<InFlightStorage>),
1244}
1245
1246impl<S: SparseTrie + Default> StorageTrieState<S> {
1247    /// Returns whether the address still owes the state root some work.
1248    fn is_pending(&self) -> bool {
1249        match self {
1250            Self::InFlight(_) => true,
1251            Self::Idle(work) => !work.pending.is_empty() || work.has_work(),
1252        }
1253    }
1254}
1255
1256/// Everything the sparse trie task knows about one storage trie, moved into a job as a whole and
1257/// handed back by it.
1258struct StorageTrieWork<S> {
1259    /// The storage trie, checked out of the [`SparseStateTrie`] for the whole run.
1260    trie: RevealableSparseTrie<S>,
1261    /// Leaf updates that are not applied to `trie` yet, either because they are new or because
1262    /// they are blocked on a blinded node.
1263    pending: B256Map<LeafUpdate>,
1264    /// Proof nodes to reveal into `trie` before the next leaf pass.
1265    proofs: Vec<ProofTrieNodeV2>,
1266    /// Slots whose proof was already requested, mapped to the broadest requested parent context
1267    /// (an unknown parent sorts before every known parent).
1268    fetched: B256Map<ProofV2TargetParent>,
1269    /// Whether a pass could act on something: proof nodes to reveal, or leaf updates that were
1270    /// not tried against the current state of the trie yet.
1271    ///
1272    /// Blocked updates alone are not enough. Retrying them without a reveal in between cannot
1273    /// apply anything and would only resort the whole set again, and only this trie's own proof
1274    /// nodes can unblock them.
1275    dirty: bool,
1276    /// Whether this address ever had storage leaf updates queued, which is what makes promotion
1277    /// of its account wait for a recomputed storage root.
1278    updated: bool,
1279}
1280
1281impl<S: SparseTrie + Default> StorageTrieWork<S> {
1282    fn new(trie: RevealableSparseTrie<S>) -> Self {
1283        Self {
1284            trie,
1285            pending: Default::default(),
1286            proofs: Vec::new(),
1287            fetched: Default::default(),
1288            dirty: false,
1289            updated: false,
1290        }
1291    }
1292
1293    /// Reveals the queued proof nodes, applies the leaf updates that are not blocked by a blinded
1294    /// node, and recomputes the root once nothing is left to apply.
1295    fn run(&mut self, new_epoch: TrieNodeEpoch, retain_updates: bool) -> StorageWorkOutput {
1296        let Self { trie, pending, proofs, fetched, dirty, .. } = self;
1297        *dirty = false;
1298        let mut output = StorageWorkOutput::default();
1299
1300        if !proofs.is_empty() {
1301            output.result = trie.reveal_v2_proof_nodes(proofs, retain_updates);
1302            proofs.clear();
1303            if output.result.is_err() {
1304                return output
1305            }
1306        }
1307
1308        if !pending.is_empty() {
1309            let updates_len_before = pending.len();
1310            let targets = &mut output.targets;
1311            output.result = trie.update_leaves(pending, |path, parent| match fetched.entry(path) {
1312                Entry::Occupied(mut entry) => {
1313                    if parent < *entry.get() {
1314                        entry.insert(parent);
1315                        targets.push(ProofV2Target::new(path).with_parent(parent));
1316                    }
1317                }
1318                Entry::Vacant(entry) => {
1319                    entry.insert(parent);
1320                    targets.push(ProofV2Target::new(path).with_parent(parent));
1321                }
1322            });
1323            output.cache_hits = (updates_len_before - pending.len()) as u64;
1324            output.cache_misses = pending.len() as u64;
1325            if output.result.is_err() {
1326                return output
1327            }
1328        }
1329
1330        if self.needs_root() {
1331            self.trie.root(new_epoch);
1332        }
1333
1334        output
1335    }
1336
1337    /// Queues leaf updates, letting a newly known value win over one that is still queued.
1338    fn queue_updates(&mut self, updates: B256Map<LeafUpdate>) {
1339        if updates.is_empty() {
1340            return
1341        }
1342
1343        self.updated = true;
1344        self.dirty = true;
1345        merge_leaf_updates(&mut self.pending, updates);
1346    }
1347
1348    /// Queues proof nodes, draining them out of the batch they arrived in.
1349    fn queue_proofs(&mut self, nodes: &mut Vec<ProofTrieNodeV2>) {
1350        if nodes.is_empty() {
1351            return
1352        }
1353
1354        self.dirty = true;
1355        self.proofs.append(nodes);
1356    }
1357
1358    /// Folds what arrived while a job owned this payload back into it.
1359    fn take_buffered(&mut self, buffered: InFlightStorage) {
1360        let InFlightStorage { mut proofs, updates, .. } = buffered;
1361        self.queue_proofs(&mut proofs);
1362        self.queue_updates(updates);
1363    }
1364
1365    /// Returns whether a pass would do anything.
1366    fn has_work(&self) -> bool {
1367        debug_assert!(self.dirty || !self.needs_root(), "a drained pass must hash its trie");
1368        self.dirty
1369    }
1370
1371    /// Returns whether the trie was modified since its root was last computed.
1372    ///
1373    /// Only a trie this block changed is hashed here: for the others the root the account leaf
1374    /// already carries is still correct.
1375    fn needs_root(&self) -> bool {
1376        self.updated &&
1377            self.pending.is_empty() &&
1378            self.trie.is_revealed() &&
1379            !self.trie.is_root_cached()
1380    }
1381
1382    /// Returns whether a pass would only turn pending updates into proof targets.
1383    ///
1384    /// A blind trie has nothing to reveal into and nothing to apply, so running the pass on the
1385    /// task itself keeps the proof request from waiting for a job round trip.
1386    const fn is_target_only(&self) -> bool {
1387        self.proofs.is_empty() && self.trie.is_blind()
1388    }
1389}
1390
1391/// Buffers for one address while a job owns its payload.
1392struct InFlightStorage {
1393    /// When the payload was handed off, for the round trip histogram.
1394    started: Instant,
1395    /// Proof nodes that arrived for this address while the payload was gone.
1396    proofs: Vec<ProofTrieNodeV2>,
1397    /// Leaf updates that were queued for this address while the payload was gone.
1398    updates: B256Map<LeafUpdate>,
1399}
1400
1401impl InFlightStorage {
1402    fn new(started: Instant) -> Self {
1403        Self { started, proofs: Vec::new(), updates: Default::default() }
1404    }
1405}
1406
1407/// A storage payload checked out of its entry and handed to a job.
1408struct StorageTrieJob<S> {
1409    /// Hashed address the payload belongs to.
1410    address: B256,
1411    /// The checked out payload.
1412    work: Box<StorageTrieWork<S>>,
1413}
1414
1415impl<S: SparseTrie + Default> StorageTrieJob<S> {
1416    fn run(mut self, new_epoch: TrieNodeEpoch, retain_updates: bool) -> StorageTrieJobDone<S> {
1417        let output = self.work.run(new_epoch, retain_updates);
1418        StorageTrieJobDone { address: self.address, work: self.work, output }
1419    }
1420}
1421
1422/// What a spawned storage job sends back to the sparse trie task.
1423enum StorageJobMessage<S> {
1424    /// The job finished and hands its payload back.
1425    Done(StorageTrieJobDone<S>),
1426    /// The job panicked and its trie is lost. The global rayon pool has no panic handler, so a
1427    /// panic escaping a spawned job would abort the process; it is caught and resumed on the task
1428    /// thread instead, where it fails the state root calculation like an inline panic.
1429    Panicked {
1430        /// Hashed address of the payload the job owned.
1431        address: B256,
1432        /// The panic payload, resumed on the task thread.
1433        payload: Box<dyn Any + Send>,
1434    },
1435}
1436
1437/// A storage payload coming back to the sparse trie task after its job finished.
1438struct StorageTrieJobDone<S> {
1439    /// Hashed address the payload belongs to.
1440    address: B256,
1441    /// The payload itself, to be put back into its entry.
1442    work: Box<StorageTrieWork<S>>,
1443    /// What the pass produced.
1444    output: StorageWorkOutput,
1445}
1446
1447/// What one storage pass produced for the sparse trie task.
1448struct StorageWorkOutput {
1449    /// Proof targets discovered while applying the leaf updates.
1450    targets: Vec<ProofV2Target>,
1451    /// Leaf updates applied without needing a new proof.
1452    cache_hits: u64,
1453    /// Leaf updates that stayed blocked on a blinded node.
1454    cache_misses: u64,
1455    /// The error the pass stopped at, if any.
1456    result: SparseTrieResult<()>,
1457}
1458
1459impl Default for StorageWorkOutput {
1460    fn default() -> Self {
1461        Self { targets: Vec::new(), cache_hits: 0, cache_misses: 0, result: Ok(()) }
1462    }
1463}
1464
1465/// Merges leaf updates into a queue, letting a newly known value win over a queued one.
1466fn merge_leaf_updates(queued: &mut B256Map<LeafUpdate>, updates: B256Map<LeafUpdate>) {
1467    if queued.is_empty() {
1468        // Take the whole map at once, no per-slot loop.
1469        *queued = updates;
1470        return
1471    }
1472
1473    for (slot, update) in updates {
1474        match queued.entry(slot) {
1475            Entry::Occupied(mut entry) => {
1476                if update.is_changed() {
1477                    entry.insert(update);
1478                }
1479            }
1480            Entry::Vacant(entry) => {
1481                entry.insert(update);
1482            }
1483        }
1484    }
1485}
1486
1487/// Number of storage tries handed to a single spawned job.
1488///
1489/// Chunking keeps a block with thousands of tiny tries from paying a rayon spawn per trie, while
1490/// staying small enough that one slow trie only delays the few behind it in its own chunk. Tries
1491/// are still handed back one at a time, so a chunk does not delay promotion of the ones it
1492/// already finished.
1493fn storage_job_chunk_len(tries: usize) -> usize {
1494    /// Upper bound on how many tries a single slow one can hold up.
1495    const MAX_CHUNK_LEN: usize = 32;
1496
1497    tries.div_ceil(rayon::current_num_threads().max(1) * 4).clamp(1, MAX_CHUNK_LEN)
1498}
1499
1500/// Metrics recorded by sparse trie and hashing tasks.
1501#[derive(Metrics, Clone)]
1502#[metrics(scope = "tree.root")]
1503pub(super) struct SparseTrieTaskMetrics {
1504    /// Histogram of durations spent revealing multiproof results into the sparse trie.
1505    pub(super) sparse_trie_reveal_multiproof_duration_histogram: Histogram,
1506    /// Histogram of durations spent coalescing multiple proof results from the channel.
1507    pub(super) sparse_trie_proof_coalesce_duration_histogram: Histogram,
1508    /// Histogram of durations the event loop spent blocked waiting on channels.
1509    pub(super) sparse_trie_channel_wait_duration_histogram: Histogram,
1510    /// Histogram of durations spent processing trie updates and promoting pending accounts.
1511    pub(super) sparse_trie_process_updates_duration_histogram: Histogram,
1512    /// Histogram of durations storage tries spend checked out for a job on another thread.
1513    pub(super) sparse_trie_storage_job_duration_histogram: Histogram,
1514    /// Histogram of sparse trie final update durations.
1515    pub(super) sparse_trie_final_update_duration_histogram: Histogram,
1516    /// Histogram of sparse trie total durations.
1517    pub(super) sparse_trie_total_duration_histogram: Histogram,
1518    /// Time spent preparing the sparse trie for reuse after state root computation.
1519    pub(super) into_trie_for_reuse_duration_histogram: Histogram,
1520    /// Time spent pruning the sparse trie by node epoch.
1521    pub(super) sparse_trie_prune_duration_histogram: Histogram,
1522    /// Time spent waiting for preserved sparse trie cache to become available.
1523    pub(super) sparse_trie_cache_wait_duration_histogram: Histogram,
1524    /// Histogram for sparse trie task idle time in seconds (waiting for updates or proof
1525    /// results). Excludes the final wait after the channel is closed.
1526    pub(super) sparse_trie_idle_time_seconds: Histogram,
1527    /// Histogram for hashing task idle time in seconds (waiting for messages from execution).
1528    /// Excludes the final wait after the channel is closed.
1529    pub(super) hashing_task_idle_time_seconds: Histogram,
1530
1531    /// Number of account leaf updates applied without needing a new proof (cache hits).
1532    pub(super) sparse_trie_account_cache_hits: Histogram,
1533    /// Number of account leaf updates that required a new proof (cache misses).
1534    pub(super) sparse_trie_account_cache_misses: Histogram,
1535    /// Number of storage leaf updates applied without needing a new proof (cache hits).
1536    pub(super) sparse_trie_storage_cache_hits: Histogram,
1537    /// Number of storage leaf updates that required a new proof (cache misses).
1538    pub(super) sparse_trie_storage_cache_misses: Histogram,
1539
1540    /// Number of storage tries retained in the preserved sparse trie cache.
1541    pub(super) sparse_trie_retained_storage_tries: Gauge,
1542}
1543
1544/// The default max targets, for limiting the number of account and storage proof targets to be
1545/// fetched by a single worker. If exceeded, chunking is forced regardless of worker availability.
1546const DEFAULT_MAX_TARGETS_FOR_CHUNKING: usize = 300;
1547
1548/// Start proof fetching while the first state-update batch is still arriving.
1549const INITIAL_UPDATE_BATCH_SIZE: usize = 64;
1550
1551/// A round of storage passes that would do at most this much work - proof nodes to reveal plus
1552/// leaf updates to apply - runs on the sparse trie task itself, because handing the tries to
1553/// another thread and waiting for them to come back costs more than the work.
1554const INLINE_STORAGE_WORK_UNITS: usize = 16;
1555
1556/// Dispatches work items as a single unit or in chunks based on target size and worker
1557/// availability.
1558#[expect(clippy::too_many_arguments)]
1559fn dispatch_with_chunking<T, I>(
1560    items: T,
1561    chunking_len: usize,
1562    chunk_size: usize,
1563    max_targets_for_chunking: usize,
1564    has_multiple_idle_account_workers: bool,
1565    has_multiple_idle_storage_workers: bool,
1566    chunker: impl FnOnce(T, usize) -> I,
1567    mut dispatch: impl FnMut(T),
1568) where
1569    I: IntoIterator<Item = T>,
1570{
1571    let has_full_chunks = chunking_len >= chunk_size.saturating_mul(2);
1572    let should_chunk = chunking_len > max_targets_for_chunking ||
1573        (has_full_chunks &&
1574            (has_multiple_idle_account_workers || has_multiple_idle_storage_workers));
1575
1576    if should_chunk && chunking_len > chunk_size {
1577        for chunk in chunker(items, chunk_size) {
1578            dispatch(chunk);
1579        }
1580        return;
1581    }
1582
1583    dispatch(items);
1584}
1585
1586/// RLP-encodes the account as a [`TrieAccount`] leaf value, or returns empty for deletions.
1587///
1588/// `Some(Account::default())` with an empty storage root is encoded as a deletion. This is valid
1589/// for post-Merge state because EIP-7523 (<https://eips.ethereum.org/EIPS/eip-7523>) prohibits
1590/// empty accounts. Do not use this encoding rule when replaying historical pre-Merge state, where
1591/// an empty account and a missing account can have different trie representations.
1592fn encode_account_leaf_value(
1593    account: Option<Account>,
1594    storage_root: B256,
1595    account_rlp_buf: &mut Vec<u8>,
1596) -> Vec<u8> {
1597    if account.as_ref().is_none_or(|account| account.is_empty()) && storage_root == EMPTY_ROOT_HASH
1598    {
1599        return Vec::new();
1600    }
1601
1602    account_rlp_buf.clear();
1603    account.unwrap_or_default().into_trie_account(storage_root).encode(account_rlp_buf);
1604    account_rlp_buf.clone()
1605}
1606
1607/// Pending proof targets queued for dispatch to proof workers, along with their count.
1608#[derive(Default)]
1609struct PendingTargets {
1610    /// The proof targets.
1611    targets: MultiProofTargetsV2,
1612    /// Number of account + storage proof targets currently queued.
1613    len: usize,
1614}
1615
1616impl PendingTargets {
1617    /// Returns the number of pending targets.
1618    const fn len(&self) -> usize {
1619        self.len
1620    }
1621
1622    /// Returns `true` if there are no pending targets.
1623    const fn is_empty(&self) -> bool {
1624        self.len == 0
1625    }
1626
1627    /// Takes the pending targets, replacing with empty defaults.
1628    fn take(&mut self) -> (MultiProofTargetsV2, usize) {
1629        (std::mem::take(&mut self.targets), std::mem::take(&mut self.len))
1630    }
1631
1632    /// Adds a target to the account targets.
1633    fn push_account_target(&mut self, target: ProofV2Target) {
1634        self.targets.account_targets.push(target);
1635        self.len += 1;
1636    }
1637
1638    /// Extends storage targets for the given address.
1639    fn extend_storage_targets(&mut self, address: &B256, targets: Vec<ProofV2Target>) {
1640        self.len += targets.len();
1641        self.targets.storage_targets.entry(*address).or_default().extend(targets);
1642    }
1643}
1644
1645/// Message type for the sparse trie task.
1646enum SparseTrieTaskMessage {
1647    /// A hashed state update ready to be processed.
1648    HashedState(HashedPostState),
1649    /// Prefetch proof targets (passed through directly).
1650    PrefetchProofs(MultiProofTargetsV2),
1651    /// Signals that all state updates have been received.
1652    FinishedStateUpdates,
1653}
1654
1655#[cfg(test)]
1656mod tests {
1657    use super::*;
1658    use alloy_primitives::{B256, U256};
1659    use reth_db_common::init::init_genesis;
1660    use reth_provider::test_utils::create_test_provider_factory;
1661    use reth_storage_overlay::{OverlayManager, OverlayStateProviderFactory};
1662    use reth_trie_common::{
1663        BranchNodeV2, ExtensionNode, LeafNode, Nibbles, RlpNode, TrieMask, TrieNodeV2,
1664    };
1665    use reth_trie_parallel::proof_task::{ProofTaskCtx, ProofWorkerCounts};
1666    use reth_trie_sparse::ArenaParallelSparseTrie;
1667
1668    fn drain_sparse_trie_tasks(runtime: &Runtime) {
1669        for task_name in ["trie-hashing", "storage-workers", "account-workers"] {
1670            runtime.spawn_blocking_named(task_name, || {}).get();
1671        }
1672    }
1673
1674    fn test_task(
1675        runtime: &Runtime,
1676        trie: SparseStateTrie,
1677    ) -> (SparseTrieCacheTask, CrossbeamSender<StateRootMessage>, CrossbeamSender<()>) {
1678        let provider_factory = create_test_provider_factory();
1679        let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
1680        let state_provider_factory = OverlayStateProviderFactory::new(
1681            provider_factory,
1682            OverlayManager::<reth_chain_state::EthPrimitives>::default()
1683                .overlay_builder(anchor_hash),
1684        );
1685        let proof_worker_handle = ProofWorkerHandle::new(
1686            runtime,
1687            ProofTaskCtx::new(state_provider_factory),
1688            ProofWorkerCounts::full(runtime),
1689            Default::default(),
1690        );
1691        let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1692        let (cancel_guard, cancel_rx) = crossbeam_channel::bounded(0);
1693        let task = SparseTrieCacheTask::new_with_trie(
1694            runtime,
1695            updates_rx,
1696            cancel_rx,
1697            std::sync::mpsc::channel().0,
1698            proof_worker_handle,
1699            SparseTrieTaskMetrics::default(),
1700            trie,
1701            EMPTY_ROOT_HASH,
1702            TrieNodeEpoch::new(1),
1703            1,
1704        );
1705        (task, updates_tx, cancel_guard)
1706    }
1707
1708    fn receive_storage_job(
1709        task: &SparseTrieCacheTask,
1710    ) -> StorageJobMessage<ArenaParallelSparseTrie> {
1711        task.storage_done_rx
1712            .recv_timeout(std::time::Duration::from_secs(5))
1713            .expect("storage job did not return")
1714    }
1715
1716    /// Checks a payload out of its entry the way a spawned job does, without racing one.
1717    fn check_out_storage(
1718        task: &mut SparseTrieCacheTask,
1719        address: B256,
1720    ) -> Box<StorageTrieWork<ArenaParallelSparseTrie>> {
1721        let state = task.storage.get_mut(&address).expect("address must have an entry");
1722        let StorageTrieState::Idle(work) = core::mem::replace(
1723            state,
1724            StorageTrieState::InFlight(Box::new(InFlightStorage::new(Instant::now()))),
1725        ) else {
1726            panic!("trie must be idle")
1727        };
1728        task.storage_in_flight += 1;
1729        work
1730    }
1731
1732    /// Hands a payload checked out by [`check_out_storage`] back, as a finished job would.
1733    fn return_storage(
1734        task: &mut SparseTrieCacheTask,
1735        address: B256,
1736        work: Box<StorageTrieWork<ArenaParallelSparseTrie>>,
1737    ) {
1738        task.on_storage_trie_returned(StorageTrieJobDone {
1739            address,
1740            work,
1741            output: StorageWorkOutput::default(),
1742        })
1743        .unwrap();
1744    }
1745
1746    fn storage_slot_value(
1747        task: &SparseTrieCacheTask,
1748        address: &B256,
1749        slot: &B256,
1750    ) -> Option<Vec<u8>> {
1751        let StorageTrieState::Idle(work) = task.storage.get(address)? else { return None };
1752        work.trie.as_revealed_ref()?.get_leaf_value(&Nibbles::unpack(slot)).cloned()
1753    }
1754
1755    fn storage_root_of(task: &mut SparseTrieCacheTask, address: B256) -> B256 {
1756        let StorageTrieState::Idle(work) = task.storage.get_mut(&address).expect("entry") else {
1757            panic!("payload is out with a job")
1758        };
1759        work.trie.root(TrieNodeEpoch::new(1)).expect("storage trie must be revealed")
1760    }
1761
1762    #[test]
1763    fn test_encode_account_leaf_value_deletion_and_empty_root_is_empty() {
1764        let mut account_rlp_buf = vec![0xAB];
1765        let encoded = encode_account_leaf_value(None, EMPTY_ROOT_HASH, &mut account_rlp_buf);
1766
1767        assert!(encoded.is_empty());
1768        // Early return should not touch the caller's buffer.
1769        assert_eq!(account_rlp_buf, vec![0xAB]);
1770    }
1771
1772    #[test]
1773    fn test_encode_account_leaf_value_empty_account_and_empty_root_is_empty() {
1774        let mut account_rlp_buf = vec![0xAB];
1775        let encoded = encode_account_leaf_value(
1776            Some(Account::default()),
1777            EMPTY_ROOT_HASH,
1778            &mut account_rlp_buf,
1779        );
1780
1781        assert!(encoded.is_empty());
1782        // Early return should not touch the caller's buffer.
1783        assert_eq!(account_rlp_buf, vec![0xAB]);
1784    }
1785
1786    #[test]
1787    #[allow(clippy::needless_update)]
1788    fn test_encode_account_leaf_value_non_empty_account_is_rlp() {
1789        let storage_root = B256::repeat_byte(0x99);
1790        let account = Some(Account {
1791            nonce: 7,
1792            balance: U256::from(42),
1793            bytecode_hash: Some(B256::repeat_byte(0xAA)),
1794            ..Default::default()
1795        });
1796        let mut account_rlp_buf = vec![0x00, 0x01];
1797
1798        let encoded = encode_account_leaf_value(account, storage_root, &mut account_rlp_buf);
1799        let decoded = TrieAccount::decode(&mut &encoded[..]).expect("valid account RLP");
1800
1801        assert_eq!(decoded.nonce, 7);
1802        assert_eq!(decoded.balance, U256::from(42));
1803        assert_eq!(decoded.storage_root, storage_root);
1804        assert_eq!(account_rlp_buf, encoded);
1805    }
1806
1807    #[test]
1808    fn hashing_task_returns_after_finish_marker_while_hint_sender_is_alive() {
1809        let runtime = Runtime::test();
1810        let (task, updates_tx, _cancel_guard) = test_task(&runtime, SparseStateTrie::default());
1811        // A hint sender outliving execution, like a prewarm task queued behind earlier blocks.
1812        let _hint_tx = updates_tx.clone();
1813        updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
1814        drop(updates_tx);
1815
1816        // The next block's hashing task runs on the same thread, so it must not wait for the hint
1817        // sender to drop.
1818        let (next_task_tx, next_task_rx) = std::sync::mpsc::channel();
1819        runtime.spawn_blocking_named("trie-hashing", move || next_task_tx.send(()).unwrap());
1820        next_task_rx
1821            .recv_timeout(std::time::Duration::from_secs(5))
1822            .expect("hashing thread is still held by the finished task");
1823        assert!(matches!(task.updates.try_recv(), Ok(SparseTrieTaskMessage::FinishedStateUpdates)));
1824    }
1825
1826    #[test]
1827    fn checked_out_storage_trie_holds_back_its_updates_until_it_returns() {
1828        let runtime = Runtime::test();
1829        let trie = SparseStateTrie::default()
1830            .with_accounts_trie(RevealableSparseTrie::<ArenaParallelSparseTrie>::revealed_empty())
1831            .with_default_storage_trie(RevealableSparseTrie::blind_from(
1832                ArenaParallelSparseTrie::default(),
1833            ))
1834            .with_updates(true);
1835        let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, trie);
1836
1837        let address = B256::repeat_byte(0x11);
1838        let revealed_slot = B256::repeat_byte(0x22);
1839        let late_slot = B256::repeat_byte(0x33);
1840        let revealed_value = alloy_rlp::encode_fixed_size(&U256::from(7)).to_vec();
1841        let late_value = alloy_rlp::encode_fixed_size(&U256::from(9)).to_vec();
1842        let later_value = alloy_rlp::encode_fixed_size(&U256::from(11)).to_vec();
1843
1844        let mut state = HashedPostState::default();
1845        state.accounts.insert(address, Some(Account { nonce: 1, ..Default::default() }));
1846        state.storages.entry(address).or_default().storage.insert(revealed_slot, U256::from(7));
1847        task.on_hashed_state_update(state);
1848        task.pending_updates = 1;
1849        task.apply_new_updates().unwrap();
1850
1851        // Check the payload out by hand so the test does not race a spawned job.
1852        let work = check_out_storage(&mut task, address);
1853        task.finished_state_updates = true;
1854
1855        assert!(task.has_pending_sparse_trie_updates());
1856        assert!(
1857            task.ensure_not_stalled(false).is_ok(),
1858            "a checked out payload can still deliver progress"
1859        );
1860
1861        // A proof for a checked out address must be buffered, not revealed into a fresh blind
1862        // trie that the returning job would then overwrite.
1863        let leaf = ProofTrieNodeV2 {
1864            path: Nibbles::default(),
1865            node: TrieNodeV2::Leaf(LeafNode::new(
1866                Nibbles::unpack(revealed_slot),
1867                revealed_value.clone(),
1868            )),
1869            masks: None,
1870        };
1871        task.on_proofs(DecodedMultiProofV2 {
1872            storage_proofs: B256Map::from_iter([(address, vec![leaf])]),
1873            ..Default::default()
1874        })
1875        .unwrap();
1876        assert!(!task.trie.storage_tries_mut().contains_key(&address));
1877        let StorageTrieState::InFlight(in_flight) = &task.storage[&address] else {
1878            panic!("payload is out with a job")
1879        };
1880        assert_eq!(in_flight.proofs.len(), 1);
1881
1882        // The same holds for leaf updates arriving while the payload is gone.
1883        let mut state = HashedPostState::default();
1884        state.storages.entry(address).or_default().storage.insert(late_slot, U256::from(9));
1885        task.on_hashed_state_update(state);
1886        task.pending_updates = 1;
1887        task.apply_new_updates().unwrap();
1888        let StorageTrieState::InFlight(in_flight) = &task.storage[&address] else {
1889            panic!("payload is out with a job")
1890        };
1891        assert!(in_flight.updates.contains_key(&late_slot));
1892
1893        return_storage(&mut task, address, work);
1894        assert_eq!(task.storage_in_flight, 0);
1895
1896        // Returned work must run even without new input messages.
1897        assert_eq!(task.pending_updates, 0);
1898        task.process_new_updates().unwrap();
1899        assert_eq!(storage_slot_value(&task, &address, &revealed_slot), Some(revealed_value));
1900        assert_eq!(storage_slot_value(&task, &address, &late_slot), Some(late_value));
1901
1902        let StorageTrieState::Idle(work) = &task.storage[&address] else {
1903            panic!("payload is back")
1904        };
1905        assert!(work.pending.is_empty());
1906        assert!(work.trie.is_root_cached(), "a drained payload hashes its trie");
1907        let root_before = storage_root_of(&mut task, address);
1908
1909        // An update arriving during a job invalidates the root the previous pass computed.
1910        let work = check_out_storage(&mut task, address);
1911        let mut state = HashedPostState::default();
1912        state.storages.entry(address).or_default().storage.insert(late_slot, U256::from(11));
1913        task.on_hashed_state_update(state);
1914        task.pending_updates = 1;
1915        task.apply_new_updates().unwrap();
1916        return_storage(&mut task, address, work);
1917
1918        let StorageTrieState::Idle(work) = &task.storage[&address] else {
1919            panic!("payload is back")
1920        };
1921        assert!(work.has_work(), "a buffered update makes the payload ready again");
1922
1923        task.process_new_updates().unwrap();
1924        assert_eq!(storage_slot_value(&task, &address, &late_slot), Some(later_value));
1925        assert_ne!(storage_root_of(&mut task, address), root_before);
1926
1927        drop(updates_tx);
1928        drop(task);
1929        drain_sparse_trie_tasks(&runtime);
1930    }
1931
1932    #[test]
1933    #[allow(clippy::clone_on_copy)]
1934    fn in_flight_storage_updates_keep_latest_values_and_deletions() {
1935        let runtime = Runtime::test();
1936        let trie = SparseStateTrie::default()
1937            .with_accounts_trie(RevealableSparseTrie::revealed_empty())
1938            .with_updates(true);
1939        let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, trie);
1940        let address = B256::repeat_byte(0x11);
1941        let removed_slot = B256::repeat_byte(0x20);
1942        let changed_slot = B256::repeat_byte(0x30);
1943        let new_slot = B256::repeat_byte(0x31);
1944        // A nested branch ensures there are persisted branch updates to compare.
1945        let mut sibling_slot = changed_slot;
1946        sibling_slot[1] = 0x40;
1947        let account = Account { nonce: 1, ..Default::default() };
1948        let parent_leaf = ProofTrieNodeV2 {
1949            path: Nibbles::default(),
1950            node: TrieNodeV2::Leaf(LeafNode::new(
1951                Nibbles::unpack(removed_slot),
1952                alloy_rlp::encode(U256::from(7)),
1953            )),
1954            masks: None,
1955        };
1956
1957        let mut state = HashedPostState::default();
1958        state.accounts.insert(address, Some(account.clone()));
1959        state.storages.entry(address).or_default().storage.extend([
1960            (removed_slot, U256::from(8)),
1961            (changed_slot, U256::from(9)),
1962            (new_slot, U256::ONE),
1963            (sibling_slot, U256::from(2)),
1964        ]);
1965        task.on_hashed_state_update(state);
1966        task.pending_updates = 1;
1967        task.apply_new_updates().unwrap();
1968
1969        let mut work = check_out_storage(&mut task, address);
1970        let output = work.run(task.new_epoch, true);
1971        assert_eq!(work.pending.len(), 4, "the blind pass keeps its original updates");
1972        assert_eq!(output.targets.len(), 4);
1973
1974        // New values and a later prewarm hint arrive while the old values are still pending.
1975        let mut state = HashedPostState::default();
1976        state
1977            .storages
1978            .entry(address)
1979            .or_default()
1980            .storage
1981            .extend([(removed_slot, U256::ZERO), (changed_slot, U256::from(11))]);
1982        task.on_hashed_state_update(state);
1983        task.pending_updates = 1;
1984        task.apply_new_updates().unwrap();
1985        task.on_prewarm_targets(MultiProofTargetsV2 {
1986            storage_targets: B256Map::from_iter([(
1987                address,
1988                vec![ProofV2Target::new(removed_slot), ProofV2Target::new(changed_slot)],
1989            )]),
1990            ..Default::default()
1991        });
1992        task.pending_updates = 1;
1993        task.apply_new_updates().unwrap();
1994
1995        task.on_storage_trie_returned(StorageTrieJobDone { address, work, output }).unwrap();
1996        // Deliver the requested parent proof directly so the interleaving is deterministic.
1997        let (targets, _) = task.pending_targets.take();
1998        assert_eq!(targets.storage_targets[&address].len(), 4);
1999        task.on_proofs(DecodedMultiProofV2 {
2000            storage_proofs: B256Map::from_iter([(address, vec![parent_leaf.clone()])]),
2001            ..Default::default()
2002        })
2003        .unwrap();
2004        updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
2005        let outcome = task.run().unwrap();
2006
2007        let expected_root = reth_trie_common::root::storage_root_unsorted([
2008            (changed_slot, U256::from(11)),
2009            (new_slot, U256::ONE),
2010            (sibling_slot, U256::from(2)),
2011        ]);
2012        assert_eq!(
2013            outcome.state_root,
2014            reth_trie_common::root::state_root_unsorted([(
2015                address,
2016                account.into_trie_account(expected_root),
2017            )])
2018        );
2019
2020        let mut serial = RevealableSparseTrie::<ArenaParallelSparseTrie>::blind();
2021        serial.reveal_v2_proof_nodes(&mut [parent_leaf], true).unwrap();
2022        let mut final_updates = B256Map::from_iter([
2023            (removed_slot, LeafUpdate::Changed(Vec::new())),
2024            (changed_slot, LeafUpdate::Changed(alloy_rlp::encode(U256::from(11)))),
2025            (new_slot, LeafUpdate::Changed(alloy_rlp::encode(U256::ONE))),
2026            (sibling_slot, LeafUpdate::Changed(alloy_rlp::encode(U256::from(2)))),
2027        ]);
2028        serial
2029            .update_leaves(&mut final_updates, |_, _| panic!("parent is fully revealed"))
2030            .unwrap();
2031        let (serial_root, serial_updates) = serial.root_with_updates(task.new_epoch).unwrap();
2032        assert_eq!(serial_root, expected_root);
2033        assert!(!serial_updates.updated_nodes.is_empty());
2034        let updates = &outcome.trie_updates.storage_tries[&address];
2035        assert_eq!(updates.storage_nodes, serial_updates.updated_nodes);
2036        assert_eq!(updates.removed_nodes, serial_updates.removed_nodes);
2037
2038        let (mut reused, _) = task.into_trie_for_reuse();
2039        assert_eq!(reused.storage_root(&address, TrieNodeEpoch::new(2)), Some(expected_root));
2040        assert!(reused.take_trie_updates().unwrap().is_empty());
2041        drop(updates_tx);
2042        drain_sparse_trie_tasks(&runtime);
2043    }
2044
2045    #[test]
2046    fn unrelated_storage_proofs_do_not_retry_blocked_leaves() {
2047        let runtime = Runtime::test();
2048        let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, SparseStateTrie::default());
2049        let address = B256::repeat_byte(0x11);
2050        let slot = B256::repeat_byte(0x22);
2051        let StorageTrieState::Idle(work) = task.storage_trie_state_mut(address) else {
2052            unreachable!()
2053        };
2054        work.queue_updates(B256Map::from_iter([(slot, LeafUpdate::Changed(vec![1]))]));
2055        task.run_ready_storage_work().unwrap();
2056        assert_eq!(task.storage_cache_misses, 1);
2057
2058        task.on_proofs(DecodedMultiProofV2 {
2059            storage_proofs: B256Map::from_iter([(
2060                B256::repeat_byte(0x33),
2061                vec![ProofTrieNodeV2::empty()],
2062            )]),
2063            ..Default::default()
2064        })
2065        .unwrap();
2066        assert_eq!(task.storage_cache_misses, 1, "unrelated reveals cannot unblock this trie");
2067        assert_eq!(storage_slot_value(&task, &address, &slot), None);
2068
2069        task.on_proofs(DecodedMultiProofV2 {
2070            storage_proofs: B256Map::from_iter([(address, vec![ProofTrieNodeV2::empty()])]),
2071            ..Default::default()
2072        })
2073        .unwrap();
2074        assert_eq!(storage_slot_value(&task, &address, &slot), Some(vec![1]));
2075        assert_eq!(task.storage_cache_hits, 1);
2076        assert!(!task.has_ready_storage_work());
2077
2078        drop(updates_tx);
2079        drop(task);
2080        drain_sparse_trie_tasks(&runtime);
2081    }
2082
2083    #[test]
2084    fn canceled_task_disconnects_checked_out_storage_jobs() {
2085        let runtime = Runtime::test();
2086        let trie = SparseStateTrie::default()
2087            .with_default_storage_trie(RevealableSparseTrie::revealed_empty());
2088        let (mut task, updates_tx, cancel_guard) = test_task(&runtime, trie);
2089        let idle_address = B256::repeat_byte(0x11);
2090        let address = B256::repeat_byte(0x22);
2091        task.storage_trie_state_mut(idle_address);
2092        task.storage_trie_state_mut(address);
2093        let work = check_out_storage(&mut task, address);
2094        let done_tx = task.storage_done_tx.clone();
2095        let epoch = task.new_epoch;
2096
2097        drop(cancel_guard);
2098        assert!(matches!(task.run(), Err(StateRootTaskError::Canceled)));
2099        let (trie, _) = task.into_cleared_trie();
2100        assert_eq!(trie.retained_storage_tries_count(), 1, "only the idle trie can be reused");
2101
2102        // Complete the held job after teardown, when its payload must be dropped by the sender.
2103        let done = StorageTrieJob { address, work }.run(epoch, false);
2104        assert!(done_tx.send(StorageJobMessage::Done(done)).is_err());
2105        drop(updates_tx);
2106        drain_sparse_trie_tasks(&runtime);
2107    }
2108
2109    #[test]
2110    fn storage_job_panics_are_resumed_on_the_task() {
2111        let runtime = Runtime::test();
2112        let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, SparseStateTrie::default());
2113        let address = B256::repeat_byte(0x11);
2114        let StorageTrieState::Idle(work) = task.storage_trie_state_mut(address) else {
2115            unreachable!()
2116        };
2117        work.queue_proofs(&mut vec![ProofTrieNodeV2 {
2118            path: Nibbles::default(),
2119            node: TrieNodeV2::Branch(BranchNodeV2 {
2120                state_mask: TrieMask::from(1),
2121                ..Default::default()
2122            }),
2123            masks: None,
2124        }]);
2125        let work = check_out_storage(&mut task, address);
2126        task.spawn_storage_jobs(vec![StorageTrieJob { address, work }]);
2127
2128        let message = receive_storage_job(&task);
2129        assert!(
2130            matches!(&message, StorageJobMessage::Panicked { address: returned, .. } if *returned == address)
2131        );
2132        let panic = panic::catch_unwind(AssertUnwindSafe(|| task.on_storage_job_message(message)));
2133        assert!(panic.is_err());
2134        assert_eq!(task.storage_in_flight, 0);
2135        assert!(!task.storage.contains_key(&address));
2136
2137        drop(updates_tx);
2138        drop(task);
2139        drain_sparse_trie_tasks(&runtime);
2140    }
2141
2142    #[test]
2143    fn unsupported_storage_root_errors_are_returned_to_the_task() {
2144        let runtime = Runtime::test();
2145        let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, SparseStateTrie::default());
2146        let address = B256::repeat_byte(0x11);
2147        let StorageTrieState::Idle(work) = task.storage_trie_state_mut(address) else {
2148            unreachable!()
2149        };
2150        work.queue_proofs(&mut vec![ProofTrieNodeV2 {
2151            path: Nibbles::default(),
2152            node: TrieNodeV2::Extension(ExtensionNode::new(
2153                Nibbles::from_nibbles([1]),
2154                RlpNode::word_rlp(&B256::ZERO),
2155            )),
2156            masks: None,
2157        }]);
2158        let work = check_out_storage(&mut task, address);
2159        task.spawn_storage_jobs(vec![StorageTrieJob { address, work }]);
2160
2161        let message = receive_storage_job(&task);
2162        assert!(matches!(&message, StorageJobMessage::Done(done) if done.address == address));
2163        let error = task.on_storage_job_message(message).unwrap_err();
2164        assert!(
2165            matches!(error.kind(), SparseTrieErrorKind::Reveal { path, .. } if path.is_empty())
2166        );
2167        assert_eq!(task.storage_in_flight, 0);
2168        assert!(matches!(task.storage.get(&address), Some(StorageTrieState::Idle(_))));
2169
2170        drop(updates_tx);
2171        drop(task);
2172        drain_sparse_trie_tasks(&runtime);
2173    }
2174
2175    #[test]
2176    fn large_storage_batches_run_off_thread() {
2177        let runtime = Runtime::test();
2178        let trie = SparseStateTrie::default()
2179            .with_accounts_trie(RevealableSparseTrie::<ArenaParallelSparseTrie>::revealed_empty())
2180            .with_default_storage_trie(RevealableSparseTrie::blind_from(
2181                ArenaParallelSparseTrie::default(),
2182            ))
2183            .with_updates(true);
2184        let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, trie);
2185
2186        // One leaf per address, enough of them to exceed the inline budget.
2187        let leaves = (0..=INLINE_STORAGE_WORK_UNITS as u8)
2188            .map(|index| {
2189                let address = B256::repeat_byte(0x10 + index);
2190                let slot = B256::repeat_byte(0x80 + index);
2191                let value = alloy_rlp::encode_fixed_size(&U256::from(index + 1)).to_vec();
2192                (address, slot, value)
2193            })
2194            .collect::<Vec<_>>();
2195        let storage_proofs = leaves
2196            .iter()
2197            .map(|(address, slot, value)| {
2198                let leaf = ProofTrieNodeV2 {
2199                    path: Nibbles::default(),
2200                    node: TrieNodeV2::Leaf(LeafNode::new(Nibbles::unpack(slot), value.clone())),
2201                    masks: None,
2202                };
2203                (*address, vec![leaf])
2204            })
2205            .collect();
2206
2207        task.on_proofs(DecodedMultiProofV2 { account_proofs: Vec::new(), storage_proofs }).unwrap();
2208
2209        assert_eq!(task.storage_in_flight, leaves.len());
2210        assert!(task.has_pending_sparse_trie_updates(), "completion must wait for the reveals");
2211
2212        while task.storage_in_flight > 0 {
2213            let message = receive_storage_job(&task);
2214            task.on_storage_job_message(message).unwrap();
2215        }
2216        for (address, slot, value) in &leaves {
2217            assert_eq!(storage_slot_value(&task, address, slot).as_ref(), Some(value));
2218        }
2219
2220        drop(updates_tx);
2221        drop(task);
2222        drain_sparse_trie_tasks(&runtime);
2223    }
2224
2225    #[test]
2226    #[allow(clippy::clone_on_copy)]
2227    fn run_waits_for_storage_tries_hashed_off_thread() {
2228        let runtime = Runtime::test();
2229        let default_trie = RevealableSparseTrie::<ArenaParallelSparseTrie>::revealed_empty();
2230        let trie = SparseStateTrie::default()
2231            .with_accounts_trie(default_trie.clone())
2232            .with_default_storage_trie(default_trie)
2233            .with_updates(true);
2234        let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, trie);
2235
2236        let accounts = (0..8u8)
2237            .map(|index| {
2238                let address = B256::repeat_byte(0x10 + index);
2239                let account = Account {
2240                    nonce: u64::from(index) + 1,
2241                    balance: U256::from(index),
2242                    ..Default::default()
2243                };
2244                let storage = (0..4u8)
2245                    .map(|slot| {
2246                        (B256::repeat_byte(0x40 + index * 4 + slot), U256::from(slot) + U256::ONE)
2247                    })
2248                    .collect::<Vec<_>>();
2249                (address, account, storage)
2250            })
2251            .collect::<Vec<_>>();
2252
2253        let mut state = HashedPostState::default();
2254        for (address, account, storage) in &accounts {
2255            state.accounts.insert(*address, Some(account.clone()));
2256            state.storages.entry(*address).or_default().storage.extend(storage.iter().copied());
2257        }
2258        updates_tx.send(StateRootMessage::HashedStateUpdate(state)).unwrap();
2259        updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
2260
2261        let outcome = task.run().expect("state root computation should succeed");
2262
2263        let expected = accounts.iter().map(|(address, account, storage)| {
2264            let storage_root =
2265                reth_trie_common::root::storage_root_unsorted(storage.iter().copied());
2266            (*address, account.clone().into_trie_account(storage_root))
2267        });
2268        assert_eq!(outcome.state_root, reth_trie_common::root::state_root_unsorted(expected));
2269        assert_eq!(task.storage_in_flight, 0);
2270        assert!(task.storage.is_empty(), "every storage trie is back in the state trie");
2271
2272        drop(updates_tx);
2273        drop(task);
2274        drain_sparse_trie_tasks(&runtime);
2275    }
2276
2277    #[test]
2278    fn first_leaf_batch_starts_proofs_before_input_queue_drains() {
2279        let runtime = reth_tasks::Runtime::test();
2280        let provider_factory = create_test_provider_factory();
2281        let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
2282        let state_provider_factory = OverlayStateProviderFactory::new(
2283            provider_factory,
2284            OverlayManager::<reth_chain_state::EthPrimitives>::default()
2285                .overlay_builder(anchor_hash),
2286        );
2287        let proof_worker_handle = ProofWorkerHandle::new(
2288            &runtime,
2289            ProofTaskCtx::new(state_provider_factory),
2290            ProofWorkerCounts::full(&runtime),
2291            Default::default(),
2292        );
2293
2294        let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
2295        let trie = SparseStateTrie::default()
2296            .with_accounts_trie(default_trie.clone())
2297            .with_default_storage_trie(default_trie)
2298            .with_updates(true);
2299
2300        let parent_state_root = B256::repeat_byte(0x55);
2301        let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
2302        let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
2303        let mut task = SparseTrieCacheTask::new_with_trie(
2304            &runtime,
2305            updates_rx,
2306            cancel_rx,
2307            std::sync::mpsc::channel().0,
2308            proof_worker_handle,
2309            SparseTrieTaskMetrics::default(),
2310            trie,
2311            parent_state_root,
2312            TrieNodeEpoch::UNMODIFIED,
2313            1,
2314        );
2315
2316        // Keep an input queued so progress cannot use its normal queue-empty flush.
2317        updates_tx.send(StateRootMessage::PrefetchProofs(Default::default())).unwrap();
2318        let deadline = std::time::Instant::now();
2319        while task.updates.is_empty() {
2320            assert!(deadline.elapsed() < std::time::Duration::from_secs(1));
2321            std::thread::yield_now();
2322        }
2323        for index in 0..INITIAL_UPDATE_BATCH_SIZE {
2324            let mut state = HashedPostState::default();
2325            state.accounts.insert(
2326                B256::repeat_byte(index as u8),
2327                Some(Account { nonce: 1, ..Default::default() }),
2328            );
2329            task.on_hashed_state_update(state);
2330            task.pending_updates += 1;
2331            assert!(!task.make_progress().unwrap());
2332            if index + 1 < INITIAL_UPDATE_BATCH_SIZE {
2333                assert_eq!(task.in_flight_proof_jobs, 0);
2334            }
2335        }
2336        assert!(task.in_flight_proof_jobs > 0, "proof work must start before the queue drains");
2337        assert_eq!(task.pending_updates, 0);
2338
2339        // A second batch remains buffered; the early flush must not become a permanent small
2340        // batch policy that repeatedly scans and sorts pending leaves.
2341        for index in INITIAL_UPDATE_BATCH_SIZE..INITIAL_UPDATE_BATCH_SIZE * 2 {
2342            let mut state = HashedPostState::default();
2343            state.accounts.insert(
2344                B256::repeat_byte(index as u8),
2345                Some(Account { nonce: 1, ..Default::default() }),
2346            );
2347            task.on_hashed_state_update(state);
2348            task.pending_updates += 1;
2349            assert!(!task.make_progress().unwrap());
2350        }
2351        assert_eq!(task.pending_updates, INITIAL_UPDATE_BATCH_SIZE);
2352        assert_eq!(task.new_account_updates.len(), INITIAL_UPDATE_BATCH_SIZE);
2353        drop(updates_tx);
2354        drop(task);
2355        drain_sparse_trie_tasks(&runtime);
2356    }
2357
2358    #[test]
2359    fn run_returns_parent_root_without_revealing_blind_trie_when_no_state_updates() {
2360        let runtime = reth_tasks::Runtime::test();
2361        let provider_factory = create_test_provider_factory();
2362        let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
2363        let state_provider_factory = OverlayStateProviderFactory::new(
2364            provider_factory,
2365            OverlayManager::<reth_chain_state::EthPrimitives>::default()
2366                .overlay_builder(anchor_hash),
2367        );
2368        let proof_worker_handle = ProofWorkerHandle::new(
2369            &runtime,
2370            ProofTaskCtx::new(state_provider_factory),
2371            ProofWorkerCounts::full(&runtime),
2372            Default::default(),
2373        );
2374
2375        let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
2376        let trie = SparseStateTrie::default()
2377            .with_accounts_trie(default_trie.clone())
2378            .with_default_storage_trie(default_trie)
2379            .with_updates(true);
2380
2381        let parent_state_root = B256::repeat_byte(0x55);
2382        let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
2383        let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
2384        let mut task = SparseTrieCacheTask::new_with_trie(
2385            &runtime,
2386            updates_rx,
2387            cancel_rx,
2388            std::sync::mpsc::channel().0,
2389            proof_worker_handle,
2390            SparseTrieTaskMetrics::default(),
2391            trie,
2392            parent_state_root,
2393            TrieNodeEpoch::UNMODIFIED,
2394            1,
2395        );
2396
2397        updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
2398        drop(updates_tx);
2399
2400        let outcome = task.run().expect("state root computation should succeed");
2401
2402        assert_eq!(outcome.state_root, parent_state_root);
2403        assert!(outcome.trie_updates.is_empty());
2404        assert!(task.trie.state_trie_ref().is_none(), "blind trie should not be revealed");
2405
2406        drop(task);
2407        drain_sparse_trie_tasks(&runtime);
2408    }
2409
2410    #[test]
2411    fn stall_check_waits_for_in_flight_proofs_then_reports_pending_updates() {
2412        let runtime = reth_tasks::Runtime::test();
2413        let provider_factory = create_test_provider_factory();
2414        let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
2415        let state_provider_factory = OverlayStateProviderFactory::new(
2416            provider_factory,
2417            OverlayManager::<reth_chain_state::EthPrimitives>::default()
2418                .overlay_builder(anchor_hash),
2419        );
2420        let proof_worker_handle = ProofWorkerHandle::new(
2421            &runtime,
2422            ProofTaskCtx::new(state_provider_factory),
2423            ProofWorkerCounts::full(&runtime),
2424            Default::default(),
2425        );
2426
2427        let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
2428        let trie = SparseStateTrie::default()
2429            .with_accounts_trie(default_trie.clone())
2430            .with_default_storage_trie(default_trie)
2431            .with_updates(true);
2432
2433        let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
2434        let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
2435        let mut task = SparseTrieCacheTask::new_with_trie(
2436            &runtime,
2437            updates_rx,
2438            cancel_rx,
2439            std::sync::mpsc::channel().0,
2440            proof_worker_handle,
2441            SparseTrieTaskMetrics::default(),
2442            trie,
2443            B256::repeat_byte(0x55),
2444            TrieNodeEpoch::UNMODIFIED,
2445            1,
2446        );
2447
2448        drop(updates_tx);
2449
2450        let account = B256::repeat_byte(0x11);
2451        let slot = B256::repeat_byte(0x22);
2452        let account_target = B256::repeat_byte(0x33);
2453        let storage_target = B256::repeat_byte(0x44);
2454
2455        task.finished_state_updates = true;
2456        task.account_updates.insert(account, LeafUpdate::Touched);
2457        task.pending_account_updates.insert(account, None);
2458        task.fetched_account_targets.insert(account_target, ProofV2TargetParent::NONE);
2459
2460        // A storage leaf that stayed blocked after its pass ran, so no pass is ready to run.
2461        let StorageTrieState::Idle(work) = task.storage_trie_state_mut(account) else {
2462            panic!("a fresh trie state is idle")
2463        };
2464        work.updated = true;
2465        work.pending.insert(slot, LeafUpdate::Touched);
2466        work.fetched.insert(storage_target, ProofV2TargetParent::new(11));
2467        task.in_flight_proof_jobs = 1;
2468
2469        assert!(task.ensure_not_stalled(false).is_ok());
2470
2471        let result = Ok(DecodedMultiProofV2::default());
2472        task.complete_proof_job(result).expect("proof result should be ok");
2473
2474        assert_eq!(task.in_flight_proof_jobs, 0);
2475        let error = task.ensure_not_stalled(false).expect_err("task should be stalled");
2476        assert!(matches!(error, StateRootTaskError::Stalled));
2477        let error = error.to_string();
2478
2479        assert!(error.contains("sparse trie task stalled"));
2480        assert!(!error.contains("account_targets"));
2481        assert!(!error.contains("storage_targets"));
2482        assert!(!error.contains(&format!("{account:?}")));
2483        assert!(!error.contains(&format!("{account_target:?}")));
2484        assert!(!error.contains(&format!("{storage_target:?}")));
2485        assert!(!error.contains("pending_account_leaves"));
2486        assert!(!error.contains("pending_storage_leaves"));
2487        assert!(!error.contains("pending_account_updates"));
2488        assert!(!error.contains(&format!("{slot:?}")));
2489
2490        drop(task);
2491        drain_sparse_trie_tasks(&runtime);
2492    }
2493
2494    #[test]
2495    fn run_completes_when_hints_follow_updates_finish() {
2496        let runtime = reth_tasks::Runtime::test();
2497        let provider_factory = create_test_provider_factory();
2498        let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
2499        let state_provider_factory = OverlayStateProviderFactory::new(
2500            provider_factory,
2501            OverlayManager::<reth_chain_state::EthPrimitives>::default()
2502                .overlay_builder(anchor_hash),
2503        );
2504        let proof_worker_handle = ProofWorkerHandle::new(
2505            &runtime,
2506            ProofTaskCtx::new(state_provider_factory),
2507            ProofWorkerCounts::full(&runtime),
2508            Default::default(),
2509        );
2510
2511        let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
2512        let trie = SparseStateTrie::default()
2513            .with_accounts_trie(default_trie.clone())
2514            .with_default_storage_trie(default_trie)
2515            .with_updates(true);
2516
2517        let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
2518        let (cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
2519        let mut task = SparseTrieCacheTask::new_with_trie(
2520            &runtime,
2521            updates_rx,
2522            cancel_rx,
2523            std::sync::mpsc::channel().0,
2524            proof_worker_handle,
2525            SparseTrieTaskMetrics::default(),
2526            trie,
2527            B256::repeat_byte(0x55),
2528            TrieNodeEpoch::UNMODIFIED,
2529            1,
2530        );
2531
2532        updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
2533        // The hashing task returns after the finish marker, so this send fails if it already
2534        // dropped the receiver.
2535        let _ = updates_tx.send(StateRootMessage::PrefetchProofs(Default::default()));
2536
2537        // The hashing task forwards the finish marker and never forwards the hint after it.
2538        let wait_start = std::time::Instant::now();
2539        while task.updates.is_empty() {
2540            assert!(
2541                wait_start.elapsed() < std::time::Duration::from_secs(1),
2542                "hashing task did not queue the finish marker"
2543            );
2544            std::thread::yield_now();
2545        }
2546
2547        let (result_tx, result_rx) = std::sync::mpsc::channel();
2548        let handle = std::thread::spawn(move || {
2549            let _ = result_tx.send(task.run());
2550        });
2551
2552        let result = result_rx.recv_timeout(std::time::Duration::from_secs(1));
2553        drop(cancel_guard);
2554        handle.join().unwrap();
2555
2556        assert!(result.expect("state root task stalled on a late hint").is_ok());
2557
2558        drop(updates_tx);
2559        drain_sparse_trie_tasks(&runtime);
2560    }
2561}