Skip to main content

reth_trie/verify/
pipeline.rs

1//! Ordered storage scheduling, root delivery, shared findings, and background task lifetime.
2
3use super::{Output, StorageJob, StorageWorker};
4use crate::{hashed_cursor::HashedStorageCursor, trie_cursor::TrieStorageCursor};
5use alloy_primitives::{B256, U256};
6use crossbeam_channel::{Receiver, Sender};
7use reth_execution_errors::StateRootError;
8use reth_storage_errors::db::DatabaseError;
9use std::{
10    panic::{catch_unwind, AssertUnwindSafe},
11    sync::Arc,
12    thread::{self, JoinHandle},
13};
14
15/// Owns storage scheduling and one pending root announcement for the account producer.
16#[derive(Debug)]
17pub(super) struct StoragePipeline {
18    context: WorkerContext,
19    background: Option<JoinHandle<()>>,
20    announcements: Receiver<(B256, Receiver<B256>)>,
21    next: Option<(B256, Receiver<B256>)>,
22}
23
24impl StoragePipeline {
25    /// Spawns the background thread which walks `addresses` and verifies their storage on one
26    /// thread per worker.
27    ///
28    /// # Panics
29    ///
30    /// Panics if `workers` is empty or `storage_lookahead` is zero.
31    pub(super) fn spawn<L, T, H, TX>(
32        addresses: L,
33        workers: Vec<StorageWorker<T, H, TX>>,
34        context: WorkerContext,
35        storage_lookahead: usize,
36    ) -> Result<Self, DatabaseError>
37    where
38        L: Iterator<Item = Result<StorageJob, DatabaseError>> + Send + 'static,
39        T: TrieStorageCursor + Send + 'static,
40        H: HashedStorageCursor<Value = U256> + Send + 'static,
41        TX: Send + 'static,
42    {
43        assert!(!workers.is_empty(), "verification requires storage workers");
44        assert!(storage_lookahead > 0, "storage lookahead must be positive");
45        // The account producer retains one announcement outside the queue. Reserving its
46        // slot here bounds all dispatched roots, including that lookahead, by the limit.
47        let (announcements_tx, announcements) = crossbeam_channel::bounded(storage_lookahead - 1);
48        let scheduler = StorageScheduler { addresses, workers, context: context.clone() };
49        let span = tracing::Span::current();
50        let background = thread::Builder::new()
51            .name("trie-lookahead".into())
52            .spawn(move || {
53                let _entered = span.enter();
54                scheduler.run(announcements_tx);
55            })
56            .map_err(|error| DatabaseError::Custom(Arc::new(error)))?;
57        Ok(Self { context, background: Some(background), announcements, next: None })
58    }
59
60    /// Returns the next root in address order up to and including `up_to`, or all remaining
61    /// roots if `up_to` is `None`. Returns `None` when the bound is reached, scanning completes
62    /// without another root, or verification is cancelled. Findings are consumed independently.
63    /// A missing worker result waits for cancellation by the findings consumer after it receives
64    /// the worker error, rather than replacing the original error with a channel error.
65    pub(super) fn next_root(&mut self, up_to: Option<B256>) -> Option<(B256, B256)> {
66        if self.context.is_cancelled() {
67            return None;
68        }
69        if self.next.is_none() {
70            self.next = crossbeam_channel::select! {
71                recv(self.announcements) -> announcement => announcement.ok(),
72                recv(self.context.cancel) -> _ => return None,
73            };
74        }
75        let Some((address, root)) = &self.next else { return None };
76        if up_to.is_some_and(|bound| *address > bound) {
77            return None;
78        }
79        let root = crossbeam_channel::select! {
80            recv(root) -> root => root,
81            recv(self.context.cancel) -> _ => return None,
82        };
83        match root {
84            Ok(root) => {
85                let address = *address;
86                self.next = None;
87                Some((address, root))
88            }
89            Err(_) => {
90                // A failing worker drops its root sender before WorkerContext::run reports the
91                // original error. The findings consumer cancels us after receiving that error;
92                // do not race it with a synthetic channel error or skip the missing root.
93                match self.context.cancel.recv() {
94                    Ok(()) | Err(_) => None,
95                }
96            }
97        }
98    }
99
100    /// Joins storage producers while the caller's iterator continues consuming findings.
101    pub(super) fn finish(&mut self) {
102        self.join();
103    }
104
105    fn join(&mut self) {
106        // Re-raising while already unwinding would abort the process.
107        if let Some(background) = self.background.take() &&
108            let Err(panic) = background.join() &&
109            !thread::panicking()
110        {
111            std::panic::resume_unwind(panic);
112        }
113    }
114}
115
116impl Drop for StoragePipeline {
117    fn drop(&mut self) {
118        self.join();
119    }
120}
121
122/// Body of the background thread: walks storage addresses and keeps the workers supplied with
123/// jobs.
124struct StorageScheduler<L, T, H, TX> {
125    addresses: L,
126    workers: Vec<StorageWorker<T, H, TX>>,
127    context: WorkerContext,
128}
129
130impl<L, T, H, TX> StorageScheduler<L, T, H, TX>
131where
132    L: Iterator<Item = Result<StorageJob, DatabaseError>>,
133    T: TrieStorageCursor + Send,
134    H: HashedStorageCursor<Value = U256> + Send,
135    TX: Send,
136{
137    fn run(mut self, announcements: Sender<(B256, Receiver<B256>)>) {
138        let context = self.context.clone();
139        // Retain the sender across setup failures until their error has been reported.
140        let mut announcements = Some(announcements);
141        context.run(|| {
142            let worker_count = self.workers.len();
143            let pool = reth_tasks::WorkerPool::new(worker_count, "trie-verify");
144            // Reserve backlog space before dispatch so a slow early root can always finish
145            // without competing with later results for space.
146            let (jobs_tx, jobs_rx) = crossbeam_channel::bounded(worker_count * 2);
147            pool.in_place_scope(|scope| {
148                for worker in self.workers.drain(..) {
149                    let jobs = jobs_rx.clone();
150                    let context = self.context.clone();
151                    let span = tracing::Span::current();
152                    scope.spawn(move |_| {
153                        let _entered = span.enter();
154                        context.run(|| worker.run(jobs, &context));
155                    });
156                }
157                drop(jobs_rx);
158                // Close announcements before joining workers: account EOF must not depend on
159                // workers finishing their findings sends.
160                // Keep scan failure ordered before EOF, including when `produce` panics.
161                if let Some(sender) = &announcements {
162                    context.run(|| self.produce(jobs_tx, sender));
163                }
164                drop(announcements.take());
165            });
166            Ok(())
167        });
168    }
169
170    fn produce(
171        &mut self,
172        jobs: Sender<(B256, Option<Sender<B256>>)>,
173        announcements: &Sender<(B256, Receiver<B256>)>,
174    ) -> Result<(), StateRootError> {
175        for job in self.addresses.by_ref() {
176            let job = job?;
177            let root = if job.root_required {
178                let (sender, receiver) = crossbeam_channel::bounded(1);
179                // Announce in scan order before dispatch, regardless of completion order.
180                if !self.context.send(announcements, (job.address, receiver)) {
181                    return Ok(());
182                }
183                Some(sender)
184            } else {
185                None
186            };
187            if !self.context.send(&jobs, (job.address, root)) {
188                return Ok(());
189            }
190        }
191        Ok(())
192    }
193}
194
195/// Shared cancellation and reporting policy for background threads.
196///
197/// Verification is cancelled once the `cancel` sender is dropped; nothing is ever sent on it.
198#[derive(Clone, Debug)]
199pub(super) struct WorkerContext {
200    events: Sender<Result<Output, StateRootError>>,
201    cancel: Receiver<()>,
202}
203
204impl WorkerContext {
205    pub(super) const fn new(
206        events: Sender<Result<Output, StateRootError>>,
207        cancel: Receiver<()>,
208    ) -> Self {
209        Self { events, cancel }
210    }
211
212    /// Sends `value`, returning `false` if it was not sent because verification was cancelled or
213    /// `sender` disconnected.
214    pub(super) fn send<T>(&self, sender: &Sender<T>, value: T) -> bool {
215        crossbeam_channel::select! {
216            send(sender, value) -> result => result.is_ok(),
217            recv(self.cancel) -> _ => false,
218        }
219    }
220
221    /// Receives the next job. Returns `None` once `jobs` is exhausted, and may return `None` early
222    /// if verification was cancelled.
223    pub(super) fn receive<T>(&self, jobs: &Receiver<T>) -> Option<T> {
224        crossbeam_channel::select! {
225            recv(jobs) -> job => job.ok(),
226            recv(self.cancel) -> _ => None,
227        }
228    }
229
230    /// Returns whether verification was cancelled.
231    pub(super) fn is_cancelled(&self) -> bool {
232        self.cancel.try_recv() == Err(crossbeam_channel::TryRecvError::Disconnected)
233    }
234
235    /// Reports one finding, applying backpressure until it is consumed or verification stops.
236    pub(super) fn report_one(&self, output: Output) -> bool {
237        self.send(&self.events, Ok(output))
238    }
239
240    /// Runs `work`, reporting its error or panic as a verification error.
241    pub(super) fn run(&self, work: impl FnOnce() -> Result<(), StateRootError>) {
242        let result = catch_unwind(AssertUnwindSafe(work)).unwrap_or_else(|panic| {
243            let message = panic
244                .downcast_ref::<&str>()
245                .copied()
246                .or_else(|| panic.downcast_ref::<String>().map(String::as_str))
247                .unwrap_or("unknown panic");
248            Err(DatabaseError::Other(format!("trie verification thread panicked: {message}"))
249                .into())
250        });
251        // The error is only lost if verification was already cancelled, in which case the
252        // verifier has stopped reading results.
253        if let Err(error) = result {
254            self.send(&self.events, Err(error));
255        }
256    }
257}