Browse Source

Nova vairants (Ova) NIFS abstraction (#165)

* add NIFS trait abstraction (based on the needs for Nova, Mova, Ova), defining a common interface between the three Nova variants

The recent Ova NIFS PR #163 (https://github.com/privacy-scaling-explorations/sonobe/pull/163)
and Mova NIFS PR #161 (https://github.com/privacy-scaling-explorations/sonobe/pull/161)
PRs add Nova NIFS variants implementations which differ from Nova in the
logic done for the `E` error terms of the instances.

The current Ova implementation (https://github.com/privacy-scaling-explorations/sonobe/pull/163)
is based on the existing Nova NIFS code base and adds the modifications
to the `E` logic on top of it, and thus duplicating the code. Similarly
for the Mova NIFS impl.

The rest of the Mova & Ova schemes logic that is not yet implemented is
pretty similar to Nova one (ie. the IVC logic, the circuits and the
Decider), so ideally that can be done reusing most of the already
existing Nova code without duplicating it. This PR is a first step in
that direction for the existing Ova NIFS code.

This commit adds the NIFS trait abstraction with the idea of allowing to
reduce the amount of duplicated code for the Ova's NIFS impl on top of
the Nova's code.

* add Ova variant on top of the new NIFS trait abstraction

This is done from the existing Ova implementation at
`folding/ova/{mod.rs,nofs.rs}`, but removing when possible code that is not
needed or duplicated from the Nova logic.

* rm old Ova duplicated code

This commit combined with the other ones (add nifs abstraction & port
Ova to the nifs abstraction) allows to effectively get rid of ~400 lines
of code that were duplicated in the Ova NIFS impl from the Nova impl.

* small polishing & rebase to latest `main` branch updates
main
arnaucube 1 month ago
committed by GitHub
parent
commit
a07e17e9db
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
14 changed files with 640 additions and 1075 deletions
  1. +27
    -9
      folding-schemes/src/folding/circuits/cyclefold.rs
  2. +0
    -1
      folding-schemes/src/folding/mod.rs
  3. +50
    -29
      folding-schemes/src/folding/nova/circuits.rs
  4. +1
    -1
      folding-schemes/src/folding/nova/decider.rs
  5. +9
    -9
      folding-schemes/src/folding/nova/decider_circuits.rs
  6. +1
    -0
      folding-schemes/src/folding/nova/decider_eth.rs
  7. +11
    -9
      folding-schemes/src/folding/nova/decider_eth_circuit.rs
  8. +15
    -13
      folding-schemes/src/folding/nova/mod.rs
  9. +168
    -315
      folding-schemes/src/folding/nova/nifs.rs
  10. +269
    -0
      folding-schemes/src/folding/nova/ova.rs
  11. +73
    -24
      folding-schemes/src/folding/nova/traits.rs
  12. +16
    -26
      folding-schemes/src/folding/nova/zk.rs
  13. +0
    -157
      folding-schemes/src/folding/ova/mod.rs
  14. +0
    -482
      folding-schemes/src/folding/ova/nifs.rs

+ 27
- 9
folding-schemes/src/folding/circuits/cyclefold.rs

@ -24,7 +24,7 @@ use super::{nonnative::uint::NonNativeUintVar, CF1, CF2};
use crate::arith::r1cs::{extract_w_x, R1CS}; use crate::arith::r1cs::{extract_w_x, R1CS};
use crate::commitment::CommitmentScheme; use crate::commitment::CommitmentScheme;
use crate::constants::NOVA_N_BITS_RO; use crate::constants::NOVA_N_BITS_RO;
use crate::folding::nova::nifs::NIFS;
use crate::folding::nova::{nifs::NIFS, traits::NIFSTrait};
use crate::transcript::{AbsorbNonNative, AbsorbNonNativeGadget, Transcript, TranscriptVar}; use crate::transcript::{AbsorbNonNative, AbsorbNonNativeGadget, Transcript, TranscriptVar};
use crate::Error; use crate::Error;
@ -570,9 +570,8 @@ where
let cf_r_Fq = C1::BaseField::from_bigint(BigInteger::from_bits_le(&cf_r_bits)) let cf_r_Fq = C1::BaseField::from_bigint(BigInteger::from_bits_le(&cf_r_bits))
.expect("cf_r_bits out of bounds"); .expect("cf_r_bits out of bounds");
let (cf_W_i1, cf_U_i1) = NIFS::<C2, CS2, H>::fold_instances(
cf_r_Fq, &cf_W_i, &cf_U_i, &cf_w_i, &cf_u_i, &cf_T, cf_cmT,
)?;
let (cf_W_i1, cf_U_i1) =
NIFS::<C2, CS2, H>::prove(cf_r_Fq, &cf_W_i, &cf_U_i, &cf_w_i, &cf_u_i, &cf_T, &cf_cmT)?;
Ok((cf_w_i, cf_u_i, cf_W_i1, cf_U_i1, cf_cmT, cf_r_Fq)) Ok((cf_w_i, cf_u_i, cf_W_i1, cf_U_i1, cf_cmT, cf_r_Fq))
} }
@ -584,10 +583,11 @@ pub mod tests {
poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge}, poseidon::{constraints::PoseidonSpongeVar, PoseidonSponge},
}; };
use ark_r1cs_std::R1CSVar; use ark_r1cs_std::R1CSVar;
use ark_std::UniformRand;
use ark_std::{One, UniformRand};
use super::*; use super::*;
use crate::folding::nova::nifs::tests::prepare_simple_fold_inputs;
use crate::commitment::pedersen::Pedersen;
use crate::folding::nova::CommittedInstance;
use crate::transcript::poseidon::poseidon_canonical_config; use crate::transcript::poseidon::poseidon_canonical_config;
use crate::utils::get_cm_coordinates; use crate::utils::get_cm_coordinates;
@ -669,12 +669,30 @@ pub mod tests {
#[test] #[test]
fn test_nifs_full_gadget() { fn test_nifs_full_gadget() {
let (_, _, _, _, ci1, _, ci2, _, ci3, _, cmT, r_bits, _) = prepare_simple_fold_inputs();
let mut rng = ark_std::test_rng();
let cs = ConstraintSystem::<Fq>::new_ref();
// prepare the committed instances to test in-circuit
let ci: Vec<CommittedInstance<Projective>> = (0..2)
.into_iter()
.map(|_| CommittedInstance::<Projective> {
cmE: Projective::rand(&mut rng),
u: Fr::rand(&mut rng),
cmW: Projective::rand(&mut rng),
x: vec![Fr::rand(&mut rng); 1],
})
.collect();
let (ci1, mut ci2) = (ci[0].clone(), ci[1].clone());
// make the 2nd instance a 'fresh' instance (ie. cmE=0, u=1)
ci2.cmE = Projective::zero();
ci2.u = Fr::one();
let r_bits: Vec<bool> =
Fr::rand(&mut rng).into_bigint().to_bits_le()[..NOVA_N_BITS_RO].to_vec();
let r_Fr = Fr::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap();
let cmT = Projective::rand(&mut rng);
let ci3 = NIFS::<Projective, Pedersen<Projective>>::verify(r_Fr, &ci1, &ci2, &cmT);
let cs = ConstraintSystem::<Fq>::new_ref();
let r_bitsVar = Vec::<Boolean<Fq>>::new_witness(cs.clone(), || Ok(r_bits)).unwrap(); let r_bitsVar = Vec::<Boolean<Fq>>::new_witness(cs.clone(), || Ok(r_bits)).unwrap();
let ci1Var = let ci1Var =
CycleFoldCommittedInstanceVar::<Projective, GVar>::new_witness(cs.clone(), || { CycleFoldCommittedInstanceVar::<Projective, GVar>::new_witness(cs.clone(), || {
Ok(ci1.clone()) Ok(ci1.clone())

+ 0
- 1
folding-schemes/src/folding/mod.rs

@ -1,6 +1,5 @@
pub mod circuits; pub mod circuits;
pub mod hypernova; pub mod hypernova;
pub mod nova; pub mod nova;
pub mod ova;
pub mod protogalaxy; pub mod protogalaxy;
pub mod traits; pub mod traits;

+ 50
- 29
folding-schemes/src/folding/nova/circuits.rs

@ -168,10 +168,11 @@ where
/// ChallengeGadget computes the RO challenge used for the Nova instances NIFS, it contains a /// ChallengeGadget computes the RO challenge used for the Nova instances NIFS, it contains a
/// rust-native and a in-circuit compatible versions. /// rust-native and a in-circuit compatible versions.
pub struct ChallengeGadget<C: CurveGroup> {
pub struct ChallengeGadget<C: CurveGroup, CI: Absorb> {
_c: PhantomData<C>, _c: PhantomData<C>,
_ci: PhantomData<CI>,
} }
impl<C: CurveGroup> ChallengeGadget<C>
impl<C: CurveGroup, CI: Absorb> ChallengeGadget<C, CI>
where where
C: CurveGroup, C: CurveGroup,
<C as CurveGroup>::BaseField: PrimeField, <C as CurveGroup>::BaseField: PrimeField,
@ -180,14 +181,17 @@ where
pub fn get_challenge_native<T: Transcript<C::ScalarField>>( pub fn get_challenge_native<T: Transcript<C::ScalarField>>(
transcript: &mut T, transcript: &mut T,
pp_hash: C::ScalarField, // public params hash pp_hash: C::ScalarField, // public params hash
U_i: CommittedInstance<C>,
u_i: CommittedInstance<C>,
cmT: C,
U_i: &CI,
u_i: &CI,
cmT: Option<&C>,
) -> Vec<bool> { ) -> Vec<bool> {
transcript.absorb(&pp_hash); transcript.absorb(&pp_hash);
transcript.absorb(&U_i); transcript.absorb(&U_i);
transcript.absorb(&u_i); transcript.absorb(&u_i);
transcript.absorb_nonnative(&cmT);
// in the Nova case we absorb the cmT, in Ova case we don't since it is not used.
if let Some(cmT_value) = cmT {
transcript.absorb_nonnative(cmT_value);
}
transcript.squeeze_bits(NOVA_N_BITS_RO) transcript.squeeze_bits(NOVA_N_BITS_RO)
} }
@ -197,12 +201,15 @@ where
pp_hash: FpVar<CF1<C>>, // public params hash pp_hash: FpVar<CF1<C>>, // public params hash
U_i_vec: Vec<FpVar<CF1<C>>>, // apready processed input, so we don't have to recompute these values U_i_vec: Vec<FpVar<CF1<C>>>, // apready processed input, so we don't have to recompute these values
u_i: CommittedInstanceVar<C>, u_i: CommittedInstanceVar<C>,
cmT: NonNativeAffineVar<C>,
cmT: Option<NonNativeAffineVar<C>>,
) -> Result<Vec<Boolean<C::ScalarField>>, SynthesisError> { ) -> Result<Vec<Boolean<C::ScalarField>>, SynthesisError> {
transcript.absorb(&pp_hash)?; transcript.absorb(&pp_hash)?;
transcript.absorb(&U_i_vec)?; transcript.absorb(&U_i_vec)?;
transcript.absorb(&u_i)?; transcript.absorb(&u_i)?;
transcript.absorb_nonnative(&cmT)?;
// in the Nova case we absorb the cmT, in Ova case we don't since it is not used.
if let Some(cmT_value) = cmT {
transcript.absorb_nonnative(&cmT_value)?;
}
transcript.squeeze_bits(NOVA_N_BITS_RO) transcript.squeeze_bits(NOVA_N_BITS_RO)
} }
} }
@ -376,12 +383,12 @@ where
// P.3. nifs.verify, obtains U_{i+1} by folding u_i & U_i . // P.3. nifs.verify, obtains U_{i+1} by folding u_i & U_i .
// compute r = H(u_i, U_i, cmT) // compute r = H(u_i, U_i, cmT)
let r_bits = ChallengeGadget::<C1>::get_challenge_gadget(
let r_bits = ChallengeGadget::<C1, CommittedInstance<C1>>::get_challenge_gadget(
&mut transcript, &mut transcript,
pp_hash.clone(), pp_hash.clone(),
U_i_vec, U_i_vec,
u_i.clone(), u_i.clone(),
cmT.clone(),
Some(cmT.clone()),
)?; )?;
let r = Boolean::le_bits_to_fp_var(&r_bits)?; let r = Boolean::le_bits_to_fp_var(&r_bits)?;
// Also convert r_bits to a `NonNativeFieldVar` // Also convert r_bits to a `NonNativeFieldVar`
@ -522,8 +529,8 @@ pub mod tests {
use ark_std::UniformRand; use ark_std::UniformRand;
use crate::commitment::pedersen::Pedersen; use crate::commitment::pedersen::Pedersen;
use crate::folding::nova::nifs::tests::prepare_simple_fold_inputs;
use crate::folding::nova::nifs::NIFS; use crate::folding::nova::nifs::NIFS;
use crate::folding::nova::traits::NIFSTrait;
use crate::folding::traits::CommittedInstanceOps; use crate::folding::traits::CommittedInstanceOps;
use crate::transcript::poseidon::poseidon_canonical_config; use crate::transcript::poseidon::poseidon_canonical_config;
@ -550,10 +557,22 @@ pub mod tests {
#[test] #[test]
fn test_nifs_gadget() { fn test_nifs_gadget() {
let (_, _, _, _, ci1, _, ci2, _, ci3, _, cmT, _, r_Fr) = prepare_simple_fold_inputs();
let mut rng = ark_std::test_rng();
let ci3_verifier = NIFS::<Projective, Pedersen<Projective>>::verify(r_Fr, &ci1, &ci2, &cmT);
assert_eq!(ci3_verifier, ci3);
// prepare the committed instances to test in-circuit
let ci: Vec<CommittedInstance<Projective>> = (0..2)
.into_iter()
.map(|_| CommittedInstance::<Projective> {
cmE: Projective::rand(&mut rng),
u: Fr::rand(&mut rng),
cmW: Projective::rand(&mut rng),
x: vec![Fr::rand(&mut rng); 1],
})
.collect();
let (ci1, ci2) = (ci[0].clone(), ci[1].clone());
let r_Fr = Fr::rand(&mut rng);
let cmT = Projective::rand(&mut rng);
let ci3 = NIFS::<Projective, Pedersen<Projective>>::verify(r_Fr, &ci1, &ci2, &cmT);
let cs = ConstraintSystem::<Fr>::new_ref(); let cs = ConstraintSystem::<Fr>::new_ref();
@ -673,13 +692,14 @@ pub mod tests {
let pp_hash = Fr::from(42u32); // only for testing let pp_hash = Fr::from(42u32); // only for testing
// compute the challenge natively // compute the challenge natively
let r_bits = ChallengeGadget::<Projective>::get_challenge_native(
&mut transcript,
pp_hash,
U_i.clone(),
u_i.clone(),
cmT,
);
let r_bits =
ChallengeGadget::<Projective, CommittedInstance<Projective>>::get_challenge_native(
&mut transcript,
pp_hash,
&U_i,
&u_i,
Some(&cmT),
);
let r = Fr::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap(); let r = Fr::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap();
let cs = ConstraintSystem::<Fr>::new_ref(); let cs = ConstraintSystem::<Fr>::new_ref();
@ -701,14 +721,15 @@ pub mod tests {
U_iVar.cmW.to_constraint_field().unwrap(), U_iVar.cmW.to_constraint_field().unwrap(),
] ]
.concat(); .concat();
let r_bitsVar = ChallengeGadget::<Projective>::get_challenge_gadget(
&mut transcriptVar,
pp_hashVar,
U_iVar_vec,
u_iVar,
cmTVar,
)
.unwrap();
let r_bitsVar =
ChallengeGadget::<Projective, CommittedInstance<Projective>>::get_challenge_gadget(
&mut transcriptVar,
pp_hashVar,
U_iVar_vec,
u_iVar,
Some(cmTVar),
)
.unwrap();
assert!(cs.is_satisfied().unwrap()); assert!(cs.is_satisfied().unwrap());
// check that the natively computed and in-circuit computed hashes match // check that the natively computed and in-circuit computed hashes match

+ 1
- 1
folding-schemes/src/folding/nova/decider.rs

@ -13,7 +13,7 @@ use ark_std::{One, Zero};
use core::marker::PhantomData; use core::marker::PhantomData;
use super::decider_circuits::{DeciderCircuit1, DeciderCircuit2}; use super::decider_circuits::{DeciderCircuit1, DeciderCircuit2};
use super::{nifs::NIFS, CommittedInstance, Nova};
use super::{nifs::NIFS, traits::NIFSTrait, CommittedInstance, Nova};
use crate::commitment::CommitmentScheme; use crate::commitment::CommitmentScheme;
use crate::folding::circuits::{ use crate::folding::circuits::{
cyclefold::CycleFoldCommittedInstance, cyclefold::CycleFoldCommittedInstance,

+ 9
- 9
folding-schemes/src/folding/nova/decider_circuits.rs

@ -27,6 +27,7 @@ use super::{
circuits::{ChallengeGadget, CommittedInstanceVar}, circuits::{ChallengeGadget, CommittedInstanceVar},
decider_eth_circuit::{KZGChallengesGadget, R1CSVar, RelaxedR1CSGadget, WitnessVar}, decider_eth_circuit::{KZGChallengesGadget, R1CSVar, RelaxedR1CSGadget, WitnessVar},
nifs::NIFS, nifs::NIFS,
traits::NIFSTrait,
CommittedInstance, Nova, Witness, CommittedInstance, Nova, Witness,
}; };
use crate::arith::r1cs::R1CS; use crate::arith::r1cs::R1CS;
@ -122,18 +123,17 @@ where
&nova.W_i.clone(), &nova.W_i.clone(),
&nova.U_i.clone(), &nova.U_i.clone(),
)?; )?;
let r_bits = ChallengeGadget::<C1>::get_challenge_native(
let r_bits = NIFS::<C1, CS1, H>::get_challenge(
&mut transcript, &mut transcript,
nova.pp_hash, nova.pp_hash,
nova.U_i.clone(),
nova.u_i.clone(),
cmT,
&nova.U_i,
&nova.u_i,
&cmT,
); );
let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits))
.ok_or(Error::OutOfBounds)?; .ok_or(Error::OutOfBounds)?;
let (W_i1, U_i1) = NIFS::<C1, CS1, H>::fold_instances(
r_Fr, &nova.W_i, &nova.U_i, &nova.w_i, &nova.u_i, &T, cmT,
)?;
let (W_i1, U_i1) =
NIFS::<C1, CS1, H>::prove(r_Fr, &nova.W_i, &nova.U_i, &nova.w_i, &nova.u_i, &T, &cmT)?;
// compute the commitment scheme challenges used as inputs in the circuit // compute the commitment scheme challenges used as inputs in the circuit
let (cs_challenge_W, cs_challenge_E) = let (cs_challenge_W, cs_challenge_E) =
@ -283,12 +283,12 @@ where
// do the actual checks later. // do the actual checks later.
let cmT = let cmT =
NonNativeAffineVar::new_input(cs.clone(), || Ok(self.cmT.unwrap_or_else(C1::zero)))?; NonNativeAffineVar::new_input(cs.clone(), || Ok(self.cmT.unwrap_or_else(C1::zero)))?;
let r_bits = ChallengeGadget::<C1>::get_challenge_gadget(
let r_bits = ChallengeGadget::<C1, CommittedInstance<C1>>::get_challenge_gadget(
&mut transcript, &mut transcript,
pp_hash, pp_hash,
U_i_vec, U_i_vec,
u_i.clone(), u_i.clone(),
cmT.clone(),
Some(cmT.clone()),
)?; )?;
// 5.1. // 5.1.
let (incircuit_c_W, incircuit_c_E) = let (incircuit_c_W, incircuit_c_E) =

+ 1
- 0
folding-schemes/src/folding/nova/decider_eth.rs

@ -15,6 +15,7 @@ use ark_std::{One, Zero};
use core::marker::PhantomData; use core::marker::PhantomData;
pub use super::decider_eth_circuit::DeciderEthCircuit; pub use super::decider_eth_circuit::DeciderEthCircuit;
use super::traits::NIFSTrait;
use super::{nifs::NIFS, CommittedInstance, Nova}; use super::{nifs::NIFS, CommittedInstance, Nova};
use crate::commitment::{ use crate::commitment::{
kzg::{Proof as KZGProof, KZG}, kzg::{Proof as KZGProof, KZG},

+ 11
- 9
folding-schemes/src/folding/nova/decider_eth_circuit.rs

@ -27,6 +27,7 @@ use core::{borrow::Borrow, marker::PhantomData};
use super::{ use super::{
circuits::{ChallengeGadget, CommittedInstanceVar}, circuits::{ChallengeGadget, CommittedInstanceVar},
nifs::NIFS, nifs::NIFS,
traits::NIFSTrait,
CommittedInstance, Nova, Witness, CommittedInstance, Nova, Witness,
}; };
use crate::commitment::{pedersen::Params as PedersenParams, CommitmentScheme}; use crate::commitment::{pedersen::Params as PedersenParams, CommitmentScheme};
@ -245,7 +246,7 @@ where
let mut transcript = PoseidonSponge::<C1::ScalarField>::new(&nova.poseidon_config); let mut transcript = PoseidonSponge::<C1::ScalarField>::new(&nova.poseidon_config);
// compute the U_{i+1}, W_{i+1} // compute the U_{i+1}, W_{i+1}
let (T, cmT) = NIFS::<C1, CS1, H>::compute_cmT(
let (aux_p, aux_v) = NIFS::<C1, CS1, H>::compute_aux(
&nova.cs_pp, &nova.cs_pp,
&nova.r1cs.clone(), &nova.r1cs.clone(),
&nova.w_i.clone(), &nova.w_i.clone(),
@ -253,17 +254,18 @@ where
&nova.W_i.clone(), &nova.W_i.clone(),
&nova.U_i.clone(), &nova.U_i.clone(),
)?; )?;
let r_bits = ChallengeGadget::<C1>::get_challenge_native(
let cmT = aux_v;
let r_bits = ChallengeGadget::<C1, CommittedInstance<C1>>::get_challenge_native(
&mut transcript, &mut transcript,
nova.pp_hash, nova.pp_hash,
nova.U_i.clone(),
nova.u_i.clone(),
cmT,
&nova.U_i,
&nova.u_i,
Some(&cmT),
); );
let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits))
.ok_or(Error::OutOfBounds)?; .ok_or(Error::OutOfBounds)?;
let (W_i1, U_i1) = NIFS::<C1, CS1, H>::fold_instances(
r_Fr, &nova.W_i, &nova.U_i, &nova.w_i, &nova.u_i, &T, cmT,
let (W_i1, U_i1) = NIFS::<C1, CS1, H>::prove(
r_Fr, &nova.W_i, &nova.U_i, &nova.w_i, &nova.u_i, &aux_p, &aux_v,
)?; )?;
// compute the KZG challenges used as inputs in the circuit // compute the KZG challenges used as inputs in the circuit
@ -483,12 +485,12 @@ where
let cmT = let cmT =
NonNativeAffineVar::new_input(cs.clone(), || Ok(self.cmT.unwrap_or_else(C1::zero)))?; NonNativeAffineVar::new_input(cs.clone(), || Ok(self.cmT.unwrap_or_else(C1::zero)))?;
// 1.1.a // 1.1.a
let r_bits = ChallengeGadget::<C1>::get_challenge_gadget(
let r_bits = ChallengeGadget::<C1, CommittedInstance<C1>>::get_challenge_gadget(
&mut transcript, &mut transcript,
pp_hash, pp_hash,
U_i_vec, U_i_vec,
u_i.clone(), u_i.clone(),
cmT.clone(),
Some(cmT),
)?; )?;
// 5.1. // 5.1.
let (incircuit_c_W, incircuit_c_E) = let (incircuit_c_W, incircuit_c_E) =

+ 15
- 13
folding-schemes/src/folding/nova/mod.rs

@ -35,15 +35,17 @@ use crate::{
}; };
use crate::{arith::Arith, commitment::CommitmentScheme}; use crate::{arith::Arith, commitment::CommitmentScheme};
use circuits::{AugmentedFCircuit, ChallengeGadget, CommittedInstanceVar};
use nifs::NIFS;
pub mod circuits; pub mod circuits;
pub mod nifs; pub mod nifs;
pub mod ova;
pub mod serialize; pub mod serialize;
pub mod traits; pub mod traits;
pub mod zk; pub mod zk;
use circuits::{AugmentedFCircuit, ChallengeGadget, CommittedInstanceVar};
use nifs::NIFS;
use traits::NIFSTrait;
// offchain decider // offchain decider
pub mod decider; pub mod decider;
pub mod decider_circuits; pub mod decider_circuits;
@ -678,15 +680,16 @@ where
.step_native(i_usize, self.z_i.clone(), external_inputs.clone())?; .step_native(i_usize, self.z_i.clone(), external_inputs.clone())?;
// compute T and cmT for AugmentedFCircuit // compute T and cmT for AugmentedFCircuit
let (T, cmT) = self.compute_cmT()?;
let (aux_p, aux_v) = self.compute_cmT()?;
let cmT = aux_v;
// r_bits is the r used to the RLC of the F' instances // r_bits is the r used to the RLC of the F' instances
let r_bits = ChallengeGadget::<C1>::get_challenge_native(
let r_bits = ChallengeGadget::<C1, CommittedInstance<C1>>::get_challenge_native(
&mut transcript, &mut transcript,
self.pp_hash, self.pp_hash,
self.U_i.clone(),
self.u_i.clone(),
cmT,
&self.U_i,
&self.u_i,
Some(&cmT),
); );
let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)) let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits))
.ok_or(Error::OutOfBounds)?; .ok_or(Error::OutOfBounds)?;
@ -694,10 +697,9 @@ where
.ok_or(Error::OutOfBounds)?; .ok_or(Error::OutOfBounds)?;
// fold Nova instances // fold Nova instances
let (W_i1, U_i1): (Witness<C1>, CommittedInstance<C1>) =
NIFS::<C1, CS1, H>::fold_instances(
r_Fr, &self.W_i, &self.U_i, &self.w_i, &self.u_i, &T, cmT,
)?;
let (W_i1, U_i1): (Witness<C1>, CommittedInstance<C1>) = NIFS::<C1, CS1, H>::prove(
r_Fr, &self.W_i, &self.U_i, &self.w_i, &self.u_i, &aux_p, &aux_v,
)?;
// folded instance output (public input, x) // folded instance output (public input, x)
// u_{i+1}.x[0] = H(i+1, z_0, z_{i+1}, U_{i+1}) // u_{i+1}.x[0] = H(i+1, z_0, z_{i+1}, U_{i+1})
@ -958,7 +960,7 @@ where
{ {
// computes T and cmT for the AugmentedFCircuit // computes T and cmT for the AugmentedFCircuit
fn compute_cmT(&self) -> Result<(Vec<C1::ScalarField>, C1), Error> { fn compute_cmT(&self) -> Result<(Vec<C1::ScalarField>, C1), Error> {
NIFS::<C1, CS1, H>::compute_cmT(
NIFS::<C1, CS1, H>::compute_aux(
&self.cs_pp, &self.cs_pp,
&self.r1cs, &self.r1cs,
&self.w_i, &self.w_i,

+ 168
- 315
folding-schemes/src/folding/nova/nifs.rs

@ -1,8 +1,12 @@
use ark_crypto_primitives::sponge::Absorb; use ark_crypto_primitives::sponge::Absorb;
use ark_ec::{CurveGroup, Group}; use ark_ec::{CurveGroup, Group};
use ark_ff::PrimeField;
use ark_std::rand::RngCore;
use ark_std::Zero; use ark_std::Zero;
use std::marker::PhantomData; use std::marker::PhantomData;
use super::circuits::ChallengeGadget;
use super::traits::NIFSTrait;
use super::{CommittedInstance, Witness}; use super::{CommittedInstance, Witness};
use crate::arith::r1cs::R1CS; use crate::arith::r1cs::R1CS;
use crate::commitment::CommitmentScheme; use crate::commitment::CommitmentScheme;
@ -19,11 +23,119 @@ pub struct NIFS, const H: bool = false
_cp: PhantomData<CS>, _cp: PhantomData<CS>,
} }
impl<C: CurveGroup, CS: CommitmentScheme<C, H>, const H: bool> NIFSTrait<C, CS, H>
for NIFS<C, CS, H>
where
<C as Group>::ScalarField: Absorb,
<C as CurveGroup>::BaseField: PrimeField,
{
type CommittedInstance = CommittedInstance<C>;
type Witness = Witness<C>;
type ProverAux = Vec<C::ScalarField>;
type VerifierAux = C;
fn new_witness(w: Vec<C::ScalarField>, e_len: usize, rng: impl RngCore) -> Self::Witness {
Witness::new::<H>(w, e_len, rng)
}
fn new_instance(
W: &Self::Witness,
params: &CS::ProverParams,
x: Vec<C::ScalarField>,
_aux: Vec<C::ScalarField>,
) -> Result<Self::CommittedInstance, Error> {
W.commit::<CS, H>(params, x)
}
fn fold_witness(
r: C::ScalarField,
W_i: &Self::Witness,
w_i: &Self::Witness,
aux: &Self::ProverAux,
) -> Result<Self::Witness, Error> {
let r2 = r * r;
let E: Vec<C::ScalarField> = vec_add(
&vec_add(&W_i.E, &vec_scalar_mul(aux, &r))?, // aux is Nova's T
&vec_scalar_mul(&w_i.E, &r2),
)?;
// use r_T=0 since we don't need hiding property for cm(T)
let rT = C::ScalarField::zero();
let rE = W_i.rE + r * rT + r2 * w_i.rE;
let W: Vec<C::ScalarField> = W_i
.W
.iter()
.zip(&w_i.W)
.map(|(a, b)| *a + (r * b))
.collect();
let rW = W_i.rW + r * w_i.rW;
Ok(Self::Witness { E, rE, W, rW })
}
fn compute_aux(
cs_prover_params: &CS::ProverParams,
r1cs: &R1CS<C::ScalarField>,
W_i: &Self::Witness,
U_i: &Self::CommittedInstance,
w_i: &Self::Witness,
u_i: &Self::CommittedInstance,
) -> Result<(Self::ProverAux, Self::VerifierAux), Error> {
let z1: Vec<C::ScalarField> = [vec![U_i.u], U_i.x.to_vec(), W_i.W.to_vec()].concat();
let z2: Vec<C::ScalarField> = [vec![u_i.u], u_i.x.to_vec(), w_i.W.to_vec()].concat();
// compute cross terms
let T = Self::compute_T(r1cs, U_i.u, u_i.u, &z1, &z2)?;
// use r_T=0 since we don't need hiding property for cm(T)
let cmT = CS::commit(cs_prover_params, &T, &C::ScalarField::zero())?;
Ok((T, cmT))
}
fn get_challenge<T: Transcript<C::ScalarField>>(
transcript: &mut T,
pp_hash: C::ScalarField, // public params hash
U_i: &Self::CommittedInstance,
u_i: &Self::CommittedInstance,
aux: &Self::VerifierAux, // cmT
) -> Vec<bool> {
ChallengeGadget::<C, Self::CommittedInstance>::get_challenge_native(
transcript,
pp_hash,
U_i,
u_i,
Some(aux),
)
}
// Notice: `prove` method is implemented at the trait level.
fn verify(
// r comes from the transcript, and is a n-bit (N_BITS_CHALLENGE) element
r: C::ScalarField,
U_i: &Self::CommittedInstance,
u_i: &Self::CommittedInstance,
cmT: &C, // VerifierAux
) -> Self::CommittedInstance {
let r2 = r * r;
let cmE = U_i.cmE + cmT.mul(r) + u_i.cmE.mul(r2);
let u = U_i.u + r * u_i.u;
let cmW = U_i.cmW + u_i.cmW.mul(r);
let x = U_i
.x
.iter()
.zip(&u_i.x)
.map(|(a, b)| *a + (r * b))
.collect::<Vec<C::ScalarField>>();
Self::CommittedInstance { cmE, u, cmW, x }
}
}
impl<C: CurveGroup, CS: CommitmentScheme<C, H>, const H: bool> NIFS<C, CS, H> impl<C: CurveGroup, CS: CommitmentScheme<C, H>, const H: bool> NIFS<C, CS, H>
where where
<C as Group>::ScalarField: Absorb, <C as Group>::ScalarField: Absorb,
<C as CurveGroup>::BaseField: PrimeField,
{ {
// compute_T: compute cross-terms T
/// compute_T: compute cross-terms T
pub fn compute_T( pub fn compute_T(
r1cs: &R1CS<C::ScalarField>, r1cs: &R1CS<C::ScalarField>,
u1: C::ScalarField, u1: C::ScalarField,
@ -49,48 +161,8 @@ where
vec_sub(&vec_sub(&vec_add(&Az1_Bz2, &Az2_Bz1)?, &u1Cz2)?, &u2Cz1) vec_sub(&vec_sub(&vec_add(&Az1_Bz2, &Az2_Bz1)?, &u1Cz2)?, &u2Cz1)
} }
pub fn fold_witness(
r: C::ScalarField,
w1: &Witness<C>,
w2: &Witness<C>,
T: &[C::ScalarField],
rT: C::ScalarField,
) -> Result<Witness<C>, Error> {
let r2 = r * r;
let E: Vec<C::ScalarField> = vec_add(
&vec_add(&w1.E, &vec_scalar_mul(T, &r))?,
&vec_scalar_mul(&w2.E, &r2),
)?;
let rE = w1.rE + r * rT + r2 * w2.rE;
let W: Vec<C::ScalarField> = w1.W.iter().zip(&w2.W).map(|(a, b)| *a + (r * b)).collect();
let rW = w1.rW + r * w2.rW;
Ok(Witness::<C> { E, rE, W, rW })
}
pub fn fold_committed_instance(
r: C::ScalarField,
ci1: &CommittedInstance<C>, // U_i
ci2: &CommittedInstance<C>, // u_i
cmT: &C,
) -> CommittedInstance<C> {
let r2 = r * r;
let cmE = ci1.cmE + cmT.mul(r) + ci2.cmE.mul(r2);
let u = ci1.u + r * ci2.u;
let cmW = ci1.cmW + ci2.cmW.mul(r);
let x = ci1
.x
.iter()
.zip(&ci2.x)
.map(|(a, b)| *a + (r * b))
.collect::<Vec<C::ScalarField>>();
CommittedInstance::<C> { cmE, u, cmW, x }
}
/// NIFS.P is the consecutive combination of compute_cmT with fold_instances
/// compute_cmT is part of the NIFS.P logic
/// In Nova, NIFS.P is the consecutive combination of compute_cmT with fold_instances,
/// ie. compute_cmT is part of the NIFS.P logic.
pub fn compute_cmT( pub fn compute_cmT(
cs_prover_params: &CS::ProverParams, cs_prover_params: &CS::ProverParams,
r1cs: &R1CS<C::ScalarField>, r1cs: &R1CS<C::ScalarField>,
@ -108,6 +180,7 @@ where
let cmT = CS::commit(cs_prover_params, &T, &C::ScalarField::zero())?; let cmT = CS::commit(cs_prover_params, &T, &C::ScalarField::zero())?;
Ok((T, cmT)) Ok((T, cmT))
} }
pub fn compute_cyclefold_cmT( pub fn compute_cyclefold_cmT(
cs_prover_params: &CS::ProverParams, cs_prover_params: &CS::ProverParams,
r1cs: &R1CS<C::ScalarField>, // R1CS over C2.Fr=C1.Fq (here C=C2) r1cs: &R1CS<C::ScalarField>, // R1CS over C2.Fr=C1.Fq (here C=C2)
@ -129,40 +202,6 @@ where
Ok((T, cmT)) Ok((T, cmT))
} }
/// fold_instances is part of the NIFS.P logic described in
/// [Nova](https://eprint.iacr.org/2021/370.pdf)'s section 4. It returns the folded Committed
/// Instances and the Witness.
pub fn fold_instances(
r: C::ScalarField,
w1: &Witness<C>,
ci1: &CommittedInstance<C>,
w2: &Witness<C>,
ci2: &CommittedInstance<C>,
T: &[C::ScalarField],
cmT: C,
) -> Result<(Witness<C>, CommittedInstance<C>), Error> {
// fold witness
// use r_T=0 since we don't need hiding property for cm(T)
let w3 = NIFS::<C, CS, H>::fold_witness(r, w1, w2, T, C::ScalarField::zero())?;
// fold committed instances
let ci3 = NIFS::<C, CS, H>::fold_committed_instance(r, ci1, ci2, &cmT);
Ok((w3, ci3))
}
/// verify implements NIFS.V logic described in [Nova](https://eprint.iacr.org/2021/370.pdf)'s
/// section 4. It returns the folded Committed Instance
pub fn verify(
// r comes from the transcript, and is a n-bit (N_BITS_CHALLENGE) element
r: C::ScalarField,
ci1: &CommittedInstance<C>,
ci2: &CommittedInstance<C>,
cmT: &C,
) -> CommittedInstance<C> {
NIFS::<C, CS, H>::fold_committed_instance(r, ci1, ci2, cmT)
}
/// Verify committed folded instance (ci) relations. Notice that this method does not open the /// Verify committed folded instance (ci) relations. Notice that this method does not open the
/// commitments, but just checks that the given committed instances (ci1, ci2) when folded /// commitments, but just checks that the given committed instances (ci1, ci2) when folded
/// result in the folded committed instance (ci3) values. /// result in the folded committed instance (ci3) values.
@ -173,7 +212,7 @@ where
ci3: &CommittedInstance<C>, ci3: &CommittedInstance<C>,
cmT: &C, cmT: &C,
) -> Result<(), Error> { ) -> Result<(), Error> {
let expected = Self::fold_committed_instance(r, ci1, ci2, cmT);
let expected = Self::verify(r, ci1, ci2, cmT);
if ci3.cmE != expected.cmE if ci3.cmE != expected.cmE
|| ci3.u != expected.u || ci3.u != expected.u
|| ci3.cmW != expected.cmW || ci3.cmW != expected.cmW
@ -202,220 +241,32 @@ where
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use super::*; use super::*;
use ark_crypto_primitives::sponge::{
poseidon::{PoseidonConfig, PoseidonSponge},
CryptographicSponge,
};
use crate::transcript::poseidon::poseidon_canonical_config;
use ark_crypto_primitives::sponge::{poseidon::PoseidonSponge, CryptographicSponge};
use ark_ff::{BigInteger, PrimeField}; use ark_ff::{BigInteger, PrimeField};
use ark_pallas::{Fr, Projective}; use ark_pallas::{Fr, Projective};
use ark_std::{ops::Mul, test_rng, UniformRand};
use ark_std::{test_rng, UniformRand};
use crate::commitment::pedersen::{Params as PedersenParams, Pedersen};
use crate::folding::nova::circuits::ChallengeGadget;
use crate::transcript::poseidon::poseidon_canonical_config;
use crate::{
arith::{
r1cs::tests::{get_test_r1cs, get_test_z},
Arith,
},
folding::traits::Dummy,
use crate::arith::{
r1cs::tests::{get_test_r1cs, get_test_z},
Arith,
}; };
use crate::commitment::pedersen::Pedersen;
use crate::folding::nova::traits::NIFSTrait;
#[allow(clippy::type_complexity)]
pub(crate) fn prepare_simple_fold_inputs<C>() -> (
PedersenParams<C>,
PoseidonConfig<C::ScalarField>,
R1CS<C::ScalarField>,
Witness<C>, // w1
CommittedInstance<C>, // ci1
Witness<C>, // w2
CommittedInstance<C>, // ci2
Witness<C>, // w3
CommittedInstance<C>, // ci3
Vec<C::ScalarField>, // T
C, // cmT
Vec<bool>, // r_bits
C::ScalarField, // r_Fr
)
where
C: CurveGroup,
<C as CurveGroup>::BaseField: PrimeField,
C::ScalarField: Absorb,
{
let r1cs = get_test_r1cs();
let z1 = get_test_z(3);
let z2 = get_test_z(4);
let (w1, x1) = r1cs.split_z(&z1);
let (w2, x2) = r1cs.split_z(&z2);
let w1 = Witness::<C>::new::<false>(w1.clone(), r1cs.A.n_rows, test_rng());
let w2 = Witness::<C>::new::<false>(w2.clone(), r1cs.A.n_rows, test_rng());
let mut rng = ark_std::test_rng();
let (pedersen_params, _) = Pedersen::<C>::setup(&mut rng, r1cs.A.n_cols).unwrap();
// compute committed instances
let ci1 = w1
.commit::<Pedersen<C>, false>(&pedersen_params, x1.clone())
.unwrap();
let ci2 = w2
.commit::<Pedersen<C>, false>(&pedersen_params, x2.clone())
.unwrap();
// NIFS.P
let (T, cmT) =
NIFS::<C, Pedersen<C>>::compute_cmT(&pedersen_params, &r1cs, &w1, &ci1, &w2, &ci2)
.unwrap();
let poseidon_config = poseidon_canonical_config::<C::ScalarField>();
let mut transcript = PoseidonSponge::<C::ScalarField>::new(&poseidon_config);
let pp_hash = C::ScalarField::from(42u32); // only for test
let r_bits = ChallengeGadget::<C>::get_challenge_native(
&mut transcript,
pp_hash,
ci1.clone(),
ci2.clone(),
cmT,
);
let r_Fr = C::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap();
let (w3, ci3) =
NIFS::<C, Pedersen<C>>::fold_instances(r_Fr, &w1, &ci1, &w2, &ci2, &T, cmT).unwrap();
(
pedersen_params,
poseidon_config,
r1cs,
w1,
ci1,
w2,
ci2,
w3,
ci3,
T,
cmT,
r_bits,
r_Fr,
)
}
// fold 2 dummy instances and check that the folded instance holds the relaxed R1CS relation
#[test] #[test]
fn test_nifs_fold_dummy() {
let r1cs = get_test_r1cs::<Fr>();
let z1 = get_test_z(3);
let (_, x1) = r1cs.split_z(&z1);
fn test_nifs_nova() {
let (W, U) = test_nifs_opt::<NIFS<Projective, Pedersen<Projective>>>();
let mut rng = ark_std::test_rng();
let (pedersen_params, _) = Pedersen::<Projective>::setup(&mut rng, r1cs.A.n_cols).unwrap();
// dummy instance, witness and public inputs zeroes
let w_dummy = Witness::<Projective>::dummy(&r1cs);
let mut u_dummy = w_dummy
.commit::<Pedersen<Projective>, false>(&pedersen_params, vec![Fr::zero(); x1.len()])
.unwrap();
u_dummy.u = Fr::zero();
let w_i = w_dummy.clone();
let u_i = u_dummy.clone();
let W_i = w_dummy.clone();
let U_i = u_dummy.clone();
r1cs.check_relation(&w_i, &u_i).unwrap();
r1cs.check_relation(&W_i, &U_i).unwrap();
let r_Fr = Fr::from(3_u32);
let (T, cmT) = NIFS::<Projective, Pedersen<Projective>>::compute_cmT(
&pedersen_params,
&r1cs,
&w_i,
&u_i,
&W_i,
&U_i,
)
.unwrap();
let (W_i1, U_i1) = NIFS::<Projective, Pedersen<Projective>>::fold_instances(
r_Fr, &w_i, &u_i, &W_i, &U_i, &T, cmT,
)
.unwrap();
r1cs.check_relation(&W_i1, &U_i1).unwrap();
}
// fold 2 instances into one
#[test]
fn test_nifs_one_fold() {
let (pedersen_params, poseidon_config, r1cs, w1, ci1, w2, ci2, w3, ci3, T, cmT, _, r) =
prepare_simple_fold_inputs();
// NIFS.V
let ci3_v = NIFS::<Projective, Pedersen<Projective>>::verify(r, &ci1, &ci2, &cmT);
assert_eq!(ci3_v, ci3);
// check that relations hold for the 2 inputted instances and the folded one
r1cs.check_relation(&w1, &ci1).unwrap();
r1cs.check_relation(&w2, &ci2).unwrap();
r1cs.check_relation(&w3, &ci3).unwrap();
// check that folded commitments from folded instance (ci) are equal to folding the
// use folded rE, rW to commit w3
let ci3_expected = w3
.commit::<Pedersen<Projective>, false>(&pedersen_params, ci3.x.clone())
.unwrap();
assert_eq!(ci3_expected.cmE, ci3.cmE);
assert_eq!(ci3_expected.cmW, ci3.cmW);
// next equalities should hold since we started from two cmE of zero-vector E's
assert_eq!(ci3.cmE, cmT.mul(r));
assert_eq!(w3.E, vec_scalar_mul(T.as_slice(), &r));
// NIFS.Verify_Folded_Instance:
NIFS::<Projective, Pedersen<Projective>>::verify_folded_instance(r, &ci1, &ci2, &ci3, &cmT)
.unwrap();
// init Prover's transcript
let mut transcript_p = PoseidonSponge::<Fr>::new(&poseidon_config);
// init Verifier's transcript
let mut transcript_v = PoseidonSponge::<Fr>::new(&poseidon_config);
// prove the ci3.cmE, ci3.cmW, cmT commitments
let cm_proofs = NIFS::<Projective, Pedersen<Projective>>::prove_commitments(
&mut transcript_p,
&pedersen_params,
&w3,
&ci3,
T,
&cmT,
)
.unwrap();
// verify the ci3.cmE, ci3.cmW, cmT commitments
assert_eq!(cm_proofs.len(), 3);
Pedersen::<Projective>::verify(
&pedersen_params,
&mut transcript_v,
&ci3.cmE,
&cm_proofs[0].clone(),
)
.unwrap();
Pedersen::<Projective>::verify(
&pedersen_params,
&mut transcript_v,
&ci3.cmW,
&cm_proofs[1].clone(),
)
.unwrap();
Pedersen::<Projective>::verify(
&pedersen_params,
&mut transcript_v,
&cmT,
&cm_proofs[2].clone(),
)
.unwrap();
// check the last folded instance relation
let r1cs = get_test_r1cs();
r1cs.check_relation(&W, &U).unwrap();
} }
#[test]
fn test_nifs_fold_loop() {
/// runs a loop using the NIFS trait, and returns the last Witness and CommittedInstance so
/// that their relation can be checked.
pub(crate) fn test_nifs_opt<N: NIFSTrait<Projective, Pedersen<Projective>>>(
) -> (N::Witness, N::CommittedInstance) {
let r1cs = get_test_r1cs(); let r1cs = get_test_r1cs();
let z = get_test_z(3); let z = get_test_z(3);
let (w, x) = r1cs.split_z(&z); let (w, x) = r1cs.split_z(&z);
@ -423,66 +274,68 @@ pub mod tests {
let mut rng = ark_std::test_rng(); let mut rng = ark_std::test_rng();
let (pedersen_params, _) = Pedersen::<Projective>::setup(&mut rng, r1cs.A.n_cols).unwrap(); let (pedersen_params, _) = Pedersen::<Projective>::setup(&mut rng, r1cs.A.n_cols).unwrap();
// prepare the running instance
let mut running_instance_w =
Witness::<Projective>::new::<false>(w.clone(), r1cs.A.n_rows, test_rng());
let mut running_committed_instance = running_instance_w
.commit::<Pedersen<Projective>, false>(&pedersen_params, x)
.unwrap();
let poseidon_config = poseidon_canonical_config::<Fr>();
let mut transcript = PoseidonSponge::<Fr>::new(&poseidon_config);
let pp_hash = Fr::rand(&mut rng);
r1cs.check_relation(&running_instance_w, &running_committed_instance)
.unwrap();
// prepare the running instance
let mut running_witness = N::new_witness(w.clone(), r1cs.A.n_rows, test_rng());
let mut running_committed_instance =
N::new_instance(&running_witness, &pedersen_params, x, vec![]).unwrap();
let num_iters = 10; let num_iters = 10;
for i in 0..num_iters { for i in 0..num_iters {
// prepare the incoming instance // prepare the incoming instance
let incoming_instance_z = get_test_z(i + 4); let incoming_instance_z = get_test_z(i + 4);
let (w, x) = r1cs.split_z(&incoming_instance_z); let (w, x) = r1cs.split_z(&incoming_instance_z);
let incoming_instance_w =
Witness::<Projective>::new::<false>(w.clone(), r1cs.A.n_rows, test_rng());
let incoming_committed_instance = incoming_instance_w
.commit::<Pedersen<Projective>, false>(&pedersen_params, x)
.unwrap();
r1cs.check_relation(&incoming_instance_w, &incoming_committed_instance)
.unwrap();
let incoming_witness = N::new_witness(w.clone(), r1cs.A.n_rows, test_rng());
let incoming_committed_instance =
N::new_instance(&incoming_witness, &pedersen_params, x, vec![]).unwrap();
let r = Fr::rand(&mut rng); // folding challenge would come from the RO
// NIFS.P
let (T, cmT) = NIFS::<Projective, Pedersen<Projective>>::compute_cmT(
let (aux_p, aux_v) = N::compute_aux(
&pedersen_params, &pedersen_params,
&r1cs, &r1cs,
&running_instance_w,
&running_witness,
&running_committed_instance, &running_committed_instance,
&incoming_instance_w,
&incoming_witness,
&incoming_committed_instance, &incoming_committed_instance,
) )
.unwrap(); .unwrap();
let (folded_w, _) = NIFS::<Projective, Pedersen<Projective>>::fold_instances(
let r_bits = N::get_challenge(
&mut transcript,
pp_hash,
&running_committed_instance,
&incoming_committed_instance,
&aux_v,
);
let r = Fr::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap();
// NIFS.P
let (folded_witness, _) = N::prove(
r, r,
&running_instance_w,
&running_witness,
&running_committed_instance, &running_committed_instance,
&incoming_instance_w,
&incoming_witness,
&incoming_committed_instance, &incoming_committed_instance,
&T,
cmT,
&aux_p,
&aux_v,
) )
.unwrap(); .unwrap();
// NIFS.V // NIFS.V
let folded_committed_instance = NIFS::<Projective, Pedersen<Projective>>::verify(
let folded_committed_instance = N::verify(
r, r,
&running_committed_instance, &running_committed_instance,
&incoming_committed_instance, &incoming_committed_instance,
&cmT,
&aux_v,
); );
r1cs.check_relation(&folded_w, &folded_committed_instance)
.unwrap();
// set running_instance for next loop iteration // set running_instance for next loop iteration
running_instance_w = folded_w;
running_witness = folded_witness;
running_committed_instance = folded_committed_instance; running_committed_instance = folded_committed_instance;
} }
(running_witness, running_committed_instance)
} }
} }

+ 269
- 0
folding-schemes/src/folding/nova/ova.rs

@ -0,0 +1,269 @@
/// This module contains the implementation the NIFSTrait for the
/// [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw) NIFS (Non-Interactive Folding Scheme) as
/// outlined in the protocol description doc:
/// <https://hackmd.io/V4838nnlRKal9ZiTHiGYzw#Construction> authored by Benedikt Bünz.
use ark_crypto_primitives::sponge::Absorb;
use ark_ec::{CurveGroup, Group};
use ark_ff::PrimeField;
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
use ark_std::fmt::Debug;
use ark_std::rand::RngCore;
use ark_std::{One, UniformRand, Zero};
use std::marker::PhantomData;
use super::{circuits::ChallengeGadget, traits::NIFSTrait};
use crate::arith::r1cs::R1CS;
use crate::commitment::CommitmentScheme;
use crate::folding::{circuits::CF1, traits::Dummy};
use crate::transcript::{AbsorbNonNative, Transcript};
use crate::utils::vec::{hadamard, mat_vec_mul, vec_scalar_mul, vec_sub};
use crate::Error;
/// A CommittedInstance in [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw) is represented by `W` or
/// `W'`. It is the result of the commitment to a vector that contains the witness `w` concatenated
/// with `t` or `e` + the public inputs `x` and a relaxation factor `u`. (Notice that in the Ova
/// document `u` is denoted as `mu`, in this implementation we use `u` so it follows the original
/// Nova notation, so code is easier to follow).
#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
pub struct CommittedInstance<C: CurveGroup> {
pub u: C::ScalarField, // in the Ova document is denoted as `mu`
pub x: Vec<C::ScalarField>,
pub cmWE: C,
}
impl<C: CurveGroup> Absorb for CommittedInstance<C>
where
C::ScalarField: Absorb,
{
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>) {
self.u.to_sponge_field_elements(dest);
self.x.to_sponge_field_elements(dest);
// We cannot call `to_native_sponge_field_elements(dest)` directly, as
// `to_native_sponge_field_elements` needs `F` to be `C::ScalarField`,
// but here `F` is a generic `PrimeField`.
self.cmWE
.to_native_sponge_field_elements_as_vec()
.to_sponge_field_elements(dest);
}
}
// #[allow(dead_code)] // Clippy flag needed for now.
/// A Witness in Ova is represented by `w`. It also contains a blinder which can or not be used
/// when committing to the witness itself.
#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
pub struct Witness<C: CurveGroup> {
pub w: Vec<C::ScalarField>,
pub rW: C::ScalarField,
}
impl<C: CurveGroup> Witness<C> {
/// Generates a new `Witness` instance from a given witness vector.
/// If `H = true`, then we assume we want to blind it at commitment time,
/// hence sampling `rW` from the randomness passed.
pub fn new<const H: bool>(w: Vec<C::ScalarField>, mut rng: impl RngCore) -> Self {
Self {
w,
rW: if H {
C::ScalarField::rand(&mut rng)
} else {
C::ScalarField::zero()
},
}
}
/// Given `x` (public inputs) and `t` or `e` (which we always concatenate in Ova) and the
/// public inputs `x`, generates a [`CommittedInstance`] as a result which will or not be
/// blinded depending on how the const generic `HC` is set up.
pub fn commit<CS: CommitmentScheme<C, HC>, const HC: bool>(
&self,
params: &CS::ProverParams,
x: Vec<C::ScalarField>,
t_or_e: Vec<C::ScalarField>,
) -> Result<CommittedInstance<C>, Error> {
let cmWE = CS::commit(params, &[self.w.clone(), t_or_e].concat(), &self.rW)?;
Ok(CommittedInstance {
u: C::ScalarField::one(),
cmWE,
x,
})
}
}
impl<C: CurveGroup> Dummy<&R1CS<CF1<C>>> for Witness<C> {
fn dummy(r1cs: &R1CS<CF1<C>>) -> Self {
Self {
w: vec![C::ScalarField::zero(); r1cs.A.n_cols - 1 - r1cs.l],
rW: C::ScalarField::zero(),
}
}
}
/// Implements the NIFS (Non-Interactive Folding Scheme) trait for Ova.
pub struct NIFS<C: CurveGroup, CS: CommitmentScheme<C, H>, const H: bool = false> {
_c: PhantomData<C>,
_cp: PhantomData<CS>,
}
impl<C: CurveGroup, CS: CommitmentScheme<C, H>, const H: bool> NIFSTrait<C, CS, H>
for NIFS<C, CS, H>
where
<C as Group>::ScalarField: Absorb,
<C as CurveGroup>::BaseField: PrimeField,
{
type CommittedInstance = CommittedInstance<C>;
type Witness = Witness<C>;
type ProverAux = ();
type VerifierAux = ();
fn new_witness(w: Vec<C::ScalarField>, _e_len: usize, rng: impl RngCore) -> Self::Witness {
Witness::new::<H>(w, rng)
}
fn new_instance(
W: &Self::Witness,
params: &CS::ProverParams,
x: Vec<C::ScalarField>,
aux: Vec<C::ScalarField>, // t_or_e
) -> Result<Self::CommittedInstance, Error> {
W.commit::<CS, H>(params, x, aux)
}
fn fold_witness(
r: C::ScalarField, // in Ova's hackmd denoted as `alpha`
W_i: &Self::Witness,
w_i: &Self::Witness,
_aux: &Self::ProverAux,
) -> Result<Self::Witness, Error> {
let w: Vec<C::ScalarField> = W_i
.w
.iter()
.zip(&w_i.w)
.map(|(a, b)| *a + (r * b))
.collect();
let rW = W_i.rW + r * w_i.rW;
Ok(Self::Witness { w, rW })
}
fn compute_aux(
_cs_prover_params: &CS::ProverParams,
_r1cs: &R1CS<C::ScalarField>,
_W_i: &Self::Witness,
_U_i: &Self::CommittedInstance,
_w_i: &Self::Witness,
_u_i: &Self::CommittedInstance,
) -> Result<(Self::ProverAux, Self::VerifierAux), Error> {
Ok(((), ()))
}
fn get_challenge<T: Transcript<C::ScalarField>>(
transcript: &mut T,
pp_hash: C::ScalarField, // public params hash
U_i: &Self::CommittedInstance,
u_i: &Self::CommittedInstance,
_aux: &Self::VerifierAux,
) -> Vec<bool> {
// reuse Nova's get_challenge method
ChallengeGadget::<C, Self::CommittedInstance>::get_challenge_native(
transcript, pp_hash, U_i, u_i, None, // empty in Ova's case
)
}
// Notice: `prove` method is implemented at the trait level.
fn verify(
// r comes from the transcript, and is a n-bit (N_BITS_CHALLENGE) element
r: C::ScalarField,
U_i: &Self::CommittedInstance,
u_i: &Self::CommittedInstance,
_aux: &Self::VerifierAux,
) -> Self::CommittedInstance {
// recall that r <==> alpha, and u <==> mu between Nova and Ova respectively
let u = U_i.u + r; // u_i.u is always 1 IN ova as we just can do sequential IVC.
let cmWE = U_i.cmWE + u_i.cmWE.mul(r);
let x = U_i
.x
.iter()
.zip(&u_i.x)
.map(|(a, b)| *a + (r * b))
.collect::<Vec<C::ScalarField>>();
Self::CommittedInstance { cmWE, u, x }
}
}
/// Computes the E parameter (error terms) for the given R1CS and the instance's z and u. This
/// method is used by the verifier to obtain E in order to check the RelaxedR1CS relation.
pub fn compute_E<C: CurveGroup>(
r1cs: &R1CS<C::ScalarField>,
z: &[C::ScalarField],
u: C::ScalarField,
) -> Result<Vec<C::ScalarField>, Error> {
let (A, B, C) = (r1cs.A.clone(), r1cs.B.clone(), r1cs.C.clone());
// this is parallelizable (for the future)
let Az = mat_vec_mul(&A, z)?;
let Bz = mat_vec_mul(&B, z)?;
let Cz = mat_vec_mul(&C, z)?;
let Az_Bz = hadamard(&Az, &Bz)?;
let uCz = vec_scalar_mul(&Cz, &u);
vec_sub(&Az_Bz, &uCz)
}
#[cfg(test)]
pub mod tests {
use super::*;
use ark_pallas::{Fr, Projective};
use crate::arith::{r1cs::tests::get_test_r1cs, Arith};
use crate::commitment::pedersen::Pedersen;
use crate::folding::nova::nifs::tests::test_nifs_opt;
// Simple auxiliary structure mainly used to help pass a witness for which we can check
// easily an R1CS relation.
// Notice that checking it requires us to have `E` as per [`Arith`] trait definition.
// But since we don't hold `E` nor `e` within the NIFS, we create this structure to pass
// `e` such that the check can be done.
#[derive(Debug, Clone)]
pub(crate) struct TestingWitness<C: CurveGroup> {
pub(crate) w: Vec<C::ScalarField>,
pub(crate) e: Vec<C::ScalarField>,
}
impl<C: CurveGroup> Arith<TestingWitness<C>, CommittedInstance<C>> for R1CS<CF1<C>> {
type Evaluation = Vec<CF1<C>>;
fn eval_relation(
&self,
w: &TestingWitness<C>,
u: &CommittedInstance<C>,
) -> Result<Self::Evaluation, Error> {
self.eval_at_z(&[&[u.u], u.x.as_slice(), &w.w].concat())
}
fn check_evaluation(
w: &TestingWitness<C>,
_u: &CommittedInstance<C>,
e: Self::Evaluation,
) -> Result<(), Error> {
(w.e == e).then_some(()).ok_or(Error::NotSatisfied)
}
}
#[test]
fn test_nifs_ova() {
let (W, U) = test_nifs_opt::<NIFS<Projective, Pedersen<Projective>>>();
// check the last folded instance relation
let r1cs = get_test_r1cs();
let z: Vec<Fr> = [&[U.u][..], &U.x, &W.w].concat();
let e = compute_E::<Projective>(&r1cs, &z, U.u).unwrap();
r1cs.check_relation(&TestingWitness::<Projective> { e, w: W.w.clone() }, &U)
.unwrap();
}
}

+ 73
- 24
folding-schemes/src/folding/nova/traits.rs

@ -1,4 +1,6 @@
use ark_crypto_primitives::sponge::Absorb;
use ark_ec::CurveGroup; use ark_ec::CurveGroup;
use ark_std::fmt::Debug;
use ark_std::{rand::RngCore, UniformRand}; use ark_std::{rand::RngCore, UniformRand};
use super::{CommittedInstance, Witness}; use super::{CommittedInstance, Witness};
@ -6,11 +8,79 @@ use crate::arith::ArithSampler;
use crate::arith::{r1cs::R1CS, Arith}; use crate::arith::{r1cs::R1CS, Arith};
use crate::commitment::CommitmentScheme; use crate::commitment::CommitmentScheme;
use crate::folding::circuits::CF1; use crate::folding::circuits::CF1;
use crate::folding::ova::{
CommittedInstance as OvaCommittedInstance, TestingWitness as OvaWitness,
};
use crate::transcript::Transcript;
use crate::Error; use crate::Error;
/// Defines the NIFS (Non-Interactive Folding Scheme) trait, initially defined in
/// [Nova](https://eprint.iacr.org/2021/370.pdf), and it's variants
/// [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw) and
/// [Mova](https://eprint.iacr.org/2024/1220.pdf).
/// `H` specifies whether the NIFS will use a blinding factor.
pub trait NIFSTrait<C: CurveGroup, CS: CommitmentScheme<C, H>, const H: bool = false> {
type CommittedInstance: Debug + Clone + Absorb;
type Witness: Debug + Clone;
type ProverAux: Debug + Clone; // Prover's aux params
type VerifierAux: Debug + Clone; // Verifier's aux params
fn new_witness(w: Vec<C::ScalarField>, e_len: usize, rng: impl RngCore) -> Self::Witness;
fn new_instance(
w: &Self::Witness,
params: &CS::ProverParams,
x: Vec<C::ScalarField>,
aux: Vec<C::ScalarField>, // t_or_e in Ova, empty for Nova
) -> Result<Self::CommittedInstance, Error>;
fn fold_witness(
r: C::ScalarField,
W: &Self::Witness, // running witness
w: &Self::Witness, // incoming witness
aux: &Self::ProverAux,
) -> Result<Self::Witness, Error>;
/// computes the auxiliary parameters, eg. in Nova: (T, cmT), in Ova: T
fn compute_aux(
cs_prover_params: &CS::ProverParams,
r1cs: &R1CS<C::ScalarField>,
W_i: &Self::Witness,
U_i: &Self::CommittedInstance,
w_i: &Self::Witness,
u_i: &Self::CommittedInstance,
) -> Result<(Self::ProverAux, Self::VerifierAux), Error>;
fn get_challenge<T: Transcript<C::ScalarField>>(
transcript: &mut T,
pp_hash: C::ScalarField, // public params hash
U_i: &Self::CommittedInstance,
u_i: &Self::CommittedInstance,
aux: &Self::VerifierAux, // ie. in Nova wouild be cmT, in Ova it's empty
) -> Vec<bool>;
/// NIFS.P. Notice that this method is implemented at the trait level, and depends on the other
/// two methods `fold_witness` and `verify`.
fn prove(
r: C::ScalarField,
W_i: &Self::Witness, // running witness
U_i: &Self::CommittedInstance, // running committed instance
w_i: &Self::Witness, // incoming witness
u_i: &Self::CommittedInstance, // incoming committed instance
aux_p: &Self::ProverAux,
aux_v: &Self::VerifierAux,
) -> Result<(Self::Witness, Self::CommittedInstance), Error> {
let w = Self::fold_witness(r, W_i, w_i, aux_p)?;
let ci = Self::verify(r, U_i, u_i, aux_v);
Ok((w, ci))
}
/// NIFS.V
fn verify(
// r comes from the transcript, and is a n-bit (N_BITS_CHALLENGE) element
r: C::ScalarField,
U_i: &Self::CommittedInstance,
u_i: &Self::CommittedInstance,
aux: &Self::VerifierAux,
) -> Self::CommittedInstance;
}
/// Implements `Arith` for R1CS, where the witness is of type [`Witness`], and /// Implements `Arith` for R1CS, where the witness is of type [`Witness`], and
/// the committed instance is of type [`CommittedInstance`]. /// the committed instance is of type [`CommittedInstance`].
/// ///
@ -98,24 +168,3 @@ impl ArithSampler, CommittedInstance> for R1CS
Ok((witness, cm_witness)) Ok((witness, cm_witness))
} }
} }
// Sadly, this forces duplication of code. We can try to abstract with more traits if needed..
impl<C: CurveGroup> Arith<OvaWitness<C>, OvaCommittedInstance<C>> for R1CS<CF1<C>> {
type Evaluation = Vec<CF1<C>>;
fn eval_relation(
&self,
w: &OvaWitness<C>,
u: &OvaCommittedInstance<C>,
) -> Result<Self::Evaluation, Error> {
self.eval_at_z(&[&[u.mu], u.x.as_slice(), &w.w].concat())
}
fn check_evaluation(
w: &OvaWitness<C>,
_u: &OvaCommittedInstance<C>,
e: Self::Evaluation,
) -> Result<(), Error> {
(w.e == e).then_some(()).ok_or(Error::NotSatisfied)
}
}

+ 16
- 26
folding-schemes/src/folding/nova/zk.rs

@ -51,7 +51,9 @@ use ark_r1cs_std::{
use crate::{commitment::CommitmentScheme, folding::circuits::CF2, frontend::FCircuit, Error}; use crate::{commitment::CommitmentScheme, folding::circuits::CF2, frontend::FCircuit, Error};
use super::{circuits::ChallengeGadget, nifs::NIFS, CommittedInstance, Nova, Witness};
use super::{
circuits::ChallengeGadget, nifs::NIFS, traits::NIFSTrait, CommittedInstance, Nova, Witness,
};
// We use the same definition of a folding proof as in https://eprint.iacr.org/2023/969.pdf // We use the same definition of a folding proof as in https://eprint.iacr.org/2023/969.pdf
// It consists in the commitment to the T term // It consists in the commitment to the T term
@ -83,7 +85,13 @@ where
u_i: CommittedInstance<C1>, u_i: CommittedInstance<C1>,
cmT: C1, cmT: C1,
) -> Result<C1::ScalarField, Error> { ) -> Result<C1::ScalarField, Error> {
let r_bits = ChallengeGadget::<C1>::get_challenge_native(sponge, pp_hash, U_i, u_i, cmT);
let r_bits = ChallengeGadget::<C1, CommittedInstance<C1>>::get_challenge_native(
sponge,
pp_hash,
&U_i,
&u_i,
Some(&cmT),
);
C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)).ok_or(Error::OutOfBounds) C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits)).ok_or(Error::OutOfBounds)
} }
@ -134,9 +142,8 @@ where
)?; )?;
// c. Compute fold // c. Compute fold
let (W_f, U_f) = NIFS::<C1, CS1, true>::fold_instances(
r, &nova.w_i, &nova.u_i, &nova.W_i, &nova.U_i, &T, cmT,
)?;
let (W_f, U_f) =
NIFS::<C1, CS1, true>::prove(r, &nova.w_i, &nova.u_i, &nova.W_i, &nova.U_i, &T, &cmT)?;
// d. Store folding proof // d. Store folding proof
let pi = FoldingProof { cmT }; let pi = FoldingProof { cmT };
@ -161,15 +168,8 @@ where
)?; )?;
// c. Compute fold // c. Compute fold
let (W_i_prime, _) = NIFS::<C1, CS1, true>::fold_instances(
r_2,
&W_f,
&U_f,
&W_r,
&U_r,
&T_i_prime,
cmT_i_prime,
)?;
let (W_i_prime, _) =
NIFS::<C1, CS1, true>::prove(r_2, &W_f, &U_f, &W_r, &U_r, &T_i_prime, &cmT_i_prime)?;
// d. Store folding proof // d. Store folding proof
let pi_prime = FoldingProof { cmT: cmT_i_prime }; let pi_prime = FoldingProof { cmT: cmT_i_prime };
@ -255,12 +255,7 @@ where
)?; )?;
// b. Get the U_f instance // b. Get the U_f instance
let U_f = NIFS::<C1, CS1, true>::fold_committed_instance(
r,
&proof.u_i,
&proof.U_i,
&proof.pi.cmT,
);
let U_f = NIFS::<C1, CS1, true>::verify(r, &proof.u_i, &proof.U_i, &proof.pi.cmT);
// 4. Obtain the U^{\prime}_i folded instance // 4. Obtain the U^{\prime}_i folded instance
// a. Compute folding challenge // a. Compute folding challenge
@ -273,12 +268,7 @@ where
)?; )?;
// b. Compute fold // b. Compute fold
let U_i_prime = NIFS::<C1, CS1, true>::fold_committed_instance(
r_2,
&U_f,
&proof.U_r,
&proof.pi_prime.cmT,
);
let U_i_prime = NIFS::<C1, CS1, true>::verify(r_2, &U_f, &proof.U_r, &proof.pi_prime.cmT);
// 5. Check that W^{\prime}_i is a satisfying witness // 5. Check that W^{\prime}_i is a satisfying witness
r1cs.check_relation(&proof.W_i_prime, &U_i_prime)?; r1cs.check_relation(&proof.W_i_prime, &U_i_prime)?;

+ 0
- 157
folding-schemes/src/folding/ova/mod.rs

@ -1,157 +0,0 @@
use std::marker::PhantomData;
/// Implements the scheme described in [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw?view).
/// Ova is a slight modification to Nova that shaves off 1 group operation and a few hashes.
/// The key idea is that we can commit to $T$ and $W$ in one commitment.
///
/// This slightly breaks the abstraction between the IVC scheme and the accumulation scheme.
/// We assume that the accumulation prover receives $\vec{w}$ as input which proves the current step
/// of the computation and that the *previous* accumulation was performed correctly.
/// Note that $\vec{w}$ can be generated without being aware of $\vec{t}$ or $\alpha$.
/// Alternatively the accumulation prover can receive the commitment to $\vec{w}$ as input
/// and add to it the commitment to $\vec{t}$.
///
/// This yields a commitment to the concatenated vector $\vec{w}||\vec{t}$.
/// This works because both $W$ and $T$ are multiplied by the same challenge $\alpha$ in Nova.
use ark_crypto_primitives::sponge::Absorb;
use ark_ec::{CurveGroup, Group};
use ark_ff::PrimeField;
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
use ark_std::fmt::Debug;
use ark_std::rand::RngCore;
use ark_std::{One, UniformRand, Zero};
use crate::arith::r1cs::R1CS;
use crate::commitment::CommitmentScheme;
use crate::constants::NOVA_N_BITS_RO;
use crate::folding::{circuits::CF1, traits::Dummy};
use crate::transcript::{AbsorbNonNative, Transcript};
use crate::Error;
pub mod nifs;
/// A CommittedInstance in [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw?view) is represented by `W` or `W'`.
/// It is the result of the commitment to a vector that contains the witness `w` concatenated
/// with `t` or `e` + the public inputs `x` and a relaxation factor `mu`.
#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
pub struct CommittedInstance<C: CurveGroup> {
pub mu: C::ScalarField,
pub x: Vec<C::ScalarField>,
pub cmWE: C,
}
impl<C: CurveGroup> Absorb for CommittedInstance<C>
where
C::ScalarField: Absorb,
{
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>) {
self.mu.to_sponge_field_elements(dest);
self.x.to_sponge_field_elements(dest);
// We cannot call `to_native_sponge_field_elements(dest)` directly, as
// `to_native_sponge_field_elements` needs `F` to be `C::ScalarField`,
// but here `F` is a generic `PrimeField`.
self.cmWE
.to_native_sponge_field_elements_as_vec()
.to_sponge_field_elements(dest);
}
}
// Clippy flag needed for now.
#[allow(dead_code)]
/// A Witness in [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw?view) is represented by `w`.
/// It also contains a blinder which can or not be used when committing to the witness itself.
#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
pub struct Witness<C: CurveGroup> {
pub w: Vec<C::ScalarField>,
pub rW: C::ScalarField,
}
impl<C: CurveGroup> Witness<C> {
/// Generates a new `Witness` instance from a given witness vector.
/// If `H = true`, then we assume we want to blind it at commitment time,
/// hence sampling `rW` from the randomness passed.
pub fn new<const H: bool>(w: Vec<C::ScalarField>, mut rng: impl RngCore) -> Self {
Self {
w,
rW: if H {
C::ScalarField::rand(&mut rng)
} else {
C::ScalarField::zero()
},
}
}
/// Computes the `W` or `W'` commitment (The accumulated-instance W' or the incoming-instance W)
/// as specified in Ova. See: <https://hackmd.io/V4838nnlRKal9ZiTHiGYzw?view#Construction>.
///
/// This is the result of concatenating the accumulated-instance `w` vector with
/// `e` or `t`.
/// Generates a [`CommittedInstance`] as a result which will or not be blinded depending on how the
/// const generic `HC` is set up.
///
/// This is the exact trick that allows Ova to save up 1 commitment with respect to Nova.
/// At the cost of loosing the PCD property and only maintaining the IVC one.
pub fn commit<CS: CommitmentScheme<C, HC>, const HC: bool>(
&self,
params: &CS::ProverParams,
t_or_e: Vec<C::ScalarField>,
x: Vec<C::ScalarField>,
) -> Result<CommittedInstance<C>, Error> {
let cmWE = CS::commit(params, &[self.w.clone(), t_or_e].concat(), &self.rW)?;
Ok(CommittedInstance {
mu: C::ScalarField::one(),
cmWE,
x,
})
}
}
impl<C: CurveGroup> Dummy<&R1CS<CF1<C>>> for Witness<C> {
fn dummy(r1cs: &R1CS<CF1<C>>) -> Self {
Self {
w: vec![C::ScalarField::zero(); r1cs.A.n_cols - 1 - r1cs.l],
rW: C::ScalarField::zero(),
}
}
}
pub struct ChallengeGadget<C: CurveGroup> {
_c: PhantomData<C>,
}
impl<C: CurveGroup> ChallengeGadget<C>
where
C: CurveGroup,
<C as CurveGroup>::BaseField: PrimeField,
<C as Group>::ScalarField: Absorb,
{
pub fn get_challenge_native<T: Transcript<C::ScalarField>>(
transcript: &mut T,
pp_hash: C::ScalarField, // public params hash
// Running instance
U_i: CommittedInstance<C>,
// Incoming instance
u_i: CommittedInstance<C>,
) -> Vec<bool> {
// NOTICE: This isn't following the order of the HackMD.
// As long as we match it. We should not have any issues.
transcript.absorb(&pp_hash);
transcript.absorb(&U_i);
transcript.absorb(&u_i);
transcript.squeeze_bits(NOVA_N_BITS_RO)
}
}
// Simple auxiliary structure mainly used to help pass a witness for which we can check
// easily an R1CS relation.
// Notice that checking it requires us to have `E` as per [`Arith`] trait definition.
// But since we don't hold `E` nor `e` within the NIFS, we create this structure to pass
// `e` such that the check can be done.
#[derive(Debug, Clone)]
pub(crate) struct TestingWitness<C: CurveGroup> {
pub(crate) w: Vec<C::ScalarField>,
pub(crate) e: Vec<C::ScalarField>,
}

+ 0
- 482
folding-schemes/src/folding/ova/nifs.rs

@ -1,482 +0,0 @@
/// This module contains the implementation of the Ova scheme NIFS (Non-Interactive Folding Scheme) as
/// outlined in the protocol description doc: <https://hackmd.io/V4838nnlRKal9ZiTHiGYzw?both#Construction>
/// authored by Benedikt Bünz.
use ark_crypto_primitives::sponge::Absorb;
use ark_ec::{CurveGroup, Group};
use ark_std::One;
use std::marker::PhantomData;
use super::{CommittedInstance, Witness};
use crate::arith::r1cs::R1CS;
use crate::commitment::CommitmentScheme;
use crate::transcript::Transcript;
use crate::utils::vec::{hadamard, mat_vec_mul, vec_scalar_mul, vec_sub};
use crate::Error;
/// Implements all the operations executed by the Non-Interactive Folding Scheme described in the protocol
/// spec by Bünz in the [original HackMD](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw?both#Construction).
/// `H` specifies whether the NIFS will use a blinding factor
pub struct NIFS<C: CurveGroup, CS: CommitmentScheme<C, H>, const H: bool = false> {
_c: PhantomData<C>,
_cp: PhantomData<CS>,
}
impl<C: CurveGroup, CS: CommitmentScheme<C, H>, const H: bool> NIFS<C, CS, H>
where
<C as Group>::ScalarField: Absorb,
{
/// Computes the T parameter (Cross Terms) as in Nova.
/// The wrapper is only in place to facilitate the calling as we need
/// to reconstruct the `z`s being folded in order to compute T.
pub fn compute_T(
r1cs: &R1CS<C::ScalarField>,
w_i: &Witness<C>,
x_i: &[C::ScalarField],
W_i: &Witness<C>,
X_i: &[C::ScalarField],
mu: C::ScalarField,
) -> Result<Vec<C::ScalarField>, Error> {
crate::folding::nova::nifs::NIFS::<C, CS, H>::compute_T(
r1cs,
C::ScalarField::one(),
mu,
&[vec![C::ScalarField::one()], x_i.to_vec(), w_i.w.to_vec()].concat(),
&[vec![mu], X_i.to_vec(), W_i.w.to_vec()].concat(),
)
}
/// Computes the E parameter (Error Terms) as in Nova.
/// The wrapper is only in place to facilitate the calling as we need
/// to reconstruct the `z`s being folded in order to compute E.
///
/// Not only that, but notice that the incoming-instance `mu` parameter is always
/// equal to 1. Therefore, we can save the some computations.
pub fn compute_E(
r1cs: &R1CS<C::ScalarField>,
W_i: &Witness<C>,
X_i: &[C::ScalarField],
mu: C::ScalarField,
) -> Result<Vec<C::ScalarField>, Error> {
let (A, B, C) = (r1cs.A.clone(), r1cs.B.clone(), r1cs.C.clone());
let z_prime = [&[mu], X_i, &W_i.w].concat();
// this is parallelizable (for the future)
let Az_prime = mat_vec_mul(&A, &z_prime)?;
let Bz_prime = mat_vec_mul(&B, &z_prime)?;
let Cz_prime = mat_vec_mul(&C, &z_prime)?;
let Az_prime_Bz_prime = hadamard(&Az_prime, &Bz_prime)?;
let muCz_prime = vec_scalar_mul(&Cz_prime, &mu);
vec_sub(&Az_prime_Bz_prime, &muCz_prime)
}
/// Folds 2 [`CommittedInstance`]s returning a freshly folded one as is specified
/// in: <https://hackmd.io/V4838nnlRKal9ZiTHiGYzw?both#Construction>.
/// Here, alpha is a randomness sampled from a [`Transcript`].
pub fn fold_committed_instance(
alpha: C::ScalarField,
// This is W (incoming)
u_i: &CommittedInstance<C>,
// This is W' (running)
U_i: &CommittedInstance<C>,
) -> CommittedInstance<C> {
let mu = U_i.mu + alpha; // u_i.mu **IS ALWAYS 1 in OVA** as we just can do sequential IVC.
let cmWE = U_i.cmWE + u_i.cmWE.mul(alpha);
let x = U_i
.x
.iter()
.zip(&u_i.x)
.map(|(a, b)| *a + (alpha * b))
.collect::<Vec<C::ScalarField>>();
CommittedInstance::<C> { cmWE, mu, x }
}
/// Folds 2 [`Witness`]s returning a freshly folded one as is specified
/// in: <https://hackmd.io/V4838nnlRKal9ZiTHiGYzw?both#Construction>.
/// Here, alpha is a randomness sampled from a [`Transcript`].
pub fn fold_witness(
alpha: C::ScalarField,
// incoming instance
w_i: &Witness<C>,
// running instance
W_i: &Witness<C>,
) -> Result<Witness<C>, Error> {
let w: Vec<C::ScalarField> = W_i
.w
.iter()
.zip(&w_i.w)
.map(|(a, b)| *a + (alpha * b))
.collect();
let rW = W_i.rW + alpha * w_i.rW;
Ok(Witness::<C> { w, rW })
}
/// fold_instances is part of the NIFS.P logic described in
/// [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw?both#Construction)'s Construction section.
/// It returns the folded [`CommittedInstance`] and [`Witness`].
pub fn fold_instances(
r: C::ScalarField,
// incoming instance
w_i: &Witness<C>,
u_i: &CommittedInstance<C>,
// running instance
W_i: &Witness<C>,
U_i: &CommittedInstance<C>,
) -> Result<(Witness<C>, CommittedInstance<C>), Error> {
// fold witness
let w3 = NIFS::<C, CS, H>::fold_witness(r, w_i, W_i)?;
// fold committed instances
let ci3 = NIFS::<C, CS, H>::fold_committed_instance(r, u_i, U_i);
Ok((w3, ci3))
}
/// Implements NIFS.V (accumulation verifier) logic described in [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw?both#Construction)'s
/// Construction section.
/// It returns the folded [`CommittedInstance`].
pub fn verify(
// r comes from the transcript, and is a n-bit (N_BITS_CHALLENGE) element
alpha: C::ScalarField,
// incoming instance
u_i: &CommittedInstance<C>,
// running instance
U_i: &CommittedInstance<C>,
) -> CommittedInstance<C> {
NIFS::<C, CS, H>::fold_committed_instance(alpha, u_i, U_i)
}
#[cfg(test)]
/// Verify committed folded instance (ui) relations. Notice that this method does not open the
/// commitments, but just checks that the given committed instances (ui1, ui2) when folded
/// result in the folded committed instance (ui3) values.
pub(crate) fn verify_folded_instance(
r: C::ScalarField,
// incoming instance
u_i: &CommittedInstance<C>,
// running instance
U_i: &CommittedInstance<C>,
// folded instance
folded_instance: &CommittedInstance<C>,
) -> Result<(), Error> {
let expected = Self::fold_committed_instance(r, u_i, U_i);
if folded_instance.mu != expected.mu
|| folded_instance.cmWE != expected.cmWE
|| folded_instance.x != expected.x
{
return Err(Error::NotSatisfied);
}
Ok(())
}
/// Generates a [`CS::Proof`] for the given [`CommittedInstance`] and [`Witness`] pair.
pub fn prove_commitment(
r1cs: &R1CS<C::ScalarField>,
tr: &mut impl Transcript<C::ScalarField>,
cs_prover_params: &CS::ProverParams,
w: &Witness<C>,
ci: &CommittedInstance<C>,
) -> Result<CS::Proof, Error> {
let e = NIFS::<C, CS, H>::compute_E(r1cs, w, &ci.x, ci.mu).unwrap();
let w_concat_e: Vec<C::ScalarField> = [w.w.clone(), e].concat();
CS::prove(cs_prover_params, tr, &ci.cmWE, &w_concat_e, &w.rW, None)
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::folding::ova::ChallengeGadget;
use crate::transcript::poseidon::poseidon_canonical_config;
use crate::{
arith::{
r1cs::tests::{get_test_r1cs, get_test_z},
Arith,
},
folding::traits::Dummy,
};
use crate::{
commitment::pedersen::{Params as PedersenParams, Pedersen},
folding::ova::TestingWitness,
};
use ark_crypto_primitives::sponge::{
poseidon::{PoseidonConfig, PoseidonSponge},
CryptographicSponge,
};
use ark_ff::{BigInteger, PrimeField};
use ark_pallas::{Fr, Projective};
use ark_std::{test_rng, UniformRand, Zero};
fn compute_E_check_relation<C: CurveGroup, CS: CommitmentScheme<C, H>, const H: bool>(
r1cs: &R1CS<C::ScalarField>,
w: &Witness<C>,
u: &CommittedInstance<C>,
) where
<C as Group>::ScalarField: Absorb,
{
let e = NIFS::<C, CS, H>::compute_E(r1cs, w, &u.x, u.mu).unwrap();
r1cs.check_relation(&TestingWitness::<C> { e, w: w.w.clone() }, u)
.unwrap();
}
#[allow(clippy::type_complexity)]
pub(crate) fn prepare_simple_fold_inputs<C>() -> (
PedersenParams<C>,
PoseidonConfig<C::ScalarField>,
R1CS<C::ScalarField>,
Witness<C>, // w
CommittedInstance<C>, // u
Witness<C>, // W
CommittedInstance<C>, // U
Witness<C>, // w_fold
CommittedInstance<C>, // u_fold
Vec<bool>, // r_bits
C::ScalarField, // r_Fr
)
where
C: CurveGroup,
<C as CurveGroup>::BaseField: PrimeField,
C::ScalarField: Absorb,
{
// Index 1 represents the incoming instance
// Index 2 represents the accumulated instance
let r1cs = get_test_r1cs();
let z1 = get_test_z(3);
let z2 = get_test_z(4);
let (w1, x1) = r1cs.split_z(&z1);
let (w2, x2) = r1cs.split_z(&z2);
let w = Witness::<C>::new::<false>(w1.clone(), test_rng());
let W: Witness<C> = Witness::<C>::new::<false>(w2.clone(), test_rng());
let mut rng = ark_std::test_rng();
let (pedersen_params, _) = Pedersen::<C>::setup(&mut rng, r1cs.A.n_cols).unwrap();
// In order to be able to compute the committed instances, we need to compute `t` and `e`
// compute t
let t = NIFS::<C, Pedersen<C>>::compute_T(&r1cs, &w, &x1, &W, &x2, C::ScalarField::one())
.unwrap();
// compute e (mu is 1 although is the running instance as we are "crafting it").
let e = NIFS::<C, Pedersen<C>>::compute_E(&r1cs, &W, &x2, C::ScalarField::one()).unwrap();
// compute committed instances
// Incoming-instance
let u = w
.commit::<Pedersen<C>, false>(&pedersen_params, t, x1.clone())
.unwrap();
// Running-instance
let U = W
.commit::<Pedersen<C>, false>(&pedersen_params, e, x2.clone())
.unwrap();
let poseidon_config = poseidon_canonical_config::<C::ScalarField>();
let mut transcript = PoseidonSponge::<C::ScalarField>::new(&poseidon_config);
let pp_hash = C::ScalarField::from(42u32); // only for test
let alpha_bits = ChallengeGadget::<C>::get_challenge_native(
&mut transcript,
pp_hash,
u.clone(),
U.clone(),
);
let alpha_Fr = C::ScalarField::from_bigint(BigInteger::from_bits_le(&alpha_bits)).unwrap();
let (w_fold, u_fold) =
NIFS::<C, Pedersen<C>, false>::fold_instances(alpha_Fr, &w, &u, &W, &U).unwrap();
// Check correctness of the R1CS relation of the folded instance.
compute_E_check_relation::<C, Pedersen<C>, false>(&r1cs, &w_fold, &u_fold);
(
pedersen_params,
poseidon_config,
r1cs,
w,
u,
W,
U,
w_fold,
u_fold,
alpha_bits,
alpha_Fr,
)
}
// fold 2 dummy instances and check that the folded instance holds the relaxed R1CS relation
#[test]
fn test_nifs_fold_dummy() {
let mut rng = ark_std::test_rng();
let r1cs = get_test_r1cs::<Fr>();
let w_dummy = Witness::<Projective>::dummy(&r1cs);
let (pedersen_params, _) = Pedersen::<Projective>::setup(&mut rng, r1cs.A.n_cols).unwrap();
// In order to be able to compute the committed instances, we need to compute `t` and `e`
// compute t
let t = NIFS::<Projective, Pedersen<Projective>>::compute_T(
&r1cs,
&w_dummy,
&[Fr::zero()],
&w_dummy,
&[Fr::zero()],
Fr::one(),
)
.unwrap();
// compute e
let e = NIFS::<Projective, Pedersen<Projective>>::compute_E(
&r1cs,
&w_dummy,
&[Fr::zero()],
Fr::one(),
)
.unwrap();
// dummy incoming instance, witness and public inputs
let u_dummy = w_dummy
.commit::<Pedersen<Projective>, false>(&pedersen_params, t, vec![Fr::zero()])
.unwrap();
// dummy accumulated instance, witness and public inputs
let U_dummy = w_dummy
.commit::<Pedersen<Projective>, false>(&pedersen_params, e.clone(), vec![Fr::zero()])
.unwrap();
let w_i = w_dummy.clone();
let u_i = u_dummy.clone();
let W_i = w_dummy.clone();
let U_i = U_dummy.clone();
// Check correctness of the R1CS relations of both instances.
compute_E_check_relation::<Projective, Pedersen<Projective>, false>(&r1cs, &w_i, &u_i);
compute_E_check_relation::<Projective, Pedersen<Projective>, false>(&r1cs, &W_i, &U_i);
// NIFS.P
let r_Fr = Fr::from(3_u32);
let (w_fold, u_fold) =
NIFS::<Projective, Pedersen<Projective>>::fold_instances(r_Fr, &w_i, &u_i, &W_i, &U_i)
.unwrap();
// Check correctness of the R1CS relation of both instances.
compute_E_check_relation::<Projective, Pedersen<Projective>, false>(
&r1cs, &w_fold, &u_fold,
);
}
// fold 2 instances into one
#[test]
fn test_nifs_one_fold() {
let (pedersen_params, poseidon_config, r1cs, w, u, W, U, w_fold, u_fold, _, r) =
prepare_simple_fold_inputs();
// NIFS.V
let u_fold_v = NIFS::<Projective, Pedersen<Projective>>::verify(r, &u, &U);
assert_eq!(u_fold_v, u_fold);
// Check that relations hold for the 2 inputted instances and the folded one
compute_E_check_relation::<Projective, Pedersen<Projective>, false>(&r1cs, &w, &u);
compute_E_check_relation::<Projective, Pedersen<Projective>, false>(&r1cs, &W, &U);
compute_E_check_relation::<Projective, Pedersen<Projective>, false>(
&r1cs, &w_fold, &u_fold,
);
// check that folded commitments from folded instance (u) are equal to folding the
// use folded rW to commit w_fold
let e_fold = NIFS::<Projective, Pedersen<Projective>>::compute_E(
&r1cs, &w_fold, &u_fold.x, u_fold.mu,
)
.unwrap();
let mut u_fold_expected = w_fold
.commit::<Pedersen<Projective>, false>(&pedersen_params, e_fold, u_fold.x.clone())
.unwrap();
u_fold_expected.mu = u_fold.mu;
assert_eq!(u_fold_expected.cmWE, u_fold.cmWE);
// NIFS.Verify_Folded_Instance:
NIFS::<Projective, Pedersen<Projective>>::verify_folded_instance(r, &u, &U, &u_fold)
.unwrap();
// init Prover's transcript
let mut transcript_p = PoseidonSponge::<Fr>::new(&poseidon_config);
// init Verifier's transcript
let mut transcript_v = PoseidonSponge::<Fr>::new(&poseidon_config);
// prove the u_fold.cmWE
let cm_proof = NIFS::<Projective, Pedersen<Projective>>::prove_commitment(
&r1cs,
&mut transcript_p,
&pedersen_params,
&w_fold,
&u_fold,
)
.unwrap();
// verify the u_fold.cmWE.
Pedersen::<Projective>::verify(
&pedersen_params,
&mut transcript_v,
&u_fold.cmWE,
&cm_proof,
)
.unwrap();
}
#[test]
fn test_nifs_fold_loop() {
let r1cs = get_test_r1cs();
let z = get_test_z(3);
let (w, x) = r1cs.split_z(&z);
let mut rng = ark_std::test_rng();
let (pedersen_params, _) = Pedersen::<Projective>::setup(&mut rng, r1cs.A.n_cols).unwrap();
// prepare the running instance
let mut W = Witness::<Projective>::new::<false>(w.clone(), &mut rng);
// Compute e
let e =
NIFS::<Projective, Pedersen<Projective>>::compute_E(&r1cs, &W, &x, Fr::one()).unwrap();
// Compute running `CommittedInstance`.
let mut U = W
.commit::<Pedersen<Projective>, false>(&pedersen_params, e, x)
.unwrap();
let num_iters = 10;
for i in 0..num_iters {
// prepare the incoming instance
let incoming_instance_z = get_test_z(i + 4);
let (w, x) = r1cs.split_z(&incoming_instance_z);
let w = Witness::<Projective>::new::<false>(w, test_rng());
let t =
NIFS::<Projective, Pedersen<Projective>>::compute_T(&r1cs, &w, &U.x, &w, &x, U.mu)
.unwrap();
let u = w
.commit::<Pedersen<Projective>, false>(&pedersen_params, t, x)
.unwrap();
// Check incoming instance is Ok.
compute_E_check_relation::<Projective, Pedersen<Projective>, false>(&r1cs, &w, &u);
// Generate "transcript randomness"
let alpha = Fr::rand(&mut rng); // folding challenge would come from the RO
// NIFS.P
let (w_folded, _) =
NIFS::<Projective, Pedersen<Projective>>::fold_instances(alpha, &w, &u, &W, &u)
.unwrap();
// NIFS.V
let u_folded = NIFS::<Projective, Pedersen<Projective>>::verify(alpha, &u, &U);
compute_E_check_relation::<Projective, Pedersen<Projective>, false>(
&r1cs, &w_folded, &u_folded,
);
// set running_instance for next loop iteration
W = w_folded;
U = u_folded;
}
}
}

Loading…
Cancel
Save