diff --git a/Cargo.toml b/Cargo.toml index 1392665..f3108ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ ark-std = "^0.4.0" ark-crypto-primitives = { version = "^0.4.0", default-features = false, features = ["r1cs", "sponge", "crh"] } ark-relations = { version = "^0.4.0", default-features = false } ark-r1cs-std = { default-features = false } # use latest version from the patch +ark-serialize = "^0.4.0" ark-circom = { git = "https://github.com/gakonst/ark-circom.git" } thiserror = "1.0" rayon = "1.7.0" @@ -18,12 +19,11 @@ num-bigint = "0.4" color-eyre = "=0.6.2" # tmp imports for espresso's sumcheck -ark-serialize = "^0.4.0" espresso_subroutines = {git="https://github.com/EspressoSystems/hyperplonk", package="subroutines"} [dev-dependencies] ark-pallas = {version="0.4.0", features=["r1cs"]} -ark-vesta = {version="0.4.0"} +ark-vesta = {version="0.4.0", features=["r1cs"]} ark-bn254 = "0.4.0" tracing = { version = "0.1", default-features = false, features = [ "attributes" ] } tracing-subscriber = { version = "0.2" } diff --git a/src/ccs/mod.rs b/src/ccs/mod.rs index 3f1270c..9dd939d 100644 --- a/src/ccs/mod.rs +++ b/src/ccs/mod.rs @@ -51,7 +51,7 @@ impl CCS { // complete the hadamard chain let mut hadamard_result = vec![C::ScalarField::one(); self.m]; for M_j in vec_M_j.into_iter() { - hadamard_result = hadamard(&hadamard_result, &mat_vec_mul_sparse(M_j, z))?; + hadamard_result = hadamard(&hadamard_result, &mat_vec_mul_sparse(M_j, z)?)?; } // multiply by the coefficient of this step diff --git a/src/ccs/r1cs.rs b/src/ccs/r1cs.rs index e31df3e..3f90933 100644 --- a/src/ccs/r1cs.rs +++ b/src/ccs/r1cs.rs @@ -19,9 +19,9 @@ impl R1CS { /// check that a R1CS structure is satisfied by a z vector. Only for testing. pub fn check_relation(&self, z: &[F]) -> Result<(), Error> { - let Az = mat_vec_mul_sparse(&self.A, z); - let Bz = mat_vec_mul_sparse(&self.B, z); - let Cz = mat_vec_mul_sparse(&self.C, z); + let Az = mat_vec_mul_sparse(&self.A, z)?; + let Bz = mat_vec_mul_sparse(&self.B, z)?; + let Cz = mat_vec_mul_sparse(&self.C, z)?; let AzBz = hadamard(&Az, &Bz)?; if AzBz != Cz { return Err(Error::NotSatisfied); @@ -57,12 +57,12 @@ pub struct RelaxedR1CS { impl RelaxedR1CS { /// check that a RelaxedR1CS structure is satisfied by a z vector. Only for testing. pub fn check_relation(&self, z: &[F]) -> Result<(), Error> { - let Az = mat_vec_mul_sparse(&self.A, z); - let Bz = mat_vec_mul_sparse(&self.B, z); - let Cz = mat_vec_mul_sparse(&self.C, z); + let Az = mat_vec_mul_sparse(&self.A, z)?; + let Bz = mat_vec_mul_sparse(&self.B, z)?; + let Cz = mat_vec_mul_sparse(&self.C, z)?; let uCz = vec_scalar_mul(&Cz, &self.u); - let uCzE = vec_add(&uCz, &self.E); - let AzBz = hadamard(&Az, &Bz); + let uCzE = vec_add(&uCz, &self.E)?; + let AzBz = hadamard(&Az, &Bz)?; if AzBz != uCzE { return Err(Error::NotSatisfied); } diff --git a/src/constants.rs b/src/constants.rs index 597fabb..742c6c7 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,3 +1,5 @@ -// used for committed instances hash, so when going to the other curve of the cycle it does not -// overflow the scalar field -pub const N_BITS_HASH: usize = 250; +// used for the RO challenges. +// From [Srinath Setty](research.microsoft.com/en-us/people/srinath/): In Nova, soundness error ≤ +// 2/|S|, where S is the subset of the field F from which the challenges are drawn. In this case, +// we keep the size of S close to 2^128. +pub const N_BITS_RO: usize = 128; diff --git a/src/folding/circuits/cyclefold.rs b/src/folding/circuits/cyclefold.rs deleted file mode 100644 index 83709e4..0000000 --- a/src/folding/circuits/cyclefold.rs +++ /dev/null @@ -1,65 +0,0 @@ -/// Implements the C_{EC} circuit described in [CycleFold paper](https://eprint.iacr.org/2023/1192.pdf) -use ark_ec::CurveGroup; -use ark_r1cs_std::{boolean::Boolean, prelude::CurveVar}; -use ark_relations::r1cs::SynthesisError; -use core::marker::PhantomData; - -use super::CF; - -/// ECRLC implements gadget that checks the Elliptic Curve points RandomLinearCombination described -/// in [CycleFold](https://eprint.iacr.org/2023/1192.pdf). -#[derive(Debug)] -pub struct ECRLC>> { - _c: PhantomData, - _gc: PhantomData, -} -impl>> ECRLC { - pub fn check( - // get r in bits format, so it can be reused across many instances of ECRLC gadget, - // reducing the number of constraints needed - r_bits: Vec>>, - p1: GC, - p2: GC, - p3: GC, - ) -> Result>, SynthesisError> { - p3.is_eq(&(p1 + p2.scalar_mul_le(r_bits.iter())?)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ark_ff::{BigInteger, PrimeField}; - use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; - use ark_r1cs_std::{alloc::AllocVar, eq::EqGadget}; - use ark_relations::r1cs::ConstraintSystem; - use ark_std::UniformRand; - use std::ops::Mul; - - /// Let Curve1=pallas and Curve2=vesta. Here our constraints system will work over Curve2::Fr = - /// vesta::Fr (=pallas::Fq), thus our points are P_i \in Curve1 (=pasta). - #[test] - fn test_ecrlc_check() { - let mut rng = ark_std::test_rng(); - - let r = Fr::rand(&mut rng); - let p1 = Projective::rand(&mut rng); - let p2 = Projective::rand(&mut rng); - let p3 = p1 + p2.mul(r); - - let cs = ConstraintSystem::::new_ref(); // CS over Curve2::Fr = Curve1::Fq - - // prepare circuit inputs - let rbitsVar: Vec> = - Vec::new_witness(cs.clone(), || Ok(r.into_bigint().to_bits_le())).unwrap(); - - let p1Var = GVar::new_witness(cs.clone(), || Ok(p1)).unwrap(); - let p2Var = GVar::new_witness(cs.clone(), || Ok(p2)).unwrap(); - let p3Var = GVar::new_witness(cs.clone(), || Ok(p3)).unwrap(); - - // check ECRLC circuit - let check_pass = ECRLC::::check(rbitsVar, p1Var, p2Var, p3Var).unwrap(); - check_pass.enforce_equal(&Boolean::::TRUE).unwrap(); - assert!(cs.is_satisfied().unwrap()); - } -} diff --git a/src/folding/circuits/mod.rs b/src/folding/circuits/mod.rs index f3691d1..ab2aab7 100644 --- a/src/folding/circuits/mod.rs +++ b/src/folding/circuits/mod.rs @@ -2,7 +2,6 @@ use ark_ec::CurveGroup; use ark_ff::Field; -pub mod cyclefold; pub mod nonnative; // CF represents the constraints field diff --git a/src/folding/circuits/nonnative.rs b/src/folding/circuits/nonnative.rs index 3424067..6d964ee 100644 --- a/src/folding/circuits/nonnative.rs +++ b/src/folding/circuits/nonnative.rs @@ -33,11 +33,9 @@ where let cs = cs.into(); let affine = val.borrow().into_affine(); - let xy_obj = &affine.xy(); - let mut xy = (&C::BaseField::zero(), &C::BaseField::one()); - if xy_obj.is_some() { - xy = xy_obj.unwrap(); - } + let zero_point = (&C::BaseField::zero(), &C::BaseField::one()); + let xy = affine.xy().unwrap_or(zero_point); + let x = NonNativeFieldVar::::new_variable( cs.clone(), || Ok(xy.0), @@ -56,8 +54,8 @@ where } } -/// point_to_nonnative_limbs is used to return (outside the circuit) the limbs representation that -/// matches the one used in-circuit. +/// point_to_nonnative_limbs is used to compute (outside the circuit) the limbs representation of a +/// point that matches the one used in-circuit. #[allow(clippy::type_complexity)] pub fn point_to_nonnative_limbs( p: C, diff --git a/src/folding/nova/circuits.rs b/src/folding/nova/circuits.rs index 9e4ba02..a15e7f3 100644 --- a/src/folding/nova/circuits.rs +++ b/src/folding/nova/circuits.rs @@ -1,40 +1,49 @@ +/// contains [Nova](https://eprint.iacr.org/2021/370.pdf) related circuits use ark_crypto_primitives::crh::{ poseidon::constraints::{CRHGadget, CRHParametersVar}, - poseidon::CRH, - CRHScheme, CRHSchemeGadget, + CRHSchemeGadget, +}; +use ark_crypto_primitives::sponge::{ + constraints::CryptographicSpongeVar, + poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, + Absorb, CryptographicSponge, }; -use ark_crypto_primitives::sponge::{poseidon::PoseidonConfig, Absorb}; use ark_ec::{AffineRepr, CurveGroup, Group}; use ark_ff::{Field, PrimeField}; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, boolean::Boolean, eq::EqGadget, - fields::{fp::FpVar, FieldVar}, + fields::{fp::FpVar, nonnative::NonNativeFieldVar, FieldVar}, groups::GroupOpsBounds, prelude::CurveVar, + ToBitsGadget, ToConstraintFieldGadget, }; use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::fmt::Debug; -use ark_std::Zero; +use ark_std::{One, Zero}; use core::{borrow::Borrow, marker::PhantomData}; -use super::CommittedInstance; -use crate::folding::circuits::{ - cyclefold::ECRLC, - nonnative::{point_to_nonnative_limbs, NonNativeAffineVar}, +use super::{ + cyclefold::{ + CycleFoldChallengeGadget, CycleFoldCommittedInstanceVar, NIFSFullGadget, CF_IO_LEN, + }, + CommittedInstance, }; +use crate::constants::N_BITS_RO; +use crate::folding::circuits::nonnative::{point_to_nonnative_limbs, NonNativeAffineVar}; /// 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. pub type CF1 = <::Affine as AffineRepr>::ScalarField; /// CF2 represents the ConstraintField used for the CycleFold circuit which is over E2::Fr=E1::Fq, /// where E2 is the auxiliary curve (from [CycleFold](https://eprint.iacr.org/2023/1192.pdf) -/// approach) where we check the folding of the commitments. +/// approach) where we check the folding of the commitments (elliptic curve points). pub type CF2 = <::BaseField as Field>::BasePrimeField; /// CommittedInstanceVar contains the u, x, cmE and cmW values which are folded on the main Nova -/// constraints field (E1::Fr, where E1 is the main curve). +/// constraints field (E1::Fr, where E1 is the main curve). The peculiarity is that cmE and cmW are +/// represented non-natively over the constraint field. #[derive(Debug, Clone)] pub struct CommittedInstanceVar { u: FpVar, @@ -108,43 +117,6 @@ where } } -/// CommittedInstanceCycleFoldVar represents the commitments to E and W from the CommittedInstance -/// on the E2, which are folded on the auxiliary curve constraints field (E2::Fr = E1::Fq). -pub struct CommittedInstanceCycleFoldVar>> -where - for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, -{ - _c: PhantomData, - cmE: GC, - cmW: GC, -} - -impl AllocVar, CF2> for CommittedInstanceCycleFoldVar -where - C: CurveGroup, - GC: CurveVar>, - for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, -{ - fn new_variable>>( - cs: impl Into>>, - f: impl FnOnce() -> Result, - mode: AllocationMode, - ) -> Result { - f().and_then(|val| { - let cs = cs.into(); - - let cmE = GC::new_variable(cs.clone(), || Ok(val.borrow().cmE), mode)?; - let cmW = GC::new_variable(cs.clone(), || Ok(val.borrow().cmW), mode)?; - - Ok(Self { - _c: PhantomData, - cmE, - cmW, - }) - }) - } -} - /// Implements the circuit that does the checks of the Non-Interactive Folding Scheme Verifier /// described in section 4 of [Nova](https://eprint.iacr.org/2021/370.pdf), where the cmE & cmW checks are /// delegated to the NIFSCycleFoldGadget. @@ -180,96 +152,12 @@ where } } -/// NIFSCycleFoldGadget performs the Nova NIFS.V elliptic curve points relation checks in the other -/// curve following [CycleFold](https://eprint.iacr.org/2023/1192.pdf). -pub struct NIFSCycleFoldGadget>> { +/// ChallengeGadget computes the RO challenge used for the Nova instances NIFS, it contains a +/// rust-native and a in-circuit compatible versions. +pub struct ChallengeGadget { _c: PhantomData, - _gc: PhantomData, } -impl>> NIFSCycleFoldGadget -where - C: CurveGroup, - GC: CurveVar>, - for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, -{ - pub fn verify( - r_bits: Vec>>, - cmT: GC, - ci1: CommittedInstanceCycleFoldVar, - ci2: CommittedInstanceCycleFoldVar, - ci3: CommittedInstanceCycleFoldVar, - ) -> Result>, SynthesisError> { - // cm(E) check: ci3.cmE == ci1.cmE + r * cmT + r^2 * ci2.cmE - let first_check = ci3.cmE.is_eq( - &((ci2.cmE.scalar_mul_le(r_bits.iter())? + cmT).scalar_mul_le(r_bits.iter())? - + ci1.cmE), - )?; - // cm(W) check: ci3.cmW == ci1.cmW + r * ci2.cmW - let second_check = ECRLC::::check(r_bits, ci1.cmW, ci2.cmW, ci3.cmW)?; - - first_check.and(&second_check) - } -} - -/// 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). -#[derive(Debug, Clone)] -pub struct AugmentedFCircuit>> { - pub poseidon_config: PoseidonConfig>, - pub i: Option>, - pub z_0: Option>, - pub z_i: Option>, - pub u_i: Option>, - pub U_i: Option>, - pub U_i1: Option>, - pub cmT: Option, - pub F: FC, // F circuit - pub x: Option>, // public inputs (u_{i+1}.x) -} - -impl>> AugmentedFCircuit { - pub fn empty(poseidon_config: &PoseidonConfig>, F_circuit: FC) -> Self { - Self { - poseidon_config: poseidon_config.clone(), - i: None, - z_0: None, - z_i: None, - u_i: None, - U_i: None, - U_i1: None, - cmT: None, - F: F_circuit, - x: None, - } - } -} - -impl>> AugmentedFCircuit +impl ChallengeGadget where C: CurveGroup, ::BaseField: PrimeField, @@ -280,13 +168,14 @@ where u_i: CommittedInstance, U_i: CommittedInstance, cmT: C, - ) -> Result { + ) -> Result, SynthesisError> { let (u_cmE_x, u_cmE_y) = point_to_nonnative_limbs::(u_i.cmE)?; let (u_cmW_x, u_cmW_y) = point_to_nonnative_limbs::(u_i.cmW)?; let (U_cmE_x, U_cmE_y) = point_to_nonnative_limbs::(U_i.cmE)?; let (U_cmW_x, U_cmW_y) = point_to_nonnative_limbs::(U_i.cmW)?; let (cmT_x, cmT_y) = point_to_nonnative_limbs::(cmT)?; + let mut sponge = PoseidonSponge::::new(poseidon_config); let input = vec![ vec![u_i.u], u_i.x.clone(), @@ -304,16 +193,22 @@ where cmT_y, ] .concat(); - Ok(CRH::::evaluate(poseidon_config, input).unwrap()) + sponge.absorb(&input); + let bits = sponge.squeeze_bits(N_BITS_RO); + Ok(bits) } - pub fn get_challenge( - crh_params: &CRHParametersVar, + // compatible with the native get_challenge_native + pub fn get_challenge_gadget( + cs: ConstraintSystemRef, + poseidon_config: &PoseidonConfig, u_i: CommittedInstanceVar, U_i: CommittedInstanceVar, cmT: NonNativeAffineVar, - ) -> Result, SynthesisError> { - let input = vec![ + ) -> Result>, SynthesisError> { + let mut sponge = PoseidonSpongeVar::::new(cs, poseidon_config); + + let input: Vec> = vec![ vec![u_i.u.clone()], u_i.x.clone(), u_i.cmE.x, @@ -330,55 +225,148 @@ where cmT.y, ] .concat(); - CRHGadget::::evaluate(crh_params, &input) + sponge.absorb(&input)?; + let bits = sponge.squeeze_bits(N_BITS_RO)?; + Ok(bits) } } -impl>> ConstraintSynthesizer> for AugmentedFCircuit +/// 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). +#[derive(Debug, Clone)] +pub struct AugmentedFCircuit< + C1: CurveGroup, + C2: CurveGroup, + GC2: CurveVar>, + FC: FCircuit>, +> where + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, +{ + pub _gc2: PhantomData, + pub poseidon_config: PoseidonConfig>, + pub i: Option>, + pub z_0: Option>, + pub z_i: Option>, + pub u_i: Option>, + pub U_i: Option>, + pub U_i1: Option>, + pub cmT: Option, + pub F: FC, // F circuit + pub x: Option>, // public inputs (u_{i+1}.x) + + // cyclefold verifier on C1 + pub cf_u_i: Option>, + pub cf_U_i: Option>, + pub cf_U_i1: Option>, + pub cf_cmT: Option, + pub cf_r_nonnat: Option, +} + +impl>, FC: FCircuit>> + AugmentedFCircuit where - C: CurveGroup, - ::BaseField: PrimeField, - ::ScalarField: Absorb, + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { - fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { - 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_else(|| vec![CF1::::zero()])) + pub fn empty(poseidon_config: &PoseidonConfig>, F_circuit: FC) -> Self { + Self { + _gc2: PhantomData, + poseidon_config: poseidon_config.clone(), + i: None, + z_0: None, + z_i: None, + u_i: None, + U_i: None, + U_i1: None, + cmT: None, + F: F_circuit, + x: None, + // cyclefold values + cf_u_i: None, + cf_U_i: None, + cf_U_i1: None, + cf_cmT: None, + cf_r_nonnat: None, + } + } +} + +impl ConstraintSynthesizer> for AugmentedFCircuit +where + C1: CurveGroup, + C2: CurveGroup, + GC2: CurveVar>, + FC: FCircuit>, + ::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 i = FpVar::>::new_witness(cs.clone(), || { + Ok(self.i.unwrap_or_else(CF1::::zero)) })?; - let z_i = Vec::>>::new_witness(cs.clone(), || { - Ok(self.z_i.unwrap_or_else(|| vec![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 u_dummy = - CommittedInstanceVar::::new_witness(cs.clone(), || Ok(u_dummy_native.clone()))?; - let u_i = CommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(self.u_i.unwrap_or_else(|| u_dummy_native.clone())) + let u_dummy_native = CommittedInstance::::dummy(1); + 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_else(|| u_dummy_native.clone())) + let U_i = CommittedInstanceVar::::new_witness(cs.clone(), || { + Ok(self.U_i.unwrap_or(u_dummy_native.clone())) })?; - let U_i1 = CommittedInstanceVar::::new_witness(cs.clone(), || { - Ok(self.U_i1.unwrap_or_else(|| u_dummy_native.clone())) + let U_i1 = CommittedInstanceVar::::new_witness(cs.clone(), || { + Ok(self.U_i1.unwrap_or(u_dummy_native.clone())) })?; let cmT = - NonNativeAffineVar::new_witness(cs.clone(), || Ok(self.cmT.unwrap_or_else(C::zero)))?; + NonNativeAffineVar::new_witness(cs.clone(), || Ok(self.cmT.unwrap_or_else(C1::zero)))?; let x = - FpVar::>::new_input(cs.clone(), || Ok(self.x.unwrap_or_else(CF1::::zero)))?; + FpVar::>::new_input(cs.clone(), || Ok(self.x.unwrap_or_else(CF1::::zero)))?; - let crh_params = - CRHParametersVar::::new_constant(cs.clone(), self.poseidon_config)?; + let crh_params = CRHParametersVar::::new_constant( + cs.clone(), + self.poseidon_config.clone(), + )?; // get z_{i+1} from the F circuit let z_i1 = self.F.generate_step_constraints(cs.clone(), z_i.clone())?; - let zero = FpVar::>::new_constant(cs.clone(), CF1::::zero())?; - let is_basecase = i.is_eq(&zero)?; + let zero = FpVar::>::new_constant(cs.clone(), CF1::::zero())?; let is_not_basecase = i.is_neq(&zero)?; - // 1. h_{i+1} = u_i.X == H(i, z_0, z_i, U_i) + // 1. 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())?; @@ -387,41 +375,107 @@ where (u_i.x[0]).conditional_enforce_equal(&u_i_x, &is_not_basecase)?; // 2. u_i.cmE==cm(0), u_i.u==1 - (u_i.cmE.x.is_eq(&u_dummy.cmE.x)?) - .conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?; - (u_i.cmE.y.is_eq(&u_dummy.cmE.y)?) - .conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?; + 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)?).conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?; + (u_i.cmE.y.is_eq(&zero_y)?).conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?; (u_i.u.is_one()?).conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?; // 3. nifs.verify, checks that folding u_i & U_i obtains U_{i+1}. // compute r = H(u_i, U_i, cmT) - let r = Self::get_challenge(&crh_params, u_i.clone(), U_i.clone(), cmT)?; + let r_bits = ChallengeGadget::::get_challenge_gadget( + cs.clone(), + &self.poseidon_config, + u_i.clone(), + U_i.clone(), + cmT.clone(), + )?; + let r = Boolean::le_bits_to_fp_var(&r_bits)?; // Notice that NIFSGadget::verify is not checking the folding of cmE & cmW, since it will // be done on the other curve. - let nifs_check = NIFSGadget::::verify(r, u_i, U_i.clone(), U_i1.clone())?; + let nifs_check = NIFSGadget::::verify(r, u_i.clone(), U_i.clone(), U_i1.clone())?; nifs_check.conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?; - // 4. (base case) u_{i+1}.X == H(1, z_0, F(z_0)=F(z_i)=z_i1, U_i) (with U_i being dummy) - let u_i1_x_basecase = U_i.hash( + // 4. u_{i+1}.x = H(i+1, z_0, z_i+1, U_{i+1}), this is the output of F' + let u_i1_x = U_i1.clone().hash( &crh_params, - FpVar::>::one(), + i + FpVar::>::one(), z_0.clone(), z_i1.clone(), )?; - // 4. (non-base case). u_{i+1}.x = H(i+1, z_0, z_i+1, U_{i+1}), this is the output of F' - let u_i1_x = U_i1.hash( - &crh_params, - i + FpVar::>::one(), - z_0.clone(), - z_i1.clone(), - )?; + u_i1_x.enforce_equal(&x)?; + + // CycleFold part + let cf_u_dummy_native = CommittedInstance::::dummy(CF_IO_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_U_i = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { + Ok(self.cf_U_i.unwrap_or_else(|| cf_u_dummy_native.clone())) + })?; + let cf_U_i1 = CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { + Ok(self.cf_U_i1.unwrap_or_else(|| cf_u_dummy_native.clone())) + })?; + let cf_cmT = GC2::new_witness(cs.clone(), || Ok(self.cf_cmT.unwrap_or_else(C2::zero)))?; + // cf_r_nonnat is an auxiliary input + let cf_r_nonnat = + NonNativeFieldVar::>::new_witness(cs.clone(), || { + Ok(self.cf_r_nonnat.unwrap_or_else(C2::ScalarField::zero)) + })?; + + // check that cf_u_i.x == [u_i, U_i, U_{i+1}] + let incircuit_x = vec![ + u_i.cmE.x, u_i.cmE.y, u_i.cmW.x, u_i.cmW.y, U_i.cmE.x, U_i.cmE.y, U_i.cmW.x, U_i.cmW.y, + U_i1.cmE.x, U_i1.cmE.y, U_i1.cmW.x, U_i1.cmW.y, + ] + .concat(); - // if i==0: check x==u_{i+1}.x_basecase - u_i1_x_basecase.conditional_enforce_equal(&x, &is_basecase)?; - // else: check x==u_{i+1}.x - u_i1_x.conditional_enforce_equal(&x, &is_not_basecase)?; + let mut cf_u_i_x: Vec>> = vec![]; + for x_i in cf_u_i.x.iter() { + let mut x_fpvar = x_i.to_constraint_field()?; + cf_u_i_x.append(&mut x_fpvar); + } + cf_u_i_x.conditional_enforce_equal(&incircuit_x, &is_not_basecase)?; + + // cf_r_bits is denoted by rho* in the paper + let cf_r_bits = CycleFoldChallengeGadget::::get_challenge_gadget( + cs.clone(), + &self.poseidon_config, + cf_u_i.clone(), + cf_U_i.clone(), + cf_cmT.clone(), + )?; + // assert that cf_r_bits == cf_r_nonnat converted to bits. cf_r_nonnat is just an auxiliary + // value used to compute RLC of NonNativeFieldVar values, since we can convert + // NonNativeFieldVar into Vec, but not in the other direction. + let cf_r_nonnat_bits = cf_r_nonnat.to_bits_le()?; + cf_r_bits.conditional_enforce_equal(&cf_r_nonnat_bits[..N_BITS_RO], &is_not_basecase)?; + + // check cf_u_i.cmE=0, cf_u_i.u=1 + (cf_u_i.cmE.is_zero()?).conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?; + (cf_u_i.u.is_one()?).conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?; + + // check the fold of all the parameteres of the CycleFold instances, where the elliptic + // curve points relations are checked natively in Curve1 circuit (this one) + let v = NIFSFullGadget::::verify( + cf_r_bits, + cf_r_nonnat, + cf_cmT, + cf_U_i, + cf_u_i, + cf_U_i1, + )?; + v.conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?; Ok(()) } @@ -431,19 +485,21 @@ where pub mod tests { use super::*; use ark_ff::BigInteger; - use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; + use ark_pallas::{Fq, Fr, Projective}; use ark_r1cs_std::{alloc::AllocVar, R1CSVar}; use ark_relations::r1cs::{ConstraintLayer, ConstraintSystem, TracingMode}; use ark_std::One; use ark_std::UniformRand; + use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; use tracing_subscriber::layer::SubscriberExt; - use crate::ccs::r1cs::tests::{get_test_r1cs, get_test_z}; - use crate::folding::nova::{nifs::NIFS, traits::NovaR1CS, Witness}; + 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::pedersen::Pedersen; - use crate::transcript::poseidon::{tests::poseidon_test_config, PoseidonTranscript}; - use crate::transcript::Transcript; + use crate::transcript::poseidon::tests::poseidon_test_config; #[derive(Clone, Copy, Debug)] /// TestFCircuit is a variation of `x^3 + x + 5 = y` (as in @@ -488,49 +544,14 @@ pub mod tests { CommittedInstanceVar::::new_witness(cs.clone(), || Ok(ci.clone())).unwrap(); assert_eq!(ciVar.u.value().unwrap(), ci.u); assert_eq!(ciVar.x.value().unwrap(), ci.x); - - // check the instantiation of the CycleFold side: - let cs = ConstraintSystem::::new_ref(); - let ciVar = - CommittedInstanceCycleFoldVar::::new_witness(cs.clone(), || { - Ok(ci.clone()) - }) - .unwrap(); - assert_eq!(ciVar.cmE.value().unwrap(), ci.cmE); - assert_eq!(ciVar.cmW.value().unwrap(), ci.cmW); + // the values cmE and cmW are checked in the CycleFold's circuit + // CommittedInstanceInCycleFoldVar in + // nova::cyclefold::tests::test_committed_instance_cyclefold_var } #[test] fn test_nifs_gadget() { - let r1cs = get_test_r1cs(); - let z1 = get_test_z(3); - let z2 = get_test_z(4); - let (w1, x1) = r1cs.split_z(&z1); - let (w2, x2) = r1cs.split_z(&z2); - - let w1 = Witness::::new(w1.clone(), r1cs.A.n_rows); - let w2 = Witness::::new(w2.clone(), r1cs.A.n_rows); - - let mut rng = ark_std::test_rng(); - let pedersen_params = Pedersen::::new_params(&mut rng, r1cs.A.n_rows); - - // compute committed instances - let ci1 = w1.commit(&pedersen_params, x1.clone()).unwrap(); - let ci2 = w2.commit(&pedersen_params, x2.clone()).unwrap(); - - // NIFS.P - let (T, cmT) = - NIFS::::compute_cmT(&pedersen_params, &r1cs, &w1, &ci1, &w2, &ci2).unwrap(); - - // get challenge from transcript, since we're in a test skip absorbing values into - // transcript - let poseidon_config = poseidon_test_config::(); - let mut tr = PoseidonTranscript::::new(&poseidon_config); - let r_bits = tr.get_challenge_nbits(128); - let r_Fr = Fr::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap(); - - let (_, ci3) = - NIFS::::fold_instances(r_Fr, &w1, &ci1, &w2, &ci2, &T, cmT).unwrap(); + let (_, _, _, _, ci1, _, ci2, _, ci3, _, cmT, _, r_Fr) = prepare_simple_fold_inputs(); let ci3_verifier = NIFS::::verify(r_Fr, &ci1, &ci2, &cmT); assert_eq!(ci3_verifier, ci3); @@ -557,36 +578,6 @@ pub mod tests { .unwrap(); nifs_check.enforce_equal(&Boolean::::TRUE).unwrap(); assert!(cs.is_satisfied().unwrap()); - - // cs_CC is the Constraint System on the Curve Cycle auxiliary curve constraints field - // (E2::Fr) - let cs_CC = ConstraintSystem::::new_ref(); - - let r_bitsVar = Vec::>::new_witness(cs_CC.clone(), || Ok(r_bits)).unwrap(); - - let cmTVar = GVar::new_witness(cs_CC.clone(), || Ok(cmT)).unwrap(); - let ci1Var = - CommittedInstanceCycleFoldVar::::new_witness(cs_CC.clone(), || { - Ok(ci1.clone()) - }) - .unwrap(); - let ci2Var = - CommittedInstanceCycleFoldVar::::new_witness(cs_CC.clone(), || { - Ok(ci2.clone()) - }) - .unwrap(); - let ci3Var = - CommittedInstanceCycleFoldVar::::new_witness(cs_CC.clone(), || { - Ok(ci3.clone()) - }) - .unwrap(); - - let nifs_cf_check = NIFSCycleFoldGadget::::verify( - r_bitsVar, cmTVar, ci1Var, ci2Var, ci3Var, - ) - .unwrap(); - nifs_cf_check.enforce_equal(&Boolean::::TRUE).unwrap(); - assert!(cs_CC.is_satisfied().unwrap()); } #[test] @@ -648,13 +639,14 @@ pub mod tests { let cmT = Projective::rand(&mut rng); // compute the challenge natively - let r = AugmentedFCircuit::>::get_challenge_native( + let r_bits = ChallengeGadget::::get_challenge_native( &poseidon_config, u_i.clone(), U_i.clone(), cmT, ) .unwrap(); + let r = Fr::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap(); let cs = ConstraintSystem::::new_ref(); let u_iVar = @@ -665,11 +657,10 @@ pub mod tests { .unwrap(); let cmTVar = NonNativeAffineVar::::new_witness(cs.clone(), || Ok(cmT)).unwrap(); - let crh_params = CRHParametersVar::::new_constant(cs.clone(), poseidon_config).unwrap(); - // compute the challenge in-circuit - let rVar = AugmentedFCircuit::>::get_challenge( - &crh_params, + let r_bitsVar = ChallengeGadget::::get_challenge_gadget( + cs.clone(), + &poseidon_config, u_iVar, U_iVar, cmTVar, @@ -678,7 +669,9 @@ pub mod tests { assert!(cs.is_satisfied().unwrap()); // check that the natively computed and in-circuit computed hashes match + let rVar = Boolean::le_bits_to_fp_var(&r_bitsVar).unwrap(); assert_eq!(rVar.value().unwrap(), r); + assert_eq!(r_bitsVar.value().unwrap(), r_bits); } #[test] @@ -699,7 +692,10 @@ pub mod tests { // prepare the circuit to obtain its R1CS let F_circuit = TestFCircuit::::new(); let mut augmented_F_circuit = - AugmentedFCircuit::>::empty(&poseidon_config, F_circuit); + AugmentedFCircuit::>::empty( + &poseidon_config, + F_circuit, + ); augmented_F_circuit .generate_constraints(cs.clone()) .unwrap(); @@ -751,18 +747,26 @@ pub mod tests { .unwrap(); // base case - augmented_F_circuit = AugmentedFCircuit::> { - poseidon_config: poseidon_config.clone(), - i: Some(i), // = 0 - z_0: Some(z_0.clone()), // = z_i=3 - z_i: Some(z_i.clone()), - u_i: Some(u_i.clone()), // = dummy - U_i: Some(U_i.clone()), // = dummy - U_i1: Some(U_i1.clone()), // = dummy - cmT: Some(cmT), - F: F_circuit, - x: Some(u_i1_x), - }; + augmented_F_circuit = + AugmentedFCircuit::> { + _gc2: PhantomData, + poseidon_config: poseidon_config.clone(), + i: Some(i), // = 0 + z_0: Some(z_0.clone()), // = z_i=3 + z_i: Some(z_i.clone()), + u_i: Some(u_i.clone()), // = dummy + U_i: Some(U_i.clone()), // = dummy + U_i1: Some(U_i1.clone()), // = dummy + cmT: Some(cmT), + F: F_circuit, + x: Some(u_i1_x), + // cyclefold instances (not tested in this test) + cf_u_i: None, + cf_U_i: None, + cf_U_i1: None, + cf_cmT: None, + cf_r_nonnat: None, + }; } else { r1cs.check_relaxed_instance_relation(&w_i, &u_i).unwrap(); r1cs.check_relaxed_instance_relation(&W_i, &U_i).unwrap(); @@ -780,13 +784,14 @@ pub mod tests { .unwrap(); // get challenge r - let r_Fr = AugmentedFCircuit::>::get_challenge_native( + let r_bits = ChallengeGadget::::get_challenge_native( &poseidon_config, u_i.clone(), U_i.clone(), cmT, ) .unwrap(); + let r_Fr = Fr::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap(); (W_i1, U_i1) = NIFS::::fold_instances(r_Fr, &w_i, &u_i, &W_i, &U_i, &T, cmT) @@ -800,18 +805,59 @@ pub mod tests { .hash(&poseidon_config, i + Fr::one(), z_0.clone(), z_i1.clone()) .unwrap(); - augmented_F_circuit = AugmentedFCircuit::> { - poseidon_config: poseidon_config.clone(), - i: Some(i), - z_0: Some(z_0.clone()), - z_i: Some(z_i.clone()), - u_i: Some(u_i), - U_i: Some(U_i.clone()), - U_i1: Some(U_i1.clone()), - cmT: Some(cmT), - F: F_circuit, - x: Some(u_i1_x), + // set up dummy cyclefold instances just for the sake of this test. Warning, this + // is only because we are in a test were we're not testing the cyclefold side of + // things. + let cf_W_i = Witness::::new(vec![Fq::zero(); 1], 1); + let cf_U_i = CommittedInstance::::dummy(CF_IO_LEN); + let cf_u_i_x = [ + get_committed_instance_coordinates(&u_i), + get_committed_instance_coordinates(&U_i), + get_committed_instance_coordinates(&U_i1), + ] + .concat(); + let cf_u_i = CommittedInstance:: { + cmE: cf_U_i.cmE, + u: Fq::one(), + cmW: cf_U_i.cmW, + x: cf_u_i_x, }; + let cf_w_i = cf_W_i.clone(); + let (cf_T, cf_cmT): (Vec, Projective2) = + (vec![Fq::zero(); cf_W_i.E.len()], Projective2::zero()); + let cf_r_bits = + CycleFoldChallengeGadget::::get_challenge_native( + &poseidon_config, + cf_u_i.clone(), + cf_U_i.clone(), + cf_cmT, + ) + .unwrap(); + let cf_r_Fq = Fq::from_bigint(BigInteger::from_bits_le(&cf_r_bits)).unwrap(); + let (_, cf_U_i1) = NIFS::::fold_instances( + cf_r_Fq, &cf_W_i, &cf_U_i, &cf_w_i, &cf_u_i, &cf_T, cf_cmT, + ) + .unwrap(); + + augmented_F_circuit = + AugmentedFCircuit::> { + _gc2: PhantomData, + poseidon_config: poseidon_config.clone(), + i: Some(i), + z_0: Some(z_0.clone()), + z_i: Some(z_i.clone()), + u_i: Some(u_i), + U_i: Some(U_i.clone()), + U_i1: Some(U_i1.clone()), + cmT: Some(cmT), + F: F_circuit, + x: Some(u_i1_x), + cf_u_i: Some(cf_u_i), + cf_U_i: Some(cf_U_i), + cf_U_i1: Some(cf_U_i1), + cf_cmT: Some(cf_cmT), + cf_r_nonnat: Some(cf_r_Fq), + }; } let cs = ConstraintSystem::::new_ref(); @@ -821,7 +867,7 @@ pub mod tests { .unwrap(); let is_satisfied = cs.is_satisfied().unwrap(); if !is_satisfied { - println!("{:?}", cs.which_is_unsatisfied()); + dbg!(cs.which_is_unsatisfied().unwrap()); } assert!(is_satisfied); diff --git a/src/folding/nova/cyclefold.rs b/src/folding/nova/cyclefold.rs new file mode 100644 index 0000000..960f0b5 --- /dev/null +++ b/src/folding/nova/cyclefold.rs @@ -0,0 +1,566 @@ +/// contains [CycleFold](https://eprint.iacr.org/2023/1192.pdf) related circuits +use ark_crypto_primitives::sponge::{ + constraints::CryptographicSpongeVar, + poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, + Absorb, CryptographicSponge, +}; +use ark_ec::CurveGroup; +use ark_ff::PrimeField; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + bits::uint8::UInt8, + boolean::Boolean, + eq::EqGadget, + fields::{fp::FpVar, nonnative::NonNativeFieldVar}, + groups::GroupOpsBounds, + prelude::CurveVar, + ToBytesGadget, +}; +use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; +use ark_serialize::CanonicalSerialize; +use ark_std::fmt::Debug; +use ark_std::Zero; +use core::{borrow::Borrow, marker::PhantomData}; + +use super::circuits::CF2; +use super::CommittedInstance; +use crate::constants::N_BITS_RO; +use crate::Error; + +// publi inputs length for the CycleFoldCircuit, |[u_i, U_i, U_{i+1}]| +pub const CF_IO_LEN: usize = 12; + +/// CycleFoldCommittedInstanceVar is the CycleFold CommittedInstance representation in the Nova +/// circuit. +#[derive(Debug, Clone)] +pub struct CycleFoldCommittedInstanceVar>> +where + for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, +{ + _c: PhantomData, + pub cmE: GC, + pub u: NonNativeFieldVar>, + pub cmW: GC, + pub x: Vec>>, +} +impl AllocVar, CF2> for CycleFoldCommittedInstanceVar +where + C: CurveGroup, + GC: CurveVar>, + ::BaseField: ark_ff::PrimeField, + for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, +{ + fn new_variable>>( + cs: impl Into>>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + + let cmE = GC::new_variable(cs.clone(), || Ok(val.borrow().cmE), mode)?; + let cmW = GC::new_variable(cs.clone(), || Ok(val.borrow().cmW), mode)?; + let u = NonNativeFieldVar::>::new_variable( + cs.clone(), + || Ok(val.borrow().u), + mode, + )?; + let x = Vec::>>::new_variable( + cs.clone(), + || Ok(val.borrow().x.clone()), + mode, + )?; + + Ok(Self { + _c: PhantomData, + cmE, + u, + cmW, + x, + }) + }) + } +} + +/// CommittedInstanceInCycleFoldVar represents the Nova CommittedInstance in the CycleFold circuit, +/// where the commitments to E and W (cmW and cmW) from the CommittedInstance on the E2, +/// 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>, +{ + _c: PhantomData, + pub cmE: GC, + pub cmW: GC, +} + +impl AllocVar, CF2> for CommittedInstanceInCycleFoldVar +where + C: CurveGroup, + GC: CurveVar>, + for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, +{ + fn new_variable>>( + cs: impl Into>>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + + let cmE = GC::new_variable(cs.clone(), || Ok(val.borrow().cmE), mode)?; + let cmW = GC::new_variable(cs.clone(), || Ok(val.borrow().cmW), mode)?; + + Ok(Self { + _c: PhantomData, + cmE, + cmW, + }) + }) + } +} + +/// NIFSinCycleFoldGadget performs the Nova NIFS.V elliptic curve points relation checks in the other +/// curve (natively) following [CycleFold](https://eprint.iacr.org/2023/1192.pdf). +pub struct NIFSinCycleFoldGadget>> { + _c: PhantomData, + _gc: PhantomData, +} +impl>> NIFSinCycleFoldGadget +where + C: CurveGroup, + GC: CurveVar>, + ::BaseField: ark_ff::PrimeField, + for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, +{ + pub fn verify( + r_bits: Vec>>, + cmT: GC, + ci1: CommittedInstanceInCycleFoldVar, + ci2: CommittedInstanceInCycleFoldVar, + ci3: CommittedInstanceInCycleFoldVar, + ) -> Result>, SynthesisError> { + // cm(E) check: ci3.cmE == ci1.cmE + r * cmT + r^2 * ci2.cmE + let first_check = ci3.cmE.is_eq( + &((ci2.cmE.scalar_mul_le(r_bits.iter())? + cmT).scalar_mul_le(r_bits.iter())? + + ci1.cmE), + )?; + // cm(W) check: ci3.cmW == ci1.cmW + r * ci2.cmW + let second_check = ci3 + .cmW + .is_eq(&(ci1.cmW + ci2.cmW.scalar_mul_le(r_bits.iter())?))?; + + first_check.and(&second_check) + } +} + +/// This is the gadget used in the AugmentedFCircuit to verify the CycleFold instances folding, +/// which checks the correct RLC of u,x,cmE,cmW (hence the name containing 'Full', since it checks +/// all the RLC values, not only the native ones). It assumes that ci2.cmE=0, ci2.u=1. +pub struct NIFSFullGadget>> { + _c: PhantomData, + _gc: PhantomData, +} +impl>> NIFSFullGadget +where + C: CurveGroup, + GC: CurveVar>, + ::BaseField: ark_ff::PrimeField, + for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, +{ + pub fn verify( + r_bits: Vec>>, + r_nonnat: NonNativeFieldVar>, + cmT: GC, + // ci1 is assumed to be always with cmE=0, u=1 (checks done previous to this method) + ci1: CycleFoldCommittedInstanceVar, + ci2: CycleFoldCommittedInstanceVar, + ci3: CycleFoldCommittedInstanceVar, + ) -> Result>, SynthesisError> { + // cm(E) check: ci3.cmE == ci1.cmE + r * cmT (ci2.cmE=0) + let first_check = ci3 + .cmE + .is_eq(&(cmT.scalar_mul_le(r_bits.iter())? + ci1.cmE))?; + + // cm(W) check: ci3.cmW == ci1.cmW + r * ci2.cmW + let second_check = ci3 + .cmW + .is_eq(&(ci1.cmW + ci2.cmW.scalar_mul_le(r_bits.iter())?))?; + + let u_rlc: NonNativeFieldVar> = ci1.u + r_nonnat.clone(); + let third_check = u_rlc.is_eq(&ci3.u)?; + + // ensure that: ci3.x == ci1.x + r * ci2.x + let x_rlc: Vec>> = ci1 + .x + .iter() + .zip(ci2.x) + .map(|(a, b)| a + &r_nonnat * &b) + .collect::>>>(); + let fourth_check = x_rlc.is_eq(&ci3.x)?; + + first_check + .and(&second_check)? + .and(&third_check)? + .and(&fourth_check) + } +} + +/// ChallengeGadget computes the RO challenge used for the CycleFold instances NIFS, it contains a +/// rust-native and a in-circuit compatible versions. +pub struct CycleFoldChallengeGadget>> { + _c: PhantomData, // Nova's Curve2, the one used for the CycleFold circuit + _gc: PhantomData, +} +impl CycleFoldChallengeGadget +where + C: CurveGroup, + GC: CurveVar>, + ::BaseField: PrimeField, + ::BaseField: Absorb, + for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, +{ + pub fn get_challenge_native( + poseidon_config: &PoseidonConfig, + u_i: CommittedInstance, + U_i: CommittedInstance, + cmT: C, + ) -> Result, Error> { + let mut sponge = PoseidonSponge::::new(poseidon_config); + + let u_i_cmE_bytes = point_to_bytes(u_i.cmE); + let u_i_cmW_bytes = point_to_bytes(u_i.cmW); + let U_i_cmE_bytes = point_to_bytes(U_i.cmE); + let U_i_cmW_bytes = point_to_bytes(U_i.cmW); + let cmT_bytes = point_to_bytes(cmT); + + let mut u_i_u_bytes = Vec::new(); + u_i.u.serialize_uncompressed(&mut u_i_u_bytes)?; + let mut u_i_x_bytes = Vec::new(); + u_i.x.serialize_uncompressed(&mut u_i_x_bytes)?; + u_i_x_bytes = u_i_x_bytes[8..].to_vec(); + let mut U_i_u_bytes = Vec::new(); + U_i.u.serialize_uncompressed(&mut U_i_u_bytes)?; + let mut U_i_x_bytes = Vec::new(); + U_i.x.serialize_uncompressed(&mut U_i_x_bytes)?; + U_i_x_bytes = U_i_x_bytes[8..].to_vec(); + + let input: Vec = [ + u_i_cmE_bytes, + u_i_u_bytes, + u_i_cmW_bytes, + u_i_x_bytes, + U_i_cmE_bytes, + U_i_u_bytes, + U_i_cmW_bytes, + U_i_x_bytes, + cmT_bytes, + ] + .concat(); + sponge.absorb(&input); + let bits = sponge.squeeze_bits(N_BITS_RO); + Ok(bits) + } + // compatible with the native get_challenge_native + pub fn get_challenge_gadget( + cs: ConstraintSystemRef, + poseidon_config: &PoseidonConfig, + u_i: CycleFoldCommittedInstanceVar, + U_i: CycleFoldCommittedInstanceVar, + cmT: GC, + ) -> Result>, SynthesisError> { + let mut sponge = PoseidonSpongeVar::::new(cs, poseidon_config); + + let u_i_x_bytes: Vec>> = u_i + .x + .iter() + .flat_map(|e| e.to_bytes().unwrap_or(vec![])) + .collect::>>>(); + let U_i_x_bytes: Vec>> = U_i + .x + .iter() + .flat_map(|e| e.to_bytes().unwrap_or(vec![])) + .collect::>>>(); + + let input: Vec>> = [ + u_i.cmE.to_bytes()?, + u_i.u.to_bytes()?, + u_i.cmW.to_bytes()?, + u_i_x_bytes, + U_i.cmE.to_bytes()?, + U_i.u.to_bytes()?, + U_i.cmW.to_bytes()?, + U_i_x_bytes, + cmT.to_bytes()?, + // TODO instead of bytes, use field elements, but needs x,y coordinates from + // u_i.{cmE,cmW}, U_i.{cmE,cmW}, cmT. Depends exposing x,y coordinates of GC. Issue to + // keep track of this: + // https://github.com/privacy-scaling-explorations/folding-schemes/issues/44 + ] + .concat(); + sponge.absorb(&input)?; + let bits = sponge.squeeze_bits(N_BITS_RO)?; + Ok(bits) + } +} + +/// returns the bytes being compatible with the ark_r1cs_std `.to_bytes` approach +fn point_to_bytes(p: C) -> Vec { + let l = p.uncompressed_size(); + let mut b = Vec::new(); + p.serialize_uncompressed(&mut b).unwrap(); + b[l - 1] = 0; + if p.is_zero() { + b[l / 2] = 1; + b[l - 1] = 1; + } + b +} + +/// CycleFoldCircuit contains the constraints that check the correct fold of the committed +/// instances from Curve1. Namely, it checks the random linear combinations of the elliptic curve +/// (Curve1) points of u_i, U_i leading to U_{i+1} +#[derive(Debug, Clone)] +pub struct CycleFoldCircuit>> { + pub _gc: PhantomData, + pub r_bits: Option>, + pub cmT: Option, + // u_i,U_i,U_i1 are the nova instances from AugmentedFCircuit which will be (their elliptic + // curve points) checked natively in CycleFoldCircuit + pub u_i: Option>, + pub U_i: Option>, + pub U_i1: Option>, + pub x: Option>>, // public inputs (cf_u_{i+1}.x) +} +impl>> CycleFoldCircuit { + pub fn empty() -> Self { + Self { + _gc: PhantomData, + r_bits: None, + cmT: None, + u_i: None, + U_i: None, + U_i1: None, + x: None, + } + } +} +impl ConstraintSynthesizer> for CycleFoldCircuit +where + C: CurveGroup, + GC: CurveVar>, + ::BaseField: ark_ff::PrimeField, + for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, +{ + fn generate_constraints(self, cs: ConstraintSystemRef>) -> Result<(), SynthesisError> { + let r_bits: Vec>> = Vec::new_witness(cs.clone(), || { + Ok(self.r_bits.unwrap_or(vec![false; N_BITS_RO])) + })?; + let cmT = GC::new_witness(cs.clone(), || Ok(self.cmT.unwrap_or(C::zero())))?; + + let u_dummy_native = CommittedInstance::::dummy(1); + + let u_i = CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { + Ok(self.u_i.unwrap_or(u_dummy_native.clone())) + })?; + let U_i = CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { + Ok(self.U_i.unwrap_or(u_dummy_native.clone())) + })?; + let U_i1 = CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { + Ok(self.U_i1.unwrap_or(u_dummy_native.clone())) + })?; + let _x = Vec::>>::new_input(cs.clone(), || { + Ok(self.x.unwrap_or(vec![CF2::::zero(); CF_IO_LEN])) + })?; + #[cfg(test)] + assert_eq!(_x.len(), CF_IO_LEN); // non-constrained sanity check + + // fold the original Nova instances natively in CycleFold + let v = + NIFSinCycleFoldGadget::::verify(r_bits.clone(), cmT, u_i.clone(), U_i, U_i1)?; + v.enforce_equal(&Boolean::TRUE)?; + + // check that x == [u_i, U_i, U_{i+1}], check that the cmW & cmW from u_i, U_i, U_{i+1} in + // the CycleFoldCircuit are the sames used in the public inputs 'x', which come from the + // AugmentedFCircuit. + // TODO: Issue to keep track of this: https://github.com/privacy-scaling-explorations/folding-schemes/issues/44 + + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use ark_ff::BigInteger; + use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; + use ark_r1cs_std::{alloc::AllocVar, R1CSVar}; + use ark_relations::r1cs::ConstraintSystem; + use ark_std::UniformRand; + + use crate::folding::nova::nifs::tests::prepare_simple_fold_inputs; + use crate::transcript::poseidon::tests::poseidon_test_config; + + #[test] + fn test_committed_instance_cyclefold_var() { + let mut rng = ark_std::test_rng(); + + let ci = CommittedInstance:: { + cmE: Projective::rand(&mut rng), + u: Fr::rand(&mut rng), + cmW: Projective::rand(&mut rng), + x: vec![Fr::rand(&mut rng); 1], + }; + + // check the instantiation of the CycleFold side: + let cs = ConstraintSystem::::new_ref(); + let ciVar = + CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { + Ok(ci.clone()) + }) + .unwrap(); + assert_eq!(ciVar.cmE.value().unwrap(), ci.cmE); + assert_eq!(ciVar.cmW.value().unwrap(), ci.cmW); + } + + #[test] + fn test_nifs_gadget_cyclefold() { + let (_, _, _, _, ci1, _, ci2, _, ci3, _, cmT, r_bits, _) = prepare_simple_fold_inputs(); + + // cs is the Constraint System on the Curve Cycle auxiliary curve constraints field + // (E2::Fr) + let cs = ConstraintSystem::::new_ref(); + + let r_bitsVar = Vec::>::new_witness(cs.clone(), || Ok(r_bits)).unwrap(); + + let cmTVar = GVar::new_witness(cs.clone(), || Ok(cmT)).unwrap(); + let ci1Var = + CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { + Ok(ci1.clone()) + }) + .unwrap(); + let ci2Var = + CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { + Ok(ci2.clone()) + }) + .unwrap(); + let ci3Var = + CommittedInstanceInCycleFoldVar::::new_witness(cs.clone(), || { + Ok(ci3.clone()) + }) + .unwrap(); + + let nifs_cf_check = NIFSinCycleFoldGadget::::verify( + r_bitsVar, cmTVar, ci1Var, ci2Var, ci3Var, + ) + .unwrap(); + nifs_cf_check.enforce_equal(&Boolean::::TRUE).unwrap(); + assert!(cs.is_satisfied().unwrap()); + dbg!(cs.num_constraints()); + } + + #[test] + fn test_nifs_full_gadget() { + let (_, _, _, _, ci1, _, ci2, _, ci3, _, cmT, r_bits, r_Fr) = prepare_simple_fold_inputs(); + + let cs = ConstraintSystem::::new_ref(); + + let r_nonnatVar = + NonNativeFieldVar::::new_witness(cs.clone(), || Ok(r_Fr)).unwrap(); + let r_bitsVar = Vec::>::new_witness(cs.clone(), || Ok(r_bits)).unwrap(); + + let ci1Var = + CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { + Ok(ci1.clone()) + }) + .unwrap(); + let ci2Var = + CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { + Ok(ci2.clone()) + }) + .unwrap(); + let ci3Var = + CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { + Ok(ci3.clone()) + }) + .unwrap(); + let cmTVar = GVar::new_witness(cs.clone(), || Ok(cmT)).unwrap(); + + let nifs_check = NIFSFullGadget::::verify( + r_bitsVar, + r_nonnatVar, + cmTVar, + ci1Var, + ci2Var, + ci3Var, + ) + .unwrap(); + nifs_check.enforce_equal(&Boolean::::TRUE).unwrap(); + assert!(cs.is_satisfied().unwrap()); + dbg!(cs.num_constraints()); + } + + #[test] + fn test_cyclefold_challenge_gadget() { + let mut rng = ark_std::test_rng(); + let poseidon_config = poseidon_test_config::(); + + let u_i = CommittedInstance:: { + cmE: Projective::zero(), // zero on purpose, so we test also the zero point case + u: Fr::zero(), + cmW: Projective::rand(&mut rng), + x: std::iter::repeat_with(|| Fr::rand(&mut rng)) + .take(CF_IO_LEN) + .collect(), + }; + let U_i = CommittedInstance:: { + cmE: Projective::rand(&mut rng), + u: Fr::rand(&mut rng), + cmW: Projective::rand(&mut rng), + x: std::iter::repeat_with(|| Fr::rand(&mut rng)) + .take(CF_IO_LEN) + .collect(), + }; + let cmT = Projective::rand(&mut rng); + + // compute the challenge natively + let r_bits = CycleFoldChallengeGadget::::get_challenge_native( + &poseidon_config, + u_i.clone(), + U_i.clone(), + cmT, + ) + .unwrap(); + + let cs = ConstraintSystem::::new_ref(); + let u_iVar = + CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { + Ok(u_i.clone()) + }) + .unwrap(); + let U_iVar = + CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { + Ok(U_i.clone()) + }) + .unwrap(); + let cmTVar = GVar::new_witness(cs.clone(), || Ok(cmT)).unwrap(); + + let r_bitsVar = CycleFoldChallengeGadget::::get_challenge_gadget( + cs.clone(), + &poseidon_config, + u_iVar, + U_iVar, + cmTVar, + ) + .unwrap(); + assert!(cs.is_satisfied().unwrap()); + + // check that the natively computed and in-circuit computed hashes match + let rVar = Boolean::le_bits_to_fp_var(&r_bitsVar).unwrap(); + let r = Fq::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap(); + assert_eq!(rVar.value().unwrap(), r); + assert_eq!(r_bitsVar.value().unwrap(), r_bits); + } +} diff --git a/src/folding/nova/ivc.rs b/src/folding/nova/ivc.rs index 56a3ede..20cbb3c 100644 --- a/src/folding/nova/ivc.rs +++ b/src/folding/nova/ivc.rs @@ -1,32 +1,44 @@ use ark_crypto_primitives::sponge::{poseidon::PoseidonConfig, Absorb}; -use ark_ec::{CurveGroup, Group}; -use ark_ff::PrimeField; -use ark_relations::r1cs::ConstraintSynthesizer; -use ark_relations::r1cs::ConstraintSystem; +use ark_ec::{AffineRepr, CurveGroup, Group}; +use ark_ff::{BigInteger, PrimeField}; +use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar}; +use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem}; use ark_std::rand::Rng; use ark_std::{One, Zero}; use core::marker::PhantomData; -use super::circuits::{AugmentedFCircuit, FCircuit}; +use super::{ + circuits::{AugmentedFCircuit, ChallengeGadget, FCircuit, CF2}, + cyclefold::{CycleFoldChallengeGadget, CycleFoldCircuit}, +}; use super::{nifs::NIFS, traits::NovaR1CS, CommittedInstance, Witness}; use crate::ccs::r1cs::R1CS; -use crate::frontend::arkworks::{extract_r1cs, extract_z}; // TODO once Frontend trait is ready, use that +use crate::frontend::arkworks::{extract_r1cs, extract_w_x}; // TODO once Frontend trait is ready, use that use crate::pedersen::{Params as PedersenParams, Pedersen}; use crate::Error; +#[cfg(test)] +use super::cyclefold::CF_IO_LEN; + /// Implements the Incremental Verifiable Computation described in sections 1.2 and 5 of /// [Nova](https://eprint.iacr.org/2021/370.pdf) -pub struct IVC +pub struct IVC where C1: CurveGroup, + GC1: CurveVar>, C2: CurveGroup, + GC2: CurveVar>, FC: FCircuit, { + _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, - F: FC, // F circuit + 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, @@ -34,15 +46,26 @@ where u_i: CommittedInstance, W_i: Witness, U_i: CommittedInstance, + + // cyclefold running instance + cf_W_i: Witness, + cf_U_i: CommittedInstance, } -impl IVC +impl IVC where C1: CurveGroup, + GC1: CurveVar>, C2: CurveGroup, + GC2: CurveVar>, FC: FCircuit, ::BaseField: PrimeField, + ::BaseField: PrimeField, ::ScalarField: Absorb, + ::ScalarField: Absorb, + C1: CurveGroup, + for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>, + for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>, { /// Initializes the IVC for the given parameters and initial state `z_0`. pub fn new( @@ -53,27 +76,41 @@ where ) -> Result { // prepare the circuit to obtain its R1CS let cs = ConstraintSystem::::new_ref(); + let cs2 = ConstraintSystem::::new_ref(); - let augmented_F_circuit = AugmentedFCircuit::::empty(&poseidon_config, F); + let augmented_F_circuit = AugmentedFCircuit::::empty(&poseidon_config, F); + let cf_circuit = CycleFoldCircuit::::empty(); augmented_F_circuit.generate_constraints(cs.clone())?; cs.finalize(); let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; 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); + + // this will not be randomly generated in this method, and will come from above levels, so + // the same params can be loaded on multiple instances let pedersen_params = Pedersen::::new_params(rng, r1cs.A.n_rows); + let cf_pedersen_params = Pedersen::::new_params(rng, cf_r1cs.A.n_rows); // setup the dummy instances let (w_dummy, u_dummy) = r1cs.dummy_instance(); + let (cf_w_dummy, cf_u_dummy) = cf_r1cs.dummy_instance(); - // W_i=W_0 is a 'dummy witness', all zeroes, but with the size corresponding to the R1CS that - // we're working with. - // Set U_i to be dummy instance + // W_dummy=W_0 is a 'dummy witness', all zeroes, but with the size corresponding to the + // R1CS that we're working with. Ok(Self { + _gc1: PhantomData, _c2: PhantomData, + _gc2: PhantomData, r1cs, + cf_r1cs, poseidon_config, pedersen_params, + cf_pedersen_params, F, i: C1::ScalarField::zero(), z_0: z_0.clone(), @@ -82,77 +119,130 @@ where u_i: u_dummy.clone(), W_i: w_dummy, U_i: u_dummy, + // cyclefold running instance + cf_W_i: cf_w_dummy.clone(), + cf_U_i: cf_u_dummy.clone(), }) } /// Implements IVC.P pub fn prove_step(&mut self) -> Result<(), Error> { - let u_i1_x: C1::ScalarField; - let augmented_F_circuit: AugmentedFCircuit; + let augmented_F_circuit: AugmentedFCircuit; + let cf_circuit: CycleFoldCircuit; + let z_i1 = self.F.step_native(self.z_i.clone()); - let (W_i1, U_i1, cmT): (Witness, CommittedInstance, C1); + // compute T and cmT for AugmentedFCircuit + let (T, cmT) = self.compute_cmT()?; + + let r_bits = ChallengeGadget::::get_challenge_native( + &self.poseidon_config, + self.u_i.clone(), + self.U_i.clone(), + cmT, + )?; + let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) + .ok_or(Error::OutOfBounds)?; + + // fold Nova instances + let (W_i1, U_i1): (Witness, CommittedInstance) = + NIFS::::fold_instances(r_Fr, &self.w_i, &self.u_i, &self.W_i, &self.U_i, &T, cmT)?; + + // folded instance output (public input, x) + // u_{i+1}.x = H(i+1, z_0, z_{i+1}, U_{i+1}) + let u_i1_x = U_i1.hash( + &self.poseidon_config, + self.i + C1::ScalarField::one(), + self.z_0.clone(), + z_i1.clone(), + )?; if self.i == C1::ScalarField::zero() { - // base case: i=0, z_i=z_0, U_i = U_d := dummy instance - // u_1.x = H(1, z_0, z_i, U_i) - u_i1_x = self.U_i.hash( - &self.poseidon_config, - C1::ScalarField::one(), - self.z_0.clone(), - z_i1.clone(), - )?; - - (W_i1, U_i1, cmT) = (self.w_i.clone(), self.u_i.clone(), C1::generator()); - // base case - augmented_F_circuit = AugmentedFCircuit:: { + augmented_F_circuit = AugmentedFCircuit:: { + _gc2: PhantomData, poseidon_config: self.poseidon_config.clone(), i: Some(C1::ScalarField::zero()), // = i=0 z_0: Some(self.z_0.clone()), // = z_i z_i: Some(self.z_i.clone()), u_i: Some(self.u_i.clone()), // = dummy U_i: Some(self.U_i.clone()), // = dummy - U_i1: Some(U_i1.clone()), // = dummy + U_i1: Some(U_i1.clone()), cmT: Some(cmT), F: self.F, x: Some(u_i1_x), + cf_u_i: None, + cf_U_i: None, + cf_U_i1: None, + cf_cmT: None, + cf_r_nonnat: None, }; + + #[cfg(test)] + NIFS::verify_folded_instance(r_Fr, &self.u_i, &self.U_i, &U_i1, &cmT)?; } else { - let T: Vec; - (T, cmT) = NIFS::::compute_cmT( - &self.pedersen_params, - &self.r1cs, - &self.w_i, - &self.u_i, - &self.W_i, - &self.U_i, - )?; + // CycleFold part: + // get the vector used as public inputs 'x' in the CycleFold circuit + let cf_u_i_x = [ + get_committed_instance_coordinates(&self.u_i), + get_committed_instance_coordinates(&self.U_i), + get_committed_instance_coordinates(&U_i1), + ] + .concat(); + + cf_circuit = CycleFoldCircuit:: { + _gc: PhantomData, + r_bits: Some(r_bits.clone()), + cmT: Some(cmT), + u_i: Some(self.u_i.clone()), + U_i: Some(self.U_i.clone()), + U_i1: Some(U_i1.clone()), + x: Some(cf_u_i_x.clone()), + }; - let r_Fr = AugmentedFCircuit::::get_challenge_native( - &self.poseidon_config, - self.u_i.clone(), - self.U_i.clone(), - cmT, - )?; + let cs2 = ConstraintSystem::::new_ref(); + cf_circuit.generate_constraints(cs2.clone())?; - // compute W_{i+1} and U_{i+1} - (W_i1, U_i1) = NIFS::::fold_instances( - r_Fr, &self.w_i, &self.u_i, &self.W_i, &self.U_i, &T, cmT, - )?; + let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?; + let (cf_w_i, cf_x_i) = extract_w_x::(&cs2); + if cf_x_i != cf_u_i_x { + return Err(Error::NotEqual); + } + + #[cfg(test)] + if cf_x_i.len() != CF_IO_LEN { + return Err(Error::NotExpectedLength(cf_x_i.len(), CF_IO_LEN)); + } - self.r1cs.check_relaxed_instance_relation(&W_i1, &U_i1)?; + // fold cyclefold instances + let cf_w_i = Witness::::new(cf_w_i.clone(), self.cf_r1cs.A.n_rows); + let cf_u_i: CommittedInstance = + cf_w_i.commit(&self.cf_pedersen_params, cf_x_i.clone())?; - // folded instance output (public input, x) - // u_{i+1}.x = H(i+1, z_0, z_{i+1}, U_{i+1}) - u_i1_x = U_i1.hash( + // compute T* and cmT* for CycleFoldCircuit + let (cf_T, cf_cmT) = self.compute_cf_cmT(&cf_w_i, &cf_u_i)?; + + let cf_r_bits = CycleFoldChallengeGadget::::get_challenge_native( &self.poseidon_config, - self.i + C1::ScalarField::one(), - self.z_0.clone(), - z_i1.clone(), + cf_u_i.clone(), + self.cf_U_i.clone(), + cf_cmT, + )?; + let cf_r_Fq = C1::BaseField::from_bigint(BigInteger::from_bits_le(&cf_r_bits)) + .ok_or(Error::OutOfBounds)?; + + let (cf_W_i1, cf_U_i1) = NIFS::::fold_instances( + cf_r_Fq, + &self.cf_W_i, + &self.cf_U_i, + &cf_w_i, + &cf_u_i, + &cf_T, + cf_cmT, )?; - augmented_F_circuit = AugmentedFCircuit:: { + augmented_F_circuit = AugmentedFCircuit:: { + _gc2: PhantomData, poseidon_config: self.poseidon_config.clone(), i: Some(self.i), z_0: Some(self.z_0.clone()), @@ -163,7 +253,23 @@ where cmT: Some(cmT), F: self.F, x: Some(u_i1_x), + // cyclefold values + cf_u_i: Some(cf_u_i.clone()), + cf_U_i: Some(self.cf_U_i.clone()), + cf_U_i1: Some(cf_U_i1.clone()), + cf_cmT: Some(cf_cmT), + cf_r_nonnat: Some(cf_r_Fq), }; + + self.cf_W_i = cf_W_i1.clone(); + self.cf_U_i = cf_U_i1.clone(); + + #[cfg(test)] + { + self.cf_r1cs.check_instance_relation(&cf_w_i, &cf_u_i)?; + self.cf_r1cs + .check_relaxed_instance_relation(&self.cf_W_i, &self.cf_U_i)?; + } } let cs = ConstraintSystem::::new_ref(); @@ -171,23 +277,30 @@ where augmented_F_circuit.generate_constraints(cs.clone())?; let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; - // 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) = self.r1cs.split_z(&Z_i1); - assert_eq!(x_i1.len(), 1); - assert_eq!(x_i1[0], u_i1_x); - - // compute committed instances, w_{i+1}, u_{i+1}, which will be used as w_i, u_i, so we - // assign them directly to w_i, u_i. - self.w_i = Witness::::new(w_i1.clone(), self.r1cs.A.n_rows); - self.u_i = self.w_i.commit(&self.pedersen_params, vec![u_i1_x])?; + let (w_i1, x_i1) = extract_w_x::(&cs); + if x_i1[0] != u_i1_x { + return Err(Error::NotEqual); + } + + #[cfg(test)] + if x_i1.len() != 1 { + return Err(Error::NotExpectedLength(x_i1.len(), 1)); + } // set values for next iteration self.i += C1::ScalarField::one(); self.z_i = z_i1.clone(); - self.U_i = U_i1.clone(); + self.w_i = Witness::::new(w_i1, self.r1cs.A.n_rows); + self.u_i = self.w_i.commit(&self.pedersen_params, vec![u_i1_x])?; self.W_i = W_i1.clone(); + self.U_i = U_i1.clone(); + + #[cfg(test)] + { + self.r1cs.check_instance_relation(&self.w_i, &self.u_i)?; + self.r1cs + .check_relaxed_instance_relation(&self.W_i, &self.U_i)?; + } Ok(()) } @@ -212,7 +325,7 @@ where } // check u_i.cmE==0, u_i.u==1 (=u_i is a un-relaxed instance) - if self.u_i.cmE != C1::zero() || self.u_i.u != C1::ScalarField::one() { + if !self.u_i.cmE.is_zero() || !self.u_i.u.is_one() { return Err(Error::IVCVerificationFail); } @@ -222,15 +335,59 @@ where self.r1cs .check_relaxed_instance_relation(&self.W_i, &self.U_i)?; + // check CycleFold RelaxedR1CS satisfiability + self.cf_r1cs + .check_relaxed_instance_relation(&self.cf_W_i, &self.cf_U_i)?; + Ok(()) } + + // computes T and cmT for the AugmentedFCircuit + fn compute_cmT(&self) -> Result<(Vec, C1), Error> { + NIFS::::compute_cmT( + &self.pedersen_params, + &self.r1cs, + &self.w_i, + &self.u_i, + &self.W_i, + &self.U_i, + ) + } + // computes T* and cmT* for the CycleFoldCircuit + fn compute_cf_cmT( + &self, + cf_w_i: &Witness, + cf_u_i: &CommittedInstance, + ) -> Result<(Vec, C2), Error> { + NIFS::::compute_cyclefold_cmT( + &self.cf_pedersen_params, + &self.cf_r1cs, + cf_w_i, + cf_u_i, + &self.cf_W_i, + &self.cf_U_i, + ) + } +} + +pub(crate) fn get_committed_instance_coordinates( + u: &CommittedInstance, +) -> Vec { + let zero = (&C::BaseField::zero(), &C::BaseField::one()); + + let cmE = u.cmE.into_affine(); + let (cmE_x, cmE_y) = cmE.xy().unwrap_or(zero); + + let cmW = u.cmW.into_affine(); + let (cmW_x, cmW_y) = cmW.xy().unwrap_or(zero); + vec![*cmE_x, *cmE_y, *cmW_x, *cmW_y] } #[cfg(test)] mod tests { use super::*; - use ark_pallas::{Fr, Projective}; - use ark_vesta::Projective as Projective2; + 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::transcript::poseidon::tests::poseidon_test_config; @@ -243,9 +400,9 @@ mod tests { let F_circuit = TestFCircuit::::new(); let z_0 = vec![Fr::from(3_u32)]; - let mut ivc = IVC::>::new( + let mut ivc = IVC::>::new( &mut rng, - poseidon_config, // poseidon config + poseidon_config, F_circuit, z_0.clone(), ) diff --git a/src/folding/nova/mod.rs b/src/folding/nova/mod.rs index 9c09ce6..04d216e 100644 --- a/src/folding/nova/mod.rs +++ b/src/folding/nova/mod.rs @@ -13,6 +13,7 @@ use crate::utils::vec::is_zero_vec; use crate::Error; pub mod circuits; +pub mod cyclefold; pub mod ivc; pub mod nifs; pub mod traits; @@ -25,11 +26,7 @@ pub struct CommittedInstance { pub x: Vec, } -impl CommittedInstance -where - ::ScalarField: Absorb, - ::BaseField: ark_ff::PrimeField, -{ +impl CommittedInstance { pub fn dummy(io_len: usize) -> Self { Self { cmE: C::zero(), @@ -38,7 +35,13 @@ where x: vec![C::ScalarField::zero(); io_len], } } +} +impl CommittedInstance +where + ::ScalarField: Absorb, + ::BaseField: ark_ff::PrimeField, +{ /// hash implements the committed instance hash compatible with the gadget implemented in /// nova/circuits.rs::CommittedInstanceVar.hash. /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and `U` is the diff --git a/src/folding/nova/nifs.rs b/src/folding/nova/nifs.rs index 5b36ab6..dd671a5 100644 --- a/src/folding/nova/nifs.rs +++ b/src/folding/nova/nifs.rs @@ -3,8 +3,8 @@ use ark_ec::{CurveGroup, Group}; use ark_std::One; use std::marker::PhantomData; +use super::{CommittedInstance, Witness}; use crate::ccs::r1cs::R1CS; -use crate::folding::nova::{CommittedInstance, Witness}; use crate::pedersen::{Params as PedersenParams, Pedersen, Proof as PedersenProof}; use crate::transcript::Transcript; use crate::utils::vec::*; @@ -31,12 +31,12 @@ where let (A, B, C) = (r1cs.A.clone(), r1cs.B.clone(), r1cs.C.clone()); // this is parallelizable (for the future) - let Az1 = mat_vec_mul_sparse(&A, z1); - let Bz1 = mat_vec_mul_sparse(&B, z1); - let Cz1 = mat_vec_mul_sparse(&C, z1); - let Az2 = mat_vec_mul_sparse(&A, z2); - let Bz2 = mat_vec_mul_sparse(&B, z2); - let Cz2 = mat_vec_mul_sparse(&C, z2); + let Az1 = mat_vec_mul_sparse(&A, z1)?; + let Bz1 = mat_vec_mul_sparse(&B, z1)?; + let Cz1 = mat_vec_mul_sparse(&C, z1)?; + let Az2 = mat_vec_mul_sparse(&A, z2)?; + let Bz2 = mat_vec_mul_sparse(&B, z2)?; + let Cz2 = mat_vec_mul_sparse(&C, z2)?; let Az1_Bz2 = hadamard(&Az1, &Bz2)?; let Az2_Bz1 = hadamard(&Az2, &Bz1)?; @@ -105,12 +105,31 @@ where let cmT = Pedersen::commit(pedersen_params, &T, &C::ScalarField::one())?; Ok((T, cmT)) } + pub fn compute_cyclefold_cmT( + pedersen_params: &PedersenParams, + r1cs: &R1CS, // R1CS over C2.Fr=C1.Fq (here C=C2) + w1: &Witness, + ci1: &CommittedInstance, + w2: &Witness, + ci2: &CommittedInstance, + ) -> Result<(Vec, C), Error> + where + ::BaseField: ark_ff::PrimeField, + { + let z1: Vec = [vec![ci1.u], ci1.x.to_vec(), w1.W.to_vec()].concat(); + let z2: Vec = [vec![ci2.u], ci2.x.to_vec(), w2.W.to_vec()].concat(); + + // compute cross terms + let T = Self::compute_T(r1cs, ci1.u, ci2.u, &z1, &z2)?; + // use r_T=1 since we don't need hiding property for cm(T) + let cmT = Pedersen::commit(pedersen_params, &T, &C::ScalarField::one())?; + Ok((T, cmT)) + } /// fold_instances is part of the NIFS.P logic described in /// [Nova](https://eprint.iacr.org/2021/370.pdf)'s section 4. It returns the folded Committed /// Instances and the Witness. pub fn fold_instances( - // r comes from the transcript, and is a n-bit (N_BITS_CHALLENGE) element r: C::ScalarField, w1: &Witness, ci1: &CommittedInstance, @@ -196,19 +215,82 @@ where #[cfg(test)] pub mod tests { use super::*; - use ark_ff::PrimeField; + use ark_ff::{BigInteger, PrimeField}; use ark_pallas::{Fr, Projective}; use ark_std::{ops::Mul, UniformRand, Zero}; use crate::ccs::r1cs::tests::{get_test_r1cs, get_test_z}; + use crate::folding::nova::circuits::ChallengeGadget; + use crate::folding::nova::traits::NovaR1CS; use crate::transcript::poseidon::{tests::poseidon_test_config, PoseidonTranscript}; use crate::utils::vec::vec_scalar_mul; + use ark_crypto_primitives::sponge::poseidon::PoseidonConfig; + + #[allow(clippy::type_complexity)] + pub(crate) fn prepare_simple_fold_inputs() -> ( + PedersenParams, + PoseidonConfig, + R1CS, + Witness, // w1 + CommittedInstance, // ci1 + Witness, // w2 + CommittedInstance, // ci2 + Witness, // w3 + CommittedInstance, // ci3 + Vec, // T + Projective, // cmT + Vec, // r_bits + Fr, // r_Fr + ) { + let r1cs = get_test_r1cs(); + let z1 = get_test_z(3); + let z2 = get_test_z(4); + let (w1, x1) = r1cs.split_z(&z1); + let (w2, x2) = r1cs.split_z(&z2); + + let w1 = Witness::::new(w1.clone(), r1cs.A.n_rows); + let w2 = Witness::::new(w2.clone(), r1cs.A.n_rows); + + let mut rng = ark_std::test_rng(); + let pedersen_params = Pedersen::::new_params(&mut rng, r1cs.A.n_cols); - pub fn check_relaxed_r1cs(r1cs: &R1CS, z: &[F], u: &F, E: &[F]) -> bool { - let Az = mat_vec_mul_sparse(&r1cs.A, z); - let Bz = mat_vec_mul_sparse(&r1cs.B, z); - let Cz = mat_vec_mul_sparse(&r1cs.C, z); - hadamard(&Az, &Bz).unwrap() == vec_add(&vec_scalar_mul(&Cz, u), E).unwrap() + // compute committed instances + let ci1 = w1.commit(&pedersen_params, x1.clone()).unwrap(); + let ci2 = w2.commit(&pedersen_params, x2.clone()).unwrap(); + + // NIFS.P + let (T, cmT) = + NIFS::::compute_cmT(&pedersen_params, &r1cs, &w1, &ci1, &w2, &ci2).unwrap(); + + let poseidon_config = poseidon_test_config::(); + + let r_bits = ChallengeGadget::::get_challenge_native( + &poseidon_config, + ci1.clone(), + ci2.clone(), + cmT, + ) + .unwrap(); + let r_Fr = Fr::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap(); + + let (w3, ci3) = + NIFS::::fold_instances(r_Fr, &w1, &ci1, &w2, &ci2, &T, cmT).unwrap(); + + ( + pedersen_params, + poseidon_config, + r1cs, + w1, + ci1, + w2, + ci2, + w3, + ci3, + T, + cmT, + r_bits, + r_Fr, + ) } // fold 2 dummy instances and check that the folded instance holds the relaxed R1CS relation @@ -232,9 +314,8 @@ pub mod tests { let u_i = u_dummy.clone(); let W_i = w_dummy.clone(); let U_i = u_dummy.clone(); - let z_dummy = vec![Fr::zero(); z1.len()]; - assert!(check_relaxed_r1cs(&r1cs, &z_dummy, &u_i.u, &w_i.E)); - assert!(check_relaxed_r1cs(&r1cs, &z_dummy, &U_i.u, &W_i.E)); + r1cs.check_relaxed_instance_relation(&w_i, &u_i).unwrap(); + r1cs.check_relaxed_instance_relation(&W_i, &U_i).unwrap(); let r_Fr = Fr::from(3_u32); @@ -243,52 +324,23 @@ pub mod tests { .unwrap(); let (W_i1, U_i1) = NIFS::::fold_instances(r_Fr, &w_i, &u_i, &W_i, &U_i, &T, cmT).unwrap(); - - let z: Vec = [vec![U_i1.u], U_i1.x.to_vec(), W_i1.W.to_vec()].concat(); - assert_eq!(z.len(), z1.len()); - assert!(check_relaxed_r1cs(&r1cs, &z, &U_i1.u, &W_i1.E)); + r1cs.check_relaxed_instance_relation(&W_i1, &U_i1).unwrap(); } // fold 2 instances into one #[test] fn test_nifs_one_fold() { - let r1cs = get_test_r1cs(); - let z1 = get_test_z(3); - let z2 = get_test_z(4); - let (w1, x1) = r1cs.split_z(&z1); - let (w2, x2) = r1cs.split_z(&z2); - - let w1 = Witness::::new(w1.clone(), r1cs.A.n_rows); - let w2 = Witness::::new(w2.clone(), r1cs.A.n_rows); - - let mut rng = ark_std::test_rng(); - let pedersen_params = Pedersen::::new_params(&mut rng, r1cs.A.n_cols); - - let r = Fr::rand(&mut rng); // folding challenge would come from the transcript - - // compute committed instances - let ci1 = w1.commit(&pedersen_params, x1.clone()).unwrap(); - let ci2 = w2.commit(&pedersen_params, x2.clone()).unwrap(); - - // NIFS.P - let (T, cmT) = - NIFS::::compute_cmT(&pedersen_params, &r1cs, &w1, &ci1, &w2, &ci2).unwrap(); - let (w3, ci3_aux) = - NIFS::::fold_instances(r, &w1, &ci1, &w2, &ci2, &T, cmT).unwrap(); + let (pedersen_params, poseidon_config, r1cs, w1, ci1, w2, ci2, w3, ci3, T, cmT, _, r) = + prepare_simple_fold_inputs(); // NIFS.V - let ci3 = NIFS::::verify(r, &ci1, &ci2, &cmT); - assert_eq!(ci3, ci3_aux); - - // naive check that the folded witness satisfies the relaxed r1cs - let z3: Vec = [vec![ci3.u], ci3.x.to_vec(), w3.W.to_vec()].concat(); - // check that z3 is as expected - let z3_aux = vec_add(&z1, &vec_scalar_mul(&z2, &r)).unwrap(); - assert_eq!(z3, z3_aux); + let ci3_v = NIFS::::verify(r, &ci1, &ci2, &cmT); + assert_eq!(ci3_v, ci3); + // check that relations hold for the 2 inputted instances and the folded one - assert!(check_relaxed_r1cs(&r1cs, &z1, &ci1.u, &w1.E)); - assert!(check_relaxed_r1cs(&r1cs, &z2, &ci2.u, &w2.E)); - assert!(check_relaxed_r1cs(&r1cs, &z3, &ci3.u, &w3.E)); + r1cs.check_relaxed_instance_relation(&w1, &ci1).unwrap(); + r1cs.check_relaxed_instance_relation(&w2, &ci2).unwrap(); + r1cs.check_relaxed_instance_relation(&w3, &ci3).unwrap(); // check that folded commitments from folded instance (ci) are equal to folding the // use folded rE, rW to commit w3 @@ -303,7 +355,6 @@ pub mod tests { // NIFS.Verify_Folded_Instance: NIFS::::verify_folded_instance(r, &ci1, &ci2, &ci3, &cmT).unwrap(); - let poseidon_config = poseidon_test_config::(); // init Prover's transcript let mut transcript_p = PoseidonTranscript::::new(&poseidon_config); // init Verifier's transcript @@ -343,12 +394,9 @@ pub mod tests { let mut running_instance_w = Witness::::new(w.clone(), r1cs.A.n_rows); let mut running_committed_instance = running_instance_w.commit(&pedersen_params, x).unwrap(); - assert!(check_relaxed_r1cs( - &r1cs, - &z, - &running_committed_instance.u, - &running_instance_w.E, - )); + + r1cs.check_relaxed_instance_relation(&running_instance_w, &running_committed_instance) + .unwrap(); let num_iters = 10; for i in 0..num_iters { @@ -358,14 +406,13 @@ pub mod tests { let incomming_instance_w = Witness::::new(w.clone(), r1cs.A.n_rows); let incomming_committed_instance = incomming_instance_w.commit(&pedersen_params, x).unwrap(); - assert!(check_relaxed_r1cs( - &r1cs, - &incomming_instance_z.clone(), - &incomming_committed_instance.u, - &incomming_instance_w.E, - )); + r1cs.check_relaxed_instance_relation( + &incomming_instance_w, + &incomming_committed_instance, + ) + .unwrap(); - let r = Fr::rand(&mut rng); // folding challenge would come from the transcript + let r = Fr::rand(&mut rng); // folding challenge would come from the RO // NIFS.P let (T, cmT) = NIFS::::compute_cmT( @@ -396,19 +443,8 @@ pub mod tests { &cmT, ); - let folded_z: Vec = [ - vec![folded_committed_instance.u], - folded_committed_instance.x.to_vec(), - folded_w.W.to_vec(), - ] - .concat(); - - assert!(check_relaxed_r1cs( - &r1cs, - &folded_z, - &folded_committed_instance.u, - &folded_w.E - )); + r1cs.check_relaxed_instance_relation(&folded_w, &folded_committed_instance) + .unwrap(); // set running_instance for next loop iteration running_instance_w = folded_w; diff --git a/src/frontend/arkworks/mod.rs b/src/frontend/arkworks/mod.rs index 35b93c3..5ef3924 100644 --- a/src/frontend/arkworks/mod.rs +++ b/src/frontend/arkworks/mod.rs @@ -59,6 +59,15 @@ pub fn extract_z(cs: &ConstraintSystem) -> Vec { .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::*; diff --git a/src/lib.rs b/src/lib.rs index 5170728..31276df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,10 +16,12 @@ pub mod frontend; pub mod pedersen; pub mod utils; -#[derive(Debug, Error, PartialEq)] +#[derive(Debug, Error)] pub enum Error { #[error("ark_relations::r1cs::SynthesisError")] SynthesisError(#[from] ark_relations::r1cs::SynthesisError), + #[error("ark_serialize::SerializationError")] + SerializationError(#[from] ark_serialize::SerializationError), #[error("{0}")] Other(String), @@ -47,6 +49,8 @@ pub enum Error { SumCheckProveError(String), #[error("Sum-check verify failed: {0}")] SumCheckVerifyError(String), + #[error("Value out of bounds")] + OutOfBounds, } /// FoldingScheme defines trait that is implemented by the diverse folding schemes. It is defined diff --git a/src/pedersen.rs b/src/pedersen.rs index b10665d..7d0a8c5 100644 --- a/src/pedersen.rs +++ b/src/pedersen.rs @@ -132,7 +132,9 @@ mod tests { // init Verifier's transcript let mut transcript_v = PoseidonTranscript::::new(&poseidon_config); - let v: Vec = vec![Fr::rand(&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(); let proof = Pedersen::::prove(¶ms, &mut transcript_p, &cm, &v, &r).unwrap(); diff --git a/src/transcript/poseidon.rs b/src/transcript/poseidon.rs index 7050f47..7d57adb 100644 --- a/src/transcript/poseidon.rs +++ b/src/transcript/poseidon.rs @@ -61,11 +61,9 @@ where // over bytes in order to have a logic that can be reproduced in-circuit. fn prepare_point(p: &C) -> Result, Error> { let affine = p.into_affine(); - let xy_obj = &affine.xy(); - let mut xy = (&C::BaseField::zero(), &C::BaseField::one()); - if xy_obj.is_some() { - xy = xy_obj.unwrap(); - } + let zero_point = (&C::BaseField::zero(), &C::BaseField::one()); + let xy = affine.xy().unwrap_or(zero_point); + let x_bi = xy.0.to_base_prime_field_elements() .next() @@ -175,7 +173,7 @@ pub mod tests { #[test] fn test_transcript_and_transcriptvar_nbits() { - let nbits = 128; + let nbits = crate::constants::N_BITS_RO; // use 'native' transcript let config = poseidon_test_config::(); diff --git a/src/utils/vec.rs b/src/utils/vec.rs index 11feb29..60b1c49 100644 --- a/src/utils/vec.rs +++ b/src/utils/vec.rs @@ -83,14 +83,17 @@ pub fn mat_vec_mul(M: &Vec>, z: &[F]) -> Result, Er Ok(r) } -pub fn mat_vec_mul_sparse(matrix: &SparseMatrix, vector: &[F]) -> Vec { - let mut res = vec![F::zero(); matrix.n_rows]; - for (row_i, row) in matrix.coeffs.iter().enumerate() { +pub fn mat_vec_mul_sparse(M: &SparseMatrix, z: &[F]) -> Result, Error> { + if M.n_cols != z.len() { + return Err(Error::NotSameLength(M.n_cols, z.len())); + } + let mut res = vec![F::zero(); M.n_rows]; + for (row_i, row) in M.coeffs.iter().enumerate() { for &(value, col_i) in row.iter() { - res[row_i] += value * vector[col_i]; + res[row_i] += value * z[col_i]; } } - res + Ok(res) } pub fn hadamard(a: &[F], b: &[F]) -> Result, Error> { @@ -142,7 +145,7 @@ pub mod tests { let z = to_F_vec(vec![1, 3, 35, 9, 27, 30]); assert_eq!(mat_vec_mul(&A, &z).unwrap(), to_F_vec(vec![3, 9, 30, 35])); assert_eq!( - mat_vec_mul_sparse(&dense_matrix_to_sparse(A), &z), + mat_vec_mul_sparse(&dense_matrix_to_sparse(A), &z).unwrap(), to_F_vec(vec![3, 9, 30, 35]) ); @@ -153,7 +156,10 @@ pub mod tests { mat_vec_mul(&A.to_dense(), &v).unwrap(), to_F_vec(vec![418, 1158, 979]) ); - assert_eq!(mat_vec_mul_sparse(&A, &v), to_F_vec(vec![418, 1158, 979])); + assert_eq!( + mat_vec_mul_sparse(&A, &v).unwrap(), + to_F_vec(vec![418, 1158, 979]) + ); } #[test]