/// This file implements the onchain (Ethereum's EVM) decider circuit. For non-ethereum use cases, /// other more efficient approaches can be used. use ark_crypto_primitives::crh::poseidon::constraints::CRHParametersVar; use ark_crypto_primitives::sponge::{poseidon::PoseidonConfig, Absorb}; use ark_ec::{CurveGroup, Group}; use ark_ff::PrimeField; 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::{One, Zero}; use core::{borrow::Borrow, marker::PhantomData}; use crate::ccs::r1cs::R1CS; use crate::commitment::{pedersen::Params as PedersenParams, CommitmentProver}; use crate::folding::nova::{ circuits::{CommittedInstanceVar, CF1, CF2}, ivc::IVC, CommittedInstance, Witness, }; use crate::frontend::FCircuit; use crate::utils::gadgets::{ hadamard, mat_vec_mul_sparse, vec_add, vec_scalar_mul, SparseMatrixVar, }; #[derive(Debug, Clone)] pub struct RelaxedR1CSGadget> { _f: PhantomData, _cf: PhantomData, _fv: PhantomData, } impl> RelaxedR1CSGadget { /// performs the RelaxedR1CS check (Az∘Bz==uCz+E) pub fn check( r1cs: R1CSVar, E: Vec, u: FV, z: Vec, ) -> Result<(), SynthesisError> { let Az = mat_vec_mul_sparse(r1cs.A, z.clone()); let Bz = mat_vec_mul_sparse(r1cs.B, z.clone()); let Cz = mat_vec_mul_sparse(r1cs.C, z.clone()); let uCz = vec_scalar_mul(&Cz, &u); let uCzE = vec_add(&uCz, &E)?; let AzBz = hadamard(&Az, &Bz)?; for i in 0..AzBz.len() { AzBz[i].enforce_equal(&uCzE[i].clone())?; } Ok(()) } } #[derive(Debug, Clone)] pub struct R1CSVar> { _f: PhantomData, _cf: PhantomData, _fv: PhantomData, pub A: SparseMatrixVar, pub B: SparseMatrixVar, pub C: SparseMatrixVar, } impl AllocVar, CF> for R1CSVar where F: PrimeField, CF: PrimeField, FV: FieldVar, { fn new_variable>>( cs: impl Into>, f: impl FnOnce() -> Result, _mode: AllocationMode, ) -> Result { f().and_then(|val| { let cs = cs.into(); let A = SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().A)?; let B = SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().B)?; let C = SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().C)?; Ok(Self { _f: PhantomData, _cf: PhantomData, _fv: PhantomData, A, B, C, }) }) } } /// In-circuit representation of the Witness associated to the CommittedInstance. #[derive(Debug, Clone)] pub struct WitnessVar { pub E: Vec>, pub rE: FpVar, pub W: Vec>, pub rW: FpVar, } impl AllocVar, CF1> for WitnessVar where C: CurveGroup, ::BaseField: PrimeField, { fn new_variable>>( cs: impl Into>>, f: impl FnOnce() -> Result, mode: AllocationMode, ) -> Result { f().and_then(|val| { let cs = cs.into(); let E: Vec> = Vec::new_variable(cs.clone(), || Ok(val.borrow().E.clone()), mode)?; let rE = FpVar::::new_variable(cs.clone(), || Ok(val.borrow().rE), mode)?; let W: Vec> = Vec::new_variable(cs.clone(), || Ok(val.borrow().W.clone()), mode)?; let rW = FpVar::::new_variable(cs.clone(), || Ok(val.borrow().rW), mode)?; Ok(Self { E, rE, W, rW }) }) } } /// In-circuit representation of the Witness associated to the CommittedInstance, but with /// non-native representation, since it is used to represent the CycleFold witness. #[derive(Debug, Clone)] pub struct CycleFoldWitnessVar { pub E: Vec>>, pub rE: NonNativeFieldVar>, pub W: Vec>>, pub rW: NonNativeFieldVar>, } impl AllocVar, CF2> for CycleFoldWitnessVar where C: CurveGroup, ::BaseField: PrimeField, { fn new_variable>>( cs: impl Into>>, f: impl FnOnce() -> Result, mode: AllocationMode, ) -> Result { f().and_then(|val| { let cs = cs.into(); let E: Vec>> = Vec::new_variable(cs.clone(), || Ok(val.borrow().E.clone()), mode)?; let rE = NonNativeFieldVar::>::new_variable( cs.clone(), || Ok(val.borrow().rE), mode, )?; let W: Vec>> = Vec::new_variable(cs.clone(), || Ok(val.borrow().W.clone()), mode)?; let rW = NonNativeFieldVar::>::new_variable( cs.clone(), || Ok(val.borrow().rW), mode, )?; Ok(Self { E, rE, W, rW }) }) } } /// Circuit that implements the in-circuit checks needed for the onchain (Ethereum's EVM) /// verification. pub struct DeciderCircuit where C1: CurveGroup, GC1: CurveVar>, C2: CurveGroup, GC2: CurveVar>, CP1: CommitmentProver, CP2: CommitmentProver, { _c1: PhantomData, _gc1: PhantomData, _c2: PhantomData, _gc2: PhantomData, _cp1: PhantomData, _cp2: PhantomData, /// E vector's length of the Nova instance witness pub E_len: usize, /// E vector's length of the CycleFold instance witness pub cf_E_len: usize, /// R1CS of the Augmented Function circuit pub r1cs: R1CS, /// R1CS of the CycleFold circuit pub cf_r1cs: R1CS, /// CycleFold PedersenParams over C2 pub cf_pedersen_params: PedersenParams, pub poseidon_config: PoseidonConfig>, pub i: Option>, /// initial state pub z_0: Option>, /// current i-th state pub z_i: Option>, /// Nova instances pub u_i: Option>, pub w_i: Option>, pub U_i: Option>, pub W_i: Option>, /// CycleFold running instance pub cf_U_i: Option>, pub cf_W_i: Option>, } impl DeciderCircuit where C1: CurveGroup, C2: CurveGroup, GC1: CurveVar>, GC2: CurveVar>, CP1: CommitmentProver, // enforce that the CP2 is Pedersen commitment, since we're at Ethereum's EVM decider CP2: CommitmentProver>, { pub fn from_ivc>( ivc: IVC, ) -> Self { Self { _c1: PhantomData, _gc1: PhantomData, _c2: PhantomData, _gc2: PhantomData, _cp1: PhantomData, _cp2: PhantomData, E_len: ivc.W_i.E.len(), cf_E_len: ivc.cf_W_i.E.len(), r1cs: ivc.r1cs, cf_r1cs: ivc.cf_r1cs, cf_pedersen_params: ivc.cf_cm_params, poseidon_config: ivc.poseidon_config, i: Some(ivc.i), z_0: Some(ivc.z_0), z_i: Some(ivc.z_i), u_i: Some(ivc.u_i), w_i: Some(ivc.w_i), U_i: Some(ivc.U_i), W_i: Some(ivc.W_i), cf_U_i: Some(ivc.cf_U_i), cf_W_i: Some(ivc.cf_W_i), } } } impl ConstraintSynthesizer> for DeciderCircuit where C1: CurveGroup, C2: CurveGroup, GC1: CurveVar>, GC2: CurveVar>, CP1: CommitmentProver, CP2: CommitmentProver, ::BaseField: PrimeField, ::BaseField: PrimeField, ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, for<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>, { fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { let r1cs = R1CSVar::, FpVar>>::new_witness(cs.clone(), || { Ok(self.r1cs.clone()) })?; let i = FpVar::>::new_witness(cs.clone(), || { Ok(self.i.unwrap_or_else(CF1::::zero)) })?; let z_0 = Vec::>>::new_witness(cs.clone(), || { Ok(self.z_0.unwrap_or(vec![CF1::::zero()])) })?; let z_i = Vec::>>::new_witness(cs.clone(), || { Ok(self.z_i.unwrap_or(vec![CF1::::zero()])) })?; let u_dummy_native = CommittedInstance::::dummy(1); let w_dummy_native = Witness::::new( vec![C1::ScalarField::zero(); self.r1cs.A.n_cols - 2 /* (2=1+1, since u_i.x.len=1) */], self.E_len, ); let u_i = CommittedInstanceVar::::new_witness(cs.clone(), || { Ok(self.u_i.unwrap_or(u_dummy_native.clone())) })?; let w_i = WitnessVar::::new_witness(cs.clone(), || { Ok(self.w_i.unwrap_or(w_dummy_native.clone())) })?; let U_i = CommittedInstanceVar::::new_witness(cs.clone(), || { Ok(self.U_i.unwrap_or(u_dummy_native.clone())) })?; let W_i = WitnessVar::::new_witness(cs.clone(), || { Ok(self.W_i.unwrap_or(w_dummy_native.clone())) })?; let crh_params = CRHParametersVar::::new_constant( cs.clone(), self.poseidon_config.clone(), )?; // 1. check RelaxedR1CS of u_i let z_u: Vec>> = [ vec![FpVar::>::one()], u_i.x.to_vec(), w_i.W.to_vec(), ] .concat(); RelaxedR1CSGadget::, FpVar>>::check( r1cs.clone(), w_i.E, u_i.u.clone(), z_u, )?; // 2. check RelaxedR1CS of U_i let z_U: Vec>> = [vec![U_i.u.clone()], U_i.x.to_vec(), W_i.W.to_vec()].concat(); RelaxedR1CSGadget::, FpVar>>::check( r1cs, W_i.E, U_i.u.clone(), z_U, )?; // 3. u_i.cmE==cm(0), u_i.u==1 // Here zero_x & zero_y are the x & y coordinates of the zero point affine representation. let zero_x = NonNativeFieldVar::::new_constant( cs.clone(), C1::BaseField::zero(), )? .to_constraint_field()?; let zero_y = NonNativeFieldVar::::new_constant( cs.clone(), C1::BaseField::one(), )? .to_constraint_field()?; (u_i.cmE.x.is_eq(&zero_x)?).enforce_equal(&Boolean::TRUE)?; (u_i.cmE.y.is_eq(&zero_y)?).enforce_equal(&Boolean::TRUE)?; (u_i.u.is_one()?).enforce_equal(&Boolean::TRUE)?; // 4. u_i.x == H(i, z_0, z_i, U_i) let u_i_x = U_i .clone() .hash(&crh_params, i.clone(), z_0.clone(), z_i.clone())?; (u_i.x[0]).enforce_equal(&u_i_x)?; // The following two checks (and their respective allocations) are disabled for normal // tests since they take ~24.5M constraints and would take several minutes (and RAM) to run // the test #[cfg(not(test))] { // imports here instead of at the top of the file, so we avoid having multiple // `#[cfg(not(test))] use crate::commitment::pedersen::PedersenGadget; use crate::folding::nova::cyclefold::{CycleFoldCommittedInstanceVar, CF_IO_LEN}; use ark_r1cs_std::ToBitsGadget; let cf_r1cs = R1CSVar::< C1::BaseField, CF1, NonNativeFieldVar>, >::new_witness(cs.clone(), || Ok(self.cf_r1cs.clone()))?; let cf_u_dummy_native = CommittedInstance::::dummy(CF_IO_LEN); let w_dummy_native = Witness::::new( vec![C2::ScalarField::zero(); self.cf_r1cs.A.n_cols - 1 - self.cf_r1cs.l], self.cf_E_len, ); let cf_U_i = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { Ok(self.cf_U_i.unwrap_or_else(|| cf_u_dummy_native.clone())) })?; let cf_W_i = CycleFoldWitnessVar::::new_witness(cs.clone(), || { Ok(self.cf_W_i.unwrap_or(w_dummy_native.clone())) })?; // 5. check Pedersen commitments of cf_U_i.{cmE, cmW} let H = GC2::new_constant(cs.clone(), self.cf_pedersen_params.h)?; let G = Vec::::new_constant(cs.clone(), self.cf_pedersen_params.generators)?; let cf_W_i_E_bits: Vec>>> = cf_W_i .E .iter() .map(|E_i| E_i.to_bits_le().unwrap()) .collect(); let cf_W_i_W_bits: Vec>>> = cf_W_i .W .iter() .map(|W_i| W_i.to_bits_le().unwrap()) .collect(); let computed_cmE = PedersenGadget::::commit( H.clone(), G.clone(), cf_W_i_E_bits, cf_W_i.rE.to_bits_le()?, )?; cf_U_i.cmE.enforce_equal(&computed_cmE)?; let computed_cmW = PedersenGadget::::commit(H, G, cf_W_i_W_bits, cf_W_i.rW.to_bits_le()?)?; cf_U_i.cmW.enforce_equal(&computed_cmW)?; // 6. check RelaxedR1CS of cf_U_i let cf_z_U: Vec>> = [vec![cf_U_i.u.clone()], cf_U_i.x.to_vec(), cf_W_i.W.to_vec()].concat(); RelaxedR1CSGadget::< C2::ScalarField, CF1, NonNativeFieldVar>, >::check(cf_r1cs, cf_W_i.E, cf_U_i.u.clone(), cf_z_U)?; } Ok(()) } } #[cfg(test)] pub mod tests { use super::*; use ark_crypto_primitives::crh::{ sha256::{ constraints::{Sha256Gadget, UnitVar}, Sha256, }, CRHScheme, CRHSchemeGadget, }; use ark_ff::BigInteger; use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; use ark_r1cs_std::{ alloc::AllocVar, bits::uint8::UInt8, eq::EqGadget, fields::{fp::FpVar, nonnative::NonNativeFieldVar}, }; use ark_relations::r1cs::ConstraintSystem; use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; use crate::commitment::pedersen::Pedersen; use crate::folding::nova::ivc::tests::get_pedersen_params_len; use crate::frontend::tests::{CubicFCircuit, CustomFCircuit, WrapperCircuit}; use crate::transcript::poseidon::tests::poseidon_test_config; use crate::ccs::r1cs::{extract_r1cs, extract_w_x}; use crate::ccs::r1cs::{ tests::{get_test_r1cs, get_test_z}, R1CS, }; #[test] fn test_relaxed_r1cs_small_gadget_handcrafted() { let r1cs: R1CS = get_test_r1cs(); let rel_r1cs = r1cs.clone().relax(); let z = get_test_z(3); let cs = ConstraintSystem::::new_ref(); let zVar = Vec::>::new_witness(cs.clone(), || Ok(z)).unwrap(); let EVar = Vec::>::new_witness(cs.clone(), || Ok(rel_r1cs.E)).unwrap(); let uVar = FpVar::::new_witness(cs.clone(), || Ok(rel_r1cs.u)).unwrap(); let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); RelaxedR1CSGadget::>::check(r1csVar, EVar, uVar, zVar).unwrap(); assert!(cs.is_satisfied().unwrap()); } // gets as input a circuit that implements the ConstraintSynthesizer trait, and that has been // initialized. fn test_relaxed_r1cs_gadget>(circuit: CS) { let cs = ConstraintSystem::::new_ref(); circuit.generate_constraints(cs.clone()).unwrap(); cs.finalize(); assert!(cs.is_satisfied().unwrap()); let cs = cs.into_inner().unwrap(); let r1cs = extract_r1cs::(&cs); let (w, x) = extract_w_x::(&cs); let z = [vec![Fr::one()], x, w].concat(); r1cs.check_relation(&z).unwrap(); let relaxed_r1cs = r1cs.clone().relax(); relaxed_r1cs.check_relation(&z).unwrap(); // set new CS for the circuit that checks the RelaxedR1CS of our original circuit let cs = ConstraintSystem::::new_ref(); // prepare the inputs for our circuit let zVar = Vec::>::new_witness(cs.clone(), || Ok(z)).unwrap(); let EVar = Vec::>::new_witness(cs.clone(), || Ok(relaxed_r1cs.E)).unwrap(); let uVar = FpVar::::new_witness(cs.clone(), || Ok(relaxed_r1cs.u)).unwrap(); let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); RelaxedR1CSGadget::>::check(r1csVar, EVar, uVar, zVar).unwrap(); assert!(cs.is_satisfied().unwrap()); } #[test] fn test_relaxed_r1cs_small_gadget_arkworks() { let z_i = vec![Fr::from(3_u32)]; let cubic_circuit = CubicFCircuit::::new(()); let circuit = WrapperCircuit::> { FC: cubic_circuit, z_i: Some(z_i.clone()), z_i1: Some(cubic_circuit.step_native(z_i).unwrap()), }; test_relaxed_r1cs_gadget(circuit); } struct Sha256TestCircuit { _f: PhantomData, pub x: Vec, pub y: Vec, } impl ConstraintSynthesizer for Sha256TestCircuit { fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { let x = Vec::>::new_witness(cs.clone(), || Ok(self.x))?; let y = Vec::>::new_input(cs.clone(), || Ok(self.y))?; let unitVar = UnitVar::default(); let comp_y = as CRHSchemeGadget>::evaluate(&unitVar, &x)?; comp_y.0.enforce_equal(&y)?; Ok(()) } } #[test] fn test_relaxed_r1cs_medium_gadget_arkworks() { let x = Fr::from(5_u32).into_bigint().to_bytes_le(); let y = ::evaluate(&(), x.clone()).unwrap(); let circuit = Sha256TestCircuit:: { _f: PhantomData, x, y, }; test_relaxed_r1cs_gadget(circuit); } #[test] fn test_relaxed_r1cs_custom_circuit() { let n_constraints = 10_000; let custom_circuit = CustomFCircuit::::new(n_constraints); let z_i = vec![Fr::from(5_u32)]; let circuit = WrapperCircuit::> { FC: custom_circuit, z_i: Some(z_i.clone()), z_i1: Some(custom_circuit.step_native(z_i).unwrap()), }; test_relaxed_r1cs_gadget(circuit); } #[test] fn test_relaxed_r1cs_nonnative_circuit() { let cs = ConstraintSystem::::new_ref(); // in practice we would use CycleFoldCircuit, but is a very big circuit (when computed // non-natively inside the RelaxedR1CS circuit), so in order to have a short test we use a // custom circuit. let custom_circuit = CustomFCircuit::::new(10); let z_i = vec![Fq::from(5_u32)]; let circuit = WrapperCircuit::> { FC: custom_circuit, z_i: Some(z_i.clone()), z_i1: Some(custom_circuit.step_native(z_i).unwrap()), }; circuit.generate_constraints(cs.clone()).unwrap(); cs.finalize(); let cs = cs.into_inner().unwrap(); let r1cs = extract_r1cs::(&cs); let (w, x) = extract_w_x::(&cs); let z = [vec![Fq::one()], x, w].concat(); let relaxed_r1cs = r1cs.clone().relax(); // natively let cs = ConstraintSystem::::new_ref(); let zVar = Vec::>::new_witness(cs.clone(), || Ok(z.clone())).unwrap(); let EVar = Vec::>::new_witness(cs.clone(), || Ok(relaxed_r1cs.clone().E)).unwrap(); let uVar = FpVar::::new_witness(cs.clone(), || Ok(relaxed_r1cs.u)).unwrap(); let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs.clone())).unwrap(); RelaxedR1CSGadget::>::check(r1csVar, EVar, uVar, zVar).unwrap(); // non-natively let cs = ConstraintSystem::::new_ref(); let zVar = Vec::>::new_witness(cs.clone(), || Ok(z)).unwrap(); let EVar = Vec::>::new_witness(cs.clone(), || Ok(relaxed_r1cs.E)) .unwrap(); let uVar = NonNativeFieldVar::::new_witness(cs.clone(), || Ok(relaxed_r1cs.u)).unwrap(); let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)) .unwrap(); RelaxedR1CSGadget::>::check(r1csVar, EVar, uVar, zVar) .unwrap(); } #[test] fn test_decider_circuit() { let mut rng = ark_std::test_rng(); let poseidon_config = poseidon_test_config::(); let F_circuit = CubicFCircuit::::new(()); let z_0 = vec![Fr::from(3_u32)]; let (pedersen_len, cf_pedersen_len) = get_pedersen_params_len::>(&poseidon_config, F_circuit).unwrap(); // generate the Pedersen params let pedersen_params = Pedersen::::new_params(&mut rng, pedersen_len); let cf_pedersen_params = Pedersen::::new_params(&mut rng, cf_pedersen_len); // generate an IVC and do a step of it let mut ivc = IVC::< Projective, GVar, Projective2, GVar2, CubicFCircuit, Pedersen, Pedersen, >::new( poseidon_config, pedersen_params, cf_pedersen_params, F_circuit, z_0.clone(), ) .unwrap(); ivc.prove_step().unwrap(); ivc.verify(z_0, 1).unwrap(); // load the DeciderCircuit from the generated IVC let decider_circuit = DeciderCircuit::< Projective, GVar, Projective2, GVar2, Pedersen, Pedersen, >::from_ivc(ivc); let cs = ConstraintSystem::::new_ref(); // generate the constraints and check that are satisfied by the inputs decider_circuit.generate_constraints(cs.clone()).unwrap(); assert!(cs.is_satisfied().unwrap()); dbg!(cs.num_constraints()); } }