/// contains [CycleFold](https://eprint.iacr.org/2023/1192.pdf) related circuits use ark_crypto_primitives::sponge::{ constraints::CryptographicSpongeVar, poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, Absorb, CryptographicSponge, }; use ark_ec::CurveGroup; use ark_ff::PrimeField; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, bits::uint8::UInt8, boolean::Boolean, eq::EqGadget, fields::{fp::FpVar, nonnative::NonNativeFieldVar}, groups::GroupOpsBounds, prelude::CurveVar, ToBytesGadget, }; use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; use ark_serialize::CanonicalSerialize; use ark_std::fmt::Debug; use ark_std::Zero; use core::{borrow::Borrow, marker::PhantomData}; use super::circuits::CF2; use super::CommittedInstance; use crate::constants::N_BITS_RO; use crate::Error; // publi inputs length for the CycleFoldCircuit, |[u_i, U_i, U_{i+1}]| pub const CF_IO_LEN: usize = 12; /// 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, }) }) } } /// 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, }) }) } } /// NIFSinCycleFoldGadget performs the Nova NIFS.V elliptic curve points relation checks in the other /// curve (natively) following [CycleFold](https://eprint.iacr.org/2023/1192.pdf). pub struct NIFSinCycleFoldGadget>> { _c: PhantomData, _gc: PhantomData, } impl>> NIFSinCycleFoldGadget where C: CurveGroup, GC: CurveVar>, ::BaseField: ark_ff::PrimeField, for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { pub fn verify( r_bits: Vec>>, cmT: GC, ci1: CommittedInstanceInCycleFoldVar, ci2: CommittedInstanceInCycleFoldVar, ci3: CommittedInstanceInCycleFoldVar, ) -> Result>, SynthesisError> { // cm(E) check: ci3.cmE == ci1.cmE + r * cmT + r^2 * ci2.cmE let first_check = ci3.cmE.is_eq( &((ci2.cmE.scalar_mul_le(r_bits.iter())? + 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())?))?; first_check.and(&second_check) } } /// 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( 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) } } /// ChallengeGadget 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>, ::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 u_i_cmE_bytes = point_to_bytes(u_i.cmE); let u_i_cmW_bytes = point_to_bytes(u_i.cmW); let U_i_cmE_bytes = point_to_bytes(U_i.cmE); let U_i_cmW_bytes = point_to_bytes(U_i.cmW); let cmT_bytes = point_to_bytes(cmT); let mut u_i_u_bytes = Vec::new(); u_i.u.serialize_uncompressed(&mut u_i_u_bytes)?; let mut u_i_x_bytes = Vec::new(); u_i.x.serialize_uncompressed(&mut u_i_x_bytes)?; u_i_x_bytes = u_i_x_bytes[8..].to_vec(); let mut U_i_u_bytes = Vec::new(); U_i.u.serialize_uncompressed(&mut U_i_u_bytes)?; let mut U_i_x_bytes = Vec::new(); U_i.x.serialize_uncompressed(&mut U_i_x_bytes)?; U_i_x_bytes = U_i_x_bytes[8..].to_vec(); let input: Vec = [ u_i_cmE_bytes, u_i_u_bytes, u_i_cmW_bytes, u_i_x_bytes, U_i_cmE_bytes, U_i_u_bytes, U_i_cmW_bytes, U_i_x_bytes, cmT_bytes, ] .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 u_i_x_bytes: Vec>> = u_i .x .iter() .flat_map(|e| e.to_bytes().unwrap_or(vec![])) .collect::>>>(); let U_i_x_bytes: Vec>> = U_i .x .iter() .flat_map(|e| e.to_bytes().unwrap_or(vec![])) .collect::>>>(); let input: Vec>> = [ u_i.cmE.to_bytes()?, u_i.u.to_bytes()?, u_i.cmW.to_bytes()?, u_i_x_bytes, U_i.cmE.to_bytes()?, U_i.u.to_bytes()?, U_i.cmW.to_bytes()?, U_i_x_bytes, cmT.to_bytes()?, // TODO instead of bytes, use field elements, but needs x,y coordinates from // u_i.{cmE,cmW}, U_i.{cmE,cmW}, cmT. Depends exposing x,y coordinates of GC. Issue to // keep track of this: // https://github.com/privacy-scaling-explorations/folding-schemes/issues/44 ] .concat(); sponge.absorb(&input)?; let bits = sponge.squeeze_bits(N_BITS_RO)?; Ok(bits) } } /// returns the bytes being compatible with the ark_r1cs_std `.to_bytes` approach fn point_to_bytes(p: C) -> Vec { let l = p.uncompressed_size(); let mut b = Vec::new(); p.serialize_uncompressed(&mut b).unwrap(); b[l - 1] = 0; if p.is_zero() { b[l / 2] = 1; b[l - 1] = 1; } b } /// 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 cmT: Option, // u_i,U_i,U_i1 are the nova instances from AugmentedFCircuit which will be (their elliptic // curve points) checked natively in CycleFoldCircuit pub u_i: Option>, pub U_i: Option>, pub U_i1: Option>, pub x: Option>>, // public inputs (cf_u_{i+1}.x) } impl>> CycleFoldCircuit { pub fn empty() -> Self { Self { _gc: PhantomData, r_bits: None, cmT: None, u_i: None, U_i: None, U_i1: None, x: None, } } } impl ConstraintSynthesizer> for CycleFoldCircuit where C: CurveGroup, GC: CurveVar>, ::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 cmT = GC::new_witness(cs.clone(), || Ok(self.cmT.unwrap_or(C::zero())))?; let u_dummy_native = CommittedInstance::::dummy(1); let u_i = CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { Ok(self.u_i.unwrap_or(u_dummy_native.clone())) })?; let U_i = CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { Ok(self.U_i.unwrap_or(u_dummy_native.clone())) })?; let U_i1 = CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { Ok(self.U_i1.unwrap_or(u_dummy_native.clone())) })?; 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 // fold the original Nova instances natively in CycleFold let v = NIFSinCycleFoldGadget::::verify(r_bits.clone(), cmT, u_i.clone(), U_i, U_i1)?; v.enforce_equal(&Boolean::TRUE)?; // check that x == [u_i, U_i, U_{i+1}], check that the cmW & cmW from u_i, U_i, U_{i+1} in // the CycleFoldCircuit are the sames used in the public inputs 'x', which come from the // AugmentedFCircuit. // TODO: Issue to keep track of this: https://github.com/privacy-scaling-explorations/folding-schemes/issues/44 // and https://github.com/privacy-scaling-explorations/folding-schemes/issues/48 Ok(()) } } #[cfg(test)] pub mod tests { use super::*; use ark_ff::BigInteger; use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; use ark_r1cs_std::{alloc::AllocVar, R1CSVar}; use ark_relations::r1cs::ConstraintSystem; use ark_std::UniformRand; 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_nifs_gadget_cyclefold() { let (_, _, _, _, ci1, _, ci2, _, ci3, _, cmT, r_bits, _) = prepare_simple_fold_inputs(); // cs is the Constraint System on the Curve Cycle auxiliary curve constraints field // (E2::Fr) let cs = ConstraintSystem::::new_ref(); let r_bitsVar = Vec::>::new_witness(cs.clone(), || Ok(r_bits)).unwrap(); let cmTVar = GVar::new_witness(cs.clone(), || Ok(cmT)).unwrap(); let ci1Var = CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { Ok(ci1.clone()) }) .unwrap(); let ci2Var = CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { Ok(ci2.clone()) }) .unwrap(); let ci3Var = CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { Ok(ci3.clone()) }) .unwrap(); let nifs_cf_check = NIFSinCycleFoldGadget::::verify( r_bitsVar, cmTVar, ci1Var, ci2Var, ci3Var, ) .unwrap(); nifs_cf_check.enforce_equal(&Boolean::::TRUE).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); } }