1use std::{
4 any::Any,
5 panic::{self, AssertUnwindSafe},
6 sync::Arc,
7};
8
9use super::{evm_state_to_hashed_post_state, StateRootComputeOutcome, StateRootMessage};
10use alloy_primitives::{
11 map::{hash_map::Entry, B256Map},
12 B256,
13};
14use alloy_rlp::{Decodable, Encodable};
15use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
16use metrics::{Gauge, Histogram};
17use reth_metrics::Metrics;
18use reth_primitives_traits::{Account, FastInstant as Instant};
19use reth_tasks::Runtime;
20use reth_trie::{
21 updates::TrieUpdates, DecodedMultiProofV2, HashedPostState, TrieAccount, EMPTY_ROOT_HASH,
22 TRIE_ACCOUNT_RLP_MAX_SIZE,
23};
24use reth_trie_common::{MultiProofTargetsV2, ProofTrieNodeV2, ProofV2Target, ProofV2TargetParent};
25use reth_trie_parallel::{
26 error::StateRootTaskError,
27 proof_task::{ProofWorkerHandle, ProofWorkerResult},
28};
29use reth_trie_sparse::{
30 errors::{
31 SparseStateTrieErrorKind, SparseStateTrieResult, SparseTrieErrorKind, SparseTrieResult,
32 },
33 ArenaParallelSparseTrie, DeferredDrops, LeafUpdate, RevealableSparseTrie, SparseStateTrie,
34 SparseTrie, TrieNodeEpoch,
35};
36use tracing::{debug, debug_span, error, instrument, trace_span};
37
38pub(super) struct SparseTrieCacheTask<A = ArenaParallelSparseTrie, S = ArenaParallelSparseTrie> {
120 updates: CrossbeamReceiver<SparseTrieTaskMessage>,
122 cancel_rx: CrossbeamReceiver<()>,
126 final_hashed_state_tx: Option<std::sync::mpsc::Sender<Arc<HashedPostState>>>,
128 trie: SparseStateTrie<A, S>,
130 parent_state_root: B256,
132 new_epoch: TrieNodeEpoch,
134 proof_worker_handle: ProofWorkerHandle,
136
137 chunk_size: usize,
140 max_targets_for_chunking: usize,
144
145 account_updates: B256Map<LeafUpdate>,
147
148 new_account_updates: B256Map<LeafUpdate>,
150 new_storage_updates: B256Map<B256Map<LeafUpdate>>,
152 pending_account_updates: B256Map<Option<Option<Account>>>,
166 fetched_account_targets: B256Map<ProofV2TargetParent>,
170 account_rlp_buf: Vec<u8>,
172 finished_state_updates: bool,
174 account_cache_hits: u64,
176 account_cache_misses: u64,
178 storage_cache_hits: u64,
180 storage_cache_misses: u64,
182 pending_targets: PendingTargets,
184 in_flight_proof_jobs: usize,
186 storage: B256Map<StorageTrieState<S>>,
194 storage_to_drop: B256Map<StorageTrieState<S>>,
196 storage_in_flight: usize,
198 storage_done_tx: CrossbeamSender<StorageJobMessage<S>>,
200 storage_done_rx: CrossbeamReceiver<StorageJobMessage<S>>,
202 pending_updates: usize,
205 initial_updates_applied: bool,
207 final_hashed_state: HashedPostState,
213
214 metrics: SparseTrieTaskMetrics,
216}
217
218impl<A, S> SparseTrieCacheTask<A, S>
219where
220 A: SparseTrie + Default,
221 S: SparseTrie + Default + Clone + 'static,
222{
223 #[expect(clippy::too_many_arguments)]
225 pub(super) fn new_with_trie(
226 executor: &Runtime,
227 updates: CrossbeamReceiver<StateRootMessage>,
228 cancel_rx: CrossbeamReceiver<()>,
229 final_hashed_state_tx: std::sync::mpsc::Sender<Arc<HashedPostState>>,
230 proof_worker_handle: ProofWorkerHandle,
231 metrics: SparseTrieTaskMetrics,
232 trie: SparseStateTrie<A, S>,
233 parent_state_root: B256,
234 new_epoch: TrieNodeEpoch,
235 chunk_size: usize,
236 ) -> Self {
237 let (hashed_state_tx, hashed_state_rx) = crossbeam_channel::unbounded();
238 let (storage_done_tx, storage_done_rx) = crossbeam_channel::unbounded();
239
240 let parent_span = tracing::Span::current();
241 let hashing_metrics = metrics.clone();
242 executor.spawn_blocking_named("trie-hashing", move || {
243 let _span = trace_span!(parent: parent_span, "run_hashing_task").entered();
244 Self::run_hashing_task(updates, hashed_state_tx, hashing_metrics)
245 });
246
247 Self {
248 updates: hashed_state_rx,
249 cancel_rx,
250 proof_worker_handle,
251 final_hashed_state_tx: Some(final_hashed_state_tx),
252 trie,
253 parent_state_root,
254 new_epoch,
255 chunk_size,
256 max_targets_for_chunking: DEFAULT_MAX_TARGETS_FOR_CHUNKING,
257 account_updates: Default::default(),
258 new_account_updates: Default::default(),
259 new_storage_updates: Default::default(),
260 pending_account_updates: Default::default(),
261 fetched_account_targets: Default::default(),
262 account_rlp_buf: Vec::with_capacity(TRIE_ACCOUNT_RLP_MAX_SIZE),
263 finished_state_updates: Default::default(),
264 account_cache_hits: 0,
265 account_cache_misses: 0,
266 storage_cache_hits: 0,
267 storage_cache_misses: 0,
268 pending_targets: Default::default(),
269 in_flight_proof_jobs: 0,
270 storage: Default::default(),
271 storage_to_drop: Default::default(),
272 storage_in_flight: 0,
273 storage_done_tx,
274 storage_done_rx,
275 pending_updates: Default::default(),
276 initial_updates_applied: false,
277 final_hashed_state: Default::default(),
278 metrics,
279 }
280 }
281
282 fn run_hashing_task(
285 updates: CrossbeamReceiver<StateRootMessage>,
286 hashed_state_tx: CrossbeamSender<SparseTrieTaskMessage>,
287 metrics: SparseTrieTaskMetrics,
288 ) {
289 let mut total_idle_time = std::time::Duration::ZERO;
290 let mut idle_start = Instant::now();
291
292 while let Ok(message) = updates.recv() {
293 total_idle_time += idle_start.elapsed();
294
295 let msg = match message {
296 StateRootMessage::PrefetchProofs(targets) => {
297 SparseTrieTaskMessage::PrefetchProofs(targets)
298 }
299 StateRootMessage::StateUpdate(state) => {
300 let _span = trace_span!(target: "engine::tree::payload_processor::sparse_trie", "hashing_state_update", n = state.len()).entered();
301 let hashed = evm_state_to_hashed_post_state(state);
302 SparseTrieTaskMessage::HashedState(hashed)
303 }
304 StateRootMessage::FinishedStateUpdates => {
305 SparseTrieTaskMessage::FinishedStateUpdates
306 }
307 StateRootMessage::HashedStateUpdate(state) => {
308 SparseTrieTaskMessage::HashedState(state)
309 }
310 };
311 if hashed_state_tx.send(msg).is_err() {
312 break;
313 }
314
315 idle_start = Instant::now();
316 }
317
318 metrics.hashing_task_idle_time_seconds.record(total_idle_time.as_secs_f64());
319 }
320
321 pub(super) fn into_trie_for_reuse(self) -> (SparseStateTrie<A, S>, DeferredDrops) {
325 debug_assert!(
326 self.storage.is_empty(),
327 "storage tries must be back before the trie is preserved"
328 );
329 let Self { mut trie, .. } = self;
330 let deferred = trie.take_deferred_drops();
331 (trie, deferred)
332 }
333
334 pub(super) fn into_cleared_trie(self) -> (SparseStateTrie<A, S>, DeferredDrops) {
339 let Self { mut trie, storage_done_rx, storage, .. } = self;
340 drop(storage_done_rx);
342 for (address, state) in storage {
343 if let StorageTrieState::Idle(work) = state {
344 trie.insert_storage_trie(address, work.trie);
345 }
346 }
347 trie.clear();
348 let deferred = trie.take_deferred_drops();
349 (trie, deferred)
350 }
351
352 #[instrument(
361 name = "SparseTrieCacheTask::run",
362 level = "debug",
363 target = "engine::tree::payload_processor::sparse_trie",
364 skip_all
365 )]
366 pub(super) fn run(&mut self) -> Result<StateRootComputeOutcome, StateRootTaskError> {
367 let now = Instant::now();
368
369 let mut total_idle_time = std::time::Duration::ZERO;
370 let mut idle_start = Instant::now();
371 let mut done = false;
372 let mut finalized_hashed_state = None;
373
374 while !self.finished_state_updates {
378 let mut t = Instant::now();
379 crossbeam_channel::select_biased! {
380 recv(self.updates) -> message => {
381 let wake = Instant::now();
382 total_idle_time += wake.duration_since(idle_start);
383 self.metrics
384 .sparse_trie_channel_wait_duration_histogram
385 .record(wake.duration_since(t));
386
387 let update = message.map_err(|_| StateRootTaskError::Other(
388 "updates channel disconnected before state root calculation".into(),
389 ))?;
390 if let Some(hashed_state) = self.on_message(update) {
391 finalized_hashed_state = Some(hashed_state);
392 }
393 self.pending_updates += 1;
394 }
395 recv(self.proof_worker_handle.results()) -> message => {
396 let wake = Instant::now();
397 total_idle_time += wake.duration_since(idle_start);
398 self.metrics
399 .sparse_trie_channel_wait_duration_histogram
400 .record(wake.duration_since(t));
401 t = wake;
402
403 let Ok(result) = message else {
404 unreachable!("proof worker results never disconnect while the handle is alive")
405 };
406 self.on_proof_worker_result(result, &mut t)?;
407 },
408 recv(self.storage_done_rx) -> message => {
409 let wake = Instant::now();
410 total_idle_time += wake.duration_since(idle_start);
411 self.metrics
412 .sparse_trie_channel_wait_duration_histogram
413 .record(wake.duration_since(t));
414
415 let Ok(returned) = message else {
416 unreachable!("we own the sender half")
417 };
418 self.on_storage_job_message(returned)?;
419 },
420 recv(self.cancel_rx) -> _ => return Err(StateRootTaskError::Canceled),
421 }
422
423 done = self.make_progress()?;
424 idle_start = Instant::now();
425 }
426
427 while !done {
432 let mut t = Instant::now();
433 crossbeam_channel::select_biased! {
434 recv(self.proof_worker_handle.results()) -> message => {
435 let wake = Instant::now();
436 total_idle_time += wake.duration_since(idle_start);
437 self.metrics
438 .sparse_trie_channel_wait_duration_histogram
439 .record(wake.duration_since(t));
440 t = wake;
441
442 let Ok(result) = message else {
443 unreachable!("proof worker results never disconnect while the handle is alive")
444 };
445 self.on_proof_worker_result(result, &mut t)?;
446 },
447 recv(self.storage_done_rx) -> message => {
448 let wake = Instant::now();
449 total_idle_time += wake.duration_since(idle_start);
450 self.metrics
451 .sparse_trie_channel_wait_duration_histogram
452 .record(wake.duration_since(t));
453
454 let Ok(returned) = message else {
455 unreachable!("we own the sender half")
456 };
457 self.on_storage_job_message(returned)?;
458 },
459 recv(self.cancel_rx) -> _ => return Err(StateRootTaskError::Canceled),
460 }
461
462 done = self.make_progress()?;
463 idle_start = Instant::now();
464 }
465
466 debug_assert_eq!(
467 self.storage_in_flight, 0,
468 "completion must wait for every checked out storage trie"
469 );
470 self.metrics.sparse_trie_idle_time_seconds.record(total_idle_time.as_secs_f64());
471
472 debug!(target: "engine::root", "All proofs processed, ending calculation");
473
474 let start = Instant::now();
475 self.return_storage_tries();
476 let (state_root, trie_updates) = match self.trie.root_with_updates(self.new_epoch) {
477 Ok(result) => result,
478 Err(err)
479 if matches!(
480 err.kind(),
481 SparseStateTrieErrorKind::Sparse(SparseTrieErrorKind::Blind)
482 ) =>
483 {
484 (self.parent_state_root, TrieUpdates::default())
488 }
489 Err(err) => return Err(StateRootTaskError::Other(err.into())),
490 };
491
492 let end = Instant::now();
493 self.metrics.sparse_trie_final_update_duration_histogram.record(end.duration_since(start));
494 self.metrics.sparse_trie_total_duration_histogram.record(end.duration_since(now));
495
496 self.metrics.sparse_trie_account_cache_hits.record(self.account_cache_hits as f64);
497 self.metrics.sparse_trie_account_cache_misses.record(self.account_cache_misses as f64);
498 self.metrics.sparse_trie_storage_cache_hits.record(self.storage_cache_hits as f64);
499 self.metrics.sparse_trie_storage_cache_misses.record(self.storage_cache_misses as f64);
500 self.account_cache_hits = 0;
501 self.account_cache_misses = 0;
502 self.storage_cache_hits = 0;
503 self.storage_cache_misses = 0;
504
505 Ok(StateRootComputeOutcome {
506 state_root,
507 trie_updates: Arc::new(trie_updates),
508 hashed_state: finalized_hashed_state
509 .expect("finished state updates publish the hashed post state"),
510 })
511 }
512
513 fn on_proof_worker_result(
516 &mut self,
517 result: ProofWorkerResult,
518 t: &mut Instant,
519 ) -> Result<(), StateRootTaskError> {
520 let mut proofs = self.complete_proof_job(result)?;
521 while let Ok(next) = self.proof_worker_handle.results().try_recv() {
522 proofs.extend(self.complete_proof_job(next)?);
523 }
524
525 let phase_end = Instant::now();
526 self.metrics
527 .sparse_trie_proof_coalesce_duration_histogram
528 .record(phase_end.duration_since(*t));
529 *t = phase_end;
530
531 self.on_proofs(proofs)?;
532 self.metrics.sparse_trie_reveal_multiproof_duration_histogram.record(t.elapsed());
533 Ok(())
534 }
535
536 fn make_progress(&mut self) -> Result<bool, StateRootTaskError> {
541 self.drain_returned_storage_tries()?;
544
545 let updates_queued = !self.finished_state_updates && !self.updates.is_empty();
546
547 if !updates_queued && self.proof_worker_handle.results().is_empty() {
548 self.dispatch_pending_targets()?;
551 let t = Instant::now();
552 self.process_new_updates()?;
553 self.promote_pending_account_updates()?;
554 self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
555
556 if self.finished_state_updates && !self.has_pending_sparse_trie_updates() {
557 return Ok(true);
558 }
559
560 self.dispatch_pending_targets()?;
561 self.ensure_not_stalled(updates_queued)?;
562
563 if self.proof_worker_handle.results().is_empty() {
566 self.trie.calculate_subtries(self.new_epoch);
567 }
568 } else if !updates_queued {
569 let t = Instant::now();
571 self.process_new_updates()?;
572 self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
573 self.dispatch_pending_targets()?;
574 } else if !self.initial_updates_applied && self.pending_updates >= INITIAL_UPDATE_BATCH_SIZE
575 {
576 let t = Instant::now();
579 self.process_new_updates()?;
580 self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
581 self.dispatch_pending_targets()?;
582 } else if self.pending_targets.len() > self.chunk_size {
583 self.dispatch_pending_targets()?;
585 }
586 Ok(false)
587 }
588
589 fn on_message(&mut self, message: SparseTrieTaskMessage) -> Option<Arc<HashedPostState>> {
591 match message {
592 SparseTrieTaskMessage::PrefetchProofs(targets) => {
593 self.on_prewarm_targets(targets);
594 None
595 }
596 SparseTrieTaskMessage::HashedState(hashed_state) => {
597 self.on_hashed_state_update(hashed_state);
598 None
599 }
600 SparseTrieTaskMessage::FinishedStateUpdates => {
601 let hashed_state = Arc::new(core::mem::take(&mut self.final_hashed_state));
602 let _ = self.final_hashed_state_tx.take().unwrap().send(Arc::clone(&hashed_state));
603 self.finished_state_updates = true;
604 Some(hashed_state)
605 }
606 }
607 }
608
609 #[instrument(
610 level = "trace",
611 target = "engine::tree::payload_processor::sparse_trie",
612 skip_all
613 )]
614 fn on_prewarm_targets(&mut self, targets: MultiProofTargetsV2) {
615 for target in targets.account_targets {
616 self.new_account_updates.entry(target.key()).or_insert(LeafUpdate::Touched);
618 }
619
620 for (address, slots) in targets.storage_targets {
621 if !slots.is_empty() {
622 let new_updates = self.new_storage_updates.entry(address).or_default();
624 for slot in slots {
625 new_updates.entry(slot.key()).or_insert(LeafUpdate::Touched);
627 }
628 }
629
630 self.new_account_updates.entry(address).or_insert(LeafUpdate::Touched);
633 }
634 }
635
636 #[instrument(
638 level = "trace",
639 target = "engine::tree::payload_processor::sparse_trie",
640 skip_all
641 )]
642 #[allow(clippy::clone_on_copy)]
643 fn on_hashed_state_update(&mut self, hashed_state_update: HashedPostState) {
644 for (&address, storage) in &hashed_state_update.storages {
645 if !storage.storage.is_empty() {
646 let new_updates = self.new_storage_updates.entry(address).or_default();
648
649 for (&slot, &value) in &storage.storage {
650 let encoded = if value.is_zero() {
651 Vec::new()
652 } else {
653 alloy_rlp::encode_fixed_size(&value).to_vec()
654 };
655 new_updates.insert(slot, LeafUpdate::Changed(encoded));
656 }
657 }
658
659 self.new_account_updates.entry(address).or_insert(LeafUpdate::Touched);
662
663 self.pending_account_updates.entry(address).or_insert(None);
666 }
667
668 for (&address, account) in &hashed_state_update.accounts {
669 self.new_account_updates.insert(address, LeafUpdate::Touched);
674
675 self.pending_account_updates.insert(address, Some(account.clone()));
678 }
679
680 self.final_hashed_state.extend(hashed_state_update);
681 }
682
683 fn on_proofs(&mut self, proofs: DecodedMultiProofV2) -> Result<(), StateRootTaskError> {
684 self.reveal_proof_result(proofs).map_err(|e| StateRootTaskError::Other(e.into()))
685 }
686
687 fn reveal_proof_result(&mut self, proofs: DecodedMultiProofV2) -> SparseStateTrieResult<()> {
692 let DecodedMultiProofV2 { account_proofs, storage_proofs } = proofs;
693
694 self.queue_storage_proofs(storage_proofs);
695 self.run_ready_storage_work()?;
697
698 self.trie.reveal_account_proof_nodes(account_proofs)
699 }
700
701 fn queue_storage_proofs(&mut self, storage_proofs: B256Map<Vec<ProofTrieNodeV2>>) {
703 let mut revealed_nodes = 0;
704 for (address, mut nodes) in storage_proofs {
705 if nodes.is_empty() {
706 continue;
707 }
708 revealed_nodes += nodes.len();
709
710 match self.storage_trie_state_mut(address) {
711 StorageTrieState::Idle(work) => work.queue_proofs(&mut nodes),
712 StorageTrieState::InFlight(in_flight) => in_flight.proofs.append(&mut nodes),
713 }
714 }
715 self.trie.record_revealed_storage_nodes(revealed_nodes);
716 }
717
718 fn storage_trie_state_mut(&mut self, address: B256) -> &mut StorageTrieState<S> {
721 let Self { storage, trie, .. } = self;
722 storage.entry(address).or_insert_with(|| {
723 StorageTrieState::Idle(Box::new(StorageTrieWork::new(
724 trie.take_or_create_storage_trie(&address),
725 )))
726 })
727 }
728
729 fn complete_proof_job(
731 &mut self,
732 result: ProofWorkerResult,
733 ) -> Result<DecodedMultiProofV2, StateRootTaskError> {
734 let proofs = result?;
735 debug_assert!(
736 self.in_flight_proof_jobs > 0,
737 "received proof result without an in-flight proof job"
738 );
739 self.in_flight_proof_jobs = self.in_flight_proof_jobs.saturating_sub(1);
741 Ok(proofs)
742 }
743
744 fn process_new_updates(&mut self) -> SparseTrieResult<()> {
747 self.apply_new_updates()?;
748 self.run_ready_storage_work()
749 }
750
751 fn apply_new_updates(&mut self) -> SparseTrieResult<()> {
753 if self.pending_updates == 0 {
754 return Ok(());
755 }
756
757 let _span = debug_span!("process_new_updates").entered();
758 self.pending_updates = 0;
759 self.initial_updates_applied = true;
760
761 let Self { storage, trie, new_storage_updates, .. } = self;
763 for (address, new) in new_storage_updates.drain() {
764 if new.is_empty() {
765 continue;
766 }
767
768 let state = storage.entry(address).or_insert_with(|| {
769 StorageTrieState::Idle(Box::new(StorageTrieWork::new(
770 trie.take_or_create_storage_trie(&address),
771 )))
772 });
773 match state {
774 StorageTrieState::Idle(work) => work.queue_updates(new),
775 StorageTrieState::InFlight(in_flight) => {
776 merge_leaf_updates(&mut in_flight.updates, new)
777 }
778 }
779 }
780
781 self.process_account_leaf_updates(true)?;
783
784 for (address, new) in self.new_account_updates.drain() {
785 match self.account_updates.entry(address) {
786 Entry::Occupied(mut entry) => {
787 if new.is_changed() {
788 entry.insert(new);
789 }
790 }
791 Entry::Vacant(entry) => {
792 entry.insert(new);
793 }
794 }
795 }
796
797 Ok(())
798 }
799
800 #[instrument(
808 level = "trace",
809 target = "engine::tree::payload_processor::sparse_trie",
810 skip_all
811 )]
812 fn run_ready_storage_work(&mut self) -> SparseTrieResult<()> {
813 let mut ready = Vec::new();
814 let mut job_units = 0;
815 for (address, state) in &self.storage {
816 let StorageTrieState::Idle(work) = state else { continue };
817 if !work.has_work() {
818 continue;
819 }
820 if !work.is_target_only() {
821 job_units += work.proofs.len() + work.pending.len();
822 }
823 ready.push(*address);
824 }
825 if ready.is_empty() {
826 return Ok(())
827 }
828
829 let inline_round = job_units <= INLINE_STORAGE_WORK_UNITS;
830 let new_epoch = self.new_epoch;
831 let retain_updates = self.trie.retains_updates();
832 let started = Instant::now();
833 let mut jobs = Vec::new();
834 for address in ready {
835 let state = self.storage.get_mut(&address).expect("entry was just seen");
836 let StorageTrieState::Idle(work) = state else {
837 unreachable!("an idle trie is only checked out from here")
838 };
839
840 if inline_round || work.is_target_only() {
841 let output = work.run(new_epoch, retain_updates);
842 self.apply_storage_output(address, output)?;
843 continue;
844 }
845
846 let StorageTrieState::Idle(work) = core::mem::replace(
847 state,
848 StorageTrieState::InFlight(Box::new(InFlightStorage::new(started))),
849 ) else {
850 unreachable!("just matched")
851 };
852 self.storage_in_flight += 1;
853 jobs.push(StorageTrieJob { address, work });
854 }
855
856 self.spawn_storage_jobs(jobs);
857
858 Ok(())
859 }
860
861 fn apply_storage_output(
864 &mut self,
865 address: B256,
866 output: StorageWorkOutput,
867 ) -> SparseTrieResult<()> {
868 let StorageWorkOutput { targets, cache_hits, cache_misses, result } = output;
869
870 self.storage_cache_hits += cache_hits;
871 self.storage_cache_misses += cache_misses;
872 if !targets.is_empty() {
873 self.pending_targets.extend_storage_targets(&address, targets);
874 }
875
876 result
877 }
878
879 #[instrument(
883 level = "trace",
884 target = "engine::tree::payload_processor::sparse_trie",
885 skip_all
886 )]
887 fn process_account_leaf_updates(&mut self, new: bool) -> SparseTrieResult<bool> {
888 let account_updates =
889 if new { &mut self.new_account_updates } else { &mut self.account_updates };
890
891 let updates_len_before = account_updates.len();
892
893 self.trie.trie_mut().update_leaves(account_updates, |target, parent| {
894 match self.fetched_account_targets.entry(target) {
895 Entry::Occupied(mut entry) => {
896 if parent < *entry.get() {
897 entry.insert(parent);
898 self.pending_targets
899 .push_account_target(ProofV2Target::new(target).with_parent(parent));
900 }
901 }
902 Entry::Vacant(entry) => {
903 entry.insert(parent);
904 self.pending_targets
905 .push_account_target(ProofV2Target::new(target).with_parent(parent));
906 }
907 }
908 })?;
909
910 let updates_len_after = account_updates.len();
911 self.account_cache_hits += (updates_len_before - updates_len_after) as u64;
912 self.account_cache_misses += updates_len_after as u64;
913
914 Ok(updates_len_after < updates_len_before)
915 }
916
917 fn spawn_storage_jobs(&self, mut jobs: Vec<StorageTrieJob<S>>) {
922 if jobs.is_empty() {
923 return;
924 }
925
926 let parent_span = debug_span!("spawn_storage_jobs", n = jobs.len());
927 let chunk_len = storage_job_chunk_len(jobs.len());
928 let new_epoch = self.new_epoch;
929 let retain_updates = self.trie.retains_updates();
930 while !jobs.is_empty() {
931 let chunk = jobs.split_off(jobs.len().saturating_sub(chunk_len));
932 let storage_done_tx = self.storage_done_tx.clone();
933 let parent_span = parent_span.clone();
934 rayon::spawn(move || {
935 let _enter = debug_span!(
936 target: "engine::tree::payload_processor::sparse_trie",
937 parent: &parent_span,
938 "storage_jobs",
939 n = chunk.len(),
940 )
941 .entered();
942 for job in chunk {
943 let address = job.address;
944 let message = match panic::catch_unwind(AssertUnwindSafe(|| {
945 job.run(new_epoch, retain_updates)
946 })) {
947 Ok(done) => StorageJobMessage::Done(done),
948 Err(payload) => StorageJobMessage::Panicked { address, payload },
949 };
950 if storage_done_tx.send(message).is_err() {
951 return;
953 }
954 }
955 });
956 }
957 }
958
959 fn on_storage_job_message(&mut self, message: StorageJobMessage<S>) -> SparseTrieResult<()> {
962 match message {
963 StorageJobMessage::Done(done) => self.on_storage_trie_returned(done),
964 StorageJobMessage::Panicked { address, payload } => {
965 if self.storage.remove(&address).is_some() {
966 self.storage_in_flight -= 1;
967 }
968 panic::resume_unwind(payload)
969 }
970 }
971 }
972
973 fn on_storage_trie_returned(&mut self, done: StorageTrieJobDone<S>) -> SparseTrieResult<()> {
976 let StorageTrieJobDone { address, work, output } = done;
977
978 let Entry::Occupied(mut entry) = self.storage.entry(address) else {
979 unreachable!("a returned payload was checked out of its entry")
980 };
981 let StorageTrieState::InFlight(in_flight) = entry.insert(StorageTrieState::Idle(work))
982 else {
983 unreachable!("a checked out address stays in flight until its job returns")
984 };
985 let started = in_flight.started;
986 let StorageTrieState::Idle(work) = entry.into_mut() else { unreachable!("just inserted") };
987 work.take_buffered(*in_flight);
988 self.storage_in_flight -= 1;
989 self.metrics.sparse_trie_storage_job_duration_histogram.record(started.elapsed());
990
991 self.apply_storage_output(address, output)
992 }
993
994 fn drain_returned_storage_tries(&mut self) -> SparseTrieResult<()> {
996 while let Ok(message) = self.storage_done_rx.try_recv() {
997 self.on_storage_job_message(message)?;
998 }
999
1000 Ok(())
1001 }
1002
1003 fn return_storage_tries(&mut self) {
1008 let mut storage = core::mem::take(&mut self.storage);
1009 for (address, state) in &mut storage {
1010 let StorageTrieState::Idle(work) = state else {
1011 unreachable!("completion must wait for every storage job")
1012 };
1013 self.trie.insert_storage_trie(*address, core::mem::take(&mut work.trie));
1014 }
1015 self.storage_to_drop = storage;
1017 }
1018
1019 #[instrument(
1025 level = "trace",
1026 target = "engine::tree::payload_processor::sparse_trie",
1027 skip_all
1028 )]
1029 fn promote_pending_account_updates(&mut self) -> SparseTrieResult<()> {
1030 self.process_account_leaf_updates(false)?;
1031
1032 if self.pending_account_updates.is_empty() {
1033 return Ok(());
1034 }
1035
1036 let new_epoch = self.new_epoch;
1037 loop {
1038 let span = trace_span!("promote_updates", promoted = tracing::field::Empty).entered();
1039 let account_rlp_buf = &mut self.account_rlp_buf;
1041 let mut num_promoted = 0;
1042 self.pending_account_updates.retain(|addr, account| {
1043 let updated_storage = match self.storage.get_mut(addr) {
1044 Some(StorageTrieState::InFlight(_)) => return true,
1046 Some(StorageTrieState::Idle(work)) if work.updated => Some(work),
1047 _ => None,
1048 };
1049
1050 if let Some(work) = updated_storage {
1051 if !work.pending.is_empty() {
1052 return true;
1054 } else if let Some(account) = account.take() {
1055 let storage_root = work.trie.root(new_epoch).expect("updates are drained, storage trie should be revealed by now");
1056 let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
1057 self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
1058 num_promoted += 1;
1059 return false;
1060 }
1061 }
1062
1063 let trie_account = match self.account_updates.get(addr) {
1065 Some(LeafUpdate::Changed(encoded)) => {
1066 Some(encoded).filter(|encoded| !encoded.is_empty())
1067 }
1068 Some(LeafUpdate::Touched) => return true,
1070 None => self.trie.get_account_value(addr),
1071 };
1072
1073 let trie_account = trie_account.map(|value| TrieAccount::decode(&mut &value[..]).expect("invalid account RLP"));
1074
1075 let (account, storage_root) = if let Some(account) = account.take() {
1076 let storage_root = trie_account.map(|account| account.storage_root).unwrap_or(EMPTY_ROOT_HASH);
1081
1082 (account, storage_root)
1083 } else {
1084 let storage_root = match self.storage.get_mut(addr) {
1087 Some(StorageTrieState::Idle(work)) => work.trie.root(new_epoch),
1088 Some(StorageTrieState::InFlight(_)) => unreachable!("returned above"),
1089 None => self.trie.storage_root(addr, new_epoch),
1090 };
1091 (trie_account.map(Into::into), storage_root.expect("account had storage updates that were applied to its trie, storage root must be revealed by now"))
1092 };
1093
1094 let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
1095 self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
1096 num_promoted += 1;
1097
1098 false
1099 });
1100 span.record("promoted", num_promoted);
1101 drop(span);
1102
1103 if num_promoted == 0 || !self.process_account_leaf_updates(false)? {
1108 break
1109 }
1110 }
1111
1112 Ok(())
1113 }
1114
1115 fn dispatch_pending_targets(&mut self) -> Result<(), StateRootTaskError> {
1116 if self.pending_targets.is_empty() {
1117 return Ok(())
1118 }
1119
1120 let _span = trace_span!("dispatch_pending_targets").entered();
1121 let (targets, chunking_length) = self.pending_targets.take();
1122 let mut dispatch_error = None;
1123 dispatch_with_chunking(
1124 targets,
1125 chunking_length,
1126 self.chunk_size,
1127 self.max_targets_for_chunking,
1128 self.proof_worker_handle.has_multiple_idle_account_workers(),
1129 self.proof_worker_handle.has_multiple_idle_storage_workers(),
1130 MultiProofTargetsV2::chunks,
1131 |proof_targets| {
1132 if dispatch_error.is_some() {
1133 return;
1134 }
1135
1136 match self.proof_worker_handle.dispatch(proof_targets) {
1139 Ok(jobs) => self.in_flight_proof_jobs += jobs,
1140 Err(e) => {
1141 error!("failed to dispatch proofs: {e}");
1142 dispatch_error = Some(StateRootTaskError::Other(Box::new(e)));
1143 }
1144 }
1145 },
1146 );
1147
1148 if let Some(error) = dispatch_error {
1149 return Err(error)
1150 }
1151
1152 Ok(())
1153 }
1154
1155 fn has_pending_sparse_trie_updates(&self) -> bool {
1156 !self.account_updates.is_empty() ||
1157 !self.pending_account_updates.is_empty() ||
1158 self.storage.values().any(|state| state.is_pending())
1159 }
1160
1161 fn has_ready_storage_work(&self) -> bool {
1164 self.storage.values().any(|state| match state {
1165 StorageTrieState::Idle(work) => work.has_work(),
1166 StorageTrieState::InFlight(_) => false,
1167 })
1168 }
1169
1170 fn ensure_not_stalled(&self, updates_queued: bool) -> Result<(), StateRootTaskError> {
1178 if self.finished_state_updates &&
1179 !updates_queued &&
1180 self.pending_updates == 0 &&
1181 self.pending_targets.is_empty() &&
1182 self.in_flight_proof_jobs == 0 &&
1183 self.proof_worker_handle.results().is_empty() &&
1184 self.storage_in_flight == 0 &&
1185 !self.has_ready_storage_work() &&
1186 self.has_pending_sparse_trie_updates()
1187 {
1188 const MAX_STALLED_PROOF_TARGETS_TO_LOG: usize = 5;
1189
1190 let mut account_targets = self
1191 .account_updates
1192 .keys()
1193 .map(|target| (*target, self.fetched_account_targets.get(target).copied()))
1194 .collect::<Vec<_>>();
1195 account_targets.sort_unstable();
1196 let account_targets_truncated =
1197 account_targets.len().saturating_sub(MAX_STALLED_PROOF_TARGETS_TO_LOG);
1198 account_targets.truncate(MAX_STALLED_PROOF_TARGETS_TO_LOG);
1199
1200 let mut storage_targets = self
1201 .storage
1202 .iter()
1203 .filter_map(|(address, state)| match state {
1204 StorageTrieState::Idle(work) => Some((address, work)),
1205 StorageTrieState::InFlight(_) => None,
1206 })
1207 .flat_map(|(address, work)| {
1208 work.pending
1209 .keys()
1210 .map(move |target| (*address, *target, work.fetched.get(target).copied()))
1211 })
1212 .collect::<Vec<_>>();
1213 storage_targets.sort_unstable();
1214 let storage_targets_truncated =
1215 storage_targets.len().saturating_sub(MAX_STALLED_PROOF_TARGETS_TO_LOG);
1216 storage_targets.truncate(MAX_STALLED_PROOF_TARGETS_TO_LOG);
1217
1218 error!(
1219 ?account_targets,
1220 account_targets_truncated,
1221 ?storage_targets,
1222 storage_targets_truncated,
1223 "sparse trie task stalled: pending updates remain but no proof targets are queued or in flight"
1224 );
1225
1226 return Err(StateRootTaskError::Stalled)
1227 }
1228
1229 Ok(())
1230 }
1231}
1232
1233enum StorageTrieState<S> {
1235 Idle(Box<StorageTrieWork<S>>),
1237 InFlight(Box<InFlightStorage>),
1240}
1241
1242impl<S: SparseTrie + Default> StorageTrieState<S> {
1243 fn is_pending(&self) -> bool {
1245 match self {
1246 Self::InFlight(_) => true,
1247 Self::Idle(work) => !work.pending.is_empty() || work.has_work(),
1248 }
1249 }
1250}
1251
1252struct StorageTrieWork<S> {
1255 trie: RevealableSparseTrie<S>,
1257 pending: B256Map<LeafUpdate>,
1260 proofs: Vec<ProofTrieNodeV2>,
1262 fetched: B256Map<ProofV2TargetParent>,
1265 dirty: bool,
1272 updated: bool,
1275}
1276
1277impl<S: SparseTrie + Default> StorageTrieWork<S> {
1278 fn new(trie: RevealableSparseTrie<S>) -> Self {
1279 Self {
1280 trie,
1281 pending: Default::default(),
1282 proofs: Vec::new(),
1283 fetched: Default::default(),
1284 dirty: false,
1285 updated: false,
1286 }
1287 }
1288
1289 fn run(&mut self, new_epoch: TrieNodeEpoch, retain_updates: bool) -> StorageWorkOutput {
1292 let Self { trie, pending, proofs, fetched, dirty, .. } = self;
1293 *dirty = false;
1294 let mut output = StorageWorkOutput::default();
1295
1296 if !proofs.is_empty() {
1297 output.result = trie.reveal_v2_proof_nodes(proofs, retain_updates);
1298 proofs.clear();
1299 if output.result.is_err() {
1300 return output
1301 }
1302 }
1303
1304 if !pending.is_empty() {
1305 let updates_len_before = pending.len();
1306 let targets = &mut output.targets;
1307 output.result = trie.update_leaves(pending, |path, parent| match fetched.entry(path) {
1308 Entry::Occupied(mut entry) => {
1309 if parent < *entry.get() {
1310 entry.insert(parent);
1311 targets.push(ProofV2Target::new(path).with_parent(parent));
1312 }
1313 }
1314 Entry::Vacant(entry) => {
1315 entry.insert(parent);
1316 targets.push(ProofV2Target::new(path).with_parent(parent));
1317 }
1318 });
1319 output.cache_hits = (updates_len_before - pending.len()) as u64;
1320 output.cache_misses = pending.len() as u64;
1321 if output.result.is_err() {
1322 return output
1323 }
1324 }
1325
1326 if self.needs_root() {
1327 self.trie.root(new_epoch);
1328 }
1329
1330 output
1331 }
1332
1333 fn queue_updates(&mut self, updates: B256Map<LeafUpdate>) {
1335 if updates.is_empty() {
1336 return
1337 }
1338
1339 self.updated = true;
1340 self.dirty = true;
1341 merge_leaf_updates(&mut self.pending, updates);
1342 }
1343
1344 fn queue_proofs(&mut self, nodes: &mut Vec<ProofTrieNodeV2>) {
1346 if nodes.is_empty() {
1347 return
1348 }
1349
1350 self.dirty = true;
1351 self.proofs.append(nodes);
1352 }
1353
1354 fn take_buffered(&mut self, buffered: InFlightStorage) {
1356 let InFlightStorage { mut proofs, updates, .. } = buffered;
1357 self.queue_proofs(&mut proofs);
1358 self.queue_updates(updates);
1359 }
1360
1361 fn has_work(&self) -> bool {
1363 debug_assert!(self.dirty || !self.needs_root(), "a drained pass must hash its trie");
1364 self.dirty
1365 }
1366
1367 fn needs_root(&self) -> bool {
1372 self.updated &&
1373 self.pending.is_empty() &&
1374 self.trie.is_revealed() &&
1375 !self.trie.is_root_cached()
1376 }
1377
1378 const fn is_target_only(&self) -> bool {
1383 self.proofs.is_empty() && self.trie.is_blind()
1384 }
1385}
1386
1387struct InFlightStorage {
1389 started: Instant,
1391 proofs: Vec<ProofTrieNodeV2>,
1393 updates: B256Map<LeafUpdate>,
1395}
1396
1397impl InFlightStorage {
1398 fn new(started: Instant) -> Self {
1399 Self { started, proofs: Vec::new(), updates: Default::default() }
1400 }
1401}
1402
1403struct StorageTrieJob<S> {
1405 address: B256,
1407 work: Box<StorageTrieWork<S>>,
1409}
1410
1411impl<S: SparseTrie + Default> StorageTrieJob<S> {
1412 fn run(mut self, new_epoch: TrieNodeEpoch, retain_updates: bool) -> StorageTrieJobDone<S> {
1413 let output = self.work.run(new_epoch, retain_updates);
1414 StorageTrieJobDone { address: self.address, work: self.work, output }
1415 }
1416}
1417
1418enum StorageJobMessage<S> {
1420 Done(StorageTrieJobDone<S>),
1422 Panicked {
1426 address: B256,
1428 payload: Box<dyn Any + Send>,
1430 },
1431}
1432
1433struct StorageTrieJobDone<S> {
1435 address: B256,
1437 work: Box<StorageTrieWork<S>>,
1439 output: StorageWorkOutput,
1441}
1442
1443struct StorageWorkOutput {
1445 targets: Vec<ProofV2Target>,
1447 cache_hits: u64,
1449 cache_misses: u64,
1451 result: SparseTrieResult<()>,
1453}
1454
1455impl Default for StorageWorkOutput {
1456 fn default() -> Self {
1457 Self { targets: Vec::new(), cache_hits: 0, cache_misses: 0, result: Ok(()) }
1458 }
1459}
1460
1461fn merge_leaf_updates(queued: &mut B256Map<LeafUpdate>, updates: B256Map<LeafUpdate>) {
1463 if queued.is_empty() {
1464 *queued = updates;
1466 return
1467 }
1468
1469 for (slot, update) in updates {
1470 match queued.entry(slot) {
1471 Entry::Occupied(mut entry) => {
1472 if update.is_changed() {
1473 entry.insert(update);
1474 }
1475 }
1476 Entry::Vacant(entry) => {
1477 entry.insert(update);
1478 }
1479 }
1480 }
1481}
1482
1483fn storage_job_chunk_len(tries: usize) -> usize {
1490 const MAX_CHUNK_LEN: usize = 32;
1492
1493 tries.div_ceil(rayon::current_num_threads().max(1) * 4).clamp(1, MAX_CHUNK_LEN)
1494}
1495
1496#[derive(Metrics, Clone)]
1498#[metrics(scope = "tree.root")]
1499pub(super) struct SparseTrieTaskMetrics {
1500 pub(super) sparse_trie_reveal_multiproof_duration_histogram: Histogram,
1502 pub(super) sparse_trie_proof_coalesce_duration_histogram: Histogram,
1504 pub(super) sparse_trie_channel_wait_duration_histogram: Histogram,
1506 pub(super) sparse_trie_process_updates_duration_histogram: Histogram,
1508 pub(super) sparse_trie_storage_job_duration_histogram: Histogram,
1510 pub(super) sparse_trie_final_update_duration_histogram: Histogram,
1512 pub(super) sparse_trie_total_duration_histogram: Histogram,
1514 pub(super) into_trie_for_reuse_duration_histogram: Histogram,
1516 pub(super) sparse_trie_prune_duration_histogram: Histogram,
1518 pub(super) sparse_trie_cache_wait_duration_histogram: Histogram,
1520 pub(super) sparse_trie_idle_time_seconds: Histogram,
1523 pub(super) hashing_task_idle_time_seconds: Histogram,
1526
1527 pub(super) sparse_trie_account_cache_hits: Histogram,
1529 pub(super) sparse_trie_account_cache_misses: Histogram,
1531 pub(super) sparse_trie_storage_cache_hits: Histogram,
1533 pub(super) sparse_trie_storage_cache_misses: Histogram,
1535
1536 pub(super) sparse_trie_retained_storage_tries: Gauge,
1538}
1539
1540const DEFAULT_MAX_TARGETS_FOR_CHUNKING: usize = 300;
1543
1544const INITIAL_UPDATE_BATCH_SIZE: usize = 64;
1546
1547const INLINE_STORAGE_WORK_UNITS: usize = 16;
1551
1552#[expect(clippy::too_many_arguments)]
1555fn dispatch_with_chunking<T, I>(
1556 items: T,
1557 chunking_len: usize,
1558 chunk_size: usize,
1559 max_targets_for_chunking: usize,
1560 has_multiple_idle_account_workers: bool,
1561 has_multiple_idle_storage_workers: bool,
1562 chunker: impl FnOnce(T, usize) -> I,
1563 mut dispatch: impl FnMut(T),
1564) where
1565 I: IntoIterator<Item = T>,
1566{
1567 let has_full_chunks = chunking_len >= chunk_size.saturating_mul(2);
1568 let should_chunk = chunking_len > max_targets_for_chunking ||
1569 (has_full_chunks &&
1570 (has_multiple_idle_account_workers || has_multiple_idle_storage_workers));
1571
1572 if should_chunk && chunking_len > chunk_size {
1573 for chunk in chunker(items, chunk_size) {
1574 dispatch(chunk);
1575 }
1576 return;
1577 }
1578
1579 dispatch(items);
1580}
1581
1582fn encode_account_leaf_value(
1589 account: Option<Account>,
1590 storage_root: B256,
1591 account_rlp_buf: &mut Vec<u8>,
1592) -> Vec<u8> {
1593 if account.as_ref().is_none_or(|account| account.is_empty()) && storage_root == EMPTY_ROOT_HASH
1594 {
1595 return Vec::new();
1596 }
1597
1598 account_rlp_buf.clear();
1599 account.unwrap_or_default().into_trie_account(storage_root).encode(account_rlp_buf);
1600 account_rlp_buf.clone()
1601}
1602
1603#[derive(Default)]
1605struct PendingTargets {
1606 targets: MultiProofTargetsV2,
1608 len: usize,
1610}
1611
1612impl PendingTargets {
1613 const fn len(&self) -> usize {
1615 self.len
1616 }
1617
1618 const fn is_empty(&self) -> bool {
1620 self.len == 0
1621 }
1622
1623 fn take(&mut self) -> (MultiProofTargetsV2, usize) {
1625 (std::mem::take(&mut self.targets), std::mem::take(&mut self.len))
1626 }
1627
1628 fn push_account_target(&mut self, target: ProofV2Target) {
1630 self.targets.account_targets.push(target);
1631 self.len += 1;
1632 }
1633
1634 fn extend_storage_targets(&mut self, address: &B256, targets: Vec<ProofV2Target>) {
1636 self.len += targets.len();
1637 self.targets.storage_targets.entry(*address).or_default().extend(targets);
1638 }
1639}
1640
1641enum SparseTrieTaskMessage {
1643 HashedState(HashedPostState),
1645 PrefetchProofs(MultiProofTargetsV2),
1647 FinishedStateUpdates,
1649}
1650
1651#[cfg(test)]
1652mod tests {
1653 use super::*;
1654 use alloy_primitives::{B256, U256};
1655 use reth_db_common::init::init_genesis;
1656 use reth_provider::test_utils::create_test_provider_factory;
1657 use reth_storage_overlay::{OverlayManager, OverlayStateProviderFactory};
1658 use reth_trie_common::{
1659 BranchNodeV2, ExtensionNode, LeafNode, Nibbles, RlpNode, TrieMask, TrieNodeV2,
1660 };
1661 use reth_trie_parallel::proof_task::{ProofTaskCtx, ProofWorkerCounts};
1662 use reth_trie_sparse::ArenaParallelSparseTrie;
1663
1664 fn drain_sparse_trie_tasks(runtime: &Runtime) {
1665 for task_name in ["trie-hashing", "storage-workers", "account-workers"] {
1666 runtime.spawn_blocking_named(task_name, || {}).get();
1667 }
1668 }
1669
1670 fn test_task(
1671 runtime: &Runtime,
1672 trie: SparseStateTrie,
1673 ) -> (SparseTrieCacheTask, CrossbeamSender<StateRootMessage>, CrossbeamSender<()>) {
1674 let provider_factory = create_test_provider_factory();
1675 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
1676 let state_provider_factory = OverlayStateProviderFactory::new(
1677 provider_factory,
1678 OverlayManager::<reth_chain_state::EthPrimitives>::default()
1679 .overlay_builder(anchor_hash),
1680 );
1681 let proof_worker_handle = ProofWorkerHandle::new(
1682 runtime,
1683 ProofTaskCtx::new(state_provider_factory),
1684 ProofWorkerCounts::full(runtime),
1685 Default::default(),
1686 );
1687 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1688 let (cancel_guard, cancel_rx) = crossbeam_channel::bounded(0);
1689 let task = SparseTrieCacheTask::new_with_trie(
1690 runtime,
1691 updates_rx,
1692 cancel_rx,
1693 std::sync::mpsc::channel().0,
1694 proof_worker_handle,
1695 SparseTrieTaskMetrics::default(),
1696 trie,
1697 EMPTY_ROOT_HASH,
1698 TrieNodeEpoch::new(1),
1699 1,
1700 );
1701 (task, updates_tx, cancel_guard)
1702 }
1703
1704 fn receive_storage_job(
1705 task: &SparseTrieCacheTask,
1706 ) -> StorageJobMessage<ArenaParallelSparseTrie> {
1707 task.storage_done_rx
1708 .recv_timeout(std::time::Duration::from_secs(5))
1709 .expect("storage job did not return")
1710 }
1711
1712 fn check_out_storage(
1714 task: &mut SparseTrieCacheTask,
1715 address: B256,
1716 ) -> Box<StorageTrieWork<ArenaParallelSparseTrie>> {
1717 let state = task.storage.get_mut(&address).expect("address must have an entry");
1718 let StorageTrieState::Idle(work) = core::mem::replace(
1719 state,
1720 StorageTrieState::InFlight(Box::new(InFlightStorage::new(Instant::now()))),
1721 ) else {
1722 panic!("trie must be idle")
1723 };
1724 task.storage_in_flight += 1;
1725 work
1726 }
1727
1728 fn return_storage(
1730 task: &mut SparseTrieCacheTask,
1731 address: B256,
1732 work: Box<StorageTrieWork<ArenaParallelSparseTrie>>,
1733 ) {
1734 task.on_storage_trie_returned(StorageTrieJobDone {
1735 address,
1736 work,
1737 output: StorageWorkOutput::default(),
1738 })
1739 .unwrap();
1740 }
1741
1742 fn storage_slot_value(
1743 task: &SparseTrieCacheTask,
1744 address: &B256,
1745 slot: &B256,
1746 ) -> Option<Vec<u8>> {
1747 let StorageTrieState::Idle(work) = task.storage.get(address)? else { return None };
1748 work.trie.as_revealed_ref()?.get_leaf_value(&Nibbles::unpack(slot)).cloned()
1749 }
1750
1751 fn storage_root_of(task: &mut SparseTrieCacheTask, address: B256) -> B256 {
1752 let StorageTrieState::Idle(work) = task.storage.get_mut(&address).expect("entry") else {
1753 panic!("payload is out with a job")
1754 };
1755 work.trie.root(TrieNodeEpoch::new(1)).expect("storage trie must be revealed")
1756 }
1757
1758 #[test]
1759 fn test_encode_account_leaf_value_deletion_and_empty_root_is_empty() {
1760 let mut account_rlp_buf = vec![0xAB];
1761 let encoded = encode_account_leaf_value(None, EMPTY_ROOT_HASH, &mut account_rlp_buf);
1762
1763 assert!(encoded.is_empty());
1764 assert_eq!(account_rlp_buf, vec![0xAB]);
1766 }
1767
1768 #[test]
1769 fn test_encode_account_leaf_value_empty_account_and_empty_root_is_empty() {
1770 let mut account_rlp_buf = vec![0xAB];
1771 let encoded = encode_account_leaf_value(
1772 Some(Account::default()),
1773 EMPTY_ROOT_HASH,
1774 &mut account_rlp_buf,
1775 );
1776
1777 assert!(encoded.is_empty());
1778 assert_eq!(account_rlp_buf, vec![0xAB]);
1780 }
1781
1782 #[test]
1783 #[allow(clippy::needless_update)]
1784 fn test_encode_account_leaf_value_non_empty_account_is_rlp() {
1785 let storage_root = B256::repeat_byte(0x99);
1786 let account = Some(Account {
1787 nonce: 7,
1788 balance: U256::from(42),
1789 bytecode_hash: Some(B256::repeat_byte(0xAA)),
1790 ..Default::default()
1791 });
1792 let mut account_rlp_buf = vec![0x00, 0x01];
1793
1794 let encoded = encode_account_leaf_value(account, storage_root, &mut account_rlp_buf);
1795 let decoded = TrieAccount::decode(&mut &encoded[..]).expect("valid account RLP");
1796
1797 assert_eq!(decoded.nonce, 7);
1798 assert_eq!(decoded.balance, U256::from(42));
1799 assert_eq!(decoded.storage_root, storage_root);
1800 assert_eq!(account_rlp_buf, encoded);
1801 }
1802
1803 #[test]
1804 fn checked_out_storage_trie_holds_back_its_updates_until_it_returns() {
1805 let runtime = Runtime::test();
1806 let trie = SparseStateTrie::default()
1807 .with_accounts_trie(RevealableSparseTrie::<ArenaParallelSparseTrie>::revealed_empty())
1808 .with_default_storage_trie(RevealableSparseTrie::blind_from(
1809 ArenaParallelSparseTrie::default(),
1810 ))
1811 .with_updates(true);
1812 let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, trie);
1813
1814 let address = B256::repeat_byte(0x11);
1815 let revealed_slot = B256::repeat_byte(0x22);
1816 let late_slot = B256::repeat_byte(0x33);
1817 let revealed_value = alloy_rlp::encode_fixed_size(&U256::from(7)).to_vec();
1818 let late_value = alloy_rlp::encode_fixed_size(&U256::from(9)).to_vec();
1819 let later_value = alloy_rlp::encode_fixed_size(&U256::from(11)).to_vec();
1820
1821 let mut state = HashedPostState::default();
1822 state.accounts.insert(address, Some(Account { nonce: 1, ..Default::default() }));
1823 state.storages.entry(address).or_default().storage.insert(revealed_slot, U256::from(7));
1824 task.on_hashed_state_update(state);
1825 task.pending_updates = 1;
1826 task.apply_new_updates().unwrap();
1827
1828 let work = check_out_storage(&mut task, address);
1830 task.finished_state_updates = true;
1831
1832 assert!(task.has_pending_sparse_trie_updates());
1833 assert!(
1834 task.ensure_not_stalled(false).is_ok(),
1835 "a checked out payload can still deliver progress"
1836 );
1837
1838 let leaf = ProofTrieNodeV2 {
1841 path: Nibbles::default(),
1842 node: TrieNodeV2::Leaf(LeafNode::new(
1843 Nibbles::unpack(revealed_slot),
1844 revealed_value.clone(),
1845 )),
1846 masks: None,
1847 };
1848 task.on_proofs(DecodedMultiProofV2 {
1849 storage_proofs: B256Map::from_iter([(address, vec![leaf])]),
1850 ..Default::default()
1851 })
1852 .unwrap();
1853 assert!(!task.trie.storage_tries_mut().contains_key(&address));
1854 let StorageTrieState::InFlight(in_flight) = &task.storage[&address] else {
1855 panic!("payload is out with a job")
1856 };
1857 assert_eq!(in_flight.proofs.len(), 1);
1858
1859 let mut state = HashedPostState::default();
1861 state.storages.entry(address).or_default().storage.insert(late_slot, U256::from(9));
1862 task.on_hashed_state_update(state);
1863 task.pending_updates = 1;
1864 task.apply_new_updates().unwrap();
1865 let StorageTrieState::InFlight(in_flight) = &task.storage[&address] else {
1866 panic!("payload is out with a job")
1867 };
1868 assert!(in_flight.updates.contains_key(&late_slot));
1869
1870 return_storage(&mut task, address, work);
1871 assert_eq!(task.storage_in_flight, 0);
1872
1873 assert_eq!(task.pending_updates, 0);
1875 task.process_new_updates().unwrap();
1876 assert_eq!(storage_slot_value(&task, &address, &revealed_slot), Some(revealed_value));
1877 assert_eq!(storage_slot_value(&task, &address, &late_slot), Some(late_value));
1878
1879 let StorageTrieState::Idle(work) = &task.storage[&address] else {
1880 panic!("payload is back")
1881 };
1882 assert!(work.pending.is_empty());
1883 assert!(work.trie.is_root_cached(), "a drained payload hashes its trie");
1884 let root_before = storage_root_of(&mut task, address);
1885
1886 let work = check_out_storage(&mut task, address);
1888 let mut state = HashedPostState::default();
1889 state.storages.entry(address).or_default().storage.insert(late_slot, U256::from(11));
1890 task.on_hashed_state_update(state);
1891 task.pending_updates = 1;
1892 task.apply_new_updates().unwrap();
1893 return_storage(&mut task, address, work);
1894
1895 let StorageTrieState::Idle(work) = &task.storage[&address] else {
1896 panic!("payload is back")
1897 };
1898 assert!(work.has_work(), "a buffered update makes the payload ready again");
1899
1900 task.process_new_updates().unwrap();
1901 assert_eq!(storage_slot_value(&task, &address, &late_slot), Some(later_value));
1902 assert_ne!(storage_root_of(&mut task, address), root_before);
1903
1904 drop(updates_tx);
1905 drop(task);
1906 drain_sparse_trie_tasks(&runtime);
1907 }
1908
1909 #[test]
1910 #[allow(clippy::clone_on_copy)]
1911 fn in_flight_storage_updates_keep_latest_values_and_deletions() {
1912 let runtime = Runtime::test();
1913 let trie = SparseStateTrie::default()
1914 .with_accounts_trie(RevealableSparseTrie::revealed_empty())
1915 .with_updates(true);
1916 let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, trie);
1917 let address = B256::repeat_byte(0x11);
1918 let removed_slot = B256::repeat_byte(0x20);
1919 let changed_slot = B256::repeat_byte(0x30);
1920 let new_slot = B256::repeat_byte(0x31);
1921 let mut sibling_slot = changed_slot;
1923 sibling_slot[1] = 0x40;
1924 let account = Account { nonce: 1, ..Default::default() };
1925 let parent_leaf = ProofTrieNodeV2 {
1926 path: Nibbles::default(),
1927 node: TrieNodeV2::Leaf(LeafNode::new(
1928 Nibbles::unpack(removed_slot),
1929 alloy_rlp::encode(U256::from(7)),
1930 )),
1931 masks: None,
1932 };
1933
1934 let mut state = HashedPostState::default();
1935 state.accounts.insert(address, Some(account.clone()));
1936 state.storages.entry(address).or_default().storage.extend([
1937 (removed_slot, U256::from(8)),
1938 (changed_slot, U256::from(9)),
1939 (new_slot, U256::ONE),
1940 (sibling_slot, U256::from(2)),
1941 ]);
1942 task.on_hashed_state_update(state);
1943 task.pending_updates = 1;
1944 task.apply_new_updates().unwrap();
1945
1946 let mut work = check_out_storage(&mut task, address);
1947 let output = work.run(task.new_epoch, true);
1948 assert_eq!(work.pending.len(), 4, "the blind pass keeps its original updates");
1949 assert_eq!(output.targets.len(), 4);
1950
1951 let mut state = HashedPostState::default();
1953 state
1954 .storages
1955 .entry(address)
1956 .or_default()
1957 .storage
1958 .extend([(removed_slot, U256::ZERO), (changed_slot, U256::from(11))]);
1959 task.on_hashed_state_update(state);
1960 task.pending_updates = 1;
1961 task.apply_new_updates().unwrap();
1962 task.on_prewarm_targets(MultiProofTargetsV2 {
1963 storage_targets: B256Map::from_iter([(
1964 address,
1965 vec![ProofV2Target::new(removed_slot), ProofV2Target::new(changed_slot)],
1966 )]),
1967 ..Default::default()
1968 });
1969 task.pending_updates = 1;
1970 task.apply_new_updates().unwrap();
1971
1972 task.on_storage_trie_returned(StorageTrieJobDone { address, work, output }).unwrap();
1973 let (targets, _) = task.pending_targets.take();
1975 assert_eq!(targets.storage_targets[&address].len(), 4);
1976 task.on_proofs(DecodedMultiProofV2 {
1977 storage_proofs: B256Map::from_iter([(address, vec![parent_leaf.clone()])]),
1978 ..Default::default()
1979 })
1980 .unwrap();
1981 updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
1982 let outcome = task.run().unwrap();
1983
1984 let expected_root = reth_trie_common::root::storage_root_unsorted([
1985 (changed_slot, U256::from(11)),
1986 (new_slot, U256::ONE),
1987 (sibling_slot, U256::from(2)),
1988 ]);
1989 assert_eq!(
1990 outcome.state_root,
1991 reth_trie_common::root::state_root_unsorted([(
1992 address,
1993 account.into_trie_account(expected_root),
1994 )])
1995 );
1996
1997 let mut serial = RevealableSparseTrie::<ArenaParallelSparseTrie>::blind();
1998 serial.reveal_v2_proof_nodes(&mut [parent_leaf], true).unwrap();
1999 let mut final_updates = B256Map::from_iter([
2000 (removed_slot, LeafUpdate::Changed(Vec::new())),
2001 (changed_slot, LeafUpdate::Changed(alloy_rlp::encode(U256::from(11)))),
2002 (new_slot, LeafUpdate::Changed(alloy_rlp::encode(U256::ONE))),
2003 (sibling_slot, LeafUpdate::Changed(alloy_rlp::encode(U256::from(2)))),
2004 ]);
2005 serial
2006 .update_leaves(&mut final_updates, |_, _| panic!("parent is fully revealed"))
2007 .unwrap();
2008 let (serial_root, serial_updates) = serial.root_with_updates(task.new_epoch).unwrap();
2009 assert_eq!(serial_root, expected_root);
2010 assert!(!serial_updates.updated_nodes.is_empty());
2011 let updates = &outcome.trie_updates.storage_tries[&address];
2012 assert_eq!(updates.storage_nodes, serial_updates.updated_nodes);
2013 assert_eq!(updates.removed_nodes, serial_updates.removed_nodes);
2014
2015 let (mut reused, _) = task.into_trie_for_reuse();
2016 assert_eq!(reused.storage_root(&address, TrieNodeEpoch::new(2)), Some(expected_root));
2017 assert!(reused.take_trie_updates().unwrap().is_empty());
2018 drop(updates_tx);
2019 drain_sparse_trie_tasks(&runtime);
2020 }
2021
2022 #[test]
2023 fn unrelated_storage_proofs_do_not_retry_blocked_leaves() {
2024 let runtime = Runtime::test();
2025 let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, SparseStateTrie::default());
2026 let address = B256::repeat_byte(0x11);
2027 let slot = B256::repeat_byte(0x22);
2028 let StorageTrieState::Idle(work) = task.storage_trie_state_mut(address) else {
2029 unreachable!()
2030 };
2031 work.queue_updates(B256Map::from_iter([(slot, LeafUpdate::Changed(vec![1]))]));
2032 task.run_ready_storage_work().unwrap();
2033 assert_eq!(task.storage_cache_misses, 1);
2034
2035 task.on_proofs(DecodedMultiProofV2 {
2036 storage_proofs: B256Map::from_iter([(
2037 B256::repeat_byte(0x33),
2038 vec![ProofTrieNodeV2::empty()],
2039 )]),
2040 ..Default::default()
2041 })
2042 .unwrap();
2043 assert_eq!(task.storage_cache_misses, 1, "unrelated reveals cannot unblock this trie");
2044 assert_eq!(storage_slot_value(&task, &address, &slot), None);
2045
2046 task.on_proofs(DecodedMultiProofV2 {
2047 storage_proofs: B256Map::from_iter([(address, vec![ProofTrieNodeV2::empty()])]),
2048 ..Default::default()
2049 })
2050 .unwrap();
2051 assert_eq!(storage_slot_value(&task, &address, &slot), Some(vec![1]));
2052 assert_eq!(task.storage_cache_hits, 1);
2053 assert!(!task.has_ready_storage_work());
2054
2055 drop(updates_tx);
2056 drop(task);
2057 drain_sparse_trie_tasks(&runtime);
2058 }
2059
2060 #[test]
2061 fn canceled_task_disconnects_checked_out_storage_jobs() {
2062 let runtime = Runtime::test();
2063 let trie = SparseStateTrie::default()
2064 .with_default_storage_trie(RevealableSparseTrie::revealed_empty());
2065 let (mut task, updates_tx, cancel_guard) = test_task(&runtime, trie);
2066 let idle_address = B256::repeat_byte(0x11);
2067 let address = B256::repeat_byte(0x22);
2068 task.storage_trie_state_mut(idle_address);
2069 task.storage_trie_state_mut(address);
2070 let work = check_out_storage(&mut task, address);
2071 let done_tx = task.storage_done_tx.clone();
2072 let epoch = task.new_epoch;
2073
2074 drop(cancel_guard);
2075 assert!(matches!(task.run(), Err(StateRootTaskError::Canceled)));
2076 let (trie, _) = task.into_cleared_trie();
2077 assert_eq!(trie.retained_storage_tries_count(), 1, "only the idle trie can be reused");
2078
2079 let done = StorageTrieJob { address, work }.run(epoch, false);
2081 assert!(done_tx.send(StorageJobMessage::Done(done)).is_err());
2082 drop(updates_tx);
2083 drain_sparse_trie_tasks(&runtime);
2084 }
2085
2086 #[test]
2087 fn storage_job_panics_are_resumed_on_the_task() {
2088 let runtime = Runtime::test();
2089 let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, SparseStateTrie::default());
2090 let address = B256::repeat_byte(0x11);
2091 let StorageTrieState::Idle(work) = task.storage_trie_state_mut(address) else {
2092 unreachable!()
2093 };
2094 work.queue_proofs(&mut vec![ProofTrieNodeV2 {
2095 path: Nibbles::default(),
2096 node: TrieNodeV2::Branch(BranchNodeV2 {
2097 state_mask: TrieMask::from(1),
2098 ..Default::default()
2099 }),
2100 masks: None,
2101 }]);
2102 let work = check_out_storage(&mut task, address);
2103 task.spawn_storage_jobs(vec![StorageTrieJob { address, work }]);
2104
2105 let message = receive_storage_job(&task);
2106 assert!(
2107 matches!(&message, StorageJobMessage::Panicked { address: returned, .. } if *returned == address)
2108 );
2109 let panic = panic::catch_unwind(AssertUnwindSafe(|| task.on_storage_job_message(message)));
2110 assert!(panic.is_err());
2111 assert_eq!(task.storage_in_flight, 0);
2112 assert!(!task.storage.contains_key(&address));
2113
2114 drop(updates_tx);
2115 drop(task);
2116 drain_sparse_trie_tasks(&runtime);
2117 }
2118
2119 #[test]
2120 fn unsupported_storage_root_errors_are_returned_to_the_task() {
2121 let runtime = Runtime::test();
2122 let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, SparseStateTrie::default());
2123 let address = B256::repeat_byte(0x11);
2124 let StorageTrieState::Idle(work) = task.storage_trie_state_mut(address) else {
2125 unreachable!()
2126 };
2127 work.queue_proofs(&mut vec![ProofTrieNodeV2 {
2128 path: Nibbles::default(),
2129 node: TrieNodeV2::Extension(ExtensionNode::new(
2130 Nibbles::from_nibbles([1]),
2131 RlpNode::word_rlp(&B256::ZERO),
2132 )),
2133 masks: None,
2134 }]);
2135 let work = check_out_storage(&mut task, address);
2136 task.spawn_storage_jobs(vec![StorageTrieJob { address, work }]);
2137
2138 let message = receive_storage_job(&task);
2139 assert!(matches!(&message, StorageJobMessage::Done(done) if done.address == address));
2140 let error = task.on_storage_job_message(message).unwrap_err();
2141 assert!(
2142 matches!(error.kind(), SparseTrieErrorKind::Reveal { path, .. } if path.is_empty())
2143 );
2144 assert_eq!(task.storage_in_flight, 0);
2145 assert!(matches!(task.storage.get(&address), Some(StorageTrieState::Idle(_))));
2146
2147 drop(updates_tx);
2148 drop(task);
2149 drain_sparse_trie_tasks(&runtime);
2150 }
2151
2152 #[test]
2153 fn large_storage_batches_run_off_thread() {
2154 let runtime = Runtime::test();
2155 let trie = SparseStateTrie::default()
2156 .with_accounts_trie(RevealableSparseTrie::<ArenaParallelSparseTrie>::revealed_empty())
2157 .with_default_storage_trie(RevealableSparseTrie::blind_from(
2158 ArenaParallelSparseTrie::default(),
2159 ))
2160 .with_updates(true);
2161 let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, trie);
2162
2163 let leaves = (0..=INLINE_STORAGE_WORK_UNITS as u8)
2165 .map(|index| {
2166 let address = B256::repeat_byte(0x10 + index);
2167 let slot = B256::repeat_byte(0x80 + index);
2168 let value = alloy_rlp::encode_fixed_size(&U256::from(index + 1)).to_vec();
2169 (address, slot, value)
2170 })
2171 .collect::<Vec<_>>();
2172 let storage_proofs = leaves
2173 .iter()
2174 .map(|(address, slot, value)| {
2175 let leaf = ProofTrieNodeV2 {
2176 path: Nibbles::default(),
2177 node: TrieNodeV2::Leaf(LeafNode::new(Nibbles::unpack(slot), value.clone())),
2178 masks: None,
2179 };
2180 (*address, vec![leaf])
2181 })
2182 .collect();
2183
2184 task.on_proofs(DecodedMultiProofV2 { account_proofs: Vec::new(), storage_proofs }).unwrap();
2185
2186 assert_eq!(task.storage_in_flight, leaves.len());
2187 assert!(task.has_pending_sparse_trie_updates(), "completion must wait for the reveals");
2188
2189 while task.storage_in_flight > 0 {
2190 let message = receive_storage_job(&task);
2191 task.on_storage_job_message(message).unwrap();
2192 }
2193 for (address, slot, value) in &leaves {
2194 assert_eq!(storage_slot_value(&task, address, slot).as_ref(), Some(value));
2195 }
2196
2197 drop(updates_tx);
2198 drop(task);
2199 drain_sparse_trie_tasks(&runtime);
2200 }
2201
2202 #[test]
2203 #[allow(clippy::clone_on_copy)]
2204 fn run_waits_for_storage_tries_hashed_off_thread() {
2205 let runtime = Runtime::test();
2206 let default_trie = RevealableSparseTrie::<ArenaParallelSparseTrie>::revealed_empty();
2207 let trie = SparseStateTrie::default()
2208 .with_accounts_trie(default_trie.clone())
2209 .with_default_storage_trie(default_trie)
2210 .with_updates(true);
2211 let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, trie);
2212
2213 let accounts = (0..8u8)
2214 .map(|index| {
2215 let address = B256::repeat_byte(0x10 + index);
2216 let account = Account {
2217 nonce: u64::from(index) + 1,
2218 balance: U256::from(index),
2219 ..Default::default()
2220 };
2221 let storage = (0..4u8)
2222 .map(|slot| {
2223 (B256::repeat_byte(0x40 + index * 4 + slot), U256::from(slot) + U256::ONE)
2224 })
2225 .collect::<Vec<_>>();
2226 (address, account, storage)
2227 })
2228 .collect::<Vec<_>>();
2229
2230 let mut state = HashedPostState::default();
2231 for (address, account, storage) in &accounts {
2232 state.accounts.insert(*address, Some(account.clone()));
2233 state.storages.entry(*address).or_default().storage.extend(storage.iter().copied());
2234 }
2235 updates_tx.send(StateRootMessage::HashedStateUpdate(state)).unwrap();
2236 updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
2237
2238 let outcome = task.run().expect("state root computation should succeed");
2239
2240 let expected = accounts.iter().map(|(address, account, storage)| {
2241 let storage_root =
2242 reth_trie_common::root::storage_root_unsorted(storage.iter().copied());
2243 (*address, account.clone().into_trie_account(storage_root))
2244 });
2245 assert_eq!(outcome.state_root, reth_trie_common::root::state_root_unsorted(expected));
2246 assert_eq!(task.storage_in_flight, 0);
2247 assert!(task.storage.is_empty(), "every storage trie is back in the state trie");
2248
2249 drop(updates_tx);
2250 drop(task);
2251 drain_sparse_trie_tasks(&runtime);
2252 }
2253
2254 #[test]
2255 fn first_leaf_batch_starts_proofs_before_input_queue_drains() {
2256 let runtime = reth_tasks::Runtime::test();
2257 let provider_factory = create_test_provider_factory();
2258 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
2259 let state_provider_factory = OverlayStateProviderFactory::new(
2260 provider_factory,
2261 OverlayManager::<reth_chain_state::EthPrimitives>::default()
2262 .overlay_builder(anchor_hash),
2263 );
2264 let proof_worker_handle = ProofWorkerHandle::new(
2265 &runtime,
2266 ProofTaskCtx::new(state_provider_factory),
2267 ProofWorkerCounts::full(&runtime),
2268 Default::default(),
2269 );
2270
2271 let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
2272 let trie = SparseStateTrie::default()
2273 .with_accounts_trie(default_trie.clone())
2274 .with_default_storage_trie(default_trie)
2275 .with_updates(true);
2276
2277 let parent_state_root = B256::repeat_byte(0x55);
2278 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
2279 let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
2280 let mut task = SparseTrieCacheTask::new_with_trie(
2281 &runtime,
2282 updates_rx,
2283 cancel_rx,
2284 std::sync::mpsc::channel().0,
2285 proof_worker_handle,
2286 SparseTrieTaskMetrics::default(),
2287 trie,
2288 parent_state_root,
2289 TrieNodeEpoch::UNMODIFIED,
2290 1,
2291 );
2292
2293 updates_tx.send(StateRootMessage::PrefetchProofs(Default::default())).unwrap();
2295 let deadline = std::time::Instant::now();
2296 while task.updates.is_empty() {
2297 assert!(deadline.elapsed() < std::time::Duration::from_secs(1));
2298 std::thread::yield_now();
2299 }
2300 for index in 0..INITIAL_UPDATE_BATCH_SIZE {
2301 let mut state = HashedPostState::default();
2302 state.accounts.insert(
2303 B256::repeat_byte(index as u8),
2304 Some(Account { nonce: 1, ..Default::default() }),
2305 );
2306 task.on_hashed_state_update(state);
2307 task.pending_updates += 1;
2308 assert!(!task.make_progress().unwrap());
2309 if index + 1 < INITIAL_UPDATE_BATCH_SIZE {
2310 assert_eq!(task.in_flight_proof_jobs, 0);
2311 }
2312 }
2313 assert!(task.in_flight_proof_jobs > 0, "proof work must start before the queue drains");
2314 assert_eq!(task.pending_updates, 0);
2315
2316 for index in INITIAL_UPDATE_BATCH_SIZE..INITIAL_UPDATE_BATCH_SIZE * 2 {
2319 let mut state = HashedPostState::default();
2320 state.accounts.insert(
2321 B256::repeat_byte(index as u8),
2322 Some(Account { nonce: 1, ..Default::default() }),
2323 );
2324 task.on_hashed_state_update(state);
2325 task.pending_updates += 1;
2326 assert!(!task.make_progress().unwrap());
2327 }
2328 assert_eq!(task.pending_updates, INITIAL_UPDATE_BATCH_SIZE);
2329 assert_eq!(task.new_account_updates.len(), INITIAL_UPDATE_BATCH_SIZE);
2330 drop(updates_tx);
2331 drop(task);
2332 drain_sparse_trie_tasks(&runtime);
2333 }
2334
2335 #[test]
2336 fn run_returns_parent_root_without_revealing_blind_trie_when_no_state_updates() {
2337 let runtime = reth_tasks::Runtime::test();
2338 let provider_factory = create_test_provider_factory();
2339 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
2340 let state_provider_factory = OverlayStateProviderFactory::new(
2341 provider_factory,
2342 OverlayManager::<reth_chain_state::EthPrimitives>::default()
2343 .overlay_builder(anchor_hash),
2344 );
2345 let proof_worker_handle = ProofWorkerHandle::new(
2346 &runtime,
2347 ProofTaskCtx::new(state_provider_factory),
2348 ProofWorkerCounts::full(&runtime),
2349 Default::default(),
2350 );
2351
2352 let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
2353 let trie = SparseStateTrie::default()
2354 .with_accounts_trie(default_trie.clone())
2355 .with_default_storage_trie(default_trie)
2356 .with_updates(true);
2357
2358 let parent_state_root = B256::repeat_byte(0x55);
2359 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
2360 let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
2361 let mut task = SparseTrieCacheTask::new_with_trie(
2362 &runtime,
2363 updates_rx,
2364 cancel_rx,
2365 std::sync::mpsc::channel().0,
2366 proof_worker_handle,
2367 SparseTrieTaskMetrics::default(),
2368 trie,
2369 parent_state_root,
2370 TrieNodeEpoch::UNMODIFIED,
2371 1,
2372 );
2373
2374 updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
2375 drop(updates_tx);
2376
2377 let outcome = task.run().expect("state root computation should succeed");
2378
2379 assert_eq!(outcome.state_root, parent_state_root);
2380 assert!(outcome.trie_updates.is_empty());
2381 assert!(task.trie.state_trie_ref().is_none(), "blind trie should not be revealed");
2382
2383 drop(task);
2384 drain_sparse_trie_tasks(&runtime);
2385 }
2386
2387 #[test]
2388 fn stall_check_waits_for_in_flight_proofs_then_reports_pending_updates() {
2389 let runtime = reth_tasks::Runtime::test();
2390 let provider_factory = create_test_provider_factory();
2391 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
2392 let state_provider_factory = OverlayStateProviderFactory::new(
2393 provider_factory,
2394 OverlayManager::<reth_chain_state::EthPrimitives>::default()
2395 .overlay_builder(anchor_hash),
2396 );
2397 let proof_worker_handle = ProofWorkerHandle::new(
2398 &runtime,
2399 ProofTaskCtx::new(state_provider_factory),
2400 ProofWorkerCounts::full(&runtime),
2401 Default::default(),
2402 );
2403
2404 let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
2405 let trie = SparseStateTrie::default()
2406 .with_accounts_trie(default_trie.clone())
2407 .with_default_storage_trie(default_trie)
2408 .with_updates(true);
2409
2410 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
2411 let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
2412 let mut task = SparseTrieCacheTask::new_with_trie(
2413 &runtime,
2414 updates_rx,
2415 cancel_rx,
2416 std::sync::mpsc::channel().0,
2417 proof_worker_handle,
2418 SparseTrieTaskMetrics::default(),
2419 trie,
2420 B256::repeat_byte(0x55),
2421 TrieNodeEpoch::UNMODIFIED,
2422 1,
2423 );
2424
2425 drop(updates_tx);
2426
2427 let account = B256::repeat_byte(0x11);
2428 let slot = B256::repeat_byte(0x22);
2429 let account_target = B256::repeat_byte(0x33);
2430 let storage_target = B256::repeat_byte(0x44);
2431
2432 task.finished_state_updates = true;
2433 task.account_updates.insert(account, LeafUpdate::Touched);
2434 task.pending_account_updates.insert(account, None);
2435 task.fetched_account_targets.insert(account_target, ProofV2TargetParent::NONE);
2436
2437 let StorageTrieState::Idle(work) = task.storage_trie_state_mut(account) else {
2439 panic!("a fresh trie state is idle")
2440 };
2441 work.updated = true;
2442 work.pending.insert(slot, LeafUpdate::Touched);
2443 work.fetched.insert(storage_target, ProofV2TargetParent::new(11));
2444 task.in_flight_proof_jobs = 1;
2445
2446 assert!(task.ensure_not_stalled(false).is_ok());
2447
2448 let result = Ok(DecodedMultiProofV2::default());
2449 task.complete_proof_job(result).expect("proof result should be ok");
2450
2451 assert_eq!(task.in_flight_proof_jobs, 0);
2452 let error = task.ensure_not_stalled(false).expect_err("task should be stalled");
2453 assert!(matches!(error, StateRootTaskError::Stalled));
2454 let error = error.to_string();
2455
2456 assert!(error.contains("sparse trie task stalled"));
2457 assert!(!error.contains("account_targets"));
2458 assert!(!error.contains("storage_targets"));
2459 assert!(!error.contains(&format!("{account:?}")));
2460 assert!(!error.contains(&format!("{account_target:?}")));
2461 assert!(!error.contains(&format!("{storage_target:?}")));
2462 assert!(!error.contains("pending_account_leaves"));
2463 assert!(!error.contains("pending_storage_leaves"));
2464 assert!(!error.contains("pending_account_updates"));
2465 assert!(!error.contains(&format!("{slot:?}")));
2466
2467 drop(task);
2468 drain_sparse_trie_tasks(&runtime);
2469 }
2470
2471 #[test]
2472 fn run_ignores_hints_queued_after_updates_finish() {
2473 let runtime = reth_tasks::Runtime::test();
2474 let provider_factory = create_test_provider_factory();
2475 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
2476 let state_provider_factory = OverlayStateProviderFactory::new(
2477 provider_factory,
2478 OverlayManager::<reth_chain_state::EthPrimitives>::default()
2479 .overlay_builder(anchor_hash),
2480 );
2481 let proof_worker_handle = ProofWorkerHandle::new(
2482 &runtime,
2483 ProofTaskCtx::new(state_provider_factory),
2484 ProofWorkerCounts::full(&runtime),
2485 Default::default(),
2486 );
2487
2488 let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
2489 let trie = SparseStateTrie::default()
2490 .with_accounts_trie(default_trie.clone())
2491 .with_default_storage_trie(default_trie)
2492 .with_updates(true);
2493
2494 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
2495 let (cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
2496 let mut task = SparseTrieCacheTask::new_with_trie(
2497 &runtime,
2498 updates_rx,
2499 cancel_rx,
2500 std::sync::mpsc::channel().0,
2501 proof_worker_handle,
2502 SparseTrieTaskMetrics::default(),
2503 trie,
2504 B256::repeat_byte(0x55),
2505 TrieNodeEpoch::UNMODIFIED,
2506 1,
2507 );
2508
2509 updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
2510 updates_tx.send(StateRootMessage::PrefetchProofs(Default::default())).unwrap();
2511
2512 let wait_start = std::time::Instant::now();
2513 while task.updates.len() < 2 {
2514 assert!(
2515 wait_start.elapsed() < std::time::Duration::from_secs(1),
2516 "hashing task did not queue the test messages"
2517 );
2518 std::thread::yield_now();
2519 }
2520
2521 let (result_tx, result_rx) = std::sync::mpsc::channel();
2522 let handle = std::thread::spawn(move || {
2523 let _ = result_tx.send(task.run());
2524 });
2525
2526 let result = result_rx.recv_timeout(std::time::Duration::from_secs(1));
2527 drop(cancel_guard);
2528 handle.join().unwrap();
2529
2530 assert!(result.expect("state root task stalled on a late hint").is_ok());
2531
2532 drop(updates_tx);
2533 drain_sparse_trie_tasks(&runtime);
2534 }
2535}