Skip to main content

zebra_consensus/primitives/
halo2.rs

1//! Async Halo2 batch verifier service
2
3use std::{
4    fmt,
5    future::Future,
6    mem,
7    pin::Pin,
8    sync::Arc,
9    task::{Context, Poll},
10};
11
12use futures::{future::BoxFuture, FutureExt};
13use once_cell::sync::Lazy;
14use orchard::{
15    bundle::BatchValidator,
16    circuit::{OrchardCircuitVersion, VerifyingKey},
17};
18use rand::thread_rng;
19use zcash_protocol::value::ZatBalance;
20use zebra_chain::{parameters::NetworkUpgrade, transaction::SigHash};
21
22use crate::{error::TransactionError, BoxError};
23use thiserror::Error;
24use tokio::sync::watch;
25use tower::Service;
26use tower_batch_control::{Batch, BatchControl, RequestWeight};
27use tower_fallback::Fallback;
28
29use super::spawn_fifo;
30
31#[cfg(test)]
32mod tests;
33
34/// Adjusted batch size for halo2 batches.
35///
36/// Unlike other batch verifiers, halo2 has aggregate proofs.
37/// This means that there can be hundreds of actions verified by some proofs,
38/// but just one action in others.
39///
40/// To compensate for larger proofs, we process the batch once there are over
41/// [`HALO2_MAX_BATCH_SIZE`] total actions among pending items in the queue.
42const HALO2_MAX_BATCH_SIZE: usize = super::MAX_BATCH_SIZE;
43
44/// The type of verification results.
45type VerifyResult = bool;
46
47/// The type of the batch sender channel.
48type Sender = watch::Sender<Option<VerifyResult>>;
49
50/// The type of a prepared verifying key.
51/// This is the key used to verify individual items.
52pub type ItemVerifyingKey = VerifyingKey;
53
54// The Orchard Action circuit — and therefore its verifying key — has changed across upgrades, and
55// a proof produced under one circuit version does not verify under another. We keep one key per
56// circuit version, each in its own dedicated verifier, and route each bundle to the correct one by
57// the block era (network upgrade) it was mined in. The circuit era is a function of the Orchard
58// pool and the block's upgrade, NOT of the transaction version (v5 vs v6): an Orchard-pool bundle
59// mined at NU6.3 commits to the NU6.3 circuit whether it is carried in a v5 or a v6 transaction
60// (see [`orchard_v5_verifier_for`] / [`orchard_v6_verifier`]):
61//
62//   * Orchard bundles before NU6.2 (NU5..NU6.2) were produced by the historical, insecure circuit
63//     and only verify under the [`InsecurePreNu6_2`] key. These must keep verifying so that nodes
64//     can re-sync and reindex pre-soft-fork Orchard history.
65//
66//   * Orchard bundles from NU6.2 until NU6.3 use the fixed circuit and only verify under the
67//     [`FixedPostNu6_2`] key.
68//
69//   * Every Orchard Action from NU6.3 onward uses the NU6.3 cross-address circuit and only verifies
70//     under the [`PostNu6_3`] key. The NU6.3 circuit extends the fixed circuit with the
71//     `disableCrossAddress` constraint that enforces the Orchard-pool cross-address restriction. Per
72//     ZIP 229 that restriction applies to every Orchard-pool Action "regardless of transaction
73//     version ... so that it cannot be bypassed by using a version 5 transaction", so v5 Orchard
74//     bundles at NU6.3, v6 Orchard bundles, and Ironwood bundles all share this one key.
75//
76// Routing therefore depends on the block era (network upgrade), not the transaction version.
77//
78// NOTE: this deliberately does NOT copy zcashd PR #176's WIP shortcut of validating everything
79// against the fixed key; that is incorrect both for re-syncing pre-soft-fork Orchard blocks (whose
80// proofs only verify under the insecure key) and for NU6.3 Orchard Actions (whose cross-address
81// restriction the fixed key cannot enforce).
82lazy_static::lazy_static! {
83    /// The Orchard Action verifying key for the **pre-NU6.2** (insecure) circuit.
84    ///
85    /// Reconstructs the verifying key of the original (NU5..NU6.2) Orchard Action circuit.
86    /// Bundles mined before NU6.2 committed to this circuit and only verify under this key, so it
87    /// MUST be retained to re-verify pre-NU6.2 history on resync. It must never be used to verify
88    /// post-NU6.2 bundles.
89    pub static ref VERIFYING_KEY_PRE_NU6_2: ItemVerifyingKey =
90        ItemVerifyingKey::build(OrchardCircuitVersion::InsecurePreNu6_2);
91
92    /// The Orchard Action verifying key for the **NU6.2-until-NU6.3** (fixed) circuit.
93    ///
94    /// Built from the fixed variable-base scalar-multiplication Orchard Action circuit shipped in
95    /// NU6.2. Orchard bundles mined from the NU6.2 activation height until NU6.3 commit to this
96    /// circuit and only verify under this key. At NU6.3 the Orchard pool moves to
97    /// [`VERIFYING_KEY_NU6_3_ONWARD`]. See [`VERIFYING_KEY_PRE_NU6_2`] for the era split.
98    pub static ref VERIFYING_KEY_NU6_2: ItemVerifyingKey =
99        ItemVerifyingKey::build(OrchardCircuitVersion::FixedPostNu6_2);
100
101    /// The Orchard Action verifying key for the **NU6.3-onward** circuit.
102    ///
103    /// Built from the NU6.3 Action circuit, which extends the fixed circuit with the
104    /// `disableCrossAddress` constraint that enforces the Orchard-pool cross-address restriction.
105    /// Every Orchard Action mined from NU6.3 onward commits to this circuit and only verifies under
106    /// this key: v5 Orchard bundles at NU6.3, v6 Orchard bundles, and Ironwood bundles.
107    pub static ref VERIFYING_KEY_NU6_3_ONWARD: ItemVerifyingKey =
108        ItemVerifyingKey::build(OrchardCircuitVersion::PostNu6_3);
109}
110
111/// A Halo2 verification item, used as the request type of the service.
112///
113/// An [`Item`] is key-agnostic: it carries only the bundle and sighash. The circuit era's verifying
114/// key is supplied by whichever [`Verifier`] processes the item, so an item is always validated
115/// against exactly one key and eras are never mixed within a batch.
116#[derive(Clone, Debug)]
117pub struct Item {
118    // `Arc`-wrapped so cloning an `Item` — which `tower-fallback` does eagerly for every request —
119    // shares the bundle instead of deep-copying its actions and multi-KB proof. `add_bundle` only
120    // needs `&Bundle`.
121    bundle: Arc<orchard::bundle::Bundle<orchard::bundle::Authorized, ZatBalance>>,
122    sighash: SigHash,
123}
124
125impl RequestWeight for Item {
126    fn request_weight(&self) -> usize {
127        self.bundle.actions().len()
128    }
129}
130
131impl Item {
132    /// Creates a new [`Item`] from a bundle and sighash.
133    pub fn new(
134        bundle: orchard::bundle::Bundle<orchard::bundle::Authorized, ZatBalance>,
135        sighash: SigHash,
136    ) -> Self {
137        Self {
138            bundle: Arc::new(bundle),
139            sighash,
140        }
141    }
142
143    /// Perform non-batched verification of this [`Item`] against `vk`.
144    ///
145    /// This is useful (in combination with `Item::clone`) for implementing
146    /// fallback logic when batch verification fails. The caller supplies the
147    /// verifying key for the item's era.
148    pub fn verify_single(self, vk: &ItemVerifyingKey) -> bool {
149        let mut batch = BatchValidator::new(vk);
150        // `add_bundle` rejects a bundle whose cross-address restriction is not supported by this
151        // era's verifying key; such an item is invalid under this key.
152        if batch.queue(self).is_err() {
153            return false;
154        }
155        batch.validate(thread_rng())
156    }
157}
158
159trait QueueBatchVerify {
160    fn queue(&mut self, item: Item) -> Result<(), orchard::bundle::BatchError>;
161}
162
163impl QueueBatchVerify for BatchValidator<'_> {
164    fn queue(&mut self, Item { bundle, sighash }: Item) -> Result<(), orchard::bundle::BatchError> {
165        self.add_bundle(bundle.as_ref(), sighash.0)
166    }
167}
168
169/// An error that may occur when verifying [Halo2 proofs of Zcash Orchard Action
170/// descriptions][actions].
171///
172/// [actions]: https://zips.z.cash/protocol/protocol.pdf#actiondesc
173// TODO: if halo2::plonk::Error gets the std::error::Error trait derived on it,
174// remove this and just wrap `halo2::plonk::Error` as an enum variant of
175// `crate::transaction::Error`, which does the trait derivation via `thiserror`
176#[derive(Clone, Debug, Error, Eq, PartialEq)]
177#[allow(missing_docs)]
178pub enum Halo2Error {
179    #[error("the constraint system is not satisfied")]
180    ConstraintSystemFailure,
181    #[error("unknown Halo2 error")]
182    Other,
183}
184
185impl From<halo2::plonk::Error> for Halo2Error {
186    fn from(err: halo2::plonk::Error) -> Halo2Error {
187        match err {
188            halo2::plonk::Error::ConstraintSystemFailure => Halo2Error::ConstraintSystemFailure,
189            _ => Halo2Error::Other,
190        }
191    }
192}
193
194/// The single-item fallback service for one Orchard circuit era.
195///
196/// When a batch fails, [`Fallback`] re-runs each item individually through this service. It holds
197/// the *same* verifying key as the batch it backs, so the fallback can never validate an item
198/// against a different era's key than the batch did. The key is named once, when the verifier is
199/// built (see [`batch_verifier`]).
200///
201/// This is a tiny named service rather than a `service_fn` closure because the closure would have
202/// to capture `vk`, and a capturing closure has an unnameable, non-`Clone` type — but the global
203/// verifier must be `Clone` to hand out per-call handles. A `&'static` field keeps this `Copy`.
204#[derive(Clone, Copy)]
205pub struct OrchardFallback {
206    /// The verifying key for this era, shared with the batch verifier it backs.
207    vk: &'static ItemVerifyingKey,
208}
209
210impl Service<Item> for OrchardFallback {
211    type Response = ();
212    type Error = BoxError;
213    type Future = BoxFuture<'static, Result<(), BoxError>>;
214
215    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
216        Poll::Ready(Ok(()))
217    }
218
219    fn call(&mut self, item: Item) -> Self::Future {
220        Verifier::verify_single_spawning(item, self.vk).boxed()
221    }
222}
223
224/// The concrete type of a global Halo2 verification service.
225///
226/// Each Orchard circuit version gets its own instance — see [`VERIFIER_PRE_NU6_2`],
227/// [`VERIFIER_NU6_2`], and [`VERIFIER_NU6_3_ONWARD`] — so that batches, fallbacks, and verifying
228/// keys are fully separated per circuit version. The Orchard verifier routing functions
229/// ([`orchard_v5_verifier_for`] / [`orchard_v6_verifier`]) return a borrow of the matching one.
230pub type VerifierService = Fallback<Batch<Verifier, Item>, OrchardFallback>;
231
232/// Builds a global Halo2 verifier that validates every item against `vk`.
233///
234/// The returned service batches contemporaneous proof verifications and, if a batch fails, falls
235/// back to verifying each item individually. The batch and its fallback share the single `vk`
236/// passed here, so an item built by this verifier is always checked against exactly one era's key.
237/// Callers select the correct era's key by which `VERIFYING_KEY_*` they pass (see the two statics
238/// below); there is no runtime key resolution.
239fn batch_verifier(vk: &'static ItemVerifyingKey) -> VerifierService {
240    Fallback::new(
241        Batch::new(
242            Verifier::new(vk),
243            HALO2_MAX_BATCH_SIZE,
244            None,
245            super::MAX_BATCH_LATENCY,
246        ),
247        OrchardFallback { vk },
248    )
249}
250
251/// Global batch verification context for **pre-NU6.2** Halo2 Action proofs.
252///
253/// Items routed here are verified against [`VERIFYING_KEY_PRE_NU6_2`] (the insecure circuit
254/// retained for historical blocks). This service transparently batches contemporaneous proof
255/// verifications, handling batch failures by falling back to individual verification.
256///
257/// Note that making a `Service` call requires mutable access to the service, so you should call
258/// `.clone()` on the global handle to create a local, mutable handle.
259pub static VERIFIER_PRE_NU6_2: Lazy<VerifierService> =
260    Lazy::new(|| batch_verifier(&VERIFYING_KEY_PRE_NU6_2));
261
262/// Global batch verification context for **NU6.2-until-NU6.3** Halo2 Action proofs.
263///
264/// Items routed here are verified against [`VERIFYING_KEY_NU6_2`] (the fixed circuit). This
265/// service transparently batches contemporaneous proof verifications, handling batch failures by
266/// falling back to individual verification.
267///
268/// Note that making a `Service` call requires mutable access to the service, so you should call
269/// `.clone()` on the global handle to create a local, mutable handle.
270pub static VERIFIER_NU6_2: Lazy<VerifierService> =
271    Lazy::new(|| batch_verifier(&VERIFYING_KEY_NU6_2));
272
273/// Global batch verification context for **NU6.3-onward** Halo2 Action proofs.
274///
275/// Items routed here are verified against [`VERIFYING_KEY_NU6_3_ONWARD`] (the NU6.3 Action circuit, which
276/// adds the `disableCrossAddress` constraint). Every Orchard Action mined from NU6.3 onward routes
277/// here — v5 Orchard bundles at NU6.3, v6 Orchard bundles, and Ironwood bundles. This service
278/// transparently batches contemporaneous proof verifications, handling batch failures by falling
279/// back to individual verification.
280///
281/// Note that making a `Service` call requires mutable access to the service, so you should call
282/// `.clone()` on the global handle to create a local, mutable handle.
283pub static VERIFIER_NU6_3_ONWARD: Lazy<VerifierService> =
284    Lazy::new(|| batch_verifier(&VERIFYING_KEY_NU6_3_ONWARD));
285
286/// Returns the global Halo2 verifier for the **Orchard-pool** bundle of a **v5** transaction in a
287/// block at `network_upgrade`.
288///
289/// The Orchard Action circuit — and therefore its verifying key — changes with the block era, and
290/// a proof produced under one circuit does not verify under another era's key. The era is a
291/// function of the block's network upgrade, **not** the transaction version, so each v5 Orchard
292/// bundle is checked against the key for the upgrade of the block it appears in:
293///
294///   * upgrades before NU6.2 → [`VERIFIER_PRE_NU6_2`] (the historical insecure key), so
295///     pre-soft-fork Orchard history still verifies on re-sync;
296///   * NU6.2 until NU6.3 → [`VERIFIER_NU6_2`] (the fixed key);
297///   * NU6.3 onward → [`VERIFIER_NU6_3_ONWARD`] (the NU6.3 circuit). The Orchard-pool cross-address
298///     restriction is enforced for every Orchard Action from NU6.3 onward regardless of transaction
299///     version, "so that it cannot be bypassed by using a version 5 transaction" (ZIP 229); that
300///     restriction lives in the NU6.3 circuit, which the NU6.2 fixed key cannot verify. So a v5
301///     Orchard bundle at NU6.3 uses the same key as v6 Orchard and Ironwood bundles.
302///
303/// v6 Orchard and Ironwood bundles use [`orchard_v6_verifier`], which returns that same
304/// NU6.3-onward key.
305///
306/// The mapping is an explicit, exhaustive `match` on every [`NetworkUpgrade`] variant: there is no
307/// version-comparison fallthrough and no default arm, so adding a future upgrade is a compile error
308/// here until it is bound to a key on purpose.
309pub fn orchard_v5_verifier_for(network_upgrade: NetworkUpgrade) -> &'static VerifierService {
310    use NetworkUpgrade::*;
311
312    match network_upgrade {
313        // Orchard did not exist before NU5, so these upgrades never carry Orchard bundles. They
314        // are bound to the pre-NU6.2 (insecure) verifier because that is the only key under which
315        // any Orchard history before NU6.2 verifies; routing them anywhere else cannot be correct.
316        Genesis | BeforeOverwinter | Overwinter | Sapling | Blossom | Heartwood | Canopy | Nu5
317        | Nu6 | Nu6_1 => &VERIFIER_PRE_NU6_2,
318
319        // NU6.2 ships the fixed circuit and is the only upgrade that uses it: it is active from the
320        // NU6.2 activation height until NU6.3.
321        Nu6_2 => &VERIFIER_NU6_2,
322
323        // NU6.3 adds the `disableCrossAddress` constraint to the Orchard Action circuit. Every
324        // Orchard Action from NU6.3 onward — including those in v5 transactions — commits to this
325        // circuit, so NU6.3 and later route to the NU6.3-onward key. Per ZIP 229 the cross-address
326        // restriction applies "regardless of transaction version ... so that it cannot be bypassed
327        // by using a version 5 transaction". Verifying these under the NU6.2 fixed key would both
328        // reject honest proofs (different key) and fail to enforce the restriction.
329        Nu6_3 | Nu7 => &VERIFIER_NU6_3_ONWARD,
330
331        // `ZFuture` only exists under the `zcash_unstable = "zfuture"` cfg. It is a post-NU6.3
332        // upgrade, so it inherits the NU6.3 circuit and is bound to the NU6.3-onward key here on
333        // purpose (rather than via a wildcard) to keep this match exhaustive and fail-closed under
334        // every build configuration.
335        #[cfg(zcash_unstable = "zfuture")]
336        ZFuture => &VERIFIER_NU6_3_ONWARD,
337    }
338}
339
340/// Returns the global Halo2 verifier for **v6** Orchard-pool and Ironwood-pool bundles.
341///
342/// v6 Orchard and Ironwood bundles only exist from NU6.3 onward, so they always use the NU6.3
343/// circuit — the same [`VERIFIER_NU6_3_ONWARD`] key that v5 Orchard bundles at NU6.3 route to.
344pub fn orchard_v6_verifier() -> &'static VerifierService {
345    &VERIFIER_NU6_3_ONWARD
346}
347
348/// Halo2 proof verifier implementation
349///
350/// This is the core implementation for the batch verification logic of the
351/// Halo2 verifier. It handles batching incoming requests, driving batches to
352/// completion, and reporting results.
353///
354/// Each verifier validates against a single, fixed [`ItemVerifyingKey`]; the three Orchard circuit
355/// eras (pre-NU6.2, NU6.2-until-NU6.3, and NU6.3-onward) are served by three independent verifiers,
356/// so a batch never mixes proofs from different eras.
357pub struct Verifier {
358    /// The verifying key that every batch and fallback from this verifier uses.
359    vk: &'static ItemVerifyingKey,
360
361    /// The synchronous Halo2 batch validator.
362    ///
363    /// Borrows `vk` (which is `'static`), so the validator is `BatchValidator<'static>`.
364    batch: BatchValidator<'static>,
365
366    /// A channel for broadcasting the result of a batch to the futures for each batch item.
367    ///
368    /// Each batch gets a newly created channel, so there is only ever one result sent per channel.
369    /// Tokio doesn't have a oneshot multi-consumer channel, so we use a watch channel.
370    tx: Sender,
371}
372
373impl Verifier {
374    /// Creates a verifier that validates every item against `vk`.
375    fn new(vk: &'static ItemVerifyingKey) -> Self {
376        let (tx, _) = watch::channel(None);
377        Self {
378            vk,
379            batch: BatchValidator::new(vk),
380            tx,
381        }
382    }
383
384    /// Returns the batch verifier and channel sender,
385    /// replacing the batch and channel with new empty ones.
386    fn take(&mut self) -> (BatchValidator<'static>, Sender) {
387        // Use a new verifier and channel for each batch.
388        let batch = mem::replace(&mut self.batch, BatchValidator::new(self.vk));
389        let (tx, _) = watch::channel(None);
390        let tx = mem::replace(&mut self.tx, tx);
391
392        (batch, tx)
393    }
394
395    /// Synchronously process the batch (the verifying key is held by the batch), and send the
396    /// result using the channel sender. This function blocks until the batch is completed.
397    fn verify(batch: BatchValidator<'static>, tx: Sender) {
398        let result = batch.validate(thread_rng());
399        let _ = tx.send(Some(result));
400    }
401
402    /// Flush the batch using a thread pool, sending the result via the channel.
403    /// This returns immediately, usually before the batch is completed.
404    fn flush_blocking(&mut self) {
405        let (batch, tx) = self.take();
406
407        // Correctness: Do CPU-intensive work on a dedicated thread, to avoid blocking other futures.
408        //
409        // We don't care about execution order here, because this method is only called on drop.
410        tokio::task::block_in_place(|| rayon::spawn_fifo(move || Self::verify(batch, tx)));
411    }
412
413    /// Flush the batch using a thread pool, returning the result via the channel. This function
414    /// returns a future that becomes ready when the batch is completed.
415    async fn flush_spawning(batch: BatchValidator<'static>, tx: Sender) {
416        // Correctness: Do CPU-intensive work on a dedicated thread, to avoid blocking other futures.
417        let start = std::time::Instant::now();
418        let result = spawn_fifo(move || batch.validate(thread_rng())).await;
419        let duration = start.elapsed().as_secs_f64();
420
421        let result_label = match &result {
422            Ok(true) => "success",
423            _ => "failure",
424        };
425        metrics::histogram!(
426            "zebra.consensus.batch.duration_seconds",
427            "verifier" => "halo2",
428            "result" => result_label
429        )
430        .record(duration);
431
432        let _ = tx.send(result.ok());
433    }
434
435    /// Verify a single item against `vk` using a thread pool, and return the result.
436    async fn verify_single_spawning(
437        item: Item,
438        vk: &'static ItemVerifyingKey,
439    ) -> Result<(), BoxError> {
440        // Correctness: Do CPU-intensive work on a dedicated thread, to avoid blocking other futures.
441        if spawn_fifo(move || item.verify_single(vk)).await? {
442            Ok(())
443        } else {
444            Err(TransactionError::Halo2VerificationFailed.into())
445        }
446    }
447}
448
449impl fmt::Debug for Verifier {
450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451        let name = "Verifier";
452        f.debug_struct(name).field("batch", &"..").finish()
453    }
454}
455
456impl Service<BatchControl<Item>> for Verifier {
457    type Response = ();
458    type Error = BoxError;
459    type Future = Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send + 'static>>;
460
461    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
462        Poll::Ready(Ok(()))
463    }
464
465    fn call(&mut self, req: BatchControl<Item>) -> Self::Future {
466        match req {
467            BatchControl::Item(item) => {
468                tracing::trace!("got item");
469                match self.batch.queue(item) {
470                    Ok(()) => {}
471                    // Reject the item on its own without poisoning the rest of the batch. In
472                    // particular `BatchError::RestrictionUnsupportedByKey` should not be reachable
473                    // (it fires only for a bundle with `cross_address_enabled = false` on a key
474                    // that does not support the restriction; `VERIFIER_NU6_3_ONWARD`'s key supports
475                    // it, and the pre-NU6.3 verifiers only ever see v5-format bundles, which always
476                    // report `cross_address_enabled = true`), but failing closed per-item is
477                    // strictly safer than panicking this shared, long-lived verifier service if a
478                    // future routing change or `#[non_exhaustive]` variant ever reaches here.
479                    Err(other) => {
480                        return Box::pin(async move {
481                            metrics::counter!("proofs.halo2.invalid").increment(1);
482                            Err(format!("could not validate halo2 proof: {other}").into())
483                        });
484                    }
485                }
486                let mut rx = self.tx.subscribe();
487                Box::pin(async move {
488                    match rx.changed().await {
489                        Ok(()) => {
490                            // We use a new channel for each batch,
491                            // so we always get the correct batch result here.
492                            let is_valid = *rx
493                                .borrow()
494                                .as_ref()
495                                .ok_or("threadpool unexpectedly dropped response channel sender. Is Zebra shutting down?")?;
496
497                            if is_valid {
498                                tracing::trace!(?is_valid, "verified halo2 proof");
499                                metrics::counter!("proofs.halo2.verified").increment(1);
500                                Ok(())
501                            } else {
502                                tracing::trace!(?is_valid, "invalid halo2 proof");
503                                metrics::counter!("proofs.halo2.invalid").increment(1);
504                                Err(TransactionError::Halo2VerificationFailed.into())
505                            }
506                        }
507                        Err(_recv_error) => panic!("verifier was dropped without flushing"),
508                    }
509                })
510            }
511
512            BatchControl::Flush => {
513                tracing::trace!("got halo2 flush command");
514
515                let (batch, tx) = self.take();
516
517                Box::pin(Self::flush_spawning(batch, tx).map(|()| Ok(())))
518            }
519        }
520    }
521}
522
523impl Drop for Verifier {
524    fn drop(&mut self) {
525        // We need to flush the current batch in case there are still any pending futures.
526        // This returns immediately, usually before the batch is completed.
527        self.flush_blocking()
528    }
529}