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}