HyperNova: add multi-instances folding to AugmentedFCircuit & IVC (#119)

- Adds the logic to support multi-instances folding in HyperNova's
AugmentedFCircuit & IVC.
- Adds also methods to generate new LCCCS & CCCS instances that don't
depend on the main folding chain, to be folded in in the next step
- Updates CycleFold circuit & methods to work other folding schemes than
  Nova, adapting it to fold multiple points per circuit (instead of
2-to-1 as till now)
- Handle multi-instances folding in the FoldingScheme trait
  interface, which expects 'None' in Nova, and 'Some' in HyperNova &
other multi-folding schemes.
This commit is contained in:
arnaucube
2024-07-16 02:59:56 +02:00
committed by GitHub
parent cc1f6316a7
commit edadcdd520
22 changed files with 807 additions and 252 deletions

View File

@@ -21,11 +21,8 @@ use cccs::CCCS;
use lcccs::LCCCS;
use nimfs::NIMFS;
use crate::arith::{
ccs::CCS,
r1cs::{extract_w_x, R1CS},
};
use crate::commitment::CommitmentScheme;
use crate::constants::N_BITS_RO;
use crate::folding::circuits::{
cyclefold::{fold_cyclefold_circuit, CycleFoldCircuit},
CF2,
@@ -37,7 +34,13 @@ use crate::folding::nova::{
use crate::frontend::FCircuit;
use crate::utils::{get_cm_coordinates, pp_hash};
use crate::Error;
use crate::FoldingScheme;
use crate::{
arith::{
ccs::CCS,
r1cs::{extract_w_x, R1CS},
},
FoldingScheme, MultiFolding,
};
/// Witness for the LCCCS & CCCS, containing the w vector, and the r_w used as randomness in the Pedersen commitment.
#[derive(Debug, Clone, Eq, PartialEq)]
@@ -70,6 +73,8 @@ where
pub cf_cs_params: CS2::ProverParams,
// if ccs is set, it will be used, if not, it will be computed at runtime
pub ccs: Option<CCS<C1::ScalarField>>,
pub mu: usize,
pub nu: usize,
}
#[derive(Debug, Clone)]
@@ -136,6 +141,8 @@ where
pub F: FC,
/// public params hash
pub pp_hash: C1::ScalarField,
pub mu: usize, // number of LCCCS instances to be folded
pub nu: usize, // number of CCCS instances to be folded
pub i: C1::ScalarField,
/// initial state
pub z_0: Vec<C1::ScalarField>,
@@ -152,6 +159,180 @@ where
pub cf_U_i: CommittedInstance<C2>,
}
impl<C1, GC1, C2, GC2, FC, CS1, CS2> MultiFolding<C1, C2, FC>
for HyperNova<C1, GC1, C2, GC2, FC, CS1, CS2>
where
C1: CurveGroup,
GC1: CurveVar<C1, CF2<C1>> + ToConstraintFieldGadget<CF2<C1>>,
C2: CurveGroup,
GC2: CurveVar<C2, CF2<C2>> + ToConstraintFieldGadget<CF2<C2>>,
FC: FCircuit<C1::ScalarField>,
CS1: CommitmentScheme<C1>,
CS2: CommitmentScheme<C2>,
<C1 as CurveGroup>::BaseField: PrimeField,
<C2 as CurveGroup>::BaseField: PrimeField,
<C1 as Group>::ScalarField: Absorb,
<C2 as Group>::ScalarField: Absorb,
C1: CurveGroup<BaseField = C2::ScalarField, ScalarField = C2::BaseField>,
for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>,
for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>,
{
type RunningInstance = (LCCCS<C1>, Witness<C1::ScalarField>);
type IncomingInstance = (CCCS<C1>, Witness<C1::ScalarField>);
type MultiInstance = (Vec<Self::RunningInstance>, Vec<Self::IncomingInstance>);
/// Creates a new LCCS instance for the given state, which satisfies the HyperNova.CCS. This
/// method can be used to generate the 'other' LCCS instances to be folded in the multi-folding
/// step.
fn new_running_instance(
&self,
mut rng: impl RngCore,
state: Vec<C1::ScalarField>,
external_inputs: Vec<C1::ScalarField>,
) -> Result<Self::RunningInstance, Error> {
let r1cs_z = self.new_instance_generic(state, external_inputs)?;
// compute committed instances, w_{i+1}, u_{i+1}, which will be used as w_i, u_i, so we
// assign them directly to w_i, u_i.
let (U_i, W_i) = self
.ccs
.to_lcccs::<_, _, CS1>(&mut rng, &self.cs_params, &r1cs_z)?;
#[cfg(test)]
U_i.check_relation(&self.ccs, &W_i)?;
Ok((U_i, W_i))
}
/// Creates a new CCCS instance for the given state, which satisfies the HyperNova.CCS. This
/// method can be used to generate the 'other' CCCS instances to be folded in the multi-folding
/// step.
fn new_incoming_instance(
&self,
mut rng: impl RngCore,
state: Vec<C1::ScalarField>,
external_inputs: Vec<C1::ScalarField>,
) -> Result<Self::IncomingInstance, Error> {
let r1cs_z = self.new_instance_generic(state, external_inputs)?;
// compute committed instances, w_{i+1}, u_{i+1}, which will be used as w_i, u_i, so we
// assign them directly to w_i, u_i.
let (u_i, w_i) = self
.ccs
.to_cccs::<_, _, CS1>(&mut rng, &self.cs_params, &r1cs_z)?;
#[cfg(test)]
u_i.check_relation(&self.ccs, &w_i)?;
Ok((u_i, w_i))
}
}
impl<C1, GC1, C2, GC2, FC, CS1, CS2> HyperNova<C1, GC1, C2, GC2, FC, CS1, CS2>
where
C1: CurveGroup,
GC1: CurveVar<C1, CF2<C1>> + ToConstraintFieldGadget<CF2<C1>>,
C2: CurveGroup,
GC2: CurveVar<C2, CF2<C2>> + ToConstraintFieldGadget<CF2<C2>>,
FC: FCircuit<C1::ScalarField>,
CS1: CommitmentScheme<C1>,
CS2: CommitmentScheme<C2>,
<C1 as CurveGroup>::BaseField: PrimeField,
<C2 as CurveGroup>::BaseField: PrimeField,
<C1 as Group>::ScalarField: Absorb,
<C2 as Group>::ScalarField: Absorb,
C1: CurveGroup<BaseField = C2::ScalarField, ScalarField = C2::BaseField>,
for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>,
for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>,
{
/// internal helper for new_running_instance & new_incoming_instance methods, returns the R1CS
/// z=[u,x,w] vector to be used to create the LCCCS & CCCS fresh instances.
fn new_instance_generic(
&self,
state: Vec<C1::ScalarField>,
external_inputs: Vec<C1::ScalarField>,
) -> Result<Vec<C1::ScalarField>, Error> {
// prepare the initial dummy instances
let U_i = LCCCS::<C1>::dummy(self.ccs.l, self.ccs.t, self.ccs.s);
let mut u_i = CCCS::<C1>::dummy(self.ccs.l);
let (_, cf_U_i): (NovaWitness<C2>, CommittedInstance<C2>) = self.cf_r1cs.dummy_instance();
let sponge = PoseidonSponge::<C1::ScalarField>::new(&self.poseidon_config);
u_i.x = vec![
U_i.hash(
&sponge,
self.pp_hash,
C1::ScalarField::zero(), // i
self.z_0.clone(),
state.clone(),
),
cf_U_i.hash_cyclefold(&sponge, self.pp_hash),
];
let us = vec![u_i.clone(); self.nu - 1];
let z_i1 = self
.F
.step_native(0, state.clone(), external_inputs.clone())?;
// compute u_{i+1}.x
let U_i1 = LCCCS::dummy(self.ccs.l, self.ccs.t, self.ccs.s);
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(),
);
let cf_u_i1_x = cf_U_i.hash_cyclefold(&sponge, self.pp_hash);
let augmented_f_circuit = AugmentedFCircuit::<C1, C2, GC2, FC> {
_c2: PhantomData,
_gc2: PhantomData,
poseidon_config: self.poseidon_config.clone(),
ccs: self.ccs.clone(),
pp_hash: Some(self.pp_hash),
mu: self.mu,
nu: self.nu,
i: Some(C1::ScalarField::zero()),
i_usize: Some(0),
z_0: Some(self.z_0.clone()),
z_i: Some(state.clone()),
external_inputs: Some(external_inputs),
U_i: Some(U_i.clone()),
Us: None,
u_i_C: Some(u_i.C),
us: Some(us),
U_i1_C: Some(U_i1.C),
F: self.F.clone(),
x: Some(u_i1_x),
nimfs_proof: None,
// cyclefold values
cf_u_i_cmW: None,
cf_U_i: None,
cf_x: Some(cf_u_i1_x),
cf_cmT: None,
};
let (cs, _) = augmented_f_circuit.compute_cs_ccs()?;
#[cfg(test)]
assert!(cs.is_satisfied()?);
let (r1cs_w_i1, r1cs_x_i1) = extract_w_x::<C1::ScalarField>(&cs); // includes 1 and public inputs
#[cfg(test)]
assert_eq!(r1cs_x_i1[0], augmented_f_circuit.x.unwrap());
let r1cs_z = [
vec![C1::ScalarField::one()],
r1cs_x_i1.clone(),
r1cs_w_i1.clone(),
]
.concat();
Ok(r1cs_z)
}
}
impl<C1, GC1, C2, GC2, FC, CS1, CS2> FoldingScheme<C1, C2, FC>
for HyperNova<C1, GC1, C2, GC2, FC, CS1, CS2>
where
@@ -170,25 +351,37 @@ where
for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>,
for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>,
{
type PreprocessorParam = PreprocessorParam<C1, C2, FC, CS1, CS2>;
/// Reuse Nova's PreprocessorParam, together with two usize values, which are mu & nu
/// respectively, which indicate the amount of LCCCS & CCCS instances to be folded at each
/// folding step.
type PreprocessorParam = (PreprocessorParam<C1, C2, FC, CS1, CS2>, usize, usize);
type ProverParam = ProverParams<C1, C2, CS1, CS2>;
type VerifierParam = VerifierParams<C1, C2, CS1, CS2>;
type RunningInstance = (LCCCS<C1>, Witness<C1::ScalarField>);
type IncomingInstance = (CCCS<C1>, Witness<C1::ScalarField>);
type MultiCommittedInstanceWithWitness =
(Vec<Self::RunningInstance>, Vec<Self::IncomingInstance>);
type CFInstance = (CommittedInstance<C2>, NovaWitness<C2>);
fn preprocess(
mut rng: impl RngCore,
prep_param: &Self::PreprocessorParam,
) -> Result<(Self::ProverParam, Self::VerifierParam), Error> {
let (prep_param, mu, nu) = prep_param;
if *mu < 1 || *nu < 1 {
return Err(Error::CantBeZero("mu,nu".to_string()));
}
let augmented_f_circuit = AugmentedFCircuit::<C1, C2, GC2, FC>::empty(
&prep_param.poseidon_config,
prep_param.F.clone(),
None,
*mu,
*nu,
)?;
let ccs = augmented_f_circuit.ccs.clone();
let cf_circuit = CycleFoldCircuit::<C1, GC1>::empty();
let cf_circuit = CycleFoldCircuit::<C1, GC1>::empty(mu + nu);
let cf_r1cs = get_r1cs_from_cs::<C2::ScalarField>(cf_circuit)?;
// if cs params exist, use them, if not, generate new ones
@@ -215,6 +408,8 @@ where
cs_params: cs_pp.clone(),
cf_cs_params: cf_cs_pp.clone(),
ccs: Some(ccs.clone()),
mu: *mu,
nu: *nu,
};
let vp = VerifierParams::<C1, C2, CS1, CS2> {
poseidon_config: prep_param.poseidon_config.clone(),
@@ -228,11 +423,15 @@ where
/// Initializes the HyperNova+CycleFold's IVC for the given parameters and initial state `z_0`.
fn init(
params: (Self::ProverParam, Self::VerifierParam),
params: &(Self::ProverParam, Self::VerifierParam),
F: FC,
z_0: Vec<C1::ScalarField>,
) -> Result<Self, Error> {
let (pp, vp) = params;
if pp.mu < 1 || pp.nu < 1 {
return Err(Error::CantBeZero("mu,nu".to_string()));
}
// `sponge` is for digest computation.
let sponge = PoseidonSponge::<C1::ScalarField>::new(&pp.poseidon_config);
@@ -242,10 +441,12 @@ where
&pp.poseidon_config,
F.clone(),
pp.ccs.clone(),
pp.mu,
pp.nu,
)?;
let ccs = augmented_f_circuit.ccs.clone();
let cf_circuit = CycleFoldCircuit::<C1, GC1>::empty();
let cf_circuit = CycleFoldCircuit::<C1, GC1>::empty(pp.mu + pp.nu);
let cf_r1cs = get_r1cs_from_cs::<C2::ScalarField>(cf_circuit)?;
// compute the public params hash
@@ -282,6 +483,8 @@ where
cf_cs_params: pp.cf_cs_params.clone(),
F,
pp_hash,
mu: pp.mu,
nu: pp.nu,
i: C1::ScalarField::zero(),
z_0: z_0.clone(),
z_i: z_0,
@@ -300,10 +503,42 @@ where
&mut self,
mut rng: impl RngCore,
external_inputs: Vec<C1::ScalarField>,
other_instances: Option<Self::MultiCommittedInstanceWithWitness>,
) -> Result<(), Error> {
// `sponge` is for digest computation.
let sponge = PoseidonSponge::<C1::ScalarField>::new(&self.poseidon_config);
let other_instances = other_instances.ok_or(Error::MissingOtherInstances)?;
#[allow(clippy::type_complexity)]
let (lcccs, cccs): (
Vec<(LCCCS<C1>, Witness<C1::ScalarField>)>,
Vec<(CCCS<C1>, Witness<C1::ScalarField>)>,
) = other_instances;
// recall, mu & nu is the number of all the LCCCS & CCCS respectively, including the
// running and incoming instances that are not part of the 'other_instances', hence the +1
// in the couple of following checks.
if lcccs.len() + 1 != self.mu {
return Err(Error::NotSameLength(
"other_instances.lcccs.len()".to_string(),
lcccs.len(),
"hypernova.mu".to_string(),
self.mu,
));
}
if cccs.len() + 1 != self.nu {
return Err(Error::NotSameLength(
"other_instances.cccs.len()".to_string(),
cccs.len(),
"hypernova.nu".to_string(),
self.nu,
));
}
let (Us, Ws): (Vec<LCCCS<C1>>, Vec<Witness<C1::ScalarField>>) = lcccs.into_iter().unzip();
let (us, ws): (Vec<CCCS<C1>>, Vec<Witness<C1::ScalarField>>) = cccs.into_iter().unzip();
let augmented_f_circuit: AugmentedFCircuit<C1, C2, GC2, FC>;
if self.z_i.len() != self.F.state_len() {
@@ -361,13 +596,17 @@ where
poseidon_config: self.poseidon_config.clone(),
ccs: self.ccs.clone(),
pp_hash: Some(self.pp_hash),
mu: self.mu,
nu: self.nu,
i: Some(C1::ScalarField::zero()),
i_usize: Some(0),
z_0: Some(self.z_0.clone()),
z_i: Some(self.z_i.clone()),
external_inputs: Some(external_inputs.clone()),
u_i_C: Some(self.u_i.C),
U_i: Some(self.U_i.clone()),
Us: Some(Us.clone()),
u_i_C: Some(self.u_i.C),
us: Some(us.clone()),
U_i1_C: Some(U_i1.C),
F: self.F.clone(),
x: Some(u_i1_x),
@@ -383,15 +622,15 @@ where
let mut transcript_p: PoseidonSponge<C1::ScalarField> =
PoseidonSponge::<C1::ScalarField>::new(&self.poseidon_config);
transcript_p.absorb(&self.pp_hash);
let (rho_bits, nimfs_proof);
(nimfs_proof, U_i1, W_i1, rho_bits) =
let (rho_powers, nimfs_proof);
(nimfs_proof, U_i1, W_i1, rho_powers) =
NIMFS::<C1, PoseidonSponge<C1::ScalarField>>::prove(
&mut transcript_p,
&self.ccs,
&[self.U_i.clone()],
&[self.u_i.clone()],
&[self.W_i.clone()],
&[self.w_i.clone()],
&[vec![self.U_i.clone()], Us.clone()].concat(),
&[vec![self.u_i.clone()], us.clone()].concat(),
&[vec![self.W_i.clone()], Ws].concat(),
&[vec![self.w_i.clone()], ws].concat(),
)?;
// sanity check: check the folded instance relation
@@ -406,29 +645,60 @@ where
z_i1.clone(),
);
let rho_Fq = C2::ScalarField::from_bigint(BigInteger::from_bits_le(&rho_bits))
.ok_or(Error::OutOfBounds)?;
let rho_powers_Fq: Vec<C1::BaseField> = rho_powers
.iter()
.map(|rho_i| {
C1::BaseField::from_bigint(BigInteger::from_bits_le(
&rho_i.into_bigint().to_bits_le(),
))
.unwrap()
})
.collect();
let rho_powers_bits: Vec<Vec<bool>> = rho_powers
.iter()
.map(|rho_i| rho_i.into_bigint().to_bits_le()[..N_BITS_RO].to_vec())
.collect();
// CycleFold part:
// get the vector used as public inputs 'x' in the CycleFold circuit
// cyclefold circuit for cmW
// get the vector used as public inputs 'x' in the CycleFold circuit.
// Place the random values and the points coordinates as the public input x:
// In Nova, this is: x == [r, p1, p2, p3].
// In multifolding schemes such as HyperNova, this is:
// computed_x = [r_0, r_1, r_2, ..., r_n, p_0, p_1, p_2, ..., p_n],
// where each p_i is in fact p_i.to_constraint_field()
let cf_u_i_x = [
vec![rho_Fq],
rho_powers_Fq,
get_cm_coordinates(&self.U_i.C),
Us.iter()
.flat_map(|Us_i| get_cm_coordinates(&Us_i.C))
.collect(),
get_cm_coordinates(&self.u_i.C),
us.iter()
.flat_map(|us_i| get_cm_coordinates(&us_i.C))
.collect(),
get_cm_coordinates(&U_i1.C),
]
.concat();
let cf_circuit = CycleFoldCircuit::<C1, GC1> {
_gc: PhantomData,
r_bits: Some(rho_bits.clone()),
p1: Some(self.U_i.clone().C),
p2: Some(self.u_i.clone().C),
n_points: self.mu + self.nu,
r_bits: Some(rho_powers_bits.clone()),
points: Some(
[
vec![self.U_i.clone().C],
Us.iter().map(|Us_i| Us_i.C).collect(),
vec![self.u_i.clone().C],
us.iter().map(|us_i| us_i.C).collect(),
]
.concat(),
),
x: Some(cf_u_i_x.clone()),
};
let (_cf_w_i, cf_u_i, cf_W_i1, cf_U_i1, cf_cmT, _) =
fold_cyclefold_circuit::<C1, GC1, C2, GC2, FC, CS1, CS2>(
self.mu + self.nu,
&mut transcript_p,
self.cf_r1cs.clone(),
self.cf_cs_params.clone(),
@@ -447,13 +717,17 @@ where
poseidon_config: self.poseidon_config.clone(),
ccs: self.ccs.clone(),
pp_hash: Some(self.pp_hash),
mu: self.mu,
nu: self.nu,
i: Some(self.i),
i_usize: Some(i_usize),
z_0: Some(self.z_0.clone()),
z_i: Some(self.z_i.clone()),
external_inputs: Some(external_inputs),
u_i_C: Some(self.u_i.C),
U_i: Some(self.U_i.clone()),
Us: Some(Us.clone()),
u_i_C: Some(self.u_i.C),
us: Some(us.clone()),
U_i1_C: Some(U_i1.C),
F: self.F.clone(),
x: Some(u_i1_x),
@@ -618,31 +892,48 @@ mod tests {
type HN<CS1, CS2> =
HyperNova<Projective, GVar, Projective2, GVar2, CubicFCircuit<Fr>, CS1, CS2>;
let (mu, nu) = (2, 3);
let prep_param =
PreprocessorParam::<Projective, Projective2, CubicFCircuit<Fr>, CS1, CS2>::new(
poseidon_config.clone(),
F_circuit,
);
let (prover_params, verifier_params) = HN::preprocess(&mut rng, &prep_param).unwrap();
let hypernova_params = HN::preprocess(&mut rng, &(prep_param, mu, nu)).unwrap();
let z_0 = vec![Fr::from(3_u32)];
let mut hypernova = HN::init(
(prover_params, verifier_params.clone()),
F_circuit,
z_0.clone(),
)
.unwrap();
let mut hypernova = HN::init(&hypernova_params, F_circuit, z_0.clone()).unwrap();
let num_steps: usize = 3;
for _ in 0..num_steps {
hypernova.prove_step(&mut rng, vec![]).unwrap();
// prepare some new instances to fold in the multifolding step
let mut lcccs = vec![];
for j in 0..mu - 1 {
let instance_state = vec![Fr::from(j as u32 + 85_u32)];
let (U, W) = hypernova
.new_running_instance(&mut rng, instance_state, vec![])
.unwrap();
lcccs.push((U, W));
}
let mut cccs = vec![];
for j in 0..nu - 1 {
let instance_state = vec![Fr::from(j as u32 + 15_u32)];
let (u, w) = hypernova
.new_incoming_instance(&mut rng, instance_state, vec![])
.unwrap();
cccs.push((u, w));
}
dbg!(&hypernova.i);
hypernova
.prove_step(&mut rng, vec![], Some((lcccs, cccs)))
.unwrap();
}
assert_eq!(Fr::from(num_steps as u32), hypernova.i);
let (running_instance, incoming_instance, cyclefold_instance) = hypernova.instances();
HN::verify(
verifier_params,
hypernova_params.1, // verifier_params
z_0,
hypernova.z_i,
hypernova.i,