/// Implements the scheme described in [Nova](https://eprint.iacr.org/2021/370.pdf) and /// [CycleFold](https://eprint.iacr.org/2023/1192.pdf). use ark_crypto_primitives::sponge::{ poseidon::{PoseidonConfig, PoseidonSponge}, Absorb, CryptographicSponge, }; use ark_ec::{AffineRepr, CurveGroup, Group}; use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::fmt::Debug; use ark_std::rand::RngCore; use ark_std::{One, UniformRand, Zero}; use core::marker::PhantomData; use crate::commitment::CommitmentScheme; use crate::folding::circuits::cyclefold::{fold_cyclefold_circuit, CycleFoldCircuit}; use crate::folding::circuits::CF2; use crate::frontend::FCircuit; use crate::transcript::{AbsorbNonNative, Transcript}; use crate::utils::vec::is_zero_vec; use crate::Error; use crate::FoldingScheme; use crate::{ arith::r1cs::{extract_r1cs, extract_w_x, R1CS}, utils::{get_cm_coordinates, pp_hash}, }; pub mod circuits; pub mod decider_eth; pub mod decider_eth_circuit; pub mod nifs; pub mod serialize; pub mod traits; use circuits::{AugmentedFCircuit, ChallengeGadget}; use nifs::NIFS; use traits::NovaR1CS; /// Number of points to be folded in the CycleFold circuit, in Nova's case, this is a fixed amount: /// 2 points to be folded. const NOVA_CF_N_POINTS: usize = 2_usize; #[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] pub struct CommittedInstance { pub cmE: C, pub u: C::ScalarField, pub cmW: C, pub x: Vec, } impl CommittedInstance { pub fn dummy(io_len: usize) -> Self { Self { cmE: C::zero(), u: C::ScalarField::zero(), cmW: C::zero(), x: vec![C::ScalarField::zero(); io_len], } } } impl Absorb for CommittedInstance where C::ScalarField: Absorb, { fn to_sponge_bytes(&self, _dest: &mut Vec) { // This is never called unimplemented!() } fn to_sponge_field_elements(&self, dest: &mut Vec) { self.u.to_sponge_field_elements(dest); self.x.to_sponge_field_elements(dest); // We cannot call `to_native_sponge_field_elements(dest)` directly, as // `to_native_sponge_field_elements` needs `F` to be `C::ScalarField`, // but here `F` is a generic `PrimeField`. self.cmE .to_native_sponge_field_elements_as_vec() .to_sponge_field_elements(dest); self.cmW .to_native_sponge_field_elements_as_vec() .to_sponge_field_elements(dest); } } impl AbsorbNonNative for CommittedInstance where ::BaseField: ark_ff::PrimeField + Absorb, { // Compatible with the in-circuit `CycleFoldCommittedInstanceVar::to_native_sponge_field_elements` // in `cyclefold.rs`. fn to_native_sponge_field_elements(&self, dest: &mut Vec) { [self.u].to_native_sponge_field_elements(dest); self.x.to_native_sponge_field_elements(dest); let (cmE_x, cmE_y) = match self.cmE.into_affine().xy() { Some((&x, &y)) => (x, y), None => (C::BaseField::zero(), C::BaseField::zero()), }; let (cmW_x, cmW_y) = match self.cmW.into_affine().xy() { Some((&x, &y)) => (x, y), None => (C::BaseField::zero(), C::BaseField::zero()), }; cmE_x.to_sponge_field_elements(dest); cmE_y.to_sponge_field_elements(dest); cmW_x.to_sponge_field_elements(dest); cmW_y.to_sponge_field_elements(dest); } } impl CommittedInstance where ::ScalarField: Absorb, ::BaseField: ark_ff::PrimeField, { /// hash implements the committed instance hash compatible with the gadget implemented in /// nova/circuits.rs::CommittedInstanceVar.hash. /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and `U_i` is the /// `CommittedInstance`. pub fn hash>( &self, sponge: &T, pp_hash: C::ScalarField, // public params hash i: C::ScalarField, z_0: Vec, z_i: Vec, ) -> C::ScalarField { let mut sponge = sponge.clone(); sponge.absorb(&pp_hash); sponge.absorb(&i); sponge.absorb(&z_0); sponge.absorb(&z_i); sponge.absorb(&self); sponge.squeeze_field_elements(1)[0] } } impl CommittedInstance where ::BaseField: ark_ff::PrimeField + Absorb, { /// hash_cyclefold implements the committed instance hash compatible with the gadget implemented in /// nova/cyclefold.rs::CycleFoldCommittedInstanceVar.hash. /// Returns `H(U_i)`, where `U_i` is the `CommittedInstance` for CycleFold. pub fn hash_cyclefold>( &self, sponge: &T, pp_hash: C::BaseField, // public params hash ) -> C::BaseField { let mut sponge = sponge.clone(); sponge.absorb(&pp_hash); sponge.absorb_nonnative(self); sponge.squeeze_field_elements(1)[0] } } #[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] pub struct Witness { pub E: Vec, pub rE: C::ScalarField, pub W: Vec, pub rW: C::ScalarField, } impl Witness where ::ScalarField: Absorb, { pub fn new(w: Vec, e_len: usize, mut rng: impl RngCore) -> Self { let (rW, rE) = if H { ( C::ScalarField::rand(&mut rng), C::ScalarField::rand(&mut rng), ) } else { (C::ScalarField::zero(), C::ScalarField::zero()) }; Self { E: vec![C::ScalarField::zero(); e_len], rE, W: w, rW, } } pub fn dummy(w_len: usize, e_len: usize) -> Self { let (rW, rE) = (C::ScalarField::zero(), C::ScalarField::zero()); let w = vec![C::ScalarField::zero(); w_len]; Self { E: vec![C::ScalarField::zero(); e_len], rE, W: w, rW, } } pub fn commit, const HC: bool>( &self, params: &CS::ProverParams, x: Vec, ) -> Result, Error> { let mut cmE = C::zero(); if !is_zero_vec::(&self.E) { cmE = CS::commit(params, &self.E, &self.rE)?; } let cmW = CS::commit(params, &self.W, &self.rW)?; Ok(CommittedInstance { cmE, u: C::ScalarField::one(), cmW, x, }) } } #[derive(Debug, Clone)] pub struct PreprocessorParam where C1: CurveGroup, C2: CurveGroup, FC: FCircuit, CS1: CommitmentScheme, CS2: CommitmentScheme, { pub poseidon_config: PoseidonConfig, pub F: FC, // cs params if not provided, will be generated at the preprocess method pub cs_pp: Option, pub cs_vp: Option, pub cf_cs_pp: Option, pub cf_cs_vp: Option, } impl PreprocessorParam where C1: CurveGroup, C2: CurveGroup, FC: FCircuit, CS1: CommitmentScheme, CS2: CommitmentScheme, { pub fn new(poseidon_config: PoseidonConfig, F: FC) -> Self { Self { poseidon_config, F, cs_pp: None, cs_vp: None, cf_cs_pp: None, cf_cs_vp: None, } } } #[derive(Debug, Clone)] pub struct ProverParams where C1: CurveGroup, C2: CurveGroup, CS1: CommitmentScheme, CS2: CommitmentScheme, { pub poseidon_config: PoseidonConfig, pub cs_pp: CS1::ProverParams, pub cf_cs_pp: CS2::ProverParams, } #[derive(Debug, Clone)] pub struct VerifierParams where C1: CurveGroup, C2: CurveGroup, CS1: CommitmentScheme, CS2: CommitmentScheme, { pub poseidon_config: PoseidonConfig, pub r1cs: R1CS, pub cf_r1cs: R1CS, pub cs_vp: CS1::VerifierParams, pub cf_cs_vp: CS2::VerifierParams, } impl VerifierParams where C1: CurveGroup, C2: CurveGroup, CS1: CommitmentScheme, CS2: CommitmentScheme, { /// returns the hash of the public parameters of Nova pub fn pp_hash(&self) -> Result { pp_hash::( &self.r1cs, &self.cf_r1cs, &self.cs_vp, &self.cf_cs_vp, &self.poseidon_config, ) } } /// Implements Nova+CycleFold's IVC, described in [Nova](https://eprint.iacr.org/2021/370.pdf) and /// [CycleFold](https://eprint.iacr.org/2023/1192.pdf), following the FoldingScheme trait /// The `H` const generic specifies whether the homorphic commitment scheme is blinding #[derive(Clone, Debug)] pub struct Nova where C1: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, C2: CurveGroup, GC2: CurveVar>, FC: FCircuit, CS1: CommitmentScheme, CS2: CommitmentScheme, { _gc1: PhantomData, _c2: PhantomData, _gc2: PhantomData, /// R1CS of the Augmented Function circuit pub r1cs: R1CS, /// R1CS of the CycleFold circuit pub cf_r1cs: R1CS, pub poseidon_config: PoseidonConfig, /// CommitmentScheme::ProverParams over C1 pub cs_pp: CS1::ProverParams, /// CycleFold CommitmentScheme::ProverParams, over C2 pub cf_cs_pp: CS2::ProverParams, /// F circuit, the circuit that is being folded pub F: FC, /// public params hash pub pp_hash: C1::ScalarField, pub i: C1::ScalarField, /// initial state pub z_0: Vec, /// current i-th state pub z_i: Vec, /// Nova instances pub w_i: Witness, pub u_i: CommittedInstance, pub W_i: Witness, pub U_i: CommittedInstance, /// CycleFold running instance pub cf_W_i: Witness, pub cf_U_i: CommittedInstance, } impl FoldingScheme for Nova where C1: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, C2: CurveGroup, GC2: CurveVar> + ToConstraintFieldGadget>, FC: FCircuit, CS1: CommitmentScheme, CS2: CommitmentScheme, ::BaseField: PrimeField, ::BaseField: PrimeField, ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { type PreprocessorParam = PreprocessorParam; type ProverParam = ProverParams; type VerifierParam = VerifierParams; type RunningInstance = (CommittedInstance, Witness); type IncomingInstance = (CommittedInstance, Witness); type MultiCommittedInstanceWithWitness = (); type CFInstance = (CommittedInstance, Witness); fn preprocess( mut rng: impl RngCore, prep_param: &Self::PreprocessorParam, ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { let (r1cs, cf_r1cs) = get_r1cs::(&prep_param.poseidon_config, prep_param.F.clone())?; // if cs params exist, use them, if not, generate new ones let cs_pp: CS1::ProverParams; let cs_vp: CS1::VerifierParams; let cf_cs_pp: CS2::ProverParams; let cf_cs_vp: CS2::VerifierParams; if prep_param.cs_pp.is_some() && prep_param.cf_cs_pp.is_some() && prep_param.cs_vp.is_some() && prep_param.cf_cs_vp.is_some() { cs_pp = prep_param.clone().cs_pp.unwrap(); cs_vp = prep_param.clone().cs_vp.unwrap(); cf_cs_pp = prep_param.clone().cf_cs_pp.unwrap(); cf_cs_vp = prep_param.clone().cf_cs_vp.unwrap(); } else { (cs_pp, cs_vp) = CS1::setup(&mut rng, r1cs.A.n_rows)?; (cf_cs_pp, cf_cs_vp) = CS2::setup(&mut rng, cf_r1cs.A.n_rows)?; } let prover_params = ProverParams:: { poseidon_config: prep_param.poseidon_config.clone(), cs_pp: cs_pp.clone(), cf_cs_pp: cf_cs_pp.clone(), }; let verifier_params = VerifierParams:: { poseidon_config: prep_param.poseidon_config.clone(), r1cs, cf_r1cs, cs_vp, cf_cs_vp, }; Ok((prover_params, verifier_params)) } /// Initializes the Nova+CycleFold's IVC for the given parameters and initial state `z_0`. fn init( params: &(Self::ProverParam, Self::VerifierParam), F: FC, z_0: Vec, ) -> Result { let (pp, vp) = params; // prepare the circuit to obtain its R1CS let cs = ConstraintSystem::::new_ref(); let cs2 = ConstraintSystem::::new_ref(); let augmented_F_circuit = AugmentedFCircuit::::empty(&pp.poseidon_config, F.clone()); let cf_circuit = CycleFoldCircuit::::empty(NOVA_CF_N_POINTS); augmented_F_circuit.generate_constraints(cs.clone())?; cs.finalize(); let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; let r1cs = extract_r1cs::(&cs); cf_circuit.generate_constraints(cs2.clone())?; cs2.finalize(); let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; let cf_r1cs = extract_r1cs::(&cs2); // compute the public params hash let pp_hash = vp.pp_hash()?; // setup the dummy instances let (w_dummy, u_dummy) = r1cs.dummy_instance(); let (cf_w_dummy, cf_u_dummy) = cf_r1cs.dummy_instance(); // W_dummy=W_0 is a 'dummy witness', all zeroes, but with the size corresponding to the // R1CS that we're working with. Ok(Self { _gc1: PhantomData, _c2: PhantomData, _gc2: PhantomData, r1cs, cf_r1cs, poseidon_config: pp.poseidon_config.clone(), cs_pp: pp.cs_pp.clone(), cf_cs_pp: pp.cf_cs_pp.clone(), F, pp_hash, i: C1::ScalarField::zero(), z_0: z_0.clone(), z_i: z_0, w_i: w_dummy.clone(), u_i: u_dummy.clone(), W_i: w_dummy, U_i: u_dummy, // cyclefold running instance cf_W_i: cf_w_dummy.clone(), cf_U_i: cf_u_dummy.clone(), }) } /// Implements IVC.P of Nova+CycleFold fn prove_step( &mut self, mut rng: impl RngCore, external_inputs: Vec, // Nova does not support multi-instances folding _other_instances: Option, ) -> Result<(), Error> { // ensure that commitments are blinding if user has specified so. if H && self.i >= C1::ScalarField::one() { let blinding_commitments = if self.i == C1::ScalarField::one() { // blinding values of the running instances are zero at the first iteration vec![self.w_i.rW, self.w_i.rE] } else { vec![self.w_i.rW, self.w_i.rE, self.W_i.rW, self.W_i.rE] }; if blinding_commitments.contains(&C1::ScalarField::zero()) { return Err(Error::IncorrectBlinding( H, format!("{:?}", blinding_commitments), )); } } // `sponge` is for digest computation. let sponge = PoseidonSponge::::new(&self.poseidon_config); // `transcript` is for challenge generation. let mut transcript = sponge.clone(); let augmented_F_circuit: AugmentedFCircuit; // Nova does not support (by design) multi-instances folding if _other_instances.is_some() { return Err(Error::NoMultiInstances); } if self.z_i.len() != self.F.state_len() { return Err(Error::NotSameLength( "z_i.len()".to_string(), self.z_i.len(), "F.state_len()".to_string(), self.F.state_len(), )); } if external_inputs.len() != self.F.external_inputs_len() { return Err(Error::NotSameLength( "F.external_inputs_len()".to_string(), self.F.external_inputs_len(), "external_inputs.len()".to_string(), external_inputs.len(), )); } if self.i > C1::ScalarField::from_le_bytes_mod_order(&usize::MAX.to_le_bytes()) { return Err(Error::MaxStep); } let mut i_bytes: [u8; 8] = [0; 8]; i_bytes.copy_from_slice(&self.i.into_bigint().to_bytes_le()[..8]); let i_usize: usize = usize::from_le_bytes(i_bytes); let z_i1 = self .F .step_native(i_usize, self.z_i.clone(), external_inputs.clone())?; // compute T and cmT for AugmentedFCircuit let (T, cmT) = self.compute_cmT()?; // r_bits is the r used to the RLC of the F' instances let r_bits = ChallengeGadget::::get_challenge_native( &mut transcript, self.pp_hash, self.U_i.clone(), self.u_i.clone(), cmT, ); let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) .ok_or(Error::OutOfBounds)?; let r_Fq = C1::BaseField::from_bigint(BigInteger::from_bits_le(&r_bits)) .ok_or(Error::OutOfBounds)?; // fold Nova instances let (W_i1, U_i1): (Witness, CommittedInstance) = NIFS::::fold_instances( r_Fr, &self.W_i, &self.U_i, &self.w_i, &self.u_i, &T, cmT, )?; // folded instance output (public input, x) // u_{i+1}.x[0] = H(i+1, z_0, z_{i+1}, U_{i+1}) let u_i1_x = U_i1.hash( &sponge, self.pp_hash, self.i + C1::ScalarField::one(), self.z_0.clone(), z_i1.clone(), ); // u_{i+1}.x[1] = H(cf_U_{i+1}) let cf_u_i1_x: C1::ScalarField; if self.i == C1::ScalarField::zero() { cf_u_i1_x = self.cf_U_i.hash_cyclefold(&sponge, self.pp_hash); // base case augmented_F_circuit = AugmentedFCircuit:: { _gc2: PhantomData, poseidon_config: self.poseidon_config.clone(), pp_hash: Some(self.pp_hash), i: Some(C1::ScalarField::zero()), // = i=0 i_usize: Some(0), z_0: Some(self.z_0.clone()), // = z_i z_i: Some(self.z_i.clone()), external_inputs: Some(external_inputs.clone()), u_i_cmW: Some(self.u_i.cmW), // = dummy U_i: Some(self.U_i.clone()), // = dummy U_i1_cmE: Some(U_i1.cmE), U_i1_cmW: Some(U_i1.cmW), cmT: Some(cmT), F: self.F.clone(), x: Some(u_i1_x), cf1_u_i_cmW: None, cf2_u_i_cmW: None, cf_U_i: None, cf1_cmT: None, cf2_cmT: None, cf_x: Some(cf_u_i1_x), }; #[cfg(test)] NIFS::::verify_folded_instance(r_Fr, &self.U_i, &self.u_i, &U_i1, &cmT)?; } else { // CycleFold part: // get the vector used as public inputs 'x' in the CycleFold circuit // cyclefold circuit for cmW let cfW_u_i_x = [ vec![r_Fq], get_cm_coordinates(&self.U_i.cmW), get_cm_coordinates(&self.u_i.cmW), get_cm_coordinates(&U_i1.cmW), ] .concat(); // cyclefold circuit for cmE let cfE_u_i_x = [ vec![r_Fq], get_cm_coordinates(&self.U_i.cmE), get_cm_coordinates(&cmT), get_cm_coordinates(&U_i1.cmE), ] .concat(); let cfW_circuit = CycleFoldCircuit:: { _gc: PhantomData, n_points: NOVA_CF_N_POINTS, r_bits: Some(vec![r_bits.clone()]), points: Some(vec![self.U_i.clone().cmW, self.u_i.clone().cmW]), x: Some(cfW_u_i_x.clone()), }; let cfE_circuit = CycleFoldCircuit:: { _gc: PhantomData, n_points: NOVA_CF_N_POINTS, r_bits: Some(vec![r_bits.clone()]), points: Some(vec![self.U_i.clone().cmE, cmT]), x: Some(cfE_u_i_x.clone()), }; // fold self.cf_U_i + cfW_U -> folded running with cfW let (_cfW_w_i, cfW_u_i, cfW_W_i1, cfW_U_i1, cfW_cmT, _) = self.fold_cyclefold_circuit( &mut transcript, self.cf_W_i.clone(), // CycleFold running instance witness self.cf_U_i.clone(), // CycleFold running instance cfW_u_i_x, cfW_circuit, &mut rng, )?; // fold [the output from folding self.cf_U_i + cfW_U] + cfE_U = folded_running_with_cfW + cfE let (_cfE_w_i, cfE_u_i, cf_W_i1, cf_U_i1, cf_cmT, _) = self.fold_cyclefold_circuit( &mut transcript, cfW_W_i1, cfW_U_i1.clone(), cfE_u_i_x, cfE_circuit, &mut rng, )?; cf_u_i1_x = cf_U_i1.hash_cyclefold(&sponge, self.pp_hash); augmented_F_circuit = AugmentedFCircuit:: { _gc2: PhantomData, poseidon_config: self.poseidon_config.clone(), pp_hash: Some(self.pp_hash), i: Some(self.i), i_usize: Some(i_usize), z_0: Some(self.z_0.clone()), z_i: Some(self.z_i.clone()), external_inputs: Some(external_inputs.clone()), u_i_cmW: Some(self.u_i.cmW), U_i: Some(self.U_i.clone()), U_i1_cmE: Some(U_i1.cmE), U_i1_cmW: Some(U_i1.cmW), cmT: Some(cmT), F: self.F.clone(), x: Some(u_i1_x), // cyclefold values cf1_u_i_cmW: Some(cfW_u_i.cmW), cf2_u_i_cmW: Some(cfE_u_i.cmW), cf_U_i: Some(self.cf_U_i.clone()), cf1_cmT: Some(cfW_cmT), cf2_cmT: Some(cf_cmT), cf_x: Some(cf_u_i1_x), }; self.cf_W_i = cf_W_i1; self.cf_U_i = cf_U_i1; #[cfg(test)] { self.cf_r1cs.check_instance_relation(&_cfW_w_i, &cfW_u_i)?; self.cf_r1cs.check_instance_relation(&_cfE_w_i, &cfE_u_i)?; self.cf_r1cs .check_relaxed_instance_relation(&self.cf_W_i, &self.cf_U_i)?; } } let cs = ConstraintSystem::::new_ref(); augmented_F_circuit.generate_constraints(cs.clone())?; #[cfg(test)] assert!(cs.is_satisfied().unwrap()); let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; let (w_i1, x_i1) = extract_w_x::(&cs); if x_i1[0] != u_i1_x || x_i1[1] != cf_u_i1_x { return Err(Error::NotEqual); } #[cfg(test)] if x_i1.len() != 2 { return Err(Error::NotExpectedLength(x_i1.len(), 2)); } // set values for next iteration self.i += C1::ScalarField::one(); self.z_i = z_i1; self.w_i = Witness::::new::(w_i1, self.r1cs.A.n_rows, &mut rng); self.u_i = self.w_i.commit::(&self.cs_pp, x_i1)?; self.W_i = W_i1; self.U_i = U_i1; #[cfg(test)] { self.r1cs.check_instance_relation(&self.w_i, &self.u_i)?; self.r1cs .check_relaxed_instance_relation(&self.W_i, &self.U_i)?; } Ok(()) } fn state(&self) -> Vec { self.z_i.clone() } fn instances( &self, ) -> ( Self::RunningInstance, Self::IncomingInstance, Self::CFInstance, ) { ( (self.U_i.clone(), self.W_i.clone()), (self.u_i.clone(), self.w_i.clone()), (self.cf_U_i.clone(), self.cf_W_i.clone()), ) } /// Implements IVC.V of Nova+CycleFold. Notice that this method does not include the /// commitments verification, which is done in the Decider. fn verify( vp: Self::VerifierParam, z_0: Vec, // initial state z_i: Vec, // last state num_steps: C1::ScalarField, running_instance: Self::RunningInstance, incoming_instance: Self::IncomingInstance, cyclefold_instance: Self::CFInstance, ) -> Result<(), Error> { let sponge = PoseidonSponge::::new(&vp.poseidon_config); if num_steps == C1::ScalarField::zero() { if z_0 != z_i { return Err(Error::IVCVerificationFail); } return Ok(()); } let (U_i, W_i) = running_instance; let (u_i, w_i) = incoming_instance; let (cf_U_i, cf_W_i) = cyclefold_instance; if u_i.x.len() != 2 || U_i.x.len() != 2 { return Err(Error::IVCVerificationFail); } let pp_hash = vp.pp_hash()?; // check that u_i's output points to the running instance // u_i.X[0] == H(i, z_0, z_i, U_i) let expected_u_i_x = U_i.hash(&sponge, pp_hash, num_steps, z_0, z_i.clone()); if expected_u_i_x != u_i.x[0] { return Err(Error::IVCVerificationFail); } // u_i.X[1] == H(cf_U_i) let expected_cf_u_i_x = cf_U_i.hash_cyclefold(&sponge, pp_hash); if expected_cf_u_i_x != u_i.x[1] { return Err(Error::IVCVerificationFail); } // check u_i.cmE==0, u_i.u==1 (=u_i is a un-relaxed instance) if !u_i.cmE.is_zero() || !u_i.u.is_one() { return Err(Error::IVCVerificationFail); } // check R1CS satisfiability vp.r1cs.check_instance_relation(&w_i, &u_i)?; // check RelaxedR1CS satisfiability vp.r1cs.check_relaxed_instance_relation(&W_i, &U_i)?; // check CycleFold RelaxedR1CS satisfiability vp.cf_r1cs .check_relaxed_instance_relation(&cf_W_i, &cf_U_i)?; Ok(()) } } impl Nova where C1: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, C2: CurveGroup, GC2: CurveVar>, FC: FCircuit, CS1: CommitmentScheme, CS2: CommitmentScheme, ::BaseField: PrimeField, ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, { // computes T and cmT for the AugmentedFCircuit fn compute_cmT(&self) -> Result<(Vec, C1), Error> { NIFS::::compute_cmT( &self.cs_pp, &self.r1cs, &self.w_i, &self.u_i, &self.W_i, &self.U_i, ) } } impl Nova where C1: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, C2: CurveGroup, GC2: CurveVar> + ToConstraintFieldGadget>, FC: FCircuit, CS1: CommitmentScheme, CS2: CommitmentScheme, ::BaseField: PrimeField, ::BaseField: PrimeField, ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { // folds the given cyclefold circuit and its instances #[allow(clippy::type_complexity)] fn fold_cyclefold_circuit>( &self, transcript: &mut T, cf_W_i: Witness, // witness of the running instance cf_U_i: CommittedInstance, // running instance cf_u_i_x: Vec, cf_circuit: CycleFoldCircuit, rng: &mut impl RngCore, ) -> Result< ( Witness, CommittedInstance, // u_i Witness, // W_i1 CommittedInstance, // U_i1 C2, // cmT C2::ScalarField, // r_Fq ), Error, > { fold_cyclefold_circuit::( NOVA_CF_N_POINTS, transcript, self.cf_r1cs.clone(), self.cf_cs_pp.clone(), self.pp_hash, cf_W_i, cf_U_i, cf_u_i_x, cf_circuit, rng, ) } } /// helper method to get the r1cs from the ConstraintSynthesizer pub fn get_r1cs_from_cs( circuit: impl ConstraintSynthesizer, ) -> Result, Error> { let cs = ConstraintSystem::::new_ref(); circuit.generate_constraints(cs.clone())?; cs.finalize(); let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; let r1cs = extract_r1cs::(&cs); Ok(r1cs) } /// helper method to get the R1CS for both the AugmentedFCircuit and the CycleFold circuit #[allow(clippy::type_complexity)] pub fn get_r1cs( poseidon_config: &PoseidonConfig, F_circuit: FC, ) -> Result<(R1CS, R1CS), Error> where C1: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, C2: CurveGroup, GC2: CurveVar> + ToConstraintFieldGadget>, FC: FCircuit, ::BaseField: PrimeField, ::BaseField: PrimeField, ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { let augmented_F_circuit = AugmentedFCircuit::::empty(poseidon_config, F_circuit); let cf_circuit = CycleFoldCircuit::::empty(NOVA_CF_N_POINTS); let r1cs = get_r1cs_from_cs::(augmented_F_circuit)?; let cf_r1cs = get_r1cs_from_cs::(cf_circuit)?; Ok((r1cs, cf_r1cs)) } /// helper method to get the pedersen params length for both the AugmentedFCircuit and the /// CycleFold circuit pub fn get_cs_params_len( poseidon_config: &PoseidonConfig, F_circuit: FC, ) -> Result<(usize, usize), Error> where C1: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, C2: CurveGroup, GC2: CurveVar> + ToConstraintFieldGadget>, FC: FCircuit, ::BaseField: PrimeField, ::BaseField: PrimeField, ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { let (r1cs, cf_r1cs) = get_r1cs::(poseidon_config, F_circuit)?; Ok((r1cs.A.n_rows, cf_r1cs.A.n_rows)) } #[cfg(test)] pub mod tests { use crate::commitment::kzg::KZG; use ark_bn254::{constraints::GVar, Bn254, Fr, G1Projective as Projective}; use ark_grumpkin::{constraints::GVar as GVar2, Projective as Projective2}; use super::*; use crate::commitment::pedersen::Pedersen; use crate::frontend::tests::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; /// This test tests the Nova+CycleFold IVC, and by consequence it is also testing the /// AugmentedFCircuit #[test] fn test_ivc() { let poseidon_config = poseidon_canonical_config::(); let F_circuit = CubicFCircuit::::new(()).unwrap(); // run the test using Pedersen commitments on both sides of the curve cycle test_ivc_opt::, Pedersen, false>( poseidon_config.clone(), F_circuit, ); test_ivc_opt::, Pedersen, true>( poseidon_config.clone(), F_circuit, ); // 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 allowing to choose the CommitmentSchemes fn test_ivc_opt< CS1: CommitmentScheme, CS2: CommitmentScheme, const H: bool, >( poseidon_config: PoseidonConfig, F_circuit: CubicFCircuit, ) { let mut rng = ark_std::test_rng(); let prep_param = PreprocessorParam::, CS1, CS2, H> { poseidon_config, F: F_circuit, cs_pp: None, cs_vp: None, cf_cs_pp: None, cf_cs_vp: None, }; let nova_params = Nova::< Projective, GVar, Projective2, GVar2, CubicFCircuit, CS1, CS2, H, >::preprocess(&mut rng, &prep_param) .unwrap(); let z_0 = vec![Fr::from(3_u32)]; let mut nova = Nova::, CS1, CS2, H>::init( &nova_params, F_circuit, z_0.clone(), ) .unwrap(); let num_steps: usize = 3; for _ in 0..num_steps { nova.prove_step(&mut rng, vec![], None).unwrap(); } assert_eq!(Fr::from(num_steps as u32), nova.i); 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, nova.i, running_instance, incoming_instance, cyclefold_instance, ) .unwrap(); } }