Add solidity verifier of the nova+cyclefold (#87)

* Add solidity verifier of the nova+cyclefold, and add method to prepare the calldata from Decider's proof. Missing conversion of the point coordinates into limbs (ark compatible)

* chore: adding comments linking to the contract's signature

* chore: update .gitignore

* chore: add num-bigint as dev dependency

* fix: work with abs path for storing generated sol code

* chore: update comment

* feat: solidity verifier working on single and multi-input circuits

* feat: multi-input folding verification working + fixing encoding of additive identity in calldata

* chore: make bigint a dependency

* refactor: import utils functions from utils.rs and make them available from anywhere

* chore: make utils and evm available publicly

* fix: pub mod instead

* chore: make relevant method public and add `get_decider_template_for_cyclefold_decider` to exported objects

* solidity-verifiers: move tests to their corresponding files

* small update: Cyclefold -> CycleFold at the missing places

* abstract nova-cyclefold solidity verifiers tests to avoid code duplication, and abstract also the computed setup params (FS & Decider) to compute them only once for all related tests to save test time

* small polish after rebase to last main branch changes

* rm unneeded Option for KZGData::g1_crs_batch_points

* add checks modifying z_0 & z_i to nova_cyclefold_solidity_verifier test

* add light-test feature to decider_eth_circuit to use it in solidity-verifier tests without the big circuit

* solidity-verifiers: groth16 template: port the fix from https://github.com/iden3/snarkjs/pull/480 & https://github.com/iden3/snarkjs/issues/479

* add print warning msg for light-test in DeciderEthCircuit

* solidity-verifiers: update limbs logic to nonnative last version, parametrize limbs params

solidity-verifiers:
* update solidity limbs logic to last nonnative impl version, and to
  last u_i.x impl
* parametrize limbs params
* add light-test feature: replace the '#[cfg(not(test))]' by the
  'light-test' feature that by default is not enabled, so when running
  the github actions we enable the feature 'light-tests', and then we can
  have a full-test that runs the test without the 'light-tests' flag, but
  we don't run this big test every time.  The choice of a feature is to
  allow us to control this from other-crates tests (for example for the
  solidity-verifier separated crate tests, to avoid running the full heavy
  circuit in the solidity tests)

* move solidity constants into template constants for auto compute of params

* polishing

* revm use only needed feature

This is to avoid c depencency for c-kzg which is behind the c-kzg flag
and not needed.

* nova_cyclefold_decider.sol header

* rearrange test helpers position, add error for min number of steps

* in solidity-verifiers: 'data'->'vk/verifier key'

* add From for NovaCycleFoldVerifierKey from original vks to simplify dev flow, also conditionally template the batchCheck related structs and methods from the KZG10 solidity template

---------

Co-authored-by: dmpierre <pdaixmoreux@gmail.com>
This commit is contained in:
2024-04-25 11:51:59 +02:00
committed by GitHub
parent 8b233031a6
commit 97df224579
24 changed files with 1019 additions and 482 deletions

View File

@@ -20,6 +20,8 @@ rayon = "1.7.0"
num-bigint = "0.4"
num-integer = "0.1"
color-eyre = "=0.6.2"
ark-bn254 = {version="0.4.0"}
ark-groth16 = { version = "^0.4.0" }
# tmp imports for espresso's sumcheck
espresso_subroutines = {git="https://github.com/EspressoSystems/hyperplonk", package="subroutines"}
@@ -29,13 +31,13 @@ ark-pallas = {version="0.4.0", features=["r1cs"]}
ark-vesta = {version="0.4.0", features=["r1cs"]}
ark-bn254 = {version="0.4.0", features=["r1cs"]}
ark-grumpkin = {version="0.4.0", features=["r1cs"]}
ark-groth16 = { version = "^0.4.0" }
rand = "0.8.5"
tracing = { version = "0.1", default-features = false, features = [ "attributes" ] }
tracing-subscriber = { version = "0.2" }
[features]
default = ["parallel"]
light-test = []
parallel = [
"ark-std/parallel",

View File

@@ -174,7 +174,7 @@ impl<F: PrimeField> ToBitsGadget<F> for LimbVar<F> {
pub struct NonNativeUintVar<F: PrimeField>(pub Vec<LimbVar<F>>);
impl<F: PrimeField> NonNativeUintVar<F> {
const fn bits_per_limb() -> usize {
pub const fn bits_per_limb() -> usize {
assert!(F::MODULUS_BIT_SIZE > 250);
// For a `F` with order > 250 bits, 55 is chosen for optimizing the most
// expensive part `Az∘Bz` when checking the R1CS relation for CycleFold.

View File

@@ -245,7 +245,7 @@ pub mod tests {
/// - M(01) = 4*eq_00(r) + 3*eq_10(r) + 9*eq_01(r) + 2*eq_11(r)
/// - M(11) = 4*eq_00(r) + 2*eq_10(r) + 2*eq_01(r) + 0*eq_11(r)
///
/// This is used by Hypernova in LCCCS to perform a verifier-chosen random linear combination between the columns
/// This is used by HyperNova in LCCCS to perform a verifier-chosen random linear combination between the columns
/// of the matrix and the z vector. This technique is also used extensively in "An Algebraic Framework for
/// Universal and Updatable SNARKs".
#[test]

View File

@@ -1,17 +1,21 @@
/// This file implements the onchain (Ethereum's EVM) decider.
use ark_bn254::Bn254;
use ark_crypto_primitives::sponge::Absorb;
use ark_ec::{CurveGroup, Group};
use ark_ff::PrimeField;
use ark_ec::{AffineRepr, CurveGroup, Group};
use ark_ff::{BigInteger, PrimeField};
use ark_groth16::Groth16;
use ark_r1cs_std::{groups::GroupOpsBounds, prelude::CurveVar, ToConstraintFieldGadget};
use ark_snark::SNARK;
use ark_std::rand::{CryptoRng, RngCore};
use ark_std::Zero;
use ark_std::{One, Zero};
use core::marker::PhantomData;
pub use super::decider_eth_circuit::{DeciderEthCircuit, KZGChallengesGadget};
use super::{circuits::CF2, nifs::NIFS, CommittedInstance, Nova};
use crate::commitment::{
kzg::Proof as KZGProof, pedersen::Params as PedersenParams, CommitmentScheme,
kzg::{Proof as KZGProof, KZG},
pedersen::Params as PedersenParams,
CommitmentScheme,
};
use crate::folding::circuits::nonnative::affine::NonNativeAffineVar;
use crate::frontend::FCircuit;
@@ -143,8 +147,12 @@ where
z_i: Vec<C1::ScalarField>,
running_instance: &Self::CommittedInstance,
incoming_instance: &Self::CommittedInstance,
proof: Self::Proof,
proof: &Self::Proof,
) -> Result<bool, Error> {
if i <= C1::ScalarField::one() {
return Err(Error::NotEnoughSteps);
}
let (snark_vk, cs_vk): (S::VerifyingKey, CS1::VerifierParams) = vp;
// compute U = U_{d+1}= NIFS.V(U_d, u_d, cmT)
@@ -199,6 +207,78 @@ where
}
}
/// Prepares solidity calldata for calling the NovaDecider contract
pub fn prepare_calldata(
function_signature_check: [u8; 4],
i: ark_bn254::Fr,
z_0: Vec<ark_bn254::Fr>,
z_i: Vec<ark_bn254::Fr>,
running_instance: &CommittedInstance<ark_bn254::G1Projective>,
incoming_instance: &CommittedInstance<ark_bn254::G1Projective>,
proof: Proof<ark_bn254::G1Projective, KZG<'static, Bn254>, Groth16<Bn254>>,
) -> Result<Vec<u8>, Error> {
Ok(vec![
function_signature_check.to_vec(),
i.into_bigint().to_bytes_be(), // i
z_0.iter()
.flat_map(|v| v.into_bigint().to_bytes_be())
.collect::<Vec<u8>>(), // z_0
z_i.iter()
.flat_map(|v| v.into_bigint().to_bytes_be())
.collect::<Vec<u8>>(), // z_i
point_to_eth_format(running_instance.cmW.into_affine())?, // U_i_cmW
point_to_eth_format(running_instance.cmE.into_affine())?, // U_i_cmE
running_instance.u.into_bigint().to_bytes_be(), // U_i_u
incoming_instance.u.into_bigint().to_bytes_be(), // u_i_u
proof.r.into_bigint().to_bytes_be(), // r
running_instance
.x
.iter()
.flat_map(|v| v.into_bigint().to_bytes_be())
.collect::<Vec<u8>>(), // U_i_x
point_to_eth_format(incoming_instance.cmW.into_affine())?, // u_i_cmW
incoming_instance
.x
.iter()
.flat_map(|v| v.into_bigint().to_bytes_be())
.collect::<Vec<u8>>(), // u_i_x
point_to_eth_format(proof.cmT.into_affine())?, // cmT
point_to_eth_format(proof.snark_proof.a)?, // pA
point2_to_eth_format(proof.snark_proof.b)?, // pB
point_to_eth_format(proof.snark_proof.c)?, // pC
proof.kzg_challenges[0].into_bigint().to_bytes_be(), // challenge_W
proof.kzg_challenges[1].into_bigint().to_bytes_be(), // challenge_E
proof.kzg_proofs[0].eval.into_bigint().to_bytes_be(), // eval W
proof.kzg_proofs[1].eval.into_bigint().to_bytes_be(), // eval E
point_to_eth_format(proof.kzg_proofs[0].proof.into_affine())?, // W kzg_proof
point_to_eth_format(proof.kzg_proofs[1].proof.into_affine())?, // E kzg_proof
]
.concat())
}
fn point_to_eth_format<C: AffineRepr>(p: C) -> Result<Vec<u8>, Error>
where
C::BaseField: PrimeField,
{
// the encoding of the additive identity is [0, 0] on the EVM
let zero_point = (&C::BaseField::zero(), &C::BaseField::zero());
let (x, y) = p.xy().unwrap_or(zero_point);
Ok([x.into_bigint().to_bytes_be(), y.into_bigint().to_bytes_be()].concat())
}
fn point2_to_eth_format(p: ark_bn254::G2Affine) -> Result<Vec<u8>, Error> {
let zero_point = (&ark_bn254::Fq2::zero(), &ark_bn254::Fq2::zero());
let (x, y) = p.xy().unwrap_or(zero_point);
Ok([
x.c1.into_bigint().to_bytes_be(),
x.c0.into_bigint().to_bytes_be(),
y.c1.into_bigint().to_bytes_be(),
y.c0.into_bigint().to_bytes_be(),
]
.concat())
}
#[cfg(test)]
pub mod tests {
use super::*;
@@ -298,7 +378,7 @@ pub mod tests {
let start = Instant::now();
let decider_vp = (g16_vk, kzg_vk);
let verified = DECIDER::verify(
decider_vp, nova.i, nova.z_0, nova.z_i, &nova.U_i, &nova.u_i, proof,
decider_vp, nova.i, nova.z_0, nova.z_i, &nova.U_i, &nova.u_i, &proof,
)
.unwrap();
assert!(verified);

View File

@@ -426,10 +426,14 @@ where
.hash(&crh_params, i.clone(), z_0.clone(), z_i.clone())?;
(u_i.x[0]).enforce_equal(&u_i_x)?;
#[cfg(feature = "light-test")]
println!("[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.
#[cfg(not(test))]
// (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))]`

View File

@@ -307,7 +307,7 @@ fn pow_i<F: PrimeField>(i: usize, betas: &Vec<F>) -> F {
/// calculates F[x] using the optimized binary-tree technique
/// described in Claim 4.4
/// of [Protogalaxy](https://eprint.iacr.org/2023/1106.pdf)
/// of [ProtoGalaxy](https://eprint.iacr.org/2023/1106.pdf)
fn calc_f_from_btree<F: PrimeField>(
fw: &[F],
betas: &[F],

View File

@@ -3,7 +3,7 @@
#![allow(non_camel_case_types)]
#![allow(clippy::upper_case_acronyms)]
use ark_ec::CurveGroup;
use ark_ec::{pairing::Pairing, CurveGroup};
use ark_ff::PrimeField;
use ark_std::rand::CryptoRng;
use ark_std::{fmt::Debug, rand::RngCore};
@@ -34,8 +34,6 @@ pub enum Error {
ProtoGalaxy(folding::protogalaxy::ProtoGalaxyError),
#[error("std::io::Error")]
IOError(#[from] std::io::Error),
#[error("{0}")]
Other(String),
// Relation errors
#[error("Relation not satisfied")]
@@ -68,6 +66,8 @@ pub enum Error {
OutOfBounds,
#[error("Could not construct the Evaluation Domain")]
NewDomainFail,
#[error("The number of folded steps must be greater than 1")]
NotEnoughSteps,
// Commitment errors
#[error("Pedersen parameters length is not sufficient (generators.len={0} < vector.len={1} unsatisfied)")]
@@ -78,6 +78,8 @@ pub enum Error {
CommitmentVerificationFail,
// Other
#[error("{0}")]
Other(String),
#[error("Randomness for blinding not found")]
MissingRandomness,
#[error("Missing value: {0}")]
@@ -178,8 +180,27 @@ pub trait Decider<
z_i: Vec<C1::ScalarField>,
running_instance: &Self::CommittedInstance,
incoming_instance: &Self::CommittedInstance,
proof: Self::Proof,
proof: &Self::Proof,
// returns `Result<bool, Error>` to differentiate between an error occurred while performing
// the verification steps, and the verification logic of the scheme not passing.
) -> Result<bool, Error>;
}
/// DeciderOnchain extends the Decider into preparing the calldata
pub trait DeciderOnchain<E: Pairing, C1: CurveGroup, C2: CurveGroup>
where
C1: CurveGroup<BaseField = C2::ScalarField, ScalarField = C2::BaseField>,
C2::BaseField: PrimeField,
{
type Proof;
type CommittedInstance: Clone + Debug;
fn prepare_calldata(
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<Vec<u8>, Error>;
}

View File

@@ -119,7 +119,7 @@ impl<C: CurveGroup> SumCheckVerifier<C> for IOPVerifierState<C> {
.clone()
.into_iter()
.zip(self.challenges.clone().into_iter())
.map(|(evaluations, challenge)| {
.map(|(coeffs, challenge)| {
// Removed check on number of evaluations here since verifier receives polynomial in coeffs form
let prover_poly = DensePolynomial::from_coefficients_slice(&coeffs);
Ok(prover_poly.evaluate(&challenge))