diff --git a/examples/circom_full_flow.rs b/examples/circom_full_flow.rs index fa74a3b..22ab09b 100644 --- a/examples/circom_full_flow.rs +++ b/examples/circom_full_flow.rs @@ -19,9 +19,12 @@ use std::time::Instant; use folding_schemes::{ commitment::{kzg::KZG, pedersen::Pedersen}, - folding::nova::{ - decider_eth::{prepare_calldata, Decider as DeciderEth}, - Nova, PreprocessorParam, + folding::{ + nova::{ + decider_eth::{prepare_calldata, Decider as DeciderEth}, + Nova, PreprocessorParam, + }, + traits::CommittedInstanceOps, }, frontend::FCircuit, transcript::poseidon::poseidon_canonical_config, @@ -83,7 +86,6 @@ fn main() { // prepare the Nova prover & verifier params let nova_preprocess_params = PreprocessorParam::new(poseidon_config, f_circuit.clone()); let nova_params = N::preprocess(&mut rng, &nova_preprocess_params).unwrap(); - let pp_hash = nova_params.1.pp_hash().unwrap(); // initialize the folding scheme engine, in our case we use Nova let mut nova = N::init(&nova_params, f_circuit.clone(), z_0).unwrap(); @@ -117,8 +119,8 @@ fn main() { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &nova.U_i.get_commitments(), + &nova.u_i.get_commitments(), &proof, ) .unwrap(); @@ -131,7 +133,6 @@ fn main() { let calldata: Vec = prepare_calldata( function_selector, - pp_hash, nova.i, nova.z_0, nova.z_i, diff --git a/examples/full_flow.rs b/examples/full_flow.rs index 7b586ea..37291f3 100644 --- a/examples/full_flow.rs +++ b/examples/full_flow.rs @@ -21,9 +21,12 @@ use std::time::Instant; use folding_schemes::{ commitment::{kzg::KZG, pedersen::Pedersen}, - folding::nova::{ - decider_eth::{prepare_calldata, Decider as DeciderEth}, - Nova, PreprocessorParam, + folding::{ + nova::{ + decider_eth::{prepare_calldata, Decider as DeciderEth}, + Nova, PreprocessorParam, + }, + traits::CommittedInstanceOps, }, frontend::FCircuit, transcript::poseidon::poseidon_canonical_config, @@ -101,7 +104,6 @@ fn main() { // prepare the Nova prover & verifier params let nova_preprocess_params = PreprocessorParam::new(poseidon_config.clone(), f_circuit); let nova_params = N::preprocess(&mut rng, &nova_preprocess_params).unwrap(); - let pp_hash = nova_params.1.pp_hash().unwrap(); // initialize the folding scheme engine, in our case we use Nova let mut nova = N::init(&nova_params, f_circuit, z_0).unwrap(); @@ -125,8 +127,8 @@ fn main() { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &nova.U_i.get_commitments(), + &nova.u_i.get_commitments(), &proof, ) .unwrap(); @@ -139,7 +141,6 @@ fn main() { let calldata: Vec = prepare_calldata( function_selector, - pp_hash, nova.i, nova.z_0, nova.z_i, diff --git a/examples/noir_full_flow.rs b/examples/noir_full_flow.rs index 6e70873..454639e 100644 --- a/examples/noir_full_flow.rs +++ b/examples/noir_full_flow.rs @@ -16,9 +16,12 @@ use ark_grumpkin::{constraints::GVar as GVar2, Projective as G2}; use folding_schemes::{ commitment::{kzg::KZG, pedersen::Pedersen}, - folding::nova::{ - decider_eth::{prepare_calldata, Decider as DeciderEth}, - Nova, PreprocessorParam, + folding::{ + nova::{ + decider_eth::{prepare_calldata, Decider as DeciderEth}, + Nova, PreprocessorParam, + }, + traits::CommittedInstanceOps, }, frontend::FCircuit, transcript::poseidon::poseidon_canonical_config, @@ -46,7 +49,7 @@ fn main() { cur_path.to_str().unwrap() ); - let circuit = load_noir_circuit(circuit_path); + let circuit = load_noir_circuit(circuit_path).unwrap(); let f_circuit = NoirFCircuit { circuit, state_len: 1, @@ -72,7 +75,6 @@ fn main() { // prepare the Nova prover & verifier params let nova_preprocess_params = PreprocessorParam::new(poseidon_config, f_circuit.clone()); let nova_params = N::preprocess(&mut rng, &nova_preprocess_params).unwrap(); - let pp_hash = nova_params.1.pp_hash().unwrap(); // initialize the folding scheme engine, in our case we use Nova let mut nova = N::init(&nova_params, f_circuit.clone(), z_0).unwrap(); @@ -104,8 +106,8 @@ fn main() { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &nova.U_i.get_commitments(), + &nova.u_i.get_commitments(), &proof, ) .unwrap(); @@ -118,7 +120,6 @@ fn main() { let calldata: Vec = prepare_calldata( function_selector, - pp_hash, nova.i, nova.z_0, nova.z_i, diff --git a/examples/noname_full_flow.rs b/examples/noname_full_flow.rs index d64493a..f4e14d8 100644 --- a/examples/noname_full_flow.rs +++ b/examples/noname_full_flow.rs @@ -17,9 +17,12 @@ use ark_grumpkin::{constraints::GVar as GVar2, Projective as G2}; use folding_schemes::{ commitment::{kzg::KZG, pedersen::Pedersen}, - folding::nova::{ - decider_eth::{prepare_calldata, Decider as DeciderEth}, - Nova, PreprocessorParam, + folding::{ + nova::{ + decider_eth::{prepare_calldata, Decider as DeciderEth}, + Nova, PreprocessorParam, + }, + traits::CommittedInstanceOps, }, frontend::FCircuit, transcript::poseidon::poseidon_canonical_config, @@ -85,7 +88,6 @@ fn main() { // prepare the Nova prover & verifier params let nova_preprocess_params = PreprocessorParam::new(poseidon_config, f_circuit.clone()); let nova_params = N::preprocess(&mut rng, &nova_preprocess_params).unwrap(); - let pp_hash = nova_params.1.pp_hash().unwrap(); // initialize the folding scheme engine, in our case we use Nova let mut nova = N::init(&nova_params, f_circuit.clone(), z_0).unwrap(); @@ -119,8 +121,8 @@ fn main() { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &nova.U_i.get_commitments(), + &nova.u_i.get_commitments(), &proof, ) .unwrap(); @@ -133,7 +135,6 @@ fn main() { let calldata: Vec = prepare_calldata( function_selector, - pp_hash, nova.i, nova.z_0, nova.z_i, diff --git a/folding-schemes/src/arith/ccs/circuits.rs b/folding-schemes/src/arith/ccs/circuits.rs new file mode 100644 index 0000000..01d7f3d --- /dev/null +++ b/folding-schemes/src/arith/ccs/circuits.rs @@ -0,0 +1,35 @@ +use super::CCS; +use crate::utils::gadgets::SparseMatrixVar; +use ark_ff::PrimeField; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + fields::fp::FpVar, +}; +use ark_relations::r1cs::{Namespace, SynthesisError}; +use ark_std::borrow::Borrow; +/// CCSMatricesVar contains the matrices 'M' of the CCS without the rest of CCS parameters. +/// +#[derive(Debug, Clone)] +pub struct CCSMatricesVar { + // we only need native representation, so the constraint field==F + pub M: Vec>>, +} + +impl AllocVar, F> for CCSMatricesVar { + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + _mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + let M: Vec>> = val + .borrow() + .M + .iter() + .map(|M| SparseMatrixVar::>::new_constant(cs.clone(), M.clone())) + .collect::>()?; + Ok(Self { M }) + }) + } +} diff --git a/folding-schemes/src/arith/ccs.rs b/folding-schemes/src/arith/ccs/mod.rs similarity index 99% rename from folding-schemes/src/arith/ccs.rs rename to folding-schemes/src/arith/ccs/mod.rs index e1c9575..eba51c4 100644 --- a/folding-schemes/src/arith/ccs.rs +++ b/folding-schemes/src/arith/ccs/mod.rs @@ -9,6 +9,8 @@ use crate::Error; use super::ArithSerializer; use super::{r1cs::R1CS, Arith}; +pub mod circuits; + /// CCS represents the Customizable Constraint Systems structure defined in /// the [CCS paper](https://eprint.iacr.org/2023/552) #[derive(Debug, Clone, Eq, PartialEq)] diff --git a/folding-schemes/src/arith/r1cs/circuits.rs b/folding-schemes/src/arith/r1cs/circuits.rs new file mode 100644 index 0000000..192a538 --- /dev/null +++ b/folding-schemes/src/arith/r1cs/circuits.rs @@ -0,0 +1,301 @@ +use crate::{ + arith::ArithGadget, + utils::gadgets::{EquivalenceGadget, MatrixGadget, SparseMatrixVar, VectorGadget}, +}; +use ark_ff::PrimeField; +use ark_r1cs_std::alloc::{AllocVar, AllocationMode}; +use ark_relations::r1cs::{Namespace, SynthesisError}; +use ark_std::{borrow::Borrow, marker::PhantomData, One}; + +use super::R1CS; + +/// An in-circuit representation of the `R1CS` struct. +/// +/// `M` is for the modulo operation involved in the satisfiability check when +/// the underlying `FVar` is `NonNativeUintVar`. +#[derive(Debug, Clone)] +pub struct R1CSMatricesVar { + _m: PhantomData, + pub A: SparseMatrixVar, + pub B: SparseMatrixVar, + pub C: SparseMatrixVar, +} + +impl> + AllocVar, ConstraintF> for R1CSMatricesVar +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + _mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + + Ok(Self { + _m: PhantomData, + A: SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().A)?, + B: SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().B)?, + C: SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().C)?, + }) + }) + } +} + +impl R1CSMatricesVar +where + SparseMatrixVar: MatrixGadget, + [FVar]: VectorGadget, +{ + pub fn eval_at_z(&self, z: &[FVar]) -> Result<(Vec, Vec), SynthesisError> { + // Multiply Cz by z[0] (u) here, allowing this method to be reused for + // both relaxed and unrelaxed R1CS. + let Az = self.A.mul_vector(z)?; + let Bz = self.B.mul_vector(z)?; + let Cz = self.C.mul_vector(z)?; + let uCz = Cz.mul_scalar(&z[0])?; + let AzBz = Az.hadamard(&Bz)?; + Ok((AzBz, uCz)) + } +} + +impl, UVar: AsRef<[FVar]>> ArithGadget + for R1CSMatricesVar +where + SparseMatrixVar: MatrixGadget, + [FVar]: VectorGadget + EquivalenceGadget, + FVar: Clone + One, +{ + /// Evaluation is a tuple of two vectors (`AzBz` and `uCz`) instead of a + /// single vector `AzBz - uCz`, because subtraction is not supported for + /// `FVar = NonNativeUintVar`. + type Evaluation = (Vec, Vec); + + fn eval_relation(&self, w: &WVar, u: &UVar) -> Result { + self.eval_at_z(&[&[FVar::one()], u.as_ref(), w.as_ref()].concat()) + } + + fn enforce_evaluation( + _w: &WVar, + _u: &UVar, + (lhs, rhs): Self::Evaluation, + ) -> Result<(), SynthesisError> { + lhs.enforce_equivalent(&rhs) + } +} + +#[cfg(test)] +pub mod tests { + use std::cmp::max; + + use ark_crypto_primitives::crh::{ + sha256::{ + constraints::{Sha256Gadget, UnitVar}, + Sha256, + }, + CRHScheme, CRHSchemeGadget, + }; + use ark_ec::CurveGroup; + use ark_ff::BigInteger; + use ark_pallas::{Fq, Fr, Projective}; + use ark_r1cs_std::{bits::uint8::UInt8, eq::EqGadget, fields::fp::FpVar}; + use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef}; + use ark_std::{ + rand::{thread_rng, Rng}, + One, UniformRand, + }; + use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; + + use super::*; + use crate::arith::{ + r1cs::{ + extract_r1cs, extract_w_x, + tests::{get_test_r1cs, get_test_z}, + }, + Arith, + }; + use crate::commitment::{pedersen::Pedersen, CommitmentScheme}; + use crate::folding::{ + circuits::{ + cyclefold::{CycleFoldCommittedInstanceVar, CycleFoldWitnessVar}, + nonnative::uint::NonNativeUintVar, + }, + nova::{ + circuits::CommittedInstanceVar, decider_eth_circuit::WitnessVar, CommittedInstance, + Witness, + }, + }; + use crate::frontend::{ + utils::{CubicFCircuit, CustomFCircuit, WrapperCircuit}, + FCircuit, + }; + + fn prepare_instances, R: Rng>( + mut rng: R, + r1cs: &R1CS, + z: &[C::ScalarField], + ) -> (Witness, CommittedInstance) { + let (w, x) = r1cs.split_z(z); + + let (cs_pp, _) = CS::setup(&mut rng, max(w.len(), r1cs.A.n_rows)).unwrap(); + + let mut w = Witness::new::(w, r1cs.A.n_rows, &mut rng); + w.E = r1cs.eval_at_z(z).unwrap(); + let mut u = w.commit::(&cs_pp, x).unwrap(); + u.u = z[0]; + + (w, u) + } + + #[test] + fn test_relaxed_r1cs_small_gadget_handcrafted() { + let rng = &mut thread_rng(); + + let r1cs: R1CS = get_test_r1cs(); + let mut z = get_test_z(3); + z[0] = Fr::rand(rng); + let (w, u) = prepare_instances::<_, Pedersen, _>(rng, &r1cs, &z); + + let cs = ConstraintSystem::::new_ref(); + + let wVar = WitnessVar::new_witness(cs.clone(), || Ok(w)).unwrap(); + let uVar = CommittedInstanceVar::new_witness(cs.clone(), || Ok(u)).unwrap(); + let r1csVar = + R1CSMatricesVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); + + r1csVar.enforce_relation(&wVar, &uVar).unwrap(); + assert!(cs.is_satisfied().unwrap()); + } + + // gets as input a circuit that implements the ConstraintSynthesizer trait, and that has been + // initialized. + fn test_relaxed_r1cs_gadget>(circuit: CS) { + let rng = &mut thread_rng(); + + let cs = ConstraintSystem::::new_ref(); + + circuit.generate_constraints(cs.clone()).unwrap(); + cs.finalize(); + assert!(cs.is_satisfied().unwrap()); + + let cs = cs.into_inner().unwrap(); + + let r1cs = extract_r1cs::(&cs).unwrap(); + let (w, x) = extract_w_x::(&cs); + r1cs.check_relation(&w, &x).unwrap(); + let mut z = [vec![Fr::one()], x, w].concat(); + z[0] = Fr::rand(rng); + + let (w, u) = prepare_instances::<_, Pedersen, _>(rng, &r1cs, &z); + r1cs.check_relation(&w, &u).unwrap(); + + // set new CS for the circuit that checks the RelaxedR1CS of our original circuit + let cs = ConstraintSystem::::new_ref(); + // prepare the inputs for our circuit + let wVar = WitnessVar::new_witness(cs.clone(), || Ok(w)).unwrap(); + let uVar = CommittedInstanceVar::new_witness(cs.clone(), || Ok(u)).unwrap(); + let r1csVar = + R1CSMatricesVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); + + r1csVar.enforce_relation(&wVar, &uVar).unwrap(); + assert!(cs.is_satisfied().unwrap()); + } + + #[test] + fn test_relaxed_r1cs_small_gadget_arkworks() { + let z_i = vec![Fr::from(3_u32)]; + let cubic_circuit = CubicFCircuit::::new(()).unwrap(); + let circuit = WrapperCircuit::> { + FC: cubic_circuit, + z_i: Some(z_i.clone()), + z_i1: Some(cubic_circuit.step_native(0, z_i, vec![]).unwrap()), + }; + + test_relaxed_r1cs_gadget(circuit); + } + + struct Sha256TestCircuit { + _f: PhantomData, + pub x: Vec, + pub y: Vec, + } + impl ConstraintSynthesizer for Sha256TestCircuit { + fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { + let x = Vec::>::new_witness(cs.clone(), || Ok(self.x))?; + let y = Vec::>::new_input(cs.clone(), || Ok(self.y))?; + + let unitVar = UnitVar::default(); + let comp_y = as CRHSchemeGadget>::evaluate(&unitVar, &x)?; + comp_y.0.enforce_equal(&y)?; + Ok(()) + } + } + #[test] + fn test_relaxed_r1cs_medium_gadget_arkworks() { + let x = Fr::from(5_u32).into_bigint().to_bytes_le(); + let y = ::evaluate(&(), x.clone()).unwrap(); + + let circuit = Sha256TestCircuit:: { + _f: PhantomData, + x, + y, + }; + test_relaxed_r1cs_gadget(circuit); + } + + #[test] + fn test_relaxed_r1cs_custom_circuit() { + let n_constraints = 10_000; + let custom_circuit = CustomFCircuit::::new(n_constraints).unwrap(); + let z_i = vec![Fr::from(5_u32)]; + let circuit = WrapperCircuit::> { + FC: custom_circuit, + z_i: Some(z_i.clone()), + z_i1: Some(custom_circuit.step_native(0, z_i, vec![]).unwrap()), + }; + test_relaxed_r1cs_gadget(circuit); + } + + #[test] + fn test_relaxed_r1cs_nonnative_circuit() { + let rng = &mut thread_rng(); + + let cs = ConstraintSystem::::new_ref(); + // in practice we would use CycleFoldCircuit, but is a very big circuit (when computed + // non-natively inside the RelaxedR1CS circuit), so in order to have a short test we use a + // custom circuit. + let custom_circuit = CustomFCircuit::::new(10).unwrap(); + let z_i = vec![Fq::from(5_u32)]; + let circuit = WrapperCircuit::> { + FC: custom_circuit, + z_i: Some(z_i.clone()), + z_i1: Some(custom_circuit.step_native(0, z_i, vec![]).unwrap()), + }; + circuit.generate_constraints(cs.clone()).unwrap(); + cs.finalize(); + let cs = cs.into_inner().unwrap(); + let r1cs = extract_r1cs::(&cs).unwrap(); + let (w, x) = extract_w_x::(&cs); + let z = [vec![Fq::rand(rng)], x, w].concat(); + + let (w, u) = prepare_instances::<_, Pedersen, _>(rng, &r1cs, &z); + + // natively + let cs = ConstraintSystem::::new_ref(); + let wVar = WitnessVar::new_witness(cs.clone(), || Ok(&w)).unwrap(); + let uVar = CommittedInstanceVar::new_witness(cs.clone(), || Ok(&u)).unwrap(); + let r1csVar = + R1CSMatricesVar::>::new_witness(cs.clone(), || Ok(&r1cs)).unwrap(); + r1csVar.enforce_relation(&wVar, &uVar).unwrap(); + + // non-natively + let cs = ConstraintSystem::::new_ref(); + let wVar = CycleFoldWitnessVar::new_witness(cs.clone(), || Ok(w)).unwrap(); + let uVar = + CycleFoldCommittedInstanceVar::<_, GVar2>::new_witness(cs.clone(), || Ok(u)).unwrap(); + let r1csVar = + R1CSMatricesVar::>::new_witness(cs.clone(), || Ok(r1cs)) + .unwrap(); + r1csVar.enforce_relation(&wVar, &uVar).unwrap(); + } +} diff --git a/folding-schemes/src/arith/r1cs.rs b/folding-schemes/src/arith/r1cs/mod.rs similarity index 95% rename from folding-schemes/src/arith/r1cs.rs rename to folding-schemes/src/arith/r1cs/mod.rs index 164528c..362567d 100644 --- a/folding-schemes/src/arith/r1cs.rs +++ b/folding-schemes/src/arith/r1cs/mod.rs @@ -10,6 +10,8 @@ use crate::utils::vec::{ }; use crate::Error; +pub mod circuits; + #[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] pub struct R1CS { pub l: usize, // io len @@ -121,8 +123,14 @@ impl From> for R1CS { /// extracts arkworks ConstraintSystem matrices into crate::utils::vec::SparseMatrix format as R1CS /// struct. -pub fn extract_r1cs(cs: &ConstraintSystem) -> R1CS { - let m = cs.to_matrices().unwrap(); +pub fn extract_r1cs(cs: &ConstraintSystem) -> Result, Error> { + let m = cs.to_matrices().ok_or_else(|| { + Error::ConversionError( + "ConstraintSystem".into(), + "ConstraintMatrices".into(), + "The matrices have not been generated yet".into(), + ) + })?; let n_rows = cs.num_constraints; let n_cols = cs.num_instance_variables + cs.num_witness_variables; // cs.num_instance_variables already counts the 1 @@ -143,12 +151,12 @@ pub fn extract_r1cs(cs: &ConstraintSystem) -> R1CS { coeffs: m.c, }; - R1CS:: { + Ok(R1CS:: { l: cs.num_instance_variables - 1, // -1 to subtract the first '1' A, B, C, - } + }) } /// extracts the witness and the public inputs from arkworks ConstraintSystem. diff --git a/folding-schemes/src/commitment/ipa.rs b/folding-schemes/src/commitment/ipa.rs index a951e7f..8a6a989 100644 --- a/folding-schemes/src/commitment/ipa.rs +++ b/folding-schemes/src/commitment/ipa.rs @@ -13,7 +13,6 @@ use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, boolean::Boolean, fields::{nonnative::NonNativeFieldVar, FieldVar}, - groups::GroupOpsBounds, prelude::CurveVar, ToBitsGadget, }; @@ -492,9 +491,6 @@ impl IPAGadget where C: CurveGroup, GC: CurveVar>, - - ::BaseField: ark_ff::PrimeField, - for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { /// Verify the IPA opening proof, K=log2(d), where d is the degree of the committed polynomial, /// and H indicates if the commitment is in hiding mode and thus uses blinding factors, if not, @@ -515,7 +511,7 @@ where return Err(SynthesisError::Unsatisfiable); } - let P_ = P + U.scalar_mul_le(v.to_bits_le()?.iter())?; + let P_ = U.scalar_mul_le(v.to_bits_le()?.iter())? + P; let mut q_0 = P_; let mut r = r.clone(); diff --git a/folding-schemes/src/commitment/pedersen.rs b/folding-schemes/src/commitment/pedersen.rs index 0a5f42f..e1d97a2 100644 --- a/folding-schemes/src/commitment/pedersen.rs +++ b/folding-schemes/src/commitment/pedersen.rs @@ -1,6 +1,6 @@ use ark_ec::CurveGroup; use ark_ff::Field; -use ark_r1cs_std::{boolean::Boolean, groups::GroupOpsBounds, prelude::CurveVar}; +use ark_r1cs_std::{boolean::Boolean, prelude::CurveVar}; use ark_relations::r1cs::SynthesisError; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::Zero; @@ -194,15 +194,12 @@ impl PedersenGadget where C: CurveGroup, GC: CurveVar>, - - ::BaseField: ark_ff::PrimeField, - for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { pub fn commit( - h: GC, - g: Vec, - v: Vec>>>, - r: Vec>>, + h: &GC, + g: &[GC], + v: &[Vec>>], + r: &[Boolean>], ) -> Result { let mut res = GC::zero(); if H { @@ -303,7 +300,7 @@ mod tests { // use the gadget let cmVar = - PedersenGadget::::commit(hVar, gVar, vVar, rVar).unwrap(); + PedersenGadget::::commit(&hVar, &gVar, &vVar, &rVar).unwrap(); cmVar.enforce_equal(&expected_cmVar).unwrap(); } } diff --git a/folding-schemes/src/folding/circuits/cyclefold.rs b/folding-schemes/src/folding/circuits/cyclefold.rs index a663287..ca71efc 100644 --- a/folding-schemes/src/folding/circuits/cyclefold.rs +++ b/folding-schemes/src/folding/circuits/cyclefold.rs @@ -8,7 +8,6 @@ use ark_r1cs_std::{ boolean::Boolean, eq::EqGadget, fields::fp::FpVar, - groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget, }; @@ -17,16 +16,23 @@ use ark_relations::r1cs::{ }; use ark_std::fmt::Debug; use ark_std::rand::RngCore; -use ark_std::Zero; +use ark_std::{One, Zero}; use core::{borrow::Borrow, marker::PhantomData}; use super::{nonnative::uint::NonNativeUintVar, CF1, CF2}; -use crate::arith::r1cs::{extract_w_x, R1CS}; use crate::commitment::CommitmentScheme; use crate::constants::NOVA_N_BITS_RO; use crate::folding::nova::nifs::{nova::NIFS, NIFSTrait}; use crate::transcript::{AbsorbNonNative, AbsorbNonNativeGadget, Transcript, TranscriptVar}; +use crate::utils::gadgets::{EquivalenceGadget, VectorGadget}; use crate::Error; +use crate::{ + arith::{ + r1cs::{circuits::R1CSMatricesVar, extract_w_x, R1CS}, + ArithGadget, + }, + folding::traits::Inputize, +}; use ark_crypto_primitives::sponge::poseidon::PoseidonSponge; /// Re-export the Nova committed instance as `CycleFoldCommittedInstance` and @@ -35,13 +41,39 @@ pub use crate::folding::nova::{ CommittedInstance as CycleFoldCommittedInstance, Witness as CycleFoldWitness, }; +impl>> Inputize, CycleFoldCommittedInstanceVar> + for CycleFoldCommittedInstance +{ + fn inputize(&self) -> Vec> { + let zero = (&C::BaseField::zero(), &C::BaseField::zero()); + let cmE = self.cmE.into_affine(); + let cmW = self.cmW.into_affine(); + let (cmE_x, cmE_y) = cmE.xy().unwrap_or(zero); + let (cmW_x, cmW_y) = cmW.xy().unwrap_or(zero); + self.u + .inputize() + .into_iter() + .chain(self.x.iter().flat_map(|x| x.inputize())) + .chain( + [ + *cmE_x, + *cmE_y, + C::BaseField::one(), + *cmW_x, + *cmW_y, + C::BaseField::one(), + ] + .into_iter() + .flat_map(|x| x.to_base_prime_field_elements()), + ) + .collect() + } +} + /// CycleFoldCommittedInstanceVar is the CycleFold CommittedInstance represented /// in folding verifier circuit #[derive(Debug, Clone)] -pub struct CycleFoldCommittedInstanceVar>> -where - for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, -{ +pub struct CycleFoldCommittedInstanceVar>> { pub cmE: GC, pub u: NonNativeUintVar>, pub cmW: GC, @@ -51,8 +83,6 @@ impl AllocVar, CF2> for CycleFoldCommitt where C: CurveGroup, GC: CurveVar>, - ::BaseField: ark_ff::PrimeField, - for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { fn new_variable>>( cs: impl Into>>, @@ -101,8 +131,7 @@ impl AbsorbNonNativeGadget for CycleFoldCommittedInstanceVa where C: CurveGroup, GC: CurveVar> + ToConstraintFieldGadget>, - ::BaseField: ark_ff::PrimeField + Absorb, - for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, + C::BaseField: PrimeField + Absorb, { /// Extracts the underlying field elements from `CycleFoldCommittedInstanceVar`, in the order /// of `u`, `x`, `cmE.x`, `cmE.y`, `cmW.x`, `cmW.y`, `cmE.is_inf || cmW.is_inf` (|| is for @@ -132,7 +161,7 @@ where impl CycleFoldCommittedInstance where - ::BaseField: ark_ff::PrimeField + Absorb, + C::BaseField: PrimeField + Absorb, { /// hash_cyclefold implements the committed instance hash compatible with the /// in-circuit implementation `CycleFoldCommittedInstanceVar::hash`. @@ -153,8 +182,7 @@ impl CycleFoldCommittedInstanceVar where C: CurveGroup, GC: CurveVar> + ToConstraintFieldGadget>, - ::BaseField: ark_ff::PrimeField + Absorb, - for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, + C::BaseField: PrimeField + Absorb, { /// hash implements the committed instance hash compatible with the native /// implementation `CycleFoldCommittedInstance::hash_cyclefold`. @@ -165,7 +193,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> { @@ -173,7 +201,11 @@ where let U_vec = self.to_native_sponge_field_elements()?; sponge.absorb(&pp_hash)?; sponge.absorb(&U_vec)?; - Ok((sponge.squeeze_field_elements(1)?.pop().unwrap(), U_vec)) + Ok(( + // `unwrap` is safe because the sponge is guaranteed to return a single element + sponge.squeeze_field_elements(1)?.pop().unwrap(), + U_vec, + )) } } @@ -182,10 +214,7 @@ where /// represented as native points, which are folded on the auxiliary curve constraints field (E2::Fr /// = E1::Fq). #[derive(Debug, Clone)] -pub struct CommittedInstanceInCycleFoldVar>> -where - for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, -{ +pub struct CommittedInstanceInCycleFoldVar>> { _c: PhantomData, pub cmE: GC, pub cmW: GC, @@ -196,7 +225,6 @@ impl AllocVar, CF2> where C: CurveGroup, GC: CurveVar>, - for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, { fn new_variable>>( cs: impl Into>>, @@ -232,7 +260,7 @@ pub struct CycleFoldWitnessVar { impl AllocVar, CF2> for CycleFoldWitnessVar where C: CurveGroup, - ::BaseField: PrimeField, + C::BaseField: PrimeField, { fn new_variable>>( cs: impl Into>>, @@ -265,8 +293,7 @@ impl>> NIFSFullGadget where C: CurveGroup, GC: CurveVar>, - ::BaseField: ark_ff::PrimeField, - for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, + C::BaseField: PrimeField, { pub fn fold_committed_instance( r_bits: Vec>>, @@ -284,13 +311,13 @@ where Ok(CycleFoldCommittedInstanceVar { cmE: cmT.scalar_mul_le(r_bits.iter())? + ci1.cmE, cmW: ci1.cmW + ci2.cmW.scalar_mul_le(r_bits.iter())?, - u: ci1.u.add_no_align(&r_nonnat).modulo::>()?, + u: ci1.u.add_no_align(&r_nonnat)?.modulo::>()?, x: ci1 .x .iter() .zip(ci2.x) .map(|(a, b)| { - a.add_no_align(&r_nonnat.mul_no_align(&b)?) + a.add_no_align(&r_nonnat.mul_no_align(&b)?)? .modulo::>() }) .collect::, _>>()?, @@ -319,6 +346,29 @@ where } } +impl>> + ArithGadget, CycleFoldCommittedInstanceVar> + for R1CSMatricesVar, NonNativeUintVar>> +{ + type Evaluation = (Vec>>, Vec>>); + + fn eval_relation( + &self, + w: &CycleFoldWitnessVar, + u: &CycleFoldCommittedInstanceVar, + ) -> Result { + self.eval_at_z(&[&[u.u.clone()][..], &u.x, &w.W].concat()) + } + + fn enforce_evaluation( + w: &CycleFoldWitnessVar, + _u: &CycleFoldCommittedInstanceVar, + (AzBz, uCz): Self::Evaluation, + ) -> Result<(), SynthesisError> { + EquivalenceGadget::>::enforce_equivalent(&AzBz[..], &uCz.add(&w.E)?[..]) + } +} + /// CycleFoldChallengeGadget computes the RO challenge used for the CycleFold instances NIFS, it contains a /// rust-native and a in-circuit compatible versions. pub struct CycleFoldChallengeGadget>> { @@ -329,9 +379,7 @@ impl CycleFoldChallengeGadget where C: CurveGroup, GC: CurveVar> + ToConstraintFieldGadget>, - ::BaseField: PrimeField, - ::BaseField: Absorb, - for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, + C::BaseField: PrimeField + Absorb, { pub fn get_challenge_native>( transcript: &mut T, @@ -425,7 +473,6 @@ impl> ConstraintSynthesizer, CFG::F: PrimeField, - for<'a> &'a GC: GroupOpsBounds<'a, CFG::C, GC>, { fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { let r_bits = Vec::>::new_witness(cs.clone(), || { @@ -510,7 +557,6 @@ pub struct CycleFoldNIFS< > where ::BaseField: PrimeField, ::BaseField: PrimeField, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { _c1: PhantomData, _c2: PhantomData, @@ -526,7 +572,6 @@ where ::ScalarField: Absorb, C1: CurveGroup, GC2: CurveVar> + ToConstraintFieldGadget>, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { fn prove( cf_r_Fq: C2::ScalarField, // C2::Fr==C1::Fq @@ -597,8 +642,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { let cs2 = ConstraintSystem::::new_ref(); cf_circuit.generate_constraints(cs2.clone())?; 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 0000000..ec63f90 --- /dev/null +++ b/folding-schemes/src/folding/circuits/decider/mod.rs @@ -0,0 +1,211 @@ +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).ok_or(SynthesisError::PolynomialDegreeTooLarge)?; + // `unwrap` below is safe because `Radix2DomainVar::new` only fails if + // `offset.enforce_not_equal(&FpVar::zero())` returns an error. + // But in our case, `offset` is `FpVar::one()`, i.e., both operands of + // `enforce_not_equal` are constants. + // Consequently, `FpVar`'s implementation of `enforce_not_equal` will + // always return `Ok(())`. + 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 6 (running NIFS.V for group elements +/// in circuit) in an NIFS-agnostic way, because different folding schemes have +/// different interfaces of folding verification now. +/// +/// In the future, we may introduce a better solution that uses 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; + type RandomnessDummyCfg; + type Randomness: Dummy; + + /// Fold the field elements in `U` and `u` inside the circuit. + /// + /// `U_vec` is `U` expressed as a vector of `FpVar`s, which can be reused + /// before or after calling this function to save constraints. + #[allow(clippy::too_many_arguments)] + fn fold_field_elements_gadget( + arith: &A, + transcript: &mut PoseidonSpongeVar>, + pp_hash: FpVar>, + U: RU::Var, + U_vec: Vec>>, + u: IU::Var, + proof: Self::Proof, + randomness: Self::Randomness, + ) -> Result; + + /// Fold the group elements (i.e., commitments) in `U` and `u` outside the + /// circuit. + fn fold_group_elements_native( + U_commitments: &[C], + u_commitments: &[C], + proof: Option, + randomness: Self::Randomness, + ) -> Result, Error>; +} + +#[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 0000000..3bb0905 --- /dev/null +++ b/folding-schemes/src/folding/circuits/decider/off_chain.rs @@ -0,0 +1,316 @@ +/// 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}, + nonnative::affine::NonNativeAffineVar, + 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, + pub randomness: D::Randomness, + + /// 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, + D::RandomnessDummyCfg, + usize, + usize, + )> for GenericOffchainDeciderCircuit1 +{ + fn dummy( + ( + arith, + cf_arith, + poseidon_config, + proof_config, + randomness_config, + state_len, + num_commitments, + ): ( + A, + &R1CS>, + PoseidonConfig>, + D::ProofDummyCfg, + D::RandomnessDummyCfg, + 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), + randomness: D::Randomness::dummy(randomness_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> + CommittedInstanceVarOps>, + 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_commitments = Vec::>::new_input(cs.clone(), || { + Ok(self.U_i1.get_commitments()) + })?; + let U_i1 = RU::Var::new_witness(cs.clone(), || Ok(self.U_i1))?; + let W_i1 = W::Var::new_witness(cs.clone(), || Ok(self.W_i1))?; + U_i1.get_commitments().enforce_equal(&U_i1_commitments)?; + + let cf_U_i = + CycleFoldCommittedInstanceVar::::new_input(cs.clone(), || Ok(self.cf_U_i))?; + + // allocate the inputs for the checks 7.1 and 7.2 + 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])?; + + // 6.1. partially enforce `NIFS.V(U_i, u_i) = U_{i+1}`. + D::fold_field_elements_gadget( + &self.arith, + &mut transcript, + pp_hash, + U_i, + U_i_vec, + u_i, + self.proof, + self.randomness, + )? + .enforce_partial_equal(&U_i1)?; + + // 7.1. compute and check KZG challenges + KZGChallengesGadget::get_challenges_gadget(&mut transcript, &U_i1)? + .enforce_equal(&kzg_challenges)?; + + // 7.2. 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))?; + + // allocate the inputs for the checks 4.1 and 4.2 + 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)?; + + // 5. enforce `cf_U_i` and `cf_W_i` satisfy `cf_r1cs` + cf_r1cs.enforce_relation(&cf_W_i, &cf_U_i)?; + + // 4.1. compute and check KZG challenges + KZGChallengesGadget::get_challenges_gadget(&mut transcript, &cf_U_i)? + .enforce_equal(&kzg_challenges)?; + + // 4.2. 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 0000000..98a6200 --- /dev/null +++ b/folding-schemes/src/folding/circuits/decider/on_chain.rs @@ -0,0 +1,325 @@ +/// 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}, + nonnative::affine::NonNativeAffineVar, + 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, + pub randomness: D::Randomness, + + /// 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, + D::RandomnessDummyCfg, + usize, + usize, + )> for GenericOnchainDeciderCircuit +{ + fn dummy( + ( + arith, + cf_arith, + cf_pedersen_params, + poseidon_config, + proof_config, + randomness_config, + state_len, + num_commitments, + ): ( + A, + R1CS>, + PedersenParams, + PoseidonConfig>, + D::ProofDummyCfg, + D::RandomnessDummyCfg, + 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), + randomness: D::Randomness::dummy(randomness_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> + CommittedInstanceVarOps>, + 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_commitments = Vec::>::new_input(cs.clone(), || { + Ok(self.U_i1.get_commitments()) + })?; + let U_i1 = RU::Var::new_witness(cs.clone(), || Ok(self.U_i1))?; + let W_i1 = W::Var::new_witness(cs.clone(), || Ok(self.W_i1))?; + U_i1.get_commitments().enforce_equal(&U_i1_commitments)?; + + let cf_U_i = + CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || Ok(self.cf_U_i))?; + + // allocate the inputs for the check 7.1 and 7.2 + 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_le()) + .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.1. partially enforce `NIFS.V(U_i, u_i) = U_{i+1}`. + D::fold_field_elements_gadget( + &self.arith, + &mut transcript, + pp_hash, + U_i, + U_i_vec, + u_i, + self.proof, + self.randomness, + )? + .enforce_partial_equal(&U_i1)?; + + // 7.1. compute and check KZG challenges + KZGChallengesGadget::get_challenges_gadget(&mut transcript, &U_i1)? + .enforce_equal(&kzg_challenges)?; + + // 7.2. 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 b442b93..48d5cf1 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/circuits/nonnative/affine.rs b/folding-schemes/src/folding/circuits/nonnative/affine.rs index da4d9e4..906976e 100644 --- a/folding-schemes/src/folding/circuits/nonnative/affine.rs +++ b/folding-schemes/src/folding/circuits/nonnative/affine.rs @@ -2,7 +2,9 @@ use ark_ec::{short_weierstrass::SWFlags, AffineRepr, CurveGroup}; use ark_ff::{Field, PrimeField}; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, + eq::EqGadget, fields::fp::FpVar, + prelude::Boolean, R1CSVar, ToConstraintFieldGadget, }; use ark_relations::r1cs::{ConstraintSystemRef, Namespace, SynthesisError}; @@ -10,7 +12,10 @@ use ark_serialize::{CanonicalSerialize, CanonicalSerializeWithFlags}; use ark_std::Zero; use core::borrow::Borrow; -use crate::transcript::{AbsorbNonNative, AbsorbNonNativeGadget}; +use crate::{ + folding::traits::Inputize, + transcript::{AbsorbNonNative, AbsorbNonNativeGadget}, +}; use super::uint::{nonnative_field_to_field_elements, NonNativeUintVar}; @@ -63,8 +68,15 @@ impl R1CSVar for NonNativeAffineVar { let y = ::BasePrimeField::from_le_bytes_mod_order( &self.y.value()?.to_bytes_le(), ); + // Below is a workaround to convert the `x` and `y` coordinates to a + // point. This is because the `CurveGroup` trait does not provide a + // method to construct a point from `BaseField` elements. let mut bytes = vec![]; + // `unwrap` below is safe because serialization of a `PrimeField` value + // only fails if the serialization flag has more than 8 bits, but here + // we call `serialize_uncompressed` which uses an empty flag. x.serialize_uncompressed(&mut bytes).unwrap(); + // `unwrap` below is also safe, because the bit size of `SWFlags` is 2. y.serialize_with_flags( &mut bytes, if x.is_zero() && y.is_zero() { @@ -76,6 +88,9 @@ impl R1CSVar for NonNativeAffineVar { }, ) .unwrap(); + // `unwrap` below is safe because `bytes` is constructed from the `x` + // and `y` coordinates of a valid point, and these coordinates are + // serialized in the same way as the `CurveGroup` implementation. Ok(C::deserialize_uncompressed_unchecked(&bytes[..]).unwrap()) } } @@ -90,6 +105,53 @@ impl ToConstraintFieldGadget for NonNativeAffineV } } +impl EqGadget for NonNativeAffineVar { + fn is_eq(&self, other: &Self) -> Result, SynthesisError> { + let mut result = Boolean::TRUE; + if self.x.0.len() != other.x.0.len() { + return Err(SynthesisError::Unsatisfiable); + } + if self.y.0.len() != other.y.0.len() { + return Err(SynthesisError::Unsatisfiable); + } + for (l, r) in self + .x + .0 + .iter() + .chain(&self.y.0) + .zip(other.x.0.iter().chain(&other.y.0)) + { + if l.ub != r.ub { + return Err(SynthesisError::Unsatisfiable); + } + result = result.and(&l.v.is_eq(&r.v)?)?; + } + Ok(result) + } + + fn enforce_equal(&self, other: &Self) -> Result<(), SynthesisError> { + if self.x.0.len() != other.x.0.len() { + return Err(SynthesisError::Unsatisfiable); + } + if self.y.0.len() != other.y.0.len() { + return Err(SynthesisError::Unsatisfiable); + } + for (l, r) in self + .x + .0 + .iter() + .chain(&self.y.0) + .zip(other.x.0.iter().chain(&other.y.0)) + { + if l.ub != r.ub { + return Err(SynthesisError::Unsatisfiable); + } + l.v.enforce_equal(&r.v)?; + } + Ok(()) + } +} + /// The out-circuit counterpart of `NonNativeAffineVar::to_constraint_field` #[allow(clippy::type_complexity)] pub(crate) fn nonnative_affine_to_field_elements( @@ -104,22 +166,22 @@ pub(crate) fn nonnative_affine_to_field_elements( (x, y) } -impl NonNativeAffineVar { - // Extracts a list of field elements of type `C::ScalarField` from the public input - // `p`, in exactly the same way as how `NonNativeAffineVar` is represented as limbs of type - // `FpVar` in-circuit. - #[allow(clippy::type_complexity)] - pub fn inputize(p: C) -> Result<(Vec, Vec), SynthesisError> { - let affine = p.into_affine(); +impl Inputize> for C { + fn inputize(&self) -> Vec { + let affine = self.into_affine(); let zero = (&C::BaseField::zero(), &C::BaseField::zero()); let (x, y) = affine.xy().unwrap_or(zero); - let x = NonNativeUintVar::inputize(*x); - let y = NonNativeUintVar::inputize(*y); - Ok((x, y)) + let x = x.inputize(); + let y = y.inputize(); + [x, y].concat() } +} +impl NonNativeAffineVar { pub fn zero() -> Self { + // `unwrap` below is safe because we are allocating a constant value, + // which is guaranteed to succeed. Self::new_constant(ConstraintSystemRef::None, C::zero()).unwrap() } } @@ -179,9 +241,11 @@ mod tests { let mut rng = ark_std::test_rng(); let p = Projective::rand(&mut rng); let pVar = NonNativeAffineVar::::new_witness(cs.clone(), || Ok(p)).unwrap(); - let (x, y) = NonNativeAffineVar::inputize(p).unwrap(); + let xy = p.inputize(); - assert_eq!(pVar.x.0.value().unwrap(), x); - assert_eq!(pVar.y.0.value().unwrap(), y); + assert_eq!( + [pVar.x.0.value().unwrap(), pVar.y.0.value().unwrap()].concat(), + xy + ); } } diff --git a/folding-schemes/src/folding/circuits/nonnative/uint.rs b/folding-schemes/src/folding/circuits/nonnative/uint.rs index 2b32177..3f16763 100644 --- a/folding-schemes/src/folding/circuits/nonnative/uint.rs +++ b/folding-schemes/src/folding/circuits/nonnative/uint.rs @@ -17,8 +17,9 @@ use num_bigint::BigUint; use num_integer::Integer; use crate::{ + folding::traits::Inputize, transcript::{AbsorbNonNative, AbsorbNonNativeGadget}, - utils::gadgets::{MatrixGadget, SparseMatrixVar, VectorGadget}, + utils::gadgets::{EquivalenceGadget, MatrixGadget, SparseMatrixVar, VectorGadget}, }; /// `LimbVar` represents a single limb of a non-native unsigned integer in the @@ -36,6 +37,11 @@ pub struct LimbVar { impl]>> From for LimbVar { fn from(bits: B) -> Self { Self { + // `Boolean::le_bits_to_fp_var` will return an error if the internal + // invocation of `Boolean::enforce_in_field_le` fails. + // However, this method is only called when the length of `bits` is + // greater than `F::MODULUS_BIT_SIZE`, which should not happen in + // our case where `bits` is guaranteed to be short. v: Boolean::le_bits_to_fp_var(bits.as_ref()).unwrap(), ub: (BigUint::one() << bits.as_ref().len()) - BigUint::one(), } @@ -219,7 +225,7 @@ impl AllocVar for NonNativeUintVar { .collect::>() .chunks(Self::bits_per_limb()) { - let limb = F::from_bigint(F::BigInt::from_bits_le(chunk)).unwrap(); + let limb = F::from(F::BigInt::from_bits_le(chunk)); let limb = FpVar::new_variable(cs.clone(), || Ok(limb), mode)?; Self::enforce_bit_length(&limb, chunk.len())?; limbs.push(LimbVar { @@ -241,12 +247,15 @@ impl AllocVar for NonNativeUintVar { let cs = cs.into().cs(); let v = f()?; assert_eq!(G::extension_degree(), 1); + // `unwrap` is safe because `G` is a field with extension degree 1, and + // thus `G::to_base_prime_field_elements` should return an iterator with + // exactly one element. let v = v.borrow().to_base_prime_field_elements().next().unwrap(); let mut limbs = vec![]; for chunk in v.into_bigint().to_bits_le().chunks(Self::bits_per_limb()) { - let limb = F::from_bigint(F::BigInt::from_bits_le(chunk)).unwrap(); + let limb = F::from(F::BigInt::from_bits_le(chunk)); let limb = FpVar::new_variable(cs.clone(), || Ok(limb), mode)?; Self::enforce_bit_length(&limb, chunk.len())?; limbs.push(LimbVar { @@ -259,16 +268,19 @@ impl AllocVar for NonNativeUintVar { } } -impl NonNativeUintVar { - pub fn inputize(x: T) -> Vec { +impl Inputize> for T { + fn inputize(&self) -> Vec { assert_eq!(T::extension_degree(), 1); - x.to_base_prime_field_elements() + // `unwrap` is safe because `T` is a field with extension degree 1, and + // thus `T::to_base_prime_field_elements` should return an iterator with + // exactly one element. + self.to_base_prime_field_elements() .next() .unwrap() .into_bigint() .to_bits_le() - .chunks(Self::bits_per_limb()) - .map(|chunk| F::from_bigint(F::BigInt::from_bits_le(chunk)).unwrap()) + .chunks(NonNativeUintVar::::bits_per_limb()) + .map(|chunk| F::from(F::BigInt::from_bits_le(chunk))) .collect() } } @@ -450,7 +462,7 @@ impl NonNativeUintVar { // (i.e., all of them are "non-negative"), implying that all // limbs should be zero to make the sum zero. LimbVar::add_many(&remaining_limbs[1..]) - .unwrap() + .ok_or(SynthesisError::Unsatisfiable)? .v .enforce_equal(&FpVar::zero())?; remaining_limbs[0].v.clone() @@ -621,15 +633,15 @@ impl NonNativeUintVar { } /// Compute `self + other`, without aligning the limbs. - pub fn add_no_align(&self, other: &Self) -> Self { + pub fn add_no_align(&self, other: &Self) -> Result { let mut z = vec![LimbVar::zero(); max(self.0.len(), other.0.len())]; for (i, v) in self.0.iter().enumerate() { - z[i] = z[i].add(v).unwrap(); + z[i] = z[i].add(v).ok_or(SynthesisError::Unsatisfiable)?; } for (i, v) in other.0.iter().enumerate() { - z[i] = z[i].add(v).unwrap(); + z[i] = z[i].add(v).ok_or(SynthesisError::Unsatisfiable)?; } - Self(z) + Ok(Self(z)) } /// Compute `self * other`, without aligning the limbs. @@ -650,7 +662,7 @@ impl NonNativeUintVar { ) }) .collect::>>() - .unwrap(); + .ok_or(SynthesisError::Unsatisfiable)?; return Ok(Self(z)); } let cs = self.cs().or(other.cs()); @@ -753,7 +765,7 @@ impl NonNativeUintVar { let m = Self::new_constant(cs.clone(), BoundedBigUint(m, M::MODULUS_BIT_SIZE as usize))?; // Enforce `self = q * m + r` q.mul_no_align(&m)? - .add_no_align(&r) + .add_no_align(&r)? .enforce_equal_unaligned(self)?; // Enforce `r < m` (and `r >= 0` already holds) r.enforce_lt(&m)?; @@ -789,8 +801,8 @@ impl NonNativeUintVar { let zero = Self::new_constant(cs.clone(), BoundedBigUint(BigUint::zero(), bits))?; let m = Self::new_constant(cs.clone(), BoundedBigUint(m, M::MODULUS_BIT_SIZE as usize))?; - let l = self.add_no_align(&is_ge.select(&zero, &q)?.mul_no_align(&m)?); - let r = other.add_no_align(&is_ge.select(&q, &zero)?.mul_no_align(&m)?); + let l = self.add_no_align(&is_ge.select(&zero, &q)?.mul_no_align(&m)?)?; + let r = other.add_no_align(&is_ge.select(&q, &zero)?.mul_no_align(&m)?)?; // If `self >= other`, enforce `self = other + q * m` // Otherwise, enforce `self + q * m = other` // Soundness holds because if `self` and `other` are not congruent, then @@ -799,6 +811,12 @@ impl NonNativeUintVar { } } +impl EquivalenceGadget for NonNativeUintVar { + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + self.enforce_congruent::(other) + } +} + impl]>> From for NonNativeUintVar { fn from(bits: B) -> Self { Self( @@ -834,6 +852,9 @@ pub(super) fn nonnative_field_to_field_elements Vec { assert_eq!(TargetField::extension_degree(), 1); + // `unwrap` is safe because `TargetField` is a field with extension degree + // 1, and thus `TargetField::to_base_prime_field_elements` should return an + // iterator with exactly one element. let bits = f .to_base_prime_field_elements() .next() @@ -864,11 +885,10 @@ pub(super) fn nonnative_field_to_field_elements VectorGadget> for [NonNativeUintVar] { fn add(&self, other: &Self) -> Result>, SynthesisError> { - Ok(self - .iter() + self.iter() .zip(other.iter()) .map(|(x, y)| x.add_no_align(y)) - .collect()) + .collect() } fn hadamard(&self, other: &Self) -> Result>, SynthesisError> { @@ -886,15 +906,12 @@ impl VectorGadget> for [NonNativeUintVar] } } -impl MatrixGadget> - for SparseMatrixVar> -{ +impl MatrixGadget> for SparseMatrixVar> { fn mul_vector( &self, v: &[NonNativeUintVar], ) -> Result>, SynthesisError> { - Ok(self - .coeffs + self.coeffs .iter() .map(|row| { let len = row @@ -906,7 +923,7 @@ impl MatrixGadget> // that results in more flattened `LinearCombination`s. // Consequently, `ConstraintSystem::inline_all_lcs` costs less // time, thus making trusted setup and proof generation faster. - let limbs = (0..len) + (0..len) .map(|i| { LimbVar::add_many( &row.iter() @@ -919,10 +936,10 @@ impl MatrixGadget> ) }) .collect::>>() - .unwrap(); - NonNativeUintVar(limbs) + .ok_or(SynthesisError::Unsatisfiable) + .map(NonNativeUintVar) }) - .collect()) + .collect::, _>>() } } @@ -1041,7 +1058,7 @@ mod tests { let mut r_var = NonNativeUintVar::new_constant(cs.clone(), BoundedBigUint(BigUint::zero(), 0))?; for (a, b) in a_var.into_iter().zip(b_var.into_iter()) { - r_var = r_var.add_no_align(&a.mul_no_align(&b)?); + r_var = r_var.add_no_align(&a.mul_no_align(&b)?)?; } r_var.enforce_congruent::(&c_var)?; diff --git a/folding-schemes/src/folding/hypernova/cccs.rs b/folding-schemes/src/folding/hypernova/cccs.rs index 9e42bd9..d797ee7 100644 --- a/folding-schemes/src/folding/hypernova/cccs.rs +++ b/folding-schemes/src/folding/hypernova/cccs.rs @@ -10,6 +10,7 @@ use super::Witness; use crate::arith::{ccs::CCS, Arith}; use crate::commitment::CommitmentScheme; use crate::folding::circuits::CF1; +use crate::folding::traits::Inputize; use crate::folding::traits::{CommittedInstanceOps, Dummy}; use crate::transcript::AbsorbNonNative; use crate::utils::mle::dense_vec_to_dense_mle; @@ -152,6 +153,12 @@ impl CommittedInstanceOps for CCCS { } } +impl Inputize> for CCCS { + fn inputize(&self) -> Vec { + [&self.C.inputize()[..], &self.x].concat() + } +} + #[cfg(test)] pub mod tests { use ark_pallas::Fr; diff --git a/folding-schemes/src/folding/hypernova/circuits.rs b/folding-schemes/src/folding/hypernova/circuits.rs index e9f42d8..319bb0f 100644 --- a/folding-schemes/src/folding/hypernova/circuits.rs +++ b/folding-schemes/src/folding/hypernova/circuits.rs @@ -12,7 +12,6 @@ use ark_r1cs_std::{ boolean::Boolean, eq::EqGadget, fields::{fp::FpVar, FieldVar}, - groups::GroupOpsBounds, prelude::CurveVar, uint8::UInt8, R1CSVar, ToConstraintFieldGadget, @@ -195,7 +194,6 @@ pub struct ProofVar { impl AllocVar, CF1> for ProofVar where C: CurveGroup, - ::BaseField: PrimeField, ::ScalarField: Absorb, { fn new_variable>>( @@ -237,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)] @@ -492,9 +487,7 @@ pub struct AugmentedFCircuit< FC: FCircuit>, const MU: usize, const NU: usize, -> where - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, -{ +> { pub(super) _c2: PhantomData, pub(super) _gc2: PhantomData, pub(super) poseidon_config: PoseidonConfig>, @@ -532,7 +525,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { pub fn default( poseidon_config: &PoseidonConfig>, @@ -588,11 +580,9 @@ where M: vec![], }; let mut augmented_f_circuit = Self::default(poseidon_config, F, initial_ccs)?; - if ccs.is_some() { - augmented_f_circuit.ccs = ccs.unwrap(); - } else { - augmented_f_circuit.ccs = augmented_f_circuit.upper_bound_ccs()?; - } + augmented_f_circuit.ccs = ccs + .ok_or(()) + .or_else(|_| augmented_f_circuit.upper_bound_ccs())?; Ok(augmented_f_circuit) } @@ -601,7 +591,7 @@ where /// For a stable FCircuit circuit, the CCS parameters can be computed in advance and can be /// feed in as parameter for the AugmentedFCircuit::empty method to avoid computing them there. pub fn upper_bound_ccs(&self) -> Result, Error> { - let r1cs = get_r1cs_from_cs::>(self.clone()).unwrap(); + let r1cs = get_r1cs_from_cs::>(self.clone())?; let mut ccs = CCS::from(r1cs); let z_0 = vec![C1::ScalarField::zero(); self.F.state_len()]; @@ -690,7 +680,7 @@ where self.clone().generate_constraints(cs.clone())?; cs.finalize(); let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs); + let r1cs = extract_r1cs::(&cs)?; let ccs = CCS::from(r1cs); Ok((cs, ccs)) @@ -709,7 +699,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { let pp_hash = FpVar::>::new_witness(cs.clone(), || { @@ -883,7 +872,7 @@ where // Non-base case: u_{i+1}.x[1] == H(cf_U_{i+1}) let (cf_u_i1_x, _) = cf_U_i1.clone().hash(&sponge, pp_hash.clone())?; let (cf_u_i1_x_base, _) = - CycleFoldCommittedInstanceVar::new_constant(cs.clone(), cf_u_dummy)? + CycleFoldCommittedInstanceVar::::new_constant(cs.clone(), cf_u_dummy)? .hash(&sponge, pp_hash)?; let cf_x = FpVar::new_input(cs.clone(), || { Ok(self.cf_x.unwrap_or(cf_u_i1_x_base.value()?)) @@ -1232,7 +1221,7 @@ mod tests { .into_inner() .ok_or(Error::NoInnerConstraintSystem) .unwrap(); - let cf_r1cs = extract_r1cs::(&cs2); + let cf_r1cs = extract_r1cs::(&cs2).unwrap(); println!("CF m x n: {} x {}", cf_r1cs.A.n_rows, cf_r1cs.A.n_cols); let (pedersen_params, _) = diff --git a/folding-schemes/src/folding/hypernova/decider_eth.rs b/folding-schemes/src/folding/hypernova/decider_eth.rs index f6b0118..fef400a 100644 --- a/folding-schemes/src/folding/hypernova/decider_eth.rs +++ b/folding-schemes/src/folding/hypernova/decider_eth.rs @@ -2,7 +2,7 @@ use ark_crypto_primitives::sponge::Absorb; use ark_ec::{CurveGroup, Group}; use ark_ff::PrimeField; -use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; +use ark_r1cs_std::{prelude::CurveVar, ToConstraintFieldGadget}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_snark::SNARK; use ark_std::rand::{CryptoRng, RngCore}; @@ -10,12 +10,15 @@ use ark_std::{One, Zero}; use core::marker::PhantomData; pub use super::decider_eth_circuit::DeciderEthCircuit; -use super::{lcccs::LCCCS, HyperNova}; +use super::decider_eth_circuit::DeciderHyperNovaGadget; +use super::HyperNova; use crate::commitment::{ kzg::Proof as KZGProof, pedersen::Params as PedersenParams, CommitmentScheme, }; -use crate::folding::circuits::{nonnative::affine::NonNativeAffineVar, CF2}; +use crate::folding::circuits::decider::DeciderEnabledNIFS; +use crate::folding::circuits::CF2; use crate::folding::nova::decider_eth::VerifierParam; +use crate::folding::traits::{Inputize, WitnessOps}; use crate::frontend::FCircuit; use crate::Error; use crate::{Decider as DeciderTrait, FoldingScheme}; @@ -31,7 +34,6 @@ where 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 // 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, @@ -75,8 +77,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'b> &'b GC1: GroupOpsBounds<'b, C1, GC1>, - for<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>, // constrain FS into HyperNova, since this is a Decider specifically for HyperNova HyperNova: From, crate::folding::hypernova::ProverParams: @@ -89,21 +89,18 @@ where type Proof = Proof; type VerifierParam = VerifierParam; type PublicInput = Vec; - type CommittedInstance = (); + type CommittedInstance = Vec; fn preprocess( mut rng: impl RngCore + CryptoRng, 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))?; // get the Groth16 specific setup for the circuit - let (g16_pk, g16_vk) = S::circuit_specific_setup(circuit, &mut rng).unwrap(); + let (g16_pk, g16_vk) = S::circuit_specific_setup(circuit, &mut rng) + .map_err(|e| Error::SNARKSetupFail(e.to_string()))?; // get the FoldingScheme prover & verifier params from HyperNova #[allow(clippy::type_complexity)] @@ -122,7 +119,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, @@ -137,40 +134,37 @@ where ) -> Result { let (snark_pk, cs_pk): (S::ProvingKey, CS1::ProverParams) = pp; - let circuit = DeciderEthCircuit::::from_hypernova::( - folding_scheme.into(), - )?; + let circuit = DeciderEthCircuit::::try_from(HyperNova::from(folding_scheme))?; - let snark_proof = S::prove(&snark_pk, circuit.clone(), &mut rng) - .map_err(|e| Error::Other(e.to_string()))?; - - // 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 rho = circuit.randomness; // 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, + rho, + 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))?, }) } @@ -180,45 +174,48 @@ where z_0: Vec, z_i: Vec, // we don't use the instances at the verifier level, since we check them in-circuit - _running_instance: &Self::CommittedInstance, - _incoming_instance: &Self::CommittedInstance, + running_commitments: &Self::CommittedInstance, + incoming_commitments: &Self::CommittedInstance, proof: &Self::Proof, ) -> Result { if i <= C1::ScalarField::one() { 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; + + // 6.2. Fold the commitments + let C = DeciderHyperNovaGadget::fold_group_elements_native( + running_commitments, + incoming_commitments, + None, + proof.rho, + )?[0]; // Note: the NIMFS proof is checked inside the DeciderEthCircuit, which ensures that the // 'proof.U_i1' is correctly computed - - let (cmC_x, cmC_y) = NonNativeAffineVar::inputize(proof.U_i1.C)?; - let public_input: Vec = [ - vec![pp_hash, i], - z_0, - z_i, - // U_i+1: - cmC_x, - cmC_y, - vec![proof.U_i1.u], - proof.U_i1.x.clone(), - proof.U_i1.r_x.clone(), - proof.U_i1.v.clone(), - vec![proof.kzg_challenge, proof.kzg_proof.eval, proof.rho], + &[pp_hash, i][..], + &z_0, + &z_i, + &C.inputize(), + &[proof.kzg_challenge, proof.kzg_proof.eval, proof.rho], ] .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); } + // 7.3. Verify the KZG proof // 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, &C, &proof.kzg_proof)?; Ok(true) } @@ -229,13 +226,14 @@ 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}; use crate::folding::hypernova::cccs::CCCS; + use crate::folding::hypernova::lcccs::LCCCS; use crate::folding::hypernova::PreprocessorParam; - use crate::folding::nova::decider_eth::VerifierParam; + use crate::folding::traits::CommittedInstanceOps; use crate::frontend::utils::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; @@ -300,8 +298,8 @@ pub mod tests { hypernova.i, hypernova.z_0, hypernova.z_i, - &(), - &(), + &hypernova.U_i.get_commitments(), + &hypernova.u_i.get_commitments(), &proof, ) .unwrap(); @@ -403,8 +401,8 @@ pub mod tests { hypernova.i, hypernova.z_0.clone(), hypernova.z_i.clone(), - &(), - &(), + &hypernova.U_i.get_commitments(), + &hypernova.u_i.get_commitments(), &proof, ) .unwrap(); @@ -470,8 +468,8 @@ pub mod tests { i_deserialized, z_0_deserialized.clone(), z_i_deserialized.clone(), - &(), - &(), + &hypernova.U_i.get_commitments(), + &hypernova.u_i.get_commitments(), &proof_deserialized, ) .unwrap(); diff --git a/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs b/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs index 256b0cb..71fa91b 100644 --- a/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs +++ b/folding-schemes/src/folding/hypernova/decider_eth_circuit.rs @@ -2,50 +2,73 @@ /// 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, eq::EqGadget, fields::fp::FpVar, - groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget, }; -use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; -use ark_std::Zero; -use core::{borrow::Borrow, marker::PhantomData}; +use 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::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, SparseMatrixVar}, - vec::poly_from_vec, -}; +use crate::utils::gadgets::{eval_mle, MatrixGadget}; use crate::Error; use crate::{ - arith::{ccs::CCS, r1cs::R1CS}, - folding::traits::{CommittedInstanceVarOps, Dummy, WitnessVarOps}, + arith::{ + ccs::{circuits::CCSMatricesVar, CCS}, + ArithGadget, + }, + folding::circuits::decider::{EvalGadget, KZGChallengesGadget}, }; use crate::{ commitment::{pedersen::Params as PedersenParams, CommitmentScheme}, - folding::nova::decider_eth_circuit::evaluate_gadget, + folding::circuits::decider::DeciderEnabledNIFS, }; +impl ArithGadget>, LCCCSVar> for CCSMatricesVar> { + type Evaluation = Vec>>; + + fn eval_relation( + &self, + w: &WitnessVar>, + u: &LCCCSVar, + ) -> Result { + let z = [&[u.u.clone()][..], &u.x, &w.w].concat(); + + self.M + .iter() + .map(|M_j| { + let s = log2(M_j.n_rows) as usize; + let Mz = M_j.mul_vector(&z)?; + Ok(eval_mle(s, Mz, u.r_x.clone())) + }) + .collect() + } + + fn enforce_evaluation( + _w: &WitnessVar>, + u: &LCCCSVar, + v: Self::Evaluation, + ) -> Result<(), SynthesisError> { + v.enforce_equal(&u.v) + } +} + /// In-circuit representation of the Witness associated to the CommittedInstance. #[derive(Debug, Clone)] pub struct WitnessVar { @@ -77,135 +100,38 @@ impl WitnessVarOps for WitnessVar { } } -/// CCSMatricesVar contains the matrices 'M' of the CCS without the rest of CCS parameters. -#[derive(Debug, Clone)] -pub struct CCSMatricesVar { - // we only need native representation, so the constraint field==F - pub M: Vec>>, -} - -impl AllocVar, F> for CCSMatricesVar -where - F: PrimeField, -{ - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - _mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let M: Vec>> = val - .borrow() - .M - .iter() - .map(|M| SparseMatrixVar::>::new_constant(cs.clone(), M.clone())) - .collect::>()?; - - Ok(Self { M }) - }) - } -} - -/// Gadget to check the LCCCS relation both over the native constraint field and over the -/// non-native constraint field. -#[derive(Debug, Clone)] -pub struct LCCCSCheckerGadget {} -impl LCCCSCheckerGadget { - /// performs in-circuit the RelaxedR1CS check for native variables (Az∘Bz==uCz+E) - pub fn check( - s: usize, - ccs_mat: CCSMatricesVar, - z: Vec>, - // LCCCS values - r_x: Vec>, - v: Vec>, - ) -> Result<(), SynthesisError> { - let computed_v: Vec> = ccs_mat - .M - .iter() - .map(|M_j| { - let Mz = M_j.mul_vector(&z)?; - Ok(eval_mle(s, Mz, r_x.clone())) - }) - .collect::>, SynthesisError>>()?; - - (computed_v).enforce_equal(&v)?; - Ok(()) - } -} - -/// Circuit that implements the in-circuit checks needed for the HyperNova's onchain (Ethereum's -/// EVM) verification. -#[derive(Clone, Debug)] -pub struct DeciderEthCircuit +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); @@ -219,288 +145,92 @@ 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, + randomness: rho, + 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, - for<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>, + 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; + type Randomness = CF1; + type RandomnessDummyCfg = (); + + fn fold_field_elements_gadget( + arith: &CCS>, + transcript: &mut PoseidonSpongeVar>, + pp_hash: FpVar>, + U: LCCCSVar, + _U_vec: Vec>>, + u: CCCSVar, + proof: Self::Proof, + randomness: Self::Randomness, + ) -> 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} - let z_U1: Vec>> = - [vec![U_i1.u.clone()], U_i1.x.to_vec(), W_i1.w.to_vec()].concat(); - LCCCSCheckerGadget::check( - self.ccs.s, - ccs_matrices, - z_U1, - U_i1.r_x.clone(), - U_i1.v.clone(), - )?; - - // 3.a u_i.x[0] == H(i, z_0, z_i, U_i) - let (u_i_x, _) = U_i.clone().hash(&sponge, &pp_hash, &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::commitment::pedersen::PedersenGadget; - use crate::folding::circuits::nonnative::uint::NonNativeUintVar; - use crate::folding::nova::decider_eth_circuit::{R1CSVar, RelaxedR1CSGadget}; - use crate::folding::{ - circuits::cyclefold::{ - CycleFoldCommittedInstanceVar, CycleFoldConfig, CycleFoldWitnessVar, - }, - nova::NovaCycleFoldConfig, - }; - use ark_r1cs_std::ToBitsGadget; - - let cf_u_dummy_native = - CycleFoldCommittedInstance::::dummy(NovaCycleFoldConfig::::IO_LEN); - let cf_w_dummy_native = CycleFoldWitness::::dummy(&self.cf_r1cs); - let cf_U_i = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(self.cf_U_i.unwrap_or_else(|| cf_u_dummy_native.clone())) - })?; - let cf_W_i = CycleFoldWitnessVar::::new_witness(cs.clone(), || { - Ok(self.cf_W_i.unwrap_or(cf_w_dummy_native.clone())) - })?; - - // 3.b u_i.x[1] == H(cf_U_i) - let (cf_u_i_x, _) = cf_U_i.clone().hash(&sponge, pp_hash.clone())?; - (u_i.x[1]).enforce_equal(&cf_u_i_x)?; - - // 4. check Pedersen commitments of cf_U_i.{cmE, cmW} - let H = GC2::new_constant(cs.clone(), self.cf_pedersen_params.h)?; - let G = Vec::::new_constant(cs.clone(), self.cf_pedersen_params.generators)?; - let cf_W_i_E_bits: Result>>>, SynthesisError> = - cf_W_i.E.iter().map(|E_i| E_i.to_bits_le()).collect(); - let cf_W_i_W_bits: Result>>>, SynthesisError> = - cf_W_i.W.iter().map(|W_i| W_i.to_bits_le()).collect(); - - let computed_cmE = PedersenGadget::::commit( - H.clone(), - G.clone(), - cf_W_i_E_bits?, - cf_W_i.rE.to_bits_le()?, - )?; - cf_U_i.cmE.enforce_equal(&computed_cmE)?; - let computed_cmW = - PedersenGadget::::commit(H, G, cf_W_i_W_bits?, cf_W_i.rW.to_bits_le()?)?; - cf_U_i.cmW.enforce_equal(&computed_cmW)?; - - let cf_r1cs = - R1CSVar::, NonNativeUintVar>>::new_witness( - cs.clone(), - || Ok(self.cf_r1cs.clone()), - )?; - - // 5. check RelaxedR1CS of cf_U_i - let cf_z_U = [vec![cf_U_i.u.clone()], cf_U_i.x.to_vec(), cf_W_i.W.to_vec()].concat(); - RelaxedR1CSGadget::check_nonnative(cf_r1cs, cf_W_i.E, cf_U_i.u.clone(), cf_z_U)?; - } - - // The following steps are in non-increasing order because the `computed_U_i1` is computed - // at step 8, and later used at step 6. Notice that in Nova, we compute U_i1 outside of the - // circuit, in the smart contract, but here we're computing it in-circuit, and we reuse the - // `rho_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 rho = FpVar::>::new_input(cs.clone(), || Ok(randomness))?; + let (computed_U_i1, rho_bits) = 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(()) + Boolean::le_bits_to_fp_var(&rho_bits)?.enforce_equal(&rho)?; + Ok(computed_U_i1) } -} -/// 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() + fn fold_group_elements_native( + U_commitments: &[C], + u_commitments: &[C], + _: Option, + r: Self::Randomness, + ) -> Result, Error> { + let U_C = U_commitments[0]; + let u_C = u_commitments[0]; + let C = U_C + u_C.mul(r); + Ok(vec![C]) } } @@ -508,10 +238,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_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem}; use ark_std::{test_rng, UniformRand}; use super::*; + use crate::arith::r1cs::R1CS; use crate::commitment::pedersen::Pedersen; use crate::folding::nova::PreprocessorParam; use crate::frontend::utils::CubicFCircuit; @@ -530,7 +261,7 @@ pub mod tests { let (pedersen_params, _) = Pedersen::::setup(&mut rng, ccs.n - ccs.l - 1).unwrap(); - let (lcccs, _) = ccs + let (lcccs, w) = ccs .to_lcccs::<_, Projective, Pedersen, false>(&mut rng, &pedersen_params, &z) .unwrap(); @@ -538,11 +269,10 @@ pub mod tests { // CCS's (sparse) matrices are constants in the circuit let ccs_mat = CCSMatricesVar::::new_constant(cs.clone(), ccs.clone()).unwrap(); - let zVar = Vec::>::new_input(cs.clone(), || Ok(z)).unwrap(); - let r_xVar = Vec::>::new_input(cs.clone(), || Ok(lcccs.r_x)).unwrap(); - let vVar = Vec::>::new_input(cs.clone(), || Ok(lcccs.v)).unwrap(); + let w_var = WitnessVar::new_witness(cs.clone(), || Ok(w)).unwrap(); + let lcccs_var = LCCCSVar::new_input(cs.clone(), || Ok(lcccs)).unwrap(); - LCCCSCheckerGadget::check(ccs.s, ccs_mat, zVar, r_xVar, vVar).unwrap(); + ccs_mat.enforce_relation(&w_var, &lcccs_var).unwrap(); assert!(cs.is_satisfied().unwrap()); } @@ -588,15 +318,8 @@ pub mod tests { HN::verify(hn_params.1, ivc_proof).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/lcccs.rs b/folding-schemes/src/folding/hypernova/lcccs.rs index b3f6f1a..8784d21 100644 --- a/folding-schemes/src/folding/hypernova/lcccs.rs +++ b/folding-schemes/src/folding/hypernova/lcccs.rs @@ -14,6 +14,7 @@ use crate::arith::ccs::CCS; use crate::arith::Arith; use crate::commitment::CommitmentScheme; use crate::folding::circuits::CF1; +use crate::folding::traits::Inputize; use crate::folding::traits::{CommittedInstanceOps, Dummy}; use crate::transcript::AbsorbNonNative; use crate::utils::mle::dense_vec_to_dense_mle; @@ -72,7 +73,7 @@ impl CCS { Ok(( LCCCS:: { C, - u: C::ScalarField::one(), + u: z[0], x: z[1..(1 + self.l)].to_vec(), r_x, v, @@ -154,6 +155,19 @@ impl CommittedInstanceOps for LCCCS { } } +impl Inputize> for LCCCS { + fn inputize(&self) -> Vec { + [ + &self.C.inputize(), + &[self.u][..], + &self.x, + &self.r_x, + &self.v, + ] + .concat() + } +} + #[cfg(test)] pub mod tests { use ark_pallas::{Fr, Projective}; diff --git a/folding-schemes/src/folding/hypernova/mod.rs b/folding-schemes/src/folding/hypernova/mod.rs index 2034b79..3bb1adc 100644 --- a/folding-schemes/src/folding/hypernova/mod.rs +++ b/folding-schemes/src/folding/hypernova/mod.rs @@ -5,7 +5,7 @@ use ark_crypto_primitives::sponge::{ }; use ark_ec::{CurveGroup, Group}; use ark_ff::{BigInteger, PrimeField}; -use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; +use ark_r1cs_std::{prelude::CurveVar, ToConstraintFieldGadget}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress, SerializationError}; use ark_std::{fmt::Debug, marker::PhantomData, rand::RngCore, One, Zero}; @@ -293,8 +293,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { type RunningInstance = (LCCCS, Witness); type IncomingInstance = (CCCS, Witness); @@ -360,8 +358,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { /// internal helper for new_running_instance & new_incoming_instance methods, returns the R1CS /// z=[u,x,w] vector to be used to create the LCCCS & CCCS fresh instances. @@ -467,8 +463,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { /// Reuse Nova's PreprocessorParam. type PreprocessorParam = PreprocessorParam; @@ -567,23 +561,14 @@ where let cf_r1cs = get_r1cs_from_cs::(cf_circuit)?; // if cs params exist, use them, if not, generate new ones - let cs_pp: CS1::ProverParams; - let cs_vp: CS1::VerifierParams; - let cf_cs_pp: CS2::ProverParams; - let cf_cs_vp: CS2::VerifierParams; - if prep_param.cs_pp.is_some() - && prep_param.cf_cs_pp.is_some() - && prep_param.cs_vp.is_some() - && prep_param.cf_cs_vp.is_some() - { - cs_pp = prep_param.clone().cs_pp.unwrap(); - cs_vp = prep_param.clone().cs_vp.unwrap(); - cf_cs_pp = prep_param.clone().cf_cs_pp.unwrap(); - cf_cs_vp = prep_param.clone().cf_cs_vp.unwrap(); - } else { - (cs_pp, cs_vp) = CS1::setup(&mut rng, ccs.n - ccs.l - 1)?; - (cf_cs_pp, cf_cs_vp) = CS2::setup(&mut rng, cf_r1cs.A.n_cols - cf_r1cs.l - 1)?; - } + let (cs_pp, cs_vp) = match (&prep_param.cs_pp, &prep_param.cs_vp) { + (Some(cs_pp), Some(cs_vp)) => (cs_pp.clone(), cs_vp.clone()), + _ => CS1::setup(&mut rng, ccs.n - ccs.l - 1)?, + }; + let (cf_cs_pp, cf_cs_vp) = match (&prep_param.cf_cs_pp, &prep_param.cf_cs_vp) { + (Some(cf_cs_pp), Some(cf_cs_vp)) => (cf_cs_pp.clone(), cf_cs_vp.clone()), + _ => CS2::setup(&mut rng, cf_r1cs.A.n_cols - cf_r1cs.l - 1)?, + }; let pp = ProverParams:: { poseidon_config: prep_param.poseidon_config.clone(), @@ -726,9 +711,9 @@ where let (Us, Ws): (Vec>, Vec>) = lcccs.into_iter().unzip(); let (us, ws): (Vec>, Vec>) = cccs.into_iter().unzip(); - (Some(Us), Some(Ws), Some(us), Some(ws)) + (Us, Ws, us, ws) } else { - (None, None, None, None) + (vec![], vec![], vec![], vec![]) }; let augmented_f_circuit: AugmentedFCircuit; @@ -807,9 +792,9 @@ where z_i: Some(self.z_i.clone()), external_inputs: Some(external_inputs.clone()), U_i: Some(self.U_i.clone()), - Us: Us.clone(), + Us: Some(Us), u_i_C: Some(self.u_i.C), - us: us.clone(), + us: Some(us), U_i1_C: Some(U_i1.C), F: self.F.clone(), x: Some(u_i1_x), @@ -826,21 +811,12 @@ where PoseidonSponge::::new(&self.poseidon_config); transcript_p.absorb(&self.pp_hash); - let (all_Us, all_us, all_Ws, all_ws) = if MU > 1 || NU > 1 { - ( - [vec![self.U_i.clone()], Us.clone().unwrap()].concat(), - [vec![self.u_i.clone()], us.clone().unwrap()].concat(), - [vec![self.W_i.clone()], Ws.unwrap()].concat(), - [vec![self.w_i.clone()], ws.unwrap()].concat(), - ) - } else { - ( - vec![self.U_i.clone()], - vec![self.u_i.clone()], - vec![self.W_i.clone()], - vec![self.w_i.clone()], - ) - }; + let (all_Us, all_us, all_Ws, all_ws) = ( + [&[self.U_i.clone()][..], &Us].concat(), + [&[self.u_i.clone()][..], &us].concat(), + [vec![self.W_i.clone()], Ws].concat(), + [vec![self.w_i.clone()], ws].concat(), + ); let (rho, nimfs_proof); (nimfs_proof, U_i1, W_i1, rho) = NIMFS::>::prove( @@ -865,7 +841,9 @@ where ); let rho_bits = rho.into_bigint().to_bits_le()[..NOVA_N_BITS_RO].to_vec(); - let rho_Fq = C1::BaseField::from_bigint(BigInteger::from_bits_le(&rho_bits)).unwrap(); + let rho_Fq = C1::BaseField::from(::BigInt::from_bits_le( + &rho_bits, + )); // CycleFold part: // get the vector used as public inputs 'x' in the CycleFold circuit. @@ -935,9 +913,9 @@ where z_i: Some(self.z_i.clone()), external_inputs: Some(external_inputs), U_i: Some(self.U_i.clone()), - Us: Us.clone(), + Us: Some(Us), u_i_C: Some(self.u_i.C), - us: us.clone(), + us: Some(us), U_i1_C: Some(U_i1.C), F: self.F.clone(), x: Some(u_i1_x), @@ -1030,7 +1008,7 @@ where } = ivc_proof; let (pp, vp) = params; - let f_circuit = FC::new(fcircuit_params).unwrap(); + let f_circuit = FC::new(fcircuit_params)?; let augmented_f_circuit = AugmentedFCircuit::::empty( &pp.poseidon_config, f_circuit.clone(), diff --git a/folding-schemes/src/folding/hypernova/nimfs.rs b/folding-schemes/src/folding/hypernova/nimfs.rs index 1fae370..a3b27bd 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], @@ -258,8 +263,9 @@ where let rho_scalar = C::ScalarField::from_le_bytes_mod_order(b"rho"); transcript.absorb(&rho_scalar); let rho_bits: Vec = transcript.get_challenge_nbits(NOVA_N_BITS_RO); - let rho: C::ScalarField = - C::ScalarField::from_bigint(BigInteger::from_bits_le(&rho_bits)).unwrap(); + let rho: C::ScalarField = C::ScalarField::from( + ::BigInt::from_bits_le(&rho_bits), + ); // Step 7: Create the folded instance let folded_lcccs = Self::fold( @@ -377,8 +383,9 @@ where let rho_scalar = C::ScalarField::from_le_bytes_mod_order(b"rho"); transcript.absorb(&rho_scalar); let rho_bits: Vec = transcript.get_challenge_nbits(NOVA_N_BITS_RO); - let rho: C::ScalarField = - C::ScalarField::from_bigint(BigInteger::from_bits_le(&rho_bits)).unwrap(); + let rho: C::ScalarField = C::ScalarField::from( + ::BigInt::from_bits_le(&rho_bits), + ); // Step 7: Compute the folded instance Ok(Self::fold( diff --git a/folding-schemes/src/folding/nova/circuits.rs b/folding-schemes/src/folding/nova/circuits.rs index b5803a2..6507674 100644 --- a/folding-schemes/src/folding/nova/circuits.rs +++ b/folding-schemes/src/folding/nova/circuits.rs @@ -11,7 +11,6 @@ use ark_r1cs_std::{ boolean::Boolean, eq::EqGadget, fields::{fp::FpVar, FieldVar}, - groups::GroupOpsBounds, prelude::CurveVar, uint8::UInt8, R1CSVar, ToConstraintFieldGadget, @@ -73,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()?) } @@ -124,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 @@ -174,8 +165,6 @@ pub struct ChallengeGadget { } impl ChallengeGadget where - C: CurveGroup, - ::BaseField: PrimeField, ::ScalarField: Absorb, { pub fn get_challenge_native>( @@ -232,9 +221,7 @@ pub struct AugmentedFCircuit< C2: CurveGroup, GC2: CurveVar>, FC: FCircuit>, -> where - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, -{ +> { pub(super) _gc2: PhantomData, pub(super) poseidon_config: PoseidonConfig>, pub(super) pp_hash: Option>, @@ -264,8 +251,6 @@ pub struct AugmentedFCircuit< impl>, FC: FCircuit>> AugmentedFCircuit -where - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { pub fn empty(poseidon_config: &PoseidonConfig>, F_circuit: FC) -> Self { Self { @@ -306,7 +291,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { let pp_hash = FpVar::>::new_witness(cs.clone(), || { @@ -508,7 +492,7 @@ where // Non-base case: u_{i+1}.x[1] == H(cf_U_{i+1}) let (cf_u_i1_x, _) = cf_U_i1.clone().hash(&sponge, pp_hash.clone())?; let (cf_u_i1_x_base, _) = - CycleFoldCommittedInstanceVar::new_constant(cs.clone(), cf_u_dummy)? + CycleFoldCommittedInstanceVar::::new_constant(cs.clone(), cf_u_dummy)? .hash(&sponge, pp_hash)?; let cf_x = FpVar::new_input(cs.clone(), || { Ok(self.cf_x.unwrap_or(cf_u_i1_x_base.value()?)) diff --git a/folding-schemes/src/folding/nova/decider.rs b/folding-schemes/src/folding/nova/decider.rs index e11757c..d651091 100644 --- a/folding-schemes/src/folding/nova/decider.rs +++ b/folding-schemes/src/folding/nova/decider.rs @@ -2,8 +2,8 @@ /// DeciderEth from decider_eth.rs file. /// 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::{poseidon::PoseidonSponge, Absorb, CryptographicSponge}; -use ark_ec::{AffineRepr, CurveGroup, Group}; +use ark_crypto_primitives::sponge::Absorb; +use ark_ec::{CurveGroup, Group}; use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; @@ -13,18 +13,16 @@ use ark_std::{One, Zero}; use core::marker::PhantomData; use super::decider_circuits::{DeciderCircuit1, DeciderCircuit2}; -use super::{ - nifs::{nova::NIFS, NIFSTrait}, - CommittedInstance, Nova, -}; +use super::decider_eth_circuit::DeciderNovaGadget; +use super::Nova; use crate::commitment::CommitmentScheme; +use crate::folding::circuits::decider::DeciderEnabledNIFS; use crate::folding::circuits::{ - cyclefold::CycleFoldCommittedInstance, - nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar}, + cyclefold::{CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar}, CF2, }; +use crate::folding::traits::{CommittedInstanceOps, Inputize, WitnessOps}; use crate::frontend::FCircuit; -use crate::transcript::poseidon::poseidon_canonical_config; use crate::Error; use crate::{Decider as DeciderTrait, FoldingScheme}; @@ -45,8 +43,9 @@ where // 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, // 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], @@ -147,22 +146,21 @@ where S2::VerifyingKey, >; type PublicInput = Vec; - type CommittedInstance = CommittedInstance; + type CommittedInstance = Vec; 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(); - let (c2_g16_pk, c2_g16_vk) = S2::circuit_specific_setup(circuit2, &mut rng).unwrap(); + let (c1_g16_pk, c1_g16_vk) = S1::circuit_specific_setup(circuit1, &mut rng) + .map_err(|e| Error::SNARKSetupFail(e.to_string()))?; + let (c2_g16_pk, c2_g16_vk) = S2::circuit_specific_setup(circuit2, &mut rng) + .map_err(|e| Error::SNARKSetupFail(e.to_string()))?; // get the FoldingScheme prover & verifier params from Nova #[allow(clippy::type_complexity)] @@ -200,76 +198,57 @@ 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 cmT = circuit1.proof; + let r = circuit1.randomness; + 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 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], + cs1_proofs: c1_kzg_proofs + .try_into() + .map_err(|e: Vec<_>| Error::NotExpectedLength(e.len(), 2))?, + cs2_proofs: c2_kzg_proofs + .try_into() + .map_err(|e: Vec<_>| Error::NotExpectedLength(e.len(), 2))?, cmT, - cf_U_i: circuit1.cf_U_i.unwrap(), - cs1_challenges: [challenge_W, challenge_E], - cs2_challenges: [c2_challenge_W, c2_challenge_E], + r, + cf_U_final, + cs1_challenges: c1_kzg_challenges + .try_into() + .map_err(|e: Vec<_>| Error::NotExpectedLength(e.len(), 2))?, + cs2_challenges: c2_kzg_challenges + .try_into() + .map_err(|e: Vec<_>| Error::NotExpectedLength(e.len(), 2))?, }) } @@ -278,72 +257,38 @@ 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_commitments: &Self::CommittedInstance, + incoming_commitments: &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 poseidon_config = poseidon_canonical_config::(); - let mut transcript = PoseidonSponge::::new(&poseidon_config); - let (U, r_bits) = NIFS::>::verify( - &mut transcript, - vp.pp_hash, - running_instance, - incoming_instance, - &proof.cmT, + // 6.2. Fold the commitments + let U_final_commitments = DeciderNovaGadget::fold_group_elements_native( + running_commitments, + incoming_commitments, + Some(proof.cmT), + proof.r, )?; - let r = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) - .ok_or(Error::OutOfBounds)?; - - let (cmE_x, cmE_y) = NonNativeAffineVar::inputize(U.cmE)?; - let (cmW_x, cmW_y) = NonNativeAffineVar::inputize(U.cmW)?; - let (cmT_x, cmT_y) = NonNativeAffineVar::inputize(proof.cmT)?; - - let zero = (&C2::BaseField::zero(), &C2::BaseField::zero()); - let cmE_affine = proof.cf_U_i.cmE.into_affine(); - let cmW_affine = proof.cf_U_i.cmW.into_affine(); - let (cf_cmE_x, cf_cmE_y) = cmE_affine.xy().unwrap_or(zero); - let cf_cmE_z = C1::ScalarField::one(); - let (cf_cmW_x, cf_cmW_y) = cmW_affine.xy().unwrap_or(zero); - let cf_cmW_z = C1::ScalarField::one(); + 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_{i+1} values: - vec![U.u], - U.x.clone(), - cmE_x, - cmE_y, - cmW_x, - cmW_y, - // 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 - ], - // cf_U_i values - NonNativeUintVar::>::inputize(proof.cf_U_i.u), - proof - .cf_U_i - .x + let c1_public_input = [ + &[vp.pp_hash, i][..], + &z_0, + &z_i, + &U_final_commitments .iter() - .flat_map(|&x_i| NonNativeUintVar::>::inputize(x_i)) - .collect::>(), - vec![ - *cf_cmE_x, *cf_cmE_y, cf_cmE_z, *cf_cmW_x, *cf_cmW_y, cf_cmW_z, - ], - // NIFS values: - cmT_x, - cmT_y, - vec![r], + .flat_map(|c| c.inputize()) + .collect::>(), + &Inputize::, CycleFoldCommittedInstanceVar>::inputize(&cf_U), + &proof.cs1_challenges, + &proof.cs1_proofs.iter().map(|p| p.eval).collect::>(), + &proof.cmT.inputize(), + &[proof.r], ] .concat(); @@ -353,26 +298,15 @@ where return Err(Error::SNARKVerificationFail); } - let (cf2_cmE_x, cf2_cmE_y) = NonNativeAffineVar::inputize(proof.cf_U_i.cmE)?; - let (cf2_cmW_x, cf2_cmW_y) = NonNativeAffineVar::inputize(proof.cf_U_i.cmW)?; - // snark proof 2 // migrate pp_hash from C1::Fr to C1::Fq 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], - vec![proof.cf_U_i.u], - proof.cf_U_i.x.clone(), - cf2_cmE_x, - cf2_cmE_y, - cf2_cmW_x, - cf2_cmW_y, - 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(); @@ -382,33 +316,24 @@ where return Err(Error::SNARKVerificationFail); } - // 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], - )?; + // 7.3. check C1 commitments (main instance commitments) + for ((cm, &c), pi) in U_final_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], - )?; + // 4.3. check C2 commitments (CycleFold instance commitments) + 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) } @@ -494,7 +419,13 @@ 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, + decider_vp, + nova.i, + nova.z_0, + nova.z_i, + &nova.U_i.get_commitments(), + &nova.u_i.get_commitments(), + &proof, ) .unwrap(); assert!(verified); diff --git a/folding-schemes/src/folding/nova/decider_circuits.rs b/folding-schemes/src/folding/nova/decider_circuits.rs index d15ccf0..82e5411 100644 --- a/folding-schemes/src/folding/nova/decider_circuits.rs +++ b/folding-schemes/src/folding/nova/decider_circuits.rs @@ -2,116 +2,63 @@ /// 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}, - groups::GroupOpsBounds, - 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, R1CSVar, RelaxedR1CSGadget, WitnessVar, - }, + decider_eth_circuit::DeciderNovaGadget, nifs::{nova::NIFS, NIFSTrait}, CommittedInstance, Nova, Witness, }; -use crate::arith::r1cs::R1CS; -use crate::commitment::CommitmentScheme; -use crate::folding::circuits::{ - cyclefold::{ - CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar, CycleFoldConfig, - CycleFoldWitness, - }, - nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar}, - CF1, CF2, +use crate::folding::{ + circuits::{CF1, CF2}, + traits::WitnessOps, }; -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>, - for<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>, -{ - 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 NIFS::prove call @@ -129,360 +76,98 @@ where let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) .ok_or(Error::OutOfBounds)?; - // 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, + randomness: r_Fr, + 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>, - for<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>, -{ - fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - let r1cs = - R1CSVar::, FpVar>>::new_witness(cs.clone(), || { - Ok(self.r1cs.clone()) - })?; - - let pp_hash = FpVar::>::new_input(cs.clone(), || { - Ok(self.pp_hash.unwrap_or_else(CF1::::zero)) - })?; - let i = - FpVar::>::new_input(cs.clone(), || Ok(self.i.unwrap_or_else(CF1::::zero)))?; - let z_0 = Vec::>>::new_input(cs.clone(), || { - Ok(self.z_0.unwrap_or(vec![CF1::::zero()])) - })?; - let z_i = Vec::>>::new_input(cs.clone(), || { - Ok(self.z_i.unwrap_or(vec![CF1::::zero()])) - })?; - - let u_dummy_native = CommittedInstance::::dummy(&self.r1cs); - let w_dummy_native = Witness::::dummy(&self.r1cs); - - let u_i = CommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(self.u_i.unwrap_or(u_dummy_native.clone())) - })?; - let U_i = CommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(self.U_i.unwrap_or(u_dummy_native.clone())) - })?; - // here (U_i1, W_i1) = NIFS.P( (U_i,W_i), (u_i,w_i)) - let U_i1 = CommittedInstanceVar::::new_input(cs.clone(), || { - Ok(self.U_i1.unwrap_or(u_dummy_native.clone())) - })?; - let W_i1 = WitnessVar::::new_witness(cs.clone(), || { - Ok(self.W_i1.unwrap_or(w_dummy_native.clone())) - })?; - - // allocate the inputs for the check 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} - let z_U1: Vec>> = - [vec![U_i1.u.clone()], U_i1.x.to_vec(), W_i1.W.to_vec()].concat(); - RelaxedR1CSGadget::check_native(r1cs, W_i1.E.clone(), U_i1.u.clone(), z_U1)?; - - // 1.1.a, 5.1 compute NIFS.V and 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, - - /// 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, +pub type DeciderCircuit2 = GenericOffchainDeciderCircuit2; - /// 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>, - for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, -{ - fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - let pp_hash = FpVar::>::new_input(cs.clone(), || { - Ok(self.pp_hash.unwrap_or_else(CF1::::zero)) - })?; - - let cf_u_dummy_native = CommittedInstance::::dummy(&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 = - R1CSVar::, FpVar>>::new_witness(cs.clone(), || { - Ok(self.cf_r1cs.clone()) - })?; - - // 6. check RelaxedR1CS of cf_U_i - let cf_z_U = [vec![cf_U_i.u.clone()], cf_U_i.x.to_vec(), cf_W_i.W.to_vec()].concat(); - RelaxedR1CSGadget::check_native(cf_r1cs, cf_W_i.E.clone(), cf_U_i.u.clone(), cf_z_U)?; - - // `transcript` is for challenge generation. - let mut transcript = - PoseidonSpongeVar::::new(cs.clone(), &self.poseidon_config); - transcript.absorb(&pp_hash)?; - - // allocate the inputs for the check 7.1 - let 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_vesta::{constraints::GVar as GVar2, Projective as Projective2}; use super::*; @@ -530,9 +215,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 d17b3fe..55e3a4d 100644 --- a/folding-schemes/src/folding/nova/decider_eth.rs +++ b/folding-schemes/src/folding/nova/decider_eth.rs @@ -3,11 +3,11 @@ /// More details can be found at the documentation page: /// https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html use ark_bn254::Bn254; -use ark_crypto_primitives::sponge::{poseidon::PoseidonSponge, Absorb, CryptographicSponge}; +use ark_crypto_primitives::sponge::Absorb; use ark_ec::{AffineRepr, CurveGroup, Group}; use ark_ff::{BigInteger, PrimeField}; use ark_groth16::Groth16; -use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; +use ark_r1cs_std::{prelude::CurveVar, ToConstraintFieldGadget}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_snark::SNARK; use ark_std::rand::{CryptoRng, RngCore}; @@ -15,37 +15,36 @@ use ark_std::{One, Zero}; use core::marker::PhantomData; pub use super::decider_eth_circuit::DeciderEthCircuit; -use super::{ - nifs::{nova::NIFS, NIFSTrait}, - CommittedInstance, Nova, -}; +use super::decider_eth_circuit::DeciderNovaGadget; +use super::{CommittedInstance, Nova}; use crate::commitment::{ kzg::{Proof as KZGProof, KZG}, pedersen::Params as PedersenParams, CommitmentScheme, }; -use crate::folding::circuits::{nonnative::affine::NonNativeAffineVar, CF2}; -use crate::folding::nova::circuits::ChallengeGadget; +use crate::folding::circuits::decider::DeciderEnabledNIFS; +use crate::folding::circuits::CF2; +use crate::folding::traits::{Inputize, WitnessOps}; use crate::frontend::FCircuit; -use crate::transcript::poseidon::poseidon_canonical_config; 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], + kzg_proofs: [CS::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, + cmT: C, + r: C::ScalarField, // 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)] @@ -98,8 +97,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'b> &'b GC1: GroupOpsBounds<'b, C1, GC1>, - for<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>, // constrain FS into Nova, since this is a Decider specifically for Nova Nova: From, crate::folding::nova::ProverParams: @@ -112,18 +109,18 @@ where type Proof = Proof; type VerifierParam = VerifierParam; type PublicInput = Vec; - type CommittedInstance = CommittedInstance; + type CommittedInstance = Vec; 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(); + let (g16_pk, g16_vk) = S::circuit_specific_setup(circuit, &mut rng) + .map_err(|e| Error::SNARKSetupFail(e.to_string()))?; // get the FoldingScheme prover & verifier params from Nova #[allow(clippy::type_complexity)] @@ -156,46 +153,39 @@ 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 W_i1 = circuit.W_i1.unwrap(); + let cmT = circuit.proof; + let r = circuit.randomness; // 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, - kzg_challenges: [challenge_W, challenge_E], + r, + kzg_proofs: kzg_proofs + .try_into() + .map_err(|e: Vec<_>| Error::NotExpectedLength(e.len(), 2))?, + kzg_challenges: kzg_challenges + .try_into() + .map_err(|e: Vec<_>| Error::NotExpectedLength(e.len(), 2))?, }) } @@ -204,71 +194,59 @@ 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_commitments: &Self::CommittedInstance, + incoming_commitments: &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 poseidon_config = poseidon_canonical_config::(); - let mut transcript = PoseidonSponge::::new(&poseidon_config); - let (U, r_bits) = NIFS::>::verify( - &mut transcript, - vp.pp_hash, - running_instance, - incoming_instance, - &proof.cmT, + let Self::VerifierParam { + pp_hash, + snark_vp, + cs_vp, + } = vp; + + // 6.2. Fold the commitments + let U_final_commitments = DeciderNovaGadget::fold_group_elements_native( + running_commitments, + incoming_commitments, + Some(proof.cmT), + proof.r, )?; - let r = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) - .ok_or(Error::OutOfBounds)?; - - let (cmE_x, cmE_y) = NonNativeAffineVar::inputize(U.cmE)?; - let (cmW_x, cmW_y) = NonNativeAffineVar::inputize(U.cmW)?; - let (cmT_x, cmT_y) = NonNativeAffineVar::inputize(proof.cmT)?; - - let public_input: Vec = [ - vec![vp.pp_hash, i], - z_0, - z_i, - vec![U.u], - U.x.clone(), - cmE_x, - cmE_y, - cmW_x, - cmW_y, - proof.kzg_challenges.to_vec(), - vec![ - proof.kzg_proofs[0].eval, // eval_W - proof.kzg_proofs[1].eval, // eval_E - ], - cmT_x, - cmT_y, - vec![r], + + let public_input = [ + &[pp_hash, i][..], + &z_0, + &z_i, + &U_final_commitments + .iter() + .flat_map(|c| c.inputize()) + .collect::>(), + &proof.kzg_challenges, + &proof.kzg_proofs.iter().map(|p| p.eval).collect::>(), + &proof.cmT.inputize(), + &[proof.r], ] .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], - )?; + // 7.3. Verify the KZG proofs + for ((cm, &c), pi) in U_final_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) } @@ -278,7 +256,6 @@ where #[allow(clippy::too_many_arguments)] pub fn prepare_calldata( function_signature_check: [u8; 4], - pp_hash: ark_bn254::Fr, i: ark_bn254::Fr, z_0: Vec, z_i: Vec, @@ -286,22 +263,6 @@ pub fn prepare_calldata( incoming_instance: &CommittedInstance, proof: Proof, Groth16>, ) -> Result, Error> { - // compute the challenge r - let poseidon_config = poseidon_canonical_config::(); - let mut transcript = PoseidonSponge::::new(&poseidon_config); - let r_bits = ChallengeGadget::< - ark_bn254::G1Projective, - CommittedInstance, - >::get_challenge_native( - &mut transcript, - pp_hash, - running_instance, - incoming_instance, - Some(&proof.cmT), - ); - let r = - ark_bn254::Fr::from_bigint(BigInteger::from_bits_le(&r_bits)).ok_or(Error::OutOfBounds)?; - Ok(vec![ function_signature_check.to_vec(), i.into_bigint().to_bytes_be(), // i @@ -311,26 +272,14 @@ 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 - r.into_bigint().to_bytes_be(), // r - running_instance - .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(running_instance.cmW.into_affine())?, + point_to_eth_format(running_instance.cmE.into_affine())?, + point_to_eth_format(incoming_instance.cmW.into_affine())?, point_to_eth_format(proof.cmT.into_affine())?, // cmT - 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 + proof.r.into_bigint().to_bytes_be(), // r + 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 proof.kzg_challenges[0].into_bigint().to_bytes_be(), // challenge_W proof.kzg_challenges[1].into_bigint().to_bytes_be(), // challenge_E proof.kzg_proofs[0].eval.into_bigint().to_bytes_be(), // eval W @@ -373,6 +322,7 @@ pub mod tests { use super::*; use crate::commitment::pedersen::Pedersen; use crate::folding::nova::{PreprocessorParam, ProverParams as NovaProverParams}; + use crate::folding::traits::CommittedInstanceOps; use crate::frontend::utils::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; @@ -434,8 +384,8 @@ pub mod tests { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &nova.U_i.get_commitments(), + &nova.u_i.get_commitments(), &proof, ) .unwrap(); @@ -444,7 +394,13 @@ pub mod tests { // 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, + decider_vp, + nova.i, + nova.z_0, + nova.z_i, + &nova.U_i.get_commitments(), + &nova.u_i.get_commitments(), + &proof, ) .unwrap(); assert!(verified); @@ -550,8 +506,8 @@ pub mod tests { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &nova.U_i.get_commitments(), + &nova.u_i.get_commitments(), &proof, ) .unwrap(); @@ -579,12 +535,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 = @@ -605,10 +555,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( @@ -616,8 +562,8 @@ pub mod tests { i_deserialized, z_0_deserialized, z_i_deserialized, - &U_i_deserialized, - &u_i_deserialized, + &nova.U_i.get_commitments(), + &nova.u_i.get_commitments(), &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 a8ae675..a2b2a3e 100644 --- a/folding-schemes/src/folding/nova/decider_eth_circuit.rs +++ b/folding-schemes/src/folding/nova/decider_eth_circuit.rs @@ -4,130 +4,42 @@ /// 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}, - groups::GroupOpsBounds, - 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::{nova::NIFS, NIFSTrait}, CommittedInstance, Nova, Witness, }; use crate::commitment::{pedersen::Params as PedersenParams, CommitmentScheme}; -use crate::folding::circuits::{ - cyclefold::{CycleFoldCommittedInstance, CycleFoldWitness}, - nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar}, - CF1, CF2, +use crate::folding::{ + circuits::{ + decider::on_chain::GenericOnchainDeciderCircuit, nonnative::affine::NonNativeAffineVar, + CF1, CF2, + }, + traits::{WitnessOps, WitnessVarOps}, }; use crate::frontend::FCircuit; -use crate::transcript::{Transcript, TranscriptVar}; -use crate::utils::{ - gadgets::{MatrixGadget, SparseMatrixVar, VectorGadget}, - vec::poly_from_vec, -}; use crate::Error; use crate::{ - arith::r1cs::R1CS, - folding::traits::{CommittedInstanceVarOps, Dummy, WitnessVarOps}, + arith::r1cs::{circuits::R1CSMatricesVar, R1CS}, + folding::circuits::decider::{DeciderEnabledNIFS, EvalGadget, KZGChallengesGadget}, }; -#[derive(Debug, Clone)] -pub struct RelaxedR1CSGadget {} -impl RelaxedR1CSGadget { - /// performs the RelaxedR1CS check for native variables (Az∘Bz==uCz+E) - pub fn check_native( - r1cs: R1CSVar>, - E: Vec>, - u: FpVar, - z: Vec>, - ) -> Result<(), SynthesisError> { - let Az = r1cs.A.mul_vector(&z)?; - let Bz = r1cs.B.mul_vector(&z)?; - let Cz = r1cs.C.mul_vector(&z)?; - let uCzE = Cz.mul_scalar(&u)?.add(&E)?; - let AzBz = Az.hadamard(&Bz)?; - AzBz.enforce_equal(&uCzE)?; - Ok(()) - } - - /// performs the RelaxedR1CS check for non-native variables (Az∘Bz==uCz+E) - pub fn check_nonnative( - r1cs: R1CSVar>, - E: Vec>, - u: NonNativeUintVar, - z: Vec>, - ) -> Result<(), SynthesisError> { - // First we do addition and multiplication without mod F's order - let Az = r1cs.A.mul_vector(&z)?; - let Bz = r1cs.B.mul_vector(&z)?; - let Cz = r1cs.C.mul_vector(&z)?; - let uCzE = Cz.mul_scalar(&u)?.add(&E)?; - let AzBz = Az.hadamard(&Bz)?; - - // Then we compare the results by checking if they are congruent - // modulo the field order - AzBz.into_iter() - .zip(uCzE) - .try_for_each(|(a, b)| a.enforce_congruent::(&b)) - } -} - -#[derive(Debug, Clone)] -pub struct R1CSVar> { - _f: PhantomData, - _cf: PhantomData, - _fv: PhantomData, - pub A: SparseMatrixVar, - pub B: SparseMatrixVar, - pub C: SparseMatrixVar, -} - -impl AllocVar, CF> for R1CSVar -where - F: PrimeField, - CF: PrimeField, - FV: AllocVar, -{ - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - _mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let A = SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().A)?; - let B = SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().B)?; - let C = SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().C)?; - - Ok(Self { - _f: PhantomData, - _cf: PhantomData, - _fv: PhantomData, - A, - B, - C, - }) - }) - } -} - /// In-circuit representation of the Witness associated to the CommittedInstance. #[derive(Debug, Clone)] pub struct WitnessVar { @@ -166,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} @@ -259,517 +129,116 @@ where .ok_or(Error::OutOfBounds)?; // 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, + randomness: r_Fr, + 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, - for<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>, + CF1: Absorb, { - fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - let r1cs = - R1CSVar::, FpVar>>::new_witness(cs.clone(), || { - Ok(self.r1cs.clone()) - })?; - - let pp_hash = FpVar::>::new_input(cs.clone(), || { - Ok(self.pp_hash.unwrap_or_else(CF1::::zero)) - })?; - let i = - FpVar::>::new_input(cs.clone(), || Ok(self.i.unwrap_or_else(CF1::::zero)))?; - let z_0 = Vec::>>::new_input(cs.clone(), || { - Ok(self.z_0.unwrap_or(vec![CF1::::zero()])) - })?; - let z_i = Vec::>>::new_input(cs.clone(), || { - Ok(self.z_i.unwrap_or(vec![CF1::::zero()])) - })?; - - let u_dummy_native = CommittedInstance::::dummy(&self.r1cs); - let w_dummy_native = Witness::::dummy(&self.r1cs); - - let u_i = CommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(self.u_i.unwrap_or(u_dummy_native.clone())) - })?; - let U_i = CommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(self.U_i.unwrap_or(u_dummy_native.clone())) - })?; - // here (U_i1, W_i1) = NIFS.P( (U_i,W_i), (u_i,w_i)) - let U_i1 = CommittedInstanceVar::::new_input(cs.clone(), || { - Ok(self.U_i1.unwrap_or(u_dummy_native.clone())) - })?; - let W_i1 = WitnessVar::::new_witness(cs.clone(), || { - Ok(self.W_i1.unwrap_or(w_dummy_native.clone())) - })?; - - // allocate the inputs for the check 5.1 - let kzg_c_W = FpVar::>::new_input(cs.clone(), || { - Ok(self.kzg_c_W.unwrap_or_else(CF1::::zero)) - })?; - let kzg_c_E = FpVar::>::new_input(cs.clone(), || { - Ok(self.kzg_c_E.unwrap_or_else(CF1::::zero)) - })?; - // allocate the inputs for the check 5.2 - let eval_W = FpVar::>::new_input(cs.clone(), || { - Ok(self.eval_W.unwrap_or_else(CF1::::zero)) - })?; - let eval_E = FpVar::>::new_input(cs.clone(), || { - Ok(self.eval_E.unwrap_or_else(CF1::::zero)) - })?; - - // `sponge` is for digest computation. - let sponge = PoseidonSpongeVar::::new(cs.clone(), &self.poseidon_config); - // `transcript` is for challenge generation. - let mut transcript = sponge.clone(); - - // The following enumeration of the steps matches the one used at the documentation page - // https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html - - // 2. u_i.cmE==cm(0), u_i.u==1 - // Here zero is the x & y coordinates of the zero point affine representation. - let zero = NonNativeUintVar::new_constant(cs.clone(), C1::BaseField::zero())?; - u_i.cmE.x.enforce_equal_unaligned(&zero)?; - u_i.cmE.y.enforce_equal_unaligned(&zero)?; - (u_i.u.is_one()?).enforce_equal(&Boolean::TRUE)?; - - // 3.a u_i.x[0] == H(i, z_0, z_i, U_i) - let (u_i_x, U_i_vec) = U_i.clone().hash(&sponge, &pp_hash, &i, &z_0, &z_i)?; - (u_i.x[0]).enforce_equal(&u_i_x)?; - - // 4. check RelaxedR1CS of U_{i+1} - let z_U1: Vec>> = - [vec![U_i1.u.clone()], U_i1.x.to_vec(), W_i1.W.to_vec()].concat(); - RelaxedR1CSGadget::check_native(r1cs, W_i1.E.clone(), U_i1.u.clone(), z_U1)?; - - #[cfg(feature = "light-test")] - log::warn!("[WARNING]: Running with the 'light-test' feature, skipping the big part of the DeciderEthCircuit.\n Only for testing purposes."); - - // The following two checks (and their respective allocations) are disabled for normal - // tests since they take several millions of constraints and would take several minutes - // (and RAM) to run the test. It is active by default, and not active only when - // 'light-test' feature is used. - #[cfg(not(feature = "light-test"))] - { - // imports here instead of at the top of the file, so we avoid having multiple - // `#[cfg(not(test))]` - use crate::commitment::pedersen::PedersenGadget; - use crate::folding::{ - circuits::cyclefold::{ - CycleFoldCommittedInstanceVar, CycleFoldConfig, CycleFoldWitnessVar, - }, - nova::NovaCycleFoldConfig, - }; - use ark_r1cs_std::ToBitsGadget; - - let cf_u_dummy_native = - CycleFoldCommittedInstance::::dummy(NovaCycleFoldConfig::::IO_LEN); - let w_dummy_native = CycleFoldWitness::::dummy(&self.cf_r1cs); - let cf_U_i = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(self.cf_U_i.unwrap_or_else(|| cf_u_dummy_native.clone())) - })?; - let cf_W_i = CycleFoldWitnessVar::::new_witness(cs.clone(), || { - Ok(self.cf_W_i.unwrap_or(w_dummy_native.clone())) - })?; - - // 3.b u_i.x[1] == H(cf_U_i) - let (cf_u_i_x, _) = cf_U_i.clone().hash(&sponge, pp_hash.clone())?; - (u_i.x[1]).enforce_equal(&cf_u_i_x)?; - - // 7. check Pedersen commitments of cf_U_i.{cmE, cmW} - let H = GC2::new_constant(cs.clone(), self.cf_pedersen_params.h)?; - let G = Vec::::new_constant(cs.clone(), self.cf_pedersen_params.generators)?; - let cf_W_i_E_bits: Result>>>, SynthesisError> = - cf_W_i.E.iter().map(|E_i| E_i.to_bits_le()).collect(); - let cf_W_i_W_bits: Result>>>, SynthesisError> = - cf_W_i.W.iter().map(|W_i| W_i.to_bits_le()).collect(); - - let computed_cmE = PedersenGadget::::commit( - H.clone(), - G.clone(), - cf_W_i_E_bits?, - cf_W_i.rE.to_bits_le()?, - )?; - cf_U_i.cmE.enforce_equal(&computed_cmE)?; - let computed_cmW = - PedersenGadget::::commit(H, G, cf_W_i_W_bits?, cf_W_i.rW.to_bits_le()?)?; - cf_U_i.cmW.enforce_equal(&computed_cmW)?; - - let cf_r1cs = - R1CSVar::, NonNativeUintVar>>::new_witness( - cs.clone(), - || Ok(self.cf_r1cs.clone()), - )?; - - // 6. check RelaxedR1CS of cf_U_i (CycleFold instance) - let cf_z_U = [vec![cf_U_i.u.clone()], cf_U_i.x.to_vec(), cf_W_i.W.to_vec()].concat(); - RelaxedR1CSGadget::check_nonnative(cf_r1cs, cf_W_i.E, cf_U_i.u.clone(), cf_z_U)?; - } - - // 1.1.a, 5.1. compute NIFS.V and KZG challenges. - // We need to ensure the order of challenge generation is the same as - // the native counterpart, so we first compute the challenges here and - // do the actual checks later. - let cmT = - NonNativeAffineVar::new_input(cs.clone(), || Ok(self.cmT.unwrap_or_else(C1::zero)))?; - // 1.1.a - let r_bits = ChallengeGadget::>::get_challenge_gadget( - &mut transcript, + type ProofDummyCfg = (); + type Proof = C; + type RandomnessDummyCfg = (); + type Randomness = CF1; + + fn fold_field_elements_gadget( + _arith: &R1CS>, + transcript: &mut PoseidonSpongeVar>, + pp_hash: FpVar>, + U: CommittedInstanceVar, + U_vec: Vec>>, + u: CommittedInstanceVar, + proof: C, + randomness: CF1, + ) -> Result, SynthesisError> { + let cs = transcript.cs(); + let cmT = NonNativeAffineVar::new_input(cs.clone(), || Ok(proof))?; + let r = FpVar::new_input(cs.clone(), || Ok(randomness))?; + 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)?; - - // 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(); + Boolean::le_bits_to_fp_var(&r_bits)?.enforce_equal(&r)?; - 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) + NIFSGadget::::fold_committed_instance(r, U, u) } - // 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)) + fn fold_group_elements_native( + U_commitments: &[C], + u_commitments: &[C], + cmT: Option, + r: Self::Randomness, + ) -> Result, Error> { + let cmT = cmT.ok_or(Error::Empty)?; + let U_cmW = U_commitments[0]; + let U_cmE = U_commitments[1]; + let u_cmW = u_commitments[0]; + let u_cmE = u_commitments[1]; + if !u_cmE.is_zero() { + return Err(Error::NotIncomingCommittedInstance); + } + let cmW = U_cmW + u_cmW.mul(r); + let cmE = U_cmE + cmT.mul(r); + Ok(vec![cmW, cmE]) } } #[cfg(test)] pub mod tests { - use std::cmp::max; - - use ark_crypto_primitives::crh::{ - sha256::{ - constraints::{Sha256Gadget, UnitVar}, - Sha256, - }, - CRHScheme, CRHSchemeGadget, - }; - use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; - use ark_r1cs_std::bits::uint8::UInt8; - use ark_relations::r1cs::ConstraintSystem; - use ark_std::{ - rand::{thread_rng, Rng}, - UniformRand, - }; + use ark_pallas::{constraints::GVar, Fr, Projective}; + use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem}; use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; use super::*; - use crate::arith::{ - r1cs::{ - extract_r1cs, extract_w_x, - tests::{get_test_r1cs, get_test_z}, - }, - Arith, - }; use crate::commitment::pedersen::Pedersen; use crate::folding::nova::PreprocessorParam; - use crate::frontend::utils::{CubicFCircuit, CustomFCircuit, WrapperCircuit}; + use crate::frontend::utils::CubicFCircuit; use crate::transcript::poseidon::poseidon_canonical_config; use crate::FoldingScheme; - // Convert `z` to a witness-instance pair for the relaxed R1CS - fn prepare_relaxed_witness_instance, R: Rng>( - mut rng: R, - r1cs: &R1CS, - z: &[C::ScalarField], - ) -> (Witness, CommittedInstance) { - let (w, x) = r1cs.split_z(z); - - let (cs_pp, _) = CS::setup(&mut rng, max(w.len(), r1cs.A.n_rows)).unwrap(); - - let mut w = Witness::new::(w, r1cs.A.n_rows, &mut rng); - w.E = r1cs.eval_at_z(z).unwrap(); - let mut u = w.commit::(&cs_pp, x).unwrap(); - u.u = z[0]; - - (w, u) - } - #[test] - fn test_relaxed_r1cs_small_gadget_handcrafted() { - let rng = &mut thread_rng(); - - let r1cs: R1CS = get_test_r1cs(); - - let mut z = get_test_z(3); - z[0] = Fr::rand(rng); // Randomize `z[0]` (i.e. `u.u`) to test the relaxed R1CS - let (w, u) = prepare_relaxed_witness_instance::<_, Pedersen, _>(rng, &r1cs, &z); - - let cs = ConstraintSystem::::new_ref(); - - let zVar = Vec::>::new_witness(cs.clone(), || Ok(z)).unwrap(); - let EVar = Vec::>::new_witness(cs.clone(), || Ok(w.E)).unwrap(); - let uVar = FpVar::::new_witness(cs.clone(), || Ok(u.u)).unwrap(); - let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); - - RelaxedR1CSGadget::check_native(r1csVar, EVar, uVar, zVar).unwrap(); - assert!(cs.is_satisfied().unwrap()); - } - - // gets as input a circuit that implements the ConstraintSynthesizer trait, and that has been - // initialized. - fn test_relaxed_r1cs_gadget>(circuit: CS) { - let rng = &mut thread_rng(); - - let cs = ConstraintSystem::::new_ref(); - - circuit.generate_constraints(cs.clone()).unwrap(); - cs.finalize(); - assert!(cs.is_satisfied().unwrap()); - - let cs = cs.into_inner().unwrap(); - - let r1cs = extract_r1cs::(&cs); - let (w, x) = extract_w_x::(&cs); - r1cs.check_relation(&w, &x).unwrap(); - - let z = [vec![Fr::rand(rng)], x, w].concat(); - let (w, u) = prepare_relaxed_witness_instance::<_, Pedersen, _>(rng, &r1cs, &z); - r1cs.check_relation(&w, &u).unwrap(); - - // set new CS for the circuit that checks the RelaxedR1CS of our original circuit - let cs = ConstraintSystem::::new_ref(); - // prepare the inputs for our circuit - let zVar = Vec::>::new_witness(cs.clone(), || Ok(z)).unwrap(); - let EVar = Vec::>::new_witness(cs.clone(), || Ok(w.E)).unwrap(); - let uVar = FpVar::::new_witness(cs.clone(), || Ok(u.u)).unwrap(); - let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); - - RelaxedR1CSGadget::check_native(r1csVar, EVar, uVar, zVar).unwrap(); - assert!(cs.is_satisfied().unwrap()); - } - - #[test] - fn test_relaxed_r1cs_small_gadget_arkworks() { - let z_i = vec![Fr::from(3_u32)]; - let cubic_circuit = CubicFCircuit::::new(()).unwrap(); - let circuit = WrapperCircuit::> { - FC: cubic_circuit, - z_i: Some(z_i.clone()), - z_i1: Some(cubic_circuit.step_native(0, z_i, vec![]).unwrap()), - }; - - test_relaxed_r1cs_gadget(circuit); - } - - struct Sha256TestCircuit { - _f: PhantomData, - pub x: Vec, - pub y: Vec, - } - impl ConstraintSynthesizer for Sha256TestCircuit { - fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { - let x = Vec::>::new_witness(cs.clone(), || Ok(self.x))?; - let y = Vec::>::new_input(cs.clone(), || Ok(self.y))?; - - let unitVar = UnitVar::default(); - let comp_y = as CRHSchemeGadget>::evaluate(&unitVar, &x)?; - comp_y.0.enforce_equal(&y)?; - Ok(()) - } - } - #[test] - fn test_relaxed_r1cs_medium_gadget_arkworks() { - let x = Fr::from(5_u32).into_bigint().to_bytes_le(); - let y = ::evaluate(&(), x.clone()).unwrap(); - - let circuit = Sha256TestCircuit:: { - _f: PhantomData, - x, - y, - }; - test_relaxed_r1cs_gadget(circuit); - } - - #[test] - fn test_relaxed_r1cs_custom_circuit() { - let n_constraints = 10_000; - let custom_circuit = CustomFCircuit::::new(n_constraints).unwrap(); - let z_i = vec![Fr::from(5_u32)]; - let circuit = WrapperCircuit::> { - FC: custom_circuit, - z_i: Some(z_i.clone()), - z_i1: Some(custom_circuit.step_native(0, z_i, vec![]).unwrap()), - }; - test_relaxed_r1cs_gadget(circuit); - } - - #[test] - fn test_relaxed_r1cs_nonnative_circuit() { - let rng = &mut thread_rng(); - - let cs = ConstraintSystem::::new_ref(); - // in practice we would use CycleFoldCircuit, but is a very big circuit (when computed - // non-natively inside the RelaxedR1CS circuit), so in order to have a short test we use a - // custom circuit. - let custom_circuit = CustomFCircuit::::new(10).unwrap(); - let z_i = vec![Fq::from(5_u32)]; - let circuit = WrapperCircuit::> { - FC: custom_circuit, - z_i: Some(z_i.clone()), - z_i1: Some(custom_circuit.step_native(0, z_i, vec![]).unwrap()), - }; - circuit.generate_constraints(cs.clone()).unwrap(); - cs.finalize(); - let cs = cs.into_inner().unwrap(); - let r1cs = extract_r1cs::(&cs); - let (w, x) = extract_w_x::(&cs); - - let z = [vec![Fq::rand(rng)], x, w].concat(); - let (w, u) = - prepare_relaxed_witness_instance::<_, Pedersen, _>(rng, &r1cs, &z); - - // natively - let cs = ConstraintSystem::::new_ref(); - let zVar = Vec::>::new_witness(cs.clone(), || Ok(z.clone())).unwrap(); - let EVar = Vec::>::new_witness(cs.clone(), || Ok(w.E.clone())).unwrap(); - let uVar = FpVar::::new_witness(cs.clone(), || Ok(u.u)).unwrap(); - let r1csVar = - R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs.clone())).unwrap(); - RelaxedR1CSGadget::check_native(r1csVar, EVar, uVar, zVar).unwrap(); - - // non-natively - let cs = ConstraintSystem::::new_ref(); - let zVar = Vec::new_witness(cs.clone(), || Ok(z)).unwrap(); - let EVar = Vec::new_witness(cs.clone(), || Ok(w.E)).unwrap(); - let uVar = NonNativeUintVar::::new_witness(cs.clone(), || Ok(u.u)).unwrap(); - let r1csVar = - R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); - RelaxedR1CSGadget::check_nonnative(r1csVar, EVar, uVar, zVar).unwrap(); - } - - #[test] - fn test_decider_eth_circuit() { + fn test_decider_circuit() { let mut rng = ark_std::test_rng(); let poseidon_config = poseidon_canonical_config::(); @@ -803,84 +272,14 @@ pub mod tests { let ivc_proof = nova.ivc_proof(); N::verify(nova_params.1, ivc_proof).unwrap(); - // load the DeciderEthCircuit from the Nova instance - let decider_eth_circuit = DeciderEthCircuit::< - Projective, - GVar, - Projective2, - GVar2, - Pedersen, - Pedersen, - >::from_nova(nova) - .unwrap(); + // 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/mod.rs b/folding-schemes/src/folding/nova/mod.rs index 5e701cd..ae358ee 100644 --- a/folding-schemes/src/folding/nova/mod.rs +++ b/folding-schemes/src/folding/nova/mod.rs @@ -10,7 +10,7 @@ use ark_crypto_primitives::sponge::{ }; use ark_ec::{CurveGroup, Group}; use ark_ff::{BigInteger, PrimeField}; -use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; +use ark_r1cs_std::{prelude::CurveVar, ToConstraintFieldGadget}; use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Valid}; use ark_std::fmt::Debug; @@ -56,7 +56,7 @@ pub mod decider_circuits; pub mod decider_eth; pub mod decider_eth_circuit; -use super::traits::{CommittedInstanceOps, WitnessOps}; +use super::traits::{CommittedInstanceOps, Inputize, WitnessOps}; /// Configuration for Nova's CycleFold circuit pub struct NovaCycleFoldConfig { @@ -137,6 +137,18 @@ impl CommittedInstanceOps for CommittedInstance { } } +impl Inputize> for CommittedInstance { + fn inputize(&self) -> Vec { + [ + &[self.u][..], + &self.x, + &self.cmE.inputize(), + &self.cmW.inputize(), + ] + .concat() + } +} + #[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] pub struct Witness { pub E: Vec, @@ -477,8 +489,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { type PreprocessorParam = PreprocessorParam; type ProverParam = ProverParams; @@ -518,7 +528,7 @@ where augmented_F_circuit.generate_constraints(cs.clone())?; cs.finalize(); let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs); + let r1cs = extract_r1cs::(&cs)?; // CycleFold circuit R1CS let cs2 = ConstraintSystem::::new_ref(); @@ -526,7 +536,7 @@ where cf_circuit.generate_constraints(cs2.clone())?; cs2.finalize(); let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let cf_r1cs = extract_r1cs::(&cs2); + let cf_r1cs = extract_r1cs::(&cs2)?; let cs_vp = CS1::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; let cf_cs_vp = CS2::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; @@ -548,23 +558,14 @@ where get_r1cs::(&prep_param.poseidon_config, prep_param.F.clone())?; // if cs params exist, use them, if not, generate new ones - let cs_pp: CS1::ProverParams; - let cs_vp: CS1::VerifierParams; - let cf_cs_pp: CS2::ProverParams; - let cf_cs_vp: CS2::VerifierParams; - if prep_param.cs_pp.is_some() - && prep_param.cf_cs_pp.is_some() - && prep_param.cs_vp.is_some() - && prep_param.cf_cs_vp.is_some() - { - cs_pp = prep_param.clone().cs_pp.unwrap(); - cs_vp = prep_param.clone().cs_vp.unwrap(); - cf_cs_pp = prep_param.clone().cf_cs_pp.unwrap(); - cf_cs_vp = prep_param.clone().cf_cs_vp.unwrap(); - } else { - (cs_pp, cs_vp) = CS1::setup(&mut rng, r1cs.A.n_rows)?; - (cf_cs_pp, cf_cs_vp) = CS2::setup(&mut rng, cf_r1cs.A.n_rows)?; - } + let (cs_pp, cs_vp) = match (&prep_param.cs_pp, &prep_param.cs_vp) { + (Some(cs_pp), Some(cs_vp)) => (cs_pp.clone(), cs_vp.clone()), + _ => CS1::setup(&mut rng, r1cs.A.n_rows)?, + }; + let (cf_cs_pp, cf_cs_vp) = match (&prep_param.cf_cs_pp, &prep_param.cf_cs_vp) { + (Some(cf_cs_pp), Some(cf_cs_vp)) => (cf_cs_pp.clone(), cf_cs_vp.clone()), + _ => CS2::setup(&mut rng, cf_r1cs.A.n_rows)?, + }; let prover_params = ProverParams:: { poseidon_config: prep_param.poseidon_config.clone(), @@ -601,12 +602,12 @@ where augmented_F_circuit.generate_constraints(cs.clone())?; cs.finalize(); let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs); + let r1cs = extract_r1cs::(&cs)?; cf_circuit.generate_constraints(cs2.clone())?; cs2.finalize(); let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let cf_r1cs = extract_r1cs::(&cs2); + let cf_r1cs = extract_r1cs::(&cs2)?; // compute the public params hash let pp_hash = vp.pp_hash()?; @@ -946,7 +947,7 @@ where } = ivc_proof; let (pp, vp) = params; - let f_circuit = FC::new(fcircuit_params).unwrap(); + let f_circuit = FC::new(fcircuit_params)?; let cs = ConstraintSystem::::new_ref(); let cs2 = ConstraintSystem::::new_ref(); let augmented_F_circuit = @@ -956,12 +957,12 @@ where augmented_F_circuit.generate_constraints(cs.clone())?; cs.finalize(); let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs); + let r1cs = extract_r1cs::(&cs)?; cf_circuit.generate_constraints(cs2.clone())?; cs2.finalize(); let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let cf_r1cs = extract_r1cs::(&cs2); + let cf_r1cs = extract_r1cs::(&cs2)?; Ok(Self { _gc1: PhantomData, @@ -1056,8 +1057,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { // folds the given cyclefold circuit and its instances #[allow(clippy::type_complexity)] @@ -1102,7 +1101,7 @@ pub fn get_r1cs_from_cs( circuit.generate_constraints(cs.clone())?; cs.finalize(); let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs); + let r1cs = extract_r1cs::(&cs)?; Ok(r1cs) } @@ -1123,8 +1122,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { let augmented_F_circuit = AugmentedFCircuit::::empty(poseidon_config, F_circuit); @@ -1151,8 +1148,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { let (r1cs, cf_r1cs) = get_r1cs::(poseidon_config, F_circuit)?; Ok((r1cs.A.n_rows, cf_r1cs.A.n_rows)) diff --git a/folding-schemes/src/folding/nova/nifs/nova.rs b/folding-schemes/src/folding/nova/nifs/nova.rs index 0323b50..95aaef8 100644 --- a/folding-schemes/src/folding/nova/nifs/nova.rs +++ b/folding-schemes/src/folding/nova/nifs/nova.rs @@ -35,8 +35,6 @@ impl, T: Transcript, c NIFSTrait for NIFS where ::ScalarField: Absorb, - ::BaseField: PrimeField, - ::ScalarField: PrimeField, { type CommittedInstance = CommittedInstance; type Witness = Witness; @@ -150,7 +148,6 @@ impl, T: Transcript, c NIFS where ::ScalarField: Absorb, - ::BaseField: PrimeField, { /// compute_T: compute cross-terms T pub fn compute_T( diff --git a/folding-schemes/src/folding/nova/traits.rs b/folding-schemes/src/folding/nova/traits.rs index 8bf336a..1df5339 100644 --- a/folding-schemes/src/folding/nova/traits.rs +++ b/folding-schemes/src/folding/nova/traits.rs @@ -1,11 +1,18 @@ use ark_ec::CurveGroup; +use ark_r1cs_std::fields::fp::FpVar; +use ark_relations::r1cs::SynthesisError; use ark_std::{rand::RngCore, UniformRand}; +use super::circuits::CommittedInstanceVar; +use super::decider_eth_circuit::WitnessVar; use super::{CommittedInstance, Witness}; -use crate::arith::ArithSampler; -use crate::arith::{r1cs::R1CS, Arith}; +use crate::arith::{ + r1cs::{circuits::R1CSMatricesVar, R1CS}, + Arith, ArithGadget, ArithSampler, +}; use crate::commitment::CommitmentScheme; use crate::folding::circuits::CF1; +use crate::utils::gadgets::{EquivalenceGadget, VectorGadget}; use crate::Error; /// Implements `Arith` for R1CS, where the witness is of type [`Witness`], and @@ -95,3 +102,25 @@ impl ArithSampler, CommittedInstance> for R1CS ArithGadget, CommittedInstanceVar> + for R1CSMatricesVar> +{ + type Evaluation = (Vec>, Vec>); + + fn eval_relation( + &self, + w: &WitnessVar, + u: &CommittedInstanceVar, + ) -> Result { + self.eval_at_z(&[&[u.u.clone()][..], &u.x, &w.W].concat()) + } + + fn enforce_evaluation( + w: &WitnessVar, + _u: &CommittedInstanceVar, + (AzBz, uCz): Self::Evaluation, + ) -> Result<(), SynthesisError> { + EquivalenceGadget::::enforce_equivalent(&AzBz[..], &uCz.add(&w.E)?[..]) + } +} diff --git a/folding-schemes/src/folding/nova/zk.rs b/folding-schemes/src/folding/nova/zk.rs index 9160c71..206d302 100644 --- a/folding-schemes/src/folding/nova/zk.rs +++ b/folding-schemes/src/folding/nova/zk.rs @@ -43,10 +43,7 @@ use ark_crypto_primitives::sponge::{ Absorb, CryptographicSponge, }; use ark_ec::{CurveGroup, Group}; -use ark_r1cs_std::{ - groups::{CurveVar, GroupOpsBounds}, - ToConstraintFieldGadget, -}; +use ark_r1cs_std::{groups::CurveVar, ToConstraintFieldGadget}; use crate::{commitment::CommitmentScheme, folding::circuits::CF2, frontend::FCircuit, Error}; @@ -90,7 +87,6 @@ where ::ScalarField: PrimeField, ::BaseField: PrimeField, ::BaseField: Absorb, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, GC2: ToConstraintFieldGadget<::BaseField>, C1: CurveGroup, { @@ -161,7 +157,6 @@ where ::ScalarField: Absorb, ::BaseField: PrimeField, ::BaseField: Absorb, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, GC2: ToConstraintFieldGadget<::BaseField>, C1: CurveGroup, { diff --git a/folding-schemes/src/folding/protogalaxy/circuits.rs b/folding-schemes/src/folding/protogalaxy/circuits.rs index 4c30886..5eadd98 100644 --- a/folding-schemes/src/folding/protogalaxy/circuits.rs +++ b/folding-schemes/src/folding/protogalaxy/circuits.rs @@ -11,7 +11,7 @@ use ark_r1cs_std::{ boolean::Boolean, eq::EqGadget, fields::{fp::FpVar, FieldVar}, - groups::{CurveVar, GroupOpsBounds}, + groups::CurveVar, poly::polynomial::univariate::dense::DensePolynomialVar, R1CSVar, ToBitsGadget, ToConstraintFieldGadget, }; @@ -77,7 +77,8 @@ impl FoldingGadget { let betas_star = betas_star_var(&instance.betas, &deltas, &alpha); let k = vec_instances.len(); - let H = GeneralEvaluationDomain::new(k + 1).unwrap(); + let H = + GeneralEvaluationDomain::new(k + 1).ok_or(SynthesisError::PolynomialDegreeTooLarge)?; let L_X = lagrange_polys(H) .into_iter() .map(|poly| { @@ -183,7 +184,6 @@ impl AugmentationGadget { ) -> Result, SynthesisError> where C2::BaseField: PrimeField + Absorb, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { assert_eq!(cf_u_cmWs.len(), cf_u_xs.len()); assert_eq!(cf_u_xs.len(), cf_cmTs.len()); @@ -268,8 +268,6 @@ pub struct AugmentedFCircuit< impl>, FC: FCircuit>> AugmentedFCircuit -where - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { pub fn empty( poseidon_config: &PoseidonConfig>, @@ -317,7 +315,6 @@ where GC2: CurveVar> + ToConstraintFieldGadget>, FC: FCircuit>, C2::BaseField: PrimeField + Absorb, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { let pp_hash = FpVar::>::new_witness(cs.clone(), || Ok(self.pp_hash))?; @@ -498,7 +495,7 @@ mod tests { let mut transcript_p = PoseidonSponge::new(&poseidon_config); let mut transcript_v = PoseidonSponge::new(&poseidon_config); - let (_, _, F_coeffs, K_coeffs, _, _) = Folding::::prove( + let (_, _, proof, _) = Folding::::prove( &mut transcript_p, &r1cs, &instance, @@ -507,20 +504,15 @@ mod tests { &witnesses, )?; - let folded_instance = Folding::::verify( - &mut transcript_v, - &instance, - &instances, - F_coeffs.clone(), - K_coeffs.clone(), - )?; + let folded_instance = + Folding::::verify(&mut transcript_v, &instance, &instances, proof.clone())?; let cs = ConstraintSystem::new_ref(); let mut transcript_var = PoseidonSpongeVar::new(cs.clone(), &poseidon_config); let instance_var = CommittedInstanceVar::new_witness(cs.clone(), || Ok(instance))?; let instances_var = Vec::new_witness(cs.clone(), || Ok(instances))?; - let F_coeffs_var = Vec::new_witness(cs.clone(), || Ok(F_coeffs))?; - let K_coeffs_var = Vec::new_witness(cs.clone(), || Ok(K_coeffs))?; + let F_coeffs_var = Vec::new_witness(cs.clone(), || Ok(proof.F_coeffs))?; + let K_coeffs_var = Vec::new_witness(cs.clone(), || Ok(proof.K_coeffs))?; let (folded_instance_var, _) = FoldingGadget::fold_committed_instance( &mut transcript_var, 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 0000000..57d6965 --- /dev/null +++ b/folding-schemes/src/folding/protogalaxy/decider_eth_circuit.rs @@ -0,0 +1,252 @@ +/// 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}, + eq::EqGadget, + 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, aux) = 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, + randomness: aux.L_X_evals, + 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); + type Randomness = Vec>; + type RandomnessDummyCfg = usize; + + fn fold_field_elements_gadget( + _arith: &R1CS>, + transcript: &mut PoseidonSpongeVar>, + _pp_hash: FpVar>, + U: CommittedInstanceVar, + _U_vec: Vec>>, + u: CommittedInstanceVar, + proof: Self::Proof, + randomness: Self::Randomness, + ) -> 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[..]))?; + let randomness = Vec::new_input(cs.clone(), || Ok(randomness))?; + + let (U_next, L_X_evals) = + FoldingGadget::fold_committed_instance(transcript, &U, &[u], F_coeffs, K_coeffs)?; + L_X_evals.enforce_equal(&randomness)?; + + Ok(U_next) + } + + fn fold_group_elements_native( + U_commitments: &[C], + u_commitments: &[C], + _: Option, + L_X_evals: Self::Randomness, + ) -> Result, Error> { + let U_phi = U_commitments[0]; + let u_phi = u_commitments[0]; + Ok(vec![U_phi * L_X_evals[0] + u_phi * L_X_evals[1]]) + } +} + +#[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 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_proof = protogalaxy.ivc_proof(); + PG::verify(pg_params.1, ivc_proof).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/folding.rs b/folding-schemes/src/folding/protogalaxy/folding.rs index 3aefee4..1e09835 100644 --- a/folding-schemes/src/folding/protogalaxy/folding.rs +++ b/folding-schemes/src/folding/protogalaxy/folding.rs @@ -15,10 +15,32 @@ use super::ProtoGalaxyError; use super::{CommittedInstance, Witness}; use crate::arith::r1cs::R1CS; +use crate::folding::traits::Dummy; use crate::transcript::Transcript; use crate::utils::vec::*; use crate::Error; +#[derive(Debug, Clone)] +pub struct ProtoGalaxyProof { + pub F_coeffs: Vec, + pub K_coeffs: Vec, +} + +impl Dummy<(usize, usize, usize)> for ProtoGalaxyProof { + fn dummy((t, d, k): (usize, usize, usize)) -> Self { + Self { + F_coeffs: vec![F::zero(); t], + K_coeffs: vec![F::zero(); d * k + 1], + } + } +} + +#[derive(Debug, Clone)] +pub struct ProtoGalaxyAux { + pub L_X_evals: Vec, + pub phi_stars: Vec, +} + #[derive(Clone, Debug)] /// Implements the protocol described in section 4 of /// [ProtoGalaxy](https://eprint.iacr.org/2023/1106.pdf) @@ -28,7 +50,6 @@ pub struct Folding { impl Folding where ::ScalarField: Absorb, - ::BaseField: Absorb, { #![allow(clippy::type_complexity)] /// implements the non-interactive Prover from the folding scheme described in section 4 @@ -45,10 +66,8 @@ where ( CommittedInstance, Witness, - Vec, // F_X coeffs - Vec, // K_X coeffs - Vec, // L_X evals - Vec, // phi_stars + ProtoGalaxyProof, + ProtoGalaxyAux, ), Error, > { @@ -243,10 +262,11 @@ where w: w_star, r_w: r_w_star, }, - F_coeffs, - K_coeffs, - L_X_evals, - phi_stars, + ProtoGalaxyProof { F_coeffs, K_coeffs }, + ProtoGalaxyAux { + L_X_evals, + phi_stars, + }, )) } @@ -258,8 +278,7 @@ where // incoming instances vec_instances: &[CommittedInstance], // polys from P - F_coeffs: Vec, - K_coeffs: Vec, + proof: ProtoGalaxyProof, ) -> Result, Error> { let t = instance.betas.len(); @@ -270,20 +289,20 @@ where let delta = transcript.get_challenge(); let deltas = exponential_powers(delta, t); - transcript.absorb(&F_coeffs); + transcript.absorb(&proof.F_coeffs); let alpha = transcript.get_challenge(); let alphas = all_powers(alpha, t); // F(alpha) = e + \sum_t F_i * alpha^i let mut F_alpha = instance.e; - for (i, F_i) in F_coeffs.iter().skip(1).enumerate() { + for (i, F_i) in proof.F_coeffs.iter().skip(1).enumerate() { F_alpha += *F_i * alphas[i + 1]; } let betas_star = betas_star(&instance.betas, &deltas, alpha); - transcript.absorb(&K_coeffs); + transcript.absorb(&proof.K_coeffs); let k = vec_instances.len(); let H = @@ -291,7 +310,7 @@ where let L_X: Vec> = lagrange_polys(H); let Z_X: DensePolynomial = H.vanishing_polynomial().into(); let K_X: DensePolynomial = - DensePolynomial::::from_coefficients_vec(K_coeffs); + DensePolynomial::::from_coefficients_vec(proof.K_coeffs); let gamma = transcript.get_challenge(); @@ -483,27 +502,20 @@ pub mod tests { let mut transcript_p = PoseidonSponge::::new(&poseidon_config); let mut transcript_v = PoseidonSponge::::new(&poseidon_config); - let (folded_instance, folded_witness, F_coeffs, K_coeffs, _, _) = - Folding::::prove( - &mut transcript_p, - &r1cs, - &instance, - &witness, - &instances, - &witnesses, - ) - .unwrap(); - - // verifier - let folded_instance_v = Folding::::verify( - &mut transcript_v, + let (folded_instance, folded_witness, proof, _) = Folding::::prove( + &mut transcript_p, + &r1cs, &instance, + &witness, &instances, - F_coeffs, - K_coeffs, + &witnesses, ) .unwrap(); + // verifier + let folded_instance_v = + Folding::::verify(&mut transcript_v, &instance, &instances, proof).unwrap(); + // check that prover & verifier folded instances are the same values assert_eq!(folded_instance.phi, folded_instance_v.phi); assert_eq!(folded_instance.betas, folded_instance_v.betas); @@ -533,24 +545,22 @@ pub mod tests { // generate the instances to be fold let (_, _, witnesses, instances) = prepare_inputs(k); - let (folded_instance, folded_witness, F_coeffs, K_coeffs, _, _) = - Folding::::prove( - &mut transcript_p, - &r1cs, - &running_instance, - &running_witness, - &instances, - &witnesses, - ) - .unwrap(); + let (folded_instance, folded_witness, proof, _) = Folding::::prove( + &mut transcript_p, + &r1cs, + &running_instance, + &running_witness, + &instances, + &witnesses, + ) + .unwrap(); // verifier let folded_instance_v = Folding::::verify( &mut transcript_v, &running_instance, &instances, - F_coeffs, - K_coeffs, + proof, ) .unwrap(); diff --git a/folding-schemes/src/folding/protogalaxy/mod.rs b/folding-schemes/src/folding/protogalaxy/mod.rs index d17655d..92d0193 100644 --- a/folding-schemes/src/folding/protogalaxy/mod.rs +++ b/folding-schemes/src/folding/protogalaxy/mod.rs @@ -9,7 +9,7 @@ use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, eq::EqGadget, fields::{fp::FpVar, FieldVar}, - groups::{CurveVar, GroupOpsBounds}, + groups::CurveVar, R1CSVar, ToConstraintFieldGadget, }; use ark_relations::r1cs::{ @@ -44,6 +44,7 @@ use crate::{ pub mod circuits; pub mod constants; +pub mod decider_eth_circuit; pub mod folding; pub mod traits; pub(crate) mod utils; @@ -52,7 +53,7 @@ use circuits::AugmentedFCircuit; use folding::Folding; use super::traits::{ - CommittedInstanceOps, CommittedInstanceVarOps, Dummy, WitnessOps, WitnessVarOps, + CommittedInstanceOps, CommittedInstanceVarOps, Dummy, Inputize, WitnessOps, WitnessVarOps, }; /// Configuration for ProtoGalaxy's CycleFold circuit @@ -121,6 +122,14 @@ impl CommittedInstanceOps for CommittedInsta } } +impl Inputize> + for CommittedInstance +{ + fn inputize(&self) -> Vec { + [&self.phi.inputize(), &self.betas, &[self.e][..], &self.x].concat() + } +} + #[derive(Clone, Debug)] pub struct CommittedInstanceVar { phi: NonNativeAffineVar, @@ -534,8 +543,6 @@ where C1::ScalarField: Absorb, C2::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { /// This method computes the parameter `t` in ProtoGalaxy for folding `F'`, /// the augmented circuit of `F` @@ -544,7 +551,7 @@ where F: &FC, d: usize, k: usize, - ) -> Result { + ) -> Result { // In ProtoGalaxy, prover and verifier are parameterized by `t = log(n)` // where `n` is the number of constraints in the circuit (known as the // mapping `f` in the paper). @@ -581,7 +588,7 @@ where // Create a dummy circuit with the same state length and external inputs // length as `F`, which replaces `F` in the augmented circuit `F'`. let dummy_circuit: DummyCircuit = - FCircuit::::new((state_len, external_inputs_len)).unwrap(); + FCircuit::::new((state_len, external_inputs_len))?; // Compute `augmentation_constraints`, the size of `F'` without `F`. let cs = ConstraintSystem::::new_ref(); @@ -640,8 +647,6 @@ where C1::ScalarField: Absorb, C2::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { type PreprocessorParam = (PoseidonConfig>, FC); type ProverParam = ProverParams; @@ -692,7 +697,7 @@ where augmented_F_circuit.generate_constraints(cs.clone())?; cs.finalize(); let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs); + let r1cs = extract_r1cs::(&cs)?; // CycleFold circuit R1CS let cs2 = ConstraintSystem::::new_ref(); @@ -700,7 +705,7 @@ where cf_circuit.generate_constraints(cs2.clone())?; cs2.finalize(); let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let cf_r1cs = extract_r1cs::(&cs2); + let cf_r1cs = extract_r1cs::(&cs2)?; let cs_vp = CS1::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; let cf_cs_vp = CS2::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?; @@ -740,12 +745,12 @@ where augmented_F_circuit.generate_constraints(cs.clone())?; cs.finalize(); let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let r1cs = extract_r1cs::(&cs); + let r1cs = extract_r1cs::(&cs)?; cf_circuit.generate_constraints(cs2.clone())?; cs2.finalize(); let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - let cf_r1cs = extract_r1cs::(&cs2); + let cf_r1cs = extract_r1cs::(&cs2)?; let (cs_pp, cs_vp) = CS1::setup(&mut rng, r1cs.A.n_rows)?; let (cf_cs_pp, cf_cs_vp) = CS2::setup(&mut rng, max(cf_r1cs.A.n_rows, cf_r1cs.A.n_cols))?; @@ -898,7 +903,7 @@ where } else { // Primary part: // Compute `U_{i+1}` by folding `u_i` into `U_i`. - let (U_i1, W_i1, F_coeffs, K_coeffs, L_evals, phi_stars) = Folding::prove( + let (U_i1, W_i1, proof, aux) = Folding::prove( &mut transcript_prover, &self.r1cs, &self.U_i, @@ -909,8 +914,8 @@ where // CycleFold part: // get the vector used as public inputs 'x' in the CycleFold circuit - let mut r0_bits = L_evals[0].into_bigint().to_bits_le(); - let mut r1_bits = L_evals[1].into_bigint().to_bits_le(); + let mut r0_bits = aux.L_X_evals[0].into_bigint().to_bits_le(); + let mut r1_bits = aux.L_X_evals[1].into_bigint().to_bits_le(); r0_bits.resize(C1::ScalarField::MODULUS_BIT_SIZE as usize, false); r1_bits.resize(C1::ScalarField::MODULUS_BIT_SIZE as usize, false); @@ -919,13 +924,12 @@ where let cf1_u_i_x = [ r0_bits .chunks(C1::BaseField::MODULUS_BIT_SIZE as usize - 1) - .map(BigInteger::from_bits_le) - .map(C1::BaseField::from_bigint) - .collect::>>() - .unwrap(), + .map(::BigInt::from_bits_le) + .map(C1::BaseField::from) + .collect::>(), get_cm_coordinates(&C1::zero()), get_cm_coordinates(&self.U_i.phi), - get_cm_coordinates(&phi_stars[0]), + get_cm_coordinates(&aux.phi_stars[0]), ] .concat(); let cf1_circuit = ProtoGalaxyCycleFoldCircuit:: { @@ -941,11 +945,10 @@ where let cf2_u_i_x = [ r1_bits .chunks(C1::BaseField::MODULUS_BIT_SIZE as usize - 1) - .map(BigInteger::from_bits_le) - .map(C1::BaseField::from_bigint) - .collect::>>() - .unwrap(), - get_cm_coordinates(&phi_stars[0]), + .map(::BigInt::from_bits_le) + .map(C1::BaseField::from) + .collect::>(), + get_cm_coordinates(&aux.phi_stars[0]), get_cm_coordinates(&self.u_i.phi), get_cm_coordinates(&U_i1.phi), ] @@ -953,7 +956,7 @@ where let cf2_circuit = ProtoGalaxyCycleFoldCircuit:: { _gc: PhantomData, r_bits: Some(r1_bits), - points: Some(vec![phi_stars[0], self.u_i.phi]), + points: Some(vec![aux.phi_stars[0], self.u_i.phi]), x: Some(cf2_u_i_x.clone()), }; @@ -998,9 +1001,9 @@ where u_i_phi: self.u_i.phi, U_i: self.U_i.clone(), U_i1_phi: U_i1.phi, - F_coeffs: F_coeffs.clone(), - K_coeffs: K_coeffs.clone(), - phi_stars, + F_coeffs: proof.F_coeffs.clone(), + K_coeffs: proof.K_coeffs.clone(), + phi_stars: aux.phi_stars, F: self.F.clone(), x: Some(u_i1_x), // cyclefold values @@ -1020,8 +1023,7 @@ where &mut transcript_verifier, &self.U_i, &[self.u_i.clone()], - F_coeffs, - K_coeffs + proof )?, U_i1 ); @@ -1108,7 +1110,7 @@ where } = ivc_proof; let (pp, vp) = params; - let f_circuit = FC::new(fcircuit_params).unwrap(); + let f_circuit = FC::new(fcircuit_params)?; Ok(Self { _gc1: PhantomData, @@ -1195,8 +1197,6 @@ where ::ScalarField: Absorb, ::ScalarField: Absorb, C1: CurveGroup, - for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, - for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { // folds the given cyclefold circuit and its instances #[allow(clippy::type_complexity)] diff --git a/folding-schemes/src/folding/protogalaxy/traits.rs b/folding-schemes/src/folding/protogalaxy/traits.rs index 51b2aa1..a5b6daf 100644 --- a/folding-schemes/src/folding/protogalaxy/traits.rs +++ b/folding-schemes/src/folding/protogalaxy/traits.rs @@ -1,14 +1,26 @@ use ark_crypto_primitives::sponge::{constraints::AbsorbGadget, Absorb}; use ark_ec::CurveGroup; use ark_ff::PrimeField; -use ark_r1cs_std::{fields::fp::FpVar, uint8::UInt8, ToConstraintFieldGadget}; +use ark_r1cs_std::{ + eq::EqGadget, + fields::{fp::FpVar, FieldVar}, + uint8::UInt8, + ToConstraintFieldGadget, +}; use ark_relations::r1cs::SynthesisError; use ark_std::{cfg_into_iter, log2, One}; use rayon::prelude::*; -use super::{constants::RUNNING, utils::pow_i, CommittedInstance, CommittedInstanceVar, Witness}; +use super::{ + constants::RUNNING, + utils::{pow_i, pow_i_var}, + CommittedInstance, CommittedInstanceVar, Witness, WitnessVar, +}; use crate::{ - arith::{r1cs::R1CS, Arith}, + arith::{ + r1cs::{circuits::R1CSMatricesVar, R1CS}, + Arith, ArithGadget, + }, folding::circuits::CF1, transcript::AbsorbNonNative, utils::vec::is_zero_vec, @@ -99,6 +111,37 @@ impl Arith>, CommittedInstance ArithGadget>, CommittedInstanceVar> + for R1CSMatricesVar, FpVar>> +{ + type Evaluation = (Vec>>, Vec>>); + + fn eval_relation( + &self, + w: &WitnessVar>, + u: &CommittedInstanceVar, + ) -> Result { + self.eval_at_z(&[&[FpVar::one()][..], &u.x, &w.W].concat()) + } + + fn enforce_evaluation( + _w: &WitnessVar, + u: &CommittedInstanceVar, + (AzBz, uCz): Self::Evaluation, + ) -> Result<(), SynthesisError> { + let mut e = vec![]; + for (i, (l, r)) in AzBz.iter().zip(uCz).enumerate() { + e.push(pow_i_var(i, &u.betas) * (l - r)); + } + // Call `sum` on a vector instead of computing the sum in the above loop + // to avoid stack overflow (the cause of this is similar to issue #80 + // https://github.com/privacy-scaling-explorations/sonobe/issues/80) + e.iter().sum::>().enforce_equal(&u.e) + } +} + #[cfg(test)] pub mod tests { use super::*; diff --git a/folding-schemes/src/folding/traits.rs b/folding-schemes/src/folding/traits.rs index 3890554..d451347 100644 --- a/folding-schemes/src/folding/traits.rs +++ b/folding-schemes/src/folding/traits.rs @@ -12,7 +12,7 @@ use crate::{transcript::Transcript, Error}; use super::circuits::CF1; -pub trait CommittedInstanceOps { +pub trait CommittedInstanceOps: Inputize, Self::Var> { /// The in-circuit representation of the committed instance. type Var: AllocVar> + CommittedInstanceVarOps; /// `hash` implements the committed instance hash compatible with the @@ -85,7 +85,11 @@ pub trait CommittedInstanceVarOps { sponge.absorb(&z_0)?; sponge.absorb(&z_i)?; sponge.absorb(&U_vec)?; - Ok((sponge.squeeze_field_elements(1)?.pop().unwrap(), U_vec)) + Ok(( + // `unwrap` is safe because the sponge is guaranteed to return a single element + sponge.squeeze_field_elements(1)?.pop().unwrap(), + U_vec, + )) } /// Returns the commitments contained in the committed instance. @@ -129,3 +133,16 @@ impl Dummy for Vec { vec![Default::default(); cfg] } } + +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. +pub trait Inputize { + fn inputize(&self) -> Vec; +} diff --git a/folding-schemes/src/lib.rs b/folding-schemes/src/lib.rs index 419d1ba..a829fe6 100644 --- a/folding-schemes/src/lib.rs +++ b/folding-schemes/src/lib.rs @@ -38,6 +38,8 @@ pub enum Error { // Relation errors #[error("Relation not satisfied")] NotSatisfied, + #[error("SNARK setup failed: {0}")] + SNARKSetupFail(String), #[error("SNARK verification failed")] SNARKVerificationFail, #[error("IVC verification failed")] @@ -102,8 +104,8 @@ pub enum Error { MaxStep, #[error("Witness calculation error: {0}")] WitnessCalculationError(String), - #[error("BigInt to PrimeField conversion error: {0}")] - BigIntConversionError(String), + #[error("Failed to convert {0} into {1}: {2}")] + ConversionError(String, String, String), #[error("Failed to serde: {0}")] JSONSerdeError(String), #[error("Multi instances folding not supported in this scheme")] diff --git a/folding-schemes/src/utils/gadgets.rs b/folding-schemes/src/utils/gadgets.rs index 16472da..e18a9e9 100644 --- a/folding-schemes/src/utils/gadgets.rs +++ b/folding-schemes/src/utils/gadgets.rs @@ -1,14 +1,36 @@ use ark_ff::PrimeField; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, + eq::EqGadget, fields::{fp::FpVar, FieldVar}, R1CSVar, }; use ark_relations::r1cs::{Namespace, SynthesisError}; -use core::{borrow::Borrow, marker::PhantomData}; +use core::borrow::Borrow; use crate::utils::vec::SparseMatrix; +/// `EquivalenceGadget` enforces that two in-circuit variables are equivalent, +/// where the equivalence relation is parameterized by `M`: +/// - For `FpVar`, it is simply a equality relation, and `M` is unused. +/// - For `NonNativeUintVar`, we consider equivalence as a congruence relation, +/// in terms of modular arithmetic, so `M` specifies the modulus. +pub trait EquivalenceGadget { + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError>; +} +impl EquivalenceGadget for FpVar { + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + self.enforce_equal(other) + } +} +impl> EquivalenceGadget for [T] { + fn enforce_equivalent(&self, other: &Self) -> Result<(), SynthesisError> { + self.iter() + .zip(other) + .try_for_each(|(a, b)| a.enforce_equivalent(b)) + } +} + pub trait MatrixGadget { fn mul_vector(&self, v: &[FV]) -> Result, SynthesisError>; } @@ -42,17 +64,14 @@ impl VectorGadget> for [FpVar] { } #[derive(Debug, Clone)] -pub struct SparseMatrixVar> { - _f: PhantomData, - _cf: PhantomData, - _fv: PhantomData, +pub struct SparseMatrixVar { pub n_rows: usize, pub n_cols: usize, // same format as the native SparseMatrix (which follows ark_relations::r1cs::Matrix format pub coeffs: Vec>, } -impl AllocVar, CF> for SparseMatrixVar +impl AllocVar, CF> for SparseMatrixVar where F: PrimeField, CF: PrimeField, @@ -77,9 +96,6 @@ where } Ok(Self { - _f: PhantomData, - _cf: PhantomData, - _fv: PhantomData, n_rows: val.borrow().n_rows, n_cols: val.borrow().n_cols, coeffs, @@ -88,7 +104,7 @@ where } } -impl MatrixGadget> for SparseMatrixVar> { +impl MatrixGadget> for SparseMatrixVar> { fn mul_vector(&self, v: &[FpVar]) -> Result>, SynthesisError> { Ok(self .coeffs diff --git a/frontends/src/circom/utils.rs b/frontends/src/circom/utils.rs index dbe8605..0050be5 100644 --- a/frontends/src/circom/utils.rs +++ b/frontends/src/circom/utils.rs @@ -108,11 +108,19 @@ impl CircomWrapper { // Converts a num_bigint::BigInt to a PrimeField::BigInt. pub fn num_bigint_to_ark_bigint(&self, value: &BigInt) -> Result { - let big_uint = value - .to_biguint() - .ok_or_else(|| Error::BigIntConversionError("BigInt is negative".to_string()))?; + let big_uint = value.to_biguint().ok_or_else(|| { + Error::ConversionError( + "BigInt".into(), + "BigUint".into(), + "BigInt is negative".into(), + ) + })?; F::BigInt::try_from(big_uint).map_err(|_| { - Error::BigIntConversionError("Failed to convert to PrimeField::BigInt".to_string()) + Error::ConversionError( + "BigUint".into(), + "PrimeField::BigInt".into(), + "BigUint is too large to fit into PrimeField::BigInt".into(), + ) }) } diff --git a/frontends/src/noir/mod.rs b/frontends/src/noir/mod.rs index 22d9577..a423b60 100644 --- a/frontends/src/noir/mod.rs +++ b/frontends/src/noir/mod.rs @@ -16,6 +16,7 @@ use ark_relations::r1cs::{ConstraintSystemRef, SynthesisError}; use folding_schemes::{frontend::FCircuit, utils::PathOrBin, Error}; use noir_arkworks_backend::{ read_program_from_binary, read_program_from_file, sonobe_bridge::AcirCircuitSonobe, + FilesystemError, }; #[derive(Clone, Debug)] @@ -216,10 +217,12 @@ impl FCircuit for NoirFCircuit { } } -pub fn load_noir_circuit(path: String) -> Circuit> { - let program: Program> = read_program_from_file(path).unwrap(); +pub fn load_noir_circuit( + path: String, +) -> Result>, FilesystemError> { + let program: Program> = read_program_from_file(path)?; let circuit: Circuit> = program.functions[0].clone(); - circuit + Ok(circuit) } #[cfg(test)] @@ -241,7 +244,7 @@ mod tests { "{}/src/noir/test_folder/test_circuit/target/test_circuit.json", cur_path.to_str().unwrap() ); - let circuit = load_noir_circuit(circuit_path); + let circuit = load_noir_circuit(circuit_path).unwrap(); let noirfcircuit = NoirFCircuit { circuit, state_len: 2, @@ -261,7 +264,7 @@ mod tests { "{}/src/noir/test_folder/test_circuit/target/test_circuit.json", cur_path.to_str().unwrap() ); - let circuit = load_noir_circuit(circuit_path); + let circuit = load_noir_circuit(circuit_path).unwrap(); let noirfcircuit = NoirFCircuit { circuit, state_len: 2, @@ -285,7 +288,7 @@ mod tests { "{}/src/noir/test_folder/test_no_external_inputs/target/test_no_external_inputs.json", cur_path.to_str().unwrap() ); - let circuit = load_noir_circuit(circuit_path); + let circuit = load_noir_circuit(circuit_path).unwrap(); let noirfcircuit = NoirFCircuit { circuit, state_len: 2, diff --git a/solidity-verifiers/src/utils/mod.rs b/solidity-verifiers/src/utils/mod.rs index d04493f..deb4c7e 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[2],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 86e7098..6df5265 100644 --- a/solidity-verifiers/src/verifiers/nova_cyclefold.rs +++ b/solidity-verifiers/src/verifiers/nova_cyclefold.rs @@ -153,9 +153,12 @@ mod tests { use folding_schemes::{ commitment::{kzg::KZG, pedersen::Pedersen}, - folding::nova::{ - decider_eth::{prepare_calldata, Decider as DeciderEth}, - Nova, PreprocessorParam, + folding::{ + nova::{ + decider_eth::{prepare_calldata, Decider as DeciderEth}, + Nova, PreprocessorParam, + }, + traits::CommittedInstanceOps, }, frontend::FCircuit, transcript::poseidon::poseidon_canonical_config, @@ -366,7 +369,6 @@ mod tests { n_steps: usize, ) { let (decider_pp, decider_vp) = decider_params; - let pp_hash = fs_params.1.pp_hash().unwrap(); let f_circuit = FC::new(()).unwrap(); @@ -389,8 +391,8 @@ mod tests { nova.i, nova.z_0.clone(), nova.z_i.clone(), - &nova.U_i, - &nova.u_i, + &nova.U_i.get_commitments(), + &nova.u_i.get_commitments(), &proof, ) .unwrap(); @@ -401,7 +403,6 @@ mod tests { let calldata: Vec = prepare_calldata( function_selector, - pp_hash, nova.i, nova.z_0, nova.z_i, diff --git a/solidity-verifiers/templates/nova_cyclefold_decider.askama.sol b/solidity-verifiers/templates/nova_cyclefold_decider.askama.sol index a82893a..c9d1d79 100644 --- a/solidity-verifiers/templates/nova_cyclefold_decider.askama.sol +++ b/solidity-verifiers/templates/nova_cyclefold_decider.askama.sol @@ -63,9 +63,8 @@ contract NovaDecider is Groth16Verifier, KZG10Verifier { // 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[2] calldata u_i_cmW, // [u_i_cmW[2]] + uint256[3] calldata cmT_r, // [cmT[2], r] uint256[2] calldata pA, // groth16 uint256[2][2] calldata pB, // groth16 uint256[2] calldata pC, // groth16 @@ -86,20 +85,26 @@ 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; + // U_i.cmW + r * u_i.cmW + uint256[2] memory mulScalarPoint = super.mulScalar([u_i_cmW[0], u_i_cmW[1]], cmT_r[2]); + uint256[2] memory cmW = super.add([U_i_cmW_U_i_cmE[0], U_i_cmW_U_i_cmE[1]], mulScalarPoint); + + { + uint256[{{num_limbs}}] memory cmW_x_limbs = LimbsDecomposition.decompose(cmW[0]); + uint256[{{num_limbs}}] memory cmW_y_limbs = LimbsDecomposition.decompose(cmW[1]); + + for (uint8 k = 0; k < {{num_limbs}}; k++) { + public_inputs[{{ z_len * 2 + 2 }} + k] = cmW_x_limbs[k]; + public_inputs[{{ z_len * 2 + 2 + num_limbs }} + k] = cmW_y_limbs[k]; + } + } + + require(this.check(cmW, kzg_proof[0], challenge_W_challenge_E_kzg_evals[0], challenge_W_challenge_E_kzg_evals[2]), "KZG: verifying proof for challenge W failed"); } { - // 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]); + // U_i.cmE + r * cmT + uint256[2] memory mulScalarPoint = super.mulScalar([cmT_r[0], cmT_r[1]], cmT_r[2]); uint256[2] memory cmE = super.add([U_i_cmW_U_i_cmE[2], U_i_cmW_U_i_cmE[3]], mulScalarPoint); { @@ -107,53 +112,35 @@ contract NovaDecider is Groth16Verifier, KZG10Verifier { uint256[{{num_limbs}}] memory cmE_y_limbs = LimbsDecomposition.decompose(cmE[1]); for (uint8 k = 0; k < {{num_limbs}}; k++) { - public_inputs[{{ z_len * 2 + 5 }} + k] = cmE_x_limbs[k]; - public_inputs[{{ z_len * 2 + 5 + num_limbs }} + k] = cmE_y_limbs[k]; + public_inputs[{{ z_len * 2 + 2 + num_limbs * 2 }} + k] = cmE_x_limbs[k]; + public_inputs[{{ z_len * 2 + 2 + num_limbs * 3 }} + k] = cmE_y_limbs[k]; } } require(this.check(cmE, kzg_proof[1], challenge_W_challenge_E_kzg_evals[1], challenge_W_challenge_E_kzg_evals[3]), "KZG: verifying proof for challenge E failed"); } - { - // 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[{{num_limbs}}] memory cmW_x_limbs = LimbsDecomposition.decompose(cmW[0]); - uint256[{{num_limbs}}] memory cmW_y_limbs = LimbsDecomposition.decompose(cmW[1]); - - for (uint8 k = 0; k < {{num_limbs}}; k++) { - public_inputs[{{ z_len * 2 + 5 + num_limbs * 2 }} + k] = cmW_x_limbs[k]; - public_inputs[{{ z_len * 2 + 5 + num_limbs * 3 }} + k] = cmW_y_limbs[k]; - } - } - - require(this.check(cmW, kzg_proof[0], challenge_W_challenge_E_kzg_evals[0], challenge_W_challenge_E_kzg_evals[2]), "KZG: verifying proof for challenge W failed"); - } - { // add challenges - public_inputs[{{ z_len * 2 + 5 + num_limbs * 4 }}] = challenge_W_challenge_E_kzg_evals[0]; - public_inputs[{{ z_len * 2 + 5 + num_limbs * 4 + 1 }}] = challenge_W_challenge_E_kzg_evals[1]; - 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]; - + public_inputs[{{ z_len * 2 + 2 + num_limbs * 4 }}] = challenge_W_challenge_E_kzg_evals[0]; + public_inputs[{{ z_len * 2 + 2 + num_limbs * 4 + 1 }}] = challenge_W_challenge_E_kzg_evals[1]; + public_inputs[{{ z_len * 2 + 2 + num_limbs * 4 + 2 }}] = challenge_W_challenge_E_kzg_evals[2]; + public_inputs[{{ z_len * 2 + 2 + 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]); + cmT_x_limbs = LimbsDecomposition.decompose(cmT_r[0]); + cmT_y_limbs = LimbsDecomposition.decompose(cmT_r[1]); 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]; + public_inputs[{{ z_len * 2 + 2 + num_limbs * 4 }} + 4 + k] = cmT_x_limbs[k]; + public_inputs[{{ z_len * 2 + 2 + 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]; - + public_inputs[{{ public_inputs_len - 2 }}] = cmT_r[2]; + bool success_g16 = this.verifyProof(pA, pB, pC, public_inputs); require(success_g16 == true, "Groth16: verifying proof failed"); }