Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SolidityGenerator #2

Merged
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]
46 changes: 44 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,44 @@
# halo2_solidity_verifier
A set of tooling related to halo2 circuits verification inside Solidity contracts
# Halo2 Solidity Verifier

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;
CPerezz marked this conversation as resolved.
Show resolved Hide resolved

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())
}
}
CPerezz marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading