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