forked from summa-dev/halo2-solidity-verifier
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement
SolidityGenerator
(summa-dev#2)
* 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
Showing
17 changed files
with
3,995 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(¶ms, &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(¶ms, &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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[[escaper]] | ||
path = "askama::Text" | ||
extensions = ["sol"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(¶ms[&K_RANGE.start], &StandardPlonk::default()).unwrap(); | ||
let generator = SolidityGenerator::new(¶ms[&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(¶ms[&k], &circuit).unwrap(); | ||
let pk = keygen_pk(¶ms[&k], vk, &circuit).unwrap(); | ||
let generator = SolidityGenerator::new(¶ms[&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(¶ms[&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()) | ||
} | ||
} |
Oops, something went wrong.