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`
This commit is contained in:
winderica
2024-09-20 01:36:19 +08:00
committed by GitHub
parent 1947ab0f51
commit dfd03ea386
15 changed files with 566 additions and 329 deletions

View File

@@ -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>) {
@@ -142,6 +143,18 @@ where
}
}
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
}
}
#[cfg(test)]
pub mod tests {
use ark_pallas::Fr;

View File

@@ -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 @@ use ark_r1cs_std::{
fields::{fp::FpVar, FieldVar},
groups::GroupOpsBounds,
prelude::CurveVar,
uint8::UInt8,
R1CSVar, ToConstraintFieldGadget,
};
use ark_relations::r1cs::{
@@ -41,6 +42,7 @@ use crate::folding::{
CF1, CF2,
},
nova::get_r1cs_from_cs,
traits::CommittedInstanceVarOps,
};
use crate::frontend::FCircuit;
use crate::utils::virtual_polynomial::VPAuxInfo;
@@ -52,19 +54,16 @@ use crate::{
/// 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 @@ where
}
}
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 @@ where
// 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 @@ where
}
}
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 @@ where
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 @@ where
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 @@ mod tests {
utils::{compute_c, compute_sigmas_thetas},
HyperNovaCycleFoldCircuit,
},
traits::CommittedInstanceOps,
},
frontend::utils::CubicFCircuit,
transcript::poseidon::poseidon_canonical_config,
@@ -1113,6 +1129,37 @@ mod tests {
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 @@ mod tests {
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 @@ mod tests {
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 @@ mod tests {
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 @@ mod tests {
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 @@ mod tests {
// 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 @@ mod tests {
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);

View File

@@ -26,8 +26,6 @@ use super::{
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 @@ use crate::utils::{
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)]
@@ -66,6 +66,12 @@ impl<F: PrimeField> AllocVar<Witness<F>, F> for WitnessVar<F> {
}
}
impl<F: PrimeField> WitnessVarOps<F> for WitnessVar<F> {
fn get_openings(&self) -> Vec<(&[FpVar<F>], FpVar<F>)> {
vec![(&self.w, self.r_w.clone())]
}
}
/// CCSMatricesVar contains the matrices 'M' of the CCS without the rest of CCS parameters.
#[derive(Debug, Clone)]
pub struct CCSMatricesVar<F: PrimeField> {
@@ -340,13 +346,7 @@ where
)?;
// 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")]

View File

@@ -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 @@ where
}
}
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
}
}

View File

@@ -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 @@ use crate::folding::circuits::{
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;
@@ -81,6 +83,14 @@ impl<F: PrimeField> Witness<F> {
}
}
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)]
}
}
/// Proving parameters for HyperNova-based IVC
#[derive(Debug, Clone)]
pub struct ProverParams<C1, C2, CS1, CS2, const H: bool>
@@ -307,8 +317,8 @@ where
&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 @@ where
&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 @@ where
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 @@ where
&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 @@ where
&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 @@ where
// 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);
}