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