diff --git a/Cargo.toml b/Cargo.toml index 0ef77d8a8..170fbb85f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ subtle = "2.2.1" [dev-dependencies] bls12_381 = "0.4" +criterion = "0.3" hex-literal = "0.3" rand = "0.8" rand_xorshift = "0.3" @@ -40,5 +41,9 @@ name = "mimc" path = "tests/mimc.rs" required-features = ["groth16"] +[[bench]] +name = "batch" +harness = false + [badges] maintenance = { status = "actively-developed" } diff --git a/benches/batch.rs b/benches/batch.rs new file mode 100644 index 000000000..8b3da98a6 --- /dev/null +++ b/benches/batch.rs @@ -0,0 +1,98 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; + +use bls12_381::Bls12; +use ff::Field; +use rand::thread_rng; + +use bellman::groth16::{ + batch, create_random_proof, generate_random_parameters, prepare_verifying_key, verify_proof, +}; + +#[path = "../tests/common/mod.rs"] +mod common; + +use common::*; + +fn bench_batch_verify(c: &mut Criterion) { + let mut group = c.benchmark_group("Batch Verification"); + + for &n in [8usize, 16, 24, 32, 40, 48, 56, 64].iter() { + group.throughput(Throughput::Elements(n as u64)); + + let mut rng = thread_rng(); + + // Generate the MiMC round constants + let constants = (0..MIMC_ROUNDS) + .map(|_| bls12_381::Scalar::random(&mut rng)) + .collect::>(); + + // Create parameters for our circuit + let params = { + let c = MiMCDemo { + xl: None, + xr: None, + constants: &constants, + }; + + generate_random_parameters::(c, &mut rng).unwrap() + }; + + // Prepare the verification key (for proof verification) + let pvk = prepare_verifying_key(¶ms.vk); + + let proofs = { + std::iter::repeat_with(|| { + // Generate a random preimage and compute the image + let xl = bls12_381::Scalar::random(&mut rng); + let xr = bls12_381::Scalar::random(&mut rng); + let image = mimc(xl, xr, &constants); + + // Create an instance of our circuit (with the + // witness) + let c = MiMCDemo { + xl: Some(xl), + xr: Some(xr), + constants: &constants, + }; + + // Create a groth16 proof with our parameters. + let proof = create_random_proof(c, ¶ms, &mut rng).unwrap(); + + (proof, image) + }) + } + .take(n) + .collect::>(); + + group.bench_with_input( + BenchmarkId::new("Unbatched verification", n), + &proofs, + |b, proofs| { + b.iter(|| { + for (proof, input) in proofs.iter() { + let _ = verify_proof(&pvk, &proof, &[*input]); + } + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("Batched verification", n), + &proofs, + |b, proofs| { + b.iter(|| { + let mut batch = batch::Verifier::new(); + for (proof, input) in proofs.iter() { + batch.queue((proof.clone(), vec![*input])); + } + batch.verify(&mut rng, ¶ms.vk) + }) + }, + ); + } + + group.finish(); +} + +criterion_group!(benches, bench_batch_verify); +criterion_main!(benches); diff --git a/src/groth16/mod.rs b/src/groth16/mod.rs index 289cb1bbc..b0e20887e 100644 --- a/src/groth16/mod.rs +++ b/src/groth16/mod.rs @@ -23,7 +23,7 @@ pub use self::generator::*; pub use self::prover::*; pub use self::verifier::*; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Proof { pub a: E::G1Affine, pub b: E::G2Affine, diff --git a/src/groth16/tests/dummy_engine.rs b/src/groth16/tests/dummy_engine.rs index 9a3590377..aaa4cfe0a 100644 --- a/src/groth16/tests/dummy_engine.rs +++ b/src/groth16/tests/dummy_engine.rs @@ -342,7 +342,7 @@ impl Engine for DummyEngine { type Gt = Fr; fn pairing(p: &Self::G1Affine, q: &Self::G2Affine) -> Self::Gt { - Self::multi_miller_loop(&[(p, &(*q).into())]).final_exponentiation() + Self::multi_miller_loop(&[(p, &(*q))]).final_exponentiation() } } diff --git a/src/groth16/verifier.rs b/src/groth16/verifier.rs index 43c69cb66..607eb70f1 100644 --- a/src/groth16/verifier.rs +++ b/src/groth16/verifier.rs @@ -6,6 +6,8 @@ use super::{PreparedVerifyingKey, Proof, VerifyingKey}; use crate::VerificationError; +pub mod batch; + pub fn prepare_verifying_key(vk: &VerifyingKey) -> PreparedVerifyingKey { let gamma = vk.gamma_g2.neg(); let delta = vk.delta_g2.neg(); diff --git a/src/groth16/verifier/batch.rs b/src/groth16/verifier/batch.rs new file mode 100644 index 000000000..a7c3ead32 --- /dev/null +++ b/src/groth16/verifier/batch.rs @@ -0,0 +1,170 @@ +//! Performs batch Groth16 proof verification. +//! +//! Batch verification asks whether *all* proofs in some set are valid, +//! rather than asking whether *each* of them is valid. This allows sharing +//! computations among all proof verifications, performing less work overall +//! at the cost of higher latency (the entire batch must complete), complexity of +//! caller code (which must assemble a batch of proofs across work-items), +//! and loss of the ability to easily pinpoint failing proofs. +//! +//! This batch verification implementation is non-adaptive, in the sense that it +//! assumes that all the proofs in the batch are verifiable by the same +//! `VerifyingKey`. The reason is that if you have different proof statements, +//! you need to specify which statement you are proving, which means that you +//! need to refer to or lookup a particular `VerifyingKey`. In practice, with +//! large enough batches, it's manageable and not much worse performance-wise to +//! keep batches of each statement type, vs one large adaptive batch. + +use std::ops::AddAssign; + +use ff::Field; +use group::{Curve, Group}; +use pairing::{MillerLoopResult, MultiMillerLoop}; +use rand_core::{CryptoRng, RngCore}; + +use crate::{ + groth16::{PreparedVerifyingKey, Proof, VerifyingKey}, + VerificationError, +}; + +/// A batch verification item. +/// +/// This struct exists to allow batch processing to be decoupled from the +/// lifetime of the message. This is useful when using the batch verification +/// API in an async context. +#[derive(Clone, Debug)] +pub struct Item { + proof: Proof, + inputs: Vec, +} + +impl From<(&Proof, &[E::Fr])> for Item { + fn from((proof, inputs): (&Proof, &[E::Fr])) -> Self { + (proof.clone(), inputs.to_owned()).into() + } +} + +impl From<(Proof, Vec)> for Item { + fn from((proof, inputs): (Proof, Vec)) -> Self { + Self { proof, inputs } + } +} + +impl Item { + /// Perform non-batched verification of this `Item`. + /// + /// This is useful (in combination with `Item::clone`) for implementing + /// fallback logic when batch verification fails. + pub fn verify_single(self, pvk: &PreparedVerifyingKey) -> Result<(), VerificationError> { + super::verify_proof(pvk, &self.proof, &self.inputs) + } +} + +/// A batch verification context. +/// +/// In practice, you would create a batch verifier for each proof statement +/// requiring the same `VerifyingKey`. +#[derive(Debug)] +pub struct Verifier { + items: Vec>, +} + +// Need to impl Default by hand to avoid a derived E: Default bound +impl Default for Verifier { + fn default() -> Self { + Self { items: Vec::new() } + } +} + +impl Verifier +where + E::G1: AddAssign, +{ + /// Construct a new batch verifier. + pub fn new() -> Self { + Self::default() + } + + /// Queue a (proof, inputs) tuple for verification. + pub fn queue>>(&mut self, item: I) { + self.items.push(item.into()) + } + + /// Perform batch verification with a particular `VerifyingKey`, returning + /// `Ok(())` if all proofs were verified and `VerificationError` otherwise. + #[allow(non_snake_case)] + pub fn verify( + self, + mut rng: R, + vk: &VerifyingKey, + ) -> Result<(), VerificationError> { + if self + .items + .iter() + .any(|Item { inputs, .. }| inputs.len() + 1 != vk.ic.len()) + { + return Err(VerificationError::InvalidVerifyingKey); + } + + let mut ml_terms = Vec::<(E::G1Affine, E::G2Prepared)>::new(); + let mut acc_Gammas = vec![E::Fr::zero(); vk.ic.len()]; + let mut acc_Delta = E::G1::identity(); + let mut acc_Y = E::Fr::zero(); + + for Item { proof, inputs } in self.items.into_iter() { + // The spec is explicit that z != 0. Field::random is defined to + // return a uniformly-random field element (which may be 0), so we + // loop until it's not, avoiding needing an assert or throwing an + // error through no fault of the batch items. This will likely never + // actually loop, but handles the edge case. + let z = loop { + let z = E::Fr::random(&mut rng); + if !z.is_zero() { + break z; + } + }; + + ml_terms.push(((proof.a * &z).into(), (-proof.b).into())); + + acc_Gammas[0] += &z; // a_0 is implicitly set to 1 + for (a_i, acc_Gamma_i) in Iterator::zip(inputs.iter(), acc_Gammas.iter_mut().skip(1)) { + *acc_Gamma_i += &(z * a_i); + } + acc_Delta += proof.c * &z; + acc_Y += &z; + } + + ml_terms.push((acc_Delta.to_affine(), E::G2Prepared::from(vk.delta_g2))); + + let Psi = vk + .ic + .iter() + .zip(acc_Gammas.iter()) + .map(|(&Psi_i, acc_Gamma_i)| Psi_i * acc_Gamma_i) + .sum(); + + ml_terms.push((E::G1Affine::from(Psi), E::G2Prepared::from(vk.gamma_g2))); + + // Covers the [acc_Y]⋅e(alpha_g1, beta_g2) component + // + // The multiplication by acc_Y is expensive -- it involves + // exponentiating by acc_Y because the result of the pairing is an + // element of a multiplicative subgroup of a large extension field. + // Instead, we add + // ([acc_Y]⋅alpha_g1, beta_g2) + // to our Miller loop terms because + // [acc_Y]⋅e(alpha_g1, beta_g2) = e([acc_Y]⋅alpha_g1, beta_g2) + ml_terms.push(( + E::G1Affine::from(vk.alpha_g1 * &acc_Y), + E::G2Prepared::from(vk.beta_g2), + )); + + let ml_terms = ml_terms.iter().map(|(a, b)| (a, b)).collect::>(); + + if E::multi_miller_loop(&ml_terms[..]).final_exponentiation() == E::Gt::identity() { + Ok(()) + } else { + Err(VerificationError::InvalidProof) + } + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 000000000..00d252272 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,128 @@ +use ff::PrimeField; + +use bellman::{Circuit, ConstraintSystem, SynthesisError}; + +pub const MIMC_ROUNDS: usize = 322; + +/// This is an implementation of MiMC, specifically a +/// variant named `LongsightF322p3` for BLS12-381. +/// See http://eprint.iacr.org/2016/492 for more +/// information about this construction. +/// +/// ``` +/// function LongsightF322p3(xL ⦂ Fp, xR ⦂ Fp) { +/// for i from 0 up to 321 { +/// xL, xR := xR + (xL + Ci)^3, xL +/// } +/// return xL +/// } +/// ``` +pub fn mimc(mut xl: S, mut xr: S, constants: &[S]) -> S { + assert_eq!(constants.len(), MIMC_ROUNDS); + + for c in constants { + let mut tmp1 = xl; + tmp1.add_assign(c); + let mut tmp2 = tmp1.square(); + tmp2.mul_assign(&tmp1); + tmp2.add_assign(&xr); + xr = xl; + xl = tmp2; + } + + xl +} + +/// This is our demo circuit for proving knowledge of the +/// preimage of a MiMC hash invocation. +pub struct MiMCDemo<'a, S: PrimeField> { + pub xl: Option, + pub xr: Option, + pub constants: &'a [S], +} + +/// Our demo circuit implements this `Circuit` trait which +/// is used during paramgen and proving in order to +/// synthesize the constraint system. +impl<'a, S: PrimeField> Circuit for MiMCDemo<'a, S> { + fn synthesize>(self, cs: &mut CS) -> Result<(), SynthesisError> { + assert_eq!(self.constants.len(), MIMC_ROUNDS); + + // Allocate the first component of the preimage. + let mut xl_value = self.xl; + let mut xl = cs.alloc( + || "preimage xl", + || xl_value.ok_or(SynthesisError::AssignmentMissing), + )?; + + // Allocate the second component of the preimage. + let mut xr_value = self.xr; + let mut xr = cs.alloc( + || "preimage xr", + || xr_value.ok_or(SynthesisError::AssignmentMissing), + )?; + + for i in 0..MIMC_ROUNDS { + // xL, xR := xR + (xL + Ci)^3, xL + let cs = &mut cs.namespace(|| format!("round {}", i)); + + // tmp = (xL + Ci)^2 + let tmp_value = xl_value.map(|mut e| { + e.add_assign(&self.constants[i]); + e.square() + }); + let tmp = cs.alloc( + || "tmp", + || tmp_value.ok_or(SynthesisError::AssignmentMissing), + )?; + + cs.enforce( + || "tmp = (xL + Ci)^2", + |lc| lc + xl + (self.constants[i], CS::one()), + |lc| lc + xl + (self.constants[i], CS::one()), + |lc| lc + tmp, + ); + + // new_xL = xR + (xL + Ci)^3 + // new_xL = xR + tmp * (xL + Ci) + // new_xL - xR = tmp * (xL + Ci) + let new_xl_value = xl_value.map(|mut e| { + e.add_assign(&self.constants[i]); + e.mul_assign(&tmp_value.unwrap()); + e.add_assign(&xr_value.unwrap()); + e + }); + + let new_xl = if i == (MIMC_ROUNDS - 1) { + // This is the last round, xL is our image and so + // we allocate a public input. + cs.alloc_input( + || "image", + || new_xl_value.ok_or(SynthesisError::AssignmentMissing), + )? + } else { + cs.alloc( + || "new_xl", + || new_xl_value.ok_or(SynthesisError::AssignmentMissing), + )? + }; + + cs.enforce( + || "new_xL = xR + (xL + Ci)^3", + |lc| lc + tmp, + |lc| lc + xl + (self.constants[i], CS::one()), + |lc| lc + new_xl - xr, + ); + + // xR = xL + xr = xl; + xr_value = xl_value; + + // xL = new_xL + xl = new_xl; + xl_value = new_xl_value; + } + + Ok(()) + } +} diff --git a/tests/mimc.rs b/tests/mimc.rs index 847d10cf6..ce0a28054 100644 --- a/tests/mimc.rs +++ b/tests/mimc.rs @@ -5,150 +5,109 @@ use rand::thread_rng; use std::time::{Duration, Instant}; // Bring in some tools for using finite fiels -use ff::{Field, PrimeField}; +use ff::Field; // We're going to use the BLS12-381 pairing-friendly elliptic curve. use bls12_381::{Bls12, Scalar}; -// We'll use these interfaces to construct our circuit. -use bellman::{Circuit, ConstraintSystem, SynthesisError}; - // We're going to use the Groth16 proving system. use bellman::groth16::{ - create_random_proof, generate_random_parameters, prepare_verifying_key, verify_proof, Proof, + batch, create_random_proof, generate_random_parameters, prepare_verifying_key, verify_proof, + Proof, }; -const MIMC_ROUNDS: usize = 322; - -/// This is an implementation of MiMC, specifically a -/// variant named `LongsightF322p3` for BLS12-381. -/// See http://eprint.iacr.org/2016/492 for more -/// information about this construction. -/// -/// ``` -/// function LongsightF322p3(xL ⦂ Fp, xR ⦂ Fp) { -/// for i from 0 up to 321 { -/// xL, xR := xR + (xL + Ci)^3, xL -/// } -/// return xL -/// } -/// ``` -fn mimc(mut xl: Scalar, mut xr: Scalar, constants: &[Scalar]) -> Scalar { - assert_eq!(constants.len(), MIMC_ROUNDS); - - for i in 0..MIMC_ROUNDS { - let mut tmp1 = xl; - tmp1.add_assign(&constants[i]); - let mut tmp2 = tmp1.square(); - tmp2.mul_assign(&tmp1); - tmp2.add_assign(&xr); - xr = xl; - xl = tmp2; - } +mod common; - xl -} +use common::*; -/// This is our demo circuit for proving knowledge of the -/// preimage of a MiMC hash invocation. -struct MiMCDemo<'a, Scalar: PrimeField> { - xl: Option, - xr: Option, - constants: &'a [Scalar], -} +#[test] +fn test_mimc() { + // This may not be cryptographically safe, use + // `OsRng` (for example) in production software. + let mut rng = thread_rng(); -/// Our demo circuit implements this `Circuit` trait which -/// is used during paramgen and proving in order to -/// synthesize the constraint system. -impl<'a, Scalar: PrimeField> Circuit for MiMCDemo<'a, Scalar> { - fn synthesize>(self, cs: &mut CS) -> Result<(), SynthesisError> { - assert_eq!(self.constants.len(), MIMC_ROUNDS); - - // Allocate the first component of the preimage. - let mut xl_value = self.xl; - let mut xl = cs.alloc( - || "preimage xl", - || xl_value.ok_or(SynthesisError::AssignmentMissing), - )?; - - // Allocate the second component of the preimage. - let mut xr_value = self.xr; - let mut xr = cs.alloc( - || "preimage xr", - || xr_value.ok_or(SynthesisError::AssignmentMissing), - )?; - - for i in 0..MIMC_ROUNDS { - // xL, xR := xR + (xL + Ci)^3, xL - let cs = &mut cs.namespace(|| format!("round {}", i)); - - // tmp = (xL + Ci)^2 - let tmp_value = xl_value.map(|mut e| { - e.add_assign(&self.constants[i]); - e.square() - }); - let tmp = cs.alloc( - || "tmp", - || tmp_value.ok_or(SynthesisError::AssignmentMissing), - )?; - - cs.enforce( - || "tmp = (xL + Ci)^2", - |lc| lc + xl + (self.constants[i], CS::one()), - |lc| lc + xl + (self.constants[i], CS::one()), - |lc| lc + tmp, - ); - - // new_xL = xR + (xL + Ci)^3 - // new_xL = xR + tmp * (xL + Ci) - // new_xL - xR = tmp * (xL + Ci) - let new_xl_value = xl_value.map(|mut e| { - e.add_assign(&self.constants[i]); - e.mul_assign(&tmp_value.unwrap()); - e.add_assign(&xr_value.unwrap()); - e - }); - - let new_xl = if i == (MIMC_ROUNDS - 1) { - // This is the last round, xL is our image and so - // we allocate a public input. - cs.alloc_input( - || "image", - || new_xl_value.ok_or(SynthesisError::AssignmentMissing), - )? - } else { - cs.alloc( - || "new_xl", - || new_xl_value.ok_or(SynthesisError::AssignmentMissing), - )? - }; + // Generate the MiMC round constants + let constants = (0..MIMC_ROUNDS) + .map(|_| Scalar::random(&mut rng)) + .collect::>(); + + println!("Creating parameters..."); + + // Create parameters for our circuit + let params = { + let c = MiMCDemo { + xl: None, + xr: None, + constants: &constants, + }; + + generate_random_parameters::(c, &mut rng).unwrap() + }; + + // Prepare the verification key (for proof verification) + let pvk = prepare_verifying_key(¶ms.vk); + + println!("Creating proofs..."); + + // Let's benchmark stuff! + const SAMPLES: u32 = 50; + let mut total_proving = Duration::new(0, 0); + let mut total_verifying = Duration::new(0, 0); + + // Just a place to put the proof data, so we can + // benchmark deserialization. + let mut proof_vec = vec![]; + + for _ in 0..SAMPLES { + // Generate a random preimage and compute the image + let xl = Scalar::random(&mut rng); + let xr = Scalar::random(&mut rng); + let image = mimc(xl, xr, &constants); - cs.enforce( - || "new_xL = xR + (xL + Ci)^3", - |lc| lc + tmp, - |lc| lc + xl + (self.constants[i], CS::one()), - |lc| lc + new_xl - xr, - ); + proof_vec.truncate(0); - // xR = xL - xr = xl; - xr_value = xl_value; + let start = Instant::now(); + { + // Create an instance of our circuit (with the + // witness) + let c = MiMCDemo { + xl: Some(xl), + xr: Some(xr), + constants: &constants, + }; - // xL = new_xL - xl = new_xl; - xl_value = new_xl_value; + // Create a groth16 proof with our parameters. + let proof = create_random_proof(c, ¶ms, &mut rng).unwrap(); + + proof.write(&mut proof_vec).unwrap(); } - Ok(()) + total_proving += start.elapsed(); + + let start = Instant::now(); + let proof = Proof::read(&proof_vec[..]).unwrap(); + // Check the proof + assert!(verify_proof(&pvk, &proof, &[image]).is_ok()); + total_verifying += start.elapsed(); } + let proving_avg = total_proving / SAMPLES; + let proving_avg = + proving_avg.subsec_nanos() as f64 / 1_000_000_000f64 + (proving_avg.as_secs() as f64); + + let verifying_avg = total_verifying / SAMPLES; + let verifying_avg = + verifying_avg.subsec_nanos() as f64 / 1_000_000_000f64 + (verifying_avg.as_secs() as f64); + + println!("Average proving time: {:?} seconds", proving_avg); + println!("Average verifying time: {:?} seconds", verifying_avg); } #[test] -fn test_mimc() { - // This may not be cryptographically safe, use - // `OsRng` (for example) in production software. +fn batch_verify() { let mut rng = thread_rng(); + let mut batch = batch::Verifier::new(); + // Generate the MiMC round constants let constants = (0..MIMC_ROUNDS) .map(|_| Scalar::random(&mut rng)) @@ -209,10 +168,24 @@ fn test_mimc() { let start = Instant::now(); let proof = Proof::read(&proof_vec[..]).unwrap(); + // Check the proof assert!(verify_proof(&pvk, &proof, &[image]).is_ok()); + total_verifying += start.elapsed(); + + // Queue the proof and inputs for batch verification. + batch.queue((proof, [image].into())); } + + let mut batch_verifying = Duration::new(0, 0); + let batch_start = Instant::now(); + + // Verify this batch for this specific verifying key + assert!(batch.verify(rng, ¶ms.vk).is_ok()); + + batch_verifying += batch_start.elapsed(); + let proving_avg = total_proving / SAMPLES; let proving_avg = proving_avg.subsec_nanos() as f64 / 1_000_000_000f64 + (proving_avg.as_secs() as f64); @@ -221,6 +194,14 @@ fn test_mimc() { let verifying_avg = verifying_avg.subsec_nanos() as f64 / 1_000_000_000f64 + (verifying_avg.as_secs() as f64); + let batch_amortized = batch_verifying / SAMPLES; + let batch_amortized = batch_amortized.subsec_nanos() as f64 / 1_000_000_000f64 + + (batch_amortized.as_secs() as f64); + println!("Average proving time: {:?} seconds", proving_avg); println!("Average verifying time: {:?} seconds", verifying_avg); + println!( + "Amortized batch verifying time: {:?} seconds", + batch_amortized + ); }