From f6a70fe1d06bddfd469f8dbda3c0d10c53e082c5 Mon Sep 17 00:00:00 2001 From: arnaucube Date: Tue, 6 Aug 2024 16:54:04 +0200 Subject: [PATCH] implement HyperNova's DeciderEth circuit (#132) The HyperNova's DeciderEthCircuit follows a similar logic as Nova's one described in https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html but adapted to HyperNova checks and values. --- folding-schemes/Cargo.toml | 1 + .../src/folding/circuits/cyclefold.rs | 35 + .../src/folding/circuits/nonnative/affine.rs | 2 +- .../folding/hypernova/decider_eth_circuit.rs | 639 ++++++++++++++++++ folding-schemes/src/folding/hypernova/mod.rs | 1 + .../src/folding/nova/decider_eth_circuit.rs | 42 +- folding-schemes/src/folding/nova/mod.rs | 5 +- folding-schemes/src/utils/gadgets.rs | 32 + 8 files changed, 719 insertions(+), 38 deletions(-) create mode 100644 folding-schemes/src/folding/hypernova/decider_eth_circuit.rs diff --git a/folding-schemes/Cargo.toml b/folding-schemes/Cargo.toml index 7a133ed..48ce64b 100644 --- a/folding-schemes/Cargo.toml +++ b/folding-schemes/Cargo.toml @@ -30,6 +30,7 @@ serde_json = "1.0.85" serde = "1.0.203" acvm = { git = "https://github.com/noir-lang/noir", rev="2b4853e", default-features = false } arkworks_backend = { git = "https://github.com/dmpierre/arkworks_backend", branch="feat/sonobe-integration" } +log = "0.4" # tmp import for espresso's sumcheck espresso_subroutines = {git="https://github.com/EspressoSystems/hyperplonk", package="subroutines"} diff --git a/folding-schemes/src/folding/circuits/cyclefold.rs b/folding-schemes/src/folding/circuits/cyclefold.rs index 285b36a..27bb75e 100644 --- a/folding-schemes/src/folding/circuits/cyclefold.rs +++ b/folding-schemes/src/folding/circuits/cyclefold.rs @@ -215,6 +215,41 @@ where } } +/// In-circuit representation of the Witness associated to the CommittedInstance, but with +/// non-native representation, since it is used to represent the CycleFold witness. This struct is +/// used in the Decider circuit. +#[derive(Debug, Clone)] +pub struct CycleFoldWitnessVar { + pub E: Vec>>, + pub rE: NonNativeUintVar>, + pub W: Vec>>, + pub rW: NonNativeUintVar>, +} + +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::new_variable(cs.clone(), || Ok(val.borrow().E.clone()), mode)?; + let rE = NonNativeUintVar::new_variable(cs.clone(), || Ok(val.borrow().rE), mode)?; + + let W = Vec::new_variable(cs.clone(), || Ok(val.borrow().W.clone()), mode)?; + let rW = NonNativeUintVar::new_variable(cs.clone(), || Ok(val.borrow().rW), mode)?; + + Ok(Self { E, rE, W, rW }) + }) + } +} + /// 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. diff --git a/folding-schemes/src/folding/circuits/nonnative/affine.rs b/folding-schemes/src/folding/circuits/nonnative/affine.rs index f4ec661..d172ac3 100644 --- a/folding-schemes/src/folding/circuits/nonnative/affine.rs +++ b/folding-schemes/src/folding/circuits/nonnative/affine.rs @@ -57,7 +57,7 @@ impl ToConstraintFieldGadget for NonNativeAffineV /// The out-circuit counterpart of `NonNativeAffineVar::to_constraint_field` #[allow(clippy::type_complexity)] -fn nonnative_affine_to_field_elements( +pub(crate) fn nonnative_affine_to_field_elements( p: C, ) -> (Vec, Vec) { let affine = p.into_affine(); diff --git a/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs b/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs new file mode 100644 index 0000000..93ba6bb --- /dev/null +++ b/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs @@ -0,0 +1,639 @@ +/// 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::sponge::{ + constraints::CryptographicSpongeVar, + poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, + Absorb, CryptographicSponge, +}; +use ark_ec::{CurveGroup, Group}; +use ark_ff::PrimeField; +use ark_poly::Polynomial; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + boolean::Boolean, + eq::EqGadget, + fields::fp::FpVar, + groups::GroupOpsBounds, + prelude::CurveVar, + ToConstraintFieldGadget, +}; +use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::Zero; +use core::{borrow::Borrow, marker::PhantomData}; + +use super::{ + circuits::{CCCSVar, LCCCSVar, NIMFSGadget, ProofVar as NIMFSProofVar}, + nimfs::{NIMFSProof, NIMFS}, + HyperNova, Witness, CCCS, LCCCS, +}; +use crate::arith::ccs::CCS; +use crate::arith::r1cs::R1CS; +use crate::commitment::{pedersen::Params as PedersenParams, CommitmentScheme}; +use crate::folding::circuits::{ + cyclefold::{CycleFoldCommittedInstance, CycleFoldWitness}, + CF1, CF2, +}; +use crate::frontend::FCircuit; +use crate::transcript::{Transcript, TranscriptVar}; +use crate::utils::{ + gadgets::{eval_mle, MatrixGadget, SparseMatrixVar}, + vec::poly_from_vec, +}; +use crate::Error; + +/// In-circuit representation of the Witness associated to the CommittedInstance. +#[derive(Debug, Clone)] +pub struct WitnessVar { + pub w: Vec>, + pub r_w: FpVar, +} + +impl AllocVar, F> for WitnessVar { + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + + let w: Vec> = + Vec::new_variable(cs.clone(), || Ok(val.borrow().w.clone()), mode)?; + let r_w = FpVar::::new_variable(cs.clone(), || Ok(val.borrow().r_w), mode)?; + + Ok(Self { w, r_w }) + }) + } +} + +/// CCSMatricesVar contains the matrices 'M' of the CCS without the rest of CCS parameters. +#[derive(Debug, Clone)] +pub struct CCSMatricesVar { + // we only need native representation, so the constraint field==F + pub M: Vec>>, +} + +impl AllocVar, F> for CCSMatricesVar +where + F: PrimeField, +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + _mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + + let M: Vec>> = val + .borrow() + .M + .iter() + .map(|M| SparseMatrixVar::>::new_constant(cs.clone(), M.clone())) + .collect::>()?; + + Ok(Self { M }) + }) + } +} + +/// Gadget to check the LCCCS relation both over the native constraint field and over the +/// non-native constraint field. +#[derive(Debug, Clone)] +pub struct LCCCSCheckerGadget {} +impl LCCCSCheckerGadget { + /// performs in-circuit the RelaxedR1CS check for native variables (Az∘Bz==uCz+E) + pub fn check( + s: usize, + ccs_mat: CCSMatricesVar, + z: Vec>, + // LCCCS values + r_x: Vec>, + v: Vec>, + ) -> Result<(), SynthesisError> { + let computed_v: Vec> = ccs_mat + .M + .iter() + .map(|M_j| { + let Mz = M_j.mul_vector(&z)?; + Ok(eval_mle(s, Mz, r_x.clone())) + }) + .collect::>, SynthesisError>>()?; + + (computed_v).enforce_equal(&v)?; + Ok(()) + } +} + +/// Circuit that implements the in-circuit checks needed for the HyperNova's 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 CycleFold instance witness + pub cf_E_len: usize, + /// CCS of the Augmented Function circuit + pub ccs: CCS, + /// 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 nimfs_proof: Option>, + // rho_0 is the first and only rho in the 'rho_powers' array, since it comes from NIMFS-folding + // only 2 instances. + pub rho_0: Option, + /// CycleFold running instance + pub cf_U_i: Option>, + pub cf_W_i: Option>, + + /// KZG challenge & eval + pub kzg_challenge: Option, + pub eval_W: 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 HyperNova struct + pub fn from_hypernova, const MU: usize, const NU: usize>( + hn: HyperNova, + ) -> Result { + // compute the U_{i+1}, W_{i+1}, by folding the last running & incoming instances + let mut transcript = PoseidonSponge::::new(&hn.poseidon_config); + transcript.absorb(&hn.pp_hash); + let (nimfs_proof, U_i1, W_i1, rho_powers) = + NIMFS::>::prove( + &mut transcript, + &hn.ccs, + &[hn.U_i.clone()], + &[hn.u_i.clone()], + &[hn.W_i.clone()], + &[hn.w_i.clone()], + )?; + + // compute the KZG challenges used as inputs in the circuit + let kzg_challenge = + KZGChallengeGadget::::get_challenge_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 p_W = poly_from_vec(W.to_vec())?; + let eval_W = p_W.evaluate(&kzg_challenge); + + // ensure that we only have 1 element in rho_powers, since we only NIMFS-fold 2 instances + if rho_powers.len() != 1 { + return Err(Error::NotExpectedLength(rho_powers.len(), 1)); + } + + Ok(Self { + _c1: PhantomData, + _gc1: PhantomData, + _c2: PhantomData, + _gc2: PhantomData, + _cs1: PhantomData, + _cs2: PhantomData, + + cf_E_len: hn.cf_W_i.E.len(), + ccs: hn.ccs, + cf_r1cs: hn.cf_r1cs, + cf_pedersen_params: hn.cf_cs_params, + poseidon_config: hn.poseidon_config, + pp_hash: Some(hn.pp_hash), + i: Some(hn.i), + z_0: Some(hn.z_0), + z_i: Some(hn.z_i), + U_i: Some(hn.U_i), + W_i: Some(hn.W_i), + u_i: Some(hn.u_i), + w_i: Some(hn.w_i), + U_i1: Some(U_i1), + W_i1: Some(W_i1), + nimfs_proof: Some(nimfs_proof), + rho_0: Some(rho_powers[0]), + cf_U_i: Some(hn.cf_U_i), + cf_W_i: Some(hn.cf_W_i), + kzg_challenge: Some(kzg_challenge), + eval_W: Some(eval_W), + }) + } +} + +impl ConstraintSynthesizer> + for DeciderEthCircuit +where + C1: CurveGroup, + C2: CurveGroup, + GC1: CurveVar>, + GC2: CurveVar> + ToConstraintFieldGadget>, + CS1: CommitmentScheme, + CS2: CommitmentScheme, + C1::ScalarField: PrimeField, + ::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 ccs_matrices = + CCSMatricesVar::::new_witness(cs.clone(), || Ok(self.ccs.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 = LCCCS::::dummy(self.ccs.l, self.ccs.t, self.ccs.s); + let u_dummy_native = CCCS::::dummy(self.ccs.l); + let w_dummy_native = Witness::::new( + vec![C1::ScalarField::zero(); self.ccs.n - 3 /* (3=2+1, since u_i.x.len=2) */], + ); + + let U_i = LCCCSVar::::new_witness(cs.clone(), || { + Ok(self.U_i.unwrap_or(U_dummy_native.clone())) + })?; + let u_i = CCCSVar::::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 = LCCCSVar::::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())) + })?; + let nimfs_proof_dummy = NIMFSProof::::dummy(&self.ccs, 1, 1); // mu=1 & nu=1 because the last fold is 2-to-1 + let nimfs_proof = NIMFSProofVar::::new_witness(cs.clone(), || { + Ok(self.nimfs_proof.unwrap_or(nimfs_proof_dummy)) + })?; + + // allocate the inputs for the check 6 + let kzg_challenge = FpVar::>::new_input(cs.clone(), || { + Ok(self.kzg_challenge.unwrap_or_else(CF1::::zero)) + })?; + let _eval_W = FpVar::>::new_input(cs.clone(), || { + Ok(self.eval_W.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(); + transcript.absorb(&pp_hash)?; + + // NOTE: we use the same enumeration as in Nova's DeciderEthCircuit described at + // https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html + // in order to make it easier to reason about. + + // 1. check LCCCS relation of U_{i+1} + let z_U1: Vec>> = + [vec![U_i1.u.clone()], U_i1.x.to_vec(), W_i1.w.to_vec()].concat(); + LCCCSCheckerGadget::check( + self.ccs.s, + ccs_matrices, + z_U1, + U_i1.r_x.clone(), + U_i1.v.clone(), + )?; + + // 3.a u_i.x[0] == H(i, z_0, z_i, U_i) + let (u_i_x, _) = U_i.clone().hash( + &sponge, + pp_hash.clone(), + i.clone(), + z_0.clone(), + z_i.clone(), + )?; + (u_i.x[0]).enforce_equal(&u_i_x)?; + + #[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::nonnative::uint::NonNativeUintVar; + use crate::folding::nova::decider_eth_circuit::{R1CSVar, RelaxedR1CSGadget}; + 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 cf_w_dummy_native = CycleFoldWitness::::dummy( + 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(cf_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)?; + + // 4. 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()), + )?; + + // 5. check RelaxedR1CS of cf_U_i + 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)?; + } + + // The following steps are in non-increasing order because the `computed_U_i1` is computed + // at step 8, and later used at step 6. Notice that in Nova, we compute U_i1 outside of the + // circuit, in the smart contract, but here we're computing it in-circuit, and we reuse the + // `rho_vec` computed along the way of computing `computed_U_i1` for the later `rho_powers` + // check (6.b). + + // Check 7 is temporary disabled due + // https://github.com/privacy-scaling-explorations/sonobe/issues/80 + log::warn!("[WARNING]: issue #80 (https://github.com/privacy-scaling-explorations/sonobe/issues/80) is not resolved yet."); + // + // 7. check eval_W==p_W(c_W) + // let incircuit_eval_W = evaluate_gadget::>(W_i1.W, incircuit_c_W)?; + // incircuit_eval_W.enforce_equal(&eval_W)?; + + // 8.a verify the NIMFS.V of the final fold, and check that the obtained rho_powers from the + // transcript match the one from the public input (so we avoid the onchain logic of the + // verifier computing it). + // Notice that the NIMFSGadget performs all the logic except of checking the fold of the + // instances C parameter, which would require non-native arithmetic, henceforth we perform + // that check outside the circuit. + let (computed_U_i1, rho_vec) = NIMFSGadget::::verify( + cs.clone(), + &self.ccs.clone(), + &mut transcript, + &[U_i], + &[u_i], + nimfs_proof, + Boolean::TRUE, // enabled + )?; + + // 6.a check KZG challenges + // Notice that this step is done after the NIMFS.V, to follow the transcript absorbs order + // done outside the circuit, where to compute the challenge it needs first to compute the + // U_{i+1} through the NIMFS.V + let incircuit_challenge = + KZGChallengeGadget::::get_challenge_gadget(&mut transcript, U_i1.clone())?; + incircuit_challenge.enforce_equal(&kzg_challenge)?; + + // 6.b check that the obtained U_{i+1} from the NIMFS.V matches the U_{i+1} from the input, + // except for the C parameter, which to compute its folding would require non-native logic + // in-circuit, and we defer it to outside the circuit. + computed_U_i1.u.enforce_equal(&U_i1.u)?; + computed_U_i1.r_x.enforce_equal(&U_i1.r_x)?; + computed_U_i1.v.enforce_equal(&U_i1.v)?; + + // 8.b check that the in-circuit computed r is equal to the inputted r. + // Notice that rho_vec only contains one element, since at the final fold we are only + // folding 2-to-1 instances. + + // Ensure that rho_vec is of length 1, note that this is not enforced at the constraint + // level but more as a check for the prover. + if rho_vec.len() != 1 { + return Err(SynthesisError::Unsatisfiable); + } + + let rho_0 = Boolean::le_bits_to_fp_var(&rho_vec[0])?; + let external_rho_0 = FpVar::>::new_input(cs.clone(), || { + Ok(self.rho_0.unwrap_or(CF1::::zero())) + })?; + rho_0.enforce_equal(&external_rho_0)?; + + Ok(()) + } +} + +/// Gadget that computes the KZG challenges, also offers the rust native implementation compatible +/// with the gadget. +pub struct KZGChallengeGadget { + _c: PhantomData, +} +#[allow(clippy::type_complexity)] +impl KZGChallengeGadget +where + C: CurveGroup, + C::ScalarField: PrimeField, + ::BaseField: PrimeField, + C::ScalarField: Absorb, +{ + pub fn get_challenge_native>( + transcript: &mut T, + U_i: LCCCS, + ) -> Result { + // compute the KZG challenges, which are computed in-circuit and checked that it matches + // the inputted one + transcript.absorb_nonnative(&U_i.C); + Ok(transcript.get_challenge()) + } + // compatible with the native get_challenge_native + pub fn get_challenge_gadget, S>>( + transcript: &mut T, + U_i: LCCCSVar, + ) -> Result, SynthesisError> { + transcript.absorb(&U_i.C.to_constraint_field()?)?; + transcript.get_challenge() + } +} + +#[cfg(test)] +pub mod tests { + use ark_bn254::{constraints::GVar, Fr, G1Projective as Projective}; + use ark_grumpkin::{constraints::GVar as GVar2, Projective as Projective2}; + use ark_relations::r1cs::ConstraintSystem; + use ark_std::One; + use ark_std::{test_rng, UniformRand}; + + use super::*; + use crate::commitment::pedersen::Pedersen; + use crate::folding::nova::PreprocessorParam; + use crate::frontend::tests::CubicFCircuit; + use crate::transcript::poseidon::poseidon_canonical_config; + use crate::FoldingScheme; + + #[test] + fn test_lcccs_checker_gadget() { + let mut rng = test_rng(); + let n_rows = 2_u32.pow(5) as usize; + let n_cols = 2_u32.pow(5) as usize; + let r1cs = R1CS::::rand(&mut rng, n_rows, n_cols); + let ccs = CCS::from_r1cs(r1cs); + let z: Vec = (0..n_cols).map(|_| Fr::rand(&mut rng)).collect(); + + let (pedersen_params, _) = + Pedersen::::setup(&mut rng, ccs.n - ccs.l - 1).unwrap(); + + let (lcccs, _) = ccs + .to_lcccs::<_, Projective, Pedersen, false>(&mut rng, &pedersen_params, &z) + .unwrap(); + + let cs = ConstraintSystem::::new_ref(); + + // CCS's (sparse) matrices are constants in the circuit + let ccs_mat = CCSMatricesVar::::new_constant(cs.clone(), ccs.clone()).unwrap(); + let zVar = Vec::>::new_input(cs.clone(), || Ok(z)).unwrap(); + let r_xVar = Vec::>::new_input(cs.clone(), || Ok(lcccs.r_x)).unwrap(); + let vVar = Vec::>::new_input(cs.clone(), || Ok(lcccs.v)).unwrap(); + + LCCCSCheckerGadget::check(ccs.s, ccs_mat, zVar, r_xVar, vVar).unwrap(); + + assert!(cs.is_satisfied().unwrap()); + } + + #[test] + fn test_decider_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)]; + + const MU: usize = 1; + const NU: usize = 1; + + type HN = HyperNova< + Projective, + GVar, + Projective2, + GVar2, + CubicFCircuit, + Pedersen, + Pedersen, + MU, + NU, + false, + >; + let prep_param = PreprocessorParam::< + Projective, + Projective2, + CubicFCircuit, + Pedersen, + Pedersen, + false, + >::new(poseidon_config, F_circuit); + let hn_params = HN::preprocess(&mut rng, &prep_param).unwrap(); + + // generate a Nova instance and do a step of it + let mut hypernova = HN::init(&hn_params, F_circuit, z_0.clone()).unwrap(); + hypernova + .prove_step(&mut rng, vec![], Some((vec![], vec![]))) + .unwrap(); + + let ivc_v = hypernova.clone(); + let (running_instance, incoming_instance, cyclefold_instance) = ivc_v.instances(); + HN::verify( + hn_params.1, // HN's verifier_params + z_0, + ivc_v.z_i, + Fr::one(), + running_instance, + incoming_instance, + cyclefold_instance, + ) + .unwrap(); + + // load the DeciderEthCircuit from the generated Nova instance + let decider_circuit = DeciderEthCircuit::< + Projective, + GVar, + Projective2, + GVar2, + Pedersen, + Pedersen, + >::from_hypernova(hypernova) + .unwrap(); + + 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()); + } +} diff --git a/folding-schemes/src/folding/hypernova/mod.rs b/folding-schemes/src/folding/hypernova/mod.rs index 4f1c054..309e9a7 100644 --- a/folding-schemes/src/folding/hypernova/mod.rs +++ b/folding-schemes/src/folding/hypernova/mod.rs @@ -10,6 +10,7 @@ use ark_std::{fmt::Debug, marker::PhantomData, rand::RngCore, One, Zero}; pub mod cccs; pub mod circuits; +pub mod decider_eth_circuit; pub mod lcccs; pub mod nimfs; pub mod utils; diff --git a/folding-schemes/src/folding/nova/decider_eth_circuit.rs b/folding-schemes/src/folding/nova/decider_eth_circuit.rs index eafe183..0a86a68 100644 --- a/folding-schemes/src/folding/nova/decider_eth_circuit.rs +++ b/folding-schemes/src/folding/nova/decider_eth_circuit.rs @@ -160,40 +160,6 @@ where } } -/// In-circuit representation of the Witness associated to the CycleFoldCommittedInstance, 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: NonNativeUintVar>, - pub W: Vec>>, - pub rW: NonNativeUintVar>, -} - -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::new_variable(cs.clone(), || Ok(val.borrow().E.clone()), mode)?; - let rE = NonNativeUintVar::new_variable(cs.clone(), || Ok(val.borrow().rE), mode)?; - - let W = Vec::new_variable(cs.clone(), || Ok(val.borrow().W.clone()), mode)?; - let rW = NonNativeUintVar::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. #[derive(Clone, Debug)] @@ -262,6 +228,7 @@ where ::ScalarField: Absorb, ::BaseField: PrimeField, { + /// returns an instance of the DeciderEthCircuit from the given Nova struct pub fn from_nova>( nova: Nova, ) -> Result { @@ -441,7 +408,7 @@ where (u_i.x[0]).enforce_equal(&u_i_x)?; #[cfg(feature = "light-test")] - println!("[WARNING]: Running with the 'light-test' feature, skipping the big part of the DeciderEthCircuit.\n Only for testing purposes."); + 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 @@ -453,7 +420,9 @@ where // `#[cfg(not(test))]` use crate::commitment::pedersen::PedersenGadget; use crate::folding::{ - circuits::cyclefold::{CycleFoldCommittedInstanceVar, CycleFoldConfig}, + circuits::cyclefold::{ + CycleFoldCommittedInstanceVar, CycleFoldConfig, CycleFoldWitnessVar, + }, nova::NovaCycleFoldConfig, }; use ark_r1cs_std::ToBitsGadget; @@ -527,6 +496,7 @@ where // Check 7 is temporary disabled due // https://github.com/privacy-scaling-explorations/sonobe/issues/80 + log::warn!("[WARNING]: issue #80 (https://github.com/privacy-scaling-explorations/sonobe/issues/80) is not resolved yet."); // // 7. 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)?; diff --git a/folding-schemes/src/folding/nova/mod.rs b/folding-schemes/src/folding/nova/mod.rs index 96f3d57..f1ecaaf 100644 --- a/folding-schemes/src/folding/nova/mod.rs +++ b/folding-schemes/src/folding/nova/mod.rs @@ -42,12 +42,15 @@ use circuits::{AugmentedFCircuit, ChallengeGadget}; use nifs::NIFS; use traits::NovaR1CS; -struct NovaCycleFoldConfig { +pub struct NovaCycleFoldConfig { _c: PhantomData, } impl CycleFoldConfig for NovaCycleFoldConfig { const RANDOMNESS_BIT_LENGTH: usize = NOVA_N_BITS_RO; + // 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 N_INPUT_POINTS: usize = 2; type C = C; type F = C::BaseField; diff --git a/folding-schemes/src/utils/gadgets.rs b/folding-schemes/src/utils/gadgets.rs index 6c22918..16472da 100644 --- a/folding-schemes/src/utils/gadgets.rs +++ b/folding-schemes/src/utils/gadgets.rs @@ -107,3 +107,35 @@ impl MatrixGadget> for SparseMatrixVar> { .collect()) } } + +/// Interprets the given vector v as the evaluations of a dense multilinear extension of n_vars, +/// and evaluates it at the given point. This method mimics the behavior of +/// `utils/mle.rs#dense_vec_to_dense_mle` + `DenseMultilinearExtension::evaluate` but in R1CS +/// constraints, since dense multilinear extensions are not supported in ark_r1cs_std. +pub fn eval_mle( + // n_vars indicates the number of variables in the MLE + n_vars: usize, + // v is the vector of the evaluations of the dense multilinear extension (MLE) + v: Vec>, + // point is the point at which we want to evaluate the MLE + point: Vec>, +) -> FpVar { + // pad to 2^n_vars + let mut poly: Vec> = [ + v.to_owned(), + std::iter::repeat(FpVar::zero()) + .take((1 << n_vars) - v.len()) + .collect(), + ] + .concat(); + + for i in 1..n_vars + 1 { + let r = point[i - 1].clone(); + for b in 0..(1 << (n_vars - 1)) { + let left = poly[b << 1].clone(); + let right = poly[(b << 1) + 1].clone(); + poly[b] = left.clone() + r.clone() * (right - left); + } + } + poly[0].clone() +}