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