1#[cfg(feature = "metrics")]
27pub mod metrics;
28
29use crate::error::StateRootTaskError;
30use alloy_primitives::B256;
31use crossbeam_channel::{unbounded, Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
32use reth_primitives_traits::FastInstant as Instant;
33use reth_provider::{DatabaseProviderROFactory, ProviderError, ProviderResult, StateProofProvider};
34use reth_tasks::{Runtime, WorkerPool};
35use reth_trie::{DecodedMultiProofV2, MultiProofTargetsV2, ProofV2Target};
36use std::{
37 panic::{catch_unwind, AssertUnwindSafe},
38 sync::{
39 atomic::{AtomicBool, AtomicUsize, Ordering},
40 Arc,
41 },
42 time::Duration,
43};
44use tracing::{debug, debug_span, error, instrument, trace};
45
46#[cfg(feature = "metrics")]
47use self::metrics::ProofWorkerIdleMetrics;
48
49#[derive(Debug)]
53struct AvailabilitySheet {
54 flags: Vec<crossbeam_utils::CachePadded<AtomicBool>>,
57}
58
59impl AvailabilitySheet {
60 fn new(count: usize) -> Self {
62 let flags =
63 (0..count).map(|_| crossbeam_utils::CachePadded::new(AtomicBool::new(false))).collect();
64 Self { flags }
65 }
66
67 fn has_multiple_idle(&self) -> bool {
72 let mut idle = 0u32;
73 for flag in &self.flags {
74 if flag.load(Ordering::Relaxed) {
75 idle += 1;
76 if idle > 1 {
77 return true;
78 }
79 }
80 }
81 false
82 }
83
84 fn mark_idle(&self, worker_id: usize) {
86 self.flags[worker_id].store(true, Ordering::Relaxed);
87 }
88
89 fn mark_busy(&self, worker_id: usize) {
91 self.flags[worker_id].store(false, Ordering::Relaxed);
92 }
93}
94
95#[derive(Debug)]
102pub struct ProofWorkerHandle {
103 _result_tx: CrossbeamSender<ProofWorkerResult>,
106 result_rx: CrossbeamReceiver<ProofWorkerResult>,
108 storage_work_tx: CrossbeamSender<StorageProofJob>,
110 account_work_tx: CrossbeamSender<AccountProofJob>,
112 storage_availability: Arc<AvailabilitySheet>,
115 account_availability: Arc<AvailabilitySheet>,
118 storage_worker_count: usize,
120 account_worker_count: usize,
122}
123
124impl ProofWorkerHandle {
125 #[instrument(
138 name = "ProofWorkerHandle::new",
139 level = "debug",
140 target = "trie::proof_task",
141 skip_all
142 )]
143 pub fn new<Factory>(
144 runtime: &Runtime,
145 task_ctx: ProofTaskCtx<Factory>,
146 worker_counts: ProofWorkerCounts,
147 #[cfg(feature = "metrics")] metrics: ProofWorkerIdleMetrics,
148 ) -> Self
149 where
150 Factory:
151 DatabaseProviderROFactory<Provider: StateProofProvider> + Clone + Send + Sync + 'static,
152 {
153 let (result_tx, result_rx) = unbounded();
154 let (storage_work_tx, storage_work_rx) = unbounded();
155 let (account_work_tx, account_work_rx) = unbounded();
156
157 let storage_worker_count =
158 worker_counts.storage.max(1).min(runtime.proof_storage_worker_pool().num_threads());
159 let account_worker_count =
160 worker_counts.account.max(1).min(runtime.proof_account_worker_pool().num_threads());
161
162 let storage_availability = Arc::new(AvailabilitySheet::new(storage_worker_count));
163 let account_availability = Arc::new(AvailabilitySheet::new(account_worker_count));
164
165 debug!(
166 target: "trie::proof_task",
167 storage_worker_count,
168 account_worker_count,
169 "Spawning proof worker pools"
170 );
171
172 spawn_workers(
173 runtime,
174 ProofWorkerKind::Storage,
175 storage_worker_count,
176 ProofWorker {
177 task_ctx: task_ctx.clone(),
178 work_rx: storage_work_rx,
179 result_tx: result_tx.clone(),
180 availability: storage_availability.clone(),
181 #[cfg(feature = "metrics")]
182 metrics: metrics.clone(),
183 },
184 ProofWorker::<_, StorageProofJob>::run,
185 );
186
187 spawn_workers(
188 runtime,
189 ProofWorkerKind::Account,
190 account_worker_count,
191 ProofWorker {
192 task_ctx,
193 work_rx: account_work_rx,
194 result_tx: result_tx.clone(),
195 availability: account_availability.clone(),
196 #[cfg(feature = "metrics")]
197 metrics,
198 },
199 ProofWorker::<_, AccountProofJob>::run,
200 );
201
202 Self {
203 _result_tx: result_tx,
204 result_rx,
205 storage_work_tx,
206 account_work_tx,
207 storage_availability,
208 account_availability,
209 storage_worker_count,
210 account_worker_count,
211 }
212 }
213
214 pub fn has_multiple_idle_storage_workers(&self) -> bool {
216 self.storage_availability.has_multiple_idle()
217 }
218
219 pub fn has_multiple_idle_account_workers(&self) -> bool {
221 self.account_availability.has_multiple_idle()
222 }
223
224 pub const fn total_storage_workers(&self) -> usize {
226 self.storage_worker_count
227 }
228
229 pub const fn total_account_workers(&self) -> usize {
231 self.account_worker_count
232 }
233
234 pub const fn results(&self) -> &CrossbeamReceiver<ProofWorkerResult> {
239 &self.result_rx
240 }
241
242 pub fn dispatch(&self, targets: MultiProofTargetsV2) -> Result<usize, ProofWorkersUnavailable> {
252 let MultiProofTargetsV2 { account_targets, storage_targets } = targets;
253 let dispatched_at = Instant::now();
254 let mut jobs = 0;
255
256 for (hashed_address, targets) in storage_targets {
257 let job = StorageProofJob { hashed_address, targets, dispatched_at };
258 self.storage_work_tx.send(job).map_err(|_| ProofWorkersUnavailable)?;
259 jobs += 1;
260 }
261
262 if !account_targets.is_empty() {
263 let job = AccountProofJob { targets: account_targets, dispatched_at };
264 self.account_work_tx.send(job).map_err(|_| ProofWorkersUnavailable)?;
265 jobs += 1;
266 }
267
268 Ok(jobs)
269 }
270}
271
272pub type ProofWorkerResult = Result<DecodedMultiProofV2, StateRootTaskError>;
278
279#[derive(Debug, Clone, Copy, thiserror::Error)]
281#[error("proof workers unavailable")]
282pub struct ProofWorkersUnavailable;
283
284#[derive(Clone, Copy, Debug, PartialEq, Eq)]
290pub struct ProofWorkerCounts {
291 pub storage: usize,
293 pub account: usize,
295}
296
297impl ProofWorkerCounts {
298 pub const fn new(storage: usize, account: usize) -> Self {
300 Self { storage, account }
301 }
302
303 pub fn full(runtime: &Runtime) -> Self {
305 Self::new(
306 runtime.proof_storage_worker_pool().num_threads(),
307 runtime.proof_account_worker_pool().num_threads(),
308 )
309 }
310}
311
312#[derive(Clone, Debug)]
314pub struct ProofTaskCtx<Factory> {
315 factory: Factory,
317 #[cfg(feature = "trie-debug")]
319 proof_jitter: Option<Duration>,
320}
321
322impl<Factory> ProofTaskCtx<Factory> {
323 pub const fn new(factory: Factory) -> Self {
325 Self {
326 factory,
327 #[cfg(feature = "trie-debug")]
328 proof_jitter: None,
329 }
330 }
331
332 #[cfg(feature = "trie-debug")]
334 pub const fn with_proof_jitter(mut self, jitter: Option<Duration>) -> Self {
335 self.proof_jitter = jitter;
336 self
337 }
338}
339
340#[derive(Clone, Copy, Debug)]
342enum ProofWorkerKind {
343 Storage,
344 Account,
345}
346
347impl ProofWorkerKind {
348 fn pool(self, runtime: &Runtime) -> &WorkerPool {
350 match self {
351 Self::Storage => runtime.proof_storage_worker_pool(),
352 Self::Account => runtime.proof_account_worker_pool(),
353 }
354 }
355
356 const fn name(self) -> &'static str {
358 match self {
359 Self::Storage => "storage",
360 Self::Account => "account",
361 }
362 }
363
364 const fn task_name(self) -> &'static str {
366 match self {
367 Self::Storage => "storage-workers",
368 Self::Account => "account-workers",
369 }
370 }
371
372 fn span(self, parent: &tracing::Span, worker_id: usize) -> tracing::Span {
374 match self {
375 Self::Storage => {
376 debug_span!(target: "trie::proof_task", parent: parent, "storage_worker", ?worker_id)
377 }
378 Self::Account => {
379 debug_span!(target: "trie::proof_task", parent: parent, "account_worker", ?worker_id)
380 }
381 }
382 }
383}
384
385fn spawn_workers<Factory, J>(
390 runtime: &Runtime,
391 kind: ProofWorkerKind,
392 worker_count: usize,
393 worker: ProofWorker<Factory, J>,
394 run: fn(&ProofWorker<Factory, J>, usize) -> ProviderResult<()>,
395) where
396 Factory: Send + Sync + 'static,
397 J: Send + 'static,
398{
399 let rt = runtime.clone();
400 let parent_span = tracing::Span::current();
401 runtime.spawn_blocking_named(kind.task_name(), move || {
402 let next_worker_id = AtomicUsize::new(0);
403 kind.pool(&rt).broadcast(worker_count, |_| {
404 let worker_id = next_worker_id.fetch_add(1, Ordering::Relaxed);
405 let _guard = kind.span(&parent_span, worker_id).entered();
406
407 if let Err(error) = run(&worker, worker_id) {
408 error!(
409 target: "trie::proof_task",
410 kind = kind.name(),
411 worker_id,
412 ?error,
413 "Proof worker failed"
414 );
415 let error = StateRootTaskError::Other(Box::new(ProofWorkerError {
416 kind,
417 worker_id,
418 source: error,
419 }));
420 let _ = worker.result_tx.send(Err(error));
422 }
423 });
424 });
425}
426
427struct ProofWorker<Factory, J> {
429 task_ctx: ProofTaskCtx<Factory>,
431 work_rx: CrossbeamReceiver<J>,
433 result_tx: CrossbeamSender<ProofWorkerResult>,
435 availability: Arc<AvailabilitySheet>,
437 #[cfg(feature = "metrics")]
439 metrics: ProofWorkerIdleMetrics,
440}
441
442impl<Factory: DatabaseProviderROFactory, J> ProofWorker<Factory, J> {
443 fn process_jobs(
452 &self,
453 worker_id: usize,
454 mut provider: Factory::Provider,
455 mut process: impl FnMut(&Factory::Provider, J) -> ProofWorkerResult,
456 ) -> ProviderResult<Duration> {
457 self.availability.mark_idle(worker_id);
459
460 let mut total_idle_time = Duration::ZERO;
461 let mut idle_start = Instant::now();
462
463 while let Ok(job) = self.work_rx.recv() {
464 total_idle_time += idle_start.elapsed();
465 self.availability.mark_busy(worker_id);
466
467 #[cfg(feature = "trie-debug")]
468 if let Some(max_jitter) = self.task_ctx.proof_jitter {
469 let jitter =
470 Duration::from_nanos(rand::random_range(0..=max_jitter.as_nanos() as u64));
471 trace!(
472 target: "trie::proof_task",
473 worker_id,
474 jitter_us = jitter.as_micros(),
475 "Proof worker applying proof jitter"
476 );
477 std::thread::sleep(jitter);
478 }
479
480 match catch_unwind(AssertUnwindSafe(|| process(&provider, job))) {
481 Ok(result) => self.send_result(worker_id, result),
482 Err(panic) => {
483 let message = panic
484 .downcast_ref::<&str>()
485 .copied()
486 .or_else(|| panic.downcast_ref::<String>().map(String::as_str))
487 .unwrap_or("unknown panic")
488 .to_string();
489 error!(target: "trie::proof_task", worker_id, %message, "Proof job panicked");
490 self.send_result(worker_id, Err(ProofJobError::Panicked { message }.into()));
491 provider = self.task_ctx.factory.database_provider_ro()?;
492 }
493 }
494
495 self.availability.mark_idle(worker_id);
496 idle_start = Instant::now();
497 }
498
499 Ok(total_idle_time)
500 }
501
502 fn send_result(&self, worker_id: usize, result: ProofWorkerResult) {
505 if self.result_tx.send(result).is_err() {
506 trace!(
507 target: "trie::proof_task",
508 worker_id,
509 "Proof result receiver dropped, discarding result"
510 );
511 }
512 }
513}
514
515impl<Factory> ProofWorker<Factory, StorageProofJob>
516where
517 Factory: DatabaseProviderROFactory<Provider: StateProofProvider>,
518{
519 fn run(&self, worker_id: usize) -> ProviderResult<()> {
521 let provider = self.task_ctx.factory.database_provider_ro()?;
522
523 trace!(target: "trie::proof_task", worker_id, "Storage worker started");
524
525 let mut storage_proofs_processed = 0u64;
526 let total_idle_time = self.process_jobs(worker_id, provider, |provider, job| {
527 let StorageProofJob { hashed_address, targets, dispatched_at } = job;
528 let _span = debug_span!(
529 target: "trie::proof_task",
530 "Storage proof calculation",
531 ?hashed_address,
532 targets = targets.len(),
533 )
534 .entered();
535 let proof_start = Instant::now();
536
537 let proof = provider.multiproof_v2(
538 MultiProofTargetsV2 {
539 account_targets: Vec::new(),
540 storage_targets: std::iter::once((hashed_address, targets)).collect(),
541 },
542 true,
543 );
544
545 trace!(
546 target: "trie::proof_task",
547 worker_id,
548 ?hashed_address,
549 proof_time_us = proof_start.elapsed().as_micros(),
550 dispatch_to_completion_us = dispatched_at.elapsed().as_micros(),
551 "Storage proof completed"
552 );
553
554 storage_proofs_processed += 1;
555 proof.map_err(|source| ProofJobError::Storage { hashed_address, source }.into())
556 })?;
557
558 trace!(
559 target: "trie::proof_task",
560 worker_id,
561 storage_proofs_processed,
562 total_idle_time_us = total_idle_time.as_micros(),
563 "Storage worker shutting down"
564 );
565
566 #[cfg(feature = "metrics")]
567 self.metrics.record_storage_worker_idle_time(total_idle_time);
568
569 Ok(())
570 }
571}
572
573impl<Factory> ProofWorker<Factory, AccountProofJob>
574where
575 Factory: DatabaseProviderROFactory<Provider: StateProofProvider>,
576{
577 fn run(&self, worker_id: usize) -> ProviderResult<()> {
579 let provider = self.task_ctx.factory.database_provider_ro()?;
580
581 trace!(target: "trie::proof_task", worker_id, "Account worker started");
582
583 let mut account_proofs_processed = 0u64;
584 let total_idle_time = self.process_jobs(worker_id, provider, |provider, job| {
585 let AccountProofJob { targets, dispatched_at } = job;
586 let _span = debug_span!(
587 target: "trie::proof_task",
588 "Account proof calculation",
589 targets = targets.len(),
590 )
591 .entered();
592 let proof_start = Instant::now();
593
594 let proof = provider.multiproof_v2(
595 MultiProofTargetsV2 {
596 account_targets: targets,
597 storage_targets: Default::default(),
598 },
599 true,
600 );
601
602 trace!(
603 target: "trie::proof_task",
604 worker_id,
605 proof_time_us = proof_start.elapsed().as_micros(),
606 dispatch_to_completion_us = dispatched_at.elapsed().as_micros(),
607 "Account proof completed"
608 );
609
610 account_proofs_processed += 1;
611 proof.map_err(|source| ProofJobError::Account { source }.into())
612 })?;
613
614 trace!(
615 target: "trie::proof_task",
616 worker_id,
617 account_proofs_processed,
618 total_idle_time_us = total_idle_time.as_micros(),
619 "Account worker shutting down"
620 );
621
622 #[cfg(feature = "metrics")]
623 self.metrics.record_account_worker_idle_time(total_idle_time);
624
625 Ok(())
626 }
627}
628
629#[derive(Debug)]
631struct StorageProofJob {
632 hashed_address: B256,
633 targets: Vec<ProofV2Target>,
634 dispatched_at: Instant,
635}
636
637#[derive(Debug)]
639struct AccountProofJob {
640 targets: Vec<ProofV2Target>,
641 dispatched_at: Instant,
642}
643
644#[derive(Debug, thiserror::Error)]
646#[error("{} worker {worker_id}: {source}", kind.name())]
647struct ProofWorkerError {
648 kind: ProofWorkerKind,
649 worker_id: usize,
650 #[source]
651 source: ProviderError,
652}
653
654#[derive(Debug, thiserror::Error)]
656enum ProofJobError {
657 #[error("storage proof of account {hashed_address}: {source}")]
658 Storage {
659 hashed_address: B256,
660 #[source]
661 source: ProviderError,
662 },
663 #[error("account proof: {source}")]
664 Account {
665 #[source]
666 source: ProviderError,
667 },
668 #[error("proof job panicked: {message}")]
669 Panicked { message: String },
670}
671
672impl From<ProofJobError> for StateRootTaskError {
673 fn from(error: ProofJobError) -> Self {
674 Self::Other(Box::new(error))
675 }
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681 use crossbeam_channel::RecvTimeoutError;
682 use reth_chainspec::ChainSpec;
683 use reth_provider::test_utils::create_test_provider_factory_with_chain_spec;
684 use std::sync::Arc;
685
686 fn test_ctx<Factory>(factory: Factory) -> ProofTaskCtx<Factory> {
687 ProofTaskCtx::new(factory)
688 }
689
690 #[test]
692 fn spawn_proof_workers_creates_handle() {
693 let chain_spec = Arc::new(ChainSpec::default());
694 let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec);
695 let anchor_hash = reth_db_common::init::init_genesis(&provider_factory).unwrap();
696 let factory = reth_storage_overlay::OverlayStateProviderFactory::new(
697 provider_factory,
698 reth_storage_overlay::OverlayManager::<
699 reth_ethereum_primitives::EthPrimitives,
700 >::default()
701 .overlay_builder(anchor_hash),
702 );
703 let ctx = test_ctx(factory.clone());
704
705 let runtime = reth_tasks::Runtime::test();
706 let proof_worker_handle = ProofWorkerHandle::new(
707 &runtime,
708 ctx,
709 ProofWorkerCounts::full(&runtime),
710 #[cfg(feature = "metrics")]
711 Default::default(),
712 );
713 let proof_result_rx = proof_worker_handle.results().clone();
714
715 drop(proof_worker_handle);
717
718 assert_eq!(
720 proof_result_rx.recv_timeout(Duration::from_secs(30)).unwrap_err(),
721 RecvTimeoutError::Disconnected
722 );
723 drop(factory);
724 }
725
726 #[test]
728 fn single_pool_serves_fractional_worker_counts() {
729 let provider_factory =
730 create_test_provider_factory_with_chain_spec(Arc::new(ChainSpec::default()));
731 let anchor_hash = reth_db_common::init::init_genesis(&provider_factory).unwrap();
732 let factory = reth_storage_overlay::OverlayStateProviderFactory::new(
733 provider_factory,
734 reth_storage_overlay::OverlayManager::<reth_ethereum_primitives::EthPrimitives>::default()
735 .overlay_builder(anchor_hash),
736 );
737 let ctx = test_ctx(factory);
738
739 let runtime = reth_tasks::RuntimeBuilder::new(
740 reth_tasks::RuntimeConfig::default()
741 .with_rayon(reth_tasks::RayonConfig { cpu_threads: Some(2), ..Default::default() }),
742 )
743 .build()
744 .unwrap();
745 let pool_threads = runtime.proof_storage_worker_pool().num_threads();
746 assert_eq!(pool_threads, 8);
747 assert_eq!(runtime.proof_account_worker_pool().num_threads(), pool_threads);
748
749 for counts in [
750 ProofWorkerCounts::new(0, 0),
751 ProofWorkerCounts::new(pool_threads / 4, pool_threads / 4),
752 ProofWorkerCounts::new(pool_threads / 2, pool_threads / 2),
753 ProofWorkerCounts::full(&runtime),
754 ProofWorkerCounts::new(pool_threads + 1, pool_threads + 2),
755 ] {
756 let proof_worker_handle = ProofWorkerHandle::new(
757 &runtime,
758 ctx.clone(),
759 counts,
760 #[cfg(feature = "metrics")]
761 Default::default(),
762 );
763 assert_eq!(
764 proof_worker_handle.total_storage_workers(),
765 counts.storage.max(1).min(pool_threads)
766 );
767 assert_eq!(
768 proof_worker_handle.total_account_workers(),
769 counts.account.max(1).min(pool_threads)
770 );
771
772 let address = B256::ZERO;
773 let storage_targets = MultiProofTargetsV2 {
774 storage_targets: std::iter::once((address, vec![ProofV2Target::new(B256::ZERO)]))
775 .collect(),
776 ..Default::default()
777 };
778 assert_eq!(proof_worker_handle.dispatch(storage_targets).unwrap(), 1);
779 let storage_proof = proof_worker_handle
780 .results()
781 .recv_timeout(Duration::from_secs(30))
782 .unwrap()
783 .unwrap();
784 assert!(storage_proof.account_proofs.is_empty());
785 assert!(storage_proof.storage_proofs.contains_key(&address));
786
787 let account_targets = MultiProofTargetsV2 {
788 account_targets: vec![ProofV2Target::new(address)],
789 ..Default::default()
790 };
791 assert_eq!(proof_worker_handle.dispatch(account_targets).unwrap(), 1);
792 let account_proof = proof_worker_handle
793 .results()
794 .recv_timeout(Duration::from_secs(30))
795 .unwrap()
796 .unwrap();
797 assert!(account_proof.storage_proofs.is_empty());
798
799 let results = proof_worker_handle.results().clone();
800 drop(proof_worker_handle);
801 assert!(matches!(
804 results.recv_timeout(Duration::from_secs(30)),
805 Err(crossbeam_channel::RecvTimeoutError::Disconnected)
806 ));
807 }
808 }
809}