arnaucube
arnaucube/sonobePublic Self-hosted repository
feat: implement nova's zk layer (#127)

* 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>
Pierre pdaixmoreux@gmail.com
Sun, 18 Aug 2024 00:19:34 +0200
commit

c09c52f12c75c063c3f5fc22571a91966c8da92d

parent

5ec9c2c57656981ced94b94ae964bddf0a539430

M folding-schemes/src/arith/r1cs.rs → 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 @@ }

Ok(()) } + + // Computes the E term, given A, B, C, z, u + fn compute_E( + A: &SparseMatrix<F>, + B: &SparseMatrix<F>, + C: &SparseMatrix<F>, + z: &[F], + u: &F, + ) -> Result<Vec<F>, 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<C, CS>( + &self, + params: &CS::ProverParams, + mut rng: impl RngCore, + ) -> Result<(CommittedInstance<C>, Witness<C>), Error> + where + C: CurveGroup, + C: CurveGroup<ScalarField = F>, + <C as Group>::ScalarField: Absorb, + CS: CommitmentScheme<C, true>, + { + 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::<Vec<F>>(); + 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::<CS, true>(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 @@

#[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, Projective}; + + #[test] + pub fn sample_relaxed_r1cs() { + let rng = rand::rngs::OsRng; + let r1cs = get_test_r1cs::<Fr>(); + let (prover_params, _) = Pedersen::<Projective>::setup(rng, r1cs.A.n_rows).unwrap(); - use ark_pallas::Fr; + let relaxed_r1cs = r1cs.relax(); + let sampled = + relaxed_r1cs.sample::<Projective, Pedersen<Projective, true>>(&prover_params, rng); + assert!(sampled.is_ok()); + } pub fn get_test_r1cs<F: PrimeField>() -> R1CS<F> { // R1CS for: x^3 + x + 5 = y (example from article
M folding-schemes/src/folding/nova/mod.rs → 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 @@ // run the test using Pedersen commitments on both sides of the curve cycle

test_ivc_opt::<Pedersen<Projective>, Pedersen<Projective2>, false>( poseidon_config.clone(), F_circuit, + 3, ); + test_ivc_opt::<Pedersen<Projective, true>, Pedersen<Projective2, true>, 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::<KZG<Bn254>, Pedersen<Projective2>, false>(poseidon_config, F_circuit); + test_ivc_opt::<KZG<Bn254>, Pedersen<Projective2>, 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<Projective, H>, CS2: CommitmentScheme<Projective2, H>, const H: bool, >( poseidon_config: PoseidonConfig<Fr>, F_circuit: CubicFCircuit<Fr>, + num_steps: usize, + ) -> ( + Vec<Fr>, + Nova<Projective, GVar, Projective2, GVar2, CubicFCircuit<Fr>, CS1, CS2, H>, ) { let mut rng = ark_std::test_rng();

@@ -1009,7 +1017,6 @@ z_0.clone(),

) .unwrap(); - let num_steps: usize = 3; for _ in 0..num_steps { nova.prove_step(&mut rng, vec![], None).unwrap(); }

@@ -1018,13 +1025,15 @@

let (running_instance, incoming_instance, cyclefold_instance) = nova.instances(); Nova::<Projective, GVar, Projective2, GVar2, CubicFCircuit<Fr>, 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) } }
A 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<C: CurveGroup> { + cmT: C, +} + +pub struct RandomizedIVCProof<C1: CurveGroup, C2: CurveGroup> { + pub U_i: CommittedInstance<C1>, + pub u_i: CommittedInstance<C1>, + pub U_r: CommittedInstance<C1>, + pub pi: FoldingProof<C1>, + pub pi_prime: FoldingProof<C1>, + pub W_i_prime: Witness<C1>, + pub cf_U_i: CommittedInstance<C2>, + pub cf_W_i: Witness<C2>, +} + +impl<C1: CurveGroup, C2: CurveGroup> RandomizedIVCProof<C1, C2> +where + <C1 as Group>::ScalarField: Absorb, + <C1 as CurveGroup>::BaseField: PrimeField, +{ + /// Computes challenge required before folding instances + fn get_folding_challenge( + sponge: &mut PoseidonSponge<C1::ScalarField>, + pp_hash: C1::ScalarField, + U_i: CommittedInstance<C1>, + u_i: CommittedInstance<C1>, + cmT: C1, + ) -> Result<C1::ScalarField, Error> { + let r_bits = ChallengeGadget::<C1>::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<C1, CF2<C1>> + ToConstraintFieldGadget<CF2<C1>>, + GC2: CurveVar<C2, CF2<C2>>, + FC: FCircuit<C1::ScalarField>, + CS1: CommitmentScheme<C1, true>, + CS2: CommitmentScheme<C2, true>, + >( + nova: &Nova<C1, GC1, C2, GC2, FC, CS1, CS2, true>, + mut rng: impl RngCore, + ) -> Result<RandomizedIVCProof<C1, C2>, Error> + where + <C1 as Group>::ScalarField: Absorb, + <C2 as Group>::ScalarField: Absorb, + <C2 as Group>::ScalarField: PrimeField, + <C2 as CurveGroup>::BaseField: PrimeField, + <C2 as CurveGroup>::BaseField: Absorb, + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, + GC2: ToConstraintFieldGadget<<C2 as CurveGroup>::BaseField>, + C1: CurveGroup<BaseField = C2::ScalarField, ScalarField = C2::BaseField>, + { + let mut challenges_sponge = PoseidonSponge::<C1::ScalarField>::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::<C1, CS1, true>::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::<C1, C2>::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::<C1, CS1, true>::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::<C1, CS1>(&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::<C1, CS1, true>::compute_cmT(&nova.cs_pp, &nova.r1cs, &W_f, &U_f, &W_r, &U_r)?; + + // b. Compute folding challenge + let r_2 = RandomizedIVCProof::<C1, C2>::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::<C1, CS1, true>::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<C1, true>, + GC2: CurveVar<C2, CF2<C2>>, + CS2: CommitmentScheme<C2, true>, + >( + r1cs: &R1CS<C1::ScalarField>, + cf_r1cs: &R1CS<C2::ScalarField>, + pp_hash: C1::ScalarField, + poseidon_config: &PoseidonConfig<C1::ScalarField>, + i: C1::ScalarField, + z_0: Vec<C1::ScalarField>, + z_i: Vec<C1::ScalarField>, + proof: &RandomizedIVCProof<C1, C2>, + ) -> Result<(), Error> + where + <C1 as Group>::ScalarField: Absorb, + <C2 as Group>::ScalarField: Absorb, + <C2 as CurveGroup>::BaseField: PrimeField, + <C2 as CurveGroup>::BaseField: Absorb, + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, + GC2: ToConstraintFieldGadget<<C2 as CurveGroup>::BaseField>, + C1: CurveGroup<BaseField = C2::ScalarField, ScalarField = C2::BaseField>, + { + // 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::<C1::ScalarField>::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::<C1, C2>::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::<C1, CS1, true>::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::<C1, C2>::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::<C1, CS1, true>::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::<Fr>(); + let F_circuit = CubicFCircuit::<Fr>::new(()).unwrap(); + let (_, nova) = test_ivc_opt::<Pedersen<Projective, true>, Pedersen<Projective2, true>, true>( + poseidon_config.clone(), + F_circuit, + 3, + ); + + let proof = RandomizedIVCProof::new(&nova, &mut rng).unwrap(); + let verify = RandomizedIVCProof::verify::< + Pedersen<Projective, true>, + GVar2, + Pedersen<Projective2, true>, + >( + &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::<Fr>(); + let F_circuit = CubicFCircuit::<Fr>::new(()).unwrap(); + let (_, nova) = test_ivc_opt::<Pedersen<Projective, true>, Pedersen<Projective2, true>, true>( + poseidon_config.clone(), + F_circuit, + 0, + ); + + let proof = RandomizedIVCProof::new(&nova, &mut rng).unwrap(); + let verify = RandomizedIVCProof::verify::< + Pedersen<Projective, true>, + GVar2, + Pedersen<Projective2, true>, + >( + &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::<Fr>(); + let F_circuit = CubicFCircuit::<Fr>::new(()).unwrap(); + let (_, nova) = test_ivc_opt::<Pedersen<Projective, true>, Pedersen<Projective2, true>, true>( + poseidon_config.clone(), + F_circuit, + 3, + ); + let (sampled_committed_instance, _) = nova + .r1cs + .clone() + .relax() + .sample::<Projective, Pedersen<Projective, true>>(&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<Projective, true>, + GVar2, + Pedersen<Projective2, true>, + >( + &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::<Fr>(); + let F_circuit = CubicFCircuit::<Fr>::new(()).unwrap(); + let (_, nova) = test_ivc_opt::<Pedersen<Projective, true>, Pedersen<Projective2, true>, true>( + poseidon_config.clone(), + F_circuit, + 3, + ); + let (_, sampled_committed_witness) = nova + .r1cs + .clone() + .relax() + .sample::<Projective, Pedersen<Projective, true>>(&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<Projective, true>, + GVar2, + Pedersen<Projective2, true>, + >( + &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()); + } +}
M folding-schemes/src/lib.rs → folding-schemes/src/lib.rs

@@ -41,6 +41,8 @@ #[error("SNARK verification failed")]

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")]