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    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/// Tracks worker availability counts.
49///
50/// It uses cacheline-aligned flags to avoid core-to-core chatter.
51#[derive(Debug)]
52struct AvailabilitySheet {
53    /// One flag per worker, each on its own cacheline. Workers store `true` when idle,
54    /// `false` when busy. Only the owning worker writes; the dispatcher only reads.
55    flags: Vec<crossbeam_utils::CachePadded<AtomicBool>>,
56}
57
58impl AvailabilitySheet {
59    /// Creates a new sheet with `count` workers, all initially marked as busy.
60    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    /// Returns `true` if more than one worker is currently idle.
67    ///
68    /// Note, that this is somewhat racy since a flag that was just saying `idle` and we counted it
69    /// as such might turn into `busy` right away.
70    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    /// Marks the given worker as idle.
84    fn mark_idle(&self, worker_id: usize) {
85        self.flags[worker_id].store(true, Ordering::Relaxed);
86    }
87
88    /// Marks the given worker as busy.
89    fn mark_busy(&self, worker_id: usize) {
90        self.flags[worker_id].store(false, Ordering::Relaxed);
91    }
92}
93
94/// Owns the storage and account proof worker pools and the channel their results arrive on.
95///
96/// Every job queued by [`Self::dispatch`] sends exactly one [`ProofWorkerResult`] to
97/// [`Self::results`]. A worker that fails to start sends an error that belongs to no job instead,
98/// and jobs left queued when every worker of a pool has failed get no result. Workers shut down
99/// once the handle is dropped.
100#[derive(Debug)]
101pub struct ProofWorkerHandle {
102    /// Sender shared with the workers, kept so that [`Self::results`] never disconnects while the
103    /// handle is alive.
104    _result_tx: CrossbeamSender<ProofWorkerResult>,
105    /// Results sent by both worker pools.
106    result_rx: CrossbeamReceiver<ProofWorkerResult>,
107    /// Direct sender to storage worker pool
108    storage_work_tx: CrossbeamSender<StorageProofJob>,
109    /// Direct sender to account worker pool
110    account_work_tx: CrossbeamSender<AccountProofJob>,
111    /// Per-worker availability flags for storage workers. Used to determine whether to chunk
112    /// multiproofs.
113    storage_availability: Arc<AvailabilitySheet>,
114    /// Per-worker availability flags for account workers. Used to determine whether to chunk
115    /// multiproofs.
116    account_availability: Arc<AvailabilitySheet>,
117    /// Total number of storage workers spawned
118    storage_worker_count: usize,
119    /// Total number of account workers spawned
120    account_worker_count: usize,
121}
122
123impl ProofWorkerHandle {
124    /// Spawns storage and account worker pools with dedicated database transactions.
125    ///
126    /// Returns a handle for submitting proof tasks to the worker pools.
127    /// Workers run until the handle is dropped.
128    ///
129    /// # Parameters
130    /// - `runtime`: The centralized runtime used to spawn blocking worker tasks
131    /// - `task_ctx`: Shared context with the factory of providers that serve the proofs. Workers
132    ///   request proofs with exact masks, as needed to produce trie updates from them.
133    /// - `worker_counts`: How many workers of each pool to spawn for this block, with a minimum of
134    ///   one worker of each kind
135    /// - `metrics`: Idle-time metric handles shared across blocks (with the metrics feature).
136    #[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    /// Returns `true` if more than one storage worker is currently idle.
214    pub fn has_multiple_idle_storage_workers(&self) -> bool {
215        self.storage_availability.has_multiple_idle()
216    }
217
218    /// Returns `true` if more than one account worker is currently idle.
219    pub fn has_multiple_idle_account_workers(&self) -> bool {
220        self.account_availability.has_multiple_idle()
221    }
222
223    /// Returns the total number of storage workers in the pool.
224    pub const fn total_storage_workers(&self) -> usize {
225        self.storage_worker_count
226    }
227
228    /// Returns the total number of account workers in the pool.
229    pub const fn total_account_workers(&self) -> usize {
230        self.account_worker_count
231    }
232
233    /// Returns the receiver of job results.
234    ///
235    /// Results arrive in completion order. The receiver never disconnects while the handle is
236    /// alive.
237    pub const fn results(&self) -> &CrossbeamReceiver<ProofWorkerResult> {
238        &self.result_rx
239    }
240
241    /// Queues the proofs for `targets` and returns the number of jobs queued.
242    ///
243    /// The storage targets of each account become one storage job, and all account targets
244    /// together become one account job. Each job sends exactly one [`ProofWorkerResult`] to
245    /// [`Self::results`]. A storage job without slot targets proves only the account's storage
246    /// root node.
247    ///
248    /// Fails once a worker pool has shut down, which only happens after its workers reported start
249    /// failures on [`Self::results`].
250    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
271/// A message received from [`ProofWorkerHandle::results`]: the outcome of one queued job, or a
272/// worker's failure to start.
273///
274/// A storage job yields the storage proof of a single account and no account proofs. An account
275/// job yields account proofs and no storage proofs. A failed job does not stop its worker.
276pub type ProofWorkerResult = Result<DecodedMultiProofV2, StateRootTaskError>;
277
278/// Error returned by [`ProofWorkerHandle::dispatch`] when a worker pool has shut down.
279#[derive(Debug, Clone, Copy, thiserror::Error)]
280#[error("proof workers unavailable")]
281pub struct ProofWorkersUnavailable;
282
283/// How many workers [`ProofWorkerHandle::new`] spawns for a block.
284///
285/// Worker instances are recreated for each block on persistent thread pools. Each worker opens a
286/// database transaction and trie cursors, so the caller sizes the counts per block. Counts are
287/// clamped to at least one worker of each kind and capped by that kind's pool capacity.
288#[derive(Clone, Copy, Debug, PartialEq, Eq)]
289pub struct ProofWorkerCounts {
290    /// Number of storage proof workers to spawn.
291    pub storage: usize,
292    /// Number of account proof workers to spawn.
293    pub account: usize,
294}
295
296impl ProofWorkerCounts {
297    /// Creates counts for the given number of storage and account workers.
298    pub const fn new(storage: usize, account: usize) -> Self {
299        Self { storage, account }
300    }
301
302    /// Creates counts that use every thread of both pools.
303    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/// Data used for opening proof providers that is shared across all proof worker instances.
312#[derive(Clone, Debug)]
313pub struct ProofTaskCtx<Factory> {
314    /// The factory for creating state providers.
315    factory: Factory,
316    /// Maximum random jitter to apply before each proof computation (trie-debug only).
317    #[cfg(feature = "trie-debug")]
318    proof_jitter: Option<Duration>,
319}
320
321impl<Factory> ProofTaskCtx<Factory> {
322    /// Creates a new [`ProofTaskCtx`] with the given factory.
323    pub const fn new(factory: Factory) -> Self {
324        Self {
325            factory,
326            #[cfg(feature = "trie-debug")]
327            proof_jitter: None,
328        }
329    }
330
331    /// Sets the maximum proof jitter duration (trie-debug only).
332    #[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/// The worker pool a proof worker belongs to.
340#[derive(Clone, Copy, Debug)]
341enum ProofWorkerKind {
342    Storage,
343    Account,
344}
345
346impl ProofWorkerKind {
347    /// Returns the runtime pool the workers of this kind run on.
348    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    /// Returns the name of the pool used in diagnostics.
356    const fn name(self) -> &'static str {
357        match self {
358            Self::Storage => "storage",
359            Self::Account => "account",
360        }
361    }
362
363    /// Returns the name of the blocking task that hosts the pool's workers.
364    const fn task_name(self) -> &'static str {
365        match self {
366            Self::Storage => "storage-workers",
367            Self::Account => "account-workers",
368        }
369    }
370
371    /// Returns the span for the worker with the given ID.
372    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
384/// Runs `run` as `worker_count` workers of the given kind on a blocking task.
385///
386/// A worker that fails to start sends a [`ProofWorkerError`] to the result channel.
387fn 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                // The send fails only once the handle is dropped, when nobody waits for results.
419                let _ = worker.result_tx.send(Err(error));
420            }
421        });
422    });
423}
424
425/// State shared by the workers of one pool, each holding its own database transaction.
426struct ProofWorker<Factory, J> {
427    /// Shared task context with database factory and prefix sets
428    task_ctx: ProofTaskCtx<Factory>,
429    /// Channel for receiving jobs
430    work_rx: CrossbeamReceiver<J>,
431    /// Sender for job results and worker failures
432    result_tx: CrossbeamSender<ProofWorkerResult>,
433    /// Per-worker availability flags
434    availability: Arc<AvailabilitySheet>,
435    /// Metrics shared by all workers of the handle.
436    #[cfg(feature = "metrics")]
437    metrics: ProofWorkerIdleMetrics,
438}
439
440impl<Factory, J> ProofWorker<Factory, J> {
441    /// Runs `process` for every job until the handle is dropped, advertising the worker as busy
442    /// while a job runs. Returns the total time spent waiting for jobs.
443    fn process_jobs(&self, worker_id: usize, mut process: impl FnMut(J)) -> Duration {
444        // Count this worker as available only after successful initialization.
445        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    /// Sends a job result. The send fails only once the handle is dropped, when the result is no
477    /// longer needed.
478    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    /// Opens a database transaction and serves storage proof jobs until the handle is dropped.
494    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    /// Opens a database transaction and serves account proof jobs until the handle is dropped.
554    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/// A queued storage proof of one account.
607#[derive(Debug)]
608struct StorageProofJob {
609    hashed_address: B256,
610    targets: Vec<ProofV2Target>,
611    dispatched_at: Instant,
612}
613
614/// A queued account proof.
615#[derive(Debug)]
616struct AccountProofJob {
617    targets: Vec<ProofV2Target>,
618    dispatched_at: Instant,
619}
620
621/// Context for a provider failure that prevents a proof worker from starting.
622#[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/// A failed proof job, identified by the proof it calculated.
632#[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    /// Ensures `ProofWorkerHandle::new` spawns workers correctly.
666    #[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        // Workers shut down automatically when the handle is dropped.
691        drop(proof_worker_handle);
692
693        // Keep the database alive until both pools release their providers.
694        assert_eq!(
695            proof_result_rx.recv_timeout(Duration::from_secs(30)).unwrap_err(),
696            RecvTimeoutError::Disconnected
697        );
698        drop(factory);
699    }
700
701    /// Ensures a single pool serves quarter, half, and full-capacity worker counts.
702    #[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            // Every broadcast holds a result sender until all its workers have exited. An
777            // initialization error is a message here, rather than a successful shutdown.
778            assert!(matches!(
779                results.recv_timeout(Duration::from_secs(30)),
780                Err(crossbeam_channel::RecvTimeoutError::Disconnected)
781            ));
782        }
783    }
784}