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