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}