Skip to main content

zcash_client_backend/sync/
decryptor.rs

1//! Full block batch decryption engine.
2
3use std::fmt;
4use std::sync::Arc;
5use std::time::Duration;
6
7use tokio::{
8    sync::{mpsc, oneshot},
9    time,
10};
11use zcash_primitives::{
12    block::{Block, BlockHeader},
13    transaction::Transaction,
14};
15use zcash_protocol::consensus::{self, BlockHeight};
16
17use crate::scanning::{
18    ScanningKeys,
19    full::{BatchResult, BatchRunners, DEFAULT_BATCH_SIZE_THRESHOLD},
20};
21
22/// The default depth of the queue between the [`Handle`]s and the [`Engine`].
23const DEFAULT_QUEUE_SIZE: usize = 1000;
24
25/// The default time the engine waits for a new queue item before flushing any pending
26/// batches.
27const DEFAULT_BATCH_START_DELAY: Duration = Duration::from_millis(500);
28
29/// Constructs a new batch decryption engine.
30pub fn new() -> Builder {
31    Builder {
32        queue_size: DEFAULT_QUEUE_SIZE,
33        sapling_batch_size_threshold: DEFAULT_BATCH_SIZE_THRESHOLD,
34        #[cfg(feature = "orchard")]
35        orchard_batch_size_threshold: DEFAULT_BATCH_SIZE_THRESHOLD,
36        batch_start_delay: DEFAULT_BATCH_START_DELAY,
37    }
38}
39
40/// Builder for an [`Engine`].
41pub struct Builder {
42    queue_size: usize,
43    sapling_batch_size_threshold: usize,
44    #[cfg(feature = "orchard")]
45    orchard_batch_size_threshold: usize,
46    batch_start_delay: Duration,
47}
48
49impl Builder {
50    /// Sets the size of the queue between [`Handle`]s and the [`Engine`].
51    ///
52    /// When the queue is full, calls to [`Handle::queue_block`] and [`Handle::queue_tx`]
53    /// will wait until the queue has space, while calls to [`Handle::try_queue_block`]
54    /// and [`Handle::try_queue_tx`] will return an error.
55    ///
56    /// Default is 1000.
57    pub fn queue_size(mut self, queue_size: usize) -> Self {
58        self.queue_size = queue_size;
59        self
60    }
61
62    /// Sets the number of outputs at which the batch runner will immediately flush a
63    /// batch, for both the Sapling and Orchard runners.
64    ///
65    /// To configure the pools independently, use [`Self::sapling_batch_size_threshold`]
66    /// and [`Self::orchard_batch_size_threshold`].
67    ///
68    /// Default is 200.
69    pub fn batch_size_threshold(self, batch_size_threshold: usize) -> Self {
70        let this = self.sapling_batch_size_threshold(batch_size_threshold);
71        #[cfg(feature = "orchard")]
72        let this = this.orchard_batch_size_threshold(batch_size_threshold);
73        this
74    }
75
76    /// Sets the number of outputs at which the Sapling batch runner will immediately
77    /// flush a batch.
78    ///
79    /// Default is 200.
80    pub fn sapling_batch_size_threshold(mut self, batch_size_threshold: usize) -> Self {
81        self.sapling_batch_size_threshold = batch_size_threshold;
82        self
83    }
84
85    /// Sets the number of outputs at which the Orchard batch runner will immediately
86    /// flush a batch.
87    ///
88    /// Default is 200.
89    #[cfg(feature = "orchard")]
90    pub fn orchard_batch_size_threshold(mut self, batch_size_threshold: usize) -> Self {
91        self.orchard_batch_size_threshold = batch_size_threshold;
92        self
93    }
94
95    /// Sets how long the engine will wait for a queue item before automatically flushing
96    /// any pending batches.
97    ///
98    /// Default is 500ms.
99    pub fn batch_start_delay(mut self, batch_start_delay: Duration) -> Self {
100        self.batch_start_delay = batch_start_delay;
101        self
102    }
103
104    /// Builds the engine with the configured settings.
105    pub fn build<AccountId, IvkTag>(
106        self,
107    ) -> (Handle<AccountId, IvkTag>, Engine<AccountId, IvkTag>) {
108        let (handle, queue) = mpsc::channel(self.queue_size);
109
110        (
111            Handle { handle },
112            Engine {
113                queue,
114                sapling_batch_size_threshold: self.sapling_batch_size_threshold,
115                #[cfg(feature = "orchard")]
116                orchard_batch_size_threshold: self.orchard_batch_size_threshold,
117                batch_start_delay: self.batch_start_delay,
118            },
119        )
120    }
121}
122
123/// The reason a non-blocking [`Handle::try_queue_block`] or [`Handle::try_queue_tx`]
124/// request was rejected.
125///
126/// The rejected payload (the `Block` or `Transaction`) is returned in both cases, so that
127/// the caller may retry.
128#[derive(Debug)]
129#[non_exhaustive]
130pub enum TryQueueError<T> {
131    /// The batch decryptor has shut down.
132    Shutdown(Box<T>),
133    /// The batch decryptor queue is full.
134    Full(Box<T>),
135}
136
137impl<T> TryQueueError<T> {
138    /// Returns the rejected payload, discarding the reason for rejection.
139    pub fn into_inner(self) -> T {
140        match self {
141            TryQueueError::Shutdown(payload) | TryQueueError::Full(payload) => *payload,
142        }
143    }
144}
145
146impl<T> fmt::Display for TryQueueError<T> {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        match self {
149            TryQueueError::Shutdown(_) => f.write_str("the batch decryptor has shut down"),
150            TryQueueError::Full(_) => f.write_str("the batch decryptor queue is full"),
151        }
152    }
153}
154
155impl<T: fmt::Debug> std::error::Error for TryQueueError<T> {}
156
157/// A handle to a batch decryption [`Engine`].
158#[derive(Clone)]
159pub struct Handle<AccountId, IvkTag> {
160    handle: mpsc::Sender<DecryptRequest<AccountId, IvkTag>>,
161}
162
163impl<AccountId, IvkTag> Handle<AccountId, IvkTag> {
164    /// Requests decryption of a block, waiting until there is space in the queue.
165    ///
166    /// Returns `None` if the batch decryptor has shut down.
167    pub async fn queue_block(
168        &self,
169        block: Block,
170    ) -> Option<oneshot::Receiver<BlockDecryptResult<AccountId, IvkTag>>> {
171        let (on_complete, rx) = oneshot::channel();
172        match self
173            .handle
174            .send(DecryptRequest::Block { block, on_complete })
175            .await
176        {
177            Ok(()) => Some(rx),
178            Err(_) => None,
179        }
180    }
181
182    /// Requests decryption of a block, without waiting for queue space.
183    ///
184    /// On failure the rejected `block` is returned, so that the caller may retry: see
185    /// [`TryQueueError`].
186    pub fn try_queue_block(
187        &self,
188        block: Block,
189    ) -> Result<oneshot::Receiver<BlockDecryptResult<AccountId, IvkTag>>, TryQueueError<Block>>
190    {
191        let (on_complete, rx) = oneshot::channel();
192        match self
193            .handle
194            .try_send(DecryptRequest::Block { block, on_complete })
195        {
196            Ok(()) => Ok(rx),
197            Err(mpsc::error::TrySendError::Full(DecryptRequest::Block { block, .. })) => {
198                Err(TryQueueError::Full(Box::new(block)))
199            }
200            Err(mpsc::error::TrySendError::Closed(DecryptRequest::Block { block, .. })) => {
201                Err(TryQueueError::Shutdown(Box::new(block)))
202            }
203            _ => unreachable!(),
204        }
205    }
206
207    /// Requests decryption of a transaction, waiting until there is space in the queue.
208    ///
209    /// Returns `None` if the batch decryptor has shut down.
210    pub async fn queue_tx(
211        &self,
212        tx: Transaction,
213        mempool_height: BlockHeight,
214    ) -> Option<oneshot::Receiver<BatchResult<IvkTag>>> {
215        let (on_complete, rx) = oneshot::channel();
216        match self
217            .handle
218            .send(DecryptRequest::Tx {
219                tx,
220                mempool_height,
221                on_complete,
222            })
223            .await
224        {
225            Ok(()) => Some(rx),
226            Err(_) => None,
227        }
228    }
229
230    /// Requests decryption of a transaction, without waiting for queue space.
231    ///
232    /// On failure the rejected `tx` is returned, so that the caller may retry: see
233    /// [`TryQueueError`].
234    pub fn try_queue_tx(
235        &self,
236        tx: Transaction,
237        mempool_height: BlockHeight,
238    ) -> Result<oneshot::Receiver<BatchResult<IvkTag>>, TryQueueError<Transaction>> {
239        let (on_complete, rx) = oneshot::channel();
240        match self.handle.try_send(DecryptRequest::Tx {
241            tx,
242            mempool_height,
243            on_complete,
244        }) {
245            Ok(()) => Ok(rx),
246            Err(mpsc::error::TrySendError::Full(DecryptRequest::Tx { tx, .. })) => {
247                Err(TryQueueError::Full(Box::new(tx)))
248            }
249            Err(mpsc::error::TrySendError::Closed(DecryptRequest::Tx { tx, .. })) => {
250                Err(TryQueueError::Shutdown(Box::new(tx)))
251            }
252            _ => unreachable!(),
253        }
254    }
255
256    /// Requests that the engine reload its scanning keys, so that accounts or keys added
257    /// since the engine started (or since the last reload) are applied to subsequently
258    /// queued blocks and transactions.
259    ///
260    /// The reload is ordered within the queue: every block or transaction queued before
261    /// this call is decrypted with the previous keys, and everything queued afterwards
262    /// with the reloaded keys.
263    ///
264    /// Returns a receiver that completes once the reload has been applied, or `None` if
265    /// the batch decryptor has shut down.
266    ///
267    /// Note that the returned receiver resolving to an error (because its sender was
268    /// dropped) does not distinguish "the engine shut down" from "the reload closure
269    /// passed to [`Engine::run`] returned an error, terminating the engine"; in both cases
270    /// no further requests will be served.
271    pub async fn reload_keys(&self) -> Option<oneshot::Receiver<()>> {
272        let (on_complete, rx) = oneshot::channel();
273        match self
274            .handle
275            .send(DecryptRequest::ReloadKeys { on_complete })
276            .await
277        {
278            Ok(()) => Some(rx),
279            Err(_) => None,
280        }
281    }
282
283    /// Returns `true` if the batch decryptor has shut down.
284    ///
285    /// This is the only reliable liveness test for the engine. The queuing methods
286    /// cannot serve as one: a request that wins its queue slot concurrently with the
287    /// engine shutting down is still accepted, so `Some` does not imply the request
288    /// will ever be served. Such a request is also never dropped while any `Handle`
289    /// remains, so its receiver resolves neither to a value nor to an error.
290    pub fn is_closed(&self) -> bool {
291        self.handle.is_closed()
292    }
293
294    /// Completes when the batch decryptor has shut down.
295    ///
296    /// Use this to wait on shutdown, or to race it against a queued request's receiver
297    /// so that a request stranded by a concurrent shutdown does not wait forever. See
298    /// [`Handle::is_closed`] for why the queuing methods cannot report liveness.
299    pub async fn closed(&self) {
300        self.handle.closed().await
301    }
302}
303
304#[allow(clippy::large_enum_variant)]
305enum DecryptRequest<AccountId, IvkTag> {
306    Block {
307        block: Block,
308        on_complete: oneshot::Sender<BlockDecryptResult<AccountId, IvkTag>>,
309    },
310    Tx {
311        tx: Transaction,
312        mempool_height: BlockHeight,
313        on_complete: oneshot::Sender<BatchResult<IvkTag>>,
314    },
315    ReloadKeys {
316        on_complete: oneshot::Sender<()>,
317    },
318}
319
320type BlockDecryptResult<AccountId, IvkTag> = (
321    Arc<ScanningKeys<AccountId, IvkTag>>,
322    BlockHeader,
323    Vec<BatchResult<IvkTag>>,
324);
325
326/// A batch decryption engine.
327pub struct Engine<AccountId, IvkTag> {
328    queue: mpsc::Receiver<DecryptRequest<AccountId, IvkTag>>,
329    sapling_batch_size_threshold: usize,
330    #[cfg(feature = "orchard")]
331    orchard_batch_size_threshold: usize,
332    batch_start_delay: Duration,
333}
334
335impl<AccountId, IvkTag> Engine<AccountId, IvkTag>
336where
337    AccountId: 'static,
338    IvkTag: Copy + Send + Sync + 'static,
339{
340    /// Runs the batch decryption engine.
341    ///
342    /// This method should be spawned as a task. It will loop indefinitely until either
343    /// all [`Handle`]s have been dropped, or `reload_keys` returns an error.
344    ///
345    /// `reload_keys` is called once at startup, and then again each time a key reload is
346    /// requested via [`Handle::reload_keys`], so that accounts or keys added after the
347    /// engine started are picked up. Each block decryption result reports the
348    /// [`ScanningKeys`] that were applied to it (see [`Handle::queue_block`]), so that a
349    /// caller can detect which keys a given block was scanned with.
350    ///
351    /// For each queued block or transaction the engine spawns a lightweight asynchronous
352    /// task that awaits the batch-runner results; decryption itself runs in parallel on
353    /// the global threadpool. No blocking-pool threads are used.
354    pub async fn run<P, E>(
355        mut self,
356        params: P,
357        mut reload_keys: impl FnMut() -> Result<ScanningKeys<AccountId, IvkTag>, E>,
358    ) -> Result<(), E>
359    where
360        P: consensus::Parameters + Send + 'static,
361    {
362        let mut scanning_keys = Arc::new(reload_keys()?);
363        let mut runners = BatchRunners::<_, (), (), ()>::for_keys(
364            self.sapling_batch_size_threshold,
365            #[cfg(feature = "orchard")]
366            self.orchard_batch_size_threshold,
367            // Ironwood outputs are Orchard-shaped, so they use the same batching threshold.
368            #[cfg(feature = "orchard")]
369            self.orchard_batch_size_threshold,
370            &scanning_keys,
371        );
372
373        // Whether a batch has been accumulated since the last flush. The idle-flush
374        // timer is only armed while this is set, so that a quiescent engine does not
375        // wake up to perform no-op flushes.
376        //
377        // The deadline is anchored to the first request after a flush and is not reset by
378        // later requests, so a steady stream of sub-threshold requests cannot postpone the
379        // flush of an aging batch indefinitely.
380        let mut idle_flush_pending = false;
381        let idle_flush = time::sleep(self.batch_start_delay);
382        tokio::pin!(idle_flush);
383
384        loop {
385            tokio::select! {
386                request = self.queue.recv() => match request {
387                    // Block decryption.
388                    Some(DecryptRequest::Block { block, on_complete }) => {
389                        let mined_height = block.claimed_height();
390                        let (header, vtx) = block.into_parts();
391                        let batches = vtx
392                            .into_iter()
393                            .map(|tx| runners.process_transaction(&params, mined_height, tx))
394                            .collect::<Vec<_>>();
395
396                        // Record the scanning keys applied to this block.
397                        let scanning_keys = scanning_keys.clone();
398
399                        // Await the batch results on a lightweight task, so the engine
400                        // loop stays free to accept further requests. The decryption
401                        // work itself is already running on the global threadpool.
402                        crate::spawn!("Block decryption", async move {
403                            let mut vtx = Vec::with_capacity(batches.len());
404                            for batch in batches {
405                                vtx.push(batch.wait_async().await);
406                            }
407                            // An error means the calling task is shutting down.
408                            let _ = on_complete.send((scanning_keys, header, vtx));
409                        });
410                        // Arm the idle-flush deadline on the first request after a flush;
411                        // subsequent requests do not reset it, so an aging batch is
412                        // flushed even under a steady stream of sub-threshold requests.
413                        if !idle_flush_pending {
414                            idle_flush
415                                .as_mut()
416                                .reset(time::Instant::now() + self.batch_start_delay);
417                            idle_flush_pending = true;
418                        }
419                    }
420
421                    // Mempool decryption.
422                    Some(DecryptRequest::Tx {
423                        tx,
424                        mempool_height,
425                        on_complete,
426                    }) => {
427                        let batch = runners.process_transaction(&params, mempool_height, tx);
428                        crate::spawn!("Mempool decryption", async move {
429                            // An error means the calling task is shutting down.
430                            let _ = on_complete.send(batch.wait_async().await);
431                        });
432                        // Arm the idle-flush deadline on the first request after a flush;
433                        // subsequent requests do not reset it, so an aging batch is
434                        // flushed even under a steady stream of sub-threshold requests.
435                        if !idle_flush_pending {
436                            idle_flush
437                                .as_mut()
438                                .reset(time::Instant::now() + self.batch_start_delay);
439                            idle_flush_pending = true;
440                        }
441                    }
442
443                    // Key reload. Flush any pending batches under the current keys
444                    // before rebuilding the runners with the reloaded set, so that
445                    // blocks queued before this request are unaffected.
446                    Some(DecryptRequest::ReloadKeys { on_complete }) => {
447                        runners.flush();
448                        scanning_keys = Arc::new(reload_keys()?);
449                        runners = BatchRunners::for_keys(
450                            self.sapling_batch_size_threshold,
451                            #[cfg(feature = "orchard")]
452                            self.orchard_batch_size_threshold,
453                            // Ironwood outputs are Orchard-shaped, so they use the same batching
454                            // threshold.
455                            #[cfg(feature = "orchard")]
456                            self.orchard_batch_size_threshold,
457                            &scanning_keys,
458                        );
459
460                        // An error means the calling task is shutting down.
461                        let _ = on_complete.send(());
462                        idle_flush_pending = false;
463                    }
464
465                    // Calling tasks are shutting down, so we are done.
466                    None => return Ok(()),
467                },
468
469                // The oldest accumulated batch has been waiting `batch_start_delay`;
470                // flush so any sub-threshold batch is run rather than left waiting.
471                _ = &mut idle_flush, if idle_flush_pending => {
472                    runners.flush();
473                    idle_flush_pending = false;
474                }
475            }
476        }
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::{Engine, Handle};
483
484    fn build() -> (Handle<u32, u32>, Engine<u32, u32>) {
485        super::new().build()
486    }
487
488    #[tokio::test]
489    async fn a_live_engine_is_not_reported_as_closed() {
490        let (handle, _engine) = build();
491        assert!(!handle.is_closed());
492    }
493
494    #[tokio::test]
495    async fn dropping_the_engine_closes_every_handle() {
496        let (handle, engine) = build();
497        let clone = handle.clone();
498        drop(engine);
499
500        handle.closed().await;
501        assert!(handle.is_closed());
502        assert!(clone.is_closed());
503    }
504
505    #[tokio::test]
506    async fn a_closed_handle_serves_no_further_requests() {
507        let (handle, engine) = build();
508        drop(engine);
509        assert!(handle.reload_keys().await.is_none());
510    }
511}