/// This file implements the onchain (Ethereum's EVM) decider circuit. For non-ethereum use cases, /// other more efficient approaches can be used. /// More details can be found at the documentation page: /// https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html use ark_crypto_primitives::sponge::{ constraints::CryptographicSpongeVar, poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, Absorb, CryptographicSponge, }; use ark_ec::{CurveGroup, Group}; use ark_ff::{BigInteger, PrimeField}; use ark_poly::Polynomial; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, boolean::Boolean, eq::EqGadget, fields::{fp::FpVar, FieldVar}, groups::GroupOpsBounds, poly::{domain::Radix2DomainVar, evaluations::univariate::EvaluationsVar}, prelude::CurveVar, ToConstraintFieldGadget, }; use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::{log2, Zero}; use core::{borrow::Borrow, marker::PhantomData}; use super::{ circuits::{ChallengeGadget, CommittedInstanceVar}, nifs::NIFS, traits::NIFSTrait, CommittedInstance, Nova, Witness, }; use crate::commitment::{pedersen::Params as PedersenParams, CommitmentScheme}; use crate::folding::circuits::{ cyclefold::{CycleFoldCommittedInstance, CycleFoldWitness}, nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar}, CF1, CF2, }; use crate::frontend::FCircuit; use crate::transcript::{Transcript, TranscriptVar}; use crate::utils::{ gadgets::{MatrixGadget, SparseMatrixVar, VectorGadget}, vec::poly_from_vec, }; use crate::Error; use crate::{ arith::r1cs::R1CS, folding::traits::{CommittedInstanceVarOps, Dummy, WitnessVarOps}, }; #[derive(Debug, Clone)] pub struct RelaxedR1CSGadget {} impl RelaxedR1CSGadget { /// performs the RelaxedR1CS check for native variables (Az∘Bz==uCz+E) pub fn check_native( r1cs: R1CSVar>, E: Vec>, u: FpVar, z: Vec>, ) -> Result<(), SynthesisError> { let Az = r1cs.A.mul_vector(&z)?; let Bz = r1cs.B.mul_vector(&z)?; let Cz = r1cs.C.mul_vector(&z)?; let uCzE = Cz.mul_scalar(&u)?.add(&E)?; let AzBz = Az.hadamard(&Bz)?; AzBz.enforce_equal(&uCzE)?; Ok(()) } /// performs the RelaxedR1CS check for non-native variables (Az∘Bz==uCz+E) pub fn check_nonnative( r1cs: R1CSVar>, E: Vec>, u: NonNativeUintVar, z: Vec>, ) -> Result<(), SynthesisError> { // First we do addition and multiplication without mod F's order let Az = r1cs.A.mul_vector(&z)?; let Bz = r1cs.B.mul_vector(&z)?; let Cz = r1cs.C.mul_vector(&z)?; let uCzE = Cz.mul_scalar(&u)?.add(&E)?; let AzBz = Az.hadamard(&Bz)?; // Then we compare the results by checking if they are congruent // modulo the field order AzBz.into_iter() .zip(uCzE) .try_for_each(|(a, b)| a.enforce_congruent::(&b)) } } #[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: AllocVar, { 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, { 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 }) }) } } impl WitnessVarOps for WitnessVar { fn get_openings(&self) -> Vec<(&[FpVar], FpVar)> { vec![(&self.E, self.rE.clone()), (&self.W, self.rW.clone())] } } /// Circuit that implements the in-circuit checks needed for the onchain (Ethereum's EVM) /// verification. #[derive(Clone, Debug)] pub struct DeciderEthCircuit where C1: CurveGroup, GC1: CurveVar>, C2: CurveGroup, GC2: CurveVar>, CS1: CommitmentScheme, CS2: CommitmentScheme, { _c1: PhantomData, _gc1: PhantomData, _c2: PhantomData, _gc2: PhantomData, _cs1: PhantomData, _cs2: 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>, /// public params hash pub pp_hash: Option, 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>, pub U_i1: Option>, pub W_i1: Option>, pub cmT: Option, pub r: Option, /// CycleFold running instance pub cf_U_i: Option>, pub cf_W_i: Option>, /// KZG challenges pub kzg_c_W: Option, pub kzg_c_E: Option, pub eval_W: Option, pub eval_E: Option, } impl DeciderEthCircuit where C1: CurveGroup, C2: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, GC2: CurveVar> + ToConstraintFieldGadget>, CS1: CommitmentScheme, // enforce that the CS2 is Pedersen commitment scheme, since we're at Ethereum's EVM decider CS2: CommitmentScheme>, ::ScalarField: Absorb, ::BaseField: PrimeField, { /// returns an instance of the DeciderEthCircuit from the given Nova struct pub fn from_nova>( nova: Nova, ) -> Result { let mut transcript = PoseidonSponge::::new(&nova.poseidon_config); // compute the U_{i+1}, W_{i+1} let (aux_p, aux_v) = NIFS::::compute_aux( &nova.cs_pp, &nova.r1cs.clone(), &nova.w_i.clone(), &nova.u_i.clone(), &nova.W_i.clone(), &nova.U_i.clone(), )?; let cmT = aux_v; let r_bits = ChallengeGadget::>::get_challenge_native( &mut transcript, nova.pp_hash, &nova.U_i, &nova.u_i, Some(&cmT), ); let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) .ok_or(Error::OutOfBounds)?; let (W_i1, U_i1) = NIFS::::prove( r_Fr, &nova.W_i, &nova.U_i, &nova.w_i, &nova.u_i, &aux_p, &aux_v, )?; // compute the KZG challenges used as inputs in the circuit let (kzg_challenge_W, kzg_challenge_E) = KZGChallengesGadget::::get_challenges_native(&mut transcript, U_i1.clone()); // get KZG evals let mut W = W_i1.W.clone(); W.extend( std::iter::repeat(C1::ScalarField::zero()) .take(W_i1.W.len().next_power_of_two() - W_i1.W.len()), ); let mut E = W_i1.E.clone(); E.extend( std::iter::repeat(C1::ScalarField::zero()) .take(W_i1.E.len().next_power_of_two() - W_i1.E.len()), ); let p_W = poly_from_vec(W.to_vec())?; let eval_W = p_W.evaluate(&kzg_challenge_W); let p_E = poly_from_vec(E.to_vec())?; let eval_E = p_E.evaluate(&kzg_challenge_E); Ok(Self { _c1: PhantomData, _gc1: PhantomData, _c2: PhantomData, _gc2: PhantomData, _cs1: PhantomData, _cs2: PhantomData, E_len: nova.W_i.E.len(), cf_E_len: nova.cf_W_i.E.len(), r1cs: nova.r1cs, cf_r1cs: nova.cf_r1cs, cf_pedersen_params: nova.cf_cs_pp, poseidon_config: nova.poseidon_config, pp_hash: Some(nova.pp_hash), i: Some(nova.i), z_0: Some(nova.z_0), z_i: Some(nova.z_i), u_i: Some(nova.u_i), w_i: Some(nova.w_i), U_i: Some(nova.U_i), W_i: Some(nova.W_i), U_i1: Some(U_i1), W_i1: Some(W_i1), cmT: Some(cmT), r: Some(r_Fr), cf_U_i: Some(nova.cf_U_i), cf_W_i: Some(nova.cf_W_i), kzg_c_W: Some(kzg_challenge_W), kzg_c_E: Some(kzg_challenge_E), eval_W: Some(eval_W), eval_E: Some(eval_E), }) } } impl ConstraintSynthesizer> for DeciderEthCircuit where C1: CurveGroup, C2: CurveGroup, GC1: CurveVar>, GC2: CurveVar> + ToConstraintFieldGadget>, CS1: CommitmentScheme, CS2: CommitmentScheme, ::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 pp_hash = FpVar::>::new_input(cs.clone(), || { Ok(self.pp_hash.unwrap_or_else(CF1::::zero)) })?; let i = FpVar::>::new_input(cs.clone(), || Ok(self.i.unwrap_or_else(CF1::::zero)))?; let z_0 = Vec::>>::new_input(cs.clone(), || { Ok(self.z_0.unwrap_or(vec![CF1::::zero()])) })?; let z_i = Vec::>>::new_input(cs.clone(), || { Ok(self.z_i.unwrap_or(vec![CF1::::zero()])) })?; let u_dummy_native = CommittedInstance::::dummy(&self.r1cs); let w_dummy_native = Witness::::dummy(&self.r1cs); let u_i = CommittedInstanceVar::::new_witness(cs.clone(), || { Ok(self.u_i.unwrap_or(u_dummy_native.clone())) })?; let U_i = CommittedInstanceVar::::new_witness(cs.clone(), || { Ok(self.U_i.unwrap_or(u_dummy_native.clone())) })?; // here (U_i1, W_i1) = NIFS.P( (U_i,W_i), (u_i,w_i)) let U_i1 = CommittedInstanceVar::::new_input(cs.clone(), || { Ok(self.U_i1.unwrap_or(u_dummy_native.clone())) })?; let W_i1 = WitnessVar::::new_witness(cs.clone(), || { Ok(self.W_i1.unwrap_or(w_dummy_native.clone())) })?; // allocate the inputs for the check 5.1 let kzg_c_W = FpVar::>::new_input(cs.clone(), || { Ok(self.kzg_c_W.unwrap_or_else(CF1::::zero)) })?; let kzg_c_E = FpVar::>::new_input(cs.clone(), || { Ok(self.kzg_c_E.unwrap_or_else(CF1::::zero)) })?; // allocate the inputs for the check 5.2 let eval_W = FpVar::>::new_input(cs.clone(), || { Ok(self.eval_W.unwrap_or_else(CF1::::zero)) })?; let eval_E = FpVar::>::new_input(cs.clone(), || { Ok(self.eval_E.unwrap_or_else(CF1::::zero)) })?; // `sponge` is for digest computation. let sponge = PoseidonSpongeVar::::new(cs.clone(), &self.poseidon_config); // `transcript` is for challenge generation. let mut transcript = sponge.clone(); // The following enumeration of the steps matches the one used at the documentation page // https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html // 2. u_i.cmE==cm(0), u_i.u==1 // Here zero is the x & y coordinates of the zero point affine representation. let zero = NonNativeUintVar::new_constant(cs.clone(), C1::BaseField::zero())?; u_i.cmE.x.enforce_equal_unaligned(&zero)?; u_i.cmE.y.enforce_equal_unaligned(&zero)?; (u_i.u.is_one()?).enforce_equal(&Boolean::TRUE)?; // 3.a u_i.x[0] == H(i, z_0, z_i, U_i) let (u_i_x, U_i_vec) = U_i.clone().hash(&sponge, &pp_hash, &i, &z_0, &z_i)?; (u_i.x[0]).enforce_equal(&u_i_x)?; // 4. check RelaxedR1CS of U_{i+1} let z_U1: Vec>> = [vec![U_i1.u.clone()], U_i1.x.to_vec(), W_i1.W.to_vec()].concat(); RelaxedR1CSGadget::check_native(r1cs, W_i1.E.clone(), U_i1.u.clone(), z_U1)?; #[cfg(feature = "light-test")] log::warn!("[WARNING]: Running with the 'light-test' feature, skipping the big part of the DeciderEthCircuit.\n Only for testing purposes."); // The following two checks (and their respective allocations) are disabled for normal // tests since they take several millions of constraints and would take several minutes // (and RAM) to run the test. It is active by default, and not active only when // 'light-test' feature is used. #[cfg(not(feature = "light-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::{ circuits::cyclefold::{ CycleFoldCommittedInstanceVar, CycleFoldConfig, CycleFoldWitnessVar, }, nova::NovaCycleFoldConfig, }; use ark_r1cs_std::ToBitsGadget; let cf_u_dummy_native = CycleFoldCommittedInstance::::dummy(NovaCycleFoldConfig::::IO_LEN); let w_dummy_native = CycleFoldWitness::::dummy(&self.cf_r1cs); 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())) })?; // 3.b u_i.x[1] == H(cf_U_i) let (cf_u_i_x, _) = cf_U_i.clone().hash(&sponge, pp_hash.clone())?; (u_i.x[1]).enforce_equal(&cf_u_i_x)?; // 7. 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: Result>>>, SynthesisError> = cf_W_i.E.iter().map(|E_i| E_i.to_bits_le()).collect(); let cf_W_i_W_bits: Result>>>, SynthesisError> = cf_W_i.W.iter().map(|W_i| W_i.to_bits_le()).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)?; let cf_r1cs = R1CSVar::, NonNativeUintVar>>::new_witness( cs.clone(), || Ok(self.cf_r1cs.clone()), )?; // 6. check RelaxedR1CS of cf_U_i (CycleFold instance) let cf_z_U = [vec![cf_U_i.u.clone()], cf_U_i.x.to_vec(), cf_W_i.W.to_vec()].concat(); RelaxedR1CSGadget::check_nonnative(cf_r1cs, cf_W_i.E, cf_U_i.u.clone(), cf_z_U)?; } // 1.1.a, 5.1. compute NIFS.V and KZG challenges. // We need to ensure the order of challenge generation is the same as // the native counterpart, so we first compute the challenges here and // do the actual checks later. let cmT = NonNativeAffineVar::new_input(cs.clone(), || Ok(self.cmT.unwrap_or_else(C1::zero)))?; // 1.1.a let r_bits = ChallengeGadget::>::get_challenge_gadget( &mut transcript, pp_hash, U_i_vec, u_i.clone(), Some(cmT), )?; // 5.1. let (incircuit_c_W, incircuit_c_E) = KZGChallengesGadget::::get_challenges_gadget(&mut transcript, U_i1.clone())?; incircuit_c_W.enforce_equal(&kzg_c_W)?; incircuit_c_E.enforce_equal(&kzg_c_E)?; // 5.2. check eval_W==p_W(c_W) and eval_E==p_E(c_E) let incircuit_eval_W = evaluate_gadget::>(W_i1.W, incircuit_c_W)?; let incircuit_eval_E = evaluate_gadget::>(W_i1.E, incircuit_c_E)?; incircuit_eval_W.enforce_equal(&eval_W)?; incircuit_eval_E.enforce_equal(&eval_E)?; // 1.1.b check that the NIFS.V challenge matches the one from the public input (so we avoid // the verifier computing it) let r_Fr = Boolean::le_bits_to_fp_var(&r_bits)?; // check that the in-circuit computed r is equal to the inputted r let r = FpVar::>::new_input(cs.clone(), || Ok(self.r.unwrap_or_else(CF1::::zero)))?; r_Fr.enforce_equal(&r)?; Ok(()) } } /// Interpolates the polynomial from the given vector, and then returns it's evaluation at the /// given point. #[allow(unused)] // unused while check 7 is disabled pub fn evaluate_gadget( mut v: Vec>, point: FpVar, ) -> Result, SynthesisError> { v.resize(v.len().next_power_of_two(), FpVar::zero()); let n = v.len() as u64; let gen = F::get_root_of_unity(n).unwrap(); let domain = Radix2DomainVar::new(gen, log2(v.len()) as u64, FpVar::one()).unwrap(); let evaluations_var = EvaluationsVar::from_vec_and_domain(v, domain, true); evaluations_var.interpolate_and_evaluate(&point) } /// Gadget that computes the KZG challenges, also offers the rust native implementation compatible /// with the gadget. pub struct KZGChallengesGadget { _c: PhantomData, } #[allow(clippy::type_complexity)] impl KZGChallengesGadget where C: CurveGroup, C::ScalarField: PrimeField, ::BaseField: PrimeField, C::ScalarField: Absorb, { pub fn get_challenges_native>( transcript: &mut T, U_i: CommittedInstance, ) -> (C::ScalarField, C::ScalarField) { // compute the KZG challenges, which are computed in-circuit and checked that it matches // the inputted one transcript.absorb_nonnative(&U_i.cmW); let challenge_W = transcript.get_challenge(); transcript.absorb_nonnative(&U_i.cmE); let challenge_E = transcript.get_challenge(); (challenge_W, challenge_E) } // compatible with the native get_challenges_native pub fn get_challenges_gadget, S>>( transcript: &mut T, U_i: CommittedInstanceVar, ) -> Result<(FpVar, FpVar), SynthesisError> { transcript.absorb(&U_i.cmW.to_constraint_field()?)?; let challenge_W = transcript.get_challenge()?; transcript.absorb(&U_i.cmE.to_constraint_field()?)?; let challenge_E = transcript.get_challenge()?; Ok((challenge_W, challenge_E)) } } #[cfg(test)] pub mod tests { use std::cmp::max; use ark_crypto_primitives::crh::{ sha256::{ constraints::{Sha256Gadget, UnitVar}, Sha256, }, CRHScheme, CRHSchemeGadget, }; use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; use ark_r1cs_std::bits::uint8::UInt8; use ark_relations::r1cs::ConstraintSystem; use ark_std::{ rand::{thread_rng, Rng}, UniformRand, }; use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; use super::*; use crate::arith::{ r1cs::{ extract_r1cs, extract_w_x, tests::{get_test_r1cs, get_test_z}, }, Arith, }; use crate::commitment::pedersen::Pedersen; use crate::folding::nova::PreprocessorParam; use crate::frontend::utils::{CubicFCircuit, CustomFCircuit, WrapperCircuit}; use crate::transcript::poseidon::poseidon_canonical_config; use crate::FoldingScheme; // Convert `z` to a witness-instance pair for the relaxed R1CS fn prepare_relaxed_witness_instance, R: Rng>( mut rng: R, r1cs: &R1CS, z: &[C::ScalarField], ) -> (Witness, CommittedInstance) { let (w, x) = r1cs.split_z(z); let (cs_pp, _) = CS::setup(&mut rng, max(w.len(), r1cs.A.n_rows)).unwrap(); let mut w = Witness::new::(w, r1cs.A.n_rows, &mut rng); w.E = r1cs.eval_at_z(z).unwrap(); let mut u = w.commit::(&cs_pp, x).unwrap(); u.u = z[0]; (w, u) } #[test] fn test_relaxed_r1cs_small_gadget_handcrafted() { let rng = &mut thread_rng(); let r1cs: R1CS = get_test_r1cs(); let mut z = get_test_z(3); z[0] = Fr::rand(rng); // Randomize `z[0]` (i.e. `u.u`) to test the relaxed R1CS let (w, u) = prepare_relaxed_witness_instance::<_, Pedersen, _>(rng, &r1cs, &z); let cs = ConstraintSystem::::new_ref(); let zVar = Vec::>::new_witness(cs.clone(), || Ok(z)).unwrap(); let EVar = Vec::>::new_witness(cs.clone(), || Ok(w.E)).unwrap(); let uVar = FpVar::::new_witness(cs.clone(), || Ok(u.u)).unwrap(); let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); RelaxedR1CSGadget::check_native(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 rng = &mut thread_rng(); 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); r1cs.check_relation(&w, &x).unwrap(); let z = [vec![Fr::rand(rng)], x, w].concat(); let (w, u) = prepare_relaxed_witness_instance::<_, Pedersen, _>(rng, &r1cs, &z); r1cs.check_relation(&w, &u).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(w.E)).unwrap(); let uVar = FpVar::::new_witness(cs.clone(), || Ok(u.u)).unwrap(); let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); RelaxedR1CSGadget::check_native(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(()).unwrap(); let circuit = WrapperCircuit::> { FC: cubic_circuit, z_i: Some(z_i.clone()), z_i1: Some(cubic_circuit.step_native(0, z_i, vec![]).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).unwrap(); 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(0, z_i, vec![]).unwrap()), }; test_relaxed_r1cs_gadget(circuit); } #[test] fn test_relaxed_r1cs_nonnative_circuit() { let rng = &mut thread_rng(); 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).unwrap(); 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(0, z_i, vec![]).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::rand(rng)], x, w].concat(); let (w, u) = prepare_relaxed_witness_instance::<_, Pedersen, _>(rng, &r1cs, &z); // 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(w.E.clone())).unwrap(); let uVar = FpVar::::new_witness(cs.clone(), || Ok(u.u)).unwrap(); let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs.clone())).unwrap(); RelaxedR1CSGadget::check_native(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(w.E)).unwrap(); let uVar = NonNativeUintVar::::new_witness(cs.clone(), || Ok(u.u)).unwrap(); let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); RelaxedR1CSGadget::check_nonnative(r1csVar, EVar, uVar, zVar).unwrap(); } #[test] fn test_decider_eth_circuit() { let mut rng = ark_std::test_rng(); let poseidon_config = poseidon_canonical_config::(); let F_circuit = CubicFCircuit::::new(()).unwrap(); let z_0 = vec![Fr::from(3_u32)]; type N = Nova< Projective, GVar, Projective2, GVar2, CubicFCircuit, Pedersen, Pedersen, false, >; let prep_param = PreprocessorParam::< Projective, Projective2, CubicFCircuit, Pedersen, Pedersen, false, >::new(poseidon_config, F_circuit); let nova_params = N::preprocess(&mut rng, &prep_param).unwrap(); // generate a Nova instance and do a step of it let mut nova = N::init(&nova_params, F_circuit, z_0.clone()).unwrap(); nova.prove_step(&mut rng, vec![], None).unwrap(); let ivc_proof = nova.ivc_proof(); N::verify(nova_params.1, ivc_proof).unwrap(); // load the DeciderEthCircuit from the Nova instance let decider_eth_circuit = DeciderEthCircuit::< Projective, GVar, Projective2, GVar2, Pedersen, Pedersen, >::from_nova(nova) .unwrap(); let cs = ConstraintSystem::::new_ref(); // generate the constraints and check that are satisfied by the inputs decider_eth_circuit .generate_constraints(cs.clone()) .unwrap(); assert!(cs.is_satisfied().unwrap()); } // checks that the gadget and native implementations of the challenge computation match #[test] fn test_kzg_challenge_gadget() { let mut rng = ark_std::test_rng(); let poseidon_config = poseidon_canonical_config::(); let mut transcript = PoseidonSponge::::new(&poseidon_config); let U_i = CommittedInstance:: { cmE: Projective::rand(&mut rng), u: Fr::rand(&mut rng), cmW: Projective::rand(&mut rng), x: vec![Fr::rand(&mut rng); 1], }; // compute the challenge natively let (challenge_W, challenge_E) = KZGChallengesGadget::::get_challenges_native(&mut transcript, U_i.clone()); let cs = ConstraintSystem::::new_ref(); let U_iVar = CommittedInstanceVar::::new_witness(cs.clone(), || Ok(U_i.clone())) .unwrap(); let mut transcript_var = PoseidonSpongeVar::::new(cs.clone(), &poseidon_config); let (challenge_W_Var, challenge_E_Var) = KZGChallengesGadget::::get_challenges_gadget(&mut transcript_var, U_iVar) .unwrap(); assert!(cs.is_satisfied().unwrap()); // check that the natively computed and in-circuit computed hashes match use ark_r1cs_std::R1CSVar; assert_eq!(challenge_W_Var.value().unwrap(), challenge_W); assert_eq!(challenge_E_Var.value().unwrap(), challenge_E); } #[test] fn test_polynomial_interpolation() { let mut rng = ark_std::test_rng(); let n = 12; let l = 1 << n; let v: Vec = std::iter::repeat_with(|| Fr::rand(&mut rng)) .take(l) .collect(); let challenge = Fr::rand(&mut rng); use ark_poly::Polynomial; let polynomial = poly_from_vec(v.to_vec()).unwrap(); let eval = polynomial.evaluate(&challenge); let cs = ConstraintSystem::::new_ref(); let vVar = Vec::>::new_witness(cs.clone(), || Ok(v)).unwrap(); let challengeVar = FpVar::::new_witness(cs.clone(), || Ok(challenge)).unwrap(); let evalVar = evaluate_gadget::(vVar, challengeVar).unwrap(); use ark_r1cs_std::R1CSVar; assert_eq!(evalVar.value().unwrap(), eval); assert!(cs.is_satisfied().unwrap()); } }