Browse Source

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>
main
Pierre 4 months ago
committed by GitHub
parent
commit
c09c52f12c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
4 changed files with 537 additions and 9 deletions
  1. +101
    -3
      folding-schemes/src/arith/r1cs.rs
  2. +15
    -6
      folding-schemes/src/folding/nova/mod.rs
  3. +419
    -0
      folding-schemes/src/folding/nova/zk.rs
  4. +2
    -0
      folding-schemes/src/lib.rs

+ 101
- 3
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<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 @@ 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::<Fr>();
let (prover_params, _) = Pedersen::<Projective>::setup(rng, r1cs.A.n_rows).unwrap();
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

+ 15
- 6
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<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 @@ 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::<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)
}
}

+ 419
- 0
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());
}
}

+ 2
- 0
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")]

Loading…
Cancel
Save