diff --git a/Cargo.toml b/Cargo.toml index a8f99d5..686dbf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ ark-ec = "0.4.2" ark-ff = "^0.4.0" ark-poly = "^0.4.0" ark-std = "^0.4.0" -ark-crypto-primitives = { version = "^0.4.0", default-features = false, features = ["r1cs", "sponge"] } +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 = { version = "^0.4.0", default-features = false } thiserror = "1.0" @@ -23,7 +23,6 @@ espresso_transcript = {git="https://github.com/EspressoSystems/hyperplonk", pack [dev-dependencies] ark-pallas = {version="0.4.0", features=["r1cs"]} ark-vesta = {version="0.4.0"} -ark-crypto-primitives = { version = "^0.4.0", default-features = false, features = ["crh"] } [features] default = ["parallel", "nova", "hypernova"] diff --git a/src/folding/circuits/mod.rs b/src/folding/circuits/mod.rs index 02fb9b6..f3691d1 100644 --- a/src/folding/circuits/mod.rs +++ b/src/folding/circuits/mod.rs @@ -3,6 +3,7 @@ use ark_ec::CurveGroup; use ark_ff::Field; pub mod cyclefold; +pub mod nonnative; // CF represents the constraints field pub type CF = <::BaseField as Field>::BasePrimeField; diff --git a/src/folding/circuits/nonnative.rs b/src/folding/circuits/nonnative.rs new file mode 100644 index 0000000..f5a37a9 --- /dev/null +++ b/src/folding/circuits/nonnative.rs @@ -0,0 +1,71 @@ +use ark_ec::{AffineRepr, CurveGroup}; +use ark_ff::PrimeField; +use ark_r1cs_std::fields::nonnative::{params::OptimizationType, AllocatedNonNativeFieldVar}; +use ark_r1cs_std::{ + alloc::{AllocVar, AllocationMode}, + fields::nonnative::NonNativeFieldVar, +}; +use ark_relations::r1cs::{Namespace, SynthesisError}; +use core::borrow::Borrow; + +/// NonNativeAffineVar represents an elliptic curve point in Affine represenation in the non-native +/// field. It is not intended to perform operations, but just to contain the affine coordinates in +/// order to perform hash operations of the point. +#[derive(Debug, Clone)] +pub struct NonNativeAffineVar { + pub x: NonNativeFieldVar, + pub y: NonNativeFieldVar, +} + +impl AllocVar for NonNativeAffineVar +where + C: CurveGroup, + ::BaseField: ark_ff::PrimeField, +{ + fn new_variable>( + cs: impl Into>, + f: impl FnOnce() -> Result, + mode: AllocationMode, + ) -> Result { + f().and_then(|val| { + let cs = cs.into(); + + let affine = val.borrow().into_affine(); + let xy = affine.xy().unwrap(); + let x = NonNativeFieldVar::::new_variable( + cs.clone(), + || Ok(xy.0), + mode, + )?; + let y = NonNativeFieldVar::::new_variable( + cs.clone(), + || Ok(xy.1), + mode, + )?; + + Ok(Self { x, y }) + }) + } +} + +/// point_to_nonnative_limbs is used to return (outside the circuit) the limbs representation that +/// matches the one used in-circuit. +#[allow(clippy::type_complexity)] +pub fn point_to_nonnative_limbs( + p: C, +) -> Result<(Vec, Vec), SynthesisError> +where + ::BaseField: ark_ff::PrimeField, +{ + let affine = p.into_affine(); + let (x, y) = affine.xy().unwrap(); + let x = AllocatedNonNativeFieldVar::::get_limbs_representations( + x, + OptimizationType::Weight, + )?; + let y = AllocatedNonNativeFieldVar::::get_limbs_representations( + y, + OptimizationType::Weight, + )?; + Ok((x, y)) +} diff --git a/src/folding/nova/circuits.rs b/src/folding/nova/circuits.rs index 69ea0a6..6557941 100644 --- a/src/folding/nova/circuits.rs +++ b/src/folding/nova/circuits.rs @@ -1,4 +1,9 @@ -use ark_ec::{AffineRepr, CurveGroup}; +use ark_crypto_primitives::crh::{ + poseidon::constraints::{CRHGadget, CRHParametersVar}, + CRHSchemeGadget, +}; +use ark_crypto_primitives::sponge::Absorb; +use ark_ec::{AffineRepr, CurveGroup, Group}; use ark_ff::Field; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, @@ -7,30 +12,38 @@ use ark_r1cs_std::{ fields::fp::FpVar, groups::GroupOpsBounds, prelude::CurveVar, + ToConstraintFieldGadget, }; use ark_relations::r1cs::{Namespace, SynthesisError}; use core::{borrow::Borrow, marker::PhantomData}; use super::CommittedInstance; -use crate::folding::circuits::cyclefold::ECRLC; +use crate::folding::circuits::{cyclefold::ECRLC, nonnative::NonNativeAffineVar}; -/// CF1 represents the ConstraintField used for the main Nova circuit which is over E1::Fr. +/// 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. +/// 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. pub type CF2 = <::BaseField as Field>::BasePrimeField; -/// CommittedInstance on E1 contains the u and x values which are folded on the main Nova -/// constraints field (E1::Fr). +/// 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). #[derive(Debug, Clone)] -pub struct CommittedInstanceE1Var { +pub struct CommittedInstanceVar { _c: PhantomData, u: FpVar, x: Vec>, + #[allow(dead_code)] // tmp while we don't have the code of the AugmentedFGadget + cmE: NonNativeAffineVar, C::ScalarField>, + cmW: NonNativeAffineVar, C::ScalarField>, } -impl AllocVar, CF1> for CommittedInstanceE1Var +impl AllocVar, CF1> for CommittedInstanceVar where C: CurveGroup, + ::BaseField: ark_ff::PrimeField, { fn new_variable>>( cs: impl Into>>, @@ -42,20 +55,63 @@ where let u = FpVar::::new_variable(cs.clone(), || Ok(val.borrow().u), mode)?; let x: Vec> = - Vec::new_variable(cs, || Ok(val.borrow().x.clone()), mode)?; + Vec::new_variable(cs.clone(), || Ok(val.borrow().x.clone()), mode)?; + + let cmE = NonNativeAffineVar::, C::ScalarField>::new_variable( + cs.clone(), + || Ok(val.borrow().cmE), + mode, + )?; + let cmW = NonNativeAffineVar::, C::ScalarField>::new_variable( + cs.clone(), + || Ok(val.borrow().cmW), + mode, + )?; Ok(Self { _c: PhantomData, u, x, + cmE, + cmW, }) }) } } -/// CommittedInstance on E2 contains the commitments to E and W, which are folded on the auxiliary -/// curve constraints field (E2::Fr = E1::Fq). -pub struct CommittedInstanceE2Var>> +impl CommittedInstanceVar +where + C: CurveGroup, + ::ScalarField: Absorb, +{ + /// hash implements the committed instance hash compatible with the native implementation from + /// CommittedInstance.hash. + /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and `U` is the + /// `CommittedInstance`. + #[allow(dead_code)] // tmp while we don't have the code of the AugmentedFGadget + fn hash( + self, + crh_params: &CRHParametersVar>, + i: FpVar>, + z_0: FpVar>, + z_i: FpVar>, + ) -> Result>, SynthesisError> { + let input = vec![ + vec![i, z_0, z_i, self.u], + self.x, + self.cmE.x.to_constraint_field()?, + self.cmE.y.to_constraint_field()?, + self.cmW.x.to_constraint_field()?, + self.cmW.y.to_constraint_field()?, + ] + .concat(); + CRHGadget::::evaluate(crh_params, &input) + } +} + +/// 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>, { @@ -64,7 +120,7 @@ where cmW: GC, } -impl AllocVar, CF2> for CommittedInstanceE2Var +impl AllocVar, CF2> for CommittedInstanceCycleFoldVar where C: CurveGroup, GC: CurveVar>, @@ -105,9 +161,9 @@ where /// the CycleFold circuit. pub fn verify( r: FpVar>, - ci1: CommittedInstanceE1Var, - ci2: CommittedInstanceE1Var, - ci3: CommittedInstanceE1Var, + ci1: CommittedInstanceVar, + ci2: CommittedInstanceVar, + ci3: CommittedInstanceVar, ) -> Result<(), SynthesisError> { // ensure that: ci3.u == ci1.u + r * ci2.u ci3.u.enforce_equal(&(ci1.u + r.clone() * ci2.u))?; @@ -140,9 +196,9 @@ where pub fn verify( r_bits: Vec>>, cmT: GC, - ci1: CommittedInstanceE2Var, - ci2: CommittedInstanceE2Var, - ci3: CommittedInstanceE2Var, + ci1: CommittedInstanceCycleFoldVar, + ci2: CommittedInstanceCycleFoldVar, + ci3: CommittedInstanceCycleFoldVar, ) -> Result<(), SynthesisError> { // cm(E) check: ci3.cmE == ci1.cmE + r * cmT + r^2 * ci2.cmE ci3.cmE.enforce_equal( @@ -183,16 +239,17 @@ mod tests { let cs = ConstraintSystem::::new_ref(); let ciVar = - CommittedInstanceE1Var::::new_witness(cs.clone(), || Ok(ci.clone())) - .unwrap(); + 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 = - CommittedInstanceE2Var::::new_witness(cs.clone(), || Ok(ci.clone())) - .unwrap(); + 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); } @@ -216,8 +273,8 @@ mod tests { let ci2 = w2.commit(&pedersen_params, x2.clone()); // get challenge from transcript - let config = poseidon_test_config::(); - let mut tr = PoseidonTranscript::::new(&config); + 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(); @@ -228,13 +285,13 @@ mod tests { let rVar = FpVar::::new_witness(cs.clone(), || Ok(r_Fr)).unwrap(); let ci1Var = - CommittedInstanceE1Var::::new_witness(cs.clone(), || Ok(ci1.clone())) + CommittedInstanceVar::::new_witness(cs.clone(), || Ok(ci1.clone())) .unwrap(); let ci2Var = - CommittedInstanceE1Var::::new_witness(cs.clone(), || Ok(ci2.clone())) + CommittedInstanceVar::::new_witness(cs.clone(), || Ok(ci2.clone())) .unwrap(); let ci3Var = - CommittedInstanceE1Var::::new_witness(cs.clone(), || Ok(ci3.clone())) + CommittedInstanceVar::::new_witness(cs.clone(), || Ok(ci3.clone())) .unwrap(); NIFSGadget::::verify( @@ -253,21 +310,60 @@ mod tests { 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 = CommittedInstanceE2Var::::new_witness(cs_CC.clone(), || { - Ok(ci1.clone()) - }) - .unwrap(); - let ci2Var = CommittedInstanceE2Var::::new_witness(cs_CC.clone(), || { - Ok(ci2.clone()) - }) - .unwrap(); - let ci3Var = CommittedInstanceE2Var::::new_witness(cs_CC.clone(), || { - Ok(ci3.clone()) - }) - .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(); NIFSCycleFoldGadget::::verify(r_bitsVar, cmTVar, ci1Var, ci2Var, ci3Var) .unwrap(); assert!(cs_CC.is_satisfied().unwrap()); } + + #[test] + fn test_committed_instance_hash() { + let mut rng = ark_std::test_rng(); + let poseidon_config = poseidon_test_config::(); + + let i = Fr::from(3_u32); + let z_0 = Fr::from(3_u32); + let z_i = Fr::from(3_u32); + 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], + }; + + // compute the CommittedInstance hash natively + let h = ci.hash(&poseidon_config, i, z_0, z_i).unwrap(); + + let cs = ConstraintSystem::::new_ref(); + + let iVar = FpVar::::new_witness(cs.clone(), || Ok(i)).unwrap(); + let z_0Var = FpVar::::new_witness(cs.clone(), || Ok(z_0)).unwrap(); + let z_iVar = FpVar::::new_witness(cs.clone(), || Ok(z_i)).unwrap(); + let ciVar = + CommittedInstanceVar::::new_witness(cs.clone(), || Ok(ci.clone())).unwrap(); + + let crh_params = CRHParametersVar::::new_constant(cs.clone(), poseidon_config).unwrap(); + + // compute the CommittedInstance hash in-circuit + let hVar = ciVar.hash(&crh_params, iVar, z_0Var, z_iVar).unwrap(); + assert!(cs.is_satisfied().unwrap()); + + // check that the natively computed and in-circuit computed hashes match + assert_eq!(hVar.value().unwrap(), h); + } } diff --git a/src/folding/nova/mod.rs b/src/folding/nova/mod.rs index 1ec59f3..704f068 100644 --- a/src/folding/nova/mod.rs +++ b/src/folding/nova/mod.rs @@ -1,10 +1,15 @@ /// Implements the scheme described in [Nova](https://eprint.iacr.org/2021/370.pdf) -use ark_crypto_primitives::sponge::Absorb; +use ark_crypto_primitives::{ + crh::{poseidon::CRH, CRHScheme}, + sponge::{poseidon::PoseidonConfig, Absorb}, +}; use ark_ec::{CurveGroup, Group}; use ark_std::fmt::Debug; use ark_std::{One, Zero}; +use crate::folding::circuits::nonnative::point_to_nonnative_limbs; use crate::pedersen::{Params as PedersenParams, Pedersen}; +use crate::Error; pub mod circuits; pub mod nifs; @@ -17,7 +22,11 @@ pub struct CommittedInstance { pub x: Vec, } -impl CommittedInstance { +impl CommittedInstance +where + ::ScalarField: Absorb, + ::BaseField: ark_ff::PrimeField, +{ pub fn empty() -> Self { CommittedInstance { cmE: C::zero(), @@ -26,6 +35,35 @@ impl CommittedInstance { x: Vec::new(), } } + + /// 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 + /// `CommittedInstance`. + pub fn hash( + &self, + poseidon_config: &PoseidonConfig, + i: C::ScalarField, + z_0: C::ScalarField, + z_i: C::ScalarField, + ) -> Result { + let (cmE_x, cmE_y) = point_to_nonnative_limbs::(self.cmE)?; + let (cmW_x, cmW_y) = point_to_nonnative_limbs::(self.cmW)?; + + Ok(CRH::::evaluate( + poseidon_config, + vec![ + vec![i, z_0, z_i, self.u], + self.x.clone(), + cmE_x, + cmE_y, + cmW_x, + cmW_y, + ] + .concat(), + ) + .unwrap()) + } } #[derive(Debug, Clone, Eq, PartialEq)]