@ -1,2 +1,3 @@ |
|||
[default.extend-words] |
|||
groth = "groth" |
|||
mimc = "mimc" |
@ -1,28 +1,156 @@ |
|||
use ark_ff::PrimeField;
|
|||
use ark_ec::CurveGroup;
|
|||
use ark_relations::r1cs::SynthesisError;
|
|||
use ark_std::rand::RngCore;
|
|||
|
|||
use crate::Error;
|
|||
use crate::{commitment::CommitmentScheme, folding::traits::Dummy, Error};
|
|||
|
|||
pub mod ccs;
|
|||
pub mod r1cs;
|
|||
|
|||
pub trait Arith<F: PrimeField> {
|
|||
/// Evaluate the given Arith structure at `z`, a vector of assignments, and
|
|||
/// return the evaluation.
|
|||
fn eval_relation(&self, z: &[F]) -> Result<Vec<F>, Error>;
|
|||
/// `Arith` defines the operations that a constraint system (e.g., R1CS, CCS,
|
|||
/// etc.) should support.
|
|||
///
|
|||
/// Here, `W` is the type of witness, and `U` is the type of statement / public
|
|||
/// input / public IO / instance.
|
|||
/// Note that the same constraint system may support different types of `W` and
|
|||
/// `U`, and the satisfiability check may vary.
|
|||
///
|
|||
/// For example, both plain R1CS and relaxed R1CS are represented by 3 matrices,
|
|||
/// but the types of `W` and `U` are different:
|
|||
/// - The plain R1CS has `W` and `U` as vectors of field elements.
|
|||
///
|
|||
/// `W = w` and `U = x` satisfy R1CS if `Az ∘ Bz = Cz`, where `z = [1, x, w]`.
|
|||
///
|
|||
/// - In Nova, Relaxed R1CS has `W` as [`crate::folding::nova::Witness`],
|
|||
/// and `U` as [`crate::folding::nova::CommittedInstance`].
|
|||
///
|
|||
/// `W = (w, e, ...)` and `U = (u, x, ...)` satisfy Relaxed R1CS if
|
|||
/// `Az ∘ Bz = uCz + e`, where `z = [u, x, w]`.
|
|||
/// (commitments in `U` are not checked here)
|
|||
///
|
|||
/// Also, `W` and `U` have non-native field elements as their components when
|
|||
/// used as CycleFold witness and instance.
|
|||
///
|
|||
/// - In ProtoGalaxy, Relaxed R1CS has `W` as [`crate::folding::protogalaxy::Witness`],
|
|||
/// and `U` as [`crate::folding::protogalaxy::CommittedInstance`].
|
|||
///
|
|||
/// `W = (w, ...)` and `U = (x, e, β, ...)` satisfy Relaxed R1CS if
|
|||
/// `e = Σ pow_i(β) v_i`, where `v = Az ∘ Bz - Cz`, `z = [1, x, w]`.
|
|||
/// (commitments in `U` are not checked here)
|
|||
///
|
|||
/// This is also the case of CCS, where `W` and `U` may be vectors of field
|
|||
/// elements, [`crate::folding::hypernova::Witness`] and [`crate::folding::hypernova::lcccs::LCCCS`],
|
|||
/// or [`crate::folding::hypernova::Witness`] and [`crate::folding::hypernova::cccs::CCCS`].
|
|||
pub trait Arith<W, U>: Clone {
|
|||
type Evaluation;
|
|||
|
|||
/// Checks that the given Arith structure is satisfied by a z vector, i.e.,
|
|||
/// if the evaluation is a zero vector
|
|||
/// Returns a dummy witness and instance
|
|||
fn dummy_witness_instance<'a>(&'a self) -> (W, U)
|
|||
where
|
|||
W: Dummy<&'a Self>,
|
|||
U: Dummy<&'a Self>,
|
|||
{
|
|||
(W::dummy(self), U::dummy(self))
|
|||
}
|
|||
|
|||
/// Evaluates the constraint system `self` at witness `w` and instance `u`.
|
|||
/// Returns the evaluation result.
|
|||
///
|
|||
/// The evaluation result is usually a vector of field elements.
|
|||
/// For instance:
|
|||
/// - Evaluating the plain R1CS at `W = w` and `U = x` returns
|
|||
/// `Az ∘ Bz - Cz`, where `z = [1, x, w]`.
|
|||
///
|
|||
/// - Evaluating the relaxed R1CS in Nova at `W = (w, e, ...)` and
|
|||
/// `U = (u, x, ...)` returns `Az ∘ Bz - uCz`, where `z = [u, x, w]`.
|
|||
///
|
|||
/// - Evaluating the relaxed R1CS in ProtoGalaxy at `W = (w, ...)` and
|
|||
/// `U = (x, e, β, ...)` returns `Az ∘ Bz - Cz`, where `z = [1, x, w]`.
|
|||
///
|
|||
/// However, we use `Self::Evaluation` to represent the evaluation result
|
|||
/// for future extensibility.
|
|||
fn eval_relation(&self, w: &W, u: &U) -> Result<Self::Evaluation, Error>;
|
|||
|
|||
/// Checks if the evaluation result is valid. The witness `w` and instance
|
|||
/// `u` are also parameters, because the validity check may need information
|
|||
/// contained in `w` and/or `u`.
|
|||
///
|
|||
/// For instance:
|
|||
/// - The evaluation `v` of plain R1CS at satisfying `W` and `U` should be
|
|||
/// an all-zero vector.
|
|||
///
|
|||
/// - The evaluation `v` of relaxed R1CS in Nova at satisfying `W` and `U`
|
|||
/// should be equal to the error term `e` in the witness.
|
|||
///
|
|||
/// - The evaluation `v` of relaxed R1CS in ProtoGalaxy at satisfying `W`
|
|||
/// and `U` should satisfy `e = Σ pow_i(β) v_i`, where `e` is the error
|
|||
/// term in the committed instance.
|
|||
fn check_evaluation(w: &W, u: &U, v: Self::Evaluation) -> Result<(), Error>;
|
|||
|
|||
/// Checks if witness `w` and instance `u` satisfy the constraint system
|
|||
/// `self` by first computing the evaluation result and then checking the
|
|||
/// validity of the evaluation result.
|
|||
///
|
|||
/// Used only for testing.
|
|||
fn check_relation(&self, z: &[F]) -> Result<(), Error> {
|
|||
if self.eval_relation(z)?.iter().all(|f| f.is_zero()) {
|
|||
Ok(())
|
|||
} else {
|
|||
Err(Error::NotSatisfied)
|
|||
}
|
|||
fn check_relation(&self, w: &W, u: &U) -> Result<(), Error> {
|
|||
let e = self.eval_relation(w, u)?;
|
|||
Self::check_evaluation(w, u, e)
|
|||
}
|
|||
}
|
|||
|
|||
/// `ArithSerializer` is for serializing constraint systems.
|
|||
///
|
|||
/// Currently we only support converting parameters to bytes, but in the future
|
|||
/// we may consider implementing methods for serializing the actual data (e.g.,
|
|||
/// R1CS matrices).
|
|||
pub trait ArithSerializer {
|
|||
/// Returns the bytes that represent the parameters, that is, the matrices sizes, the amount of
|
|||
/// public inputs, etc, without the matrices/polynomials values.
|
|||
fn params_to_le_bytes(&self) -> Vec<u8>;
|
|||
}
|
|||
|
|||
/// `ArithSampler` allows sampling random pairs of witness and instance that
|
|||
/// satisfy the constraint system `self`.
|
|||
///
|
|||
/// This is useful for constructing a zero-knowledge layer for a folding-based
|
|||
/// IVC.
|
|||
/// An example of such a layer can be found in Appendix D of the [HyperNova]
|
|||
/// paper.
|
|||
///
|
|||
/// Note that we use a separate trait for sampling, because this operation may
|
|||
/// not be supported by all witness-instance pairs.
|
|||
/// For instance, it is difficult (if not impossible) to do this for `w` and `x`
|
|||
/// in a plain R1CS.
|
|||
///
|
|||
/// [HyperNova]: https://eprint.iacr.org/2023/573.pdf
|
|||
pub trait ArithSampler<C: CurveGroup, W, U>: Arith<W, U> {
|
|||
/// Samples a random witness and instance that satisfy the constraint system.
|
|||
fn sample_witness_instance<CS: CommitmentScheme<C, true>>(
|
|||
&self,
|
|||
params: &CS::ProverParams,
|
|||
rng: impl RngCore,
|
|||
) -> Result<(W, U), Error>;
|
|||
}
|
|||
|
|||
/// `ArithGadget` defines the in-circuit counterparts of operations specified in
|
|||
/// `Arith` on constraint systems.
|
|||
pub trait ArithGadget<WVar, UVar> {
|
|||
type Evaluation;
|
|||
|
|||
/// Evaluates the constraint system `self` at witness `w` and instance `u`.
|
|||
/// Returns the evaluation result.
|
|||
fn eval_relation(&self, w: &WVar, u: &UVar) -> Result<Self::Evaluation, SynthesisError>;
|
|||
|
|||
/// Generates constraints for enforcing that witness `w` and instance `u`
|
|||
/// satisfy the constraint system `self` by first computing the evaluation
|
|||
/// result and then checking the validity of the evaluation result.
|
|||
fn enforce_relation(&self, w: &WVar, u: &UVar) -> Result<(), SynthesisError> {
|
|||
let e = self.eval_relation(w, u)?;
|
|||
Self::enforce_evaluation(w, u, e)
|
|||
}
|
|||
|
|||
/// Generates constraints for enforcing that the evaluation result is valid.
|
|||
/// The witness `w` and instance `u` are also parameters, because the
|
|||
/// validity check may need information contained in `w` and/or `u`.
|
|||
fn enforce_evaluation(w: &WVar, u: &UVar, e: Self::Evaluation) -> Result<(), SynthesisError>;
|
|||
}
|
@ -1,420 +0,0 @@ |
|||
use crate::arith::ccs::CCS;
|
|||
use crate::arith::r1cs::R1CS;
|
|||
use crate::folding::hypernova::ProverParams;
|
|||
use crate::folding::hypernova::VerifierParams;
|
|||
use ark_crypto_primitives::sponge::poseidon::PoseidonConfig;
|
|||
use ark_crypto_primitives::sponge::Absorb;
|
|||
use ark_ec::{CurveGroup, Group};
|
|||
use ark_ff::PrimeField;
|
|||
use ark_r1cs_std::groups::{CurveVar, GroupOpsBounds};
|
|||
use ark_r1cs_std::ToConstraintFieldGadget;
|
|||
use ark_serialize::CanonicalDeserialize;
|
|||
use ark_serialize::{CanonicalSerialize, Compress, SerializationError, Validate};
|
|||
use ark_std::marker::PhantomData;
|
|||
|
|||
use crate::folding::hypernova::cccs::CCCS;
|
|||
use crate::folding::hypernova::lcccs::LCCCS;
|
|||
use crate::folding::hypernova::Witness;
|
|||
use crate::folding::nova::{
|
|||
CommittedInstance as CycleFoldCommittedInstance, Witness as CycleFoldWitness,
|
|||
};
|
|||
use crate::FoldingScheme;
|
|||
use crate::{
|
|||
commitment::CommitmentScheme,
|
|||
folding::{circuits::CF2, nova::PreprocessorParam},
|
|||
frontend::FCircuit,
|
|||
};
|
|||
|
|||
use super::HyperNova;
|
|||
|
|||
impl<C1, GC1, C2, GC2, FC, CS1, CS2, const MU: usize, const NU: usize, const H: bool>
|
|||
CanonicalSerialize for HyperNova<C1, GC1, C2, GC2, FC, CS1, CS2, MU, NU, H>
|
|||
where
|
|||
C1: CurveGroup,
|
|||
GC1: CurveVar<C1, CF2<C1>> + ToConstraintFieldGadget<CF2<C1>>,
|
|||
C2: CurveGroup,
|
|||
GC2: CurveVar<C2, CF2<C2>>,
|
|||
FC: FCircuit<C1::ScalarField>,
|
|||
CS1: CommitmentScheme<C1, H>,
|
|||
CS2: CommitmentScheme<C2, H>,
|
|||
{
|
|||
fn serialize_compressed<W: std::io::prelude::Write>(
|
|||
&self,
|
|||
writer: W,
|
|||
) -> Result<(), ark_serialize::SerializationError> {
|
|||
self.serialize_with_mode(writer, ark_serialize::Compress::Yes)
|
|||
}
|
|||
|
|||
fn compressed_size(&self) -> usize {
|
|||
self.serialized_size(ark_serialize::Compress::Yes)
|
|||
}
|
|||
|
|||
fn serialize_uncompressed<W: std::io::prelude::Write>(
|
|||
&self,
|
|||
writer: W,
|
|||
) -> Result<(), ark_serialize::SerializationError> {
|
|||
self.serialize_with_mode(writer, ark_serialize::Compress::No)
|
|||
}
|
|||
|
|||
fn uncompressed_size(&self) -> usize {
|
|||
self.serialized_size(ark_serialize::Compress::No)
|
|||
}
|
|||
|
|||
fn serialize_with_mode<W: std::io::prelude::Write>(
|
|||
&self,
|
|||
mut writer: W,
|
|||
compress: ark_serialize::Compress,
|
|||
) -> Result<(), ark_serialize::SerializationError> {
|
|||
self.pp_hash.serialize_with_mode(&mut writer, compress)?;
|
|||
self.i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.z_0.serialize_with_mode(&mut writer, compress)?;
|
|||
self.z_i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.W_i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.U_i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.w_i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.u_i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.cf_W_i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.cf_U_i.serialize_with_mode(&mut writer, compress)
|
|||
}
|
|||
|
|||
fn serialized_size(&self, compress: ark_serialize::Compress) -> usize {
|
|||
self.pp_hash.serialized_size(compress)
|
|||
+ self.i.serialized_size(compress)
|
|||
+ self.z_0.serialized_size(compress)
|
|||
+ self.z_i.serialized_size(compress)
|
|||
+ self.W_i.serialized_size(compress)
|
|||
+ self.U_i.serialized_size(compress)
|
|||
+ self.w_i.serialized_size(compress)
|
|||
+ self.u_i.serialized_size(compress)
|
|||
+ self.cf_W_i.serialized_size(compress)
|
|||
+ self.cf_U_i.serialized_size(compress)
|
|||
}
|
|||
}
|
|||
|
|||
impl<C1, GC1, C2, GC2, FC, CS1, CS2, const MU: usize, const NU: usize, const H: bool>
|
|||
HyperNova<C1, GC1, C2, GC2, FC, CS1, CS2, MU, NU, H>
|
|||
where
|
|||
C1: CurveGroup,
|
|||
GC1: CurveVar<C1, CF2<C1>> + ToConstraintFieldGadget<CF2<C1>>,
|
|||
C2: CurveGroup,
|
|||
GC2: CurveVar<C2, CF2<C2>> + ToConstraintFieldGadget<CF2<C2>>,
|
|||
FC: FCircuit<C1::ScalarField, Params = ()>,
|
|||
CS1: CommitmentScheme<C1, H>,
|
|||
CS2: CommitmentScheme<C2, H>,
|
|||
<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>,
|
|||
{
|
|||
#[allow(clippy::too_many_arguments)]
|
|||
pub fn deserialize_hypernova<R: std::io::prelude::Read>(
|
|||
mut reader: R,
|
|||
compress: Compress,
|
|||
validate: Validate,
|
|||
poseidon_config: PoseidonConfig<C1::ScalarField>,
|
|||
cs_pp: CS1::ProverParams,
|
|||
cs_vp: CS1::VerifierParams,
|
|||
cf_cs_pp: CS2::ProverParams,
|
|||
cf_cs_vp: CS2::VerifierParams,
|
|||
) -> Result<Self, SerializationError> {
|
|||
let f_circuit = FC::new(()).unwrap();
|
|||
let prep_param = PreprocessorParam {
|
|||
poseidon_config: poseidon_config.clone(),
|
|||
F: f_circuit.clone(),
|
|||
cs_pp: Some(cs_pp.clone()),
|
|||
cs_vp: Some(cs_vp.clone()),
|
|||
cf_cs_pp: Some(cf_cs_pp.clone()),
|
|||
cf_cs_vp: Some(cf_cs_vp.clone()),
|
|||
};
|
|||
// `test_rng` won't be used in `preprocess`, since parameters have already been initialized
|
|||
let (prover_params, verifier_params) = Self::preprocess(ark_std::test_rng(), &prep_param)
|
|||
.or(Err(SerializationError::InvalidData))?;
|
|||
let pp_hash = C1::ScalarField::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let i = C1::ScalarField::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let z_0 = Vec::<C1::ScalarField>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let z_i = Vec::<C1::ScalarField>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let W_i =
|
|||
Witness::<C1::ScalarField>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let U_i = LCCCS::<C1>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let w_i =
|
|||
Witness::<C1::ScalarField>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let u_i = CCCS::<C1>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let cf_W_i =
|
|||
CycleFoldWitness::<C2>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let cf_U_i = CycleFoldCommittedInstance::<C2>::deserialize_with_mode(
|
|||
&mut reader,
|
|||
compress,
|
|||
validate,
|
|||
)?;
|
|||
let ccs = prover_params.ccs.ok_or(SerializationError::InvalidData)?;
|
|||
|
|||
Ok(HyperNova {
|
|||
_gc1: PhantomData,
|
|||
_c2: PhantomData,
|
|||
_gc2: PhantomData,
|
|||
ccs,
|
|||
cf_r1cs: verifier_params.cf_r1cs,
|
|||
poseidon_config,
|
|||
cs_pp,
|
|||
cf_cs_pp,
|
|||
F: f_circuit,
|
|||
pp_hash,
|
|||
i,
|
|||
z_0,
|
|||
z_i,
|
|||
W_i,
|
|||
U_i,
|
|||
w_i,
|
|||
u_i,
|
|||
cf_W_i,
|
|||
cf_U_i,
|
|||
})
|
|||
}
|
|||
}
|
|||
|
|||
impl<
|
|||
C1: CurveGroup,
|
|||
C2: CurveGroup,
|
|||
CS1: CommitmentScheme<C1, H>,
|
|||
CS2: CommitmentScheme<C2, H>,
|
|||
const H: bool,
|
|||
> CanonicalSerialize for ProverParams<C1, C2, CS1, CS2, H>
|
|||
{
|
|||
fn serialize_compressed<W: std::io::prelude::Write>(
|
|||
&self,
|
|||
writer: W,
|
|||
) -> Result<(), SerializationError> {
|
|||
self.serialize_with_mode(writer, Compress::Yes)
|
|||
}
|
|||
|
|||
fn compressed_size(&self) -> usize {
|
|||
self.serialized_size(Compress::Yes)
|
|||
}
|
|||
|
|||
fn serialize_uncompressed<W: std::io::prelude::Write>(
|
|||
&self,
|
|||
writer: W,
|
|||
) -> Result<(), SerializationError> {
|
|||
self.serialize_with_mode(writer, Compress::No)
|
|||
}
|
|||
|
|||
fn uncompressed_size(&self) -> usize {
|
|||
self.serialized_size(Compress::No)
|
|||
}
|
|||
|
|||
fn serialize_with_mode<W: std::io::prelude::Write>(
|
|||
&self,
|
|||
mut writer: W,
|
|||
compress: Compress,
|
|||
) -> Result<(), SerializationError> {
|
|||
self.cs_pp.serialize_with_mode(&mut writer, compress)?;
|
|||
self.cf_cs_pp.serialize_with_mode(&mut writer, compress)
|
|||
}
|
|||
|
|||
fn serialized_size(&self, compress: Compress) -> usize {
|
|||
self.cs_pp.serialized_size(compress) + self.cf_cs_pp.serialized_size(compress)
|
|||
}
|
|||
}
|
|||
|
|||
impl<
|
|||
C1: CurveGroup,
|
|||
C2: CurveGroup,
|
|||
CS1: CommitmentScheme<C1, H>,
|
|||
CS2: CommitmentScheme<C2, H>,
|
|||
const H: bool,
|
|||
> ProverParams<C1, C2, CS1, CS2, H>
|
|||
{
|
|||
pub fn deserialize_prover_params<R: std::io::prelude::Read>(
|
|||
mut reader: R,
|
|||
compress: Compress,
|
|||
validate: Validate,
|
|||
ccs: &Option<CCS<C1::ScalarField>>,
|
|||
poseidon_config: &PoseidonConfig<C1::ScalarField>,
|
|||
) -> Result<Self, SerializationError> {
|
|||
let cs_pp = CS1::ProverParams::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let cf_cs_pp = CS2::ProverParams::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
|
|||
Ok(ProverParams {
|
|||
cs_pp,
|
|||
cf_cs_pp,
|
|||
ccs: ccs.clone(),
|
|||
poseidon_config: poseidon_config.clone(),
|
|||
})
|
|||
}
|
|||
}
|
|||
|
|||
impl<
|
|||
C1: CurveGroup,
|
|||
C2: CurveGroup,
|
|||
CS1: CommitmentScheme<C1, H>,
|
|||
CS2: CommitmentScheme<C2, H>,
|
|||
const H: bool,
|
|||
> CanonicalSerialize for VerifierParams<C1, C2, CS1, CS2, H>
|
|||
{
|
|||
fn serialize_compressed<W: std::io::prelude::Write>(
|
|||
&self,
|
|||
writer: W,
|
|||
) -> Result<(), SerializationError> {
|
|||
self.serialize_with_mode(writer, Compress::Yes)
|
|||
}
|
|||
|
|||
fn compressed_size(&self) -> usize {
|
|||
self.serialized_size(Compress::Yes)
|
|||
}
|
|||
|
|||
fn serialize_uncompressed<W: std::io::prelude::Write>(
|
|||
&self,
|
|||
writer: W,
|
|||
) -> Result<(), SerializationError> {
|
|||
self.serialize_with_mode(writer, Compress::No)
|
|||
}
|
|||
|
|||
fn uncompressed_size(&self) -> usize {
|
|||
self.serialized_size(Compress::No)
|
|||
}
|
|||
|
|||
fn serialize_with_mode<W: std::io::prelude::Write>(
|
|||
&self,
|
|||
mut writer: W,
|
|||
compress: Compress,
|
|||
) -> Result<(), SerializationError> {
|
|||
self.cf_r1cs.serialize_with_mode(&mut writer, compress)?;
|
|||
self.cs_vp.serialize_with_mode(&mut writer, compress)?;
|
|||
self.cf_cs_vp.serialize_with_mode(&mut writer, compress)
|
|||
}
|
|||
|
|||
fn serialized_size(&self, compress: Compress) -> usize {
|
|||
self.cf_r1cs.serialized_size(compress)
|
|||
+ self.cs_vp.serialized_size(compress)
|
|||
+ self.cf_cs_vp.serialized_size(compress)
|
|||
}
|
|||
}
|
|||
|
|||
impl<
|
|||
C1: CurveGroup,
|
|||
C2: CurveGroup,
|
|||
CS1: CommitmentScheme<C1, H>,
|
|||
CS2: CommitmentScheme<C2, H>,
|
|||
const H: bool,
|
|||
> VerifierParams<C1, C2, CS1, CS2, H>
|
|||
{
|
|||
pub fn deserialize_verifier_params<R: std::io::Read>(
|
|||
mut reader: R,
|
|||
compress: Compress,
|
|||
validate: Validate,
|
|||
ccs: &CCS<C1::ScalarField>,
|
|||
poseidon_config: &PoseidonConfig<C1::ScalarField>,
|
|||
) -> Result<Self, SerializationError> {
|
|||
let cf_r1cs = R1CS::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let cs_vp = CS1::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let cf_cs_vp = CS2::VerifierParams::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
Ok(VerifierParams {
|
|||
ccs: ccs.clone(),
|
|||
poseidon_config: poseidon_config.clone(),
|
|||
cf_r1cs,
|
|||
cs_vp,
|
|||
cf_cs_vp,
|
|||
})
|
|||
}
|
|||
}
|
|||
|
|||
#[cfg(test)]
|
|||
pub mod tests {
|
|||
use crate::FoldingScheme;
|
|||
use crate::MultiFolding;
|
|||
use ark_serialize::{Compress, Validate, Write};
|
|||
use std::fs;
|
|||
|
|||
use crate::{
|
|||
commitment::{kzg::KZG, pedersen::Pedersen},
|
|||
folding::hypernova::{tests::test_ivc_opt, HyperNova},
|
|||
frontend::{utils::CubicFCircuit, FCircuit},
|
|||
transcript::poseidon::poseidon_canonical_config,
|
|||
};
|
|||
use ark_bn254::{constraints::GVar, Bn254, Fr, G1Projective as Projective};
|
|||
use ark_grumpkin::{constraints::GVar as GVar2, Projective as Projective2};
|
|||
use ark_serialize::CanonicalSerialize;
|
|||
|
|||
#[test]
|
|||
fn test_serde_hypernova() {
|
|||
let poseidon_config = poseidon_canonical_config::<Fr>();
|
|||
let F_circuit = CubicFCircuit::<Fr>::new(()).unwrap();
|
|||
let (mut hn, (_, verifier_params), _, _, _) = test_ivc_opt::<
|
|||
KZG<Bn254>,
|
|||
Pedersen<Projective2>,
|
|||
false,
|
|||
>(poseidon_config.clone(), F_circuit);
|
|||
|
|||
let mut writer = vec![];
|
|||
assert!(hn.serialize_compressed(&mut writer).is_ok());
|
|||
let mut writer = vec![];
|
|||
assert!(hn.serialize_uncompressed(&mut writer).is_ok());
|
|||
|
|||
let mut file = fs::OpenOptions::new()
|
|||
.create(true)
|
|||
.write(true)
|
|||
.open("./hypernova.serde")
|
|||
.unwrap();
|
|||
|
|||
file.write_all(&writer).unwrap();
|
|||
|
|||
let bytes = fs::read("./hypernova.serde").unwrap();
|
|||
|
|||
let mut hn_deserialized = HyperNova::<
|
|||
Projective,
|
|||
GVar,
|
|||
Projective2,
|
|||
GVar2,
|
|||
CubicFCircuit<Fr>,
|
|||
KZG<Bn254>,
|
|||
Pedersen<Projective2>,
|
|||
2,
|
|||
3,
|
|||
false,
|
|||
>::deserialize_hypernova(
|
|||
bytes.as_slice(),
|
|||
Compress::No,
|
|||
Validate::No,
|
|||
poseidon_config,
|
|||
hn.cs_pp.clone(),
|
|||
verifier_params.cs_vp,
|
|||
hn.cf_cs_pp.clone(),
|
|||
verifier_params.cf_cs_vp,
|
|||
)
|
|||
.unwrap();
|
|||
|
|||
assert_eq!(hn.i, hn_deserialized.i);
|
|||
|
|||
let mut rng = ark_std::test_rng();
|
|||
for _ in 0..3 {
|
|||
// prepare some new instances to fold in the multifolding step
|
|||
let mut lcccs = vec![];
|
|||
for j in 0..1 {
|
|||
let instance_state = vec![Fr::from(j as u32 + 85_u32)];
|
|||
let (U, W) = hn
|
|||
.new_running_instance(&mut rng, instance_state, vec![])
|
|||
.unwrap();
|
|||
lcccs.push((U, W));
|
|||
}
|
|||
let mut cccs = vec![];
|
|||
for j in 0..2 {
|
|||
let instance_state = vec![Fr::from(j as u32 + 15_u32)];
|
|||
let (u, w) = hn
|
|||
.new_incoming_instance(&mut rng, instance_state, vec![])
|
|||
.unwrap();
|
|||
cccs.push((u, w));
|
|||
}
|
|||
|
|||
hn.prove_step(&mut rng, vec![], Some((lcccs.clone(), cccs.clone())))
|
|||
.unwrap();
|
|||
hn_deserialized
|
|||
.prove_step(&mut rng, vec![], Some((lcccs, cccs)))
|
|||
.unwrap();
|
|||
}
|
|||
|
|||
assert_eq!(hn.z_i, hn_deserialized.z_i);
|
|||
}
|
|||
}
|
@ -0,0 +1,492 @@ |
|||
/// This file implements the offchain decider. For ethereum use cases, use the
|
|||
/// DeciderEth from decider_eth.rs file.
|
|||
/// More details can be found at the documentation page:
|
|||
/// https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-offchain.html
|
|||
use ark_crypto_primitives::sponge::Absorb;
|
|||
use ark_ec::{AffineRepr, CurveGroup, Group};
|
|||
use ark_ff::{BigInteger, PrimeField};
|
|||
use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget};
|
|||
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
|
|||
use ark_snark::SNARK;
|
|||
use ark_std::rand::{CryptoRng, RngCore};
|
|||
use ark_std::{One, Zero};
|
|||
use core::marker::PhantomData;
|
|||
|
|||
use super::decider_circuits::{DeciderCircuit1, DeciderCircuit2};
|
|||
use super::{nifs::NIFS, traits::NIFSTrait, CommittedInstance, Nova};
|
|||
use crate::commitment::CommitmentScheme;
|
|||
use crate::folding::circuits::{
|
|||
cyclefold::CycleFoldCommittedInstance,
|
|||
nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar},
|
|||
CF2,
|
|||
};
|
|||
use crate::frontend::FCircuit;
|
|||
use crate::Error;
|
|||
use crate::{Decider as DeciderTrait, FoldingScheme};
|
|||
|
|||
#[derive(Debug, Clone, Eq, PartialEq)]
|
|||
pub struct Proof<C1, C2, CS1, CS2, S1, S2>
|
|||
where
|
|||
C1: CurveGroup,
|
|||
C2: CurveGroup,
|
|||
CS1: CommitmentScheme<C1>,
|
|||
CS2: CommitmentScheme<C2>,
|
|||
S1: SNARK<C1::ScalarField>,
|
|||
S2: SNARK<C2::ScalarField>,
|
|||
{
|
|||
c1_snark_proof: S1::Proof,
|
|||
c2_snark_proof: S2::Proof,
|
|||
cs1_proofs: [CS1::Proof; 2],
|
|||
cs2_proofs: [CS2::Proof; 2],
|
|||
// cmT and r are values for the last fold, U_{i+1}=NIFS.V(r, U_i, u_i, cmT), and they are
|
|||
// checked in-circuit
|
|||
cmT: C1,
|
|||
r: C1::ScalarField,
|
|||
// cyclefold committed instance
|
|||
cf_U_i: CycleFoldCommittedInstance<C2>,
|
|||
// the CS challenges are provided by the prover, but in-circuit they are checked to match the
|
|||
// in-circuit computed computed ones.
|
|||
cs1_challenges: [C1::ScalarField; 2],
|
|||
cs2_challenges: [C2::ScalarField; 2],
|
|||
}
|
|||
|
|||
#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
|
|||
pub struct ProverParam<CS1_ProvingKey, S1_ProvingKey, CS2_ProvingKey, S2_ProvingKey>
|
|||
where
|
|||
CS1_ProvingKey: Clone + CanonicalSerialize + CanonicalDeserialize,
|
|||
S1_ProvingKey: Clone + CanonicalSerialize + CanonicalDeserialize,
|
|||
CS2_ProvingKey: Clone + CanonicalSerialize + CanonicalDeserialize,
|
|||
S2_ProvingKey: Clone + CanonicalSerialize + CanonicalDeserialize,
|
|||
{
|
|||
pub c1_snark_pp: S1_ProvingKey,
|
|||
pub c1_cs_pp: CS1_ProvingKey,
|
|||
pub c2_snark_pp: S2_ProvingKey,
|
|||
pub c2_cs_pp: CS2_ProvingKey,
|
|||
}
|
|||
|
|||
#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
|
|||
pub struct VerifierParam<C1, CS1_VerifyingKey, S1_VerifyingKey, CS2_VerifyingKey, S2_VerifyingKey>
|
|||
where
|
|||
C1: CurveGroup,
|
|||
CS1_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize,
|
|||
S1_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize,
|
|||
CS2_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize,
|
|||
S2_VerifyingKey: Clone + CanonicalSerialize + CanonicalDeserialize,
|
|||
{
|
|||
pub pp_hash: C1::ScalarField,
|
|||
pub c1_snark_vp: S1_VerifyingKey,
|
|||
pub c1_cs_vp: CS1_VerifyingKey,
|
|||
pub c2_snark_vp: S2_VerifyingKey,
|
|||
pub c2_cs_vp: CS2_VerifyingKey,
|
|||
}
|
|||
|
|||
/// Onchain Decider, for ethereum use cases
|
|||
#[derive(Clone, Debug)]
|
|||
pub struct Decider<C1, GC1, C2, GC2, FC, CS1, CS2, S1, S2, FS> {
|
|||
_c1: PhantomData<C1>,
|
|||
_gc1: PhantomData<GC1>,
|
|||
_c2: PhantomData<C2>,
|
|||
_gc2: PhantomData<GC2>,
|
|||
_fc: PhantomData<FC>,
|
|||
_cs1: PhantomData<CS1>,
|
|||
_cs2: PhantomData<CS2>,
|
|||
_s1: PhantomData<S1>,
|
|||
_s2: PhantomData<S2>,
|
|||
_fs: PhantomData<FS>,
|
|||
}
|
|||
|
|||
impl<C1, GC1, C2, GC2, FC, CS1, CS2, S1, S2, FS> DeciderTrait<C1, C2, FC, FS>
|
|||
for Decider<C1, GC1, C2, GC2, FC, CS1, CS2, S1, S2, FS>
|
|||
where
|
|||
C1: CurveGroup,
|
|||
C2: CurveGroup,
|
|||
GC1: CurveVar<C1, CF2<C1>> + ToConstraintFieldGadget<CF2<C1>>,
|
|||
GC2: CurveVar<C2, CF2<C2>> + ToConstraintFieldGadget<CF2<C2>>,
|
|||
FC: FCircuit<C1::ScalarField>,
|
|||
CS1: CommitmentScheme<
|
|||
C1,
|
|||
ProverChallenge = C1::ScalarField,
|
|||
Challenge = C1::ScalarField,
|
|||
Proof = crate::commitment::kzg::Proof<C1>,
|
|||
>,
|
|||
CS2: CommitmentScheme<
|
|||
C2,
|
|||
ProverChallenge = C2::ScalarField,
|
|||
Challenge = C2::ScalarField,
|
|||
Proof = crate::commitment::kzg::Proof<C2>,
|
|||
>,
|
|||
S1: SNARK<C1::ScalarField>,
|
|||
S2: SNARK<C2::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 GC1: GroupOpsBounds<'b, C1, GC1>,
|
|||
for<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>,
|
|||
// constrain FS into Nova, since this is a Decider specifically for Nova
|
|||
Nova<C1, GC1, C2, GC2, FC, CS1, CS2, false>: From<FS>,
|
|||
crate::folding::nova::ProverParams<C1, C2, CS1, CS2, false>: |
|||
From<<FS as FoldingScheme<C1, C2, FC>>::ProverParam>,
|
|||
crate::folding::nova::VerifierParams<C1, C2, CS1, CS2, false>: |
|||
From<<FS as FoldingScheme<C1, C2, FC>>::VerifierParam>,
|
|||
{
|
|||
type PreprocessorParam = (FS::ProverParam, FS::VerifierParam);
|
|||
type ProverParam =
|
|||
ProverParam<CS1::ProverParams, S1::ProvingKey, CS2::ProverParams, S2::ProvingKey>;
|
|||
type Proof = Proof<C1, C2, CS1, CS2, S1, S2>;
|
|||
type VerifierParam = VerifierParam<
|
|||
C1,
|
|||
CS1::VerifierParams,
|
|||
S1::VerifyingKey,
|
|||
CS2::VerifierParams,
|
|||
S2::VerifyingKey,
|
|||
>;
|
|||
type PublicInput = Vec<C1::ScalarField>;
|
|||
type CommittedInstance = CommittedInstance<C1>;
|
|||
|
|||
fn preprocess(
|
|||
mut rng: impl RngCore + CryptoRng,
|
|||
prep_param: Self::PreprocessorParam,
|
|||
fs: FS,
|
|||
) -> Result<(Self::ProverParam, Self::VerifierParam), Error> {
|
|||
let circuit1 = DeciderCircuit1::<C1, C2, GC2>::from_nova::<GC1, CS1, CS2, false, FC>(
|
|||
fs.clone().into(),
|
|||
)?;
|
|||
let circuit2 =
|
|||
DeciderCircuit2::<C1, GC1, C2>::from_nova::<GC2, CS1, CS2, false, FC>(fs.into())?;
|
|||
|
|||
// get the Groth16 specific setup for the circuits
|
|||
let (c1_g16_pk, c1_g16_vk) = S1::circuit_specific_setup(circuit1, &mut rng).unwrap();
|
|||
let (c2_g16_pk, c2_g16_vk) = S2::circuit_specific_setup(circuit2, &mut rng).unwrap();
|
|||
|
|||
// get the FoldingScheme prover & verifier params from Nova
|
|||
#[allow(clippy::type_complexity)]
|
|||
let nova_pp: <Nova<C1, GC1, C2, GC2, FC, CS1, CS2, false> as FoldingScheme<
|
|||
C1,
|
|||
C2,
|
|||
FC,
|
|||
>>::ProverParam = prep_param.0.clone().into();
|
|||
#[allow(clippy::type_complexity)]
|
|||
let nova_vp: <Nova<C1, GC1, C2, GC2, FC, CS1, CS2, false> as FoldingScheme<
|
|||
C1,
|
|||
C2,
|
|||
FC,
|
|||
>>::VerifierParam = prep_param.1.clone().into();
|
|||
|
|||
let pp_hash = nova_vp.pp_hash()?;
|
|||
let pp = Self::ProverParam {
|
|||
c1_snark_pp: c1_g16_pk,
|
|||
c1_cs_pp: nova_pp.cs_pp,
|
|||
c2_snark_pp: c2_g16_pk,
|
|||
c2_cs_pp: nova_pp.cf_cs_pp,
|
|||
};
|
|||
let vp = Self::VerifierParam {
|
|||
pp_hash,
|
|||
c1_snark_vp: c1_g16_vk,
|
|||
c1_cs_vp: nova_vp.cs_vp,
|
|||
c2_snark_vp: c2_g16_vk,
|
|||
c2_cs_vp: nova_vp.cf_cs_vp,
|
|||
};
|
|||
Ok((pp, vp))
|
|||
}
|
|||
|
|||
fn prove(
|
|||
mut rng: impl RngCore + CryptoRng,
|
|||
pp: Self::ProverParam,
|
|||
fs: FS,
|
|||
) -> Result<Self::Proof, Error> {
|
|||
let circuit1 = DeciderCircuit1::<C1, C2, GC2>::from_nova::<GC1, CS1, CS2, false, FC>(
|
|||
fs.clone().into(),
|
|||
)?;
|
|||
let circuit2 =
|
|||
DeciderCircuit2::<C1, GC1, C2>::from_nova::<GC2, CS1, CS2, false, FC>(fs.into())?;
|
|||
|
|||
let c1_snark_proof = S1::prove(&pp.c1_snark_pp, circuit1.clone(), &mut rng)
|
|||
.map_err(|e| Error::Other(e.to_string()))?;
|
|||
let c2_snark_proof = S2::prove(&pp.c2_snark_pp, circuit2.clone(), &mut rng)
|
|||
.map_err(|e| Error::Other(e.to_string()))?;
|
|||
|
|||
let cmT = circuit1.cmT.unwrap();
|
|||
let r_Fr = circuit1.r.unwrap();
|
|||
let W_i1 = circuit1.W_i1.unwrap();
|
|||
let cf_W_i = circuit2.cf_W_i.unwrap();
|
|||
|
|||
// get the challenges that have been already computed when preparing the circuits inputs in
|
|||
// the above `from_nova` calls
|
|||
let challenge_W = circuit1
|
|||
.cs_c_W
|
|||
.ok_or(Error::MissingValue("cs_c_W".to_string()))?;
|
|||
let challenge_E = circuit1
|
|||
.cs_c_E
|
|||
.ok_or(Error::MissingValue("cs_c_E".to_string()))?;
|
|||
let c2_challenge_W = circuit2
|
|||
.cs_c_W
|
|||
.ok_or(Error::MissingValue("c2's cs_c_W".to_string()))?;
|
|||
let c2_challenge_E = circuit2
|
|||
.cs_c_E
|
|||
.ok_or(Error::MissingValue("c2's cs_c_E".to_string()))?;
|
|||
|
|||
// generate CommitmentScheme proofs for the main instance
|
|||
let U_cmW_proof = CS1::prove_with_challenge(
|
|||
&pp.c1_cs_pp,
|
|||
challenge_W,
|
|||
&W_i1.W,
|
|||
&C1::ScalarField::zero(),
|
|||
None,
|
|||
)?;
|
|||
let U_cmE_proof = CS1::prove_with_challenge(
|
|||
&pp.c1_cs_pp,
|
|||
challenge_E,
|
|||
&W_i1.E,
|
|||
&C1::ScalarField::zero(),
|
|||
None,
|
|||
)?;
|
|||
// CS proofs for the CycleFold instance
|
|||
let cf_cmW_proof = CS2::prove_with_challenge(
|
|||
&pp.c2_cs_pp,
|
|||
c2_challenge_W,
|
|||
&cf_W_i.W,
|
|||
&C2::ScalarField::zero(),
|
|||
None,
|
|||
)?;
|
|||
let cf_cmE_proof = CS2::prove_with_challenge(
|
|||
&pp.c2_cs_pp,
|
|||
c2_challenge_E,
|
|||
&cf_W_i.E,
|
|||
&C2::ScalarField::zero(),
|
|||
None,
|
|||
)?;
|
|||
|
|||
Ok(Self::Proof {
|
|||
c1_snark_proof,
|
|||
c2_snark_proof,
|
|||
cs1_proofs: [U_cmW_proof, U_cmE_proof],
|
|||
cs2_proofs: [cf_cmW_proof, cf_cmE_proof],
|
|||
cmT,
|
|||
r: r_Fr,
|
|||
cf_U_i: circuit1.cf_U_i.unwrap(),
|
|||
cs1_challenges: [challenge_W, challenge_E],
|
|||
cs2_challenges: [c2_challenge_W, c2_challenge_E],
|
|||
})
|
|||
}
|
|||
|
|||
fn verify(
|
|||
vp: Self::VerifierParam,
|
|||
i: C1::ScalarField,
|
|||
z_0: Vec<C1::ScalarField>,
|
|||
z_i: Vec<C1::ScalarField>,
|
|||
running_instance: &Self::CommittedInstance,
|
|||
incoming_instance: &Self::CommittedInstance,
|
|||
proof: &Self::Proof,
|
|||
) -> Result<bool, Error> {
|
|||
if i <= C1::ScalarField::one() {
|
|||
return Err(Error::NotEnoughSteps);
|
|||
}
|
|||
|
|||
// compute U = U_{d+1}= NIFS.V(U_d, u_d, cmT)
|
|||
let U = NIFS::<C1, CS1>::verify(proof.r, running_instance, incoming_instance, &proof.cmT);
|
|||
|
|||
let (cmE_x, cmE_y) = NonNativeAffineVar::inputize(U.cmE)?;
|
|||
let (cmW_x, cmW_y) = NonNativeAffineVar::inputize(U.cmW)?;
|
|||
let (cmT_x, cmT_y) = NonNativeAffineVar::inputize(proof.cmT)?;
|
|||
|
|||
let zero = (&C2::BaseField::zero(), &C2::BaseField::zero());
|
|||
let cmE_affine = proof.cf_U_i.cmE.into_affine();
|
|||
let cmW_affine = proof.cf_U_i.cmW.into_affine();
|
|||
let (cf_cmE_x, cf_cmE_y) = cmE_affine.xy().unwrap_or(zero);
|
|||
let cf_cmE_z = C1::ScalarField::one();
|
|||
let (cf_cmW_x, cf_cmW_y) = cmW_affine.xy().unwrap_or(zero);
|
|||
let cf_cmW_z = C1::ScalarField::one();
|
|||
|
|||
// snark proof 1
|
|||
let c1_public_input: Vec<C1::ScalarField> = [
|
|||
vec![vp.pp_hash, i],
|
|||
z_0,
|
|||
z_i,
|
|||
// U_{i+1} values:
|
|||
vec![U.u],
|
|||
U.x.clone(),
|
|||
cmE_x,
|
|||
cmE_y,
|
|||
cmW_x,
|
|||
cmW_y,
|
|||
// CS1 values:
|
|||
proof.cs1_challenges.to_vec(), // c_W, c_E
|
|||
vec![
|
|||
proof.cs1_proofs[0].eval, // eval_W
|
|||
proof.cs1_proofs[1].eval, // eval_E
|
|||
],
|
|||
// cf_U_i values
|
|||
NonNativeUintVar::<CF2<C2>>::inputize(proof.cf_U_i.u),
|
|||
proof
|
|||
.cf_U_i
|
|||
.x
|
|||
.iter()
|
|||
.flat_map(|&x_i| NonNativeUintVar::<CF2<C2>>::inputize(x_i))
|
|||
.collect::<Vec<C1::ScalarField>>(),
|
|||
vec![
|
|||
*cf_cmE_x, *cf_cmE_y, cf_cmE_z, *cf_cmW_x, *cf_cmW_y, cf_cmW_z,
|
|||
],
|
|||
// NIFS values:
|
|||
cmT_x,
|
|||
cmT_y,
|
|||
vec![proof.r],
|
|||
]
|
|||
.concat();
|
|||
|
|||
let c1_snark_v = S1::verify(&vp.c1_snark_vp, &c1_public_input, &proof.c1_snark_proof)
|
|||
.map_err(|e| Error::Other(e.to_string()))?;
|
|||
if !c1_snark_v {
|
|||
return Err(Error::SNARKVerificationFail);
|
|||
}
|
|||
|
|||
let (cf2_cmE_x, cf2_cmE_y) = NonNativeAffineVar::inputize(proof.cf_U_i.cmE)?;
|
|||
let (cf2_cmW_x, cf2_cmW_y) = NonNativeAffineVar::inputize(proof.cf_U_i.cmW)?;
|
|||
|
|||
// snark proof 2
|
|||
// migrate pp_hash from C1::Fr to C1::Fq
|
|||
let pp_hash_Fq =
|
|||
C2::ScalarField::from_le_bytes_mod_order(&vp.pp_hash.into_bigint().to_bytes_le());
|
|||
let c2_public_input: Vec<C2::ScalarField> = [
|
|||
vec![pp_hash_Fq],
|
|||
vec![proof.cf_U_i.u],
|
|||
proof.cf_U_i.x.clone(),
|
|||
cf2_cmE_x,
|
|||
cf2_cmE_y,
|
|||
cf2_cmW_x,
|
|||
cf2_cmW_y,
|
|||
proof.cs2_challenges.to_vec(),
|
|||
vec![
|
|||
proof.cs2_proofs[0].eval, // eval_W
|
|||
proof.cs2_proofs[1].eval, // eval_E
|
|||
],
|
|||
]
|
|||
.concat();
|
|||
|
|||
let c2_snark_v = S2::verify(&vp.c2_snark_vp, &c2_public_input, &proof.c2_snark_proof)
|
|||
.map_err(|e| Error::Other(e.to_string()))?;
|
|||
if !c2_snark_v {
|
|||
return Err(Error::SNARKVerificationFail);
|
|||
}
|
|||
|
|||
// check C1 commitments (main instance commitments)
|
|||
CS1::verify_with_challenge(
|
|||
&vp.c1_cs_vp,
|
|||
proof.cs1_challenges[0],
|
|||
&U.cmW,
|
|||
&proof.cs1_proofs[0],
|
|||
)?;
|
|||
CS1::verify_with_challenge(
|
|||
&vp.c1_cs_vp,
|
|||
proof.cs1_challenges[1],
|
|||
&U.cmE,
|
|||
&proof.cs1_proofs[1],
|
|||
)?;
|
|||
|
|||
// check C2 commitments (CycleFold instance commitments)
|
|||
CS2::verify_with_challenge(
|
|||
&vp.c2_cs_vp,
|
|||
proof.cs2_challenges[0],
|
|||
&proof.cf_U_i.cmW,
|
|||
&proof.cs2_proofs[0],
|
|||
)?;
|
|||
CS2::verify_with_challenge(
|
|||
&vp.c2_cs_vp,
|
|||
proof.cs2_challenges[1],
|
|||
&proof.cf_U_i.cmE,
|
|||
&proof.cs2_proofs[1],
|
|||
)?;
|
|||
|
|||
Ok(true)
|
|||
}
|
|||
}
|
|||
|
|||
#[cfg(test)]
|
|||
pub mod tests {
|
|||
use ark_groth16::Groth16;
|
|||
|
|||
// Note: do not use the MNTx_298 curves in practice, these are just for tests. Use the MNTx_753
|
|||
// curves instead.
|
|||
use ark_mnt4_298::{
|
|||
constraints::G1Var as GVar, Fr, G1Projective as Projective, MNT4_298 as MNT4,
|
|||
};
|
|||
use ark_mnt6_298::{
|
|||
constraints::G1Var as GVar2, G1Projective as Projective2, MNT6_298 as MNT6,
|
|||
};
|
|||
use std::time::Instant;
|
|||
|
|||
use super::*;
|
|||
use crate::commitment::kzg::KZG;
|
|||
use crate::folding::nova::PreprocessorParam;
|
|||
use crate::frontend::utils::CubicFCircuit;
|
|||
use crate::transcript::poseidon::poseidon_canonical_config;
|
|||
|
|||
#[test]
|
|||
fn test_decider() {
|
|||
// use Nova as FoldingScheme
|
|||
type N = Nova<
|
|||
Projective,
|
|||
GVar,
|
|||
Projective2,
|
|||
GVar2,
|
|||
CubicFCircuit<Fr>,
|
|||
KZG<'static, MNT4>,
|
|||
KZG<'static, MNT6>,
|
|||
false,
|
|||
>;
|
|||
type D = Decider<
|
|||
Projective,
|
|||
GVar,
|
|||
Projective2,
|
|||
GVar2,
|
|||
CubicFCircuit<Fr>,
|
|||
KZG<'static, MNT4>,
|
|||
KZG<'static, MNT6>,
|
|||
Groth16<MNT4>,
|
|||
Groth16<MNT6>,
|
|||
N, // here we define the FoldingScheme to use
|
|||
>;
|
|||
|
|||
let mut rng = ark_std::test_rng();
|
|||
let poseidon_config = poseidon_canonical_config::<Fr>();
|
|||
|
|||
let F_circuit = CubicFCircuit::<Fr>::new(()).unwrap();
|
|||
let z_0 = vec![Fr::from(3_u32)];
|
|||
|
|||
let start = Instant::now();
|
|||
let prep_param = PreprocessorParam::new(poseidon_config, F_circuit);
|
|||
let nova_params = N::preprocess(&mut rng, &prep_param).unwrap();
|
|||
println!("Nova preprocess, {:?}", start.elapsed());
|
|||
|
|||
let start = Instant::now();
|
|||
let mut nova = N::init(&nova_params, F_circuit, z_0.clone()).unwrap();
|
|||
println!("Nova initialized, {:?}", start.elapsed());
|
|||
let start = Instant::now();
|
|||
nova.prove_step(&mut rng, vec![], None).unwrap();
|
|||
println!("prove_step, {:?}", start.elapsed());
|
|||
nova.prove_step(&mut rng, vec![], None).unwrap(); // do a 2nd step
|
|||
|
|||
let mut rng = rand::rngs::OsRng;
|
|||
|
|||
// prepare the Decider prover & verifier params
|
|||
let start = Instant::now();
|
|||
let (decider_pp, decider_vp) = D::preprocess(&mut rng, nova_params, nova.clone()).unwrap();
|
|||
println!("Decider preprocess, {:?}", start.elapsed());
|
|||
|
|||
// decider proof generation
|
|||
let start = Instant::now();
|
|||
let proof = D::prove(rng, decider_pp, nova.clone()).unwrap();
|
|||
println!("Decider prove, {:?}", start.elapsed());
|
|||
|
|||
// decider proof verification
|
|||
let start = Instant::now();
|
|||
let verified = D::verify(
|
|||
decider_vp, nova.i, nova.z_0, nova.z_i, &nova.U_i, &nova.u_i, &proof,
|
|||
)
|
|||
.unwrap();
|
|||
assert!(verified);
|
|||
println!("Decider verify, {:?}", start.elapsed());
|
|||
}
|
|||
}
|
@ -0,0 +1,553 @@ |
|||
/// This file implements the offchain decider circuit. For ethereum use cases, use the
|
|||
/// DeciderEthCircuit.
|
|||
/// More details can be found at the documentation page:
|
|||
/// https://privacy-scaling-explorations.github.io/sonobe-docs/design/nova-decider-offchain.html
|
|||
use ark_crypto_primitives::sponge::{
|
|||
constraints::CryptographicSpongeVar,
|
|||
poseidon::{constraints::PoseidonSpongeVar, PoseidonConfig, PoseidonSponge},
|
|||
Absorb, CryptographicSponge,
|
|||
};
|
|||
use ark_ec::{CurveGroup, Group};
|
|||
use ark_ff::{BigInteger, PrimeField};
|
|||
use ark_poly::Polynomial;
|
|||
use ark_r1cs_std::{
|
|||
alloc::AllocVar,
|
|||
boolean::Boolean,
|
|||
eq::EqGadget,
|
|||
fields::{fp::FpVar, FieldVar},
|
|||
groups::GroupOpsBounds,
|
|||
prelude::CurveVar,
|
|||
ToConstraintFieldGadget,
|
|||
};
|
|||
use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError};
|
|||
use ark_std::Zero;
|
|||
use core::marker::PhantomData;
|
|||
|
|||
use super::{
|
|||
circuits::{ChallengeGadget, CommittedInstanceVar},
|
|||
decider_eth_circuit::{
|
|||
evaluate_gadget, KZGChallengesGadget, R1CSVar, RelaxedR1CSGadget, WitnessVar,
|
|||
},
|
|||
nifs::NIFS,
|
|||
traits::NIFSTrait,
|
|||
CommittedInstance, Nova, Witness,
|
|||
};
|
|||
use crate::arith::r1cs::R1CS;
|
|||
use crate::commitment::CommitmentScheme;
|
|||
use crate::folding::circuits::{
|
|||
cyclefold::{
|
|||
CycleFoldCommittedInstance, CycleFoldCommittedInstanceVar, CycleFoldConfig,
|
|||
CycleFoldWitness,
|
|||
},
|
|||
nonnative::{affine::NonNativeAffineVar, uint::NonNativeUintVar},
|
|||
CF1, CF2,
|
|||
};
|
|||
use crate::folding::nova::NovaCycleFoldConfig;
|
|||
use crate::folding::traits::{CommittedInstanceVarOps, Dummy};
|
|||
use crate::frontend::FCircuit;
|
|||
use crate::utils::vec::poly_from_vec;
|
|||
use crate::Error;
|
|||
|
|||
/// Circuit that implements part of the in-circuit checks needed for the offchain verification over
|
|||
/// the Curve2's BaseField (=Curve1's ScalarField).
|
|||
#[derive(Clone, Debug)]
|
|||
pub struct DeciderCircuit1<C1, C2, GC2>
|
|||
where
|
|||
C1: CurveGroup,
|
|||
C2: CurveGroup,
|
|||
GC2: CurveVar<C2, CF2<C2>>,
|
|||
{
|
|||
_c1: PhantomData<C1>,
|
|||
_c2: PhantomData<C2>,
|
|||
_gc2: PhantomData<GC2>,
|
|||
|
|||
/// 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>,
|
|||
pub poseidon_config: PoseidonConfig<CF1<C1>>,
|
|||
/// public params hash
|
|||
pub pp_hash: Option<C1::ScalarField>,
|
|||
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>>,
|
|||
pub U_i1: Option<CommittedInstance<C1>>,
|
|||
pub W_i1: Option<Witness<C1>>,
|
|||
pub cmT: Option<C1>,
|
|||
pub r: Option<C1::ScalarField>,
|
|||
/// CycleFold running instance
|
|||
pub cf_U_i: Option<CycleFoldCommittedInstance<C2>>,
|
|||
|
|||
/// Commitment Scheme challenges
|
|||
pub cs_c_W: Option<C1::ScalarField>,
|
|||
pub cs_c_E: Option<C1::ScalarField>,
|
|||
/// Evaluations of the committed polynomials at the challenge
|
|||
pub eval_W: Option<C1::ScalarField>,
|
|||
pub eval_E: Option<C1::ScalarField>,
|
|||
}
|
|||
impl<C1, C2, GC2> DeciderCircuit1<C1, C2, GC2>
|
|||
where
|
|||
C1: CurveGroup,
|
|||
<C1 as CurveGroup>::BaseField: PrimeField,
|
|||
<C1 as Group>::ScalarField: Absorb,
|
|||
C2: CurveGroup,
|
|||
GC2: CurveVar<C2, CF2<C2>> + ToConstraintFieldGadget<CF2<C2>>,
|
|||
for<'b> &'b GC2: GroupOpsBounds<'b, C2, GC2>,
|
|||
{
|
|||
pub fn from_nova<GC1, CS1, CS2, const H: bool, FC: FCircuit<C1::ScalarField>>(
|
|||
nova: Nova<C1, GC1, C2, GC2, FC, CS1, CS2, H>,
|
|||
) -> Result<Self, Error>
|
|||
where
|
|||
C2: CurveGroup,
|
|||
GC1: CurveVar<C1, CF2<C1>> + ToConstraintFieldGadget<CF2<C1>>,
|
|||
GC2: CurveVar<C2, CF2<C2>> + ToConstraintFieldGadget<CF2<C2>>,
|
|||
CS1: CommitmentScheme<C1, H>,
|
|||
CS2: CommitmentScheme<C2, H>,
|
|||
{
|
|||
let mut transcript = PoseidonSponge::<C1::ScalarField>::new(&nova.poseidon_config);
|
|||
// pp_hash is absorbed to transcript at the ChallengeGadget::get_challenge_native call
|
|||
|
|||
// compute the U_{i+1}, W_{i+1}
|
|||
let (T, cmT) = NIFS::<C1, CS1, H>::compute_cmT(
|
|||
&nova.cs_pp,
|
|||
&nova.r1cs.clone(),
|
|||
&nova.w_i.clone(),
|
|||
&nova.u_i.clone(),
|
|||
&nova.W_i.clone(),
|
|||
&nova.U_i.clone(),
|
|||
)?;
|
|||
let r_bits = NIFS::<C1, CS1, H>::get_challenge(
|
|||
&mut transcript,
|
|||
nova.pp_hash,
|
|||
&nova.U_i,
|
|||
&nova.u_i,
|
|||
&cmT,
|
|||
);
|
|||
let r_Fr = C1::ScalarField::from_bigint(BigInteger::from_bits_le(&r_bits))
|
|||
.ok_or(Error::OutOfBounds)?;
|
|||
let (W_i1, U_i1) =
|
|||
NIFS::<C1, CS1, H>::prove(r_Fr, &nova.W_i, &nova.U_i, &nova.w_i, &nova.u_i, &T, &cmT)?;
|
|||
|
|||
// compute the commitment scheme challenges used as inputs in the circuit
|
|||
let (cs_challenge_W, cs_challenge_E) =
|
|||
KZGChallengesGadget::<C1>::get_challenges_native(&mut transcript, U_i1.clone());
|
|||
|
|||
// get evals of the committed polys at the challenges
|
|||
let mut W = W_i1.W.clone();
|
|||
W.extend(
|
|||
std::iter::repeat(C1::ScalarField::zero())
|
|||
.take(W_i1.W.len().next_power_of_two() - W_i1.W.len()),
|
|||
);
|
|||
let mut E = W_i1.E.clone();
|
|||
E.extend(
|
|||
std::iter::repeat(C1::ScalarField::zero())
|
|||
.take(W_i1.E.len().next_power_of_two() - W_i1.E.len()),
|
|||
);
|
|||
let p_W = poly_from_vec(W.to_vec())?;
|
|||
let eval_W = p_W.evaluate(&cs_challenge_W);
|
|||
let p_E = poly_from_vec(E.to_vec())?;
|
|||
let eval_E = p_E.evaluate(&cs_challenge_E);
|
|||
|
|||
Ok(Self {
|
|||
_c1: PhantomData,
|
|||
_c2: PhantomData,
|
|||
_gc2: PhantomData,
|
|||
|
|||
E_len: nova.W_i.E.len(),
|
|||
cf_E_len: nova.cf_W_i.E.len(),
|
|||
r1cs: nova.r1cs,
|
|||
poseidon_config: nova.poseidon_config,
|
|||
pp_hash: Some(nova.pp_hash),
|
|||
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),
|
|||
U_i1: Some(U_i1),
|
|||
W_i1: Some(W_i1),
|
|||
cmT: Some(cmT),
|
|||
r: Some(r_Fr),
|
|||
cf_U_i: Some(nova.cf_U_i),
|
|||
cs_c_W: Some(cs_challenge_W),
|
|||
cs_c_E: Some(cs_challenge_E),
|
|||
eval_W: Some(eval_W),
|
|||
eval_E: Some(eval_E),
|
|||
})
|
|||
}
|
|||
}
|
|||
|
|||
impl<C1, C2, GC2> ConstraintSynthesizer<CF1<C1>> for DeciderCircuit1<C1, C2, GC2>
|
|||
where
|
|||
C1: CurveGroup,
|
|||
<C1 as CurveGroup>::BaseField: PrimeField,
|
|||
<C1 as Group>::ScalarField: Absorb,
|
|||
C2: CurveGroup,
|
|||
<C2 as CurveGroup>::BaseField: PrimeField,
|
|||
<C2 as Group>::ScalarField: Absorb,
|
|||
C1: CurveGroup<BaseField = C2::ScalarField, ScalarField = C2::BaseField>,
|
|||
GC2: CurveVar<C2, CF2<C2>> + ToConstraintFieldGadget<CF2<C2>>,
|
|||
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 pp_hash = FpVar::<CF1<C1>>::new_input(cs.clone(), || {
|
|||
Ok(self.pp_hash.unwrap_or_else(CF1::<C1>::zero))
|
|||
})?;
|
|||
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(&self.r1cs);
|
|||
let w_dummy_native = Witness::<C1>::dummy(&self.r1cs);
|
|||
|
|||
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()))
|
|||
})?;
|
|||
// here (U_i1, W_i1) = NIFS.P( (U_i,W_i), (u_i,w_i))
|
|||
let U_i1 = CommittedInstanceVar::<C1>::new_input(cs.clone(), || {
|
|||
Ok(self.U_i1.unwrap_or(u_dummy_native.clone()))
|
|||
})?;
|
|||
let W_i1 = WitnessVar::<C1>::new_witness(cs.clone(), || {
|
|||
Ok(self.W_i1.unwrap_or(w_dummy_native.clone()))
|
|||
})?;
|
|||
|
|||
// allocate the inputs for the check 6
|
|||
let cs_c_W = FpVar::<CF1<C1>>::new_input(cs.clone(), || {
|
|||
Ok(self.cs_c_W.unwrap_or_else(CF1::<C1>::zero))
|
|||
})?;
|
|||
let cs_c_E = FpVar::<CF1<C1>>::new_input(cs.clone(), || {
|
|||
Ok(self.cs_c_E.unwrap_or_else(CF1::<C1>::zero))
|
|||
})?;
|
|||
let eval_W = FpVar::<CF1<C1>>::new_input(cs.clone(), || {
|
|||
Ok(self.eval_W.unwrap_or_else(CF1::<C1>::zero))
|
|||
})?;
|
|||
let eval_E = FpVar::<CF1<C1>>::new_input(cs.clone(), || {
|
|||
Ok(self.eval_E.unwrap_or_else(CF1::<C1>::zero))
|
|||
})?;
|
|||
|
|||
// `sponge` is for digest computation.
|
|||
let sponge = PoseidonSpongeVar::<C1::ScalarField>::new(cs.clone(), &self.poseidon_config);
|
|||
// `transcript` is for challenge generation.
|
|||
let mut transcript = sponge.clone();
|
|||
// notice that the `pp_hash` is absorbed inside the ChallengeGadget::get_challenge_gadget call
|
|||
|
|||
// 2. u_i.cmE==cm(0), u_i.u==1
|
|||
// Here zero is the x & y coordinates of the zero point affine representation.
|
|||
let zero = NonNativeUintVar::new_constant(cs.clone(), C1::BaseField::zero())?;
|
|||
u_i.cmE.x.enforce_equal_unaligned(&zero)?;
|
|||
u_i.cmE.y.enforce_equal_unaligned(&zero)?;
|
|||
(u_i.u.is_one()?).enforce_equal(&Boolean::TRUE)?;
|
|||
|
|||
// 3.a u_i.x[0] == H(i, z_0, z_i, U_i)
|
|||
let (u_i_x, U_i_vec) = U_i.clone().hash(&sponge, &pp_hash, &i, &z_0, &z_i)?;
|
|||
(u_i.x[0]).enforce_equal(&u_i_x)?;
|
|||
|
|||
// 3.b u_i.x[1] == H(cf_U_i)
|
|||
let cf_u_dummy_native =
|
|||
CycleFoldCommittedInstance::<C2>::dummy(NovaCycleFoldConfig::<C1>::IO_LEN);
|
|||
let cf_U_i = CycleFoldCommittedInstanceVar::<C2, GC2>::new_input(cs.clone(), || {
|
|||
Ok(self.cf_U_i.unwrap_or_else(|| cf_u_dummy_native.clone()))
|
|||
})?;
|
|||
let (cf_u_i_x, _) = cf_U_i.clone().hash(&sponge, pp_hash.clone())?;
|
|||
(u_i.x[1]).enforce_equal(&cf_u_i_x)?;
|
|||
|
|||
// 4. check RelaxedR1CS of U_{i+1}
|
|||
let z_U1: Vec<FpVar<CF1<C1>>> =
|
|||
[vec![U_i1.u.clone()], U_i1.x.to_vec(), W_i1.W.to_vec()].concat();
|
|||
RelaxedR1CSGadget::check_native(r1cs, W_i1.E.clone(), U_i1.u.clone(), z_U1)?;
|
|||
|
|||
// 1.1.a, 5.1 compute NIFS.V and Commitment Scheme challenges.
|
|||
// We need to ensure the order of challenge generation is the same as
|
|||
// the native counterpart, so we first compute the challenges here and
|
|||
// do the actual checks later.
|
|||
let cmT =
|
|||
NonNativeAffineVar::new_input(cs.clone(), || Ok(self.cmT.unwrap_or_else(C1::zero)))?;
|
|||
let r_bits = ChallengeGadget::<C1, CommittedInstance<C1>>::get_challenge_gadget(
|
|||
&mut transcript,
|
|||
pp_hash,
|
|||
U_i_vec,
|
|||
u_i.clone(),
|
|||
Some(cmT.clone()),
|
|||
)?;
|
|||
// 5.1.
|
|||
let (incircuit_c_W, incircuit_c_E) =
|
|||
KZGChallengesGadget::<C1>::get_challenges_gadget(&mut transcript, U_i1.clone())?;
|
|||
incircuit_c_W.enforce_equal(&cs_c_W)?;
|
|||
incircuit_c_E.enforce_equal(&cs_c_E)?;
|
|||
|
|||
// 5.2. check eval_W==p_W(c_W) and eval_E==p_E(c_E)
|
|||
let incircuit_eval_W = evaluate_gadget::<CF1<C1>>(W_i1.W, incircuit_c_W)?;
|
|||
let incircuit_eval_E = evaluate_gadget::<CF1<C1>>(W_i1.E, incircuit_c_E)?;
|
|||
incircuit_eval_W.enforce_equal(&eval_W)?;
|
|||
incircuit_eval_E.enforce_equal(&eval_E)?;
|
|||
|
|||
// 1.1.b check that the NIFS.V challenge matches the one from the public input (so we avoid
|
|||
// the verifier computing it)
|
|||
let r_Fr = Boolean::le_bits_to_fp_var(&r_bits)?;
|
|||
// check that the in-circuit computed r is equal to the inputted r
|
|||
let r =
|
|||
FpVar::<CF1<C1>>::new_input(cs.clone(), || Ok(self.r.unwrap_or_else(CF1::<C1>::zero)))?;
|
|||
r_Fr.enforce_equal(&r)?;
|
|||
|
|||
Ok(())
|
|||
}
|
|||
}
|
|||
|
|||
/// Circuit that implements part of the in-circuit checks needed for the offchain verification over
|
|||
/// the Curve1's BaseField (=Curve2's ScalarField).
|
|||
#[derive(Clone, Debug)]
|
|||
pub struct DeciderCircuit2<C1, GC1, C2>
|
|||
where
|
|||
C1: CurveGroup,
|
|||
C2: CurveGroup,
|
|||
{
|
|||
_c1: PhantomData<C1>,
|
|||
_gc1: PhantomData<GC1>,
|
|||
_c2: PhantomData<C2>,
|
|||
|
|||
/// E vector's length of the CycleFold instance witness
|
|||
pub cf_E_len: usize,
|
|||
/// R1CS of the CycleFold circuit
|
|||
pub cf_r1cs: R1CS<C2::ScalarField>,
|
|||
pub poseidon_config: PoseidonConfig<CF1<C2>>,
|
|||
/// public params hash
|
|||
pub pp_hash: Option<C2::ScalarField>,
|
|||
|
|||
/// CycleFold running instance. Notice that here we use Nova's CommittedInstance (instead of
|
|||
/// CycleFoldCommittedInstance), since we are over C2::Fr, so that the CycleFold instances can
|
|||
/// be computed natively
|
|||
pub cf_U_i: Option<CommittedInstance<C2>>,
|
|||
pub cf_W_i: Option<CycleFoldWitness<C2>>,
|
|||
/// Commitment Scheme challenges
|
|||
pub cs_c_W: Option<C2::ScalarField>,
|
|||
pub cs_c_E: Option<C2::ScalarField>,
|
|||
/// Evaluations of the committed polynomials at the challenge
|
|||
pub eval_W: Option<C2::ScalarField>,
|
|||
pub eval_E: Option<C2::ScalarField>,
|
|||
}
|
|||
impl<C1, GC1, C2> DeciderCircuit2<C1, GC1, C2>
|
|||
where
|
|||
C1: CurveGroup,
|
|||
C2: CurveGroup,
|
|||
<C1 as CurveGroup>::BaseField: PrimeField,
|
|||
<C1 as Group>::ScalarField: Absorb,
|
|||
<C2 as CurveGroup>::BaseField: PrimeField,
|
|||
<C2 as Group>::ScalarField: Absorb,
|
|||
GC1: CurveVar<C1, CF2<C1>> + ToConstraintFieldGadget<CF2<C1>>,
|
|||
{
|
|||
pub fn from_nova<GC2, CS1, CS2, const H: bool, FC: FCircuit<C1::ScalarField>>(
|
|||
nova: Nova<C1, GC1, C2, GC2, FC, CS1, CS2, H>,
|
|||
) -> Result<Self, Error>
|
|||
where
|
|||
GC2: CurveVar<C2, CF2<C2>> + ToConstraintFieldGadget<CF2<C2>>,
|
|||
CS1: CommitmentScheme<C1, H>,
|
|||
CS2: CommitmentScheme<C2, H>,
|
|||
{
|
|||
// compute the Commitment Scheme challenges of the CycleFold instance commitments, used as
|
|||
// inputs in the circuit
|
|||
let poseidon_config =
|
|||
crate::transcript::poseidon::poseidon_canonical_config::<C2::ScalarField>();
|
|||
let mut transcript = PoseidonSponge::<C2::ScalarField>::new(&poseidon_config);
|
|||
let pp_hash_Fq =
|
|||
C2::ScalarField::from_le_bytes_mod_order(&nova.pp_hash.into_bigint().to_bytes_le());
|
|||
transcript.absorb(&pp_hash_Fq);
|
|||
|
|||
let (cs_challenge_W, cs_challenge_E) =
|
|||
KZGChallengesGadget::<C2>::get_challenges_native(&mut transcript, nova.cf_U_i.clone());
|
|||
|
|||
// get evals of the committed polynomials at the challenge
|
|||
let mut W = nova.cf_W_i.W.clone();
|
|||
W.extend(
|
|||
std::iter::repeat(C2::ScalarField::zero())
|
|||
.take(nova.cf_W_i.W.len().next_power_of_two() - nova.cf_W_i.W.len()),
|
|||
);
|
|||
let mut E = nova.cf_W_i.E.clone();
|
|||
E.extend(
|
|||
std::iter::repeat(C2::ScalarField::zero())
|
|||
.take(nova.cf_W_i.E.len().next_power_of_two() - nova.cf_W_i.E.len()),
|
|||
);
|
|||
let p_W = poly_from_vec(W.to_vec())?;
|
|||
let eval_W = p_W.evaluate(&cs_challenge_W);
|
|||
let p_E = poly_from_vec(E.to_vec())?;
|
|||
let eval_E = p_E.evaluate(&cs_challenge_E);
|
|||
|
|||
Ok(Self {
|
|||
_c1: PhantomData,
|
|||
_gc1: PhantomData,
|
|||
_c2: PhantomData,
|
|||
|
|||
cf_E_len: nova.cf_W_i.E.len(),
|
|||
cf_r1cs: nova.cf_r1cs,
|
|||
poseidon_config,
|
|||
pp_hash: Some(pp_hash_Fq),
|
|||
|
|||
cf_U_i: Some(nova.cf_U_i),
|
|||
cf_W_i: Some(nova.cf_W_i),
|
|||
|
|||
// CycleFold instance commitments challenges
|
|||
cs_c_W: Some(cs_challenge_W),
|
|||
cs_c_E: Some(cs_challenge_E),
|
|||
eval_W: Some(eval_W),
|
|||
eval_E: Some(eval_E),
|
|||
})
|
|||
}
|
|||
}
|
|||
|
|||
impl<C1, GC1, C2> ConstraintSynthesizer<CF1<C2>> for DeciderCircuit2<C1, GC1, C2>
|
|||
where
|
|||
C1: CurveGroup,
|
|||
C2: CurveGroup,
|
|||
<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>,
|
|||
GC1: CurveVar<C1, CF2<C1>> + ToConstraintFieldGadget<CF2<C1>>,
|
|||
for<'a> &'a GC1: GroupOpsBounds<'a, C1, GC1>,
|
|||
{
|
|||
fn generate_constraints(self, cs: ConstraintSystemRef<CF1<C2>>) -> Result<(), SynthesisError> {
|
|||
let pp_hash = FpVar::<CF1<C2>>::new_input(cs.clone(), || {
|
|||
Ok(self.pp_hash.unwrap_or_else(CF1::<C2>::zero))
|
|||
})?;
|
|||
|
|||
let cf_u_dummy_native = CommittedInstance::<C2>::dummy(&self.cf_r1cs);
|
|||
let w_dummy_native = Witness::<C2>::dummy(&self.cf_r1cs);
|
|||
let cf_U_i = CommittedInstanceVar::<C2>::new_input(cs.clone(), || {
|
|||
Ok(self.cf_U_i.unwrap_or_else(|| cf_u_dummy_native.clone()))
|
|||
})?;
|
|||
let cf_W_i = WitnessVar::<C2>::new_witness(cs.clone(), || {
|
|||
Ok(self.cf_W_i.unwrap_or(w_dummy_native.clone()))
|
|||
})?;
|
|||
|
|||
let cf_r1cs =
|
|||
R1CSVar::<C2::ScalarField, CF1<C2>, FpVar<CF1<C2>>>::new_witness(cs.clone(), || {
|
|||
Ok(self.cf_r1cs.clone())
|
|||
})?;
|
|||
|
|||
// 6. check RelaxedR1CS of cf_U_i
|
|||
let cf_z_U = [vec![cf_U_i.u.clone()], cf_U_i.x.to_vec(), cf_W_i.W.to_vec()].concat();
|
|||
RelaxedR1CSGadget::check_native(cf_r1cs, cf_W_i.E.clone(), cf_U_i.u.clone(), cf_z_U)?;
|
|||
|
|||
// `transcript` is for challenge generation.
|
|||
let mut transcript =
|
|||
PoseidonSpongeVar::<C2::ScalarField>::new(cs.clone(), &self.poseidon_config);
|
|||
transcript.absorb(&pp_hash)?;
|
|||
|
|||
// allocate the inputs for the check 7.1
|
|||
let cs_c_W = FpVar::<CF1<C2>>::new_input(cs.clone(), || {
|
|||
Ok(self.cs_c_W.unwrap_or_else(CF1::<C2>::zero))
|
|||
})?;
|
|||
let cs_c_E = FpVar::<CF1<C2>>::new_input(cs.clone(), || {
|
|||
Ok(self.cs_c_E.unwrap_or_else(CF1::<C2>::zero))
|
|||
})?;
|
|||
// allocate the inputs for the check 7.2
|
|||
let eval_W = FpVar::<CF1<C2>>::new_input(cs.clone(), || {
|
|||
Ok(self.eval_W.unwrap_or_else(CF1::<C2>::zero))
|
|||
})?;
|
|||
let eval_E = FpVar::<CF1<C2>>::new_input(cs.clone(), || {
|
|||
Ok(self.eval_E.unwrap_or_else(CF1::<C2>::zero))
|
|||
})?;
|
|||
|
|||
// 7.1. check the commitment scheme challenges correct computation
|
|||
let (incircuit_c_W, incircuit_c_E) =
|
|||
KZGChallengesGadget::<C2>::get_challenges_gadget(&mut transcript, cf_U_i.clone())?;
|
|||
incircuit_c_W.enforce_equal(&cs_c_W)?;
|
|||
incircuit_c_E.enforce_equal(&cs_c_E)?;
|
|||
|
|||
// 7.2. check eval_W==p_W(c_W) and eval_E==p_E(c_E)
|
|||
let incircuit_eval_W = evaluate_gadget::<CF1<C2>>(cf_W_i.W, incircuit_c_W)?;
|
|||
let incircuit_eval_E = evaluate_gadget::<CF1<C2>>(cf_W_i.E, incircuit_c_E)?;
|
|||
incircuit_eval_W.enforce_equal(&eval_W)?;
|
|||
incircuit_eval_E.enforce_equal(&eval_E)?;
|
|||
|
|||
Ok(())
|
|||
}
|
|||
}
|
|||
|
|||
#[cfg(test)]
|
|||
pub mod tests {
|
|||
use ark_pallas::{constraints::GVar, Fq, Fr, Projective};
|
|||
use ark_relations::r1cs::ConstraintSystem;
|
|||
use ark_vesta::{constraints::GVar as GVar2, Projective as Projective2};
|
|||
|
|||
use super::*;
|
|||
use crate::commitment::pedersen::Pedersen;
|
|||
use crate::folding::nova::PreprocessorParam;
|
|||
use crate::frontend::utils::CubicFCircuit;
|
|||
use crate::transcript::poseidon::poseidon_canonical_config;
|
|||
use crate::FoldingScheme;
|
|||
|
|||
#[test]
|
|||
fn test_decider_circuits() {
|
|||
let mut rng = ark_std::test_rng();
|
|||
let poseidon_config = poseidon_canonical_config::<Fr>();
|
|||
|
|||
let F_circuit = CubicFCircuit::<Fr>::new(()).unwrap();
|
|||
let z_0 = vec![Fr::from(3_u32)];
|
|||
|
|||
type N = Nova<
|
|||
Projective,
|
|||
GVar,
|
|||
Projective2,
|
|||
GVar2,
|
|||
CubicFCircuit<Fr>,
|
|||
Pedersen<Projective>,
|
|||
Pedersen<Projective2>,
|
|||
false,
|
|||
>;
|
|||
|
|||
let prep_param = PreprocessorParam::<
|
|||
Projective,
|
|||
Projective2,
|
|||
CubicFCircuit<Fr>,
|
|||
Pedersen<Projective>,
|
|||
Pedersen<Projective2>,
|
|||
false,
|
|||
>::new(poseidon_config, F_circuit);
|
|||
let nova_params = N::preprocess(&mut rng, &prep_param).unwrap();
|
|||
|
|||
// generate a Nova instance and do a step of it
|
|||
let mut nova = N::init(&nova_params, F_circuit, z_0.clone()).unwrap();
|
|||
nova.prove_step(&mut rng, vec![], None).unwrap();
|
|||
// verify the IVC
|
|||
let ivc_proof = nova.ivc_proof();
|
|||
N::verify(nova_params.1, ivc_proof).unwrap();
|
|||
|
|||
// load the DeciderCircuit 1 & 2 from the Nova instance
|
|||
let decider_circuit1 =
|
|||
DeciderCircuit1::<Projective, Projective2, GVar2>::from_nova(nova.clone()).unwrap();
|
|||
let decider_circuit2 =
|
|||
DeciderCircuit2::<Projective, GVar, Projective2>::from_nova(nova).unwrap();
|
|||
|
|||
// generate the constraints of both circuits and check that are satisfied by the inputs
|
|||
let cs1 = ConstraintSystem::<Fr>::new_ref();
|
|||
decider_circuit1.generate_constraints(cs1.clone()).unwrap();
|
|||
assert!(cs1.is_satisfied().unwrap());
|
|||
let cs2 = ConstraintSystem::<Fq>::new_ref();
|
|||
decider_circuit2.generate_constraints(cs2.clone()).unwrap();
|
|||
assert!(cs2.is_satisfied().unwrap());
|
|||
}
|
|||
}
|
@ -0,0 +1,269 @@ |
|||
/// This module contains the implementation the NIFSTrait for the
|
|||
/// [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw) NIFS (Non-Interactive Folding Scheme) as
|
|||
/// outlined in the protocol description doc:
|
|||
/// <https://hackmd.io/V4838nnlRKal9ZiTHiGYzw#Construction> authored by Benedikt Bünz.
|
|||
use ark_crypto_primitives::sponge::Absorb;
|
|||
use ark_ec::{CurveGroup, Group};
|
|||
use ark_ff::PrimeField;
|
|||
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
|
|||
use ark_std::fmt::Debug;
|
|||
use ark_std::rand::RngCore;
|
|||
use ark_std::{One, UniformRand, Zero};
|
|||
use std::marker::PhantomData;
|
|||
|
|||
use super::{circuits::ChallengeGadget, traits::NIFSTrait};
|
|||
use crate::arith::r1cs::R1CS;
|
|||
use crate::commitment::CommitmentScheme;
|
|||
use crate::folding::{circuits::CF1, traits::Dummy};
|
|||
use crate::transcript::{AbsorbNonNative, Transcript};
|
|||
use crate::utils::vec::{hadamard, mat_vec_mul, vec_scalar_mul, vec_sub};
|
|||
use crate::Error;
|
|||
|
|||
/// A CommittedInstance in [Ova](https://hackmd.io/V4838nnlRKal9ZiTHiGYzw) is represented by `W` or
|
|||
/// `W'`. It is the result of the commitment to a vector that contains the witness `w` concatenated
|
|||
/// with `t` or `e` + the public inputs `x` and a relaxation factor `u`. (Notice that in the Ova
|
|||
/// document `u` is denoted as `mu`, in this implementation we use `u` so it follows the original
|
|||
/// Nova notation, so code is easier to follow).
|
|||
#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
|
|||
pub struct CommittedInstance<C: CurveGroup> {
|
|||
pub u: C::ScalarField, // in the Ova document is denoted as `mu`
|
|||
pub x: Vec<C::ScalarField>,
|
|||
pub cmWE: C,
|
|||
}
|
|||
|
|||
impl<C: CurveGroup> Absorb for CommittedInstance<C>
|
|||
where
|
|||
C::ScalarField: Absorb,
|
|||
{
|
|||
fn to_sponge_bytes(&self, dest: &mut Vec<u8>) {
|
|||
C::ScalarField::batch_to_sponge_bytes(&self.to_sponge_field_elements_as_vec(), dest);
|
|||
}
|
|||
|
|||
fn to_sponge_field_elements<F: PrimeField>(&self, dest: &mut Vec<F>) {
|
|||
self.u.to_sponge_field_elements(dest);
|
|||
self.x.to_sponge_field_elements(dest);
|
|||
// We cannot call `to_native_sponge_field_elements(dest)` directly, as
|
|||
// `to_native_sponge_field_elements` needs `F` to be `C::ScalarField`,
|
|||
// but here `F` is a generic `PrimeField`.
|
|||
self.cmWE
|
|||
.to_native_sponge_field_elements_as_vec()
|
|||
.to_sponge_field_elements(dest);
|
|||
}
|
|||
}
|
|||
|
|||
// #[allow(dead_code)] // Clippy flag needed for now.
|
|||
/// A Witness in Ova is represented by `w`. It also contains a blinder which can or not be used
|
|||
/// when committing to the witness itself.
|
|||
#[derive(Debug, Clone, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
|
|||
pub struct Witness<C: CurveGroup> {
|
|||
pub w: Vec<C::ScalarField>,
|
|||
pub rW: C::ScalarField,
|
|||
}
|
|||
|
|||
impl<C: CurveGroup> Witness<C> {
|
|||
/// Generates a new `Witness` instance from a given witness vector.
|
|||
/// If `H = true`, then we assume we want to blind it at commitment time,
|
|||
/// hence sampling `rW` from the randomness passed.
|
|||
pub fn new<const H: bool>(w: Vec<C::ScalarField>, mut rng: impl RngCore) -> Self {
|
|||
Self {
|
|||
w,
|
|||
rW: if H {
|
|||
C::ScalarField::rand(&mut rng)
|
|||
} else {
|
|||
C::ScalarField::zero()
|
|||
},
|
|||
}
|
|||
}
|
|||
|
|||
/// Given `x` (public inputs) and `t` or `e` (which we always concatenate in Ova) and the
|
|||
/// public inputs `x`, generates a [`CommittedInstance`] as a result which will or not be
|
|||
/// blinded depending on how the const generic `HC` is set up.
|
|||
pub fn commit<CS: CommitmentScheme<C, HC>, const HC: bool>(
|
|||
&self,
|
|||
params: &CS::ProverParams,
|
|||
x: Vec<C::ScalarField>,
|
|||
t_or_e: Vec<C::ScalarField>,
|
|||
) -> Result<CommittedInstance<C>, Error> {
|
|||
let cmWE = CS::commit(params, &[self.w.clone(), t_or_e].concat(), &self.rW)?;
|
|||
Ok(CommittedInstance {
|
|||
u: C::ScalarField::one(),
|
|||
cmWE,
|
|||
x,
|
|||
})
|
|||
}
|
|||
}
|
|||
|
|||
impl<C: CurveGroup> Dummy<&R1CS<CF1<C>>> for Witness<C> {
|
|||
fn dummy(r1cs: &R1CS<CF1<C>>) -> Self {
|
|||
Self {
|
|||
w: vec![C::ScalarField::zero(); r1cs.A.n_cols - 1 - r1cs.l],
|
|||
rW: C::ScalarField::zero(),
|
|||
}
|
|||
}
|
|||
}
|
|||
|
|||
/// Implements the NIFS (Non-Interactive Folding Scheme) trait for Ova.
|
|||
pub struct NIFS<C: CurveGroup, CS: CommitmentScheme<C, H>, const H: bool = false> {
|
|||
_c: PhantomData<C>,
|
|||
_cp: PhantomData<CS>,
|
|||
}
|
|||
|
|||
impl<C: CurveGroup, CS: CommitmentScheme<C, H>, const H: bool> NIFSTrait<C, CS, H>
|
|||
for NIFS<C, CS, H>
|
|||
where
|
|||
<C as Group>::ScalarField: Absorb,
|
|||
<C as CurveGroup>::BaseField: PrimeField,
|
|||
{
|
|||
type CommittedInstance = CommittedInstance<C>;
|
|||
type Witness = Witness<C>;
|
|||
type ProverAux = ();
|
|||
type VerifierAux = ();
|
|||
|
|||
fn new_witness(w: Vec<C::ScalarField>, _e_len: usize, rng: impl RngCore) -> Self::Witness {
|
|||
Witness::new::<H>(w, rng)
|
|||
}
|
|||
|
|||
fn new_instance(
|
|||
W: &Self::Witness,
|
|||
params: &CS::ProverParams,
|
|||
x: Vec<C::ScalarField>,
|
|||
aux: Vec<C::ScalarField>, // t_or_e
|
|||
) -> Result<Self::CommittedInstance, Error> {
|
|||
W.commit::<CS, H>(params, x, aux)
|
|||
}
|
|||
|
|||
fn fold_witness(
|
|||
r: C::ScalarField, // in Ova's hackmd denoted as `alpha`
|
|||
W_i: &Self::Witness,
|
|||
w_i: &Self::Witness,
|
|||
_aux: &Self::ProverAux,
|
|||
) -> Result<Self::Witness, Error> {
|
|||
let w: Vec<C::ScalarField> = W_i
|
|||
.w
|
|||
.iter()
|
|||
.zip(&w_i.w)
|
|||
.map(|(a, b)| *a + (r * b))
|
|||
.collect();
|
|||
|
|||
let rW = W_i.rW + r * w_i.rW;
|
|||
Ok(Self::Witness { w, rW })
|
|||
}
|
|||
|
|||
fn compute_aux(
|
|||
_cs_prover_params: &CS::ProverParams,
|
|||
_r1cs: &R1CS<C::ScalarField>,
|
|||
_W_i: &Self::Witness,
|
|||
_U_i: &Self::CommittedInstance,
|
|||
_w_i: &Self::Witness,
|
|||
_u_i: &Self::CommittedInstance,
|
|||
) -> Result<(Self::ProverAux, Self::VerifierAux), Error> {
|
|||
Ok(((), ()))
|
|||
}
|
|||
|
|||
fn get_challenge<T: Transcript<C::ScalarField>>(
|
|||
transcript: &mut T,
|
|||
pp_hash: C::ScalarField, // public params hash
|
|||
U_i: &Self::CommittedInstance,
|
|||
u_i: &Self::CommittedInstance,
|
|||
_aux: &Self::VerifierAux,
|
|||
) -> Vec<bool> {
|
|||
// reuse Nova's get_challenge method
|
|||
ChallengeGadget::<C, Self::CommittedInstance>::get_challenge_native(
|
|||
transcript, pp_hash, U_i, u_i, None, // empty in Ova's case
|
|||
)
|
|||
}
|
|||
|
|||
// Notice: `prove` method is implemented at the trait level.
|
|||
|
|||
fn verify(
|
|||
// r comes from the transcript, and is a n-bit (N_BITS_CHALLENGE) element
|
|||
r: C::ScalarField,
|
|||
U_i: &Self::CommittedInstance,
|
|||
u_i: &Self::CommittedInstance,
|
|||
_aux: &Self::VerifierAux,
|
|||
) -> Self::CommittedInstance {
|
|||
// recall that r <==> alpha, and u <==> mu between Nova and Ova respectively
|
|||
let u = U_i.u + r; // u_i.u is always 1 IN ova as we just can do sequential IVC.
|
|||
let cmWE = U_i.cmWE + u_i.cmWE.mul(r);
|
|||
let x = U_i
|
|||
.x
|
|||
.iter()
|
|||
.zip(&u_i.x)
|
|||
.map(|(a, b)| *a + (r * b))
|
|||
.collect::<Vec<C::ScalarField>>();
|
|||
|
|||
Self::CommittedInstance { cmWE, u, x }
|
|||
}
|
|||
}
|
|||
|
|||
/// Computes the E parameter (error terms) for the given R1CS and the instance's z and u. This
|
|||
/// method is used by the verifier to obtain E in order to check the RelaxedR1CS relation.
|
|||
pub fn compute_E<C: CurveGroup>(
|
|||
r1cs: &R1CS<C::ScalarField>,
|
|||
z: &[C::ScalarField],
|
|||
u: C::ScalarField,
|
|||
) -> Result<Vec<C::ScalarField>, Error> {
|
|||
let (A, B, C) = (r1cs.A.clone(), r1cs.B.clone(), r1cs.C.clone());
|
|||
|
|||
// this is parallelizable (for the future)
|
|||
let Az = mat_vec_mul(&A, z)?;
|
|||
let Bz = mat_vec_mul(&B, z)?;
|
|||
let Cz = mat_vec_mul(&C, z)?;
|
|||
|
|||
let Az_Bz = hadamard(&Az, &Bz)?;
|
|||
let uCz = vec_scalar_mul(&Cz, &u);
|
|||
|
|||
vec_sub(&Az_Bz, &uCz)
|
|||
}
|
|||
|
|||
#[cfg(test)]
|
|||
pub mod tests {
|
|||
use super::*;
|
|||
use ark_pallas::{Fr, Projective};
|
|||
|
|||
use crate::arith::{r1cs::tests::get_test_r1cs, Arith};
|
|||
use crate::commitment::pedersen::Pedersen;
|
|||
use crate::folding::nova::nifs::tests::test_nifs_opt;
|
|||
|
|||
// Simple auxiliary structure mainly used to help pass a witness for which we can check
|
|||
// easily an R1CS relation.
|
|||
// Notice that checking it requires us to have `E` as per [`Arith`] trait definition.
|
|||
// But since we don't hold `E` nor `e` within the NIFS, we create this structure to pass
|
|||
// `e` such that the check can be done.
|
|||
#[derive(Debug, Clone)]
|
|||
pub(crate) struct TestingWitness<C: CurveGroup> {
|
|||
pub(crate) w: Vec<C::ScalarField>,
|
|||
pub(crate) e: Vec<C::ScalarField>,
|
|||
}
|
|||
impl<C: CurveGroup> Arith<TestingWitness<C>, CommittedInstance<C>> for R1CS<CF1<C>> {
|
|||
type Evaluation = Vec<CF1<C>>;
|
|||
|
|||
fn eval_relation(
|
|||
&self,
|
|||
w: &TestingWitness<C>,
|
|||
u: &CommittedInstance<C>,
|
|||
) -> Result<Self::Evaluation, Error> {
|
|||
self.eval_at_z(&[&[u.u], u.x.as_slice(), &w.w].concat())
|
|||
}
|
|||
|
|||
fn check_evaluation(
|
|||
w: &TestingWitness<C>,
|
|||
_u: &CommittedInstance<C>,
|
|||
e: Self::Evaluation,
|
|||
) -> Result<(), Error> {
|
|||
(w.e == e).then_some(()).ok_or(Error::NotSatisfied)
|
|||
}
|
|||
}
|
|||
|
|||
#[test]
|
|||
fn test_nifs_ova() {
|
|||
let (W, U) = test_nifs_opt::<NIFS<Projective, Pedersen<Projective>>>();
|
|||
|
|||
// check the last folded instance relation
|
|||
let r1cs = get_test_r1cs();
|
|||
let z: Vec<Fr> = [&[U.u][..], &U.x, &W.w].concat();
|
|||
let e = compute_E::<Projective>(&r1cs, &z, U.u).unwrap();
|
|||
r1cs.check_relation(&TestingWitness::<Projective> { e, w: W.w.clone() }, &U)
|
|||
.unwrap();
|
|||
}
|
|||
}
|
@ -1,268 +0,0 @@ |
|||
use ark_crypto_primitives::sponge::{poseidon::PoseidonConfig, Absorb};
|
|||
use ark_ec::{CurveGroup, Group};
|
|||
use ark_ff::PrimeField;
|
|||
use ark_r1cs_std::{
|
|||
groups::{CurveVar, GroupOpsBounds},
|
|||
ToConstraintFieldGadget,
|
|||
};
|
|||
use ark_relations::r1cs::ConstraintSynthesizer;
|
|||
use ark_relations::r1cs::ConstraintSystem;
|
|||
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, SerializationError, Write};
|
|||
use std::marker::PhantomData;
|
|||
|
|||
use super::{
|
|||
circuits::AugmentedFCircuit, CommittedInstance, Nova, NovaCycleFoldCircuit, ProverParams,
|
|||
Witness,
|
|||
};
|
|||
use crate::{
|
|||
arith::r1cs::extract_r1cs,
|
|||
commitment::CommitmentScheme,
|
|||
folding::circuits::{CF1, CF2},
|
|||
frontend::FCircuit,
|
|||
};
|
|||
|
|||
impl<C1, GC1, C2, GC2, FC, CS1, CS2, const H: bool> CanonicalSerialize
|
|||
for Nova<C1, GC1, C2, GC2, FC, CS1, CS2, H>
|
|||
where
|
|||
C1: CurveGroup,
|
|||
C2: CurveGroup,
|
|||
FC: FCircuit<C1::ScalarField>,
|
|||
CS1: CommitmentScheme<C1, H>,
|
|||
CS2: CommitmentScheme<C2, H>,
|
|||
<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>,
|
|||
GC1: CurveVar<C1, <C2 as Group>::ScalarField>,
|
|||
GC1: ToConstraintFieldGadget<<C2 as Group>::ScalarField>,
|
|||
GC2: CurveVar<C2, <C2 as CurveGroup>::BaseField>,
|
|||
{
|
|||
fn serialize_with_mode<W: Write>(
|
|||
&self,
|
|||
mut writer: W,
|
|||
compress: ark_serialize::Compress,
|
|||
) -> Result<(), ark_serialize::SerializationError> {
|
|||
self.pp_hash.serialize_with_mode(&mut writer, compress)?;
|
|||
self.i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.z_0.serialize_with_mode(&mut writer, compress)?;
|
|||
self.z_i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.w_i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.u_i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.W_i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.U_i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.cf_W_i.serialize_with_mode(&mut writer, compress)?;
|
|||
self.cf_U_i.serialize_with_mode(&mut writer, compress)
|
|||
}
|
|||
|
|||
fn serialized_size(&self, compress: ark_serialize::Compress) -> usize {
|
|||
self.pp_hash.serialized_size(compress)
|
|||
+ self.i.serialized_size(compress)
|
|||
+ self.z_0.serialized_size(compress)
|
|||
+ self.z_i.serialized_size(compress)
|
|||
+ self.w_i.serialized_size(compress)
|
|||
+ self.u_i.serialized_size(compress)
|
|||
+ self.W_i.serialized_size(compress)
|
|||
+ self.U_i.serialized_size(compress)
|
|||
+ self.cf_W_i.serialized_size(compress)
|
|||
+ self.cf_U_i.serialized_size(compress)
|
|||
}
|
|||
|
|||
fn serialize_compressed<W: Write>(
|
|||
&self,
|
|||
writer: W,
|
|||
) -> Result<(), ark_serialize::SerializationError> {
|
|||
self.serialize_with_mode(writer, ark_serialize::Compress::Yes)
|
|||
}
|
|||
|
|||
fn compressed_size(&self) -> usize {
|
|||
self.serialized_size(ark_serialize::Compress::Yes)
|
|||
}
|
|||
|
|||
fn serialize_uncompressed<W: Write>(
|
|||
&self,
|
|||
writer: W,
|
|||
) -> Result<(), ark_serialize::SerializationError> {
|
|||
self.serialize_with_mode(writer, ark_serialize::Compress::No)
|
|||
}
|
|||
|
|||
fn uncompressed_size(&self) -> usize {
|
|||
self.serialized_size(ark_serialize::Compress::No)
|
|||
}
|
|||
}
|
|||
|
|||
// Note that we can't derive or implement `CanonicalDeserialize` directly.
|
|||
// This is because `CurveVar` notably does not implement the `Sync` trait.
|
|||
impl<C1, GC1, C2, GC2, FC, CS1, CS2, const H: bool> Nova<C1, GC1, C2, GC2, FC, CS1, CS2, H>
|
|||
where
|
|||
C1: CurveGroup,
|
|||
C2: CurveGroup,
|
|||
FC: FCircuit<CF1<C1>, Params = ()>,
|
|||
CS1: CommitmentScheme<C1, H>,
|
|||
CS2: CommitmentScheme<C2, H>,
|
|||
<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>,
|
|||
GC1: CurveVar<C1, <C2 as Group>::ScalarField>,
|
|||
GC1: ToConstraintFieldGadget<<C2 as Group>::ScalarField>,
|
|||
GC2: CurveVar<C2, CF2<C2>>,
|
|||
GC2: ToConstraintFieldGadget<<C2 as CurveGroup>::BaseField>,
|
|||
{
|
|||
pub fn deserialize_nova<R: std::io::prelude::Read>(
|
|||
mut reader: R,
|
|||
compress: ark_serialize::Compress,
|
|||
validate: ark_serialize::Validate,
|
|||
prover_params: ProverParams<C1, C2, CS1, CS2, H>,
|
|||
poseidon_config: PoseidonConfig<C1::ScalarField>,
|
|||
) -> Result<Self, ark_serialize::SerializationError> {
|
|||
let pp_hash = C1::ScalarField::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let i = C1::ScalarField::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let z_0 = Vec::<C1::ScalarField>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let z_i = Vec::<C1::ScalarField>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let w_i = Witness::<C1>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let u_i = CommittedInstance::<C1>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let W_i = Witness::<C1>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let U_i = CommittedInstance::<C1>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let cf_W_i = Witness::<C2>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
let cf_U_i =
|
|||
CommittedInstance::<C2>::deserialize_with_mode(&mut reader, compress, validate)?;
|
|||
|
|||
let f_circuit = FC::new(()).unwrap();
|
|||
let cs = ConstraintSystem::<C1::ScalarField>::new_ref();
|
|||
let cs2 = ConstraintSystem::<C1::BaseField>::new_ref();
|
|||
let augmented_F_circuit =
|
|||
AugmentedFCircuit::<C1, C2, GC2, FC>::empty(&poseidon_config, f_circuit.clone());
|
|||
let cf_circuit = NovaCycleFoldCircuit::<C1, GC1>::empty();
|
|||
|
|||
augmented_F_circuit
|
|||
.generate_constraints(cs.clone())
|
|||
.map_err(|_| SerializationError::InvalidData)?;
|
|||
cs.finalize();
|
|||
let cs = cs.into_inner().ok_or(SerializationError::InvalidData)?;
|
|||
let r1cs = extract_r1cs::<C1::ScalarField>(&cs);
|
|||
|
|||
cf_circuit
|
|||
.generate_constraints(cs2.clone())
|
|||
.map_err(|_| SerializationError::InvalidData)?;
|
|||
cs2.finalize();
|
|||
let cs2 = cs2.into_inner().ok_or(SerializationError::InvalidData)?;
|
|||
let cf_r1cs = extract_r1cs::<C1::BaseField>(&cs2);
|
|||
Ok(Nova {
|
|||
_gc1: PhantomData,
|
|||
_c2: PhantomData,
|
|||
_gc2: PhantomData,
|
|||
r1cs,
|
|||
cf_r1cs,
|
|||
poseidon_config,
|
|||
cs_pp: prover_params.cs_pp,
|
|||
cf_cs_pp: prover_params.cf_cs_pp,
|
|||
F: f_circuit,
|
|||
pp_hash,
|
|||
i,
|
|||
z_0,
|
|||
z_i,
|
|||
w_i,
|
|||
u_i,
|
|||
W_i,
|
|||
U_i,
|
|||
cf_W_i,
|
|||
cf_U_i,
|
|||
})
|
|||
}
|
|||
}
|
|||
|
|||
#[cfg(test)]
|
|||
pub mod tests {
|
|||
use ark_bn254::{constraints::GVar, Bn254, Fr, G1Projective as Projective};
|
|||
use ark_grumpkin::{constraints::GVar as GVar2, Projective as Projective2};
|
|||
use ark_serialize::{CanonicalSerialize, Compress, Validate};
|
|||
use std::{fs, io::Write};
|
|||
|
|||
use crate::{
|
|||
commitment::{kzg::KZG, pedersen::Pedersen},
|
|||
folding::nova::{Nova, PreprocessorParam},
|
|||
frontend::{utils::CubicFCircuit, FCircuit},
|
|||
transcript::poseidon::poseidon_canonical_config,
|
|||
FoldingScheme,
|
|||
};
|
|||
|
|||
#[test]
|
|||
fn test_serde_nova() {
|
|||
let mut rng = ark_std::test_rng();
|
|||
let poseidon_config = poseidon_canonical_config::<Fr>();
|
|||
let F_circuit = CubicFCircuit::<Fr>::new(()).unwrap();
|
|||
|
|||
// Initialize nova and make multiple `prove_step()`
|
|||
type N = Nova<
|
|||
Projective,
|
|||
GVar,
|
|||
Projective2,
|
|||
GVar2,
|
|||
CubicFCircuit<Fr>,
|
|||
KZG<'static, Bn254>,
|
|||
Pedersen<Projective2>,
|
|||
false,
|
|||
>;
|
|||
let prep_param = PreprocessorParam::new(poseidon_config.clone(), F_circuit);
|
|||
let nova_params = N::preprocess(&mut rng, &prep_param).unwrap();
|
|||
|
|||
let z_0 = vec![Fr::from(3_u32)];
|
|||
let mut nova = N::init(&nova_params, F_circuit, z_0.clone()).unwrap();
|
|||
|
|||
let num_steps: usize = 3;
|
|||
for _ in 0..num_steps {
|
|||
nova.prove_step(&mut rng, vec![], None).unwrap();
|
|||
}
|
|||
|
|||
let mut writer = vec![];
|
|||
assert!(nova
|
|||
.serialize_with_mode(&mut writer, ark_serialize::Compress::No)
|
|||
.is_ok());
|
|||
|
|||
let mut file = fs::OpenOptions::new()
|
|||
.create(true)
|
|||
.write(true)
|
|||
.open("./nova.serde")
|
|||
.unwrap();
|
|||
|
|||
file.write_all(&writer).unwrap();
|
|||
|
|||
let bytes = fs::read("./nova.serde").unwrap();
|
|||
|
|||
let mut deserialized_nova = Nova::<
|
|||
Projective,
|
|||
GVar,
|
|||
Projective2,
|
|||
GVar2,
|
|||
CubicFCircuit<Fr>,
|
|||
KZG<Bn254>,
|
|||
Pedersen<Projective2>,
|
|||
false,
|
|||
>::deserialize_nova(
|
|||
bytes.as_slice(),
|
|||
Compress::No,
|
|||
Validate::No,
|
|||
nova_params.0, // Nova's prover params
|
|||
poseidon_config,
|
|||
)
|
|||
.unwrap();
|
|||
|
|||
assert_eq!(nova.i, deserialized_nova.i);
|
|||
|
|||
let num_steps: usize = 3;
|
|||
for _ in 0..num_steps {
|
|||
deserialized_nova
|
|||
.prove_step(&mut rng, vec![], None)
|
|||
.unwrap();
|
|||
nova.prove_step(&mut rng, vec![], None).unwrap();
|
|||
}
|
|||
|
|||
assert_eq!(deserialized_nova.w_i, nova.w_i);
|
|||
}
|
|||
}
|
@ -0,0 +1,4 @@ |
|||
/// `RUNNING` indicates that the committed instance is a running instance.
|
|||
pub const RUNNING: bool = true;
|
|||
/// `INCOMING` indicates that the committed instance is an incoming instance.
|
|||
pub const INCOMING: bool = false;
|
@ -0,0 +1,131 @@ |
|||
use ark_crypto_primitives::sponge::{
|
|||
constraints::{AbsorbGadget, CryptographicSpongeVar},
|
|||
poseidon::constraints::PoseidonSpongeVar,
|
|||
Absorb,
|
|||
};
|
|||
use ark_ec::CurveGroup;
|
|||
use ark_ff::PrimeField;
|
|||
use ark_r1cs_std::{alloc::AllocVar, fields::fp::FpVar, ToConstraintFieldGadget};
|
|||
use ark_relations::r1cs::SynthesisError;
|
|||
|
|||
use crate::{transcript::Transcript, Error};
|
|||
|
|||
use super::circuits::CF1;
|
|||
|
|||
pub trait CommittedInstanceOps<C: CurveGroup> {
|
|||
/// The in-circuit representation of the committed instance.
|
|||
type Var: AllocVar<Self, CF1<C>> + CommittedInstanceVarOps<C>;
|
|||
/// `hash` implements the committed instance hash compatible with the
|
|||
/// in-circuit implementation from `CommittedInstanceVarOps::hash`.
|
|||
///
|
|||
/// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and
|
|||
/// `U_i` is the committed instance `self`.
|
|||
fn hash<T: Transcript<CF1<C>>>(
|
|||
&self,
|
|||
sponge: &T,
|
|||
pp_hash: CF1<C>, // public params hash
|
|||
i: CF1<C>,
|
|||
z_0: &[CF1<C>],
|
|||
z_i: &[CF1<C>],
|
|||
) -> CF1<C>
|
|||
where
|
|||
CF1<C>: Absorb,
|
|||
Self: Sized + Absorb,
|
|||
{
|
|||
let mut sponge = sponge.clone();
|
|||
sponge.absorb(&pp_hash);
|
|||
sponge.absorb(&i);
|
|||
sponge.absorb(&z_0);
|
|||
sponge.absorb(&z_i);
|
|||
sponge.absorb(&self);
|
|||
sponge.squeeze_field_elements(1)[0]
|
|||
}
|
|||
|
|||
/// Returns the commitments contained in the committed instance.
|
|||
fn get_commitments(&self) -> Vec<C>;
|
|||
|
|||
/// Returns `true` if the committed instance is an incoming instance, and
|
|||
/// `false` if it is a running instance.
|
|||
fn is_incoming(&self) -> bool;
|
|||
|
|||
/// Checks if the committed instance is an incoming instance.
|
|||
fn check_incoming(&self) -> Result<(), Error> {
|
|||
self.is_incoming()
|
|||
.then_some(())
|
|||
.ok_or(Error::NotIncomingCommittedInstance)
|
|||
}
|
|||
}
|
|||
|
|||
pub trait CommittedInstanceVarOps<C: CurveGroup> {
|
|||
type PointVar: ToConstraintFieldGadget<CF1<C>>;
|
|||
/// `hash` implements the in-circuit committed instance hash compatible with
|
|||
/// the native implementation from `CommittedInstanceOps::hash`.
|
|||
/// Returns `H(i, z_0, z_i, U_i)`, where `i` can be `i` but also `i+1`, and
|
|||
/// `U_i` is the committed instance `self`.
|
|||
///
|
|||
/// Additionally it returns the in-circuit representation of the committed
|
|||
/// instance `self` as a vector of field elements, so they can be reused in
|
|||
/// other gadgets avoiding recalculating (reconstraining) them.
|
|||
#[allow(clippy::type_complexity)]
|
|||
fn hash(
|
|||
&self,
|
|||
sponge: &PoseidonSpongeVar<CF1<C>>,
|
|||
pp_hash: &FpVar<CF1<C>>,
|
|||
i: &FpVar<CF1<C>>,
|
|||
z_0: &[FpVar<CF1<C>>],
|
|||
z_i: &[FpVar<CF1<C>>],
|
|||
) -> Result<(FpVar<CF1<C>>, Vec<FpVar<CF1<C>>>), SynthesisError>
|
|||
where
|
|||
Self: AbsorbGadget<CF1<C>>,
|
|||
{
|
|||
let mut sponge = sponge.clone();
|
|||
let U_vec = self.to_sponge_field_elements()?;
|
|||
sponge.absorb(&pp_hash)?;
|
|||
sponge.absorb(&i)?;
|
|||
sponge.absorb(&z_0)?;
|
|||
sponge.absorb(&z_i)?;
|
|||
sponge.absorb(&U_vec)?;
|
|||
Ok((sponge.squeeze_field_elements(1)?.pop().unwrap(), U_vec))
|
|||
}
|
|||
|
|||
/// Returns the commitments contained in the committed instance.
|
|||
fn get_commitments(&self) -> Vec<Self::PointVar>;
|
|||
|
|||
/// Returns the public inputs contained in the committed instance.
|
|||
fn get_public_inputs(&self) -> &[FpVar<CF1<C>>];
|
|||
|
|||
/// Generates constraints to enforce that the committed instance is an
|
|||
/// incoming instance.
|
|||
fn enforce_incoming(&self) -> Result<(), SynthesisError>;
|
|||
|
|||
/// Generates constraints to enforce that the committed instance `self` is
|
|||
/// partially equal to another committed instance `other`.
|
|||
/// Here, only field elements are compared, while commitments (points) are
|
|||
/// not.
|
|||
fn enforce_partial_equal(&self, other: &Self) -> Result<(), SynthesisError>;
|
|||
}
|
|||
|
|||
pub trait WitnessOps<F: PrimeField> {
|
|||
/// The in-circuit representation of the witness.
|
|||
type Var: AllocVar<Self, F> + WitnessVarOps<F>;
|
|||
|
|||
/// Returns the openings (i.e., the values being committed to and the
|
|||
/// randomness) contained in the witness.
|
|||
fn get_openings(&self) -> Vec<(&[F], F)>;
|
|||
}
|
|||
|
|||
pub trait WitnessVarOps<F: PrimeField> {
|
|||
/// Returns the openings (i.e., the values being committed to and the
|
|||
/// randomness) contained in the witness.
|
|||
fn get_openings(&self) -> Vec<(&[FpVar<F>], FpVar<F>)>;
|
|||
}
|
|||
|
|||
pub trait Dummy<Cfg> {
|
|||
fn dummy(cfg: Cfg) -> Self;
|
|||
}
|
|||
|
|||
impl<T: Default + Clone> Dummy<usize> for Vec<T> {
|
|||
fn dummy(cfg: usize) -> Self {
|
|||
vec![Default::default(); cfg]
|
|||
}
|
|||
}
|