You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

325 lines
13 KiB

/// 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::sponge::{
constraints::{AbsorbGadget, CryptographicSpongeVar},
poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig},
Absorb,
};
use ark_ec::CurveGroup;
use ark_r1cs_std::{
alloc::AllocVar, eq::EqGadget, fields::fp::FpVar, prelude::CurveVar, ToConstraintFieldGadget,
};
use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError};
use ark_std::{marker::PhantomData, Zero};
use crate::{
arith::{r1cs::R1CS, Arith, ArithGadget},
commitment::pedersen::Params as PedersenParams,
folding::{
circuits::{
cyclefold::{
CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar, CycleFoldWitness,
},
decider::{EvalGadget, KZGChallengesGadget},
nonnative::affine::NonNativeAffineVar,
CF1, CF2,
},
traits::{CommittedInstanceOps, CommittedInstanceVarOps, Dummy, WitnessOps, WitnessVarOps},
},
};
use super::DeciderEnabledNIFS;
/// A generic circuit tailored for the onchain (Ethereum's EVM) verification of
/// IVC proofs, where we support IVC built upon any folding scheme.
///
/// Specifically, `GenericDeciderEthCircuit` implements the in-circuit version
/// of the IVC verification algorithm, which essentially checks the following:
/// - `R_arith(W_i, U_i)`:
/// The running instance `U_i` and witness `W_i` satisfy `arith`,
/// and the commitments in `U_i` open to the values in `W_i`.
/// - `R_arith(w_i, u_i)`:
/// The incoming instance `u_i` and witness `w_i` satisfy `arith`,
/// and the commitments in `u_i` open to the values in `w_i`.
/// - `R_cf_arith(cf_W_i, cf_U_i)`:
/// The CycleFold instance `cf_U_i` and witness `cf_W_i` satisfy `cf_arith`,
/// and the commitments in `cf_U_i` open to the values in `cf_W_i`.
/// - `u_i` contains the correct hash of the initial and final states.
///
/// To reduce the number of relation checks, the prover, before invoking the
/// circuit, further folds `U_i, u_i` into `U_{i+1}`, and `W_i, w_i` into
/// `W_{i+1}`.
/// Now, the circuit only needs to perform two relation checks, i.e.,
/// `R_arith(W_{i+1}, U_{i+1})` and `R_cf_arith(cf_W_i, cf_U_i)`, plus a few
/// constraints for enforcing the correct hash in `u_i` and the correct folding
/// from `U_i, u_i` to `U_{i+1}`.
///
/// We further reduce the circuit size by avoiding the non-native commitment
/// checks involved in `R_arith(W_{i+1}, U_{i+1})`.
/// Now, we now only check the satisfiability of the constraint system `arith`
/// with the witness `W_{i+1}` and instance `U_{i+1}` in the circuit, but the
/// actual commitment checks are done with the help of KZG.
///
/// For more details, see [https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html].
pub struct GenericOnchainDeciderCircuit<
C1: CurveGroup,
C2: CurveGroup,
GC2: CurveVar<C2, CF2<C2>> + ToConstraintFieldGadget<CF2<C2>>,
RU: CommittedInstanceOps<C1>, // Running instance
IU: CommittedInstanceOps<C1>, // Incoming instance
W: WitnessOps<CF1<C1>>, // Witness
A: Arith<W, RU>, // Constraint system
AVar: ArithGadget<W::Var, RU::Var>, // In-circuit representation of `A`
D: DeciderEnabledNIFS<C1, RU, IU, W, A>,
> {
pub _gc2: PhantomData<GC2>,
pub _avar: PhantomData<AVar>,
/// Constraint system of the Augmented Function circuit
pub arith: A,
/// R1CS of the CycleFold circuit
pub cf_arith: R1CS<CF1<C2>>,
/// CycleFold PedersenParams over C2
pub cf_pedersen_params: PedersenParams<C2>,
pub poseidon_config: PoseidonConfig<CF1<C1>>,
/// public params hash
pub pp_hash: CF1<C1>,
pub i: CF1<C1>,
/// initial state
pub z_0: Vec<CF1<C1>>,
/// current i-th state
pub z_i: Vec<CF1<C1>>,
/// Folding scheme instances
pub U_i: RU,
pub W_i: W,
pub u_i: IU,
pub w_i: W,
pub U_i1: RU,
pub W_i1: W,
/// Helper for folding verification
pub proof: D::Proof,
pub randomness: D::Randomness,
/// CycleFold running instance
pub cf_U_i: CycleFoldCommittedInstance<C2>,
pub cf_W_i: CycleFoldWitness<C2>,
/// KZG challenges
pub kzg_challenges: Vec<CF1<C1>>,
pub kzg_evaluations: Vec<CF1<C1>>,
}
impl<
C1: CurveGroup,
C2: CurveGroup<ScalarField = CF2<C1>, BaseField = CF1<C1>>,
GC2: CurveVar<C2, CF2<C2>> + ToConstraintFieldGadget<CF2<C2>>,
RU: CommittedInstanceOps<C1> + for<'a> Dummy<&'a A>,
IU: CommittedInstanceOps<C1> + for<'a> Dummy<&'a A>,
W: WitnessOps<CF1<C1>> + for<'a> Dummy<&'a A>,
A: Arith<W, RU>,
AVar: ArithGadget<W::Var, RU::Var> + AllocVar<A, CF1<C1>>,
D: DeciderEnabledNIFS<C1, RU, IU, W, A>,
>
Dummy<(
A,
R1CS<CF1<C2>>,
PedersenParams<C2>,
PoseidonConfig<CF1<C1>>,
D::ProofDummyCfg,
D::RandomnessDummyCfg,
usize,
usize,
)> for GenericOnchainDeciderCircuit<C1, C2, GC2, RU, IU, W, A, AVar, D>
{
fn dummy(
(
arith,
cf_arith,
cf_pedersen_params,
poseidon_config,
proof_config,
randomness_config,
state_len,
num_commitments,
): (
A,
R1CS<CF1<C2>>,
PedersenParams<C2>,
PoseidonConfig<CF1<C1>>,
D::ProofDummyCfg,
D::RandomnessDummyCfg,
usize,
usize,
),
) -> Self {
Self {
_gc2: PhantomData,
_avar: PhantomData,
cf_pedersen_params,
poseidon_config,
pp_hash: Zero::zero(),
i: Zero::zero(),
z_0: vec![Zero::zero(); state_len],
z_i: vec![Zero::zero(); state_len],
U_i: RU::dummy(&arith),
W_i: W::dummy(&arith),
u_i: IU::dummy(&arith),
w_i: W::dummy(&arith),
U_i1: RU::dummy(&arith),
W_i1: W::dummy(&arith),
proof: D::Proof::dummy(proof_config),
randomness: D::Randomness::dummy(randomness_config),
cf_U_i: CycleFoldCommittedInstance::dummy(&cf_arith),
cf_W_i: CycleFoldWitness::dummy(&cf_arith),
kzg_challenges: vec![Zero::zero(); num_commitments],
kzg_evaluations: vec![Zero::zero(); num_commitments],
arith,
cf_arith,
}
}
}
impl<
C1: CurveGroup,
C2: CurveGroup<ScalarField = CF2<C1>, BaseField = CF1<C1>>,
GC2: CurveVar<C2, CF2<C2>> + ToConstraintFieldGadget<CF2<C2>>,
RU: CommittedInstanceOps<C1>,
IU: CommittedInstanceOps<C1>,
W: WitnessOps<CF1<C1>>,
A: Arith<W, RU>,
AVar: ArithGadget<W::Var, RU::Var> + AllocVar<A, CF1<C1>>,
D: DeciderEnabledNIFS<C1, RU, IU, W, A>,
> ConstraintSynthesizer<CF1<C1>>
for GenericOnchainDeciderCircuit<C1, C2, GC2, RU, IU, W, A, AVar, D>
where
RU::Var: AbsorbGadget<CF1<C1>> + CommittedInstanceVarOps<C1, PointVar = NonNativeAffineVar<C1>>,
CF1<C1>: Absorb,
{
fn generate_constraints(self, cs: ConstraintSystemRef<CF1<C1>>) -> Result<(), SynthesisError> {
let arith = AVar::new_witness(cs.clone(), || Ok(&self.arith))?;
let pp_hash = FpVar::new_input(cs.clone(), || Ok(self.pp_hash))?;
let i = FpVar::new_input(cs.clone(), || Ok(self.i))?;
let z_0 = Vec::new_input(cs.clone(), || Ok(self.z_0))?;
let z_i = Vec::new_input(cs.clone(), || Ok(self.z_i))?;
let u_i = IU::Var::new_witness(cs.clone(), || Ok(self.u_i))?;
let U_i = RU::Var::new_witness(cs.clone(), || Ok(self.U_i))?;
// here (U_i1, W_i1) = NIFS.P( (U_i,W_i), (u_i,w_i))
let U_i1_commitments = Vec::<NonNativeAffineVar<C1>>::new_input(cs.clone(), || {
Ok(self.U_i1.get_commitments())
})?;
let U_i1 = RU::Var::new_witness(cs.clone(), || Ok(self.U_i1))?;
let W_i1 = W::Var::new_witness(cs.clone(), || Ok(self.W_i1))?;
U_i1.get_commitments().enforce_equal(&U_i1_commitments)?;
let cf_U_i =
CycleFoldCommittedInstanceVar::<C2, GC2>::new_witness(cs.clone(), || Ok(self.cf_U_i))?;
// allocate the inputs for the check 7.1 and 7.2
let kzg_challenges = Vec::new_input(cs.clone(), || Ok(self.kzg_challenges))?;
let kzg_evaluations = Vec::new_input(cs.clone(), || Ok(self.kzg_evaluations))?;
// `sponge` is for digest computation.
let sponge = PoseidonSpongeVar::new(cs.clone(), &self.poseidon_config);
// `transcript` is for challenge generation.
let mut transcript = sponge.clone();
// NOTE: we use the same enumeration as in
// https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-onchain.html
// in order to make it easier to reason about.
// 1. enforce `U_{i+1}` and `W_{i+1}` satisfy `arith`
arith.enforce_relation(&W_i1, &U_i1)?;
// 2. enforce `u_i` is an incoming instance
u_i.enforce_incoming()?;
// 3. u_i.x[0] == H(i, z_0, z_i, U_i), u_i.x[1] == H(cf_U_i)
let (u_i_x, U_i_vec) = U_i.hash(&sponge, &pp_hash, &i, &z_0, &z_i)?;
let (cf_u_i_x, _) = cf_U_i.hash(&sponge, pp_hash.clone())?;
u_i.get_public_inputs().enforce_equal(&[u_i_x, cf_u_i_x])?;
#[cfg(feature = "light-test")]
log::warn!("[WARNING]: Running with the 'light-test' feature, skipping the big part of the DeciderEthCircuit.\n Only for testing purposes.");
// The following two checks (and their respective allocations) are disabled for normal
// tests since they take several millions of constraints and would take several minutes
// (and RAM) to run the test. It is active by default, and not active only when
// 'light-test' feature is used.
#[cfg(not(feature = "light-test"))]
{
// imports here instead of at the top of the file, so we avoid having multiple
// `#[cfg(not(test))]`
use crate::{
arith::r1cs::circuits::R1CSMatricesVar,
commitment::pedersen::PedersenGadget,
folding::circuits::{
cyclefold::CycleFoldWitnessVar, nonnative::uint::NonNativeUintVar,
},
};
use ark_r1cs_std::ToBitsGadget;
let cf_W_i = CycleFoldWitnessVar::<C2>::new_witness(cs.clone(), || Ok(self.cf_W_i))?;
// 4. check Pedersen commitments of cf_U_i.{cmE, cmW}
let H = GC2::constant(self.cf_pedersen_params.h);
let G = self
.cf_pedersen_params
.generators
.iter()
.map(|&g| GC2::constant(g.into()))
.collect::<Vec<_>>();
let cf_W_i_E_bits = cf_W_i
.E
.iter()
.map(|E_i| E_i.to_bits_le())
.collect::<Result<Vec<_>, _>>()?;
let cf_W_i_W_bits = cf_W_i
.W
.iter()
.map(|W_i| W_i.to_bits_le())
.collect::<Result<Vec<_>, _>>()?;
PedersenGadget::<C2, GC2>::commit(&H, &G, &cf_W_i_E_bits, &cf_W_i.rE.to_bits_le()?)?
.enforce_equal(&cf_U_i.cmE)?;
PedersenGadget::<C2, GC2>::commit(&H, &G, &cf_W_i_W_bits, &cf_W_i.rW.to_bits_le()?)?
.enforce_equal(&cf_U_i.cmW)?;
let cf_r1cs = R1CSMatricesVar::<CF1<C2>, NonNativeUintVar<CF2<C2>>>::new_constant(
ConstraintSystemRef::None,
self.cf_arith,
)?;
// 5. enforce `cf_U_i` and `cf_W_i` satisfy `cf_r1cs`
cf_r1cs.enforce_relation(&cf_W_i, &cf_U_i)?;
}
// 6.1. partially enforce `NIFS.V(U_i, u_i) = U_{i+1}`.
D::fold_field_elements_gadget(
&self.arith,
&mut transcript,
pp_hash,
U_i,
U_i_vec,
u_i,
self.proof,
self.randomness,
)?
.enforce_partial_equal(&U_i1)?;
// 7.1. compute and check KZG challenges
KZGChallengesGadget::get_challenges_gadget(&mut transcript, &U_i1)?
.enforce_equal(&kzg_challenges)?;
// 7.2. check the claimed evaluations
for (((v, _r), c), e) in W_i1
.get_openings()
.iter()
.zip(&kzg_challenges)
.zip(&kzg_evaluations)
{
// The randomness `_r` is currently not used.
EvalGadget::evaluate_gadget(v, c)?.enforce_equal(e)?;
}
Ok(())
}
}