diff --git a/README.md b/README.md index e36179af8..d6ac2d137 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Nova: Recursive SNARKs without trusted setup +# Nova: High-speed recursive arguments from folding schemes > [!NOTE] > This repository is a fork of the original hosted at [https://github.com/microsoft/nova](https://github.com/microsoft/nova). It's an incubator for experimenting with more advanced variants of the original software and working out the kinks in them. diff --git a/examples/minroot.rs b/examples/minroot.rs index 74cee7e5a..c81397018 100644 --- a/examples/minroot.rs +++ b/examples/minroot.rs @@ -2,28 +2,27 @@ //! iterations of the `MinRoot` function, thereby realizing a Nova-based verifiable delay function (VDF). //! We execute a configurable number of iterations of the `MinRoot` function per step of Nova's recursion. use arecibo::{ - provider::{PallasEngine, VestaEngine}, + provider::{Bn256EngineKZG, GrumpkinEngine}, traits::{ circuit::{StepCircuit, TrivialCircuit}, - snark::default_ck_hint, - Engine, + snark::RelaxedR1CSSNARKTrait, + Engine, Group, }, CompressedSNARK, PublicParams, RecursiveSNARK, }; use bellpepper_core::{num::AllocatedNum, ConstraintSystem, SynthesisError}; -use ff::PrimeField; +use ff::Field; use flate2::{write::ZlibEncoder, Compression}; +use halo2curves::bn256::Bn256; use num_bigint::BigUint; use std::time::Instant; use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry}; use tracing_texray::TeXRayLayer; -type E1 = PallasEngine; -type E2 = VestaEngine; - #[cfg(feature = "abomonate")] mod utils { use super::*; + use ff::PrimeField; use std::{io::Write, mem::size_of}; pub const FILEPATH: &str = "/tmp/data"; @@ -49,7 +48,7 @@ mod utils { std::ptr::write(f, std::ptr::read(mine)); rest } - impl abomonation::Abomonation for MinRootIteration { + impl abomonation::Abomonation for MinRootIteration { unsafe fn entomb(&self, bytes: &mut W) -> std::io::Result<()> { entomb_F(&self.x_i, bytes)?; entomb_F(&self.y_i, bytes)?; @@ -73,29 +72,32 @@ mod utils { } #[derive(Clone, Debug, PartialEq)] -struct MinRootIteration { - x_i: F, - y_i: F, - x_i_plus_1: F, - y_i_plus_1: F, +struct MinRootIteration { + x_i: G::Scalar, + y_i: G::Scalar, + x_i_plus_1: G::Scalar, + y_i_plus_1: G::Scalar, } -impl MinRootIteration { +impl MinRootIteration { // produces a sample non-deterministic advice, executing one invocation of MinRoot per step - fn new(num_iters: usize, x_0: &F, y_0: &F) -> (Vec, Vec) { - // although this code is written generically, it is tailored to Pallas' scalar field - // (p - 3 / 5) - let exp = BigUint::parse_bytes( - b"23158417847463239084714197001737581570690445185553317903743794198714690358477", - 10, - ) - .unwrap(); + fn new(num_iters: usize, x_0: &G::Scalar, y_0: &G::Scalar) -> (Vec, Vec) { + // exp = (p - 3 / 5), where p is the order of the group + // x^{exp} mod p provides the fifth root of x + let exp = { + let p = G::group_params().2.to_biguint().unwrap(); + let two = BigUint::parse_bytes(b"2", 10).unwrap(); + let three = BigUint::parse_bytes(b"3", 10).unwrap(); + let five = BigUint::parse_bytes(b"5", 10).unwrap(); + let five_inv = five.modpow(&(&p - &two), &p); + (&five_inv * (&p - &three)) % &p + }; let mut res = Vec::new(); let mut x_i = *x_0; let mut y_i = *y_0; for _i in 0..num_iters { - let x_i_plus_1 = (x_i + y_i).pow_vartime(exp.to_u64_digits()); // computes the fifth root of x_i + y_i + let x_i_plus_1 = (x_i + y_i).pow_vartime(&exp.to_u64_digits()); // computes the fifth root of x_i + y_i // sanity check if cfg!(debug_assertions) { @@ -125,21 +127,21 @@ impl MinRootIteration { } #[derive(Clone, Debug, PartialEq)] -struct MinRootCircuit { - seq: Vec>, +struct MinRootCircuit { + seq: Vec>, } -impl StepCircuit for MinRootCircuit { +impl StepCircuit for MinRootCircuit { fn arity(&self) -> usize { 2 } - fn synthesize>( + fn synthesize>( &self, cs: &mut CS, - z: &[AllocatedNum], - ) -> Result>, SynthesisError> { - let mut z_out: Result>, SynthesisError> = + z: &[AllocatedNum], + ) -> Result>, SynthesisError> { + let mut z_out: Result>, SynthesisError> = Err(SynthesisError::AssignmentMissing); // use the provided inputs @@ -220,13 +222,13 @@ fn main() { let pp = PublicParams::< E1, E2, - MinRootCircuit<::Scalar>, + MinRootCircuit<::GE>, TrivialCircuit<::Scalar>, >::setup( &circuit_primary, &circuit_secondary, - &*default_ck_hint(), - &*default_ck_hint(), + &*S1::ck_floor(), + &*S2::ck_floor(), ); println!("PublicParams::setup, took {:?} ", start.elapsed()); #[cfg(feature = "abomonate")] @@ -271,7 +273,7 @@ fn main() { PublicParams< E1, E2, - MinRootCircuit<::Scalar>, + MinRootCircuit<::GE>, TrivialCircuit<::Scalar>, >, >(&mut bytes) @@ -284,7 +286,7 @@ fn main() { } // produce non-deterministic advice - let (z0_primary, minroot_iterations) = MinRootIteration::new( + let (z0_primary, minroot_iterations) = MinRootIteration::<::GE>::new( num_iters_per_step * num_steps, &::Scalar::zero(), &::Scalar::one(), @@ -304,7 +306,7 @@ fn main() { let z0_secondary = vec![::Scalar::zero()]; - type C1 = MinRootCircuit<::Scalar>; + type C1 = MinRootCircuit<::GE>; type C2 = TrivialCircuit<::Scalar>; // produce a recursive SNARK println!("Generating a RecursiveSNARK..."); @@ -342,14 +344,16 @@ fn main() { assert!(res.is_ok()); // produce a compressed SNARK - println!("Generating a CompressedSNARK using Spartan with IPA-PC..."); + println!("Generating a CompressedSNARK using Spartan with multilinear KZG..."); let (pk, vk) = CompressedSNARK::<_, _, _, _, S1, S2>::setup(&pp).unwrap(); let start = Instant::now(); - type EE1 = arecibo::provider::ipa_pc::EvaluationEngine; + type E1 = Bn256EngineKZG; + type E2 = GrumpkinEngine; + type EE1 = arecibo::provider::mlkzg::EvaluationEngine; type EE2 = arecibo::provider::ipa_pc::EvaluationEngine; - type S1 = arecibo::spartan::snark::RelaxedR1CSSNARK; - type S2 = arecibo::spartan::snark::RelaxedR1CSSNARK; + type S1 = arecibo::spartan::snark::RelaxedR1CSSNARK; // non-preprocessing SNARK + type S2 = arecibo::spartan::snark::RelaxedR1CSSNARK; // non-preprocessing SNARK let res = CompressedSNARK::<_, _, _, _, S1, S2>::prove(&pp, &pk, &recursive_snark); println!( diff --git a/src/lib.rs b/src/lib.rs index 5b38d10c8..585b27a91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1000,8 +1000,8 @@ mod tests { use super::*; use crate::{ provider::{ - non_hiding_zeromorph::ZMPCS, traits::DlogGroup, Bn256Engine, Bn256EngineZM, GrumpkinEngine, - PallasEngine, Secp256k1Engine, Secq256k1Engine, VestaEngine, + non_hiding_zeromorph::ZMPCS, traits::DlogGroup, Bn256Engine, Bn256EngineKZG, Bn256EngineZM, + GrumpkinEngine, PallasEngine, Secp256k1Engine, Secq256k1Engine, VestaEngine, }, traits::{evaluation::EvaluationEngineTrait, snark::default_ck_hint}, }; @@ -1387,6 +1387,13 @@ mod tests { ZMPCS, EE<_>, >(); + + test_ivc_nontrivial_with_spark_compression_with::< + Bn256EngineKZG, + GrumpkinEngine, + provider::mlkzg::EvaluationEngine, + EE<_>, + >(); } fn test_ivc_nontrivial_with_spark_compression_with() diff --git a/src/provider/bn256_grumpkin.rs b/src/provider/bn256_grumpkin.rs index 88f7bec45..4b72e2976 100644 --- a/src/provider/bn256_grumpkin.rs +++ b/src/provider/bn256_grumpkin.rs @@ -2,8 +2,8 @@ use crate::{ impl_traits, provider::{ - msm::cpu_best_msm, traits::{CompressedGroup, DlogGroup}, + util::msm::cpu_best_msm, }, traits::{Group, PrimeFieldExt, TranscriptReprTrait}, }; diff --git a/src/provider/kzg_commitment.rs b/src/provider/kzg_commitment.rs index f1e6fa5ab..3663a379b 100644 --- a/src/provider/kzg_commitment.rs +++ b/src/provider/kzg_commitment.rs @@ -16,7 +16,7 @@ use crate::traits::{ }; use crate::provider::{ - non_hiding_kzg::{UVKZGCommitment, UVUniversalKZGParam}, + non_hiding_kzg::{UVKZGCommitment, UniversalKZGParam}, pedersen::Commitment, traits::DlogGroup, }; @@ -35,7 +35,7 @@ where E::G2Affine: Serialize + for<'de> Deserialize<'de>, E::Fr: PrimeFieldBits, // TODO due to use of gen_srs_for_testing, make optional { - type CommitmentKey = UVUniversalKZGParam; + type CommitmentKey = UniversalKZGParam; type Commitment = Commitment; fn setup(label: &'static [u8], n: usize) -> Self::CommitmentKey { @@ -44,7 +44,7 @@ where let len = label.len().min(32); bytes[..len].copy_from_slice(&label[..len]); let rng = &mut StdRng::from_seed(bytes); - UVUniversalKZGParam::gen_srs_for_testing(rng, n.next_power_of_two()) + UniversalKZGParam::gen_srs_for_testing(rng, n.next_power_of_two()) } fn commit(ck: &Self::CommitmentKey, v: &[::Scalar]) -> Self::Commitment { diff --git a/src/provider/mlkzg.rs b/src/provider/mlkzg.rs new file mode 100644 index 000000000..530246500 --- /dev/null +++ b/src/provider/mlkzg.rs @@ -0,0 +1,612 @@ +//! This module implements Nova's evaluation engine using multilinear KZG +#![allow(non_snake_case)] +use crate::{ + errors::NovaError, + provider::{ + kzg_commitment::KZGCommitmentEngine, + non_hiding_kzg::{KZGProverKey, KZGVerifierKey, UniversalKZGParam}, + pedersen::Commitment, + traits::DlogGroup, + }, + spartan::polys::univariate::UniPoly, + traits::{ + commitment::{CommitmentEngineTrait, Len}, + evaluation::EvaluationEngineTrait, + Engine as NovaEngine, Group, TranscriptEngineTrait, TranscriptReprTrait, + }, + zip_with, +}; +use core::marker::PhantomData; +use ff::{Field, PrimeFieldBits}; +use group::{Curve, Group as _}; +use itertools::Itertools as _; +use pairing::{Engine, MillerLoopResult, MultiMillerLoop}; +use rayon::prelude::*; +use ref_cast::RefCast as _; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +/// Provides an implementation of a polynomial evaluation argument +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(bound( + serialize = "E::G1Affine: Serialize, E::Fr: Serialize", + deserialize = "E::G1Affine: Deserialize<'de>, E::Fr: Deserialize<'de>" +))] +pub struct EvaluationArgument { + evals_r: Vec, + evals_neg_r: Vec, + evals_r_squared: Vec>, +} + +/// Provides an implementation of a polynomial evaluation engine using KZG +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EvaluationEngine { + _p: PhantomData<(E, NE)>, +} + +// This impl block defines helper functions that are not a part of +// EvaluationEngineTrait, but that we will use to implement the trait methods. +impl EvaluationEngine +where + E: Engine, + NE: NovaEngine, + E::G1: DlogGroup, + E::Fr: TranscriptReprTrait, + E::G1Affine: TranscriptReprTrait, // TODO: this bound on DlogGroup is really unusable! +{ + fn compute_challenge( + C: &E::G1Affine, + y: &E::Fr, + com: &[E::G1Affine], + transcript: &mut impl TranscriptEngineTrait, + ) -> E::Fr { + transcript.absorb(b"C", C); + transcript.absorb(b"y", y); + transcript.absorb(b"c", &com.to_vec().as_slice()); + + transcript.squeeze(b"c").unwrap() + } + + // Compute challenge q = Hash(vk, C0, ..., C_{k-1}, u0, ...., u_{t-1}, + // (f_i(u_j))_{i=0..k-1,j=0..t-1}) + fn get_batch_challenge( + C: &[E::G1Affine], + u: &[E::Fr], + v: &[Vec], + transcript: &mut impl TranscriptEngineTrait, + ) -> E::Fr { + transcript.absorb(b"C", &C.to_vec().as_slice()); + transcript.absorb(b"u", &u.to_vec().as_slice()); + transcript.absorb( + b"v", + &v.iter() + .flatten() + .cloned() + .collect::>() + .as_slice(), + ); + + transcript.squeeze(b"r").unwrap() + } + + fn batch_challenge_powers(q: E::Fr, k: usize) -> Vec { + // Compute powers of q : (1, q, q^2, ..., q^(k-1)) + std::iter::successors(Some(E::Fr::ONE), |&x| Some(x * q)) + .take(k) + .collect() + } + + fn verifier_second_challenge( + C_B: &E::G1Affine, + W: &[E::G1Affine], + transcript: &mut impl TranscriptEngineTrait, + ) -> E::Fr { + transcript.absorb(b"C_b", C_B); + transcript.absorb(b"W", &W.to_vec().as_slice()); + + transcript.squeeze(b"d").unwrap() + } +} + +impl EvaluationEngineTrait for EvaluationEngine +where + E: MultiMillerLoop, + NE: NovaEngine>, + E::Fr: Serialize + DeserializeOwned, + E::G1Affine: Serialize + DeserializeOwned, + E::G2Affine: Serialize + DeserializeOwned, + E::G1: DlogGroup, + ::Base: TranscriptReprTrait, // Note: due to the move of the bound TranscriptReprTrait on G::Base from Group to Engine + E::Fr: PrimeFieldBits, // TODO due to use of gen_srs_for_testing, make optional + E::Fr: TranscriptReprTrait, + E::G1Affine: TranscriptReprTrait, +{ + type EvaluationArgument = EvaluationArgument; + type ProverKey = KZGProverKey; + type VerifierKey = KZGVerifierKey; + + fn setup(ck: &UniversalKZGParam) -> (Self::ProverKey, Self::VerifierKey) { + ck.trim(ck.length() - 1) + } + + fn prove( + ck: &UniversalKZGParam, + _pk: &Self::ProverKey, + transcript: &mut ::TE, + C: &Commitment, + hat_P: &[E::Fr], + point: &[E::Fr], + eval: &E::Fr, + ) -> Result { + let x: Vec = point.to_vec(); + + //////////////// begin helper closures ////////// + let kzg_open = |f: &[E::Fr], u: E::Fr| -> E::G1Affine { + // On input f(x) and u compute the witness polynomial used to prove + // that f(u) = v. The main part of this is to compute the + // division (f(x) - f(u)) / (x - u), but we don't use a general + // division algorithm, we make use of the fact that the division + // never has a remainder, and that the denominator is always a linear + // polynomial. The cost is (d-1) mults + (d-1) adds in E::Scalar, where + // d is the degree of f. + // + // We use the fact that if we compute the quotient of f(x)/(x-u), + // there will be a remainder, but it'll be v = f(u). Put another way + // the quotient of f(x)/(x-u) and (f(x) - f(v))/(x-u) is the + // same. One advantage is that computing f(u) could be decoupled + // from kzg_open, it could be done later or separate from computing W. + + let compute_witness_polynomial = |f: &[E::Fr], u: E::Fr| -> Vec { + let d = f.len(); + + // Compute h(x) = f(x)/(x - u) + let mut h = vec![E::Fr::ZERO; d]; + for i in (1..d).rev() { + h[i - 1] = f[i] + h[i] * u; + } + + h + }; + + let h = compute_witness_polynomial(f, u); + + >::commit(ck, &h) + .comm + .preprocessed() + }; + + let kzg_open_batch = |C: &[E::G1Affine], + f: &[Vec], + u: &[E::Fr], + transcript: &mut ::TE| + -> (Vec, Vec>) { + let scalar_vector_muladd = |a: &mut Vec, v: &Vec, s: E::Fr| { + assert!(a.len() >= v.len()); + #[allow(clippy::disallowed_methods)] + a.par_iter_mut() + .zip(v.par_iter()) + .for_each(|(c, v)| *c += s * v); + }; + + let kzg_compute_batch_polynomial = |f: &[Vec], q: E::Fr| -> Vec { + let k = f.len(); // Number of polynomials we're batching + + let q_powers = Self::batch_challenge_powers(q, k); + + // Compute B(x) = f[0] + q*f[1] + q^2 * f[2] + ... q^(k-1) * f[k-1] + let mut B = f[0].clone(); + for i in 1..k { + scalar_vector_muladd(&mut B, &f[i], q_powers[i]); // B += q_powers[i] * f[i] + } + + B + }; + ///////// END kzg_open_batch closure helpers + + let k = f.len(); + let t = u.len(); + assert!(C.len() == k); + + // The verifier needs f_i(u_j), so we compute them here + // (V will compute B(u_j) itself) + let mut v = vec![vec!(E::Fr::ZERO; k); t]; + v.par_iter_mut().enumerate().for_each(|(i, v_i)| { + // for each point u + v_i.par_iter_mut().zip_eq(f).for_each(|(v_ij, f)| { + // for each poly f + *v_ij = UniPoly::ref_cast(f).evaluate(&u[i]); + }); + }); + + let q = Self::get_batch_challenge(C, u, &v, transcript); + let B = kzg_compute_batch_polynomial(f, q); + + // Now open B at u0, ..., u_{t-1} + let w = u.par_iter().map(|ui| kzg_open(&B, *ui)).collect::>(); + + // Compute the commitment to the batched polynomial B(X) + let q_powers = Self::batch_challenge_powers(q, k); + let c_0: E::G1 = C[0].into(); + let C_B = (c_0 + NE::GE::vartime_multiscalar_mul(&q_powers[1..k], &C[1..k])).preprocessed(); + + // The prover computes the challenge to keep the transcript in the same + // state as that of the verifier + let _d_0 = Self::verifier_second_challenge(&C_B, &w, transcript); + + (w, v) + }; + + ///// END helper closures ////////// + + let ell = x.len(); + let n = hat_P.len(); + assert_eq!(n, 1 << ell); // Below we assume that n is a power of two + + // Phase 1 -- create commitments com_1, ..., com_\ell + let mut polys: Vec> = Vec::new(); + polys.push(hat_P.to_vec()); + for i in 0..ell { + let Pi_len = polys[i].len() / 2; + let mut Pi = vec![E::Fr::ZERO; Pi_len]; + + #[allow(clippy::needless_range_loop)] + for j in 0..Pi_len { + Pi[j] = x[ell-i-1] * polys[i][2*j + 1] // Odd part of P^(i-1) + + (E::Fr::ONE - x[ell-i-1]) * polys[i][2*j]; // Even part of P^(i-1) + } + + if i == ell - 1 && *eval != Pi[0] { + return Err(NovaError::UnSat); + } + + polys.push(Pi); + } + + // We do not need to commit to the first polynomial as it is already committed. + // Compute commitments in parallel + let com: Vec = (1..polys.len()) + .into_par_iter() + .map(|i| { + >::commit(ck, &polys[i]) + .comm + .preprocessed() + }) + .collect(); + + // Phase 2 + // We do not need to add x to the transcript, because in our context x was + // obtained from the transcript. + let r = Self::compute_challenge(&C.comm.preprocessed(), eval, &com, transcript); + let u = vec![r, -r, r * r]; + + // Phase 3 -- create response + let mut com_all = com.clone(); + com_all.insert(0, C.comm.preprocessed()); + let (w, v) = kzg_open_batch(&com_all, &polys, &u, transcript); + + Ok(EvaluationArgument { + evals_r: com, + evals_neg_r: w, + evals_r_squared: v, + }) + } + + /// A method to verify purported evaluations of a batch of polynomials + fn verify( + vk: &Self::VerifierKey, + transcript: &mut ::TE, + C: &Commitment, + point: &[E::Fr], + P_of_x: &E::Fr, + pi: &Self::EvaluationArgument, + ) -> Result<(), NovaError> { + let x = point.to_vec(); + let y = P_of_x; + + // vk is hashed in transcript already, so we do not add it here + + let kzg_verify_batch = |vk: &KZGVerifierKey, + C: &Vec, + W: &Vec, + u: &Vec, + v: &Vec>, + transcript: &mut ::TE| + -> bool { + let k = C.len(); + let t = u.len(); + + let q = Self::get_batch_challenge(C, u, v, transcript); + let q_powers = Self::batch_challenge_powers(q, k); // 1, q, q^2, ..., q^(k-1) + + // Compute the commitment to the batched polynomial B(X) + let c_0: E::G1 = C[0].into(); + let C_B = (c_0 + NE::GE::vartime_multiscalar_mul(&q_powers[1..k], &C[1..k])).preprocessed(); + + // Compute the batched openings + // compute B(u_i) = v[i][0] + q*v[i][1] + ... + q^(t-1) * v[i][t-1] + let B_u = v + .iter() + .map(|v_i| zip_with!(iter, (q_powers, v_i), |a, b| *a * *b).sum()) + .collect::>(); + + let d_0 = Self::verifier_second_challenge(&C_B, W, transcript); + let d = [d_0, d_0 * d_0]; + + assert!(t == 3); + // We write a special case for t=3, since this what is required for + // mlkzg. Following the paper directly, we must compute: + // let L0 = C_B - vk.G * B_u[0] + W[0] * u[0]; + // let L1 = C_B - vk.G * B_u[1] + W[1] * u[1]; + // let L2 = C_B - vk.G * B_u[2] + W[2] * u[2]; + // let R0 = -W[0]; + // let R1 = -W[1]; + // let R2 = -W[2]; + // let L = L0 + L1*d[0] + L2*d[1]; + // let R = R0 + R1*d[0] + R2*d[1]; + // + // We group terms to reduce the number of scalar mults (to seven): + // In Rust, we could use MSMs for these, and speed up verification. + let L = E::G1::from(C_B) * (E::Fr::ONE + d[0] + d[1]) + - E::G1::from(vk.g) * (B_u[0] + d[0] * B_u[1] + d[1] * B_u[2]) + + E::G1::from(W[0]) * u[0] + + E::G1::from(W[1]) * (u[1] * d[0]) + + E::G1::from(W[2]) * (u[2] * d[1]); + + let R0 = E::G1::from(W[0]); + let R1 = E::G1::from(W[1]); + let R2 = E::G1::from(W[2]); + let R = R0 + R1 * d[0] + R2 * d[1]; + + // Check that e(L, vk.H) == e(R, vk.tau_H) + let pairing_inputs = [ + (&(-L).to_affine(), &E::G2Prepared::from(vk.h)), + (&R.to_affine(), &E::G2Prepared::from(vk.beta_h)), + ]; + + let pairing_result = E::multi_miller_loop(&pairing_inputs).final_exponentiation(); + pairing_result.is_identity().into() + }; + ////// END verify() closure helpers + + let ell = x.len(); + + let mut com = pi.evals_r.clone(); + + // we do not need to add x to the transcript, because in our context x was + // obtained from the transcript + let r = Self::compute_challenge(&C.comm.preprocessed(), y, &com, transcript); + + if r == E::Fr::ZERO || C.comm == E::G1::zero() { + return Err(NovaError::ProofVerifyError); + } + com.insert(0, C.comm.preprocessed()); // set com_0 = C, shifts other commitments to the right + + let u = vec![r, -r, r * r]; + + // Setup vectors (Y, ypos, yneg) from pi.v + let v = &pi.evals_r_squared; + if v.len() != 3 { + return Err(NovaError::ProofVerifyError); + } + if v[0].len() != ell + 1 || v[1].len() != ell + 1 || v[2].len() != ell + 1 { + return Err(NovaError::ProofVerifyError); + } + let ypos = &v[0]; + let yneg = &v[1]; + let Y = &v[2]; + + // Check consistency of (Y, ypos, yneg) + if Y[ell] != *y { + return Err(NovaError::ProofVerifyError); + } + + let two = E::Fr::from(2u64); + for i in 0..ell { + if two * r * Y[i + 1] + != r * (E::Fr::ONE - x[ell - i - 1]) * (ypos[i] + yneg[i]) + + x[ell - i - 1] * (ypos[i] - yneg[i]) + { + return Err(NovaError::ProofVerifyError); + } + // Note that we don't make any checks about Y[0] here, but our batching + // check below requires it + } + + // Check commitments to (Y, ypos, yneg) are valid + if !kzg_verify_batch( + vk, + &com, + &pi.evals_neg_r, + &u, + &pi.evals_r_squared, + transcript, + ) { + return Err(NovaError::ProofVerifyError); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + provider::keccak::Keccak256Transcript, spartan::polys::multilinear::MultilinearPolynomial, + traits::commitment::CommitmentTrait, CommitmentKey, + }; + use bincode::Options; + use rand::SeedableRng; + + type E = halo2curves::bn256::Bn256; + type NE = crate::provider::Bn256EngineKZG; + type Fr = ::Scalar; + + #[test] + fn test_mlkzg_eval() { + // Test with poly(X1, X2) = 1 + X1 + X2 + X1*X2 + let n = 4; + let ck: CommitmentKey = + as CommitmentEngineTrait>::setup(b"test", n); + let (pk, _vk): (KZGProverKey, KZGVerifierKey) = EvaluationEngine::::setup(&ck); + + // poly is in eval. representation; evaluated at [(0,0), (0,1), (1,0), (1,1)] + let poly = vec![Fr::from(1), Fr::from(2), Fr::from(2), Fr::from(4)]; + + let C = as CommitmentEngineTrait>::commit(&ck, &poly); + let mut tr = Keccak256Transcript::::new(b"TestEval"); + + // Call the prover with a (point, eval) pair. The prover recomputes + // poly(point) = eval', and fails if eval' != eval + let point = vec![Fr::from(0), Fr::from(0)]; + let eval = Fr::ONE; + assert!(EvaluationEngine::::prove(&ck, &pk, &mut tr, &C, &poly, &point, &eval).is_ok()); + + let point = vec![Fr::from(0), Fr::from(1)]; + let eval = Fr::from(2); + assert!(EvaluationEngine::::prove(&ck, &pk, &mut tr, &C, &poly, &point, &eval).is_ok()); + + let point = vec![Fr::from(1), Fr::from(1)]; + let eval = Fr::from(4); + assert!(EvaluationEngine::::prove(&ck, &pk, &mut tr, &C, &poly, &point, &eval).is_ok()); + + let point = vec![Fr::from(0), Fr::from(2)]; + let eval = Fr::from(3); + assert!(EvaluationEngine::::prove(&ck, &pk, &mut tr, &C, &poly, &point, &eval).is_ok()); + + let point = vec![Fr::from(2), Fr::from(2)]; + let eval = Fr::from(9); + assert!(EvaluationEngine::::prove(&ck, &pk, &mut tr, &C, &poly, &point, &eval).is_ok()); + + // Try a couple incorrect evaluations and expect failure + let point = vec![Fr::from(2), Fr::from(2)]; + let eval = Fr::from(50); + assert!(EvaluationEngine::::prove(&ck, &pk, &mut tr, &C, &poly, &point, &eval).is_err()); + + let point = vec![Fr::from(0), Fr::from(2)]; + let eval = Fr::from(4); + assert!(EvaluationEngine::::prove(&ck, &pk, &mut tr, &C, &poly, &point, &eval).is_err()); + } + + #[test] + fn test_mlkzg() { + let n = 4; + + // poly = [1, 2, 1, 4] + let poly = vec![Fr::ONE, Fr::from(2), Fr::from(1), Fr::from(4)]; + + // point = [4,3] + let point = vec![Fr::from(4), Fr::from(3)]; + + // eval = 28 + let eval = Fr::from(28); + + let ck: CommitmentKey = + as CommitmentEngineTrait>::setup(b"test", n); + let (pk, vk): (KZGProverKey, KZGVerifierKey) = EvaluationEngine::::setup(&ck); + + // make a commitment + let C = KZGCommitmentEngine::commit(&ck, &poly); + + // prove an evaluation + let mut prover_transcript = Keccak256Transcript::new(b"TestEval"); + let proof = + EvaluationEngine::::prove(&ck, &pk, &mut prover_transcript, &C, &poly, &point, &eval) + .unwrap(); + let post_c_p = prover_transcript.squeeze(b"c").unwrap(); + + // verify the evaluation + let mut verifier_transcript = Keccak256Transcript::::new(b"TestEval"); + assert!(EvaluationEngine::::verify( + &vk, + &mut verifier_transcript, + &C, + &point, + &eval, + &proof + ) + .is_ok()); + let post_c_v = verifier_transcript.squeeze(b"c").unwrap(); + + // check if the prover transcript and verifier transcript are kept in the + // same state + assert_eq!(post_c_p, post_c_v); + + let my_options = bincode::DefaultOptions::new() + .with_big_endian() + .with_fixint_encoding(); + let mut output_bytes = my_options.serialize(&vk).unwrap(); + output_bytes.append(&mut my_options.serialize(&C.compress()).unwrap()); + output_bytes.append(&mut my_options.serialize(&point).unwrap()); + output_bytes.append(&mut my_options.serialize(&eval).unwrap()); + output_bytes.append(&mut my_options.serialize(&proof).unwrap()); + println!("total output = {} bytes", output_bytes.len()); + + // Change the proof and expect verification to fail + let mut bad_proof = proof.clone(); + bad_proof.evals_r[0] = (bad_proof.evals_r[0] + bad_proof.evals_r[1]).to_affine(); + let mut verifier_transcript2 = Keccak256Transcript::::new(b"TestEval"); + assert!(EvaluationEngine::::verify( + &vk, + &mut verifier_transcript2, + &C, + &point, + &eval, + &bad_proof + ) + .is_err()); + } + + #[test] + fn test_mlkzg_more() { + // test the mlkzg prover and verifier with random instances (derived from a seed) + for ell in [4, 5, 6] { + let mut rng = rand::rngs::StdRng::seed_from_u64(ell as u64); + + let n = 1 << ell; // n = 2^ell + + let poly = (0..n).map(|_| Fr::random(&mut rng)).collect::>(); + let point = (0..ell).map(|_| Fr::random(&mut rng)).collect::>(); + let eval = MultilinearPolynomial::evaluate_with(&poly, &point); + + let ck: CommitmentKey = + as CommitmentEngineTrait>::setup(b"test", n); + let (pk, vk): (KZGProverKey, KZGVerifierKey) = EvaluationEngine::::setup(&ck); + + // make a commitment + let C = as CommitmentEngineTrait>::commit(&ck, &poly); + + // prove an evaluation + let mut prover_transcript = Keccak256Transcript::::new(b"TestEval"); + let proof: EvaluationArgument = EvaluationEngine::::prove( + &ck, + &pk, + &mut prover_transcript, + &C, + &poly, + &point, + &eval, + ) + .unwrap(); + + // verify the evaluation + let mut verifier_tr = Keccak256Transcript::::new(b"TestEval"); + assert!( + EvaluationEngine::::verify(&vk, &mut verifier_tr, &C, &point, &eval, &proof).is_ok() + ); + + // Change the proof and expect verification to fail + let mut bad_proof = proof.clone(); + bad_proof.evals_r[0] = (bad_proof.evals_r[0] + bad_proof.evals_r[1]).to_affine(); + let mut verifier_tr2 = Keccak256Transcript::::new(b"TestEval"); + assert!(EvaluationEngine::::verify( + &vk, + &mut verifier_tr2, + &C, + &point, + &eval, + &bad_proof + ) + .is_err()); + } + } +} diff --git a/src/provider/mod.rs b/src/provider/mod.rs index 1ba2e27b6..6bbf9bec5 100644 --- a/src/provider/mod.rs +++ b/src/provider/mod.rs @@ -2,6 +2,7 @@ // public modules to be used as an evaluation engine with Spartan pub mod ipa_pc; +pub mod mlkzg; pub mod non_hiding_zeromorph; // crate-public modules, made crate-public mostly for tests @@ -18,7 +19,6 @@ mod util; // crate-private modules mod keccak; -mod msm; use crate::{ provider::{ @@ -76,6 +76,19 @@ impl Engine for Bn256EngineZM { type TE = Keccak256Transcript; type CE = KZGCommitmentEngine; } +/// An implementation of Nova traits with multilinear KZG over the BN256 curve +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Bn256EngineKZG; + +impl Engine for Bn256EngineKZG { + type Base = bn256::Base; + type Scalar = bn256::Scalar; + type GE = bn256::Point; + type RO = PoseidonRO; + type ROCircuit = PoseidonROCircuit; + type TE = Keccak256Transcript; + type CE = KZGCommitmentEngine; +} /// An implementation of the Nova `Engine` trait with Secp256k1 curve and Pedersen commitment scheme #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -137,9 +150,9 @@ impl Engine for VestaEngine { mod tests { use crate::provider::{ bn256_grumpkin::{bn256, grumpkin}, - msm::cpu_best_msm, secp_secq::{secp256k1, secq256k1}, traits::DlogGroup, + util::msm::cpu_best_msm, }; use digest::{ExtendableOutput, Update}; use group::{ff::Field, Curve, Group}; diff --git a/src/provider/non_hiding_kzg.rs b/src/provider/non_hiding_kzg.rs index ff6bb57a4..f9b7af9a2 100644 --- a/src/provider/non_hiding_kzg.rs +++ b/src/provider/non_hiding_kzg.rs @@ -21,7 +21,7 @@ use crate::{ deserialize = "E::G1Affine: Deserialize<'de>, E::G2Affine: Deserialize<'de>" ))] #[abomonation_omit_bounds] -pub struct UVUniversalKZGParam { +pub struct UniversalKZGParam { /// Group elements of the form `{ β^i G }`, where `i` ranges from 0 to /// `degree`. #[abomonate_with(Vec<[u64; 8]>)] // // this is a hack; we just assume the size of the element. @@ -32,14 +32,14 @@ pub struct UVUniversalKZGParam { pub powers_of_h: Vec, } -impl PartialEq for UVUniversalKZGParam { - fn eq(&self, other: &UVUniversalKZGParam) -> bool { +impl PartialEq for UniversalKZGParam { + fn eq(&self, other: &UniversalKZGParam) -> bool { self.powers_of_g == other.powers_of_g && self.powers_of_h == other.powers_of_h } } // for the purpose of the Len trait, we count commitment bases, i.e. G1 elements -impl Len for UVUniversalKZGParam { +impl Len for UniversalKZGParam { fn length(&self) -> usize { self.powers_of_g.len() } @@ -52,7 +52,7 @@ impl Len for UVUniversalKZGParam { serialize = "E::G1Affine: Serialize", deserialize = "E::G1Affine: Deserialize<'de>" ))] -pub struct UVKZGProverKey { +pub struct KZGProverKey { /// generators #[abomonate_with(Vec<[u64; 8]>)] // this is a hack; we just assume the size of the element. pub powers_of_g: Vec, @@ -66,7 +66,7 @@ pub struct UVKZGProverKey { serialize = "E::G1Affine: Serialize, E::G2Affine: Serialize", deserialize = "E::G1Affine: Deserialize<'de>, E::G2Affine: Deserialize<'de>" ))] -pub struct UVKZGVerifierKey { +pub struct KZGVerifierKey { /// The generator of G1. #[abomonate_with([u64; 8])] // this is a hack; we just assume the size of the element. pub g: E::G1Affine, @@ -78,7 +78,7 @@ pub struct UVKZGVerifierKey { pub beta_h: E::G2Affine, } -impl UVUniversalKZGParam { +impl UniversalKZGParam { /// Returns the maximum supported degree pub fn max_degree(&self) -> usize { self.powers_of_g.len() @@ -88,21 +88,21 @@ impl UVUniversalKZGParam { /// /// # Panics /// if `supported_size` is greater than `self.max_degree()` - pub fn extract_prover_key(&self, supported_size: usize) -> UVKZGProverKey { + pub fn extract_prover_key(&self, supported_size: usize) -> KZGProverKey { let powers_of_g = self.powers_of_g[..=supported_size].to_vec(); - UVKZGProverKey { powers_of_g } + KZGProverKey { powers_of_g } } /// Returns the verifier parameters /// /// # Panics /// If self.prover_params is empty. - pub fn extract_verifier_key(&self, supported_size: usize) -> UVKZGVerifierKey { + pub fn extract_verifier_key(&self, supported_size: usize) -> KZGVerifierKey { assert!( self.powers_of_g.len() >= supported_size, "supported_size is greater than self.max_degree()" ); - UVKZGVerifierKey { + KZGVerifierKey { g: self.powers_of_g[0], h: self.powers_of_h[0], beta_h: self.powers_of_h[1], @@ -116,11 +116,11 @@ impl UVUniversalKZGParam { /// /// # Panics /// If `supported_size` is greater than `self.max_degree()`, or `self.max_degree()` is zero. - pub fn trim(&self, supported_size: usize) -> (UVKZGProverKey, UVKZGVerifierKey) { + pub fn trim(&self, supported_size: usize) -> (KZGProverKey, KZGVerifierKey) { let powers_of_g = self.powers_of_g[..=supported_size].to_vec(); - let pk = UVKZGProverKey { powers_of_g }; - let vk = UVKZGVerifierKey { + let pk = KZGProverKey { powers_of_g }; + let vk = KZGVerifierKey { g: self.powers_of_g[0], h: self.powers_of_h[0], beta_h: self.powers_of_h[1], @@ -129,7 +129,7 @@ impl UVUniversalKZGParam { } } -impl UVUniversalKZGParam +impl UniversalKZGParam where E::Fr: PrimeFieldBits, { @@ -239,7 +239,7 @@ where /// Generate a commitment for a polynomial /// Note that the scheme is not hidding pub fn commit( - prover_param: impl Borrow>, + prover_param: impl Borrow>, poly: &UVKZGPoly, ) -> Result, NovaError> { let prover_param = prover_param.borrow(); @@ -257,7 +257,7 @@ where /// On input a polynomial `p` and a point `point`, outputs a proof for the /// same. pub fn open( - prover_param: impl Borrow>, + prover_param: impl Borrow>, polynomial: &UVKZGPoly, point: &E::Fr, ) -> Result<(UVKZGProof, UVKZGEvaluation), NovaError> { @@ -287,7 +287,7 @@ where /// committed inside `comm`. #[allow(dead_code)] pub fn verify( - verifier_param: impl Borrow>, + verifier_param: impl Borrow>, commitment: &UVKZGCommitment, point: &E::Fr, proof: &UVKZGProof, @@ -334,7 +334,7 @@ mod tests { let mut rng = &mut thread_rng(); let degree = rng.gen_range(2..20); - let pp = UVUniversalKZGParam::::gen_srs_for_testing(&mut rng, degree); + let pp = UniversalKZGParam::::gen_srs_for_testing(&mut rng, degree); let (ck, vk) = pp.trim(degree); let p = random(degree, rng); let comm = UVKZGPCS::::commit(&ck, &p)?; diff --git a/src/provider/non_hiding_zeromorph.rs b/src/provider/non_hiding_zeromorph.rs index 7f9de2e73..d0d30b745 100644 --- a/src/provider/non_hiding_zeromorph.rs +++ b/src/provider/non_hiding_zeromorph.rs @@ -6,8 +6,8 @@ use crate::{ errors::{NovaError, PCSError}, provider::{ non_hiding_kzg::{ - UVKZGCommitment, UVKZGEvaluation, UVKZGPoly, UVKZGProof, UVKZGProverKey, UVKZGVerifierKey, - UVUniversalKZGParam, UVKZGPCS, + KZGProverKey, KZGVerifierKey, UVKZGCommitment, UVKZGEvaluation, UVKZGPoly, UVKZGProof, + UniversalKZGParam, UVKZGPCS, }, traits::DlogGroup, }, @@ -41,8 +41,8 @@ use crate::provider::kzg_commitment::KZGCommitmentEngine; deserialize = "E::G1Affine: Deserialize<'de>" ))] pub struct ZMProverKey { - commit_pp: UVKZGProverKey, - open_pp: UVKZGProverKey, + commit_pp: KZGProverKey, + open_pp: KZGProverKey, } /// `ZMVerifierKey` is used to check evaluation proofs for a given @@ -54,7 +54,7 @@ pub struct ZMProverKey { deserialize = "E::G1Affine: Deserialize<'de>, E::G2Affine: Deserialize<'de>" ))] pub struct ZMVerifierKey { - vp: UVKZGVerifierKey, + vp: KZGVerifierKey, #[abomonate_with([u64; 16])] // this is a hack; we just assume the size of the element. s_offset_h: E::G2Affine, } @@ -70,14 +70,14 @@ pub struct ZMVerifierKey { // TODO: important, we need a better way to handle that the commitment key should be 2^max_degree sized, // see the runtime error in commit() below pub fn trim( - params: &UVUniversalKZGParam, + params: &UniversalKZGParam, max_degree: usize, ) -> (ZMProverKey, ZMVerifierKey) { let (commit_pp, vp) = params.trim(max_degree); let offset = params.powers_of_g.len() - max_degree; let open_pp = { let offset_powers_of_g1 = params.powers_of_g[offset..].to_vec(); - UVKZGProverKey { + KZGProverKey { powers_of_g: offset_powers_of_g1, } }; @@ -474,12 +474,12 @@ where type EvaluationArgument = ZMProof; - fn setup(ck: &UVUniversalKZGParam) -> (Self::ProverKey, Self::VerifierKey) { + fn setup(ck: &UniversalKZGParam) -> (Self::ProverKey, Self::VerifierKey) { trim(ck, ck.length() - 1) } fn prove( - _ck: &UVUniversalKZGParam, + _ck: &UniversalKZGParam, pk: &Self::ProverKey, transcript: &mut NE::TE, comm: &Commitment, @@ -529,7 +529,7 @@ mod test { use crate::{ provider::{ keccak::Keccak256Transcript, - non_hiding_kzg::{UVKZGPoly, UVUniversalKZGParam}, + non_hiding_kzg::{UVKZGPoly, UniversalKZGParam}, non_hiding_zeromorph::{ batched_lifted_degree_quotient, eval_and_quotient_scalars, trim, ZMEvaluation, ZMPCS, }, @@ -549,7 +549,7 @@ mod test { let max_vars = 16; let mut rng = thread_rng(); let max_poly_size = 1 << (max_vars + 1); - let universal_setup = UVUniversalKZGParam::::gen_srs_for_testing(&mut rng, max_poly_size); + let universal_setup = UniversalKZGParam::::gen_srs_for_testing(&mut rng, max_poly_size); for num_vars in 3..max_vars { // Setup diff --git a/src/provider/pasta.rs b/src/provider/pasta.rs index 0adfe4db3..8be7c95ad 100644 --- a/src/provider/pasta.rs +++ b/src/provider/pasta.rs @@ -1,8 +1,8 @@ //! This module implements the Nova traits for `pallas::Point`, `pallas::Scalar`, `vesta::Point`, `vesta::Scalar`. use crate::{ provider::{ - msm::cpu_best_msm, traits::{CompressedGroup, DlogGroup}, + util::msm::cpu_best_msm, }, traits::{Group, PrimeFieldExt, TranscriptReprTrait}, }; @@ -187,6 +187,29 @@ macro_rules! impl_traits { self.to_repr().to_vec() } } + + impl TranscriptReprTrait for $name::Affine { + fn to_transcript_bytes(&self) -> Vec { + let (x, y, is_infinity_byte) = { + let coordinates = self.coordinates(); + if coordinates.is_some().unwrap_u8() == 1 { + ( + *coordinates.unwrap().x(), + *coordinates.unwrap().y(), + u8::from(false), + ) + } else { + ($name::Base::zero(), $name::Base::zero(), u8::from(true)) + } + }; + + x.to_repr() + .into_iter() + .chain(y.to_repr().into_iter()) + .chain(std::iter::once(is_infinity_byte)) + .collect() + } + } }; } diff --git a/src/provider/secp_secq.rs b/src/provider/secp_secq.rs index 39b582096..5be35bd6b 100644 --- a/src/provider/secp_secq.rs +++ b/src/provider/secp_secq.rs @@ -2,8 +2,8 @@ use crate::{ impl_traits, provider::{ - msm::cpu_best_msm, traits::{CompressedGroup, DlogGroup}, + util::msm::cpu_best_msm, }, traits::{Group, PrimeFieldExt, TranscriptReprTrait}, }; diff --git a/src/provider/traits.rs b/src/provider/traits.rs index aca4d2903..22ba4696c 100644 --- a/src/provider/traits.rs +++ b/src/provider/traits.rs @@ -65,7 +65,8 @@ pub trait DlogGroup: + Send + Sync + Serialize - + for<'de> Deserialize<'de>; + + for<'de> Deserialize<'de> + + TranscriptReprTrait; /// A method to compute a multiexponentation fn vartime_multiscalar_mul( @@ -224,5 +225,25 @@ macro_rules! impl_traits { self.to_repr().to_vec() } } + + impl TranscriptReprTrait for $name::Affine { + fn to_transcript_bytes(&self) -> Vec { + let (x, y, is_infinity_byte) = { + let coordinates = self.coordinates(); + if coordinates.is_some().unwrap_u8() == 1 && ($name_curve_affine::identity() != *self) { + let c = coordinates.unwrap(); + (*c.x(), *c.y(), u8::from(false)) + } else { + ($name::Base::zero(), $name::Base::zero(), u8::from(false)) + } + }; + + x.to_repr() + .into_iter() + .chain(y.to_repr().into_iter()) + .chain(std::iter::once(is_infinity_byte)) + .collect() + } + } }; } diff --git a/src/provider/util/mod.rs b/src/provider/util/mod.rs index 43a544123..40a0443fa 100644 --- a/src/provider/util/mod.rs +++ b/src/provider/util/mod.rs @@ -1,2 +1,3 @@ /// Utilities for provider module -pub(crate) mod fb_msm; +pub(in crate::provider) mod fb_msm; +pub(in crate::provider) mod msm; diff --git a/src/provider/msm.rs b/src/provider/util/msm.rs similarity index 100% rename from src/provider/msm.rs rename to src/provider/util/msm.rs diff --git a/src/spartan/mod.rs b/src/spartan/mod.rs index 215ba13d9..1e97b2f5f 100644 --- a/src/spartan/mod.rs +++ b/src/spartan/mod.rs @@ -30,12 +30,9 @@ use rayon::{iter::IntoParallelRefIterator, prelude::*}; // Creates a vector of the first `n` powers of `s`. fn powers(s: &E::Scalar, n: usize) -> Vec { assert!(n >= 1); - let mut powers = Vec::with_capacity(n); - powers.push(E::Scalar::ONE); - for i in 1..n { - powers.push(powers[i - 1] * s); - } - powers + std::iter::successors(Some(E::Scalar::ONE), |&x| Some(x * s)) + .take(n) + .collect() } /// A type that holds a witness to a polynomial evaluation instance