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