From 6a7dd935bd5f200cfbc5ccec4f780fd34cea9627 Mon Sep 17 00:00:00 2001 From: winderica Date: Thu, 11 Apr 2024 10:07:32 +0100 Subject: [PATCH] Add the digest of the Relaxed R1CS instance for CycleFold as a public input to `AugmentedFCircuit` (#84) * Treat (the digest of) `cf_U_i1` as an additional public input to `AugmentedFCircuit` for full soundness * Fix the y-coordinate in the affine form of zero points This in turn fixes the inconsistency between the digest of a constant affine point and that of a witness affine point in circuits. * Set `cf_u_i1_x` to the correct value * Fix the number of public inputs in dummy instance and witness * Unify the logic behind `CycleFoldCommittedInstanceVar::hash` and `CycleFoldChallengeGadget::get_challenge_gadget` * Add `ToConstraintFieldGadget` bound to `GC2` * Remove unnecessary code used for debugging * Make clippy and rustfmt happy * Move conversion methods for `NonNativeFieldVar` to `folding/circuits/nonnative.rs` * Simplify the check of zero coordinates * Gracefully handle the result of `nonnative_field_var_to_constraint_field` * Make clippy happy again --- Cargo.toml | 10 +- .../src/folding/circuits/nonnative.rs | 68 ++++- folding-schemes/src/folding/nova/circuits.rs | 42 ++-- folding-schemes/src/folding/nova/cyclefold.rs | 237 ++++++++++-------- .../src/folding/nova/decider_eth.rs | 2 +- .../src/folding/nova/decider_eth_circuit.rs | 46 ++-- folding-schemes/src/folding/nova/mod.rs | 92 +++++-- folding-schemes/src/transcript/poseidon.rs | 6 +- 8 files changed, 328 insertions(+), 175 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 349b199..8445a18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,13 @@ resolver = "2" [patch.crates-io] # The following patch is to use a version of ark-r1cs-std compatible with -# v0.4.0 but that includes a cherry-picked commit from after v0.4.0 which fixes -# the in-circuit scalar multiplication of the zero point. The commit is from -# https://github.com/arkworks-rs/r1cs-std/pull/124, without including other +# v0.4.0 but that includes two cherry-picked commits from after v0.4.0 which +# fixes the in-circuit scalar multiplication of the zero point and the +# y-coordinate of the zero point. The commits are respectively from +# https://github.com/arkworks-rs/r1cs-std/pull/124 and +# https://github.com/arkworks-rs/r1cs-std/pull/126, without including other # changes done between v0.4.0 and this fix which would break compatibility. -ark-r1cs-std = { git = "https://github.com/arnaucube/ark-r1cs-std-cherry-picked/" } +ark-r1cs-std = { git = "https://github.com/winderica/r1cs-std", branch="cherry-pick" } # patch ark_curves to use a cherry-picked version which contains # bn254::constraints & grumpkin for v0.4.0 (once arkworks v0.5.0 is released # this will no longer be needed) diff --git a/folding-schemes/src/folding/circuits/nonnative.rs b/folding-schemes/src/folding/circuits/nonnative.rs index 599d718..943a243 100644 --- a/folding-schemes/src/folding/circuits/nonnative.rs +++ b/folding-schemes/src/folding/circuits/nonnative.rs @@ -1,12 +1,72 @@ use ark_ec::{AffineRepr, CurveGroup}; +use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, - fields::nonnative::{params::OptimizationType, AllocatedNonNativeFieldVar, NonNativeFieldVar}, + fields::{ + fp::FpVar, + nonnative::{params::OptimizationType, AllocatedNonNativeFieldVar, NonNativeFieldVar}, + FieldVar, + }, + ToBitsGadget, }; use ark_relations::r1cs::{Namespace, SynthesisError}; -use ark_std::{One, Zero}; +use ark_std::Zero; use core::borrow::Borrow; +/// A more efficient version of `NonNativeFieldVar::to_constraint_field` +pub fn nonnative_field_var_to_constraint_field( + f: &NonNativeFieldVar, +) -> Result>, SynthesisError> { + let bits = f.to_bits_le()?; + + let bits_per_limb = BaseField::MODULUS_BIT_SIZE as usize - 1; + let num_limbs = (TargetField::MODULUS_BIT_SIZE as usize).div_ceil(bits_per_limb); + + let mut limbs = bits + .chunks(bits_per_limb) + .map(|chunk| { + let mut limb = FpVar::::zero(); + let mut w = BaseField::one(); + for b in chunk.iter() { + limb += FpVar::from(b.clone()) * w; + w.double_in_place(); + } + limb + }) + .collect::>>(); + limbs.resize(num_limbs, FpVar::zero()); + limbs.reverse(); + + Ok(limbs) +} + +/// The out-circuit counterpart of `nonnative_field_var_to_constraint_field` +pub fn nonnative_field_to_field_elements( + f: &TargetField, +) -> Vec { + let bits = f.into_bigint().to_bits_le(); + + let bits_per_limb = BaseField::MODULUS_BIT_SIZE as usize - 1; + let num_limbs = (TargetField::MODULUS_BIT_SIZE as usize).div_ceil(bits_per_limb); + + let mut limbs = bits + .chunks(bits_per_limb) + .map(|chunk| { + let mut limb = BaseField::zero(); + let mut w = BaseField::one(); + for &b in chunk.iter() { + limb += BaseField::from(b) * w; + w.double_in_place(); + } + limb + }) + .collect::>(); + limbs.resize(num_limbs, BaseField::zero()); + limbs.reverse(); + + limbs +} + /// NonNativeAffineVar represents an elliptic curve point in Affine representation in the non-native /// field, over the constraint field. It is not intended to perform operations, but just to contain /// the affine coordinates in order to perform hash operations of the point. @@ -33,7 +93,7 @@ where let cs = cs.into(); let affine = val.borrow().into_affine(); - let zero_point = (&C::BaseField::zero(), &C::BaseField::one()); + let zero_point = (&C::BaseField::zero(), &C::BaseField::zero()); let xy = affine.xy().unwrap_or(zero_point); let x = NonNativeFieldVar::::new_variable( @@ -84,7 +144,7 @@ where )?; let y = AllocatedNonNativeFieldVar::::get_limbs_representations( - &C::BaseField::one(), + &C::BaseField::zero(), optimization_type, )?; return Ok((x, y)); diff --git a/folding-schemes/src/folding/nova/circuits.rs b/folding-schemes/src/folding/nova/circuits.rs index 767f65b..3c35532 100644 --- a/folding-schemes/src/folding/nova/circuits.rs +++ b/folding-schemes/src/folding/nova/circuits.rs @@ -21,7 +21,7 @@ use ark_r1cs_std::{ }; use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; use ark_std::fmt::Debug; -use ark_std::{One, Zero}; +use ark_std::Zero; use core::{borrow::Borrow, marker::PhantomData}; use super::{ @@ -254,7 +254,7 @@ pub struct AugmentedFCircuit< pub r_nonnat: Option>, pub cmT: Option, pub F: FC, // F circuit - pub x: Option>, // public inputs (u_{i+1}.x) + pub x: Option>, // public input (u_{i+1}.x[0]) // cyclefold verifier on C1 // Here 'cf1, cf2' are for each of the CycleFold circuits, corresponding to the fold of cmW and @@ -268,6 +268,7 @@ pub struct AugmentedFCircuit< pub cf2_cmT: Option, pub cf1_r_nonnat: Option, pub cf2_r_nonnat: Option, + pub cf_x: Option>, // public input (u_{i+1}.x[1]) } impl>, FC: FCircuit>> @@ -300,6 +301,7 @@ where cf2_cmT: None, cf1_r_nonnat: None, cf2_r_nonnat: None, + cf_x: None, } } } @@ -308,7 +310,7 @@ impl ConstraintSynthesizer> for AugmentedFCircuit>, + GC2: CurveVar> + ToConstraintFieldGadget>, FC: FCircuit>, ::BaseField: PrimeField, ::BaseField: PrimeField, @@ -332,7 +334,7 @@ where .unwrap_or(vec![CF1::::zero(); self.F.state_len()])) })?; - let u_dummy_native = CommittedInstance::::dummy(1); + let u_dummy_native = CommittedInstance::::dummy(2); let u_i = CommittedInstanceVar::::new_witness(cs.clone(), || { Ok(self.u_i.unwrap_or(u_dummy_native.clone())) })?; @@ -365,24 +367,16 @@ where let zero = FpVar::>::new_constant(cs.clone(), CF1::::zero())?; let is_not_basecase = i.is_neq(&zero)?; - // 1. u_i.x == H(i, z_0, z_i, U_i) + // 1.a u_i.x[0] == H(i, z_0, z_i, U_i) let (u_i_x, U_i_vec) = U_i.clone() .hash(&crh_params, i.clone(), z_0.clone(), z_i.clone())?; - // check that h == u_i.x + // check that h == u_i.x[0] (u_i.x[0]).conditional_enforce_equal(&u_i_x, &is_not_basecase)?; // 2. u_i.cmE==cm(0), u_i.u==1 - let zero_x = NonNativeFieldVar::::new_constant( - cs.clone(), - C1::BaseField::zero(), - )?; - let zero_y = NonNativeFieldVar::::new_constant( - cs.clone(), - C1::BaseField::one(), - )?; - (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.cmE.x.is_zero()?).conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?; + (u_i.cmE.y.is_zero()?).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}. @@ -406,7 +400,7 @@ where 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. u_{i+1}.x = H(i+1, z_0, z_i+1, U_{i+1}), this is the output of F' + // 4.a u_{i+1}.x[0] = H(i+1, z_0, z_i+1, U_{i+1}), this is the first output of F' let (u_i1_x, _) = U_i1.clone().hash( &crh_params, i + FpVar::>::one(), @@ -444,6 +438,9 @@ where NonNativeFieldVar::::new_witness(cs.clone(), || { Ok(self.cf2_r_nonnat.unwrap_or_else(C2::ScalarField::zero)) })?; + let cf_x = FpVar::>::new_input(cs.clone(), || { + Ok(self.cf_x.unwrap_or_else(C1::ScalarField::zero)) + })?; let cfW_x: Vec> = vec![ r_nonnat.clone(), @@ -458,6 +455,15 @@ where r_nonnat, U_i.cmE.x, U_i.cmE.y, cmT.x, cmT.y, U_i1.cmE.x, U_i1.cmE.y, ]; + // 1.b u_i.x[1] == H(cf_U_i) + let (cf_u_i_x, _) = cf_U_i.clone().hash(&crh_params)?; + // check that h == u_i.x[1] + (u_i.x[1]).conditional_enforce_equal(&cf_u_i_x, &is_not_basecase)?; + + // 4.b u_{i+1}.x[1] = H(cf_U_{i+1}), this is the second output of F' + let (cf_u_i1_x, _) = cf_U_i1.clone().hash(&crh_params)?; + cf_u_i1_x.enforce_equal(&cf_x)?; + // ensure that cf1_u & cf2_u have as public inputs the cmW & cmE from main instances U_i, // u_i, U_i+1 coordinates of the commitments cf1_u_i @@ -527,7 +533,7 @@ pub mod tests { use super::*; use ark_bn254::{Fr, G1Projective as Projective}; use ark_ff::BigInteger; - use ark_r1cs_std::{alloc::AllocVar, R1CSVar}; + use ark_r1cs_std::R1CSVar; use ark_relations::r1cs::ConstraintSystem; use ark_std::UniformRand; diff --git a/folding-schemes/src/folding/nova/cyclefold.rs b/folding-schemes/src/folding/nova/cyclefold.rs index 202fffa..e04e234 100644 --- a/folding-schemes/src/folding/nova/cyclefold.rs +++ b/folding-schemes/src/folding/nova/cyclefold.rs @@ -1,30 +1,35 @@ /// 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_crypto_primitives::{ + crh::{ + poseidon::constraints::{CRHGadget, CRHParametersVar}, + CRHSchemeGadget, + }, + sponge::{ + constraints::CryptographicSpongeVar, + poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, + Absorb, CryptographicSponge, + }, }; -use ark_ec::CurveGroup; -use ark_ff::PrimeField; +use ark_ec::{AffineRepr, CurveGroup}; +use ark_ff::{Field, PrimeField, ToConstraintField}; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, - bits::uint8::UInt8, boolean::Boolean, eq::EqGadget, - fields::{fp::FpVar, nonnative::NonNativeFieldVar}, + fields::{fp::FpVar, nonnative::NonNativeFieldVar, FieldVar}, groups::GroupOpsBounds, prelude::CurveVar, - ToBytesGadget, ToConstraintFieldGadget, + ToConstraintFieldGadget, }; use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; -use ark_serialize::CanonicalSerialize; use ark_std::fmt::Debug; -use ark_std::Zero; +use ark_std::{One, Zero}; use core::{borrow::Borrow, marker::PhantomData}; use super::circuits::CF2; use super::CommittedInstance; use crate::constants::N_BITS_RO; +use crate::folding::circuits::nonnative::nonnative_field_var_to_constraint_field; use crate::Error; // public inputs length for the CycleFoldCircuit: |[r, p1.x,y, p2.x,y, p3.x,y]| @@ -82,6 +87,61 @@ where } } +impl ToConstraintFieldGadget> for CycleFoldCommittedInstanceVar +where + C: CurveGroup, + GC: CurveVar> + ToConstraintFieldGadget>, + ::BaseField: ark_ff::PrimeField + Absorb, + for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, +{ + // Extract the underlying field elements from `CycleFoldCommittedInstanceVar`, in the order of + // `u`, `x`, `cmE.x`, `cmE.y`, `cmW.x`, `cmW.y`, `cmE.is_inf || cmW.is_inf` (|| is for concat). + fn to_constraint_field(&self) -> Result>>, SynthesisError> { + let mut cmE_elems = self.cmE.to_constraint_field()?; + let mut cmW_elems = self.cmW.to_constraint_field()?; + + let cmE_is_inf = cmE_elems.pop().unwrap(); + let cmW_is_inf = cmW_elems.pop().unwrap(); + // Concatenate `cmE_is_inf` and `cmW_is_inf` to save constraints for CRHGadget::evaluate + let is_inf = cmE_is_inf.double()? + cmW_is_inf; + + Ok([ + nonnative_field_var_to_constraint_field(&self.u)?, + self.x + .iter() + .map(nonnative_field_var_to_constraint_field) + .collect::, _>>()? + .concat(), + cmE_elems, + cmW_elems, + vec![is_inf], + ] + .concat()) + } +} + +impl CycleFoldCommittedInstanceVar +where + C: CurveGroup, + GC: CurveVar> + ToConstraintFieldGadget>, + ::BaseField: ark_ff::PrimeField + Absorb, + for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, +{ + /// hash implements the committed instance hash compatible with the native implementation from + /// CommittedInstance.hash_cyclefold. + /// Returns `H(U_i)`, where `U` is the `CommittedInstance` for CycleFold. + /// Additionally it returns the vector of the field elements from the self parameters, so they + /// can be reused in other gadgets avoiding recalculating (reconstraining) them. + #[allow(clippy::type_complexity)] + pub fn hash( + self, + crh_params: &CRHParametersVar>, + ) -> Result<(FpVar>, Vec>>), SynthesisError> { + let U_vec = self.to_constraint_field()?; + Ok((CRHGadget::evaluate(crh_params, &U_vec)?, U_vec)) + } +} + /// 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 @@ -184,7 +244,7 @@ pub struct CycleFoldChallengeGadget>> { impl CycleFoldChallengeGadget where C: CurveGroup, - GC: CurveVar>, + GC: CurveVar> + ToConstraintFieldGadget>, ::BaseField: PrimeField, ::BaseField: Absorb, for<'a> &'a GC: GroupOpsBounds<'a, C, GC>, @@ -197,39 +257,30 @@ where ) -> 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(); + let mut U_vec = U_i.to_field_elements().unwrap(); + let mut u_vec = u_i.to_field_elements().unwrap(); + let (cmT_x, cmT_y, cmT_is_inf) = match cmT.into_affine().xy() { + Some((&x, &y)) => (x, y, C::BaseField::zero()), + None => ( + C::BaseField::zero(), + C::BaseField::zero(), + C::BaseField::one(), + ), + }; + + let U_cm_is_inf = U_vec.pop().unwrap(); + let u_cm_is_inf = u_vec.pop().unwrap(); + + // Concatenate `U_i.cmE_is_inf`, `U_i.cmW_is_inf`, `u_i.cmE_is_inf`, `u_i.cmW_is_inf`, `cmT_is_inf` + // to save constraints for sponge.squeeze_bits in the corresponding circuit + let is_inf = U_cm_is_inf * CF2::::from(8u8) + u_cm_is_inf.double() + cmT_is_inf; + + let input = [U_vec, u_vec, vec![cmT_x, cmT_y, is_inf]].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, @@ -240,57 +291,25 @@ where ) -> 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>> = [ - pointvar_to_bytes(U_i.cmE)?, - U_i.u.to_bytes()?, - pointvar_to_bytes(U_i.cmW)?, - U_i_x_bytes, - pointvar_to_bytes(u_i.cmE)?, - u_i.u.to_bytes()?, - pointvar_to_bytes(u_i.cmW)?, - u_i_x_bytes, - pointvar_to_bytes(cmT)?, - ] - .concat(); + let mut U_vec = U_i.to_constraint_field()?; + let mut u_vec = u_i.to_constraint_field()?; + let mut cmT_vec = cmT.to_constraint_field()?; + + let U_cm_is_inf = U_vec.pop().unwrap(); + let u_cm_is_inf = u_vec.pop().unwrap(); + let cmT_is_inf = cmT_vec.pop().unwrap(); + + // Concatenate `U_i.cmE_is_inf`, `U_i.cmW_is_inf`, `u_i.cmE_is_inf`, `u_i.cmW_is_inf`, `cmT_is_inf` + // to save constraints for sponge.squeeze_bits + let is_inf = U_cm_is_inf * CF2::::from(8u8) + u_cm_is_inf.double()? + cmT_is_inf; + + let input = [U_vec, u_vec, cmT_vec, vec![is_inf]].concat(); sponge.absorb(&input)?; let bits = sponge.squeeze_bits(N_BITS_RO)?; Ok(bits) } } -/// returns the bytes being compatible with the pointvar_to_bytes method. -/// These methods are temporary once arkworks has the fix to prevent different to_bytes behaviour -/// across different curves. Eg, in pasta and bn254: pasta returns 65 bytes both native and gadget, -/// whereas bn254 returns 64 bytes native and 65 in gadget, also the penultimate byte is different -/// natively than in gadget. -fn point_to_bytes(p: C) -> Result, Error> { - let l = p.uncompressed_size(); - let mut b = Vec::new(); - p.serialize_uncompressed(&mut b)?; - if p.is_zero() { - b[l / 2] = 1; - b[l - 1] = 1; - } - Ok(b[..63].to_vec()) -} -fn pointvar_to_bytes>>( - p: GC, -) -> Result>>, SynthesisError> { - let b = p.to_bytes()?; - Ok(b[..63].to_vec()) -} - /// 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} @@ -362,7 +381,7 @@ pub mod tests { use super::*; use ark_bn254::{constraints::GVar, Fq, Fr, G1Projective as Projective}; use ark_ff::BigInteger; - use ark_r1cs_std::{alloc::AllocVar, R1CSVar}; + use ark_r1cs_std::R1CSVar; use ark_relations::r1cs::ConstraintSystem; use ark_std::UniformRand; @@ -480,21 +499,6 @@ pub mod tests { assert!(cs.is_satisfied().unwrap()); } - #[test] - fn test_point_bytes() { - let mut rng = ark_std::test_rng(); - - let p = Projective::rand(&mut rng); - let p_bytes = point_to_bytes(p).unwrap(); - - let cs = ConstraintSystem::::new_ref(); - let pVar = GVar::new_witness(cs.clone(), || Ok(p)).unwrap(); - assert_eq!(pVar.value().unwrap(), p); - - let p_bytesVar = &pointvar_to_bytes(pVar).unwrap(); - assert_eq!(p_bytesVar.value().unwrap(), p_bytes); - } - #[test] fn test_cyclefold_challenge_gadget() { let mut rng = ark_std::test_rng(); @@ -556,4 +560,33 @@ pub mod tests { assert_eq!(rVar.value().unwrap(), r); assert_eq!(r_bitsVar.value().unwrap(), r_bits); } + + #[test] + fn test_cyclefold_hash_gadget() { + let mut rng = ark_std::test_rng(); + let poseidon_config = poseidon_test_config::(); + + 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 h = U_i.hash_cyclefold(&poseidon_config).unwrap(); + + let cs = ConstraintSystem::::new_ref(); + let U_iVar = + CycleFoldCommittedInstanceVar::::new_witness(cs.clone(), || { + Ok(U_i.clone()) + }) + .unwrap(); + let (hVar, _) = U_iVar + .hash(&CRHParametersVar::new_constant(cs.clone(), poseidon_config).unwrap()) + .unwrap(); + hVar.enforce_equal(&FpVar::new_witness(cs.clone(), || Ok(h)).unwrap()) + .unwrap(); + assert!(cs.is_satisfied().unwrap()); + } } diff --git a/folding-schemes/src/folding/nova/decider_eth.rs b/folding-schemes/src/folding/nova/decider_eth.rs index 1e3dcc5..5f8cb00 100644 --- a/folding-schemes/src/folding/nova/decider_eth.rs +++ b/folding-schemes/src/folding/nova/decider_eth.rs @@ -57,7 +57,7 @@ where C1: CurveGroup, C2: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, - GC2: CurveVar>, + GC2: CurveVar> + ToConstraintFieldGadget>, FC: FCircuit, CS1: CommitmentScheme< C1, diff --git a/folding-schemes/src/folding/nova/decider_eth_circuit.rs b/folding-schemes/src/folding/nova/decider_eth_circuit.rs index db50ef6..0a00e05 100644 --- a/folding-schemes/src/folding/nova/decider_eth_circuit.rs +++ b/folding-schemes/src/folding/nova/decider_eth_circuit.rs @@ -16,7 +16,7 @@ use ark_r1cs_std::{ ToConstraintFieldGadget, }; use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError}; -use ark_std::{log2, One, Zero}; +use ark_std::{log2, Zero}; use core::{borrow::Borrow, marker::PhantomData}; use super::{circuits::ChallengeGadget, nifs::NIFS}; @@ -245,7 +245,7 @@ where C1: CurveGroup, C2: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, - GC2: CurveVar>, + GC2: CurveVar> + ToConstraintFieldGadget>, CS1: CommitmentScheme, // enforce that the CS2 is Pedersen commitment scheme, since we're at Ethereum's EVM decider CS2: CommitmentScheme>, @@ -337,7 +337,7 @@ where C1: CurveGroup, C2: CurveGroup, GC1: CurveVar>, - GC2: CurveVar>, + GC2: CurveVar> + ToConstraintFieldGadget>, CS1: CommitmentScheme, CS2: CommitmentScheme, ::BaseField: PrimeField, @@ -362,9 +362,9 @@ where Ok(self.z_i.unwrap_or(vec![CF1::::zero()])) })?; - let u_dummy_native = CommittedInstance::::dummy(1); + let u_dummy_native = CommittedInstance::::dummy(2); 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) */], + vec![C1::ScalarField::zero(); self.r1cs.A.n_cols - 3 /* (3=2+1, since u_i.x.len=2) */], self.E_len, ); @@ -412,20 +412,13 @@ where )?; // 2. 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(), - )?; - let zero_y = NonNativeFieldVar::::new_constant( - cs.clone(), - C1::BaseField::one(), - )?; - u_i.cmE.x.enforce_equal(&zero_x)?; - u_i.cmE.y.enforce_equal(&zero_y)?; + // Here zero is the x & y coordinates of the zero point affine representation. + let zero = NonNativeFieldVar::::zero(); + u_i.cmE.x.enforce_equal(&zero)?; + u_i.cmE.y.enforce_equal(&zero)?; (u_i.u.is_one()?).enforce_equal(&Boolean::TRUE)?; - // 3. u_i.x == H(i, z_0, z_i, U_i) + // 3.a u_i.x[0] == H(i, z_0, z_i, U_i) let (u_i_x, U_i_vec) = U_i.clone() .hash(&crh_params, i.clone(), z_0.clone(), z_i.clone())?; @@ -454,6 +447,10 @@ where Ok(self.cf_W_i.unwrap_or(w_dummy_native.clone())) })?; + // 3.b u_i.x[1] == H(cf_U_i) + let (cf_u_i_x, _) = cf_U_i.clone().hash(&crh_params)?; + (u_i.x[1]).enforce_equal(&cf_u_i_x)?; + // 4. check Pedersen commitments of cf_U_i.{cmE, cmW} let H = GC2::new_constant(cs.clone(), self.cf_pedersen_params.h)?; let G = Vec::::new_constant(cs.clone(), self.cf_pedersen_params.generators)?; @@ -615,16 +612,10 @@ pub mod tests { }, 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_r1cs_std::bits::uint8::UInt8; use ark_relations::r1cs::ConstraintSystem; - use ark_std::UniformRand; + use ark_std::{One, UniformRand}; use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2}; use crate::commitment::pedersen::Pedersen; @@ -633,11 +624,8 @@ pub mod tests { use crate::transcript::poseidon::poseidon_test_config; use crate::FoldingScheme; + use crate::ccs::r1cs::tests::{get_test_r1cs, get_test_z}; use crate::ccs::r1cs::{extract_r1cs, extract_w_x}; - use crate::ccs::r1cs::{ - tests::{get_test_r1cs, get_test_z}, - R1CS, - }; #[test] fn test_relaxed_r1cs_small_gadget_handcrafted() { diff --git a/folding-schemes/src/folding/nova/mod.rs b/folding-schemes/src/folding/nova/mod.rs index fc8956c..dab6632 100644 --- a/folding-schemes/src/folding/nova/mod.rs +++ b/folding-schemes/src/folding/nova/mod.rs @@ -5,7 +5,7 @@ use ark_crypto_primitives::{ sponge::{poseidon::PoseidonConfig, Absorb}, }; use ark_ec::{AffineRepr, CurveGroup, Group}; -use ark_ff::{BigInteger, PrimeField}; +use ark_ff::{BigInteger, Field, PrimeField, ToConstraintField}; use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget}; use ark_std::fmt::Debug; use ark_std::{One, Zero}; @@ -15,7 +15,9 @@ use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem}; use crate::ccs::r1cs::{extract_r1cs, extract_w_x, R1CS}; use crate::commitment::CommitmentScheme; -use crate::folding::circuits::nonnative::point_to_nonnative_limbs; +use crate::folding::circuits::nonnative::{ + nonnative_field_to_field_elements, point_to_nonnative_limbs, +}; use crate::frontend::FCircuit; use crate::utils::vec::is_zero_vec; use crate::Error; @@ -93,6 +95,56 @@ where } } +impl ToConstraintField for CommittedInstance +where + ::BaseField: ark_ff::PrimeField + Absorb, +{ + fn to_field_elements(&self) -> Option> { + let u = nonnative_field_to_field_elements(&self.u); + let x = self + .x + .iter() + .flat_map(nonnative_field_to_field_elements) + .collect::>(); + let (cmE_x, cmE_y, cmE_is_inf) = match self.cmE.into_affine().xy() { + Some((&x, &y)) => (x, y, C::BaseField::zero()), + None => ( + C::BaseField::zero(), + C::BaseField::zero(), + C::BaseField::one(), + ), + }; + let (cmW_x, cmW_y, cmW_is_inf) = match self.cmW.into_affine().xy() { + Some((&x, &y)) => (x, y, C::BaseField::zero()), + None => ( + C::BaseField::zero(), + C::BaseField::zero(), + C::BaseField::one(), + ), + }; + // Concatenate `cmE_is_inf` and `cmW_is_inf` to save constraints for CRHGadget::evaluate in the corresponding circuit + let is_inf = cmE_is_inf.double() + cmW_is_inf; + + Some([u, x, vec![cmE_x, cmE_y, cmW_x, cmW_y, is_inf]].concat()) + } +} + +impl CommittedInstance +where + ::BaseField: ark_ff::PrimeField + Absorb, +{ + /// hash_cyclefold implements the committed instance hash compatible with the gadget implemented in + /// nova/cyclefold.rs::CycleFoldCommittedInstanceVar.hash. + /// Returns `H(U_i)`, where `U_i` is the `CommittedInstance` for CycleFold. + pub fn hash_cyclefold( + &self, + poseidon_config: &PoseidonConfig, + ) -> Result { + CRH::::evaluate(poseidon_config, self.to_field_elements().unwrap()) + .map_err(|e| Error::Other(e.to_string())) + } +} + #[derive(Debug, Clone, Eq, PartialEq)] pub struct Witness { pub E: Vec, @@ -203,7 +255,7 @@ where C1: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, C2: CurveGroup, - GC2: CurveVar>, + GC2: CurveVar> + ToConstraintFieldGadget>, FC: FCircuit, CS1: CommitmentScheme, CS2: CommitmentScheme, @@ -320,15 +372,18 @@ where )?; // folded instance output (public input, x) - // u_{i+1}.x = H(i+1, z_0, z_{i+1}, U_{i+1}) + // u_{i+1}.x[0] = 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(), )?; + // u_{i+1}.x[1] = H(cf_U_{i+1}) + let cf_u_i1_x: C1::ScalarField; if self.i == C1::ScalarField::zero() { + cf_u_i1_x = self.cf_U_i.hash_cyclefold(&self.poseidon_config)?; // base case augmented_F_circuit = AugmentedFCircuit:: { _gc2: PhantomData, @@ -353,6 +408,7 @@ where cf2_cmT: None, cf1_r_nonnat: None, cf2_r_nonnat: None, + cf_x: Some(cf_u_i1_x), }; #[cfg(test)] @@ -406,6 +462,8 @@ where let (_cfE_w_i, cfE_u_i, cf_W_i1, cf_U_i1, cf_cmT, cf_r2_Fq) = self.fold_cyclefold_circuit(cfW_W_i1, cfW_U_i1.clone(), cfE_u_i_x, cfE_circuit)?; + cf_u_i1_x = cf_U_i1.hash_cyclefold(&self.poseidon_config)?; + augmented_F_circuit = AugmentedFCircuit:: { _gc2: PhantomData, poseidon_config: self.poseidon_config.clone(), @@ -430,6 +488,7 @@ where cf2_cmT: Some(cf_cmT), cf1_r_nonnat: Some(cfW_r1_Fq), cf2_r_nonnat: Some(cf_r2_Fq), + cf_x: Some(cf_u_i1_x), }; self.cf_W_i = cf_W_i1.clone(); @@ -453,20 +512,20 @@ where let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?; let (w_i1, x_i1) = extract_w_x::(&cs); - if x_i1[0] != u_i1_x { + if x_i1[0] != u_i1_x || x_i1[1] != cf_u_i1_x { return Err(Error::NotEqual); } #[cfg(test)] - if x_i1.len() != 1 { - return Err(Error::NotExpectedLength(x_i1.len(), 1)); + if x_i1.len() != 2 { + return Err(Error::NotExpectedLength(x_i1.len(), 2)); } // set values for next iteration self.i += C1::ScalarField::one(); self.z_i = z_i1.clone(); self.w_i = Witness::::new(w_i1, self.r1cs.A.n_rows); - self.u_i = self.w_i.commit::(&self.cs_params, vec![u_i1_x])?; + self.u_i = self.w_i.commit::(&self.cs_params, x_i1)?; self.W_i = W_i1.clone(); self.U_i = U_i1.clone(); @@ -511,16 +570,21 @@ where let (u_i, w_i) = incoming_instance; let (cf_U_i, cf_W_i) = cyclefold_instance; - if u_i.x.len() != 1 || U_i.x.len() != 1 { + if u_i.x.len() != 2 || U_i.x.len() != 2 { return Err(Error::IVCVerificationFail); } // check that u_i's output points to the running instance - // u_i.X == H(i, z_0, z_i, U_i) + // u_i.X[0] == H(i, z_0, z_i, U_i) let expected_u_i_x = U_i.hash(&vp.poseidon_config, num_steps, z_0, z_i.clone())?; if expected_u_i_x != u_i.x[0] { return Err(Error::IVCVerificationFail); } + // u_i.X[1] == H(cf_U_i) + let expected_cf_u_i_x = cf_U_i.hash_cyclefold(&vp.poseidon_config)?; + if expected_cf_u_i_x != u_i.x[1] { + return Err(Error::IVCVerificationFail); + } // check u_i.cmE==0, u_i.u==1 (=u_i is a un-relaxed instance) if !u_i.cmE.is_zero() || !u_i.u.is_one() { @@ -589,7 +653,7 @@ where C1: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, C2: CurveGroup, - GC2: CurveVar>, + GC2: CurveVar> + ToConstraintFieldGadget>, FC: FCircuit, CS1: CommitmentScheme, CS2: CommitmentScheme, @@ -680,7 +744,7 @@ where C1: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, C2: CurveGroup, - GC2: CurveVar>, + GC2: CurveVar> + ToConstraintFieldGadget>, FC: FCircuit, ::BaseField: PrimeField, ::BaseField: PrimeField, @@ -708,7 +772,7 @@ where C1: CurveGroup, GC1: CurveVar> + ToConstraintFieldGadget>, C2: CurveGroup, - GC2: CurveVar>, + GC2: CurveVar> + ToConstraintFieldGadget>, FC: FCircuit, ::BaseField: PrimeField, ::BaseField: PrimeField, @@ -725,7 +789,7 @@ where /// returns the coordinates of a commitment point. This is compatible with the arkworks /// GC.to_constraint_field()[..2] pub(crate) fn get_cm_coordinates(cm: &C) -> Vec { - let zero = (&C::BaseField::zero(), &C::BaseField::one()); + let zero = (&C::BaseField::zero(), &C::BaseField::zero()); let cm = cm.into_affine(); let (cm_x, cm_y) = cm.xy().unwrap_or(zero); vec![*cm_x, *cm_y] diff --git a/folding-schemes/src/transcript/poseidon.rs b/folding-schemes/src/transcript/poseidon.rs index 71c368f..309054e 100644 --- a/folding-schemes/src/transcript/poseidon.rs +++ b/folding-schemes/src/transcript/poseidon.rs @@ -7,7 +7,7 @@ use ark_ec::{AffineRepr, CurveGroup, Group}; use ark_ff::{BigInteger, Field, PrimeField}; use ark_r1cs_std::{boolean::Boolean, fields::fp::FpVar}; use ark_relations::r1cs::{ConstraintSystemRef, SynthesisError}; -use ark_std::{One, Zero}; +use ark_std::Zero; use crate::transcript::Transcript; use crate::Error; @@ -61,7 +61,7 @@ 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 zero_point = (&C::BaseField::zero(), &C::BaseField::one()); + let zero_point = (&C::BaseField::zero(), &C::BaseField::zero()); let xy = affine.xy().unwrap_or(zero_point); let x_bi = @@ -145,7 +145,7 @@ pub fn poseidon_test_config() -> PoseidonConfig { pub mod tests { use super::*; use ark_pallas::{constraints::GVar, Fq, Fr, Projective}; - use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, groups::CurveVar, R1CSVar}; + use ark_r1cs_std::{alloc::AllocVar, groups::CurveVar, R1CSVar}; use ark_relations::r1cs::ConstraintSystem; use ark_vesta::Projective as E2Projective; use std::ops::Mul;