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