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