Browse Source

Add CLI interface for verifier contract generation (#74)

* add: solidity-verifier workspace member

* chore: Update toolchain to 1.74

* feat: Add basic clap cli interface for solidity verifier

This includes a cli parser that serves as a way to the user to generate the desired Solidity contracts.

* chore: Expose SoldityVerifier template struct

* feat: Finish first working version

* change: Modify some settings

* fix: Fix rebase conflicts

* chore: Leave resolver 2 for workspace

* chore: Rename KZG+G16 template

Now the template refers to Nova + Cyclefold and has a Warning attached to it

* fixup

* chore: Rename to NovaCyclefoldDecider the template

* chore: Change constructors to `new` instead of `from`

* add: ProtocolData trait helper

This trait helps to treat the serialized data required by the Template
as a single element while still allowing a flexible usage.

This is specially interesting as allows the cli to operate considering a
single path of input data where all the data for the selected protocol
co-exists. Reducing the amount of parsing and arguments the user needs
to pass to the cli.

* chore: Create `From` impls formally

Previously we had functions called `from` which had nothing to do with
the trait `From`. This addresses this issue and fixes it.

Now both `new` and `from` are avaliable. But `from` follows the `From`
trait.

* add: Support G16, KZG and Nova+Cyclefold in cli

This adds a `render` fn for `Protocol` which makes it easier to add new
protocols to the CLI as is mainly based in the `ProtocolData` impl
behind the scenes of the selected protocol.

Aside from that, this commit reworks some minor parts of the CLI config
as shorteners for commands or adding `pragma` as an optional parameter.

* chore: Adapt `main.rs` to new cli changes

As seen, this allows to have a much easier `main.rs` which doesn't have
to do any `match` over the selected protocol.

* chore: Make solidity helper fns `cfg(test)`

* chore: Rework folding-schemes-solidity structure

* chore: Remove g1_crs_batch_points_len from KZGData

* add: Serde tests for all template targets

* tmp: Add NovaCyclefold testing

* add: HeaderInclusion template

When we use templates that are composed by others (as happens with
`NovaCyclefold` one) we sadly see that the License and the `pragma`
attributes are rendered once per sub-template.

This generic structure solves this issue by being actually the only item
rendered which has a sub-template the template we indeed want to render
at the end.

* chore: Add tests for NovaCyclefold contract

This also includes small changes to the lib architecture such as adding
constants for GPL3_SDPX_IDENTIFIER or move the default pragma versions
used to `mod.rs`

* chore: Update g16 to use HeaderInclusion template rendering

Now the `ProtocolData` impl falls back to the usage of `HeaderInclusion`
it is easier to handle complex templates like `NovaCyclefold`.

* add: Small builder-pattern to construct HeaderInclusion Templates

As mentioned in previous commits, the idea is that the header is set on
an automatic wrapper template applied to the one that we actually want
to render.

This builder pattern makes it less complex to do such a thing. Specially
avoiding unidiomatic `From` implementations.

* remove: sdpx & pragma from KZG template

Those are externalized and handled by HeaderInclusion template utility

* chore: Update templates to use HeaderInclusion builder

* chore: Update tests to use HeaderInclusion builderPattern

* remove: fixed pragma version in novacyclefold template

* chore: Accept Into<Template> in builder

* tmp: Only KZG return passes. Fix Groth

* fix: Prevent `revert` from paniking for negative tests

* feat: Merge G16 and KZG contract results in NovaCyclefold

* chore: Add assets for quicker/easier testing

Now instead of generating the protocoldata & proofs on each test, we just deserialize

* fix: Address clippy & warnings

* fix: Spelling to prevent PR farmers LOL

* chore: Add about and long_about to CLI tool

* add: README.md

* chore: Revert  asset-based testing approach

* remove: Assets folder

* fix: Rebase issues

* fix: use &mut for Reader

* fix: rebase error with Contract name

* chore: Reduce tests LOC with setup fn

* chore: Set MIT license indentifier for CLI & KZG

* chore: Add extra usage example

* chore: Update novacyclefold contract comments on soundess

* chore: Typo

* chore: Allow type complexity clippy for setup fn

* chore: Address Pierre's comments

* chore: Rename workspace members

- folding-schemes-solidity -> soliity-verifiers
main
Carlos Pérez 6 months ago
committed by GitHub
parent
commit
1072b66e92
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
29 changed files with 1016 additions and 463 deletions
  1. +1
    -1
      .gitignore
  2. +3
    -2
      Cargo.toml
  3. +38
    -0
      cli/Cargo.toml
  4. +72
    -0
      cli/README.md
  5. +36
    -0
      cli/src/main.rs
  6. +111
    -0
      cli/src/settings.rs
  7. +0
    -3
      folding-schemes-solidity/README.md
  8. +0
    -5
      folding-schemes-solidity/src/lib.rs
  9. +0
    -1
      folding-schemes-solidity/src/utils/mod.rs
  10. +0
    -287
      folding-schemes-solidity/src/verifiers/mod.rs
  11. +0
    -113
      folding-schemes-solidity/src/verifiers/templates.rs
  12. +0
    -20
      folding-schemes-solidity/templates/kzg10_groth16_decider_verifier.askama.sol
  13. +1
    -0
      folding-schemes/src/commitment/kzg.rs
  14. +1
    -1
      rust-toolchain
  15. +1
    -1
      solidity-verifiers/Cargo.toml
  16. +3
    -0
      solidity-verifiers/README.md
  17. +0
    -0
      solidity-verifiers/askama.toml
  18. +6
    -21
      solidity-verifiers/src/evm.rs
  19. +7
    -0
      solidity-verifiers/src/lib.rs
  20. +0
    -0
      solidity-verifiers/src/utils/encoding.rs
  21. +66
    -0
      solidity-verifiers/src/utils/mod.rs
  22. +72
    -0
      solidity-verifiers/src/verifiers/g16.rs
  23. +72
    -0
      solidity-verifiers/src/verifiers/kzg.rs
  24. +428
    -0
      solidity-verifiers/src/verifiers/mod.rs
  25. +69
    -0
      solidity-verifiers/src/verifiers/nova_cyclefold.rs
  26. +1
    -4
      solidity-verifiers/templates/groth16_verifier.askama.sol
  27. +4
    -0
      solidity-verifiers/templates/header_template.askama.sol
  28. +0
    -4
      solidity-verifiers/templates/kzg10_verifier.askama.sol
  29. +24
    -0
      solidity-verifiers/templates/nova_cyclefold_decider.askama.sol

+ 1
- 1
.gitignore

@ -5,4 +5,4 @@ Cargo.lock
/src/frontend/circom/test_folder/test_circuit_js/ /src/frontend/circom/test_folder/test_circuit_js/
# generated contracts at test time # generated contracts at test time
folding-schemes-solidity/generated
solidity-verifiers/generated

+ 3
- 2
Cargo.toml

@ -1,9 +1,10 @@
[workspace] [workspace]
resolver = "1"
members = [ members = [
"folding-schemes", "folding-schemes",
"folding-schemes-solidity"
"solidity-verifiers",
"cli"
] ]
resolver = "2"
# The following patch is to use a version of ark-r1cs-std compatible with # The following patch is to use a version of ark-r1cs-std compatible with
# v0.4.0 but that includes a cherry-picked commit from after v0.4.0 which fixes # v0.4.0 but that includes a cherry-picked commit from after v0.4.0 which fixes

+ 38
- 0
cli/Cargo.toml

@ -0,0 +1,38 @@
[package]
name = "solidity-verifiers-cli"
version = "0.1.0"
edition = "2021"
[dependencies]
ark-ec = "0.4"
ark-ff = "0.4"
ark-poly = "0.4"
ark-std = "0.4"
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"
solidity-verifiers = { path = "../solidity-verifiers" }
itertools = "0.12.1"
ark-serialize = "0.4.1"
clap = { version = "4.4", features = ["derive", "string"] }
clap-verbosity-flag = "2.1"
log = "0.4"
env_logger = "0.10"
[dev-dependencies]
revm = "3.5.0"
tracing = { version = "0.1", default-features = false, features = [ "attributes" ] }
tracing-subscriber = { version = "0.2" }
[features]
default = ["parallel"]
parallel = [
"ark-std/parallel",
"ark-ff/parallel",
"ark-poly/parallel",
]

+ 72
- 0
cli/README.md

@ -0,0 +1,72 @@
# Solidity Verifier
_____ ______ ______ ______ ______ ______ ______
| |__| || |__| || |__| || |__| || |__| || |__| || |__| |
| () || () || () || () || () || () || () |
|______||______||______||______||______||______||______|
______ ______
| |__| | ____ _ _ _ _ _ | |__| |
| () | / ___| ___ | (_) __| (_) |_ _ _ | () |
|______| \___ \ / _ \| | |/ _` | | __| | | | |______|
______ ___) | (_) | | | (_| | | |_| |_| | ______
| |__| | |____/ \___/|_|_|\__,_|_|\__|\__, | | |__| |
| () | __ __ _ __ _ |___/ | () |
|______| \ \ / /__ _ __(_)/ _(_) ___ _ __ |______|
______ \ \ / / _ \ '__| | |_| |/ _ \ '__| ______
| |__| | \ V / __/ | | | _| | __/ | | |__| |
| () | \_/ \___|_| |_|_| |_|\___|_| | () |
|______| |______|
______ ______ ______ ______ ______ ______ ______
| |__| || |__| || |__| || |__| || |__| || |__| || |__| |
| () || () || () || () || () || () || () |
|______||______||______||______||______||______||______|
Welcome to Solidity Verifier, a powerful 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.
As an open-source project, Solidity Verifier is released under the GPL3 license.
## Supported Protocols
Solidity Verifier currently supports the generation of Solidity smart contracts for the verification of proofs in the following Zero Knowledge protocols:
- **Groth16:**
- Efficient and succinct zero-knowledge proof system.
- Template credit: [Jordi Baylina - Groth16 Verifier Template](https://github.com/iden3/snarkjs/blob/master/templates/verifier_groth16.sol.ejs)
- **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)
- **Nova + CycleFold Decider:**
- Implements the decider circuit verification for the Nova zero-knowledge proof system in conjunction with the CycleFold protocol optimization.
- Template inspiration and setup credit: [Han - revm/Solidity Contract Testing Functions](https://github.com/privacy-scaling-explorations/halo2-solidity-verifier/tree/main)
## Usage
```bash
solidity-verifier [OPTIONS] -p <PROTOCOL> -pd <PROTOCOL_DATA> -o <OUTPUT_PATH>
```
A real use case (which was used to test the tool itself):
`solidity-verifier -p groth16 -pd ./folding-verifier-solidity/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`.
### 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 (default: /home/kr0/Desktop/HDD/ethereum/folding-schemes/verifier.sol)
-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
--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
## License
Solidity Verifier is released under the GPL3 license for any of the protocols that include `Groth16`. See the LICENSE file in the project repository for more details.
For the rest of contracts/protocols and the CLI itself, this tooling is released under MIT/Apache license.
## Contributing
Feel free to explore, use, and contribute to Solidity Verifier as we strive to enhance privacy and scalability in the blockchain space!
We welcome contributions to Solidity Verifier! 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.

+ 36
- 0
cli/src/main.rs

@ -0,0 +1,36 @@
use ::clap::Parser;
use ark_serialize::Write;
use settings::Cli;
use std::path::Path;
use std::{fs, io};
mod settings;
fn create_or_open_then_write<T: AsRef<[u8]>>(path: &Path, content: &T) -> Result<(), io::Error> {
let mut file = fs::OpenOptions::new().create(true).write(true).open(path)?;
file.write_all(content.as_ref())
}
fn main() {
let cli = Cli::parse();
// generate a subscriber with the desired log level
env_logger::builder()
.format_timestamp_secs()
.filter_level(cli.verbosity.log_level_filter())
.init();
let out_path = cli.out;
// 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();
// 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(),
)
.unwrap();
}

+ 111
- 0
cli/src/settings.rs

@ -0,0 +1,111 @@
use ark_serialize::SerializationError;
use clap::{Parser, ValueEnum};
use solidity_verifiers::{Groth16Data, KzgData, NovaCyclefoldData, ProtocolData};
use std::{env, fmt::Display, path::PathBuf};
fn get_default_out_path() -> PathBuf {
let mut path = env::current_dir().unwrap();
path.push("verifier.sol");
path
}
#[derive(Debug, Copy, Clone, ValueEnum)]
pub(crate) enum Protocol {
Groth16,
Kzg,
NovaCyclefold,
}
impl Display for Protocol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
// Would be nice to link this to the `Template` or `ProtocolData` 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(
&self,
data: &[u8],
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))
}
}
}
}
const ABOUT: &str = "A powerful Command-Line Interface (CLI) tool designed to simplify the generation of Solidity smart contracts that verify proofs of Zero Knowledge cryptographic protocols.
";
const LONG_ABOUT: &str = "
_____ ______ ______ ______ ______ ______ ______
| |__| || |__| || |__| || |__| || |__| || |__| || |__| |
| () || () || () || () || () || () || () |
|______||______||______||______||______||______||______|
______ ______
| |__| | ____ _ _ _ _ _ | |__| |
| () | / ___| ___ | (_) __| (_) |_ _ _ | () |
|______| \\___ \\ / _ \\| | |/ _` | | __| | | | |______|
______ ___) | (_) | | | (_| | | |_| |_| | ______
| |__| | |____/ \\___/|_|_|\\__,_|_|\\__|\\__, | | |__| |
| () | __ __ _ __ _ |___/ | () |
|______| \\ \\ / /__ _ __(_)/ _(_) ___ _ __ |______|
______ \\ \\ / / _ \\ '__| | |_| |/ _ \\ '__| ______
| |__| | \\ V / __/ | | | _| | __/ | | |__| |
| () | \\_/ \\___|_| |_|_| |_|\\___|_| | () |
|______| |______|
______ ______ ______ ______ ______ ______ ______
| |__| || |__| || |__| || |__| || |__| || |__| || |__| |
| () || () || () || () || () || () || () |
|______||______||______||______||______||______||______|
Welcome to Solidity Verifier, a powerful Command-Line Interface (CLI) tool designed to simplify the generation of Solidity smart contracts that verify proofs of Zero Knowledge cryptographic protocols.
for Zero Knowledge protocols. This tool is developed by the collaborative efforts of the PSE (Privacy & Scaling Explorations) and 0XPARC teams.
As an open-source project, Solidity Verifier is released under the GPL3 license.
Solidity Verifier currently supports the generation of Solidity smart contracts for the verification of proofs in the following Zero Knowledge protocols:
Groth16:
Efficient and succinct zero-knowledge proof system.
KZG:
Uses the Kate-Zaverucha-Goldberg polynomial commitment scheme.
Nova + CycleFold Decider:
Implements the decider circuit verification for the Nova zero-knowledge 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(propagate_version = true)]
/// A tool to create Solidity Contracts which act as verifiers for the major Folding Schemes implemented
/// within the `folding-schemes` repo.
pub(crate) struct Cli {
#[command(flatten)]
pub verbosity: clap_verbosity_flag::Verbosity,
/// Selects the protocol for which we want to generate the Solidity Verifier contract.
#[arg(short = 'p', long, value_enum, rename_all = "lower")]
pub protocol: Protocol,
#[arg(short = 'o', long, default_value=get_default_out_path().into_os_string())]
/// 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,
/// Selects the Solidity compiler version to be set in the Solidity Verifier contract artifact.
#[arg(long, default_value=None)]
pub pragma: Option<String>,
}

+ 0
- 3
folding-schemes-solidity/README.md

@ -1,3 +0,0 @@
# `folding-schemes-solidity`
This crate implements templating logic to output verifier contracts for `folding-schemes`-generated decider proofs.

+ 0
- 5
folding-schemes-solidity/src/lib.rs

@ -1,5 +0,0 @@
pub use evm::*;
pub use verifiers::templates::*;
mod evm;
mod utils;
mod verifiers;

+ 0
- 1
folding-schemes-solidity/src/utils/mod.rs

@ -1 +0,0 @@
pub mod encoding;

+ 0
- 287
folding-schemes-solidity/src/verifiers/mod.rs

@ -1,287 +0,0 @@
pub mod templates;
#[cfg(test)]
mod tests {
use crate::evm::{compile_solidity, save_solidity, Evm};
use crate::verifiers::templates::{Groth16Verifier, KZG10Verifier};
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_groth16::Groth16;
use ark_poly_commit::kzg10::VerifierKey;
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::{KZGProver, KZGSetup, ProverKey},
CommitmentProver,
},
transcript::{
poseidon::{poseidon_test_config, PoseidonTranscript},
Transcript,
},
};
use itertools::chain;
use std::marker::PhantomData;
// 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];
// Pragma statements for verifiers
pub const PRAGMA_GROTH16_VERIFIER: &str = "pragma solidity >=0.7.0 <0.9.0;"; // from snarkjs, avoid changing
pub const PRAGMA_KZG10_VERIFIER: &str = "pragma solidity >=0.8.1 <=0.8.4;";
struct TestAddCircuit<F: PrimeField> {
_f: PhantomData<F>,
pub x: u8,
pub y: u8,
pub z: u8,
}
impl<F: PrimeField> ConstraintSynthesizer<F> for TestAddCircuit<F> {
fn generate_constraints(self, cs: ConstraintSystemRef<F>) -> Result<(), SynthesisError> {
let x = FpVar::<F>::new_witness(cs.clone(), || Ok(F::from(self.x)))?;
let y = FpVar::<F>::new_witness(cs.clone(), || Ok(F::from(self.y)))?;
let z = FpVar::<F>::new_input(cs.clone(), || Ok(F::from(self.z)))?;
let comp_z = x.clone() + y.clone();
comp_z.enforce_equal(&z)?;
Ok(())
}
}
#[test]
fn test_groth16_kzg10_decider_template_renders() {
let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64());
let (x, y, z) = (21, 21, 42);
let (_, vk) = {
let c = TestAddCircuit::<Fr> {
_f: PhantomData,
x,
y,
z,
};
Groth16::<Bn254>::setup(c, &mut rng).unwrap()
};
let groth16_template = Groth16Verifier::from(vk, None);
let (pk, vk): (ProverKey<G1>, VerifierKey<Bn254>) = KZGSetup::<Bn254>::setup(&mut rng, 5);
let kzg10_template = KZG10Verifier::from(&vk, &pk.powers_of_g[..5], None, None);
let decider_template = super::templates::Groth16KZG10DeciderVerifier {
groth16_verifier: groth16_template,
kzg10_verifier: kzg10_template,
};
save_solidity("decider.sol", &decider_template.render().unwrap());
}
#[test]
fn test_groth16_kzg10_decider_template_compiles() {
let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64());
let (x, y, z) = (21, 21, 42);
let (_, vk) = {
let c = TestAddCircuit::<Fr> {
_f: PhantomData,
x,
y,
z,
};
Groth16::<Bn254>::setup(c, &mut rng).unwrap()
};
// we dont specify any pragma values for both verifiers, the pragma from the decider takes over
let groth16_template = Groth16Verifier::from(vk, None);
let (pk, vk): (ProverKey<G1>, VerifierKey<Bn254>) = KZGSetup::<Bn254>::setup(&mut rng, 5);
let kzg10_template = KZG10Verifier::from(&vk, &pk.powers_of_g[..5], None, None);
let decider_template = super::templates::Groth16KZG10DeciderVerifier {
groth16_verifier: groth16_template,
kzg10_verifier: kzg10_template,
};
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_template_renders() {
let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64());
let (x, y, z) = (21, 21, 42);
let (_, vk) = {
let c = TestAddCircuit::<Fr> {
_f: PhantomData,
x,
y,
z,
};
Groth16::<Bn254>::setup(c, &mut rng).unwrap()
};
let template = Groth16Verifier::from(vk, Some(PRAGMA_GROTH16_VERIFIER.to_string()));
save_solidity("groth16_verifier.sol", &template.render().unwrap());
_ = template.render().unwrap();
}
#[test]
fn test_groth16_verifier_template_compiles() {
let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64());
let (x, y, z) = (21, 21, 42);
let (_, vk) = {
let c = TestAddCircuit::<Fr> {
_f: PhantomData,
x,
y,
z,
};
Groth16::<Bn254>::setup(c, &mut rng).unwrap()
};
let res = Groth16Verifier::from(vk, Some(PRAGMA_GROTH16_VERIFIER.to_string()))
.render()
.unwrap();
let groth16_verifier_bytecode = compile_solidity(res, "Verifier");
let mut evm = Evm::default();
_ = evm.create(groth16_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 (x, y, z) = (21, 21, 42);
let (pk, vk) = {
let c = TestAddCircuit::<Fr> {
_f: PhantomData,
x,
y,
z,
};
Groth16::<Bn254>::setup(c, &mut rng).unwrap()
};
let c = TestAddCircuit::<Fr> {
_f: PhantomData,
x,
y,
z,
};
let proof = Groth16::<Bn254>::prove(&pk, c, &mut rng).unwrap();
let res = Groth16Verifier::from(vk, Some(PRAGMA_GROTH16_VERIFIER.to_string()))
.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(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 test_kzg_verifier_template_renders() {
let rng = &mut test_rng();
let n = 10;
let (pk, vk): (ProverKey<G1>, VerifierKey<Bn254>) = KZGSetup::<Bn254>::setup(rng, n);
let template = KZG10Verifier::from(
&vk,
&pk.powers_of_g[..5],
Some(PRAGMA_KZG10_VERIFIER.to_string()),
None,
);
let res = template.render().unwrap();
assert!(res.contains(&vk.g.x.to_string()));
}
#[test]
fn test_kzg_verifier_compiles() {
let rng = &mut test_rng();
let n = 10;
let (pk, vk): (ProverKey<G1>, VerifierKey<Bn254>) = KZGSetup::<Bn254>::setup(rng, n);
let template = KZG10Verifier::from(
&vk,
&pk.powers_of_g[..5],
Some(PRAGMA_KZG10_VERIFIER.to_string()),
None,
);
let res = template.render().unwrap();
let kzg_verifier_bytecode = compile_solidity(res, "KZG10");
let mut evm = Evm::default();
_ = evm.create(kzg_verifier_bytecode);
}
#[test]
fn test_kzg_verifier_accepts_and_rejects_proofs() {
let rng = &mut test_rng();
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 n = 10;
let (pk, vk): (ProverKey<G1>, VerifierKey<Bn254>) = KZGSetup::<Bn254>::setup(rng, n);
let v: Vec<Fr> = std::iter::repeat_with(|| Fr::rand(rng)).take(n).collect();
let cm = KZGProver::<G1>::commit(&pk, &v, &Fr::zero()).unwrap();
let (eval, proof) =
KZGProver::<G1>::prove(&pk, transcript_p, &cm, &v, &Fr::zero(), None).unwrap();
let template = KZG10Verifier::from(
&vk,
&pk.powers_of_g[..5],
Some(PRAGMA_KZG10_VERIFIER.to_string()),
None,
);
let res = template.render().unwrap();
let kzg_verifier_bytecode = compile_solidity(res, "KZG10");
let mut evm = Evm::default();
let verifier_address = evm.create(kzg_verifier_bytecode);
let (cm_affine, proof_affine) = (cm.into_affine(), proof.into_affine());
let (x_comm, y_comm) = cm_affine.xy().unwrap();
let (x_proof, y_proof) = proof_affine.xy().unwrap();
let y = 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);
}
}

+ 0
- 113
folding-schemes-solidity/src/verifiers/templates.rs

@ -1,113 +0,0 @@
use std::ops::Deref;
use crate::utils::encoding::{g1_to_fq_repr, g2_to_fq_repr};
/// Solidity templates for the verifier contracts.
/// We use askama for templating and define which variables are required for each template.
use crate::utils::encoding::{G1Repr, G2Repr};
use ark_bn254::{Bn254, G1Affine};
use ark_groth16::VerifyingKey;
use ark_poly_commit::kzg10::VerifierKey;
use askama::Template;
#[derive(Template, Default)]
#[template(path = "groth16_verifier.askama.sol", ext = "sol")]
pub struct Groth16Verifier {
/// SPDX-License-Identifier
pub sdpx: String,
/// The `pragma` statement.
pub pragma_version: String,
/// The `alpha * G`, where `G` is the generator of `G1`.
pub vkey_alpha_g1: G1Repr,
/// The `alpha * H`, where `H` is the generator of `G2`.
pub vkey_beta_g2: G2Repr,
/// The `gamma * H`, where `H` is the generator of `G2`.
pub vkey_gamma_g2: G2Repr,
/// The `delta * H`, where `H` is the generator of `G2`.
pub vkey_delta_g2: G2Repr,
/// Length of the `gamma_abc_g1` vector.
pub gamma_abc_len: usize,
/// The `gamma^{-1} * (beta * a_i + alpha * b_i + c_i) * H`, where `H` is the generator of `E::G1`.
pub gamma_abc_g1: Vec<G1Repr>,
}
impl Groth16Verifier {
pub fn from(value: VerifyingKey<Bn254>, pragma: Option<String>) -> Self {
let pragma_version = pragma.unwrap_or_default();
let sdpx = "// SPDX-License-Identifier: GPL-3.0".to_string();
Self {
pragma_version,
sdpx,
vkey_alpha_g1: g1_to_fq_repr(value.alpha_g1),
vkey_beta_g2: g2_to_fq_repr(value.beta_g2),
vkey_gamma_g2: g2_to_fq_repr(value.gamma_g2),
vkey_delta_g2: g2_to_fq_repr(value.delta_g2),
gamma_abc_len: value.gamma_abc_g1.len(),
gamma_abc_g1: value
.gamma_abc_g1
.iter()
.copied()
.map(g1_to_fq_repr)
.collect(),
}
}
}
#[derive(Template, Default)]
#[template(path = "kzg10_verifier.askama.sol", ext = "sol")]
pub struct KZG10Verifier {
/// SPDX-License-Identifier
pub sdpx: String,
/// The `pragma` statement.
pub pragma_version: String,
/// The generator of `G1`.
pub g1: G1Repr,
/// The generator of `G2`.
pub g2: G2Repr,
/// The verification key
pub vk: G2Repr,
/// Length of the trusted setup vector.
pub g1_crs_len: usize,
/// The trusted setup vector.
pub g1_crs: Vec<G1Repr>,
}
impl KZG10Verifier {
pub fn from(
vk: &VerifierKey<Bn254>,
crs: &[G1Affine],
pragma: Option<String>,
sdpx: Option<String>,
) -> KZG10Verifier {
let g1_string_repr = g1_to_fq_repr(vk.g);
let g2_string_repr = g2_to_fq_repr(vk.h);
let vk_string_repr = g2_to_fq_repr(vk.beta_h);
let g1_crs_len = crs.len();
let g1_crs = crs.iter().map(|g1| g1_to_fq_repr(*g1)).collect();
let sdpx = sdpx.unwrap_or_default();
let pragma_version = pragma.unwrap_or_default();
KZG10Verifier {
sdpx,
pragma_version,
g1: g1_string_repr,
g2: g2_string_repr,
vk: vk_string_repr,
g1_crs,
g1_crs_len,
}
}
}
#[derive(Template)]
#[template(path = "kzg10_groth16_decider_verifier.askama.sol", ext = "sol")]
pub struct Groth16KZG10DeciderVerifier {
pub groth16_verifier: Groth16Verifier,
pub kzg10_verifier: KZG10Verifier,
}
impl Deref for Groth16KZG10DeciderVerifier {
type Target = Groth16Verifier;
fn deref(&self) -> &Self::Target {
&self.groth16_verifier
}
}

+ 0
- 20
folding-schemes-solidity/templates/kzg10_groth16_decider_verifier.askama.sol

@ -1,20 +0,0 @@
pragma solidity ^0.8.4;
{{ groth16_verifier }}
{{ kzg10_verifier }}
/**
* @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.
*/
contract NovaDecider is Groth16Verifier, KZG10Verifier {
function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[{{ gamma_abc_len - 1 }}] calldata _pubSignals, uint256[2] calldata c, uint256[2] calldata pi, uint256 x, uint256 y) public view returns (bool) {
require(super.verifyProof(_pA, _pB, _pC, _pubSignals) == true, "Groth16 verification failed");
// for now, we do not relate the Groth16 and KZG10 proofs
// this will done in the future, by computing challenge points from the groth16 proof's data
require(super.check(c, pi, x, y) == true, "KZG10 verification failed");
return true;
}
}

+ 1
- 0
folding-schemes/src/commitment/kzg.rs

@ -35,6 +35,7 @@ pub struct ProverKey<'a, C: CurveGroup> {
pub powers_of_g: Cow<'a, [C::Affine]>, pub powers_of_g: Cow<'a, [C::Affine]>,
} }
#[derive(Debug)]
pub struct KZGSetup<P: Pairing> { pub struct KZGSetup<P: Pairing> {
_p: PhantomData<P>, _p: PhantomData<P>,
} }

+ 1
- 1
rust-toolchain

@ -1 +1 @@
1.75.0
1.74.0

folding-schemes-solidity/Cargo.toml → solidity-verifiers/Cargo.toml

@ -1,5 +1,5 @@
[package] [package]
name = "folding-schemes-solidity"
name = "solidity-verifiers"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"

+ 3
- 0
solidity-verifiers/README.md

@ -0,0 +1,3 @@
# `solidity-verifiers`
This crate implements templating logic to output verifier contracts for `folding-schemes`-generated decider proofs.

folding-schemes-solidity/askama.toml → solidity-verifiers/askama.toml


folding-schemes-solidity/src/evm.rs → solidity-verifiers/src/evm.rs

@ -12,7 +12,7 @@ use std::{
}; };
// from: https://github.com/privacy-scaling-explorations/halo2-solidity-verifier/blob/85cb77b171ce3ee493628007c7a1cfae2ea878e6/examples/separately.rs#L56 // from: https://github.com/privacy-scaling-explorations/halo2-solidity-verifier/blob/85cb77b171ce3ee493628007c7a1cfae2ea878e6/examples/separately.rs#L56
pub fn save_solidity(name: impl AsRef<str>, solidity: &str) {
pub(crate) fn save_solidity(name: impl AsRef<str>, solidity: &str) {
const DIR_GENERATED: &str = "./generated"; const DIR_GENERATED: &str = "./generated";
create_dir_all(DIR_GENERATED).unwrap(); create_dir_all(DIR_GENERATED).unwrap();
File::create(format!("{DIR_GENERATED}/{}", name.as_ref())) File::create(format!("{DIR_GENERATED}/{}", name.as_ref()))
@ -25,7 +25,7 @@ pub fn save_solidity(name: impl AsRef, solidity: &str) {
/// ///
/// # Panics /// # Panics
/// Panics if executable `solc` can not be found, or compilation fails. /// Panics if executable `solc` can not be found, or compilation fails.
pub fn compile_solidity(solidity: impl AsRef<[u8]>, contract_name: &str) -> Vec<u8> {
pub(crate) fn compile_solidity(solidity: impl AsRef<[u8]>, contract_name: &str) -> Vec<u8> {
let mut process = match Command::new("solc") let mut process = match Command::new("solc")
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
@ -72,7 +72,7 @@ fn find_binary(stdout: &str, contract_name: &str) -> Option> {
} }
/// Evm runner. /// Evm runner.
pub struct Evm {
pub(crate) struct Evm {
evm: EVM<InMemoryDB>, evm: EVM<InMemoryDB>,
} }
@ -98,25 +98,12 @@ impl Default for Evm {
} }
impl Evm { impl Evm {
/// Return code_size of given address.
///
/// # Panics
/// Panics if given address doesn't have bytecode.
pub fn code_size(&mut self, address: Address) -> usize {
self.evm.db.as_ref().unwrap().accounts[&address]
.info
.code
.as_ref()
.unwrap()
.len()
}
/// Apply create transaction with given `bytecode` as creation bytecode. /// Apply create transaction with given `bytecode` as creation bytecode.
/// Return created `address`. /// Return created `address`.
/// ///
/// # Panics /// # Panics
/// Panics if execution reverts or halts unexpectedly. /// Panics if execution reverts or halts unexpectedly.
pub fn create(&mut self, bytecode: Vec<u8>) -> Address {
pub(crate) fn create(&mut self, bytecode: Vec<u8>) -> Address {
let (_, output) = self.transact_success_or_panic(TxEnv { let (_, output) = self.transact_success_or_panic(TxEnv {
gas_limit: u64::MAX, gas_limit: u64::MAX,
transact_to: TransactTo::Create(CreateScheme::Create), transact_to: TransactTo::Create(CreateScheme::Create),
@ -134,7 +121,7 @@ impl Evm {
/// ///
/// # Panics /// # Panics
/// Panics if execution reverts or halts unexpectedly. /// Panics if execution reverts or halts unexpectedly.
pub fn call(&mut self, address: Address, calldata: Vec<u8>) -> (u64, Vec<u8>) {
pub(crate) fn call(&mut self, address: Address, calldata: Vec<u8>) -> (u64, Vec<u8>) {
let (gas_used, output) = self.transact_success_or_panic(TxEnv { let (gas_used, output) = self.transact_success_or_panic(TxEnv {
gas_limit: u64::MAX, gas_limit: u64::MAX,
transact_to: TransactTo::Call(address), transact_to: TransactTo::Call(address),
@ -170,9 +157,7 @@ impl Evm {
} }
(gas_used, output) (gas_used, output)
} }
ExecutionResult::Revert { gas_used, output } => {
panic!("Transaction reverts with gas_used {gas_used} and output {output:#x}")
}
ExecutionResult::Revert { gas_used, output } => (gas_used, Output::Call(output)),
ExecutionResult::Halt { reason, gas_used } => panic!( ExecutionResult::Halt { reason, gas_used } => panic!(
"Transaction halts unexpectedly with gas_used {gas_used} and reason {reason:?}" "Transaction halts unexpectedly with gas_used {gas_used} and reason {reason:?}"
), ),

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

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

folding-schemes-solidity/src/utils/encoding.rs → solidity-verifiers/src/utils/encoding.rs


+ 66
- 0
solidity-verifiers/src/utils/mod.rs

@ -0,0 +1,66 @@
use crate::{GPL3_SDPX_IDENTIFIER, PRAGMA_GROTH16_VERIFIER};
use askama::Template;
pub mod encoding;
#[derive(Template)]
#[template(path = "header_template.askama.sol", ext = "sol")]
pub(crate) struct HeaderInclusion<T: Template> {
/// SPDX-License-Identifier
pub sdpx: String,
/// The `pragma` statement.
pub pragma_version: String,
/// The template to render alongside the header.
pub template: T,
}
impl<T: Template + Default> HeaderInclusion<T> {
pub fn builder() -> HeaderInclusionBuilder<T> {
HeaderInclusionBuilder::default()
}
}
#[derive(Debug)]
pub struct HeaderInclusionBuilder<T: Template + Default> {
/// SPDX-License-Identifier
sdpx: String,
/// The `pragma` statement.
pragma_version: String,
/// The template to render alongside the header.
template: T,
}
impl<T: Template + Default> Default for HeaderInclusionBuilder<T> {
fn default() -> Self {
Self {
sdpx: GPL3_SDPX_IDENTIFIER.to_string(),
pragma_version: PRAGMA_GROTH16_VERIFIER.to_string(),
template: T::default(),
}
}
}
impl<T: Template + Default> HeaderInclusionBuilder<T> {
pub fn sdpx<S: Into<String>>(mut self, sdpx: S) -> Self {
self.sdpx = sdpx.into();
self
}
pub fn pragma_version<S: Into<String>>(mut self, pragma_version: S) -> Self {
self.pragma_version = pragma_version.into();
self
}
pub fn template(mut self, template: impl Into<T>) -> Self {
self.template = template.into();
self
}
pub fn build(self) -> HeaderInclusion<T> {
HeaderInclusion {
sdpx: self.sdpx,
pragma_version: self.pragma_version,
template: self.template,
}
}
}

+ 72
- 0
solidity-verifiers/src/verifiers/g16.rs

@ -0,0 +1,72 @@
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 ark_bn254::Bn254;
use ark_groth16::VerifyingKey;
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
use askama::Template;
use super::PRAGMA_GROTH16_VERIFIER;
#[derive(Template, Default)]
#[template(path = "groth16_verifier.askama.sol", ext = "sol")]
pub(crate) struct Groth16Verifier {
/// The `alpha * G`, where `G` is the generator of `G1`.
pub(crate) vkey_alpha_g1: G1Repr,
/// The `alpha * H`, where `H` is the generator of `G2`.
pub(crate) vkey_beta_g2: G2Repr,
/// The `gamma * H`, where `H` is the generator of `G2`.
pub(crate) vkey_gamma_g2: G2Repr,
/// The `delta * H`, where `H` is the generator of `G2`.
pub(crate) vkey_delta_g2: G2Repr,
/// Length of the `gamma_abc_g1` vector.
pub(crate) gamma_abc_len: usize,
/// The `gamma^{-1} * (beta * a_i + alpha * b_i + c_i) * H`, where `H` is the generator of `E::G1`.
pub(crate) gamma_abc_g1: Vec<G1Repr>,
}
impl From<Groth16Data> for Groth16Verifier {
fn from(g16_data: Groth16Data) -> 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
.0
.gamma_abc_g1
.iter()
.copied()
.map(g1_to_fq_repr)
.collect(),
}
}
}
// 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.
#[derive(CanonicalDeserialize, CanonicalSerialize, Clone, PartialEq, Debug)]
pub struct Groth16Data(pub(crate) VerifyingKey<Bn254>);
impl From<VerifyingKey<Bn254>> for Groth16Data {
fn from(value: VerifyingKey<Bn254>) -> Self {
Self(value)
}
}
impl ProtocolData for Groth16Data {
const PROTOCOL_NAME: &'static str = "Groth16";
fn render_as_template(self, pragma: Option<String>) -> Vec<u8> {
HeaderInclusion::<Groth16Verifier>::builder()
.sdpx(GPL3_SDPX_IDENTIFIER.to_string())
.pragma_version(pragma.unwrap_or(PRAGMA_GROTH16_VERIFIER.to_string()))
.template(self)
.build()
.render()
.unwrap()
.into_bytes()
}
}

+ 72
- 0
solidity-verifiers/src/verifiers/kzg.rs

@ -0,0 +1,72 @@
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 ark_bn254::{Bn254, G1Affine};
use ark_poly_commit::kzg10::VerifierKey;
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
use askama::Template;
use super::PRAGMA_KZG10_VERIFIER;
#[derive(Template, Default)]
#[template(path = "kzg10_verifier.askama.sol", ext = "sol")]
pub(crate) struct KZG10Verifier {
/// The generator of `G1`.
pub(crate) g1: G1Repr,
/// The generator of `G2`.
pub(crate) g2: G2Repr,
/// The verification key
pub(crate) vk: G2Repr,
/// Length of the trusted setup vector.
pub(crate) g1_crs_len: usize,
/// The trusted setup vector.
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();
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
.iter()
.map(|g1| g1_to_fq_repr(*g1))
.collect(),
}
}
}
#[derive(CanonicalDeserialize, CanonicalSerialize, Clone, PartialEq, Debug)]
pub struct KzgData {
pub(crate) vk: VerifierKey<Bn254>,
pub(crate) g1_crs_batch_points: Option<Vec<G1Affine>>,
}
impl From<(VerifierKey<Bn254>, Option<Vec<G1Affine>>)> for KzgData {
fn from(value: (VerifierKey<Bn254>, Option<Vec<G1Affine>>)) -> Self {
Self {
vk: value.0,
g1_crs_batch_points: value.1,
}
}
}
impl ProtocolData for KzgData {
const PROTOCOL_NAME: &'static str = "KZG";
fn render_as_template(self, pragma: Option<String>) -> Vec<u8> {
HeaderInclusion::<KZG10Verifier>::builder()
.sdpx(MIT_SDPX_IDENTIFIER.to_string())
.pragma_version(pragma.unwrap_or(PRAGMA_KZG10_VERIFIER.to_string()))
.template(self)
.build()
.render()
.unwrap()
.into_bytes()
}
}

+ 428
- 0
solidity-verifiers/src/verifiers/mod.rs

@ -0,0 +1,428 @@
//! Solidity templates for the verifier contracts.
//! We use askama for templating and define which variables are required for each template.
// Pragma statements for verifiers
pub(crate) const PRAGMA_GROTH16_VERIFIER: &str = "pragma solidity >=0.7.0 <0.9.0;"; // from snarkjs, avoid changing
pub(crate) const PRAGMA_KZG10_VERIFIER: &str = "pragma solidity >=0.8.1 <=0.8.4;";
/// Default SDPX License identifier
pub(crate) const GPL3_SDPX_IDENTIFIER: &str = "// SPDX-License-Identifier: GPL-3.0";
pub(crate) const MIT_SDPX_IDENTIFIER: &str = "// SPDX-License-Identifier: MIT";
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Read, SerializationError, Write};
mod g16;
mod kzg;
mod nova_cyclefold;
pub use g16::Groth16Data;
pub use kzg::KzgData;
pub use nova_cyclefold::NovaCyclefoldData;
pub trait ProtocolData: CanonicalDeserialize + CanonicalSerialize {
const PROTOCOL_NAME: &'static str;
fn serialize_name<W: Write>(&self, writer: &mut W) -> Result<(), SerializationError> {
Self::PROTOCOL_NAME
.to_string()
.serialize_uncompressed(writer)
}
fn serialize_protocol_data<W: Write>(&self, writer: &mut W) -> Result<(), SerializationError> {
self.serialize_name(writer)?;
self.serialize_compressed(writer)
}
fn deserialize_protocol_data<R: Read + Copy>(
mut reader: R,
) -> Result<Self, SerializationError> {
let name: String = String::deserialize_uncompressed(&mut reader)?;
let data = Self::deserialize_compressed(&mut reader)?;
if name != Self::PROTOCOL_NAME {
return Err(SerializationError::InvalidData);
}
Ok(data)
}
fn render_as_template(self, pragma: Option<String>) -> Vec<u8>;
}
#[cfg(test)]
mod tests {
use crate::evm::{compile_solidity, save_solidity, Evm};
use crate::utils::HeaderInclusion;
use crate::{Groth16Data, KzgData, NovaCyclefoldData, ProtocolData};
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_groth16::Groth16;
use ark_poly_commit::kzg10::VerifierKey;
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::{KZGProver, KZGSetup, ProverKey},
CommitmentProver,
},
transcript::{
poseidon::{poseidon_test_config, PoseidonTranscript},
Transcript,
},
};
use itertools::chain;
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];
/// Default setup length for testing.
const DEFAULT_SETUP_LEN: usize = 5;
#[derive(Debug, Clone, Copy)]
struct TestAddCircuit<F: PrimeField> {
_f: PhantomData<F>,
pub x: u8,
pub y: u8,
pub z: u8,
}
impl<F: PrimeField> ConstraintSynthesizer<F> for TestAddCircuit<F> {
fn generate_constraints(self, cs: ConstraintSystemRef<F>) -> Result<(), SynthesisError> {
let x = FpVar::<F>::new_witness(cs.clone(), || Ok(F::from(self.x)))?;
let y = FpVar::<F>::new_witness(cs.clone(), || Ok(F::from(self.y)))?;
let z = FpVar::<F>::new_input(cs.clone(), || Ok(F::from(self.z)))?;
let comp_z = x.clone() + y.clone();
comp_z.enforce_equal(&z)?;
Ok(())
}
}
#[allow(clippy::type_complexity)]
fn setup<'a>(
n: usize,
) -> (
ProverKey<'a, G1>,
VerifierKey<Bn254>,
ark_groth16::ProvingKey<Bn254>,
ark_groth16::VerifyingKey<Bn254>,
TestAddCircuit<Fr>,
) {
let mut rng = ark_std::rand::rngs::StdRng::seed_from_u64(test_rng().next_u64());
let (x, y, z) = (21, 21, 42);
let circuit = TestAddCircuit::<Fr> {
_f: PhantomData,
x,
y,
z,
};
let (g16_pk, g16_vk) = Groth16::<Bn254>::setup(circuit, &mut rng).unwrap();
let (kzg_pk, kzg_vk): (ProverKey<G1>, VerifierKey<Bn254>) =
KZGSetup::<Bn254>::setup(&mut rng, n);
(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 = KZGProver::<G1>::commit(&kzg_pk, &v, &Fr::zero()).unwrap();
let (eval, proof) =
KZGProver::<G1>::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.into_affine());
let (x_comm, y_comm) = cm_affine.xy().unwrap();
let (x_proof, y_proof) = proof_affine.xy().unwrap();
let y = 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 = KZGProver::<G1>::commit(&kzg_pk, &v, &Fr::zero()).unwrap();
let (eval, proof) =
KZGProver::<G1>::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.into_affine());
let (x_comm, y_comm) = cm_affine.xy().unwrap();
let (x_proof, y_proof) = proof_affine.xy().unwrap();
let y = 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);
}
}

+ 69
- 0
solidity-verifiers/src/verifiers/nova_cyclefold.rs

@ -0,0 +1,69 @@
use crate::utils::HeaderInclusion;
use crate::{Groth16Data, KzgData, ProtocolData, PRAGMA_GROTH16_VERIFIER};
use ark_bn254::{Bn254, G1Affine};
use ark_groth16::VerifyingKey;
use ark_poly_commit::kzg10::VerifierKey;
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
use askama::Template;
use super::g16::Groth16Verifier;
use super::kzg::KZG10Verifier;
#[derive(Template, Default)]
#[template(path = "nova_cyclefold_decider.askama.sol", ext = "sol")]
pub(crate) struct NovaCyclefoldDecider {
groth16_verifier: Groth16Verifier,
kzg10_verifier: KZG10Verifier,
}
impl From<NovaCyclefoldData> for NovaCyclefoldDecider {
fn from(value: NovaCyclefoldData) -> Self {
Self {
groth16_verifier: Groth16Verifier::from(value.g16_data),
kzg10_verifier: KZG10Verifier::from(value.kzg_data),
}
}
}
#[derive(CanonicalDeserialize, CanonicalSerialize, PartialEq, Debug)]
pub struct NovaCyclefoldData {
g16_data: Groth16Data,
kzg_data: KzgData,
}
impl ProtocolData for NovaCyclefoldData {
const PROTOCOL_NAME: &'static str = "NovaCyclefold";
fn render_as_template(self, pragma: Option<String>) -> Vec<u8> {
HeaderInclusion::<NovaCyclefoldDecider>::builder()
.pragma_version(pragma.unwrap_or(PRAGMA_GROTH16_VERIFIER.to_string()))
.template(self)
.build()
.render()
.unwrap()
.into_bytes()
}
}
impl From<(Groth16Data, KzgData)> for NovaCyclefoldData {
fn from(value: (Groth16Data, KzgData)) -> Self {
Self {
g16_data: value.0,
kzg_data: value.1,
}
}
}
impl NovaCyclefoldData {
pub fn new(
vkey_g16: VerifyingKey<Bn254>,
vkey_kzg: VerifierKey<Bn254>,
crs_points: Vec<G1Affine>,
) -> Self {
Self {
g16_data: Groth16Data::from(vkey_g16),
// TODO: Remove option from crs points
kzg_data: KzgData::from((vkey_kzg, Some(crs_points))),
}
}
}

folding-schemes-solidity/templates/groth16_verifier.askama.sol → solidity-verifiers/templates/groth16_verifier.askama.sol

@ -1,8 +1,7 @@
{{ sdpx }}
/* /*
Copyright 2021 0KIMS association. Copyright 2021 0KIMS association.
* `folding-schemes-solidity` added comment
* `solidity-verifiers` added comment
This file is a template built out of [snarkJS](https://github.com/iden3/snarkjs) groth16 verifier. This file is a template built out of [snarkJS](https://github.com/iden3/snarkjs) groth16 verifier.
See the original ejs template [here](https://github.com/iden3/snarkjs/blob/master/templates/verifier_groth16.sol.ejs) See the original ejs template [here](https://github.com/iden3/snarkjs/blob/master/templates/verifier_groth16.sol.ejs)
* *
@ -21,8 +20,6 @@
along with snarkJS. If not, see <https://www.gnu.org/licenses/>. along with snarkJS. If not, see <https://www.gnu.org/licenses/>.
*/ */
{{ pragma_version }}
contract Groth16Verifier { contract Groth16Verifier {
// Scalar field size // Scalar field size
uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617;

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

@ -0,0 +1,4 @@
{{ sdpx }}
{{ pragma_version }}
{{template}}

folding-schemes-solidity/templates/kzg10_verifier.askama.sol → solidity-verifiers/templates/kzg10_verifier.askama.sol

@ -1,7 +1,3 @@
{{ sdpx }}
{{ pragma_version }}
/** /**
* @author Privacy and Scaling Explorations team - pse.dev * @author Privacy and Scaling Explorations team - pse.dev
* @dev Contains utility functions for ops in BN254; in G_1 mostly. * @dev Contains utility functions for ops in BN254; in G_1 mostly.

+ 24
- 0
solidity-verifiers/templates/nova_cyclefold_decider.askama.sol

@ -0,0 +1,24 @@
{{ groth16_verifier }}
{{ kzg10_verifier }}
/**
* @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.
*/
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");
// 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");
return(true);
}
}

Loading…
Cancel
Save