From c09c52f12c75c063c3f5fc22571a91966c8da92d Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 18 Aug 2024 00:19:34 +0200 Subject: [PATCH] feat: implement nova's zk layer (#127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: zk nova layer * chore: clippy + trigger CI * chore: add comment for `new` (generating a zk nova ivc proof) * chore: adding text reference to `sample` * chore: use `debug_assert` instead of `cfg(test)` * improve: pass `poseidon_config` by ref Co-authored-by: Carlos Pérez <37264926+CPerezz@users.noreply.github.com> * improve: pass `z_0` by ref Co-authored-by: Carlos Pérez <37264926+CPerezz@users.noreply.github.com> * improve: pass `r1cs` and `cf_r1cs` by ref Co-authored-by: Carlos Pérez <37264926+CPerezz@users.noreply.github.com> * chore: appropriate docs (2) * chore: pass by ref modifications * improve: use single sponge * fix: remove blinding the cyclefold instance, add verifier checks on the prover provided cyclefold intance * fix: assert that the sampled relaxed r1cs is correct * fix: check length of `u_i.x` --------- Co-authored-by: Carlos Pérez <37264926+CPerezz@users.noreply.github.com> --- folding-schemes/src/arith/r1cs.rs | 104 +++++- folding-schemes/src/folding/nova/mod.rs | 21 +- folding-schemes/src/folding/nova/zk.rs | 419 ++++++++++++++++++++++++ folding-schemes/src/lib.rs | 2 + 4 files changed, 537 insertions(+), 9 deletions(-) create mode 100644 folding-schemes/src/folding/nova/zk.rs diff --git a/folding-schemes/src/arith/r1cs.rs b/folding-schemes/src/arith/r1cs.rs index f510b67..f6414c7 100644 --- a/folding-schemes/src/arith/r1cs.rs +++ b/folding-schemes/src/arith/r1cs.rs @@ -1,10 +1,15 @@ +use crate::commitment::CommitmentScheme; +use crate::folding::nova::{CommittedInstance, Witness}; +use crate::RngCore; +use ark_crypto_primitives::sponge::Absorb; +use ark_ec::{CurveGroup, Group}; use ark_ff::PrimeField; use ark_relations::r1cs::ConstraintSystem; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::rand::Rng; use super::Arith; -use crate::utils::vec::{hadamard, mat_vec_mul, vec_add, vec_scalar_mul, SparseMatrix}; +use crate::utils::vec::{hadamard, mat_vec_mul, vec_add, vec_scalar_mul, vec_sub, SparseMatrix}; use crate::Error; #[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] @@ -92,6 +97,84 @@ impl RelaxedR1CS { Ok(()) } + + // Computes the E term, given A, B, C, z, u + fn compute_E( + A: &SparseMatrix, + B: &SparseMatrix, + C: &SparseMatrix, + z: &[F], + u: &F, + ) -> Result, Error> { + let Az = mat_vec_mul(A, z)?; + let Bz = mat_vec_mul(B, z)?; + let AzBz = hadamard(&Az, &Bz)?; + + let Cz = mat_vec_mul(C, z)?; + let uCz = vec_scalar_mul(&Cz, u); + vec_sub(&AzBz, &uCz) + } + + pub fn check_sampled_relaxed_r1cs(&self, u: F, E: &[F], z: &[F]) -> bool { + let sampled = RelaxedR1CS { + l: self.l, + A: self.A.clone(), + B: self.B.clone(), + C: self.C.clone(), + u, + E: E.to_vec(), + }; + sampled.check_relation(z).is_ok() + } + + // Implements sampling a (committed) RelaxedR1CS + // See construction 5 in https://eprint.iacr.org/2023/573.pdf + pub fn sample( + &self, + params: &CS::ProverParams, + mut rng: impl RngCore, + ) -> Result<(CommittedInstance, Witness), Error> + where + C: CurveGroup, + C: CurveGroup, + ::ScalarField: Absorb, + CS: CommitmentScheme, + { + let u = C::ScalarField::rand(&mut rng); + let rE = C::ScalarField::rand(&mut rng); + let rW = C::ScalarField::rand(&mut rng); + + let W = (0..self.A.n_cols - self.l - 1) + .map(|_| F::rand(&mut rng)) + .collect(); + let x = (0..self.l).map(|_| F::rand(&mut rng)).collect::>(); + let mut z = vec![u]; + z.extend(&x); + z.extend(&W); + + let E = RelaxedR1CS::compute_E(&self.A, &self.B, &self.C, &z, &u)?; + + debug_assert!( + z.len() == self.A.n_cols, + "Length of z is {}, while A has {} columns.", + z.len(), + self.A.n_cols + ); + + debug_assert!( + self.check_sampled_relaxed_r1cs(u, &E, &z), + "Sampled a non satisfiable relaxed R1CS, sampled u: {}, computed E: {:?}", + u, + E + ); + + let witness = Witness { E, rE, W, rW }; + let mut cm_witness = witness.commit::(params, x)?; + + // witness.commit() sets u to 1, we set it to the sampled u value + cm_witness.u = u; + Ok((cm_witness, witness)) + } } /// extracts arkworks ConstraintSystem matrices into crate::utils::vec::SparseMatrix format as R1CS @@ -138,9 +221,24 @@ pub fn extract_w_x(cs: &ConstraintSystem) -> (Vec, Vec) #[cfg(test)] pub mod tests { use super::*; - use crate::utils::vec::tests::{to_F_matrix, to_F_vec}; + use crate::{ + commitment::pedersen::Pedersen, + utils::vec::tests::{to_F_matrix, to_F_vec}, + }; - use ark_pallas::Fr; + use ark_pallas::{Fr, Projective}; + + #[test] + pub fn sample_relaxed_r1cs() { + let rng = rand::rngs::OsRng; + let r1cs = get_test_r1cs::(); + let (prover_params, _) = Pedersen::::setup(rng, r1cs.A.n_rows).unwrap(); + + let relaxed_r1cs = r1cs.relax(); + let sampled = + relaxed_r1cs.sample::>(&prover_params, rng); + assert!(sampled.is_ok()); + } pub fn get_test_r1cs() -> R1CS { // R1CS for: x^3 + x + 5 = y (example from article diff --git a/folding-schemes/src/folding/nova/mod.rs b/folding-schemes/src/folding/nova/mod.rs index 0ae277c..961a534 100644 --- a/folding-schemes/src/folding/nova/mod.rs +++ b/folding-schemes/src/folding/nova/mod.rs @@ -37,7 +37,7 @@ pub mod decider_eth_circuit; pub mod nifs; pub mod serialize; pub mod traits; - +pub mod zk; use circuits::{AugmentedFCircuit, ChallengeGadget}; use nifs::NIFS; use traits::NovaR1CS; @@ -957,25 +957,33 @@ pub mod tests { test_ivc_opt::, Pedersen, false>( poseidon_config.clone(), F_circuit, + 3, ); + test_ivc_opt::, Pedersen, true>( poseidon_config.clone(), F_circuit, + 3, ); // run the test using KZG for the commitments on the main curve, and Pedersen for the // commitments on the secondary curve - test_ivc_opt::, Pedersen, false>(poseidon_config, F_circuit); + test_ivc_opt::, Pedersen, false>(poseidon_config, F_circuit, 3); } // test_ivc allowing to choose the CommitmentSchemes - fn test_ivc_opt< + #[allow(clippy::type_complexity)] + pub(crate) fn test_ivc_opt< CS1: CommitmentScheme, CS2: CommitmentScheme, const H: bool, >( poseidon_config: PoseidonConfig, F_circuit: CubicFCircuit, + num_steps: usize, + ) -> ( + Vec, + Nova, CS1, CS2, H>, ) { let mut rng = ark_std::test_rng(); @@ -1009,7 +1017,6 @@ pub mod tests { ) .unwrap(); - let num_steps: usize = 3; for _ in 0..num_steps { nova.prove_step(&mut rng, vec![], None).unwrap(); } @@ -1018,13 +1025,15 @@ pub mod tests { let (running_instance, incoming_instance, cyclefold_instance) = nova.instances(); Nova::, CS1, CS2, H>::verify( nova_params.1, // Nova's verifier params - z_0, - nova.z_i, + z_0.clone(), + nova.z_i.clone(), nova.i, running_instance, incoming_instance, cyclefold_instance, ) .unwrap(); + + (z_0, nova) } } diff --git a/folding-schemes/src/folding/nova/zk.rs b/folding-schemes/src/folding/nova/zk.rs new file mode 100644 index 0000000..3ec198e --- /dev/null +++ b/folding-schemes/src/folding/nova/zk.rs @@ -0,0 +1,419 @@ +// Implements nova's zero-knowledge layer, as described in https://eprint.iacr.org/2023/573.pdf +use crate::folding::nova::traits::NovaR1CS; +use ark_crypto_primitives::sponge::CryptographicSponge; +use ark_ff::{BigInteger, PrimeField}; +use ark_std::{One, Zero}; + +use crate::{ + arith::r1cs::{RelaxedR1CS, R1CS}, + RngCore, +}; +use ark_crypto_primitives::sponge::{ + poseidon::{PoseidonConfig, PoseidonSponge}, + Absorb, +}; +use ark_ec::{CurveGroup, Group}; +use ark_r1cs_std::{ + groups::{CurveVar, GroupOpsBounds}, + ToConstraintFieldGadget, +}; + +use crate::{commitment::CommitmentScheme, folding::circuits::CF2, frontend::FCircuit, Error}; + +use super::{circuits::ChallengeGadget, nifs::NIFS, CommittedInstance, Nova, Witness}; + +// We use the same definition of a folding proof as in https://eprint.iacr.org/2023/969.pdf +// It consists in the commitment to the T term +pub struct FoldingProof { + cmT: C, +} + +pub struct RandomizedIVCProof { + pub U_i: CommittedInstance, + pub u_i: CommittedInstance, + pub U_r: CommittedInstance, + pub pi: FoldingProof, + pub pi_prime: FoldingProof, + pub W_i_prime: Witness, + pub cf_U_i: CommittedInstance, + pub cf_W_i: Witness, +} + +impl RandomizedIVCProof +where + ::ScalarField: Absorb, + ::BaseField: PrimeField, +{ + /// Computes challenge required before folding instances + fn get_folding_challenge( + sponge: &mut PoseidonSponge, + pp_hash: C1::ScalarField, + U_i: CommittedInstance, + u_i: CommittedInstance, + cmT: C1, + ) -> Result { + let r_bits = ChallengeGadget::::get_challenge_native(sponge, pp_hash, U_i, u_i, cmT); + C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)).ok_or(Error::OutOfBounds) + } + + /// Compute a zero-knowledge proof of a Nova IVC proof + /// It implements the prover of appendix D.4.in https://eprint.iacr.org/2023/573.pdf + /// For further details on why folding is hiding, see lemma 9 + pub fn new< + GC1: CurveVar> + ToConstraintFieldGadget>, + GC2: CurveVar>, + FC: FCircuit, + CS1: CommitmentScheme, + CS2: CommitmentScheme, + >( + nova: &Nova, + mut rng: impl RngCore, + ) -> Result, Error> + where + ::ScalarField: Absorb, + ::ScalarField: Absorb, + ::ScalarField: PrimeField, + ::BaseField: PrimeField, + ::BaseField: Absorb, + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, + GC2: ToConstraintFieldGadget<::BaseField>, + C1: CurveGroup, + { + let mut challenges_sponge = PoseidonSponge::::new(&nova.poseidon_config); + + // I. Compute proof for 'regular' instances + // 1. Fold the instance-witness pairs (U_i, W_i) with (u_i, w_i) + // a. Compute T + let (T, cmT) = NIFS::::compute_cmT( + &nova.cs_pp, + &nova.r1cs, + &nova.w_i, + &nova.u_i, + &nova.W_i, + &nova.U_i, + )?; + + // b. Compute folding challenge + let r = RandomizedIVCProof::::get_folding_challenge( + &mut challenges_sponge, + nova.pp_hash, + nova.U_i.clone(), + nova.u_i.clone(), + cmT, + )?; + + // c. Compute fold + let (W_f, U_f) = NIFS::::fold_instances( + r, &nova.w_i, &nova.u_i, &nova.W_i, &nova.U_i, &T, cmT, + )?; + + // d. Store folding proof + let pi = FoldingProof { cmT }; + + // 2. Sample a satisfying relaxed R1CS instance-witness pair (U_r, W_r) + let relaxed_instance = nova.r1cs.clone().relax(); + let (U_r, W_r) = relaxed_instance.sample::(&nova.cs_pp, &mut rng)?; + + // 3. Fold the instance-witness pair (U_f, W_f) with (U_r, W_r) + // a. Compute T + let (T_i_prime, cmT_i_prime) = + NIFS::::compute_cmT(&nova.cs_pp, &nova.r1cs, &W_f, &U_f, &W_r, &U_r)?; + + // b. Compute folding challenge + let r_2 = RandomizedIVCProof::::get_folding_challenge( + &mut challenges_sponge, + nova.pp_hash, + U_f.clone(), + U_r.clone(), + cmT_i_prime, + )?; + + // c. Compute fold + let (W_i_prime, _) = NIFS::::fold_instances( + r_2, + &W_f, + &U_f, + &W_r, + &U_r, + &T_i_prime, + cmT_i_prime, + )?; + + // d. Store folding proof + let pi_prime = FoldingProof { cmT: cmT_i_prime }; + + Ok(RandomizedIVCProof { + U_i: nova.U_i.clone(), + u_i: nova.u_i.clone(), + U_r, + pi, + pi_prime, + W_i_prime, + cf_U_i: nova.cf_U_i.clone(), + cf_W_i: nova.cf_W_i.clone(), + }) + } + + /// Verify a zero-knowledge proof of a Nova IVC proof + /// It implements the verifier of appendix D.4. in https://eprint.iacr.org/2023/573.pdf + #[allow(clippy::too_many_arguments)] + pub fn verify< + CS1: CommitmentScheme, + GC2: CurveVar>, + CS2: CommitmentScheme, + >( + r1cs: &R1CS, + cf_r1cs: &R1CS, + pp_hash: C1::ScalarField, + poseidon_config: &PoseidonConfig, + i: C1::ScalarField, + z_0: Vec, + z_i: Vec, + proof: &RandomizedIVCProof, + ) -> Result<(), Error> + where + ::ScalarField: Absorb, + ::ScalarField: Absorb, + ::BaseField: PrimeField, + ::BaseField: Absorb, + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, + GC2: ToConstraintFieldGadget<::BaseField>, + C1: CurveGroup, + { + // Handles case where i=0 + if i == C1::ScalarField::zero() { + if z_0 == z_i { + return Ok(()); + } else { + return Err(Error::zkIVCVerificationFail); + } + } + + // 1. Check that u_i.x is correct - including the cyclefold running instance + // a. Check length + if proof.u_i.x.len() != 2 { + return Err(Error::IVCVerificationFail); + } + + // b. Check computed hashes are correct + let mut sponge = PoseidonSponge::::new(poseidon_config); + let expected_u_i_x = proof.U_i.hash(&sponge, pp_hash, i, z_0, z_i); + if expected_u_i_x != proof.u_i.x[0] { + return Err(Error::zkIVCVerificationFail); + } + + let expected_cf_u_i_x = proof.cf_U_i.hash_cyclefold(&sponge, pp_hash); + if expected_cf_u_i_x != proof.u_i.x[1] { + return Err(Error::IVCVerificationFail); + } + + // 2. Check that u_i values are correct + if !proof.u_i.cmE.is_zero() || proof.u_i.u != C1::ScalarField::one() { + return Err(Error::zkIVCVerificationFail); + } + + // 3. Obtain the U_f folded instance + // a. Compute folding challenge + let r = RandomizedIVCProof::::get_folding_challenge( + &mut sponge, + pp_hash, + proof.U_i.clone(), + proof.u_i.clone(), + proof.pi.cmT, + )?; + + // b. Get the U_f instance + let U_f = NIFS::::fold_committed_instance( + r, + &proof.u_i, + &proof.U_i, + &proof.pi.cmT, + ); + + // 4. Obtain the U^{\prime}_i folded instance + // a. Compute folding challenge + let r_2 = RandomizedIVCProof::::get_folding_challenge( + &mut sponge, + pp_hash, + U_f.clone(), + proof.U_r.clone(), + proof.pi_prime.cmT, + )?; + + // b. Compute fold + let U_i_prime = NIFS::::fold_committed_instance( + r_2, + &U_f, + &proof.U_r, + &proof.pi_prime.cmT, + ); + + // 5. Check that W^{\prime}_i is a satisfying witness + let mut z = vec![U_i_prime.u]; + z.extend(&U_i_prime.x); + z.extend(&proof.W_i_prime.W); + let relaxed_r1cs = RelaxedR1CS { + l: r1cs.l, + A: r1cs.A.clone(), + B: r1cs.B.clone(), + C: r1cs.C.clone(), + u: U_i_prime.u, + E: proof.W_i_prime.E.clone(), + }; + relaxed_r1cs.check_relation(&z)?; + + // 6. Check that the cyclefold instance-witness pair satisfies the cyclefold relaxed r1cs + cf_r1cs.check_relaxed_instance_relation(&proof.cf_W_i, &proof.cf_U_i)?; + + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::commitment::pedersen::Pedersen; + use crate::folding::nova::tests::test_ivc_opt; + use crate::frontend::tests::CubicFCircuit; + use crate::transcript::poseidon::poseidon_canonical_config; + use ark_bn254::{Fr, G1Projective as Projective}; + use ark_grumpkin::{constraints::GVar as GVar2, Projective as Projective2}; + use rand::rngs::OsRng; + + // Tests zk proof generation and verification for a valid nova IVC proof + #[test] + fn test_zk_nova_ivc() { + let mut rng = OsRng; + let poseidon_config = poseidon_canonical_config::(); + let F_circuit = CubicFCircuit::::new(()).unwrap(); + let (_, nova) = test_ivc_opt::, Pedersen, true>( + poseidon_config.clone(), + F_circuit, + 3, + ); + + let proof = RandomizedIVCProof::new(&nova, &mut rng).unwrap(); + let verify = RandomizedIVCProof::verify::< + Pedersen, + GVar2, + Pedersen, + >( + &nova.r1cs, + &nova.cf_r1cs, + nova.pp_hash, + &nova.poseidon_config, + nova.i, + nova.z_0, + nova.z_i, + &proof, + ); + assert!(verify.is_ok()); + } + + #[test] + fn test_zk_nova_when_i_is_zero() { + let mut rng = OsRng; + let poseidon_config = poseidon_canonical_config::(); + let F_circuit = CubicFCircuit::::new(()).unwrap(); + let (_, nova) = test_ivc_opt::, Pedersen, true>( + poseidon_config.clone(), + F_circuit, + 0, + ); + + let proof = RandomizedIVCProof::new(&nova, &mut rng).unwrap(); + let verify = RandomizedIVCProof::verify::< + Pedersen, + GVar2, + Pedersen, + >( + &nova.r1cs, + &nova.cf_r1cs, + nova.pp_hash, + &nova.poseidon_config, + nova.i, + nova.z_0, + nova.z_i, + &proof, + ); + assert!(verify.is_ok()); + } + + #[test] + fn test_zk_nova_verification_fails_with_wrong_running_instance() { + let mut rng = OsRng; + let poseidon_config = poseidon_canonical_config::(); + let F_circuit = CubicFCircuit::::new(()).unwrap(); + let (_, nova) = test_ivc_opt::, Pedersen, true>( + poseidon_config.clone(), + F_circuit, + 3, + ); + let (sampled_committed_instance, _) = nova + .r1cs + .clone() + .relax() + .sample::>(&nova.cs_pp, rng) + .unwrap(); + + // proof verification fails with incorrect running instance + let mut nova_with_incorrect_running_instance = nova.clone(); + nova_with_incorrect_running_instance.U_i = sampled_committed_instance; + let incorrect_proof = + RandomizedIVCProof::new(&nova_with_incorrect_running_instance, &mut rng).unwrap(); + let verify = RandomizedIVCProof::verify::< + Pedersen, + GVar2, + Pedersen, + >( + &nova_with_incorrect_running_instance.r1cs, + &nova_with_incorrect_running_instance.cf_r1cs, + nova_with_incorrect_running_instance.pp_hash, + &nova_with_incorrect_running_instance.poseidon_config, + nova_with_incorrect_running_instance.i, + nova_with_incorrect_running_instance.z_0, + nova_with_incorrect_running_instance.z_i, + &incorrect_proof, + ); + assert!(verify.is_err()); + } + + #[test] + fn test_zk_nova_verification_fails_with_wrong_running_witness() { + let mut rng = OsRng; + let poseidon_config = poseidon_canonical_config::(); + let F_circuit = CubicFCircuit::::new(()).unwrap(); + let (_, nova) = test_ivc_opt::, Pedersen, true>( + poseidon_config.clone(), + F_circuit, + 3, + ); + let (_, sampled_committed_witness) = nova + .r1cs + .clone() + .relax() + .sample::>(&nova.cs_pp, rng) + .unwrap(); + + // proof generation fails with incorrect running witness + let mut nova_with_incorrect_running_witness = nova.clone(); + nova_with_incorrect_running_witness.W_i = sampled_committed_witness; + let incorrect_proof = + RandomizedIVCProof::new(&nova_with_incorrect_running_witness, &mut rng).unwrap(); + let verify = RandomizedIVCProof::verify::< + Pedersen, + GVar2, + Pedersen, + >( + &nova_with_incorrect_running_witness.r1cs, + &nova_with_incorrect_running_witness.cf_r1cs, + nova_with_incorrect_running_witness.pp_hash, + &nova_with_incorrect_running_witness.poseidon_config, + nova_with_incorrect_running_witness.i, + nova_with_incorrect_running_witness.z_0, + nova_with_incorrect_running_witness.z_i, + &incorrect_proof, + ); + assert!(verify.is_err()); + } +} diff --git a/folding-schemes/src/lib.rs b/folding-schemes/src/lib.rs index 4e2764d..9082169 100644 --- a/folding-schemes/src/lib.rs +++ b/folding-schemes/src/lib.rs @@ -41,6 +41,8 @@ pub enum Error { SNARKVerificationFail, #[error("IVC verification failed")] IVCVerificationFail, + #[error("zkIVC verification failed")] + zkIVCVerificationFail, #[error("R1CS instance is expected to not be relaxed")] R1CSUnrelaxedFail, #[error("Could not find the inner ConstraintSystem")]