Skip to main content

reth_trie_parallel/proof_task/
mod.rs

1//! Parallel proof computation using worker pools with dedicated database transactions.
2//!
3//!
4//! # Architecture
5//!
6//! - **Worker Pools**: Pre-spawned workers with dedicated database transactions
7//!   - Storage pool: Handles storage proofs, one account's storage trie per job
8//!   - Account pool: Handles account proofs, calculating storage roots inline
9//! - **Automatic Shutdown**: Workers terminate gracefully when the handle is dropped
10//!
11//! # Message Flow
12//!
13//! 1. [`ProofWorkerHandle::dispatch`] splits proof targets into storage and account jobs and queues
14//!    each job for the matching worker pool.
15//! 2. A worker receives a job, runs the proof, and sends one [`ProofWorkerResult`] to the handle's
16//!    result channel.
17//! 3. The owner of the handle receives the results through [`ProofWorkerHandle::results`].
18//!
19//! ```text
20//! ProofWorkerHandle::dispatch -> Storage/Account Worker
21//!                                          |
22//!                                          v
23//! ProofWorkerHandle::results <-- ProofWorkerResult
24//! ```
25
26#[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/// Tracks worker availability counts.
50///
51/// It uses cacheline-aligned flags to avoid core-to-core chatter.
52#[derive(Debug)]
53struct AvailabilitySheet {
54    /// One flag per worker, each on its own cacheline. Workers store `true` when idle,
55    /// `false` when busy. Only the owning worker writes; the dispatcher only reads.
56    flags: Vec<crossbeam_utils::CachePadded<AtomicBool>>,
57}
58
59impl AvailabilitySheet {
60    /// Creates a new sheet with `count` workers, all initially marked as busy.
61    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    /// Returns `true` if more than one worker is currently idle.
68    ///
69    /// Note, that this is somewhat racy since a flag that was just saying `idle` and we counted it
70    /// as such might turn into `busy` right away.
71    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    /// Marks the given worker as idle.
85    fn mark_idle(&self, worker_id: usize) {
86        self.flags[worker_id].store(true, Ordering::Relaxed);
87    }
88
89    /// Marks the given worker as busy.
90    fn mark_busy(&self, worker_id: usize) {
91        self.flags[worker_id].store(false, Ordering::Relaxed);
92    }
93}
94
95/// Owns the storage and account proof worker pools and the channel their results arrive on.
96///
97/// Every job queued by [`Self::dispatch`] sends exactly one [`ProofWorkerResult`] to
98/// [`Self::results`]. A worker that fails to open its provider, at start or after a panicking
99/// job, sends an error that belongs to no job instead, and jobs left queued when every worker of a
100/// pool has failed get no result. Workers shut down once the handle is dropped.
101#[derive(Debug)]
102pub struct ProofWorkerHandle {
103    /// Sender shared with the workers, kept so that [`Self::results`] never disconnects while the
104    /// handle is alive.
105    _result_tx: CrossbeamSender<ProofWorkerResult>,
106    /// Results sent by both worker pools.
107    result_rx: CrossbeamReceiver<ProofWorkerResult>,
108    /// Direct sender to storage worker pool
109    storage_work_tx: CrossbeamSender<StorageProofJob>,
110    /// Direct sender to account worker pool
111    account_work_tx: CrossbeamSender<AccountProofJob>,
112    /// Per-worker availability flags for storage workers. Used to determine whether to chunk
113    /// multiproofs.
114    storage_availability: Arc<AvailabilitySheet>,
115    /// Per-worker availability flags for account workers. Used to determine whether to chunk
116    /// multiproofs.
117    account_availability: Arc<AvailabilitySheet>,
118    /// Total number of storage workers spawned
119    storage_worker_count: usize,
120    /// Total number of account workers spawned
121    account_worker_count: usize,
122}
123
124impl ProofWorkerHandle {
125    /// Spawns storage and account worker pools with dedicated database transactions.
126    ///
127    /// Returns a handle for submitting proof tasks to the worker pools.
128    /// Workers run until the handle is dropped.
129    ///
130    /// # Parameters
131    /// - `runtime`: The centralized runtime used to spawn blocking worker tasks
132    /// - `task_ctx`: Shared context with the factory of providers that serve the proofs. Workers
133    ///   request proofs with exact masks, as needed to produce trie updates from them.
134    /// - `worker_counts`: How many workers of each pool to spawn for this block, with a minimum of
135    ///   one worker of each kind
136    /// - `metrics`: Idle-time metric handles shared across blocks (with the metrics feature).
137    #[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    /// Returns `true` if more than one storage worker is currently idle.
215    pub fn has_multiple_idle_storage_workers(&self) -> bool {
216        self.storage_availability.has_multiple_idle()
217    }
218
219    /// Returns `true` if more than one account worker is currently idle.
220    pub fn has_multiple_idle_account_workers(&self) -> bool {
221        self.account_availability.has_multiple_idle()
222    }
223
224    /// Returns the total number of storage workers in the pool.
225    pub const fn total_storage_workers(&self) -> usize {
226        self.storage_worker_count
227    }
228
229    /// Returns the total number of account workers in the pool.
230    pub const fn total_account_workers(&self) -> usize {
231        self.account_worker_count
232    }
233
234    /// Returns the receiver of job results.
235    ///
236    /// Results arrive in completion order. The receiver never disconnects while the handle is
237    /// alive.
238    pub const fn results(&self) -> &CrossbeamReceiver<ProofWorkerResult> {
239        &self.result_rx
240    }
241
242    /// Queues the proofs for `targets` and returns the number of jobs queued.
243    ///
244    /// The storage targets of each account become one storage job, and all account targets
245    /// together become one account job. Each job sends exactly one [`ProofWorkerResult`] to
246    /// [`Self::results`]. A storage job without slot targets proves only the account's storage
247    /// root node.
248    ///
249    /// Fails once a worker pool has shut down, which only happens after its workers reported start
250    /// failures on [`Self::results`].
251    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
272/// A message received from [`ProofWorkerHandle::results`]: the outcome of one queued job, or a
273/// worker's failure to start.
274///
275/// A storage job yields the storage proof of a single account and no account proofs. An account
276/// job yields account proofs and no storage proofs. A failed job does not stop its worker.
277pub type ProofWorkerResult = Result<DecodedMultiProofV2, StateRootTaskError>;
278
279/// Error returned by [`ProofWorkerHandle::dispatch`] when a worker pool has shut down.
280#[derive(Debug, Clone, Copy, thiserror::Error)]
281#[error("proof workers unavailable")]
282pub struct ProofWorkersUnavailable;
283
284/// How many workers [`ProofWorkerHandle::new`] spawns for a block.
285///
286/// Worker instances are recreated for each block on persistent thread pools. Each worker opens a
287/// database transaction and trie cursors, so the caller sizes the counts per block. Counts are
288/// clamped to at least one worker of each kind and capped by that kind's pool capacity.
289#[derive(Clone, Copy, Debug, PartialEq, Eq)]
290pub struct ProofWorkerCounts {
291    /// Number of storage proof workers to spawn.
292    pub storage: usize,
293    /// Number of account proof workers to spawn.
294    pub account: usize,
295}
296
297impl ProofWorkerCounts {
298    /// Creates counts for the given number of storage and account workers.
299    pub const fn new(storage: usize, account: usize) -> Self {
300        Self { storage, account }
301    }
302
303    /// Creates counts that use every thread of both pools.
304    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/// Data used for opening proof providers that is shared across all proof worker instances.
313#[derive(Clone, Debug)]
314pub struct ProofTaskCtx<Factory> {
315    /// The factory for creating state providers.
316    factory: Factory,
317    /// Maximum random jitter to apply before each proof computation (trie-debug only).
318    #[cfg(feature = "trie-debug")]
319    proof_jitter: Option<Duration>,
320}
321
322impl<Factory> ProofTaskCtx<Factory> {
323    /// Creates a new [`ProofTaskCtx`] with the given factory.
324    pub const fn new(factory: Factory) -> Self {
325        Self {
326            factory,
327            #[cfg(feature = "trie-debug")]
328            proof_jitter: None,
329        }
330    }
331
332    /// Sets the maximum proof jitter duration (trie-debug only).
333    #[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/// The worker pool a proof worker belongs to.
341#[derive(Clone, Copy, Debug)]
342enum ProofWorkerKind {
343    Storage,
344    Account,
345}
346
347impl ProofWorkerKind {
348    /// Returns the runtime pool the workers of this kind run on.
349    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    /// Returns the name of the pool used in diagnostics.
357    const fn name(self) -> &'static str {
358        match self {
359            Self::Storage => "storage",
360            Self::Account => "account",
361        }
362    }
363
364    /// Returns the name of the blocking task that hosts the pool's workers.
365    const fn task_name(self) -> &'static str {
366        match self {
367            Self::Storage => "storage-workers",
368            Self::Account => "account-workers",
369        }
370    }
371
372    /// Returns the span for the worker with the given ID.
373    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
385/// Runs `run` as `worker_count` workers of the given kind on a blocking task.
386///
387/// A worker that fails to open its provider, at start or after a panicking job, sends a
388/// [`ProofWorkerError`] to the result channel and stops.
389fn 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                // The send fails only once the handle is dropped, when nobody waits for results.
421                let _ = worker.result_tx.send(Err(error));
422            }
423        });
424    });
425}
426
427/// State shared by the workers of one pool, each holding its own database transaction.
428struct ProofWorker<Factory, J> {
429    /// Shared task context with database factory and prefix sets
430    task_ctx: ProofTaskCtx<Factory>,
431    /// Channel for receiving jobs
432    work_rx: CrossbeamReceiver<J>,
433    /// Sender for job results and worker failures
434    result_tx: CrossbeamSender<ProofWorkerResult>,
435    /// Per-worker availability flags
436    availability: Arc<AvailabilitySheet>,
437    /// Metrics shared by all workers of the handle.
438    #[cfg(feature = "metrics")]
439    metrics: ProofWorkerIdleMetrics,
440}
441
442impl<Factory: DatabaseProviderROFactory, J> ProofWorker<Factory, J> {
443    /// Runs `process` with `provider` for every job until the handle is dropped and sends each
444    /// job's result, advertising the worker as busy while a job runs. Returns the total time spent
445    /// waiting for jobs.
446    ///
447    /// A job that panics sends [`ProofJobError::Panicked`] instead of its result, so the job's
448    /// receiver fails instead of waiting for a result that never arrives. The worker then replaces
449    /// its provider, whose cached proof state must not be reused after a panic, and moves on to the
450    /// next job. Fails if the new provider cannot be opened.
451    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        // Count this worker as available only after successful initialization.
458        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    /// Sends a job result. The send fails only once the handle is dropped, when the result is no
503    /// longer needed.
504    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    /// Opens a database transaction and serves storage proof jobs until the handle is dropped.
520    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    /// Opens a database transaction and serves account proof jobs until the handle is dropped.
578    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/// A queued storage proof of one account.
630#[derive(Debug)]
631struct StorageProofJob {
632    hashed_address: B256,
633    targets: Vec<ProofV2Target>,
634    dispatched_at: Instant,
635}
636
637/// A queued account proof.
638#[derive(Debug)]
639struct AccountProofJob {
640    targets: Vec<ProofV2Target>,
641    dispatched_at: Instant,
642}
643
644/// Context for a provider failure that stops a proof worker, at start or after a panicking job.
645#[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/// A failed proof job, identified by the proof it calculated.
655#[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    /// Ensures `ProofWorkerHandle::new` spawns workers correctly.
691    #[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        // Workers shut down automatically when the handle is dropped.
716        drop(proof_worker_handle);
717
718        // Keep the database alive until both pools release their providers.
719        assert_eq!(
720            proof_result_rx.recv_timeout(Duration::from_secs(30)).unwrap_err(),
721            RecvTimeoutError::Disconnected
722        );
723        drop(factory);
724    }
725
726    /// Ensures a single pool serves quarter, half, and full-capacity worker counts.
727    #[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            // Every broadcast holds a result sender until all its workers have exited. An
802            // initialization error is a message here, rather than a successful shutdown.
803            assert!(matches!(
804                results.recv_timeout(Duration::from_secs(30)),
805                Err(crossbeam_channel::RecvTimeoutError::Disconnected)
806            ));
807        }
808    }
809}