Browse Source

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>
update-nifs-interface
arnaucube 6 months ago
committed by GitHub
parent
commit
97df224579
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
24 changed files with 1020 additions and 483 deletions
  1. +2
    -2
      .github/workflows/ci.yml
  2. +1
    -0
      .gitignore
  3. +6
    -29
      cli/README.md
  4. +2
    -2
      cli/src/main.rs
  5. +20
    -17
      cli/src/settings.rs
  6. +3
    -1
      folding-schemes/Cargo.toml
  7. +1
    -1
      folding-schemes/src/folding/circuits/nonnative/uint.rs
  8. +1
    -1
      folding-schemes/src/folding/hypernova/utils.rs
  9. +86
    -6
      folding-schemes/src/folding/nova/decider_eth.rs
  10. +6
    -2
      folding-schemes/src/folding/nova/decider_eth_circuit.rs
  11. +1
    -1
      folding-schemes/src/folding/protogalaxy/folding.rs
  12. +25
    -4
      folding-schemes/src/lib.rs
  13. +1
    -1
      folding-schemes/src/utils/espresso/sum_check/verifier.rs
  14. +8
    -4
      solidity-verifiers/Cargo.toml
  15. +15
    -9
      solidity-verifiers/src/evm.rs
  16. +7
    -5
      solidity-verifiers/src/lib.rs
  17. +28
    -2
      solidity-verifiers/src/utils/mod.rs
  18. +88
    -14
      solidity-verifiers/src/verifiers/g16.rs
  19. +126
    -12
      solidity-verifiers/src/verifiers/kzg.rs
  20. +25
    -331
      solidity-verifiers/src/verifiers/mod.rs
  21. +410
    -23
      solidity-verifiers/src/verifiers/nova_cyclefold.rs
  22. +2
    -2
      solidity-verifiers/templates/groth16_verifier.askama.sol
  23. +4
    -0
      solidity-verifiers/templates/kzg10_verifier.askama.sol
  24. +152
    -14
      solidity-verifiers/templates/nova_cyclefold_decider.askama.sol

+ 2
- 2
.github/workflows/ci.yml

@ -64,10 +64,10 @@ jobs:
- name: Build
# This build will be reused by nextest,
# and also checks (--all-targets) that benches don't bit-rot
run: cargo build --release --all-targets --no-default-features --features "${{ matrix.feature }}"
run: cargo build --release --all-targets --no-default-features --features "light-test,${{ matrix.feature }}"
- name: Test
run: |
cargo nextest run --profile ci --release --workspace --no-default-features --features "${{ matrix.feature }}"
cargo nextest run --profile ci --release --workspace --no-default-features --features "light-test,${{ matrix.feature }}"
- name: Doctests # nextest does not support doc tests
run: |
cargo test --doc

+ 1
- 0
.gitignore

@ -1,5 +1,6 @@
/target
Cargo.lock
# Circom generated files
folding-schemes/src/frontend/circom/test_folder/cubic_circuit.r1cs
folding-schemes/src/frontend/circom/test_folder/cubic_circuit_js/

+ 6
- 29
cli/README.md

@ -1,26 +1,6 @@
# Solidity Verifiers CLI
_____ ______ ______ ______ ______ ______ ______
| |__| || |__| || |__| || |__| || |__| || |__| || |__| |
| () || () || () || () || () || () || () |
|______||______||______||______||______||______||______|
______ ______
| |__| | ____ _ _ _ _ _ | |__| |
| () | / ___| ___ | (_) __| (_) |_ _ _ | () |
|______| \___ \ / _ \| | |/ _` | | __| | | | |______|
______ ___) | (_) | | | (_| | | |_| |_| | ______
| |__| | |____/ \___/|_|_|\__,_|_|\__|\__, | | |__| |
| () | __ __ _ __ _ |___/ | () |
|______| \ \ / /__ _ __(_)/ _(_) ___ _ __ |______|
______ \ \ / / _ \ '__| | |_| |/ _ \ '__| ______
| |__| | \ V / __/ | | | _| | __/ | | |__| |
| () | \_/ \___|_| |_|_| |_|\___|_| | () |
|______| |______|
______ ______ ______ ______ ______ ______ ______
| |__| || |__| || |__| || |__| || |__| || |__| || |__| |
| () || () || () || () || () || () || () |
|______||______||______||______||______||______||______|
Welcome to Solidity Verifiers CLI, a Command-Line Interface (CLI) tool designed to simplify the generation of Solidity smart contracts that verify proofs of Zero Knowledge cryptographic protocols. This tool is developed by the collaborative efforts of the PSE (Privacy & Scaling Explorations) and 0XPARC teams.
Solidity Verifiers CLI is a Command-Line Interface (CLI) tool to generate the Solidity smart contracts that verify proofs of Zero Knowledge cryptographic protocols. This tool is developed by the collaborative efforts of the PSE (Privacy & Scaling Explorations) and 0xPARC teams.
Solidity Verifiers CLI is released under the MIT license, but notice that the Solidity template for the Groth16 verification has GPL-3.0 license, hence the generated Solidity verifiers that use the Groth16 template will have that license too.
@ -34,7 +14,7 @@ Solidity Verifier currently supports the generation of Solidity smart contracts
- **KZG:**
- Uses the Kate-Zaverucha-Goldberg polynomial commitment scheme.
- Example credit: [weijiekoh - KZG10 Verifier Contract](https://github.com/weijiekoh/libkzg/blob/master/sol/KZGVerifier.sol)
- Template credit: [weijiekoh - KZG10 Verifier Contract](https://github.com/weijiekoh/libkzg/blob/master/sol/KZGVerifier.sol)
- **Nova + CycleFold Decider:**
- Implements the decider circuit verification for the Nova proof system in conjunction with the CycleFold protocol optimization.
@ -43,19 +23,19 @@ Solidity Verifier currently supports the generation of Solidity smart contracts
## Usage
```bash
solidity-verifiers-cli [OPTIONS] -p <PROTOCOL> -d <PROTOCOL_DATA> -o <OUTPUT_PATH>
solidity-verifiers-cli [OPTIONS] -p <PROTOCOL> -k <PROTOCOL_VK> -o <OUTPUT_PATH>
```
A real use case (which was used to test the tool itself):
`solidity-verifiers-cli -p groth16 -d ./solidity-verifiers/assets/G16_test_vk_data`
This would generate a Groth16 verifier contract for the given G16 data (which consists on the G16_Vkey only) and store this contract in `$pwd`.
`solidity-verifiers-cli -p groth16 -k ./solidity-verifiers/assets/G16_test_vk`
This would generate a Groth16 verifier contract for the given G16 verifier key (which consists on the G16_Vk only) and store this contract in `$pwd`.
### Options:
-v, --verbose: Increase logging verbosity
-q, --quiet: Decrease logging verbosity
-p, --protocol <PROTOCOL>: Selects the protocol for which to generate the Decider circuit Solidity Verifier (possible values: groth16, kzg, nova-cyclefold)
-o, --out <OUT>: Sets the output path for all generated artifacts
-d, --protocol-data <PROTOCOL_DATA>: Sets the input path for the file containing all the data required by the chosen protocol for verification contract generation
-k, --protocol-vk <PROTOCOL_VK>: Sets the input path for the file containing the verifier key required by the protocol chosen such that the verification contract can be generated.
--pragma <PRAGMA>: Selects the Solidity compiler version to be set in the Solidity Verifier contract artifact
-h, --help: Print help (see a summary with '-h')
-V, --version: Print version
@ -66,6 +46,3 @@ Solidity Verifier CLI is released under the MIT license, but notice that the Sol
## Contributing
Feel free to explore, use, and contribute to Solidity Verifiers CLI as we strive to enhance privacy and scalability in the blockchain space!
We welcome contributions to Solidity Verifiers CLI! If you encounter any issues, have feature requests, or want to contribute to the codebase, please check out the GitHub repository and follow the guidelines outlined in the contributing documentation.

+ 2
- 2
cli/src/main.rs

@ -25,12 +25,12 @@ fn main() {
// Fetch the exact protocol for which we need to generate the Decider verifier contract.
let protocol = cli.protocol;
// Fetch the protocol data passed by the user from the file.
let protocol_data = std::fs::read(cli.protocol_data).unwrap();
let protocol_vk = std::fs::read(cli.protocol_vk).unwrap();
// Generate the Solidity Verifier contract for the selected protocol with the given data.
create_or_open_then_write(
&out_path,
&protocol.render(&protocol_data, cli.pragma).unwrap(),
&protocol.render(&protocol_vk, cli.pragma).unwrap(),
)
.unwrap();
}

+ 20
- 17
cli/src/settings.rs

@ -1,6 +1,8 @@
use ark_serialize::SerializationError;
use clap::{Parser, ValueEnum};
use solidity_verifiers::{Groth16Data, KzgData, NovaCyclefoldData, ProtocolData};
use solidity_verifiers::{
Groth16VerifierKey, KZG10VerifierKey, NovaCycleFoldVerifierKey, ProtocolVerifierKey,
};
use std::{env, fmt::Display, path::PathBuf};
fn get_default_out_path() -> PathBuf {
@ -13,7 +15,7 @@ fn get_default_out_path() -> PathBuf {
pub(crate) enum Protocol {
Groth16,
Kzg,
NovaCyclefold,
NovaCycleFold,
}
impl Display for Protocol {
@ -22,7 +24,7 @@ impl Display for Protocol {
}
}
// Would be nice to link this to the `Template` or `ProtocolData` traits.
// Would be nice to link this to the `Template` or `ProtocolVerifierKey` traits.
// Sadly, this requires Boxing with `dyn` or similar which would complicate the code more than is actually required.
impl Protocol {
pub(crate) fn render(
@ -31,19 +33,20 @@ impl Protocol {
pragma: Option<String>,
) -> Result<Vec<u8>, SerializationError> {
match self {
Self::Groth16 => {
Ok(Groth16Data::deserialize_protocol_data(data)?.render_as_template(pragma))
}
Self::Kzg => Ok(KzgData::deserialize_protocol_data(data)?.render_as_template(pragma)),
Self::NovaCyclefold => {
Ok(NovaCyclefoldData::deserialize_protocol_data(data)?.render_as_template(pragma))
}
Self::Groth16 => Ok(Groth16VerifierKey::deserialize_protocol_verifier_key(data)?
.render_as_template(pragma)),
Self::Kzg => Ok(KZG10VerifierKey::deserialize_protocol_verifier_key(data)?
.render_as_template(pragma)),
Self::NovaCycleFold => Ok(NovaCycleFoldVerifierKey::deserialize_protocol_verifier_key(
data,
)?
.render_as_template(pragma)),
}
}
}
const ABOUT: &str = "A Command-Line Interface (CLI) tool designed to simplify the generation of Solidity smart contracts that verify proofs of Zero Knowledge cryptographic protocols.
const ABOUT: &str = "A Command-Line Interface (CLI) tool to generate the Solidity smart contracts that verify proofs of Zero Knowledge cryptographic protocols.
";
const LONG_ABOUT: &str = "
@ -68,7 +71,7 @@ const LONG_ABOUT: &str = "
| () || () || () || () || () || () || () |
|______||______||______||______||______||______||______|
Welcome to Solidity Verifiers CLI, a Command-Line Interface (CLI) tool designed to simplify the generation of Solidity smart contracts that verify proofs of Zero Knowledge cryptographic protocols. This tool is developed by the collaborative efforts of the PSE (Privacy & Scaling Explorations) and 0XPARC teams.
Welcome to Solidity Verifiers CLI, a Command-Line Interface (CLI) tool designed to simplify the generation of Solidity smart contracts that verify proofs of Zero Knowledge cryptographic protocols. This tool is developed by the collaborative efforts of the PSE (Privacy & Scaling Explorations) and 0xPARC teams.
Solidity Verifiers CLI is released under the MIT license, but notice that the Solidity template for the Groth16 verification has GPL-3.0 license, hence the generated Solidity verifiers that use the Groth16 template will have that license too.
@ -84,7 +87,7 @@ Solidity Verifier currently supports the generation of Solidity smart contracts
Implements the decider circuit verification for the Nova proof system in conjunction with the CycleFold protocol optimization.
";
#[derive(Debug, Parser)]
#[command(author = "0XPARC & PSE", version, about = ABOUT, long_about = Some(LONG_ABOUT))]
#[command(author = "0xPARC & PSE", version, about = ABOUT, long_about = Some(LONG_ABOUT))]
#[command(propagate_version = true)]
/// A tool to create Solidity Contracts which act as verifiers for the major Folding Schemes implemented
/// within the `sonobe` repo.
@ -100,9 +103,9 @@ pub(crate) struct Cli {
/// Sets the output path for all the artifacts generated by the command.
pub out: PathBuf,
#[arg(short = 'd', long)]
/// Sets the input path for the file containing all the data required by the protocol chosen such that the verification contract can be generated.
pub protocol_data: PathBuf,
#[arg(short = 'k', long)]
/// Sets the input path for the file containing the verifier key required by the protocol chosen such that the verification contract can be generated.
pub protocol_vk: PathBuf,
/// Selects the Solidity compiler version to be set in the Solidity Verifier contract artifact.
#[arg(long, default_value=None)]

+ 3
- 1
folding-schemes/Cargo.toml

@ -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",

+ 1
- 1
folding-schemes/src/folding/circuits/nonnative/uint.rs

@ -174,7 +174,7 @@ impl ToBitsGadget for LimbVar {
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.

+ 1
- 1
folding-schemes/src/folding/hypernova/utils.rs

@ -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]

+ 86
- 6
folding-schemes/src/folding/nova/decider_eth.rs

@ -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);

+ 6
- 2
folding-schemes/src/folding/nova/decider_eth_circuit.rs

@ -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))]`

+ 1
- 1
folding-schemes/src/folding/protogalaxy/folding.rs

@ -307,7 +307,7 @@ fn pow_i(i: usize, betas: &Vec) -> 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],

+ 25
- 4
folding-schemes/src/lib.rs

@ -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>;
}

+ 1
- 1
folding-schemes/src/utils/espresso/sum_check/verifier.rs

@ -119,7 +119,7 @@ impl SumCheckVerifier for IOPVerifierState {
.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))

+ 8
- 4
solidity-verifiers/Cargo.toml

@ -12,19 +12,23 @@ ark-groth16 = "0.4"
askama = { version = "0.12.0", features = ["config"], default-features = false }
ark-bn254 = "0.4.0"
ark-poly-commit = "0.4.0"
folding-schemes = { path = "../folding-schemes/" }
itertools = "0.12.1"
ark-serialize = "0.4.1"
revm = "3.5.0"
revm = {version="3.5.0", default-features=false, features=["std"]}
rust-crypto = "0.2"
num-bigint = "0.4.3"
folding-schemes = { path = "../folding-schemes/"} # without 'light-test' enabled
[dev-dependencies]
ark-crypto-primitives = "0.4.0"
ark-groth16 = "0.4"
ark-r1cs-std = "0.4.0"
ark-relations = "0.4.0"
revm = "3.5.0"
tracing = { version = "0.1", default-features = false, features = [ "attributes" ] }
tracing-subscriber = { version = "0.2" }
ark-bn254 = {version="0.4.0", features=["r1cs"]}
ark-grumpkin = {version="0.4.0", features=["r1cs"]}
rand = "0.8.5"
folding-schemes = { path = "../folding-schemes/", features=["light-test"]}
[features]
default = ["parallel"]

+ 15
- 9
solidity-verifiers/src/evm.rs

@ -5,17 +5,23 @@ use revm::{
};
use std::{
fmt::{self, Debug, Formatter},
fs::{create_dir_all, File},
fs::{self, create_dir_all, File},
io::{self, Write},
path::PathBuf,
process::{Command, Stdio},
str,
};
// from: https://github.com/privacy-scaling-explorations/halo2-solidity-verifier/blob/85cb77b171ce3ee493628007c7a1cfae2ea878e6/examples/separately.rs#L56
pub(crate) fn save_solidity(name: impl AsRef<str>, solidity: &str) {
const DIR_GENERATED: &str = "./generated";
create_dir_all(DIR_GENERATED).unwrap();
File::create(format!("{DIR_GENERATED}/{}", name.as_ref()))
pub fn save_solidity(name: impl AsRef<str>, solidity: &str) {
let curdir = PathBuf::from(".");
let curdir_abs_path = fs::canonicalize(curdir).expect("Failed to get current directory");
let curdir_abs_path = curdir_abs_path
.to_str()
.expect("Failed to convert path to string");
let dir_generated = format!("{curdir_abs_path}/generated");
create_dir_all(dir_generated.clone()).unwrap();
File::create(format!("{}/{}", dir_generated, name.as_ref()))
.unwrap()
.write_all(solidity.as_bytes())
.unwrap();
@ -25,7 +31,7 @@ pub(crate) fn save_solidity(name: impl AsRef, solidity: &str) {
///
/// # Panics
/// Panics if executable `solc` can not be found, or compilation fails.
pub(crate) fn compile_solidity(solidity: impl AsRef<[u8]>, contract_name: &str) -> Vec<u8> {
pub fn compile_solidity(solidity: impl AsRef<[u8]>, contract_name: &str) -> Vec<u8> {
let mut process = match Command::new("solc")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
@ -72,7 +78,7 @@ fn find_binary(stdout: &str, contract_name: &str) -> Option> {
}
/// Evm runner.
pub(crate) struct Evm {
pub struct Evm {
evm: EVM<InMemoryDB>,
}
@ -103,7 +109,7 @@ impl Evm {
///
/// # Panics
/// Panics if execution reverts or halts unexpectedly.
pub(crate) fn create(&mut self, bytecode: Vec<u8>) -> Address {
pub fn create(&mut self, bytecode: Vec<u8>) -> Address {
let (_, output) = self.transact_success_or_panic(TxEnv {
gas_limit: u64::MAX,
transact_to: TransactTo::Create(CreateScheme::Create),
@ -121,7 +127,7 @@ impl Evm {
///
/// # Panics
/// Panics if execution reverts or halts unexpectedly.
pub(crate) fn call(&mut self, address: Address, calldata: Vec<u8>) -> (u64, Vec<u8>) {
pub fn call(&mut self, address: Address, calldata: Vec<u8>) -> (u64, Vec<u8>) {
let (gas_used, output) = self.transact_success_or_panic(TxEnv {
gas_limit: u64::MAX,
transact_to: TransactTo::Call(address),

+ 7
- 5
solidity-verifiers/src/lib.rs

@ -1,7 +1,9 @@
#[cfg(test)]
mod evm;
mod utils;
mod verifiers;
pub mod evm;
pub mod utils;
pub mod verifiers;
pub use verifiers::*;
pub use verifiers::{Groth16Data, KzgData, NovaCyclefoldData, ProtocolData};
pub use verifiers::{
get_decider_template_for_cyclefold_decider, Groth16VerifierKey, KZG10VerifierKey,
NovaCycleFoldVerifierKey, ProtocolVerifierKey,
};

+ 28
- 2
solidity-verifiers/src/utils/mod.rs

@ -1,11 +1,37 @@
use crate::{GPL3_SDPX_IDENTIFIER, PRAGMA_GROTH16_VERIFIER};
use askama::Template;
use crypto::{digest::Digest, sha3::Sha3};
use num_bigint::BigUint;
pub mod encoding;
/// Formats call data from a vec of bytes to a hashmap
/// Useful for debugging directly on the EVM
/// !! Should follow the contract's function signature, we assuming the order of arguments is correct
pub fn get_formatted_calldata(calldata: Vec<u8>) -> Vec<String> {
let mut formatted_calldata = vec![];
for i in (4..calldata.len()).step_by(32) {
let val = BigUint::from_bytes_be(&calldata[i..i + 32]);
formatted_calldata.push(format!("{}", val));
}
formatted_calldata
}
/// Computes the function selector for the nova cyclefold verifier
/// It is computed on the fly since it depends on the length of the first parameter array
pub fn get_function_selector_for_nova_cyclefold_verifier(
first_param_array_length: usize,
) -> [u8; 4] {
let mut hasher = Sha3::keccak256();
let fn_sig = format!("verifyNovaProof(uint256[{}],uint256[4],uint256[3],uint256[4],uint256[4],uint256[2],uint256[2][2],uint256[2],uint256[4],uint256[2][2])", first_param_array_length);
hasher.input_str(&fn_sig);
let hash = &mut [0u8; 32];
hasher.result(hash);
[hash[0], hash[1], hash[2], hash[3]]
}
#[derive(Template)]
#[template(path = "header_template.askama.sol", ext = "sol")]
pub(crate) struct HeaderInclusion<T: Template> {
pub struct HeaderInclusion<T: Template> {
/// SPDX-License-Identifier
pub sdpx: String,
/// The `pragma` statement.

+ 88
- 14
solidity-verifiers/src/verifiers/g16.rs

@ -1,7 +1,7 @@
use crate::utils::encoding::{g1_to_fq_repr, g2_to_fq_repr};
use crate::utils::encoding::{G1Repr, G2Repr};
use crate::utils::HeaderInclusion;
use crate::{ProtocolData, GPL3_SDPX_IDENTIFIER};
use crate::{ProtocolVerifierKey, GPL3_SDPX_IDENTIFIER};
use ark_bn254::Bn254;
use ark_groth16::VerifyingKey;
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
@ -26,15 +26,15 @@ pub(crate) struct Groth16Verifier {
pub(crate) gamma_abc_g1: Vec<G1Repr>,
}
impl From<Groth16Data> for Groth16Verifier {
fn from(g16_data: Groth16Data) -> Self {
impl From<Groth16VerifierKey> for Groth16Verifier {
fn from(g16_vk: Groth16VerifierKey) -> Self {
Self {
vkey_alpha_g1: g1_to_fq_repr(g16_data.0.alpha_g1),
vkey_beta_g2: g2_to_fq_repr(g16_data.0.beta_g2),
vkey_gamma_g2: g2_to_fq_repr(g16_data.0.gamma_g2),
vkey_delta_g2: g2_to_fq_repr(g16_data.0.delta_g2),
gamma_abc_len: g16_data.0.gamma_abc_g1.len(),
gamma_abc_g1: g16_data
vkey_alpha_g1: g1_to_fq_repr(g16_vk.0.alpha_g1),
vkey_beta_g2: g2_to_fq_repr(g16_vk.0.beta_g2),
vkey_gamma_g2: g2_to_fq_repr(g16_vk.0.gamma_g2),
vkey_delta_g2: g2_to_fq_repr(g16_vk.0.delta_g2),
gamma_abc_len: g16_vk.0.gamma_abc_g1.len(),
gamma_abc_g1: g16_vk
.0
.gamma_abc_g1
.iter()
@ -45,18 +45,18 @@ impl From for Groth16Verifier {
}
}
// Ideally I would like to link this to the `Decider` trait in FoldingSchemes.
// For now, this is the easiest as NovaCyclefold isn't clear target from where we can get all it's needed arguments.
// Ideally this would be linked to the `Decider` trait in FoldingSchemes.
// For now, this is the easiest as NovaCycleFold isn't clear target from where we can get all it's needed arguments.
#[derive(CanonicalDeserialize, CanonicalSerialize, Clone, PartialEq, Debug)]
pub struct Groth16Data(pub(crate) VerifyingKey<Bn254>);
pub struct Groth16VerifierKey(pub(crate) VerifyingKey<Bn254>);
impl From<VerifyingKey<Bn254>> for Groth16Data {
impl From<VerifyingKey<Bn254>> for Groth16VerifierKey {
fn from(value: VerifyingKey<Bn254>) -> Self {
Self(value)
}
}
impl ProtocolData for Groth16Data {
impl ProtocolVerifierKey for Groth16VerifierKey {
const PROTOCOL_NAME: &'static str = "Groth16";
fn render_as_template(self, pragma: Option<String>) -> Vec<u8> {
@ -70,3 +70,77 @@ impl ProtocolData for Groth16Data {
.into_bytes()
}
}
#[cfg(test)]
mod tests {
use super::Groth16VerifierKey;
use crate::{
evm::{compile_solidity, save_solidity, Evm},
ProtocolVerifierKey,
};
use ark_bn254::{Bn254, Fr};
use ark_crypto_primitives::snark::SNARK;
use ark_ec::AffineRepr;
use ark_ff::{BigInt, BigInteger, PrimeField};
use ark_groth16::Groth16;
use ark_std::rand::{RngCore, SeedableRng};
use ark_std::test_rng;
use askama::Template;
use itertools::chain;
use super::Groth16Verifier;
use crate::verifiers::tests::{setup, DEFAULT_SETUP_LEN};
pub const FUNCTION_SELECTOR_GROTH16_VERIFY_PROOF: [u8; 4] = [0x43, 0x75, 0x3b, 0x4d];
#[test]
fn groth16_vk_serde_roundtrip() {
let (_, _, _, vk, _) = setup(DEFAULT_SETUP_LEN);
let g16_vk = Groth16VerifierKey::from(vk);
let mut bytes = vec![];
g16_vk.serialize_protocol_verifier_key(&mut bytes).unwrap();
let obtained_g16_vk =
Groth16VerifierKey::deserialize_protocol_verifier_key(bytes.as_slice()).unwrap();
assert_eq!(g16_vk, obtained_g16_vk)
}
#[test]
fn test_groth16_verifier_accepts_and_rejects_proofs() {
let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64());
let (_, _, g16_pk, g16_vk, circuit) = setup(DEFAULT_SETUP_LEN);
let g16_vk = Groth16VerifierKey::from(g16_vk);
let proof = Groth16::<Bn254>::prove(&g16_pk, circuit, &mut rng).unwrap();
let res = Groth16Verifier::from(g16_vk).render().unwrap();
save_solidity("groth16_verifier.sol", &res);
let groth16_verifier_bytecode = compile_solidity(&res, "Verifier");
let mut evm = Evm::default();
let verifier_address = evm.create(groth16_verifier_bytecode);
let (a_x, a_y) = proof.a.xy().unwrap();
let (b_x, b_y) = proof.b.xy().unwrap();
let (c_x, c_y) = proof.c.xy().unwrap();
let mut calldata: Vec<u8> = chain![
FUNCTION_SELECTOR_GROTH16_VERIFY_PROOF,
a_x.into_bigint().to_bytes_be(),
a_y.into_bigint().to_bytes_be(),
b_x.c1.into_bigint().to_bytes_be(),
b_x.c0.into_bigint().to_bytes_be(),
b_y.c1.into_bigint().to_bytes_be(),
b_y.c0.into_bigint().to_bytes_be(),
c_x.into_bigint().to_bytes_be(),
c_y.into_bigint().to_bytes_be(),
BigInt::from(Fr::from(circuit.z)).to_bytes_be(),
]
.collect();
let (_, output) = evm.call(verifier_address, calldata.clone());
assert_eq!(*output.last().unwrap(), 1);
// change calldata to make it invalid
let last_calldata_element = calldata.last_mut().unwrap();
*last_calldata_element = 0;
let (_, output) = evm.call(verifier_address, calldata);
assert_eq!(*output.last().unwrap(), 0);
}
}

+ 126
- 12
solidity-verifiers/src/verifiers/kzg.rs

@ -1,7 +1,7 @@
use crate::utils::encoding::{g1_to_fq_repr, g2_to_fq_repr};
use crate::utils::encoding::{G1Repr, G2Repr};
use crate::utils::HeaderInclusion;
use crate::{ProtocolData, MIT_SDPX_IDENTIFIER};
use crate::{ProtocolVerifierKey, MIT_SDPX_IDENTIFIER};
use ark_bn254::{Bn254, G1Affine};
use ark_poly_commit::kzg10::VerifierKey;
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
@ -24,16 +24,15 @@ pub(crate) struct KZG10Verifier {
pub(crate) g1_crs: Vec<G1Repr>,
}
impl From<KzgData> for KZG10Verifier {
fn from(data: KzgData) -> Self {
let g1_crs_batch_points = data.g1_crs_batch_points.unwrap_or_default();
impl From<KZG10VerifierKey> for KZG10Verifier {
fn from(data: KZG10VerifierKey) -> Self {
Self {
g1: g1_to_fq_repr(data.vk.g),
g2: g2_to_fq_repr(data.vk.h),
vk: g2_to_fq_repr(data.vk.beta_h),
g1_crs_len: g1_crs_batch_points.len(),
g1_crs: g1_crs_batch_points
g1_crs_len: data.g1_crs_batch_points.len(),
g1_crs: data
.g1_crs_batch_points
.iter()
.map(|g1| g1_to_fq_repr(*g1))
.collect(),
@ -42,13 +41,13 @@ impl From for KZG10Verifier {
}
#[derive(CanonicalDeserialize, CanonicalSerialize, Clone, PartialEq, Debug)]
pub struct KzgData {
pub struct KZG10VerifierKey {
pub(crate) vk: VerifierKey<Bn254>,
pub(crate) g1_crs_batch_points: Option<Vec<G1Affine>>,
pub(crate) g1_crs_batch_points: Vec<G1Affine>,
}
impl From<(VerifierKey<Bn254>, Option<Vec<G1Affine>>)> for KzgData {
fn from(value: (VerifierKey<Bn254>, Option<Vec<G1Affine>>)) -> Self {
impl From<(VerifierKey<Bn254>, Vec<G1Affine>)> for KZG10VerifierKey {
fn from(value: (VerifierKey<Bn254>, Vec<G1Affine>)) -> Self {
Self {
vk: value.0,
g1_crs_batch_points: value.1,
@ -56,7 +55,7 @@ impl From<(VerifierKey, Option>)> for KzgData {
}
}
impl ProtocolData for KzgData {
impl ProtocolVerifierKey for KZG10VerifierKey {
const PROTOCOL_NAME: &'static str = "KZG";
fn render_as_template(self, pragma: Option<String>) -> Vec<u8> {
@ -70,3 +69,118 @@ impl ProtocolData for KzgData {
.into_bytes()
}
}
#[cfg(test)]
mod tests {
use super::KZG10VerifierKey;
use crate::{
evm::{compile_solidity, Evm},
utils::HeaderInclusion,
ProtocolVerifierKey,
};
use ark_bn254::{Bn254, Fr, G1Projective as G1};
use ark_ec::{AffineRepr, CurveGroup};
use ark_ff::{BigInteger, PrimeField};
use ark_std::rand::{RngCore, SeedableRng};
use ark_std::Zero;
use ark_std::{test_rng, UniformRand};
use askama::Template;
use itertools::chain;
use folding_schemes::{
commitment::{kzg::KZG, CommitmentScheme},
transcript::{
poseidon::{poseidon_test_config, PoseidonTranscript},
Transcript,
},
};
use super::KZG10Verifier;
use crate::verifiers::tests::{setup, DEFAULT_SETUP_LEN};
const FUNCTION_SELECTOR_KZG10_CHECK: [u8; 4] = [0x9e, 0x78, 0xcc, 0xf7];
#[test]
fn kzg_vk_serde_roundtrip() {
let (pk, vk, _, _, _) = setup(DEFAULT_SETUP_LEN);
let kzg_vk = KZG10VerifierKey::from((vk, pk.powers_of_g[0..3].to_vec()));
let mut bytes = vec![];
kzg_vk.serialize_protocol_verifier_key(&mut bytes).unwrap();
let obtained_kzg_vk =
KZG10VerifierKey::deserialize_protocol_verifier_key(bytes.as_slice()).unwrap();
assert_eq!(kzg_vk, obtained_kzg_vk)
}
#[test]
fn kzg_verifier_compiles() {
let (kzg_pk, kzg_vk, _, _, _) = setup(DEFAULT_SETUP_LEN);
let kzg_vk = KZG10VerifierKey::from((kzg_vk.clone(), kzg_pk.powers_of_g[0..3].to_vec()));
let res = HeaderInclusion::<KZG10Verifier>::builder()
.template(kzg_vk)
.build()
.render()
.unwrap();
let kzg_verifier_bytecode = compile_solidity(res, "KZG10");
let mut evm = Evm::default();
_ = evm.create(kzg_verifier_bytecode);
}
#[test]
fn kzg_verifier_accepts_and_rejects_proofs() {
let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64());
let poseidon_config = poseidon_test_config::<Fr>();
let transcript_p = &mut PoseidonTranscript::<G1>::new(&poseidon_config);
let transcript_v = &mut PoseidonTranscript::<G1>::new(&poseidon_config);
let (kzg_pk, kzg_vk, _, _, _) = setup(DEFAULT_SETUP_LEN);
let kzg_vk = KZG10VerifierKey::from((kzg_vk.clone(), kzg_pk.powers_of_g[0..3].to_vec()));
let v: Vec<Fr> = std::iter::repeat_with(|| Fr::rand(&mut rng))
.take(DEFAULT_SETUP_LEN)
.collect();
let cm = KZG::<Bn254>::commit(&kzg_pk, &v, &Fr::zero()).unwrap();
let proof = KZG::<Bn254>::prove(&kzg_pk, transcript_p, &cm, &v, &Fr::zero(), None).unwrap();
let template = HeaderInclusion::<KZG10Verifier>::builder()
.template(kzg_vk)
.build()
.render()
.unwrap();
let kzg_verifier_bytecode = compile_solidity(template, "KZG10");
let mut evm = Evm::default();
let verifier_address = evm.create(kzg_verifier_bytecode);
let (cm_affine, proof_affine) = (cm.into_affine(), proof.proof.into_affine());
let (x_comm, y_comm) = cm_affine.xy().unwrap();
let (x_proof, y_proof) = proof_affine.xy().unwrap();
let y = proof.eval.into_bigint().to_bytes_be();
transcript_v.absorb_point(&cm).unwrap();
let x = transcript_v.get_challenge();
let x = x.into_bigint().to_bytes_be();
let mut calldata: Vec<u8> = chain![
FUNCTION_SELECTOR_KZG10_CHECK,
x_comm.into_bigint().to_bytes_be(),
y_comm.into_bigint().to_bytes_be(),
x_proof.into_bigint().to_bytes_be(),
y_proof.into_bigint().to_bytes_be(),
x.clone(),
y,
]
.collect();
let (_, output) = evm.call(verifier_address, calldata.clone());
assert_eq!(*output.last().unwrap(), 1);
// change calldata to make it invalid
let last_calldata_element = calldata.last_mut().unwrap();
*last_calldata_element = 0;
let (_, output) = evm.call(verifier_address, calldata);
assert_eq!(*output.last().unwrap(), 0);
}
}

+ 25
- 331
solidity-verifiers/src/verifiers/mod.rs

@ -14,11 +14,11 @@ mod g16;
mod kzg;
mod nova_cyclefold;
pub use g16::Groth16Data;
pub use kzg::KzgData;
pub use nova_cyclefold::NovaCyclefoldData;
pub use g16::Groth16VerifierKey;
pub use kzg::KZG10VerifierKey;
pub use nova_cyclefold::{get_decider_template_for_cyclefold_decider, NovaCycleFoldVerifierKey};
pub trait ProtocolData: CanonicalDeserialize + CanonicalSerialize {
pub trait ProtocolVerifierKey: CanonicalDeserialize + CanonicalSerialize {
const PROTOCOL_NAME: &'static str;
fn serialize_name<W: Write>(&self, writer: &mut W) -> Result<(), SerializationError> {
@ -27,11 +27,14 @@ pub trait ProtocolData: CanonicalDeserialize + CanonicalSerialize {
.serialize_uncompressed(writer)
}
fn serialize_protocol_data<W: Write>(&self, writer: &mut W) -> Result<(), SerializationError> {
fn serialize_protocol_verifier_key<W: Write>(
&self,
writer: &mut W,
) -> Result<(), SerializationError> {
self.serialize_name(writer)?;
self.serialize_compressed(writer)
}
fn deserialize_protocol_data<R: Read + Copy>(
fn deserialize_protocol_verifier_key<R: Read + Copy>(
mut reader: R,
) -> Result<Self, SerializationError> {
let name: String = String::deserialize_uncompressed(&mut reader)?;
@ -48,51 +51,31 @@ pub trait ProtocolData: CanonicalDeserialize + CanonicalSerialize {
}
#[cfg(test)]
mod tests {
use crate::evm::{compile_solidity, save_solidity, Evm};
use crate::utils::HeaderInclusion;
use crate::{Groth16Data, KzgData, NovaCyclefoldData, ProtocolData};
pub mod tests {
use ark_bn254::{Bn254, Fr, G1Projective as G1};
use ark_crypto_primitives::snark::{CircuitSpecificSetupSNARK, SNARK};
use ark_ec::{AffineRepr, CurveGroup};
use ark_ff::{BigInt, BigInteger, PrimeField};
use ark_crypto_primitives::snark::CircuitSpecificSetupSNARK;
use ark_ff::PrimeField;
use ark_groth16::Groth16;
use ark_poly_commit::kzg10::VerifierKey;
use ark_poly_commit::kzg10::VerifierKey as KZGVerifierKey;
use ark_r1cs_std::alloc::AllocVar;
use ark_r1cs_std::eq::EqGadget;
use ark_r1cs_std::fields::fp::FpVar;
use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError};
use ark_std::rand::{RngCore, SeedableRng};
use ark_std::Zero;
use ark_std::{test_rng, UniformRand};
use askama::Template;
use folding_schemes::{
commitment::{
kzg::{ProverKey, KZG},
CommitmentScheme,
},
transcript::{
poseidon::{poseidon_test_config, PoseidonTranscript},
Transcript,
},
};
use itertools::chain;
use ark_std::test_rng;
use std::marker::PhantomData;
use super::g16::Groth16Verifier;
use super::kzg::KZG10Verifier;
use super::nova_cyclefold::NovaCyclefoldDecider;
// Function signatures for proof verification on kzg10 and groth16 contracts
pub const FUNCTION_SIGNATURE_KZG10_CHECK: [u8; 4] = [0x9e, 0x78, 0xcc, 0xf7];
pub const FUNCTION_SIGNATURE_GROTH16_VERIFY_PROOF: [u8; 4] = [0x43, 0x75, 0x3b, 0x4d];
pub const FUNCTION_SIGNATURE_NOVA_CYCLEFOLD_CHECK: [u8; 4] = [0x37, 0x98, 0x0b, 0xb6];
use folding_schemes::commitment::{
kzg::{ProverKey as KZGProverKey, KZG},
CommitmentScheme,
};
/// Default setup length for testing.
const DEFAULT_SETUP_LEN: usize = 5;
pub const DEFAULT_SETUP_LEN: usize = 5;
/// Test circuit used to test the Groth16 proof generation
#[derive(Debug, Clone, Copy)]
struct TestAddCircuit<F: PrimeField> {
pub struct TestAddCircuit<F: PrimeField> {
_f: PhantomData<F>,
pub x: u8,
pub y: u8,
@ -111,11 +94,11 @@ mod tests {
}
#[allow(clippy::type_complexity)]
fn setup<'a>(
pub fn setup<'a>(
n: usize,
) -> (
ProverKey<'a, G1>,
VerifierKey<Bn254>,
KZGProverKey<'a, G1>,
KZGVerifierKey<Bn254>,
ark_groth16::ProvingKey<Bn254>,
ark_groth16::VerifyingKey<Bn254>,
TestAddCircuit<Fr>,
@ -130,297 +113,8 @@ mod tests {
};
let (g16_pk, g16_vk) = Groth16::<Bn254>::setup(circuit, &mut rng).unwrap();
let (kzg_pk, kzg_vk): (ProverKey<G1>, VerifierKey<Bn254>) =
let (kzg_pk, kzg_vk): (KZGProverKey<G1>, KZGVerifierKey<Bn254>) =
KZG::<Bn254>::setup(&mut rng, n).unwrap();
(kzg_pk, kzg_vk, g16_pk, g16_vk, circuit)
}
#[test]
fn groth16_data_serde_roundtrip() {
let (_, _, _, vk, _) = setup(DEFAULT_SETUP_LEN);
let g16_data = Groth16Data::from(vk);
let mut bytes = vec![];
g16_data.serialize_protocol_data(&mut bytes).unwrap();
let obtained_g16_data = Groth16Data::deserialize_protocol_data(bytes.as_slice()).unwrap();
assert_eq!(g16_data, obtained_g16_data)
}
#[test]
fn kzg_data_serde_roundtrip() {
let (pk, vk, _, _, _) = setup(DEFAULT_SETUP_LEN);
let kzg_data = KzgData::from((vk, Some(pk.powers_of_g[0..3].to_vec())));
let mut bytes = vec![];
kzg_data.serialize_protocol_data(&mut bytes).unwrap();
let obtained_kzg_data = KzgData::deserialize_protocol_data(bytes.as_slice()).unwrap();
assert_eq!(kzg_data, obtained_kzg_data)
}
#[test]
fn nova_cyclefold_data_serde_roundtrip() {
let (kzg_pk, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN);
let g16_data = Groth16Data::from(g16_vk);
let kzg_data = KzgData::from((kzg_vk, Some(kzg_pk.powers_of_g[0..3].to_vec())));
let mut bytes = vec![];
let nova_cyclefold_data = NovaCyclefoldData::from((g16_data, kzg_data));
nova_cyclefold_data
.serialize_protocol_data(&mut bytes)
.unwrap();
let obtained_nova_cyclefold_data =
NovaCyclefoldData::deserialize_protocol_data(bytes.as_slice()).unwrap();
assert_eq!(nova_cyclefold_data, obtained_nova_cyclefold_data)
}
#[test]
fn nova_cyclefold_decider_template_renders() {
let (kzg_pk, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN);
let g16_data = Groth16Data::from(g16_vk);
let kzg_data = KzgData::from((kzg_vk, Some(kzg_pk.powers_of_g[0..3].to_vec())));
let nova_cyclefold_data = NovaCyclefoldData::from((g16_data, kzg_data));
let decider_template = HeaderInclusion::<NovaCyclefoldDecider>::builder()
.template(nova_cyclefold_data)
.build();
save_solidity("NovaDecider.sol", &decider_template.render().unwrap());
}
#[test]
fn nova_cyclefold_decider_template_compiles() {
let (kzg_pk, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN);
let g16_data = Groth16Data::from(g16_vk);
let kzg_data = KzgData::from((kzg_vk, Some(kzg_pk.powers_of_g[0..3].to_vec())));
let nova_cyclefold_data = NovaCyclefoldData::from((g16_data, kzg_data));
let decider_template = HeaderInclusion::<NovaCyclefoldDecider>::builder()
.template(nova_cyclefold_data)
.build();
let decider_verifier_bytecode =
compile_solidity(decider_template.render().unwrap(), "NovaDecider");
let mut evm = Evm::default();
_ = evm.create(decider_verifier_bytecode);
}
#[test]
fn test_groth16_verifier_accepts_and_rejects_proofs() {
let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64());
let (_, _, g16_pk, g16_vk, circuit) = setup(DEFAULT_SETUP_LEN);
let g16_data = Groth16Data::from(g16_vk);
let proof = Groth16::<Bn254>::prove(&g16_pk, circuit, &mut rng).unwrap();
let res = Groth16Verifier::from(g16_data).render().unwrap();
save_solidity("groth16_verifier.sol", &res);
let groth16_verifier_bytecode = compile_solidity(&res, "Verifier");
let mut evm = Evm::default();
let verifier_address = evm.create(groth16_verifier_bytecode);
let (a_x, a_y) = proof.a.xy().unwrap();
let (b_x, b_y) = proof.b.xy().unwrap();
let (c_x, c_y) = proof.c.xy().unwrap();
let mut calldata: Vec<u8> = chain![
FUNCTION_SIGNATURE_GROTH16_VERIFY_PROOF,
a_x.into_bigint().to_bytes_be(),
a_y.into_bigint().to_bytes_be(),
b_x.c1.into_bigint().to_bytes_be(),
b_x.c0.into_bigint().to_bytes_be(),
b_y.c1.into_bigint().to_bytes_be(),
b_y.c0.into_bigint().to_bytes_be(),
c_x.into_bigint().to_bytes_be(),
c_y.into_bigint().to_bytes_be(),
BigInt::from(Fr::from(circuit.z)).to_bytes_be(),
]
.collect();
let (_, output) = evm.call(verifier_address, calldata.clone());
assert_eq!(*output.last().unwrap(), 1);
// change calldata to make it invalid
let last_calldata_element = calldata.last_mut().unwrap();
*last_calldata_element = 0;
let (_, output) = evm.call(verifier_address, calldata);
assert_eq!(*output.last().unwrap(), 0);
}
#[test]
fn kzg_verifier_template_renders() {
let (kzg_pk, kzg_vk, _, _, _) = setup(DEFAULT_SETUP_LEN);
let kzg_data = KzgData::from((kzg_vk.clone(), Some(kzg_pk.powers_of_g[0..3].to_vec())));
let res = HeaderInclusion::<KZG10Verifier>::builder()
.template(kzg_data)
.build()
.render()
.unwrap();
// TODO: Unsure what this is testing. If we want to test correct rendering,
// we should first check that it COMPLETELY renders to what we expect.
assert!(res.contains(&kzg_vk.g.x.to_string()));
}
#[test]
fn kzg_verifier_compiles() {
let (kzg_pk, kzg_vk, _, _, _) = setup(DEFAULT_SETUP_LEN);
let kzg_data = KzgData::from((kzg_vk.clone(), Some(kzg_pk.powers_of_g[0..3].to_vec())));
let res = HeaderInclusion::<KZG10Verifier>::builder()
.template(kzg_data)
.build()
.render()
.unwrap();
let kzg_verifier_bytecode = compile_solidity(res, "KZG10");
let mut evm = Evm::default();
_ = evm.create(kzg_verifier_bytecode);
}
#[test]
fn kzg_verifier_accepts_and_rejects_proofs() {
let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64());
let poseidon_config = poseidon_test_config::<Fr>();
let transcript_p = &mut PoseidonTranscript::<G1>::new(&poseidon_config);
let transcript_v = &mut PoseidonTranscript::<G1>::new(&poseidon_config);
let (kzg_pk, kzg_vk, _, _, _) = setup(DEFAULT_SETUP_LEN);
let kzg_data = KzgData::from((kzg_vk.clone(), Some(kzg_pk.powers_of_g[0..3].to_vec())));
let v: Vec<Fr> = std::iter::repeat_with(|| Fr::rand(&mut rng))
.take(DEFAULT_SETUP_LEN)
.collect();
let cm = KZG::<Bn254>::commit(&kzg_pk, &v, &Fr::zero()).unwrap();
let proof = KZG::<Bn254>::prove(&kzg_pk, transcript_p, &cm, &v, &Fr::zero(), None).unwrap();
let template = HeaderInclusion::<KZG10Verifier>::builder()
.template(kzg_data)
.build()
.render()
.unwrap();
let kzg_verifier_bytecode = compile_solidity(template, "KZG10");
let mut evm = Evm::default();
let verifier_address = evm.create(kzg_verifier_bytecode);
let (cm_affine, proof_affine) = (cm.into_affine(), proof.proof.into_affine());
let (x_comm, y_comm) = cm_affine.xy().unwrap();
let (x_proof, y_proof) = proof_affine.xy().unwrap();
let y = proof.eval.into_bigint().to_bytes_be();
transcript_v.absorb_point(&cm).unwrap();
let x = transcript_v.get_challenge();
let x = x.into_bigint().to_bytes_be();
let mut calldata: Vec<u8> = chain![
FUNCTION_SIGNATURE_KZG10_CHECK,
x_comm.into_bigint().to_bytes_be(),
y_comm.into_bigint().to_bytes_be(),
x_proof.into_bigint().to_bytes_be(),
y_proof.into_bigint().to_bytes_be(),
x.clone(),
y,
]
.collect();
let (_, output) = evm.call(verifier_address, calldata.clone());
assert_eq!(*output.last().unwrap(), 1);
// change calldata to make it invalid
let last_calldata_element = calldata.last_mut().unwrap();
*last_calldata_element = 0;
let (_, output) = evm.call(verifier_address, calldata);
assert_eq!(*output.last().unwrap(), 0);
}
#[test]
fn nova_cyclefold_verifier_compiles() {
let (kzg_pk, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN);
let g16_data = Groth16Data::from(g16_vk);
let kzg_data = KzgData::from((kzg_vk, Some(kzg_pk.powers_of_g[0..3].to_vec())));
let nova_cyclefold_data = NovaCyclefoldData::from((g16_data, kzg_data));
let decider_template = HeaderInclusion::<NovaCyclefoldDecider>::builder()
.template(nova_cyclefold_data)
.build()
.render()
.unwrap();
let nova_cyclefold_verifier_bytecode = compile_solidity(decider_template, "NovaDecider");
let mut evm = Evm::default();
_ = evm.create(nova_cyclefold_verifier_bytecode);
}
#[test]
fn nova_cyclefold_verifier_accepts_and_rejects_proofs() {
let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64());
let (kzg_pk, kzg_vk, g16_pk, g16_vk, circuit) = setup(DEFAULT_SETUP_LEN);
let g16_data = Groth16Data::from(g16_vk);
let kzg_data = KzgData::from((kzg_vk, Some(kzg_pk.powers_of_g[0..3].to_vec())));
let nova_cyclefold_data = NovaCyclefoldData::from((g16_data, kzg_data));
let g16_proof = Groth16::<Bn254>::prove(&g16_pk, circuit, &mut rng).unwrap();
let (a_x, a_y) = g16_proof.a.xy().unwrap();
let (b_x, b_y) = g16_proof.b.xy().unwrap();
let (c_x, c_y) = g16_proof.c.xy().unwrap();
let poseidon_config = poseidon_test_config::<Fr>();
let transcript_p = &mut PoseidonTranscript::<G1>::new(&poseidon_config);
let transcript_v = &mut PoseidonTranscript::<G1>::new(&poseidon_config);
let v: Vec<Fr> = std::iter::repeat_with(|| Fr::rand(&mut rng))
.take(DEFAULT_SETUP_LEN)
.collect();
let cm = KZG::<Bn254>::commit(&kzg_pk, &v, &Fr::zero()).unwrap();
let proof = KZG::<Bn254>::prove(&kzg_pk, transcript_p, &cm, &v, &Fr::zero(), None).unwrap();
let decider_template = HeaderInclusion::<NovaCyclefoldDecider>::builder()
.template(nova_cyclefold_data)
.build()
.render()
.unwrap();
let nova_cyclefold_verifier_bytecode = compile_solidity(decider_template, "NovaDecider");
let mut evm = Evm::default();
let verifier_address = evm.create(nova_cyclefold_verifier_bytecode);
let (cm_affine, proof_affine) = (cm.into_affine(), proof.proof.into_affine());
let (x_comm, y_comm) = cm_affine.xy().unwrap();
let (x_proof, y_proof) = proof_affine.xy().unwrap();
let y = proof.eval.into_bigint().to_bytes_be();
transcript_v.absorb_point(&cm).unwrap();
let x = transcript_v.get_challenge();
let x = x.into_bigint().to_bytes_be();
let mut calldata: Vec<u8> = chain![
FUNCTION_SIGNATURE_NOVA_CYCLEFOLD_CHECK,
a_x.into_bigint().to_bytes_be(),
a_y.into_bigint().to_bytes_be(),
b_x.c1.into_bigint().to_bytes_be(),
b_x.c0.into_bigint().to_bytes_be(),
b_y.c1.into_bigint().to_bytes_be(),
b_y.c0.into_bigint().to_bytes_be(),
c_x.into_bigint().to_bytes_be(),
c_y.into_bigint().to_bytes_be(),
BigInt::from(Fr::from(circuit.z)).to_bytes_be(),
x_comm.into_bigint().to_bytes_be(),
y_comm.into_bigint().to_bytes_be(),
x_proof.into_bigint().to_bytes_be(),
y_proof.into_bigint().to_bytes_be(),
x.clone(),
y,
]
.collect();
let (_, output) = evm.call(verifier_address, calldata.clone());
assert_eq!(*output.last().unwrap(), 1);
// change calldata to make it invalid
let last_calldata_element = calldata.last_mut().unwrap();
*last_calldata_element = 0;
let (_, output) = evm.call(verifier_address, calldata);
assert_eq!(*output.last().unwrap(), 0);
}
}

+ 410
- 23
solidity-verifiers/src/verifiers/nova_cyclefold.rs

@ -1,41 +1,69 @@
use crate::utils::HeaderInclusion;
use crate::{Groth16Data, KzgData, ProtocolData, PRAGMA_GROTH16_VERIFIER};
use ark_bn254::{Bn254, G1Affine};
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
use ark_bn254::{Bn254, Fq, G1Affine};
use ark_groth16::VerifyingKey;
use ark_poly_commit::kzg10::VerifierKey;
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
use askama::Template;
use folding_schemes::folding::circuits::nonnative::uint::NonNativeUintVar;
use super::g16::Groth16Verifier;
use super::kzg::KZG10Verifier;
use crate::utils::HeaderInclusion;
use crate::{Groth16VerifierKey, KZG10VerifierKey, ProtocolVerifierKey, PRAGMA_GROTH16_VERIFIER};
pub fn get_decider_template_for_cyclefold_decider(
nova_cyclefold_vk: NovaCycleFoldVerifierKey,
) -> String {
HeaderInclusion::<NovaCycleFoldDecider>::builder()
.template(nova_cyclefold_vk)
.build()
.render()
.unwrap()
}
#[derive(Template, Default)]
#[template(path = "nova_cyclefold_decider.askama.sol", ext = "sol")]
pub(crate) struct NovaCyclefoldDecider {
pub(crate) struct NovaCycleFoldDecider {
groth16_verifier: Groth16Verifier,
kzg10_verifier: KZG10Verifier,
// z_len denotes the FCircuit state (z_i) length
z_len: usize,
public_inputs_len: usize,
num_limbs: usize,
bits_per_limb: usize,
}
impl From<NovaCyclefoldData> for NovaCyclefoldDecider {
fn from(value: NovaCyclefoldData) -> Self {
impl From<NovaCycleFoldVerifierKey> for NovaCycleFoldDecider {
fn from(value: NovaCycleFoldVerifierKey) -> Self {
let groth16_verifier = Groth16Verifier::from(value.g16_vk);
let public_inputs_len = groth16_verifier.gamma_abc_len;
let bits_per_limb = NonNativeUintVar::<Fq>::bits_per_limb();
Self {
groth16_verifier: Groth16Verifier::from(value.g16_data),
kzg10_verifier: KZG10Verifier::from(value.kzg_data),
groth16_verifier,
kzg10_verifier: KZG10Verifier::from(value.kzg_vk),
z_len: value.z_len,
public_inputs_len,
num_limbs: (250_f32 / (bits_per_limb as f32)).ceil() as usize,
bits_per_limb,
}
}
}
#[derive(CanonicalDeserialize, CanonicalSerialize, PartialEq, Debug)]
pub struct NovaCyclefoldData {
g16_data: Groth16Data,
kzg_data: KzgData,
#[derive(CanonicalDeserialize, CanonicalSerialize, PartialEq, Debug, Clone)]
pub struct NovaCycleFoldVerifierKey {
g16_vk: Groth16VerifierKey,
kzg_vk: KZG10VerifierKey,
z_len: usize,
}
impl ProtocolData for NovaCyclefoldData {
const PROTOCOL_NAME: &'static str = "NovaCyclefold";
impl ProtocolVerifierKey for NovaCycleFoldVerifierKey {
const PROTOCOL_NAME: &'static str = "NovaCycleFold";
fn render_as_template(self, pragma: Option<String>) -> Vec<u8> {
HeaderInclusion::<NovaCyclefoldDecider>::builder()
HeaderInclusion::<NovaCycleFoldDecider>::builder()
.pragma_version(pragma.unwrap_or(PRAGMA_GROTH16_VERIFIER.to_string()))
.template(self)
.build()
@ -45,25 +73,384 @@ impl ProtocolData for NovaCyclefoldData {
}
}
impl From<(Groth16Data, KzgData)> for NovaCyclefoldData {
fn from(value: (Groth16Data, KzgData)) -> Self {
impl From<(Groth16VerifierKey, KZG10VerifierKey, usize)> for NovaCycleFoldVerifierKey {
fn from(value: (Groth16VerifierKey, KZG10VerifierKey, usize)) -> Self {
Self {
g16_vk: value.0,
kzg_vk: value.1,
z_len: value.2,
}
}
}
// implements From assuming that the 'batchCheck' method from the KZG10 template will not be used
// in the NovaCycleFoldDecider verifier contract
impl From<(VerifyingKey<Bn254>, VerifierKey<Bn254>, usize)> for NovaCycleFoldVerifierKey {
fn from(value: (VerifyingKey<Bn254>, VerifierKey<Bn254>, usize)) -> Self {
let g16_vk = Groth16VerifierKey::from(value.0);
// pass `Vec::new()` since batchCheck will not be used
let kzg_vk = KZG10VerifierKey::from((value.1, Vec::new()));
Self {
g16_data: value.0,
kzg_data: value.1,
g16_vk,
kzg_vk,
z_len: value.2,
}
}
}
impl NovaCyclefoldData {
impl NovaCycleFoldVerifierKey {
pub fn new(
vkey_g16: VerifyingKey<Bn254>,
vkey_kzg: VerifierKey<Bn254>,
crs_points: Vec<G1Affine>,
z_len: usize,
) -> Self {
Self {
g16_data: Groth16Data::from(vkey_g16),
// TODO: Remove option from crs points
kzg_data: KzgData::from((vkey_kzg, Some(crs_points))),
g16_vk: Groth16VerifierKey::from(vkey_g16),
kzg_vk: KZG10VerifierKey::from((vkey_kzg, crs_points)),
z_len,
}
}
}
#[cfg(test)]
mod tests {
use ark_bn254::{constraints::GVar, Bn254, Fr, G1Projective as G1};
use ark_crypto_primitives::snark::SNARK;
use ark_ff::PrimeField;
use ark_groth16::VerifyingKey as G16VerifierKey;
use ark_groth16::{Groth16, ProvingKey};
use ark_grumpkin::{constraints::GVar as GVar2, Projective as G2};
use ark_poly_commit::kzg10::VerifierKey as KZGVerifierKey;
use ark_r1cs_std::alloc::AllocVar;
use ark_r1cs_std::fields::fp::FpVar;
use ark_relations::r1cs::{ConstraintSystemRef, SynthesisError};
use ark_std::Zero;
use askama::Template;
use std::marker::PhantomData;
use std::time::Instant;
use folding_schemes::{
commitment::{
kzg::{ProverKey as KZGProverKey, KZG},
pedersen::Pedersen,
CommitmentScheme,
},
folding::nova::{
decider_eth::{prepare_calldata, Decider as DeciderEth},
decider_eth_circuit::DeciderEthCircuit,
get_cs_params_len, Nova, ProverParams,
},
frontend::FCircuit,
transcript::poseidon::poseidon_test_config,
Decider, Error, FoldingScheme,
};
use super::NovaCycleFoldDecider;
use crate::verifiers::tests::{setup, DEFAULT_SETUP_LEN};
use crate::{
evm::{compile_solidity, save_solidity, Evm},
utils::{get_function_selector_for_nova_cyclefold_verifier, HeaderInclusion},
verifiers::nova_cyclefold::get_decider_template_for_cyclefold_decider,
NovaCycleFoldVerifierKey, ProtocolVerifierKey,
};
/// Test circuit to be folded
#[derive(Clone, Copy, Debug)]
pub struct CubicFCircuit<F: PrimeField> {
_f: PhantomData<F>,
}
impl<F: PrimeField> FCircuit<F> for CubicFCircuit<F> {
type Params = ();
fn new(_params: Self::Params) -> Self {
Self { _f: PhantomData }
}
fn state_len(&self) -> usize {
1
}
fn step_native(&self, _i: usize, z_i: Vec<F>) -> Result<Vec<F>, Error> {
Ok(vec![z_i[0] * z_i[0] * z_i[0] + z_i[0] + F::from(5_u32)])
}
fn generate_step_constraints(
&self,
cs: ConstraintSystemRef<F>,
_i: usize,
z_i: Vec<FpVar<F>>,
) -> Result<Vec<FpVar<F>>, SynthesisError> {
let five = FpVar::<F>::new_constant(cs.clone(), F::from(5u32))?;
let z_i = z_i[0].clone();
Ok(vec![&z_i * &z_i * &z_i + &z_i + &five])
}
}
/// This is the circuit that we want to fold, it implements the FCircuit trait. The parameter z_i
/// denotes the current state which contains 5 elements, and z_{i+1} denotes the next state which
/// we get by applying the step.
/// In this example we set z_i and z_{i+1} to have five elements, and at each step we do different
/// operations on each of them.
#[derive(Clone, Copy, Debug)]
pub struct MultiInputsFCircuit<F: PrimeField> {
_f: PhantomData<F>,
}
impl<F: PrimeField> FCircuit<F> for MultiInputsFCircuit<F> {
type Params = ();
fn new(_params: Self::Params) -> Self {
Self { _f: PhantomData }
}
fn state_len(&self) -> usize {
5
}
/// computes the next state values in place, assigning z_{i+1} into z_i, and computing the new
/// z_{i+1}
fn step_native(&self, _i: usize, z_i: Vec<F>) -> Result<Vec<F>, Error> {
let a = z_i[0] + F::from(4_u32);
let b = z_i[1] + F::from(40_u32);
let c = z_i[2] * F::from(4_u32);
let d = z_i[3] * F::from(40_u32);
let e = z_i[4] + F::from(100_u32);
Ok(vec![a, b, c, d, e])
}
/// generates the constraints for the step of F for the given z_i
fn generate_step_constraints(
&self,
cs: ConstraintSystemRef<F>,
_i: usize,
z_i: Vec<FpVar<F>>,
) -> Result<Vec<FpVar<F>>, SynthesisError> {
let four = FpVar::<F>::new_constant(cs.clone(), F::from(4u32))?;
let forty = FpVar::<F>::new_constant(cs.clone(), F::from(40u32))?;
let onehundred = FpVar::<F>::new_constant(cs.clone(), F::from(100u32))?;
let a = z_i[0].clone() + four.clone();
let b = z_i[1].clone() + forty.clone();
let c = z_i[2].clone() * four;
let d = z_i[3].clone() * forty;
let e = z_i[4].clone() + onehundred;
Ok(vec![a, b, c, d, e])
}
}
#[test]
fn nova_cyclefold_vk_serde_roundtrip() {
let (_, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN);
let mut bytes = vec![];
let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from((g16_vk, kzg_vk, 1));
nova_cyclefold_vk
.serialize_protocol_verifier_key(&mut bytes)
.unwrap();
let obtained_nova_cyclefold_vk =
NovaCycleFoldVerifierKey::deserialize_protocol_verifier_key(bytes.as_slice()).unwrap();
assert_eq!(nova_cyclefold_vk, obtained_nova_cyclefold_vk)
}
#[test]
fn nova_cyclefold_decider_template_renders() {
let (_, kzg_vk, _, g16_vk, _) = setup(DEFAULT_SETUP_LEN);
let nova_cyclefold_vk = NovaCycleFoldVerifierKey::from((g16_vk, kzg_vk, 1));
let decider_solidity_code = HeaderInclusion::<NovaCycleFoldDecider>::builder()
.template(nova_cyclefold_vk)
.build();
save_solidity("NovaDecider.sol", &decider_solidity_code.render().unwrap());
}
#[allow(clippy::type_complexity)]
fn init_test_prover_params<FC: FCircuit<Fr, Params = ()>>() -> (
ProverParams<G1, G2, KZG<'static, Bn254>, Pedersen<G2>>,
KZGVerifierKey<Bn254>,
) {
let mut rng = ark_std::test_rng();
let poseidon_config = poseidon_test_config::<Fr>();
let f_circuit = FC::new(());
let (cs_len, cf_cs_len) =
get_cs_params_len::<G1, GVar, G2, GVar2, FC>(&poseidon_config, f_circuit).unwrap();
let (kzg_pk, kzg_vk): (KZGProverKey<G1>, KZGVerifierKey<Bn254>) =
KZG::<Bn254>::setup(&mut rng, cs_len).unwrap();
let (cf_pedersen_params, _) = Pedersen::<G2>::setup(&mut rng, cf_cs_len).unwrap();
let fs_prover_params = ProverParams::<G1, G2, KZG<Bn254>, Pedersen<G2>> {
poseidon_config: poseidon_config.clone(),
cs_params: kzg_pk.clone(),
cf_cs_params: cf_pedersen_params,
};
(fs_prover_params, kzg_vk)
}
/// Initializes Nova parameters and DeciderEth parameters. Only for test purposes.
#[allow(clippy::type_complexity)]
fn init_params<FC: FCircuit<Fr, Params = ()>>() -> (
ProverParams<G1, G2, KZG<'static, Bn254>, Pedersen<G2>>,
KZGVerifierKey<Bn254>,
ProvingKey<Bn254>,
G16VerifierKey<Bn254>,
) {
let mut rng = rand::rngs::OsRng;
let start = Instant::now();
let (fs_prover_params, kzg_vk) = init_test_prover_params::<FC>();
println!("generated Nova folding params: {:?}", start.elapsed());
let f_circuit = FC::new(());
pub type NOVA_FCircuit<FC> =
Nova<G1, GVar, G2, GVar2, FC, KZG<'static, Bn254>, Pedersen<G2>>;
let z_0 = vec![Fr::zero(); f_circuit.state_len()];
let nova = NOVA_FCircuit::init(&fs_prover_params, f_circuit, z_0.clone()).unwrap();
let decider_circuit =
DeciderEthCircuit::<G1, GVar, G2, GVar2, KZG<Bn254>, Pedersen<G2>>::from_nova::<FC>(
nova.clone(),
)
.unwrap();
let start = Instant::now();
let (g16_pk, g16_vk) =
Groth16::<Bn254>::circuit_specific_setup(decider_circuit.clone(), &mut rng).unwrap();
println!(
"generated G16 (Decider circuit) params: {:?}",
start.elapsed()
);
(fs_prover_params, kzg_vk, g16_pk, g16_vk)
}
/// This function allows to define which FCircuit to use for the test, and how many prove_step
/// rounds to perform.
/// Actions performed by this test:
/// - runs the NovaCycleFold folding scheme for the given FCircuit and n_steps times
/// - generates a DeciderEth proof, and executes it through the EVM
/// - modifies the calldata and checks that it does not pass the EVM check
/// - modifies the z_0 and checks that it does not pass the EVM check
#[allow(clippy::type_complexity)]
fn nova_cyclefold_solidity_verifier_opt<FC: FCircuit<Fr, Params = ()>>(
params: (
ProverParams<G1, G2, KZG<'static, Bn254>, Pedersen<G2>>,
KZGVerifierKey<Bn254>,
ProvingKey<Bn254>,
G16VerifierKey<Bn254>,
),
z_0: Vec<Fr>,
n_steps: usize,
) {
let (fs_prover_params, kzg_vk, g16_pk, g16_vk) = params.clone();
pub type NOVA_FCircuit<FC> =
Nova<G1, GVar, G2, GVar2, FC, KZG<'static, Bn254>, Pedersen<G2>>;
pub type DECIDERETH_FCircuit<FC> = DeciderEth<
G1,
GVar,
G2,
GVar2,
FC,
KZG<'static, Bn254>,
Pedersen<G2>,
Groth16<Bn254>,
NOVA_FCircuit<FC>,
>;
let f_circuit = FC::new(());
let nova_cyclefold_vk =
NovaCycleFoldVerifierKey::from((g16_vk.clone(), kzg_vk.clone(), f_circuit.state_len()));
let mut nova = NOVA_FCircuit::init(&fs_prover_params, f_circuit, z_0).unwrap();
for _ in 0..n_steps {
nova.prove_step().unwrap();
}
let rng = rand::rngs::OsRng;
let start = Instant::now();
let proof = DECIDERETH_FCircuit::prove(
(g16_pk, fs_prover_params.cs_params.clone()),
rng,
nova.clone(),
)
.unwrap();
println!("generated Decider proof: {:?}", start.elapsed());
let verified = DECIDERETH_FCircuit::<FC>::verify(
(g16_vk, kzg_vk),
nova.i,
nova.z_0.clone(),
nova.z_i.clone(),
&nova.U_i,
&nova.u_i,
&proof,
)
.unwrap();
assert!(verified);
let function_selector =
get_function_selector_for_nova_cyclefold_verifier(nova.z_0.len() * 2 + 1);
let calldata: Vec<u8> = prepare_calldata(
function_selector,
nova.i,
nova.z_0,
nova.z_i,
&nova.U_i,
&nova.u_i,
proof,
)
.unwrap();
let decider_solidity_code = get_decider_template_for_cyclefold_decider(nova_cyclefold_vk);
let nova_cyclefold_verifier_bytecode =
compile_solidity(decider_solidity_code, "NovaDecider");
let mut evm = Evm::default();
let verifier_address = evm.create(nova_cyclefold_verifier_bytecode);
let (_, output) = evm.call(verifier_address, calldata.clone());
assert_eq!(*output.last().unwrap(), 1);
// change i to make calldata invalid, placed between bytes 4 - 35
let mut invalid_calldata = calldata.clone();
invalid_calldata[35] += 1;
let (_, output) = evm.call(verifier_address, invalid_calldata.clone());
assert_eq!(*output.last().unwrap(), 0);
// change z_0 to make the EVM check fail, placed between bytes 35 - 67
let mut invalid_calldata = calldata.clone();
invalid_calldata[67] += 1;
let (_, output) = evm.call(verifier_address, invalid_calldata.clone());
assert_eq!(*output.last().unwrap(), 0);
// change z_i to make the EVM check fail, placed between bytes 68 - 100
let mut invalid_calldata = calldata.clone();
invalid_calldata[99] += 1;
let (_, output) = evm.call(verifier_address, invalid_calldata.clone());
assert_eq!(*output.last().unwrap(), 0);
}
#[test]
fn nova_cyclefold_solidity_verifier() {
let params = init_params::<CubicFCircuit<Fr>>();
let z_0 = vec![Fr::from(3_u32)];
nova_cyclefold_solidity_verifier_opt::<CubicFCircuit<Fr>>(params.clone(), z_0.clone(), 2);
nova_cyclefold_solidity_verifier_opt::<CubicFCircuit<Fr>>(params.clone(), z_0.clone(), 3);
let params = init_params::<MultiInputsFCircuit<Fr>>();
let z_0 = vec![
Fr::from(1_u32),
Fr::from(1_u32),
Fr::from(1_u32),
Fr::from(1_u32),
Fr::from(1_u32),
];
nova_cyclefold_solidity_verifier_opt::<MultiInputsFCircuit<Fr>>(
params.clone(),
z_0.clone(),
2,
);
nova_cyclefold_solidity_verifier_opt::<MultiInputsFCircuit<Fr>>(
params.clone(),
z_0.clone(),
3,
);
}
}

+ 2
- 2
solidity-verifiers/templates/groth16_verifier.askama.sol

@ -56,7 +56,7 @@ contract Groth16Verifier {
function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[{{ gamma_abc_len - 1 }}] calldata _pubSignals) public view returns (bool) {
assembly {
function checkField(v) {
if iszero(lt(v, q)) {
if iszero(lt(v, r)) {
mstore(0, 0)
return(0, 0x20)
}
@ -166,4 +166,4 @@ contract Groth16Verifier {
return(0, 0x20)
}
}
}
}

+ 4
- 0
solidity-verifiers/templates/kzg10_verifier.askama.sol

@ -136,6 +136,7 @@ contract KZG10Verifier {
]
];
{% if g1_crs_len>0 %} // only enabled if g1_crs_len>0, for batch_check
uint256[2][{{ g1_crs_len }}] G1_CRS = [
{%- for (i, point) in g1_crs.iter().enumerate() %}
[
@ -148,6 +149,7 @@ contract KZG10Verifier {
{%- endif -%}
{% endfor -%}
];
{%~ endif %}
/**
* @notice Verifies a single point evaluation proof. Function name follows `ark-poly`.
@ -198,6 +200,7 @@ contract KZG10Verifier {
return result;
}
{% if g1_crs_len>0 %} // only enabled if g1_crs_len>0, for batch_check
/**
* @notice Ensures that z(x) == 0 and l(x) == y for all x in x_vals and y in y_vals. It returns the commitment to z(x) and l(x).
* @param z_coeffs coefficients of the zero polynomial z(x) = (x - x_1)(x - x_2)...(x - x_n).
@ -268,4 +271,5 @@ contract KZG10Verifier {
uint256[2] memory neg_commit = negate(c);
return pairing(z_commit, pi, add(l_commit, neg_commit), G_2);
}
{%~ endif %}
}

+ 152
- 14
solidity-verifiers/templates/nova_cyclefold_decider.askama.sol

@ -1,24 +1,162 @@
{{ groth16_verifier }}
/*
Sonobe's Nova + CycleFold decider verifier.
Joint effort by 0xPARC & PSE.
More details at https://github.com/privacy-scaling-explorations/sonobe
Usage and design documentation at https://privacy-scaling-explorations.github.io/sonobe-docs/
Uses the https://github.com/iden3/snarkjs/blob/master/templates/verifier_groth16.sol.ejs
Groth16 verifier implementation and a KZG10 Solidity template adapted from
https://github.com/weijiekoh/libkzg.
Additionally we implement the NovaDecider contract, which combines the
Groth16 and KZG10 verifiers to verify the zkSNARK proofs coming from
Nova+CycleFold folding.
*/
/* =============================== */
/* KZG10 verifier methods */
{{ kzg10_verifier }}
/* =============================== */
/* Groth16 verifier methods */
{{ groth16_verifier }}
/* =============================== */
/* Nova+CycleFold Decider verifier */
/**
* @notice Computes the decomposition of a `uint256` into num_limbs limbs of bits_per_limb bits each.
* @dev Compatible with sonobe::folding-schemes::folding::circuits::nonnative::nonnative_field_to_field_elements.
*/
library LimbsDecomposition {
function decompose(uint256 x) internal pure returns (uint256[{{num_limbs}}] memory) {
uint256[{{num_limbs}}] memory limbs;
for (uint8 i = 0; i < {{num_limbs}}; i++) {
limbs[i] = (x >> ({{bits_per_limb}} * i)) & ((1 << {{bits_per_limb}}) - 1);
}
return limbs;
}
}
/**
* @author PSE & 0xPARC
* @title NovaDecider contract, for verifying zk-snarks Nova IVC proofs.
* @dev This is an askama template. It will feature a snarkjs groth16 and a kzg10 verifier, from which this contract inherits.
* WARNING: This contract is not complete nor finished. It lacks checks to ensure that no soundness issues can happen.
* Indeed, we know some of the checks that are missing. And we're working on the solution
* but for now, it's good enough for testing and benchmarking.
* @title NovaDecider contract, for verifying Nova IVC SNARK proofs.
* @dev This is an askama template which, when templated, features a Groth16 and KZG10 verifiers from which this contract inherits.
*/
contract NovaDecider is Groth16Verifier, KZG10Verifier {
function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[1] calldata _pubSignals, uint256[2] calldata c, uint256[2] calldata pi, uint256 x, uint256 y) public view returns (bool) {
bool success_kzg = super.check(c, pi, x, y);
require(success_kzg == true, "KZG Failed");
/**
* @notice Computes the linear combination of a and b with r as the coefficient.
* @dev All ops are done mod the BN254 scalar field prime
*/
function rlc(uint256 a, uint256 r, uint256 b) internal pure returns (uint256 result) {
assembly {
result := addmod(a, mulmod(r, b, BN254_SCALAR_FIELD), BN254_SCALAR_FIELD)
}
}
/**
* @notice Verifies a nova cyclefold proof consisting of two KZG proofs and of a groth16 proof.
* @dev The selector of this function is "dynamic", since it depends on `z_len`.
*/
function verifyNovaProof(
// inputs are grouped to prevent errors due stack too deep
uint256[{{ 1 + z_len * 2 }}] calldata i_z0_zi, // [i, z0, zi] where |z0| == |zi|
uint256[4] calldata U_i_cmW_U_i_cmE, // [U_i_cmW[2], U_i_cmE[2]]
uint256[3] calldata U_i_u_u_i_u_r, // [U_i_u, u_i_u, r]
uint256[4] calldata U_i_x_u_i_cmW, // [U_i_x[2], u_i_cmW[2]]
uint256[4] calldata u_i_x_cmT, // [u_i_x[2], cmT[2]]
uint256[2] calldata pA, // groth16
uint256[2][2] calldata pB, // groth16
uint256[2] calldata pC, // groth16
uint256[4] calldata challenge_W_challenge_E_kzg_evals, // [challenge_W, challenge_E, eval_W, eval_E]
uint256[2][2] calldata kzg_proof // [proof_W, proof_E]
) public view returns (bool) {
require(i_z0_zi[0] >= 2, "Folding: the number of folded steps should be at least 2");
// from gamma_abc_len, we subtract 1.
uint256[{{ public_inputs_len - 1 }}] memory public_inputs;
public_inputs[0] = i_z0_zi[0];
for (uint i = 0; i < {{ z_len * 2 }}; i++) {
public_inputs[1 + i] = i_z0_zi[1 + i];
}
{
// U_i.u + r * u_i.u
uint256 u = rlc(U_i_u_u_i_u_r[0], U_i_u_u_i_u_r[2], U_i_u_u_i_u_r[1]);
// U_i.x + r * u_i.x
uint256 x0 = rlc(U_i_x_u_i_cmW[0], U_i_u_u_i_u_r[2], u_i_x_cmT[0]);
uint256 x1 = rlc(U_i_x_u_i_cmW[1], U_i_u_u_i_u_r[2], u_i_x_cmT[1]);
public_inputs[{{ z_len * 2 + 1 }}] = u;
public_inputs[{{ z_len * 2 + 2 }}] = x0;
public_inputs[{{ z_len * 2 + 3 }}] = x1;
}
{
// U_i.cmE + r * u_i.cmT
uint256[2] memory mulScalarPoint = super.mulScalar([u_i_x_cmT[2], u_i_x_cmT[3]], U_i_u_u_i_u_r[2]);
uint256[2] memory cmE = super.add([U_i_cmW_U_i_cmE[2], U_i_cmW_U_i_cmE[3]], mulScalarPoint);
{
uint256[{{num_limbs}}] memory cmE_x_limbs = LimbsDecomposition.decompose(cmE[0]);
uint256[{{num_limbs}}] memory cmE_y_limbs = LimbsDecomposition.decompose(cmE[1]);
for (uint8 k = 0; k < {{num_limbs}}; k++) {
public_inputs[{{ z_len * 2 + 4 }} + k] = cmE_x_limbs[k];
public_inputs[{{ z_len * 2 + 4 + num_limbs }} + k] = cmE_y_limbs[k];
}
}
require(this.check(cmE, kzg_proof[1], challenge_W_challenge_E_kzg_evals[1], challenge_W_challenge_E_kzg_evals[3]), "KZG: verifying proof for challenge E failed");
}
{
// U_i.cmW + r * u_i.cmW
uint256[2] memory mulScalarPoint = super.mulScalar([U_i_x_u_i_cmW[2], U_i_x_u_i_cmW[3]], U_i_u_u_i_u_r[2]);
uint256[2] memory cmW = super.add([U_i_cmW_U_i_cmE[0], U_i_cmW_U_i_cmE[1]], mulScalarPoint);
{
uint256[{{num_limbs}}] memory cmW_x_limbs = LimbsDecomposition.decompose(cmW[0]);
uint256[{{num_limbs}}] memory cmW_y_limbs = LimbsDecomposition.decompose(cmW[1]);
for (uint8 k = 0; k < {{num_limbs}}; k++) {
public_inputs[{{ z_len * 2 + 4 + num_limbs * 2 }} + k] = cmW_x_limbs[k];
public_inputs[{{ z_len * 2 + 4 + num_limbs * 3 }} + k] = cmW_y_limbs[k];
}
}
require(this.check(cmW, kzg_proof[0], challenge_W_challenge_E_kzg_evals[0], challenge_W_challenge_E_kzg_evals[2]), "KZG: verifying proof for challenge W failed");
}
{
// add challenges
public_inputs[{{ z_len * 2 + 4 + num_limbs * 4 }}] = challenge_W_challenge_E_kzg_evals[0];
public_inputs[{{ z_len * 2 + 4 + num_limbs * 4 + 1 }}] = challenge_W_challenge_E_kzg_evals[1];
public_inputs[{{ z_len * 2 + 4 + num_limbs * 4 + 2 }}] = challenge_W_challenge_E_kzg_evals[2];
public_inputs[{{ z_len * 2 + 4 + num_limbs * 4 + 3 }}] = challenge_W_challenge_E_kzg_evals[3];
uint256[{{num_limbs}}] memory cmT_x_limbs;
uint256[{{num_limbs}}] memory cmT_y_limbs;
cmT_x_limbs = LimbsDecomposition.decompose(u_i_x_cmT[2]);
cmT_y_limbs = LimbsDecomposition.decompose(u_i_x_cmT[3]);
for (uint8 k = 0; k < {{num_limbs}}; k++) {
public_inputs[{{ z_len * 2 + 4 + num_limbs * 4 }} + 4 + k] = cmT_x_limbs[k];
public_inputs[{{ z_len * 2 + 4 + num_limbs * 5}} + 4 + k] = cmT_y_limbs[k];
}
// for now, we do not relate the Groth16 and KZG10 proofs
bool success_g16 = super.verifyProof(_pA, _pB, _pC, _pubSignals);
require(success_g16 == true, "G16 Failed");
// last element of the groth16 proof's public inputs is `r`
public_inputs[{{ public_inputs_len - 2 }}] = U_i_u_u_i_u_r[2];
bool success_g16 = this.verifyProof(pA, pB, pC, public_inputs);
require(success_g16 == true, "Groth16: verifying proof failed");
}
return(true);
}
}
}
}

Loading…
Cancel
Save