Traits for witnesses and committed instances (#157)
* Add traits for witness and committed instance
* Implement witness and committed instance traits for Nova and HyperNova
* Implement witness and committed instance traits for ProtoGalaxy
* Improve the clarity of docs for `Witness{Var}Ext::get_openings`
* Avoid cloning `z_i`
* Fix grammar issues
* Rename `Ext` traits for committed instances and witnesses to `Ops`
* Implement `to_sponge_bytes`
jump to
@@ -9,9 +9,11 @@ use std::sync::Arc;
use ark_std::rand::Rng; +use super::circuits::CCCSVar; use super::Witness; use crate::arith::{ccs::CCS, Arith}; use crate::commitment::CommitmentScheme; +use crate::folding::traits::CommittedInstanceOps; use crate::transcript::AbsorbNonNative; use crate::utils::mle::dense_vec_to_dense_mle; use crate::utils::vec::mat_vec_mul;@@ -126,9 +128,8 @@ impl<C: CurveGroup> Absorb for CCCS<C>
where C::ScalarField: Absorb, { - fn to_sponge_bytes(&self, _dest: &mut Vec<u8>) { - // This is never called - unimplemented!() + fn to_sponge_bytes(&self, dest: &mut Vec<u8>) { + C::ScalarField::batch_to_sponge_bytes(&self.to_sponge_field_elements_as_vec(), dest); } fn to_sponge_field_elements<F: PrimeField>(&self, dest: &mut Vec<F>) {@@ -139,6 +140,18 @@ self.C
.to_native_sponge_field_elements_as_vec() .to_sponge_field_elements(dest); self.x.to_sponge_field_elements(dest); + } +} + +impl<C: CurveGroup> CommittedInstanceOps<C> for CCCS<C> { + type Var = CCCSVar<C>; + + fn get_commitments(&self) -> Vec<C> { + vec![self.C] + } + + fn is_incoming(&self) -> bool { + true } }
@@ -1,6 +1,6 @@
/// Implementation of [HyperNova](https://eprint.iacr.org/2023/573.pdf) circuits use ark_crypto_primitives::sponge::{ - constraints::CryptographicSpongeVar, + constraints::{AbsorbGadget, CryptographicSpongeVar}, poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}, CryptographicSponge, };@@ -14,6 +14,7 @@ eq::EqGadget,
fields::{fp::FpVar, FieldVar}, groups::GroupOpsBounds, prelude::CurveVar, + uint8::UInt8, R1CSVar, ToConstraintFieldGadget, }; use ark_relations::r1cs::{@@ -41,6 +42,7 @@ utils::EqEvalGadget,
CF1, CF2, }, nova::get_r1cs_from_cs, + traits::CommittedInstanceVarOps, }; use crate::frontend::FCircuit; use crate::utils::virtual_polynomial::VPAuxInfo;@@ -52,19 +54,16 @@ };
/// Committed CCS instance #[derive(Debug, Clone)] -pub struct CCCSVar<C: CurveGroup> -where - <C as CurveGroup>::BaseField: PrimeField, -{ +pub struct CCCSVar<C: CurveGroup> { // Commitment to witness pub C: NonNativeAffineVar<C>, // Public io pub x: Vec<FpVar<CF1<C>>>, } + impl<C> AllocVar<CCCS<C>, CF1<C>> for CCCSVar<C> where C: CurveGroup, - <C as ark_ec::CurveGroup>::BaseField: PrimeField, { fn new_variable<T: Borrow<CCCS<C>>>( cs: impl Into<Namespace<CF1<C>>>,@@ -83,12 +82,30 @@ })
} } +impl<C: CurveGroup> CommittedInstanceVarOps<C> for CCCSVar<C> { + type PointVar = NonNativeAffineVar<C>; + + fn get_commitments(&self) -> Vec<Self::PointVar> { + vec![self.C.clone()] + } + + fn get_public_inputs(&self) -> &[FpVar<CF1<C>>] { + &self.x + } + + fn enforce_incoming(&self) -> Result<(), SynthesisError> { + // `CCCSVar` is always the incoming instance + Ok(()) + } + + fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError> { + self.x.enforce_equal(&other.x) + } +} + /// Linearized Committed CCS instance #[derive(Debug, Clone)] -pub struct LCCCSVar<C: CurveGroup> -where - <C as CurveGroup>::BaseField: PrimeField, -{ +pub struct LCCCSVar<C: CurveGroup> { // Commitment to witness pub C: NonNativeAffineVar<C>, // Relaxation factor of z for folded LCCCS@@ -100,10 +117,10 @@ pub r_x: Vec<FpVar<CF1<C>>>,
// Vector of v_i pub v: Vec<FpVar<CF1<C>>>, } + impl<C> AllocVar<LCCCS<C>, CF1<C>> for LCCCSVar<C> where C: CurveGroup, - <C as ark_ec::CurveGroup>::BaseField: PrimeField, { fn new_variable<T: Borrow<LCCCS<C>>>( cs: impl Into<Namespace<CF1<C>>>,@@ -127,41 +144,44 @@ })
} } -impl<C> LCCCSVar<C> -where - C: CurveGroup, - <C as Group>::ScalarField: Absorb, - <C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField, -{ - /// [`LCCCSVar`].hash implements the LCCCS instance hash compatible with the native - /// implementation from LCCCS.hash. - /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and `U` is the LCCCS. - /// 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, - sponge: &PoseidonSpongeVar<CF1<C>>, - pp_hash: FpVar<CF1<C>>, - i: FpVar<CF1<C>>, - z_0: Vec<FpVar<CF1<C>>>, - z_i: Vec<FpVar<CF1<C>>>, - ) -> Result<(FpVar<CF1<C>>, Vec<FpVar<CF1<C>>>), SynthesisError> { - let mut sponge = sponge.clone(); - let U_vec = [ - self.C.to_constraint_field()?, - vec![self.u], - self.x, - self.r_x, - self.v, +impl<C: CurveGroup> AbsorbGadget<C::ScalarField> for LCCCSVar<C> { + fn to_sponge_bytes(&self) -> Result<Vec<UInt8<C::ScalarField>>, SynthesisError> { + FpVar::batch_to_sponge_bytes(&self.to_sponge_field_elements()?) + } + + fn to_sponge_field_elements(&self) -> Result<Vec<FpVar<C::ScalarField>>, SynthesisError> { + Ok([ + &self.C.to_constraint_field()?, + &[self.u.clone()][..], + &self.x, + &self.r_x, + &self.v, ] - .concat(); - sponge.absorb(&pp_hash)?; - sponge.absorb(&i)?; - sponge.absorb(&z_0)?; - sponge.absorb(&z_i)?; - sponge.absorb(&U_vec)?; - Ok((sponge.squeeze_field_elements(1)?.pop().unwrap(), U_vec)) + .concat()) + } +} + +impl<C: CurveGroup> CommittedInstanceVarOps<C> for LCCCSVar<C> { + type PointVar = NonNativeAffineVar<C>; + + fn get_commitments(&self) -> Vec<Self::PointVar> { + vec![self.C.clone()] + } + + fn get_public_inputs(&self) -> &[FpVar<CF1<C>>] { + &self.x + } + + fn enforce_incoming(&self) -> Result<(), SynthesisError> { + // `LCCCSVar` is always the running instance + Err(SynthesisError::Unsatisfiable) + } + + fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError> { + self.u.enforce_equal(&other.u)?; + self.x.enforce_equal(&other.x)?; + self.r_x.enforce_equal(&other.r_x)?; + self.v.enforce_equal(&other.v) } }@@ -742,25 +762,13 @@ let cf_cmT = GC2::new_witness(cs.clone(), || Ok(self.cf_cmT.unwrap_or_else(C2::zero)))?;
let sponge = PoseidonSpongeVar::<C1::ScalarField>::new(cs.clone(), &self.poseidon_config); - // get z_{i+1} from the F circuit - let i_usize = self.i_usize.unwrap_or(0); - let z_i1 = - self.F - .generate_step_constraints(cs.clone(), i_usize, z_i.clone(), external_inputs)?; - let is_basecase = i.is_zero()?; let is_not_basecase = is_basecase.not(); // Primary Part // P.1. Compute u_i.x // u_i.x[0] = H(i, z_0, z_i, U_i) - let (u_i_x, _) = U_i.clone().hash( - &sponge, - pp_hash.clone(), - i.clone(), - z_0.clone(), - z_i.clone(), - )?; + let (u_i_x, _) = U_i.clone().hash(&sponge, &pp_hash, &i, &z_0, &z_i)?; // u_i.x[1] = H(cf_U_i) let (cf_u_i_x, cf_U_i_vec) = cf_U_i.clone().hash(&sponge, pp_hash.clone())?;@@ -795,19 +803,26 @@ )?;
U_i1.C = U_i1_C; // P.4.a compute and check the first output of F' + + // get z_{i+1} from the F circuit + let i_usize = self.i_usize.unwrap_or(0); + let z_i1 = self + .F + .generate_step_constraints(cs.clone(), i_usize, z_i, external_inputs)?; + let (u_i1_x, _) = U_i1.clone().hash( &sponge, - pp_hash.clone(), - i + FpVar::<CF1<C1>>::one(), - z_0.clone(), - z_i1.clone(), + &pp_hash, + &(i + FpVar::<CF1<C1>>::one()), + &z_0, + &z_i1, )?; let (u_i1_x_base, _) = LCCCSVar::new_constant(cs.clone(), U_dummy)?.hash( &sponge, - pp_hash.clone(), - FpVar::<CF1<C1>>::one(), - z_0.clone(), - z_i1.clone(), + &pp_hash, + &FpVar::<CF1<C1>>::one(), + &z_0, + &z_i1, )?; let x = FpVar::new_input(cs.clone(), || Ok(self.x.unwrap_or(u_i1_x_base.value()?)))?; x.enforce_equal(&is_basecase.select(&u_i1_x_base, &u_i1_x)?)?;@@ -900,6 +915,7 @@ hypernova::{
utils::{compute_c, compute_sigmas_thetas}, HyperNovaCycleFoldCircuit, }, + traits::CommittedInstanceOps, }, frontend::utils::CubicFCircuit, transcript::poseidon::poseidon_canonical_config,@@ -1113,6 +1129,37 @@ assert!(cs.is_satisfied().unwrap());
assert_eq!(folded_lcccsVar.u.value().unwrap(), folded_lcccs.u); } + /// test that checks the native LCCCS.to_sponge_{bytes,field_elements} vs + /// the R1CS constraints version + #[test] + pub fn test_lcccs_to_sponge_preimage() { + let mut rng = test_rng(); + + let ccs = get_test_ccs(); + let z1 = get_test_z::<Fr>(3); + + let (pedersen_params, _) = + Pedersen::<Projective>::setup(&mut rng, ccs.n - ccs.l - 1).unwrap(); + + let (lcccs, _) = ccs + .to_lcccs::<_, _, Pedersen<Projective, true>, true>(&mut rng, &pedersen_params, &z1) + .unwrap(); + let bytes = lcccs.to_sponge_bytes_as_vec(); + let field_elements = lcccs.to_sponge_field_elements_as_vec(); + + let cs = ConstraintSystem::<Fr>::new_ref(); + + let lcccsVar = LCCCSVar::<Projective>::new_witness(cs.clone(), || Ok(lcccs)).unwrap(); + let bytes_var = lcccsVar.to_sponge_bytes().unwrap(); + let field_elements_var = lcccsVar.to_sponge_field_elements().unwrap(); + + assert!(cs.is_satisfied().unwrap()); + + // check that the natively computed and in-circuit computed hashes match + assert_eq!(bytes_var.value().unwrap(), bytes); + assert_eq!(field_elements_var.value().unwrap(), field_elements); + } + /// test that checks the native LCCCS.hash vs the R1CS constraints version #[test] pub fn test_lcccs_hash() {@@ -1133,9 +1180,7 @@ let z_i = vec![Fr::from(3_u32)];
let (lcccs, _) = ccs .to_lcccs::<_, _, Pedersen<Projective, true>, true>(&mut rng, &pedersen_params, &z1) .unwrap(); - let h = lcccs - .clone() - .hash(&sponge, pp_hash, i, z_0.clone(), z_i.clone()); + let h = lcccs.clone().hash(&sponge, pp_hash, i, &z_0, &z_i); let cs = ConstraintSystem::<Fr>::new_ref();@@ -1147,13 +1192,7 @@ let z_iVar = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z_i.clone())).unwrap();
let lcccsVar = LCCCSVar::<Projective>::new_witness(cs.clone(), || Ok(lcccs)).unwrap(); let (hVar, _) = lcccsVar .clone() - .hash( - &spongeVar, - pp_hashVar, - iVar.clone(), - z_0Var.clone(), - z_iVar.clone(), - ) + .hash(&spongeVar, &pp_hashVar, &iVar, &z_0Var, &z_iVar) .unwrap(); assert!(cs.is_satisfied().unwrap());@@ -1225,7 +1264,7 @@ let mut u_i = u_dummy.clone();
let mut cf_W_i = cf_W_dummy.clone(); let mut cf_U_i = cf_U_dummy.clone(); u_i.x = vec![ - U_i.hash(&sponge, pp_hash, Fr::zero(), z_0.clone(), z_i.clone()), + U_i.hash(&sponge, pp_hash, Fr::zero(), &z_0, &z_i), cf_U_i.hash_cyclefold(&sponge, pp_hash), ];@@ -1252,7 +1291,7 @@ if i == 0 {
W_i1 = Witness::<Fr>::dummy(&ccs); U_i1 = LCCCS::dummy(ccs.l, ccs.t, ccs.s); - let u_i1_x = U_i1.hash(&sponge, pp_hash, Fr::one(), z_0.clone(), z_i1.clone()); + let u_i1_x = U_i1.hash(&sponge, pp_hash, Fr::one(), &z_0, &z_i1); // hash the initial (dummy) CycleFold instance, which is used as the 2nd public // input in the AugmentedFCircuit@@ -1309,8 +1348,7 @@
// sanity check: check the folded instance relation U_i1.check_relation(&ccs, &W_i1).unwrap(); - let u_i1_x = - U_i1.hash(&sponge, pp_hash, iFr + Fr::one(), z_0.clone(), z_i1.clone()); + let u_i1_x = U_i1.hash(&sponge, pp_hash, iFr + Fr::one(), &z_0, &z_i1); let rho_bits = rho.into_bigint().to_bits_le()[..NOVA_N_BITS_RO].to_vec(); let rho_Fq = Fq::from_bigint(BigInteger::from_bits_le(&rho_bits)).unwrap();@@ -1434,8 +1472,7 @@ assert_eq!(w_i.w, r1cs_w_i1);
assert_eq!(u_i.x, r1cs_x_i1); assert_eq!(u_i.x[0], augmented_f_circuit.x.unwrap()); assert_eq!(u_i.x[1], augmented_f_circuit.cf_x.unwrap()); - let expected_u_i1_x = - U_i1.hash(&sponge, pp_hash, iFr + Fr::one(), z_0.clone(), z_i1.clone()); + let expected_u_i1_x = U_i1.hash(&sponge, pp_hash, iFr + Fr::one(), &z_0, &z_i1); let expected_cf_U_i1_x = cf_U_i.hash_cyclefold(&sponge, pp_hash); // u_i is already u_i1 at this point, check that has the expected value at x[0] assert_eq!(u_i.x[0], expected_u_i1_x);
@@ -26,8 +26,6 @@ circuits::{CCCSVar, LCCCSVar, NIMFSGadget, ProofVar as NIMFSProofVar},
nimfs::{NIMFSProof, NIMFS}, HyperNova, Witness, CCCS, LCCCS, }; -use crate::arith::ccs::CCS; -use crate::arith::r1cs::R1CS; use crate::commitment::{pedersen::Params as PedersenParams, CommitmentScheme}; use crate::folding::circuits::{ cyclefold::{CycleFoldCommittedInstance, CycleFoldWitness},@@ -40,6 +38,8 @@ gadgets::{eval_mle, MatrixGadget, SparseMatrixVar},
vec::poly_from_vec, }; use crate::Error; +use crate::{arith::ccs::CCS, folding::traits::CommittedInstanceVarOps}; +use crate::{arith::r1cs::R1CS, folding::traits::WitnessVarOps}; /// In-circuit representation of the Witness associated to the CommittedInstance. #[derive(Debug, Clone)]@@ -63,6 +63,12 @@ let r_w = FpVar::<F>::new_variable(cs.clone(), || Ok(val.borrow().r_w), mode)?;
Ok(Self { w, r_w }) }) + } +} + +impl<F: PrimeField> WitnessVarOps<F> for WitnessVar<F> { + fn get_openings(&self) -> Vec<(&[FpVar<F>], FpVar<F>)> { + vec![(&self.w, self.r_w.clone())] } }@@ -340,13 +346,7 @@ U_i1.v.clone(),
)?; // 3.a u_i.x[0] == H(i, z_0, z_i, U_i) - let (u_i_x, _) = U_i.clone().hash( - &sponge, - pp_hash.clone(), - i.clone(), - z_0.clone(), - z_i.clone(), - )?; + let (u_i_x, _) = U_i.clone().hash(&sponge, &pp_hash, &i, &z_0, &z_i)?; (u_i.x[0]).enforce_equal(&u_i_x)?; #[cfg(feature = "light-test")]
@@ -1,5 +1,5 @@
use ark_crypto_primitives::sponge::Absorb; -use ark_ec::{CurveGroup, Group}; +use ark_ec::CurveGroup; use ark_ff::PrimeField; use ark_poly::DenseMultilinearExtension; use ark_poly::MultilinearExtension;@@ -8,10 +8,12 @@ use ark_serialize::CanonicalSerialize;
use ark_std::rand::Rng; use ark_std::Zero; +use super::circuits::LCCCSVar; use super::Witness; use crate::arith::ccs::CCS; use crate::commitment::CommitmentScheme; -use crate::transcript::{AbsorbNonNative, Transcript}; +use crate::folding::traits::CommittedInstanceOps; +use crate::transcript::AbsorbNonNative; use crate::utils::mle::dense_vec_to_dense_mle; use crate::utils::vec::mat_vec_mul; use crate::Error;@@ -121,9 +123,8 @@ impl<C: CurveGroup> Absorb for LCCCS<C>
where C::ScalarField: Absorb, { - fn to_sponge_bytes(&self, _dest: &mut Vec<u8>) { - // This is never called - unimplemented!() + fn to_sponge_bytes(&self, dest: &mut Vec<u8>) { + C::ScalarField::batch_to_sponge_bytes(&self.to_sponge_field_elements_as_vec(), dest); } fn to_sponge_field_elements<F: PrimeField>(&self, dest: &mut Vec<F>) {@@ -140,29 +141,15 @@ self.v.to_sponge_field_elements(dest);
} } -impl<C: CurveGroup> LCCCS<C> -where - <C as Group>::ScalarField: Absorb, - <C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField, -{ - /// [`LCCCS`].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_i` is the LCCCS. - pub fn hash<T: Transcript<C::ScalarField>>( - &self, - sponge: &T, - pp_hash: C::ScalarField, - i: C::ScalarField, - z_0: Vec<C::ScalarField>, - z_i: Vec<C::ScalarField>, - ) -> C::ScalarField { - let mut sponge = sponge.clone(); - sponge.absorb(&pp_hash); - sponge.absorb(&i); - sponge.absorb(&z_0); - sponge.absorb(&z_i); - sponge.absorb(&self); - sponge.squeeze_field_elements(1)[0] +impl<C: CurveGroup> CommittedInstanceOps<C> for LCCCS<C> { + type Var = LCCCSVar<C>; + + fn get_commitments(&self) -> Vec<C> { + vec![self.C] + } + + fn is_incoming(&self) -> bool { + false } }
@@ -20,6 +20,7 @@ pub mod utils;
use cccs::CCCS; use circuits::AugmentedFCircuit; +use decider_eth_circuit::WitnessVar; use lcccs::LCCCS; use nimfs::NIMFS;@@ -32,6 +33,7 @@ },
CF2, }; use crate::folding::nova::{get_r1cs_from_cs, PreprocessorParam}; +use crate::folding::traits::{CommittedInstanceOps, WitnessOps}; use crate::frontend::FCircuit; use crate::utils::{get_cm_coordinates, pp_hash}; use crate::Error;@@ -78,6 +80,14 @@ Self { w, r_w: F::zero() }
} pub fn dummy(ccs: &CCS<F>) -> Self { Witness::<F>::new(vec![F::zero(); ccs.n - ccs.l - 1]) + } +} + +impl<F: PrimeField> WitnessOps<F> for Witness<F> { + type Var = WitnessVar<F>; + + fn get_openings(&self) -> Vec<(&[F], F)> { + vec![(&self.w, self.r_w)] } }@@ -307,8 +317,8 @@ U_i.hash(
&sponge, self.pp_hash, C1::ScalarField::zero(), // i - self.z_0.clone(), - state.clone(), + &self.z_0, + &state, ), cf_U_i.hash_cyclefold(&sponge, self.pp_hash), ];@@ -324,8 +334,8 @@ let u_i1_x = U_i1.hash(
&sponge, self.pp_hash, C1::ScalarField::one(), // i+1, where i=0 - self.z_0.clone(), - z_i1.clone(), + &self.z_0, + &z_i1, ); let cf_u_i1_x = cf_U_i.hash_cyclefold(&sponge, self.pp_hash);@@ -494,13 +504,7 @@ let mut u_dummy = CCCS::<C1>::dummy(ccs.l);
let (cf_W_dummy, cf_U_dummy): (CycleFoldWitness<C2>, CycleFoldCommittedInstance<C2>) = cf_r1cs.dummy_running_instance(); u_dummy.x = vec![ - U_dummy.hash( - &sponge, - pp_hash, - C1::ScalarField::zero(), - z_0.clone(), - z_0.clone(), - ), + U_dummy.hash(&sponge, pp_hash, C1::ScalarField::zero(), &z_0, &z_0), cf_U_dummy.hash_cyclefold(&sponge, pp_hash), ];@@ -643,8 +647,8 @@ let u_i1_x = U_i1.hash(
&sponge, self.pp_hash, C1::ScalarField::one(), - self.z_0.clone(), - z_i1.clone(), + &self.z_0, + &z_i1, ); // hash the initial (dummy) CycleFold instance, which is used as the 2nd public@@ -699,8 +703,8 @@ let u_i1_x = U_i1.hash(
&sponge, self.pp_hash, self.i + C1::ScalarField::one(), - self.z_0.clone(), - z_i1.clone(), + &self.z_0, + &z_i1, ); let rho_bits = rho.into_bigint().to_bits_le()[..NOVA_N_BITS_RO].to_vec();@@ -884,7 +888,7 @@ let pp_hash = vp.pp_hash()?;
// check that u_i's output points to the running instance // u_i.X[0] == H(i, z_0, z_i, U_i) - let expected_u_i_x = U_i.hash(&sponge, pp_hash, num_steps, z_0, z_i.clone()); + let expected_u_i_x = U_i.hash(&sponge, pp_hash, num_steps, &z_0, &z_i); if expected_u_i_x != u_i.x[0] { return Err(Error::IVCVerificationFail); }
@@ -2,3 +2,4 @@ pub mod circuits;
pub mod hypernova; pub mod nova; pub mod protogalaxy; +pub mod traits;
@@ -21,7 +21,6 @@ use ark_std::{fmt::Debug, One, Zero};
use core::{borrow::Borrow, marker::PhantomData}; use super::{CommittedInstance, NovaCycleFoldConfig}; -use crate::constants::NOVA_N_BITS_RO; use crate::folding::circuits::{ cyclefold::{ CycleFoldChallengeGadget, CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar,@@ -32,15 +31,13 @@ CF1, CF2,
}; use crate::frontend::FCircuit; use crate::transcript::{AbsorbNonNativeGadget, Transcript, TranscriptVar}; +use crate::{constants::NOVA_N_BITS_RO, folding::traits::CommittedInstanceVarOps}; /// 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). The peculiarity is that cmE and cmW are /// represented non-natively over the constraint field. #[derive(Debug, Clone)] -pub struct CommittedInstanceVar<C: CurveGroup> -where - <C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField, -{ +pub struct CommittedInstanceVar<C: CurveGroup> { pub u: FpVar<C::ScalarField>, pub x: Vec<FpVar<C::ScalarField>>, pub cmE: NonNativeAffineVar<C>,@@ -50,7 +47,6 @@
impl<C> AllocVar<CommittedInstance<C>, CF1<C>> for CommittedInstanceVar<C> where C: CurveGroup, - <C as ark_ec::CurveGroup>::BaseField: PrimeField, { fn new_variable<T: Borrow<CommittedInstance<C>>>( cs: impl Into<Namespace<CF1<C>>>,@@ -80,7 +76,7 @@ C: CurveGroup,
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField, { fn to_sponge_bytes(&self) -> Result<Vec<UInt8<C::ScalarField>>, SynthesisError> { - unimplemented!() + FpVar::batch_to_sponge_bytes(&self.to_sponge_field_elements()?) } fn to_sponge_field_elements(&self) -> Result<Vec<FpVar<C::ScalarField>>, SynthesisError> {@@ -94,35 +90,27 @@ .concat())
} } -impl<C> CommittedInstanceVar<C> -where - C: CurveGroup, - <C as Group>::ScalarField: Absorb, - <C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField, -{ - /// 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`. - /// 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<S: CryptographicSponge, T: TranscriptVar<CF1<C>, S>>( - self, - sponge: &T, - pp_hash: FpVar<CF1<C>>, - i: FpVar<CF1<C>>, - z_0: Vec<FpVar<CF1<C>>>, - z_i: Vec<FpVar<CF1<C>>>, - ) -> Result<(FpVar<CF1<C>>, Vec<FpVar<CF1<C>>>), SynthesisError> { - let mut sponge = sponge.clone(); - let U_vec = self.to_sponge_field_elements()?; - sponge.absorb(&pp_hash)?; - sponge.absorb(&i)?; - sponge.absorb(&z_0)?; - sponge.absorb(&z_i)?; - sponge.absorb(&U_vec)?; - Ok((sponge.squeeze_field_elements(1)?.pop().unwrap(), U_vec)) +impl<C: CurveGroup> CommittedInstanceVarOps<C> for CommittedInstanceVar<C> { + type PointVar = NonNativeAffineVar<C>; + + fn get_commitments(&self) -> Vec<Self::PointVar> { + vec![self.cmW.clone(), self.cmE.clone()] + } + + fn get_public_inputs(&self) -> &[FpVar<CF1<C>>] { + &self.x + } + + fn enforce_incoming(&self) -> Result<(), SynthesisError> { + let zero = NonNativeUintVar::new_constant(ConstraintSystemRef::None, CF2::<C>::zero())?; + self.cmE.x.enforce_equal_unaligned(&zero)?; + self.cmE.y.enforce_equal_unaligned(&zero)?; + self.u.enforce_equal(&FpVar::one()) + } + + fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError> { + self.u.enforce_equal(&other.u)?; + self.x.enforce_equal(&other.x) } }@@ -358,25 +346,13 @@ // `sponge` is for digest computation.
let sponge = PoseidonSpongeVar::<C1::ScalarField>::new(cs.clone(), &self.poseidon_config); // `transcript` is for challenge generation. let mut transcript = sponge.clone(); - - // get z_{i+1} from the F circuit - let i_usize = self.i_usize.unwrap_or(0); - let z_i1 = - self.F - .generate_step_constraints(cs.clone(), i_usize, z_i.clone(), external_inputs)?; let is_basecase = i.is_zero()?; // Primary Part // P.1. Compute u_i.x // u_i.x[0] = H(i, z_0, z_i, U_i) - let (u_i_x, U_i_vec) = U_i.clone().hash( - &sponge, - pp_hash.clone(), - i.clone(), - z_0.clone(), - z_i.clone(), - )?; + let (u_i_x, U_i_vec) = U_i.clone().hash(&sponge, &pp_hash, &i, &z_0, &z_i)?; // u_i.x[1] = H(cf_U_i) let (cf_u_i_x, cf_U_i_vec) = cf_U_i.clone().hash(&sponge, pp_hash.clone())?;@@ -421,21 +397,28 @@ U_i1.cmE = U_i1_cmE;
U_i1.cmW = U_i1_cmW; // P.4.a compute and check the first output of F' + + // get z_{i+1} from the F circuit + let i_usize = self.i_usize.unwrap_or(0); + let z_i1 = self + .F + .generate_step_constraints(cs.clone(), i_usize, z_i, external_inputs)?; + // Base case: u_{i+1}.x[0] == H((i+1, z_0, z_{i+1}, U_{\bot}) // Non-base case: u_{i+1}.x[0] == H((i+1, z_0, z_{i+1}, U_{i+1}) let (u_i1_x, _) = U_i1.clone().hash( &sponge, - pp_hash.clone(), - i + FpVar::<CF1<C1>>::one(), - z_0.clone(), - z_i1.clone(), + &pp_hash, + &(i + FpVar::<CF1<C1>>::one()), + &z_0, + &z_i1, )?; let (u_i1_x_base, _) = CommittedInstanceVar::new_constant(cs.clone(), u_dummy)?.hash( &sponge, - pp_hash.clone(), - FpVar::<CF1<C1>>::one(), - z_0.clone(), - z_i1.clone(), + &pp_hash, + &FpVar::<CF1<C1>>::one(), + &z_0, + &z_i1, )?; let x = FpVar::new_input(cs.clone(), || Ok(self.x.unwrap_or(u_i1_x_base.value()?)))?; x.enforce_equal(&is_basecase.select(&u_i1_x_base, &u_i1_x)?)?;@@ -538,6 +521,7 @@
use crate::commitment::pedersen::Pedersen; use crate::folding::nova::nifs::tests::prepare_simple_fold_inputs; use crate::folding::nova::nifs::NIFS; + use crate::folding::traits::CommittedInstanceOps; use crate::transcript::poseidon::poseidon_canonical_config; #[test]@@ -591,6 +575,36 @@ .unwrap();
assert!(cs.is_satisfied().unwrap()); } + /// test that checks the native CommittedInstance.to_sponge_{bytes,field_elements} + /// vs the R1CS constraints version + #[test] + pub fn test_committed_instance_to_sponge_preimage() { + let mut rng = ark_std::test_rng(); + + let ci = CommittedInstance::<Projective> { + cmE: Projective::rand(&mut rng), + u: Fr::rand(&mut rng), + cmW: Projective::rand(&mut rng), + x: vec![Fr::rand(&mut rng); 1], + }; + + let bytes = ci.to_sponge_bytes_as_vec(); + let field_elements = ci.to_sponge_field_elements_as_vec(); + + let cs = ConstraintSystem::<Fr>::new_ref(); + + let ciVar = + CommittedInstanceVar::<Projective>::new_witness(cs.clone(), || Ok(ci.clone())).unwrap(); + let bytes_var = ciVar.to_sponge_bytes().unwrap(); + let field_elements_var = ciVar.to_sponge_field_elements().unwrap(); + + assert!(cs.is_satisfied().unwrap()); + + // check that the natively computed and in-circuit computed hashes match + assert_eq!(bytes_var.value().unwrap(), bytes); + assert_eq!(field_elements_var.value().unwrap(), field_elements); + } + #[test] fn test_committed_instance_hash() { let mut rng = ark_std::test_rng();@@ -609,7 +623,7 @@ x: vec![Fr::rand(&mut rng); 1],
}; // compute the CommittedInstance hash natively - let h = ci.hash(&sponge, pp_hash, i, z_0.clone(), z_i.clone()); + let h = ci.hash(&sponge, pp_hash, i, &z_0, &z_i); let cs = ConstraintSystem::<Fr>::new_ref();@@ -624,7 +638,7 @@ let sponge = PoseidonSpongeVar::<Fr>::new(cs.clone(), &poseidon_config);
// compute the CommittedInstance hash in-circuit let (hVar, _) = ciVar - .hash(&sponge, pp_hashVar, iVar, z_0Var, z_iVar) + .hash(&sponge, &pp_hashVar, &iVar, &z_0Var, &z_iVar) .unwrap(); assert!(cs.is_satisfied().unwrap());
@@ -27,8 +27,6 @@ circuits::{ChallengeGadget, CommittedInstanceVar},
nifs::NIFS, CommittedInstance, Nova, Witness, }; -use crate::arith::r1cs::R1CS; -use crate::commitment::{pedersen::Params as PedersenParams, CommitmentScheme}; use crate::folding::circuits::{ cyclefold::{CycleFoldCommittedInstance, CycleFoldWitness}, nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar},@@ -41,6 +39,11 @@ gadgets::{MatrixGadget, SparseMatrixVar, VectorGadget},
vec::poly_from_vec, }; use crate::Error; +use crate::{arith::r1cs::R1CS, folding::traits::WitnessVarOps}; +use crate::{ + commitment::{pedersen::Params as PedersenParams, CommitmentScheme}, + folding::traits::CommittedInstanceVarOps, +}; #[derive(Debug, Clone)] pub struct RelaxedR1CSGadget {}@@ -135,7 +138,6 @@
impl<C> AllocVar<Witness<C>, CF1<C>> for WitnessVar<C> where C: CurveGroup, - <C as ark_ec::CurveGroup>::BaseField: PrimeField, { fn new_variable<T: Borrow<Witness<C>>>( cs: impl Into<Namespace<CF1<C>>>,@@ -157,6 +159,12 @@ FpVar::<C::ScalarField>::new_variable(cs.clone(), || Ok(val.borrow().rW), mode)?;
Ok(Self { E, rE, W, rW }) }) + } +} + +impl<C: CurveGroup> WitnessVarOps<C::ScalarField> for WitnessVar<C> { + fn get_openings(&self) -> Vec<(&[FpVar<C::ScalarField>], FpVar<C::ScalarField>)> { + vec![(&self.E, self.rE.clone()), (&self.W, self.rW.clone())] } }@@ -398,13 +406,7 @@ u_i.cmE.y.enforce_equal_unaligned(&zero)?;
(u_i.u.is_one()?).enforce_equal(&Boolean::TRUE)?; // 3.a u_i.x[0] == H(i, z_0, z_i, U_i) - let (u_i_x, U_i_vec) = U_i.clone().hash( - &sponge, - pp_hash.clone(), - i.clone(), - z_0.clone(), - z_i.clone(), - )?; + let (u_i_x, U_i_vec) = U_i.clone().hash(&sponge, &pp_hash, &i, &z_0, &z_i)?; (u_i.x[0]).enforce_equal(&u_i_x)?; #[cfg(feature = "light-test")]
@@ -13,6 +13,7 @@ use ark_std::fmt::Debug;
use ark_std::rand::RngCore; use ark_std::{One, UniformRand, Zero}; use core::marker::PhantomData; +use decider_eth_circuit::WitnessVar; use crate::folding::circuits::cyclefold::{ fold_cyclefold_circuit, CycleFoldCircuit, CycleFoldCommittedInstance, CycleFoldConfig,@@ -38,8 +39,10 @@ pub mod nifs;
pub mod serialize; pub mod traits; pub mod zk; -use circuits::{AugmentedFCircuit, ChallengeGadget}; +use circuits::{AugmentedFCircuit, ChallengeGadget, CommittedInstanceVar}; use nifs::NIFS; + +use super::traits::{CommittedInstanceOps, WitnessOps}; /// Configuration for Nova's CycleFold circuit pub struct NovaCycleFoldConfig<C: CurveGroup> {@@ -83,9 +86,8 @@ impl<C: CurveGroup> Absorb for CommittedInstance<C>
where C::ScalarField: Absorb, { - fn to_sponge_bytes(&self, _dest: &mut Vec<u8>) { - // This is never called - unimplemented!() + fn to_sponge_bytes(&self, dest: &mut Vec<u8>) { + C::ScalarField::batch_to_sponge_bytes(&self.to_sponge_field_elements_as_vec(), dest); } fn to_sponge_field_elements<F: PrimeField>(&self, dest: &mut Vec<F>) {@@ -103,30 +105,15 @@ .to_sponge_field_elements(dest);
} } -impl<C: CurveGroup> CommittedInstance<C> -where - <C as Group>::ScalarField: Absorb, - <C as ark_ec::CurveGroup>::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_i` is the - /// `CommittedInstance`. - pub fn hash<T: Transcript<C::ScalarField>>( - &self, - sponge: &T, - pp_hash: C::ScalarField, // public params hash - i: C::ScalarField, - z_0: Vec<C::ScalarField>, - z_i: Vec<C::ScalarField>, - ) -> C::ScalarField { - let mut sponge = sponge.clone(); - sponge.absorb(&pp_hash); - sponge.absorb(&i); - sponge.absorb(&z_0); - sponge.absorb(&z_i); - sponge.absorb(&self); - sponge.squeeze_field_elements(1)[0] +impl<C: CurveGroup> CommittedInstanceOps<C> for CommittedInstance<C> { + type Var = CommittedInstanceVar<C>; + + fn get_commitments(&self) -> Vec<C> { + vec![self.cmW, self.cmE] + } + + fn is_incoming(&self) -> bool { + self.cmE == C::zero() && self.u == One::one() } }@@ -185,6 +172,14 @@ u: C::ScalarField::one(),
cmW, x, }) + } +} + +impl<C: CurveGroup> WitnessOps<C::ScalarField> for Witness<C> { + type Var = WitnessVar<C>; + + fn get_openings(&self) -> Vec<(&[C::ScalarField], C::ScalarField)> { + vec![(&self.W, self.rW), (&self.E, self.rE)] } }@@ -696,8 +691,8 @@ let u_i1_x = U_i1.hash(
&sponge, self.pp_hash, self.i + C1::ScalarField::one(), - self.z_0.clone(), - z_i1.clone(), + &self.z_0, + &z_i1, ); // u_{i+1}.x[1] = H(cf_U_{i+1}) let cf_u_i1_x: C1::ScalarField;@@ -907,7 +902,7 @@ let pp_hash = vp.pp_hash()?;
// check that u_i's output points to the running instance // u_i.X[0] == H(i, z_0, z_i, U_i) - let expected_u_i_x = U_i.hash(&sponge, pp_hash, num_steps, z_0, z_i.clone()); + let expected_u_i_x = U_i.hash(&sponge, pp_hash, num_steps, &z_0, &z_i); if expected_u_i_x != u_i.x[0] { return Err(Error::IVCVerificationFail); }
@@ -36,6 +36,7 @@ use ark_std::{One, Zero};
use crate::{ arith::r1cs::{RelaxedR1CS, R1CS}, + folding::traits::CommittedInstanceOps, RngCore, }; use ark_crypto_primitives::sponge::{@@ -226,7 +227,7 @@ }
// b. Check computed hashes are correct let mut sponge = PoseidonSponge::<C1::ScalarField>::new(poseidon_config); - let expected_u_i_x = proof.U_i.hash(&sponge, pp_hash, i, z_0, z_i); + let expected_u_i_x = proof.U_i.hash(&sponge, pp_hash, i, &z_0, &z_i); if expected_u_i_x != proof.u_i.x[0] { return Err(Error::zkIVCVerificationFail); }
@@ -24,13 +24,16 @@ utils::{all_powers_var, betas_star_var, exponential_powers_var},
CommittedInstance, CommittedInstanceVar, ProtoGalaxyCycleFoldConfig, }; use crate::{ - folding::circuits::{ - cyclefold::{ - CycleFoldChallengeGadget, CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar, - CycleFoldConfig, NIFSFullGadget, + folding::{ + circuits::{ + cyclefold::{ + CycleFoldChallengeGadget, CycleFoldCommittedInstance, + CycleFoldCommittedInstanceVar, CycleFoldConfig, NIFSFullGadget, + }, + nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar}, + CF1, CF2, }, - nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar}, - CF1, CF2, + traits::CommittedInstanceVarOps, }, frontend::FCircuit, transcript::{AbsorbNonNativeGadget, TranscriptVar},@@ -346,24 +349,12 @@ let sponge = PoseidonSpongeVar::<C1::ScalarField>::new(cs.clone(), &self.poseidon_config);
// `transcript` is for challenge generation. let mut transcript = sponge.clone(); - // get z_{i+1} from the F circuit - let i_usize = self.i_usize; - let z_i1 = - self.F - .generate_step_constraints(cs.clone(), i_usize, z_i.clone(), external_inputs)?; - let is_basecase = i.is_zero()?; // Primary Part // P.1. Compute u_i.x // u_i.x[0] = H(i, z_0, z_i, U_i) - let (u_i_x, _) = U_i.clone().hash( - &sponge, - pp_hash.clone(), - i.clone(), - z_0.clone(), - z_i.clone(), - )?; + let (u_i_x, _) = U_i.clone().hash(&sponge, &pp_hash, &i, &z_0, &z_i)?; // u_i.x[1] = H(cf_U_i) let (cf_u_i_x, _) = cf_U_i.clone().hash(&sponge, pp_hash.clone())?;@@ -380,21 +371,27 @@ K_coeffs,
)?; // P.4.a compute and check the first output of F' + + // get z_{i+1} from the F circuit + let z_i1 = + self.F + .generate_step_constraints(cs.clone(), self.i_usize, z_i, external_inputs)?; + // Base case: u_{i+1}.x[0] == H((i+1, z_0, z_{i+1}, U_{\bot}) // Non-base case: u_{i+1}.x[0] == H((i+1, z_0, z_{i+1}, U_{i+1}) let (u_i1_x, _) = U_i1.clone().hash( &sponge, - pp_hash.clone(), - i + FpVar::<CF1<C1>>::one(), - z_0.clone(), - z_i1.clone(), + &pp_hash, + &(i + FpVar::<CF1<C1>>::one()), + &z_0, + &z_i1, )?; let (u_i1_x_base, _) = CommittedInstanceVar::new_constant(cs.clone(), u_dummy)?.hash( &sponge, - pp_hash.clone(), - FpVar::<CF1<C1>>::one(), - z_0.clone(), - z_i1.clone(), + &pp_hash, + &FpVar::<CF1<C1>>::one(), + &z_0, + &z_i1, )?; let x = FpVar::new_input(cs.clone(), || Ok(self.x.unwrap_or(u_i1_x_base.value()?)))?; x.enforce_equal(&is_basecase.select(&u_i1_x_base, &u_i1_x)?)?;
@@ -1,14 +1,14 @@
/// Implements the scheme described in [ProtoGalaxy](https://eprint.iacr.org/2023/1106.pdf) use ark_crypto_primitives::sponge::{ - constraints::{AbsorbGadget, CryptographicSpongeVar}, - poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge}, + poseidon::{PoseidonConfig, PoseidonSponge}, Absorb, CryptographicSponge, }; use ark_ec::{CurveGroup, Group}; use ark_ff::{BigInteger, PrimeField}; use ark_r1cs_std::{ alloc::{AllocVar, AllocationMode}, - fields::fp::FpVar, + eq::EqGadget, + fields::{fp::FpVar, FieldVar}, groups::{CurveVar, GroupOpsBounds}, R1CSVar, ToConstraintFieldGadget, };@@ -43,6 +43,8 @@ pub(crate) mod utils;
use circuits::AugmentedFCircuit; use folding::Folding; + +use super::traits::{CommittedInstanceOps, CommittedInstanceVarOps, WitnessOps, WitnessVarOps}; /// Configuration for ProtoGalaxy's CycleFold circuit pub struct ProtoGalaxyCycleFoldConfig<C: CurveGroup> {@@ -83,30 +85,15 @@ Self::dummy_running(io_len, 0)
} } -impl<C: CurveGroup> CommittedInstance<C> -where - C::ScalarField: Absorb, - C::BaseField: PrimeField, -{ - /// hash implements the committed instance hash compatible with the gadget implemented in - /// CommittedInstanceVar.hash. - /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and `U_i` is the - /// `CommittedInstance`. - pub fn hash( - &self, - sponge: &PoseidonSponge<C::ScalarField>, - pp_hash: C::ScalarField, - i: C::ScalarField, - z_0: Vec<C::ScalarField>, - z_i: Vec<C::ScalarField>, - ) -> C::ScalarField { - let mut sponge = sponge.clone(); - sponge.absorb(&pp_hash); - sponge.absorb(&i); - sponge.absorb(&z_0); - sponge.absorb(&z_i); - sponge.absorb(&self); - sponge.squeeze_field_elements(1)[0] +impl<C: CurveGroup> CommittedInstanceOps<C> for CommittedInstance<C> { + type Var = CommittedInstanceVar<C>; + + fn get_commitments(&self) -> Vec<C> { + vec![self.phi] + } + + fn is_incoming(&self) -> bool { + self.e == Zero::zero() && self.betas.is_empty() } }@@ -164,34 +151,29 @@ })
} } -impl<C: CurveGroup> CommittedInstanceVar<C> -where - C::ScalarField: Absorb, - C::BaseField: PrimeField, -{ - /// 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`. - /// 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, - sponge: &PoseidonSpongeVar<CF1<C>>, - pp_hash: FpVar<CF1<C>>, - i: FpVar<CF1<C>>, - z_0: Vec<FpVar<CF1<C>>>, - z_i: Vec<FpVar<CF1<C>>>, - ) -> Result<(FpVar<CF1<C>>, Vec<FpVar<CF1<C>>>), SynthesisError> { - let mut sponge = sponge.clone(); - let U_vec = self.to_sponge_field_elements()?; - sponge.absorb(&pp_hash)?; - sponge.absorb(&i)?; - sponge.absorb(&z_0)?; - sponge.absorb(&z_i)?; - sponge.absorb(&U_vec)?; - Ok((sponge.squeeze_field_elements(1)?.pop().unwrap(), U_vec)) +impl<C: CurveGroup> CommittedInstanceVarOps<C> for CommittedInstanceVar<C> { + type PointVar = NonNativeAffineVar<C>; + + fn get_commitments(&self) -> Vec<Self::PointVar> { + vec![self.phi.clone()] + } + + fn get_public_inputs(&self) -> &[FpVar<CF1<C>>] { + &self.x + } + + fn enforce_incoming(&self) -> Result<(), SynthesisError> { + if self.betas.is_empty() { + self.e.enforce_equal(&FpVar::zero()) + } else { + Err(SynthesisError::Unsatisfiable) + } + } + + fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError> { + self.betas.enforce_equal(&other.betas)?; + self.e.enforce_equal(&other.e)?; + self.x.enforce_equal(&other.x) } }@@ -221,6 +203,44 @@ x,
e: F::zero(), betas: vec![], }) + } +} + +impl<F: PrimeField> WitnessOps<F> for Witness<F> { + type Var = WitnessVar<F>; + + fn get_openings(&self) -> Vec<(&[F], F)> { + vec![(&self.w, self.r_w)] + } +} + +/// In-circuit representation of the Witness associated to the CommittedInstance. +#[derive(Debug, Clone)] +pub struct WitnessVar<F: PrimeField> { + pub W: Vec<FpVar<F>>, + pub rW: FpVar<F>, +} + +impl<F: PrimeField> AllocVar<Witness<F>, F> for WitnessVar<F> { + fn new_variable<T: Borrow<Witness<F>>>( + cs: impl Into<Namespace<F>>, + f: impl FnOnce() -> Result<T, SynthesisError>, + mode: AllocationMode, + ) -> Result<Self, SynthesisError> { + f().and_then(|val| { + let cs = cs.into(); + + let W = Vec::new_variable(cs.clone(), || Ok(val.borrow().w.to_vec()), mode)?; + let rW = FpVar::new_variable(cs.clone(), || Ok(val.borrow().r_w), mode)?; + + Ok(Self { W, rW }) + }) + } +} + +impl<F: PrimeField> WitnessVarOps<F> for WitnessVar<F> { + fn get_openings(&self) -> Vec<(&[FpVar<F>], FpVar<F>)> { + vec![(&self.W, self.rW.clone())] } }@@ -636,8 +656,8 @@ u_i1_x = self.U_i.hash(
&sponge, self.pp_hash, self.i + C1::ScalarField::one(), - self.z_0.clone(), - z_i1.clone(), + &self.z_0, + &z_i1, ); // `cf_U_{i+1}` (i.e., `cf_U_1`) is fixed to `cf_U_dummy`, so we // just use `self.cf_U_i = cf_U_0 = cf_U_dummy`.@@ -744,8 +764,8 @@ u_i1_x = U_i1.hash(
&sponge, self.pp_hash, self.i + C1::ScalarField::one(), - self.z_0.clone(), - z_i1.clone(), + &self.z_0, + &z_i1, ); cf_u_i1_x = cf_U_i1.hash_cyclefold(&sponge, self.pp_hash);@@ -874,7 +894,7 @@ let pp_hash = vp.pp_hash()?;
// check that u_i's output points to the running instance // u_i.X[0] == H(i, z_0, z_i, U_i) - let expected_u_i_x = U_i.hash(&sponge, pp_hash, num_steps, z_0, z_i.clone()); + let expected_u_i_x = U_i.hash(&sponge, pp_hash, num_steps, &z_0, &z_i); if expected_u_i_x != u_i.x[0] { return Err(Error::IVCVerificationFail); }
@@ -18,8 +18,8 @@ impl<C: CurveGroup> Absorb for CommittedInstance<C>
where C::ScalarField: Absorb, { - fn to_sponge_bytes(&self, _dest: &mut Vec<u8>) { - unimplemented!() + fn to_sponge_bytes(&self, dest: &mut Vec<u8>) { + C::ScalarField::batch_to_sponge_bytes(&self.to_sponge_field_elements_as_vec(), dest); } fn to_sponge_field_elements<F: PrimeField>(&self, dest: &mut Vec<F>) {@@ -35,7 +35,7 @@
// Implements the trait for absorbing ProtoGalaxy's CommittedInstanceVar in-circuit. impl<C: CurveGroup> AbsorbGadget<C::ScalarField> for CommittedInstanceVar<C> { fn to_sponge_bytes(&self) -> Result<Vec<UInt8<C::ScalarField>>, SynthesisError> { - unimplemented!() + FpVar::batch_to_sponge_bytes(&self.to_sponge_field_elements()?) } fn to_sponge_field_elements(&self) -> Result<Vec<FpVar<C::ScalarField>>, SynthesisError> {@@ -114,3 +114,46 @@ // Tracking issue: https://github.com/privacy-scaling-explorations/sonobe/issues/82
unimplemented!() } } + +#[cfg(test)] +pub mod tests { + use super::*; + use ark_bn254::{Fr, G1Projective as Projective}; + use ark_r1cs_std::{alloc::AllocVar, R1CSVar}; + use ark_relations::r1cs::ConstraintSystem; + use ark_std::UniformRand; + use rand::Rng; + + /// test that checks the native CommittedInstance.to_sponge_{bytes,field_elements} + /// vs the R1CS constraints version + #[test] + pub fn test_committed_instance_to_sponge_preimage() { + let mut rng = ark_std::test_rng(); + + let t = rng.gen::<u8>() as usize; + let io_len = rng.gen::<u8>() as usize; + + let ci = CommittedInstance::<Projective> { + phi: Projective::rand(&mut rng), + betas: (0..t).map(|_| Fr::rand(&mut rng)).collect(), + e: Fr::rand(&mut rng), + x: (0..io_len).map(|_| Fr::rand(&mut rng)).collect(), + }; + + let bytes = ci.to_sponge_bytes_as_vec(); + let field_elements = ci.to_sponge_field_elements_as_vec(); + + let cs = ConstraintSystem::<Fr>::new_ref(); + + let ciVar = + CommittedInstanceVar::<Projective>::new_witness(cs.clone(), || Ok(ci.clone())).unwrap(); + let bytes_var = ciVar.to_sponge_bytes().unwrap(); + let field_elements_var = ciVar.to_sponge_field_elements().unwrap(); + + assert!(cs.is_satisfied().unwrap()); + + // check that the natively computed and in-circuit computed hashes match + assert_eq!(bytes_var.value().unwrap(), bytes); + assert_eq!(field_elements_var.value().unwrap(), field_elements); + } +}
@@ -0,0 +1,121 @@
+use ark_crypto_primitives::sponge::{ + constraints::{AbsorbGadget, CryptographicSpongeVar}, + poseidon::constraints::PoseidonSpongeVar, + Absorb, +}; +use ark_ec::CurveGroup; +use ark_ff::PrimeField; +use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, ToConstraintFieldGadget}; +use ark_relations::r1cs::SynthesisError; + +use crate::{transcript::Transcript, Error}; + +use super::circuits::CF1; + +pub trait CommittedInstanceOps<C: CurveGroup> { + /// The in-circuit representation of the committed instance. + type Var: AllocVar<Self, CF1<C>> + CommittedInstanceVarOps<C>; + /// `hash` implements the committed instance hash compatible with the + /// in-circuit implementation from `CommittedInstanceVarOps::hash`. + /// + /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and + /// `U_i` is the committed instance `self`. + fn hash<T: Transcript<CF1<C>>>( + &self, + sponge: &T, + pp_hash: CF1<C>, // public params hash + i: CF1<C>, + z_0: &[CF1<C>], + z_i: &[CF1<C>], + ) -> CF1<C> + where + CF1<C>: Absorb, + Self: Sized + Absorb, + { + let mut sponge = sponge.clone(); + sponge.absorb(&pp_hash); + sponge.absorb(&i); + sponge.absorb(&z_0); + sponge.absorb(&z_i); + sponge.absorb(&self); + sponge.squeeze_field_elements(1)[0] + } + + /// Returns the commitments contained in the committed instance. + fn get_commitments(&self) -> Vec<C>; + + /// Returns `true` if the committed instance is an incoming instance, and + /// `false` if it is a running instance. + fn is_incoming(&self) -> bool; + + /// Checks if the committed instance is an incoming instance. + fn check_incoming(&self) -> Result<(), Error> { + self.is_incoming() + .then_some(()) + .ok_or(Error::NotIncomingCommittedInstance) + } +} + +pub trait CommittedInstanceVarOps<C: CurveGroup> { + type PointVar: ToConstraintFieldGadget<CF1<C>>; + /// `hash` implements the in-circuit committed instance hash compatible with + /// the native implementation from `CommittedInstanceOps::hash`. + /// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and + /// `U_i` is the committed instance `self`. + /// + /// Additionally it returns the in-circuit representation of the committed + /// instance `self` as a vector of field elements, so they can be reused in + /// other gadgets avoiding recalculating (reconstraining) them. + #[allow(clippy::type_complexity)] + fn hash( + &self, + sponge: &PoseidonSpongeVar<CF1<C>>, + pp_hash: &FpVar<CF1<C>>, + i: &FpVar<CF1<C>>, + z_0: &[FpVar<CF1<C>>], + z_i: &[FpVar<CF1<C>>], + ) -> Result<(FpVar<CF1<C>>, Vec<FpVar<CF1<C>>>), SynthesisError> + where + Self: AbsorbGadget<CF1<C>>, + { + let mut sponge = sponge.clone(); + let U_vec = self.to_sponge_field_elements()?; + sponge.absorb(&pp_hash)?; + sponge.absorb(&i)?; + sponge.absorb(&z_0)?; + sponge.absorb(&z_i)?; + sponge.absorb(&U_vec)?; + Ok((sponge.squeeze_field_elements(1)?.pop().unwrap(), U_vec)) + } + + /// Returns the commitments contained in the committed instance. + fn get_commitments(&self) -> Vec<Self::PointVar>; + + /// Returns the public inputs contained in the committed instance. + fn get_public_inputs(&self) -> &[FpVar<CF1<C>>]; + + /// Generates constraints to enforce that the committed instance is an + /// incoming instance. + fn enforce_incoming(&self) -> Result<(), SynthesisError>; + + /// Generates constraints to enforce that the committed instance `self` is + /// partially equal to another committed instance `other`. + /// Here, only field elements are compared, while commitments (points) are + /// not. + fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError>; +} + +pub trait WitnessOps<F: PrimeField> { + /// The in-circuit representation of the witness. + type Var: AllocVar<Self, F> + WitnessVarOps<F>; + + /// Returns the openings (i.e., the values being committed to and the + /// randomness) contained in the witness. + fn get_openings(&self) -> Vec<(&[F], F)>; +} + +pub trait WitnessVarOps<F: PrimeField> { + /// Returns the openings (i.e., the values being committed to and the + /// randomness) contained in the witness. + fn get_openings(&self) -> Vec<(&[FpVar<F>], FpVar<F>)>; +}
@@ -43,6 +43,8 @@ #[error("IVC verification failed")]
IVCVerificationFail, #[error("zkIVC verification failed")] zkIVCVerificationFail, + #[error("Committed instance is expected to be an incoming (fresh) instance")] + NotIncomingCommittedInstance, #[error("R1CS instance is expected to not be relaxed")] R1CSUnrelaxedFail, #[error("Could not find the inner ConstraintSystem")]