From 80e991aa45e4d9d335b4934416a080bdd554f83b Mon Sep 17 00:00:00 2001 From: winderica Date: Sat, 5 Oct 2024 23:54:35 +0800 Subject: [PATCH] Generic decider circuits --- examples/circom_full_flow.rs | 4 +- examples/full_flow.rs | 4 +- examples/noir_full_flow.rs | 4 +- examples/noname_full_flow.rs | 4 +- .../src/folding/circuits/cyclefold.rs | 2 +- .../src/folding/circuits/decider/mod.rs | 189 ++++++ .../src/folding/circuits/decider/off_chain.rs | 297 ++++++++++ .../src/folding/circuits/decider/on_chain.rs | 314 ++++++++++ folding-schemes/src/folding/circuits/mod.rs | 5 +- .../src/folding/hypernova/circuits.rs | 6 +- .../src/folding/hypernova/decider_eth.rs | 95 +-- .../folding/hypernova/decider_eth_circuit.rs | 456 ++++----------- .../src/folding/hypernova/nimfs.rs | 21 +- folding-schemes/src/folding/nova/circuits.rs | 14 +- folding-schemes/src/folding/nova/decider.rs | 209 +++---- .../src/folding/nova/decider_circuits.rs | 513 ++++------------ .../src/folding/nova/decider_eth.rs | 190 +++--- .../src/folding/nova/decider_eth_circuit.rs | 553 ++++-------------- folding-schemes/src/folding/nova/nifs.rs | 3 - .../protogalaxy/decider_eth_circuit.rs | 242 ++++++++ .../src/folding/protogalaxy/mod.rs | 1 + folding-schemes/src/folding/traits.rs | 6 + solidity-verifiers/src/utils/mod.rs | 2 +- .../src/verifiers/nova_cyclefold.rs | 4 +- .../nova_cyclefold_decider.askama.sol | 40 +- 25 files changed, 1586 insertions(+), 1592 deletions(-) create mode 100644 folding-schemes/src/folding/circuits/decider/mod.rs create mode 100644 folding-schemes/src/folding/circuits/decider/off_chain.rs create mode 100644 folding-schemes/src/folding/circuits/decider/on_chain.rs create mode 100644 folding-schemes/src/folding/protogalaxy/decider_eth_circuit.rs diff --git a/examples/circom_full_flow.rs b/examples/circom_full_flow.rs index 10a79ba2..cfa6ff16 100644 --- a/examples/circom_full_flow.rs +++ b/examples/circom_full_flow.rs @@ -108,8 +108,8 @@ fn main() { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &(), + &(), &proof, ) .unwrap(); diff --git a/examples/full_flow.rs b/examples/full_flow.rs index bf1f28f3..ed56b468 100644 --- a/examples/full_flow.rs +++ b/examples/full_flow.rs @@ -124,8 +124,8 @@ fn main() { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &(), + &(), &proof, ) .unwrap(); diff --git a/examples/noir_full_flow.rs b/examples/noir_full_flow.rs index ec5107a9..41dba458 100644 --- a/examples/noir_full_flow.rs +++ b/examples/noir_full_flow.rs @@ -97,8 +97,8 @@ fn main() { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &(), + &(), &proof, ) .unwrap(); diff --git a/examples/noname_full_flow.rs b/examples/noname_full_flow.rs index 73a1a673..5093e95c 100644 --- a/examples/noname_full_flow.rs +++ b/examples/noname_full_flow.rs @@ -108,8 +108,8 @@ fn main() { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &(), + &(), &proof, ) .unwrap(); diff --git a/folding-schemes/src/folding/circuits/cyclefold.rs b/folding-schemes/src/folding/circuits/cyclefold.rs index 9e86651a..bad155cd 100644 --- a/folding-schemes/src/folding/circuits/cyclefold.rs +++ b/folding-schemes/src/folding/circuits/cyclefold.rs @@ -192,7 +192,7 @@ where /// (reconstraining) them. #[allow(clippy::type_complexity)] pub fn hash, S>>( - self, + &self, sponge: &T, pp_hash: FpVar>, // public params hash ) -> Result<(FpVar>, Vec>>), SynthesisError> { diff --git a/folding-schemes/src/folding/circuits/decider/mod.rs b/folding-schemes/src/folding/circuits/decider/mod.rs new file mode 100644 index 00000000..5f14d3a3 --- /dev/null +++ b/folding-schemes/src/folding/circuits/decider/mod.rs @@ -0,0 +1,189 @@ +use ark_crypto_primitives::sponge::{ + poseidon::constraints::PoseidonSpongeVar, CryptographicSponge, +}; +use ark_ec::CurveGroup; +use ark_ff::PrimeField; +use ark_poly::Polynomial; +use ark_r1cs_std::{ + fields::{fp::FpVar, FieldVar}, + poly::{domain::Radix2DomainVar, evaluations::univariate::EvaluationsVar}, + ToConstraintFieldGadget, +}; +use ark_relations::r1cs::SynthesisError; +use ark_std::log2; + +use crate::folding::traits::{CommittedInstanceOps, CommittedInstanceVarOps, Dummy, WitnessOps}; +use crate::transcript::{Transcript, TranscriptVar}; +use crate::utils::vec::poly_from_vec; +use crate::Error; +use crate::{arith::Arith, folding::circuits::CF1}; + +pub mod off_chain; +pub mod on_chain; + +/// Gadget that computes the KZG challenges. +/// It also offers the rust native implementation compatible with the gadget. +pub struct KZGChallengesGadget {} + +impl KZGChallengesGadget { + pub fn get_challenges_native< + C: CurveGroup, + T: Transcript>, + U: CommittedInstanceOps, + >( + transcript: &mut T, + U_i: &U, + ) -> Vec> { + let mut challenges = vec![]; + for cm in U_i.get_commitments() { + transcript.absorb_nonnative(&cm); + challenges.push(transcript.get_challenge()); + } + challenges + } + + pub fn get_challenges_gadget< + C: CurveGroup, + S: CryptographicSponge, + T: TranscriptVar, S>, + U: CommittedInstanceVarOps, + >( + transcript: &mut T, + U_i: &U, + ) -> Result>>, SynthesisError> { + let mut challenges = vec![]; + for cm in U_i.get_commitments() { + transcript.absorb(&cm.to_constraint_field()?)?; + challenges.push(transcript.get_challenge()?); + } + Ok(challenges) + } +} + +/// Gadget that interpolates the polynomial from the given vector and returns +/// its evaluation at the given point. +/// It also offers the rust native implementation compatible with the gadget. +pub struct EvalGadget {} + +impl EvalGadget { + pub fn evaluate_native(v: &[F], point: F) -> Result { + let mut v = v.to_vec(); + v.resize(v.len().next_power_of_two(), F::zero()); + + Ok(poly_from_vec(v)?.evaluate(&point)) + } + + pub fn evaluate_gadget( + v: &[FpVar], + point: &FpVar, + ) -> Result, SynthesisError> { + let mut v = v.to_vec(); + 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) + } +} + +/// This is a temporary workaround for step 8 (running NIFS.V in circuit) in a +/// folding scheme-agnostic way, as different folding schemes have different +/// interfaces of folding verification now. +/// +/// In the future, we may introduce a better solution that use a trait for all +/// folding schemes that specifies their native and in-circuit behaviors. +pub trait DeciderEnabledNIFS< + C: CurveGroup, + RU: CommittedInstanceOps, // Running instance + IU: CommittedInstanceOps, // Incoming instance + W: WitnessOps>, + A: Arith, +> +{ + type ProofDummyCfg; + type Proof: Dummy; + + #[allow(clippy::too_many_arguments)] + fn fold_gadget( + arith: &A, + transcript: &mut PoseidonSpongeVar>, + pp_hash: FpVar>, + U: RU::Var, + U_vec: Vec>>, + u: IU::Var, + proof: Self::Proof, + ) -> Result; +} + +#[cfg(test)] +pub mod tests { + use ark_crypto_primitives::sponge::{ + constraints::CryptographicSpongeVar, poseidon::PoseidonSponge, + }; + use ark_pallas::{Fr, Projective}; + use ark_r1cs_std::{alloc::AllocVar, R1CSVar}; + use ark_relations::r1cs::ConstraintSystem; + use ark_std::UniformRand; + + use super::*; + use crate::folding::nova::{circuits::CommittedInstanceVar, CommittedInstance}; + use crate::transcript::poseidon::poseidon_canonical_config; + + // 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 challenges = KZGChallengesGadget::get_challenges_native(&mut transcript, &U_i); + + 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 challenges_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 + assert_eq!(challenges_var.value().unwrap(), challenges); + } + + #[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 = EvalGadget::evaluate_gadget(&vVar, &challengeVar).unwrap(); + + assert_eq!(evalVar.value().unwrap(), eval); + assert!(cs.is_satisfied().unwrap()); + } +} diff --git a/folding-schemes/src/folding/circuits/decider/off_chain.rs b/folding-schemes/src/folding/circuits/decider/off_chain.rs new file mode 100644 index 00000000..9c5f8ed7 --- /dev/null +++ b/folding-schemes/src/folding/circuits/decider/off_chain.rs @@ -0,0 +1,297 @@ +/// This file implements a generic offchain decider circuit. +/// For ethereum use cases, use the `GenericOnchainDeciderCircuit`. +/// 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::{AbsorbGadget, CryptographicSpongeVar}, + poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig}, + Absorb, +}; +use ark_ec::CurveGroup; +use ark_r1cs_std::{ + alloc::AllocVar, eq::EqGadget, fields::fp::FpVar, prelude::CurveVar, ToConstraintFieldGadget, +}; +use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; +use ark_std::{marker::PhantomData, Zero}; + +use crate::{ + arith::{ + r1cs::{circuits::R1CSMatricesVar, R1CS}, + Arith, ArithGadget, + }, + folding::{ + circuits::{ + cyclefold::{ + CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar, CycleFoldWitness, + }, + decider::{EvalGadget, KZGChallengesGadget}, + CF1, CF2, + }, + nova::{circuits::CommittedInstanceVar, decider_eth_circuit::WitnessVar}, + traits::{CommittedInstanceOps, CommittedInstanceVarOps, Dummy, WitnessOps, WitnessVarOps}, + }, +}; + +use super::DeciderEnabledNIFS; + +/// Circuit that implements part of the in-circuit checks needed for the offchain verification over +/// the Curve2's BaseField (=Curve1's ScalarField). +pub struct GenericOffchainDeciderCircuit1< + C1: CurveGroup, + C2: CurveGroup, + GC2: CurveVar> + ToConstraintFieldGadget>, + RU: CommittedInstanceOps, // Running instance + IU: CommittedInstanceOps, // Incoming instance + W: WitnessOps>, // Witness + A: Arith, // Constraint system + AVar: ArithGadget, // In-circuit representation of `A` + D: DeciderEnabledNIFS, +> { + pub _gc2: PhantomData, + pub _avar: PhantomData, + /// Constraint system of the Augmented Function circuit + pub arith: A, + pub poseidon_config: PoseidonConfig>, + /// public params hash + pub pp_hash: CF1, + pub i: CF1, + /// initial state + pub z_0: Vec>, + /// current i-th state + pub z_i: Vec>, + /// Folding scheme instances + pub U_i: RU, + pub W_i: W, + pub u_i: IU, + pub w_i: W, + pub U_i1: RU, + pub W_i1: W, + + /// Helper for folding verification + pub proof: D::Proof, + + /// CycleFold running instance + pub cf_U_i: CycleFoldCommittedInstance, + + /// KZG challenges + pub kzg_challenges: Vec>, + pub kzg_evaluations: Vec>, +} + +impl< + C1: CurveGroup, + C2: CurveGroup, BaseField = CF1>, + GC2: CurveVar> + ToConstraintFieldGadget>, + RU: CommittedInstanceOps + for<'a> Dummy<&'a A>, + IU: CommittedInstanceOps + for<'a> Dummy<&'a A>, + W: WitnessOps> + for<'a> Dummy<&'a A>, + A: Arith, + AVar: ArithGadget + AllocVar>, + D: DeciderEnabledNIFS, + > + Dummy<( + A, + &R1CS>, + PoseidonConfig>, + D::ProofDummyCfg, + usize, + usize, + )> for GenericOffchainDeciderCircuit1 +{ + fn dummy( + (arith, cf_arith, poseidon_config, proof_config, state_len, num_commitments): ( + A, + &R1CS>, + PoseidonConfig>, + D::ProofDummyCfg, + usize, + usize, + ), + ) -> Self { + Self { + _gc2: PhantomData, + _avar: PhantomData, + poseidon_config, + pp_hash: Zero::zero(), + i: Zero::zero(), + z_0: vec![Zero::zero(); state_len], + z_i: vec![Zero::zero(); state_len], + U_i: RU::dummy(&arith), + W_i: W::dummy(&arith), + u_i: IU::dummy(&arith), + w_i: W::dummy(&arith), + U_i1: RU::dummy(&arith), + W_i1: W::dummy(&arith), + proof: D::Proof::dummy(proof_config), + cf_U_i: CycleFoldCommittedInstance::dummy(cf_arith), + kzg_challenges: vec![Zero::zero(); num_commitments], + kzg_evaluations: vec![Zero::zero(); num_commitments], + arith, + } + } +} + +impl< + C1: CurveGroup, + C2: CurveGroup, BaseField = CF1>, + GC2: CurveVar> + ToConstraintFieldGadget>, + RU: CommittedInstanceOps, + IU: CommittedInstanceOps, + W: WitnessOps>, + A: Arith, + AVar: ArithGadget + AllocVar>, + D: DeciderEnabledNIFS, + > ConstraintSynthesizer> + for GenericOffchainDeciderCircuit1 +where + RU::Var: AbsorbGadget>, + CF1: Absorb, +{ + fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { + let arith = AVar::new_witness(cs.clone(), || Ok(&self.arith))?; + + let pp_hash = FpVar::new_input(cs.clone(), || Ok(self.pp_hash))?; + let i = FpVar::new_input(cs.clone(), || Ok(self.i))?; + let z_0 = Vec::new_input(cs.clone(), || Ok(self.z_0))?; + let z_i = Vec::new_input(cs.clone(), || Ok(self.z_i))?; + + let u_i = IU::Var::new_witness(cs.clone(), || Ok(self.u_i))?; + let U_i = RU::Var::new_witness(cs.clone(), || Ok(self.U_i))?; + // here (U_i1, W_i1) = NIFS.P( (U_i,W_i), (u_i,w_i)) + let U_i1 = RU::Var::new_input(cs.clone(), || Ok(self.U_i1))?; + let W_i1 = W::Var::new_witness(cs.clone(), || Ok(self.W_i1))?; + + let cf_U_i = + CycleFoldCommittedInstanceVar::::new_input(cs.clone(), || Ok(self.cf_U_i))?; + + // allocate the inputs for the check 6 + let kzg_challenges = Vec::new_input(cs.clone(), || Ok(self.kzg_challenges))?; + let kzg_evaluations = Vec::new_input(cs.clone(), || Ok(self.kzg_evaluations))?; + + // `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 + + // 1. enforce `U_{i+1}` and `W_{i+1}` satisfy `arith` + arith.enforce_relation(&W_i1, &U_i1)?; + + // 2. enforce `u_i` is an incoming instance + u_i.enforce_incoming()?; + + // 3. u_i.x[0] == H(i, z_0, z_i, U_i), u_i.x[1] == H(cf_U_i) + let (u_i_x, U_i_vec) = U_i.hash(&sponge, &pp_hash, &i, &z_0, &z_i)?; + let (cf_u_i_x, _) = cf_U_i.hash(&sponge, pp_hash.clone())?; + u_i.get_public_inputs().enforce_equal(&[u_i_x, cf_u_i_x])?; + + // 4. enforce `NIFS.V(U_i, u_i) = U_{i+1}`. + D::fold_gadget( + &self.arith, + &mut transcript, + pp_hash, + U_i, + U_i_vec, + u_i, + self.proof, + )? + .enforce_partial_equal(&U_i1)?; + + // 5. compute and check KZG challenges + KZGChallengesGadget::get_challenges_gadget(&mut transcript, &U_i1)? + .enforce_equal(&kzg_challenges)?; + + // 6. check the claimed evaluations + for (((v, _r), c), e) in W_i1 + .get_openings() + .iter() + .zip(&kzg_challenges) + .zip(&kzg_evaluations) + { + // The randomness `_r` is currently not used. + EvalGadget::evaluate_gadget(v, c)?.enforce_equal(e)?; + } + + Ok(()) + } +} + +/// Circuit that implements part of the in-circuit checks needed for the offchain verification over +/// the Curve1's BaseField (=Curve2's ScalarField). +pub struct GenericOffchainDeciderCircuit2 { + /// R1CS of the CycleFold circuit + pub cf_arith: R1CS>, + pub poseidon_config: PoseidonConfig>, + /// public params hash + pub pp_hash: CF1, + + /// CycleFold running instance + pub cf_U_i: CycleFoldCommittedInstance, + pub cf_W_i: CycleFoldWitness, + + /// KZG challenges + pub kzg_challenges: Vec>, + pub kzg_evaluations: Vec>, +} + +impl Dummy<(R1CS>, PoseidonConfig>, usize)> + for GenericOffchainDeciderCircuit2 +{ + fn dummy( + (cf_arith, poseidon_config, num_commitments): ( + R1CS>, + PoseidonConfig>, + usize, + ), + ) -> Self { + Self { + poseidon_config, + pp_hash: Zero::zero(), + cf_U_i: CycleFoldCommittedInstance::dummy(&cf_arith), + cf_W_i: CycleFoldWitness::dummy(&cf_arith), + kzg_challenges: vec![Zero::zero(); num_commitments], + kzg_evaluations: vec![Zero::zero(); num_commitments], + cf_arith, + } + } +} + +impl ConstraintSynthesizer> for GenericOffchainDeciderCircuit2 { + fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { + let cf_r1cs = R1CSMatricesVar::, FpVar>>::new_witness(cs.clone(), || { + Ok(self.cf_arith.clone()) + })?; + + let pp_hash = FpVar::new_input(cs.clone(), || Ok(self.pp_hash))?; + + let cf_U_i = CommittedInstanceVar::new_input(cs.clone(), || Ok(self.cf_U_i))?; + let cf_W_i = WitnessVar::new_witness(cs.clone(), || Ok(self.cf_W_i))?; + + let kzg_challenges = Vec::new_input(cs.clone(), || Ok(self.kzg_challenges))?; + let kzg_evaluations = Vec::new_input(cs.clone(), || Ok(self.kzg_evaluations))?; + + // `transcript` is for challenge generation. + let mut transcript = PoseidonSpongeVar::new(cs.clone(), &self.poseidon_config); + transcript.absorb(&pp_hash)?; + + // 1. enforce `cf_U_i` and `cf_W_i` satisfy `cf_r1cs` + cf_r1cs.enforce_relation(&cf_W_i, &cf_U_i)?; + + // 2. compute and check KZG challenges + KZGChallengesGadget::get_challenges_gadget(&mut transcript, &cf_U_i)? + .enforce_equal(&kzg_challenges)?; + + // 3. check the claimed evaluations + for (((v, _r), c), e) in cf_W_i + .get_openings() + .iter() + .zip(&kzg_challenges) + .zip(&kzg_evaluations) + { + // The randomness `_r` is currently not used. + EvalGadget::evaluate_gadget(v, c)?.enforce_equal(e)?; + } + + Ok(()) + } +} diff --git a/folding-schemes/src/folding/circuits/decider/on_chain.rs b/folding-schemes/src/folding/circuits/decider/on_chain.rs new file mode 100644 index 00000000..22d9c9df --- /dev/null +++ b/folding-schemes/src/folding/circuits/decider/on_chain.rs @@ -0,0 +1,314 @@ +/// 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::{AbsorbGadget, CryptographicSpongeVar}, + poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig}, + Absorb, +}; +use ark_ec::CurveGroup; +use ark_r1cs_std::{ + alloc::AllocVar, eq::EqGadget, fields::fp::FpVar, prelude::CurveVar, ToConstraintFieldGadget, +}; +use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; +use ark_std::{marker::PhantomData, Zero}; + +use crate::{ + arith::{r1cs::R1CS, Arith, ArithGadget}, + commitment::pedersen::Params as PedersenParams, + folding::{ + circuits::{ + cyclefold::{ + CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar, CycleFoldWitness, + }, + decider::{EvalGadget, KZGChallengesGadget}, + CF1, CF2, + }, + traits::{CommittedInstanceOps, CommittedInstanceVarOps, Dummy, WitnessOps, WitnessVarOps}, + }, +}; + +use super::DeciderEnabledNIFS; + +/// A generic circuit tailored for the onchain (Ethereum's EVM) verification of +/// IVC proofs, where we support IVC built upon any folding scheme. +/// +/// Specifically, `GenericDeciderEthCircuit` implements the in-circuit version +/// of the IVC verification algorithm, which essentially checks the following: +/// - `R_arith(W_i, U_i)`: +/// The running instance `U_i` and witness `W_i` satisfy `arith`, +/// and the commitments in `U_i` open to the values in `W_i`. +/// - `R_arith(w_i, u_i)`: +/// The incoming instance `u_i` and witness `w_i` satisfy `arith`, +/// and the commitments in `u_i` open to the values in `w_i`. +/// - `R_cf_arith(cf_W_i, cf_U_i)`: +/// The CycleFold instance `cf_U_i` and witness `cf_W_i` satisfy `cf_arith`, +/// and the commitments in `cf_U_i` open to the values in `cf_W_i`. +/// - `u_i` contains the correct hash of the initial and final states. +/// +/// To reduce the number of relation checks, the prover, before invoking the +/// circuit, further folds `U_i, u_i` into `U_{i+1}`, and `W_i, w_i` into +/// `W_{i+1}`. +/// Now, the circuit only needs to perform two relation checks, i.e., +/// `R_arith(W_{i+1}, U_{i+1})` and `R_cf_arith(cf_W_i, cf_U_i)`, plus a few +/// constraints for enforcing the correct hash in `u_i` and the correct folding +/// from `U_i, u_i` to `U_{i+1}`. +/// +/// We further reduce the circuit size by avoiding the non-native commitment +/// checks involved in `R_arith(W_{i+1}, U_{i+1})`. +/// Now, we now only check the satisfiability of the constraint system `arith` +/// with the witness `W_{i+1}` and instance `U_{i+1}` in the circuit, but the +/// actual commitment checks are done with the help of KZG. +/// +/// For more details, see [https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html]. +pub struct GenericOnchainDeciderCircuit< + C1: CurveGroup, + C2: CurveGroup, + GC2: CurveVar> + ToConstraintFieldGadget>, + RU: CommittedInstanceOps, // Running instance + IU: CommittedInstanceOps, // Incoming instance + W: WitnessOps>, // Witness + A: Arith, // Constraint system + AVar: ArithGadget, // In-circuit representation of `A` + D: DeciderEnabledNIFS, +> { + pub _gc2: PhantomData, + pub _avar: PhantomData, + /// Constraint system of the Augmented Function circuit + pub arith: A, + /// R1CS of the CycleFold circuit + pub cf_arith: R1CS>, + /// CycleFold PedersenParams over C2 + pub cf_pedersen_params: PedersenParams, + pub poseidon_config: PoseidonConfig>, + /// public params hash + pub pp_hash: CF1, + pub i: CF1, + /// initial state + pub z_0: Vec>, + /// current i-th state + pub z_i: Vec>, + /// Folding scheme instances + pub U_i: RU, + pub W_i: W, + pub u_i: IU, + pub w_i: W, + pub U_i1: RU, + pub W_i1: W, + + /// Helper for folding verification + pub proof: D::Proof, + + /// CycleFold running instance + pub cf_U_i: CycleFoldCommittedInstance, + pub cf_W_i: CycleFoldWitness, + + /// KZG challenges + pub kzg_challenges: Vec>, + pub kzg_evaluations: Vec>, +} + +impl< + C1: CurveGroup, + C2: CurveGroup, BaseField = CF1>, + GC2: CurveVar> + ToConstraintFieldGadget>, + RU: CommittedInstanceOps + for<'a> Dummy<&'a A>, + IU: CommittedInstanceOps + for<'a> Dummy<&'a A>, + W: WitnessOps> + for<'a> Dummy<&'a A>, + A: Arith, + AVar: ArithGadget + AllocVar>, + D: DeciderEnabledNIFS, + > + Dummy<( + A, + R1CS>, + PedersenParams, + PoseidonConfig>, + D::ProofDummyCfg, + usize, + usize, + )> for GenericOnchainDeciderCircuit +{ + fn dummy( + ( + arith, + cf_arith, + cf_pedersen_params, + poseidon_config, + proof_config, + state_len, + num_commitments, + ): ( + A, + R1CS>, + PedersenParams, + PoseidonConfig>, + D::ProofDummyCfg, + usize, + usize, + ), + ) -> Self { + Self { + _gc2: PhantomData, + _avar: PhantomData, + cf_pedersen_params, + poseidon_config, + pp_hash: Zero::zero(), + i: Zero::zero(), + z_0: vec![Zero::zero(); state_len], + z_i: vec![Zero::zero(); state_len], + U_i: RU::dummy(&arith), + W_i: W::dummy(&arith), + u_i: IU::dummy(&arith), + w_i: W::dummy(&arith), + U_i1: RU::dummy(&arith), + W_i1: W::dummy(&arith), + proof: D::Proof::dummy(proof_config), + cf_U_i: CycleFoldCommittedInstance::dummy(&cf_arith), + cf_W_i: CycleFoldWitness::dummy(&cf_arith), + kzg_challenges: vec![Zero::zero(); num_commitments], + kzg_evaluations: vec![Zero::zero(); num_commitments], + arith, + cf_arith, + } + } +} + +impl< + C1: CurveGroup, + C2: CurveGroup, BaseField = CF1>, + GC2: CurveVar> + ToConstraintFieldGadget>, + RU: CommittedInstanceOps, + IU: CommittedInstanceOps, + W: WitnessOps>, + A: Arith, + AVar: ArithGadget + AllocVar>, + D: DeciderEnabledNIFS, + > ConstraintSynthesizer> + for GenericOnchainDeciderCircuit +where + RU::Var: AbsorbGadget>, + CF1: Absorb, +{ + fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { + let arith = AVar::new_witness(cs.clone(), || Ok(&self.arith))?; + + let pp_hash = FpVar::new_input(cs.clone(), || Ok(self.pp_hash))?; + let i = FpVar::new_input(cs.clone(), || Ok(self.i))?; + let z_0 = Vec::new_input(cs.clone(), || Ok(self.z_0))?; + let z_i = Vec::new_input(cs.clone(), || Ok(self.z_i))?; + + let u_i = IU::Var::new_witness(cs.clone(), || Ok(self.u_i))?; + let U_i = RU::Var::new_witness(cs.clone(), || Ok(self.U_i))?; + // here (U_i1, W_i1) = NIFS.P( (U_i,W_i), (u_i,w_i)) + let U_i1 = RU::Var::new_input(cs.clone(), || Ok(self.U_i1))?; + let W_i1 = W::Var::new_witness(cs.clone(), || Ok(self.W_i1))?; + + let cf_U_i = + CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || Ok(self.cf_U_i))?; + + // allocate the inputs for the check 6 + let kzg_challenges = Vec::new_input(cs.clone(), || Ok(self.kzg_challenges))?; + let kzg_evaluations = Vec::new_input(cs.clone(), || Ok(self.kzg_evaluations))?; + + // `sponge` is for digest computation. + let sponge = PoseidonSpongeVar::new(cs.clone(), &self.poseidon_config); + // `transcript` is for challenge generation. + let mut transcript = sponge.clone(); + + // NOTE: we use the same enumeration as in + // https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html + // in order to make it easier to reason about. + + // 1. enforce `U_{i+1}` and `W_{i+1}` satisfy `arith` + arith.enforce_relation(&W_i1, &U_i1)?; + + // 2. enforce `u_i` is an incoming instance + u_i.enforce_incoming()?; + + // 3. u_i.x[0] == H(i, z_0, z_i, U_i), u_i.x[1] == H(cf_U_i) + let (u_i_x, U_i_vec) = U_i.hash(&sponge, &pp_hash, &i, &z_0, &z_i)?; + let (cf_u_i_x, _) = cf_U_i.hash(&sponge, pp_hash.clone())?; + u_i.get_public_inputs().enforce_equal(&[u_i_x, cf_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::{ + arith::r1cs::circuits::R1CSMatricesVar, + commitment::pedersen::PedersenGadget, + folding::circuits::{ + cyclefold::CycleFoldWitnessVar, nonnative::uint::NonNativeUintVar, + }, + }; + use ark_r1cs_std::ToBitsGadget; + let cf_W_i = CycleFoldWitnessVar::::new_witness(cs.clone(), || Ok(self.cf_W_i))?; + // 4. check Pedersen commitments of cf_U_i.{cmE, cmW} + let H = GC2::constant(self.cf_pedersen_params.h); + let G = self + .cf_pedersen_params + .generators + .iter() + .map(|&g| GC2::constant(g.into())) + .collect::>(); + let cf_W_i_E_bits = cf_W_i + .E + .iter() + .map(|E_i| E_i.to_bits_le()) + .collect::, _>>()?; + let cf_W_i_W_bits = cf_W_i + .W + .iter() + .map(|W_i| W_i.to_bits_be()) + .collect::, _>>()?; + PedersenGadget::::commit(&H, &G, &cf_W_i_E_bits, &cf_W_i.rE.to_bits_le()?)? + .enforce_equal(&cf_U_i.cmE)?; + PedersenGadget::::commit(&H, &G, &cf_W_i_W_bits, &cf_W_i.rW.to_bits_le()?)? + .enforce_equal(&cf_U_i.cmW)?; + + let cf_r1cs = R1CSMatricesVar::, NonNativeUintVar>>::new_constant( + ConstraintSystemRef::None, + self.cf_arith, + )?; + + // 5. enforce `cf_U_i` and `cf_W_i` satisfy `cf_r1cs` + cf_r1cs.enforce_relation(&cf_W_i, &cf_U_i)?; + } + + // 6. enforce `NIFS.V(U_i, u_i) = U_{i+1}`. + D::fold_gadget( + &self.arith, + &mut transcript, + pp_hash, + U_i, + U_i_vec, + u_i, + self.proof, + )? + .enforce_partial_equal(&U_i1)?; + + // 7. compute and check KZG challenges + KZGChallengesGadget::get_challenges_gadget(&mut transcript, &U_i1)? + .enforce_equal(&kzg_challenges)?; + + // 6. check the claimed evaluations + for (((v, _r), c), e) in W_i1 + .get_openings() + .iter() + .zip(&kzg_challenges) + .zip(&kzg_evaluations) + { + // The randomness `_r` is currently not used. + EvalGadget::evaluate_gadget(v, c)?.enforce_equal(e)?; + } + + Ok(()) + } +} diff --git a/folding-schemes/src/folding/circuits/mod.rs b/folding-schemes/src/folding/circuits/mod.rs index b442b93f..48d5cf11 100644 --- a/folding-schemes/src/folding/circuits/mod.rs +++ b/folding-schemes/src/folding/circuits/mod.rs @@ -1,8 +1,9 @@ /// Circuits and gadgets shared across the different folding schemes. -use ark_ec::{AffineRepr, CurveGroup}; +use ark_ec::{CurveGroup, Group}; use ark_ff::Field; pub mod cyclefold; +pub mod decider; pub mod nonnative; pub mod sum_check; pub mod utils; @@ -10,7 +11,7 @@ pub mod utils; /// 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; +pub type CF1 = ::ScalarField; /// 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 diff --git a/folding-schemes/src/folding/hypernova/circuits.rs b/folding-schemes/src/folding/hypernova/circuits.rs index 42d0e8ca..a13cbde4 100644 --- a/folding-schemes/src/folding/hypernova/circuits.rs +++ b/folding-schemes/src/folding/hypernova/circuits.rs @@ -194,7 +194,6 @@ pub struct ProofVar { impl AllocVar, CF1> for ProofVar where C: CurveGroup, - ::BaseField: PrimeField, ::ScalarField: Absorb, { fn new_variable>>( @@ -236,10 +235,7 @@ where pub struct NIMFSGadget { _c: PhantomData, } -impl NIMFSGadget -where - ::BaseField: PrimeField, -{ +impl NIMFSGadget { /// Runs (in-circuit) the NIMFS.V, which outputs the new folded LCCCS instance together with /// the rho_powers, which will be used in other parts of the AugmentedFCircuit #[allow(clippy::type_complexity)] diff --git a/folding-schemes/src/folding/hypernova/decider_eth.rs b/folding-schemes/src/folding/hypernova/decider_eth.rs index b29baff0..07b232be 100644 --- a/folding-schemes/src/folding/hypernova/decider_eth.rs +++ b/folding-schemes/src/folding/hypernova/decider_eth.rs @@ -16,7 +16,7 @@ use crate::commitment::{ }; use crate::folding::circuits::CF2; use crate::folding::nova::decider_eth::VerifierParam; -use crate::folding::traits::Inputize; +use crate::folding::traits::{CommittedInstanceOps, Inputize, WitnessOps}; use crate::frontend::FCircuit; use crate::Error; use crate::{Decider as DeciderTrait, FoldingScheme}; @@ -30,9 +30,7 @@ where { snark_proof: S::Proof, kzg_proof: CS1::Proof, - // rho used at the last fold, U_{i+1}=NIMFS.V(rho, U_i, u_i), it is checked in-circuit - rho: C1::ScalarField, - U_i1: LCCCS, // U_{i+1}, which is checked in-circuit + U_final: LCCCS, // the KZG challenge is provided by the prover, but in-circuit it is checked to match // the in-circuit computed computed one. kzg_challenge: C1::ScalarField, @@ -95,11 +93,7 @@ where prep_param: Self::PreprocessorParam, fs: FS, ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { - let circuit = - DeciderEthCircuit::::from_hypernova::( - fs.into(), - ) - .unwrap(); + let circuit = DeciderEthCircuit::::try_from(HyperNova::from(fs)).unwrap(); // get the Groth16 specific setup for the circuit let (g16_pk, g16_vk) = S::circuit_specific_setup(circuit, &mut rng).unwrap(); @@ -121,7 +115,7 @@ where let pp = (g16_pk, hypernova_pp.cs_pp); - let vp = VerifierParam { + let vp = Self::VerifierParam { pp_hash, snark_vp: g16_vk, cs_vp: hypernova_vp.cs_vp, @@ -136,40 +130,38 @@ where ) -> Result { let (snark_pk, cs_pk): (S::ProvingKey, CS1::ProverParams) = pp; - let circuit = DeciderEthCircuit::::from_hypernova::( - folding_scheme.into(), - )?; - - let snark_proof = S::prove(&snark_pk, circuit.clone(), &mut rng) - .map_err(|e| Error::Other(e.to_string()))?; + let circuit = + DeciderEthCircuit::::try_from(HyperNova::from(folding_scheme)).unwrap(); - // Notice that since the `circuit` has been constructed at the `from_hypernova` call, which - // in case of failure it would have returned an error there, the next two unwraps should - // never reach an error. - let rho_Fr = circuit.rho.ok_or(Error::Empty)?; - let W_i1 = circuit.W_i1.ok_or(Error::Empty)?; + let U_final = circuit.U_i1.clone(); // get the challenges that have been already computed when preparing the circuit inputs in - // the above `from_hypernova` call - let challenge_W = circuit - .kzg_challenge - .ok_or(Error::MissingValue("kzg_challenge".to_string()))?; + // the above `try_from` call + let kzg_challenges = circuit.kzg_challenges.clone(); // generate KZG proofs - let U_cmW_proof = CS1::prove_with_challenge( - &cs_pk, - challenge_W, - &W_i1.w, - &C1::ScalarField::zero(), - None, - )?; + let kzg_proofs = circuit + .W_i1 + .get_openings() + .iter() + .zip(&kzg_challenges) + .map(|((v, _), &c)| { + CS1::prove_with_challenge(&cs_pk, c, v, &C1::ScalarField::zero(), None) + }) + .collect::, _>>()?; + + let snark_proof = + S::prove(&snark_pk, circuit, &mut rng).map_err(|e| Error::Other(e.to_string()))?; Ok(Self::Proof { snark_proof, - kzg_proof: U_cmW_proof, - rho: rho_Fr, - U_i1: circuit.U_i1.ok_or(Error::Empty)?, - kzg_challenge: challenge_W, + U_final, + kzg_proof: (kzg_proofs.len() == 1) + .then(|| kzg_proofs[0].clone()) + .ok_or(Error::NotExpectedLength(kzg_proofs.len(), 1))?, + kzg_challenge: (kzg_challenges.len() == 1) + .then(|| kzg_challenges[0]) + .ok_or(Error::NotExpectedLength(kzg_challenges.len(), 1))?, }) } @@ -187,29 +179,43 @@ where return Err(Error::NotEnoughSteps); } - let (pp_hash, snark_vk, cs_vk): (C1::ScalarField, S::VerifyingKey, CS1::VerifierParams) = - (vp.pp_hash, vp.snark_vp, vp.cs_vp); + let Self::VerifierParam { + pp_hash, + snark_vp, + cs_vp, + } = vp; + + let U = proof.U_final.clone(); // Note: the NIMFS proof is checked inside the DeciderEthCircuit, which ensures that the // 'proof.U_i1' is correctly computed - let public_input: Vec = [ vec![pp_hash, i], z_0, z_i, - proof.U_i1.inputize(), - vec![proof.kzg_challenge, proof.kzg_proof.eval, proof.rho], + U.inputize(), + vec![proof.kzg_challenge, proof.kzg_proof.eval], ] .concat(); - let snark_v = S::verify(&snark_vk, &public_input, &proof.snark_proof) + let snark_v = S::verify(&snark_vp, &public_input, &proof.snark_proof) .map_err(|e| Error::Other(e.to_string()))?; if !snark_v { return Err(Error::SNARKVerificationFail); } + let commitments = U.get_commitments(); + if commitments.len() != 1 { + return Err(Error::NotExpectedLength(commitments.len(), 1)); + } + // we're at the Ethereum EVM case, so the CS1 is KZG commitments - CS1::verify_with_challenge(&cs_vk, proof.kzg_challenge, &proof.U_i1.C, &proof.kzg_proof)?; + CS1::verify_with_challenge( + &cs_vp, + proof.kzg_challenge, + &commitments[0], + &proof.kzg_proof, + )?; Ok(true) } @@ -220,7 +226,7 @@ pub mod tests { use ark_bn254::{constraints::GVar, Bn254, Fr, G1Projective as Projective}; use ark_groth16::Groth16; use ark_grumpkin::{constraints::GVar as GVar2, Projective as Projective2}; - use ark_serialize::{Compress, Validate}; + use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress, Validate}; use super::*; use crate::commitment::{kzg::KZG, pedersen::Pedersen}; @@ -228,7 +234,6 @@ pub mod tests { use crate::folding::hypernova::{ PreprocessorParam, ProverParams, VerifierParams as HyperNovaVerifierParams, }; - use crate::folding::nova::decider_eth::VerifierParam; use crate::frontend::utils::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; diff --git a/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs b/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs index 4da5f6a7..5ecff42c 100644 --- a/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs +++ b/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs @@ -2,12 +2,11 @@ /// other more efficient approaches can be used. use ark_crypto_primitives::sponge::{ constraints::CryptographicSpongeVar, - poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, + poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}, Absorb, CryptographicSponge, }; -use ark_ec::{CurveGroup, Group}; +use ark_ec::CurveGroup; use ark_ff::PrimeField; -use ark_poly::Polynomial; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, boolean::Boolean, @@ -16,34 +15,29 @@ use ark_r1cs_std::{ prelude::CurveVar, ToConstraintFieldGadget, }; -use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; -use ark_std::{log2, Zero}; -use core::{borrow::Borrow, marker::PhantomData}; +use ark_relations::r1cs::{Namespace, SynthesisError}; +use ark_std::{borrow::Borrow, log2, marker::PhantomData}; use super::{ circuits::{CCCSVar, LCCCSVar, NIMFSGadget, ProofVar as NIMFSProofVar}, nimfs::{NIMFSProof, NIMFS}, HyperNova, Witness, CCCS, LCCCS, }; -use crate::folding::circuits::{ - cyclefold::{CycleFoldCommittedInstance, CycleFoldWitness}, - CF1, CF2, -}; -use crate::folding::nova::decider_eth_circuit::evaluate_gadget; +use crate::folding::circuits::{decider::on_chain::GenericOnchainDeciderCircuit, CF1, CF2}; +use crate::folding::traits::{WitnessOps, WitnessVarOps}; use crate::frontend::FCircuit; -use crate::transcript::{Transcript, TranscriptVar}; -use crate::utils::{ - gadgets::{eval_mle, MatrixGadget}, - vec::poly_from_vec, -}; +use crate::utils::gadgets::{eval_mle, MatrixGadget}; use crate::Error; use crate::{ - arith::{ccs::circuits::CCSMatricesVar, ArithGadget}, - commitment::{pedersen::Params as PedersenParams, CommitmentScheme}, + arith::{ + ccs::{circuits::CCSMatricesVar, CCS}, + ArithGadget, + }, + folding::circuits::decider::{EvalGadget, KZGChallengesGadget}, }; use crate::{ - arith::{ccs::CCS, r1cs::R1CS}, - folding::traits::{CommittedInstanceVarOps, Dummy, WitnessVarOps}, + commitment::{pedersen::Params as PedersenParams, CommitmentScheme}, + folding::circuits::decider::DeciderEnabledNIFS, }; impl ArithGadget>, LCCCSVar> for CCSMatricesVar> { @@ -106,80 +100,42 @@ impl WitnessVarOps for WitnessVar { } } -/// Circuit that implements the in-circuit checks needed for the HyperNova's onchain (Ethereum's -/// EVM) verification. -#[derive(Clone, Debug)] -pub struct DeciderEthCircuit +pub type DeciderEthCircuit = GenericOnchainDeciderCircuit< + C1, + C2, + GC2, + LCCCS, + CCCS, + Witness>, + CCS>, + CCSMatricesVar>, + DeciderHyperNovaGadget, +>; + +impl< + C1: CurveGroup, + GC1: CurveVar> + ToConstraintFieldGadget>, + C2: CurveGroup, + GC2: CurveVar> + ToConstraintFieldGadget>, + FC: FCircuit, + CS1: CommitmentScheme, + // enforce that the CS2 is Pedersen commitment scheme, since we're at Ethereum's EVM decider + CS2: CommitmentScheme>, + const MU: usize, + const NU: usize, + const H: bool, + > TryFrom> + for DeciderEthCircuit where - C1: CurveGroup, - GC1: CurveVar>, - C2: CurveGroup, - GC2: CurveVar>, - CS1: CommitmentScheme, - CS2: CommitmentScheme, + CF1: Absorb, { - _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 is the 'random' value used for the fold of the last 2 instances - pub rho: 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, -} + type Error = Error; -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 { + fn try_from(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) = NIMFS::>::prove( + let (nimfs_proof, U_i1, W_i1, _) = NIMFS::>::prove( &mut transcript, &hn.ccs, &[hn.U_i.clone()], @@ -189,281 +145,74 @@ where )?; // compute the KZG challenges used as inputs in the circuit - let kzg_challenge = - KZGChallengeGadget::::get_challenge_native(&mut transcript, U_i1.clone())?; + let kzg_challenges = KZGChallengesGadget::get_challenges_native(&mut transcript, &U_i1); // 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); + let kzg_evaluations = W_i1 + .get_openings() + .iter() + .zip(&kzg_challenges) + .map(|((v, _), &c)| EvalGadget::evaluate_native(v, c)) + .collect::, _>>()?; 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, + _avar: PhantomData, + arith: hn.ccs, + cf_arith: hn.cf_r1cs, cf_pedersen_params: hn.cf_cs_pp, 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: Some(rho), - 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), + pp_hash: hn.pp_hash, + i: hn.i, + z_0: hn.z_0, + z_i: hn.z_i, + U_i: hn.U_i, + W_i: hn.W_i, + u_i: hn.u_i, + w_i: hn.w_i, + U_i1, + W_i1, + proof: nimfs_proof, + cf_U_i: hn.cf_U_i, + cf_W_i: hn.cf_W_i, + kzg_challenges, + kzg_evaluations, }) } } -impl ConstraintSynthesizer> - for DeciderEthCircuit +pub struct DeciderHyperNovaGadget; + +impl DeciderEnabledNIFS, CCCS, Witness, CCS>> + for DeciderHyperNovaGadget 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, + CF1: Absorb, { - 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); - let u_dummy_native = CCCS::::dummy(&self.ccs); - 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(); + type ProofDummyCfg = (usize, usize, usize, usize); + type Proof = NIMFSProof; + + fn fold_gadget( + arith: &CCS>, + transcript: &mut PoseidonSpongeVar>, + pp_hash: FpVar>, + U: LCCCSVar, + _U_vec: Vec>>, + u: CCCSVar, + proof: Self::Proof, + ) -> Result, SynthesisError> { + let cs = transcript.cs(); 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} - ccs_matrices.enforce_relation(&W_i1, &U_i1)?; - - // 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, &i, &z_0, &z_i)?; - (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::arith::r1cs::circuits::R1CSMatricesVar; - use crate::commitment::pedersen::PedersenGadget; - use crate::folding::circuits::nonnative::uint::NonNativeUintVar; - 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); - 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, - &G, - &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 = R1CSMatricesVar::>>::new_witness( - cs.clone(), - || Ok(self.cf_r1cs.clone()), - )?; - - // 5. check RelaxedR1CS of cf_U_i - cf_r1cs.enforce_relation(&cf_W_i, &cf_U_i)?; - } - - // 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_bits` computed along the way of computing `computed_U_i1` for the later `rho_powers` - // check (6.b). - - // 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_bits) = NIMFSGadget::::verify( + let nimfs_proof = NIMFSProofVar::::new_witness(cs.clone(), || Ok(proof))?; + let (computed_U_i1, _) = NIMFSGadget::::verify( cs.clone(), - &self.ccs.clone(), - &mut transcript, - &[U_i], - &[u_i], + arith, + transcript, + &[U], + &[u], 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)?; - - // 7. check eval_W==p_W(c_W) - let incircuit_eval_W = evaluate_gadget::>(W_i1.w, incircuit_challenge)?; - incircuit_eval_W.enforce_equal(&eval_W)?; - - // 8.b check that the in-circuit computed r is equal to the inputted r. - - let rho = Boolean::le_bits_to_fp_var(&rho_bits)?; - let external_rho = - FpVar::>::new_input(cs.clone(), || Ok(self.rho.unwrap_or(CF1::::zero())))?; - rho.enforce_equal(&external_rho)?; - - 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() + Ok(computed_U_i1) } } @@ -471,11 +220,11 @@ where 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 ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem}; + use ark_std::{test_rng, One, UniformRand}; use super::*; + use crate::arith::r1cs::R1CS; use crate::commitment::pedersen::Pedersen; use crate::folding::nova::PreprocessorParam; use crate::frontend::utils::CubicFCircuit; @@ -563,15 +312,8 @@ pub mod tests { .unwrap(); // load the DeciderEthCircuit from the generated Nova instance - let decider_circuit = DeciderEthCircuit::< - Projective, - GVar, - Projective2, - GVar2, - Pedersen, - Pedersen, - >::from_hypernova(hypernova) - .unwrap(); + let decider_circuit = + DeciderEthCircuit::::try_from(hypernova).unwrap(); let cs = ConstraintSystem::::new_ref(); diff --git a/folding-schemes/src/folding/hypernova/nimfs.rs b/folding-schemes/src/folding/hypernova/nimfs.rs index 1fae370f..e74f02f7 100644 --- a/folding-schemes/src/folding/hypernova/nimfs.rs +++ b/folding-schemes/src/folding/hypernova/nimfs.rs @@ -31,28 +31,34 @@ pub struct NIMFSProof { pub sigmas_thetas: SigmasThetas, } -impl Dummy<(&CCS>, usize, usize)> for NIMFSProof { - fn dummy((ccs, mu, nu): (&CCS>, usize, usize)) -> Self { +impl Dummy<(usize, usize, usize, usize)> for NIMFSProof { + fn dummy((s, t, mu, nu): (usize, usize, usize, usize)) -> Self { // use 'C::ScalarField::one()' instead of 'zero()' to enforce the NIMFSProof to have the // same in-circuit representation to match the number of constraints of an actual proof. NIMFSProof:: { sc_proof: SumCheckProof:: { - point: vec![C::ScalarField::one(); ccs.s], + point: vec![C::ScalarField::one(); s], proofs: vec![ IOPProverMessage { - coeffs: vec![C::ScalarField::one(); ccs.t + 1] + coeffs: vec![C::ScalarField::one(); t + 1] }; - ccs.s + s ], }, sigmas_thetas: SigmasThetas( - vec![vec![C::ScalarField::one(); ccs.t]; mu], - vec![vec![C::ScalarField::one(); ccs.t]; nu], + vec![vec![C::ScalarField::one(); t]; mu], + vec![vec![C::ScalarField::one(); t]; nu], ), } } } +impl Dummy<(&CCS>, usize, usize)> for NIMFSProof { + fn dummy((ccs, mu, nu): (&CCS>, usize, usize)) -> Self { + NIMFSProof::dummy((ccs.s, ccs.t, mu, nu)) + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct SigmasThetas(pub Vec>, pub Vec>); @@ -67,7 +73,6 @@ pub struct NIMFS> { impl> NIMFS where ::ScalarField: Absorb, - C::BaseField: PrimeField, { pub fn fold( lcccs: &[LCCCS], diff --git a/folding-schemes/src/folding/nova/circuits.rs b/folding-schemes/src/folding/nova/circuits.rs index a0689ae0..7cf24080 100644 --- a/folding-schemes/src/folding/nova/circuits.rs +++ b/folding-schemes/src/folding/nova/circuits.rs @@ -72,11 +72,7 @@ where } } -impl AbsorbGadget for CommittedInstanceVar -where - C: CurveGroup, - ::BaseField: ark_ff::PrimeField, -{ +impl AbsorbGadget for CommittedInstanceVar { fn to_sponge_bytes(&self) -> Result>, SynthesisError> { FpVar::batch_to_sponge_bytes(&self.to_sponge_field_elements()?) } @@ -123,11 +119,7 @@ pub struct NIFSGadget { _c: PhantomData, } -impl NIFSGadget -where - C: CurveGroup, - ::BaseField: ark_ff::PrimeField, -{ +impl NIFSGadget { pub fn fold_committed_instance( r: FpVar>, ci1: CommittedInstanceVar, // U_i @@ -173,8 +165,6 @@ pub struct ChallengeGadget { } impl ChallengeGadget where - C: CurveGroup, - ::BaseField: PrimeField, ::ScalarField: Absorb, { pub fn get_challenge_native>( diff --git a/folding-schemes/src/folding/nova/decider.rs b/folding-schemes/src/folding/nova/decider.rs index e67d77da..faa654b9 100644 --- a/folding-schemes/src/folding/nova/decider.rs +++ b/folding-schemes/src/folding/nova/decider.rs @@ -13,13 +13,13 @@ use ark_std::{One, Zero}; use core::marker::PhantomData; use super::decider_circuits::{DeciderCircuit1, DeciderCircuit2}; -use super::{nifs::NIFS, traits::NIFSTrait, CommittedInstance, Nova}; +use super::{CommittedInstance, Nova}; use crate::commitment::CommitmentScheme; use crate::folding::circuits::{ cyclefold::{CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar}, CF2, }; -use crate::folding::traits::Inputize; +use crate::folding::traits::{CommittedInstanceOps, Inputize, WitnessOps}; use crate::frontend::FCircuit; use crate::Error; use crate::{Decider as DeciderTrait, FoldingScheme}; @@ -38,12 +38,9 @@ where c2_snark_proof: S2::Proof, cs1_proofs: [CS1::Proof; 2], cs2_proofs: [CS2::Proof; 2], - // cmT and r are values for the last fold, U_{i+1}=NIFS.V(r, U_i, u_i, cmT), and they are - // checked in-circuit - cmT: C1, - r: C1::ScalarField, + U_final: CommittedInstance, // cyclefold committed instance - cf_U_i: CycleFoldCommittedInstance, + cf_U_final: CycleFoldCommittedInstance, // the CS challenges are provided by the prover, but in-circuit they are checked to match the // in-circuit computed computed ones. cs1_challenges: [C1::ScalarField; 2], @@ -144,18 +141,15 @@ where S2::VerifyingKey, >; type PublicInput = Vec; - type CommittedInstance = CommittedInstance; + type CommittedInstance = (); fn preprocess( mut rng: impl RngCore + CryptoRng, prep_param: Self::PreprocessorParam, fs: FS, ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { - let circuit1 = DeciderCircuit1::::from_nova::( - fs.clone().into(), - )?; - let circuit2 = - DeciderCircuit2::::from_nova::(fs.into())?; + let circuit1 = DeciderCircuit1::::try_from(Nova::from(fs.clone()))?; + let circuit2 = DeciderCircuit2::::try_from(Nova::from(fs))?; // get the Groth16 specific setup for the circuits let (c1_g16_pk, c1_g16_vk) = S1::circuit_specific_setup(circuit1, &mut rng).unwrap(); @@ -197,78 +191,47 @@ where pp: Self::ProverParam, fs: FS, ) -> Result { - let circuit1 = DeciderCircuit1::::from_nova::( - fs.clone().into(), - )?; - let circuit2 = - DeciderCircuit2::::from_nova::(fs.into())?; - - let c1_snark_proof = S1::prove(&pp.c1_snark_pp, circuit1.clone(), &mut rng) + let circuit1 = DeciderCircuit1::::try_from(Nova::from(fs.clone()))?; + let circuit2 = DeciderCircuit2::::try_from(Nova::from(fs))?; + + let U_final = circuit1.U_i1.clone(); + let cf_U_final = circuit1.cf_U_i.clone(); + + let c1_kzg_challenges = circuit1.kzg_challenges.clone(); + let c1_kzg_proofs = circuit1 + .W_i1 + .get_openings() + .iter() + .zip(&c1_kzg_challenges) + .map(|((v, _), &c)| { + CS1::prove_with_challenge(&pp.c1_cs_pp, c, v, &C1::ScalarField::zero(), None) + }) + .collect::, _>>()?; + let c2_kzg_challenges = circuit2.kzg_challenges.clone(); + let c2_kzg_proofs = circuit2 + .cf_W_i + .get_openings() + .iter() + .zip(&c2_kzg_challenges) + .map(|((v, _), &c)| { + CS2::prove_with_challenge(&pp.c2_cs_pp, c, v, &C2::ScalarField::zero(), None) + }) + .collect::, _>>()?; + + let c1_snark_proof = S1::prove(&pp.c1_snark_pp, circuit1, &mut rng) .map_err(|e| Error::Other(e.to_string()))?; - let c2_snark_proof = S2::prove(&pp.c2_snark_pp, circuit2.clone(), &mut rng) + let c2_snark_proof = S2::prove(&pp.c2_snark_pp, circuit2, &mut rng) .map_err(|e| Error::Other(e.to_string()))?; - let cmT = circuit1.cmT.unwrap(); - let r_Fr = circuit1.r.unwrap(); - let W_i1 = circuit1.W_i1.unwrap(); - let cf_W_i = circuit2.cf_W_i.unwrap(); - - // get the challenges that have been already computed when preparing the circuits inputs in - // the above `from_nova` calls - let challenge_W = circuit1 - .cs_c_W - .ok_or(Error::MissingValue("cs_c_W".to_string()))?; - let challenge_E = circuit1 - .cs_c_E - .ok_or(Error::MissingValue("cs_c_E".to_string()))?; - let c2_challenge_W = circuit2 - .cs_c_W - .ok_or(Error::MissingValue("c2's cs_c_W".to_string()))?; - let c2_challenge_E = circuit2 - .cs_c_E - .ok_or(Error::MissingValue("c2's cs_c_E".to_string()))?; - - // generate CommitmentScheme proofs for the main instance - let U_cmW_proof = CS1::prove_with_challenge( - &pp.c1_cs_pp, - challenge_W, - &W_i1.W, - &C1::ScalarField::zero(), - None, - )?; - let U_cmE_proof = CS1::prove_with_challenge( - &pp.c1_cs_pp, - challenge_E, - &W_i1.E, - &C1::ScalarField::zero(), - None, - )?; - // CS proofs for the CycleFold instance - let cf_cmW_proof = CS2::prove_with_challenge( - &pp.c2_cs_pp, - c2_challenge_W, - &cf_W_i.W, - &C2::ScalarField::zero(), - None, - )?; - let cf_cmE_proof = CS2::prove_with_challenge( - &pp.c2_cs_pp, - c2_challenge_E, - &cf_W_i.E, - &C2::ScalarField::zero(), - None, - )?; - Ok(Self::Proof { c1_snark_proof, c2_snark_proof, - cs1_proofs: [U_cmW_proof, U_cmE_proof], - cs2_proofs: [cf_cmW_proof, cf_cmE_proof], - cmT, - r: r_Fr, - cf_U_i: circuit1.cf_U_i.unwrap(), - cs1_challenges: [challenge_W, challenge_E], - cs2_challenges: [c2_challenge_W, c2_challenge_E], + cs1_proofs: c1_kzg_proofs.try_into().unwrap(), + cs2_proofs: c2_kzg_proofs.try_into().unwrap(), + U_final, + cf_U_final, + cs1_challenges: c1_kzg_challenges.try_into().unwrap(), + cs2_challenges: c2_kzg_challenges.try_into().unwrap(), }) } @@ -277,33 +240,27 @@ where i: C1::ScalarField, z_0: Vec, z_i: Vec, - running_instance: &Self::CommittedInstance, - incoming_instance: &Self::CommittedInstance, + // we don't use the instances at the verifier level, since we check them in-circuit + _running_instance: &Self::CommittedInstance, + _incoming_instance: &Self::CommittedInstance, proof: &Self::Proof, ) -> Result { if i <= C1::ScalarField::one() { return Err(Error::NotEnoughSteps); } - // compute U = U_{d+1}= NIFS.V(U_d, u_d, cmT) - let U = NIFS::::verify(proof.r, running_instance, incoming_instance, &proof.cmT); + let U = proof.U_final.clone(); + let cf_U = proof.cf_U_final.clone(); // snark proof 1 - let c1_public_input: Vec = [ - vec![vp.pp_hash, i], - z_0, - z_i, - U.inputize(), - // CS1 values: - proof.cs1_challenges.to_vec(), // c_W, c_E - vec![ - proof.cs1_proofs[0].eval, // eval_W - proof.cs1_proofs[1].eval, // eval_E - ], - Inputize::, CycleFoldCommittedInstanceVar>::inputize(&proof.cf_U_i), - // NIFS values: - proof.cmT.inputize(), - vec![proof.r], + let c1_public_input = [ + &[vp.pp_hash, i][..], + &z_0, + &z_i, + &U.inputize(), + &Inputize::, CycleFoldCommittedInstanceVar>::inputize(&cf_U), + &proof.cs1_challenges, + &proof.cs1_proofs.iter().map(|p| p.eval).collect::>(), ] .concat(); @@ -318,13 +275,10 @@ where let pp_hash_Fq = C2::ScalarField::from_le_bytes_mod_order(&vp.pp_hash.into_bigint().to_bytes_le()); let c2_public_input: Vec = [ - vec![pp_hash_Fq], - proof.cf_U_i.inputize(), - proof.cs2_challenges.to_vec(), - vec![ - proof.cs2_proofs[0].eval, // eval_W - proof.cs2_proofs[1].eval, // eval_E - ], + &[pp_hash_Fq][..], + &cf_U.inputize(), + &proof.cs2_challenges, + &proof.cs2_proofs.iter().map(|p| p.eval).collect::>(), ] .concat(); @@ -335,32 +289,24 @@ where } // check C1 commitments (main instance commitments) - CS1::verify_with_challenge( - &vp.c1_cs_vp, - proof.cs1_challenges[0], - &U.cmW, - &proof.cs1_proofs[0], - )?; - CS1::verify_with_challenge( - &vp.c1_cs_vp, - proof.cs1_challenges[1], - &U.cmE, - &proof.cs1_proofs[1], - )?; + for ((cm, &c), pi) in U + .get_commitments() + .iter() + .zip(&proof.cs1_challenges) + .zip(&proof.cs1_proofs) + { + CS1::verify_with_challenge(&vp.c1_cs_vp, c, cm, pi)?; + } // check C2 commitments (CycleFold instance commitments) - CS2::verify_with_challenge( - &vp.c2_cs_vp, - proof.cs2_challenges[0], - &proof.cf_U_i.cmW, - &proof.cs2_proofs[0], - )?; - CS2::verify_with_challenge( - &vp.c2_cs_vp, - proof.cs2_challenges[1], - &proof.cf_U_i.cmE, - &proof.cs2_proofs[1], - )?; + for ((cm, &c), pi) in cf_U + .get_commitments() + .iter() + .zip(&proof.cs2_challenges) + .zip(&proof.cs2_proofs) + { + CS2::verify_with_challenge(&vp.c2_cs_vp, c, cm, pi)?; + } Ok(true) } @@ -445,10 +391,7 @@ pub mod tests { // decider proof verification let start = Instant::now(); - let verified = D::verify( - decider_vp, nova.i, nova.z_0, nova.z_i, &nova.U_i, &nova.u_i, &proof, - ) - .unwrap(); + let verified = D::verify(decider_vp, nova.i, nova.z_0, nova.z_i, &(), &(), &proof).unwrap(); assert!(verified); println!("Decider verify, {:?}", start.elapsed()); } diff --git a/folding-schemes/src/folding/nova/decider_circuits.rs b/folding-schemes/src/folding/nova/decider_circuits.rs index 0a3492ec..160f3909 100644 --- a/folding-schemes/src/folding/nova/decider_circuits.rs +++ b/folding-schemes/src/folding/nova/decider_circuits.rs @@ -2,116 +2,62 @@ /// 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_crypto_primitives::sponge::{poseidon::PoseidonSponge, Absorb, CryptographicSponge}; +use ark_ec::CurveGroup; use ark_ff::{BigInteger, PrimeField}; -use ark_poly::Polynomial; -use ark_r1cs_std::{ - alloc::AllocVar, - boolean::Boolean, - eq::EqGadget, - fields::{fp::FpVar, FieldVar}, - prelude::CurveVar, - ToConstraintFieldGadget, -}; -use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; -use ark_std::Zero; +use ark_r1cs_std::{fields::fp::FpVar, prelude::CurveVar, ToConstraintFieldGadget}; use core::marker::PhantomData; use super::{ - circuits::{ChallengeGadget, CommittedInstanceVar}, - decider_eth_circuit::{evaluate_gadget, KZGChallengesGadget, WitnessVar}, - nifs::NIFS, - traits::NIFSTrait, - CommittedInstance, Nova, Witness, + decider_eth_circuit::DeciderNovaGadget, nifs::NIFS, traits::NIFSTrait, CommittedInstance, Nova, + Witness, }; -use crate::arith::{ - r1cs::{circuits::R1CSMatricesVar, R1CS}, - ArithGadget, +use crate::folding::{ + circuits::{CF1, CF2}, + traits::WitnessOps, }; -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, Dummy}; use crate::frontend::FCircuit; -use crate::utils::vec::poly_from_vec; use crate::Error; +use crate::{ + arith::r1cs::{circuits::R1CSMatricesVar, R1CS}, + folding::circuits::decider::{ + off_chain::{GenericOffchainDeciderCircuit1, GenericOffchainDeciderCircuit2}, + EvalGadget, KZGChallengesGadget, + }, +}; +use crate::{commitment::CommitmentScheme, transcript::poseidon::poseidon_canonical_config}; /// 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>, - /// Commitment Scheme challenges - pub cs_c_W: Option, - pub cs_c_E: Option, - /// Evaluations of the committed polynomials at the challenge - pub eval_W: Option, - pub eval_E: Option, -} -impl DeciderCircuit1 -where - C1: CurveGroup, - ::BaseField: PrimeField, - ::ScalarField: Absorb, - C2: CurveGroup, - GC2: CurveVar> + ToConstraintFieldGadget>, -{ - pub fn from_nova>( - nova: Nova, - ) -> Result - where - C2: CurveGroup, +pub type DeciderCircuit1 = GenericOffchainDeciderCircuit1< + C1, + C2, + GC2, + CommittedInstance, + CommittedInstance, + Witness, + R1CS>, + R1CSMatricesVar, FpVar>>, + DeciderNovaGadget, +>; + +impl< + C1: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, + C2: CurveGroup, GC2: CurveVar> + ToConstraintFieldGadget>, + FC: FCircuit, CS1: CommitmentScheme, CS2: CommitmentScheme, - { + const H: bool, + > TryFrom> for DeciderCircuit1 +where + CF1: Absorb, +{ + type Error = Error; + + fn try_from(nova: Nova) -> Result { let mut transcript = PoseidonSponge::::new(&nova.poseidon_config); // pp_hash is absorbed to transcript at the ChallengeGadget::get_challenge_native call @@ -136,355 +82,97 @@ where let (W_i1, U_i1) = NIFS::::prove(r_Fr, &nova.W_i, &nova.U_i, &nova.w_i, &nova.u_i, &T, &cmT)?; - // compute the commitment scheme challenges used as inputs in the circuit - let (cs_challenge_W, cs_challenge_E) = - KZGChallengesGadget::::get_challenges_native(&mut transcript, U_i1.clone()); + // compute the KZG challenges used as inputs in the circuit + let kzg_challenges = KZGChallengesGadget::get_challenges_native(&mut transcript, &U_i1); - // get evals of the committed polys at the challenges - 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(&cs_challenge_W); - let p_E = poly_from_vec(E.to_vec())?; - let eval_E = p_E.evaluate(&cs_challenge_E); + // get KZG evals + let kzg_evaluations = W_i1 + .get_openings() + .iter() + .zip(&kzg_challenges) + .map(|((v, _), &c)| EvalGadget::evaluate_native(v, c)) + .collect::, _>>()?; 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, + _avar: PhantomData, + arith: 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), - cs_c_W: Some(cs_challenge_W), - cs_c_E: Some(cs_challenge_E), - eval_W: Some(eval_W), - eval_E: Some(eval_E), + pp_hash: nova.pp_hash, + i: nova.i, + z_0: nova.z_0, + z_i: nova.z_i, + U_i: nova.U_i, + W_i: nova.W_i, + u_i: nova.u_i, + w_i: nova.w_i, + U_i1, + W_i1, + proof: cmT, + cf_U_i: nova.cf_U_i, + kzg_challenges, + kzg_evaluations, }) } } -impl ConstraintSynthesizer> for DeciderCircuit1 -where - C1: CurveGroup, - ::BaseField: PrimeField, - ::ScalarField: Absorb, - C2: CurveGroup, - ::BaseField: PrimeField, - ::ScalarField: Absorb, - C1: CurveGroup, - GC2: CurveVar> + ToConstraintFieldGadget>, -{ - fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - let r1cs = - R1CSMatricesVar::>>::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 6 - let cs_c_W = FpVar::>::new_input(cs.clone(), || { - Ok(self.cs_c_W.unwrap_or_else(CF1::::zero)) - })?; - let cs_c_E = FpVar::>::new_input(cs.clone(), || { - Ok(self.cs_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} - r1cs.enforce_relation(&W_i1, &U_i1)?; - - // 1.1.a, 5.1 compute NIFS.V and Commitment Scheme 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(), - Some(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(&cs_c_W)?; - incircuit_c_E.enforce_equal(&cs_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(()) - } -} - /// 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, +pub type DeciderCircuit2 = GenericOffchainDeciderCircuit2; - /// 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>, - /// Commitment Scheme challenges - pub cs_c_W: Option, - pub cs_c_E: Option, - /// Evaluations of the committed polynomials at the challenge - 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 +impl< + C1: CurveGroup, + GC1: CurveVar> + ToConstraintFieldGadget>, + C2: CurveGroup, GC2: CurveVar> + ToConstraintFieldGadget>, + FC: FCircuit, CS1: CommitmentScheme, CS2: CommitmentScheme, - { + const H: bool, + > TryFrom> for DeciderCircuit2 +where + CF1: Absorb, +{ + type Error = Error; + + fn try_from(nova: Nova) -> Result { // compute the Commitment Scheme challenges of the CycleFold instance commitments, used as // inputs in the circuit - let poseidon_config = - crate::transcript::poseidon::poseidon_canonical_config::(); + let poseidon_config = 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 (cs_challenge_W, cs_challenge_E) = - KZGChallengesGadget::::get_challenges_native(&mut transcript, nova.cf_U_i.clone()); + // compute the KZG challenges used as inputs in the circuit + let kzg_challenges = + KZGChallengesGadget::get_challenges_native(&mut transcript, &nova.cf_U_i); - // get evals of the committed polynomials at the challenge - 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(&cs_challenge_W); - let p_E = poly_from_vec(E.to_vec())?; - let eval_E = p_E.evaluate(&cs_challenge_E); + // get KZG evals + let kzg_evaluations = nova + .cf_W_i + .get_openings() + .iter() + .zip(&kzg_challenges) + .map(|((v, _), &c)| EvalGadget::evaluate_native(v, c)) + .collect::, _>>()?; Ok(Self { - _c1: PhantomData, - _gc1: PhantomData, - _c2: PhantomData, - - cf_E_len: nova.cf_W_i.E.len(), - cf_r1cs: nova.cf_r1cs, + cf_arith: 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 challenges - cs_c_W: Some(cs_challenge_W), - cs_c_E: Some(cs_challenge_E), - eval_W: Some(eval_W), - eval_E: Some(eval_E), + pp_hash: pp_hash_Fq, + cf_U_i: nova.cf_U_i, + cf_W_i: nova.cf_W_i, + kzg_challenges, + kzg_evaluations, }) } } -impl ConstraintSynthesizer> for DeciderCircuit2 -where - C1: CurveGroup, - C2: CurveGroup, - ::BaseField: PrimeField, - ::BaseField: PrimeField, - ::ScalarField: Absorb, - ::ScalarField: Absorb, - C1: CurveGroup, - GC1: CurveVar> + ToConstraintFieldGadget>, -{ - 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(&self.cf_r1cs); - let w_dummy_native = Witness::::dummy(&self.cf_r1cs); - 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 = - R1CSMatricesVar::>>::new_witness(cs.clone(), || { - Ok(self.cf_r1cs.clone()) - })?; - - // 6. check RelaxedR1CS of cf_U_i - cf_r1cs.enforce_relation(&cf_W_i, &cf_U_i)?; - - // `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 cs_c_W = FpVar::>::new_input(cs.clone(), || { - Ok(self.cs_c_W.unwrap_or_else(CF1::::zero)) - })?; - let cs_c_E = FpVar::>::new_input(cs.clone(), || { - Ok(self.cs_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 commitment scheme 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(&cs_c_W)?; - incircuit_c_E.enforce_equal(&cs_c_E)?; - - // 7.2. check eval_W==p_W(c_W) and eval_E==p_E(c_E) - let incircuit_eval_W = evaluate_gadget::>(cf_W_i.W, incircuit_c_W)?; - let incircuit_eval_E = evaluate_gadget::>(cf_W_i.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_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem}; use ark_std::One; use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; @@ -542,9 +230,8 @@ pub mod tests { // 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(); + DeciderCircuit1::::try_from(nova.clone()).unwrap(); + let decider_circuit2 = DeciderCircuit2::::try_from(nova).unwrap(); // generate the constraints of both circuits and check that are satisfied by the inputs let cs1 = ConstraintSystem::::new_ref(); diff --git a/folding-schemes/src/folding/nova/decider_eth.rs b/folding-schemes/src/folding/nova/decider_eth.rs index 7bc73f5e..92a2e295 100644 --- a/folding-schemes/src/folding/nova/decider_eth.rs +++ b/folding-schemes/src/folding/nova/decider_eth.rs @@ -15,35 +15,31 @@ use ark_std::{One, Zero}; use core::marker::PhantomData; pub use super::decider_eth_circuit::DeciderEthCircuit; -use super::traits::NIFSTrait; -use super::{nifs::NIFS, CommittedInstance, Nova}; +use super::{CommittedInstance, Nova}; use crate::commitment::{ kzg::{Proof as KZGProof, KZG}, pedersen::Params as PedersenParams, CommitmentScheme, }; use crate::folding::circuits::CF2; -use crate::folding::traits::Inputize; +use crate::folding::traits::{CommittedInstanceOps, Inputize, WitnessOps}; use crate::frontend::FCircuit; use crate::Error; use crate::{Decider as DeciderTrait, FoldingScheme}; #[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] -pub struct Proof +pub struct Proof where - C1: CurveGroup, - CS1: CommitmentScheme, - S: SNARK, + C: CurveGroup, + CS: CommitmentScheme, + S: SNARK, { snark_proof: S::Proof, - kzg_proofs: [CS1::Proof; 2], - // cmT and r are values for the last fold, U_{i+1}=NIFS.V(r, U_i, u_i, cmT), and they are - // checked in-circuit - cmT: C1, - r: C1::ScalarField, + kzg_proofs: [CS::Proof; 2], + U_final: CommittedInstance, // the KZG challenges are provided by the prover, but in-circuit they are checked to match // the in-circuit computed computed ones. - kzg_challenges: [C1::ScalarField; 2], + kzg_challenges: [C::ScalarField; 2], } #[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] @@ -108,15 +104,14 @@ where type Proof = Proof; type VerifierParam = VerifierParam; type PublicInput = Vec; - type CommittedInstance = CommittedInstance; + type CommittedInstance = (); fn preprocess( mut rng: impl RngCore + CryptoRng, prep_param: Self::PreprocessorParam, fs: FS, ) -> Result<(Self::ProverParam, Self::VerifierParam), Error> { - let circuit = - DeciderEthCircuit::::from_nova::(fs.into()).unwrap(); + let circuit = DeciderEthCircuit::::try_from(Nova::from(fs))?; // get the Groth16 specific setup for the circuit let (g16_pk, g16_vk) = S::circuit_specific_setup(circuit, &mut rng).unwrap(); @@ -152,48 +147,33 @@ where ) -> Result { let (snark_pk, cs_pk): (S::ProvingKey, CS1::ProverParams) = pp; - let circuit = DeciderEthCircuit::::from_nova::( - folding_scheme.into(), - )?; + let circuit = DeciderEthCircuit::::try_from(Nova::from(folding_scheme))?; - let snark_proof = S::prove(&snark_pk, circuit.clone(), &mut rng) - .map_err(|e| Error::Other(e.to_string()))?; - - let cmT = circuit.cmT.unwrap(); - let r_Fr = circuit.r.unwrap(); - let W_i1 = circuit.W_i1.unwrap(); + let U_final = circuit.U_i1.clone(); // get the challenges that have been already computed when preparing the circuit inputs in - // the above `from_nova` call - let challenge_W = circuit - .kzg_c_W - .ok_or(Error::MissingValue("kzg_c_W".to_string()))?; - let challenge_E = circuit - .kzg_c_E - .ok_or(Error::MissingValue("kzg_c_E".to_string()))?; + // the above `try_from` call + let kzg_challenges = circuit.kzg_challenges.clone(); // generate KZG proofs - let U_cmW_proof = CS1::prove_with_challenge( - &cs_pk, - challenge_W, - &W_i1.W, - &C1::ScalarField::zero(), - None, - )?; - let U_cmE_proof = CS1::prove_with_challenge( - &cs_pk, - challenge_E, - &W_i1.E, - &C1::ScalarField::zero(), - None, - )?; + let kzg_proofs = circuit + .W_i1 + .get_openings() + .iter() + .zip(&kzg_challenges) + .map(|((v, _), &c)| { + CS1::prove_with_challenge(&cs_pk, c, v, &C1::ScalarField::zero(), None) + }) + .collect::, _>>()?; + + let snark_proof = + S::prove(&snark_pk, circuit, &mut rng).map_err(|e| Error::Other(e.to_string()))?; Ok(Self::Proof { snark_proof, - kzg_proofs: [U_cmW_proof, U_cmE_proof], - cmT, - r: r_Fr, - kzg_challenges: [challenge_W, challenge_E], + U_final, + kzg_proofs: kzg_proofs.try_into().unwrap(), + kzg_challenges: kzg_challenges.try_into().unwrap(), }) } @@ -202,51 +182,48 @@ where i: C1::ScalarField, z_0: Vec, z_i: Vec, - running_instance: &Self::CommittedInstance, - incoming_instance: &Self::CommittedInstance, + // we don't use the instances at the verifier level, since we check them in-circuit + _running_instance: &Self::CommittedInstance, + _incoming_instance: &Self::CommittedInstance, proof: &Self::Proof, ) -> Result { if i <= C1::ScalarField::one() { return Err(Error::NotEnoughSteps); } - // compute U = U_{d+1}= NIFS.V(U_d, u_d, cmT) - let U = NIFS::::verify(proof.r, running_instance, incoming_instance, &proof.cmT); - - let public_input: Vec = [ - vec![vp.pp_hash, i], - z_0, - z_i, - U.inputize(), - proof.kzg_challenges.to_vec(), - vec![ - proof.kzg_proofs[0].eval, // eval_W - proof.kzg_proofs[1].eval, // eval_E - ], - proof.cmT.inputize(), - vec![proof.r], + let Self::VerifierParam { + pp_hash, + snark_vp, + cs_vp, + } = vp; + + let U = proof.U_final.clone(); + + let public_input = [ + &[pp_hash, i][..], + &z_0, + &z_i, + &U.inputize(), + &proof.kzg_challenges, + &proof.kzg_proofs.iter().map(|p| p.eval).collect::>(), ] .concat(); - let snark_v = S::verify(&vp.snark_vp, &public_input, &proof.snark_proof) + let snark_v = S::verify(&snark_vp, &public_input, &proof.snark_proof) .map_err(|e| Error::Other(e.to_string()))?; if !snark_v { return Err(Error::SNARKVerificationFail); } - // we're at the Ethereum EVM case, so the CS1 is KZG commitments - CS1::verify_with_challenge( - &vp.cs_vp, - proof.kzg_challenges[0], - &U.cmW, - &proof.kzg_proofs[0], - )?; - CS1::verify_with_challenge( - &vp.cs_vp, - proof.kzg_challenges[1], - &U.cmE, - &proof.kzg_proofs[1], - )?; + for ((cm, &c), pi) in U + .get_commitments() + .iter() + .zip(&proof.kzg_challenges) + .zip(&proof.kzg_proofs) + { + // we're at the Ethereum EVM case, so the CS1 is KZG commitments + CS1::verify_with_challenge(&cs_vp, c, cm, pi)?; + } Ok(true) } @@ -258,8 +235,8 @@ pub fn prepare_calldata( i: ark_bn254::Fr, z_0: Vec, z_i: Vec, - running_instance: &CommittedInstance, - incoming_instance: &CommittedInstance, + _running_instance: &CommittedInstance, + _incoming_instance: &CommittedInstance, proof: Proof, Groth16>, ) -> Result, Error> { Ok(vec![ @@ -271,23 +248,15 @@ pub fn prepare_calldata( z_i.iter() .flat_map(|v| v.into_bigint().to_bytes_be()) .collect::>(), // z_i - point_to_eth_format(running_instance.cmW.into_affine())?, // U_i_cmW - point_to_eth_format(running_instance.cmE.into_affine())?, // U_i_cmE - running_instance.u.into_bigint().to_bytes_be(), // U_i_u - incoming_instance.u.into_bigint().to_bytes_be(), // u_i_u - proof.r.into_bigint().to_bytes_be(), // r - running_instance + point_to_eth_format(proof.U_final.cmW.into_affine())?, // U_final_cmW + point_to_eth_format(proof.U_final.cmE.into_affine())?, // U_final_cmE + proof.U_final.u.into_bigint().to_bytes_be(), // U_final_u + proof + .U_final .x .iter() .flat_map(|v| v.into_bigint().to_bytes_be()) - .collect::>(), // U_i_x - point_to_eth_format(incoming_instance.cmW.into_affine())?, // u_i_cmW - incoming_instance - .x - .iter() - .flat_map(|v| v.into_bigint().to_bytes_be()) - .collect::>(), // u_i_x - point_to_eth_format(proof.cmT.into_affine())?, // cmT + .collect::>(), // U_final_x point_to_eth_format(proof.snark_proof.a)?, // pA point2_to_eth_format(proof.snark_proof.b)?, // pB point_to_eth_format(proof.snark_proof.c)?, // pC @@ -396,8 +365,8 @@ pub mod tests { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &(), + &(), &proof, ) .unwrap(); @@ -405,10 +374,7 @@ pub mod tests { println!("Decider verify, {:?}", start.elapsed()); // decider proof verification using the deserialized data - let verified = D::verify( - decider_vp, nova.i, nova.z_0, nova.z_i, &nova.U_i, &nova.u_i, &proof, - ) - .unwrap(); + let verified = D::verify(decider_vp, nova.i, nova.z_0, nova.z_i, &(), &(), &proof).unwrap(); assert!(verified); } @@ -510,8 +476,8 @@ pub mod tests { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &(), + &(), &proof, ) .unwrap(); @@ -539,12 +505,6 @@ pub mod tests { nova.z_i .serialize_compressed(&mut public_inputs_serialized) .unwrap(); - nova.U_i - .serialize_compressed(&mut public_inputs_serialized) - .unwrap(); - nova.u_i - .serialize_compressed(&mut public_inputs_serialized) - .unwrap(); // deserialize back the verifier_params, proof and public inputs let decider_vp_deserialized = @@ -565,10 +525,6 @@ pub mod tests { let i_deserialized = Fr::deserialize_compressed(&mut reader).unwrap(); let z_0_deserialized = Vec::::deserialize_compressed(&mut reader).unwrap(); let z_i_deserialized = Vec::::deserialize_compressed(&mut reader).unwrap(); - let U_i_deserialized = - CommittedInstance::::deserialize_compressed(&mut reader).unwrap(); - let u_i_deserialized = - CommittedInstance::::deserialize_compressed(&mut reader).unwrap(); // decider proof verification using the deserialized data let verified = D::verify( @@ -576,8 +532,8 @@ pub mod tests { i_deserialized, z_0_deserialized, z_i_deserialized, - &U_i_deserialized, - &u_i_deserialized, + &(), + &(), &proof_deserialized, ) .unwrap(); diff --git a/folding-schemes/src/folding/nova/decider_eth_circuit.rs b/folding-schemes/src/folding/nova/decider_eth_circuit.rs index ef635148..18167060 100644 --- a/folding-schemes/src/folding/nova/decider_eth_circuit.rs +++ b/folding-schemes/src/folding/nova/decider_eth_circuit.rs @@ -4,47 +4,40 @@ /// 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}, + poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}, Absorb, CryptographicSponge, }; -use ark_ec::{CurveGroup, Group}; +use ark_ec::CurveGroup; use ark_ff::{BigInteger, PrimeField}; -use ark_poly::Polynomial; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, boolean::Boolean, - eq::EqGadget, - fields::{fp::FpVar, FieldVar}, - poly::{domain::Radix2DomainVar, evaluations::univariate::EvaluationsVar}, + fields::fp::FpVar, prelude::CurveVar, ToConstraintFieldGadget, }; -use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; -use ark_std::{log2, Zero}; -use core::{borrow::Borrow, marker::PhantomData}; +use ark_relations::r1cs::{Namespace, SynthesisError}; +use ark_std::{borrow::Borrow, marker::PhantomData}; use super::{ - circuits::{ChallengeGadget, CommittedInstanceVar}, + circuits::{ChallengeGadget, CommittedInstanceVar, NIFSGadget}, nifs::NIFS, traits::NIFSTrait, CommittedInstance, Nova, Witness, }; -use crate::folding::circuits::{ - cyclefold::{CycleFoldCommittedInstance, CycleFoldWitness}, - nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar}, - CF1, CF2, +use crate::commitment::{pedersen::Params as PedersenParams, CommitmentScheme}; +use crate::folding::{ + circuits::{ + decider::on_chain::GenericOnchainDeciderCircuit, nonnative::affine::NonNativeAffineVar, + CF1, CF2, + }, + traits::{WitnessOps, WitnessVarOps}, }; -use crate::folding::traits::{CommittedInstanceVarOps, Dummy, WitnessVarOps}; use crate::frontend::FCircuit; -use crate::transcript::{Transcript, TranscriptVar}; -use crate::utils::vec::poly_from_vec; use crate::Error; use crate::{ - arith::{ - r1cs::{circuits::R1CSMatricesVar, R1CS}, - ArithGadget, - }, - commitment::{pedersen::Params as PedersenParams, CommitmentScheme}, + arith::r1cs::{circuits::R1CSMatricesVar, R1CS}, + folding::circuits::decider::{DeciderEnabledNIFS, EvalGadget, KZGChallengesGadget}, }; /// In-circuit representation of the Witness associated to the CommittedInstance. @@ -85,82 +78,40 @@ where impl WitnessVarOps for WitnessVar { fn get_openings(&self) -> Vec<(&[FpVar], FpVar)> { - vec![(&self.E, self.rE.clone()), (&self.W, self.rW.clone())] + vec![(&self.W, self.rW.clone()), (&self.E, self.rE.clone())] } } -/// Circuit that implements the in-circuit checks needed for the onchain (Ethereum's EVM) -/// verification. -#[derive(Clone, Debug)] -pub struct DeciderEthCircuit +pub type DeciderEthCircuit = GenericOnchainDeciderCircuit< + C1, + C2, + GC2, + CommittedInstance, + CommittedInstance, + Witness, + R1CS>, + R1CSMatricesVar, FpVar>>, + DeciderNovaGadget, +>; + +/// returns an instance of the DeciderEthCircuit from the given Nova struct +impl< + C1: CurveGroup, + GC1: CurveVar> + ToConstraintFieldGadget>, + C2: CurveGroup, + GC2: CurveVar> + ToConstraintFieldGadget>, + FC: FCircuit, + CS1: CommitmentScheme, + // enforce that the CS2 is Pedersen commitment scheme, since we're at Ethereum's EVM decider + CS2: CommitmentScheme>, + const H: bool, + > TryFrom> for DeciderEthCircuit where - C1: CurveGroup, - GC1: CurveVar>, - C2: CurveGroup, - GC2: CurveVar>, - CS1: CommitmentScheme, - CS2: CommitmentScheme, + CF1: Absorb, { - _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>, + type Error = Error; - /// 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 { + fn try_from(nova: Nova) -> Result { let mut transcript = PoseidonSponge::::new(&nova.poseidon_config); // compute the U_{i+1}, W_{i+1} @@ -187,314 +138,82 @@ where )?; // 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()); + let kzg_challenges = KZGChallengesGadget::get_challenges_native(&mut transcript, &U_i1); // 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); + let kzg_evaluations = W_i1 + .get_openings() + .iter() + .zip(&kzg_challenges) + .map(|((v, _), &c)| EvalGadget::evaluate_native(v, c)) + .collect::, _>>()?; 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, + _avar: PhantomData, + arith: nova.r1cs, + cf_arith: 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), + pp_hash: nova.pp_hash, + i: nova.i, + z_0: nova.z_0, + z_i: nova.z_i, + U_i: nova.U_i, + W_i: nova.W_i, + u_i: nova.u_i, + w_i: nova.w_i, + U_i1, + W_i1, + proof: cmT, + cf_U_i: nova.cf_U_i, + cf_W_i: nova.cf_W_i, + kzg_challenges, + kzg_evaluations, }) } } -impl ConstraintSynthesizer> - for DeciderEthCircuit +pub struct DeciderNovaGadget; + +impl + DeciderEnabledNIFS, CommittedInstance, Witness, R1CS>> + for DeciderNovaGadget where - C1: CurveGroup, - C2: CurveGroup, - GC1: CurveVar>, - GC2: CurveVar> + ToConstraintFieldGadget>, - CS1: CommitmentScheme, - CS2: CommitmentScheme, - ::BaseField: PrimeField, - ::BaseField: PrimeField, - ::ScalarField: Absorb, - ::ScalarField: Absorb, - C1: CurveGroup, + CF1: Absorb, { - fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - let r1cs = - R1CSMatricesVar::>>::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} - r1cs.enforce_relation(&W_i1, &U_i1)?; - - #[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, - &G, - &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 = R1CSMatricesVar::>>::new_witness( - cs.clone(), - || Ok(self.cf_r1cs.clone()), - )?; - - // 6. check RelaxedR1CS of cf_U_i (CycleFold instance) - cf_r1cs.enforce_relation(&cf_W_i, &cf_U_i)?; - } - - // 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, + type ProofDummyCfg = (); + type Proof = C; + + fn fold_gadget( + _arith: &R1CS>, + transcript: &mut PoseidonSpongeVar>, + pp_hash: FpVar>, + U: CommittedInstanceVar, + U_vec: Vec>>, + u: CommittedInstanceVar, + proof: C, + ) -> Result, SynthesisError> { + let cs = transcript.cs(); + let cmT = NonNativeAffineVar::new_witness(cs.clone(), || Ok(proof))?; + let r_bits = ChallengeGadget::>::get_challenge_gadget( + transcript, pp_hash, - U_i_vec, - u_i.clone(), + U_vec, + u.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)?; + let r = Boolean::le_bits_to_fp_var(&r_bits)?; - // 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)) + NIFSGadget::::fold_committed_instance(r, U, u) } } #[cfg(test)] pub mod tests { use ark_pallas::{constraints::GVar, Fr, Projective}; - use ark_relations::r1cs::ConstraintSystem; - use ark_std::{One, UniformRand}; + use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem}; + use ark_std::One; use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; use super::*; @@ -505,7 +224,7 @@ pub mod tests { use crate::FoldingScheme; #[test] - fn test_decider_eth_circuit() { + fn test_decider_circuit() { let mut rng = ark_std::test_rng(); let poseidon_config = poseidon_canonical_config::(); @@ -549,84 +268,14 @@ pub mod tests { ) .unwrap(); - // load the DeciderEthCircuit from the Nova instance - let decider_eth_circuit = DeciderEthCircuit::< - Projective, - GVar, - Projective2, - GVar2, - Pedersen, - Pedersen, - >::from_nova(nova) - .unwrap(); + // load the DeciderEthCircuit from the generated Nova instance + let decider_circuit = + DeciderEthCircuit::::try_from(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); + decider_circuit.generate_constraints(cs.clone()).unwrap(); assert!(cs.is_satisfied().unwrap()); } } diff --git a/folding-schemes/src/folding/nova/nifs.rs b/folding-schemes/src/folding/nova/nifs.rs index 0a2d807e..3afb5239 100644 --- a/folding-schemes/src/folding/nova/nifs.rs +++ b/folding-schemes/src/folding/nova/nifs.rs @@ -1,6 +1,5 @@ use ark_crypto_primitives::sponge::Absorb; use ark_ec::{CurveGroup, Group}; -use ark_ff::PrimeField; use ark_std::rand::RngCore; use ark_std::Zero; use std::marker::PhantomData; @@ -27,7 +26,6 @@ impl, const H: bool> NIFSTrait where ::ScalarField: Absorb, - ::BaseField: PrimeField, { type CommittedInstance = CommittedInstance; type Witness = Witness; @@ -133,7 +131,6 @@ where impl, const H: bool> NIFS where ::ScalarField: Absorb, - ::BaseField: PrimeField, { /// compute_T: compute cross-terms T pub fn compute_T( diff --git a/folding-schemes/src/folding/protogalaxy/decider_eth_circuit.rs b/folding-schemes/src/folding/protogalaxy/decider_eth_circuit.rs new file mode 100644 index 00000000..7dd0f671 --- /dev/null +++ b/folding-schemes/src/folding/protogalaxy/decider_eth_circuit.rs @@ -0,0 +1,242 @@ +/// 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, PoseidonSponge}, + Absorb, CryptographicSponge, +}; +use ark_ec::CurveGroup; +use ark_ff::PrimeField; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + fields::fp::FpVar, + groups::CurveVar, + ToConstraintFieldGadget, +}; +use ark_relations::r1cs::{Namespace, SynthesisError}; +use ark_std::{borrow::Borrow, marker::PhantomData}; + +use crate::{ + arith::r1cs::{circuits::R1CSMatricesVar, R1CS}, + commitment::{pedersen::Params as PedersenParams, CommitmentScheme}, + folding::{ + circuits::{ + decider::{ + on_chain::GenericOnchainDeciderCircuit, DeciderEnabledNIFS, EvalGadget, + KZGChallengesGadget, + }, + CF1, CF2, + }, + traits::{WitnessOps, WitnessVarOps}, + }, + frontend::FCircuit, + Error, +}; + +use super::{ + circuits::FoldingGadget, + constants::{INCOMING, RUNNING}, + folding::{Folding, ProtoGalaxyProof}, + CommittedInstance, CommittedInstanceVar, ProtoGalaxy, Witness, +}; + +/// In-circuit representation of the Witness associated to the CommittedInstance. +#[derive(Debug, Clone)] +pub struct WitnessVar { + pub W: Vec>, + pub rW: 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::new_variable(cs.clone(), || Ok(val.borrow().w.to_vec()), mode)?; + let rW = FpVar::new_variable(cs.clone(), || Ok(val.borrow().r_w), mode)?; + + Ok(Self { W, rW }) + }) + } +} + +impl WitnessVarOps for WitnessVar { + fn get_openings(&self) -> Vec<(&[FpVar], FpVar)> { + vec![(&self.W, self.rW.clone())] + } +} + +pub type DeciderEthCircuit = GenericOnchainDeciderCircuit< + C1, + C2, + GC2, + CommittedInstance, + CommittedInstance, + Witness>, + R1CS>, + R1CSMatricesVar, FpVar>>, + DeciderProtoGalaxyGadget, +>; + +/// returns an instance of the DeciderEthCircuit from the given ProtoGalaxy struct +impl< + C1: CurveGroup, + GC1: CurveVar> + ToConstraintFieldGadget>, + C2: CurveGroup, + GC2: CurveVar> + ToConstraintFieldGadget>, + FC: FCircuit, + CS1: CommitmentScheme, + // enforce that the CS2 is Pedersen commitment scheme, since we're at Ethereum's EVM decider + CS2: CommitmentScheme>, + > TryFrom> for DeciderEthCircuit +where + CF1: Absorb, +{ + type Error = Error; + + fn try_from(protogalaxy: ProtoGalaxy) -> Result { + let mut transcript = PoseidonSponge::::new(&protogalaxy.poseidon_config); + + let (U_i1, W_i1, proof, _) = Folding::prove( + &mut transcript, + &protogalaxy.r1cs, + &protogalaxy.U_i, + &protogalaxy.W_i, + &[protogalaxy.u_i.clone()], + &[protogalaxy.w_i.clone()], + )?; + + // compute the KZG challenges used as inputs in the circuit + let kzg_challenges = KZGChallengesGadget::get_challenges_native(&mut transcript, &U_i1); + + // get KZG evals + let kzg_evaluations = W_i1 + .get_openings() + .iter() + .zip(&kzg_challenges) + .map(|((v, _), &c)| EvalGadget::evaluate_native(v, c)) + .collect::, _>>()?; + + Ok(Self { + _gc2: PhantomData, + _avar: PhantomData, + arith: protogalaxy.r1cs, + cf_arith: protogalaxy.cf_r1cs, + cf_pedersen_params: protogalaxy.cf_cs_params, + poseidon_config: protogalaxy.poseidon_config, + pp_hash: protogalaxy.pp_hash, + i: protogalaxy.i, + z_0: protogalaxy.z_0, + z_i: protogalaxy.z_i, + U_i: protogalaxy.U_i, + W_i: protogalaxy.W_i, + u_i: protogalaxy.u_i, + w_i: protogalaxy.w_i, + U_i1, + W_i1, + proof, + cf_U_i: protogalaxy.cf_U_i, + cf_W_i: protogalaxy.cf_W_i, + kzg_challenges, + kzg_evaluations, + }) + } +} + +pub struct DeciderProtoGalaxyGadget; + +impl + DeciderEnabledNIFS< + C, + CommittedInstance, + CommittedInstance, + Witness>, + R1CS>, + > for DeciderProtoGalaxyGadget +{ + type Proof = ProtoGalaxyProof>; + type ProofDummyCfg = (usize, usize, usize); + + fn fold_gadget( + _arith: &R1CS>, + transcript: &mut PoseidonSpongeVar>, + _pp_hash: FpVar>, + U: CommittedInstanceVar, + _U_vec: Vec>>, + u: CommittedInstanceVar, + proof: Self::Proof, + ) -> Result, SynthesisError> { + let cs = transcript.cs(); + let F_coeffs = Vec::new_witness(cs.clone(), || Ok(&proof.F_coeffs[..]))?; + let K_coeffs = Vec::new_witness(cs.clone(), || Ok(&proof.K_coeffs[..]))?; + + Ok(FoldingGadget::fold_committed_instance(transcript, &U, &[u], F_coeffs, K_coeffs)?.0) + } +} + +#[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::{ConstraintSynthesizer, ConstraintSystem}; + use ark_std::One; + + use super::*; + use crate::commitment::pedersen::Pedersen; + use crate::folding::protogalaxy::ProtoGalaxy; + use crate::frontend::{utils::CubicFCircuit, FCircuit}; + use crate::transcript::poseidon::poseidon_canonical_config; + use crate::FoldingScheme; + + #[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)]; + + type PG = ProtoGalaxy< + Projective, + GVar, + Projective2, + GVar2, + CubicFCircuit, + Pedersen, + Pedersen, + >; + let pg_params = PG::preprocess(&mut rng, &(poseidon_config, F_circuit)).unwrap(); + + // generate a Nova instance and do a step of it + let mut protogalaxy = PG::init(&pg_params, F_circuit, z_0.clone()).unwrap(); + protogalaxy.prove_step(&mut rng, vec![], None).unwrap(); + + let ivc_v = protogalaxy.clone(); + let (running_instance, incoming_instance, cyclefold_instance) = ivc_v.instances(); + PG::verify( + pg_params.1, // PG'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::::try_from(protogalaxy).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/protogalaxy/mod.rs b/folding-schemes/src/folding/protogalaxy/mod.rs index 28c1f0b9..0a7cd046 100644 --- a/folding-schemes/src/folding/protogalaxy/mod.rs +++ b/folding-schemes/src/folding/protogalaxy/mod.rs @@ -42,6 +42,7 @@ use crate::{ pub mod circuits; pub mod constants; +pub mod decider_eth_circuit; pub mod folding; pub mod traits; pub(crate) mod utils; diff --git a/folding-schemes/src/folding/traits.rs b/folding-schemes/src/folding/traits.rs index 1cae1865..49ba15a6 100644 --- a/folding-schemes/src/folding/traits.rs +++ b/folding-schemes/src/folding/traits.rs @@ -130,6 +130,12 @@ impl Dummy for Vec { } } +impl Dummy<()> for T { + fn dummy(_: ()) -> Self { + Default::default() + } +} + /// Converts a value `self` into a vector of field elements, ordered in the same /// way as how a variable of type `Var` would be represented in the circuit. /// This is useful for the verifier to compute the public inputs. diff --git a/solidity-verifiers/src/utils/mod.rs b/solidity-verifiers/src/utils/mod.rs index d04493f3..d44b0924 100644 --- a/solidity-verifiers/src/utils/mod.rs +++ b/solidity-verifiers/src/utils/mod.rs @@ -22,7 +22,7 @@ pub fn get_function_selector_for_nova_cyclefold_verifier( first_param_array_length: usize, ) -> [u8; 4] { let mut hasher = Sha3::keccak256(); - let fn_sig = format!("verifyNovaProof(uint256[{}],uint256[4],uint256[3],uint256[4],uint256[4],uint256[2],uint256[2][2],uint256[2],uint256[4],uint256[2][2])", first_param_array_length); + let fn_sig = format!("verifyNovaProof(uint256[{}],uint256[4],uint256[3],uint256[2],uint256[2][2],uint256[2],uint256[4],uint256[2][2])", first_param_array_length); hasher.input_str(&fn_sig); let hash = &mut [0u8; 32]; hasher.result(hash); diff --git a/solidity-verifiers/src/verifiers/nova_cyclefold.rs b/solidity-verifiers/src/verifiers/nova_cyclefold.rs index e82bd1f3..13117406 100644 --- a/solidity-verifiers/src/verifiers/nova_cyclefold.rs +++ b/solidity-verifiers/src/verifiers/nova_cyclefold.rs @@ -388,8 +388,8 @@ mod tests { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &(), + &(), &proof, ) .unwrap(); diff --git a/solidity-verifiers/templates/nova_cyclefold_decider.askama.sol b/solidity-verifiers/templates/nova_cyclefold_decider.askama.sol index a82893a1..b27f9473 100644 --- a/solidity-verifiers/templates/nova_cyclefold_decider.askama.sol +++ b/solidity-verifiers/templates/nova_cyclefold_decider.askama.sol @@ -62,10 +62,8 @@ contract NovaDecider is Groth16Verifier, KZG10Verifier { function verifyNovaProof( // inputs are grouped to prevent errors due stack too deep uint256[{{ 1 + z_len * 2 }}] calldata i_z0_zi, // [i, z0, zi] where |z0| == |zi| - uint256[4] calldata U_i_cmW_U_i_cmE, // [U_i_cmW[2], U_i_cmE[2]] - uint256[3] calldata U_i_u_u_i_u_r, // [U_i_u, u_i_u, r] - uint256[4] calldata U_i_x_u_i_cmW, // [U_i_x[2], u_i_cmW[2]] - uint256[4] calldata u_i_x_cmT, // [u_i_x[2], cmT[2]] + uint256[4] calldata U_final_cmW_U_final_cmE, // [U_final_cmW[2], U_final_cmE[2]] + uint256[3] calldata U_final_u_U_final_x, // [U_final_u, U_final_x[2]] uint256[2] calldata pA, // groth16 uint256[2][2] calldata pB, // groth16 uint256[2] calldata pC, // groth16 @@ -86,21 +84,13 @@ contract NovaDecider is Groth16Verifier, KZG10Verifier { } { - // U_i.u + r * u_i.u - uint256 u = rlc(U_i_u_u_i_u_r[0], U_i_u_u_i_u_r[2], U_i_u_u_i_u_r[1]); - // U_i.x + r * u_i.x - uint256 x0 = rlc(U_i_x_u_i_cmW[0], U_i_u_u_i_u_r[2], u_i_x_cmT[0]); - uint256 x1 = rlc(U_i_x_u_i_cmW[1], U_i_u_u_i_u_r[2], u_i_x_cmT[1]); - - public_inputs[{{ z_len * 2 + 2 }}] = u; - public_inputs[{{ z_len * 2 + 3 }}] = x0; - public_inputs[{{ z_len * 2 + 4 }}] = x1; + public_inputs[{{ z_len * 2 + 2 }}] = U_final_u_U_final_x[0]; + public_inputs[{{ z_len * 2 + 3 }}] = U_final_u_U_final_x[1]; + public_inputs[{{ z_len * 2 + 4 }}] = U_final_u_U_final_x[2]; } { - // U_i.cmE + r * u_i.cmT - uint256[2] memory mulScalarPoint = super.mulScalar([u_i_x_cmT[2], u_i_x_cmT[3]], U_i_u_u_i_u_r[2]); - uint256[2] memory cmE = super.add([U_i_cmW_U_i_cmE[2], U_i_cmW_U_i_cmE[3]], mulScalarPoint); + uint256[2] memory cmE = [U_final_cmW_U_final_cmE[2], U_final_cmW_U_final_cmE[3]]; { uint256[{{num_limbs}}] memory cmE_x_limbs = LimbsDecomposition.decompose(cmE[0]); @@ -116,9 +106,7 @@ contract NovaDecider is Groth16Verifier, KZG10Verifier { } { - // U_i.cmW + r * u_i.cmW - uint256[2] memory mulScalarPoint = super.mulScalar([U_i_x_u_i_cmW[2], U_i_x_u_i_cmW[3]], U_i_u_u_i_u_r[2]); - uint256[2] memory cmW = super.add([U_i_cmW_U_i_cmE[0], U_i_cmW_U_i_cmE[1]], mulScalarPoint); + uint256[2] memory cmW = [U_final_cmW_U_final_cmE[0], U_final_cmW_U_final_cmE[1]]; { uint256[{{num_limbs}}] memory cmW_x_limbs = LimbsDecomposition.decompose(cmW[0]); @@ -140,20 +128,6 @@ contract NovaDecider is Groth16Verifier, KZG10Verifier { public_inputs[{{ z_len * 2 + 5 + num_limbs * 4 + 2 }}] = challenge_W_challenge_E_kzg_evals[2]; public_inputs[{{ z_len * 2 + 5 + num_limbs * 4 + 3 }}] = challenge_W_challenge_E_kzg_evals[3]; - uint256[{{num_limbs}}] memory cmT_x_limbs; - uint256[{{num_limbs}}] memory cmT_y_limbs; - - cmT_x_limbs = LimbsDecomposition.decompose(u_i_x_cmT[2]); - cmT_y_limbs = LimbsDecomposition.decompose(u_i_x_cmT[3]); - - for (uint8 k = 0; k < {{num_limbs}}; k++) { - public_inputs[{{ z_len * 2 + 5 + num_limbs * 4 }} + 4 + k] = cmT_x_limbs[k]; - public_inputs[{{ z_len * 2 + 5 + num_limbs * 5}} + 4 + k] = cmT_y_limbs[k]; - } - - // last element of the groth16 proof's public inputs is `r` - public_inputs[{{ public_inputs_len - 2 }}] = U_i_u_u_i_u_r[2]; - bool success_g16 = this.verifyProof(pA, pB, pC, public_inputs); require(success_g16 == true, "Groth16: verifying proof failed"); }