From 1e9c13f8521a3a9f6f72835681cf8890fbf0ea8d Mon Sep 17 00:00:00 2001 From: arnaucube Date: Tue, 24 Sep 2024 18:04:35 +0200 Subject: [PATCH] Implement Nova's Offchain-Decider circuits (#160) * Implement Nova's Offchain-Decider circuits (on both curves) (curve1 circuit: ~152k constraints, curve2 circuit: ~8k constraints) following the enumeration of the Offchain Decider docs: https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-offchain.html * Update enumeration of checks in Onchain-Decider circuit (decider_eth_circuit.rs) to match the updated Onchain Decider docs: https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html --- .../src/folding/circuits/cyclefold.rs | 4 +- folding-schemes/src/folding/circuits/mod.rs | 13 +- .../src/folding/nova/decider_circuits.rs | 570 ++++++++++++++++++ .../src/folding/nova/decider_eth.rs | 2 + .../src/folding/nova/decider_eth_circuit.rs | 44 +- folding-schemes/src/folding/nova/mod.rs | 13 +- 6 files changed, 617 insertions(+), 29 deletions(-) create mode 100644 folding-schemes/src/folding/nova/decider_circuits.rs diff --git a/folding-schemes/src/folding/circuits/cyclefold.rs b/folding-schemes/src/folding/circuits/cyclefold.rs index 2e293e8..dc22061 100644 --- a/folding-schemes/src/folding/circuits/cyclefold.rs +++ b/folding-schemes/src/folding/circuits/cyclefold.rs @@ -61,10 +61,10 @@ where 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 = NonNativeUintVar::new_variable(cs.clone(), || Ok(val.borrow().u), mode)?; let x = Vec::new_variable(cs.clone(), || Ok(val.borrow().x.clone()), mode)?; + 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 { cmE, u, cmW, x }) }) diff --git a/folding-schemes/src/folding/circuits/mod.rs b/folding-schemes/src/folding/circuits/mod.rs index 40d6b86..b442b93 100644 --- a/folding-schemes/src/folding/circuits/mod.rs +++ b/folding-schemes/src/folding/circuits/mod.rs @@ -7,10 +7,13 @@ pub mod nonnative; pub mod sum_check; pub mod utils; -/// CF1 represents the ConstraintField used for the main folding circuit which is over E1::Fr, where -/// E1 is the main curve where we do the folding. +/// CF1 uses the ScalarField of the given C. CF1 represents the ConstraintField used for the main +/// folding circuit which is over E1::Fr, where E1 is the main curve where we do the folding. +/// In CF1, the points of C can not be natively represented. pub type CF1 = <::Affine as AffineRepr>::ScalarField; -/// CF2 represents the ConstraintField used for the CycleFold circuit which is over E2::Fr=E1::Fq, -/// where E2 is the auxiliary curve (from [CycleFold](https://eprint.iacr.org/2023/1192.pdf) -/// approach) where we check the folding of the commitments (elliptic curve points). +/// CF2 uses the BaseField of the given C. CF2 represents the ConstraintField used for the +/// CycleFold circuit which is over E2::Fr=E1::Fq, where E2 is the auxiliary curve (from +/// [CycleFold](https://eprint.iacr.org/2023/1192.pdf) approach) where we check the folding of the +/// commitments (elliptic curve points). +/// In CF2, the points of C can be natively represented. pub type CF2 = <::BaseField as Field>::BasePrimeField; diff --git a/folding-schemes/src/folding/nova/decider_circuits.rs b/folding-schemes/src/folding/nova/decider_circuits.rs new file mode 100644 index 0000000..31d7d60 --- /dev/null +++ b/folding-schemes/src/folding/nova/decider_circuits.rs @@ -0,0 +1,570 @@ +/// This file implements the offchain decider circuit. For ethereum use cases, use the +/// DeciderEthCircuit. +/// More details can be found at the documentation page: +/// https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-offchain.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, + boolean::Boolean, + eq::EqGadget, + fields::{fp::FpVar, FieldVar}, + groups::GroupOpsBounds, + prelude::CurveVar, + ToConstraintFieldGadget, +}; +use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; +use ark_std::Zero; +use core::marker::PhantomData; + +use super::{ + circuits::{ChallengeGadget, CommittedInstanceVar}, + decider_eth_circuit::{KZGChallengesGadget, R1CSVar, RelaxedR1CSGadget, WitnessVar}, + nifs::NIFS, + CommittedInstance, Nova, Witness, +}; +use crate::arith::r1cs::R1CS; +use crate::commitment::CommitmentScheme; +use crate::folding::circuits::{ + cyclefold::{ + CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar, CycleFoldConfig, + CycleFoldWitness, + }, + nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar}, + CF1, CF2, +}; +use crate::folding::nova::NovaCycleFoldConfig; +use crate::folding::traits::CommittedInstanceVarOps; +use crate::frontend::FCircuit; +use crate::utils::vec::poly_from_vec; +use crate::Error; + +/// Circuit that implements part of the in-circuit checks needed for the offchain verification over +/// the Curve2's BaseField (=Curve1's ScalarField). +#[derive(Clone, Debug)] +pub struct DeciderCircuit1 +where + C1: CurveGroup, + C2: CurveGroup, + GC2: CurveVar>, +{ + _c1: PhantomData, + _c2: PhantomData, + _gc2: 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, + 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>, + + /// KZG challenges + pub kzg_c_W: Option, + pub kzg_c_E: Option, + pub eval_W: Option, + pub eval_E: Option, +} +impl DeciderCircuit1 +where + C1: CurveGroup, + ::BaseField: PrimeField, + ::ScalarField: Absorb, + C2: CurveGroup, + GC2: CurveVar> + ToConstraintFieldGadget>, + for<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>, +{ + pub fn from_nova>( + nova: Nova, + ) -> Result + where + C2: CurveGroup, + GC1: CurveVar> + ToConstraintFieldGadget>, + GC2: CurveVar> + ToConstraintFieldGadget>, + CS1: CommitmentScheme, + CS2: CommitmentScheme, + { + let mut transcript = PoseidonSponge::::new(&nova.poseidon_config); + // pp_hash is absorbed to transcript at the ChallengeGadget::get_challenge_native call + + // compute the U_{i+1}, W_{i+1} + let (T, cmT) = NIFS::::compute_cmT( + &nova.cs_pp, + &nova.r1cs.clone(), + &nova.w_i.clone(), + &nova.u_i.clone(), + &nova.W_i.clone(), + &nova.U_i.clone(), + )?; + let r_bits = ChallengeGadget::::get_challenge_native( + &mut transcript, + nova.pp_hash, + nova.U_i.clone(), + nova.u_i.clone(), + cmT, + ); + let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) + .ok_or(Error::OutOfBounds)?; + let (W_i1, U_i1) = NIFS::::fold_instances( + r_Fr, &nova.W_i, &nova.U_i, &nova.w_i, &nova.u_i, &T, cmT, + )?; + + // 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, + _c2: PhantomData, + _gc2: PhantomData, + + E_len: nova.W_i.E.len(), + cf_E_len: nova.cf_W_i.E.len(), + r1cs: nova.r1cs, + 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), + 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 DeciderCircuit1 +where + C1: CurveGroup, + ::BaseField: PrimeField, + ::ScalarField: Absorb, + C2: CurveGroup, + ::BaseField: PrimeField, + ::ScalarField: Absorb, + C1: CurveGroup, + GC2: CurveVar> + ToConstraintFieldGadget>, + 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(2); + let w_dummy_native = Witness::::dummy( + self.r1cs.A.n_cols - 3, /* (3=2+1, since u_i.x.len=2) */ + self.E_len, + ); + + 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 6 + 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)) + })?; + 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(); + // notice that the `pp_hash` is absorbed inside the ChallengeGadget::get_challenge_gadget call + + // 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)?; + + // 3.b u_i.x[1] == H(cf_U_i) + let cf_u_dummy_native = + CycleFoldCommittedInstance::::dummy(NovaCycleFoldConfig::::IO_LEN); + let cf_U_i = CycleFoldCommittedInstanceVar::::new_input(cs.clone(), || { + Ok(self.cf_U_i.unwrap_or_else(|| cf_u_dummy_native.clone())) + })?; + 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 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)?; + + // 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)))?; + let r_bits = ChallengeGadget::::get_challenge_gadget( + &mut transcript, + pp_hash, + U_i_vec, + u_i.clone(), + cmT.clone(), + )?; + // 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)?; + + // Check 5.2 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."); + // + // 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(()) + } +} + +/// Circuit that implements part of the in-circuit checks needed for the offchain verification over +/// the Curve1's BaseField (=Curve2's ScalarField). +#[derive(Clone, Debug)] +pub struct DeciderCircuit2 +where + C1: CurveGroup, + C2: CurveGroup, +{ + _c1: PhantomData, + _gc1: PhantomData, + _c2: PhantomData, + + /// E vector's length of the CycleFold instance witness + pub cf_E_len: usize, + /// R1CS of the CycleFold circuit + pub cf_r1cs: R1CS, + pub poseidon_config: PoseidonConfig>, + /// public params hash + pub pp_hash: Option, + + /// CycleFold running instance. Notice that here we use Nova's CommittedInstance (instead of + /// CycleFoldCommittedInstance), since we are over C2::Fr, so that the CycleFold instances can + /// be computed natively + 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 DeciderCircuit2 +where + C1: CurveGroup, + C2: CurveGroup, + ::BaseField: PrimeField, + ::ScalarField: Absorb, + ::BaseField: PrimeField, + ::ScalarField: Absorb, + GC1: CurveVar> + ToConstraintFieldGadget>, +{ + pub fn from_nova>( + nova: Nova, + ) -> Result + where + GC2: CurveVar> + ToConstraintFieldGadget>, + CS1: CommitmentScheme, + CS2: CommitmentScheme, + { + // compute the KZG challenges of the CycleFold instance commitments, used as inputs in the + // circuit + let poseidon_config = + crate::transcript::poseidon::poseidon_canonical_config::(); + let mut transcript = PoseidonSponge::::new(&poseidon_config); + let pp_hash_Fq = + C2::ScalarField::from_le_bytes_mod_order(&nova.pp_hash.into_bigint().to_bytes_le()); + transcript.absorb(&pp_hash_Fq); + + let (kzg_challenge_W, kzg_challenge_E) = + KZGChallengesGadget::::get_challenges_native(&mut transcript, nova.cf_U_i.clone()); + + // get KZG evals + let mut W = nova.cf_W_i.W.clone(); + W.extend( + std::iter::repeat(C2::ScalarField::zero()) + .take(nova.cf_W_i.W.len().next_power_of_two() - nova.cf_W_i.W.len()), + ); + let mut E = nova.cf_W_i.E.clone(); + E.extend( + std::iter::repeat(C2::ScalarField::zero()) + .take(nova.cf_W_i.E.len().next_power_of_two() - nova.cf_W_i.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, + + cf_E_len: nova.cf_W_i.E.len(), + cf_r1cs: nova.cf_r1cs, + poseidon_config, + pp_hash: Some(pp_hash_Fq), + + cf_U_i: Some(nova.cf_U_i), + cf_W_i: Some(nova.cf_W_i), + + // CycleFold instance commitments kzg challenges + 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 DeciderCircuit2 +where + C1: CurveGroup, + C2: CurveGroup, + ::BaseField: PrimeField, + ::BaseField: PrimeField, + ::ScalarField: Absorb, + ::ScalarField: Absorb, + C1: CurveGroup, + GC1: CurveVar> + ToConstraintFieldGadget>, + for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, +{ + fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { + let pp_hash = FpVar::>::new_input(cs.clone(), || { + Ok(self.pp_hash.unwrap_or_else(CF1::::zero)) + })?; + + let cf_u_dummy_native = CommittedInstance::::dummy(NovaCycleFoldConfig::::IO_LEN); + let w_dummy_native = + Witness::::dummy(self.cf_r1cs.A.n_cols - 1 - self.cf_r1cs.l, self.cf_E_len); + let cf_U_i = CommittedInstanceVar::::new_input(cs.clone(), || { + Ok(self.cf_U_i.unwrap_or_else(|| cf_u_dummy_native.clone())) + })?; + let cf_W_i = WitnessVar::::new_witness(cs.clone(), || { + Ok(self.cf_W_i.unwrap_or(w_dummy_native.clone())) + })?; + + let cf_r1cs = + R1CSVar::, FpVar>>::new_witness(cs.clone(), || { + Ok(self.cf_r1cs.clone()) + })?; + + // 6. 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_native(cf_r1cs, cf_W_i.E, cf_U_i.u.clone(), cf_z_U)?; + + // `transcript` is for challenge generation. + let mut transcript = + PoseidonSpongeVar::::new(cs.clone(), &self.poseidon_config); + transcript.absorb(&pp_hash)?; + + // allocate the inputs for the check 7.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 7.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)) + })?; + + // 7.1. check the KZG challenges correct computation + let (incircuit_c_W, incircuit_c_E) = + KZGChallengesGadget::::get_challenges_gadget(&mut transcript, cf_U_i.clone())?; + incircuit_c_W.enforce_equal(&kzg_c_W)?; + incircuit_c_E.enforce_equal(&kzg_c_E)?; + + // Check 7.2 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.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)?; + + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; + use ark_relations::r1cs::ConstraintSystem; + use ark_std::One; + use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; + + use super::*; + use crate::commitment::pedersen::Pedersen; + use crate::folding::nova::PreprocessorParam; + use crate::frontend::utils::CubicFCircuit; + use crate::transcript::poseidon::poseidon_canonical_config; + use crate::FoldingScheme; + + #[test] + fn test_decider_circuits() { + 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_v = nova.clone(); + let (running_instance, incoming_instance, cyclefold_instance) = ivc_v.instances(); + N::verify( + nova_params.1, // verifier_params + z_0, + ivc_v.z_i, + Fr::one(), + running_instance, + incoming_instance, + cyclefold_instance, + ) + .unwrap(); + + // load the DeciderCircuit 1 & 2 from the Nova instance + let decider_circuit1 = + DeciderCircuit1::::from_nova(nova.clone()).unwrap(); + let decider_circuit2 = + DeciderCircuit2::::from_nova(nova).unwrap(); + + // generate the constraints of both circuits and check that are satisfied by the inputs + let cs1 = ConstraintSystem::::new_ref(); + decider_circuit1.generate_constraints(cs1.clone()).unwrap(); + assert!(cs1.is_satisfied().unwrap()); + let cs2 = ConstraintSystem::::new_ref(); + decider_circuit2.generate_constraints(cs2.clone()).unwrap(); + assert!(cs2.is_satisfied().unwrap()); + } +} diff --git a/folding-schemes/src/folding/nova/decider_eth.rs b/folding-schemes/src/folding/nova/decider_eth.rs index 570c351..12ab244 100644 --- a/folding-schemes/src/folding/nova/decider_eth.rs +++ b/folding-schemes/src/folding/nova/decider_eth.rs @@ -1,4 +1,6 @@ /// This file implements the Nova's onchain (Ethereum's EVM) decider. +/// More details can be found at the documentation page: +/// https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html use ark_bn254::Bn254; use ark_crypto_primitives::sponge::Absorb; use ark_ec::{AffineRepr, CurveGroup, Group}; diff --git a/folding-schemes/src/folding/nova/decider_eth_circuit.rs b/folding-schemes/src/folding/nova/decider_eth_circuit.rs index 27d83f1..049ed25 100644 --- a/folding-schemes/src/folding/nova/decider_eth_circuit.rs +++ b/folding-schemes/src/folding/nova/decider_eth_circuit.rs @@ -1,5 +1,7 @@ /// 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}, @@ -374,13 +376,14 @@ where Ok(self.W_i1.unwrap_or(w_dummy_native.clone())) })?; - // allocate the inputs for the check 6 + // 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)) })?; @@ -393,10 +396,8 @@ where // `transcript` is for challenge generation. let mut transcript = sponge.clone(); - // 1. 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)?; + // 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. @@ -409,6 +410,11 @@ where 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."); @@ -446,7 +452,7 @@ where 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} + // 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> = @@ -471,17 +477,18 @@ where || Ok(self.cf_r1cs.clone()), )?; - // 5. check RelaxedR1CS of cf_U_i + // 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)?; } - // 8.a, 6.a compute NIFS.V and KZG challenges. + // 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, @@ -489,25 +496,24 @@ where u_i.clone(), cmT.clone(), )?; + // 5.1. let (incircuit_c_W, incircuit_c_E) = KZGChallengesGadget::::get_challenges_gadget(&mut transcript, U_i1.clone())?; - - // 6.b check KZG challenges incircuit_c_W.enforce_equal(&kzg_c_W)?; incircuit_c_E.enforce_equal(&kzg_c_E)?; - // Check 7 is temporary disabled due + // Check 5.2 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) + // 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)?; - // 8.b check the NIFS.V challenge matches the one from the public input (so we - // avoid the verifier computing it) + // 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 = @@ -785,7 +791,7 @@ pub mod tests { } #[test] - fn test_decider_circuit() { + fn test_decider_eth_circuit() { let mut rng = ark_std::test_rng(); let poseidon_config = poseidon_canonical_config::(); @@ -829,8 +835,8 @@ pub mod tests { ) .unwrap(); - // load the DeciderEthCircuit from the generated Nova instance - let decider_circuit = DeciderEthCircuit::< + // load the DeciderEthCircuit from the Nova instance + let decider_eth_circuit = DeciderEthCircuit::< Projective, GVar, Projective2, @@ -843,7 +849,9 @@ pub mod tests { let cs = ConstraintSystem::::new_ref(); // generate the constraints and check that are satisfied by the inputs - decider_circuit.generate_constraints(cs.clone()).unwrap(); + decider_eth_circuit + .generate_constraints(cs.clone()) + .unwrap(); assert!(cs.is_satisfied().unwrap()); } diff --git a/folding-schemes/src/folding/nova/mod.rs b/folding-schemes/src/folding/nova/mod.rs index 97150a0..5863f7a 100644 --- a/folding-schemes/src/folding/nova/mod.rs +++ b/folding-schemes/src/folding/nova/mod.rs @@ -32,15 +32,20 @@ use crate::{ utils::{get_cm_coordinates, pp_hash}, }; +use circuits::{AugmentedFCircuit, ChallengeGadget, CommittedInstanceVar}; +use nifs::NIFS; + pub mod circuits; -pub mod decider_eth; -pub mod decider_eth_circuit; pub mod nifs; pub mod serialize; pub mod traits; pub mod zk; -use circuits::{AugmentedFCircuit, ChallengeGadget, CommittedInstanceVar}; -use nifs::NIFS; + +// offchain decider +pub mod decider_circuits; +// onchain decider +pub mod decider_eth; +pub mod decider_eth_circuit; use super::traits::{CommittedInstanceOps, WitnessOps};