1use std::sync::Arc;
4
5use super::{evm_state_to_hashed_post_state, StateRootComputeOutcome, StateRootMessage};
6use alloy_primitives::{
7 map::{hash_map::Entry, B256Map},
8 B256,
9};
10use alloy_rlp::{Decodable, Encodable};
11use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
12use metrics::{Gauge, Histogram};
13use rayon::iter::{IntoParallelIterator, ParallelIterator};
14use reth_metrics::Metrics;
15use reth_primitives_traits::{Account, FastInstant as Instant};
16use reth_tasks::Runtime;
17use reth_trie::{
18 updates::TrieUpdates, DecodedMultiProofV2, HashedPostState, TrieAccount, EMPTY_ROOT_HASH,
19 TRIE_ACCOUNT_RLP_MAX_SIZE,
20};
21use reth_trie_common::{MultiProofTargetsV2, ProofV2Target, ProofV2TargetParent};
22use reth_trie_parallel::{
23 error::StateRootTaskError,
24 proof_task::{
25 AccountMultiproofInput, ProofResultContext, ProofResultMessage, ProofResultSender,
26 ProofWorkerHandle,
27 },
28};
29use reth_trie_sparse::{
30 errors::{SparseStateTrieErrorKind, SparseTrieErrorKind, SparseTrieResult},
31 ArenaParallelSparseTrie, DeferredDrops, LeafUpdate, RevealableSparseTrie, SparseStateTrie,
32 SparseTrie, TrieNodeEpoch,
33};
34use tracing::{debug, debug_span, error, instrument, trace_span};
35
36pub(super) struct SparseTrieCacheTask<A = ArenaParallelSparseTrie, S = ArenaParallelSparseTrie> {
38 proof_result_tx: ProofResultSender,
40 proof_result_rx: CrossbeamReceiver<ProofResultMessage>,
42 updates: CrossbeamReceiver<SparseTrieTaskMessage>,
44 cancel_rx: CrossbeamReceiver<()>,
48 final_hashed_state_tx: Option<std::sync::mpsc::Sender<Arc<HashedPostState>>>,
50 trie: SparseStateTrie<A, S>,
52 parent_state_root: B256,
54 new_epoch: TrieNodeEpoch,
56 proof_worker_handle: ProofWorkerHandle,
58
59 chunk_size: usize,
62 max_targets_for_chunking: usize,
66
67 account_updates: B256Map<LeafUpdate>,
69 storage_updates: B256Map<B256Map<LeafUpdate>>,
71
72 new_account_updates: B256Map<LeafUpdate>,
74 new_storage_updates: B256Map<B256Map<LeafUpdate>>,
76 pending_account_updates: B256Map<Option<Option<Account>>>,
90 fetched_account_targets: B256Map<ProofV2TargetParent>,
94 fetched_storage_targets: B256Map<B256Map<ProofV2TargetParent>>,
97 account_rlp_buf: Vec<u8>,
99 finished_state_updates: bool,
101 account_cache_hits: u64,
103 account_cache_misses: u64,
105 storage_cache_hits: u64,
107 storage_cache_misses: u64,
109 pending_targets: PendingTargets,
111 in_flight_proof_batches: usize,
113 pending_updates: usize,
116 final_hashed_state: HashedPostState,
122
123 metrics: SparseTrieTaskMetrics,
125}
126
127impl<A, S> SparseTrieCacheTask<A, S>
128where
129 A: SparseTrie + Default,
130 S: SparseTrie + Default + Clone,
131{
132 #[expect(clippy::too_many_arguments)]
134 pub(super) fn new_with_trie(
135 executor: &Runtime,
136 updates: CrossbeamReceiver<StateRootMessage>,
137 cancel_rx: CrossbeamReceiver<()>,
138 final_hashed_state_tx: std::sync::mpsc::Sender<Arc<HashedPostState>>,
139 proof_worker_handle: ProofWorkerHandle,
140 proof_result_tx: ProofResultSender,
141 proof_result_rx: CrossbeamReceiver<ProofResultMessage>,
142 metrics: SparseTrieTaskMetrics,
143 trie: SparseStateTrie<A, S>,
144 parent_state_root: B256,
145 new_epoch: TrieNodeEpoch,
146 chunk_size: usize,
147 ) -> Self {
148 let (hashed_state_tx, hashed_state_rx) = crossbeam_channel::unbounded();
149
150 let parent_span = tracing::Span::current();
151 let hashing_metrics = metrics.clone();
152 executor.spawn_blocking_named("trie-hashing", move || {
153 let _span = trace_span!(parent: parent_span, "run_hashing_task").entered();
154 Self::run_hashing_task(updates, hashed_state_tx, hashing_metrics)
155 });
156
157 Self {
158 proof_result_tx,
159 proof_result_rx,
160 updates: hashed_state_rx,
161 cancel_rx,
162 proof_worker_handle,
163 final_hashed_state_tx: Some(final_hashed_state_tx),
164 trie,
165 parent_state_root,
166 new_epoch,
167 chunk_size,
168 max_targets_for_chunking: DEFAULT_MAX_TARGETS_FOR_CHUNKING,
169 account_updates: Default::default(),
170 storage_updates: Default::default(),
171 new_account_updates: Default::default(),
172 new_storage_updates: Default::default(),
173 pending_account_updates: Default::default(),
174 fetched_account_targets: Default::default(),
175 fetched_storage_targets: Default::default(),
176 account_rlp_buf: Vec::with_capacity(TRIE_ACCOUNT_RLP_MAX_SIZE),
177 finished_state_updates: Default::default(),
178 account_cache_hits: 0,
179 account_cache_misses: 0,
180 storage_cache_hits: 0,
181 storage_cache_misses: 0,
182 pending_targets: Default::default(),
183 in_flight_proof_batches: 0,
184 pending_updates: Default::default(),
185 final_hashed_state: Default::default(),
186 metrics,
187 }
188 }
189
190 fn run_hashing_task(
193 updates: CrossbeamReceiver<StateRootMessage>,
194 hashed_state_tx: CrossbeamSender<SparseTrieTaskMessage>,
195 metrics: SparseTrieTaskMetrics,
196 ) {
197 let mut total_idle_time = std::time::Duration::ZERO;
198 let mut idle_start = Instant::now();
199
200 while let Ok(message) = updates.recv() {
201 total_idle_time += idle_start.elapsed();
202
203 let msg = match message {
204 StateRootMessage::PrefetchProofs(targets) => {
205 SparseTrieTaskMessage::PrefetchProofs(targets)
206 }
207 StateRootMessage::StateUpdate(state) => {
208 let _span = trace_span!(target: "engine::tree::payload_processor::sparse_trie", "hashing_state_update", n = state.len()).entered();
209 let hashed = evm_state_to_hashed_post_state(state);
210 SparseTrieTaskMessage::HashedState(hashed)
211 }
212 StateRootMessage::FinishedStateUpdates => {
213 SparseTrieTaskMessage::FinishedStateUpdates
214 }
215 StateRootMessage::HashedStateUpdate(state) => {
216 SparseTrieTaskMessage::HashedState(state)
217 }
218 };
219 if hashed_state_tx.send(msg).is_err() {
220 break;
221 }
222
223 idle_start = Instant::now();
224 }
225
226 metrics.hashing_task_idle_time_seconds.record(total_idle_time.as_secs_f64());
227 }
228
229 pub(super) fn into_trie_for_reuse(self) -> (SparseStateTrie<A, S>, DeferredDrops) {
233 let Self { mut trie, .. } = self;
234 let deferred = trie.take_deferred_drops();
235 (trie, deferred)
236 }
237
238 pub(super) fn into_cleared_trie(self) -> (SparseStateTrie<A, S>, DeferredDrops) {
243 let Self { mut trie, .. } = self;
244 trie.clear();
245 let deferred = trie.take_deferred_drops();
246 (trie, deferred)
247 }
248
249 #[instrument(
256 name = "SparseTrieCacheTask::run",
257 level = "debug",
258 target = "engine::tree::payload_processor::sparse_trie",
259 skip_all
260 )]
261 pub(super) fn run(&mut self) -> Result<StateRootComputeOutcome, StateRootTaskError> {
262 let now = Instant::now();
263
264 let mut total_idle_time = std::time::Duration::ZERO;
265 let mut idle_start = Instant::now();
266 let mut done = false;
267 let mut finalized_hashed_state = None;
268
269 while !self.finished_state_updates {
273 let mut t = Instant::now();
274 crossbeam_channel::select_biased! {
275 recv(self.updates) -> message => {
276 let wake = Instant::now();
277 total_idle_time += wake.duration_since(idle_start);
278 self.metrics
279 .sparse_trie_channel_wait_duration_histogram
280 .record(wake.duration_since(t));
281
282 let update = message.map_err(|_| StateRootTaskError::Other(
283 "updates channel disconnected before state root calculation".to_string(),
284 ))?;
285 if let Some(hashed_state) = self.on_message(update) {
286 finalized_hashed_state = Some(hashed_state);
287 }
288 self.pending_updates += 1;
289 }
290 recv(self.proof_result_rx) -> message => {
291 let wake = Instant::now();
292 total_idle_time += wake.duration_since(idle_start);
293 self.metrics
294 .sparse_trie_channel_wait_duration_histogram
295 .record(wake.duration_since(t));
296 t = wake;
297
298 let Ok(result) = message else {
299 unreachable!("we own the sender half")
300 };
301 self.on_proof_results(result, &mut t)?;
302 },
303 recv(self.cancel_rx) -> _ => return Err(StateRootTaskError::Canceled),
304 }
305
306 done = self.make_progress()?;
307 idle_start = Instant::now();
308 }
309
310 while !done {
315 let mut t = Instant::now();
316 crossbeam_channel::select_biased! {
317 recv(self.proof_result_rx) -> message => {
318 let wake = Instant::now();
319 total_idle_time += wake.duration_since(idle_start);
320 self.metrics
321 .sparse_trie_channel_wait_duration_histogram
322 .record(wake.duration_since(t));
323 t = wake;
324
325 let Ok(result) = message else {
326 unreachable!("we own the sender half")
327 };
328 self.on_proof_results(result, &mut t)?;
329 },
330 recv(self.cancel_rx) -> _ => return Err(StateRootTaskError::Canceled),
331 }
332
333 done = self.make_progress()?;
334 idle_start = Instant::now();
335 }
336
337 self.metrics.sparse_trie_idle_time_seconds.record(total_idle_time.as_secs_f64());
338
339 debug!(target: "engine::root", "All proofs processed, ending calculation");
340
341 let start = Instant::now();
342 let (state_root, trie_updates) = match self.trie.root_with_updates(self.new_epoch) {
343 Ok(result) => result,
344 Err(err)
345 if matches!(
346 err.kind(),
347 SparseStateTrieErrorKind::Sparse(SparseTrieErrorKind::Blind)
348 ) =>
349 {
350 (self.parent_state_root, TrieUpdates::default())
354 }
355 Err(err) => {
356 return Err(StateRootTaskError::Other(format!(
357 "could not calculate state root: {err:?}"
358 )))
359 }
360 };
361
362 #[cfg(feature = "trie-debug")]
363 let debug_recorders = self.trie.take_debug_recorders();
364
365 let end = Instant::now();
366 self.metrics.sparse_trie_final_update_duration_histogram.record(end.duration_since(start));
367 self.metrics.sparse_trie_total_duration_histogram.record(end.duration_since(now));
368
369 self.metrics.sparse_trie_account_cache_hits.record(self.account_cache_hits as f64);
370 self.metrics.sparse_trie_account_cache_misses.record(self.account_cache_misses as f64);
371 self.metrics.sparse_trie_storage_cache_hits.record(self.storage_cache_hits as f64);
372 self.metrics.sparse_trie_storage_cache_misses.record(self.storage_cache_misses as f64);
373 self.account_cache_hits = 0;
374 self.account_cache_misses = 0;
375 self.storage_cache_hits = 0;
376 self.storage_cache_misses = 0;
377
378 Ok(StateRootComputeOutcome {
379 state_root,
380 trie_updates: Arc::new(trie_updates),
381 hashed_state: finalized_hashed_state
382 .expect("finished state updates publish the hashed post state"),
383 #[cfg(feature = "trie-debug")]
384 debug_recorders,
385 })
386 }
387
388 fn on_proof_results(
391 &mut self,
392 message: ProofResultMessage,
393 t: &mut Instant,
394 ) -> Result<(), StateRootTaskError> {
395 let mut result = self.on_proof_result_message(message)?;
396 while let Ok(next) = self.proof_result_rx.try_recv() {
397 let res = self.on_proof_result_message(next)?;
398 result.extend(res);
399 }
400
401 let phase_end = Instant::now();
402 self.metrics
403 .sparse_trie_proof_coalesce_duration_histogram
404 .record(phase_end.duration_since(*t));
405 *t = phase_end;
406
407 self.on_proof_result(result)?;
408 self.metrics.sparse_trie_reveal_multiproof_duration_histogram.record(t.elapsed());
409 Ok(())
410 }
411
412 fn make_progress(&mut self) -> Result<bool, StateRootTaskError> {
417 let updates_queued = !self.finished_state_updates && !self.updates.is_empty();
418
419 if !updates_queued && self.proof_result_rx.is_empty() {
420 self.dispatch_pending_targets()?;
423 let t = Instant::now();
424 self.process_new_updates()?;
425 self.promote_pending_account_updates()?;
426 self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
427
428 if self.finished_state_updates && !self.has_pending_sparse_trie_updates() {
429 return Ok(true);
430 }
431
432 self.dispatch_pending_targets()?;
433 self.ensure_not_stalled(updates_queued)?;
434
435 if self.proof_result_rx.is_empty() {
438 self.trie.calculate_subtries(self.new_epoch);
439 }
440 } else if !updates_queued {
441 let t = Instant::now();
443 self.process_new_updates()?;
444 self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
445 self.dispatch_pending_targets()?;
446 } else if self.pending_targets.len() > self.chunk_size {
447 self.dispatch_pending_targets()?;
449 }
450 Ok(false)
451 }
452
453 fn on_message(&mut self, message: SparseTrieTaskMessage) -> Option<Arc<HashedPostState>> {
455 match message {
456 SparseTrieTaskMessage::PrefetchProofs(targets) => {
457 self.on_prewarm_targets(targets);
458 None
459 }
460 SparseTrieTaskMessage::HashedState(hashed_state) => {
461 self.on_hashed_state_update(hashed_state);
462 None
463 }
464 SparseTrieTaskMessage::FinishedStateUpdates => {
465 let hashed_state = Arc::new(core::mem::take(&mut self.final_hashed_state));
466 let _ = self.final_hashed_state_tx.take().unwrap().send(Arc::clone(&hashed_state));
467 self.finished_state_updates = true;
468 Some(hashed_state)
469 }
470 }
471 }
472
473 #[instrument(
474 level = "trace",
475 target = "engine::tree::payload_processor::sparse_trie",
476 skip_all
477 )]
478 fn on_prewarm_targets(&mut self, targets: MultiProofTargetsV2) {
479 for target in targets.account_targets {
480 self.new_account_updates.entry(target.key()).or_insert(LeafUpdate::Touched);
482 }
483
484 for (address, slots) in targets.storage_targets {
485 if !slots.is_empty() {
486 let new_updates = self.new_storage_updates.entry(address).or_default();
488 for slot in slots {
489 new_updates.entry(slot.key()).or_insert(LeafUpdate::Touched);
491 }
492 }
493
494 self.new_account_updates.entry(address).or_insert(LeafUpdate::Touched);
497 }
498 }
499
500 #[instrument(
502 level = "trace",
503 target = "engine::tree::payload_processor::sparse_trie",
504 skip_all
505 )]
506 fn on_hashed_state_update(&mut self, hashed_state_update: HashedPostState) {
507 for (&address, storage) in &hashed_state_update.storages {
508 if !storage.storage.is_empty() {
509 let new_updates = self.new_storage_updates.entry(address).or_default();
511 let mut existing_updates = self.storage_updates.get_mut(&address);
512
513 for (&slot, &value) in &storage.storage {
514 let encoded = if value.is_zero() {
515 Vec::new()
516 } else {
517 alloy_rlp::encode_fixed_size(&value).to_vec()
518 };
519 new_updates.insert(slot, LeafUpdate::Changed(encoded));
520
521 if let Some(ref mut existing) = existing_updates {
523 existing.remove(&slot);
524 }
525 }
526 }
527
528 self.new_account_updates.entry(address).or_insert(LeafUpdate::Touched);
531
532 self.pending_account_updates.entry(address).or_insert(None);
535 }
536
537 for (&address, &account) in &hashed_state_update.accounts {
538 self.new_account_updates.insert(address, LeafUpdate::Touched);
543
544 self.pending_account_updates.insert(address, Some(account));
547 }
548
549 self.final_hashed_state.extend(hashed_state_update);
550 }
551
552 fn on_proof_result(&mut self, result: DecodedMultiProofV2) -> Result<(), StateRootTaskError> {
553 self.trie
554 .reveal_decoded_multiproof_v2(result)
555 .map_err(|e| StateRootTaskError::Other(format!("could not reveal multiproof: {e:?}")))
556 }
557
558 fn on_proof_result_message(
559 &mut self,
560 message: ProofResultMessage,
561 ) -> Result<DecodedMultiProofV2, StateRootTaskError> {
562 let result = message.result?;
563 debug_assert!(
564 self.in_flight_proof_batches > 0,
565 "received proof result without an in-flight proof batch"
566 );
567 self.in_flight_proof_batches = self.in_flight_proof_batches.saturating_sub(1);
568 Ok(result)
569 }
570
571 fn process_new_updates(&mut self) -> SparseTrieResult<()> {
572 if self.pending_updates == 0 {
573 return Ok(());
574 }
575
576 let _span = debug_span!("process_new_updates").entered();
577 self.pending_updates = 0;
578
579 self.process_leaf_updates(true)?;
581
582 for (address, mut new) in self.new_storage_updates.drain() {
583 match self.storage_updates.entry(address) {
584 Entry::Vacant(entry) => {
585 entry.insert(new); }
587 Entry::Occupied(mut entry) => {
588 let updates = entry.get_mut();
589 for (slot, new) in new.drain() {
590 match updates.entry(slot) {
591 Entry::Occupied(mut slot_entry) => {
592 if new.is_changed() {
593 slot_entry.insert(new);
594 }
595 }
596 Entry::Vacant(slot_entry) => {
597 slot_entry.insert(new);
598 }
599 }
600 }
601 }
602 }
603 }
604
605 for (address, new) in self.new_account_updates.drain() {
606 match self.account_updates.entry(address) {
607 Entry::Occupied(mut entry) => {
608 if new.is_changed() {
609 entry.insert(new);
610 }
611 }
612 Entry::Vacant(entry) => {
613 entry.insert(new);
614 }
615 }
616 }
617
618 Ok(())
619 }
620
621 #[instrument(
624 level = "trace",
625 target = "engine::tree::payload_processor::sparse_trie",
626 skip_all
627 )]
628 fn process_leaf_updates(&mut self, new: bool) -> SparseTrieResult<()> {
629 let storage_updates =
630 if new { &mut self.new_storage_updates } else { &mut self.storage_updates };
631
632 let span = trace_span!("process_storage_leaf_updates").entered();
634 for (address, updates) in storage_updates {
635 if updates.is_empty() {
636 continue;
637 }
638 let _enter = trace_span!(target: "engine::tree::payload_processor::sparse_trie", parent: &span, "storage_trie_leaf_updates", a=%address).entered();
639
640 let trie = self.trie.get_or_create_storage_trie_mut(*address);
641 let fetched = self.fetched_storage_targets.entry(*address).or_default();
642 let mut targets = Vec::new();
643
644 let updates_len_before = updates.len();
645 trie.update_leaves(updates, |path, parent| match fetched.entry(path) {
646 Entry::Occupied(mut entry) => {
647 if parent < *entry.get() {
648 entry.insert(parent);
649 targets.push(ProofV2Target::new(path).with_parent(parent));
650 }
651 }
652 Entry::Vacant(entry) => {
653 entry.insert(parent);
654 targets.push(ProofV2Target::new(path).with_parent(parent));
655 }
656 })?;
657 let updates_len_after = updates.len();
658 self.storage_cache_hits += (updates_len_before - updates_len_after) as u64;
659 self.storage_cache_misses += updates_len_after as u64;
660
661 if !targets.is_empty() {
662 self.pending_targets.extend_storage_targets(address, targets);
663 }
664 }
665
666 drop(span);
667
668 self.process_account_leaf_updates(new)?;
670
671 Ok(())
672 }
673
674 #[instrument(
678 level = "trace",
679 target = "engine::tree::payload_processor::sparse_trie",
680 skip_all
681 )]
682 fn process_account_leaf_updates(&mut self, new: bool) -> SparseTrieResult<bool> {
683 let account_updates =
684 if new { &mut self.new_account_updates } else { &mut self.account_updates };
685
686 let updates_len_before = account_updates.len();
687
688 self.trie.trie_mut().update_leaves(account_updates, |target, parent| {
689 match self.fetched_account_targets.entry(target) {
690 Entry::Occupied(mut entry) => {
691 if parent < *entry.get() {
692 entry.insert(parent);
693 self.pending_targets
694 .push_account_target(ProofV2Target::new(target).with_parent(parent));
695 }
696 }
697 Entry::Vacant(entry) => {
698 entry.insert(parent);
699 self.pending_targets
700 .push_account_target(ProofV2Target::new(target).with_parent(parent));
701 }
702 }
703 })?;
704
705 let updates_len_after = account_updates.len();
706 self.account_cache_hits += (updates_len_before - updates_len_after) as u64;
707 self.account_cache_misses += updates_len_after as u64;
708
709 Ok(updates_len_after < updates_len_before)
710 }
711
712 fn compute_drained_storage_roots(&mut self) {
721 let addresses_to_compute_roots: Vec<_> = self
722 .storage_updates
723 .iter()
724 .filter_map(|(address, updates)| updates.is_empty().then_some(*address))
725 .collect();
726
727 struct SendStorageTriePtr<S>(*mut RevealableSparseTrie<S>);
728 unsafe impl<S: Send> Send for SendStorageTriePtr<S> {}
731
732 let mut tries_to_compute_roots: Vec<(B256, SendStorageTriePtr<S>)> =
733 Vec::with_capacity(addresses_to_compute_roots.len());
734 for address in addresses_to_compute_roots {
735 if let Some(trie) = self.trie.storage_tries_mut().get_mut(&address) &&
736 !trie.is_root_cached()
737 {
738 tries_to_compute_roots.push((address, SendStorageTriePtr(trie)));
739 }
740 }
741
742 if tries_to_compute_roots.is_empty() {
743 return;
744 }
745
746 let parent_span =
747 debug_span!("compute_drained_storage_roots", n = tries_to_compute_roots.len());
748 let new_epoch = self.new_epoch;
749 tries_to_compute_roots.into_par_iter().for_each(|(address, SendStorageTriePtr(trie))| {
750 let span = if tracing::enabled!(tracing::Level::TRACE) {
751 debug_span!(
752 target: "engine::tree::payload_processor::sparse_trie",
753 parent: &parent_span,
754 "storage_root",
755 ?address
756 )
757 } else {
758 debug_span!(
759 target: "engine::tree::payload_processor::sparse_trie",
760 parent: &parent_span,
761 "storage_root",
762 )
763 };
764 let _enter = span.entered();
765 unsafe {
772 (*trie)
773 .root(new_epoch)
774 .expect("updates are drained, trie should be revealed by now")
775 };
776 });
777 }
778
779 #[instrument(
783 level = "trace",
784 target = "engine::tree::payload_processor::sparse_trie",
785 skip_all
786 )]
787 fn promote_pending_account_updates(&mut self) -> SparseTrieResult<()> {
788 self.process_leaf_updates(false)?;
789
790 if self.pending_account_updates.is_empty() {
791 return Ok(());
792 }
793
794 self.compute_drained_storage_roots();
795
796 loop {
797 let span = trace_span!("promote_updates", promoted = tracing::field::Empty).entered();
798 let account_rlp_buf = &mut self.account_rlp_buf;
800 let mut num_promoted = 0;
801 self.pending_account_updates.retain(|addr, account| {
802 if let Some(updates) = self.storage_updates.get(addr) {
803 if !updates.is_empty() {
804 return true;
806 } else if let Some(account) = account.take() {
807 let storage_root = self.trie.storage_root(addr, self.new_epoch).expect("updates are drained, storage trie should be revealed by now");
808 let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
809 self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
810 num_promoted += 1;
811 return false;
812 }
813 }
814
815 let trie_account = match self.account_updates.get(addr) {
817 Some(LeafUpdate::Changed(encoded)) => {
818 Some(encoded).filter(|encoded| !encoded.is_empty())
819 }
820 Some(LeafUpdate::Touched) => return true,
822 None => self.trie.get_account_value(addr),
823 };
824
825 let trie_account = trie_account.map(|value| TrieAccount::decode(&mut &value[..]).expect("invalid account RLP"));
826
827 let (account, storage_root) = if let Some(account) = account.take() {
828 let storage_root = trie_account.map(|account| account.storage_root).unwrap_or(EMPTY_ROOT_HASH);
833
834 (account, storage_root)
835 } else {
836 (trie_account.map(Into::into), self.trie.storage_root(addr, self.new_epoch).expect("account had storage updates that were applied to its trie, storage root must be revealed by now"))
837 };
838
839 let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
840 self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
841 num_promoted += 1;
842
843 false
844 });
845 span.record("promoted", num_promoted);
846 drop(span);
847
848 if num_promoted == 0 || !self.process_account_leaf_updates(false)? {
853 break
854 }
855 }
856
857 Ok(())
858 }
859
860 fn dispatch_pending_targets(&mut self) -> Result<(), StateRootTaskError> {
861 if self.pending_targets.is_empty() {
862 return Ok(())
863 }
864
865 let _span = trace_span!("dispatch_pending_targets").entered();
866 let (targets, chunking_length) = self.pending_targets.take();
867 let mut dispatch_error = None;
868 dispatch_with_chunking(
869 targets,
870 chunking_length,
871 self.chunk_size,
872 self.max_targets_for_chunking,
873 self.proof_worker_handle.has_multiple_idle_account_workers(),
874 self.proof_worker_handle.has_multiple_idle_storage_workers(),
875 MultiProofTargetsV2::chunks,
876 |proof_targets| {
877 if dispatch_error.is_some() {
878 return;
879 }
880
881 match self.proof_worker_handle.dispatch_account_multiproof(AccountMultiproofInput {
882 targets: proof_targets,
883 proof_result_sender: ProofResultContext::new(
884 self.proof_result_tx.clone(),
885 HashedPostState::default(),
886 Instant::now(),
887 ),
888 }) {
889 Ok(()) => {
890 self.in_flight_proof_batches += 1;
891 }
892 Err(e) => {
893 error!("failed to dispatch account multiproof: {e:?}");
894 dispatch_error = Some(StateRootTaskError::ProofDispatch(e));
895 }
896 }
897 },
898 );
899
900 if let Some(error) = dispatch_error {
901 return Err(error)
902 }
903
904 Ok(())
905 }
906
907 fn has_pending_sparse_trie_updates(&self) -> bool {
908 !self.account_updates.is_empty() ||
909 self.storage_updates.values().any(|updates| !updates.is_empty()) ||
910 !self.pending_account_updates.is_empty()
911 }
912
913 fn ensure_not_stalled(&self, updates_queued: bool) -> Result<(), StateRootTaskError> {
921 if self.finished_state_updates &&
922 !updates_queued &&
923 self.pending_updates == 0 &&
924 self.pending_targets.is_empty() &&
925 self.in_flight_proof_batches == 0 &&
926 self.proof_result_rx.is_empty() &&
927 self.has_pending_sparse_trie_updates()
928 {
929 const MAX_STALLED_PROOF_TARGETS_TO_LOG: usize = 5;
930
931 let mut account_targets = self
932 .account_updates
933 .keys()
934 .map(|target| (*target, self.fetched_account_targets.get(target).copied()))
935 .collect::<Vec<_>>();
936 account_targets.sort_unstable();
937 let account_targets_truncated =
938 account_targets.len().saturating_sub(MAX_STALLED_PROOF_TARGETS_TO_LOG);
939 account_targets.truncate(MAX_STALLED_PROOF_TARGETS_TO_LOG);
940
941 let mut storage_targets = self
942 .storage_updates
943 .iter()
944 .flat_map(|(address, updates)| {
945 let fetched_targets = self.fetched_storage_targets.get(address);
946 updates.keys().map(move |target| {
947 (
948 *address,
949 *target,
950 fetched_targets.and_then(|targets| targets.get(target)).copied(),
951 )
952 })
953 })
954 .collect::<Vec<_>>();
955 storage_targets.sort_unstable();
956 let storage_targets_truncated =
957 storage_targets.len().saturating_sub(MAX_STALLED_PROOF_TARGETS_TO_LOG);
958 storage_targets.truncate(MAX_STALLED_PROOF_TARGETS_TO_LOG);
959
960 error!(
961 ?account_targets,
962 account_targets_truncated,
963 ?storage_targets,
964 storage_targets_truncated,
965 "sparse trie task stalled: pending updates remain but no proof targets are queued or in flight"
966 );
967
968 return Err(StateRootTaskError::Stalled)
969 }
970
971 Ok(())
972 }
973}
974
975#[derive(Metrics, Clone)]
977#[metrics(scope = "tree.root")]
978pub(super) struct SparseTrieTaskMetrics {
979 pub(super) sparse_trie_reveal_multiproof_duration_histogram: Histogram,
981 pub(super) sparse_trie_proof_coalesce_duration_histogram: Histogram,
983 pub(super) sparse_trie_channel_wait_duration_histogram: Histogram,
985 pub(super) sparse_trie_process_updates_duration_histogram: Histogram,
987 pub(super) sparse_trie_final_update_duration_histogram: Histogram,
989 pub(super) sparse_trie_total_duration_histogram: Histogram,
991 pub(super) into_trie_for_reuse_duration_histogram: Histogram,
993 pub(super) sparse_trie_prune_duration_histogram: Histogram,
995 pub(super) sparse_trie_cache_wait_duration_histogram: Histogram,
997 pub(super) sparse_trie_idle_time_seconds: Histogram,
1000 pub(super) hashing_task_idle_time_seconds: Histogram,
1003
1004 pub(super) sparse_trie_account_cache_hits: Histogram,
1006 pub(super) sparse_trie_account_cache_misses: Histogram,
1008 pub(super) sparse_trie_storage_cache_hits: Histogram,
1010 pub(super) sparse_trie_storage_cache_misses: Histogram,
1012
1013 pub(super) sparse_trie_retained_storage_tries: Gauge,
1015}
1016
1017const DEFAULT_MAX_TARGETS_FOR_CHUNKING: usize = 300;
1020
1021#[expect(clippy::too_many_arguments)]
1024fn dispatch_with_chunking<T, I>(
1025 items: T,
1026 chunking_len: usize,
1027 chunk_size: usize,
1028 max_targets_for_chunking: usize,
1029 has_multiple_idle_account_workers: bool,
1030 has_multiple_idle_storage_workers: bool,
1031 chunker: impl FnOnce(T, usize) -> I,
1032 mut dispatch: impl FnMut(T),
1033) where
1034 I: IntoIterator<Item = T>,
1035{
1036 let has_full_chunks = chunking_len >= chunk_size.saturating_mul(2);
1037 let should_chunk = chunking_len > max_targets_for_chunking ||
1038 (has_full_chunks &&
1039 (has_multiple_idle_account_workers || has_multiple_idle_storage_workers));
1040
1041 if should_chunk && chunking_len > chunk_size {
1042 for chunk in chunker(items, chunk_size) {
1043 dispatch(chunk);
1044 }
1045 return;
1046 }
1047
1048 dispatch(items);
1049}
1050
1051fn encode_account_leaf_value(
1058 account: Option<Account>,
1059 storage_root: B256,
1060 account_rlp_buf: &mut Vec<u8>,
1061) -> Vec<u8> {
1062 if account.is_none_or(|account| account.is_empty()) && storage_root == EMPTY_ROOT_HASH {
1063 return Vec::new();
1064 }
1065
1066 account_rlp_buf.clear();
1067 account.unwrap_or_default().into_trie_account(storage_root).encode(account_rlp_buf);
1068 account_rlp_buf.clone()
1069}
1070
1071#[derive(Default)]
1073struct PendingTargets {
1074 targets: MultiProofTargetsV2,
1076 len: usize,
1078}
1079
1080impl PendingTargets {
1081 const fn len(&self) -> usize {
1083 self.len
1084 }
1085
1086 const fn is_empty(&self) -> bool {
1088 self.len == 0
1089 }
1090
1091 fn take(&mut self) -> (MultiProofTargetsV2, usize) {
1093 (std::mem::take(&mut self.targets), std::mem::take(&mut self.len))
1094 }
1095
1096 fn push_account_target(&mut self, target: ProofV2Target) {
1098 self.targets.account_targets.push(target);
1099 self.len += 1;
1100 }
1101
1102 fn extend_storage_targets(&mut self, address: &B256, targets: Vec<ProofV2Target>) {
1104 self.len += targets.len();
1105 self.targets.storage_targets.entry(*address).or_default().extend(targets);
1106 }
1107}
1108
1109enum SparseTrieTaskMessage {
1111 HashedState(HashedPostState),
1113 PrefetchProofs(MultiProofTargetsV2),
1115 FinishedStateUpdates,
1117}
1118
1119#[cfg(test)]
1120mod tests {
1121 use super::*;
1122 use alloy_primitives::{keccak256, Address, B256, U256};
1123 use reth_db_common::init::init_genesis;
1124 use reth_provider::{
1125 providers::OverlayStateProviderFactory, test_utils::create_test_provider_factory,
1126 };
1127 use reth_storage_overlay::OverlayManager;
1128 use reth_trie_parallel::proof_task::ProofTaskCtx;
1129 use reth_trie_sparse::ArenaParallelSparseTrie;
1130
1131 fn drain_sparse_trie_tasks(runtime: &Runtime) {
1132 for task_name in ["trie-hashing", "storage-workers", "account-workers"] {
1133 runtime.spawn_blocking_named(task_name, || {}).get();
1134 }
1135 }
1136
1137 #[test]
1138 fn test_run_hashing_task_hashed_state_update_forwards() {
1139 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1140 let (hashed_state_tx, hashed_state_rx) = crossbeam_channel::unbounded();
1141
1142 let address = keccak256(Address::random());
1143 let slot = keccak256(U256::from(42).to_be_bytes::<32>());
1144 let value = U256::from(999);
1145
1146 let mut hashed_state = HashedPostState::default();
1147 hashed_state.accounts.insert(
1148 address,
1149 Some(Account { balance: U256::from(100), nonce: 1, bytecode_hash: None }),
1150 );
1151 let mut storage = reth_trie::HashedStorage::new(false);
1152 storage.storage.insert(slot, value);
1153 hashed_state.storages.insert(address, storage);
1154
1155 let expected_state = hashed_state.clone();
1156
1157 let handle = std::thread::spawn(move || {
1158 SparseTrieCacheTask::<ArenaParallelSparseTrie, ArenaParallelSparseTrie>::run_hashing_task(
1159 updates_rx,
1160 hashed_state_tx,
1161 SparseTrieTaskMetrics::default(),
1162 );
1163 });
1164
1165 updates_tx.send(StateRootMessage::HashedStateUpdate(hashed_state)).unwrap();
1166 updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
1167 drop(updates_tx);
1168
1169 let SparseTrieTaskMessage::HashedState(received) = hashed_state_rx.recv().unwrap() else {
1170 panic!("expected HashedState message");
1171 };
1172
1173 let account = received.accounts.get(&address).unwrap().unwrap();
1174 assert_eq!(account.balance, expected_state.accounts[&address].unwrap().balance);
1175 assert_eq!(account.nonce, expected_state.accounts[&address].unwrap().nonce);
1176
1177 let storage = received.storages.get(&address).unwrap();
1178 assert_eq!(*storage.storage.get(&slot).unwrap(), value);
1179
1180 let second = hashed_state_rx.recv().unwrap();
1181 assert!(matches!(second, SparseTrieTaskMessage::FinishedStateUpdates));
1182
1183 assert!(hashed_state_rx.recv().is_err());
1184 handle.join().unwrap();
1185 }
1186
1187 #[test]
1188 fn test_encode_account_leaf_value_deletion_and_empty_root_is_empty() {
1189 let mut account_rlp_buf = vec![0xAB];
1190 let encoded = encode_account_leaf_value(None, EMPTY_ROOT_HASH, &mut account_rlp_buf);
1191
1192 assert!(encoded.is_empty());
1193 assert_eq!(account_rlp_buf, vec![0xAB]);
1195 }
1196
1197 #[test]
1198 fn test_encode_account_leaf_value_empty_account_and_empty_root_is_empty() {
1199 let mut account_rlp_buf = vec![0xAB];
1200 let encoded = encode_account_leaf_value(
1201 Some(Account::default()),
1202 EMPTY_ROOT_HASH,
1203 &mut account_rlp_buf,
1204 );
1205
1206 assert!(encoded.is_empty());
1207 assert_eq!(account_rlp_buf, vec![0xAB]);
1209 }
1210
1211 #[test]
1212 fn test_encode_account_leaf_value_non_empty_account_is_rlp() {
1213 let storage_root = B256::from([0x99; 32]);
1214 let account = Some(Account {
1215 nonce: 7,
1216 balance: U256::from(42),
1217 bytecode_hash: Some(B256::from([0xAA; 32])),
1218 });
1219 let mut account_rlp_buf = vec![0x00, 0x01];
1220
1221 let encoded = encode_account_leaf_value(account, storage_root, &mut account_rlp_buf);
1222 let decoded = TrieAccount::decode(&mut &encoded[..]).expect("valid account RLP");
1223
1224 assert_eq!(decoded.nonce, 7);
1225 assert_eq!(decoded.balance, U256::from(42));
1226 assert_eq!(decoded.storage_root, storage_root);
1227 assert_eq!(account_rlp_buf, encoded);
1228 }
1229
1230 #[test]
1231 fn run_returns_parent_root_without_revealing_blind_trie_when_no_state_updates() {
1232 let runtime = reth_tasks::Runtime::test();
1233 let provider_factory = create_test_provider_factory();
1234 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
1235 let overlay_factory = OverlayStateProviderFactory::new(
1236 provider_factory,
1237 OverlayManager::<reth_chain_state::EthPrimitives>::default()
1238 .overlay_builder(anchor_hash),
1239 );
1240 let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
1241 let proof_worker_handle = ProofWorkerHandle::new(
1242 &runtime,
1243 ProofTaskCtx::new(overlay_factory),
1244 false,
1245 proof_result_tx.clone(),
1246 );
1247
1248 let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
1249 let trie = SparseStateTrie::default()
1250 .with_accounts_trie(default_trie.clone())
1251 .with_default_storage_trie(default_trie)
1252 .with_updates(true);
1253
1254 let parent_state_root = B256::from([0x55; 32]);
1255 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1256 let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
1257 let mut task = SparseTrieCacheTask::new_with_trie(
1258 &runtime,
1259 updates_rx,
1260 cancel_rx,
1261 std::sync::mpsc::channel().0,
1262 proof_worker_handle,
1263 proof_result_tx,
1264 proof_result_rx,
1265 SparseTrieTaskMetrics::default(),
1266 trie,
1267 parent_state_root,
1268 TrieNodeEpoch::UNMODIFIED,
1269 1,
1270 );
1271
1272 updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
1273 drop(updates_tx);
1274
1275 let outcome = task.run().expect("state root computation should succeed");
1276
1277 assert_eq!(outcome.state_root, parent_state_root);
1278 assert!(outcome.trie_updates.is_empty());
1279 assert!(task.trie.state_trie_ref().is_none(), "blind trie should not be revealed");
1280
1281 drop(task);
1282 drain_sparse_trie_tasks(&runtime);
1283 }
1284
1285 #[test]
1286 fn stall_check_waits_for_in_flight_proofs_then_reports_pending_updates() {
1287 let runtime = reth_tasks::Runtime::test();
1288 let provider_factory = create_test_provider_factory();
1289 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
1290 let overlay_factory = OverlayStateProviderFactory::new(
1291 provider_factory,
1292 OverlayManager::<reth_chain_state::EthPrimitives>::default()
1293 .overlay_builder(anchor_hash),
1294 );
1295 let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
1296 let proof_worker_handle = ProofWorkerHandle::new(
1297 &runtime,
1298 ProofTaskCtx::new(overlay_factory),
1299 false,
1300 proof_result_tx.clone(),
1301 );
1302
1303 let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
1304 let trie = SparseStateTrie::default()
1305 .with_accounts_trie(default_trie.clone())
1306 .with_default_storage_trie(default_trie)
1307 .with_updates(true);
1308
1309 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1310 let (_cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
1311 let mut task = SparseTrieCacheTask::new_with_trie(
1312 &runtime,
1313 updates_rx,
1314 cancel_rx,
1315 std::sync::mpsc::channel().0,
1316 proof_worker_handle,
1317 proof_result_tx,
1318 proof_result_rx,
1319 SparseTrieTaskMetrics::default(),
1320 trie,
1321 B256::from([0x55; 32]),
1322 TrieNodeEpoch::UNMODIFIED,
1323 1,
1324 );
1325
1326 drop(updates_tx);
1327
1328 let account = B256::from([0x11; 32]);
1329 let slot = B256::from([0x22; 32]);
1330 let account_target = B256::from([0x33; 32]);
1331 let storage_target = B256::from([0x44; 32]);
1332
1333 task.finished_state_updates = true;
1334 task.account_updates.insert(account, LeafUpdate::Touched);
1335 task.storage_updates.entry(account).or_default().insert(slot, LeafUpdate::Touched);
1336 task.pending_account_updates.insert(account, None);
1337 task.fetched_account_targets.insert(account_target, ProofV2TargetParent::NONE);
1338 task.fetched_storage_targets
1339 .entry(account)
1340 .or_default()
1341 .insert(storage_target, ProofV2TargetParent::new(11));
1342 task.in_flight_proof_batches = 1;
1343
1344 assert!(task.ensure_not_stalled(false).is_ok());
1345
1346 let result = ProofResultMessage {
1347 result: Ok(DecodedMultiProofV2::default()),
1348 elapsed: std::time::Duration::ZERO,
1349 state: HashedPostState::default(),
1350 };
1351 task.on_proof_result_message(result).expect("proof result should be ok");
1352
1353 assert_eq!(task.in_flight_proof_batches, 0);
1354 let error = task.ensure_not_stalled(false).expect_err("task should be stalled");
1355 assert!(matches!(error, StateRootTaskError::Stalled));
1356 let error = error.to_string();
1357
1358 assert!(error.contains("sparse trie task stalled"));
1359 assert!(!error.contains("account_targets"));
1360 assert!(!error.contains("storage_targets"));
1361 assert!(!error.contains(&format!("{account:?}")));
1362 assert!(!error.contains(&format!("{account_target:?}")));
1363 assert!(!error.contains(&format!("{storage_target:?}")));
1364 assert!(!error.contains("pending_account_leaves"));
1365 assert!(!error.contains("pending_storage_leaves"));
1366 assert!(!error.contains("pending_account_updates"));
1367 assert!(!error.contains(&format!("{slot:?}")));
1368
1369 drop(task);
1370 drain_sparse_trie_tasks(&runtime);
1371 }
1372
1373 #[test]
1374 fn run_errors_when_cancel_guard_drops_before_updates_finish() {
1375 let runtime = reth_tasks::Runtime::test();
1376 let provider_factory = create_test_provider_factory();
1377 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
1378 let overlay_factory = OverlayStateProviderFactory::new(
1379 provider_factory,
1380 OverlayManager::<reth_chain_state::EthPrimitives>::default()
1381 .overlay_builder(anchor_hash),
1382 );
1383 let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
1384 let proof_worker_handle = ProofWorkerHandle::new(
1385 &runtime,
1386 ProofTaskCtx::new(overlay_factory),
1387 false,
1388 proof_result_tx.clone(),
1389 );
1390
1391 let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
1392 let trie = SparseStateTrie::default()
1393 .with_accounts_trie(default_trie.clone())
1394 .with_default_storage_trie(default_trie)
1395 .with_updates(true);
1396
1397 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1398 let (cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
1399 let mut task = SparseTrieCacheTask::new_with_trie(
1400 &runtime,
1401 updates_rx,
1402 cancel_rx,
1403 std::sync::mpsc::channel().0,
1404 proof_worker_handle,
1405 proof_result_tx,
1406 proof_result_rx,
1407 SparseTrieTaskMetrics::default(),
1408 trie,
1409 B256::from([0x55; 32]),
1410 TrieNodeEpoch::UNMODIFIED,
1411 1,
1412 );
1413
1414 drop(cancel_guard);
1417
1418 let error = task.run().expect_err("canceled task must return an error");
1419 assert!(matches!(error, StateRootTaskError::Canceled));
1420
1421 drop(updates_tx);
1422 drop(task);
1423 drain_sparse_trie_tasks(&runtime);
1424 }
1425
1426 #[test]
1427 fn run_ignores_hints_queued_after_updates_finish() {
1428 let runtime = reth_tasks::Runtime::test();
1429 let provider_factory = create_test_provider_factory();
1430 let anchor_hash = init_genesis(&provider_factory).expect("failed to initialize genesis");
1431 let overlay_factory = OverlayStateProviderFactory::new(
1432 provider_factory,
1433 OverlayManager::<reth_chain_state::EthPrimitives>::default()
1434 .overlay_builder(anchor_hash),
1435 );
1436 let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
1437 let proof_worker_handle = ProofWorkerHandle::new(
1438 &runtime,
1439 ProofTaskCtx::new(overlay_factory),
1440 false,
1441 proof_result_tx.clone(),
1442 );
1443
1444 let default_trie = RevealableSparseTrie::blind_from(ArenaParallelSparseTrie::default());
1445 let trie = SparseStateTrie::default()
1446 .with_accounts_trie(default_trie.clone())
1447 .with_default_storage_trie(default_trie)
1448 .with_updates(true);
1449
1450 let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
1451 let (cancel_guard, cancel_rx) = crossbeam_channel::bounded::<()>(0);
1452 let mut task = SparseTrieCacheTask::new_with_trie(
1453 &runtime,
1454 updates_rx,
1455 cancel_rx,
1456 std::sync::mpsc::channel().0,
1457 proof_worker_handle,
1458 proof_result_tx,
1459 proof_result_rx,
1460 SparseTrieTaskMetrics::default(),
1461 trie,
1462 B256::from([0x55; 32]),
1463 TrieNodeEpoch::UNMODIFIED,
1464 1,
1465 );
1466
1467 updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
1468 updates_tx.send(StateRootMessage::PrefetchProofs(Default::default())).unwrap();
1469
1470 let wait_start = std::time::Instant::now();
1471 while task.updates.len() < 2 {
1472 assert!(
1473 wait_start.elapsed() < std::time::Duration::from_secs(1),
1474 "hashing task did not queue the test messages"
1475 );
1476 std::thread::yield_now();
1477 }
1478
1479 let (result_tx, result_rx) = std::sync::mpsc::channel();
1480 let handle = std::thread::spawn(move || {
1481 let _ = result_tx.send(task.run());
1482 });
1483
1484 let result = result_rx.recv_timeout(std::time::Duration::from_secs(1));
1485 drop(cancel_guard);
1486 handle.join().unwrap();
1487
1488 assert!(result.expect("state root task stalled on a late hint").is_ok());
1489
1490 drop(updates_tx);
1491 drain_sparse_trie_tasks(&runtime);
1492 }
1493}