Skip to content

Commit

Permalink
feat: allow more instances and add document with example
Browse files Browse the repository at this point in the history
  • Loading branch information
han0110 committed Sep 8, 2023
1 parent dc38bea commit 9440187
Show file tree
Hide file tree
Showing 16 changed files with 800 additions and 385 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ jobs:
run: (hash svm 2>/dev/null || cargo install --version 0.3.0 svm-rs) && svm install 0.8.21 && solc --version

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

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

lint:
name: Lint
Expand All @@ -51,4 +50,4 @@ jobs:
run: cargo fmt --all -- --check

- name: Run clippy
run: cargo clippy --all --all-features --all-targets -- -D warnings
run: cargo clippy --workspace --all-features --all-targets -- -D warnings
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ Cargo.lock
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

.vscode
.vscode
generated/
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,21 @@ ruint = "1.10.1"
sha3 = "0.10"
itertools = "0.11.0"

# 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" }

[patch."https://github.com/privacy-scaling-explorations/halo2.git"]
halo2_proofs = { git = "https://github.com/han0110/halo2", branch = "tmp/expose-transcript-repr-for-0420" }

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

[[example]]
name = "separately"
required-features = ["evm"]
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
# Halo2 Solidity Verifier

A set of tooling related to halo2 circuits verification inside solidity contracts.
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.

## Limitation

- 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#L18) 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).
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 9440187

Please sign in to comment.