Skip to main content

reth_engine_tree/tree/state_root_strategy/
sparse_trie.rs

1//! Sparse Trie task related functionality.
2
3use std::sync::Arc;
4
5use super::{evm_state_to_hashed_post_state, StateRootComputeOutcome, StateRootMessage};
6use alloy_primitives::{
7    map::{hash_map::Entry, B256Map},
8    B256,
9};
10use alloy_rlp::{Decodable, Encodable};
11use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
12use metrics::{Gauge, Histogram};
13use rayon::iter::{IntoParallelIterator, ParallelIterator};
14use reth_metrics::Metrics;
15use reth_primitives_traits::{Account, FastInstant as Instant};
16use reth_tasks::Runtime;
17use reth_trie::{
18    updates::TrieUpdates, DecodedMultiProofV2, HashedPostState, TrieAccount, EMPTY_ROOT_HASH,
19    TRIE_ACCOUNT_RLP_MAX_SIZE,
20};
21use reth_trie_common::{MultiProofTargetsV2, ProofV2Target, ProofV2TargetParent};
22use reth_trie_parallel::{
23    error::StateRootTaskError,
24    proof_task::{
25        AccountMultiproofInput, ProofResultContext, ProofResultMessage, ProofResultSender,
26        ProofWorkerHandle,
27    },
28};
29use reth_trie_sparse::{
30    errors::{SparseStateTrieErrorKind, SparseTrieErrorKind, SparseTrieResult},
31    ArenaParallelSparseTrie, DeferredDrops, LeafUpdate, RevealableSparseTrie, SparseStateTrie,
32    SparseTrie, TrieNodeEpoch,
33};
34use tracing::{debug, debug_span, error, instrument, trace_span};
35
36/// Sparse trie task implementation that uses in-memory sparse trie data to schedule proof fetching.
37pub(super) struct SparseTrieCacheTask<A = ArenaParallelSparseTrie, S = ArenaParallelSparseTrie> {
38    /// Sender for proof results.
39    proof_result_tx: ProofResultSender,
40    /// Receiver for proof results directly from workers.
41    proof_result_rx: CrossbeamReceiver<ProofResultMessage>,
42    /// Receives updates from execution and prewarming.
43    updates: CrossbeamReceiver<SparseTrieTaskMessage>,
44    /// Fires (by disconnecting) when the consumer drops its cancel guard, meaning nobody is
45    /// waiting for the result anymore. This is the teardown path for a task whose pending
46    /// work never drains, since the updates channel closing is a normal end of stream.
47    cancel_rx: CrossbeamReceiver<()>,
48    /// Sender half for the channel to send final hashed state to.
49    final_hashed_state_tx: Option<std::sync::mpsc::Sender<Arc<HashedPostState>>>,
50    /// `SparseStateTrie` used for computing the state root.
51    trie: SparseStateTrie<A, S>,
52    /// The parent block's state root.
53    parent_state_root: B256,
54    /// The new epoch assigned to nodes modified by this task.
55    new_epoch: TrieNodeEpoch,
56    /// Handle to the proof worker pools (storage and account).
57    proof_worker_handle: ProofWorkerHandle,
58
59    /// The size of proof targets chunk to spawn in one calculation.
60    /// If None, chunking is disabled and all targets are processed in a single proof.
61    chunk_size: usize,
62    /// If this number is exceeded and chunking is enabled, then this will override whether or not
63    /// there are any active workers and force chunking across workers. This is to prevent tasks
64    /// which are very long from hitting a single worker.
65    max_targets_for_chunking: usize,
66
67    /// Account trie updates.
68    account_updates: B256Map<LeafUpdate>,
69    /// Storage trie updates. hashed address -> slot -> update.
70    storage_updates: B256Map<B256Map<LeafUpdate>>,
71
72    /// Account updates that are buffered but were not yet applied to the trie.
73    new_account_updates: B256Map<LeafUpdate>,
74    /// Storage updates that are buffered but were not yet applied to the trie.
75    new_storage_updates: B256Map<B256Map<LeafUpdate>>,
76    /// Account updates that are blocked by storage root calculation or account reveal.
77    ///
78    /// Those are being moved into `account_updates` once storage roots
79    /// are revealed and/or calculated.
80    ///
81    /// Invariant: for each entry in `pending_account_updates` account must either be already
82    /// revealed in the trie or have an entry in `account_updates`.
83    ///
84    /// Values can be either of:
85    ///   - None: account had a storage update and is awaiting storage root calculation and/or
86    ///     account node reveal to complete.
87    ///   - Some(_): account was changed/destroyed and is awaiting storage root calculation/reveal
88    ///     to complete.
89    pending_account_updates: B256Map<Option<Option<Account>>>,
90    /// Cache of account proof targets that were already fetched/requested from the proof workers.
91    /// Account to the broadest requested parent context (an unknown parent sorts before every
92    /// known parent).
93    fetched_account_targets: B256Map<ProofV2TargetParent>,
94    /// Cache of storage proof targets that have already been fetched/requested from the proof
95    /// workers. Account to slot to the broadest requested parent context.
96    fetched_storage_targets: B256Map<B256Map<ProofV2TargetParent>>,
97    /// Reusable buffer for RLP encoding of accounts.
98    account_rlp_buf: Vec<u8>,
99    /// Whether the last state update has been received.
100    finished_state_updates: bool,
101    /// Accumulated account leaf update cache hits.
102    account_cache_hits: u64,
103    /// Accumulated account leaf update cache misses.
104    account_cache_misses: u64,
105    /// Accumulated storage leaf update cache hits.
106    storage_cache_hits: u64,
107    /// Accumulated storage leaf update cache misses.
108    storage_cache_misses: u64,
109    /// Pending proof targets queued for dispatch to proof workers.
110    pending_targets: PendingTargets,
111    /// Proof batches dispatched to workers and not yet received.
112    in_flight_proof_batches: usize,
113    /// Number of pending execution/prewarming updates received but not yet passed to
114    /// `update_leaves`.
115    pending_updates: usize,
116    /// Whether the first buffered leaf batch has been applied.
117    initial_updates_applied: bool,
118    /// Combined final hashed state.
119    ///
120    /// Sparse trie task observes and hashes all state updates, allowing it to cheaply construct a
121    /// final [`HashedPostState`] and share it with main engine thread without requiring any extra
122    /// hashing work.
123    final_hashed_state: HashedPostState,
124
125    /// Metrics for the sparse trie.
126    metrics: SparseTrieTaskMetrics,
127}
128
129impl<A, S> SparseTrieCacheTask<A, S>
130where
131    A: SparseTrie + Default,
132    S: SparseTrie + Default + Clone,
133{
134    /// Creates a new sparse trie, pre-populating with an existing [`SparseStateTrie`].
135    #[expect(clippy::too_many_arguments)]
136    pub(super) fn new_with_trie(
137        executor: &Runtime,
138        updates: CrossbeamReceiver<StateRootMessage>,
139        cancel_rx: CrossbeamReceiver<()>,
140        final_hashed_state_tx: std::sync::mpsc::Sender<Arc<HashedPostState>>,
141        proof_worker_handle: ProofWorkerHandle,
142        proof_result_tx: ProofResultSender,
143        proof_result_rx: CrossbeamReceiver<ProofResultMessage>,
144        metrics: SparseTrieTaskMetrics,
145        trie: SparseStateTrie<A, S>,
146        parent_state_root: B256,
147        new_epoch: TrieNodeEpoch,
148        chunk_size: usize,
149    ) -> Self {
150        let (hashed_state_tx, hashed_state_rx) = crossbeam_channel::unbounded();
151
152        let parent_span = tracing::Span::current();
153        let hashing_metrics = metrics.clone();
154        executor.spawn_blocking_named("trie-hashing", move || {
155            let _span = trace_span!(parent: parent_span, "run_hashing_task").entered();
156            Self::run_hashing_task(updates, hashed_state_tx, hashing_metrics)
157        });
158
159        Self {
160            proof_result_tx,
161            proof_result_rx,
162            updates: hashed_state_rx,
163            cancel_rx,
164            proof_worker_handle,
165            final_hashed_state_tx: Some(final_hashed_state_tx),
166            trie,
167            parent_state_root,
168            new_epoch,
169            chunk_size,
170            max_targets_for_chunking: DEFAULT_MAX_TARGETS_FOR_CHUNKING,
171            account_updates: Default::default(),
172            storage_updates: Default::default(),
173            new_account_updates: Default::default(),
174            new_storage_updates: Default::default(),
175            pending_account_updates: Default::default(),
176            fetched_account_targets: Default::default(),
177            fetched_storage_targets: Default::default(),
178            account_rlp_buf: Vec::with_capacity(TRIE_ACCOUNT_RLP_MAX_SIZE),
179            finished_state_updates: Default::default(),
180            account_cache_hits: 0,
181            account_cache_misses: 0,
182            storage_cache_hits: 0,
183            storage_cache_misses: 0,
184            pending_targets: Default::default(),
185            in_flight_proof_batches: 0,
186            pending_updates: Default::default(),
187            initial_updates_applied: false,
188            final_hashed_state: Default::default(),
189            metrics,
190        }
191    }
192
193    /// Runs the hashing task that drains updates from the channel and converts them to
194    /// `HashedPostState` in parallel.
195    fn run_hashing_task(
196        updates: CrossbeamReceiver<StateRootMessage>,
197        hashed_state_tx: CrossbeamSender<SparseTrieTaskMessage>,
198        metrics: SparseTrieTaskMetrics,
199    ) {
200        let mut total_idle_time = std::time::Duration::ZERO;
201        let mut idle_start = Instant::now();
202
203        while let Ok(message) = updates.recv() {
204            total_idle_time += idle_start.elapsed();
205
206            let msg = match message {
207                StateRootMessage::PrefetchProofs(targets) => {
208                    SparseTrieTaskMessage::PrefetchProofs(targets)
209                }
210                StateRootMessage::StateUpdate(state) => {
211                    let _span = trace_span!(target: "engine::tree::payload_processor::sparse_trie", "hashing_state_update", n = state.len()).entered();
212                    let hashed = evm_state_to_hashed_post_state(state);
213                    SparseTrieTaskMessage::HashedState(hashed)
214                }
215                StateRootMessage::FinishedStateUpdates => {
216                    SparseTrieTaskMessage::FinishedStateUpdates
217                }
218                StateRootMessage::HashedStateUpdate(state) => {
219                    SparseTrieTaskMessage::HashedState(state)
220                }
221            };
222            if hashed_state_tx.send(msg).is_err() {
223                break;
224            }
225
226            idle_start = Instant::now();
227        }
228
229        metrics.hashing_task_idle_time_seconds.record(total_idle_time.as_secs_f64());
230    }
231
232    /// Returns the trie for reuse in the next payload built on top of this one.
233    ///
234    /// Should be called after the state root result has been sent.
235    pub(super) fn into_trie_for_reuse(self) -> (SparseStateTrie<A, S>, DeferredDrops) {
236        let Self { mut trie, .. } = self;
237        let deferred = trie.take_deferred_drops();
238        (trie, deferred)
239    }
240
241    /// Clears the trie, discarding all state.
242    ///
243    /// Use this when the payload was invalid or cancelled - we don't want to preserve
244    /// potentially invalid trie state, but we keep the allocations for reuse.
245    pub(super) fn into_cleared_trie(self) -> (SparseStateTrie<A, S>, DeferredDrops) {
246        let Self { mut trie, .. } = self;
247        trie.clear();
248        let deferred = trie.take_deferred_drops();
249        (trie, deferred)
250    }
251
252    /// Runs the sparse trie task to completion.
253    ///
254    /// This waits for new incoming [`SparseTrieTaskMessage`]s, applies updates
255    /// to the trie and schedules proof fetching when needed.
256    ///
257    /// This concludes once the last state update has been received and processed.
258    #[instrument(
259        name = "SparseTrieCacheTask::run",
260        level = "debug",
261        target = "engine::tree::payload_processor::sparse_trie",
262        skip_all
263    )]
264    pub(super) fn run(&mut self) -> Result<StateRootComputeOutcome, StateRootTaskError> {
265        let now = Instant::now();
266
267        let mut total_idle_time = std::time::Duration::ZERO;
268        let mut idle_start = Instant::now();
269        let mut done = false;
270        let mut finalized_hashed_state = None;
271
272        // Streaming phase: updates are still arriving. Ends when the finish marker is
273        // processed. Only producers hold update senders, so the channel closing before the
274        // marker means they died without finishing the stream.
275        while !self.finished_state_updates {
276            let mut t = Instant::now();
277            crossbeam_channel::select_biased! {
278                recv(self.updates) -> message => {
279                    let wake = Instant::now();
280                    total_idle_time += wake.duration_since(idle_start);
281                    self.metrics
282                        .sparse_trie_channel_wait_duration_histogram
283                        .record(wake.duration_since(t));
284
285                    let update = message.map_err(|_| StateRootTaskError::Other(
286                        "updates channel disconnected before state root calculation".to_string(),
287                    ))?;
288                    if let Some(hashed_state) = self.on_message(update) {
289                        finalized_hashed_state = Some(hashed_state);
290                    }
291                    self.pending_updates += 1;
292                }
293                recv(self.proof_result_rx) -> message => {
294                    let wake = Instant::now();
295                    total_idle_time += wake.duration_since(idle_start);
296                    self.metrics
297                        .sparse_trie_channel_wait_duration_histogram
298                        .record(wake.duration_since(t));
299                    t = wake;
300
301                    let Ok(result) = message else {
302                        unreachable!("we own the sender half")
303                    };
304                    self.on_proof_results(result, &mut t)?;
305                },
306                recv(self.cancel_rx) -> _ => return Err(StateRootTaskError::Canceled),
307            }
308
309            done = self.make_progress()?;
310            idle_start = Instant::now();
311        }
312
313        // Draining phase: the marker is the last message read from the updates channel, so
314        // after it only proof results and cancellation can occur. The channel closing when
315        // the producers drop their senders is not observed here, and late best-effort hints
316        // are ignored: with all updates known, prefetching has nothing left to help.
317        while !done {
318            let mut t = Instant::now();
319            crossbeam_channel::select_biased! {
320                recv(self.proof_result_rx) -> message => {
321                    let wake = Instant::now();
322                    total_idle_time += wake.duration_since(idle_start);
323                    self.metrics
324                        .sparse_trie_channel_wait_duration_histogram
325                        .record(wake.duration_since(t));
326                    t = wake;
327
328                    let Ok(result) = message else {
329                        unreachable!("we own the sender half")
330                    };
331                    self.on_proof_results(result, &mut t)?;
332                },
333                recv(self.cancel_rx) -> _ => return Err(StateRootTaskError::Canceled),
334            }
335
336            done = self.make_progress()?;
337            idle_start = Instant::now();
338        }
339
340        self.metrics.sparse_trie_idle_time_seconds.record(total_idle_time.as_secs_f64());
341
342        debug!(target: "engine::root", "All proofs processed, ending calculation");
343
344        let start = Instant::now();
345        let (state_root, trie_updates) = match self.trie.root_with_updates(self.new_epoch) {
346            Ok(result) => result,
347            Err(err)
348                if matches!(
349                    err.kind(),
350                    SparseStateTrieErrorKind::Sparse(SparseTrieErrorKind::Blind)
351                ) =>
352            {
353                // A still-blind account trie means this block never changed state, so preserve
354                // the cached parent root instead of fetching and revealing
355                // the unchanged root node.
356                (self.parent_state_root, TrieUpdates::default())
357            }
358            Err(err) => {
359                return Err(StateRootTaskError::Other(format!(
360                    "could not calculate state root: {err:?}"
361                )))
362            }
363        };
364
365        let end = Instant::now();
366        self.metrics.sparse_trie_final_update_duration_histogram.record(end.duration_since(start));
367        self.metrics.sparse_trie_total_duration_histogram.record(end.duration_since(now));
368
369        self.metrics.sparse_trie_account_cache_hits.record(self.account_cache_hits as f64);
370        self.metrics.sparse_trie_account_cache_misses.record(self.account_cache_misses as f64);
371        self.metrics.sparse_trie_storage_cache_hits.record(self.storage_cache_hits as f64);
372        self.metrics.sparse_trie_storage_cache_misses.record(self.storage_cache_misses as f64);
373        self.account_cache_hits = 0;
374        self.account_cache_misses = 0;
375        self.storage_cache_hits = 0;
376        self.storage_cache_misses = 0;
377
378        Ok(StateRootComputeOutcome {
379            state_root,
380            trie_updates: Arc::new(trie_updates),
381            hashed_state: finalized_hashed_state
382                .expect("finished state updates publish the hashed post state"),
383        })
384    }
385
386    /// Handles a received proof result: coalesces everything already queued, reveals the
387    /// proof in the trie, and records timing metrics.
388    fn on_proof_results(
389        &mut self,
390        message: ProofResultMessage,
391        t: &mut Instant,
392    ) -> Result<(), StateRootTaskError> {
393        let mut result = self.on_proof_result_message(message)?;
394        while let Ok(next) = self.proof_result_rx.try_recv() {
395            let res = self.on_proof_result_message(next)?;
396            result.extend(res);
397        }
398
399        let phase_end = Instant::now();
400        self.metrics
401            .sparse_trie_proof_coalesce_duration_histogram
402            .record(phase_end.duration_since(*t));
403        *t = phase_end;
404
405        self.on_proof_result(result)?;
406        self.metrics.sparse_trie_reveal_multiproof_duration_histogram.record(t.elapsed());
407        Ok(())
408    }
409
410    /// Applies buffered updates to the trie and dispatches proof targets.
411    ///
412    /// Messages queued after the finish marker are best-effort hints and are not actionable.
413    /// Returns `true` once the finish marker was received and all pending trie work is done.
414    fn make_progress(&mut self) -> Result<bool, StateRootTaskError> {
415        let updates_queued = !self.finished_state_updates && !self.updates.is_empty();
416
417        if !updates_queued && self.proof_result_rx.is_empty() {
418            // If we don't have any pending messages, we can spend some time on computing
419            // storage roots and promoting account updates.
420            self.dispatch_pending_targets()?;
421            let t = Instant::now();
422            self.process_new_updates()?;
423            self.promote_pending_account_updates()?;
424            self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
425
426            if self.finished_state_updates && !self.has_pending_sparse_trie_updates() {
427                return Ok(true);
428            }
429
430            self.dispatch_pending_targets()?;
431            self.ensure_not_stalled(updates_queued)?;
432
433            // If there's still no pending updates spend some time pre-computing the account
434            // trie upper hashes
435            if self.proof_result_rx.is_empty() {
436                self.trie.calculate_subtries(self.new_epoch);
437            }
438        } else if !updates_queued {
439            // If we don't have any pending updates, apply them to the trie,
440            let t = Instant::now();
441            self.process_new_updates()?;
442            self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
443            self.dispatch_pending_targets()?;
444        } else if !self.initial_updates_applied && self.pending_updates >= INITIAL_UPDATE_BATCH_SIZE
445        {
446            // Start proof fetching before a continuously arriving state stream drains. Later
447            // batches retain the usual coalescing policy to avoid repeatedly sorting small maps.
448            let t = Instant::now();
449            self.process_new_updates()?;
450            self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
451            self.dispatch_pending_targets()?;
452        } else if self.pending_targets.len() > self.chunk_size {
453            // Make sure to dispatch targets if we've accumulated a lot of them.
454            self.dispatch_pending_targets()?;
455        }
456        Ok(false)
457    }
458
459    /// Processes a [`SparseTrieTaskMessage`] from the hashing task.
460    fn on_message(&mut self, message: SparseTrieTaskMessage) -> Option<Arc<HashedPostState>> {
461        match message {
462            SparseTrieTaskMessage::PrefetchProofs(targets) => {
463                self.on_prewarm_targets(targets);
464                None
465            }
466            SparseTrieTaskMessage::HashedState(hashed_state) => {
467                self.on_hashed_state_update(hashed_state);
468                None
469            }
470            SparseTrieTaskMessage::FinishedStateUpdates => {
471                let hashed_state = Arc::new(core::mem::take(&mut self.final_hashed_state));
472                let _ = self.final_hashed_state_tx.take().unwrap().send(Arc::clone(&hashed_state));
473                self.finished_state_updates = true;
474                Some(hashed_state)
475            }
476        }
477    }
478
479    #[instrument(
480        level = "trace",
481        target = "engine::tree::payload_processor::sparse_trie",
482        skip_all
483    )]
484    fn on_prewarm_targets(&mut self, targets: MultiProofTargetsV2) {
485        for target in targets.account_targets {
486            // Only touch accounts that are not yet present in the updates set.
487            self.new_account_updates.entry(target.key()).or_insert(LeafUpdate::Touched);
488        }
489
490        for (address, slots) in targets.storage_targets {
491            if !slots.is_empty() {
492                // Look up outer map once per address instead of once per slot.
493                let new_updates = self.new_storage_updates.entry(address).or_default();
494                for slot in slots {
495                    // Only touch storages that are not yet present in the updates set.
496                    new_updates.entry(slot.key()).or_insert(LeafUpdate::Touched);
497                }
498            }
499
500            // Touch corresponding account leaf to make sure its revealed in accounts trie for
501            // storage root update.
502            self.new_account_updates.entry(address).or_insert(LeafUpdate::Touched);
503        }
504    }
505
506    /// Processes a hashed state update and encodes all state changes as trie updates.
507    #[instrument(
508        level = "trace",
509        target = "engine::tree::payload_processor::sparse_trie",
510        skip_all
511    )]
512    fn on_hashed_state_update(&mut self, hashed_state_update: HashedPostState) {
513        for (&address, storage) in &hashed_state_update.storages {
514            if !storage.storage.is_empty() {
515                // Look up outer maps once per address instead of once per slot.
516                let new_updates = self.new_storage_updates.entry(address).or_default();
517                let mut existing_updates = self.storage_updates.get_mut(&address);
518
519                for (&slot, &value) in &storage.storage {
520                    let encoded = if value.is_zero() {
521                        Vec::new()
522                    } else {
523                        alloy_rlp::encode_fixed_size(&value).to_vec()
524                    };
525                    new_updates.insert(slot, LeafUpdate::Changed(encoded));
526
527                    // Remove an existing storage update if it exists.
528                    if let Some(ref mut existing) = existing_updates {
529                        existing.remove(&slot);
530                    }
531                }
532            }
533
534            // Make sure account is tracked in `account_updates` so that it is revealed in accounts
535            // trie for storage root update.
536            self.new_account_updates.entry(address).or_insert(LeafUpdate::Touched);
537
538            // Make sure account is tracked in `pending_account_updates` so that once storage root
539            // is computed, it will be updated in the accounts trie.
540            self.pending_account_updates.entry(address).or_insert(None);
541        }
542
543        for (&address, &account) in &hashed_state_update.accounts {
544            // Track account as touched.
545            //
546            // This might overwrite an existing update, which is fine, because storage root from it
547            // is already tracked in the trie and can be easily fetched again.
548            self.new_account_updates.insert(address, LeafUpdate::Touched);
549
550            // Track account in `pending_account_updates` so that once storage root is computed,
551            // it will be updated in the accounts trie.
552            self.pending_account_updates.insert(address, Some(account));
553        }
554
555        self.final_hashed_state.extend(hashed_state_update);
556    }
557
558    fn on_proof_result(&mut self, result: DecodedMultiProofV2) -> Result<(), StateRootTaskError> {
559        self.trie
560            .reveal_decoded_multiproof_v2(result)
561            .map_err(|e| StateRootTaskError::Other(format!("could not reveal multiproof: {e:?}")))
562    }
563
564    fn on_proof_result_message(
565        &mut self,
566        message: ProofResultMessage,
567    ) -> Result<DecodedMultiProofV2, StateRootTaskError> {
568        let result = message.result?;
569        debug_assert!(
570            self.in_flight_proof_batches > 0,
571            "received proof result without an in-flight proof batch"
572        );
573        self.in_flight_proof_batches = self.in_flight_proof_batches.saturating_sub(1);
574        Ok(result)
575    }
576
577    fn process_new_updates(&mut self) -> SparseTrieResult<()> {
578        if self.pending_updates == 0 {
579            return Ok(());
580        }
581
582        let _span = debug_span!("process_new_updates").entered();
583        self.pending_updates = 0;
584        self.initial_updates_applied = true;
585
586        // Firstly apply all new storage and account updates to the tries.
587        self.process_leaf_updates(true)?;
588
589        for (address, mut new) in self.new_storage_updates.drain() {
590            match self.storage_updates.entry(address) {
591                Entry::Vacant(entry) => {
592                    entry.insert(new); // insert the whole map at once, no per-slot loop
593                }
594                Entry::Occupied(mut entry) => {
595                    let updates = entry.get_mut();
596                    for (slot, new) in new.drain() {
597                        match updates.entry(slot) {
598                            Entry::Occupied(mut slot_entry) => {
599                                if new.is_changed() {
600                                    slot_entry.insert(new);
601                                }
602                            }
603                            Entry::Vacant(slot_entry) => {
604                                slot_entry.insert(new);
605                            }
606                        }
607                    }
608                }
609            }
610        }
611
612        for (address, new) in self.new_account_updates.drain() {
613            match self.account_updates.entry(address) {
614                Entry::Occupied(mut entry) => {
615                    if new.is_changed() {
616                        entry.insert(new);
617                    }
618                }
619                Entry::Vacant(entry) => {
620                    entry.insert(new);
621                }
622            }
623        }
624
625        Ok(())
626    }
627
628    /// Applies all account and storage leaf updates to corresponding tries and collects any new
629    /// multiproof targets.
630    #[instrument(
631        level = "trace",
632        target = "engine::tree::payload_processor::sparse_trie",
633        skip_all
634    )]
635    fn process_leaf_updates(&mut self, new: bool) -> SparseTrieResult<()> {
636        let storage_updates =
637            if new { &mut self.new_storage_updates } else { &mut self.storage_updates };
638
639        // Process all storage updates, skipping tries with no pending updates.
640        let span = trace_span!("process_storage_leaf_updates").entered();
641        for (address, updates) in storage_updates {
642            if updates.is_empty() {
643                continue;
644            }
645            let _enter = trace_span!(target: "engine::tree::payload_processor::sparse_trie", parent: &span, "storage_trie_leaf_updates", a=%address).entered();
646
647            let trie = self.trie.get_or_create_storage_trie_mut(*address);
648            let fetched = self.fetched_storage_targets.entry(*address).or_default();
649            let mut targets = Vec::new();
650
651            let updates_len_before = updates.len();
652            trie.update_leaves(updates, |path, parent| match fetched.entry(path) {
653                Entry::Occupied(mut entry) => {
654                    if parent < *entry.get() {
655                        entry.insert(parent);
656                        targets.push(ProofV2Target::new(path).with_parent(parent));
657                    }
658                }
659                Entry::Vacant(entry) => {
660                    entry.insert(parent);
661                    targets.push(ProofV2Target::new(path).with_parent(parent));
662                }
663            })?;
664            let updates_len_after = updates.len();
665            self.storage_cache_hits += (updates_len_before - updates_len_after) as u64;
666            self.storage_cache_misses += updates_len_after as u64;
667
668            if !targets.is_empty() {
669                self.pending_targets.extend_storage_targets(address, targets);
670            }
671        }
672
673        drop(span);
674
675        // Process account trie updates and fill the account targets.
676        self.process_account_leaf_updates(new)?;
677
678        Ok(())
679    }
680
681    /// Invokes `update_leaves` for the accounts trie and collects any new targets.
682    ///
683    /// Returns whether any updates were drained (applied to the trie).
684    #[instrument(
685        level = "trace",
686        target = "engine::tree::payload_processor::sparse_trie",
687        skip_all
688    )]
689    fn process_account_leaf_updates(&mut self, new: bool) -> SparseTrieResult<bool> {
690        let account_updates =
691            if new { &mut self.new_account_updates } else { &mut self.account_updates };
692
693        let updates_len_before = account_updates.len();
694
695        self.trie.trie_mut().update_leaves(account_updates, |target, parent| {
696            match self.fetched_account_targets.entry(target) {
697                Entry::Occupied(mut entry) => {
698                    if parent < *entry.get() {
699                        entry.insert(parent);
700                        self.pending_targets
701                            .push_account_target(ProofV2Target::new(target).with_parent(parent));
702                    }
703                }
704                Entry::Vacant(entry) => {
705                    entry.insert(parent);
706                    self.pending_targets
707                        .push_account_target(ProofV2Target::new(target).with_parent(parent));
708                }
709            }
710        })?;
711
712        let updates_len_after = account_updates.len();
713        self.account_cache_hits += (updates_len_before - updates_len_after) as u64;
714        self.account_cache_misses += updates_len_after as u64;
715
716        Ok(updates_len_after < updates_len_before)
717    }
718
719    /// Computes storage roots for accounts whose storage updates are fully drained.
720    ///
721    /// For each storage trie T that:
722    /// 1. was modified in the current block,
723    /// 2. all the storage updates are fully drained,
724    /// 3. but the storage root hasn't been updated yet,
725    ///
726    /// we trigger state root computation on a rayon pool.
727    fn compute_drained_storage_roots(&mut self) {
728        struct SendStorageTriePtr<S>(*mut RevealableSparseTrie<S>);
729        // SAFETY: this wrapper only forwards the pointer across rayon; deref invariants are
730        // documented at the use site below.
731        unsafe impl<S: Send> Send for SendStorageTriePtr<S> {}
732
733        let mut tries_to_compute_roots: Vec<(B256, SendStorageTriePtr<S>)> = Vec::new();
734        for (address, updates) in &self.storage_updates {
735            if updates.is_empty() &&
736                let Some(trie) = self.trie.storage_tries_mut().get_mut(address) &&
737                !trie.is_root_cached()
738            {
739                tries_to_compute_roots.push((*address, SendStorageTriePtr(trie)));
740            }
741        }
742
743        if tries_to_compute_roots.is_empty() {
744            return;
745        }
746
747        let parent_span =
748            debug_span!("compute_drained_storage_roots", n = tries_to_compute_roots.len());
749        let new_epoch = self.new_epoch;
750        tries_to_compute_roots.into_par_iter().for_each(|(address, SendStorageTriePtr(trie))| {
751            let span = if tracing::enabled!(tracing::Level::TRACE) {
752                debug_span!(
753                    target: "engine::tree::payload_processor::sparse_trie",
754                    parent: &parent_span,
755                    "storage_root",
756                    ?address
757                )
758            } else {
759                debug_span!(
760                    target: "engine::tree::payload_processor::sparse_trie",
761                    parent: &parent_span,
762                    "storage_root",
763                )
764            };
765            let _enter = span.entered();
766            // SAFETY:
767            // - pointers are created from `storage_tries_mut().get_mut(address)` above;
768            // - `storage_updates` is a map, so addresses are unique;
769            // - we do not insert/remove entries between pointer collection and use, so pointers
770            //   stay valid and map reallocation cannot occur;
771            // - each pointer is consumed by at most one rayon task, so no aliasing mutable access.
772            unsafe {
773                (*trie)
774                    .root(new_epoch)
775                    .expect("updates are drained, trie should be revealed by now")
776            };
777        });
778    }
779
780    /// Iterates through all storage tries for which all updates were processed, computes their
781    /// storage roots, and promotes corresponding pending account updates into proper leaf updates
782    /// for accounts trie.
783    #[instrument(
784        level = "trace",
785        target = "engine::tree::payload_processor::sparse_trie",
786        skip_all
787    )]
788    fn promote_pending_account_updates(&mut self) -> SparseTrieResult<()> {
789        self.process_leaf_updates(false)?;
790
791        if self.pending_account_updates.is_empty() {
792            return Ok(());
793        }
794
795        self.compute_drained_storage_roots();
796
797        loop {
798            let span = trace_span!("promote_updates", promoted = tracing::field::Empty).entered();
799            // Now handle pending account updates that can be upgraded to a proper update.
800            let account_rlp_buf = &mut self.account_rlp_buf;
801            let mut num_promoted = 0;
802            self.pending_account_updates.retain(|addr, account| {
803                if let Some(updates) = self.storage_updates.get(addr) {
804                    if !updates.is_empty() {
805                        // If account has pending storage updates, it is still pending.
806                        return true;
807                    } else if let Some(account) = account.take() {
808                        let storage_root = self.trie.storage_root(addr, self.new_epoch).expect("updates are drained, storage trie should be revealed by now");
809                        let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
810                        self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
811                        num_promoted += 1;
812                        return false;
813                    }
814                }
815
816                // Get the current account state either from the trie or from latest account update.
817                let trie_account = match self.account_updates.get(addr) {
818                    Some(LeafUpdate::Changed(encoded)) => {
819                        Some(encoded).filter(|encoded| !encoded.is_empty())
820                    }
821                    // Needs to be revealed first
822                    Some(LeafUpdate::Touched) => return true,
823                    None => self.trie.get_account_value(addr),
824                };
825
826                let trie_account = trie_account.map(|value| TrieAccount::decode(&mut &value[..]).expect("invalid account RLP"));
827
828                let (account, storage_root) = if let Some(account) = account.take() {
829                    // If account is Some(_) here it means it didn't have any storage updates
830                    // and we can fetch the storage root directly from the account trie.
831                    //
832                    // If it did have storage updates, we would've had processed it above when iterating over storage tries.
833                    let storage_root = trie_account.map(|account| account.storage_root).unwrap_or(EMPTY_ROOT_HASH);
834
835                    (account, storage_root)
836                } else {
837                    (trie_account.map(Into::into), self.trie.storage_root(addr, self.new_epoch).expect("account had storage updates that were applied to its trie, storage root must be revealed by now"))
838                };
839
840                let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
841                self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
842                num_promoted += 1;
843
844                false
845            });
846            span.record("promoted", num_promoted);
847            drop(span);
848
849            // Only exit when no new updates are processed.
850            //
851            // We need to keep iterating if any updates are being drained because that might
852            // indicate that more pending account updates can be promoted.
853            if num_promoted == 0 || !self.process_account_leaf_updates(false)? {
854                break
855            }
856        }
857
858        Ok(())
859    }
860
861    fn dispatch_pending_targets(&mut self) -> Result<(), StateRootTaskError> {
862        if self.pending_targets.is_empty() {
863            return Ok(())
864        }
865
866        let _span = trace_span!("dispatch_pending_targets").entered();
867        let (targets, chunking_length) = self.pending_targets.take();
868        let mut dispatch_error = None;
869        dispatch_with_chunking(
870            targets,
871            chunking_length,
872            self.chunk_size,
873            self.max_targets_for_chunking,
874            self.proof_worker_handle.has_multiple_idle_account_workers(),
875            self.proof_worker_handle.has_multiple_idle_storage_workers(),
876            MultiProofTargetsV2::chunks,
877            |proof_targets| {
878                if dispatch_error.is_some() {
879                    return;
880                }
881
882                match self.proof_worker_handle.dispatch_account_multiproof(AccountMultiproofInput {
883                    targets: proof_targets,
884                    proof_result_sender: ProofResultContext::new(
885                        self.proof_result_tx.clone(),
886                        HashedPostState::default(),
887                        Instant::now(),
888                    ),
889                }) {
890                    Ok(()) => {
891                        self.in_flight_proof_batches += 1;
892                    }
893                    Err(e) => {
894                        error!("failed to dispatch account multiproof: {e:?}");
895                        dispatch_error = Some(StateRootTaskError::ProofDispatch(e));
896                    }
897                }
898            },
899        );
900
901        if let Some(error) = dispatch_error {
902            return Err(error)
903        }
904
905        Ok(())
906    }
907
908    fn has_pending_sparse_trie_updates(&self) -> bool {
909        !self.account_updates.is_empty() ||
910            self.storage_updates.values().any(|updates| !updates.is_empty()) ||
911            !self.pending_account_updates.is_empty()
912    }
913
914    /// Errors when pending trie updates remain but nothing can deliver them: no update
915    /// messages are queued, no proof targets are queued or in flight, and no proof results
916    /// are waiting.
917    ///
918    /// `updates_queued` is passed in instead of reading `self.updates` directly, because in
919    /// the draining phase the updates channel is not read anymore and may hold ignored late
920    /// hints that must not mask a stall.
921    fn ensure_not_stalled(&self, updates_queued: bool) -> Result<(), StateRootTaskError> {
922        if self.finished_state_updates &&
923            !updates_queued &&
924            self.pending_updates == 0 &&
925            self.pending_targets.is_empty() &&
926            self.in_flight_proof_batches == 0 &&
927            self.proof_result_rx.is_empty() &&
928            self.has_pending_sparse_trie_updates()
929        {
930            const MAX_STALLED_PROOF_TARGETS_TO_LOG: usize = 5;
931
932            let mut account_targets = self
933                .account_updates
934                .keys()
935                .map(|target| (*target, self.fetched_account_targets.get(target).copied()))
936                .collect::<Vec<_>>();
937            account_targets.sort_unstable();
938            let account_targets_truncated =
939                account_targets.len().saturating_sub(MAX_STALLED_PROOF_TARGETS_TO_LOG);
940            account_targets.truncate(MAX_STALLED_PROOF_TARGETS_TO_LOG);
941
942            let mut storage_targets = self
943                .storage_updates
944                .iter()
945                .flat_map(|(address, updates)| {
946                    let fetched_targets = self.fetched_storage_targets.get(address);
947                    updates.keys().map(move |target| {
948                        (
949                            *address,
950                            *target,
951                            fetched_targets.and_then(|targets| targets.get(target)).copied(),
952                        )
953                    })
954                })
955                .collect::<Vec<_>>();
956            storage_targets.sort_unstable();
957            let storage_targets_truncated =
958                storage_targets.len().saturating_sub(MAX_STALLED_PROOF_TARGETS_TO_LOG);
959            storage_targets.truncate(MAX_STALLED_PROOF_TARGETS_TO_LOG);
960
961            error!(
962                ?account_targets,
963                account_targets_truncated,
964                ?storage_targets,
965                storage_targets_truncated,
966                "sparse trie task stalled: pending updates remain but no proof targets are queued or in flight"
967            );
968
969            return Err(StateRootTaskError::Stalled)
970        }
971
972        Ok(())
973    }
974}
975
976/// Metrics recorded by sparse trie and hashing tasks.
977#[derive(Metrics, Clone)]
978#[metrics(scope = "tree.root")]
979pub(super) struct SparseTrieTaskMetrics {
980    /// Histogram of durations spent revealing multiproof results into the sparse trie.
981    pub(super) sparse_trie_reveal_multiproof_duration_histogram: Histogram,
982    /// Histogram of durations spent coalescing multiple proof results from the channel.
983    pub(super) sparse_trie_proof_coalesce_duration_histogram: Histogram,
984    /// Histogram of durations the event loop spent blocked waiting on channels.
985    pub(super) sparse_trie_channel_wait_duration_histogram: Histogram,
986    /// Histogram of durations spent processing trie updates and promoting pending accounts.
987    pub(super) sparse_trie_process_updates_duration_histogram: Histogram,
988    /// Histogram of sparse trie final update durations.
989    pub(super) sparse_trie_final_update_duration_histogram: Histogram,
990    /// Histogram of sparse trie total durations.
991    pub(super) sparse_trie_total_duration_histogram: Histogram,
992    /// Time spent preparing the sparse trie for reuse after state root computation.
993    pub(super) into_trie_for_reuse_duration_histogram: Histogram,
994    /// Time spent pruning the sparse trie by node epoch.
995    pub(super) sparse_trie_prune_duration_histogram: Histogram,
996    /// Time spent waiting for preserved sparse trie cache to become available.
997    pub(super) sparse_trie_cache_wait_duration_histogram: Histogram,
998    /// Histogram for sparse trie task idle time in seconds (waiting for updates or proof
999    /// results). Excludes the final wait after the channel is closed.
1000    pub(super) sparse_trie_idle_time_seconds: Histogram,
1001    /// Histogram for hashing task idle time in seconds (waiting for messages from execution).
1002    /// Excludes the final wait after the channel is closed.
1003    pub(super) hashing_task_idle_time_seconds: Histogram,
1004
1005    /// Number of account leaf updates applied without needing a new proof (cache hits).
1006    pub(super) sparse_trie_account_cache_hits: Histogram,
1007    /// Number of account leaf updates that required a new proof (cache misses).
1008    pub(super) sparse_trie_account_cache_misses: Histogram,
1009    /// Number of storage leaf updates applied without needing a new proof (cache hits).
1010    pub(super) sparse_trie_storage_cache_hits: Histogram,
1011    /// Number of storage leaf updates that required a new proof (cache misses).
1012    pub(super) sparse_trie_storage_cache_misses: Histogram,
1013
1014    /// Number of storage tries retained in the preserved sparse trie cache.
1015    pub(super) sparse_trie_retained_storage_tries: Gauge,
1016}
1017
1018/// The default max targets, for limiting the number of account and storage proof targets to be
1019/// fetched by a single worker. If exceeded, chunking is forced regardless of worker availability.
1020const DEFAULT_MAX_TARGETS_FOR_CHUNKING: usize = 300;
1021
1022/// Start proof fetching while the first state-update batch is still arriving.
1023const INITIAL_UPDATE_BATCH_SIZE: usize = 64;
1024
1025/// Dispatches work items as a single unit or in chunks based on target size and worker
1026/// availability.
1027#[expect(clippy::too_many_arguments)]
1028fn dispatch_with_chunking<T, I>(
1029    items: T,
1030    chunking_len: usize,
1031    chunk_size: usize,
1032    max_targets_for_chunking: usize,
1033    has_multiple_idle_account_workers: bool,
1034    has_multiple_idle_storage_workers: bool,
1035    chunker: impl FnOnce(T, usize) -> I,
1036    mut dispatch: impl FnMut(T),
1037) where
1038    I: IntoIterator<Item = T>,
1039{
1040    let has_full_chunks = chunking_len >= chunk_size.saturating_mul(2);
1041    let should_chunk = chunking_len > max_targets_for_chunking ||
1042        (has_full_chunks &&
1043            (has_multiple_idle_account_workers || has_multiple_idle_storage_workers));
1044
1045    if should_chunk && chunking_len > chunk_size {
1046        for chunk in chunker(items, chunk_size) {
1047            dispatch(chunk);
1048        }
1049        return;
1050    }
1051
1052    dispatch(items);
1053}
1054
1055/// RLP-encodes the account as a [`TrieAccount`] leaf value, or returns empty for deletions.
1056///
1057/// `Some(Account::default())` with an empty storage root is encoded as a deletion. This is valid
1058/// for post-Merge state because EIP-7523 (<https://eips.ethereum.org/EIPS/eip-7523>) prohibits
1059/// empty accounts. Do not use this encoding rule when replaying historical pre-Merge state, where
1060/// an empty account and a missing account can have different trie representations.
1061fn encode_account_leaf_value(
1062    account: Option<Account>,
1063    storage_root: B256,
1064    account_rlp_buf: &mut Vec<u8>,
1065) -> Vec<u8> {
1066    if account.is_none_or(|account| account.is_empty()) && storage_root == EMPTY_ROOT_HASH {
1067        return Vec::new();
1068    }
1069
1070    account_rlp_buf.clear();
1071    account.unwrap_or_default().into_trie_account(storage_root).encode(account_rlp_buf);
1072    account_rlp_buf.clone()
1073}
1074
1075/// Pending proof targets queued for dispatch to proof workers, along with their count.
1076#[derive(Default)]
1077struct PendingTargets {
1078    /// The proof targets.
1079    targets: MultiProofTargetsV2,
1080    /// Number of account + storage proof targets currently queued.
1081    len: usize,
1082}
1083
1084impl PendingTargets {
1085    /// Returns the number of pending targets.
1086    const fn len(&self) -> usize {
1087        self.len
1088    }
1089
1090    /// Returns `true` if there are no pending targets.
1091    const fn is_empty(&self) -> bool {
1092        self.len == 0
1093    }
1094
1095    /// Takes the pending targets, replacing with empty defaults.
1096    fn take(&mut self) -> (MultiProofTargetsV2, usize) {
1097        (std::mem::take(&mut self.targets), std::mem::take(&mut self.len))
1098    }
1099
1100    /// Adds a target to the account targets.
1101    fn push_account_target(&mut self, target: ProofV2Target) {
1102        self.targets.account_targets.push(target);
1103        self.len += 1;
1104    }
1105
1106    /// Extends storage targets for the given address.
1107    fn extend_storage_targets(&mut self, address: &B256, targets: Vec<ProofV2Target>) {
1108        self.len += targets.len();
1109        self.targets.storage_targets.entry(*address).or_default().extend(targets);
1110    }
1111}
1112
1113/// Message type for the sparse trie task.
1114enum SparseTrieTaskMessage {
1115    /// A hashed state update ready to be processed.
1116    HashedState(HashedPostState),
1117    /// Prefetch proof targets (passed through directly).
1118    PrefetchProofs(MultiProofTargetsV2),
1119    /// Signals that all state updates have been received.
1120    FinishedStateUpdates,
1121}
1122
1123#[cfg(test)]
1124mod tests {
1125    use super::*;
1126    use alloy_primitives::{keccak256, Address, B256, U256};
1127    use reth_db_common::init::init_genesis;
1128    use reth_provider::test_utils::create_test_provider_factory;
1129    use reth_storage_overlay::{OverlayManager, OverlayStateProviderFactory};
1130    use reth_trie_parallel::proof_task::ProofTaskCtx;
1131    use reth_trie_sparse::ArenaParallelSparseTrie;
1132
1133    fn drain_sparse_trie_tasks(runtime: &Runtime) {
1134        for task_name in ["trie-hashing", "storage-workers", "account-workers"] {
1135            runtime.spawn_blocking_named(task_name, || {}).get();
1136        }
1137    }
1138
1139    #[test]
1140    fn test_run_hashing_task_hashed_state_update_forwards() {
1141        let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1142        let (hashed_state_tx, hashed_state_rx) = crossbeam_channel::unbounded();
1143
1144        let address = keccak256(Address::random());
1145        let slot = keccak256(U256::from(42).to_be_bytes::<32>());
1146        let value = U256::from(999);
1147
1148        let mut hashed_state = HashedPostState::default();
1149        hashed_state.accounts.insert(
1150            address,
1151            Some(Account { balance: U256::from(100), nonce: 1, bytecode_hash: None }),
1152        );
1153        let mut storage = reth_trie::HashedStorage::default();
1154        storage.storage.insert(slot, value);
1155        hashed_state.storages.insert(address, storage);
1156
1157        let expected_state = hashed_state.clone();
1158
1159        let handle = std::thread::spawn(move || {
1160            SparseTrieCacheTask::<ArenaParallelSparseTrie, ArenaParallelSparseTrie>::run_hashing_task(
1161                updates_rx,
1162                hashed_state_tx,
1163                SparseTrieTaskMetrics::default(),
1164            );
1165        });
1166
1167        updates_tx.send(StateRootMessage::HashedStateUpdate(hashed_state)).unwrap();
1168        updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
1169        drop(updates_tx);
1170
1171        let SparseTrieTaskMessage::HashedState(received) = hashed_state_rx.recv().unwrap() else {
1172            panic!("expected HashedState message");
1173        };
1174
1175        let account = received.accounts.get(&address).unwrap().unwrap();
1176        assert_eq!(account.balance, expected_state.accounts[&address].unwrap().balance);
1177        assert_eq!(account.nonce, expected_state.accounts[&address].unwrap().nonce);
1178
1179        let storage = received.storages.get(&address).unwrap();
1180        assert_eq!(*storage.storage.get(&slot).unwrap(), value);
1181
1182        let second = hashed_state_rx.recv().unwrap();
1183        assert!(matches!(second, SparseTrieTaskMessage::FinishedStateUpdates));
1184
1185        assert!(hashed_state_rx.recv().is_err());
1186        handle.join().unwrap();
1187    }
1188
1189    #[test]
1190    fn test_encode_account_leaf_value_deletion_and_empty_root_is_empty() {
1191        let mut account_rlp_buf = vec![0xAB];
1192        let encoded = encode_account_leaf_value(None, EMPTY_ROOT_HASH, &mut account_rlp_buf);
1193
1194        assert!(encoded.is_empty());
1195        // Early return should not touch the caller's buffer.
1196        assert_eq!(account_rlp_buf, vec![0xAB]);
1197    }
1198
1199    #[test]
1200    fn test_encode_account_leaf_value_empty_account_and_empty_root_is_empty() {
1201        let mut account_rlp_buf = vec![0xAB];
1202        let encoded = encode_account_leaf_value(
1203            Some(Account::default()),
1204            EMPTY_ROOT_HASH,
1205            &mut account_rlp_buf,
1206        );
1207
1208        assert!(encoded.is_empty());
1209        // Early return should not touch the caller's buffer.
1210        assert_eq!(account_rlp_buf, vec![0xAB]);
1211    }
1212
1213    #[test]
1214    fn test_encode_account_leaf_value_non_empty_account_is_rlp() {
1215        let storage_root = B256::from([0x99; 32]);
1216        let account = Some(Account {
1217            nonce: 7,
1218            balance: U256::from(42),
1219            bytecode_hash: Some(B256::from([0xAA; 32])),
1220        });
1221        let mut account_rlp_buf = vec![0x00, 0x01];
1222
1223        let encoded = encode_account_leaf_value(account, storage_root, &mut account_rlp_buf);
1224        let decoded = TrieAccount::decode(&mut &encoded[..]).expect("valid account RLP");
1225
1226        assert_eq!(decoded.nonce, 7);
1227        assert_eq!(decoded.balance, U256::from(42));
1228        assert_eq!(decoded.storage_root, storage_root);
1229        assert_eq!(account_rlp_buf, encoded);
1230    }
1231
1232    #[test]
1233    fn first_leaf_batch_starts_proofs_before_input_queue_drains() {
1234        let runtime = reth_tasks::Runtime::test();
1235        let provider_factory = create_test_provider_factory();
1236        let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
1237        let state_provider_factory = OverlayStateProviderFactory::new(
1238            provider_factory,
1239            OverlayManager::<reth_chain_state::EthPrimitives>::default()
1240                .overlay_builder(anchor_hash),
1241        );
1242        let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
1243        let proof_worker_handle = ProofWorkerHandle::new(
1244            &runtime,
1245            ProofTaskCtx::new(state_provider_factory),
1246            false,
1247            proof_result_tx.clone(),
1248        );
1249
1250        let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
1251        let trie = SparseStateTrie::default()
1252            .with_accounts_trie(default_trie.clone())
1253            .with_default_storage_trie(default_trie)
1254            .with_updates(true);
1255
1256        let parent_state_root = B256::from([0x55; 32]);
1257        let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1258        let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
1259        let mut task = SparseTrieCacheTask::new_with_trie(
1260            &runtime,
1261            updates_rx,
1262            cancel_rx,
1263            std::sync::mpsc::channel().0,
1264            proof_worker_handle,
1265            proof_result_tx,
1266            proof_result_rx,
1267            SparseTrieTaskMetrics::default(),
1268            trie,
1269            parent_state_root,
1270            TrieNodeEpoch::UNMODIFIED,
1271            1,
1272        );
1273
1274        // Keep an input queued so progress cannot use its normal queue-empty flush.
1275        updates_tx.send(StateRootMessage::PrefetchProofs(Default::default())).unwrap();
1276        let deadline = std::time::Instant::now();
1277        while task.updates.is_empty() {
1278            assert!(deadline.elapsed() < std::time::Duration::from_secs(1));
1279            std::thread::yield_now();
1280        }
1281        for index in 0..INITIAL_UPDATE_BATCH_SIZE {
1282            let mut state = HashedPostState::default();
1283            state.accounts.insert(
1284                B256::repeat_byte(index as u8),
1285                Some(Account { nonce: 1, ..Default::default() }),
1286            );
1287            task.on_hashed_state_update(state);
1288            task.pending_updates += 1;
1289            assert!(!task.make_progress().unwrap());
1290            if index + 1 < INITIAL_UPDATE_BATCH_SIZE {
1291                assert_eq!(task.in_flight_proof_batches, 0);
1292            }
1293        }
1294        assert!(task.in_flight_proof_batches > 0, "proof work must start before the queue drains");
1295        assert_eq!(task.pending_updates, 0);
1296
1297        // A second batch remains buffered; the early flush must not become a permanent small
1298        // batch policy that repeatedly scans and sorts pending leaves.
1299        for index in INITIAL_UPDATE_BATCH_SIZE..INITIAL_UPDATE_BATCH_SIZE * 2 {
1300            let mut state = HashedPostState::default();
1301            state.accounts.insert(
1302                B256::repeat_byte(index as u8),
1303                Some(Account { nonce: 1, ..Default::default() }),
1304            );
1305            task.on_hashed_state_update(state);
1306            task.pending_updates += 1;
1307            assert!(!task.make_progress().unwrap());
1308        }
1309        assert_eq!(task.pending_updates, INITIAL_UPDATE_BATCH_SIZE);
1310        assert_eq!(task.new_account_updates.len(), INITIAL_UPDATE_BATCH_SIZE);
1311        drop(updates_tx);
1312        drop(task);
1313        drain_sparse_trie_tasks(&runtime);
1314    }
1315
1316    #[test]
1317    fn run_returns_parent_root_without_revealing_blind_trie_when_no_state_updates() {
1318        let runtime = reth_tasks::Runtime::test();
1319        let provider_factory = create_test_provider_factory();
1320        let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
1321        let state_provider_factory = OverlayStateProviderFactory::new(
1322            provider_factory,
1323            OverlayManager::<reth_chain_state::EthPrimitives>::default()
1324                .overlay_builder(anchor_hash),
1325        );
1326        let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
1327        let proof_worker_handle = ProofWorkerHandle::new(
1328            &runtime,
1329            ProofTaskCtx::new(state_provider_factory),
1330            false,
1331            proof_result_tx.clone(),
1332        );
1333
1334        let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
1335        let trie = SparseStateTrie::default()
1336            .with_accounts_trie(default_trie.clone())
1337            .with_default_storage_trie(default_trie)
1338            .with_updates(true);
1339
1340        let parent_state_root = B256::from([0x55; 32]);
1341        let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1342        let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
1343        let mut task = SparseTrieCacheTask::new_with_trie(
1344            &runtime,
1345            updates_rx,
1346            cancel_rx,
1347            std::sync::mpsc::channel().0,
1348            proof_worker_handle,
1349            proof_result_tx,
1350            proof_result_rx,
1351            SparseTrieTaskMetrics::default(),
1352            trie,
1353            parent_state_root,
1354            TrieNodeEpoch::UNMODIFIED,
1355            1,
1356        );
1357
1358        updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
1359        drop(updates_tx);
1360
1361        let outcome = task.run().expect("state root computation should succeed");
1362
1363        assert_eq!(outcome.state_root, parent_state_root);
1364        assert!(outcome.trie_updates.is_empty());
1365        assert!(task.trie.state_trie_ref().is_none(), "blind trie should not be revealed");
1366
1367        drop(task);
1368        drain_sparse_trie_tasks(&runtime);
1369    }
1370
1371    #[test]
1372    fn stall_check_waits_for_in_flight_proofs_then_reports_pending_updates() {
1373        let runtime = reth_tasks::Runtime::test();
1374        let provider_factory = create_test_provider_factory();
1375        let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
1376        let state_provider_factory = OverlayStateProviderFactory::new(
1377            provider_factory,
1378            OverlayManager::<reth_chain_state::EthPrimitives>::default()
1379                .overlay_builder(anchor_hash),
1380        );
1381        let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
1382        let proof_worker_handle = ProofWorkerHandle::new(
1383            &runtime,
1384            ProofTaskCtx::new(state_provider_factory),
1385            false,
1386            proof_result_tx.clone(),
1387        );
1388
1389        let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
1390        let trie = SparseStateTrie::default()
1391            .with_accounts_trie(default_trie.clone())
1392            .with_default_storage_trie(default_trie)
1393            .with_updates(true);
1394
1395        let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1396        let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
1397        let mut task = SparseTrieCacheTask::new_with_trie(
1398            &runtime,
1399            updates_rx,
1400            cancel_rx,
1401            std::sync::mpsc::channel().0,
1402            proof_worker_handle,
1403            proof_result_tx,
1404            proof_result_rx,
1405            SparseTrieTaskMetrics::default(),
1406            trie,
1407            B256::from([0x55; 32]),
1408            TrieNodeEpoch::UNMODIFIED,
1409            1,
1410        );
1411
1412        drop(updates_tx);
1413
1414        let account = B256::from([0x11; 32]);
1415        let slot = B256::from([0x22; 32]);
1416        let account_target = B256::from([0x33; 32]);
1417        let storage_target = B256::from([0x44; 32]);
1418
1419        task.finished_state_updates = true;
1420        task.account_updates.insert(account, LeafUpdate::Touched);
1421        task.storage_updates.entry(account).or_default().insert(slot, LeafUpdate::Touched);
1422        task.pending_account_updates.insert(account, None);
1423        task.fetched_account_targets.insert(account_target, ProofV2TargetParent::NONE);
1424        task.fetched_storage_targets
1425            .entry(account)
1426            .or_default()
1427            .insert(storage_target, ProofV2TargetParent::new(11));
1428        task.in_flight_proof_batches = 1;
1429
1430        assert!(task.ensure_not_stalled(false).is_ok());
1431
1432        let result = ProofResultMessage {
1433            result: Ok(DecodedMultiProofV2::default()),
1434            elapsed: std::time::Duration::ZERO,
1435            state: HashedPostState::default(),
1436        };
1437        task.on_proof_result_message(result).expect("proof result should be ok");
1438
1439        assert_eq!(task.in_flight_proof_batches, 0);
1440        let error = task.ensure_not_stalled(false).expect_err("task should be stalled");
1441        assert!(matches!(error, StateRootTaskError::Stalled));
1442        let error = error.to_string();
1443
1444        assert!(error.contains("sparse trie task stalled"));
1445        assert!(!error.contains("account_targets"));
1446        assert!(!error.contains("storage_targets"));
1447        assert!(!error.contains(&format!("{account:?}")));
1448        assert!(!error.contains(&format!("{account_target:?}")));
1449        assert!(!error.contains(&format!("{storage_target:?}")));
1450        assert!(!error.contains("pending_account_leaves"));
1451        assert!(!error.contains("pending_storage_leaves"));
1452        assert!(!error.contains("pending_account_updates"));
1453        assert!(!error.contains(&format!("{slot:?}")));
1454
1455        drop(task);
1456        drain_sparse_trie_tasks(&runtime);
1457    }
1458
1459    #[test]
1460    fn run_errors_when_cancel_guard_drops_before_updates_finish() {
1461        let runtime = reth_tasks::Runtime::test();
1462        let provider_factory = create_test_provider_factory();
1463        let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
1464        let state_provider_factory = OverlayStateProviderFactory::new(
1465            provider_factory,
1466            OverlayManager::<reth_chain_state::EthPrimitives>::default()
1467                .overlay_builder(anchor_hash),
1468        );
1469        let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
1470        let proof_worker_handle = ProofWorkerHandle::new(
1471            &runtime,
1472            ProofTaskCtx::new(state_provider_factory),
1473            false,
1474            proof_result_tx.clone(),
1475        );
1476
1477        let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
1478        let trie = SparseStateTrie::default()
1479            .with_accounts_trie(default_trie.clone())
1480            .with_default_storage_trie(default_trie)
1481            .with_updates(true);
1482
1483        let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1484        let (cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
1485        let mut task = SparseTrieCacheTask::new_with_trie(
1486            &runtime,
1487            updates_rx,
1488            cancel_rx,
1489            std::sync::mpsc::channel().0,
1490            proof_worker_handle,
1491            proof_result_tx,
1492            proof_result_rx,
1493            SparseTrieTaskMetrics::default(),
1494            trie,
1495            B256::from([0x55; 32]),
1496            TrieNodeEpoch::UNMODIFIED,
1497            1,
1498        );
1499
1500        // The consumer abandons the computation. The updates channel is still open (no finish
1501        // marker was sent), so without the cancel signal the task would wait forever.
1502        drop(cancel_guard);
1503
1504        let error = task.run().expect_err("canceled task must return an error");
1505        assert!(matches!(error, StateRootTaskError::Canceled));
1506
1507        drop(updates_tx);
1508        drop(task);
1509        drain_sparse_trie_tasks(&runtime);
1510    }
1511
1512    #[test]
1513    fn run_ignores_hints_queued_after_updates_finish() {
1514        let runtime = reth_tasks::Runtime::test();
1515        let provider_factory = create_test_provider_factory();
1516        let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
1517        let state_provider_factory = OverlayStateProviderFactory::new(
1518            provider_factory,
1519            OverlayManager::<reth_chain_state::EthPrimitives>::default()
1520                .overlay_builder(anchor_hash),
1521        );
1522        let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
1523        let proof_worker_handle = ProofWorkerHandle::new(
1524            &runtime,
1525            ProofTaskCtx::new(state_provider_factory),
1526            false,
1527            proof_result_tx.clone(),
1528        );
1529
1530        let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
1531        let trie = SparseStateTrie::default()
1532            .with_accounts_trie(default_trie.clone())
1533            .with_default_storage_trie(default_trie)
1534            .with_updates(true);
1535
1536        let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1537        let (cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
1538        let mut task = SparseTrieCacheTask::new_with_trie(
1539            &runtime,
1540            updates_rx,
1541            cancel_rx,
1542            std::sync::mpsc::channel().0,
1543            proof_worker_handle,
1544            proof_result_tx,
1545            proof_result_rx,
1546            SparseTrieTaskMetrics::default(),
1547            trie,
1548            B256::from([0x55; 32]),
1549            TrieNodeEpoch::UNMODIFIED,
1550            1,
1551        );
1552
1553        updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
1554        updates_tx.send(StateRootMessage::PrefetchProofs(Default::default())).unwrap();
1555
1556        let wait_start = std::time::Instant::now();
1557        while task.updates.len() < 2 {
1558            assert!(
1559                wait_start.elapsed() < std::time::Duration::from_secs(1),
1560                "hashing task did not queue the test messages"
1561            );
1562            std::thread::yield_now();
1563        }
1564
1565        let (result_tx, result_rx) = std::sync::mpsc::channel();
1566        let handle = std::thread::spawn(move || {
1567            let _ = result_tx.send(task.run());
1568        });
1569
1570        let result = result_rx.recv_timeout(std::time::Duration::from_secs(1));
1571        drop(cancel_guard);
1572        handle.join().unwrap();
1573
1574        assert!(result.expect("state root task stalled on a late hint").is_ok());
1575
1576        drop(updates_tx);
1577        drain_sparse_trie_tasks(&runtime);
1578    }
1579}