-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: allow more instances and add document with example
- Loading branch information
Showing
16 changed files
with
800 additions
and
385 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
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
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,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(¶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. | ||
|
||
## 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). |
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.