From 498198057b78224e4ec9f92be077b2c873b5e894 Mon Sep 17 00:00:00 2001 From: arnaucube Date: Wed, 10 Jan 2024 11:36:22 +0100 Subject: [PATCH 1/3] Nova+CycleFold Decider circuit (for onchain use case) (#49) * Add Pedersen commitments gadget * Add Nova+CycleFold Decider circuit (for onchain approach) "onchain"==Ethereum's EVM * merge src/decider into src/folding/nova/decider * PR review updates --- src/decider/circuit.rs | 391 --------------------- src/decider/mod.rs | 1 - src/folding/nova/circuits.rs | 10 +- src/folding/nova/decider.rs | 656 +++++++++++++++++++++++++++++++++++ src/folding/nova/ivc.rs | 42 ++- src/folding/nova/mod.rs | 1 + src/lib.rs | 1 - src/pedersen.rs | 93 ++++- src/utils/gadgets.rs | 105 ++++++ src/utils/mod.rs | 1 + 10 files changed, 879 insertions(+), 422 deletions(-) delete mode 100644 src/decider/circuit.rs delete mode 100644 src/decider/mod.rs create mode 100644 src/folding/nova/decider.rs create mode 100644 src/utils/gadgets.rs diff --git a/src/decider/circuit.rs b/src/decider/circuit.rs deleted file mode 100644 index b0085605..00000000 --- a/src/decider/circuit.rs +++ /dev/null @@ -1,391 +0,0 @@ -use ark_ec::CurveGroup; -use ark_ff::{Field, PrimeField}; -use ark_r1cs_std::{ - alloc::{AllocVar, AllocationMode}, - fields::FieldVar, -}; -use ark_relations::r1cs::{Namespace, SynthesisError}; -use core::{borrow::Borrow, marker::PhantomData}; - -use crate::ccs::r1cs::RelaxedR1CS; -use crate::utils::vec::SparseMatrix; -use crate::Error; - -pub type ConstraintF = <::BaseField as Field>::BasePrimeField; - -#[derive(Debug, Clone)] -pub struct RelaxedR1CSGadget> { - _f: PhantomData, - _cf: PhantomData, - _fv: PhantomData, -} -impl> RelaxedR1CSGadget { - /// performs the RelaxedR1CS check (Az∘Bz==uCz+E) - pub fn check(rel_r1cs: RelaxedR1CSVar, z: Vec) -> Result<(), Error> { - let Az = mat_vec_mul_sparse(rel_r1cs.A, z.clone()); - let Bz = mat_vec_mul_sparse(rel_r1cs.B, z.clone()); - let Cz = mat_vec_mul_sparse(rel_r1cs.C, z.clone()); - let uCz = vec_scalar_mul(&Cz, &rel_r1cs.u); - let uCzE = vec_add(&uCz, &rel_r1cs.E)?; - let AzBz = hadamard(&Az, &Bz)?; - for i in 0..AzBz.len() { - AzBz[i].enforce_equal(&uCzE[i].clone())?; - } - Ok(()) - } -} - -fn mat_vec_mul_sparse>( - m: SparseMatrixVar, - v: Vec, -) -> Vec { - let mut res = vec![FV::zero(); m.n_rows]; - for (row_i, row) in m.coeffs.iter().enumerate() { - for (value, col_i) in row.iter() { - res[row_i] += value.clone().mul(&v[*col_i].clone()); - } - } - res -} -pub fn vec_add>( - a: &Vec, - b: &Vec, -) -> Result, Error> { - if a.len() != b.len() { - return Err(Error::NotSameLength( - "a.len()".to_string(), - a.len(), - "b.len()".to_string(), - b.len(), - )); - } - let mut r: Vec = vec![FV::zero(); a.len()]; - for i in 0..a.len() { - r[i] = a[i].clone() + b[i].clone(); - } - Ok(r) -} -pub fn vec_scalar_mul>( - vec: &Vec, - c: &FV, -) -> Vec { - let mut result = vec![FV::zero(); vec.len()]; - for (i, a) in vec.iter().enumerate() { - result[i] = a.clone() * c; - } - result -} -pub fn hadamard>( - a: &Vec, - b: &Vec, -) -> Result, Error> { - if a.len() != b.len() { - return Err(Error::NotSameLength( - "a.len()".to_string(), - a.len(), - "b.len()".to_string(), - b.len(), - )); - } - let mut r: Vec = vec![FV::zero(); a.len()]; - for i in 0..a.len() { - r[i] = a[i].clone() * b[i].clone(); - } - Ok(r) -} - -#[derive(Debug, Clone)] -pub struct SparseMatrixVar> { - _f: PhantomData, - _cf: PhantomData, - _fv: PhantomData, - 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 -where - F: PrimeField, - CF: PrimeField, - FV: FieldVar, -{ - fn new_variable>>( - cs: impl Into>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let mut coeffs: Vec> = Vec::new(); - for row in val.borrow().coeffs.iter() { - let mut rowVar: Vec<(FV, usize)> = Vec::new(); - for &(value, col_i) in row.iter() { - let coeffVar = FV::new_variable(cs.clone(), || Ok(value), mode)?; - rowVar.push((coeffVar, col_i)); - } - coeffs.push(rowVar); - } - - Ok(Self { - _f: PhantomData, - _cf: PhantomData, - _fv: PhantomData, - n_rows: val.borrow().n_rows, - n_cols: val.borrow().n_cols, - coeffs, - }) - }) - } -} - -#[derive(Debug, Clone)] -pub struct RelaxedR1CSVar> { - _f: PhantomData, - _cf: PhantomData, - _fv: PhantomData, - pub A: SparseMatrixVar, - pub B: SparseMatrixVar, - pub C: SparseMatrixVar, - pub u: FV, - pub E: Vec, -} - -impl AllocVar, CF> for RelaxedR1CSVar -where - F: PrimeField, - CF: PrimeField, - FV: FieldVar, -{ - 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)?; - let E = Vec::::new_variable(cs.clone(), || Ok(val.borrow().E.clone()), mode)?; - let u = FV::new_variable(cs.clone(), || Ok(val.borrow().u), mode)?; - - Ok(Self { - _f: PhantomData, - _cf: PhantomData, - _fv: PhantomData, - A, - B, - C, - E, - u, - }) - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ark_crypto_primitives::crh::{ - sha256::{ - constraints::{Sha256Gadget, UnitVar}, - Sha256, - }, - CRHScheme, CRHSchemeGadget, - }; - use ark_ff::BigInteger; - use ark_pallas::{Fq, Fr}; - use ark_r1cs_std::{ - alloc::AllocVar, - bits::uint8::UInt8, - eq::EqGadget, - fields::{fp::FpVar, nonnative::NonNativeFieldVar}, - }; - use ark_relations::r1cs::{ - ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, - }; - use ark_std::One; - - use crate::ccs::r1cs::{ - tests::{get_test_r1cs, get_test_z}, - R1CS, - }; - use crate::frontend::arkworks::{extract_r1cs_and_z, tests::TestCircuit}; - - #[test] - fn test_relaxed_r1cs_small_gadget_handcrafted() { - let r1cs: R1CS = get_test_r1cs(); - let rel_r1cs = r1cs.relax(); - let z = get_test_z(3); - - let cs = ConstraintSystem::::new_ref(); - - let zVar = Vec::>::new_witness(cs.clone(), || Ok(z)).unwrap(); - let rel_r1csVar = - RelaxedR1CSVar::>::new_witness(cs.clone(), || Ok(rel_r1cs)).unwrap(); - - RelaxedR1CSGadget::>::check(rel_r1csVar, 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 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, z) = extract_r1cs_and_z::(&cs); - r1cs.check_relation(&z).unwrap(); - - let relaxed_r1cs = r1cs.relax(); - relaxed_r1cs.check_relation(&z).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 rel_r1csVar = - RelaxedR1CSVar::>::new_witness(cs.clone(), || Ok(relaxed_r1cs)) - .unwrap(); - - RelaxedR1CSGadget::>::check(rel_r1csVar, zVar).unwrap(); - assert!(cs.is_satisfied().unwrap()); - } - - #[test] - fn test_relaxed_r1cs_small_gadget_arkworks() { - let x = Fr::from(5_u32); - let y = x * x * x + x + Fr::from(5_u32); - let circuit = TestCircuit:: { x, y }; - 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); - } - - // circuit that has the number of constraints specified in the `n_constraints` parameter. Note - // that the generated circuit will have very sparse matrices, so the resulting constraints - // number of the RelaxedR1CS gadget must take that into account. - struct CustomTestCircuit { - _f: PhantomData, - pub n_constraints: usize, - pub x: F, - pub y: F, - } - impl CustomTestCircuit { - fn new(n_constraints: usize) -> Self { - let x = F::from(5_u32); - let mut y = F::one(); - for _ in 0..n_constraints - 1 { - y *= x; - } - Self { - _f: PhantomData, - n_constraints, - x, - y, - } - } - } - impl ConstraintSynthesizer for CustomTestCircuit { - fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { - let x = FpVar::::new_witness(cs.clone(), || Ok(self.x))?; - let y = FpVar::::new_input(cs.clone(), || Ok(self.y))?; - - let mut comp_y = FpVar::::new_witness(cs.clone(), || Ok(F::one()))?; - for _ in 0..self.n_constraints - 1 { - comp_y *= x.clone(); - } - - comp_y.enforce_equal(&y)?; - Ok(()) - } - } - - #[test] - fn test_relaxed_r1cs_custom_circuit() { - let n_constraints = 10_000; - let x = Fr::from(5_u32); - let mut y = Fr::one(); - for _ in 0..n_constraints - 1 { - y *= x; - } - - let circuit = CustomTestCircuit:: { - _f: PhantomData, - n_constraints, - x, - y, - }; - test_relaxed_r1cs_gadget(circuit); - } - - #[test] - fn test_relaxed_r1cs_nonnative_circuit() { - 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 circuit = CustomTestCircuit::::new(10); - circuit.generate_constraints(cs.clone()).unwrap(); - cs.finalize(); - let cs = cs.into_inner().unwrap(); - let (r1cs, z) = extract_r1cs_and_z::(&cs); - - let relaxed_r1cs = r1cs.relax(); - - // natively - let cs = ConstraintSystem::::new_ref(); - let zVar = Vec::>::new_witness(cs.clone(), || Ok(z.clone())).unwrap(); - let rel_r1csVar = RelaxedR1CSVar::>::new_witness(cs.clone(), || { - Ok(relaxed_r1cs.clone()) - }) - .unwrap(); - RelaxedR1CSGadget::>::check(rel_r1csVar, zVar).unwrap(); - - // non-natively - let cs = ConstraintSystem::::new_ref(); - let zVar = Vec::>::new_witness(cs.clone(), || Ok(z)).unwrap(); - let rel_r1csVar = - RelaxedR1CSVar::>::new_witness(cs.clone(), || { - Ok(relaxed_r1cs) - }) - .unwrap(); - RelaxedR1CSGadget::>::check(rel_r1csVar, zVar).unwrap(); - } -} diff --git a/src/decider/mod.rs b/src/decider/mod.rs deleted file mode 100644 index db89e612..00000000 --- a/src/decider/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod circuit; diff --git a/src/folding/nova/circuits.rs b/src/folding/nova/circuits.rs index a15e7f3f..afa7cc58 100644 --- a/src/folding/nova/circuits.rs +++ b/src/folding/nova/circuits.rs @@ -46,10 +46,10 @@ pub type CF2 = <::BaseField as Field>::BasePrimeField; /// represented non-natively over the constraint field. #[derive(Debug, Clone)] pub struct CommittedInstanceVar { - u: FpVar, - x: Vec>, - cmE: NonNativeAffineVar, - cmW: NonNativeAffineVar, + pub u: FpVar, + pub x: Vec>, + pub cmE: NonNativeAffineVar, + pub cmW: NonNativeAffineVar, } impl AllocVar, CF1> for CommittedInstanceVar @@ -94,7 +94,7 @@ where /// CommittedInstance.hash. /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and `U` is the /// `CommittedInstance`. - fn hash( + pub fn hash( self, crh_params: &CRHParametersVar>, i: FpVar>, diff --git a/src/folding/nova/decider.rs b/src/folding/nova/decider.rs new file mode 100644 index 00000000..adafc01a --- /dev/null +++ b/src/folding/nova/decider.rs @@ -0,0 +1,656 @@ +/// 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::crh::poseidon::constraints::CRHParametersVar; +use ark_crypto_primitives::sponge::{poseidon::PoseidonConfig, Absorb}; +use ark_ec::{CurveGroup, Group}; +use ark_ff::PrimeField; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + boolean::Boolean, + eq::EqGadget, + fields::{fp::FpVar, nonnative::NonNativeFieldVar, FieldVar}, + groups::GroupOpsBounds, + prelude::CurveVar, + ToConstraintFieldGadget, +}; +use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; +use ark_std::{One, Zero}; +use core::{borrow::Borrow, marker::PhantomData}; + +use crate::ccs::r1cs::R1CS; +use crate::folding::nova::{ + circuits::{CommittedInstanceVar, FCircuit, CF1, CF2}, + ivc::IVC, + CommittedInstance, Witness, +}; +use crate::pedersen::Params as PedersenParams; +use crate::utils::gadgets::{ + hadamard, mat_vec_mul_sparse, vec_add, vec_scalar_mul, SparseMatrixVar, +}; + +#[derive(Debug, Clone)] +pub struct RelaxedR1CSGadget> { + _f: PhantomData, + _cf: PhantomData, + _fv: PhantomData, +} +impl> RelaxedR1CSGadget { + /// performs the RelaxedR1CS check (Az∘Bz==uCz+E) + pub fn check( + r1cs: R1CSVar, + E: Vec, + u: FV, + z: Vec, + ) -> Result<(), SynthesisError> { + let Az = mat_vec_mul_sparse(r1cs.A, z.clone()); + let Bz = mat_vec_mul_sparse(r1cs.B, z.clone()); + let Cz = mat_vec_mul_sparse(r1cs.C, z.clone()); + let uCz = vec_scalar_mul(&Cz, &u); + let uCzE = vec_add(&uCz, &E)?; + let AzBz = hadamard(&Az, &Bz)?; + for i in 0..AzBz.len() { + AzBz[i].enforce_equal(&uCzE[i].clone())?; + } + Ok(()) + } +} + +#[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: FieldVar, +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + _mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + + let A = SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().A)?; + let B = SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().B)?; + let C = SparseMatrixVar::::new_constant(cs.clone(), &val.borrow().C)?; + + Ok(Self { + _f: PhantomData, + _cf: PhantomData, + _fv: PhantomData, + A, + B, + C, + }) + }) + } +} + +/// In-circuit representation of the Witness associated to the CommittedInstance. +#[derive(Debug, Clone)] +pub struct WitnessVar { + pub E: Vec>, + pub rE: FpVar, + pub W: Vec>, + pub rW: FpVar, +} + +impl AllocVar, CF1> for WitnessVar +where + C: CurveGroup, + ::BaseField: PrimeField, +{ + fn new_variable>>( + cs: impl Into>>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + + let E: Vec> = + Vec::new_variable(cs.clone(), || Ok(val.borrow().E.clone()), mode)?; + let rE = + FpVar::::new_variable(cs.clone(), || Ok(val.borrow().rE), mode)?; + + let W: Vec> = + Vec::new_variable(cs.clone(), || Ok(val.borrow().W.clone()), mode)?; + let rW = + FpVar::::new_variable(cs.clone(), || Ok(val.borrow().rW), mode)?; + + Ok(Self { E, rE, W, rW }) + }) + } +} + +/// In-circuit representation of the Witness associated to the CommittedInstance, but with +/// non-native representation, since it is used to represent the CycleFold witness. +#[derive(Debug, Clone)] +pub struct CycleFoldWitnessVar { + pub E: Vec>>, + pub rE: NonNativeFieldVar>, + pub W: Vec>>, + pub rW: NonNativeFieldVar>, +} + +impl AllocVar, CF2> for CycleFoldWitnessVar +where + C: CurveGroup, + ::BaseField: PrimeField, +{ + fn new_variable>>( + cs: impl Into>>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + + let E: Vec>> = + Vec::new_variable(cs.clone(), || Ok(val.borrow().E.clone()), mode)?; + let rE = NonNativeFieldVar::>::new_variable( + cs.clone(), + || Ok(val.borrow().rE), + mode, + )?; + + let W: Vec>> = + Vec::new_variable(cs.clone(), || Ok(val.borrow().W.clone()), mode)?; + let rW = NonNativeFieldVar::>::new_variable( + cs.clone(), + || Ok(val.borrow().rW), + mode, + )?; + + Ok(Self { E, rE, W, rW }) + }) + } +} + +/// Circuit that implements the in-circuit checks needed for the onchain (Ethereum's EVM) +/// verification. +pub struct DeciderCircuit +where + C1: CurveGroup, + GC1: CurveVar>, + C2: CurveGroup, + GC2: CurveVar>, +{ + _c1: PhantomData, + _gc1: 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, + /// R1CS of the CycleFold circuit + pub cf_r1cs: R1CS, + /// CycleFold PedersenParams, over C2 + pub cf_pedersen_params: PedersenParams, + pub poseidon_config: PoseidonConfig>, + 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>, + /// CycleFold running instance + pub cf_U_i: Option>, + pub cf_W_i: Option>, +} +impl DeciderCircuit +where + C1: CurveGroup, + C2: CurveGroup, + GC1: CurveVar>, + GC2: CurveVar>, +{ + pub fn from_ivc>(ivc: IVC) -> Self { + Self { + _c1: PhantomData, + _gc1: PhantomData, + _c2: PhantomData, + _gc2: PhantomData, + + E_len: ivc.W_i.E.len(), + cf_E_len: ivc.cf_W_i.E.len(), + r1cs: ivc.r1cs, + cf_r1cs: ivc.cf_r1cs, + cf_pedersen_params: ivc.cf_pedersen_params, + poseidon_config: ivc.poseidon_config, + i: Some(ivc.i), + z_0: Some(ivc.z_0), + z_i: Some(ivc.z_i), + u_i: Some(ivc.u_i), + w_i: Some(ivc.w_i), + U_i: Some(ivc.U_i), + W_i: Some(ivc.W_i), + cf_U_i: Some(ivc.cf_U_i), + cf_W_i: Some(ivc.cf_W_i), + } + } +} + +impl ConstraintSynthesizer> for DeciderCircuit +where + C1: CurveGroup, + C2: CurveGroup, + GC1: CurveVar>, + GC2: CurveVar>, + ::BaseField: PrimeField, + ::BaseField: PrimeField, + ::ScalarField: Absorb, + ::ScalarField: Absorb, + C1: CurveGroup, + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, +{ + fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { + let r1cs = + R1CSVar::, FpVar>>::new_witness(cs.clone(), || { + Ok(self.r1cs.clone()) + })?; + + let i = FpVar::>::new_witness(cs.clone(), || { + Ok(self.i.unwrap_or_else(CF1::::zero)) + })?; + let z_0 = Vec::>>::new_witness(cs.clone(), || { + Ok(self.z_0.unwrap_or(vec![CF1::::zero()])) + })?; + let z_i = Vec::>>::new_witness(cs.clone(), || { + Ok(self.z_i.unwrap_or(vec![CF1::::zero()])) + })?; + + let u_dummy_native = CommittedInstance::::dummy(1); + let w_dummy_native = Witness::::new( + vec![C1::ScalarField::zero(); self.r1cs.A.n_cols - 2 /* (2=1+1, since u_i.x.len=1) */], + self.E_len, + ); + + let u_i = CommittedInstanceVar::::new_witness(cs.clone(), || { + Ok(self.u_i.unwrap_or(u_dummy_native.clone())) + })?; + let w_i = WitnessVar::::new_witness(cs.clone(), || { + Ok(self.w_i.unwrap_or(w_dummy_native.clone())) + })?; + let U_i = CommittedInstanceVar::::new_witness(cs.clone(), || { + Ok(self.U_i.unwrap_or(u_dummy_native.clone())) + })?; + let W_i = WitnessVar::::new_witness(cs.clone(), || { + Ok(self.W_i.unwrap_or(w_dummy_native.clone())) + })?; + + let crh_params = CRHParametersVar::::new_constant( + cs.clone(), + self.poseidon_config.clone(), + )?; + + // 1. check RelaxedR1CS of u_i + let z_u: Vec>> = [ + vec![FpVar::>::one()], + u_i.x.to_vec(), + w_i.W.to_vec(), + ] + .concat(); + RelaxedR1CSGadget::, FpVar>>::check( + r1cs.clone(), + w_i.E, + u_i.u.clone(), + z_u, + )?; + + // 2. check RelaxedR1CS of U_i + let z_U: Vec>> = + [vec![U_i.u.clone()], U_i.x.to_vec(), W_i.W.to_vec()].concat(); + RelaxedR1CSGadget::, FpVar>>::check( + r1cs, + W_i.E, + U_i.u.clone(), + z_U, + )?; + + // 3. u_i.cmE==cm(0), u_i.u==1 + // Here zero_x & zero_y are the x & y coordinates of the zero point affine representation. + let zero_x = NonNativeFieldVar::::new_constant( + cs.clone(), + C1::BaseField::zero(), + )? + .to_constraint_field()?; + let zero_y = NonNativeFieldVar::::new_constant( + cs.clone(), + C1::BaseField::one(), + )? + .to_constraint_field()?; + (u_i.cmE.x.is_eq(&zero_x)?).enforce_equal(&Boolean::TRUE)?; + (u_i.cmE.y.is_eq(&zero_y)?).enforce_equal(&Boolean::TRUE)?; + (u_i.u.is_one()?).enforce_equal(&Boolean::TRUE)?; + + // 4. u_i.x == H(i, z_0, z_i, U_i) + let u_i_x = U_i + .clone() + .hash(&crh_params, i.clone(), z_0.clone(), z_i.clone())?; + (u_i.x[0]).enforce_equal(&u_i_x)?; + + // The following two checks (and their respective allocations) are disabled for normal + // tests since they take ~24.5M constraints and would take several minutes (and RAM) to run + // the test + #[cfg(not(test))] + { + // imports here instead of at the top of the file, so we avoid having multiple + // `#[cfg(not(test))] + use crate::folding::nova::cyclefold::{CycleFoldCommittedInstanceVar, CF_IO_LEN}; + use crate::pedersen::PedersenGadget; + use ark_r1cs_std::ToBitsGadget; + + let cf_r1cs = R1CSVar::< + C1::BaseField, + CF1, + NonNativeFieldVar>, + >::new_witness(cs.clone(), || Ok(self.cf_r1cs.clone()))?; + + let cf_u_dummy_native = CommittedInstance::::dummy(CF_IO_LEN); + let w_dummy_native = Witness::::new( + vec![C2::ScalarField::zero(); self.cf_r1cs.A.n_cols - 1 - self.cf_r1cs.l], + self.cf_E_len, + ); + let cf_U_i = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { + Ok(self.cf_U_i.unwrap_or_else(|| cf_u_dummy_native.clone())) + })?; + let cf_W_i = CycleFoldWitnessVar::::new_witness(cs.clone(), || { + Ok(self.cf_W_i.unwrap_or(w_dummy_native.clone())) + })?; + + // 5. 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: Vec>>> = cf_W_i + .E + .iter() + .map(|E_i| E_i.to_bits_le().unwrap()) + .collect(); + let cf_W_i_W_bits: Vec>>> = cf_W_i + .W + .iter() + .map(|W_i| W_i.to_bits_le().unwrap()) + .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)?; + + // 6. check RelaxedR1CS of cf_U_i + let cf_z_U: Vec>> = + [vec![cf_U_i.u.clone()], cf_U_i.x.to_vec(), cf_W_i.W.to_vec()].concat(); + RelaxedR1CSGadget::< + C2::ScalarField, + CF1, + NonNativeFieldVar>, + >::check(cf_r1cs, cf_W_i.E, cf_U_i.u.clone(), cf_z_U)?; + } + + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use ark_crypto_primitives::crh::{ + sha256::{ + constraints::{Sha256Gadget, UnitVar}, + Sha256, + }, + CRHScheme, CRHSchemeGadget, + }; + use ark_ff::BigInteger; + use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; + use ark_r1cs_std::{ + alloc::AllocVar, + bits::uint8::UInt8, + eq::EqGadget, + fields::{fp::FpVar, nonnative::NonNativeFieldVar}, + }; + use ark_relations::r1cs::ConstraintSystem; + use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; + + use crate::folding::nova::circuits::{tests::TestFCircuit, FCircuit}; + use crate::folding::nova::ivc::IVC; + use crate::transcript::poseidon::tests::poseidon_test_config; + + use crate::ccs::r1cs::{ + tests::{get_test_r1cs, get_test_z}, + R1CS, + }; + use crate::frontend::arkworks::{extract_r1cs_and_z, tests::TestCircuit}; + + #[test] + fn test_relaxed_r1cs_small_gadget_handcrafted() { + let r1cs: R1CS = get_test_r1cs(); + let rel_r1cs = r1cs.clone().relax(); + let z = get_test_z(3); + + let cs = ConstraintSystem::::new_ref(); + + let zVar = Vec::>::new_witness(cs.clone(), || Ok(z)).unwrap(); + let EVar = Vec::>::new_witness(cs.clone(), || Ok(rel_r1cs.E)).unwrap(); + let uVar = FpVar::::new_witness(cs.clone(), || Ok(rel_r1cs.u)).unwrap(); + let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); + + RelaxedR1CSGadget::>::check(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 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, z) = extract_r1cs_and_z::(&cs); + r1cs.check_relation(&z).unwrap(); + + let relaxed_r1cs = r1cs.clone().relax(); + relaxed_r1cs.check_relation(&z).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(relaxed_r1cs.E)).unwrap(); + let uVar = FpVar::::new_witness(cs.clone(), || Ok(relaxed_r1cs.u)).unwrap(); + let r1csVar = R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)).unwrap(); + + RelaxedR1CSGadget::>::check(r1csVar, EVar, uVar, zVar).unwrap(); + assert!(cs.is_satisfied().unwrap()); + } + + #[test] + fn test_relaxed_r1cs_small_gadget_arkworks() { + let x = Fr::from(5_u32); + let y = x * x * x + x + Fr::from(5_u32); + let circuit = TestCircuit:: { x, y }; + 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); + } + + // circuit that has the number of constraints specified in the `n_constraints` parameter. Note + // that the generated circuit will have very sparse matrices, so the resulting constraints + // number of the RelaxedR1CS gadget must take that into account. + struct CustomTestCircuit { + _f: PhantomData, + pub n_constraints: usize, + pub x: F, + pub y: F, + } + impl CustomTestCircuit { + fn new(n_constraints: usize) -> Self { + let x = F::from(5_u32); + let mut y = F::one(); + for _ in 0..n_constraints - 1 { + y *= x; + } + Self { + _f: PhantomData, + n_constraints, + x, + y, + } + } + } + impl ConstraintSynthesizer for CustomTestCircuit { + fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { + let x = FpVar::::new_witness(cs.clone(), || Ok(self.x))?; + let y = FpVar::::new_input(cs.clone(), || Ok(self.y))?; + + let mut comp_y = FpVar::::new_witness(cs.clone(), || Ok(F::one()))?; + for _ in 0..self.n_constraints - 1 { + comp_y *= x.clone(); + } + + comp_y.enforce_equal(&y)?; + Ok(()) + } + } + + #[test] + fn test_relaxed_r1cs_custom_circuit() { + let n_constraints = 10_000; + let x = Fr::from(5_u32); + let mut y = Fr::one(); + for _ in 0..n_constraints - 1 { + y *= x; + } + + let circuit = CustomTestCircuit:: { + _f: PhantomData, + n_constraints, + x, + y, + }; + test_relaxed_r1cs_gadget(circuit); + } + + #[test] + fn test_relaxed_r1cs_nonnative_circuit() { + 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 circuit = CustomTestCircuit::::new(10); + circuit.generate_constraints(cs.clone()).unwrap(); + cs.finalize(); + let cs = cs.into_inner().unwrap(); + let (r1cs, z) = extract_r1cs_and_z::(&cs); + + let relaxed_r1cs = r1cs.clone().relax(); + + // 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(relaxed_r1cs.clone().E)).unwrap(); + let uVar = FpVar::::new_witness(cs.clone(), || Ok(relaxed_r1cs.u)).unwrap(); + let r1csVar = + R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs.clone())).unwrap(); + RelaxedR1CSGadget::>::check(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(relaxed_r1cs.E)) + .unwrap(); + let uVar = + NonNativeFieldVar::::new_witness(cs.clone(), || Ok(relaxed_r1cs.u)).unwrap(); + let r1csVar = + R1CSVar::>::new_witness(cs.clone(), || Ok(r1cs)) + .unwrap(); + RelaxedR1CSGadget::>::check(r1csVar, EVar, uVar, zVar) + .unwrap(); + } + + #[test] + fn test_decider_circuit() { + let mut rng = ark_std::test_rng(); + let poseidon_config = poseidon_test_config::(); + + let F_circuit = TestFCircuit::::new(); + let z_0 = vec![Fr::from(3_u32)]; + + // generate an IVC and do a step of it + let mut ivc = IVC::>::new( + &mut rng, + poseidon_config, + F_circuit, + z_0.clone(), + ) + .unwrap(); + ivc.prove_step().unwrap(); + ivc.verify(z_0, 1).unwrap(); + + // load the DeciderCircuit from the generated IVC + let decider_circuit = DeciderCircuit::::from_ivc(ivc); + + 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/src/folding/nova/ivc.rs b/src/folding/nova/ivc.rs index 20cbb3c4..2c4f156c 100644 --- a/src/folding/nova/ivc.rs +++ b/src/folding/nova/ivc.rs @@ -33,23 +33,31 @@ where _gc1: PhantomData, _c2: PhantomData, _gc2: PhantomData, - r1cs: R1CS, - cf_r1cs: R1CS, // Notice that this is a different set of R1CS constraints than the 'r1cs'. This is the R1CS of the CycleFoldCircuit - poseidon_config: PoseidonConfig, - pedersen_params: PedersenParams, // PedersenParams over C1 - cf_pedersen_params: PedersenParams, // CycleFold PedersenParams, over C2 - F: FC, // F circuit - i: C1::ScalarField, - z_0: Vec, - z_i: Vec, - w_i: Witness, - u_i: CommittedInstance, - W_i: Witness, - U_i: CommittedInstance, - - // cyclefold running instance - cf_W_i: Witness, - cf_U_i: CommittedInstance, + /// R1CS of the Augmented Function circuit + pub r1cs: R1CS, + /// R1CS of the CycleFold circuit + pub cf_r1cs: R1CS, + pub poseidon_config: PoseidonConfig, + /// PedersenParams over C1 + pub pedersen_params: PedersenParams, + /// CycleFold PedersenParams, over C2 + pub cf_pedersen_params: PedersenParams, + /// F circuit, the circuit that is being folded + pub F: FC, + pub i: C1::ScalarField, + /// initial state + pub z_0: Vec, + /// current i-th state + pub z_i: Vec, + /// Nova instances + pub w_i: Witness, + pub u_i: CommittedInstance, + pub W_i: Witness, + pub U_i: CommittedInstance, + + /// CycleFold running instance + pub cf_W_i: Witness, + pub cf_U_i: CommittedInstance, } impl IVC diff --git a/src/folding/nova/mod.rs b/src/folding/nova/mod.rs index 04d216ee..a6e3eb62 100644 --- a/src/folding/nova/mod.rs +++ b/src/folding/nova/mod.rs @@ -14,6 +14,7 @@ use crate::Error; pub mod circuits; pub mod cyclefold; +pub mod decider; pub mod ivc; pub mod nifs; pub mod traits; diff --git a/src/lib.rs b/src/lib.rs index ffc19c8d..873cdb36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,6 @@ pub mod transcript; use transcript::Transcript; pub mod ccs; pub mod constants; -pub mod decider; pub mod folding; pub mod frontend; pub mod pedersen; diff --git a/src/pedersen.rs b/src/pedersen.rs index 7d0a8c56..82857140 100644 --- a/src/pedersen.rs +++ b/src/pedersen.rs @@ -1,6 +1,9 @@ use ark_ec::CurveGroup; +use ark_ff::Field; +use ark_r1cs_std::{boolean::Boolean, groups::GroupOpsBounds, prelude::CurveVar}; +use ark_relations::r1cs::SynthesisError; use ark_std::{rand::Rng, UniformRand}; -use std::marker::PhantomData; +use core::marker::PhantomData; use crate::utils::vec::{vec_add, vec_scalar_mul}; @@ -9,14 +12,14 @@ use crate::Error; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Proof { - R: C, - u: Vec, - r_u: C::ScalarField, + pub R: C, + pub u: Vec, + pub r_u: C::ScalarField, } #[derive(Debug, Clone, Eq, PartialEq)] pub struct Params { - h: C, + pub h: C, pub generators: Vec, } @@ -111,13 +114,52 @@ impl Pedersen { } } +pub type CF = <::BaseField as Field>::BasePrimeField; + +pub struct PedersenGadget +where + C: CurveGroup, + GC: CurveVar>, +{ + _cf: PhantomData>, + _c: PhantomData, + _gc: PhantomData, +} + +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>>, + ) -> Result { + let mut res = GC::zero(); + res += h.scalar_mul_le(r.iter())?; + for (i, v_i) in v.iter().enumerate() { + res += g[i].scalar_mul_le(v_i.iter())?; + } + Ok(res) + } +} + #[cfg(test)] mod tests { + use ark_ff::{BigInteger, PrimeField}; + use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; + use ark_r1cs_std::{alloc::AllocVar, bits::boolean::Boolean, eq::EqGadget}; + use ark_relations::r1cs::ConstraintSystem; + use ark_std::UniformRand; + use super::*; use crate::transcript::poseidon::{tests::poseidon_test_config, PoseidonTranscript}; - use ark_pallas::{Fr, Projective}; - #[test] fn test_pedersen_vector() { let mut rng = ark_std::test_rng(); @@ -140,4 +182,41 @@ mod tests { let proof = Pedersen::::prove(¶ms, &mut transcript_p, &cm, &v, &r).unwrap(); Pedersen::::verify(¶ms, &mut transcript_v, cm, proof).unwrap(); } + + #[test] + fn test_pedersen_circuit() { + let mut rng = ark_std::test_rng(); + + const n: usize = 10; + // setup params + let params = Pedersen::::new_params(&mut rng, n); + + let v: Vec = std::iter::repeat_with(|| Fr::rand(&mut rng)) + .take(n) + .collect(); + let r: Fr = Fr::rand(&mut rng); + let cm = Pedersen::::commit(¶ms, &v, &r).unwrap(); + + // circuit + let cs = ConstraintSystem::::new_ref(); + + let v_bits: Vec> = v.iter().map(|val| val.into_bigint().to_bits_le()).collect(); + let r_bits: Vec = r.into_bigint().to_bits_le(); + + // prepare inputs + let vVar: Vec>> = v_bits + .iter() + .map(|val_bits| { + Vec::>::new_witness(cs.clone(), || Ok(val_bits.clone())).unwrap() + }) + .collect(); + let rVar = Vec::>::new_witness(cs.clone(), || Ok(r_bits)).unwrap(); + let gVar = Vec::::new_witness(cs.clone(), || Ok(params.generators)).unwrap(); + let hVar = GVar::new_witness(cs.clone(), || Ok(params.h)).unwrap(); + let expected_cmVar = GVar::new_witness(cs.clone(), || Ok(cm)).unwrap(); + + // use the gadget + let cmVar = PedersenGadget::::commit(hVar, gVar, vVar, rVar).unwrap(); + cmVar.enforce_equal(&expected_cmVar).unwrap(); + } } diff --git a/src/utils/gadgets.rs b/src/utils/gadgets.rs new file mode 100644 index 00000000..dc0db117 --- /dev/null +++ b/src/utils/gadgets.rs @@ -0,0 +1,105 @@ +use ark_ff::PrimeField; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + fields::FieldVar, +}; +use ark_relations::r1cs::{Namespace, SynthesisError}; +use core::{borrow::Borrow, marker::PhantomData}; + +use crate::utils::vec::SparseMatrix; + +pub fn mat_vec_mul_sparse>( + m: SparseMatrixVar, + v: Vec, +) -> Vec { + let mut res = vec![FV::zero(); m.n_rows]; + for (row_i, row) in m.coeffs.iter().enumerate() { + for (value, col_i) in row.iter() { + res[row_i] += value.clone().mul(&v[*col_i].clone()); + } + } + res +} +pub fn vec_add>( + a: &Vec, + b: &Vec, +) -> Result, SynthesisError> { + if a.len() != b.len() { + return Err(SynthesisError::Unsatisfiable); + } + let mut r: Vec = vec![FV::zero(); a.len()]; + for i in 0..a.len() { + r[i] = a[i].clone() + b[i].clone(); + } + Ok(r) +} +pub fn vec_scalar_mul>( + vec: &Vec, + c: &FV, +) -> Vec { + let mut result = vec![FV::zero(); vec.len()]; + for (i, a) in vec.iter().enumerate() { + result[i] = a.clone() * c; + } + result +} +pub fn hadamard>( + a: &Vec, + b: &Vec, +) -> Result, SynthesisError> { + if a.len() != b.len() { + return Err(SynthesisError::Unsatisfiable); + } + let mut r: Vec = vec![FV::zero(); a.len()]; + for i in 0..a.len() { + r[i] = a[i].clone() * b[i].clone(); + } + Ok(r) +} + +#[derive(Debug, Clone)] +pub struct SparseMatrixVar> { + _f: PhantomData, + _cf: PhantomData, + _fv: PhantomData, + 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 +where + F: PrimeField, + CF: PrimeField, + FV: FieldVar, +{ + fn new_variable>>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + + let mut coeffs: Vec> = Vec::new(); + for row in val.borrow().coeffs.iter() { + let mut rowVar: Vec<(FV, usize)> = Vec::new(); + for &(value, col_i) in row.iter() { + let coeffVar = FV::new_variable(cs.clone(), || Ok(value), mode)?; + rowVar.push((coeffVar, col_i)); + } + coeffs.push(rowVar); + } + + Ok(Self { + _f: PhantomData, + _cf: PhantomData, + _fv: PhantomData, + n_rows: val.borrow().n_rows, + n_cols: val.borrow().n_cols, + coeffs, + }) + }) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 31e71ddc..a095df03 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,5 @@ pub mod bit; +pub mod gadgets; pub mod hypercube; pub mod lagrange_poly; pub mod mle; From 46e538775bda42108eb152197a61a9e1bda26e8a Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 15 Jan 2024 17:18:51 +0100 Subject: [PATCH 2/3] A circuit for computing c, from section 5, step 5 of "A multi-folding scheme for CCS" (#61) * feat: start hypernova nimfs verifier * refactor: change where nimfs verifier lives * feat: `EqEvalGadget` for computing `eq(x, y)` * refactor: rename to `utils.rs` * feat: implement a `VecFpVar` struct, representing a vector of `FpVar`s * refactor: extract a `sum_muls_gamma_pows_eq_sigma` function to make circuit tests easier * feat: implement a `SumMulsGammaPowEqSigmaGadget` to compute the first term of the sum of section 5, step 5 * refactor: update gadget name and method name to match `sum_muls_gamma_pows_eq_sigma` * fix: update method call * refactor: remove usage of `GammaVar` Co-authored-by: arnaucube * refactor: move hypernova circuit related types and methods into `src/folding/hypernova/circuits.rs` * refactor: remove all of `GammaVar` wrapper * chore: update type to `&[F]` * refactor: update from `new_constant` to `new_witness` * fix: actual file deletion * refactor: remove `VecFpVar` struct * chore: update comment doc * refactor: extract a `sum_ci_mul_prod_thetaj` function for testing * feat: `test_sum_ci_mul_prod_thetaj_gadget` passing * refactor: update docs and add a helper `get_prepared_thetas` function * refactor: clearer arg name * fix: clippy typing * chore: correct latex comments * refactor: remove unncessary `get_prepared_thetas` fn * feat: test passing for rough first pass on `ComputeCFromSigmasAndThetasGadget` * chore: add additional doc comments * chore: add `#[allow(clippy::too_many_arguments)]` * refactor: make gadget generic over a curve group * chore: clippy fixes * chore: correct latex in doc comment * refactor: refactor `sum_muls_gamma_pows_eq_sigma` and `sum_ci_mul_prod_thetaj` in `ComputeCFromSigmasAndThetasGadget` --------- Co-authored-by: arnaucube --- src/folding/circuits/mod.rs | 1 + src/folding/circuits/utils.rs | 75 ++++++++ src/folding/hypernova/circuit.rs | 318 +++++++++++++++++++++++++++++++ src/folding/hypernova/mod.rs | 1 + src/folding/hypernova/utils.rs | 46 +++-- 5 files changed, 429 insertions(+), 12 deletions(-) create mode 100644 src/folding/circuits/utils.rs create mode 100644 src/folding/hypernova/circuit.rs diff --git a/src/folding/circuits/mod.rs b/src/folding/circuits/mod.rs index d021b7da..08fa8071 100644 --- a/src/folding/circuits/mod.rs +++ b/src/folding/circuits/mod.rs @@ -4,6 +4,7 @@ use ark_ff::Field; pub mod nonnative; pub mod sum_check; +pub mod utils; // CF represents the constraints field pub type CF = <::BaseField as Field>::BasePrimeField; diff --git a/src/folding/circuits/utils.rs b/src/folding/circuits/utils.rs new file mode 100644 index 00000000..50b954a0 --- /dev/null +++ b/src/folding/circuits/utils.rs @@ -0,0 +1,75 @@ +use ark_ff::PrimeField; +use ark_r1cs_std::fields::{fp::FpVar, FieldVar}; +use ark_relations::r1cs::SynthesisError; +use std::marker::PhantomData; + +/// EqEval is a gadget for computing $\tilde{eq}(a, b) = \prod_{i=1}^{l}(a_i \cdot b_i + (1 - a_i)(1 - b_i))$ +/// :warning: This is not the ark_r1cs_std::eq::EqGadget +pub struct EqEvalGadget { + _f: PhantomData, +} + +impl EqEvalGadget { + /// Gadget to evaluate eq polynomial. + /// Follows the implementation of `eq_eval` found in this crate. + pub fn eq_eval(x: Vec>, y: Vec>) -> Result, SynthesisError> { + if x.len() != y.len() { + return Err(SynthesisError::Unsatisfiable); + } + if x.is_empty() || y.is_empty() { + return Err(SynthesisError::AssignmentMissing); + } + let mut e = FpVar::::one(); + for (xi, yi) in x.iter().zip(y.iter()) { + let xi_yi = xi * yi; + e *= xi_yi.clone() + xi_yi - xi - yi + F::one(); + } + Ok(e) + } +} + +#[cfg(test)] +mod tests { + + use crate::utils::virtual_polynomial::eq_eval; + + use super::EqEvalGadget; + use ark_ff::Field; + use ark_pallas::Fr; + use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, R1CSVar}; + use ark_relations::r1cs::ConstraintSystem; + use ark_std::{test_rng, UniformRand}; + + #[test] + pub fn test_eq_eval_gadget() { + let mut rng = test_rng(); + let cs = ConstraintSystem::::new_ref(); + + for i in 1..20 { + let x_vec: Vec = (0..i).map(|_| Fr::rand(&mut rng)).collect(); + let y_vec: Vec = (0..i).map(|_| Fr::rand(&mut rng)).collect(); + let x: Vec> = x_vec + .iter() + .map(|x| FpVar::::new_witness(cs.clone(), || Ok(x)).unwrap()) + .collect(); + let y: Vec> = y_vec + .iter() + .map(|y| FpVar::::new_witness(cs.clone(), || Ok(y)).unwrap()) + .collect(); + let expected_eq_eval = eq_eval::(&x_vec, &y_vec).unwrap(); + let gadget_eq_eval: FpVar = EqEvalGadget::::eq_eval(x, y).unwrap(); + assert_eq!(expected_eq_eval, gadget_eq_eval.value().unwrap()); + } + + let x: Vec> = vec![]; + let y: Vec> = vec![]; + let gadget_eq_eval = EqEvalGadget::::eq_eval(x, y); + assert!(gadget_eq_eval.is_err()); + + let x: Vec> = vec![]; + let y: Vec> = + vec![FpVar::::new_witness(cs.clone(), || Ok(&Fr::ONE)).unwrap()]; + let gadget_eq_eval = EqEvalGadget::::eq_eval(x, y); + assert!(gadget_eq_eval.is_err()); + } +} diff --git a/src/folding/hypernova/circuit.rs b/src/folding/hypernova/circuit.rs new file mode 100644 index 00000000..4789bfa1 --- /dev/null +++ b/src/folding/hypernova/circuit.rs @@ -0,0 +1,318 @@ +// hypernova nimfs verifier circuit +// see section 5 in https://eprint.iacr.org/2023/573.pdf + +use crate::{ccs::CCS, folding::circuits::utils::EqEvalGadget}; +use ark_ec::CurveGroup; +use ark_r1cs_std::{ + alloc::AllocVar, + fields::{fp::FpVar, FieldVar}, + ToBitsGadget, +}; +use ark_relations::r1cs::{ConstraintSystemRef, SynthesisError}; +use ark_std::Zero; +use std::marker::PhantomData; + +/// Gadget to compute $\sum_{j \in [t]} \gamma^{j} \cdot e_1 \cdot \sigma_j + \gamma^{t+1} \cdot e_2 \cdot \sum_{i=1}^{q} c_i * \prod_{j \in S_i} \theta_j$. +/// This is the sum computed by the verifier and laid out in section 5, step 5 of "A multi-folding scheme for CCS". +pub struct ComputeCFromSigmasAndThetasGadget { + _c: PhantomData, +} + +impl ComputeCFromSigmasAndThetasGadget { + /// Computes the sum $\sum_{j}^{j + n} \gamma^{j} \cdot eq_eval \cdot \sigma_{j}$, where $n$ is the length of the `sigmas` vector + /// It corresponds to the first term of the sum that $\mathcal{V}$ has to compute at section 5, step 5 of "A multi-folding scheme for CCS". + /// + /// # Arguments + /// - `sigmas`: vector of $\sigma_j$ values + /// - `eq_eval`: the value of $\tilde{eq}(x_j, x^{\prime})$ + /// - `gamma`: value $\gamma$ + /// - `j`: the power at which we start to compute $\gamma^{j}$. This is needed in the context of multifolding. + /// + /// # Notes + /// In the context of multifolding, `j` corresponds to `ccs.t` in `compute_c_from_sigmas_and_thetas` + fn sum_muls_gamma_pows_eq_sigma( + gamma: FpVar, + eq_eval: FpVar, + sigmas: Vec>, + j: FpVar, + ) -> Result, SynthesisError> { + let mut result = FpVar::::zero(); + let mut gamma_pow = gamma.pow_le(&j.to_bits_le()?)?; + for sigma in sigmas { + result += gamma_pow.clone() * eq_eval.clone() * sigma; + gamma_pow *= gamma.clone(); + } + Ok(result) + } + + /// Computes $\sum_{i=1}^{q} c_i * \prod_{j \in S_i} theta_j$ + /// + /// # Arguments + /// - `c_i`: vector of $c_i$ values + /// - `thetas`: vector of pre-processed $\thetas[j]$ values corresponding to a particular `ccs.S[i]` + /// + /// # Notes + /// This is a part of the second term of the sum that $\mathcal{V}$ has to compute at section 5, step 5 of "A multi-folding scheme for CCS". + /// The first term is computed by `SumMulsGammaPowsEqSigmaGadget::sum_muls_gamma_pows_eq_sigma`. + /// This is a doct product between a vector of c_i values and a vector of pre-processed $\theta_j$ values, where $j$ is a value from $S_i$. + /// Hence, this requires some pre-processing of the $\theta_j$ values, before running this gadget. + fn sum_ci_mul_prod_thetaj( + c_i: Vec>, + thetas: Vec>>, + ) -> Result, SynthesisError> { + let mut result = FpVar::::zero(); + for (i, c_i) in c_i.iter().enumerate() { + let prod = &thetas[i].iter().fold(FpVar::one(), |acc, e| acc * e); + result += c_i * prod; + } + Ok(result) + } + + /// Computes the sum that the verifier has to compute at section 5, step 5 of "A multi-folding scheme for CCS". + /// + /// # Arguments + /// - `cs`: constraint system + /// - `ccs`: the CCS instance + /// - `vec_sigmas`: vector of $\sigma_j$ values + /// - `vec_thetas`: vector of $\theta_j$ values + /// - `gamma`: value $\gamma$ + /// - `beta`: vector of $\beta_j$ values + /// - `vec_r_x`: vector of $r_{x_j}$ values + /// - `vec_r_x_prime`: vector of $r_{x_j}^{\prime}$ values + /// + /// # Notes + /// Arguments to this function are *almost* the same as the arguments to `compute_c_from_sigmas_and_thetas` in `utils.rs`. + #[allow(clippy::too_many_arguments)] + pub fn compute_c_from_sigmas_and_thetas( + cs: ConstraintSystemRef, + ccs: &CCS, + vec_sigmas: Vec>>, + vec_thetas: Vec>>, + gamma: FpVar, + beta: Vec>, + vec_r_x: Vec>>, + vec_r_x_prime: Vec>, + ) -> Result, SynthesisError> { + let mut c = + FpVar::::new_witness(cs.clone(), || Ok(C::ScalarField::zero()))?; + let t = FpVar::::new_witness(cs.clone(), || { + Ok(C::ScalarField::from(ccs.t as u64)) + })?; + + let mut e_lcccs = Vec::new(); + for r_x in vec_r_x.iter() { + let e_1 = EqEvalGadget::eq_eval(r_x.to_vec(), vec_r_x_prime.to_vec())?; + e_lcccs.push(e_1); + } + + for (i, sigmas) in vec_sigmas.iter().enumerate() { + let i_var = FpVar::::new_witness(cs.clone(), || { + Ok(C::ScalarField::from(i as u64)) + })?; + let pow = i_var * t.clone(); + c += Self::sum_muls_gamma_pows_eq_sigma( + gamma.clone(), + e_lcccs[i].clone(), + sigmas.to_vec(), + pow, + )?; + } + + let mu = FpVar::::new_witness(cs.clone(), || { + Ok(C::ScalarField::from(vec_sigmas.len() as u64)) + })?; + let e_2 = EqEvalGadget::eq_eval(beta, vec_r_x_prime)?; + for (k, thetas) in vec_thetas.iter().enumerate() { + // get prepared thetas. only step different from original `compute_c_from_sigmas_and_thetas` + let mut prepared_thetas = Vec::new(); + for i in 0..ccs.q { + let prepared: Vec> = + ccs.S[i].iter().map(|j| thetas[*j].clone()).collect(); + prepared_thetas.push(prepared.to_vec()); + } + + let c_i = Vec::>::new_witness(cs.clone(), || Ok(ccs.c.clone())) + .unwrap(); + let lhs = Self::sum_ci_mul_prod_thetaj(c_i.clone(), prepared_thetas.clone())?; + + // compute gamma^(t+1) + let pow = mu.clone() * t.clone() + + FpVar::::new_witness(cs.clone(), || { + Ok(C::ScalarField::from(k as u64)) + })?; + let gamma_t1 = gamma.pow_le(&pow.to_bits_le()?)?; + + c += gamma_t1.clone() * e_2.clone() * lhs.clone(); + } + + Ok(c) + } +} + +#[cfg(test)] +mod tests { + use super::ComputeCFromSigmasAndThetasGadget; + use crate::{ + ccs::{ + tests::{get_test_ccs, get_test_z}, + CCS, + }, + folding::hypernova::utils::{ + compute_c_from_sigmas_and_thetas, compute_sigmas_and_thetas, sum_ci_mul_prod_thetaj, + sum_muls_gamma_pows_eq_sigma, + }, + pedersen::Pedersen, + utils::virtual_polynomial::eq_eval, + }; + use ark_pallas::{Fr, Projective}; + use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, R1CSVar}; + use ark_relations::r1cs::ConstraintSystem; + use ark_std::{test_rng, UniformRand}; + + #[test] + pub fn test_sum_muls_gamma_pow_eq_sigma_gadget() { + let mut rng = test_rng(); + let ccs: CCS = get_test_ccs(); + let z1 = get_test_z(3); + let z2 = get_test_z(4); + + let gamma: Fr = Fr::rand(&mut rng); + let r_x_prime: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); + + // Initialize a multifolding object + let pedersen_params = Pedersen::new_params(&mut rng, ccs.n - ccs.l - 1); + let (lcccs_instance, _) = ccs.to_lcccs(&mut rng, &pedersen_params, &z1).unwrap(); + let sigmas_thetas = + compute_sigmas_and_thetas(&ccs, &[z1.clone()], &[z2.clone()], &r_x_prime); + + let mut e_lcccs = Vec::new(); + for r_x in &vec![lcccs_instance.r_x] { + e_lcccs.push(eq_eval(r_x, &r_x_prime).unwrap()); + } + + // Initialize cs and gamma + let cs = ConstraintSystem::::new_ref(); + let gamma_var = FpVar::::new_witness(cs.clone(), || Ok(gamma)).unwrap(); + + for (i, sigmas) in sigmas_thetas.0.iter().enumerate() { + let expected = + sum_muls_gamma_pows_eq_sigma(gamma, e_lcccs[i], sigmas, (i * ccs.t) as u64); + let sigmas_var = + Vec::>::new_witness(cs.clone(), || Ok(sigmas.clone())).unwrap(); + let eq_var = FpVar::::new_witness(cs.clone(), || Ok(e_lcccs[i])).unwrap(); + let pow = + FpVar::::new_witness(cs.clone(), || Ok(Fr::from((i * ccs.t) as u64))).unwrap(); + let computed = + ComputeCFromSigmasAndThetasGadget::::sum_muls_gamma_pows_eq_sigma( + gamma_var.clone(), + eq_var, + sigmas_var, + pow, + ) + .unwrap(); + assert_eq!(expected, computed.value().unwrap()); + } + } + + #[test] + pub fn test_sum_ci_mul_prod_thetaj_gadget() { + let mut rng = test_rng(); + let ccs: CCS = get_test_ccs(); + let z1 = get_test_z(3); + let z2 = get_test_z(4); + + let r_x_prime: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); + + // Initialize a multifolding object + let pedersen_params = Pedersen::new_params(&mut rng, ccs.n - ccs.l - 1); + let (lcccs_instance, _) = ccs.to_lcccs(&mut rng, &pedersen_params, &z1).unwrap(); + let sigmas_thetas = + compute_sigmas_and_thetas(&ccs, &[z1.clone()], &[z2.clone()], &r_x_prime); + + let mut e_lcccs = Vec::new(); + for r_x in &vec![lcccs_instance.r_x] { + e_lcccs.push(eq_eval(r_x, &r_x_prime).unwrap()); + } + + // Initialize cs + let cs = ConstraintSystem::::new_ref(); + let vec_thetas = sigmas_thetas.1; + for (_, thetas) in vec_thetas.iter().enumerate() { + // sum c_i * prod theta_j + let expected = sum_ci_mul_prod_thetaj(&ccs, thetas); // from `compute_c_from_sigmas_and_thetas` + let mut prepared_thetas = Vec::new(); + for i in 0..ccs.q { + let prepared: Vec = ccs.S[i].iter().map(|j| thetas[*j]).collect(); + prepared_thetas + .push(Vec::>::new_witness(cs.clone(), || Ok(prepared)).unwrap()); + } + let computed = ComputeCFromSigmasAndThetasGadget::::sum_ci_mul_prod_thetaj( + Vec::>::new_witness(cs.clone(), || Ok(ccs.c.clone())).unwrap(), + prepared_thetas, + ) + .unwrap(); + assert_eq!(expected, computed.value().unwrap()); + } + } + + #[test] + pub fn test_compute_c_from_sigmas_and_thetas_gadget() { + let ccs: CCS = get_test_ccs(); + let z1 = get_test_z(3); + let z2 = get_test_z(4); + + let mut rng = test_rng(); + let gamma: Fr = Fr::rand(&mut rng); + let beta: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); + let r_x_prime: Vec = (0..ccs.s).map(|_| Fr::rand(&mut rng)).collect(); + + // Initialize a multifolding object + let pedersen_params = Pedersen::new_params(&mut rng, ccs.n - ccs.l - 1); + let (lcccs_instance, _) = ccs.to_lcccs(&mut rng, &pedersen_params, &z1).unwrap(); + let sigmas_thetas = + compute_sigmas_and_thetas(&ccs, &[z1.clone()], &[z2.clone()], &r_x_prime); + + let expected_c = compute_c_from_sigmas_and_thetas( + &ccs, + &sigmas_thetas, + gamma, + &beta, + &vec![lcccs_instance.r_x.clone()], + &r_x_prime, + ); + + let cs = ConstraintSystem::::new_ref(); + let mut vec_sigmas = Vec::new(); + let mut vec_thetas = Vec::new(); + for sigmas in sigmas_thetas.0 { + vec_sigmas + .push(Vec::>::new_witness(cs.clone(), || Ok(sigmas.clone())).unwrap()); + } + for thetas in sigmas_thetas.1 { + vec_thetas + .push(Vec::>::new_witness(cs.clone(), || Ok(thetas.clone())).unwrap()); + } + let vec_r_x = + vec![ + Vec::>::new_witness(cs.clone(), || Ok(lcccs_instance.r_x.clone())) + .unwrap(), + ]; + let vec_r_x_prime = + Vec::>::new_witness(cs.clone(), || Ok(r_x_prime.clone())).unwrap(); + let gamma_var = FpVar::::new_witness(cs.clone(), || Ok(gamma)).unwrap(); + let beta_var = Vec::>::new_witness(cs.clone(), || Ok(beta.clone())).unwrap(); + let computed_c = ComputeCFromSigmasAndThetasGadget::compute_c_from_sigmas_and_thetas( + cs, + &ccs, + vec_sigmas, + vec_thetas, + gamma_var, + beta_var, + vec_r_x, + vec_r_x_prime, + ) + .unwrap(); + + assert_eq!(expected_c, computed_c.value().unwrap()); + } +} diff --git a/src/folding/hypernova/mod.rs b/src/folding/hypernova/mod.rs index c890b968..54507794 100644 --- a/src/folding/hypernova/mod.rs +++ b/src/folding/hypernova/mod.rs @@ -1,5 +1,6 @@ /// Implements the scheme described in [HyperNova](https://eprint.iacr.org/2023/573.pdf) pub mod cccs; +pub mod circuit; pub mod lcccs; pub mod nimfs; pub mod utils; diff --git a/src/folding/hypernova/utils.rs b/src/folding/hypernova/utils.rs index 6974c5e8..0db62f13 100644 --- a/src/folding/hypernova/utils.rs +++ b/src/folding/hypernova/utils.rs @@ -86,6 +86,38 @@ pub fn compute_sigmas_and_thetas( SigmasThetas(sigmas, thetas) } +/// Computes the sum $\sum_{j = 0}^{n} \gamma^{\text{pow} + j} \cdot eq_eval \cdot \sigma_{j}$ +/// `pow` corresponds to `i * ccs.t` in `compute_c_from_sigmas_and_thetas` +pub fn sum_muls_gamma_pows_eq_sigma( + gamma: F, + eq_eval: F, + sigmas: &[F], + pow: u64, +) -> F { + let mut result = F::zero(); + for (j, sigma_j) in sigmas.iter().enumerate() { + let gamma_j = gamma.pow([(pow + (j as u64))]); + result += gamma_j * eq_eval * sigma_j; + } + result +} + +/// Computes $\sum_{i=1}^{q} c_i * \prod_{j \in S_i} theta_j$ +pub fn sum_ci_mul_prod_thetaj( + ccs: &CCS, + thetas: &[C::ScalarField], +) -> C::ScalarField { + let mut result = C::ScalarField::zero(); + for i in 0..ccs.q { + let mut prod = C::ScalarField::one(); + for j in ccs.S[i].clone() { + prod *= thetas[j]; + } + result += ccs.c[i] * prod; + } + result +} + /// Compute the right-hand-side of step 5 of the multifolding scheme pub fn compute_c_from_sigmas_and_thetas( ccs: &CCS, @@ -104,24 +136,14 @@ pub fn compute_c_from_sigmas_and_thetas( } for (i, sigmas) in vec_sigmas.iter().enumerate() { // (sum gamma^j * e_i * sigma_j) - for (j, sigma_j) in sigmas.iter().enumerate() { - let gamma_j = gamma.pow([(i * ccs.t + j) as u64]); - c += gamma_j * e_lcccs[i] * sigma_j; - } + c += sum_muls_gamma_pows_eq_sigma(gamma, e_lcccs[i], sigmas, (i * ccs.t) as u64); } let mu = vec_sigmas.len(); let e2 = eq_eval(beta, r_x_prime).unwrap(); for (k, thetas) in vec_thetas.iter().enumerate() { // + gamma^{t+1} * e2 * sum c_i * prod theta_j - let mut lhs = C::ScalarField::zero(); - for i in 0..ccs.q { - let mut prod = C::ScalarField::one(); - for j in ccs.S[i].clone() { - prod *= thetas[j]; - } - lhs += ccs.c[i] * prod; - } + let lhs = sum_ci_mul_prod_thetaj(ccs, thetas); let gamma_t1 = gamma.pow([(mu * ccs.t + k) as u64]); c += gamma_t1 * e2 * lhs; } From 7e3d2dfa4360068a3d594fdf91d6d6e3378b288c Mon Sep 17 00:00:00 2001 From: arnaucube Date: Tue, 16 Jan 2024 09:23:47 +0100 Subject: [PATCH 3/3] refactor frontend composition, update usage of arkworks CS helpers, refactor test circuits (#54) Changes: - get rid of `extract_r1cs_and_z` and `extract_z` - move `extract_r1cs` and `extract_w_x` from `frontend/arkworks` into `r1cs.rs` The reasoning: they are not methods needed for the Frontend interface, but only needed internally for the folding scheme to extract values from the AugmentedF circuit and similar. - set the `FCircuit` as the trait for the `src/frontend` - remove the `frontend/arkworks` since the `FCircuit` trait can be directly implemented without a middle layer - reorganize test circuits into `src/frontend/mod.rs`, updating them into `CubicFCircuit`: the typical x^3+x+5=y circuit `CustomFCircuit`: a circuit in which you can specify the number of constraints that it will take where both fulfill the `FCircuit` trait, and they are used for different tests being folded. --- src/ccs/r1cs.rs | 44 +++++++++ src/folding/nova/circuits.rs | 77 +++------------- src/folding/nova/decider.rs | 94 +++++++------------ src/folding/nova/ivc.rs | 13 +-- src/frontend/arkworks/mod.rs | 122 ------------------------- src/frontend/mod.rs | 169 ++++++++++++++++++++++++++++++++++- 6 files changed, 263 insertions(+), 256 deletions(-) delete mode 100644 src/frontend/arkworks/mod.rs diff --git a/src/ccs/r1cs.rs b/src/ccs/r1cs.rs index 3f909339..6e85ebd5 100644 --- a/src/ccs/r1cs.rs +++ b/src/ccs/r1cs.rs @@ -2,6 +2,7 @@ use ark_ff::PrimeField; use crate::utils::vec::*; use crate::Error; +use ark_relations::r1cs::ConstraintSystem; #[derive(Debug, Clone, Eq, PartialEq)] pub struct R1CS { @@ -71,10 +72,53 @@ impl RelaxedR1CS { } } +/// 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(); + + 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 + + let A = SparseMatrix:: { + n_rows, + n_cols, + coeffs: m.a, + }; + let B = SparseMatrix:: { + n_rows, + n_cols, + coeffs: m.b, + }; + let C = SparseMatrix:: { + n_rows, + n_cols, + coeffs: m.c, + }; + + R1CS:: { + l: cs.num_instance_variables - 1, // -1 to substract the first '1' + A, + B, + C, + } +} + +/// extracts the witness and the public inputs from arkworks ConstraintSystem. +pub fn extract_w_x(cs: &ConstraintSystem) -> (Vec, Vec) { + ( + cs.witness_assignment.clone(), + // skip the first element which is '1' + cs.instance_assignment[1..].to_vec(), + ) +} + #[cfg(test)] pub mod tests { use super::*; use crate::utils::vec::tests::{to_F_matrix, to_F_vec}; + + use ark_ff::PrimeField; use ark_pallas::Fr; pub fn get_test_r1cs() -> R1CS { diff --git a/src/folding/nova/circuits.rs b/src/folding/nova/circuits.rs index afa7cc58..109c605c 100644 --- a/src/folding/nova/circuits.rs +++ b/src/folding/nova/circuits.rs @@ -32,6 +32,7 @@ use super::{ }; use crate::constants::N_BITS_RO; use crate::folding::circuits::nonnative::{point_to_nonnative_limbs, NonNativeAffineVar}; +use crate::frontend::FCircuit; /// CF1 represents the ConstraintField used for the main Nova circuit which is over E1::Fr, where /// E1 is the main curve where we do the folding. @@ -231,31 +232,6 @@ where } } -/// FCircuit defines the trait of the circuit of the F function, which is the one being executed -/// inside the agmented F' function. -pub trait FCircuit: Clone + Copy + Debug { - /// returns a new FCircuit instance - fn new() -> Self; - - /// computes the next state values in place, assigning z_{i+1} into z_i, and - /// computing the new z_i - fn step_native( - // this method uses self, so that each FCircuit implementation (and different frontends) - // can hold a state if needed to store data to compute the next state. - self, - z_i: Vec, - ) -> Vec; - - /// generates the constraints for the step of F for the given z_i - fn generate_step_constraints( - // this method uses self, so that each FCircuit implementation (and different frontends) - // can hold a state if needed to store data to generate the constraints. - self, - cs: ConstraintSystemRef, - z_i: Vec>, - ) -> Result>, SynthesisError>; -} - /// AugmentedFCircuit implements the F' circuit (augmented F) defined in /// [Nova](https://eprint.iacr.org/2021/370.pdf) together with the extra constraints defined in /// [CycleFold](https://eprint.iacr.org/2023/1192.pdf). @@ -493,41 +469,15 @@ pub mod tests { use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; use tracing_subscriber::layer::SubscriberExt; + use crate::ccs::r1cs::{extract_r1cs, extract_w_x}; use crate::folding::nova::nifs::tests::prepare_simple_fold_inputs; use crate::folding::nova::{ ivc::get_committed_instance_coordinates, nifs::NIFS, traits::NovaR1CS, Witness, }; - use crate::frontend::arkworks::{extract_r1cs, extract_z}; + use crate::frontend::tests::CubicFCircuit; use crate::pedersen::Pedersen; use crate::transcript::poseidon::tests::poseidon_test_config; - #[derive(Clone, Copy, Debug)] - /// TestFCircuit is a variation of `x^3 + x + 5 = y` (as in - /// src/frontend/arkworks/mod.rs#tests::TestCircuit), adapted to have 2 public inputs which are - /// used as the state. `z_i` is used as `x`, and `z_{i+1}` is used as `y`, and at the next - /// step, `z_{i+1}` will be assigned to `z_i`, and a new `z+{i+1}` will be computted. - pub struct TestFCircuit { - _f: PhantomData, - } - impl FCircuit for TestFCircuit { - fn new() -> Self { - Self { _f: PhantomData } - } - fn step_native(self, z_i: Vec) -> Vec { - vec![z_i[0] * z_i[0] * z_i[0] + z_i[0] + F::from(5_u32)] - } - fn generate_step_constraints( - self, - cs: ConstraintSystemRef, - z_i: Vec>, - ) -> Result>, SynthesisError> { - let five = FpVar::::new_constant(cs.clone(), F::from(5u32))?; - let z_i = z_i[0].clone(); - - Ok(vec![&z_i * &z_i * &z_i + &z_i + &five]) - } - } - #[test] fn test_committed_instance_var() { let mut rng = ark_std::test_rng(); @@ -675,7 +625,7 @@ pub mod tests { } #[test] - /// test_augmented_f_circuit folds the TestFCircuit circuit in multiple iterations, feeding the + /// test_augmented_f_circuit folds the CubicFCircuit circuit in multiple iterations, feeding the /// values into the AugmentedFCircuit. fn test_augmented_f_circuit() { let mut layer = ConstraintLayer::default(); @@ -690,9 +640,9 @@ pub mod tests { let cs = ConstraintSystem::::new_ref(); // prepare the circuit to obtain its R1CS - let F_circuit = TestFCircuit::::new(); + let F_circuit = CubicFCircuit::::new(()); let mut augmented_F_circuit = - AugmentedFCircuit::>::empty( + AugmentedFCircuit::>::empty( &poseidon_config, F_circuit, ); @@ -703,9 +653,7 @@ pub mod tests { println!("num_constraints={:?}", cs.num_constraints()); let cs = cs.into_inner().unwrap(); let r1cs = extract_r1cs::(&cs); - let z = extract_z::(&cs); // includes 1 and public inputs - let (w, x) = r1cs.split_z(&z); - assert_eq!(z.len(), r1cs.A.n_cols); + let (w, x) = extract_w_x::(&cs); assert_eq!(1 + x.len() + w.len(), r1cs.A.n_cols); assert_eq!(r1cs.l, x.len()); @@ -748,7 +696,7 @@ pub mod tests { // base case augmented_F_circuit = - AugmentedFCircuit::> { + AugmentedFCircuit::> { _gc2: PhantomData, poseidon_config: poseidon_config.clone(), i: Some(i), // = 0 @@ -840,7 +788,7 @@ pub mod tests { .unwrap(); augmented_F_circuit = - AugmentedFCircuit::> { + AugmentedFCircuit::> { _gc2: PhantomData, poseidon_config: poseidon_config.clone(), i: Some(i), @@ -873,10 +821,7 @@ pub mod tests { cs.finalize(); let cs = cs.into_inner().unwrap(); - // notice that here we use 'Z' (uppercase) to denote the 'z-vector' as in the paper, - // not the value 'z' (lowercase) which is the state - let Z_i1 = extract_z::(&cs); - let (w_i1, x_i1) = r1cs.split_z(&Z_i1); + let (w_i1, x_i1) = extract_w_x::(&cs); assert_eq!(x_i1.len(), 1); assert_eq!(x_i1[0], u_i1_x); @@ -892,7 +837,7 @@ pub mod tests { i += Fr::one(); // advance the F circuit state z_i = z_i1.clone(); - z_i1 = F_circuit.step_native(z_i.clone()); + z_i1 = F_circuit.step_native(z_i.clone()).unwrap(); U_i = U_i1.clone(); W_i = W_i1.clone(); } diff --git a/src/folding/nova/decider.rs b/src/folding/nova/decider.rs index adafc01a..75022ef6 100644 --- a/src/folding/nova/decider.rs +++ b/src/folding/nova/decider.rs @@ -19,10 +19,11 @@ use core::{borrow::Borrow, marker::PhantomData}; use crate::ccs::r1cs::R1CS; use crate::folding::nova::{ - circuits::{CommittedInstanceVar, FCircuit, CF1, CF2}, + circuits::{CommittedInstanceVar, CF1, CF2}, ivc::IVC, CommittedInstance, Witness, }; +use crate::frontend::FCircuit; use crate::pedersen::Params as PedersenParams; use crate::utils::gadgets::{ hadamard, mat_vec_mul_sparse, vec_add, vec_scalar_mul, SparseMatrixVar, @@ -436,15 +437,15 @@ pub mod tests { use ark_relations::r1cs::ConstraintSystem; use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; - use crate::folding::nova::circuits::{tests::TestFCircuit, FCircuit}; use crate::folding::nova::ivc::IVC; + use crate::frontend::tests::{CubicFCircuit, CustomFCircuit, WrapperCircuit}; use crate::transcript::poseidon::tests::poseidon_test_config; + use crate::ccs::r1cs::{extract_r1cs, extract_w_x}; use crate::ccs::r1cs::{ tests::{get_test_r1cs, get_test_z}, R1CS, }; - use crate::frontend::arkworks::{extract_r1cs_and_z, tests::TestCircuit}; #[test] fn test_relaxed_r1cs_small_gadget_handcrafted() { @@ -474,7 +475,9 @@ pub mod tests { let cs = cs.into_inner().unwrap(); - let (r1cs, z) = extract_r1cs_and_z::(&cs); + let r1cs = extract_r1cs::(&cs); + let (w, x) = extract_w_x::(&cs); + let z = [vec![Fr::one()], x, w].concat(); r1cs.check_relation(&z).unwrap(); let relaxed_r1cs = r1cs.clone().relax(); @@ -494,9 +497,14 @@ pub mod tests { #[test] fn test_relaxed_r1cs_small_gadget_arkworks() { - let x = Fr::from(5_u32); - let y = x * x * x + x + Fr::from(5_u32); - let circuit = TestCircuit:: { x, y }; + let z_i = vec![Fr::from(3_u32)]; + let cubic_circuit = CubicFCircuit::::new(()); + let circuit = WrapperCircuit::> { + FC: cubic_circuit, + z_i: Some(z_i.clone()), + z_i1: Some(cubic_circuit.step_native(z_i).unwrap()), + }; + test_relaxed_r1cs_gadget(circuit); } @@ -529,59 +537,15 @@ pub mod tests { test_relaxed_r1cs_gadget(circuit); } - // circuit that has the number of constraints specified in the `n_constraints` parameter. Note - // that the generated circuit will have very sparse matrices, so the resulting constraints - // number of the RelaxedR1CS gadget must take that into account. - struct CustomTestCircuit { - _f: PhantomData, - pub n_constraints: usize, - pub x: F, - pub y: F, - } - impl CustomTestCircuit { - fn new(n_constraints: usize) -> Self { - let x = F::from(5_u32); - let mut y = F::one(); - for _ in 0..n_constraints - 1 { - y *= x; - } - Self { - _f: PhantomData, - n_constraints, - x, - y, - } - } - } - impl ConstraintSynthesizer for CustomTestCircuit { - fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { - let x = FpVar::::new_witness(cs.clone(), || Ok(self.x))?; - let y = FpVar::::new_input(cs.clone(), || Ok(self.y))?; - - let mut comp_y = FpVar::::new_witness(cs.clone(), || Ok(F::one()))?; - for _ in 0..self.n_constraints - 1 { - comp_y *= x.clone(); - } - - comp_y.enforce_equal(&y)?; - Ok(()) - } - } - #[test] fn test_relaxed_r1cs_custom_circuit() { let n_constraints = 10_000; - let x = Fr::from(5_u32); - let mut y = Fr::one(); - for _ in 0..n_constraints - 1 { - y *= x; - } - - let circuit = CustomTestCircuit:: { - _f: PhantomData, - n_constraints, - x, - y, + let custom_circuit = CustomFCircuit::::new(n_constraints); + 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(z_i).unwrap()), }; test_relaxed_r1cs_gadget(circuit); } @@ -592,11 +556,19 @@ pub mod tests { // 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 circuit = CustomTestCircuit::::new(10); + let custom_circuit = CustomFCircuit::::new(10); + 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(z_i).unwrap()), + }; circuit.generate_constraints(cs.clone()).unwrap(); cs.finalize(); let cs = cs.into_inner().unwrap(); - let (r1cs, z) = extract_r1cs_and_z::(&cs); + let r1cs = extract_r1cs::(&cs); + let (w, x) = extract_w_x::(&cs); + let z = [vec![Fq::one()], x, w].concat(); let relaxed_r1cs = r1cs.clone().relax(); @@ -629,11 +601,11 @@ pub mod tests { let mut rng = ark_std::test_rng(); let poseidon_config = poseidon_test_config::(); - let F_circuit = TestFCircuit::::new(); + let F_circuit = CubicFCircuit::::new(()); let z_0 = vec![Fr::from(3_u32)]; // generate an IVC and do a step of it - let mut ivc = IVC::>::new( + let mut ivc = IVC::>::new( &mut rng, poseidon_config, F_circuit, diff --git a/src/folding/nova/ivc.rs b/src/folding/nova/ivc.rs index 2c4f156c..287bf742 100644 --- a/src/folding/nova/ivc.rs +++ b/src/folding/nova/ivc.rs @@ -8,12 +8,13 @@ use ark_std::{One, Zero}; use core::marker::PhantomData; use super::{ - circuits::{AugmentedFCircuit, ChallengeGadget, FCircuit, CF2}, + circuits::{AugmentedFCircuit, ChallengeGadget, CF2}, cyclefold::{CycleFoldChallengeGadget, CycleFoldCircuit}, }; use super::{nifs::NIFS, traits::NovaR1CS, CommittedInstance, Witness}; use crate::ccs::r1cs::R1CS; -use crate::frontend::arkworks::{extract_r1cs, extract_w_x}; // TODO once Frontend trait is ready, use that +use crate::ccs::r1cs::{extract_r1cs, extract_w_x}; +use crate::frontend::FCircuit; use crate::pedersen::{Params as PedersenParams, Pedersen}; use crate::Error; @@ -138,7 +139,7 @@ where let augmented_F_circuit: AugmentedFCircuit; let cf_circuit: CycleFoldCircuit; - let z_i1 = self.F.step_native(self.z_i.clone()); + let z_i1 = self.F.step_native(self.z_i.clone())?; // compute T and cmT for AugmentedFCircuit let (T, cmT) = self.compute_cmT()?; @@ -397,7 +398,7 @@ mod tests { use ark_pallas::{constraints::GVar, Fr, Projective}; use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; - use crate::folding::nova::circuits::tests::TestFCircuit; + use crate::frontend::tests::CubicFCircuit; use crate::transcript::poseidon::tests::poseidon_test_config; #[test] @@ -405,10 +406,10 @@ mod tests { let mut rng = ark_std::test_rng(); let poseidon_config = poseidon_test_config::(); - let F_circuit = TestFCircuit::::new(); + let F_circuit = CubicFCircuit::::new(()); let z_0 = vec![Fr::from(3_u32)]; - let mut ivc = IVC::>::new( + let mut ivc = IVC::>::new( &mut rng, poseidon_config, F_circuit, diff --git a/src/frontend/arkworks/mod.rs b/src/frontend/arkworks/mod.rs deleted file mode 100644 index 5ef39243..00000000 --- a/src/frontend/arkworks/mod.rs +++ /dev/null @@ -1,122 +0,0 @@ -/// arkworks frontend -use ark_ff::PrimeField; -use ark_relations::r1cs::ConstraintSystem; - -use crate::ccs::r1cs::R1CS; -use crate::utils::vec::SparseMatrix; - -/// extracts arkworks ConstraintSystem matrices into crate::utils::vec::SparseMatrix format, and -/// extracts public inputs and witness into z vector. Returns a tuple containing (R1CS, z). -pub fn extract_r1cs_and_z(cs: &ConstraintSystem) -> (R1CS, Vec) { - let r1cs = extract_r1cs(cs); - - // z = (1, x, w) - let z = extract_z(cs); - - (r1cs, z) -} - -/// 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(); - - 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 - - let A = SparseMatrix:: { - n_rows, - n_cols, - coeffs: m.a, - }; - let B = SparseMatrix:: { - n_rows, - n_cols, - coeffs: m.b, - }; - let C = SparseMatrix:: { - n_rows, - n_cols, - coeffs: m.c, - }; - - R1CS:: { - l: cs.num_instance_variables - 1, // -1 to substract the first '1' - A, - B, - C, - } -} - -/// extracts public inputs and witness into z vector from arkworks ConstraintSystem. -pub fn extract_z(cs: &ConstraintSystem) -> Vec { - // z = (1, x, w) - [ - // 1 is already included in cs.instance_assignment - cs.instance_assignment.clone(), - cs.witness_assignment.clone(), - ] - .concat() -} - -/// extracts the witness and the public inputs from arkworks ConstraintSystem. -pub fn extract_w_x(cs: &ConstraintSystem) -> (Vec, Vec) { - ( - cs.witness_assignment.clone(), - // skip the first element which is '1' - cs.instance_assignment[1..].to_vec(), - ) -} - -#[cfg(test)] -pub mod tests { - use super::*; - use ark_ff::PrimeField; - use ark_pallas::Fr; - use ark_r1cs_std::{alloc::AllocVar, eq::EqGadget, fields::fp::FpVar}; - use ark_relations::r1cs::{ - ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, - }; - - // TestCircuit implements the R1CS for: x^3 + x + 5 = y (example from article - // https://www.vitalik.ca/general/2016/12/10/qap.html ) - #[derive(Clone, Copy, Debug)] - pub struct TestCircuit { - pub x: F, - pub y: F, - } - impl ConstraintSynthesizer for TestCircuit { - fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { - let x = FpVar::::new_input(cs.clone(), || Ok(self.x))?; - let y = FpVar::::new_witness(cs.clone(), || Ok(self.y))?; - let five = FpVar::::new_constant(cs.clone(), F::from(5u32))?; - - let comp_y = (&x * &x * &x) + &x + &five; - comp_y.enforce_equal(&y)?; - Ok(()) - } - } - - #[test] - fn test_cs_to_matrices() { - let cs = ConstraintSystem::::new_ref(); - - let x = Fr::from(5_u32); - let y = x * x * x + x + Fr::from(5_u32); - - let test_circuit = TestCircuit:: { x, y }; - test_circuit.generate_constraints(cs.clone()).unwrap(); - cs.finalize(); - assert!(cs.is_satisfied().unwrap()); - - let cs = cs.into_inner().unwrap(); - - // ensure that num_instance_variables is 2: 1 + 1 public input - assert_eq!(cs.num_instance_variables, 2); - - let (r1cs, z) = extract_r1cs_and_z::(&cs); - r1cs.check_relation(&z).unwrap(); - // ensure that number of public inputs (l) is 1 - assert_eq!(r1cs.l, 1); - } -} diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index 80a1046b..dda6ab57 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -1,2 +1,169 @@ -pub mod arkworks; pub mod circom; + +use crate::Error; +use ark_ff::PrimeField; +use ark_r1cs_std::fields::fp::FpVar; +use ark_relations::r1cs::{ConstraintSystemRef, SynthesisError}; +use ark_std::fmt::Debug; + +/// FCircuit defines the trait of the circuit of the F function, which is the one being folded (ie. +/// inside the agmented F' function). +pub trait FCircuit: Clone + Copy + Debug { + type Params: Debug; + + /// returns a new FCircuit instance + fn new(params: Self::Params) -> Self; + + /// computes the next state values in place, assigning z_{i+1} into z_i, and computing the new + /// z_{i+1} + fn step_native( + // this method uses self, so that each FCircuit implementation (and different frontends) + // can hold a state if needed to store data to compute the next state. + self, + z_i: Vec, + ) -> Result, Error>; + + /// generates the constraints for the step of F for the given z_i + fn generate_step_constraints( + // this method uses self, so that each FCircuit implementation (and different frontends) + // can hold a state if needed to store data to generate the constraints. + self, + cs: ConstraintSystemRef, + z_i: Vec>, + ) -> Result>, SynthesisError>; +} + +#[cfg(test)] +pub mod tests { + use super::*; + use ark_pallas::Fr; + use ark_r1cs_std::{alloc::AllocVar, eq::EqGadget}; + use ark_relations::r1cs::{ + ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, + }; + use core::marker::PhantomData; + + /// CubicFCircuit is a struct that implements the FCircuit trait, for the R1CS example circuit + /// from https://www.vitalik.ca/general/2016/12/10/qap.html, which checks `x^3 + x + 5 = y`. It + /// has 2 public inputs which are used as the state. `z_i` is used as `x`, and `z_{i+1}` is + /// used as `y`, and at the next step, `z_{i+1}` will be assigned to `z_i`, and a new `z+{i+1}` + /// will be computted. + #[derive(Clone, Copy, Debug)] + pub struct CubicFCircuit { + _f: PhantomData, + } + impl FCircuit for CubicFCircuit { + type Params = (); + fn new(_params: Self::Params) -> Self { + Self { _f: PhantomData } + } + fn step_native(self, z_i: Vec) -> Result, Error> { + Ok(vec![z_i[0] * z_i[0] * z_i[0] + z_i[0] + F::from(5_u32)]) + } + fn generate_step_constraints( + self, + cs: ConstraintSystemRef, + z_i: Vec>, + ) -> Result>, SynthesisError> { + let five = FpVar::::new_constant(cs.clone(), F::from(5u32))?; + let z_i = z_i[0].clone(); + + Ok(vec![&z_i * &z_i * &z_i + &z_i + &five]) + } + } + + /// CustomFCircuit is a circuit that has the number of constraints specified in the + /// `n_constraints` parameter. Note that the generated circuit will have very sparse matrices. + #[derive(Clone, Copy, Debug)] + pub struct CustomFCircuit { + _f: PhantomData, + pub n_constraints: usize, + } + impl FCircuit for CustomFCircuit { + type Params = usize; + + fn new(params: Self::Params) -> Self { + Self { + _f: PhantomData, + n_constraints: params, + } + } + fn step_native(self, z_i: Vec) -> Result, Error> { + let mut z_i1 = F::one(); + for _ in 0..self.n_constraints - 1 { + z_i1 *= z_i[0]; + } + Ok(vec![z_i1]) + } + fn generate_step_constraints( + self, + cs: ConstraintSystemRef, + z_i: Vec>, + ) -> Result>, SynthesisError> { + let mut z_i1 = FpVar::::new_witness(cs.clone(), || Ok(F::one()))?; + for _ in 0..self.n_constraints - 1 { + z_i1 *= z_i[0].clone(); + } + + Ok(vec![z_i1]) + } + } + + /// WrapperCircuit is a circuit that wraps any circuit that implements the FCircuit trait. This + /// is used to test the `FCircuit.generate_step_constraints` method. This is a similar wrapping + /// than the one done in the `AugmentedFCircuit`, but without adding all the extra constraints + /// of the AugmentedF circuit logic, in order to run lighter tests when we're not interested in + /// the the AugmentedF logic but in the wrapping of the circuits. + pub struct WrapperCircuit> { + pub FC: FC, // F circuit + pub z_i: Option>, + pub z_i1: Option>, + } + impl ConstraintSynthesizer for WrapperCircuit + where + F: PrimeField, + FC: FCircuit, + { + fn generate_constraints(self, cs: ConstraintSystemRef) -> Result<(), SynthesisError> { + let z_i = Vec::>::new_witness(cs.clone(), || { + Ok(self.z_i.unwrap_or(vec![F::zero()])) + })?; + let z_i1 = Vec::>::new_input(cs.clone(), || { + Ok(self.z_i1.unwrap_or(vec![F::zero()])) + })?; + let computed_z_i1 = self.FC.generate_step_constraints(cs.clone(), z_i.clone())?; + + computed_z_i1.enforce_equal(&z_i1)?; + Ok(()) + } + } + + #[test] + fn test_testfcircuit() { + let cs = ConstraintSystem::::new_ref(); + let F_circuit = CubicFCircuit::::new(()); + + let wrapper_circuit = WrapperCircuit::> { + FC: F_circuit, + z_i: Some(vec![Fr::from(3_u32)]), + z_i1: Some(vec![Fr::from(35_u32)]), + }; + wrapper_circuit.generate_constraints(cs.clone()).unwrap(); + assert_eq!(cs.num_constraints(), 3); + } + + #[test] + fn test_customtestfcircuit() { + let cs = ConstraintSystem::::new_ref(); + let n_constraints = 1000; + let custom_circuit = CustomFCircuit::::new(n_constraints); + let z_i = vec![Fr::from(5_u32)]; + let wrapper_circuit = WrapperCircuit::> { + FC: custom_circuit, + z_i: Some(z_i.clone()), + z_i1: Some(custom_circuit.step_native(z_i).unwrap()), + }; + wrapper_circuit.generate_constraints(cs.clone()).unwrap(); + assert_eq!(cs.num_constraints(), n_constraints); + } +}