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
This commit is contained in:
Carlos Pérez
2024-03-18 11:09:22 +01:00
committed by GitHub
parent a4905c8a06
commit 1072b66e92
28 changed files with 1014 additions and 461 deletions

View File

@@ -0,0 +1,169 @@
/*
Copyright 2021 0KIMS association.
* `solidity-verifiers` added comment
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)
*
snarkJS is a free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
snarkJS is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
License for more details.
You should have received a copy of the GNU General Public License
along with snarkJS. If not, see <https://www.gnu.org/licenses/>.
*/
contract Groth16Verifier {
// Scalar field size
uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
// Base field size
uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
// Verification Key data
uint256 constant alphax = {{ vkey_alpha_g1.0[0] }};
uint256 constant alphay = {{ vkey_alpha_g1.0[1] }};
uint256 constant betax1 = {{ vkey_beta_g2.0[0][1] }};
uint256 constant betax2 = {{ vkey_beta_g2.0[0][0] }};
uint256 constant betay1 = {{ vkey_beta_g2.0[1][1] }};
uint256 constant betay2 = {{ vkey_beta_g2.0[1][0] }};
uint256 constant gammax1 = {{ vkey_gamma_g2.0[0][1] }};
uint256 constant gammax2 = {{ vkey_gamma_g2.0[0][0] }};
uint256 constant gammay1 = {{ vkey_gamma_g2.0[1][1] }};
uint256 constant gammay2 = {{ vkey_gamma_g2.0[1][0] }};
uint256 constant deltax1 = {{ vkey_delta_g2.0[0][1] }};
uint256 constant deltax2 = {{ vkey_delta_g2.0[0][0] }};
uint256 constant deltay1 = {{ vkey_delta_g2.0[1][1] }};
uint256 constant deltay2 = {{ vkey_delta_g2.0[1][0] }};
{% for (i, point) in gamma_abc_g1.iter().enumerate() %}
uint256 constant IC{{i}}x = {{ point.0[0] }};
uint256 constant IC{{i}}y = {{ point.0[1] }};
{% endfor %}
// Memory data
uint16 constant pVk = 0;
uint16 constant pPairing = 128;
uint16 constant pLastMem = 896;
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)) {
mstore(0, 0)
return(0, 0x20)
}
}
// G1 function to multiply a G1 value(x,y) to value in an address
function g1_mulAccC(pR, x, y, s) {
let success
let mIn := mload(0x40)
mstore(mIn, x)
mstore(add(mIn, 32), y)
mstore(add(mIn, 64), s)
success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64)
if iszero(success) {
mstore(0, 0)
return(0, 0x20)
}
mstore(add(mIn, 64), mload(pR))
mstore(add(mIn, 96), mload(add(pR, 32)))
success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64)
if iszero(success) {
mstore(0, 0)
return(0, 0x20)
}
}
function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk {
let _pPairing := add(pMem, pPairing)
let _pVk := add(pMem, pVk)
mstore(_pVk, IC0x)
mstore(add(_pVk, 32), IC0y)
// Compute the linear combination vk_x
{% for (i, _) in gamma_abc_g1.iter().enumerate() %}
{% if loop.first -%}
{%- else -%}
g1_mulAccC(_pVk, IC{{i}}x, IC{{i}}y, calldataload(add(pubSignals, {{(i-1)*32}})))
{%- endif -%}
{% endfor %}
// -A
mstore(_pPairing, calldataload(pA))
mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q))
// B
mstore(add(_pPairing, 64), calldataload(pB))
mstore(add(_pPairing, 96), calldataload(add(pB, 32)))
mstore(add(_pPairing, 128), calldataload(add(pB, 64)))
mstore(add(_pPairing, 160), calldataload(add(pB, 96)))
// alpha1
mstore(add(_pPairing, 192), alphax)
mstore(add(_pPairing, 224), alphay)
// beta2
mstore(add(_pPairing, 256), betax1)
mstore(add(_pPairing, 288), betax2)
mstore(add(_pPairing, 320), betay1)
mstore(add(_pPairing, 352), betay2)
// vk_x
mstore(add(_pPairing, 384), mload(add(pMem, pVk)))
mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32))))
// gamma2
mstore(add(_pPairing, 448), gammax1)
mstore(add(_pPairing, 480), gammax2)
mstore(add(_pPairing, 512), gammay1)
mstore(add(_pPairing, 544), gammay2)
// C
mstore(add(_pPairing, 576), calldataload(pC))
mstore(add(_pPairing, 608), calldataload(add(pC, 32)))
// delta2
mstore(add(_pPairing, 640), deltax1)
mstore(add(_pPairing, 672), deltax2)
mstore(add(_pPairing, 704), deltay1)
mstore(add(_pPairing, 736), deltay2)
let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20)
isOk := and(success, mload(_pPairing))
}
let pMem := mload(0x40)
mstore(0x40, add(pMem, pLastMem))
// Validate that all evaluations ∈ F
{% for (i, _) in gamma_abc_g1.iter().enumerate() %}
checkField(calldataload(add(_pubSignals, {{i*32}})))
{% endfor %}
// Validate all evaluations
let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem)
mstore(0, isValid)
return(0, 0x20)
}
}
}

View File

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

View File

@@ -0,0 +1,271 @@
/**
* @author Privacy and Scaling Explorations team - pse.dev
* @dev Contains utility functions for ops in BN254; in G_1 mostly.
* @notice Forked from https://github.com/weijiekoh/libkzg/tree/master.
* Among others, a few of the changes we did on this fork were:
* - Templating the pragma version
* - Removing type wrappers and use uints instead
* - Performing changes on arg types
* - Update some of the `require` statements
* - Use the bn254 scalar field instead of checking for overflow on the babyjub prime
* - In batch checking, we compute auxiliary polynomials and their commitments at the same time.
*/
contract KZG10Verifier {
// prime of field F_p over which y^2 = x^3 + 3 is defined
uint256 public constant BN254_PRIME_FIELD =
21888242871839275222246405745257275088696311157297823662689037894645226208583;
uint256 public constant BN254_SCALAR_FIELD =
21888242871839275222246405745257275088548364400416034343698204186575808495617;
/**
* @notice Performs scalar multiplication in G_1.
* @param p G_1 point to multiply
* @param s Scalar to multiply by
* @return r G_1 point p multiplied by scalar s
*/
function mulScalar(uint256[2] memory p, uint256 s) internal view returns (uint256[2] memory r) {
uint256[3] memory input;
input[0] = p[0];
input[1] = p[1];
input[2] = s;
bool success;
assembly {
success := staticcall(sub(gas(), 2000), 7, input, 0x60, r, 0x40)
switch success
case 0 { invalid() }
}
require(success, "bn254: scalar mul failed");
}
/**
* @notice Negates a point in G_1.
* @param p G_1 point to negate
* @return uint256[2] G_1 point -p
*/
function negate(uint256[2] memory p) internal pure returns (uint256[2] memory) {
if (p[0] == 0 && p[1] == 0) {
return p;
}
return [p[0], BN254_PRIME_FIELD - (p[1] % BN254_PRIME_FIELD)];
}
/**
* @notice Adds two points in G_1.
* @param p1 G_1 point 1
* @param p2 G_1 point 2
* @return r G_1 point p1 + p2
*/
function add(uint256[2] memory p1, uint256[2] memory p2) internal view returns (uint256[2] memory r) {
bool success;
uint256[4] memory input = [p1[0], p1[1], p2[0], p2[1]];
assembly {
success := staticcall(sub(gas(), 2000), 6, input, 0x80, r, 0x40)
switch success
case 0 { invalid() }
}
require(success, "bn254: point add failed");
}
/**
* @notice Computes the pairing check e(p1, p2) * e(p3, p4) == 1
* @dev Note that G_2 points a*i + b are encoded as two elements of F_p, (a, b)
* @param a_1 G_1 point 1
* @param a_2 G_2 point 1
* @param b_1 G_1 point 2
* @param b_2 G_2 point 2
* @return result true if pairing check is successful
*/
function pairing(uint256[2] memory a_1, uint256[2][2] memory a_2, uint256[2] memory b_1, uint256[2][2] memory b_2)
internal
view
returns (bool result)
{
uint256[12] memory input = [
a_1[0],
a_1[1],
a_2[0][1], // imaginary part first
a_2[0][0],
a_2[1][1], // imaginary part first
a_2[1][0],
b_1[0],
b_1[1],
b_2[0][1], // imaginary part first
b_2[0][0],
b_2[1][1], // imaginary part first
b_2[1][0]
];
uint256[1] memory out;
bool success;
assembly {
success := staticcall(sub(gas(), 2000), 8, input, 0x180, out, 0x20)
switch success
case 0 { invalid() }
}
require(success, "bn254: pairing failed");
return out[0] == 1;
}
uint256[2] G_1 = [
{{ g1.0[0] }},
{{ g1.0[1] }}
];
uint256[2][2] G_2 = [
[
{{ g2.0[0][0] }},
{{ g2.0[0][1] }}
],
[
{{ g2.0[1][0] }},
{{ g2.0[1][1] }}
]
];
uint256[2][2] VK = [
[
{{ vk.0[0][0] }},
{{ vk.0[0][1] }}
],
[
{{ vk.0[1][0] }},
{{ vk.0[1][1] }}
]
];
uint256[2][{{ g1_crs_len }}] G1_CRS = [
{%- for (i, point) in g1_crs.iter().enumerate() %}
[
{{ point.0[0] }},
{{ point.0[1] }}
{% if loop.last -%}
]
{%- else -%}
],
{%- endif -%}
{% endfor -%}
];
/**
* @notice Verifies a single point evaluation proof. Function name follows `ark-poly`.
* @dev To avoid ops in G_2, we slightly tweak how the verification is done.
* @param c G_1 point commitment to polynomial.
* @param pi G_1 point proof.
* @param x Value to prove evaluation of polynomial at.
* @param y Evaluation poly(x).
* @return result Indicates if KZG proof is correct.
*/
function check(uint256[2] calldata c, uint256[2] calldata pi, uint256 x, uint256 y)
public
view
returns (bool result)
{
//
// we want to:
// 1. avoid gas intensive ops in G2
// 2. format the pairing check in line with what the evm opcode expects.
//
// we can do this by tweaking the KZG check to be:
//
// e(pi, vk - x * g2) = e(c - y * g1, g2) [initial check]
// e(pi, vk - x * g2) * e(c - y * g1, g2)^{-1} = 1
// e(pi, vk - x * g2) * e(-c + y * g1, g2) = 1 [bilinearity of pairing for all subsequent steps]
// e(pi, vk) * e(pi, -x * g2) * e(-c + y * g1, g2) = 1
// e(pi, vk) * e(-x * pi, g2) * e(-c + y * g1, g2) = 1
// e(pi, vk) * e(x * -pi - c + y * g1, g2) = 1 [done]
// |_ rhs_pairing _|
//
uint256[2] memory rhs_pairing =
add(mulScalar(negate(pi), x), add(negate(c), mulScalar(G_1, y)));
return pairing(pi, VK, rhs_pairing, G_2);
}
function evalPolyAt(uint256[] memory _coefficients, uint256 _index) public pure returns (uint256) {
uint256 m = BN254_SCALAR_FIELD;
uint256 result = 0;
uint256 powerOfX = 1;
for (uint256 i = 0; i < _coefficients.length; i++) {
uint256 coeff = _coefficients[i];
assembly {
result := addmod(result, mulmod(powerOfX, coeff, m), m)
powerOfX := mulmod(powerOfX, _index, m)
}
}
return result;
}
/**
* @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).
* @param l_coeffs coefficients of the lagrange polynomial l(x).
* @param x_vals x values to evaluate the polynomials at.
* @param y_vals y values to which l(x) should evaluate to.
* @return uint256[2] commitment to z(x).
* @return uint256[2] commitment to l(x).
*/
function checkAndCommitAuxPolys(
uint256[] memory z_coeffs,
uint256[] memory l_coeffs,
uint256[] memory x_vals,
uint256[] memory y_vals
) public view returns (uint256[2] memory, uint256[2] memory) {
// z(x) is of degree len(x_vals), it is a product of linear polynomials (x - x_i)
// l(x) is of degree len(x_vals) - 1
uint256[2] memory z_commit;
uint256[2] memory l_commit;
for (uint256 i = 0; i < x_vals.length; i++) {
z_commit = add(z_commit, mulScalar(G1_CRS[i], z_coeffs[i])); // update commitment to z(x)
l_commit = add(l_commit, mulScalar(G1_CRS[i], l_coeffs[i])); // update commitment to l(x)
uint256 eval_z = evalPolyAt(z_coeffs, x_vals[i]);
uint256 eval_l = evalPolyAt(l_coeffs, x_vals[i]);
require(eval_z == 0, "checkAndCommitAuxPolys: wrong zero poly");
require(eval_l == y_vals[i], "checkAndCommitAuxPolys: wrong lagrange poly");
}
// z(x) has len(x_vals) + 1 coeffs, we add to the commitment the last coeff of z(x)
z_commit = add(z_commit, mulScalar(G1_CRS[z_coeffs.length - 1], z_coeffs[z_coeffs.length - 1]));
return (z_commit, l_commit);
}
/**
* @notice Verifies a batch of point evaluation proofs. Function name follows `ark-poly`.
* @dev To avoid ops in G_2, we slightly tweak how the verification is done.
* @param c G1 point commitment to polynomial.
* @param pi G2 point proof.
* @param x_vals Values to prove evaluation of polynomial at.
* @param y_vals Evaluation poly(x).
* @param l_coeffs Coefficients of the lagrange polynomial.
* @param z_coeffs Coefficients of the zero polynomial z(x) = (x - x_1)(x - x_2)...(x - x_n).
* @return result Indicates if KZG proof is correct.
*/
function batchCheck(
uint256[2] calldata c,
uint256[2][2] calldata pi,
uint256[] calldata x_vals,
uint256[] calldata y_vals,
uint256[] calldata l_coeffs,
uint256[] calldata z_coeffs
) public view returns (bool result) {
//
// we want to:
// 1. avoid gas intensive ops in G2
// 2. format the pairing check in line with what the evm opcode expects.
//
// we can do this by tweaking the KZG check to be:
//
// e(z(r) * g1, pi) * e(g1, l(r) * g2) = e(c, g2) [initial check]
// e(z(r) * g1, pi) * e(l(r) * g1, g2) * e(c, g2)^{-1} = 1 [bilinearity of pairing]
// e(z(r) * g1, pi) * e(l(r) * g1 - c, g2) = 1 [done]
//
(uint256[2] memory z_commit, uint256[2] memory l_commit) =
checkAndCommitAuxPolys(z_coeffs, l_coeffs, x_vals, y_vals);
uint256[2] memory neg_commit = negate(c);
return pairing(z_commit, pi, add(l_commit, neg_commit), G_2);
}
}

View File

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