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::TrieUpdatesSorted, 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::{
28 AccountMultiproofInput, ProofResultContext, ProofResultMessage, ProofResultSender,
29 ProofWorkerHandle,
30 },
31};
32use reth_trie_sparse::{
33 errors::{
34 SparseStateTrieErrorKind, SparseStateTrieResult, SparseTrieErrorKind, SparseTrieResult,
35 },
36 ArenaParallelSparseTrie, DeferredDrops, LeafUpdate, RevealableSparseTrie, SparseStateTrie,
37 SparseTrie, TrieNodeEpoch,
38};
39use tracing::{debug, debug_span, error, instrument, trace_span};
40
41pub(super) struct SparseTrieCacheTask<A = ArenaParallelSparseTrie, S = ArenaParallelSparseTrie> {
123 proof_result_tx: ProofResultSender,
125 proof_result_rx: CrossbeamReceiver<ProofResultMessage>,
127 updates: CrossbeamReceiver<SparseTrieTaskMessage>,
129 cancel_rx: CrossbeamReceiver<()>,
133 final_hashed_state_tx: Option<std::sync::mpsc::Sender<Arc<HashedPostState>>>,
135 trie: SparseStateTrie<A, S>,
137 parent_state_root: B256,
139 new_epoch: TrieNodeEpoch,
141 proof_worker_handle: ProofWorkerHandle,
143
144 chunk_size: usize,
147 max_targets_for_chunking: usize,
151
152 account_updates: B256Map<LeafUpdate>,
154
155 new_account_updates: B256Map<LeafUpdate>,
157 new_storage_updates: B256Map<B256Map<LeafUpdate>>,
159 pending_account_updates: B256Map<Option<Option<Account>>>,
173 fetched_account_targets: B256Map<ProofV2TargetParent>,
177 account_rlp_buf: Vec<u8>,
179 finished_state_updates: bool,
181 account_cache_hits: u64,
183 account_cache_misses: u64,
185 storage_cache_hits: u64,
187 storage_cache_misses: u64,
189 pending_targets: PendingTargets,
191 in_flight_proof_batches: usize,
193 storage: B256Map<StorageTrieState<S>>,
201 storage_to_drop: B256Map<StorageTrieState<S>>,
203 storage_in_flight: usize,
205 storage_done_tx: CrossbeamSender<StorageJobMessage<S>>,
207 storage_done_rx: CrossbeamReceiver<StorageJobMessage<S>>,
209 pending_updates: usize,
212 initial_updates_applied: bool,
214 final_hashed_state: HashedPostState,
220
221 metrics: SparseTrieTaskMetrics,
223}
224
225impl<A, S> SparseTrieCacheTask<A, S>
226where
227 A: SparseTrie + Default,
228 S: SparseTrie + Default + Clone + 'static,
229{
230 #[expect(clippy::too_many_arguments)]
232 pub(super) fn new_with_trie(
233 executor: &Runtime,
234 updates: CrossbeamReceiver<StateRootMessage>,
235 cancel_rx: CrossbeamReceiver<()>,
236 final_hashed_state_tx: std::sync::mpsc::Sender<Arc<HashedPostState>>,
237 proof_worker_handle: ProofWorkerHandle,
238 proof_result_tx: ProofResultSender,
239 proof_result_rx: CrossbeamReceiver<ProofResultMessage>,
240 metrics: SparseTrieTaskMetrics,
241 trie: SparseStateTrie<A, S>,
242 parent_state_root: B256,
243 new_epoch: TrieNodeEpoch,
244 chunk_size: usize,
245 ) -> Self {
246 let (hashed_state_tx, hashed_state_rx) = crossbeam_channel::unbounded();
247 let (storage_done_tx, storage_done_rx) = crossbeam_channel::unbounded();
248
249 let parent_span = tracing::Span::current();
250 let hashing_metrics = metrics.clone();
251 executor.spawn_blocking_named("trie-hashing", move || {
252 let _span = trace_span!(parent: parent_span, "run_hashing_task").entered();
253 Self::run_hashing_task(updates, hashed_state_tx, hashing_metrics)
254 });
255
256 Self {
257 proof_result_tx,
258 proof_result_rx,
259 updates: hashed_state_rx,
260 cancel_rx,
261 proof_worker_handle,
262 final_hashed_state_tx: Some(final_hashed_state_tx),
263 trie,
264 parent_state_root,
265 new_epoch,
266 chunk_size,
267 max_targets_for_chunking: DEFAULT_MAX_TARGETS_FOR_CHUNKING,
268 account_updates: Default::default(),
269 new_account_updates: Default::default(),
270 new_storage_updates: Default::default(),
271 pending_account_updates: Default::default(),
272 fetched_account_targets: Default::default(),
273 account_rlp_buf: Vec::with_capacity(TRIE_ACCOUNT_RLP_MAX_SIZE),
274 finished_state_updates: Default::default(),
275 account_cache_hits: 0,
276 account_cache_misses: 0,
277 storage_cache_hits: 0,
278 storage_cache_misses: 0,
279 pending_targets: Default::default(),
280 in_flight_proof_batches: 0,
281 storage: Default::default(),
282 storage_to_drop: Default::default(),
283 storage_in_flight: 0,
284 storage_done_tx,
285 storage_done_rx,
286 pending_updates: Default::default(),
287 initial_updates_applied: false,
288 final_hashed_state: Default::default(),
289 metrics,
290 }
291 }
292
293 fn run_hashing_task(
296 updates: CrossbeamReceiver<StateRootMessage>,
297 hashed_state_tx: CrossbeamSender<SparseTrieTaskMessage>,
298 metrics: SparseTrieTaskMetrics,
299 ) {
300 let mut total_idle_time = std::time::Duration::ZERO;
301 let mut idle_start = Instant::now();
302
303 while let Ok(message) = updates.recv() {
304 total_idle_time += idle_start.elapsed();
305
306 let msg = match message {
307 StateRootMessage::PrefetchProofs(targets) => {
308 SparseTrieTaskMessage::PrefetchProofs(targets)
309 }
310 StateRootMessage::StateUpdate(state) => {
311 let _span = trace_span!(target: "engine::tree::payload_processor::sparse_trie", "hashing_state_update", n = state.len()).entered();
312 let hashed = evm_state_to_hashed_post_state(state);
313 SparseTrieTaskMessage::HashedState(hashed)
314 }
315 StateRootMessage::FinishedStateUpdates => {
316 SparseTrieTaskMessage::FinishedStateUpdates
317 }
318 StateRootMessage::HashedStateUpdate(state) => {
319 SparseTrieTaskMessage::HashedState(state)
320 }
321 };
322 if hashed_state_tx.send(msg).is_err() {
323 break;
324 }
325
326 idle_start = Instant::now();
327 }
328
329 metrics.hashing_task_idle_time_seconds.record(total_idle_time.as_secs_f64());
330 }
331
332 pub(super) fn into_trie_for_reuse(self) -> (SparseStateTrie<A, S>, DeferredDrops) {
336 debug_assert!(
337 self.storage.is_empty(),
338 "storage tries must be back before the trie is preserved"
339 );
340 let Self { mut trie, .. } = self;
341 let deferred = trie.take_deferred_drops();
342 (trie, deferred)
343 }
344
345 pub(super) fn into_cleared_trie(self) -> (SparseStateTrie<A, S>, DeferredDrops) {
350 let Self { mut trie, storage_done_rx, storage, .. } = self;
351 drop(storage_done_rx);
353 for (address, state) in storage {
354 if let StorageTrieState::Idle(work) = state {
355 trie.insert_storage_trie(address, work.trie);
356 }
357 }
358 trie.clear();
359 let deferred = trie.take_deferred_drops();
360 (trie, deferred)
361 }
362
363 #[instrument(
372 name = "SparseTrieCacheTask::run",
373 level = "debug",
374 target = "engine::tree::payload_processor::sparse_trie",
375 skip_all
376 )]
377 pub(super) fn run(&mut self) -> Result<StateRootComputeOutcome, StateRootTaskError> {
378 let now = Instant::now();
379
380 let mut total_idle_time = std::time::Duration::ZERO;
381 let mut idle_start = Instant::now();
382 let mut done = false;
383 let mut finalized_hashed_state = None;
384
385 while !self.finished_state_updates {
389 let mut t = Instant::now();
390 crossbeam_channel::select_biased! {
391 recv(self.updates) -> message => {
392 let wake = Instant::now();
393 total_idle_time += wake.duration_since(idle_start);
394 self.metrics
395 .sparse_trie_channel_wait_duration_histogram
396 .record(wake.duration_since(t));
397
398 let update = message.map_err(|_| StateRootTaskError::Other(
399 "updates channel disconnected before state root calculation".to_string(),
400 ))?;
401 if let Some(hashed_state) = self.on_message(update) {
402 finalized_hashed_state = Some(hashed_state);
403 }
404 self.pending_updates += 1;
405 }
406 recv(self.proof_result_rx) -> message => {
407 let wake = Instant::now();
408 total_idle_time += wake.duration_since(idle_start);
409 self.metrics
410 .sparse_trie_channel_wait_duration_histogram
411 .record(wake.duration_since(t));
412 t = wake;
413
414 let Ok(result) = message else {
415 unreachable!("we own the sender half")
416 };
417 self.on_proof_results(result, &mut t)?;
418 },
419 recv(self.storage_done_rx) -> message => {
420 let wake = Instant::now();
421 total_idle_time += wake.duration_since(idle_start);
422 self.metrics
423 .sparse_trie_channel_wait_duration_histogram
424 .record(wake.duration_since(t));
425
426 let Ok(returned) = message else {
427 unreachable!("we own the sender half")
428 };
429 self.on_storage_job_message(returned)?;
430 },
431 recv(self.cancel_rx) -> _ => return Err(StateRootTaskError::Canceled),
432 }
433
434 done = self.make_progress()?;
435 idle_start = Instant::now();
436 }
437
438 while !done {
443 let mut t = Instant::now();
444 crossbeam_channel::select_biased! {
445 recv(self.proof_result_rx) -> message => {
446 let wake = Instant::now();
447 total_idle_time += wake.duration_since(idle_start);
448 self.metrics
449 .sparse_trie_channel_wait_duration_histogram
450 .record(wake.duration_since(t));
451 t = wake;
452
453 let Ok(result) = message else {
454 unreachable!("we own the sender half")
455 };
456 self.on_proof_results(result, &mut t)?;
457 },
458 recv(self.storage_done_rx) -> message => {
459 let wake = Instant::now();
460 total_idle_time += wake.duration_since(idle_start);
461 self.metrics
462 .sparse_trie_channel_wait_duration_histogram
463 .record(wake.duration_since(t));
464
465 let Ok(returned) = message else {
466 unreachable!("we own the sender half")
467 };
468 self.on_storage_job_message(returned)?;
469 },
470 recv(self.cancel_rx) -> _ => return Err(StateRootTaskError::Canceled),
471 }
472
473 done = self.make_progress()?;
474 idle_start = Instant::now();
475 }
476
477 debug_assert_eq!(
478 self.storage_in_flight, 0,
479 "completion must wait for every checked out storage trie"
480 );
481 self.metrics.sparse_trie_idle_time_seconds.record(total_idle_time.as_secs_f64());
482
483 debug!(target: "engine::root", "All proofs processed, ending calculation");
484
485 let start = Instant::now();
486 self.return_storage_tries();
487 let (state_root, trie_updates) = match self.trie.root_with_updates(self.new_epoch) {
488 Ok(result) => result,
489 Err(err)
490 if matches!(
491 err.kind(),
492 SparseStateTrieErrorKind::Sparse(SparseTrieErrorKind::Blind)
493 ) =>
494 {
495 (self.parent_state_root, TrieUpdatesSorted::default())
499 }
500 Err(err) => {
501 return Err(StateRootTaskError::Other(format!(
502 "could not calculate state root: {err:?}"
503 )))
504 }
505 };
506
507 let end = Instant::now();
508 self.metrics.sparse_trie_final_update_duration_histogram.record(end.duration_since(start));
509 self.metrics.sparse_trie_total_duration_histogram.record(end.duration_since(now));
510
511 self.metrics.sparse_trie_account_cache_hits.record(self.account_cache_hits as f64);
512 self.metrics.sparse_trie_account_cache_misses.record(self.account_cache_misses as f64);
513 self.metrics.sparse_trie_storage_cache_hits.record(self.storage_cache_hits as f64);
514 self.metrics.sparse_trie_storage_cache_misses.record(self.storage_cache_misses as f64);
515 self.account_cache_hits = 0;
516 self.account_cache_misses = 0;
517 self.storage_cache_hits = 0;
518 self.storage_cache_misses = 0;
519
520 Ok(StateRootComputeOutcome {
521 state_root,
522 trie_updates: Arc::new(trie_updates),
523 hashed_state: finalized_hashed_state
524 .expect("finished state updates publish the hashed post state"),
525 })
526 }
527
528 fn on_proof_results(
531 &mut self,
532 message: ProofResultMessage,
533 t: &mut Instant,
534 ) -> Result<(), StateRootTaskError> {
535 let mut result = self.on_proof_result_message(message)?;
536 while let Ok(next) = self.proof_result_rx.try_recv() {
537 let res = self.on_proof_result_message(next)?;
538 result.extend(res);
539 }
540
541 let phase_end = Instant::now();
542 self.metrics
543 .sparse_trie_proof_coalesce_duration_histogram
544 .record(phase_end.duration_since(*t));
545 *t = phase_end;
546
547 self.on_proof_result(result)?;
548 self.metrics.sparse_trie_reveal_multiproof_duration_histogram.record(t.elapsed());
549 Ok(())
550 }
551
552 fn make_progress(&mut self) -> Result<bool, StateRootTaskError> {
557 self.drain_returned_storage_tries()?;
560
561 let updates_queued = !self.finished_state_updates && !self.updates.is_empty();
562
563 if !updates_queued && self.proof_result_rx.is_empty() {
564 self.dispatch_pending_targets()?;
567 let t = Instant::now();
568 self.process_new_updates()?;
569 self.promote_pending_account_updates()?;
570 self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
571
572 if self.finished_state_updates && !self.has_pending_sparse_trie_updates() {
573 return Ok(true);
574 }
575
576 self.dispatch_pending_targets()?;
577 self.ensure_not_stalled(updates_queued)?;
578
579 if self.proof_result_rx.is_empty() {
582 self.trie.calculate_subtries(self.new_epoch);
583 }
584 } else if !updates_queued {
585 let t = Instant::now();
587 self.process_new_updates()?;
588 self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
589 self.dispatch_pending_targets()?;
590 } else if !self.initial_updates_applied && self.pending_updates >= INITIAL_UPDATE_BATCH_SIZE
591 {
592 let t = Instant::now();
595 self.process_new_updates()?;
596 self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
597 self.dispatch_pending_targets()?;
598 } else if self.pending_targets.len() > self.chunk_size {
599 self.dispatch_pending_targets()?;
601 }
602 Ok(false)
603 }
604
605 fn on_message(&mut self, message: SparseTrieTaskMessage) -> Option<Arc<HashedPostState>> {
607 match message {
608 SparseTrieTaskMessage::PrefetchProofs(targets) => {
609 self.on_prewarm_targets(targets);
610 None
611 }
612 SparseTrieTaskMessage::HashedState(hashed_state) => {
613 self.on_hashed_state_update(hashed_state);
614 None
615 }
616 SparseTrieTaskMessage::FinishedStateUpdates => {
617 let hashed_state = Arc::new(core::mem::take(&mut self.final_hashed_state));
618 let _ = self.final_hashed_state_tx.take().unwrap().send(Arc::clone(&hashed_state));
619 self.finished_state_updates = true;
620 Some(hashed_state)
621 }
622 }
623 }
624
625 #[instrument(
626 level = "trace",
627 target = "engine::tree::payload_processor::sparse_trie",
628 skip_all
629 )]
630 fn on_prewarm_targets(&mut self, targets: MultiProofTargetsV2) {
631 for target in targets.account_targets {
632 self.new_account_updates.entry(target.key()).or_insert(LeafUpdate::Touched);
634 }
635
636 for (address, slots) in targets.storage_targets {
637 if !slots.is_empty() {
638 let new_updates = self.new_storage_updates.entry(address).or_default();
640 for slot in slots {
641 new_updates.entry(slot.key()).or_insert(LeafUpdate::Touched);
643 }
644 }
645
646 self.new_account_updates.entry(address).or_insert(LeafUpdate::Touched);
649 }
650 }
651
652 #[instrument(
654 level = "trace",
655 target = "engine::tree::payload_processor::sparse_trie",
656 skip_all
657 )]
658 #[allow(clippy::clone_on_copy)]
659 fn on_hashed_state_update(&mut self, hashed_state_update: HashedPostState) {
660 for (&address, storage) in &hashed_state_update.storages {
661 if !storage.storage.is_empty() {
662 let new_updates = self.new_storage_updates.entry(address).or_default();
664
665 for (&slot, &value) in &storage.storage {
666 let encoded = if value.is_zero() {
667 Vec::new()
668 } else {
669 alloy_rlp::encode_fixed_size(&value).to_vec()
670 };
671 new_updates.insert(slot, LeafUpdate::Changed(encoded));
672 }
673 }
674
675 self.new_account_updates.entry(address).or_insert(LeafUpdate::Touched);
678
679 self.pending_account_updates.entry(address).or_insert(None);
682 }
683
684 for (&address, account) in &hashed_state_update.accounts {
685 self.new_account_updates.insert(address, LeafUpdate::Touched);
690
691 self.pending_account_updates.insert(address, Some(account.clone()));
694 }
695
696 self.final_hashed_state.extend(hashed_state_update);
697 }
698
699 fn on_proof_result(&mut self, result: DecodedMultiProofV2) -> Result<(), StateRootTaskError> {
700 self.reveal_proof_result(result)
701 .map_err(|e| StateRootTaskError::Other(format!("could not reveal multiproof: {e:?}")))
702 }
703
704 fn reveal_proof_result(&mut self, result: DecodedMultiProofV2) -> SparseStateTrieResult<()> {
709 let DecodedMultiProofV2 { account_proofs, storage_proofs } = result;
710
711 self.queue_storage_proofs(storage_proofs);
712 self.run_ready_storage_work()?;
714
715 self.trie.reveal_account_proof_nodes(account_proofs)
716 }
717
718 fn queue_storage_proofs(&mut self, storage_proofs: B256Map<Vec<ProofTrieNodeV2>>) {
720 let mut revealed_nodes = 0;
721 for (address, mut nodes) in storage_proofs {
722 if nodes.is_empty() {
723 continue;
724 }
725 revealed_nodes += nodes.len();
726
727 match self.storage_trie_state_mut(address) {
728 StorageTrieState::Idle(work) => work.queue_proofs(&mut nodes),
729 StorageTrieState::InFlight(in_flight) => in_flight.proofs.append(&mut nodes),
730 }
731 }
732 self.trie.record_revealed_storage_nodes(revealed_nodes);
733 }
734
735 fn storage_trie_state_mut(&mut self, address: B256) -> &mut StorageTrieState<S> {
738 let Self { storage, trie, .. } = self;
739 storage.entry(address).or_insert_with(|| {
740 StorageTrieState::Idle(Box::new(StorageTrieWork::new(
741 trie.take_or_create_storage_trie(&address),
742 )))
743 })
744 }
745
746 fn on_proof_result_message(
747 &mut self,
748 message: ProofResultMessage,
749 ) -> Result<DecodedMultiProofV2, StateRootTaskError> {
750 let result = message.result?;
751 debug_assert!(
752 self.in_flight_proof_batches > 0,
753 "received proof result without an in-flight proof batch"
754 );
755 self.in_flight_proof_batches = self.in_flight_proof_batches.saturating_sub(1);
756 Ok(result)
757 }
758
759 fn process_new_updates(&mut self) -> SparseTrieResult<()> {
762 self.apply_new_updates()?;
763 self.run_ready_storage_work()
764 }
765
766 fn apply_new_updates(&mut self) -> SparseTrieResult<()> {
768 if self.pending_updates == 0 {
769 return Ok(());
770 }
771
772 let _span = debug_span!("process_new_updates").entered();
773 self.pending_updates = 0;
774 self.initial_updates_applied = true;
775
776 let Self { storage, trie, new_storage_updates, .. } = self;
778 for (address, new) in new_storage_updates.drain() {
779 if new.is_empty() {
780 continue;
781 }
782
783 let state = storage.entry(address).or_insert_with(|| {
784 StorageTrieState::Idle(Box::new(StorageTrieWork::new(
785 trie.take_or_create_storage_trie(&address),
786 )))
787 });
788 match state {
789 StorageTrieState::Idle(work) => work.queue_updates(new),
790 StorageTrieState::InFlight(in_flight) => {
791 merge_leaf_updates(&mut in_flight.updates, new)
792 }
793 }
794 }
795
796 self.process_account_leaf_updates(true)?;
798
799 for (address, new) in self.new_account_updates.drain() {
800 match self.account_updates.entry(address) {
801 Entry::Occupied(mut entry) => {
802 if new.is_changed() {
803 entry.insert(new);
804 }
805 }
806 Entry::Vacant(entry) => {
807 entry.insert(new);
808 }
809 }
810 }
811
812 Ok(())
813 }
814
815 #[instrument(
823 level = "trace",
824 target = "engine::tree::payload_processor::sparse_trie",
825 skip_all
826 )]
827 fn run_ready_storage_work(&mut self) -> SparseTrieResult<()> {
828 let mut ready = Vec::new();
829 let mut job_units = 0;
830 for (address, state) in &self.storage {
831 let StorageTrieState::Idle(work) = state else { continue };
832 if !work.has_work() {
833 continue;
834 }
835 if !work.is_target_only() {
836 job_units += work.proofs.len() + work.pending.len();
837 }
838 ready.push(*address);
839 }
840 if ready.is_empty() {
841 return Ok(())
842 }
843
844 let inline_round = job_units <= INLINE_STORAGE_WORK_UNITS;
845 let new_epoch = self.new_epoch;
846 let retain_updates = self.trie.retains_updates();
847 let started = Instant::now();
848 let mut jobs = Vec::new();
849 for address in ready {
850 let state = self.storage.get_mut(&address).expect("entry was just seen");
851 let StorageTrieState::Idle(work) = state else {
852 unreachable!("an idle trie is only checked out from here")
853 };
854
855 if inline_round || work.is_target_only() {
856 let output = work.run(new_epoch, retain_updates);
857 self.apply_storage_output(address, output)?;
858 continue;
859 }
860
861 let StorageTrieState::Idle(work) = core::mem::replace(
862 state,
863 StorageTrieState::InFlight(Box::new(InFlightStorage::new(started))),
864 ) else {
865 unreachable!("just matched")
866 };
867 self.storage_in_flight += 1;
868 jobs.push(StorageTrieJob { address, work });
869 }
870
871 self.spawn_storage_jobs(jobs);
872
873 Ok(())
874 }
875
876 fn apply_storage_output(
879 &mut self,
880 address: B256,
881 output: StorageWorkOutput,
882 ) -> SparseTrieResult<()> {
883 let StorageWorkOutput { targets, cache_hits, cache_misses, result } = output;
884
885 self.storage_cache_hits += cache_hits;
886 self.storage_cache_misses += cache_misses;
887 if !targets.is_empty() {
888 self.pending_targets.extend_storage_targets(&address, targets);
889 }
890
891 result
892 }
893
894 #[instrument(
898 level = "trace",
899 target = "engine::tree::payload_processor::sparse_trie",
900 skip_all
901 )]
902 fn process_account_leaf_updates(&mut self, new: bool) -> SparseTrieResult<bool> {
903 let account_updates =
904 if new { &mut self.new_account_updates } else { &mut self.account_updates };
905
906 let updates_len_before = account_updates.len();
907
908 self.trie.trie_mut().update_leaves(account_updates, |target, parent| {
909 match self.fetched_account_targets.entry(target) {
910 Entry::Occupied(mut entry) => {
911 if parent < *entry.get() {
912 entry.insert(parent);
913 self.pending_targets
914 .push_account_target(ProofV2Target::new(target).with_parent(parent));
915 }
916 }
917 Entry::Vacant(entry) => {
918 entry.insert(parent);
919 self.pending_targets
920 .push_account_target(ProofV2Target::new(target).with_parent(parent));
921 }
922 }
923 })?;
924
925 let updates_len_after = account_updates.len();
926 self.account_cache_hits += (updates_len_before - updates_len_after) as u64;
927 self.account_cache_misses += updates_len_after as u64;
928
929 Ok(updates_len_after < updates_len_before)
930 }
931
932 fn spawn_storage_jobs(&self, mut jobs: Vec<StorageTrieJob<S>>) {
937 if jobs.is_empty() {
938 return;
939 }
940
941 let parent_span = debug_span!("spawn_storage_jobs", n = jobs.len());
942 let chunk_len = storage_job_chunk_len(jobs.len());
943 let new_epoch = self.new_epoch;
944 let retain_updates = self.trie.retains_updates();
945 while !jobs.is_empty() {
946 let chunk = jobs.split_off(jobs.len().saturating_sub(chunk_len));
947 let storage_done_tx = self.storage_done_tx.clone();
948 let parent_span = parent_span.clone();
949 rayon::spawn(move || {
950 let _enter = debug_span!(
951 target: "engine::tree::payload_processor::sparse_trie",
952 parent: &parent_span,
953 "storage_jobs",
954 n = chunk.len(),
955 )
956 .entered();
957 for job in chunk {
958 let address = job.address;
959 let message = match panic::catch_unwind(AssertUnwindSafe(|| {
960 job.run(new_epoch, retain_updates)
961 })) {
962 Ok(done) => StorageJobMessage::Done(done),
963 Err(payload) => StorageJobMessage::Panicked { address, payload },
964 };
965 if storage_done_tx.send(message).is_err() {
966 return;
968 }
969 }
970 });
971 }
972 }
973
974 fn on_storage_job_message(&mut self, message: StorageJobMessage<S>) -> SparseTrieResult<()> {
977 match message {
978 StorageJobMessage::Done(done) => self.on_storage_trie_returned(done),
979 StorageJobMessage::Panicked { address, payload } => {
980 if self.storage.remove(&address).is_some() {
981 self.storage_in_flight -= 1;
982 }
983 panic::resume_unwind(payload)
984 }
985 }
986 }
987
988 fn on_storage_trie_returned(&mut self, done: StorageTrieJobDone<S>) -> SparseTrieResult<()> {
991 let StorageTrieJobDone { address, work, output } = done;
992
993 let Entry::Occupied(mut entry) = self.storage.entry(address) else {
994 unreachable!("a returned payload was checked out of its entry")
995 };
996 let StorageTrieState::InFlight(in_flight) = entry.insert(StorageTrieState::Idle(work))
997 else {
998 unreachable!("a checked out address stays in flight until its job returns")
999 };
1000 let started = in_flight.started;
1001 let StorageTrieState::Idle(work) = entry.into_mut() else { unreachable!("just inserted") };
1002 work.take_buffered(*in_flight);
1003 self.storage_in_flight -= 1;
1004 self.metrics.sparse_trie_storage_job_duration_histogram.record(started.elapsed());
1005
1006 self.apply_storage_output(address, output)
1007 }
1008
1009 fn drain_returned_storage_tries(&mut self) -> SparseTrieResult<()> {
1011 while let Ok(message) = self.storage_done_rx.try_recv() {
1012 self.on_storage_job_message(message)?;
1013 }
1014
1015 Ok(())
1016 }
1017
1018 fn return_storage_tries(&mut self) {
1023 let mut storage = core::mem::take(&mut self.storage);
1024 for (address, state) in &mut storage {
1025 let StorageTrieState::Idle(work) = state else {
1026 unreachable!("completion must wait for every storage job")
1027 };
1028 self.trie.insert_storage_trie(*address, core::mem::take(&mut work.trie));
1029 }
1030 self.storage_to_drop = storage;
1032 }
1033
1034 #[instrument(
1040 level = "trace",
1041 target = "engine::tree::payload_processor::sparse_trie",
1042 skip_all
1043 )]
1044 fn promote_pending_account_updates(&mut self) -> SparseTrieResult<()> {
1045 self.process_account_leaf_updates(false)?;
1046
1047 if self.pending_account_updates.is_empty() {
1048 return Ok(());
1049 }
1050
1051 let new_epoch = self.new_epoch;
1052 loop {
1053 let span = trace_span!("promote_updates", promoted = tracing::field::Empty).entered();
1054 let account_rlp_buf = &mut self.account_rlp_buf;
1056 let mut num_promoted = 0;
1057 self.pending_account_updates.retain(|addr, account| {
1058 let updated_storage = match self.storage.get_mut(addr) {
1059 Some(StorageTrieState::InFlight(_)) => return true,
1061 Some(StorageTrieState::Idle(work)) if work.updated => Some(work),
1062 _ => None,
1063 };
1064
1065 if let Some(work) = updated_storage {
1066 if !work.pending.is_empty() {
1067 return true;
1069 } else if let Some(account) = account.take() {
1070 let storage_root = work.trie.root(new_epoch).expect("updates are drained, storage trie should be revealed by now");
1071 let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
1072 self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
1073 num_promoted += 1;
1074 return false;
1075 }
1076 }
1077
1078 let trie_account = match self.account_updates.get(addr) {
1080 Some(LeafUpdate::Changed(encoded)) => {
1081 Some(encoded).filter(|encoded| !encoded.is_empty())
1082 }
1083 Some(LeafUpdate::Touched) => return true,
1085 None => self.trie.get_account_value(addr),
1086 };
1087
1088 let trie_account = trie_account.map(|value| TrieAccount::decode(&mut &value[..]).expect("invalid account RLP"));
1089
1090 let (account, storage_root) = if let Some(account) = account.take() {
1091 let storage_root = trie_account.map(|account| account.storage_root).unwrap_or(EMPTY_ROOT_HASH);
1096
1097 (account, storage_root)
1098 } else {
1099 let storage_root = match self.storage.get_mut(addr) {
1102 Some(StorageTrieState::Idle(work)) => work.trie.root(new_epoch),
1103 Some(StorageTrieState::InFlight(_)) => unreachable!("returned above"),
1104 None => self.trie.storage_root(addr, new_epoch),
1105 };
1106 (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"))
1107 };
1108
1109 let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
1110 self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
1111 num_promoted += 1;
1112
1113 false
1114 });
1115 span.record("promoted", num_promoted);
1116 drop(span);
1117
1118 if num_promoted == 0 || !self.process_account_leaf_updates(false)? {
1123 break
1124 }
1125 }
1126
1127 Ok(())
1128 }
1129
1130 fn dispatch_pending_targets(&mut self) -> Result<(), StateRootTaskError> {
1131 if self.pending_targets.is_empty() {
1132 return Ok(())
1133 }
1134
1135 let _span = trace_span!("dispatch_pending_targets").entered();
1136 let (targets, chunking_length) = self.pending_targets.take();
1137 let mut dispatch_error = None;
1138 dispatch_with_chunking(
1139 targets,
1140 chunking_length,
1141 self.chunk_size,
1142 self.max_targets_for_chunking,
1143 self.proof_worker_handle.has_multiple_idle_account_workers(),
1144 self.proof_worker_handle.has_multiple_idle_storage_workers(),
1145 MultiProofTargetsV2::chunks,
1146 |proof_targets| {
1147 if dispatch_error.is_some() {
1148 return;
1149 }
1150
1151 match self.proof_worker_handle.dispatch_account_multiproof(AccountMultiproofInput {
1152 targets: proof_targets,
1153 proof_result_sender: ProofResultContext::new(
1154 self.proof_result_tx.clone(),
1155 HashedPostState::default(),
1156 Instant::now(),
1157 ),
1158 }) {
1159 Ok(()) => {
1160 self.in_flight_proof_batches += 1;
1161 }
1162 Err(e) => {
1163 error!("failed to dispatch account multiproof: {e:?}");
1164 dispatch_error = Some(StateRootTaskError::ProofDispatch(e));
1165 }
1166 }
1167 },
1168 );
1169
1170 if let Some(error) = dispatch_error {
1171 return Err(error)
1172 }
1173
1174 Ok(())
1175 }
1176
1177 fn has_pending_sparse_trie_updates(&self) -> bool {
1178 !self.account_updates.is_empty() ||
1179 !self.pending_account_updates.is_empty() ||
1180 self.storage.values().any(|state| state.is_pending())
1181 }
1182
1183 fn has_ready_storage_work(&self) -> bool {
1186 self.storage.values().any(|state| match state {
1187 StorageTrieState::Idle(work) => work.has_work(),
1188 StorageTrieState::InFlight(_) => false,
1189 })
1190 }
1191
1192 fn ensure_not_stalled(&self, updates_queued: bool) -> Result<(), StateRootTaskError> {
1200 if self.finished_state_updates &&
1201 !updates_queued &&
1202 self.pending_updates == 0 &&
1203 self.pending_targets.is_empty() &&
1204 self.in_flight_proof_batches == 0 &&
1205 self.proof_result_rx.is_empty() &&
1206 self.storage_in_flight == 0 &&
1207 !self.has_ready_storage_work() &&
1208 self.has_pending_sparse_trie_updates()
1209 {
1210 const MAX_STALLED_PROOF_TARGETS_TO_LOG: usize = 5;
1211
1212 let mut account_targets = self
1213 .account_updates
1214 .keys()
1215 .map(|target| (*target, self.fetched_account_targets.get(target).copied()))
1216 .collect::<Vec<_>>();
1217 account_targets.sort_unstable();
1218 let account_targets_truncated =
1219 account_targets.len().saturating_sub(MAX_STALLED_PROOF_TARGETS_TO_LOG);
1220 account_targets.truncate(MAX_STALLED_PROOF_TARGETS_TO_LOG);
1221
1222 let mut storage_targets = self
1223 .storage
1224 .iter()
1225 .filter_map(|(address, state)| match state {
1226 StorageTrieState::Idle(work) => Some((address, work)),
1227 StorageTrieState::InFlight(_) => None,
1228 })
1229 .flat_map(|(address, work)| {
1230 work.pending
1231 .keys()
1232 .map(move |target| (*address, *target, work.fetched.get(target).copied()))
1233 })
1234 .collect::<Vec<_>>();
1235 storage_targets.sort_unstable();
1236 let storage_targets_truncated =
1237 storage_targets.len().saturating_sub(MAX_STALLED_PROOF_TARGETS_TO_LOG);
1238 storage_targets.truncate(MAX_STALLED_PROOF_TARGETS_TO_LOG);
1239
1240 error!(
1241 ?account_targets,
1242 account_targets_truncated,
1243 ?storage_targets,
1244 storage_targets_truncated,
1245 "sparse trie task stalled: pending updates remain but no proof targets are queued or in flight"
1246 );
1247
1248 return Err(StateRootTaskError::Stalled)
1249 }
1250
1251 Ok(())
1252 }
1253}
1254
1255enum StorageTrieState<S> {
1257 Idle(Box<StorageTrieWork<S>>),
1259 InFlight(Box<InFlightStorage>),
1262}
1263
1264impl<S: SparseTrie + Default> StorageTrieState<S> {
1265 fn is_pending(&self) -> bool {
1267 match self {
1268 Self::InFlight(_) => true,
1269 Self::Idle(work) => !work.pending.is_empty() || work.has_work(),
1270 }
1271 }
1272}
1273
1274struct StorageTrieWork<S> {
1277 trie: RevealableSparseTrie<S>,
1279 pending: B256Map<LeafUpdate>,
1282 proofs: Vec<ProofTrieNodeV2>,
1284 fetched: B256Map<ProofV2TargetParent>,
1287 dirty: bool,
1294 updated: bool,
1297}
1298
1299impl<S: SparseTrie + Default> StorageTrieWork<S> {
1300 fn new(trie: RevealableSparseTrie<S>) -> Self {
1301 Self {
1302 trie,
1303 pending: Default::default(),
1304 proofs: Vec::new(),
1305 fetched: Default::default(),
1306 dirty: false,
1307 updated: false,
1308 }
1309 }
1310
1311 fn run(&mut self, new_epoch: TrieNodeEpoch, retain_updates: bool) -> StorageWorkOutput {
1314 let Self { trie, pending, proofs, fetched, dirty, .. } = self;
1315 *dirty = false;
1316 let mut output = StorageWorkOutput::default();
1317
1318 if !proofs.is_empty() {
1319 output.result = trie.reveal_v2_proof_nodes(proofs, retain_updates);
1320 proofs.clear();
1321 if output.result.is_err() {
1322 return output
1323 }
1324 }
1325
1326 if !pending.is_empty() {
1327 let updates_len_before = pending.len();
1328 let targets = &mut output.targets;
1329 output.result = trie.update_leaves(pending, |path, parent| match fetched.entry(path) {
1330 Entry::Occupied(mut entry) => {
1331 if parent < *entry.get() {
1332 entry.insert(parent);
1333 targets.push(ProofV2Target::new(path).with_parent(parent));
1334 }
1335 }
1336 Entry::Vacant(entry) => {
1337 entry.insert(parent);
1338 targets.push(ProofV2Target::new(path).with_parent(parent));
1339 }
1340 });
1341 output.cache_hits = (updates_len_before - pending.len()) as u64;
1342 output.cache_misses = pending.len() as u64;
1343 if output.result.is_err() {
1344 return output
1345 }
1346 }
1347
1348 if self.needs_root() {
1349 self.trie.root(new_epoch);
1350 }
1351
1352 output
1353 }
1354
1355 fn queue_updates(&mut self, updates: B256Map<LeafUpdate>) {
1357 if updates.is_empty() {
1358 return
1359 }
1360
1361 self.updated = true;
1362 self.dirty = true;
1363 merge_leaf_updates(&mut self.pending, updates);
1364 }
1365
1366 fn queue_proofs(&mut self, nodes: &mut Vec<ProofTrieNodeV2>) {
1368 if nodes.is_empty() {
1369 return
1370 }
1371
1372 self.dirty = true;
1373 self.proofs.append(nodes);
1374 }
1375
1376 fn take_buffered(&mut self, buffered: InFlightStorage) {
1378 let InFlightStorage { mut proofs, updates, .. } = buffered;
1379 self.queue_proofs(&mut proofs);
1380 self.queue_updates(updates);
1381 }
1382
1383 fn has_work(&self) -> bool {
1385 debug_assert!(self.dirty || !self.needs_root(), "a drained pass must hash its trie");
1386 self.dirty
1387 }
1388
1389 fn needs_root(&self) -> bool {
1394 self.updated &&
1395 self.pending.is_empty() &&
1396 self.trie.is_revealed() &&
1397 !self.trie.is_root_cached()
1398 }
1399
1400 const fn is_target_only(&self) -> bool {
1405 self.proofs.is_empty() && self.trie.is_blind()
1406 }
1407}
1408
1409struct InFlightStorage {
1411 started: Instant,
1413 proofs: Vec<ProofTrieNodeV2>,
1415 updates: B256Map<LeafUpdate>,
1417}
1418
1419impl InFlightStorage {
1420 fn new(started: Instant) -> Self {
1421 Self { started, proofs: Vec::new(), updates: Default::default() }
1422 }
1423}
1424
1425struct StorageTrieJob<S> {
1427 address: B256,
1429 work: Box<StorageTrieWork<S>>,
1431}
1432
1433impl<S: SparseTrie + Default> StorageTrieJob<S> {
1434 fn run(mut self, new_epoch: TrieNodeEpoch, retain_updates: bool) -> StorageTrieJobDone<S> {
1435 let output = self.work.run(new_epoch, retain_updates);
1436 StorageTrieJobDone { address: self.address, work: self.work, output }
1437 }
1438}
1439
1440enum StorageJobMessage<S> {
1442 Done(StorageTrieJobDone<S>),
1444 Panicked {
1448 address: B256,
1450 payload: Box<dyn Any + Send>,
1452 },
1453}
1454
1455struct StorageTrieJobDone<S> {
1457 address: B256,
1459 work: Box<StorageTrieWork<S>>,
1461 output: StorageWorkOutput,
1463}
1464
1465struct StorageWorkOutput {
1467 targets: Vec<ProofV2Target>,
1469 cache_hits: u64,
1471 cache_misses: u64,
1473 result: SparseTrieResult<()>,
1475}
1476
1477impl Default for StorageWorkOutput {
1478 fn default() -> Self {
1479 Self { targets: Vec::new(), cache_hits: 0, cache_misses: 0, result: Ok(()) }
1480 }
1481}
1482
1483fn merge_leaf_updates(queued: &mut B256Map<LeafUpdate>, updates: B256Map<LeafUpdate>) {
1485 if queued.is_empty() {
1486 *queued = updates;
1488 return
1489 }
1490
1491 for (slot, update) in updates {
1492 match queued.entry(slot) {
1493 Entry::Occupied(mut entry) => {
1494 if update.is_changed() {
1495 entry.insert(update);
1496 }
1497 }
1498 Entry::Vacant(entry) => {
1499 entry.insert(update);
1500 }
1501 }
1502 }
1503}
1504
1505fn storage_job_chunk_len(tries: usize) -> usize {
1512 const MAX_CHUNK_LEN: usize = 32;
1514
1515 tries.div_ceil(rayon::current_num_threads().max(1) * 4).clamp(1, MAX_CHUNK_LEN)
1516}
1517
1518#[derive(Metrics, Clone)]
1520#[metrics(scope = "tree.root")]
1521pub(super) struct SparseTrieTaskMetrics {
1522 pub(super) sparse_trie_reveal_multiproof_duration_histogram: Histogram,
1524 pub(super) sparse_trie_proof_coalesce_duration_histogram: Histogram,
1526 pub(super) sparse_trie_channel_wait_duration_histogram: Histogram,
1528 pub(super) sparse_trie_process_updates_duration_histogram: Histogram,
1530 pub(super) sparse_trie_storage_job_duration_histogram: Histogram,
1532 pub(super) sparse_trie_final_update_duration_histogram: Histogram,
1534 pub(super) sparse_trie_total_duration_histogram: Histogram,
1536 pub(super) into_trie_for_reuse_duration_histogram: Histogram,
1538 pub(super) sparse_trie_prune_duration_histogram: Histogram,
1540 pub(super) sparse_trie_cache_wait_duration_histogram: Histogram,
1542 pub(super) sparse_trie_idle_time_seconds: Histogram,
1545 pub(super) hashing_task_idle_time_seconds: Histogram,
1548
1549 pub(super) sparse_trie_account_cache_hits: Histogram,
1551 pub(super) sparse_trie_account_cache_misses: Histogram,
1553 pub(super) sparse_trie_storage_cache_hits: Histogram,
1555 pub(super) sparse_trie_storage_cache_misses: Histogram,
1557
1558 pub(super) sparse_trie_retained_storage_tries: Gauge,
1560}
1561
1562const DEFAULT_MAX_TARGETS_FOR_CHUNKING: usize = 300;
1565
1566const INITIAL_UPDATE_BATCH_SIZE: usize = 64;
1568
1569const INLINE_STORAGE_WORK_UNITS: usize = 16;
1573
1574#[expect(clippy::too_many_arguments)]
1577fn dispatch_with_chunking<T, I>(
1578 items: T,
1579 chunking_len: usize,
1580 chunk_size: usize,
1581 max_targets_for_chunking: usize,
1582 has_multiple_idle_account_workers: bool,
1583 has_multiple_idle_storage_workers: bool,
1584 chunker: impl FnOnce(T, usize) -> I,
1585 mut dispatch: impl FnMut(T),
1586) where
1587 I: IntoIterator<Item = T>,
1588{
1589 let has_full_chunks = chunking_len >= chunk_size.saturating_mul(2);
1590 let should_chunk = chunking_len > max_targets_for_chunking ||
1591 (has_full_chunks &&
1592 (has_multiple_idle_account_workers || has_multiple_idle_storage_workers));
1593
1594 if should_chunk && chunking_len > chunk_size {
1595 for chunk in chunker(items, chunk_size) {
1596 dispatch(chunk);
1597 }
1598 return;
1599 }
1600
1601 dispatch(items);
1602}
1603
1604fn encode_account_leaf_value(
1611 account: Option<Account>,
1612 storage_root: B256,
1613 account_rlp_buf: &mut Vec<u8>,
1614) -> Vec<u8> {
1615 if account.as_ref().is_none_or(|account| account.is_empty()) && storage_root == EMPTY_ROOT_HASH
1616 {
1617 return Vec::new();
1618 }
1619
1620 account_rlp_buf.clear();
1621 account.unwrap_or_default().into_trie_account(storage_root).encode(account_rlp_buf);
1622 account_rlp_buf.clone()
1623}
1624
1625#[derive(Default)]
1627struct PendingTargets {
1628 targets: MultiProofTargetsV2,
1630 len: usize,
1632}
1633
1634impl PendingTargets {
1635 const fn len(&self) -> usize {
1637 self.len
1638 }
1639
1640 const fn is_empty(&self) -> bool {
1642 self.len == 0
1643 }
1644
1645 fn take(&mut self) -> (MultiProofTargetsV2, usize) {
1647 (std::mem::take(&mut self.targets), std::mem::take(&mut self.len))
1648 }
1649
1650 fn push_account_target(&mut self, target: ProofV2Target) {
1652 self.targets.account_targets.push(target);
1653 self.len += 1;
1654 }
1655
1656 fn extend_storage_targets(&mut self, address: &B256, targets: Vec<ProofV2Target>) {
1658 self.len += targets.len();
1659 self.targets.storage_targets.entry(*address).or_default().extend(targets);
1660 }
1661}
1662
1663enum SparseTrieTaskMessage {
1665 HashedState(HashedPostState),
1667 PrefetchProofs(MultiProofTargetsV2),
1669 FinishedStateUpdates,
1671}
1672
1673#[cfg(test)]
1674mod tests {
1675 use super::*;
1676 use alloy_primitives::{keccak256, Address, B256, U256};
1677 use reth_db_common::init::init_genesis;
1678 use reth_provider::test_utils::create_test_provider_factory;
1679 use reth_storage_overlay::{OverlayManager, OverlayStateProviderFactory};
1680 use reth_trie_common::{ExtensionNode, LeafNode, Nibbles, RlpNode, TrieNodeV2};
1681 use reth_trie_parallel::proof_task::ProofTaskCtx;
1682 use reth_trie_sparse::ArenaParallelSparseTrie;
1683
1684 fn drain_sparse_trie_tasks(runtime: &Runtime) {
1685 for task_name in ["trie-hashing", "storage-workers", "account-workers"] {
1686 runtime.spawn_blocking_named(task_name, || {}).get();
1687 }
1688 }
1689
1690 fn test_task(
1691 runtime: &Runtime,
1692 trie: SparseStateTrie,
1693 ) -> (SparseTrieCacheTask, CrossbeamSender<StateRootMessage>, CrossbeamSender<()>) {
1694 let provider_factory = create_test_provider_factory();
1695 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
1696 let state_provider_factory = OverlayStateProviderFactory::new(
1697 provider_factory,
1698 OverlayManager::<reth_chain_state::EthPrimitives>::default()
1699 .overlay_builder(anchor_hash),
1700 );
1701 let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
1702 let proof_worker_handle = ProofWorkerHandle::new(
1703 runtime,
1704 ProofTaskCtx::new(state_provider_factory),
1705 false,
1706 proof_result_tx.clone(),
1707 );
1708 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1709 let (cancel_guard, cancel_rx) = crossbeam_channel::bounded(0);
1710 let task = SparseTrieCacheTask::new_with_trie(
1711 runtime,
1712 updates_rx,
1713 cancel_rx,
1714 std::sync::mpsc::channel().0,
1715 proof_worker_handle,
1716 proof_result_tx,
1717 proof_result_rx,
1718 SparseTrieTaskMetrics::default(),
1719 trie,
1720 EMPTY_ROOT_HASH,
1721 TrieNodeEpoch::new(1),
1722 1,
1723 );
1724 (task, updates_tx, cancel_guard)
1725 }
1726
1727 fn receive_storage_job(
1728 task: &SparseTrieCacheTask,
1729 ) -> StorageJobMessage<ArenaParallelSparseTrie> {
1730 task.storage_done_rx
1731 .recv_timeout(std::time::Duration::from_secs(5))
1732 .expect("storage job did not return")
1733 }
1734
1735 fn check_out_storage(
1737 task: &mut SparseTrieCacheTask,
1738 address: B256,
1739 ) -> Box<StorageTrieWork<ArenaParallelSparseTrie>> {
1740 let state = task.storage.get_mut(&address).expect("address must have an entry");
1741 let StorageTrieState::Idle(work) = core::mem::replace(
1742 state,
1743 StorageTrieState::InFlight(Box::new(InFlightStorage::new(Instant::now()))),
1744 ) else {
1745 panic!("trie must be idle")
1746 };
1747 task.storage_in_flight += 1;
1748 work
1749 }
1750
1751 fn return_storage(
1753 task: &mut SparseTrieCacheTask,
1754 address: B256,
1755 work: Box<StorageTrieWork<ArenaParallelSparseTrie>>,
1756 ) {
1757 task.on_storage_trie_returned(StorageTrieJobDone {
1758 address,
1759 work,
1760 output: StorageWorkOutput::default(),
1761 })
1762 .unwrap();
1763 }
1764
1765 fn storage_slot_value(
1766 task: &SparseTrieCacheTask,
1767 address: &B256,
1768 slot: &B256,
1769 ) -> Option<Vec<u8>> {
1770 let StorageTrieState::Idle(work) = task.storage.get(address)? else { return None };
1771 work.trie.as_revealed_ref()?.get_leaf_value(&Nibbles::unpack(slot)).cloned()
1772 }
1773
1774 fn storage_root_of(task: &mut SparseTrieCacheTask, address: B256) -> B256 {
1775 let StorageTrieState::Idle(work) = task.storage.get_mut(&address).expect("entry") else {
1776 panic!("payload is out with a job")
1777 };
1778 work.trie.root(TrieNodeEpoch::new(1)).expect("storage trie must be revealed")
1779 }
1780
1781 #[test]
1782 fn test_run_hashing_task_hashed_state_update_forwards() {
1783 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1784 let (hashed_state_tx, hashed_state_rx) = crossbeam_channel::unbounded();
1785
1786 let address = keccak256(Address::random());
1787 let slot = keccak256(U256::from(42).to_be_bytes::<32>());
1788 let value = U256::from(999);
1789
1790 let mut hashed_state = HashedPostState::default();
1791 hashed_state.accounts.insert(
1792 address,
1793 Some(Account { balance: U256::from(100), nonce: 1, ..Default::default() }),
1794 );
1795 let mut storage = reth_trie::HashedStorage::default();
1796 storage.storage.insert(slot, value);
1797 hashed_state.storages.insert(address, storage);
1798
1799 let expected_state = hashed_state.clone();
1800
1801 let handle = std::thread::spawn(move || {
1802 SparseTrieCacheTask::<ArenaParallelSparseTrie, ArenaParallelSparseTrie>::run_hashing_task(
1803 updates_rx,
1804 hashed_state_tx,
1805 SparseTrieTaskMetrics::default(),
1806 );
1807 });
1808
1809 updates_tx.send(StateRootMessage::HashedStateUpdate(hashed_state)).unwrap();
1810 updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
1811 drop(updates_tx);
1812
1813 let SparseTrieTaskMessage::HashedState(received) = hashed_state_rx.recv().unwrap() else {
1814 panic!("expected HashedState message");
1815 };
1816
1817 let account = received.accounts.get(&address).unwrap().as_ref().unwrap();
1818 assert_eq!(account.balance, expected_state.accounts[&address].as_ref().unwrap().balance);
1819 assert_eq!(account.nonce, expected_state.accounts[&address].as_ref().unwrap().nonce);
1820
1821 let storage = received.storages.get(&address).unwrap();
1822 assert_eq!(*storage.storage.get(&slot).unwrap(), value);
1823
1824 let second = hashed_state_rx.recv().unwrap();
1825 assert!(matches!(second, SparseTrieTaskMessage::FinishedStateUpdates));
1826
1827 assert!(hashed_state_rx.recv().is_err());
1828 handle.join().unwrap();
1829 }
1830
1831 #[test]
1832 fn test_encode_account_leaf_value_deletion_and_empty_root_is_empty() {
1833 let mut account_rlp_buf = vec![0xAB];
1834 let encoded = encode_account_leaf_value(None, EMPTY_ROOT_HASH, &mut account_rlp_buf);
1835
1836 assert!(encoded.is_empty());
1837 assert_eq!(account_rlp_buf, vec![0xAB]);
1839 }
1840
1841 #[test]
1842 fn test_encode_account_leaf_value_empty_account_and_empty_root_is_empty() {
1843 let mut account_rlp_buf = vec![0xAB];
1844 let encoded = encode_account_leaf_value(
1845 Some(Account::default()),
1846 EMPTY_ROOT_HASH,
1847 &mut account_rlp_buf,
1848 );
1849
1850 assert!(encoded.is_empty());
1851 assert_eq!(account_rlp_buf, vec![0xAB]);
1853 }
1854
1855 #[test]
1856 #[allow(clippy::needless_update)]
1857 fn test_encode_account_leaf_value_non_empty_account_is_rlp() {
1858 let storage_root = B256::from([0x99; 32]);
1859 let account = Some(Account {
1860 nonce: 7,
1861 balance: U256::from(42),
1862 bytecode_hash: Some(B256::from([0xAA; 32])),
1863 ..Default::default()
1864 });
1865 let mut account_rlp_buf = vec![0x00, 0x01];
1866
1867 let encoded = encode_account_leaf_value(account, storage_root, &mut account_rlp_buf);
1868 let decoded = TrieAccount::decode(&mut &encoded[..]).expect("valid account RLP");
1869
1870 assert_eq!(decoded.nonce, 7);
1871 assert_eq!(decoded.balance, U256::from(42));
1872 assert_eq!(decoded.storage_root, storage_root);
1873 assert_eq!(account_rlp_buf, encoded);
1874 }
1875
1876 #[test]
1877 fn checked_out_storage_trie_holds_back_its_updates_until_it_returns() {
1878 let runtime = Runtime::test();
1879 let trie = SparseStateTrie::default()
1880 .with_accounts_trie(RevealableSparseTrie::<ArenaParallelSparseTrie>::revealed_empty())
1881 .with_default_storage_trie(RevealableSparseTrie::blind_from(
1882 ArenaParallelSparseTrie::default(),
1883 ))
1884 .with_updates(true);
1885 let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, trie);
1886
1887 let address = B256::repeat_byte(0x11);
1888 let revealed_slot = B256::repeat_byte(0x22);
1889 let late_slot = B256::repeat_byte(0x33);
1890 let revealed_value = alloy_rlp::encode_fixed_size(&U256::from(7)).to_vec();
1891 let late_value = alloy_rlp::encode_fixed_size(&U256::from(9)).to_vec();
1892 let later_value = alloy_rlp::encode_fixed_size(&U256::from(11)).to_vec();
1893
1894 let mut state = HashedPostState::default();
1895 state.accounts.insert(address, Some(Account { nonce: 1, ..Default::default() }));
1896 state.storages.entry(address).or_default().storage.insert(revealed_slot, U256::from(7));
1897 task.on_hashed_state_update(state);
1898 task.pending_updates = 1;
1899 task.apply_new_updates().unwrap();
1900
1901 let work = check_out_storage(&mut task, address);
1903 task.finished_state_updates = true;
1904
1905 assert!(task.has_pending_sparse_trie_updates());
1906 assert!(
1907 task.ensure_not_stalled(false).is_ok(),
1908 "a checked out payload can still deliver progress"
1909 );
1910
1911 let leaf = ProofTrieNodeV2 {
1914 path: Nibbles::default(),
1915 node: TrieNodeV2::Leaf(LeafNode::new(
1916 Nibbles::unpack(revealed_slot),
1917 revealed_value.clone(),
1918 )),
1919 masks: None,
1920 };
1921 task.on_proof_result(DecodedMultiProofV2 {
1922 storage_proofs: B256Map::from_iter([(address, vec![leaf])]),
1923 ..Default::default()
1924 })
1925 .unwrap();
1926 assert!(!task.trie.storage_tries_mut().contains_key(&address));
1927 let StorageTrieState::InFlight(in_flight) = &task.storage[&address] else {
1928 panic!("payload is out with a job")
1929 };
1930 assert_eq!(in_flight.proofs.len(), 1);
1931
1932 let mut state = HashedPostState::default();
1934 state.storages.entry(address).or_default().storage.insert(late_slot, U256::from(9));
1935 task.on_hashed_state_update(state);
1936 task.pending_updates = 1;
1937 task.apply_new_updates().unwrap();
1938 let StorageTrieState::InFlight(in_flight) = &task.storage[&address] else {
1939 panic!("payload is out with a job")
1940 };
1941 assert!(in_flight.updates.contains_key(&late_slot));
1942
1943 return_storage(&mut task, address, work);
1944 assert_eq!(task.storage_in_flight, 0);
1945
1946 assert_eq!(task.pending_updates, 0);
1948 task.process_new_updates().unwrap();
1949 assert_eq!(storage_slot_value(&task, &address, &revealed_slot), Some(revealed_value));
1950 assert_eq!(storage_slot_value(&task, &address, &late_slot), Some(late_value));
1951
1952 let StorageTrieState::Idle(work) = &task.storage[&address] else {
1953 panic!("payload is back")
1954 };
1955 assert!(work.pending.is_empty());
1956 assert!(work.trie.is_root_cached(), "a drained payload hashes its trie");
1957 let root_before = storage_root_of(&mut task, address);
1958
1959 let work = check_out_storage(&mut task, address);
1961 let mut state = HashedPostState::default();
1962 state.storages.entry(address).or_default().storage.insert(late_slot, U256::from(11));
1963 task.on_hashed_state_update(state);
1964 task.pending_updates = 1;
1965 task.apply_new_updates().unwrap();
1966 return_storage(&mut task, address, work);
1967
1968 let StorageTrieState::Idle(work) = &task.storage[&address] else {
1969 panic!("payload is back")
1970 };
1971 assert!(work.has_work(), "a buffered update makes the payload ready again");
1972
1973 task.process_new_updates().unwrap();
1974 assert_eq!(storage_slot_value(&task, &address, &late_slot), Some(later_value));
1975 assert_ne!(storage_root_of(&mut task, address), root_before);
1976
1977 drop(updates_tx);
1978 drop(task);
1979 drain_sparse_trie_tasks(&runtime);
1980 }
1981
1982 #[test]
1983 #[allow(clippy::clone_on_copy)]
1984 fn in_flight_storage_updates_keep_latest_values_and_deletions() {
1985 let runtime = Runtime::test();
1986 let trie = SparseStateTrie::default()
1987 .with_accounts_trie(RevealableSparseTrie::revealed_empty())
1988 .with_updates(true);
1989 let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, trie);
1990 let address = B256::repeat_byte(0x11);
1991 let removed_slot = B256::repeat_byte(0x20);
1992 let changed_slot = B256::repeat_byte(0x30);
1993 let new_slot = B256::repeat_byte(0x31);
1994 let mut sibling_slot = changed_slot;
1996 sibling_slot[1] = 0x40;
1997 let account = Account { nonce: 1, ..Default::default() };
1998 let parent_leaf = ProofTrieNodeV2 {
1999 path: Nibbles::default(),
2000 node: TrieNodeV2::Leaf(LeafNode::new(
2001 Nibbles::unpack(removed_slot),
2002 alloy_rlp::encode(U256::from(7)),
2003 )),
2004 masks: None,
2005 };
2006
2007 let mut state = HashedPostState::default();
2008 state.accounts.insert(address, Some(account.clone()));
2009 state.storages.entry(address).or_default().storage.extend([
2010 (removed_slot, U256::from(8)),
2011 (changed_slot, U256::from(9)),
2012 (new_slot, U256::from(1)),
2013 (sibling_slot, U256::from(2)),
2014 ]);
2015 task.on_hashed_state_update(state);
2016 task.pending_updates = 1;
2017 task.apply_new_updates().unwrap();
2018
2019 let mut work = check_out_storage(&mut task, address);
2020 let output = work.run(task.new_epoch, true);
2021 assert_eq!(work.pending.len(), 4, "the blind pass keeps its original updates");
2022 assert_eq!(output.targets.len(), 4);
2023
2024 let mut state = HashedPostState::default();
2026 state
2027 .storages
2028 .entry(address)
2029 .or_default()
2030 .storage
2031 .extend([(removed_slot, U256::ZERO), (changed_slot, U256::from(11))]);
2032 task.on_hashed_state_update(state);
2033 task.pending_updates = 1;
2034 task.apply_new_updates().unwrap();
2035 task.on_prewarm_targets(MultiProofTargetsV2 {
2036 storage_targets: B256Map::from_iter([(
2037 address,
2038 vec![ProofV2Target::new(removed_slot), ProofV2Target::new(changed_slot)],
2039 )]),
2040 ..Default::default()
2041 });
2042 task.pending_updates = 1;
2043 task.apply_new_updates().unwrap();
2044
2045 task.on_storage_trie_returned(StorageTrieJobDone { address, work, output }).unwrap();
2046 let (targets, _) = task.pending_targets.take();
2048 assert_eq!(targets.storage_targets[&address].len(), 4);
2049 task.on_proof_result(DecodedMultiProofV2 {
2050 storage_proofs: B256Map::from_iter([(address, vec![parent_leaf.clone()])]),
2051 ..Default::default()
2052 })
2053 .unwrap();
2054 updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
2055 let outcome = task.run().unwrap();
2056
2057 let expected_root = reth_trie_common::root::storage_root_unsorted([
2058 (changed_slot, U256::from(11)),
2059 (new_slot, U256::from(1)),
2060 (sibling_slot, U256::from(2)),
2061 ]);
2062 assert_eq!(
2063 outcome.state_root,
2064 reth_trie_common::root::state_root_unsorted([(
2065 address,
2066 account.into_trie_account(expected_root),
2067 )])
2068 );
2069
2070 let mut serial = RevealableSparseTrie::<ArenaParallelSparseTrie>::blind();
2071 serial.reveal_v2_proof_nodes(&mut [parent_leaf], true).unwrap();
2072 let mut final_updates = B256Map::from_iter([
2073 (removed_slot, LeafUpdate::Changed(Vec::new())),
2074 (changed_slot, LeafUpdate::Changed(alloy_rlp::encode(U256::from(11)))),
2075 (new_slot, LeafUpdate::Changed(alloy_rlp::encode(U256::from(1)))),
2076 (sibling_slot, LeafUpdate::Changed(alloy_rlp::encode(U256::from(2)))),
2077 ]);
2078 serial
2079 .update_leaves(&mut final_updates, |_, _| panic!("parent is fully revealed"))
2080 .unwrap();
2081 let (serial_root, serial_updates) = serial.root_with_updates(task.new_epoch).unwrap();
2082 assert_eq!(serial_root, expected_root);
2083 assert!(serial_updates.iter().any(|(_, node)| node.is_some()));
2084 let updates = &outcome.trie_updates.storage_tries_ref()[&address];
2085 assert_eq!(updates.storage_nodes, serial_updates);
2086
2087 let (mut reused, _) = task.into_trie_for_reuse();
2088 assert_eq!(reused.storage_root(&address, TrieNodeEpoch::new(2)), Some(expected_root));
2089 assert!(reused.take_trie_updates().unwrap().is_empty());
2090 drop(updates_tx);
2091 drain_sparse_trie_tasks(&runtime);
2092 }
2093
2094 #[test]
2095 fn unrelated_storage_proofs_do_not_retry_blocked_leaves() {
2096 let runtime = Runtime::test();
2097 let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, SparseStateTrie::default());
2098 let address = B256::repeat_byte(0x11);
2099 let slot = B256::repeat_byte(0x22);
2100 let StorageTrieState::Idle(work) = task.storage_trie_state_mut(address) else {
2101 unreachable!()
2102 };
2103 work.queue_updates(B256Map::from_iter([(slot, LeafUpdate::Changed(vec![1]))]));
2104 task.run_ready_storage_work().unwrap();
2105 assert_eq!(task.storage_cache_misses, 1);
2106
2107 task.on_proof_result(DecodedMultiProofV2 {
2108 storage_proofs: B256Map::from_iter([(
2109 B256::repeat_byte(0x33),
2110 vec![ProofTrieNodeV2::empty()],
2111 )]),
2112 ..Default::default()
2113 })
2114 .unwrap();
2115 assert_eq!(task.storage_cache_misses, 1, "unrelated reveals cannot unblock this trie");
2116 assert_eq!(storage_slot_value(&task, &address, &slot), None);
2117
2118 task.on_proof_result(DecodedMultiProofV2 {
2119 storage_proofs: B256Map::from_iter([(address, vec![ProofTrieNodeV2::empty()])]),
2120 ..Default::default()
2121 })
2122 .unwrap();
2123 assert_eq!(storage_slot_value(&task, &address, &slot), Some(vec![1]));
2124 assert_eq!(task.storage_cache_hits, 1);
2125 assert!(!task.has_ready_storage_work());
2126
2127 drop(updates_tx);
2128 drop(task);
2129 drain_sparse_trie_tasks(&runtime);
2130 }
2131
2132 #[test]
2133 fn canceled_task_disconnects_checked_out_storage_jobs() {
2134 let runtime = Runtime::test();
2135 let trie = SparseStateTrie::default()
2136 .with_default_storage_trie(RevealableSparseTrie::revealed_empty());
2137 let (mut task, updates_tx, cancel_guard) = test_task(&runtime, trie);
2138 let idle_address = B256::repeat_byte(0x11);
2139 let address = B256::repeat_byte(0x22);
2140 task.storage_trie_state_mut(idle_address);
2141 task.storage_trie_state_mut(address);
2142 let work = check_out_storage(&mut task, address);
2143 let done_tx = task.storage_done_tx.clone();
2144 let epoch = task.new_epoch;
2145
2146 drop(cancel_guard);
2147 assert!(matches!(task.run(), Err(StateRootTaskError::Canceled)));
2148 let (trie, _) = task.into_cleared_trie();
2149 assert_eq!(trie.retained_storage_tries_count(), 1, "only the idle trie can be reused");
2150
2151 let done = StorageTrieJob { address, work }.run(epoch, false);
2153 assert!(done_tx.send(StorageJobMessage::Done(done)).is_err());
2154 drop(updates_tx);
2155 drain_sparse_trie_tasks(&runtime);
2156 }
2157
2158 #[test]
2159 fn storage_job_panics_are_resumed_on_the_task() {
2160 let runtime = Runtime::test();
2161 let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, SparseStateTrie::default());
2162 let address = B256::repeat_byte(0x11);
2163 let StorageTrieState::Idle(work) = task.storage_trie_state_mut(address) else {
2164 unreachable!()
2165 };
2166 work.queue_proofs(&mut vec![ProofTrieNodeV2 {
2168 path: Nibbles::default(),
2169 node: TrieNodeV2::Extension(ExtensionNode::new(
2170 Nibbles::from_nibbles([1]),
2171 RlpNode::word_rlp(&B256::ZERO),
2172 )),
2173 masks: None,
2174 }]);
2175 let work = check_out_storage(&mut task, address);
2176 task.spawn_storage_jobs(vec![StorageTrieJob { address, work }]);
2177
2178 let message = receive_storage_job(&task);
2179 assert!(
2180 matches!(&message, StorageJobMessage::Panicked { address: returned, .. } if *returned == address)
2181 );
2182 let panic = panic::catch_unwind(AssertUnwindSafe(|| task.on_storage_job_message(message)));
2183 assert!(panic.is_err());
2184 assert_eq!(task.storage_in_flight, 0);
2185 assert!(!task.storage.contains_key(&address));
2186
2187 drop(updates_tx);
2188 drop(task);
2189 drain_sparse_trie_tasks(&runtime);
2190 }
2191
2192 #[test]
2193 fn large_storage_batches_run_off_thread() {
2194 let runtime = Runtime::test();
2195 let trie = SparseStateTrie::default()
2196 .with_accounts_trie(RevealableSparseTrie::<ArenaParallelSparseTrie>::revealed_empty())
2197 .with_default_storage_trie(RevealableSparseTrie::blind_from(
2198 ArenaParallelSparseTrie::default(),
2199 ))
2200 .with_updates(true);
2201 let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, trie);
2202
2203 let leaves = (0..=INLINE_STORAGE_WORK_UNITS as u8)
2205 .map(|index| {
2206 let address = B256::repeat_byte(0x10 + index);
2207 let slot = B256::repeat_byte(0x80 + index);
2208 let value = alloy_rlp::encode_fixed_size(&U256::from(index + 1)).to_vec();
2209 (address, slot, value)
2210 })
2211 .collect::<Vec<_>>();
2212 let storage_proofs = leaves
2213 .iter()
2214 .map(|(address, slot, value)| {
2215 let leaf = ProofTrieNodeV2 {
2216 path: Nibbles::default(),
2217 node: TrieNodeV2::Leaf(LeafNode::new(Nibbles::unpack(slot), value.clone())),
2218 masks: None,
2219 };
2220 (*address, vec![leaf])
2221 })
2222 .collect();
2223
2224 task.on_proof_result(DecodedMultiProofV2 { account_proofs: Vec::new(), storage_proofs })
2225 .unwrap();
2226
2227 assert_eq!(task.storage_in_flight, leaves.len());
2228 assert!(task.has_pending_sparse_trie_updates(), "completion must wait for the reveals");
2229
2230 while task.storage_in_flight > 0 {
2231 let message = receive_storage_job(&task);
2232 task.on_storage_job_message(message).unwrap();
2233 }
2234 for (address, slot, value) in &leaves {
2235 assert_eq!(storage_slot_value(&task, address, slot).as_ref(), Some(value));
2236 }
2237
2238 drop(updates_tx);
2239 drop(task);
2240 drain_sparse_trie_tasks(&runtime);
2241 }
2242
2243 #[test]
2244 #[allow(clippy::clone_on_copy)]
2245 fn run_waits_for_storage_tries_hashed_off_thread() {
2246 let runtime = Runtime::test();
2247 let default_trie = RevealableSparseTrie::<ArenaParallelSparseTrie>::revealed_empty();
2248 let trie = SparseStateTrie::default()
2249 .with_accounts_trie(default_trie.clone())
2250 .with_default_storage_trie(default_trie)
2251 .with_updates(true);
2252 let (mut task, updates_tx, _cancel_guard) = test_task(&runtime, trie);
2253
2254 let accounts = (0..8u8)
2255 .map(|index| {
2256 let address = B256::repeat_byte(0x10 + index);
2257 let account = Account {
2258 nonce: u64::from(index) + 1,
2259 balance: U256::from(index),
2260 ..Default::default()
2261 };
2262 let storage = (0..4u8)
2263 .map(|slot| {
2264 (
2265 B256::repeat_byte(0x40 + index * 4 + slot),
2266 U256::from(slot) + U256::from(1),
2267 )
2268 })
2269 .collect::<Vec<_>>();
2270 (address, account, storage)
2271 })
2272 .collect::<Vec<_>>();
2273
2274 let mut state = HashedPostState::default();
2275 for (address, account, storage) in &accounts {
2276 state.accounts.insert(*address, Some(account.clone()));
2277 state.storages.entry(*address).or_default().storage.extend(storage.iter().copied());
2278 }
2279 updates_tx.send(StateRootMessage::HashedStateUpdate(state)).unwrap();
2280 updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
2281
2282 let outcome = task.run().expect("state root computation should succeed");
2283
2284 let expected = accounts.iter().map(|(address, account, storage)| {
2285 let storage_root =
2286 reth_trie_common::root::storage_root_unsorted(storage.iter().copied());
2287 (*address, account.clone().into_trie_account(storage_root))
2288 });
2289 assert_eq!(outcome.state_root, reth_trie_common::root::state_root_unsorted(expected));
2290 assert_eq!(task.storage_in_flight, 0);
2291 assert!(task.storage.is_empty(), "every storage trie is back in the state trie");
2292
2293 drop(updates_tx);
2294 drop(task);
2295 drain_sparse_trie_tasks(&runtime);
2296 }
2297
2298 #[test]
2299 fn first_leaf_batch_starts_proofs_before_input_queue_drains() {
2300 let runtime = reth_tasks::Runtime::test();
2301 let provider_factory = create_test_provider_factory();
2302 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
2303 let state_provider_factory = OverlayStateProviderFactory::new(
2304 provider_factory,
2305 OverlayManager::<reth_chain_state::EthPrimitives>::default()
2306 .overlay_builder(anchor_hash),
2307 );
2308 let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
2309 let proof_worker_handle = ProofWorkerHandle::new(
2310 &runtime,
2311 ProofTaskCtx::new(state_provider_factory),
2312 false,
2313 proof_result_tx.clone(),
2314 );
2315
2316 let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
2317 let trie = SparseStateTrie::default()
2318 .with_accounts_trie(default_trie.clone())
2319 .with_default_storage_trie(default_trie)
2320 .with_updates(true);
2321
2322 let parent_state_root = B256::from([0x55; 32]);
2323 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
2324 let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
2325 let mut task = SparseTrieCacheTask::new_with_trie(
2326 &runtime,
2327 updates_rx,
2328 cancel_rx,
2329 std::sync::mpsc::channel().0,
2330 proof_worker_handle,
2331 proof_result_tx,
2332 proof_result_rx,
2333 SparseTrieTaskMetrics::default(),
2334 trie,
2335 parent_state_root,
2336 TrieNodeEpoch::UNMODIFIED,
2337 1,
2338 );
2339
2340 updates_tx.send(StateRootMessage::PrefetchProofs(Default::default())).unwrap();
2342 let deadline = std::time::Instant::now();
2343 while task.updates.is_empty() {
2344 assert!(deadline.elapsed() < std::time::Duration::from_secs(1));
2345 std::thread::yield_now();
2346 }
2347 for index in 0..INITIAL_UPDATE_BATCH_SIZE {
2348 let mut state = HashedPostState::default();
2349 state.accounts.insert(
2350 B256::repeat_byte(index as u8),
2351 Some(Account { nonce: 1, ..Default::default() }),
2352 );
2353 task.on_hashed_state_update(state);
2354 task.pending_updates += 1;
2355 assert!(!task.make_progress().unwrap());
2356 if index + 1 < INITIAL_UPDATE_BATCH_SIZE {
2357 assert_eq!(task.in_flight_proof_batches, 0);
2358 }
2359 }
2360 assert!(task.in_flight_proof_batches > 0, "proof work must start before the queue drains");
2361 assert_eq!(task.pending_updates, 0);
2362
2363 for index in INITIAL_UPDATE_BATCH_SIZE..INITIAL_UPDATE_BATCH_SIZE * 2 {
2366 let mut state = HashedPostState::default();
2367 state.accounts.insert(
2368 B256::repeat_byte(index as u8),
2369 Some(Account { nonce: 1, ..Default::default() }),
2370 );
2371 task.on_hashed_state_update(state);
2372 task.pending_updates += 1;
2373 assert!(!task.make_progress().unwrap());
2374 }
2375 assert_eq!(task.pending_updates, INITIAL_UPDATE_BATCH_SIZE);
2376 assert_eq!(task.new_account_updates.len(), INITIAL_UPDATE_BATCH_SIZE);
2377 drop(updates_tx);
2378 drop(task);
2379 drain_sparse_trie_tasks(&runtime);
2380 }
2381
2382 #[test]
2383 fn run_returns_parent_root_without_revealing_blind_trie_when_no_state_updates() {
2384 let runtime = reth_tasks::Runtime::test();
2385 let provider_factory = create_test_provider_factory();
2386 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
2387 let state_provider_factory = OverlayStateProviderFactory::new(
2388 provider_factory,
2389 OverlayManager::<reth_chain_state::EthPrimitives>::default()
2390 .overlay_builder(anchor_hash),
2391 );
2392 let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
2393 let proof_worker_handle = ProofWorkerHandle::new(
2394 &runtime,
2395 ProofTaskCtx::new(state_provider_factory),
2396 false,
2397 proof_result_tx.clone(),
2398 );
2399
2400 let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
2401 let trie = SparseStateTrie::default()
2402 .with_accounts_trie(default_trie.clone())
2403 .with_default_storage_trie(default_trie)
2404 .with_updates(true);
2405
2406 let parent_state_root = B256::from([0x55; 32]);
2407 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
2408 let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
2409 let mut task = SparseTrieCacheTask::new_with_trie(
2410 &runtime,
2411 updates_rx,
2412 cancel_rx,
2413 std::sync::mpsc::channel().0,
2414 proof_worker_handle,
2415 proof_result_tx,
2416 proof_result_rx,
2417 SparseTrieTaskMetrics::default(),
2418 trie,
2419 parent_state_root,
2420 TrieNodeEpoch::UNMODIFIED,
2421 1,
2422 );
2423
2424 updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
2425 drop(updates_tx);
2426
2427 let outcome = task.run().expect("state root computation should succeed");
2428
2429 assert_eq!(outcome.state_root, parent_state_root);
2430 assert!(outcome.trie_updates.is_empty());
2431 assert!(task.trie.state_trie_ref().is_none(), "blind trie should not be revealed");
2432
2433 drop(task);
2434 drain_sparse_trie_tasks(&runtime);
2435 }
2436
2437 #[test]
2438 fn stall_check_waits_for_in_flight_proofs_then_reports_pending_updates() {
2439 let runtime = reth_tasks::Runtime::test();
2440 let provider_factory = create_test_provider_factory();
2441 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
2442 let state_provider_factory = OverlayStateProviderFactory::new(
2443 provider_factory,
2444 OverlayManager::<reth_chain_state::EthPrimitives>::default()
2445 .overlay_builder(anchor_hash),
2446 );
2447 let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
2448 let proof_worker_handle = ProofWorkerHandle::new(
2449 &runtime,
2450 ProofTaskCtx::new(state_provider_factory),
2451 false,
2452 proof_result_tx.clone(),
2453 );
2454
2455 let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
2456 let trie = SparseStateTrie::default()
2457 .with_accounts_trie(default_trie.clone())
2458 .with_default_storage_trie(default_trie)
2459 .with_updates(true);
2460
2461 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
2462 let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
2463 let mut task = SparseTrieCacheTask::new_with_trie(
2464 &runtime,
2465 updates_rx,
2466 cancel_rx,
2467 std::sync::mpsc::channel().0,
2468 proof_worker_handle,
2469 proof_result_tx,
2470 proof_result_rx,
2471 SparseTrieTaskMetrics::default(),
2472 trie,
2473 B256::from([0x55; 32]),
2474 TrieNodeEpoch::UNMODIFIED,
2475 1,
2476 );
2477
2478 drop(updates_tx);
2479
2480 let account = B256::from([0x11; 32]);
2481 let slot = B256::from([0x22; 32]);
2482 let account_target = B256::from([0x33; 32]);
2483 let storage_target = B256::from([0x44; 32]);
2484
2485 task.finished_state_updates = true;
2486 task.account_updates.insert(account, LeafUpdate::Touched);
2487 task.pending_account_updates.insert(account, None);
2488 task.fetched_account_targets.insert(account_target, ProofV2TargetParent::NONE);
2489
2490 let StorageTrieState::Idle(work) = task.storage_trie_state_mut(account) else {
2492 panic!("a fresh trie state is idle")
2493 };
2494 work.updated = true;
2495 work.pending.insert(slot, LeafUpdate::Touched);
2496 work.fetched.insert(storage_target, ProofV2TargetParent::new(11));
2497 task.in_flight_proof_batches = 1;
2498
2499 assert!(task.ensure_not_stalled(false).is_ok());
2500
2501 let result = ProofResultMessage {
2502 result: Ok(DecodedMultiProofV2::default()),
2503 elapsed: std::time::Duration::ZERO,
2504 state: HashedPostState::default(),
2505 };
2506 task.on_proof_result_message(result).expect("proof result should be ok");
2507
2508 assert_eq!(task.in_flight_proof_batches, 0);
2509 let error = task.ensure_not_stalled(false).expect_err("task should be stalled");
2510 assert!(matches!(error, StateRootTaskError::Stalled));
2511 let error = error.to_string();
2512
2513 assert!(error.contains("sparse trie task stalled"));
2514 assert!(!error.contains("account_targets"));
2515 assert!(!error.contains("storage_targets"));
2516 assert!(!error.contains(&format!("{account:?}")));
2517 assert!(!error.contains(&format!("{account_target:?}")));
2518 assert!(!error.contains(&format!("{storage_target:?}")));
2519 assert!(!error.contains("pending_account_leaves"));
2520 assert!(!error.contains("pending_storage_leaves"));
2521 assert!(!error.contains("pending_account_updates"));
2522 assert!(!error.contains(&format!("{slot:?}")));
2523
2524 drop(task);
2525 drain_sparse_trie_tasks(&runtime);
2526 }
2527
2528 #[test]
2529 fn run_errors_when_cancel_guard_drops_before_updates_finish() {
2530 let runtime = reth_tasks::Runtime::test();
2531 let provider_factory = create_test_provider_factory();
2532 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
2533 let state_provider_factory = OverlayStateProviderFactory::new(
2534 provider_factory,
2535 OverlayManager::<reth_chain_state::EthPrimitives>::default()
2536 .overlay_builder(anchor_hash),
2537 );
2538 let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
2539 let proof_worker_handle = ProofWorkerHandle::new(
2540 &runtime,
2541 ProofTaskCtx::new(state_provider_factory),
2542 false,
2543 proof_result_tx.clone(),
2544 );
2545
2546 let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
2547 let trie = SparseStateTrie::default()
2548 .with_accounts_trie(default_trie.clone())
2549 .with_default_storage_trie(default_trie)
2550 .with_updates(true);
2551
2552 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
2553 let (cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
2554 let mut task = SparseTrieCacheTask::new_with_trie(
2555 &runtime,
2556 updates_rx,
2557 cancel_rx,
2558 std::sync::mpsc::channel().0,
2559 proof_worker_handle,
2560 proof_result_tx,
2561 proof_result_rx,
2562 SparseTrieTaskMetrics::default(),
2563 trie,
2564 B256::from([0x55; 32]),
2565 TrieNodeEpoch::UNMODIFIED,
2566 1,
2567 );
2568
2569 drop(cancel_guard);
2572
2573 let error = task.run().expect_err("canceled task must return an error");
2574 assert!(matches!(error, StateRootTaskError::Canceled));
2575
2576 drop(updates_tx);
2577 drop(task);
2578 drain_sparse_trie_tasks(&runtime);
2579 }
2580
2581 #[test]
2582 fn run_ignores_hints_queued_after_updates_finish() {
2583 let runtime = reth_tasks::Runtime::test();
2584 let provider_factory = create_test_provider_factory();
2585 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
2586 let state_provider_factory = OverlayStateProviderFactory::new(
2587 provider_factory,
2588 OverlayManager::<reth_chain_state::EthPrimitives>::default()
2589 .overlay_builder(anchor_hash),
2590 );
2591 let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
2592 let proof_worker_handle = ProofWorkerHandle::new(
2593 &runtime,
2594 ProofTaskCtx::new(state_provider_factory),
2595 false,
2596 proof_result_tx.clone(),
2597 );
2598
2599 let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
2600 let trie = SparseStateTrie::default()
2601 .with_accounts_trie(default_trie.clone())
2602 .with_default_storage_trie(default_trie)
2603 .with_updates(true);
2604
2605 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
2606 let (cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
2607 let mut task = SparseTrieCacheTask::new_with_trie(
2608 &runtime,
2609 updates_rx,
2610 cancel_rx,
2611 std::sync::mpsc::channel().0,
2612 proof_worker_handle,
2613 proof_result_tx,
2614 proof_result_rx,
2615 SparseTrieTaskMetrics::default(),
2616 trie,
2617 B256::from([0x55; 32]),
2618 TrieNodeEpoch::UNMODIFIED,
2619 1,
2620 );
2621
2622 updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
2623 updates_tx.send(StateRootMessage::PrefetchProofs(Default::default())).unwrap();
2624
2625 let wait_start = std::time::Instant::now();
2626 while task.updates.len() < 2 {
2627 assert!(
2628 wait_start.elapsed() < std::time::Duration::from_secs(1),
2629 "hashing task did not queue the test messages"
2630 );
2631 std::thread::yield_now();
2632 }
2633
2634 let (result_tx, result_rx) = std::sync::mpsc::channel();
2635 let handle = std::thread::spawn(move || {
2636 let _ = result_tx.send(task.run());
2637 });
2638
2639 let result = result_rx.recv_timeout(std::time::Duration::from_secs(1));
2640 drop(cancel_guard);
2641 handle.join().unwrap();
2642
2643 assert!(result.expect("state root task stalled on a late hint").is_ok());
2644
2645 drop(updates_tx);
2646 drain_sparse_trie_tasks(&runtime);
2647 }
2648}