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

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

View File

@@ -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<str>, 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<Vec<u8>> {
}
/// 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),

View File

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

View File

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

View File

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

View File

@@ -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<KzgData> 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<Bn254>, Option<Vec<G1Affine>>)> 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);
}
}

View File

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

View File

@@ -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_data: value.0,
kzg_data: value.1,
g16_vk: value.0,
kzg_vk: value.1,
z_len: value.2,
}
}
}
impl NovaCyclefoldData {
// 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_vk,
kzg_vk,
z_len: value.2,
}
}
}
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,
);
}
}

View File

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

View File

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

View File

@@ -1,24 +1,162 @@
/*
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 }}
{{ kzg10_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);
// 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");
{
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];
}
// 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);
}
}
}
}