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(¶ms, 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(¶ms, 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}