/// contains [CycleFold](https://eprint.iacr.org/2023/1192.pdf) related circuits use ark_crypto_primitives::{ crh::{ poseidon::constraints::{CRHGadget, CRHParametersVar}, CRHSchemeGadget, }, sponge::{ constraints::CryptographicSpongeVar, poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, Absorb, CryptographicSponge, }, }; use ark_ec::{AffineRepr, CurveGroup}; use ark_ff::{Field, PrimeField, ToConstraintField}; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, boolean::Boolean, eq::EqGadget, fields::{fp::FpVar, nonnative::NonNativeFieldVar, FieldVar}, groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget, }; use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::fmt::Debug; use ark_std::{One, Zero}; use core::{borrow::Borrow, marker::PhantomData}; use super::circuits::CF2; use super::CommittedInstance; use crate::constants::N_BITS_RO; use crate::folding::circuits::nonnative::nonnative_field_var_to_constraint_field; use crate::Error; // public inputs length for the CycleFoldCircuit: |[r, p1.x,y, p2.x,y, p3.x,y]| pub const CF_IO_LEN: usize = 7; /// CycleFoldCommittedInstanceVar is the CycleFold CommittedInstance representation in the Nova /// circuit. #[derive(Debug, Clone)] pub struct CycleFoldCommittedInstanceVar>> where for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { _c: PhantomData, pub cmE: GC, pub u: NonNativeFieldVar>, pub cmW: GC, pub x: Vec>>, } impl AllocVar, CF2> for CycleFoldCommittedInstanceVar where C: CurveGroup, GC: CurveVar>, ::BaseField: ark_ff::PrimeField, for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { fn new_variable>>( cs: impl Into>>, f: impl FnOnce() -> Result, mode: AllocationMode, ) -> Result { f().and_then(|val| { let cs = cs.into(); let cmE = GC::new_variable(cs.clone(), || Ok(val.borrow().cmE), mode)?; let cmW = GC::new_variable(cs.clone(), || Ok(val.borrow().cmW), mode)?; let u = NonNativeFieldVar::>::new_variable( cs.clone(), || Ok(val.borrow().u), mode, )?; let x = Vec::>>::new_variable( cs.clone(), || Ok(val.borrow().x.clone()), mode, )?; Ok(Self { _c: PhantomData, cmE, u, cmW, x, }) }) } } impl ToConstraintFieldGadget> for CycleFoldCommittedInstanceVar where C: CurveGroup, GC: CurveVar> + ToConstraintFieldGadget>, ::BaseField: ark_ff::PrimeField + Absorb, for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { // Extract the underlying field elements from `CycleFoldCommittedInstanceVar`, in the order of // `u`, `x`, `cmE.x`, `cmE.y`, `cmW.x`, `cmW.y`, `cmE.is_inf || cmW.is_inf` (|| is for concat). fn to_constraint_field(&self) -> Result>>, SynthesisError> { let mut cmE_elems = self.cmE.to_constraint_field()?; let mut cmW_elems = self.cmW.to_constraint_field()?; let cmE_is_inf = cmE_elems.pop().unwrap(); let cmW_is_inf = cmW_elems.pop().unwrap(); // Concatenate `cmE_is_inf` and `cmW_is_inf` to save constraints for CRHGadget::evaluate let is_inf = cmE_is_inf.double()? + cmW_is_inf; Ok([ nonnative_field_var_to_constraint_field(&self.u)?, self.x .iter() .map(nonnative_field_var_to_constraint_field) .collect::, _>>()? .concat(), cmE_elems, cmW_elems, vec![is_inf], ] .concat()) } } impl CycleFoldCommittedInstanceVar where C: CurveGroup, GC: CurveVar> + ToConstraintFieldGadget>, ::BaseField: ark_ff::PrimeField + Absorb, for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { /// hash implements the committed instance hash compatible with the native implementation from /// CommittedInstance.hash_cyclefold. /// Returns `H(U_i)`, where `U` is the `CommittedInstance` for CycleFold. /// Additionally it returns the vector of the field elements from the self parameters, so they /// can be reused in other gadgets avoiding recalculating (reconstraining) them. #[allow(clippy::type_complexity)] pub fn hash( self, crh_params: &CRHParametersVar>, ) -> Result<(FpVar>, Vec>>), SynthesisError> { let U_vec = self.to_constraint_field()?; Ok((CRHGadget::evaluate(crh_params, &U_vec)?, U_vec)) } } /// CommittedInstanceInCycleFoldVar represents the Nova CommittedInstance in the CycleFold circuit, /// where the commitments to E and W (cmW and cmW) from the CommittedInstance on the E2, /// represented as native points, which are folded on the auxiliary curve constraints field (E2::Fr /// = E1::Fq). #[derive(Debug, Clone)] pub struct CommittedInstanceInCycleFoldVar>> where for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { _c: PhantomData, pub cmE: GC, pub cmW: GC, } impl AllocVar, CF2> for CommittedInstanceInCycleFoldVar where C: CurveGroup, GC: CurveVar>, for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { fn new_variable>>( cs: impl Into>>, f: impl FnOnce() -> Result, mode: AllocationMode, ) -> Result { f().and_then(|val| { let cs = cs.into(); let cmE = GC::new_variable(cs.clone(), || Ok(val.borrow().cmE), mode)?; let cmW = GC::new_variable(cs.clone(), || Ok(val.borrow().cmW), mode)?; Ok(Self { _c: PhantomData, cmE, cmW, }) }) } } /// This is the gadget used in the AugmentedFCircuit to verify the CycleFold instances folding, /// which checks the correct RLC of u,x,cmE,cmW (hence the name containing 'Full', since it checks /// all the RLC values, not only the native ones). It assumes that ci2.cmE=0, ci2.u=1. pub struct NIFSFullGadget>> { _c: PhantomData, _gc: PhantomData, } impl>> NIFSFullGadget where C: CurveGroup, GC: CurveVar>, ::BaseField: ark_ff::PrimeField, for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { pub fn verify( // assumes that r_bits is equal to r_nonnat just that in a different format r_bits: Vec>>, r_nonnat: NonNativeFieldVar>, cmT: GC, // ci1 is assumed to be always with cmE=0, u=1 (checks done previous to this method) ci1: CycleFoldCommittedInstanceVar, ci2: CycleFoldCommittedInstanceVar, ci3: CycleFoldCommittedInstanceVar, ) -> Result>, SynthesisError> { // cm(E) check: ci3.cmE == ci1.cmE + r * cmT (ci2.cmE=0) let first_check = ci3 .cmE .is_eq(&(cmT.scalar_mul_le(r_bits.iter())? + ci1.cmE))?; // cm(W) check: ci3.cmW == ci1.cmW + r * ci2.cmW let second_check = ci3 .cmW .is_eq(&(ci1.cmW + ci2.cmW.scalar_mul_le(r_bits.iter())?))?; let u_rlc: NonNativeFieldVar> = ci1.u + r_nonnat.clone(); let third_check = u_rlc.is_eq(&ci3.u)?; // ensure that: ci3.x == ci1.x + r * ci2.x let x_rlc: Vec>> = ci1 .x .iter() .zip(ci2.x) .map(|(a, b)| a + &r_nonnat * &b) .collect::>>>(); let fourth_check = x_rlc.is_eq(&ci3.x)?; first_check .and(&second_check)? .and(&third_check)? .and(&fourth_check) } } /// CycleFoldChallengeGadget computes the RO challenge used for the CycleFold instances NIFS, it contains a /// rust-native and a in-circuit compatible versions. pub struct CycleFoldChallengeGadget>> { _c: PhantomData, // Nova's Curve2, the one used for the CycleFold circuit _gc: PhantomData, } impl CycleFoldChallengeGadget where C: CurveGroup, GC: CurveVar> + ToConstraintFieldGadget>, ::BaseField: PrimeField, ::BaseField: Absorb, for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { pub fn get_challenge_native( poseidon_config: &PoseidonConfig, U_i: CommittedInstance, u_i: CommittedInstance, cmT: C, ) -> Result, Error> { let mut sponge = PoseidonSponge::::new(poseidon_config); let mut U_vec = U_i.to_field_elements().unwrap(); let mut u_vec = u_i.to_field_elements().unwrap(); let (cmT_x, cmT_y, cmT_is_inf) = match cmT.into_affine().xy() { Some((&x, &y)) => (x, y, C::BaseField::zero()), None => ( C::BaseField::zero(), C::BaseField::zero(), C::BaseField::one(), ), }; let U_cm_is_inf = U_vec.pop().unwrap(); let u_cm_is_inf = u_vec.pop().unwrap(); // Concatenate `U_i.cmE_is_inf`, `U_i.cmW_is_inf`, `u_i.cmE_is_inf`, `u_i.cmW_is_inf`, `cmT_is_inf` // to save constraints for sponge.squeeze_bits in the corresponding circuit let is_inf = U_cm_is_inf * CF2::::from(8u8) + u_cm_is_inf.double() + cmT_is_inf; let input = [U_vec, u_vec, vec![cmT_x, cmT_y, is_inf]].concat(); sponge.absorb(&input); let bits = sponge.squeeze_bits(N_BITS_RO); Ok(bits) } // compatible with the native get_challenge_native pub fn get_challenge_gadget( cs: ConstraintSystemRef, poseidon_config: &PoseidonConfig, U_i: CycleFoldCommittedInstanceVar, u_i: CycleFoldCommittedInstanceVar, cmT: GC, ) -> Result>, SynthesisError> { let mut sponge = PoseidonSpongeVar::::new(cs, poseidon_config); let mut U_vec = U_i.to_constraint_field()?; let mut u_vec = u_i.to_constraint_field()?; let mut cmT_vec = cmT.to_constraint_field()?; let U_cm_is_inf = U_vec.pop().unwrap(); let u_cm_is_inf = u_vec.pop().unwrap(); let cmT_is_inf = cmT_vec.pop().unwrap(); // Concatenate `U_i.cmE_is_inf`, `U_i.cmW_is_inf`, `u_i.cmE_is_inf`, `u_i.cmW_is_inf`, `cmT_is_inf` // to save constraints for sponge.squeeze_bits let is_inf = U_cm_is_inf * CF2::::from(8u8) + u_cm_is_inf.double()? + cmT_is_inf; let input = [U_vec, u_vec, cmT_vec, vec![is_inf]].concat(); sponge.absorb(&input)?; let bits = sponge.squeeze_bits(N_BITS_RO)?; Ok(bits) } } /// CycleFoldCircuit contains the constraints that check the correct fold of the committed /// instances from Curve1. Namely, it checks the random linear combinations of the elliptic curve /// (Curve1) points of u_i, U_i leading to U_{i+1} #[derive(Debug, Clone)] pub struct CycleFoldCircuit>> { pub _gc: PhantomData, pub r_bits: Option>, pub p1: Option, pub p2: Option, pub p3: Option, pub x: Option>>, // public inputs (cf_u_{i+1}.x) } impl>> CycleFoldCircuit { pub fn empty() -> Self { Self { _gc: PhantomData, r_bits: None, p1: None, p2: None, p3: None, x: None, } } } impl ConstraintSynthesizer> for CycleFoldCircuit where C: CurveGroup, GC: CurveVar> + ToConstraintFieldGadget>, ::BaseField: ark_ff::PrimeField, for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { let r_bits: Vec>> = Vec::new_witness(cs.clone(), || { Ok(self.r_bits.unwrap_or(vec![false; N_BITS_RO])) })?; let p1 = GC::new_witness(cs.clone(), || Ok(self.p1.unwrap_or(C::zero())))?; let p2 = GC::new_witness(cs.clone(), || Ok(self.p2.unwrap_or(C::zero())))?; let p3 = GC::new_witness(cs.clone(), || Ok(self.p3.unwrap_or(C::zero())))?; let x = Vec::>>::new_input(cs.clone(), || { Ok(self.x.unwrap_or(vec![CF2::::zero(); CF_IO_LEN])) })?; #[cfg(test)] assert_eq!(x.len(), CF_IO_LEN); // non-constrained sanity check // check that the points coordinates are placed as the public input x: x == [r, p1, p2, p3] let r: FpVar> = Boolean::le_bits_to_fp_var(&r_bits)?; let points_coords: Vec>> = [ vec![r], p1.clone().to_constraint_field()?[..2].to_vec(), p2.clone().to_constraint_field()?[..2].to_vec(), p3.clone().to_constraint_field()?[..2].to_vec(), ] .concat(); points_coords.enforce_equal(&x)?; // Fold the original Nova instances natively in CycleFold // For the cmW we're checking: U_i1.cmW == U_i.cmW + r * u_i.cmW // For the cmE we're checking: U_i1.cmE == U_i.cmE + r * cmT + r^2 * u_i.cmE, where u_i.cmE // is assumed to be 0, so, U_i1.cmE == U_i.cmE + r * cmT p3.enforce_equal(&(p1 + p2.scalar_mul_le(r_bits.iter())?))?; Ok(()) } } #[cfg(test)] pub mod tests { use super::*; use ark_bn254::{constraints::GVar, Fq, Fr, G1Projective as Projective}; use ark_ff::BigInteger; use ark_r1cs_std::R1CSVar; use ark_relations::r1cs::ConstraintSystem; use ark_std::UniformRand; use crate::folding::nova::get_cm_coordinates; use crate::folding::nova::nifs::tests::prepare_simple_fold_inputs; use crate::transcript::poseidon::poseidon_test_config; #[test] fn test_committed_instance_cyclefold_var() { let mut rng = ark_std::test_rng(); let ci = CommittedInstance:: { cmE: Projective::rand(&mut rng), u: Fr::rand(&mut rng), cmW: Projective::rand(&mut rng), x: vec![Fr::rand(&mut rng); 1], }; // check the instantiation of the CycleFold side: let cs = ConstraintSystem::::new_ref(); let ciVar = CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { Ok(ci.clone()) }) .unwrap(); assert_eq!(ciVar.cmE.value().unwrap(), ci.cmE); assert_eq!(ciVar.cmW.value().unwrap(), ci.cmW); } #[test] fn test_CycleFoldCircuit_constraints() { let (_, _, _, _, ci1, _, ci2, _, ci3, _, cmT, r_bits, _) = prepare_simple_fold_inputs(); let r_Fq = Fq::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap(); // cs is the Constraint System on the Curve Cycle auxiliary curve constraints field // (E1::Fq=E2::Fr) let cs = ConstraintSystem::::new_ref(); let cfW_u_i_x: Vec = [ vec![r_Fq], get_cm_coordinates(&ci1.cmW), get_cm_coordinates(&ci2.cmW), get_cm_coordinates(&ci3.cmW), ] .concat(); let cfW_circuit = CycleFoldCircuit:: { _gc: PhantomData, r_bits: Some(r_bits.clone()), p1: Some(ci1.clone().cmW), p2: Some(ci2.clone().cmW), p3: Some(ci3.clone().cmW), x: Some(cfW_u_i_x.clone()), }; cfW_circuit.generate_constraints(cs.clone()).unwrap(); assert!(cs.is_satisfied().unwrap()); // same for E: let cs = ConstraintSystem::::new_ref(); let cfE_u_i_x = [ vec![r_Fq], get_cm_coordinates(&ci1.cmE), get_cm_coordinates(&cmT), get_cm_coordinates(&ci3.cmE), ] .concat(); let cfE_circuit = CycleFoldCircuit:: { _gc: PhantomData, r_bits: Some(r_bits.clone()), p1: Some(ci1.clone().cmE), p2: Some(cmT), p3: Some(ci3.clone().cmE), x: Some(cfE_u_i_x.clone()), }; cfE_circuit.generate_constraints(cs.clone()).unwrap(); assert!(cs.is_satisfied().unwrap()); } #[test] fn test_nifs_full_gadget() { let (_, _, _, _, ci1, _, ci2, _, ci3, _, cmT, r_bits, r_Fr) = prepare_simple_fold_inputs(); let cs = ConstraintSystem::::new_ref(); let r_nonnatVar = NonNativeFieldVar::::new_witness(cs.clone(), || Ok(r_Fr)).unwrap(); let r_bitsVar = Vec::>::new_witness(cs.clone(), || Ok(r_bits)).unwrap(); let ci1Var = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { Ok(ci1.clone()) }) .unwrap(); let ci2Var = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { Ok(ci2.clone()) }) .unwrap(); let ci3Var = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { Ok(ci3.clone()) }) .unwrap(); let cmTVar = GVar::new_witness(cs.clone(), || Ok(cmT)).unwrap(); let nifs_check = NIFSFullGadget::::verify( r_bitsVar, r_nonnatVar, cmTVar, ci1Var, ci2Var, ci3Var, ) .unwrap(); nifs_check.enforce_equal(&Boolean::::TRUE).unwrap(); assert!(cs.is_satisfied().unwrap()); } #[test] fn test_cyclefold_challenge_gadget() { let mut rng = ark_std::test_rng(); let poseidon_config = poseidon_test_config::(); let u_i = CommittedInstance:: { cmE: Projective::zero(), // zero on purpose, so we test also the zero point case u: Fr::zero(), cmW: Projective::rand(&mut rng), x: std::iter::repeat_with(|| Fr::rand(&mut rng)) .take(CF_IO_LEN) .collect(), }; let U_i = CommittedInstance:: { cmE: Projective::rand(&mut rng), u: Fr::rand(&mut rng), cmW: Projective::rand(&mut rng), x: std::iter::repeat_with(|| Fr::rand(&mut rng)) .take(CF_IO_LEN) .collect(), }; let cmT = Projective::rand(&mut rng); // compute the challenge natively let r_bits = CycleFoldChallengeGadget::::get_challenge_native( &poseidon_config, U_i.clone(), u_i.clone(), cmT, ) .unwrap(); let cs = ConstraintSystem::::new_ref(); let u_iVar = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { Ok(u_i.clone()) }) .unwrap(); let U_iVar = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { Ok(U_i.clone()) }) .unwrap(); let cmTVar = GVar::new_witness(cs.clone(), || Ok(cmT)).unwrap(); let r_bitsVar = CycleFoldChallengeGadget::::get_challenge_gadget( cs.clone(), &poseidon_config, U_iVar, u_iVar, cmTVar, ) .unwrap(); assert!(cs.is_satisfied().unwrap()); // check that the natively computed and in-circuit computed hashes match let rVar = Boolean::le_bits_to_fp_var(&r_bitsVar).unwrap(); let r = Fq::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap(); assert_eq!(rVar.value().unwrap(), r); assert_eq!(r_bitsVar.value().unwrap(), r_bits); } #[test] fn test_cyclefold_hash_gadget() { let mut rng = ark_std::test_rng(); let poseidon_config = poseidon_test_config::(); let U_i = CommittedInstance:: { cmE: Projective::rand(&mut rng), u: Fr::rand(&mut rng), cmW: Projective::rand(&mut rng), x: std::iter::repeat_with(|| Fr::rand(&mut rng)) .take(CF_IO_LEN) .collect(), }; let h = U_i.hash_cyclefold(&poseidon_config).unwrap(); let cs = ConstraintSystem::::new_ref(); let U_iVar = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { Ok(U_i.clone()) }) .unwrap(); let (hVar, _) = U_iVar .hash(&CRHParametersVar::new_constant(cs.clone(), poseidon_config).unwrap()) .unwrap(); hVar.enforce_equal(&FpVar::new_witness(cs.clone(), || Ok(h)).unwrap()) .unwrap(); assert!(cs.is_satisfied().unwrap()); } }