Skip to content

Commit

Permalink
Implement SolidityGenerator (#2)
Browse files Browse the repository at this point in the history
* feat: implement `SolidityGenerator` to generate vk and verifier separately

* refactor: rename `generate` into `render`

* feat: implement `render` with test

* refactor: add `ConstraintSystemMeta` and `Data`

* refactor: better `Evaluator`

* refactor: fmt-able

* refactor: better `pcs`

* refactor: generally

* refactor: more `for_loop`

* refactor: docs

* refactor: cheaper than `snark-verifier`

* feat: add ci

* refactor: typo

* refactor: less generic

* refactor: `Word`

* feat: allow more instances and add document with example

* refactor: avoid patching `halo2_proofs`

* refactor: add more comments and reduce `encode_calldata` verbosity

* ci: install `svm-rs` with its `Cargo.lock`

* feat: handle `solc` related error in `compile_solidity` more properly

* docs: add warning on `README.md`
  • Loading branch information
han0110 authored Sep 27, 2023
1 parent a13c798 commit ab4e36b
Show file tree
Hide file tree
Showing 17 changed files with 3,995 additions and 2 deletions.
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal

- uses: Swatinem/rust-cache@v1
with:
cache-on-failure: true

- name: Install solc
run: (hash svm 2>/dev/null || cargo install --locked --git https://github.com/alloy-rs/svm-rs) && svm install 0.8.21 && solc --version

- name: Run test
run: cargo test --workspace --all-features --all-targets -- --nocapture

lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
components: rustfmt, clippy

- uses: Swatinem/rust-cache@v1
with:
cache-on-failure: true

- name: Run fmt
run: cargo fmt --all -- --check

- name: Run clippy
run: cargo clippy --workspace --all-features --all-targets -- -D warnings
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ Cargo.lock

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

.vscode
generated/
31 changes: 31 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "halo2_solidity_verifier"
version = "0.1.0"
edition = "2021"

[dependencies]
halo2_proofs = { git = "https://github.com/privacy-scaling-explorations/halo2", tag = "v2023_04_20" }
askama = { version = "0.12.0", features = ["config"], default-features = false }
hex = "0.4.3"
ruint = "1.10.1"
sha3 = "0.10"
itertools = "0.11.0"

# Remove when `vk.transcript_repr()` is ready for usage.
blake2b_simd = "1"

# For feature = "evm"
revm = { version = "3.3.0", optional = true }

[dev-dependencies]
rand = "0.8.5"
revm = "3.3.0"
halo2_maingate = { git = "https://github.com/privacy-scaling-explorations/halo2wrong", tag = "v2023_04_20", package = "maingate" }

[features]
default = []
evm = ["dep:revm"]

[[example]]
name = "separately"
required-features = ["evm"]
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,46 @@
# halo2_solidity_verifier
A set of tooling related to halo2 circuits verification inside Solidity contracts
# Halo2 Solidity Verifier

> ⚠️ This repo has NOT been audited and is NOT intended for a production environment yet.
Solidity verifier generator for [`halo2`](http://github.com/privacy-scaling-explorations/halo2) proof with KZG polynomial commitment scheme on BN254

## Usage

### Generate verifier and verifying key separately as 2 solidity contracts

```rust
let generator = SolidityGenerator::new(&params, &vk, Bdfg21, num_instances);
let (verifier_solidity, vk_solidity) = generator.render_separately().unwrap();
```

Check [`examples/separately.rs`](./examples/separately.rs) for more details.

### Generate verifier and verifying key in a single solidity contract

```rust
let generator = SolidityGenerator::new(&params, &vk, Bdfg21, num_instances);
let verifier_solidity = generator.render().unwrap();
```

### Encode proof into calldata to invoke `verifyProof`

```rust
let calldata = encode_calldata(vk_address, &proof, &instances);
```

Note that function selector is already included.

## Limitations

- It only allows circuit with **exact 1 instance column** and **no rotated query to this instance column**.
- Option `--via-ir` seems necessary when compiling the generated contract, otherwise it'd cause stack too deep error. However, `--via-ir` is not allowed to be used with `--standard-json`, not sure how to work around this yet.
- Even the `configure` is same, the [selector compression](https://github.com/privacy-scaling-explorations/halo2/blob/7a2165617195d8baa422ca7b2b364cef02380390/halo2_proofs/src/plonk/circuit/compress_selectors.rs#L51) might lead to different configuration when selector assignments are different. To avoid this we might need to update halo2 to support disabling selector compression.
- Now it only supports BDFG21 batch open scheme (aka SHPLONK), GWC19 is not yet implemented.

## Compatibility

The [`Keccak256Transcript`](./src/transcript.rs#L19) behaves exactly same as the `EvmTranscript` in `snark-verifier`.

## Acknowledgement

The template is heavily inspired by Aztec's [`BaseUltraVerifier.sol`](https://github.com/AztecProtocol/barretenberg/blob/4c456a2b196282160fd69bead6a1cea85289af37/sol/src/ultra/BaseUltraVerifier.sol).
3 changes: 3 additions & 0 deletions askama.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[[escaper]]
path = "askama::Text"
extensions = ["sol"]
242 changes: 242 additions & 0 deletions examples/separately.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
use application::StandardPlonk;
use prelude::*;

use halo2_solidity_verifier::{
compile_solidity, encode_calldata, BatchOpenScheme::Bdfg21, Evm, Keccak256Transcript,
SolidityGenerator,
};

const K_RANGE: Range<u32> = 10..17;

fn main() {
let mut rng = seeded_std_rng();

let params = setup(K_RANGE, &mut rng);

let vk = keygen_vk(&params[&K_RANGE.start], &StandardPlonk::default()).unwrap();
let generator = SolidityGenerator::new(&params[&K_RANGE.start], &vk, Bdfg21, 0);
let (verifier_solidity, _) = generator.render_separately().unwrap();
save_solidity("Halo2Verifier.sol", &verifier_solidity);

let verifier_creation_code = compile_solidity(&verifier_solidity);
let verifier_creation_code_size = verifier_creation_code.len();
println!("Verifier creation code size: {verifier_creation_code_size}");

let mut evm = Evm::default();
let verifier_address = evm.create(verifier_creation_code);

let deployed_verifier_solidity = verifier_solidity;

for k in K_RANGE {
let num_instances = k as usize;
let circuit = StandardPlonk::rand(num_instances, &mut rng);

let vk = keygen_vk(&params[&k], &circuit).unwrap();
let pk = keygen_pk(&params[&k], vk, &circuit).unwrap();
let generator = SolidityGenerator::new(&params[&k], pk.get_vk(), Bdfg21, num_instances);
let (verifier_solidity, vk_solidity) = generator.render_separately().unwrap();
save_solidity(format!("Halo2VerifyingKey-{k}.sol"), &vk_solidity);

assert_eq!(deployed_verifier_solidity, verifier_solidity);

let vk_creation_code = compile_solidity(&vk_solidity);
let vk_address = evm.create(vk_creation_code);

let calldata = {
let instances = circuit.instances();
let proof = create_proof_checked(&params[&k], &pk, circuit, &instances, &mut rng);
encode_calldata(vk_address.0.into(), &proof, &instances)
};
let (gas_cost, output) = evm.call(verifier_address, calldata);
assert_eq!(output, [vec![0; 31], vec![1]].concat());
println!("Gas cost of verifying standard Plonk with 2^{k} rows: {gas_cost}");
}
}

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()))
.unwrap()
.write_all(solidity.as_bytes())
.unwrap();
}

fn setup(k_range: Range<u32>, mut rng: impl RngCore) -> HashMap<u32, ParamsKZG<Bn256>> {
k_range
.clone()
.zip(k_range.map(|k| ParamsKZG::<Bn256>::setup(k, &mut rng)))
.collect()
}

fn create_proof_checked(
params: &ParamsKZG<Bn256>,
pk: &ProvingKey<G1Affine>,
circuit: impl Circuit<Fr>,
instances: &[Fr],
mut rng: impl RngCore,
) -> Vec<u8> {
use halo2_proofs::{
poly::kzg::{
multiopen::{ProverSHPLONK, VerifierSHPLONK},
strategy::SingleStrategy,
},
transcript::TranscriptWriterBuffer,
};

let proof = {
let mut transcript = Keccak256Transcript::new(Vec::new());
create_proof::<_, ProverSHPLONK<_>, _, _, _, _>(
params,
pk,
&[circuit],
&[&[instances]],
&mut rng,
&mut transcript,
)
.unwrap();
transcript.finalize()
};

let result = {
let mut transcript = Keccak256Transcript::new(proof.as_slice());
verify_proof::<_, VerifierSHPLONK<_>, _, _, SingleStrategy<_>>(
params,
pk.get_vk(),
SingleStrategy::new(params),
&[&[instances]],
&mut transcript,
)
};
assert!(result.is_ok());

proof
}

mod application {
use crate::prelude::*;

#[derive(Clone)]
pub struct StandardPlonkConfig {
selectors: [Column<Fixed>; 5],
wires: [Column<Advice>; 3],
}

impl StandardPlonkConfig {
fn configure(meta: &mut ConstraintSystem<impl PrimeField>) -> Self {
let [w_l, w_r, w_o] = [(); 3].map(|_| meta.advice_column());
let [q_l, q_r, q_o, q_m, q_c] = [(); 5].map(|_| meta.fixed_column());
let pi = meta.instance_column();
[w_l, w_r, w_o].map(|column| meta.enable_equality(column));
meta.create_gate(
"q_l·w_l + q_r·w_r + q_o·w_o + q_m·w_l·w_r + q_c + pi = 0",
|meta| {
let [w_l, w_r, w_o] =
[w_l, w_r, w_o].map(|column| meta.query_advice(column, Rotation::cur()));
let [q_l, q_r, q_o, q_m, q_c] = [q_l, q_r, q_o, q_m, q_c]
.map(|column| meta.query_fixed(column, Rotation::cur()));
let pi = meta.query_instance(pi, Rotation::cur());
Some(
q_l * w_l.clone()
+ q_r * w_r.clone()
+ q_o * w_o
+ q_m * w_l * w_r
+ q_c
+ pi,
)
},
);
StandardPlonkConfig {
selectors: [q_l, q_r, q_o, q_m, q_c],
wires: [w_l, w_r, w_o],
}
}
}

#[derive(Clone, Debug, Default)]
pub struct StandardPlonk<F>(Vec<F>);

impl<F: PrimeField> StandardPlonk<F> {
pub fn rand<R: RngCore>(num_instances: usize, mut rng: R) -> Self {
Self((0..num_instances).map(|_| F::random(&mut rng)).collect())
}

pub fn instances(&self) -> Vec<F> {
self.0.clone()
}
}

impl<F: PrimeField> Circuit<F> for StandardPlonk<F> {
type Config = StandardPlonkConfig;
type FloorPlanner = SimpleFloorPlanner;

fn without_witnesses(&self) -> Self {
unimplemented!()
}

fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
meta.set_minimum_degree(4);
StandardPlonkConfig::configure(meta)
}

fn synthesize(
&self,
config: Self::Config,
mut layouter: impl Layouter<F>,
) -> Result<(), Error> {
let [q_l, q_r, q_o, q_m, q_c] = config.selectors;
let [w_l, w_r, w_o] = config.wires;
layouter.assign_region(
|| "",
|mut region| {
for (offset, instance) in self.0.iter().enumerate() {
region.assign_advice(|| "", w_l, offset, || Value::known(*instance))?;
region.assign_fixed(|| "", q_l, offset, || Value::known(-F::ONE))?;
}
let offset = self.0.len();
let a = region.assign_advice(|| "", w_l, offset, || Value::known(F::ONE))?;
a.copy_advice(|| "", &mut region, w_r, offset)?;
a.copy_advice(|| "", &mut region, w_o, offset)?;
let offset = offset + 1;
region.assign_advice(|| "", w_l, offset, || Value::known(-F::from(5)))?;
for (column, idx) in [q_l, q_r, q_o, q_m, q_c].iter().zip(1..) {
region.assign_fixed(
|| "",
*column,
offset,
|| Value::known(F::from(idx)),
)?;
}
Ok(())
},
)
}
}
}

mod prelude {
pub use halo2_proofs::{
circuit::{Layouter, SimpleFloorPlanner, Value},
halo2curves::{
bn256::{Bn256, Fr, G1Affine},
ff::PrimeField,
},
plonk::*,
poly::{commitment::Params, kzg::commitment::ParamsKZG, Rotation},
};
pub use rand::{
rngs::{OsRng, StdRng},
RngCore, SeedableRng,
};
pub use std::{
collections::HashMap,
fs::{create_dir_all, File},
io::Write,
ops::Range,
};

pub fn seeded_std_rng() -> impl RngCore {
StdRng::seed_from_u64(OsRng.next_u64())
}
}
Loading

0 comments on commit ab4e36b

Please sign in to comment.