Add solidity groth16, kzg10 and final decider verifiers in a dedicated workspace (#70)

* change: Refactor structure into workspace

* chore: Add empty readme

* change: Transform repo into workspace

* add: Create folding-verifier-solidity crate

* add: Include askama.toml for `sol` extension escaper

* add: Jordi's old Groth16 verifier .sol template and adapt it

* tmp: create simple template struct to test

* Update FoldingSchemes trait, fit Nova+CycleFold

- update lib.rs's `FoldingScheme` trait interface
- fit Nova+CycleFold into the `FoldingScheme` trait
- refactor `src/nova/*`

* chore: add serialization assets for testing

Now we include an `assets` folder with a serialized proof & vk for tests

* Add `examples` dir, with Nova's `FoldingScheme` example

* polishing

* expose poseidon_test_config outside tests

* change: Refactor structure into workspace

* chore: Add empty readme

* change: Transform repo into workspace

* add: Create folding-verifier-solidity crate

* add: Include askama.toml for `sol` extension escaper

* add: Jordi's old Groth16 verifier .sol template and adapt it

* tmp: create simple template struct to test

* feat: templating kzg working

* chore: add emv and revm

* feat: start evm file

* chore: add ark-poly-commit

* chore: move `commitment` to `folding-schemes`

* chore: update `.gitignore` to ignore generated contracts

* chore: update template with bn254 lib on it (avoids import), update for loop to account for whitespaces

* refactor: update template with no lib

* feat: add evm deploy code, compile and create kzg verifier

* chore: update `Cargo.toml` to have `folding-schemes` available with verifiers

* feat: start kzg prove and verify with sol

* chore: compute crs from kzg prover

* feat: evm kzg verification passing

* tmp

* change: Swap order of G2 coordinates within the template

* Update way to serialize proof with correct order

* chore: update `Cargo.toml`

* chore: add revm

* chore: add `save_solidity`

* refactor: verifiers in dedicated mod

* refactor: have dedicated `utils` module

* chore: expose modules

* chore: update verifier for kzg

* chore: rename templates

* fix: look for binary using also name of contract

* refactor: generate groth16 proof for sha256 pre-image, generate groth16 template with verifying key

* chore: template renaming

* fix: switch circuit for circuit that simply adds

* feat: generates test data on the fly

* feat: update to latest groth16 verifier

* refactor: rename folder, update `.gitignore`

* chore: update `Cargo.toml`

* chore: update templates extension to indicate that they are templates

* chore: rename templates, both files and structs

* fix: template inheritance working

* feat: template spdx and pragma statements

* feat: decider verifier compiles, update test for kzg10 and groth16 templates

* feat: parameterize which size of the crs should be stored on the contract

* chore: add comment on how the groth16 and kzg10 proofs will be linked together

* chore: cargo clippy run

* chore: cargo clippy tests

* chore: cargo fmt

* refactor: remove unused lifetime parameter

* chore: end merge

* chore: move examples to `folding-schemes` workspace

* get latest main changes

* fix: temp fix clippy warnings, will remove lints once not used in tests only

* fix: cargo clippy lint added on `code_size`

* fix: update path to test circuit and add step for installing solc

* chore: remove `save_solidity` steps

* fix: the borrowed expression implements the required traits

* chore: update `Cargo.toml`

* chore: remove extra `[patch.crates-io]`

* fix: update to patch at the workspace level and add comment explaining this

* refactor: correct `staticcall` with valid input/output sizes and change return syntax for pairing

* refactor: expose modules and remove `dead_code` calls

* chore: update `README.md`, add additional comments on `kzg10` template and update `groth16` template comments

* chore: be clearer on attributions on `kzg10`

---------

Co-authored-by: CPerezz <c.perezbaro@gmail.com>
Co-authored-by: arnaucube <root@arnaucube.com>
This commit is contained in:
Pierre
2024-02-09 08:19:25 +01:00
committed by GitHub
parent 97e973a685
commit 63dbbfe1bc
67 changed files with 1208 additions and 53 deletions

View File

@@ -0,0 +1,848 @@
/// contains [Nova](https://eprint.iacr.org/2021/370.pdf) related circuits
use ark_crypto_primitives::crh::{
poseidon::constraints::{CRHGadget, CRHParametersVar},
CRHSchemeGadget,
};
use ark_crypto_primitives::sponge::{
constraints::CryptographicSpongeVar,
poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge},
Absorb, CryptographicSponge,
};
use ark_ec::{AffineRepr, CurveGroup, Group};
use ark_ff::{Field, PrimeField};
use ark_r1cs_std::{
alloc::{AllocVar, AllocationMode},
boolean::Boolean,
eq::EqGadget,
fields::{fp::FpVar, nonnative::NonNativeFieldVar, FieldVar},
groups::GroupOpsBounds,
prelude::CurveVar,
ToBitsGadget, ToConstraintFieldGadget,
};
use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError};
use ark_std::fmt::Debug;
use ark_std::{One, Zero};
use core::{borrow::Borrow, marker::PhantomData};
use super::{
cyclefold::{
CycleFoldChallengeGadget, CycleFoldCommittedInstanceVar, NIFSFullGadget, CF_IO_LEN,
},
CommittedInstance,
};
use crate::constants::N_BITS_RO;
use crate::folding::circuits::nonnative::{point_to_nonnative_limbs, NonNativeAffineVar};
use crate::frontend::FCircuit;
/// CF1 represents the ConstraintField used for the main Nova circuit which is over E1::Fr, where
/// E1 is the main curve where we do the folding.
pub type CF1<C> = <<C as CurveGroup>::Affine as AffineRepr>::ScalarField;
/// CF2 represents the ConstraintField used for the CycleFold circuit which is over E2::Fr=E1::Fq,
/// where E2 is the auxiliary curve (from [CycleFold](https://eprint.iacr.org/2023/1192.pdf)
/// approach) where we check the folding of the commitments (elliptic curve points).
pub type CF2<C> = <<C as CurveGroup>::BaseField as Field>::BasePrimeField;
/// CommittedInstanceVar contains the u, x, cmE and cmW values which are folded on the main Nova
/// constraints field (E1::Fr, where E1 is the main curve). The peculiarity is that cmE and cmW are
/// represented non-natively over the constraint field.
#[derive(Debug, Clone)]
pub struct CommittedInstanceVar<C: CurveGroup> {
pub u: FpVar<C::ScalarField>,
pub x: Vec<FpVar<C::ScalarField>>,
pub cmE: NonNativeAffineVar<C::ScalarField>,
pub cmW: NonNativeAffineVar<C::ScalarField>,
}
impl<C> AllocVar<CommittedInstance<C>, CF1<C>> for CommittedInstanceVar<C>
where
C: CurveGroup,
<C as ark_ec::CurveGroup>::BaseField: PrimeField,
{
fn new_variable<T: Borrow<CommittedInstance<C>>>(
cs: impl Into<Namespace<CF1<C>>>,
f: impl FnOnce() -> Result<T, SynthesisError>,
mode: AllocationMode,
) -> Result<Self, SynthesisError> {
f().and_then(|val| {
let cs = cs.into();
let u = FpVar::<C::ScalarField>::new_variable(cs.clone(), || Ok(val.borrow().u), mode)?;
let x: Vec<FpVar<C::ScalarField>> =
Vec::new_variable(cs.clone(), || Ok(val.borrow().x.clone()), mode)?;
let cmE = NonNativeAffineVar::<C::ScalarField>::new_variable(
cs.clone(),
|| Ok(val.borrow().cmE),
mode,
)?;
let cmW = NonNativeAffineVar::<C::ScalarField>::new_variable(
cs.clone(),
|| Ok(val.borrow().cmW),
mode,
)?;
Ok(Self { u, x, cmE, cmW })
})
}
}
impl<C> CommittedInstanceVar<C>
where
C: CurveGroup,
<C as Group>::ScalarField: Absorb,
{
/// hash implements the committed instance hash compatible with the native implementation from
/// CommittedInstance.hash.
/// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and `U` is the
/// `CommittedInstance`.
pub fn hash(
self,
crh_params: &CRHParametersVar<CF1<C>>,
i: FpVar<CF1<C>>,
z_0: Vec<FpVar<CF1<C>>>,
z_i: Vec<FpVar<CF1<C>>>,
) -> Result<FpVar<CF1<C>>, SynthesisError> {
let input = vec![
vec![i],
z_0,
z_i,
vec![self.u],
self.x,
self.cmE.x,
self.cmE.y,
self.cmW.x,
self.cmW.y,
]
.concat();
CRHGadget::<C::ScalarField>::evaluate(crh_params, &input)
}
}
/// Implements the circuit that does the checks of the Non-Interactive Folding Scheme Verifier
/// described in section 4 of [Nova](https://eprint.iacr.org/2021/370.pdf), where the cmE & cmW checks are
/// delegated to the NIFSCycleFoldGadget.
pub struct NIFSGadget<C: CurveGroup> {
_c: PhantomData<C>,
}
impl<C: CurveGroup> NIFSGadget<C>
where
C: CurveGroup,
{
/// Implements the constraints for NIFS.V for u and x, since cm(E) and cm(W) are delegated to
/// the CycleFold circuit.
pub fn verify(
r: FpVar<CF1<C>>,
ci1: CommittedInstanceVar<C>,
ci2: CommittedInstanceVar<C>,
ci3: CommittedInstanceVar<C>,
) -> Result<Boolean<CF1<C>>, SynthesisError> {
// ensure that: ci3.u == ci1.u + r * ci2.u
let first_check = ci3.u.is_eq(&(ci1.u + r.clone() * ci2.u))?;
// ensure that: ci3.x == ci1.x + r * ci2.x
let x_rlc = ci1
.x
.iter()
.zip(ci2.x)
.map(|(a, b)| a + &r * &b)
.collect::<Vec<FpVar<CF1<C>>>>();
let second_check = x_rlc.is_eq(&ci3.x)?;
first_check.and(&second_check)
}
}
/// ChallengeGadget computes the RO challenge used for the Nova instances NIFS, it contains a
/// rust-native and a in-circuit compatible versions.
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(
poseidon_config: &PoseidonConfig<C::ScalarField>,
u_i: CommittedInstance<C>,
U_i: CommittedInstance<C>,
cmT: C,
) -> Result<Vec<bool>, SynthesisError> {
let (u_cmE_x, u_cmE_y) = point_to_nonnative_limbs::<C>(u_i.cmE)?;
let (u_cmW_x, u_cmW_y) = point_to_nonnative_limbs::<C>(u_i.cmW)?;
let (U_cmE_x, U_cmE_y) = point_to_nonnative_limbs::<C>(U_i.cmE)?;
let (U_cmW_x, U_cmW_y) = point_to_nonnative_limbs::<C>(U_i.cmW)?;
let (cmT_x, cmT_y) = point_to_nonnative_limbs::<C>(cmT)?;
let mut sponge = PoseidonSponge::<C::ScalarField>::new(poseidon_config);
let input = vec![
vec![u_i.u],
u_i.x.clone(),
u_cmE_x,
u_cmE_y,
u_cmW_x,
u_cmW_y,
vec![U_i.u],
U_i.x.clone(),
U_cmE_x,
U_cmE_y,
U_cmW_x,
U_cmW_y,
cmT_x,
cmT_y,
]
.concat();
sponge.absorb(&input);
let bits = sponge.squeeze_bits(N_BITS_RO);
Ok(bits)
}
// compatible with the native get_challenge_native
pub fn get_challenge_gadget(
cs: ConstraintSystemRef<C::ScalarField>,
poseidon_config: &PoseidonConfig<C::ScalarField>,
u_i: CommittedInstanceVar<C>,
U_i: CommittedInstanceVar<C>,
cmT: NonNativeAffineVar<C::ScalarField>,
) -> Result<Vec<Boolean<C::ScalarField>>, SynthesisError> {
let mut sponge = PoseidonSpongeVar::<C::ScalarField>::new(cs, poseidon_config);
let input: Vec<FpVar<C::ScalarField>> = vec![
vec![u_i.u.clone()],
u_i.x.clone(),
u_i.cmE.x,
u_i.cmE.y,
u_i.cmW.x,
u_i.cmW.y,
vec![U_i.u.clone()],
U_i.x.clone(),
U_i.cmE.x,
U_i.cmE.y,
U_i.cmW.x,
U_i.cmW.y,
cmT.x,
cmT.y,
]
.concat();
sponge.absorb(&input)?;
let bits = sponge.squeeze_bits(N_BITS_RO)?;
Ok(bits)
}
}
/// AugmentedFCircuit implements the F' circuit (augmented F) defined in
/// [Nova](https://eprint.iacr.org/2021/370.pdf) together with the extra constraints defined in
/// [CycleFold](https://eprint.iacr.org/2023/1192.pdf).
#[derive(Debug, Clone)]
pub struct AugmentedFCircuit<
C1: CurveGroup,
C2: CurveGroup,
GC2: CurveVar<C2, CF2<C2>>,
FC: FCircuit<CF1<C1>>,
> where
for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>,
{
pub _gc2: PhantomData<GC2>,
pub poseidon_config: PoseidonConfig<CF1<C1>>,
pub i: Option<CF1<C1>>,
pub z_0: Option<Vec<C1::ScalarField>>,
pub z_i: Option<Vec<C1::ScalarField>>,
pub u_i: Option<CommittedInstance<C1>>,
pub U_i: Option<CommittedInstance<C1>>,
pub U_i1: Option<CommittedInstance<C1>>,
pub cmT: Option<C1>,
pub F: FC, // F circuit
pub x: Option<CF1<C1>>, // public inputs (u_{i+1}.x)
// cyclefold verifier on C1
pub cf_u_i: Option<CommittedInstance<C2>>,
pub cf_U_i: Option<CommittedInstance<C2>>,
pub cf_U_i1: Option<CommittedInstance<C2>>,
pub cf_cmT: Option<C2>,
pub cf_r_nonnat: Option<C2::ScalarField>,
}
impl<C1: CurveGroup, C2: CurveGroup, GC2: CurveVar<C2, CF2<C2>>, FC: FCircuit<CF1<C1>>>
AugmentedFCircuit<C1, C2, GC2, FC>
where
for<'a> &'a GC2: GroupOpsBounds<'a, C2, GC2>,
{
pub fn empty(poseidon_config: &PoseidonConfig<CF1<C1>>, F_circuit: FC) -> Self {
Self {
_gc2: PhantomData,
poseidon_config: poseidon_config.clone(),
i: None,
z_0: None,
z_i: None,
u_i: None,
U_i: None,
U_i1: None,
cmT: None,
F: F_circuit,
x: None,
// cyclefold values
cf_u_i: None,
cf_U_i: None,
cf_U_i1: None,
cf_cmT: None,
cf_r_nonnat: None,
}
}
}
impl<C1, C2, GC2, FC> ConstraintSynthesizer<CF1<C1>> for AugmentedFCircuit<C1, C2, GC2, FC>
where
C1: CurveGroup,
C2: CurveGroup,
GC2: CurveVar<C2, CF2<C2>>,
FC: FCircuit<CF1<C1>>,
<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 GC2: GroupOpsBounds<'a, C2, GC2>,
{
fn generate_constraints(self, cs: ConstraintSystemRef<CF1<C1>>) -> Result<(), SynthesisError> {
let i = FpVar::<CF1<C1>>::new_witness(cs.clone(), || {
Ok(self.i.unwrap_or_else(CF1::<C1>::zero))
})?;
let z_0 = Vec::<FpVar<CF1<C1>>>::new_witness(cs.clone(), || {
Ok(self.z_0.unwrap_or(vec![CF1::<C1>::zero()]))
})?;
let z_i = Vec::<FpVar<CF1<C1>>>::new_witness(cs.clone(), || {
Ok(self.z_i.unwrap_or(vec![CF1::<C1>::zero()]))
})?;
let u_dummy_native = CommittedInstance::<C1>::dummy(1);
let u_i = CommittedInstanceVar::<C1>::new_witness(cs.clone(), || {
Ok(self.u_i.unwrap_or(u_dummy_native.clone()))
})?;
let U_i = CommittedInstanceVar::<C1>::new_witness(cs.clone(), || {
Ok(self.U_i.unwrap_or(u_dummy_native.clone()))
})?;
let U_i1 = CommittedInstanceVar::<C1>::new_witness(cs.clone(), || {
Ok(self.U_i1.unwrap_or(u_dummy_native.clone()))
})?;
let cmT =
NonNativeAffineVar::new_witness(cs.clone(), || Ok(self.cmT.unwrap_or_else(C1::zero)))?;
let x =
FpVar::<CF1<C1>>::new_input(cs.clone(), || Ok(self.x.unwrap_or_else(CF1::<C1>::zero)))?;
let crh_params = CRHParametersVar::<C1::ScalarField>::new_constant(
cs.clone(),
self.poseidon_config.clone(),
)?;
// get z_{i+1} from the F circuit
let z_i1 = self.F.generate_step_constraints(cs.clone(), z_i.clone())?;
let zero = FpVar::<CF1<C1>>::new_constant(cs.clone(), CF1::<C1>::zero())?;
let is_not_basecase = i.is_neq(&zero)?;
// 1. u_i.x == H(i, z_0, z_i, U_i)
let u_i_x = U_i
.clone()
.hash(&crh_params, i.clone(), z_0.clone(), z_i.clone())?;
// check that h == u_i.x
(u_i.x[0]).conditional_enforce_equal(&u_i_x, &is_not_basecase)?;
// 2. u_i.cmE==cm(0), u_i.u==1
let zero_x = NonNativeFieldVar::<C1::BaseField, C1::ScalarField>::new_constant(
cs.clone(),
C1::BaseField::zero(),
)?
.to_constraint_field()?;
let zero_y = NonNativeFieldVar::<C1::BaseField, C1::ScalarField>::new_constant(
cs.clone(),
C1::BaseField::one(),
)?
.to_constraint_field()?;
(u_i.cmE.x.is_eq(&zero_x)?).conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?;
(u_i.cmE.y.is_eq(&zero_y)?).conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?;
(u_i.u.is_one()?).conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?;
// 3. nifs.verify, checks that folding u_i & U_i obtains U_{i+1}.
// compute r = H(u_i, U_i, cmT)
let r_bits = ChallengeGadget::<C1>::get_challenge_gadget(
cs.clone(),
&self.poseidon_config,
u_i.clone(),
U_i.clone(),
cmT.clone(),
)?;
let r = Boolean::le_bits_to_fp_var(&r_bits)?;
// Notice that NIFSGadget::verify is not checking the folding of cmE & cmW, since it will
// be done on the other curve.
let nifs_check = NIFSGadget::<C1>::verify(r, u_i.clone(), U_i.clone(), U_i1.clone())?;
nifs_check.conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?;
// 4. u_{i+1}.x = H(i+1, z_0, z_i+1, U_{i+1}), this is the output of F'
let u_i1_x = U_i1.clone().hash(
&crh_params,
i + FpVar::<CF1<C1>>::one(),
z_0.clone(),
z_i1.clone(),
)?;
u_i1_x.enforce_equal(&x)?;
// CycleFold part
let cf_u_dummy_native = CommittedInstance::<C2>::dummy(CF_IO_LEN);
let cf_u_i = CycleFoldCommittedInstanceVar::<C2, GC2>::new_witness(cs.clone(), || {
Ok(self.cf_u_i.unwrap_or_else(|| cf_u_dummy_native.clone()))
})?;
let cf_U_i = CycleFoldCommittedInstanceVar::<C2, GC2>::new_witness(cs.clone(), || {
Ok(self.cf_U_i.unwrap_or_else(|| cf_u_dummy_native.clone()))
})?;
let cf_U_i1 = CycleFoldCommittedInstanceVar::<C2, GC2>::new_witness(cs.clone(), || {
Ok(self.cf_U_i1.unwrap_or_else(|| cf_u_dummy_native.clone()))
})?;
let cf_cmT = GC2::new_witness(cs.clone(), || Ok(self.cf_cmT.unwrap_or_else(C2::zero)))?;
// cf_r_nonnat is an auxiliary input
let cf_r_nonnat =
NonNativeFieldVar::<C2::ScalarField, CF2<C2>>::new_witness(cs.clone(), || {
Ok(self.cf_r_nonnat.unwrap_or_else(C2::ScalarField::zero))
})?;
// check that cf_u_i.x == [u_i, U_i, U_{i+1}]
let incircuit_x = vec![
u_i.cmE.x, u_i.cmE.y, u_i.cmW.x, u_i.cmW.y, U_i.cmE.x, U_i.cmE.y, U_i.cmW.x, U_i.cmW.y,
U_i1.cmE.x, U_i1.cmE.y, U_i1.cmW.x, U_i1.cmW.y,
]
.concat();
let mut cf_u_i_x: Vec<FpVar<CF2<C2>>> = vec![];
for x_i in cf_u_i.x.iter() {
let mut x_fpvar = x_i.to_constraint_field()?;
cf_u_i_x.append(&mut x_fpvar);
}
cf_u_i_x.conditional_enforce_equal(&incircuit_x, &is_not_basecase)?;
// cf_r_bits is denoted by rho* in the paper
let cf_r_bits = CycleFoldChallengeGadget::<C2, GC2>::get_challenge_gadget(
cs.clone(),
&self.poseidon_config,
cf_u_i.clone(),
cf_U_i.clone(),
cf_cmT.clone(),
)?;
// assert that cf_r_bits == cf_r_nonnat converted to bits. cf_r_nonnat is just an auxiliary
// value used to compute RLC of NonNativeFieldVar values, since we can convert
// NonNativeFieldVar into Vec<Boolean>, but not in the other direction.
let cf_r_nonnat_bits = cf_r_nonnat.to_bits_le()?;
cf_r_bits.conditional_enforce_equal(&cf_r_nonnat_bits[..N_BITS_RO], &is_not_basecase)?;
// check cf_u_i.cmE=0, cf_u_i.u=1
(cf_u_i.cmE.is_zero()?).conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?;
(cf_u_i.u.is_one()?).conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?;
// check the fold of all the parameteres of the CycleFold instances, where the elliptic
// curve points relations are checked natively in Curve1 circuit (this one)
let v = NIFSFullGadget::<C2, GC2>::verify(
cf_r_bits,
cf_r_nonnat,
cf_cmT,
cf_U_i,
cf_u_i,
cf_U_i1,
)?;
v.conditional_enforce_equal(&Boolean::TRUE, &is_not_basecase)?;
Ok(())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use ark_ff::BigInteger;
use ark_pallas::{Fq, Fr, Projective};
use ark_r1cs_std::{alloc::AllocVar, R1CSVar};
use ark_relations::r1cs::{ConstraintLayer, ConstraintSystem, TracingMode};
use ark_std::One;
use ark_std::UniformRand;
use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2};
use tracing_subscriber::layer::SubscriberExt;
use crate::ccs::r1cs::{extract_r1cs, extract_w_x};
use crate::commitment::pedersen::Pedersen;
use crate::folding::nova::nifs::tests::prepare_simple_fold_inputs;
use crate::folding::nova::{
get_committed_instance_coordinates, nifs::NIFS, traits::NovaR1CS, Witness,
};
use crate::frontend::tests::CubicFCircuit;
use crate::transcript::poseidon::poseidon_test_config;
#[test]
fn test_committed_instance_var() {
let mut rng = ark_std::test_rng();
let ci = CommittedInstance::<Projective> {
cmE: Projective::rand(&mut rng),
u: Fr::rand(&mut rng),
cmW: Projective::rand(&mut rng),
x: vec![Fr::rand(&mut rng); 1],
};
let cs = ConstraintSystem::<Fr>::new_ref();
let ciVar =
CommittedInstanceVar::<Projective>::new_witness(cs.clone(), || Ok(ci.clone())).unwrap();
assert_eq!(ciVar.u.value().unwrap(), ci.u);
assert_eq!(ciVar.x.value().unwrap(), ci.x);
// the values cmE and cmW are checked in the CycleFold's circuit
// CommittedInstanceInCycleFoldVar in
// nova::cyclefold::tests::test_committed_instance_cyclefold_var
}
#[test]
fn test_nifs_gadget() {
let (_, _, _, _, ci1, _, ci2, _, ci3, _, cmT, _, r_Fr) = prepare_simple_fold_inputs();
let ci3_verifier = NIFS::<Projective, Pedersen<Projective>>::verify(r_Fr, &ci1, &ci2, &cmT);
assert_eq!(ci3_verifier, ci3);
let cs = ConstraintSystem::<Fr>::new_ref();
let rVar = FpVar::<Fr>::new_witness(cs.clone(), || Ok(r_Fr)).unwrap();
let ci1Var =
CommittedInstanceVar::<Projective>::new_witness(cs.clone(), || Ok(ci1.clone()))
.unwrap();
let ci2Var =
CommittedInstanceVar::<Projective>::new_witness(cs.clone(), || Ok(ci2.clone()))
.unwrap();
let ci3Var =
CommittedInstanceVar::<Projective>::new_witness(cs.clone(), || Ok(ci3.clone()))
.unwrap();
let nifs_check = NIFSGadget::<Projective>::verify(
rVar.clone(),
ci1Var.clone(),
ci2Var.clone(),
ci3Var.clone(),
)
.unwrap();
nifs_check.enforce_equal(&Boolean::<Fr>::TRUE).unwrap();
assert!(cs.is_satisfied().unwrap());
}
#[test]
fn test_committed_instance_hash() {
let mut rng = ark_std::test_rng();
let poseidon_config = poseidon_test_config::<Fr>();
let i = Fr::from(3_u32);
let z_0 = vec![Fr::from(3_u32)];
let z_i = vec![Fr::from(3_u32)];
let ci = CommittedInstance::<Projective> {
cmE: Projective::rand(&mut rng),
u: Fr::rand(&mut rng),
cmW: Projective::rand(&mut rng),
x: vec![Fr::rand(&mut rng); 1],
};
// compute the CommittedInstance hash natively
let h = ci
.hash(&poseidon_config, i, z_0.clone(), z_i.clone())
.unwrap();
let cs = ConstraintSystem::<Fr>::new_ref();
let iVar = FpVar::<Fr>::new_witness(cs.clone(), || Ok(i)).unwrap();
let z_0Var = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z_0.clone())).unwrap();
let z_iVar = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z_i.clone())).unwrap();
let ciVar =
CommittedInstanceVar::<Projective>::new_witness(cs.clone(), || Ok(ci.clone())).unwrap();
let crh_params = CRHParametersVar::<Fr>::new_constant(cs.clone(), poseidon_config).unwrap();
// compute the CommittedInstance hash in-circuit
let hVar = ciVar.hash(&crh_params, iVar, z_0Var, z_iVar).unwrap();
assert!(cs.is_satisfied().unwrap());
// check that the natively computed and in-circuit computed hashes match
assert_eq!(hVar.value().unwrap(), h);
}
// checks that the gadget and native implementations of the challenge computation matcbh
#[test]
fn test_challenge_gadget() {
let mut rng = ark_std::test_rng();
let poseidon_config = poseidon_test_config::<Fr>();
let u_i = CommittedInstance::<Projective> {
cmE: Projective::rand(&mut rng),
u: Fr::rand(&mut rng),
cmW: Projective::rand(&mut rng),
x: vec![Fr::rand(&mut rng); 1],
};
let U_i = CommittedInstance::<Projective> {
cmE: Projective::rand(&mut rng),
u: Fr::rand(&mut rng),
cmW: Projective::rand(&mut rng),
x: vec![Fr::rand(&mut rng); 1],
};
let cmT = Projective::rand(&mut rng);
// compute the challenge natively
let r_bits = ChallengeGadget::<Projective>::get_challenge_native(
&poseidon_config,
u_i.clone(),
U_i.clone(),
cmT,
)
.unwrap();
let r = Fr::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap();
let cs = ConstraintSystem::<Fr>::new_ref();
let u_iVar =
CommittedInstanceVar::<Projective>::new_witness(cs.clone(), || Ok(u_i.clone()))
.unwrap();
let U_iVar =
CommittedInstanceVar::<Projective>::new_witness(cs.clone(), || Ok(U_i.clone()))
.unwrap();
let cmTVar = NonNativeAffineVar::<Fr>::new_witness(cs.clone(), || Ok(cmT)).unwrap();
// compute the challenge in-circuit
let r_bitsVar = ChallengeGadget::<Projective>::get_challenge_gadget(
cs.clone(),
&poseidon_config,
u_iVar,
U_iVar,
cmTVar,
)
.unwrap();
assert!(cs.is_satisfied().unwrap());
// check that the natively computed and in-circuit computed hashes match
let rVar = Boolean::le_bits_to_fp_var(&r_bitsVar).unwrap();
assert_eq!(rVar.value().unwrap(), r);
assert_eq!(r_bitsVar.value().unwrap(), r_bits);
}
#[test]
/// test_augmented_f_circuit folds the CubicFCircuit circuit in multiple iterations, feeding the
/// values into the AugmentedFCircuit.
fn test_augmented_f_circuit() {
let mut layer = ConstraintLayer::default();
layer.mode = TracingMode::OnlyConstraints;
let subscriber = tracing_subscriber::Registry::default().with(layer);
let _guard = tracing::subscriber::set_default(subscriber);
let mut rng = ark_std::test_rng();
let poseidon_config = poseidon_test_config::<Fr>();
// compute z vector for the initial instance
let cs = ConstraintSystem::<Fr>::new_ref();
// prepare the circuit to obtain its R1CS
let F_circuit = CubicFCircuit::<Fr>::new(());
let mut augmented_F_circuit =
AugmentedFCircuit::<Projective, Projective2, GVar2, CubicFCircuit<Fr>>::empty(
&poseidon_config,
F_circuit,
);
augmented_F_circuit
.generate_constraints(cs.clone())
.unwrap();
cs.finalize();
println!("num_constraints={:?}", cs.num_constraints());
let cs = cs.into_inner().unwrap();
let r1cs = extract_r1cs::<Fr>(&cs);
let (w, x) = extract_w_x::<Fr>(&cs);
assert_eq!(1 + x.len() + w.len(), r1cs.A.n_cols);
assert_eq!(r1cs.l, x.len());
let pedersen_params = Pedersen::<Projective>::new_params(&mut rng, r1cs.A.n_rows);
// first step, set z_i=z_0=3 and z_{i+1}=35 (initial values)
let z_0 = vec![Fr::from(3_u32)];
let mut z_i = z_0.clone();
let mut z_i1 = vec![Fr::from(35_u32)];
let w_dummy = Witness::<Projective>::new(vec![Fr::zero(); w.len()], r1cs.A.n_rows);
let u_dummy = CommittedInstance::<Projective>::dummy(x.len());
// W_i is a 'dummy witness', all zeroes, but with the size corresponding to the R1CS that
// we're working with.
// set U_i <-- dummy instance
let mut W_i = w_dummy.clone();
let mut U_i = u_dummy.clone();
r1cs.check_relaxed_instance_relation(&W_i, &U_i).unwrap();
let mut w_i = w_dummy.clone();
let mut u_i = u_dummy.clone();
let (mut W_i1, mut U_i1, mut cmT): (
Witness<Projective>,
CommittedInstance<Projective>,
Projective,
) = (w_dummy.clone(), u_dummy.clone(), Projective::generator());
// as expected, dummy instances pass the relaxed_r1cs check
r1cs.check_relaxed_instance_relation(&W_i1, &U_i1).unwrap();
let mut i = Fr::zero();
let mut u_i1_x: Fr;
for _ in 0..4 {
if i == Fr::zero() {
// base case: i=0, z_i=z_0, U_i = U_d := dummy instance
// u_1.x = H(1, z_0, z_i, U_i)
u_i1_x = U_i
.hash(&poseidon_config, Fr::one(), z_0.clone(), z_i1.clone())
.unwrap();
// base case
augmented_F_circuit =
AugmentedFCircuit::<Projective, Projective2, GVar2, CubicFCircuit<Fr>> {
_gc2: PhantomData,
poseidon_config: poseidon_config.clone(),
i: Some(i), // = 0
z_0: Some(z_0.clone()), // = z_i=3
z_i: Some(z_i.clone()),
u_i: Some(u_i.clone()), // = dummy
U_i: Some(U_i.clone()), // = dummy
U_i1: Some(U_i1.clone()), // = dummy
cmT: Some(cmT),
F: F_circuit,
x: Some(u_i1_x),
// cyclefold instances (not tested in this test)
cf_u_i: None,
cf_U_i: None,
cf_U_i1: None,
cf_cmT: None,
cf_r_nonnat: None,
};
} else {
r1cs.check_relaxed_instance_relation(&w_i, &u_i).unwrap();
r1cs.check_relaxed_instance_relation(&W_i, &U_i).unwrap();
// U_{i+1}
let T: Vec<Fr>;
(T, cmT) = NIFS::<Projective, Pedersen<Projective>>::compute_cmT(
&pedersen_params,
&r1cs,
&w_i,
&u_i,
&W_i,
&U_i,
)
.unwrap();
// get challenge r
let r_bits = ChallengeGadget::<Projective>::get_challenge_native(
&poseidon_config,
u_i.clone(),
U_i.clone(),
cmT,
)
.unwrap();
let r_Fr = Fr::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap();
(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_relaxed_instance_relation(&W_i1, &U_i1).unwrap();
// folded instance output (public input, x)
// u_{i+1}.x = H(i+1, z_0, z_{i+1}, U_{i+1})
u_i1_x = U_i1
.hash(&poseidon_config, i + Fr::one(), z_0.clone(), z_i1.clone())
.unwrap();
// set up dummy cyclefold instances just for the sake of this test. Warning, this
// is only because we are in a test were we're not testing the cyclefold side of
// things.
let cf_W_i = Witness::<Projective2>::new(vec![Fq::zero(); 1], 1);
let cf_U_i = CommittedInstance::<Projective2>::dummy(CF_IO_LEN);
let cf_u_i_x = [
get_committed_instance_coordinates(&u_i),
get_committed_instance_coordinates(&U_i),
get_committed_instance_coordinates(&U_i1),
]
.concat();
let cf_u_i = CommittedInstance::<Projective2> {
cmE: cf_U_i.cmE,
u: Fq::one(),
cmW: cf_U_i.cmW,
x: cf_u_i_x,
};
let cf_w_i = cf_W_i.clone();
let (cf_T, cf_cmT): (Vec<Fq>, Projective2) =
(vec![Fq::zero(); cf_W_i.E.len()], Projective2::zero());
let cf_r_bits =
CycleFoldChallengeGadget::<Projective2, GVar2>::get_challenge_native(
&poseidon_config,
cf_u_i.clone(),
cf_U_i.clone(),
cf_cmT,
)
.unwrap();
let cf_r_Fq = Fq::from_bigint(BigInteger::from_bits_le(&cf_r_bits)).unwrap();
let (_, cf_U_i1) = NIFS::<Projective2, Pedersen<Projective2>>::fold_instances(
cf_r_Fq, &cf_W_i, &cf_U_i, &cf_w_i, &cf_u_i, &cf_T, cf_cmT,
)
.unwrap();
augmented_F_circuit =
AugmentedFCircuit::<Projective, Projective2, GVar2, CubicFCircuit<Fr>> {
_gc2: PhantomData,
poseidon_config: poseidon_config.clone(),
i: Some(i),
z_0: Some(z_0.clone()),
z_i: Some(z_i.clone()),
u_i: Some(u_i),
U_i: Some(U_i.clone()),
U_i1: Some(U_i1.clone()),
cmT: Some(cmT),
F: F_circuit,
x: Some(u_i1_x),
cf_u_i: Some(cf_u_i),
cf_U_i: Some(cf_U_i),
cf_U_i1: Some(cf_U_i1),
cf_cmT: Some(cf_cmT),
cf_r_nonnat: Some(cf_r_Fq),
};
}
let cs = ConstraintSystem::<Fr>::new_ref();
augmented_F_circuit
.generate_constraints(cs.clone())
.unwrap();
let is_satisfied = cs.is_satisfied().unwrap();
if !is_satisfied {
dbg!(cs.which_is_unsatisfied().unwrap());
}
assert!(is_satisfied);
cs.finalize();
let cs = cs.into_inner().unwrap();
let (w_i1, x_i1) = extract_w_x::<Fr>(&cs);
assert_eq!(x_i1.len(), 1);
assert_eq!(x_i1[0], u_i1_x);
// 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.
w_i = Witness::<Projective>::new(w_i1.clone(), r1cs.A.n_rows);
u_i = w_i
.commit::<Pedersen<Projective>>(&pedersen_params, vec![u_i1_x])
.unwrap();
r1cs.check_relaxed_instance_relation(&w_i, &u_i).unwrap();
r1cs.check_relaxed_instance_relation(&W_i1, &U_i1).unwrap();
// set values for next iteration
i += Fr::one();
// advance the F circuit state
z_i = z_i1.clone();
z_i1 = F_circuit.step_native(z_i.clone()).unwrap();
U_i = U_i1.clone();
W_i = W_i1.clone();
}
}
}

View File

@@ -0,0 +1,567 @@
/// contains [CycleFold](https://eprint.iacr.org/2023/1192.pdf) related circuits
use ark_crypto_primitives::sponge::{
constraints::CryptographicSpongeVar,
poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge},
Absorb, CryptographicSponge,
};
use ark_ec::CurveGroup;
use ark_ff::PrimeField;
use ark_r1cs_std::{
alloc::{AllocVar, AllocationMode},
bits::uint8::UInt8,
boolean::Boolean,
eq::EqGadget,
fields::{fp::FpVar, nonnative::NonNativeFieldVar},
groups::GroupOpsBounds,
prelude::CurveVar,
ToBytesGadget,
};
use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError};
use ark_serialize::CanonicalSerialize;
use ark_std::fmt::Debug;
use ark_std::Zero;
use core::{borrow::Borrow, marker::PhantomData};
use super::circuits::CF2;
use super::CommittedInstance;
use crate::constants::N_BITS_RO;
use crate::Error;
// publi inputs length for the CycleFoldCircuit, |[u_i, U_i, U_{i+1}]|
pub const CF_IO_LEN: usize = 12;
/// CycleFoldCommittedInstanceVar is the CycleFold CommittedInstance representation in the Nova
/// circuit.
#[derive(Debug, Clone)]
pub struct CycleFoldCommittedInstanceVar<C: CurveGroup, GC: CurveVar<C, CF2<C>>>
where
for<'a> &'a GC: GroupOpsBounds<'a, C, GC>,
{
_c: PhantomData<C>,
pub cmE: GC,
pub u: NonNativeFieldVar<C::ScalarField, CF2<C>>,
pub cmW: GC,
pub x: Vec<NonNativeFieldVar<C::ScalarField, CF2<C>>>,
}
impl<C, GC> AllocVar<CommittedInstance<C>, CF2<C>> for CycleFoldCommittedInstanceVar<C, GC>
where
C: CurveGroup,
GC: CurveVar<C, CF2<C>>,
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField,
for<'a> &'a GC: GroupOpsBounds<'a, C, GC>,
{
fn new_variable<T: Borrow<CommittedInstance<C>>>(
cs: impl Into<Namespace<CF2<C>>>,
f: impl FnOnce() -> Result<T, SynthesisError>,
mode: AllocationMode,
) -> Result<Self, SynthesisError> {
f().and_then(|val| {
let cs = cs.into();
let cmE = GC::new_variable(cs.clone(), || Ok(val.borrow().cmE), mode)?;
let cmW = GC::new_variable(cs.clone(), || Ok(val.borrow().cmW), mode)?;
let u = NonNativeFieldVar::<C::ScalarField, CF2<C>>::new_variable(
cs.clone(),
|| Ok(val.borrow().u),
mode,
)?;
let x = Vec::<NonNativeFieldVar<C::ScalarField, CF2<C>>>::new_variable(
cs.clone(),
|| Ok(val.borrow().x.clone()),
mode,
)?;
Ok(Self {
_c: PhantomData,
cmE,
u,
cmW,
x,
})
})
}
}
/// CommittedInstanceInCycleFoldVar represents the Nova CommittedInstance in the CycleFold circuit,
/// where the commitments to E and W (cmW and cmW) from the CommittedInstance on the E2,
/// represented as native points, which are folded on the auxiliary curve constraints field (E2::Fr
/// = E1::Fq).
#[derive(Debug, Clone)]
pub struct CommittedInstanceInCycleFoldVar<C: CurveGroup, GC: CurveVar<C, CF2<C>>>
where
for<'a> &'a GC: GroupOpsBounds<'a, C, GC>,
{
_c: PhantomData<C>,
pub cmE: GC,
pub cmW: GC,
}
impl<C, GC> AllocVar<CommittedInstance<C>, CF2<C>> for CommittedInstanceInCycleFoldVar<C, GC>
where
C: CurveGroup,
GC: CurveVar<C, CF2<C>>,
for<'a> &'a GC: GroupOpsBounds<'a, C, GC>,
{
fn new_variable<T: Borrow<CommittedInstance<C>>>(
cs: impl Into<Namespace<CF2<C>>>,
f: impl FnOnce() -> Result<T, SynthesisError>,
mode: AllocationMode,
) -> Result<Self, SynthesisError> {
f().and_then(|val| {
let cs = cs.into();
let cmE = GC::new_variable(cs.clone(), || Ok(val.borrow().cmE), mode)?;
let cmW = GC::new_variable(cs.clone(), || Ok(val.borrow().cmW), mode)?;
Ok(Self {
_c: PhantomData,
cmE,
cmW,
})
})
}
}
/// NIFSinCycleFoldGadget performs the Nova NIFS.V elliptic curve points relation checks in the other
/// curve (natively) following [CycleFold](https://eprint.iacr.org/2023/1192.pdf).
pub struct NIFSinCycleFoldGadget<C: CurveGroup, GC: CurveVar<C, CF2<C>>> {
_c: PhantomData<C>,
_gc: PhantomData<GC>,
}
impl<C: CurveGroup, GC: CurveVar<C, CF2<C>>> NIFSinCycleFoldGadget<C, GC>
where
C: CurveGroup,
GC: CurveVar<C, CF2<C>>,
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField,
for<'a> &'a GC: GroupOpsBounds<'a, C, GC>,
{
pub fn verify(
r_bits: Vec<Boolean<CF2<C>>>,
cmT: GC,
ci1: CommittedInstanceInCycleFoldVar<C, GC>,
ci2: CommittedInstanceInCycleFoldVar<C, GC>,
ci3: CommittedInstanceInCycleFoldVar<C, GC>,
) -> Result<Boolean<CF2<C>>, SynthesisError> {
// cm(E) check: ci3.cmE == ci1.cmE + r * cmT + r^2 * ci2.cmE
let first_check = ci3.cmE.is_eq(
&((ci2.cmE.scalar_mul_le(r_bits.iter())? + cmT).scalar_mul_le(r_bits.iter())?
+ ci1.cmE),
)?;
// cm(W) check: ci3.cmW == ci1.cmW + r * ci2.cmW
let second_check = ci3
.cmW
.is_eq(&(ci1.cmW + ci2.cmW.scalar_mul_le(r_bits.iter())?))?;
first_check.and(&second_check)
}
}
/// This is the gadget used in the AugmentedFCircuit to verify the CycleFold instances folding,
/// which checks the correct RLC of u,x,cmE,cmW (hence the name containing 'Full', since it checks
/// all the RLC values, not only the native ones). It assumes that ci2.cmE=0, ci2.u=1.
pub struct NIFSFullGadget<C: CurveGroup, GC: CurveVar<C, CF2<C>>> {
_c: PhantomData<C>,
_gc: PhantomData<GC>,
}
impl<C: CurveGroup, GC: CurveVar<C, CF2<C>>> NIFSFullGadget<C, GC>
where
C: CurveGroup,
GC: CurveVar<C, CF2<C>>,
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField,
for<'a> &'a GC: GroupOpsBounds<'a, C, GC>,
{
pub fn verify(
r_bits: Vec<Boolean<CF2<C>>>,
r_nonnat: NonNativeFieldVar<C::ScalarField, CF2<C>>,
cmT: GC,
// ci1 is assumed to be always with cmE=0, u=1 (checks done previous to this method)
ci1: CycleFoldCommittedInstanceVar<C, GC>,
ci2: CycleFoldCommittedInstanceVar<C, GC>,
ci3: CycleFoldCommittedInstanceVar<C, GC>,
) -> Result<Boolean<CF2<C>>, SynthesisError> {
// cm(E) check: ci3.cmE == ci1.cmE + r * cmT (ci2.cmE=0)
let first_check = ci3
.cmE
.is_eq(&(cmT.scalar_mul_le(r_bits.iter())? + ci1.cmE))?;
// cm(W) check: ci3.cmW == ci1.cmW + r * ci2.cmW
let second_check = ci3
.cmW
.is_eq(&(ci1.cmW + ci2.cmW.scalar_mul_le(r_bits.iter())?))?;
let u_rlc: NonNativeFieldVar<C::ScalarField, CF2<C>> = ci1.u + r_nonnat.clone();
let third_check = u_rlc.is_eq(&ci3.u)?;
// ensure that: ci3.x == ci1.x + r * ci2.x
let x_rlc: Vec<NonNativeFieldVar<C::ScalarField, CF2<C>>> = ci1
.x
.iter()
.zip(ci2.x)
.map(|(a, b)| a + &r_nonnat * &b)
.collect::<Vec<NonNativeFieldVar<C::ScalarField, CF2<C>>>>();
let fourth_check = x_rlc.is_eq(&ci3.x)?;
first_check
.and(&second_check)?
.and(&third_check)?
.and(&fourth_check)
}
}
/// ChallengeGadget computes the RO challenge used for the CycleFold instances NIFS, it contains a
/// rust-native and a in-circuit compatible versions.
pub struct CycleFoldChallengeGadget<C: CurveGroup, GC: CurveVar<C, CF2<C>>> {
_c: PhantomData<C>, // Nova's Curve2, the one used for the CycleFold circuit
_gc: PhantomData<GC>,
}
impl<C, GC> CycleFoldChallengeGadget<C, GC>
where
C: CurveGroup,
GC: CurveVar<C, CF2<C>>,
<C as CurveGroup>::BaseField: PrimeField,
<C as CurveGroup>::BaseField: Absorb,
for<'a> &'a GC: GroupOpsBounds<'a, C, GC>,
{
pub fn get_challenge_native(
poseidon_config: &PoseidonConfig<C::BaseField>,
u_i: CommittedInstance<C>,
U_i: CommittedInstance<C>,
cmT: C,
) -> Result<Vec<bool>, Error> {
let mut sponge = PoseidonSponge::<C::BaseField>::new(poseidon_config);
let u_i_cmE_bytes = point_to_bytes(u_i.cmE);
let u_i_cmW_bytes = point_to_bytes(u_i.cmW);
let U_i_cmE_bytes = point_to_bytes(U_i.cmE);
let U_i_cmW_bytes = point_to_bytes(U_i.cmW);
let cmT_bytes = point_to_bytes(cmT);
let mut u_i_u_bytes = Vec::new();
u_i.u.serialize_uncompressed(&mut u_i_u_bytes)?;
let mut u_i_x_bytes = Vec::new();
u_i.x.serialize_uncompressed(&mut u_i_x_bytes)?;
u_i_x_bytes = u_i_x_bytes[8..].to_vec();
let mut U_i_u_bytes = Vec::new();
U_i.u.serialize_uncompressed(&mut U_i_u_bytes)?;
let mut U_i_x_bytes = Vec::new();
U_i.x.serialize_uncompressed(&mut U_i_x_bytes)?;
U_i_x_bytes = U_i_x_bytes[8..].to_vec();
let input: Vec<u8> = [
u_i_cmE_bytes,
u_i_u_bytes,
u_i_cmW_bytes,
u_i_x_bytes,
U_i_cmE_bytes,
U_i_u_bytes,
U_i_cmW_bytes,
U_i_x_bytes,
cmT_bytes,
]
.concat();
sponge.absorb(&input);
let bits = sponge.squeeze_bits(N_BITS_RO);
Ok(bits)
}
// compatible with the native get_challenge_native
pub fn get_challenge_gadget(
cs: ConstraintSystemRef<C::BaseField>,
poseidon_config: &PoseidonConfig<C::BaseField>,
u_i: CycleFoldCommittedInstanceVar<C, GC>,
U_i: CycleFoldCommittedInstanceVar<C, GC>,
cmT: GC,
) -> Result<Vec<Boolean<C::BaseField>>, SynthesisError> {
let mut sponge = PoseidonSpongeVar::<C::BaseField>::new(cs, poseidon_config);
let u_i_x_bytes: Vec<UInt8<CF2<C>>> = u_i
.x
.iter()
.flat_map(|e| e.to_bytes().unwrap_or(vec![]))
.collect::<Vec<UInt8<CF2<C>>>>();
let U_i_x_bytes: Vec<UInt8<CF2<C>>> = U_i
.x
.iter()
.flat_map(|e| e.to_bytes().unwrap_or(vec![]))
.collect::<Vec<UInt8<CF2<C>>>>();
let input: Vec<UInt8<CF2<C>>> = [
u_i.cmE.to_bytes()?,
u_i.u.to_bytes()?,
u_i.cmW.to_bytes()?,
u_i_x_bytes,
U_i.cmE.to_bytes()?,
U_i.u.to_bytes()?,
U_i.cmW.to_bytes()?,
U_i_x_bytes,
cmT.to_bytes()?,
// TODO instead of bytes, use field elements, but needs x,y coordinates from
// u_i.{cmE,cmW}, U_i.{cmE,cmW}, cmT. Depends exposing x,y coordinates of GC. Issue to
// keep track of this:
// https://github.com/privacy-scaling-explorations/folding-schemes/issues/44
]
.concat();
sponge.absorb(&input)?;
let bits = sponge.squeeze_bits(N_BITS_RO)?;
Ok(bits)
}
}
/// returns the bytes being compatible with the ark_r1cs_std `.to_bytes` approach
fn point_to_bytes<C: CurveGroup>(p: C) -> Vec<u8> {
let l = p.uncompressed_size();
let mut b = Vec::new();
p.serialize_uncompressed(&mut b).unwrap();
b[l - 1] = 0;
if p.is_zero() {
b[l / 2] = 1;
b[l - 1] = 1;
}
b
}
/// CycleFoldCircuit contains the constraints that check the correct fold of the committed
/// instances from Curve1. Namely, it checks the random linear combinations of the elliptic curve
/// (Curve1) points of u_i, U_i leading to U_{i+1}
#[derive(Debug, Clone)]
pub struct CycleFoldCircuit<C: CurveGroup, GC: CurveVar<C, CF2<C>>> {
pub _gc: PhantomData<GC>,
pub r_bits: Option<Vec<bool>>,
pub cmT: Option<C>,
// u_i,U_i,U_i1 are the nova instances from AugmentedFCircuit which will be (their elliptic
// curve points) checked natively in CycleFoldCircuit
pub u_i: Option<CommittedInstance<C>>,
pub U_i: Option<CommittedInstance<C>>,
pub U_i1: Option<CommittedInstance<C>>,
pub x: Option<Vec<CF2<C>>>, // public inputs (cf_u_{i+1}.x)
}
impl<C: CurveGroup, GC: CurveVar<C, CF2<C>>> CycleFoldCircuit<C, GC> {
pub fn empty() -> Self {
Self {
_gc: PhantomData,
r_bits: None,
cmT: None,
u_i: None,
U_i: None,
U_i1: None,
x: None,
}
}
}
impl<C, GC> ConstraintSynthesizer<CF2<C>> for CycleFoldCircuit<C, GC>
where
C: CurveGroup,
GC: CurveVar<C, CF2<C>>,
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField,
for<'a> &'a GC: GroupOpsBounds<'a, C, GC>,
{
fn generate_constraints(self, cs: ConstraintSystemRef<CF2<C>>) -> Result<(), SynthesisError> {
let r_bits: Vec<Boolean<CF2<C>>> = Vec::new_witness(cs.clone(), || {
Ok(self.r_bits.unwrap_or(vec![false; N_BITS_RO]))
})?;
let cmT = GC::new_witness(cs.clone(), || Ok(self.cmT.unwrap_or(C::zero())))?;
let u_dummy_native = CommittedInstance::<C>::dummy(1);
let u_i = CommittedInstanceInCycleFoldVar::<C, GC>::new_witness(cs.clone(), || {
Ok(self.u_i.unwrap_or(u_dummy_native.clone()))
})?;
let U_i = CommittedInstanceInCycleFoldVar::<C, GC>::new_witness(cs.clone(), || {
Ok(self.U_i.unwrap_or(u_dummy_native.clone()))
})?;
let U_i1 = CommittedInstanceInCycleFoldVar::<C, GC>::new_witness(cs.clone(), || {
Ok(self.U_i1.unwrap_or(u_dummy_native.clone()))
})?;
let _x = Vec::<FpVar<CF2<C>>>::new_input(cs.clone(), || {
Ok(self.x.unwrap_or(vec![CF2::<C>::zero(); CF_IO_LEN]))
})?;
#[cfg(test)]
assert_eq!(_x.len(), CF_IO_LEN); // non-constrained sanity check
// fold the original Nova instances natively in CycleFold
let v =
NIFSinCycleFoldGadget::<C, GC>::verify(r_bits.clone(), cmT, u_i.clone(), U_i, U_i1)?;
v.enforce_equal(&Boolean::TRUE)?;
// check that x == [u_i, U_i, U_{i+1}], check that the cmW & cmW from u_i, U_i, U_{i+1} in
// the CycleFoldCircuit are the sames used in the public inputs 'x', which come from the
// AugmentedFCircuit.
// TODO: Issue to keep track of this: https://github.com/privacy-scaling-explorations/folding-schemes/issues/44
// and https://github.com/privacy-scaling-explorations/folding-schemes/issues/48
Ok(())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use ark_ff::BigInteger;
use ark_pallas::{constraints::GVar, Fq, Fr, Projective};
use ark_r1cs_std::{alloc::AllocVar, R1CSVar};
use ark_relations::r1cs::ConstraintSystem;
use ark_std::UniformRand;
use crate::folding::nova::nifs::tests::prepare_simple_fold_inputs;
use crate::transcript::poseidon::poseidon_test_config;
#[test]
fn test_committed_instance_cyclefold_var() {
let mut rng = ark_std::test_rng();
let ci = CommittedInstance::<Projective> {
cmE: Projective::rand(&mut rng),
u: Fr::rand(&mut rng),
cmW: Projective::rand(&mut rng),
x: vec![Fr::rand(&mut rng); 1],
};
// check the instantiation of the CycleFold side:
let cs = ConstraintSystem::<Fq>::new_ref();
let ciVar =
CommittedInstanceInCycleFoldVar::<Projective, GVar>::new_witness(cs.clone(), || {
Ok(ci.clone())
})
.unwrap();
assert_eq!(ciVar.cmE.value().unwrap(), ci.cmE);
assert_eq!(ciVar.cmW.value().unwrap(), ci.cmW);
}
#[test]
fn test_nifs_gadget_cyclefold() {
let (_, _, _, _, ci1, _, ci2, _, ci3, _, cmT, r_bits, _) = prepare_simple_fold_inputs();
// cs is the Constraint System on the Curve Cycle auxiliary curve constraints field
// (E2::Fr)
let cs = ConstraintSystem::<Fq>::new_ref();
let r_bitsVar = Vec::<Boolean<Fq>>::new_witness(cs.clone(), || Ok(r_bits)).unwrap();
let cmTVar = GVar::new_witness(cs.clone(), || Ok(cmT)).unwrap();
let ci1Var =
CommittedInstanceInCycleFoldVar::<Projective, GVar>::new_witness(cs.clone(), || {
Ok(ci1.clone())
})
.unwrap();
let ci2Var =
CommittedInstanceInCycleFoldVar::<Projective, GVar>::new_witness(cs.clone(), || {
Ok(ci2.clone())
})
.unwrap();
let ci3Var =
CommittedInstanceInCycleFoldVar::<Projective, GVar>::new_witness(cs.clone(), || {
Ok(ci3.clone())
})
.unwrap();
let nifs_cf_check = NIFSinCycleFoldGadget::<Projective, GVar>::verify(
r_bitsVar, cmTVar, ci1Var, ci2Var, ci3Var,
)
.unwrap();
nifs_cf_check.enforce_equal(&Boolean::<Fq>::TRUE).unwrap();
assert!(cs.is_satisfied().unwrap());
dbg!(cs.num_constraints());
}
#[test]
fn test_nifs_full_gadget() {
let (_, _, _, _, ci1, _, ci2, _, ci3, _, cmT, r_bits, r_Fr) = prepare_simple_fold_inputs();
let cs = ConstraintSystem::<Fq>::new_ref();
let r_nonnatVar =
NonNativeFieldVar::<Fr, Fq>::new_witness(cs.clone(), || Ok(r_Fr)).unwrap();
let r_bitsVar = Vec::<Boolean<Fq>>::new_witness(cs.clone(), || Ok(r_bits)).unwrap();
let ci1Var =
CycleFoldCommittedInstanceVar::<Projective, GVar>::new_witness(cs.clone(), || {
Ok(ci1.clone())
})
.unwrap();
let ci2Var =
CycleFoldCommittedInstanceVar::<Projective, GVar>::new_witness(cs.clone(), || {
Ok(ci2.clone())
})
.unwrap();
let ci3Var =
CycleFoldCommittedInstanceVar::<Projective, GVar>::new_witness(cs.clone(), || {
Ok(ci3.clone())
})
.unwrap();
let cmTVar = GVar::new_witness(cs.clone(), || Ok(cmT)).unwrap();
let nifs_check = NIFSFullGadget::<Projective, GVar>::verify(
r_bitsVar,
r_nonnatVar,
cmTVar,
ci1Var,
ci2Var,
ci3Var,
)
.unwrap();
nifs_check.enforce_equal(&Boolean::<Fq>::TRUE).unwrap();
assert!(cs.is_satisfied().unwrap());
dbg!(cs.num_constraints());
}
#[test]
fn test_cyclefold_challenge_gadget() {
let mut rng = ark_std::test_rng();
let poseidon_config = poseidon_test_config::<Fq>();
let u_i = CommittedInstance::<Projective> {
cmE: Projective::zero(), // zero on purpose, so we test also the zero point case
u: Fr::zero(),
cmW: Projective::rand(&mut rng),
x: std::iter::repeat_with(|| Fr::rand(&mut rng))
.take(CF_IO_LEN)
.collect(),
};
let U_i = CommittedInstance::<Projective> {
cmE: Projective::rand(&mut rng),
u: Fr::rand(&mut rng),
cmW: Projective::rand(&mut rng),
x: std::iter::repeat_with(|| Fr::rand(&mut rng))
.take(CF_IO_LEN)
.collect(),
};
let cmT = Projective::rand(&mut rng);
// compute the challenge natively
let r_bits = CycleFoldChallengeGadget::<Projective, GVar>::get_challenge_native(
&poseidon_config,
u_i.clone(),
U_i.clone(),
cmT,
)
.unwrap();
let cs = ConstraintSystem::<Fq>::new_ref();
let u_iVar =
CycleFoldCommittedInstanceVar::<Projective, GVar>::new_witness(cs.clone(), || {
Ok(u_i.clone())
})
.unwrap();
let U_iVar =
CycleFoldCommittedInstanceVar::<Projective, GVar>::new_witness(cs.clone(), || {
Ok(U_i.clone())
})
.unwrap();
let cmTVar = GVar::new_witness(cs.clone(), || Ok(cmT)).unwrap();
let r_bitsVar = CycleFoldChallengeGadget::<Projective, GVar>::get_challenge_gadget(
cs.clone(),
&poseidon_config,
u_iVar,
U_iVar,
cmTVar,
)
.unwrap();
assert!(cs.is_satisfied().unwrap());
// check that the natively computed and in-circuit computed hashes match
let rVar = Boolean::le_bits_to_fp_var(&r_bitsVar).unwrap();
let r = Fq::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap();
assert_eq!(rVar.value().unwrap(), r);
assert_eq!(r_bitsVar.value().unwrap(), r_bits);
}
}

View File

@@ -0,0 +1,205 @@
/// This file implements the onchain (Ethereum's EVM) decider.
use ark_crypto_primitives::sponge::Absorb;
use ark_ec::{CurveGroup, Group};
use ark_ff::PrimeField;
use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar};
use ark_snark::SNARK;
use ark_std::rand::CryptoRng;
use ark_std::rand::RngCore;
use core::marker::PhantomData;
pub use super::decider_eth_circuit::DeciderEthCircuit;
use crate::commitment::{pedersen::Params as PedersenParams, CommitmentProver};
use crate::folding::circuits::nonnative::point_to_nonnative_limbs_custom_opt;
use crate::folding::nova::{circuits::CF2, CommittedInstance, Nova};
use crate::frontend::FCircuit;
use crate::Error;
use crate::{Decider as DeciderTrait, FoldingScheme};
use ark_r1cs_std::fields::nonnative::params::OptimizationType;
/// Onchain Decider, for ethereum use cases
#[derive(Clone, Debug)]
pub struct Decider<C1, GC1, C2, GC2, FC, CP1, CP2, S, FS> {
_c1: PhantomData<C1>,
_gc1: PhantomData<GC1>,
_c2: PhantomData<C2>,
_gc2: PhantomData<GC2>,
_fc: PhantomData<FC>,
_cp1: PhantomData<CP1>,
_cp2: PhantomData<CP2>,
_s: PhantomData<S>,
_fs: PhantomData<FS>,
}
impl<C1, GC1, C2, GC2, FC, CP1, CP2, S, FS> DeciderTrait<C1, C2, FC, FS>
for Decider<C1, GC1, C2, GC2, FC, CP1, CP2, S, FS>
where
C1: CurveGroup,
C2: CurveGroup,
GC1: CurveVar<C1, CF2<C1>>,
GC2: CurveVar<C2, CF2<C2>>,
FC: FCircuit<C1::ScalarField>,
CP1: CommitmentProver<C1>,
// enforce that the CP2 is Pedersen commitment, since we're at Ethereum's EVM decider
CP2: CommitmentProver<C2, Params = PedersenParams<C2>>,
S: SNARK<C1::ScalarField>,
FS: FoldingScheme<C1, C2, FC>,
<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<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>,
// constrain FS into Nova, since this is a Decider specificly for Nova
Nova<C1, GC1, C2, GC2, FC, CP1, CP2>: From<FS>,
{
type ProverParam = S::ProvingKey;
type Proof = S::Proof;
type VerifierParam = S::VerifyingKey;
type PublicInput = Vec<C1::ScalarField>;
type CommittedInstanceWithWitness = ();
type CommittedInstance = CommittedInstance<C1>;
fn prove(
pp: &Self::ProverParam,
mut rng: impl RngCore + CryptoRng,
folding_scheme: FS,
) -> Result<Self::Proof, Error> {
let circuit =
DeciderEthCircuit::<C1, GC1, C2, GC2, CP1, CP2>::from_nova::<FC>(folding_scheme.into());
let proof = S::prove(pp, circuit.clone(), &mut rng).unwrap();
Ok(proof)
}
fn verify(
vp: &Self::VerifierParam,
i: C1::ScalarField,
z_0: Vec<C1::ScalarField>,
z_i: Vec<C1::ScalarField>,
running_instance: &Self::CommittedInstance,
proof: Self::Proof,
) -> Result<bool, Error> {
let (cmE_x, cmE_y) = point_to_nonnative_limbs_custom_opt::<C1>(
running_instance.cmE,
OptimizationType::Constraints,
)?;
let (cmW_x, cmW_y) = point_to_nonnative_limbs_custom_opt::<C1>(
running_instance.cmW,
OptimizationType::Constraints,
)?;
let public_input: Vec<C1::ScalarField> = vec![
vec![i],
z_0,
z_i,
vec![running_instance.u],
running_instance.x.clone(),
cmE_x,
cmE_y,
cmW_x,
cmW_y,
]
.concat();
S::verify(vp, &public_input, &proof).map_err(|e| Error::Other(e.to_string()))
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use ark_groth16::Groth16;
use ark_mnt4_298::{constraints::G1Var as GVar, Fr, G1Projective as Projective, MNT4_298};
use ark_mnt6_298::{constraints::G1Var as GVar2, G1Projective as Projective2};
use std::time::Instant;
use crate::commitment::pedersen::Pedersen;
use crate::folding::nova::{get_pedersen_params_len, ProverParams};
use crate::frontend::tests::CubicFCircuit;
use crate::transcript::poseidon::poseidon_test_config;
// Note: since we're testing a big circuit, this test takes a bit more of computation and time,
// do not run in the normal CI.
// To run the test use `--ignored` flag, eg. `cargo test -- --ignored`
#[test]
#[ignore]
fn test_decider() {
type NOVA = Nova<
Projective,
GVar,
Projective2,
GVar2,
CubicFCircuit<Fr>,
Pedersen<Projective>,
Pedersen<Projective2>,
>;
type DECIDER = Decider<
Projective,
GVar,
Projective2,
GVar2,
CubicFCircuit<Fr>,
Pedersen<Projective>,
Pedersen<Projective2>,
Groth16<MNT4_298>, // here we define the Snark to use in the decider
NOVA, // here we define the FoldingScheme to use
>;
let mut rng = ark_std::test_rng();
let poseidon_config = poseidon_test_config::<Fr>();
let F_circuit = CubicFCircuit::<Fr>::new(());
let z_0 = vec![Fr::from(3_u32)];
let (cm_len, cf_cm_len) =
get_pedersen_params_len::<Projective, GVar, Projective2, GVar2, CubicFCircuit<Fr>>(
&poseidon_config,
F_circuit,
)
.unwrap();
let pedersen_params = Pedersen::<Projective>::new_params(&mut rng, cm_len);
let cf_pedersen_params = Pedersen::<Projective2>::new_params(&mut rng, cf_cm_len);
let start = Instant::now();
let prover_params =
ProverParams::<Projective, Projective2, Pedersen<Projective>, Pedersen<Projective2>> {
poseidon_config: poseidon_config.clone(),
cm_params: pedersen_params,
cf_cm_params: cf_pedersen_params,
};
println!("generating pedersen params, {:?}", start.elapsed());
// use Nova as FoldingScheme
let start = Instant::now();
let mut nova = NOVA::init(&prover_params, F_circuit, z_0.clone()).unwrap();
println!("Nova initialized, {:?}", start.elapsed());
let start = Instant::now();
nova.prove_step().unwrap();
println!("prove_step, {:?}", start.elapsed());
// generate Groth16 setup
let circuit = DeciderEthCircuit::<
Projective,
GVar,
Projective2,
GVar2,
Pedersen<Projective>,
Pedersen<Projective2>,
>::from_nova::<CubicFCircuit<Fr>>(nova.clone());
let mut rng = rand::rngs::OsRng;
let start = Instant::now();
let (pk, vk) =
Groth16::<MNT4_298>::circuit_specific_setup(circuit.clone(), &mut rng).unwrap();
println!("Groth16 setup, {:?}", start.elapsed());
// decider proof generation
let start = Instant::now();
let proof = DECIDER::prove(&pk, rng, nova.clone()).unwrap();
println!("Decider Groth16 prove, {:?}", start.elapsed());
// decider proof verification
let verified = DECIDER::verify(&vk, nova.i, nova.z_0, nova.z_i, &nova.U_i, proof).unwrap();
assert!(verified);
}
}

View File

@@ -0,0 +1,686 @@
/// This file implements the onchain (Ethereum's EVM) decider circuit. For non-ethereum use cases,
/// other more efficient approaches can be used.
use ark_crypto_primitives::crh::poseidon::constraints::CRHParametersVar;
use ark_crypto_primitives::sponge::{poseidon::PoseidonConfig, Absorb};
use ark_ec::{CurveGroup, Group};
use ark_ff::PrimeField;
use ark_r1cs_std::{
alloc::{AllocVar, AllocationMode},
boolean::Boolean,
eq::EqGadget,
fields::{fp::FpVar, nonnative::NonNativeFieldVar, FieldVar},
groups::GroupOpsBounds,
prelude::CurveVar,
ToConstraintFieldGadget,
};
use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, Namespace, SynthesisError};
use ark_std::{One, Zero};
use core::{borrow::Borrow, marker::PhantomData};
use crate::ccs::r1cs::R1CS;
use crate::commitment::{pedersen::Params as PedersenParams, CommitmentProver};
use crate::folding::nova::{
circuits::{CommittedInstanceVar, CF1, CF2},
CommittedInstance, Nova, Witness,
};
use crate::frontend::FCircuit;
use crate::utils::gadgets::{
hadamard, mat_vec_mul_sparse, vec_add, vec_scalar_mul, SparseMatrixVar,
};
#[derive(Debug, Clone)]
pub struct RelaxedR1CSGadget<F: PrimeField, CF: PrimeField, FV: FieldVar<F, CF>> {
_f: PhantomData<F>,
_cf: PhantomData<CF>,
_fv: PhantomData<FV>,
}
impl<F: PrimeField, CF: PrimeField, FV: FieldVar<F, CF>> RelaxedR1CSGadget<F, CF, FV> {
/// performs the RelaxedR1CS check (Az∘Bz==uCz+E)
pub fn check(
r1cs: R1CSVar<F, CF, FV>,
E: Vec<FV>,
u: FV,
z: Vec<FV>,
) -> Result<(), SynthesisError> {
let Az = mat_vec_mul_sparse(r1cs.A, z.clone());
let Bz = mat_vec_mul_sparse(r1cs.B, z.clone());
let Cz = mat_vec_mul_sparse(r1cs.C, z.clone());
let uCz = vec_scalar_mul(&Cz, &u);
let uCzE = vec_add(&uCz, &E)?;
let AzBz = hadamard(&Az, &Bz)?;
for i in 0..AzBz.len() {
AzBz[i].enforce_equal(&uCzE[i].clone())?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct R1CSVar<F: PrimeField, CF: PrimeField, FV: FieldVar<F, CF>> {
_f: PhantomData<F>,
_cf: PhantomData<CF>,
_fv: PhantomData<FV>,
pub A: SparseMatrixVar<F, CF, FV>,
pub B: SparseMatrixVar<F, CF, FV>,
pub C: SparseMatrixVar<F, CF, FV>,
}
impl<F, CF, FV> AllocVar<R1CS<F>, CF> for R1CSVar<F, CF, FV>
where
F: PrimeField,
CF: PrimeField,
FV: FieldVar<F, CF>,
{
fn new_variable<T: Borrow<R1CS<F>>>(
cs: impl Into<Namespace<CF>>,
f: impl FnOnce() -> Result<T, SynthesisError>,
_mode: AllocationMode,
) -> Result<Self, SynthesisError> {
f().and_then(|val| {
let cs = cs.into();
let A = SparseMatrixVar::<F, CF, FV>::new_constant(cs.clone(), &val.borrow().A)?;
let B = SparseMatrixVar::<F, CF, FV>::new_constant(cs.clone(), &val.borrow().B)?;
let C = SparseMatrixVar::<F, CF, FV>::new_constant(cs.clone(), &val.borrow().C)?;
Ok(Self {
_f: PhantomData,
_cf: PhantomData,
_fv: PhantomData,
A,
B,
C,
})
})
}
}
/// In-circuit representation of the Witness associated to the CommittedInstance.
#[derive(Debug, Clone)]
pub struct WitnessVar<C: CurveGroup> {
pub E: Vec<FpVar<C::ScalarField>>,
pub rE: FpVar<C::ScalarField>,
pub W: Vec<FpVar<C::ScalarField>>,
pub rW: FpVar<C::ScalarField>,
}
impl<C> AllocVar<Witness<C>, CF1<C>> for WitnessVar<C>
where
C: CurveGroup,
<C as ark_ec::CurveGroup>::BaseField: PrimeField,
{
fn new_variable<T: Borrow<Witness<C>>>(
cs: impl Into<Namespace<CF1<C>>>,
f: impl FnOnce() -> Result<T, SynthesisError>,
mode: AllocationMode,
) -> Result<Self, SynthesisError> {
f().and_then(|val| {
let cs = cs.into();
let E: Vec<FpVar<C::ScalarField>> =
Vec::new_variable(cs.clone(), || Ok(val.borrow().E.clone()), mode)?;
let rE =
FpVar::<C::ScalarField>::new_variable(cs.clone(), || Ok(val.borrow().rE), mode)?;
let W: Vec<FpVar<C::ScalarField>> =
Vec::new_variable(cs.clone(), || Ok(val.borrow().W.clone()), mode)?;
let rW =
FpVar::<C::ScalarField>::new_variable(cs.clone(), || Ok(val.borrow().rW), mode)?;
Ok(Self { E, rE, W, rW })
})
}
}
/// In-circuit representation of the Witness associated to the CommittedInstance, but with
/// non-native representation, since it is used to represent the CycleFold witness.
#[derive(Debug, Clone)]
pub struct CycleFoldWitnessVar<C: CurveGroup> {
pub E: Vec<NonNativeFieldVar<C::ScalarField, CF2<C>>>,
pub rE: NonNativeFieldVar<C::ScalarField, CF2<C>>,
pub W: Vec<NonNativeFieldVar<C::ScalarField, CF2<C>>>,
pub rW: NonNativeFieldVar<C::ScalarField, CF2<C>>,
}
impl<C> AllocVar<Witness<C>, CF2<C>> for CycleFoldWitnessVar<C>
where
C: CurveGroup,
<C as ark_ec::CurveGroup>::BaseField: PrimeField,
{
fn new_variable<T: Borrow<Witness<C>>>(
cs: impl Into<Namespace<CF2<C>>>,
f: impl FnOnce() -> Result<T, SynthesisError>,
mode: AllocationMode,
) -> Result<Self, SynthesisError> {
f().and_then(|val| {
let cs = cs.into();
let E: Vec<NonNativeFieldVar<C::ScalarField, CF2<C>>> =
Vec::new_variable(cs.clone(), || Ok(val.borrow().E.clone()), mode)?;
let rE = NonNativeFieldVar::<C::ScalarField, CF2<C>>::new_variable(
cs.clone(),
|| Ok(val.borrow().rE),
mode,
)?;
let W: Vec<NonNativeFieldVar<C::ScalarField, CF2<C>>> =
Vec::new_variable(cs.clone(), || Ok(val.borrow().W.clone()), mode)?;
let rW = NonNativeFieldVar::<C::ScalarField, CF2<C>>::new_variable(
cs.clone(),
|| Ok(val.borrow().rW),
mode,
)?;
Ok(Self { E, rE, W, rW })
})
}
}
/// Circuit that implements the in-circuit checks needed for the onchain (Ethereum's EVM)
/// verification.
#[derive(Clone, Debug)]
pub struct DeciderEthCircuit<C1, GC1, C2, GC2, CP1, CP2>
where
C1: CurveGroup,
GC1: CurveVar<C1, CF2<C1>>,
C2: CurveGroup,
GC2: CurveVar<C2, CF2<C2>>,
CP1: CommitmentProver<C1>,
CP2: CommitmentProver<C2>,
{
_c1: PhantomData<C1>,
_gc1: PhantomData<GC1>,
_c2: PhantomData<C2>,
_gc2: PhantomData<GC2>,
_cp1: PhantomData<CP1>,
_cp2: PhantomData<CP2>,
/// E vector's length of the Nova instance witness
pub E_len: usize,
/// E vector's length of the CycleFold instance witness
pub cf_E_len: usize,
/// R1CS of the Augmented Function circuit
pub r1cs: R1CS<C1::ScalarField>,
/// R1CS of the CycleFold circuit
pub cf_r1cs: R1CS<C2::ScalarField>,
/// CycleFold PedersenParams over C2
pub cf_pedersen_params: PedersenParams<C2>,
pub poseidon_config: PoseidonConfig<CF1<C1>>,
pub i: Option<CF1<C1>>,
/// initial state
pub z_0: Option<Vec<C1::ScalarField>>,
/// current i-th state
pub z_i: Option<Vec<C1::ScalarField>>,
/// Nova instances
pub u_i: Option<CommittedInstance<C1>>,
pub w_i: Option<Witness<C1>>,
pub U_i: Option<CommittedInstance<C1>>,
pub W_i: Option<Witness<C1>>,
/// CycleFold running instance
pub cf_U_i: Option<CommittedInstance<C2>>,
pub cf_W_i: Option<Witness<C2>>,
}
impl<C1, GC1, C2, GC2, CP1, CP2> DeciderEthCircuit<C1, GC1, C2, GC2, CP1, CP2>
where
C1: CurveGroup,
C2: CurveGroup,
GC1: CurveVar<C1, CF2<C1>>,
GC2: CurveVar<C2, CF2<C2>>,
CP1: CommitmentProver<C1>,
// enforce that the CP2 is Pedersen commitment, since we're at Ethereum's EVM decider
CP2: CommitmentProver<C2, Params = PedersenParams<C2>>,
{
pub fn from_nova<FC: FCircuit<C1::ScalarField>>(
nova: Nova<C1, GC1, C2, GC2, FC, CP1, CP2>,
) -> Self {
Self {
_c1: PhantomData,
_gc1: PhantomData,
_c2: PhantomData,
_gc2: PhantomData,
_cp1: PhantomData,
_cp2: PhantomData,
E_len: nova.W_i.E.len(),
cf_E_len: nova.cf_W_i.E.len(),
r1cs: nova.r1cs,
cf_r1cs: nova.cf_r1cs,
cf_pedersen_params: nova.cf_cm_params,
poseidon_config: nova.poseidon_config,
i: Some(nova.i),
z_0: Some(nova.z_0),
z_i: Some(nova.z_i),
u_i: Some(nova.u_i),
w_i: Some(nova.w_i),
U_i: Some(nova.U_i),
W_i: Some(nova.W_i),
cf_U_i: Some(nova.cf_U_i),
cf_W_i: Some(nova.cf_W_i),
}
}
}
impl<C1, GC1, C2, GC2, CP1, CP2> ConstraintSynthesizer<CF1<C1>>
for DeciderEthCircuit<C1, GC1, C2, GC2, CP1, CP2>
where
C1: CurveGroup,
C2: CurveGroup,
GC1: CurveVar<C1, CF2<C1>>,
GC2: CurveVar<C2, CF2<C2>>,
CP1: CommitmentProver<C1>,
CP2: CommitmentProver<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<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>,
{
fn generate_constraints(self, cs: ConstraintSystemRef<CF1<C1>>) -> Result<(), SynthesisError> {
let r1cs =
R1CSVar::<C1::ScalarField, CF1<C1>, FpVar<CF1<C1>>>::new_witness(cs.clone(), || {
Ok(self.r1cs.clone())
})?;
let i =
FpVar::<CF1<C1>>::new_input(cs.clone(), || Ok(self.i.unwrap_or_else(CF1::<C1>::zero)))?;
let z_0 = Vec::<FpVar<CF1<C1>>>::new_input(cs.clone(), || {
Ok(self.z_0.unwrap_or(vec![CF1::<C1>::zero()]))
})?;
let z_i = Vec::<FpVar<CF1<C1>>>::new_input(cs.clone(), || {
Ok(self.z_i.unwrap_or(vec![CF1::<C1>::zero()]))
})?;
let u_dummy_native = CommittedInstance::<C1>::dummy(1);
let w_dummy_native = Witness::<C1>::new(
vec![C1::ScalarField::zero(); self.r1cs.A.n_cols - 2 /* (2=1+1, since u_i.x.len=1) */],
self.E_len,
);
let u_i = CommittedInstanceVar::<C1>::new_witness(cs.clone(), || {
Ok(self.u_i.unwrap_or(u_dummy_native.clone()))
})?;
let w_i = WitnessVar::<C1>::new_witness(cs.clone(), || {
Ok(self.w_i.unwrap_or(w_dummy_native.clone()))
})?;
let U_i = CommittedInstanceVar::<C1>::new_input(cs.clone(), || {
Ok(self.U_i.unwrap_or(u_dummy_native.clone()))
})?;
let W_i = WitnessVar::<C1>::new_witness(cs.clone(), || {
Ok(self.W_i.unwrap_or(w_dummy_native.clone()))
})?;
let crh_params = CRHParametersVar::<C1::ScalarField>::new_constant(
cs.clone(),
self.poseidon_config.clone(),
)?;
// 1. check RelaxedR1CS of u_i
let z_u: Vec<FpVar<CF1<C1>>> = [
vec![FpVar::<CF1<C1>>::one()],
u_i.x.to_vec(),
w_i.W.to_vec(),
]
.concat();
RelaxedR1CSGadget::<C1::ScalarField, CF1<C1>, FpVar<CF1<C1>>>::check(
r1cs.clone(),
w_i.E,
u_i.u.clone(),
z_u,
)?;
// 2. check RelaxedR1CS of U_i
let z_U: Vec<FpVar<CF1<C1>>> =
[vec![U_i.u.clone()], U_i.x.to_vec(), W_i.W.to_vec()].concat();
RelaxedR1CSGadget::<C1::ScalarField, CF1<C1>, FpVar<CF1<C1>>>::check(
r1cs,
W_i.E,
U_i.u.clone(),
z_U,
)?;
// 3. u_i.cmE==cm(0), u_i.u==1
// Here zero_x & zero_y are the x & y coordinates of the zero point affine representation.
let zero_x = NonNativeFieldVar::<C1::BaseField, C1::ScalarField>::new_constant(
cs.clone(),
C1::BaseField::zero(),
)?
.to_constraint_field()?;
let zero_y = NonNativeFieldVar::<C1::BaseField, C1::ScalarField>::new_constant(
cs.clone(),
C1::BaseField::one(),
)?
.to_constraint_field()?;
(u_i.cmE.x.is_eq(&zero_x)?).enforce_equal(&Boolean::TRUE)?;
(u_i.cmE.y.is_eq(&zero_y)?).enforce_equal(&Boolean::TRUE)?;
(u_i.u.is_one()?).enforce_equal(&Boolean::TRUE)?;
// 4. u_i.x == H(i, z_0, z_i, U_i)
let u_i_x = U_i
.clone()
.hash(&crh_params, i.clone(), z_0.clone(), z_i.clone())?;
(u_i.x[0]).enforce_equal(&u_i_x)?;
// The following two checks (and their respective allocations) are disabled for normal
// tests since they take ~24.5M constraints and would take several minutes (and RAM) to run
// the test
#[cfg(not(test))]
{
// imports here instead of at the top of the file, so we avoid having multiple
// `#[cfg(not(test))]`
use crate::commitment::pedersen::PedersenGadget;
use crate::folding::nova::cyclefold::{CycleFoldCommittedInstanceVar, CF_IO_LEN};
use ark_r1cs_std::ToBitsGadget;
let cf_r1cs = R1CSVar::<
C1::BaseField,
CF1<C1>,
NonNativeFieldVar<C1::BaseField, CF1<C1>>,
>::new_witness(cs.clone(), || Ok(self.cf_r1cs.clone()))?;
let cf_u_dummy_native = CommittedInstance::<C2>::dummy(CF_IO_LEN);
let w_dummy_native = Witness::<C2>::new(
vec![C2::ScalarField::zero(); self.cf_r1cs.A.n_cols - 1 - self.cf_r1cs.l],
self.cf_E_len,
);
let cf_U_i = CycleFoldCommittedInstanceVar::<C2, GC2>::new_witness(cs.clone(), || {
Ok(self.cf_U_i.unwrap_or_else(|| cf_u_dummy_native.clone()))
})?;
let cf_W_i = CycleFoldWitnessVar::<C2>::new_witness(cs.clone(), || {
Ok(self.cf_W_i.unwrap_or(w_dummy_native.clone()))
})?;
// 5. check Pedersen commitments of cf_U_i.{cmE, cmW}
let H = GC2::new_constant(cs.clone(), self.cf_pedersen_params.h)?;
let G = Vec::<GC2>::new_constant(cs.clone(), self.cf_pedersen_params.generators)?;
let cf_W_i_E_bits: Vec<Vec<Boolean<CF1<C1>>>> = cf_W_i
.E
.iter()
.map(|E_i| E_i.to_bits_le().unwrap())
.collect();
let cf_W_i_W_bits: Vec<Vec<Boolean<CF1<C1>>>> = cf_W_i
.W
.iter()
.map(|W_i| W_i.to_bits_le().unwrap())
.collect();
let computed_cmE = PedersenGadget::<C2, GC2>::commit(
H.clone(),
G.clone(),
cf_W_i_E_bits,
cf_W_i.rE.to_bits_le()?,
)?;
cf_U_i.cmE.enforce_equal(&computed_cmE)?;
let computed_cmW =
PedersenGadget::<C2, GC2>::commit(H, G, cf_W_i_W_bits, cf_W_i.rW.to_bits_le()?)?;
cf_U_i.cmW.enforce_equal(&computed_cmW)?;
// 6. check RelaxedR1CS of cf_U_i
let cf_z_U: Vec<NonNativeFieldVar<C2::ScalarField, CF1<C1>>> =
[vec![cf_U_i.u.clone()], cf_U_i.x.to_vec(), cf_W_i.W.to_vec()].concat();
RelaxedR1CSGadget::<
C2::ScalarField,
CF1<C1>,
NonNativeFieldVar<C2::ScalarField, CF1<C1>>,
>::check(cf_r1cs, cf_W_i.E, cf_U_i.u.clone(), cf_z_U)?;
}
Ok(())
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use ark_crypto_primitives::crh::{
sha256::{
constraints::{Sha256Gadget, UnitVar},
Sha256,
},
CRHScheme, CRHSchemeGadget,
};
use ark_ff::BigInteger;
use ark_pallas::{constraints::GVar, Fq, Fr, Projective};
use ark_r1cs_std::{
alloc::AllocVar,
bits::uint8::UInt8,
eq::EqGadget,
fields::{fp::FpVar, nonnative::NonNativeFieldVar},
};
use ark_relations::r1cs::ConstraintSystem;
use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2};
use crate::commitment::pedersen::Pedersen;
use crate::folding::nova::{get_pedersen_params_len, ProverParams, VerifierParams};
use crate::frontend::tests::{CubicFCircuit, CustomFCircuit, WrapperCircuit};
use crate::transcript::poseidon::poseidon_test_config;
use crate::FoldingScheme;
use crate::ccs::r1cs::{extract_r1cs, extract_w_x};
use crate::ccs::r1cs::{
tests::{get_test_r1cs, get_test_z},
R1CS,
};
#[test]
fn test_relaxed_r1cs_small_gadget_handcrafted() {
let r1cs: R1CS<Fr> = get_test_r1cs();
let rel_r1cs = r1cs.clone().relax();
let z = get_test_z(3);
let cs = ConstraintSystem::<Fr>::new_ref();
let zVar = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z)).unwrap();
let EVar = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(rel_r1cs.E)).unwrap();
let uVar = FpVar::<Fr>::new_witness(cs.clone(), || Ok(rel_r1cs.u)).unwrap();
let r1csVar = R1CSVar::<Fr, Fr, FpVar<Fr>>::new_witness(cs.clone(), || Ok(r1cs)).unwrap();
RelaxedR1CSGadget::<Fr, Fr, FpVar<Fr>>::check(r1csVar, EVar, uVar, zVar).unwrap();
assert!(cs.is_satisfied().unwrap());
}
// gets as input a circuit that implements the ConstraintSynthesizer trait, and that has been
// initialized.
fn test_relaxed_r1cs_gadget<CS: ConstraintSynthesizer<Fr>>(circuit: CS) {
let cs = ConstraintSystem::<Fr>::new_ref();
circuit.generate_constraints(cs.clone()).unwrap();
cs.finalize();
assert!(cs.is_satisfied().unwrap());
let cs = cs.into_inner().unwrap();
let r1cs = extract_r1cs::<Fr>(&cs);
let (w, x) = extract_w_x::<Fr>(&cs);
let z = [vec![Fr::one()], x, w].concat();
r1cs.check_relation(&z).unwrap();
let relaxed_r1cs = r1cs.clone().relax();
relaxed_r1cs.check_relation(&z).unwrap();
// set new CS for the circuit that checks the RelaxedR1CS of our original circuit
let cs = ConstraintSystem::<Fr>::new_ref();
// prepare the inputs for our circuit
let zVar = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(z)).unwrap();
let EVar = Vec::<FpVar<Fr>>::new_witness(cs.clone(), || Ok(relaxed_r1cs.E)).unwrap();
let uVar = FpVar::<Fr>::new_witness(cs.clone(), || Ok(relaxed_r1cs.u)).unwrap();
let r1csVar = R1CSVar::<Fr, Fr, FpVar<Fr>>::new_witness(cs.clone(), || Ok(r1cs)).unwrap();
RelaxedR1CSGadget::<Fr, Fr, FpVar<Fr>>::check(r1csVar, EVar, uVar, zVar).unwrap();
assert!(cs.is_satisfied().unwrap());
}
#[test]
fn test_relaxed_r1cs_small_gadget_arkworks() {
let z_i = vec![Fr::from(3_u32)];
let cubic_circuit = CubicFCircuit::<Fr>::new(());
let circuit = WrapperCircuit::<Fr, CubicFCircuit<Fr>> {
FC: cubic_circuit,
z_i: Some(z_i.clone()),
z_i1: Some(cubic_circuit.step_native(z_i).unwrap()),
};
test_relaxed_r1cs_gadget(circuit);
}
struct Sha256TestCircuit<F: PrimeField> {
_f: PhantomData<F>,
pub x: Vec<u8>,
pub y: Vec<u8>,
}
impl<F: PrimeField> ConstraintSynthesizer<F> for Sha256TestCircuit<F> {
fn generate_constraints(self, cs: ConstraintSystemRef<F>) -> Result<(), SynthesisError> {
let x = Vec::<UInt8<F>>::new_witness(cs.clone(), || Ok(self.x))?;
let y = Vec::<UInt8<F>>::new_input(cs.clone(), || Ok(self.y))?;
let unitVar = UnitVar::default();
let comp_y = <Sha256Gadget<F> as CRHSchemeGadget<Sha256, F>>::evaluate(&unitVar, &x)?;
comp_y.0.enforce_equal(&y)?;
Ok(())
}
}
#[test]
fn test_relaxed_r1cs_medium_gadget_arkworks() {
let x = Fr::from(5_u32).into_bigint().to_bytes_le();
let y = <Sha256 as CRHScheme>::evaluate(&(), x.clone()).unwrap();
let circuit = Sha256TestCircuit::<Fr> {
_f: PhantomData,
x,
y,
};
test_relaxed_r1cs_gadget(circuit);
}
#[test]
fn test_relaxed_r1cs_custom_circuit() {
let n_constraints = 10_000;
let custom_circuit = CustomFCircuit::<Fr>::new(n_constraints);
let z_i = vec![Fr::from(5_u32)];
let circuit = WrapperCircuit::<Fr, CustomFCircuit<Fr>> {
FC: custom_circuit,
z_i: Some(z_i.clone()),
z_i1: Some(custom_circuit.step_native(z_i).unwrap()),
};
test_relaxed_r1cs_gadget(circuit);
}
#[test]
fn test_relaxed_r1cs_nonnative_circuit() {
let cs = ConstraintSystem::<Fq>::new_ref();
// in practice we would use CycleFoldCircuit, but is a very big circuit (when computed
// non-natively inside the RelaxedR1CS circuit), so in order to have a short test we use a
// custom circuit.
let custom_circuit = CustomFCircuit::<Fq>::new(10);
let z_i = vec![Fq::from(5_u32)];
let circuit = WrapperCircuit::<Fq, CustomFCircuit<Fq>> {
FC: custom_circuit,
z_i: Some(z_i.clone()),
z_i1: Some(custom_circuit.step_native(z_i).unwrap()),
};
circuit.generate_constraints(cs.clone()).unwrap();
cs.finalize();
let cs = cs.into_inner().unwrap();
let r1cs = extract_r1cs::<Fq>(&cs);
let (w, x) = extract_w_x::<Fq>(&cs);
let z = [vec![Fq::one()], x, w].concat();
let relaxed_r1cs = r1cs.clone().relax();
// natively
let cs = ConstraintSystem::<Fq>::new_ref();
let zVar = Vec::<FpVar<Fq>>::new_witness(cs.clone(), || Ok(z.clone())).unwrap();
let EVar =
Vec::<FpVar<Fq>>::new_witness(cs.clone(), || Ok(relaxed_r1cs.clone().E)).unwrap();
let uVar = FpVar::<Fq>::new_witness(cs.clone(), || Ok(relaxed_r1cs.u)).unwrap();
let r1csVar =
R1CSVar::<Fq, Fq, FpVar<Fq>>::new_witness(cs.clone(), || Ok(r1cs.clone())).unwrap();
RelaxedR1CSGadget::<Fq, Fq, FpVar<Fq>>::check(r1csVar, EVar, uVar, zVar).unwrap();
// non-natively
let cs = ConstraintSystem::<Fr>::new_ref();
let zVar = Vec::<NonNativeFieldVar<Fq, Fr>>::new_witness(cs.clone(), || Ok(z)).unwrap();
let EVar = Vec::<NonNativeFieldVar<Fq, Fr>>::new_witness(cs.clone(), || Ok(relaxed_r1cs.E))
.unwrap();
let uVar =
NonNativeFieldVar::<Fq, Fr>::new_witness(cs.clone(), || Ok(relaxed_r1cs.u)).unwrap();
let r1csVar =
R1CSVar::<Fq, Fr, NonNativeFieldVar<Fq, Fr>>::new_witness(cs.clone(), || Ok(r1cs))
.unwrap();
RelaxedR1CSGadget::<Fq, Fr, NonNativeFieldVar<Fq, Fr>>::check(r1csVar, EVar, uVar, zVar)
.unwrap();
}
#[test]
fn test_decider_circuit() {
let mut rng = ark_std::test_rng();
let poseidon_config = poseidon_test_config::<Fr>();
let F_circuit = CubicFCircuit::<Fr>::new(());
let z_0 = vec![Fr::from(3_u32)];
// get the CM & CF_CM len
let (cm_len, cf_cm_len) =
get_pedersen_params_len::<Projective, GVar, Projective2, GVar2, CubicFCircuit<Fr>>(
&poseidon_config,
F_circuit,
)
.unwrap();
let pedersen_params = Pedersen::<Projective>::new_params(&mut rng, cm_len);
let cf_pedersen_params = Pedersen::<Projective2>::new_params(&mut rng, cf_cm_len);
let prover_params =
ProverParams::<Projective, Projective2, Pedersen<Projective>, Pedersen<Projective2>> {
poseidon_config: poseidon_config.clone(),
cm_params: pedersen_params,
cf_cm_params: cf_pedersen_params,
};
type NOVA = Nova<
Projective,
GVar,
Projective2,
GVar2,
CubicFCircuit<Fr>,
Pedersen<Projective>,
Pedersen<Projective2>,
>;
// generate a Nova instance and do a step of it
let mut nova = NOVA::init(&prover_params, F_circuit, z_0.clone()).unwrap();
nova.prove_step().unwrap();
let ivc_v = nova.clone();
let verifier_params = VerifierParams::<Projective, Projective2> {
poseidon_config: poseidon_config.clone(),
r1cs: ivc_v.r1cs,
cf_r1cs: ivc_v.cf_r1cs,
};
NOVA::verify(
verifier_params,
z_0,
ivc_v.z_i,
Fr::one(),
(ivc_v.U_i, ivc_v.W_i),
(ivc_v.u_i, ivc_v.w_i),
(ivc_v.cf_U_i, ivc_v.cf_W_i),
)
.unwrap();
// load the DeciderEthCircuit from the generated Nova instance
let decider_circuit = DeciderEthCircuit::<
Projective,
GVar,
Projective2,
GVar2,
Pedersen<Projective>,
Pedersen<Projective2>,
>::from_nova(nova);
let cs = ConstraintSystem::<Fr>::new_ref();
// generate the constraints and check that are satisfied by the inputs
decider_circuit.generate_constraints(cs.clone()).unwrap();
assert!(cs.is_satisfied().unwrap());
dbg!(cs.num_constraints());
}
}

View File

@@ -0,0 +1,717 @@
/// Implements the scheme described in [Nova](https://eprint.iacr.org/2021/370.pdf) and
/// [CycleFold](https://eprint.iacr.org/2023/1192.pdf).
use ark_crypto_primitives::{
crh::{poseidon::CRH, CRHScheme},
sponge::{poseidon::PoseidonConfig, Absorb},
};
use ark_ec::{AffineRepr, CurveGroup, Group};
use ark_ff::{BigInteger, PrimeField};
use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar};
use ark_std::fmt::Debug;
use ark_std::{One, Zero};
use core::marker::PhantomData;
use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem};
use crate::ccs::r1cs::{extract_r1cs, extract_w_x, R1CS};
use crate::commitment::CommitmentProver;
use crate::folding::circuits::nonnative::point_to_nonnative_limbs;
use crate::frontend::FCircuit;
use crate::utils::vec::is_zero_vec;
use crate::Error;
use crate::FoldingScheme;
pub mod circuits;
pub mod cyclefold;
pub mod decider_eth;
pub mod decider_eth_circuit;
pub mod nifs;
pub mod traits;
use circuits::{AugmentedFCircuit, ChallengeGadget, CF2};
use cyclefold::{CycleFoldChallengeGadget, CycleFoldCircuit};
use nifs::NIFS;
use traits::NovaR1CS;
#[cfg(test)]
use cyclefold::CF_IO_LEN;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct CommittedInstance<C: CurveGroup> {
pub cmE: C,
pub u: C::ScalarField,
pub cmW: C,
pub x: Vec<C::ScalarField>,
}
impl<C: CurveGroup> CommittedInstance<C> {
pub fn dummy(io_len: usize) -> Self {
Self {
cmE: C::zero(),
u: C::ScalarField::zero(),
cmW: C::zero(),
x: vec![C::ScalarField::zero(); io_len],
}
}
}
impl<C: CurveGroup> CommittedInstance<C>
where
<C as Group>::ScalarField: Absorb,
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField,
{
/// hash implements the committed instance hash compatible with the gadget implemented in
/// nova/circuits.rs::CommittedInstanceVar.hash.
/// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and `U_i` is the
/// `CommittedInstance`.
pub fn hash(
&self,
poseidon_config: &PoseidonConfig<C::ScalarField>,
i: C::ScalarField,
z_0: Vec<C::ScalarField>,
z_i: Vec<C::ScalarField>,
) -> Result<C::ScalarField, Error> {
let (cmE_x, cmE_y) = point_to_nonnative_limbs::<C>(self.cmE)?;
let (cmW_x, cmW_y) = point_to_nonnative_limbs::<C>(self.cmW)?;
CRH::<C::ScalarField>::evaluate(
poseidon_config,
vec![
vec![i],
z_0,
z_i,
vec![self.u],
self.x.clone(),
cmE_x,
cmE_y,
cmW_x,
cmW_y,
]
.concat(),
)
.map_err(|e| Error::Other(e.to_string()))
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Witness<C: CurveGroup> {
pub E: Vec<C::ScalarField>,
pub rE: C::ScalarField,
pub W: Vec<C::ScalarField>,
pub rW: C::ScalarField,
}
impl<C: CurveGroup> Witness<C>
where
<C as Group>::ScalarField: Absorb,
{
pub fn new(w: Vec<C::ScalarField>, e_len: usize) -> Self {
// note: at the current version, we don't use the blinding factors and we set them to 0
// always.
Self {
E: vec![C::ScalarField::zero(); e_len],
rE: C::ScalarField::zero(),
W: w,
rW: C::ScalarField::zero(),
}
}
pub fn commit<CP: CommitmentProver<C>>(
&self,
params: &CP::Params,
x: Vec<C::ScalarField>,
) -> Result<CommittedInstance<C>, Error> {
let mut cmE = C::zero();
if !is_zero_vec::<C::ScalarField>(&self.E) {
cmE = CP::commit(params, &self.E, &self.rE)?;
}
let cmW = CP::commit(params, &self.W, &self.rW)?;
Ok(CommittedInstance {
cmE,
u: C::ScalarField::one(),
cmW,
x,
})
}
}
#[derive(Debug, Clone)]
pub struct ProverParams<C1, C2, CP1, CP2>
where
C1: CurveGroup,
C2: CurveGroup,
CP1: CommitmentProver<C1>,
CP2: CommitmentProver<C2>,
{
pub poseidon_config: PoseidonConfig<C1::ScalarField>,
pub cm_params: CP1::Params,
pub cf_cm_params: CP2::Params,
}
#[derive(Debug, Clone)]
pub struct VerifierParams<C1: CurveGroup, C2: CurveGroup> {
pub poseidon_config: PoseidonConfig<C1::ScalarField>,
pub r1cs: R1CS<C1::ScalarField>,
pub cf_r1cs: R1CS<C2::ScalarField>,
}
/// Implements Nova+CycleFold's IVC, described in [Nova](https://eprint.iacr.org/2021/370.pdf) and
/// [CycleFold](https://eprint.iacr.org/2023/1192.pdf), following the FoldingScheme trait
#[derive(Clone, Debug)]
pub struct Nova<C1, GC1, C2, GC2, FC, CP1, CP2>
where
C1: CurveGroup,
GC1: CurveVar<C1, CF2<C1>>,
C2: CurveGroup,
GC2: CurveVar<C2, CF2<C2>>,
FC: FCircuit<C1::ScalarField>,
CP1: CommitmentProver<C1>,
CP2: CommitmentProver<C2>,
{
_gc1: PhantomData<GC1>,
_c2: PhantomData<C2>,
_gc2: PhantomData<GC2>,
/// R1CS of the Augmented Function circuit
pub r1cs: R1CS<C1::ScalarField>,
/// R1CS of the CycleFold circuit
pub cf_r1cs: R1CS<C2::ScalarField>,
pub poseidon_config: PoseidonConfig<C1::ScalarField>,
/// CommitmentProver::Params over C1
pub cm_params: CP1::Params,
/// CycleFold CommitmentProver::Params, over C2
pub cf_cm_params: CP2::Params,
/// F circuit, the circuit that is being folded
pub F: FC,
pub i: C1::ScalarField,
/// initial state
pub z_0: Vec<C1::ScalarField>,
/// current i-th state
pub z_i: Vec<C1::ScalarField>,
/// Nova instances
pub w_i: Witness<C1>,
pub u_i: CommittedInstance<C1>,
pub W_i: Witness<C1>,
pub U_i: CommittedInstance<C1>,
/// CycleFold running instance
pub cf_W_i: Witness<C2>,
pub cf_U_i: CommittedInstance<C2>,
}
impl<C1, GC1, C2, GC2, FC, CP1, CP2> FoldingScheme<C1, C2, FC>
for Nova<C1, GC1, C2, GC2, FC, CP1, CP2>
where
C1: CurveGroup,
GC1: CurveVar<C1, CF2<C1>>,
C2: CurveGroup,
GC2: CurveVar<C2, CF2<C2>>,
FC: FCircuit<C1::ScalarField>,
CP1: CommitmentProver<C1>,
CP2: CommitmentProver<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 PreprocessorParam = (Self::ProverParam, FC);
type ProverParam = ProverParams<C1, C2, CP1, CP2>;
type VerifierParam = VerifierParams<C1, C2>;
type CommittedInstanceWithWitness = (CommittedInstance<C1>, Witness<C1>);
type CFCommittedInstanceWithWitness = (CommittedInstance<C2>, Witness<C2>);
fn preprocess(
prep_param: &Self::PreprocessorParam,
) -> Result<(Self::ProverParam, Self::VerifierParam), Error> {
let (prover_params, F_circuit) = prep_param;
let (r1cs, cf_r1cs) =
get_r1cs::<C1, GC1, C2, GC2, FC>(&prover_params.poseidon_config, *F_circuit)?;
let verifier_params = VerifierParams::<C1, C2> {
poseidon_config: prover_params.poseidon_config.clone(),
r1cs,
cf_r1cs,
};
Ok((prover_params.clone(), verifier_params))
}
/// Initializes the Nova+CycleFold's IVC for the given parameters and initial state `z_0`.
fn init(pp: &Self::ProverParam, F: FC, z_0: Vec<C1::ScalarField>) -> Result<Self, Error> {
// prepare the circuit to obtain its R1CS
let cs = ConstraintSystem::<C1::ScalarField>::new_ref();
let cs2 = ConstraintSystem::<C1::BaseField>::new_ref();
let augmented_F_circuit =
AugmentedFCircuit::<C1, C2, GC2, FC>::empty(&pp.poseidon_config, F);
let cf_circuit = CycleFoldCircuit::<C1, GC1>::empty();
augmented_F_circuit.generate_constraints(cs.clone())?;
cs.finalize();
let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?;
let r1cs = extract_r1cs::<C1::ScalarField>(&cs);
cf_circuit.generate_constraints(cs2.clone())?;
cs2.finalize();
let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?;
let cf_r1cs = extract_r1cs::<C1::BaseField>(&cs2);
// setup the dummy instances
let (w_dummy, u_dummy) = r1cs.dummy_instance();
let (cf_w_dummy, cf_u_dummy) = cf_r1cs.dummy_instance();
// W_dummy=W_0 is a 'dummy witness', all zeroes, but with the size corresponding to the
// R1CS that we're working with.
Ok(Self {
_gc1: PhantomData,
_c2: PhantomData,
_gc2: PhantomData,
r1cs,
cf_r1cs,
poseidon_config: pp.poseidon_config.clone(),
cm_params: pp.cm_params.clone(),
cf_cm_params: pp.cf_cm_params.clone(),
F,
i: C1::ScalarField::zero(),
z_0: z_0.clone(),
z_i: z_0,
w_i: w_dummy.clone(),
u_i: u_dummy.clone(),
W_i: w_dummy,
U_i: u_dummy,
// cyclefold running instance
cf_W_i: cf_w_dummy.clone(),
cf_U_i: cf_u_dummy.clone(),
})
}
/// Implements IVC.P of Nova+CycleFold
fn prove_step(&mut self) -> Result<(), Error> {
let augmented_F_circuit: AugmentedFCircuit<C1, C2, GC2, FC>;
let cf_circuit: CycleFoldCircuit<C1, GC1>;
let z_i1 = self.F.step_native(self.z_i.clone())?;
// compute T and cmT for AugmentedFCircuit
let (T, cmT) = self.compute_cmT()?;
let r_bits = ChallengeGadget::<C1>::get_challenge_native(
&self.poseidon_config,
self.u_i.clone(),
self.U_i.clone(),
cmT,
)?;
let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits))
.ok_or(Error::OutOfBounds)?;
// fold Nova instances
let (W_i1, U_i1): (Witness<C1>, CommittedInstance<C1>) = NIFS::<C1, CP1>::fold_instances(
r_Fr, &self.w_i, &self.u_i, &self.W_i, &self.U_i, &T, cmT,
)?;
// folded instance output (public input, x)
// u_{i+1}.x = H(i+1, z_0, z_{i+1}, U_{i+1})
let u_i1_x = U_i1.hash(
&self.poseidon_config,
self.i + C1::ScalarField::one(),
self.z_0.clone(),
z_i1.clone(),
)?;
if self.i == C1::ScalarField::zero() {
// base case
augmented_F_circuit = AugmentedFCircuit::<C1, C2, GC2, FC> {
_gc2: PhantomData,
poseidon_config: self.poseidon_config.clone(),
i: Some(C1::ScalarField::zero()), // = i=0
z_0: Some(self.z_0.clone()), // = z_i
z_i: Some(self.z_i.clone()),
u_i: Some(self.u_i.clone()), // = dummy
U_i: Some(self.U_i.clone()), // = dummy
U_i1: Some(U_i1.clone()),
cmT: Some(cmT),
F: self.F,
x: Some(u_i1_x),
cf_u_i: None,
cf_U_i: None,
cf_U_i1: None,
cf_cmT: None,
cf_r_nonnat: None,
};
#[cfg(test)]
NIFS::<C1, CP1>::verify_folded_instance(r_Fr, &self.u_i, &self.U_i, &U_i1, &cmT)?;
} else {
// CycleFold part:
// get the vector used as public inputs 'x' in the CycleFold circuit
let cf_u_i_x = [
get_committed_instance_coordinates(&self.u_i),
get_committed_instance_coordinates(&self.U_i),
get_committed_instance_coordinates(&U_i1),
]
.concat();
cf_circuit = CycleFoldCircuit::<C1, GC1> {
_gc: PhantomData,
r_bits: Some(r_bits.clone()),
cmT: Some(cmT),
u_i: Some(self.u_i.clone()),
U_i: Some(self.U_i.clone()),
U_i1: Some(U_i1.clone()),
x: Some(cf_u_i_x.clone()),
};
let cs2 = ConstraintSystem::<C1::BaseField>::new_ref();
cf_circuit.generate_constraints(cs2.clone())?;
let cs2 = cs2.into_inner().ok_or(Error::NoInnerConstraintSystem)?;
let (cf_w_i, cf_x_i) = extract_w_x::<C1::BaseField>(&cs2);
if cf_x_i != cf_u_i_x {
return Err(Error::NotEqual);
}
#[cfg(test)]
if cf_x_i.len() != CF_IO_LEN {
return Err(Error::NotExpectedLength(cf_x_i.len(), CF_IO_LEN));
}
// fold cyclefold instances
let cf_w_i = Witness::<C2>::new(cf_w_i.clone(), self.cf_r1cs.A.n_rows);
let cf_u_i: CommittedInstance<C2> =
cf_w_i.commit::<CP2>(&self.cf_cm_params, cf_x_i.clone())?;
// compute T* and cmT* for CycleFoldCircuit
let (cf_T, cf_cmT) = self.compute_cf_cmT(&cf_w_i, &cf_u_i)?;
let cf_r_bits = CycleFoldChallengeGadget::<C2, GC2>::get_challenge_native(
&self.poseidon_config,
cf_u_i.clone(),
self.cf_U_i.clone(),
cf_cmT,
)?;
let cf_r_Fq = C1::BaseField::from_bigint(BigInteger::from_bits_le(&cf_r_bits))
.ok_or(Error::OutOfBounds)?;
let (cf_W_i1, cf_U_i1) = NIFS::<C2, CP2>::fold_instances(
cf_r_Fq,
&self.cf_W_i,
&self.cf_U_i,
&cf_w_i,
&cf_u_i,
&cf_T,
cf_cmT,
)?;
augmented_F_circuit = AugmentedFCircuit::<C1, C2, GC2, FC> {
_gc2: PhantomData,
poseidon_config: self.poseidon_config.clone(),
i: Some(self.i),
z_0: Some(self.z_0.clone()),
z_i: Some(self.z_i.clone()),
u_i: Some(self.u_i.clone()),
U_i: Some(self.U_i.clone()),
U_i1: Some(U_i1.clone()),
cmT: Some(cmT),
F: self.F,
x: Some(u_i1_x),
// cyclefold values
cf_u_i: Some(cf_u_i.clone()),
cf_U_i: Some(self.cf_U_i.clone()),
cf_U_i1: Some(cf_U_i1.clone()),
cf_cmT: Some(cf_cmT),
cf_r_nonnat: Some(cf_r_Fq),
};
self.cf_W_i = cf_W_i1.clone();
self.cf_U_i = cf_U_i1.clone();
#[cfg(test)]
{
self.cf_r1cs.check_instance_relation(&cf_w_i, &cf_u_i)?;
self.cf_r1cs
.check_relaxed_instance_relation(&self.cf_W_i, &self.cf_U_i)?;
}
}
let cs = ConstraintSystem::<C1::ScalarField>::new_ref();
augmented_F_circuit.generate_constraints(cs.clone())?;
let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?;
let (w_i1, x_i1) = extract_w_x::<C1::ScalarField>(&cs);
if x_i1[0] != u_i1_x {
return Err(Error::NotEqual);
}
#[cfg(test)]
if x_i1.len() != 1 {
return Err(Error::NotExpectedLength(x_i1.len(), 1));
}
// set values for next iteration
self.i += C1::ScalarField::one();
self.z_i = z_i1.clone();
self.w_i = Witness::<C1>::new(w_i1, self.r1cs.A.n_rows);
self.u_i = self.w_i.commit::<CP1>(&self.cm_params, vec![u_i1_x])?;
self.W_i = W_i1.clone();
self.U_i = U_i1.clone();
#[cfg(test)]
{
self.r1cs.check_instance_relation(&self.w_i, &self.u_i)?;
self.r1cs
.check_relaxed_instance_relation(&self.W_i, &self.U_i)?;
}
Ok(())
}
fn state(&self) -> Vec<C1::ScalarField> {
self.z_i.clone()
}
fn instances(
&self,
) -> (
Self::CommittedInstanceWithWitness,
Self::CommittedInstanceWithWitness,
Self::CFCommittedInstanceWithWitness,
) {
(
(self.U_i.clone(), self.W_i.clone()),
(self.u_i.clone(), self.w_i.clone()),
(self.cf_U_i.clone(), self.cf_W_i.clone()),
)
}
/// Implements IVC.V of Nova+CycleFold
fn verify(
vp: Self::VerifierParam,
z_0: Vec<C1::ScalarField>, // initial state
z_i: Vec<C1::ScalarField>, // last state
num_steps: C1::ScalarField,
running_instance: Self::CommittedInstanceWithWitness,
incomming_instance: Self::CommittedInstanceWithWitness,
cyclefold_instance: Self::CFCommittedInstanceWithWitness,
) -> Result<(), Error> {
let (U_i, W_i) = running_instance;
let (u_i, w_i) = incomming_instance;
let (cf_U_i, cf_W_i) = cyclefold_instance;
if u_i.x.len() != 1 || U_i.x.len() != 1 {
return Err(Error::IVCVerificationFail);
}
// check that u_i's output points to the running instance
// u_i.X == H(i, z_0, z_i, U_i)
let expected_u_i_x = U_i.hash(&vp.poseidon_config, num_steps, z_0, z_i.clone())?;
if expected_u_i_x != u_i.x[0] {
return Err(Error::IVCVerificationFail);
}
// check u_i.cmE==0, u_i.u==1 (=u_i is a un-relaxed instance)
if !u_i.cmE.is_zero() || !u_i.u.is_one() {
return Err(Error::IVCVerificationFail);
}
// check R1CS satisfiability
vp.r1cs.check_instance_relation(&w_i, &u_i)?;
// check RelaxedR1CS satisfiability
vp.r1cs.check_relaxed_instance_relation(&W_i, &U_i)?;
// check CycleFold RelaxedR1CS satisfiability
vp.cf_r1cs
.check_relaxed_instance_relation(&cf_W_i, &cf_U_i)?;
Ok(())
}
}
impl<C1, GC1, C2, GC2, FC, CP1, CP2> Nova<C1, GC1, C2, GC2, FC, CP1, CP2>
where
C1: CurveGroup,
GC1: CurveVar<C1, CF2<C1>>,
C2: CurveGroup,
GC2: CurveVar<C2, CF2<C2>>,
FC: FCircuit<C1::ScalarField>,
CP1: CommitmentProver<C1>,
CP2: CommitmentProver<C2>,
<C2 as CurveGroup>::BaseField: PrimeField,
<C1 as Group>::ScalarField: Absorb,
<C2 as Group>::ScalarField: Absorb,
C1: CurveGroup<BaseField = C2::ScalarField, ScalarField = C2::BaseField>,
{
// computes T and cmT for the AugmentedFCircuit
fn compute_cmT(&self) -> Result<(Vec<C1::ScalarField>, C1), Error> {
NIFS::<C1, CP1>::compute_cmT(
&self.cm_params,
&self.r1cs,
&self.w_i,
&self.u_i,
&self.W_i,
&self.U_i,
)
}
// computes T* and cmT* for the CycleFoldCircuit
fn compute_cf_cmT(
&self,
cf_w_i: &Witness<C2>,
cf_u_i: &CommittedInstance<C2>,
) -> Result<(Vec<C2::ScalarField>, C2), Error> {
NIFS::<C2, CP2>::compute_cyclefold_cmT(
&self.cf_cm_params,
&self.cf_r1cs,
cf_w_i,
cf_u_i,
&self.cf_W_i,
&self.cf_U_i,
)
}
}
/// helper method to get the r1cs from the ConstraintSynthesizer
pub fn get_r1cs_from_cs<F: PrimeField>(
circuit: impl ConstraintSynthesizer<F>,
) -> Result<R1CS<F>, Error> {
let cs = ConstraintSystem::<F>::new_ref();
circuit.generate_constraints(cs.clone())?;
cs.finalize();
let cs = cs.into_inner().ok_or(Error::NoInnerConstraintSystem)?;
let r1cs = extract_r1cs::<F>(&cs);
Ok(r1cs)
}
/// helper method to get the R1CS for both the AugmentedFCircuit and the CycleFold circuit
#[allow(clippy::type_complexity)]
pub fn get_r1cs<C1, GC1, C2, GC2, FC>(
poseidon_config: &PoseidonConfig<C1::ScalarField>,
F_circuit: FC,
) -> Result<(R1CS<C1::ScalarField>, R1CS<C2::ScalarField>), Error>
where
C1: CurveGroup,
GC1: CurveVar<C1, CF2<C1>>,
C2: CurveGroup,
GC2: CurveVar<C2, CF2<C2>>,
FC: FCircuit<C1::ScalarField>,
<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>,
{
let augmented_F_circuit =
AugmentedFCircuit::<C1, C2, GC2, FC>::empty(poseidon_config, F_circuit);
let cf_circuit = CycleFoldCircuit::<C1, GC1>::empty();
let r1cs = get_r1cs_from_cs::<C1::ScalarField>(augmented_F_circuit)?;
let cf_r1cs = get_r1cs_from_cs::<C2::ScalarField>(cf_circuit)?;
Ok((r1cs, cf_r1cs))
}
/// helper method to get the pedersen params length for both the AugmentedFCircuit and the
/// CycleFold circuit
pub fn get_pedersen_params_len<C1, GC1, C2, GC2, FC>(
poseidon_config: &PoseidonConfig<C1::ScalarField>,
F_circuit: FC,
) -> Result<(usize, usize), Error>
where
C1: CurveGroup,
GC1: CurveVar<C1, CF2<C1>>,
C2: CurveGroup,
GC2: CurveVar<C2, CF2<C2>>,
FC: FCircuit<C1::ScalarField>,
<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>,
{
let (r1cs, cf_r1cs) = get_r1cs::<C1, GC1, C2, GC2, FC>(poseidon_config, F_circuit)?;
Ok((r1cs.A.n_rows, cf_r1cs.A.n_rows))
}
pub(crate) fn get_committed_instance_coordinates<C: CurveGroup>(
u: &CommittedInstance<C>,
) -> Vec<C::BaseField> {
let zero = (&C::BaseField::zero(), &C::BaseField::one());
let cmE = u.cmE.into_affine();
let (cmE_x, cmE_y) = cmE.xy().unwrap_or(zero);
let cmW = u.cmW.into_affine();
let (cmW_x, cmW_y) = cmW.xy().unwrap_or(zero);
vec![*cmE_x, *cmE_y, *cmW_x, *cmW_y]
}
#[cfg(test)]
pub mod tests {
use super::*;
use ark_pallas::{constraints::GVar, Fr, Projective};
use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2};
use crate::commitment::pedersen::Pedersen;
use crate::frontend::tests::CubicFCircuit;
use crate::transcript::poseidon::poseidon_test_config;
#[test]
fn test_ivc() {
type NOVA = Nova<
Projective,
GVar,
Projective2,
GVar2,
CubicFCircuit<Fr>,
Pedersen<Projective>,
Pedersen<Projective2>,
>;
let mut rng = ark_std::test_rng();
let poseidon_config = poseidon_test_config::<Fr>();
let F_circuit = CubicFCircuit::<Fr>::new(());
let z_0 = vec![Fr::from(3_u32)];
let (cm_len, cf_cm_len) =
get_pedersen_params_len::<Projective, GVar, Projective2, GVar2, CubicFCircuit<Fr>>(
&poseidon_config,
F_circuit,
)
.unwrap();
let pedersen_params = Pedersen::<Projective>::new_params(&mut rng, cm_len);
let cf_pedersen_params = Pedersen::<Projective2>::new_params(&mut rng, cf_cm_len);
let prover_params =
ProverParams::<Projective, Projective2, Pedersen<Projective>, Pedersen<Projective2>> {
poseidon_config: poseidon_config.clone(),
cm_params: pedersen_params,
cf_cm_params: cf_pedersen_params,
};
let mut nova = NOVA::init(&prover_params, F_circuit, z_0.clone()).unwrap();
let num_steps: usize = 3;
for _ in 0..num_steps {
nova.prove_step().unwrap();
}
assert_eq!(Fr::from(num_steps as u32), nova.i);
let verifier_params = VerifierParams::<Projective, Projective2> {
poseidon_config,
r1cs: nova.r1cs,
cf_r1cs: nova.cf_r1cs,
};
NOVA::verify(
verifier_params,
z_0,
nova.z_i,
nova.i,
(nova.U_i, nova.W_i),
(nova.u_i, nova.w_i),
(nova.cf_U_i, nova.cf_W_i),
)
.unwrap();
}
}

View File

@@ -0,0 +1,481 @@
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::ccs::r1cs::R1CS;
use crate::commitment::CommitmentProver;
use crate::transcript::Transcript;
use crate::utils::vec::*;
use crate::Error;
/// Implements the Non-Interactive Folding Scheme described in section 4 of
/// [Nova](https://eprint.iacr.org/2021/370.pdf)
pub struct NIFS<C: CurveGroup, CP: CommitmentProver<C>> {
_c: PhantomData<C>,
_cp: PhantomData<CP>,
}
impl<C: CurveGroup, CP: CommitmentProver<C>> NIFS<C, CP>
where
<C as Group>::ScalarField: Absorb,
{
// compute_T: compute cross-terms T
pub fn compute_T(
r1cs: &R1CS<C::ScalarField>,
u1: C::ScalarField,
u2: C::ScalarField,
z1: &[C::ScalarField],
z2: &[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 Az1 = mat_vec_mul_sparse(&A, z1)?;
let Bz1 = mat_vec_mul_sparse(&B, z1)?;
let Cz1 = mat_vec_mul_sparse(&C, z1)?;
let Az2 = mat_vec_mul_sparse(&A, z2)?;
let Bz2 = mat_vec_mul_sparse(&B, z2)?;
let Cz2 = mat_vec_mul_sparse(&C, z2)?;
let Az1_Bz2 = hadamard(&Az1, &Bz2)?;
let Az2_Bz1 = hadamard(&Az2, &Bz1)?;
let u1Cz2 = vec_scalar_mul(&Cz2, &u1);
let u2Cz1 = vec_scalar_mul(&Cz1, &u2);
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>,
ci2: &CommittedInstance<C>,
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
pub fn compute_cmT(
cm_prover_params: &CP::Params,
r1cs: &R1CS<C::ScalarField>,
w1: &Witness<C>,
ci1: &CommittedInstance<C>,
w2: &Witness<C>,
ci2: &CommittedInstance<C>,
) -> Result<(Vec<C::ScalarField>, C), Error> {
let z1: Vec<C::ScalarField> = [vec![ci1.u], ci1.x.to_vec(), w1.W.to_vec()].concat();
let z2: Vec<C::ScalarField> = [vec![ci2.u], ci2.x.to_vec(), w2.W.to_vec()].concat();
// compute cross terms
let T = Self::compute_T(r1cs, ci1.u, ci2.u, &z1, &z2)?;
// use r_T=1 since we don't need hiding property for cm(T)
let cmT = CP::commit(cm_prover_params, &T, &C::ScalarField::one())?;
Ok((T, cmT))
}
pub fn compute_cyclefold_cmT(
cm_prover_params: &CP::Params,
r1cs: &R1CS<C::ScalarField>, // R1CS over C2.Fr=C1.Fq (here C=C2)
w1: &Witness<C>,
ci1: &CommittedInstance<C>,
w2: &Witness<C>,
ci2: &CommittedInstance<C>,
) -> Result<(Vec<C::ScalarField>, C), Error>
where
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField,
{
let z1: Vec<C::ScalarField> = [vec![ci1.u], ci1.x.to_vec(), w1.W.to_vec()].concat();
let z2: Vec<C::ScalarField> = [vec![ci2.u], ci2.x.to_vec(), w2.W.to_vec()].concat();
// compute cross terms
let T = Self::compute_T(r1cs, ci1.u, ci2.u, &z1, &z2)?;
// use r_T=1 since we don't need hiding property for cm(T)
let cmT = CP::commit(cm_prover_params, &T, &C::ScalarField::one())?;
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=1 since we don't need hiding property for cm(T)
let w3 = NIFS::<C, CP>::fold_witness(r, w1, w2, T, C::ScalarField::one())?;
// fold committed instancs
let ci3 = NIFS::<C, CP>::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, CP>::fold_committed_instance(r, ci1, ci2, cmT)
}
/// Verify commited 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
/// result in the folded committed instance (ci3) values.
pub fn verify_folded_instance(
r: C::ScalarField,
ci1: &CommittedInstance<C>,
ci2: &CommittedInstance<C>,
ci3: &CommittedInstance<C>,
cmT: &C,
) -> Result<(), Error> {
let expected = Self::fold_committed_instance(r, ci1, ci2, cmT);
if ci3.cmE != expected.cmE
|| ci3.u != expected.u
|| ci3.cmW != expected.cmW
|| ci3.x != expected.x
{
return Err(Error::NotSatisfied);
}
Ok(())
}
pub fn prove_commitments(
tr: &mut impl Transcript<C>,
cm_prover_params: &CP::Params,
w: &Witness<C>,
ci: &CommittedInstance<C>,
T: Vec<C::ScalarField>,
cmT: &C,
) -> Result<[CP::Proof; 3], Error> {
let cmE_proof = CP::prove(cm_prover_params, tr, &ci.cmE, &w.E, &w.rE)?;
let cmW_proof = CP::prove(cm_prover_params, tr, &ci.cmW, &w.W, &w.rW)?;
let cmT_proof = CP::prove(cm_prover_params, tr, cmT, &T, &C::ScalarField::one())?; // cm(T) is committed with rT=1
Ok([cmE_proof, cmW_proof, cmT_proof])
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use ark_crypto_primitives::sponge::poseidon::PoseidonConfig;
use ark_ff::{BigInteger, PrimeField};
use ark_pallas::{Fr, Projective};
use ark_std::{ops::Mul, UniformRand, Zero};
use crate::ccs::r1cs::tests::{get_test_r1cs, get_test_z};
use crate::commitment::pedersen::{Params as PedersenParams, Pedersen};
use crate::folding::nova::circuits::ChallengeGadget;
use crate::folding::nova::traits::NovaR1CS;
use crate::transcript::poseidon::{poseidon_test_config, PoseidonTranscript};
use crate::utils::vec::vec_scalar_mul;
#[allow(clippy::type_complexity)]
pub(crate) fn prepare_simple_fold_inputs() -> (
PedersenParams<Projective>,
PoseidonConfig<Fr>,
R1CS<Fr>,
Witness<Projective>, // w1
CommittedInstance<Projective>, // ci1
Witness<Projective>, // w2
CommittedInstance<Projective>, // ci2
Witness<Projective>, // w3
CommittedInstance<Projective>, // ci3
Vec<Fr>, // T
Projective, // cmT
Vec<bool>, // r_bits
Fr, // r_Fr
) {
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::<Projective>::new(w1.clone(), r1cs.A.n_rows);
let w2 = Witness::<Projective>::new(w2.clone(), r1cs.A.n_rows);
let mut rng = ark_std::test_rng();
let pedersen_params = Pedersen::<Projective>::new_params(&mut rng, r1cs.A.n_cols);
// compute committed instances
let ci1 = w1
.commit::<Pedersen<Projective>>(&pedersen_params, x1.clone())
.unwrap();
let ci2 = w2
.commit::<Pedersen<Projective>>(&pedersen_params, x2.clone())
.unwrap();
// NIFS.P
let (T, cmT) = NIFS::<Projective, Pedersen<Projective>>::compute_cmT(
&pedersen_params,
&r1cs,
&w1,
&ci1,
&w2,
&ci2,
)
.unwrap();
let poseidon_config = poseidon_test_config::<Fr>();
let r_bits = ChallengeGadget::<Projective>::get_challenge_native(
&poseidon_config,
ci1.clone(),
ci2.clone(),
cmT,
)
.unwrap();
let r_Fr = Fr::from_bigint(BigInteger::from_bits_le(&r_bits)).unwrap();
let (w3, ci3) = NIFS::<Projective, Pedersen<Projective>>::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]
fn test_nifs_fold_dummy() {
let r1cs = get_test_r1cs::<Fr>();
let z1 = get_test_z(3);
let (w1, x1) = r1cs.split_z(&z1);
let mut rng = ark_std::test_rng();
let pedersen_params = Pedersen::<Projective>::new_params(&mut rng, r1cs.A.n_cols);
// dummy instance, witness and public inputs zeroes
let w_dummy = Witness::<Projective>::new(vec![Fr::zero(); w1.len()], r1cs.A.n_rows);
let mut u_dummy = w_dummy
.commit::<Pedersen<Projective>>(&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_relaxed_instance_relation(&w_i, &u_i).unwrap();
r1cs.check_relaxed_instance_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_relaxed_instance_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_relaxed_instance_relation(&w1, &ci1).unwrap();
r1cs.check_relaxed_instance_relation(&w2, &ci2).unwrap();
r1cs.check_relaxed_instance_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>>(&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, &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 = PoseidonTranscript::<Projective>::new(&poseidon_config);
// init Verifier's transcript
let mut transcript_v = PoseidonTranscript::<Projective>::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();
}
#[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>::new_params(&mut rng, r1cs.A.n_cols);
// prepare the running instance
let mut running_instance_w = Witness::<Projective>::new(w.clone(), r1cs.A.n_rows);
let mut running_committed_instance = running_instance_w
.commit::<Pedersen<Projective>>(&pedersen_params, x)
.unwrap();
r1cs.check_relaxed_instance_relation(&running_instance_w, &running_committed_instance)
.unwrap();
let num_iters = 10;
for i in 0..num_iters {
// prepare the incomming instance
let incomming_instance_z = get_test_z(i + 4);
let (w, x) = r1cs.split_z(&incomming_instance_z);
let incomming_instance_w = Witness::<Projective>::new(w.clone(), r1cs.A.n_rows);
let incomming_committed_instance = incomming_instance_w
.commit::<Pedersen<Projective>>(&pedersen_params, x)
.unwrap();
r1cs.check_relaxed_instance_relation(
&incomming_instance_w,
&incomming_committed_instance,
)
.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(
&pedersen_params,
&r1cs,
&running_instance_w,
&running_committed_instance,
&incomming_instance_w,
&incomming_committed_instance,
)
.unwrap();
let (folded_w, _) = NIFS::<Projective, Pedersen<Projective>>::fold_instances(
r,
&running_instance_w,
&running_committed_instance,
&incomming_instance_w,
&incomming_committed_instance,
&T,
cmT,
)
.unwrap();
// NIFS.V
let folded_committed_instance = NIFS::<Projective, Pedersen<Projective>>::verify(
r,
&running_committed_instance,
&incomming_committed_instance,
&cmT,
);
r1cs.check_relaxed_instance_relation(&folded_w, &folded_committed_instance)
.unwrap();
// set running_instance for next loop iteration
running_instance_w = folded_w;
running_committed_instance = folded_committed_instance;
}
}
}

View File

@@ -0,0 +1,67 @@
use ark_crypto_primitives::sponge::Absorb;
use ark_ec::{CurveGroup, Group};
use ark_std::{One, Zero};
use super::{CommittedInstance, Witness};
use crate::ccs::r1cs::R1CS;
use crate::Error;
/// NovaR1CS extends R1CS methods with Nova specific methods
pub trait NovaR1CS<C: CurveGroup> {
/// returns a dummy instance (Witness and CommittedInstance) for the current R1CS structure
fn dummy_instance(&self) -> (Witness<C>, CommittedInstance<C>);
/// checks the R1CS relation (un-relaxed) for the given Witness and CommittedInstance.
fn check_instance_relation(
&self,
W: &Witness<C>,
U: &CommittedInstance<C>,
) -> Result<(), Error>;
/// checks the Relaxed R1CS relation (corresponding to the current R1CS) for the given Witness
/// and CommittedInstance.
fn check_relaxed_instance_relation(
&self,
W: &Witness<C>,
U: &CommittedInstance<C>,
) -> Result<(), Error>;
}
impl<C: CurveGroup> NovaR1CS<C> for R1CS<C::ScalarField>
where
<C as Group>::ScalarField: Absorb,
<C as ark_ec::CurveGroup>::BaseField: ark_ff::PrimeField,
{
fn dummy_instance(&self) -> (Witness<C>, CommittedInstance<C>) {
let w_len = self.A.n_cols - 1 - self.l;
let w_dummy = Witness::<C>::new(vec![C::ScalarField::zero(); w_len], self.A.n_rows);
let u_dummy = CommittedInstance::<C>::dummy(self.l);
(w_dummy, u_dummy)
}
fn check_instance_relation(
&self,
W: &Witness<C>,
U: &CommittedInstance<C>,
) -> Result<(), Error> {
if U.cmE != C::zero() || U.u != C::ScalarField::one() {
return Err(Error::R1CSUnrelaxedFail);
}
let Z: Vec<C::ScalarField> = [vec![U.u], U.x.to_vec(), W.W.to_vec()].concat();
self.check_relation(&Z)
}
fn check_relaxed_instance_relation(
&self,
W: &Witness<C>,
U: &CommittedInstance<C>,
) -> Result<(), Error> {
let mut rel_r1cs = self.clone().relax();
rel_r1cs.u = U.u;
rel_r1cs.E = W.E.clone();
let Z: Vec<C::ScalarField> = [vec![U.u], U.x.to_vec(), W.W.to_vec()].concat();
rel_r1cs.check_relation(&Z)
}
}