Skip to main content

reth_trie_parallel/
proof_task.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
8//!   - Account pool: Handles account multiproofs
9//! - **Direct Channel Access**: `ProofWorkerHandle` provides type-safe queue methods with direct
10//!   access to worker channels, eliminating routing overhead
11//! - **Automatic Shutdown**: Workers terminate gracefully when all handles are dropped
12//!
13//! # Message Flow
14//!
15//! 1. The `SparseTrieCacheTask` prepares a storage or account job and hands it to
16//!    `ProofWorkerHandle`. The job carries a `ProofResultContext` so the worker knows how to send
17//!    the result back.
18//! 2. A worker receives the job, runs the proof, and sends a `ProofResultMessage` through the
19//!    provided `ProofResultSender`.
20//! 3. The `SparseTrieCacheTask` receives the message and proceeds with its state-root logic.
21//!
22//! Each job gets its own direct channel so results go straight back to the `SparseTrieCacheTask`.
23//! That keeps ordering decisions in one place and lets workers run independently.
24//!
25//! ```text
26//! SparseTrieCacheTask -> ProofWorkerHandle -> Storage/Account Worker
27//!        ^                       |
28//!        |                       v
29//! ProofResultMessage <-- ProofResultSender
30//! ```
31
32use crate::{
33    error::StateRootTaskError,
34    value_encoder::{AsyncAccountValueEncoder, ValueEncoderStats},
35};
36use alloy_primitives::{
37    map::{B256Map, B256Set},
38    B256, U256,
39};
40use crossbeam_channel::{unbounded, Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
41use reth_execution_errors::StateProofError;
42use reth_primitives_traits::FastInstant as Instant;
43use reth_provider::{DatabaseProviderROFactory, ProviderError, ProviderResult};
44use reth_storage_errors::db::DatabaseError;
45use reth_tasks::Runtime;
46use reth_trie::{
47    hashed_cursor::{HashedCursorFactory, HashedStorageCursor, InstrumentedHashedCursor},
48    proof_v2,
49    trie_cursor::{InstrumentedTrieCursor, TrieCursorFactory, TrieStorageCursor},
50    DecodedMultiProofV2, HashedPostState, MultiProofTargetsV2, ProofTrieNodeV2, ProofV2Target,
51};
52use std::{
53    cell::RefCell,
54    rc::Rc,
55    sync::{
56        atomic::{AtomicBool, AtomicUsize, Ordering},
57        Arc,
58    },
59    time::Duration,
60};
61use tracing::{debug, debug_span, error, instrument, trace};
62
63#[cfg(feature = "metrics")]
64use crate::proof_task_metrics::{
65    ProofTaskCursorMetrics, ProofTaskCursorMetricsCache, ProofTaskTrieMetrics,
66};
67
68/// Type alias for the V2 account proof calculator with instrumented cursors.
69type V2AccountProofCalculator<'a, Provider> = proof_v2::ProofCalculator<
70    InstrumentedTrieCursor<'a, <Provider as TrieCursorFactory>::AccountTrieCursor<'a>>,
71    InstrumentedHashedCursor<'a, <Provider as HashedCursorFactory>::AccountCursor<'a>>,
72    AsyncAccountValueEncoder<
73        InstrumentedTrieCursor<'a, <Provider as TrieCursorFactory>::StorageTrieCursor<'a>>,
74        InstrumentedHashedCursor<'a, <Provider as HashedCursorFactory>::StorageCursor<'a>>,
75    >,
76>;
77
78/// Type alias for the V2 storage proof calculator with instrumented cursors.
79type V2StorageProofCalculator<'a, Provider> = proof_v2::StorageProofCalculator<
80    InstrumentedTrieCursor<'a, <Provider as TrieCursorFactory>::StorageTrieCursor<'a>>,
81    InstrumentedHashedCursor<'a, <Provider as HashedCursorFactory>::StorageCursor<'a>>,
82>;
83
84/// Tracks worker availability counts.
85///
86/// It uses cacheline-aligned flags to avoid core-to-core chatter.
87#[derive(Debug)]
88struct AvailabilitySheet {
89    /// One flag per worker, each on its own cacheline. Workers store `true` when idle,
90    /// `false` when busy. Only the owning worker writes; the dispatcher only reads.
91    flags: Vec<crossbeam_utils::CachePadded<AtomicBool>>,
92}
93
94impl AvailabilitySheet {
95    /// Creates a new sheet with `count` workers, all initially marked as busy.
96    fn new(count: usize) -> Self {
97        let flags =
98            (0..count).map(|_| crossbeam_utils::CachePadded::new(AtomicBool::new(false))).collect();
99        Self { flags }
100    }
101
102    /// Returns `true` if more than one worker is currently idle.
103    ///
104    /// Note, that this is somewhat racy since a flag that was just saying `idle` and we counted it
105    /// as such might turn into `busy` right away.
106    fn has_multiple_idle(&self) -> bool {
107        let mut idle = 0u32;
108        for flag in &self.flags {
109            if flag.load(Ordering::Relaxed) {
110                idle += 1;
111                if idle > 1 {
112                    return true;
113                }
114            }
115        }
116        false
117    }
118
119    /// Marks the given worker as idle.
120    fn mark_idle(&self, worker_id: usize) {
121        self.flags[worker_id].store(true, Ordering::Relaxed);
122    }
123
124    /// Marks the given worker as busy.
125    fn mark_busy(&self, worker_id: usize) {
126        self.flags[worker_id].store(false, Ordering::Relaxed);
127    }
128}
129
130/// A handle that provides type-safe access to proof worker pools.
131///
132/// The handle stores direct senders to both storage and account worker pools,
133/// eliminating the need for a routing thread. All handles share reference-counted
134/// channels, and workers shut down gracefully when all handles are dropped.
135#[derive(Debug, Clone)]
136pub struct ProofWorkerHandle {
137    /// Direct sender to storage worker pool
138    storage_work_tx: CrossbeamSender<StorageWorkerJob>,
139    /// Direct sender to account worker pool
140    account_work_tx: CrossbeamSender<AccountWorkerJob>,
141    /// Per-worker availability flags for storage workers. Used to determine whether to chunk
142    /// multiproofs.
143    storage_availability: Arc<AvailabilitySheet>,
144    /// Per-worker availability flags for account workers. Used to determine whether to chunk
145    /// multiproofs.
146    account_availability: Arc<AvailabilitySheet>,
147    /// Total number of storage workers spawned
148    storage_worker_count: usize,
149    /// Total number of account workers spawned
150    account_worker_count: usize,
151}
152
153impl ProofWorkerHandle {
154    /// Spawns storage and account worker pools with dedicated database transactions.
155    ///
156    /// Returns a handle for submitting proof tasks to the worker pools.
157    /// Workers run until the last handle is dropped.
158    ///
159    /// # Parameters
160    /// - `runtime`: The centralized runtime used to spawn blocking worker tasks
161    /// - `task_ctx`: Shared context with database view and prefix sets
162    /// - `halve_workers`: Whether to halve the worker pool size (for small blocks)
163    #[instrument(
164        name = "ProofWorkerHandle::new",
165        level = "debug",
166        target = "trie::proof_task",
167        skip_all
168    )]
169    pub fn new<Factory>(
170        runtime: &Runtime,
171        task_ctx: ProofTaskCtx<Factory>,
172        halve_workers: bool,
173        proof_result_tx: ProofResultSender,
174    ) -> Self
175    where
176        Factory: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
177            + Clone
178            + Send
179            + Sync
180            + 'static,
181    {
182        let (storage_work_tx, storage_work_rx) = unbounded::<StorageWorkerJob>();
183        let (account_work_tx, account_work_rx) = unbounded::<AccountWorkerJob>();
184
185        let divisor = if halve_workers { 2 } else { 1 };
186        let storage_worker_count =
187            runtime.proof_storage_worker_pool().current_num_threads() / divisor;
188        let account_worker_count =
189            runtime.proof_account_worker_pool().current_num_threads() / divisor;
190
191        let storage_availability = Arc::new(AvailabilitySheet::new(storage_worker_count));
192        let account_availability = Arc::new(AvailabilitySheet::new(account_worker_count));
193
194        debug!(
195            target: "trie::proof_task",
196            storage_worker_count,
197            account_worker_count,
198            halve_workers,
199            "Spawning proof worker pools"
200        );
201
202        // broadcast blocks until all workers exit (channel close), so run on
203        // tokio's blocking pool.
204        let storage_rt = runtime.clone();
205        let storage_task_ctx = task_ctx.clone();
206        let storage_avail = storage_availability.clone();
207        let storage_result_tx = proof_result_tx.clone();
208        let storage_parent_span = tracing::Span::current();
209        runtime.spawn_blocking_named("storage-workers", move || {
210            let worker_id = AtomicUsize::new(0);
211            storage_rt.proof_storage_worker_pool().broadcast(storage_worker_count, |_| {
212                let worker_id = worker_id.fetch_add(1, Ordering::Relaxed);
213                let span = debug_span!(target: "trie::proof_task", parent: storage_parent_span.clone(), "storage_worker", ?worker_id);
214                let _guard = span.enter();
215
216                #[cfg(feature = "metrics")]
217                let metrics = ProofTaskTrieMetrics::default();
218                #[cfg(feature = "metrics")]
219                let cursor_metrics = ProofTaskCursorMetrics::new();
220
221                let worker = StorageProofWorker::new(
222                    storage_task_ctx.clone(),
223                    storage_work_rx.clone(),
224                    worker_id,
225                    storage_avail.clone(),
226                    #[cfg(feature = "metrics")]
227                    metrics,
228                    #[cfg(feature = "metrics")]
229                    cursor_metrics,
230                );
231                if let Err(error) = worker.run() {
232                    error!(
233                        target: "trie::proof_task",
234                        worker_id,
235                        ?error,
236                        "Storage worker failed"
237                    );
238                    let _ = storage_result_tx.send(ProofResultMessage {
239                        result: Err(StateRootTaskError::ProofWorker(format!(
240                            "storage worker {worker_id}: {error}"
241                        ))),
242                        elapsed: Duration::ZERO,
243                        state: Default::default(),
244                    });
245                }
246            });
247        });
248
249        let account_rt = runtime.clone();
250        let account_tx = storage_work_tx.clone();
251        let account_avail = account_availability.clone();
252        let account_result_tx = proof_result_tx;
253        let account_parent_span = tracing::Span::current();
254        runtime.spawn_blocking_named("account-workers", move || {
255            let worker_id = AtomicUsize::new(0);
256            account_rt.proof_account_worker_pool().broadcast(account_worker_count, |_| {
257                let worker_id = worker_id.fetch_add(1, Ordering::Relaxed);
258                let span = debug_span!(target: "trie::proof_task", parent: account_parent_span.clone(), "account_worker", ?worker_id);
259                let _guard = span.enter();
260
261                #[cfg(feature = "metrics")]
262                let metrics = ProofTaskTrieMetrics::default();
263                #[cfg(feature = "metrics")]
264                let cursor_metrics = ProofTaskCursorMetrics::new();
265
266                let worker = AccountProofWorker::new(
267                    task_ctx.clone(),
268                    account_work_rx.clone(),
269                    worker_id,
270                    account_tx.clone(),
271                    account_avail.clone(),
272                    #[cfg(feature = "metrics")]
273                    metrics,
274                    #[cfg(feature = "metrics")]
275                    cursor_metrics,
276                );
277                if let Err(error) = worker.run() {
278                    error!(
279                        target: "trie::proof_task",
280                        worker_id,
281                        ?error,
282                        "Account worker failed"
283                    );
284                    let _ = account_result_tx.send(ProofResultMessage {
285                        result: Err(StateRootTaskError::ProofWorker(format!(
286                            "account worker {worker_id}: {error}"
287                        ))),
288                        elapsed: Duration::ZERO,
289                        state: Default::default(),
290                    });
291                }
292            });
293        });
294
295        Self {
296            storage_work_tx,
297            account_work_tx,
298            storage_availability,
299            account_availability,
300            storage_worker_count,
301            account_worker_count,
302        }
303    }
304
305    /// Returns `true` if more than one storage worker is currently idle.
306    pub fn has_multiple_idle_storage_workers(&self) -> bool {
307        self.storage_availability.has_multiple_idle()
308    }
309
310    /// Returns `true` if more than one account worker is currently idle.
311    pub fn has_multiple_idle_account_workers(&self) -> bool {
312        self.account_availability.has_multiple_idle()
313    }
314
315    /// Returns the number of pending storage tasks in the queue.
316    pub fn pending_storage_tasks(&self) -> usize {
317        self.storage_work_tx.len()
318    }
319
320    /// Returns the number of pending account tasks in the queue.
321    pub fn pending_account_tasks(&self) -> usize {
322        self.account_work_tx.len()
323    }
324
325    /// Returns the total number of storage workers in the pool.
326    pub const fn total_storage_workers(&self) -> usize {
327        self.storage_worker_count
328    }
329
330    /// Returns the total number of account workers in the pool.
331    pub const fn total_account_workers(&self) -> usize {
332        self.account_worker_count
333    }
334
335    /// Dispatch a storage proof computation to storage worker pool
336    ///
337    /// The result will be sent via the `proof_result_sender` channel.
338    pub fn dispatch_storage_proof(
339        &self,
340        input: StorageProofInput,
341        proof_result_sender: CrossbeamSender<StorageProofResultMessage>,
342    ) -> Result<(), ProviderError> {
343        let hashed_address = input.hashed_address;
344        self.storage_work_tx
345            .send(StorageWorkerJob::StorageProof { input, proof_result_sender })
346            .map_err(|err| {
347                let StorageWorkerJob::StorageProof { proof_result_sender, .. } = err.0;
348                let _ = proof_result_sender.send(StorageProofResultMessage {
349                    hashed_address,
350                    result: Err(
351                        DatabaseError::Other("storage workers unavailable".to_string()).into()
352                    ),
353                });
354
355                ProviderError::other(std::io::Error::other("storage workers unavailable"))
356            })
357    }
358
359    /// Dispatch an account multiproof computation
360    ///
361    /// The result will be sent via the `result_sender` channel included in the input.
362    pub fn dispatch_account_multiproof(
363        &self,
364        input: AccountMultiproofInput,
365    ) -> Result<(), ProviderError> {
366        self.account_work_tx
367            .send(AccountWorkerJob::AccountMultiproof { input: Box::new(input) })
368            .map_err(|err| {
369                let error =
370                    ProviderError::other(std::io::Error::other("account workers unavailable"));
371
372                let AccountWorkerJob::AccountMultiproof { input } = err.0;
373                let ProofResultContext { sender: result_tx, state, start_time: start } =
374                    input.into_proof_result_sender();
375
376                let _ = result_tx.send(ProofResultMessage {
377                    result: Err(StateRootTaskError::ProofDispatch(error.clone())),
378                    elapsed: start.elapsed(),
379                    state,
380                });
381
382                error
383            })
384    }
385}
386
387/// Data used for initializing cursor factories that is shared across all proof worker instances.
388#[derive(Clone, Debug)]
389pub struct ProofTaskCtx<Factory> {
390    /// The factory for creating state providers.
391    factory: Factory,
392    /// Maximum random jitter to apply before each proof computation (trie-debug only).
393    #[cfg(feature = "trie-debug")]
394    proof_jitter: Option<Duration>,
395}
396
397impl<Factory> ProofTaskCtx<Factory> {
398    /// Creates a new [`ProofTaskCtx`] with the given factory.
399    pub const fn new(factory: Factory) -> Self {
400        Self {
401            factory,
402            #[cfg(feature = "trie-debug")]
403            proof_jitter: None,
404        }
405    }
406
407    /// Sets the maximum proof jitter duration (trie-debug only).
408    #[cfg(feature = "trie-debug")]
409    pub const fn with_proof_jitter(mut self, jitter: Option<Duration>) -> Self {
410        self.proof_jitter = jitter;
411        self
412    }
413}
414
415/// This contains all information shared between account proof worker instances.
416#[derive(Debug)]
417pub struct ProofTaskTx<Provider> {
418    /// The provider that implements `TrieCursorFactory` and `HashedCursorFactory`.
419    provider: Provider,
420
421    /// Identifier for the worker within the worker pool, used only for tracing.
422    id: usize,
423}
424
425impl<Provider> ProofTaskTx<Provider> {
426    /// Initializes a [`ProofTaskTx`] with the given provider and ID.
427    const fn new(provider: Provider, id: usize) -> Self {
428        Self { provider, id }
429    }
430}
431
432impl<Provider> ProofTaskTx<Provider>
433where
434    Provider: TrieCursorFactory + HashedCursorFactory,
435{
436    fn compute_v2_storage_proof<TC, HC>(
437        &self,
438        input: StorageProofInput,
439        calculator: &mut proof_v2::StorageProofCalculator<TC, HC>,
440    ) -> Result<StorageProofResult, StateProofError>
441    where
442        TC: TrieStorageCursor,
443        HC: HashedStorageCursor<Value = U256>,
444    {
445        let StorageProofInput { hashed_address, mut targets, needs_root } = input;
446
447        let span = debug_span!(
448            target: "trie::proof_task",
449            "Storage proof calculation",
450            n = %targets.len(),
451        );
452        let _span_guard = span.enter();
453
454        let proof_start = Instant::now();
455
456        // If targets is empty it means the caller only wants the root node.
457        let (proof, root) = if targets.is_empty() {
458            let root_node = calculator.storage_root_node(hashed_address)?;
459            let root = calculator.compute_root_hash(core::slice::from_ref(&root_node))?;
460            (vec![root_node], root)
461        } else {
462            // A partial proof cannot provide the storage root. Calculate it separately without
463            // changing the target's parent context, then reset the storage cursors by starting the
464            // targeted proof.
465            let root = if needs_root && targets.iter().all(|target| target.parent.is_known()) {
466                let root_node = calculator.storage_root_node(hashed_address)?;
467                calculator.compute_root_hash(core::slice::from_ref(&root_node))?
468            } else {
469                None
470            };
471
472            let proof = calculator.storage_proof(hashed_address, &mut targets)?;
473            let root = if root.is_some() { root } else { calculator.compute_root_hash(&proof)? };
474            (proof, root)
475        };
476
477        trace!(
478            target: "trie::proof_task",
479            hashed_address = ?hashed_address,
480            proof_time_us = proof_start.elapsed().as_micros(),
481            ?root,
482            worker_id = self.id,
483            "Completed V2 storage proof calculation"
484        );
485
486        Ok(StorageProofResult { proof, root })
487    }
488}
489
490/// Channel used by worker threads to deliver proof results back to
491/// `SparseTrieCacheTask`.
492///
493/// Workers use this sender to deliver proof results or terminal initialization errors directly to
494/// `SparseTrieCacheTask`.
495pub type ProofResultSender = CrossbeamSender<ProofResultMessage>;
496
497/// Message containing a completed proof result with metadata for direct delivery to
498/// `SparseTrieCacheTask`.
499///
500/// This type enables workers to send proof results directly to the `SparseTrieCacheTask` event
501/// loop.
502#[derive(Debug)]
503pub struct ProofResultMessage {
504    /// The proof calculation result
505    pub result: Result<DecodedMultiProofV2, StateRootTaskError>,
506    /// Time taken for the entire proof calculation (from dispatch to completion)
507    pub elapsed: Duration,
508    /// Original state update that triggered this proof
509    pub state: HashedPostState,
510}
511
512/// Context for sending proof calculation results back to `SparseTrieCacheTask`.
513///
514/// This struct contains all context needed to send and track proof calculation results.
515/// Workers use this to deliver completed proofs back to the main event loop.
516#[derive(Debug, Clone)]
517pub struct ProofResultContext {
518    /// Channel sender for result delivery
519    pub sender: ProofResultSender,
520    /// Original state update that triggered this proof
521    pub state: HashedPostState,
522    /// Calculation start time for measuring elapsed duration
523    pub start_time: Instant,
524}
525
526impl ProofResultContext {
527    /// Creates a new proof result context.
528    pub const fn new(
529        sender: ProofResultSender,
530        state: HashedPostState,
531        start_time: Instant,
532    ) -> Self {
533        Self { sender, state, start_time }
534    }
535}
536
537/// The results of a storage proof calculation.
538#[derive(Debug)]
539pub(crate) struct StorageProofResult {
540    /// The calculated V2 proof nodes
541    pub proof: Vec<ProofTrieNodeV2>,
542    /// The storage root calculated by the V2 proof
543    pub root: Option<B256>,
544}
545
546impl StorageProofResult {
547    /// Returns the calculated root of the trie, if one can be calculated from the proof.
548    const fn root(&self) -> Option<B256> {
549        self.root
550    }
551}
552
553/// Message containing a completed storage proof result with metadata.
554#[derive(Debug)]
555pub struct StorageProofResultMessage {
556    /// The hashed address this storage proof belongs to
557    #[allow(dead_code)]
558    pub(crate) hashed_address: B256,
559    /// The storage proof calculation result
560    pub(crate) result: Result<StorageProofResult, StateProofError>,
561}
562
563/// Internal message for storage workers.
564#[derive(Debug)]
565pub(crate) enum StorageWorkerJob {
566    /// Storage proof computation request
567    StorageProof {
568        /// Storage proof input parameters
569        input: StorageProofInput,
570        /// Context for sending the proof result.
571        proof_result_sender: CrossbeamSender<StorageProofResultMessage>,
572    },
573}
574
575/// Worker for storage trie operations.
576///
577/// Each worker maintains a dedicated database transaction and processes
578/// storage proof requests.
579struct StorageProofWorker<Factory> {
580    /// Shared task context with database factory and prefix sets
581    task_ctx: ProofTaskCtx<Factory>,
582    /// Channel for receiving work
583    work_rx: CrossbeamReceiver<StorageWorkerJob>,
584    /// Unique identifier for this worker (used for tracing)
585    worker_id: usize,
586    /// Per-worker availability flags
587    availability: Arc<AvailabilitySheet>,
588    /// Metrics collector for this worker
589    #[cfg(feature = "metrics")]
590    metrics: ProofTaskTrieMetrics,
591    /// Cursor metrics for this worker
592    #[cfg(feature = "metrics")]
593    cursor_metrics: ProofTaskCursorMetrics,
594}
595
596impl<Factory> StorageProofWorker<Factory>
597where
598    Factory: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>,
599{
600    /// Creates a new storage proof worker.
601    const fn new(
602        task_ctx: ProofTaskCtx<Factory>,
603        work_rx: CrossbeamReceiver<StorageWorkerJob>,
604        worker_id: usize,
605        availability: Arc<AvailabilitySheet>,
606        #[cfg(feature = "metrics")] metrics: ProofTaskTrieMetrics,
607        #[cfg(feature = "metrics")] cursor_metrics: ProofTaskCursorMetrics,
608    ) -> Self {
609        Self {
610            task_ctx,
611            work_rx,
612            worker_id,
613            availability,
614            #[cfg(feature = "metrics")]
615            metrics,
616            #[cfg(feature = "metrics")]
617            cursor_metrics,
618        }
619    }
620
621    /// Runs the worker loop, processing jobs until the channel closes.
622    ///
623    /// # Lifecycle
624    ///
625    /// 1. Initializes database provider and transaction
626    /// 2. Advertises availability
627    /// 3. Processes jobs in a loop:
628    ///    - Receives job from channel
629    ///    - Marks worker as busy
630    ///    - Processes the job
631    ///    - Marks worker as available
632    /// 4. Shuts down when channel closes
633    ///
634    /// # Panic Safety
635    ///
636    /// If this function panics, the worker thread terminates but other workers
637    /// continue operating and the system degrades gracefully.
638    fn run(mut self) -> ProviderResult<()> {
639        // Create provider from factory
640        let provider = self.task_ctx.factory.database_provider_ro()?;
641        let proof_tx = ProofTaskTx::new(provider, self.worker_id);
642
643        trace!(
644            target: "trie::proof_task",
645            worker_id = self.worker_id,
646            "Storage worker started"
647        );
648
649        let mut storage_proofs_processed = 0u64;
650        let mut cursor_metrics_cache = ProofTaskCursorMetricsCache::default();
651        let trie_cursor = proof_tx.provider.storage_trie_cursor(B256::ZERO)?;
652        let hashed_cursor = proof_tx.provider.hashed_storage_cursor(B256::ZERO)?;
653        let instrumented_trie_cursor =
654            InstrumentedTrieCursor::new(trie_cursor, &mut cursor_metrics_cache.storage_trie_cursor);
655        let instrumented_hashed_cursor = InstrumentedHashedCursor::new(
656            hashed_cursor,
657            &mut cursor_metrics_cache.storage_hashed_cursor,
658        );
659        let mut v2_calculator = proof_v2::StorageProofCalculator::new_storage(
660            instrumented_trie_cursor,
661            instrumented_hashed_cursor,
662        );
663
664        // Initially mark this worker as available.
665        self.availability.mark_idle(self.worker_id);
666
667        let mut total_idle_time = Duration::ZERO;
668        let mut idle_start = Instant::now();
669
670        while let Ok(job) = self.work_rx.recv() {
671            total_idle_time += idle_start.elapsed();
672
673            // Mark worker as busy.
674            self.availability.mark_busy(self.worker_id);
675
676            #[cfg(feature = "trie-debug")]
677            if let Some(max_jitter) = self.task_ctx.proof_jitter {
678                let jitter =
679                    Duration::from_nanos(rand::random_range(0..=max_jitter.as_nanos() as u64));
680                trace!(
681                    target: "trie::proof_task",
682                    worker_id = self.worker_id,
683                    jitter_us = jitter.as_micros(),
684                    "Storage worker applying proof jitter"
685                );
686                std::thread::sleep(jitter);
687            }
688
689            match job {
690                StorageWorkerJob::StorageProof { input, proof_result_sender } => {
691                    self.process_storage_proof(
692                        &proof_tx,
693                        &mut v2_calculator,
694                        input,
695                        proof_result_sender,
696                        &mut storage_proofs_processed,
697                    );
698                }
699            }
700
701            // Mark worker as available again.
702            self.availability.mark_idle(self.worker_id);
703
704            idle_start = Instant::now();
705        }
706
707        // Drop calculator to release mutable borrows on cursor_metrics_cache.
708        drop(v2_calculator);
709
710        trace!(
711            target: "trie::proof_task",
712            worker_id = self.worker_id,
713            storage_proofs_processed,
714            total_idle_time_us = total_idle_time.as_micros(),
715            "Storage worker shutting down"
716        );
717
718        #[cfg(feature = "metrics")]
719        {
720            self.metrics.record_storage_worker_idle_time(total_idle_time);
721            self.cursor_metrics.record(&mut cursor_metrics_cache);
722        }
723
724        Ok(())
725    }
726
727    /// Processes a storage proof request.
728    fn process_storage_proof<Provider, TC, HC>(
729        &self,
730        proof_tx: &ProofTaskTx<Provider>,
731        v2_calculator: &mut proof_v2::StorageProofCalculator<TC, HC>,
732        input: StorageProofInput,
733        proof_result_sender: CrossbeamSender<StorageProofResultMessage>,
734        storage_proofs_processed: &mut u64,
735    ) where
736        Provider: TrieCursorFactory + HashedCursorFactory,
737        TC: TrieStorageCursor,
738        HC: HashedStorageCursor<Value = U256>,
739    {
740        let hashed_address = input.hashed_address;
741        let proof_start = Instant::now();
742
743        trace!(
744            target: "trie::proof_task",
745            worker_id = self.worker_id,
746            hashed_address = ?hashed_address,
747            targets_len = input.targets.len(),
748            "Processing V2 storage proof"
749        );
750
751        let result = proof_tx.compute_v2_storage_proof(input, v2_calculator);
752
753        let proof_elapsed = proof_start.elapsed();
754        *storage_proofs_processed += 1;
755
756        let root = result.as_ref().ok().and_then(|result| result.root());
757
758        if proof_result_sender.send(StorageProofResultMessage { hashed_address, result }).is_err() {
759            trace!(
760                target: "trie::proof_task",
761                worker_id = self.worker_id,
762                hashed_address = ?hashed_address,
763                storage_proofs_processed,
764                "Proof result receiver dropped, discarding result"
765            );
766        }
767
768        trace!(
769            target: "trie::proof_task",
770            worker_id = self.worker_id,
771            hashed_address = ?hashed_address,
772            proof_time_us = proof_elapsed.as_micros(),
773            total_processed = storage_proofs_processed,
774            ?root,
775            "Storage proof completed"
776        );
777    }
778}
779
780/// Worker for account trie operations.
781///
782/// Each worker maintains a dedicated database transaction and processes
783/// account multiproof requests.
784struct AccountProofWorker<Factory> {
785    /// Shared task context with database factory and prefix sets
786    task_ctx: ProofTaskCtx<Factory>,
787    /// Channel for receiving work
788    work_rx: CrossbeamReceiver<AccountWorkerJob>,
789    /// Unique identifier for this worker (used for tracing)
790    worker_id: usize,
791    /// Channel for dispatching storage proof work (for pre-dispatched target proofs)
792    storage_work_tx: CrossbeamSender<StorageWorkerJob>,
793    /// Per-worker availability flags
794    availability: Arc<AvailabilitySheet>,
795    /// Metrics collector for this worker
796    #[cfg(feature = "metrics")]
797    metrics: ProofTaskTrieMetrics,
798    /// Cursor metrics for this worker
799    #[cfg(feature = "metrics")]
800    cursor_metrics: ProofTaskCursorMetrics,
801}
802
803impl<Factory> AccountProofWorker<Factory>
804where
805    Factory: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>,
806{
807    /// Creates a new account proof worker.
808    const fn new(
809        task_ctx: ProofTaskCtx<Factory>,
810        work_rx: CrossbeamReceiver<AccountWorkerJob>,
811        worker_id: usize,
812        storage_work_tx: CrossbeamSender<StorageWorkerJob>,
813        availability: Arc<AvailabilitySheet>,
814        #[cfg(feature = "metrics")] metrics: ProofTaskTrieMetrics,
815        #[cfg(feature = "metrics")] cursor_metrics: ProofTaskCursorMetrics,
816    ) -> Self {
817        Self {
818            task_ctx,
819            work_rx,
820            worker_id,
821            storage_work_tx,
822            availability,
823            #[cfg(feature = "metrics")]
824            metrics,
825            #[cfg(feature = "metrics")]
826            cursor_metrics,
827        }
828    }
829
830    /// Runs the worker loop, processing jobs until the channel closes.
831    ///
832    /// # Lifecycle
833    ///
834    /// 1. Initializes database provider and transaction
835    /// 2. Advertises availability
836    /// 3. Processes jobs in a loop:
837    ///    - Receives job from channel
838    ///    - Marks worker as busy
839    ///    - Processes the job
840    ///    - Marks worker as available
841    /// 4. Shuts down when channel closes
842    ///
843    /// # Panic Safety
844    ///
845    /// If this function panics, the worker thread terminates but other workers
846    /// continue operating and the system degrades gracefully.
847    fn run(mut self) -> ProviderResult<()> {
848        let provider = self.task_ctx.factory.database_provider_ro()?;
849
850        trace!(
851            target: "trie::proof_task",
852            worker_id=self.worker_id,
853            "Account worker started"
854        );
855
856        let mut account_proofs_processed = 0u64;
857        let mut cursor_metrics_cache = ProofTaskCursorMetricsCache::default();
858
859        // Create both account and storage calculators for V2 proofs.
860        // The storage calculator is wrapped in Rc<RefCell<...>> for sharing with value encoders.
861        let account_trie_cursor = provider.account_trie_cursor()?;
862        let account_hashed_cursor = provider.hashed_account_cursor()?;
863
864        let storage_trie_cursor = provider.storage_trie_cursor(B256::ZERO)?;
865        let storage_hashed_cursor = provider.hashed_storage_cursor(B256::ZERO)?;
866
867        let instrumented_account_trie_cursor = InstrumentedTrieCursor::new(
868            account_trie_cursor,
869            &mut cursor_metrics_cache.account_trie_cursor,
870        );
871        let instrumented_account_hashed_cursor = InstrumentedHashedCursor::new(
872            account_hashed_cursor,
873            &mut cursor_metrics_cache.account_hashed_cursor,
874        );
875        let instrumented_storage_trie_cursor = InstrumentedTrieCursor::new(
876            storage_trie_cursor,
877            &mut cursor_metrics_cache.storage_trie_cursor,
878        );
879        let instrumented_storage_hashed_cursor = InstrumentedHashedCursor::new(
880            storage_hashed_cursor,
881            &mut cursor_metrics_cache.storage_hashed_cursor,
882        );
883
884        let mut v2_account_calculator =
885            proof_v2::ProofCalculator::<
886                _,
887                _,
888                AsyncAccountValueEncoder<
889                    InstrumentedTrieCursor<
890                        '_,
891                        <Factory::Provider as TrieCursorFactory>::StorageTrieCursor<'_>,
892                    >,
893                    InstrumentedHashedCursor<
894                        '_,
895                        <Factory::Provider as HashedCursorFactory>::StorageCursor<'_>,
896                    >,
897                >,
898            >::new(instrumented_account_trie_cursor, instrumented_account_hashed_cursor);
899        let v2_storage_calculator =
900            Rc::new(RefCell::new(proof_v2::StorageProofCalculator::new_storage(
901                instrumented_storage_trie_cursor,
902                instrumented_storage_hashed_cursor,
903            )));
904
905        // Count this worker as available only after successful initialization.
906        self.availability.mark_idle(self.worker_id);
907
908        let mut total_idle_time = Duration::ZERO;
909        let mut idle_start = Instant::now();
910        let mut value_encoder_stats_cache = ValueEncoderStats::default();
911
912        while let Ok(job) = self.work_rx.recv() {
913            total_idle_time += idle_start.elapsed();
914
915            // Mark worker as busy.
916            self.availability.mark_busy(self.worker_id);
917
918            #[cfg(feature = "trie-debug")]
919            if let Some(max_jitter) = self.task_ctx.proof_jitter {
920                let jitter =
921                    Duration::from_nanos(rand::random_range(0..=max_jitter.as_nanos() as u64));
922                trace!(
923                    target: "trie::proof_task",
924                    worker_id = self.worker_id,
925                    jitter_us = jitter.as_micros(),
926                    "Account worker applying proof jitter"
927                );
928                std::thread::sleep(jitter);
929            }
930
931            match job {
932                AccountWorkerJob::AccountMultiproof { input } => {
933                    let value_encoder_stats = self.process_account_multiproof::<Factory::Provider>(
934                        &mut v2_account_calculator,
935                        v2_storage_calculator.clone(),
936                        *input,
937                        &mut account_proofs_processed,
938                    );
939                    total_idle_time += value_encoder_stats.storage_wait_time;
940                    value_encoder_stats_cache.extend(&value_encoder_stats);
941                }
942            }
943
944            // Mark worker as available again.
945            self.availability.mark_idle(self.worker_id);
946
947            idle_start = Instant::now();
948        }
949
950        // Drop calculators to release mutable borrows on cursor_metrics_cache.
951        drop(v2_account_calculator);
952        drop(v2_storage_calculator);
953
954        trace!(
955            target: "trie::proof_task",
956            worker_id=self.worker_id,
957            account_proofs_processed,
958            total_idle_time_us = total_idle_time.as_micros(),
959            "Account worker shutting down"
960        );
961
962        #[cfg(feature = "metrics")]
963        {
964            self.metrics.record_account_worker_idle_time(total_idle_time);
965            self.cursor_metrics.record(&mut cursor_metrics_cache);
966            self.metrics.record_value_encoder_stats(&value_encoder_stats_cache);
967        }
968
969        Ok(())
970    }
971
972    fn compute_v2_account_multiproof<'a, Provider>(
973        &self,
974        v2_account_calculator: &mut V2AccountProofCalculator<'a, Provider>,
975        v2_storage_calculator: Rc<RefCell<V2StorageProofCalculator<'a, Provider>>>,
976        targets: MultiProofTargetsV2,
977    ) -> Result<(DecodedMultiProofV2, ValueEncoderStats), StateRootTaskError>
978    where
979        Provider: TrieCursorFactory + HashedCursorFactory + 'a,
980    {
981        let MultiProofTargetsV2 { mut account_targets, storage_targets } = targets;
982
983        let span = debug_span!(
984            target: "trie::proof_task",
985            "Account multiproof calculation",
986            account_targets = account_targets.len(),
987            storage_targets = storage_targets.values().map(|t| t.len()).sum::<usize>(),
988        );
989        let _span_guard = span.enter();
990
991        trace!(target: "trie::proof_task", "Processing V2 account multiproof");
992
993        let storage_proof_receivers =
994            dispatch_v2_storage_proofs(&self.storage_work_tx, &account_targets, storage_targets)?;
995
996        let mut value_encoder =
997            AsyncAccountValueEncoder::new(storage_proof_receivers, v2_storage_calculator);
998
999        let account_proofs =
1000            v2_account_calculator.proof(&mut value_encoder, &mut account_targets)?;
1001
1002        let (storage_proofs, value_encoder_stats) = value_encoder.finalize()?;
1003
1004        let proof = DecodedMultiProofV2 { account_proofs, storage_proofs };
1005
1006        Ok((proof, value_encoder_stats))
1007    }
1008
1009    /// Processes an account multiproof request.
1010    ///
1011    /// Returns stats from the value encoder used during proof computation.
1012    fn process_account_multiproof<'a, Provider>(
1013        &self,
1014        v2_account_calculator: &mut V2AccountProofCalculator<'a, Provider>,
1015        v2_storage_calculator: Rc<RefCell<V2StorageProofCalculator<'a, Provider>>>,
1016        input: AccountMultiproofInput,
1017        account_proofs_processed: &mut u64,
1018    ) -> ValueEncoderStats
1019    where
1020        Provider: TrieCursorFactory + HashedCursorFactory + 'a,
1021    {
1022        let proof_start = Instant::now();
1023
1024        let AccountMultiproofInput { targets, proof_result_sender } = input;
1025        let (result, value_encoder_stats) = match self.compute_v2_account_multiproof::<Provider>(
1026            v2_account_calculator,
1027            v2_storage_calculator,
1028            targets,
1029        ) {
1030            Ok((proof, stats)) => (Ok(proof), stats),
1031            Err(e) => (Err(e), ValueEncoderStats::default()),
1032        };
1033
1034        let ProofResultContext { sender: result_tx, state, start_time: start } =
1035            proof_result_sender;
1036
1037        let proof_elapsed = proof_start.elapsed();
1038        let total_elapsed = start.elapsed();
1039        *account_proofs_processed += 1;
1040
1041        // Send result to SparseTrieCacheTask
1042        if result_tx.send(ProofResultMessage { result, elapsed: total_elapsed, state }).is_err() {
1043            trace!(
1044                target: "trie::proof_task",
1045                worker_id=self.worker_id,
1046                account_proofs_processed,
1047                "Account multiproof receiver dropped, discarding result"
1048            );
1049        }
1050
1051        trace!(
1052            target: "trie::proof_task",
1053            proof_time_us = proof_elapsed.as_micros(),
1054            total_elapsed_us = total_elapsed.as_micros(),
1055            total_processed = account_proofs_processed,
1056            "Account multiproof completed"
1057        );
1058
1059        value_encoder_stats
1060    }
1061}
1062
1063/// Queues V2 storage proofs for all accounts in the targets and returns receivers.
1064///
1065/// This function queues all storage proof tasks to the worker pool but returns immediately
1066/// with receivers, allowing the account trie walk to proceed in parallel with storage proof
1067/// computation. This enables interleaved parallelism for better performance.
1068///
1069/// Propagates errors up if queuing fails. Receivers must be consumed by the caller.
1070fn dispatch_v2_storage_proofs(
1071    storage_work_tx: &CrossbeamSender<StorageWorkerJob>,
1072    account_targets: &[ProofV2Target],
1073    storage_targets: B256Map<Vec<ProofV2Target>>,
1074) -> Result<B256Map<CrossbeamReceiver<StorageProofResultMessage>>, StateRootTaskError> {
1075    if storage_targets.is_empty() {
1076        return Ok(B256Map::default())
1077    }
1078
1079    let mut storage_proof_receivers =
1080        B256Map::with_capacity_and_hasher(storage_targets.len(), Default::default());
1081
1082    // Collect hashed addresses from account targets that need their storage roots computed.
1083    let account_target_addresses: B256Set = account_targets.iter().map(|t| t.key()).collect();
1084
1085    // Sort storage targets by address for optimal dispatch order.
1086    // Since trie walk processes accounts in lexicographical order, dispatching in the same order
1087    // reduces head-of-line blocking when consuming results.
1088    let mut sorted_storage_targets: Vec<_> = storage_targets.into_iter().collect();
1089    sorted_storage_targets.sort_unstable_by_key(|(addr, _)| *addr);
1090
1091    // Dispatch all proofs for targeted storage slots
1092    for (hashed_address, targets) in sorted_storage_targets {
1093        // Create channel for receiving StorageProofResultMessage
1094        let (result_tx, result_rx) = crossbeam_channel::unbounded();
1095        let needs_root = account_target_addresses.contains(&hashed_address);
1096        let input = StorageProofInput::new(hashed_address, targets, needs_root);
1097
1098        storage_work_tx
1099            .send(StorageWorkerJob::StorageProof { input, proof_result_sender: result_tx })
1100            .map_err(|_| {
1101                StateRootTaskError::Other(format!(
1102                    "Failed to queue storage proof for {hashed_address:?}: storage worker pool unavailable",
1103                ))
1104            })?;
1105
1106        storage_proof_receivers.insert(hashed_address, result_rx);
1107    }
1108
1109    Ok(storage_proof_receivers)
1110}
1111
1112/// Input parameters for storage proof computation.
1113#[derive(Debug)]
1114pub struct StorageProofInput {
1115    /// The hashed address for which the proof is calculated.
1116    pub hashed_address: B256,
1117    /// The set of proof targets
1118    pub targets: Vec<ProofV2Target>,
1119    /// Whether the account proof needs the storage root for leaf encoding.
1120    pub needs_root: bool,
1121}
1122
1123impl StorageProofInput {
1124    /// Creates a new [`StorageProofInput`] with the given hashed address and target slots.
1125    pub const fn new(hashed_address: B256, targets: Vec<ProofV2Target>, needs_root: bool) -> Self {
1126        Self { hashed_address, targets, needs_root }
1127    }
1128}
1129
1130/// Input parameters for account multiproof computation.
1131#[derive(Debug)]
1132pub struct AccountMultiproofInput {
1133    /// The targets for which to compute the multiproof.
1134    pub targets: MultiProofTargetsV2,
1135    /// Context for sending the proof result.
1136    pub proof_result_sender: ProofResultContext,
1137}
1138
1139impl AccountMultiproofInput {
1140    /// Returns the [`ProofResultContext`] for this input, consuming the input.
1141    fn into_proof_result_sender(self) -> ProofResultContext {
1142        self.proof_result_sender
1143    }
1144}
1145
1146/// Internal message for account workers.
1147#[derive(Debug)]
1148enum AccountWorkerJob {
1149    /// Account multiproof computation request
1150    AccountMultiproof {
1151        /// Account multiproof input parameters
1152        input: Box<AccountMultiproofInput>,
1153    },
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158    use super::*;
1159    use reth_chainspec::ChainSpec;
1160    use reth_provider::test_utils::create_test_provider_factory_with_chain_spec;
1161    use std::sync::Arc;
1162
1163    fn test_ctx<Factory>(factory: Factory) -> ProofTaskCtx<Factory> {
1164        ProofTaskCtx::new(factory)
1165    }
1166
1167    /// Ensures `ProofWorkerHandle::new` spawns workers correctly.
1168    #[test]
1169    fn spawn_proof_workers_creates_handle() {
1170        let chain_spec = Arc::new(ChainSpec::default());
1171        let anchor_hash = chain_spec.genesis_hash();
1172        let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec);
1173        let factory = reth_storage_overlay::OverlayStateProviderFactory::new(
1174            provider_factory,
1175            reth_storage_overlay::OverlayManager::<
1176                reth_ethereum_primitives::EthPrimitives,
1177            >::default()
1178            .overlay_builder(anchor_hash),
1179        );
1180        let ctx = test_ctx(factory);
1181
1182        let runtime = reth_tasks::Runtime::test();
1183        let (proof_result_tx, _) = unbounded();
1184        let proof_handle = ProofWorkerHandle::new(&runtime, ctx, false, proof_result_tx);
1185
1186        // Verify handle can be cloned
1187        let _cloned_handle = proof_handle.clone();
1188
1189        // Workers shut down automatically when handle is dropped
1190        drop(proof_handle);
1191    }
1192}