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