diff --git a/.gitignore b/.gitignore index ec2971fb..15a4e716 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ testdata Cargo.lock params agg.pk -break_points.json \ No newline at end of file +break_points.json +*.srs \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 53c98f1a..5fd04b0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,3 +34,6 @@ incremental = false [profile.flamegraph] inherits = "release" debug = true + +[patch."https://github.com/privacy-scaling-explorations/halo2.git"] +halo2_proofs = { git = "https://github.com/scroll-tech/halo2.git", branch = "sync-halo2-lib-0.4.0" } diff --git a/snark-verifier-sdk/Cargo.toml b/snark-verifier-sdk/Cargo.toml index dd900125..cfe31457 100644 --- a/snark-verifier-sdk/Cargo.toml +++ b/snark-verifier-sdk/Cargo.toml @@ -17,18 +17,18 @@ serde_json = "1.0" serde_with = { version = "2.2", optional = true } bincode = "1.3.3" ark-std = { version = "0.3.0", features = ["print-trace"], optional = true } -halo2-base = { git = "https://github.com/axiom-crypto/halo2-lib.git", branch = "community-edition", default-features = false } +halo2-base = { git = "https://github.com/scroll-tech/halo2-lib.git", branch = "sync-halo2-lib-0.4.0", default-features = false } snark-verifier = { path = "../snark-verifier", default-features = false } getset = "0.1.2" # loader_evm ethereum-types = { version = "0.14.1", default-features = false, features = ["std"], optional = true } -# zkevm benchmarks -zkevm-circuits = { git = "https://github.com/privacy-scaling-explorations/zkevm-circuits.git", rev = "f834e61", features = ["test"], optional = true } -bus-mapping = { git = "https://github.com/privacy-scaling-explorations/zkevm-circuits.git", rev = "f834e61", optional = true } -eth-types = { git = "https://github.com/privacy-scaling-explorations/zkevm-circuits.git", rev = "f834e61", optional = true } -mock = { git = "https://github.com/privacy-scaling-explorations/zkevm-circuits.git", rev = "f834e61", optional = true } +# # zkevm benchmarks +# zkevm-circuits = { git = "https://github.com/privacy-scaling-explorations/zkevm-circuits.git", rev = "f834e61", features = ["test"], optional = true } +# bus-mapping = { git = "https://github.com/privacy-scaling-explorations/zkevm-circuits.git", rev = "f834e61", optional = true } +# eth-types = { git = "https://github.com/privacy-scaling-explorations/zkevm-circuits.git", rev = "f834e61", optional = true } +# mock = { git = "https://github.com/privacy-scaling-explorations/zkevm-circuits.git", rev = "f834e61", optional = true } [dev-dependencies] ark-std = { version = "0.3.0", features = ["print-trace"] } @@ -41,7 +41,7 @@ crossterm = { version = "0.25" } tui = { version = "0.19", default-features = false, features = ["crossterm"] } [features] -default = ["loader_halo2", "loader_evm", "halo2-axiom", "halo2-base/jemallocator", "display"] +default = ["loader_halo2", "loader_evm", "halo2-pse", "halo2-base/jemallocator", "display"] display = ["snark-verifier/display", "dep:ark-std"] loader_evm = ["snark-verifier/loader_evm", "dep:ethereum-types"] loader_halo2 = ["snark-verifier/loader_halo2"] diff --git a/snark-verifier-sdk/examples/k_as_witness.rs b/snark-verifier-sdk/examples/k_as_witness.rs index 3561447f..e9994d8d 100644 --- a/snark-verifier-sdk/examples/k_as_witness.rs +++ b/snark-verifier-sdk/examples/k_as_witness.rs @@ -87,6 +87,7 @@ mod application { impl Circuit for StandardPlonk { type Config = StandardPlonkConfig; type FloorPlanner = SimpleFloorPlanner; + type Params = (); fn without_witnesses(&self) -> Self { Self(Fr::zero(), self.1) @@ -105,9 +106,9 @@ mod application { layouter.assign_region( || "", |mut region| { - region.assign_advice(config.a, 0, Value::known(self.0)); - region.assign_fixed(config.q_a, 0, -Fr::one()); - region.assign_advice(config.a, 1, Value::known(-Fr::from(5u64))); + region.assign_advice(|| "", config.a, 0, || Value::known(self.0)); + region.assign_fixed(|| "", config.q_a, 0, || Value::known(-Fr::one())); + region.assign_advice(|| "", config.a, 1, || Value::known(-Fr::from(5u64))); for (idx, column) in (1..).zip([ config.q_a, config.q_b, @@ -115,17 +116,22 @@ mod application { config.q_ab, config.constant, ]) { - region.assign_fixed(column, 1, Fr::from(idx as u64)); + region.assign_fixed(|| "", column, 1, ||Value::known(Fr::from(idx as u64))); } - let a = region.assign_advice(config.a, 2, Value::known(Fr::one())); - a.copy_advice(&mut region, config.b, 3); - a.copy_advice(&mut region, config.c, 4); + let a = region.assign_advice(|| "", config.a, 2, || Value::known(Fr::one()))?; + a.copy_advice(|| "", &mut region, config.b, 3); + a.copy_advice(|| "", &mut region, config.c, 4); // assuming <= 10 blinding factors // fill in most of circuit with a computation let n = self.1; for offset in 5..n - 10 { - region.assign_advice(config.a, offset, Value::known(-Fr::from(5u64))); + region.assign_advice( + || "", + config.a, + offset, + || Value::known(-Fr::from(5u64)), + ); for (idx, column) in (1..).zip([ config.q_a, config.q_b, @@ -133,7 +139,12 @@ mod application { config.q_ab, config.constant, ]) { - region.assign_fixed(column, offset, Fr::from(idx as u64)); + region.assign_fixed( + || "", + column, + offset, + || Value::known(Fr::from(idx as u64)), + ); } } diff --git a/snark-verifier-sdk/examples/standard_plonk.rs b/snark-verifier-sdk/examples/standard_plonk.rs index 7ad9c971..939c529d 100644 --- a/snark-verifier-sdk/examples/standard_plonk.rs +++ b/snark-verifier-sdk/examples/standard_plonk.rs @@ -92,6 +92,7 @@ mod application { impl Circuit for StandardPlonk { type Config = StandardPlonkConfig; type FloorPlanner = SimpleFloorPlanner; + type Params = (); fn without_witnesses(&self) -> Self { Self::default() diff --git a/snark-verifier-sdk/examples/vkey_as_witness.rs b/snark-verifier-sdk/examples/vkey_as_witness.rs index ca066d80..ca27b6cc 100644 --- a/snark-verifier-sdk/examples/vkey_as_witness.rs +++ b/snark-verifier-sdk/examples/vkey_as_witness.rs @@ -95,6 +95,7 @@ mod application { impl Circuit for StandardPlonk { type Config = StandardPlonkConfig; type FloorPlanner = SimpleFloorPlanner; + type Params = (); fn without_witnesses(&self) -> Self { Self(Fr::zero(), self.1) @@ -113,9 +114,9 @@ mod application { layouter.assign_region( || "", |mut region| { - region.assign_advice(config.a, 0, Value::known(self.0)); - region.assign_fixed(config.q_a, 0, -Fr::one()); - region.assign_advice(config.a, 1, Value::known(-Fr::from(5u64))); + region.assign_advice(|| "", config.a, 0, || Value::known(self.0)); + region.assign_fixed(|| "", config.q_a, 0, || Value::known(-Fr::one())); + region.assign_advice(|| "", config.a, 1, || Value::known(-Fr::from(5u64))); if self.1 != ComputeFlag::SkipFixed { for (idx, column) in (1..).zip([ config.q_a, @@ -124,13 +125,18 @@ mod application { config.q_ab, config.constant, ]) { - region.assign_fixed(column, 1, Fr::from(idx as u64)); + region.assign_fixed( + || "", + column, + 1, + || Value::known(Fr::from(idx as u64)), + ); } } - let a = region.assign_advice(config.a, 2, Value::known(Fr::one())); + let a = region.assign_advice(|| "", config.a, 2, || Value::known(Fr::one()))?; if self.1 != ComputeFlag::SkipCopy { - a.copy_advice(&mut region, config.b, 3); - a.copy_advice(&mut region, config.c, 4); + a.copy_advice(|| "", &mut region, config.b, 3); + a.copy_advice(|| "", &mut region, config.c, 4); } Ok(()) diff --git a/snark-verifier-sdk/src/aggregation.rs b/snark-verifier-sdk/src/aggregation.rs new file mode 100644 index 00000000..b5dc148d --- /dev/null +++ b/snark-verifier-sdk/src/aggregation.rs @@ -0,0 +1,440 @@ +use super::PlonkSuccinctVerifier; +use crate::{BITS, LIMBS}; +use halo2_base::{ + gates::{ + builder::{ + CircuitBuilderStage, FlexGateConfigParams, GateThreadBuilder, + MultiPhaseThreadBreakPoints, RangeCircuitBuilder, RangeWithInstanceCircuitBuilder, + RangeWithInstanceConfig, + }, + RangeChip, + }, + halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner}, + halo2curves::bn256::{Bn256, Fr, G1Affine}, + plonk::{self, Circuit, ConstraintSystem, Selector}, + poly::{ + commitment::{Params, ParamsProver}, + kzg::commitment::ParamsKZG, + }, + }, + utils::ScalarField, + AssignedValue, +}; +use itertools::Itertools; +use rand::{rngs::StdRng, SeedableRng}; +use serde::{Deserialize, Serialize}; +#[cfg(debug_assertions)] +use snark_verifier::util::arithmetic::fe_to_limbs; +use snark_verifier::{ + loader::{ + self, + halo2::halo2_ecc::{self, bn254::FpChip}, + native::NativeLoader, + }, + pcs::{ + kzg::{KzgAccumulator, KzgAsProvingKey, KzgAsVerifyingKey, KzgSuccinctVerifyingKey}, + AccumulationScheme, AccumulationSchemeProver, PolynomialCommitmentScheme, + }, + verifier::SnarkVerifier, +}; +use std::{ + env::{set_var, var}, + fs::File, + path::Path, + rc::Rc, +}; + +use super::{CircuitExt, PoseidonTranscript, Snark, POSEIDON_SPEC}; + +pub type Svk = KzgSuccinctVerifyingKey; +pub type BaseFieldEccChip<'chip> = halo2_ecc::ecc::BaseFieldEccChip<'chip, G1Affine>; +pub type Halo2Loader<'chip> = loader::halo2::Halo2Loader>; + +#[allow(clippy::type_complexity)] +/// Core function used in `synthesize` to aggregate multiple `snarks`. +/// +/// Returns the assigned instances of previous snarks and the new final pair that needs to be verified in a pairing check. +/// For each previous snark, we concatenate all instances into a single vector. We return a vector of vectors, +/// one vector per snark, for convenience. +/// +/// # Assumptions +/// * `snarks` is not empty +pub fn aggregate<'a, AS>( + svk: &Svk, + loader: &Rc>, + snarks: &[Snark], + as_proof: &[u8], +) -> (Vec>>, KzgAccumulator>>) +where + AS: PolynomialCommitmentScheme< + G1Affine, + Rc>, + VerifyingKey = Svk, + Output = KzgAccumulator>>, + > + AccumulationScheme< + G1Affine, + Rc>, + Accumulator = KzgAccumulator>>, + VerifyingKey = KzgAsVerifyingKey, + >, +{ + assert!(!snarks.is_empty(), "trying to aggregate 0 snarks"); + let assign_instances = |instances: &[Vec]| { + instances + .iter() + .map(|instances| { + instances.iter().map(|instance| loader.assign_scalar(*instance)).collect_vec() + }) + .collect_vec() + }; + + let mut previous_instances = Vec::with_capacity(snarks.len()); + // to avoid re-loading the spec each time, we create one transcript and clear the stream + let mut transcript = PoseidonTranscript::>, &[u8]>::from_spec( + loader, + &[], + POSEIDON_SPEC.clone(), + ); + + let mut accumulators = snarks + .iter() + .flat_map(|snark| { + let protocol = snark.protocol.loaded(loader); + let instances = assign_instances(&snark.instances); + + // read the transcript and perform Fiat-Shamir + // run through verification computation and produce the final pair `succinct` + transcript.new_stream(snark.proof()); + let proof = PlonkSuccinctVerifier::::read_proof( + svk, + &protocol, + &instances, + &mut transcript, + ) + .unwrap(); + let accumulator = + PlonkSuccinctVerifier::::verify(svk, &protocol, &instances, &proof).unwrap(); + + previous_instances.push( + instances.into_iter().flatten().map(|scalar| scalar.into_assigned()).collect(), + ); + + accumulator + }) + .collect_vec(); + + let accumulator = if accumulators.len() > 1 { + transcript.new_stream(as_proof); + let proof = >::read_proof( + &Default::default(), + &accumulators, + &mut transcript, + ) + .unwrap(); + >::verify(&Default::default(), &accumulators, &proof) + .unwrap() + } else { + accumulators.pop().unwrap() + }; + + (previous_instances, accumulator) +} + +/// Same as `FlexGateConfigParams` except we assume a single Phase and default 'Vertical' strategy. +/// Also adds `lookup_bits` field. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AggregationConfigParams { + pub degree: u32, + pub num_advice: usize, + pub num_lookup_advice: usize, + pub num_fixed: usize, + pub lookup_bits: usize, +} + +impl AggregationConfigParams { + pub fn from_path(path: impl AsRef) -> Self { + serde_json::from_reader(File::open(path).expect("Aggregation config path does not exist")) + .unwrap() + } +} + +#[derive(Clone, Debug)] +pub struct AggregationCircuit { + pub inner: RangeWithInstanceCircuitBuilder, + // the public instances from previous snarks that were aggregated, now collected as PRIVATE assigned values + // the user can optionally append these to `inner.assigned_instances` to expose them + pub previous_instances: Vec>>, + // accumulation scheme proof, private input + pub as_proof: Vec, // not sure this needs to be stored, keeping for now +} + +// trait just so we can have a generic that is either SHPLONK or GWC +pub trait Halo2KzgAccumulationScheme<'a> = PolynomialCommitmentScheme< + G1Affine, + Rc>, + VerifyingKey = Svk, + Output = KzgAccumulator>>, + > + AccumulationScheme< + G1Affine, + Rc>, + Accumulator = KzgAccumulator>>, + VerifyingKey = KzgAsVerifyingKey, + > + PolynomialCommitmentScheme< + G1Affine, + NativeLoader, + VerifyingKey = Svk, + Output = KzgAccumulator, + > + AccumulationScheme< + G1Affine, + NativeLoader, + Accumulator = KzgAccumulator, + VerifyingKey = KzgAsVerifyingKey, + > + AccumulationSchemeProver>; + +impl AggregationCircuit { + /// Given snarks, this creates a circuit and runs the `GateThreadBuilder` to verify all the snarks. + /// By default, the returned circuit has public instances equal to the limbs of the pair of elliptic curve points, referred to as the `accumulator`, that need to be verified in a final pairing check. + /// + /// The user can optionally modify the circuit after calling this function to add more instances to `assigned_instances` to expose. + /// + /// Warning: will fail silently if `snarks` were created using a different multi-open scheme than `AS` + /// where `AS` can be either [`crate::SHPLONK`] or [`crate::GWC`] (for original PLONK multi-open scheme) + pub fn new( + stage: CircuitBuilderStage, + break_points: Option, + lookup_bits: usize, + params: &ParamsKZG, + snarks: impl IntoIterator, + ) -> Self + where + AS: for<'a> Halo2KzgAccumulationScheme<'a>, + { + let svk: Svk = params.get_g()[0].into(); + let snarks = snarks.into_iter().collect_vec(); + + let mut transcript_read = + PoseidonTranscript::::from_spec(&[], POSEIDON_SPEC.clone()); + // TODO: the snarks can probably store these accumulators + let accumulators = snarks + .iter() + .flat_map(|snark| { + transcript_read.new_stream(snark.proof()); + let proof = PlonkSuccinctVerifier::::read_proof( + &svk, + &snark.protocol, + &snark.instances, + &mut transcript_read, + ) + .unwrap(); + PlonkSuccinctVerifier::::verify(&svk, &snark.protocol, &snark.instances, &proof) + .unwrap() + }) + .collect_vec(); + + let (_accumulator, as_proof) = { + let mut transcript_write = PoseidonTranscript::>::from_spec( + vec![], + POSEIDON_SPEC.clone(), + ); + let rng = StdRng::from_entropy(); + let accumulator = + AS::create_proof(&Default::default(), &accumulators, &mut transcript_write, rng) + .unwrap(); + (accumulator, transcript_write.finalize()) + }; + + // create thread builder and run aggregation witness gen + let builder = match stage { + CircuitBuilderStage::Mock => GateThreadBuilder::mock(), + CircuitBuilderStage::Prover => GateThreadBuilder::prover(), + CircuitBuilderStage::Keygen => GateThreadBuilder::keygen(), + }; + // create halo2loader + let range = RangeChip::::default(lookup_bits); + let fp_chip = FpChip::::new(&range, BITS, LIMBS); + let ecc_chip = BaseFieldEccChip::new(&fp_chip); + let loader = Halo2Loader::new(ecc_chip, builder); + + let (previous_instances, accumulator) = + aggregate::(&svk, &loader, &snarks, as_proof.as_slice()); + let lhs = accumulator.lhs.assigned(); + let rhs = accumulator.rhs.assigned(); + let assigned_instances = lhs + .x() + .limbs() + .iter() + .chain(lhs.y().limbs().iter()) + .chain(rhs.x().limbs().iter()) + .chain(rhs.y().limbs().iter()) + .copied() + .collect_vec(); + + #[cfg(debug_assertions)] + { + let KzgAccumulator { lhs, rhs } = _accumulator; + let instances = + [lhs.x, lhs.y, rhs.x, rhs.y].map(fe_to_limbs::<_, Fr, LIMBS, BITS>).concat(); + for (lhs, rhs) in instances.iter().zip(assigned_instances.iter()) { + assert_eq!(lhs, rhs.value()); + } + } + + let builder = loader.take_ctx(); + let circuit = match stage { + CircuitBuilderStage::Mock => RangeCircuitBuilder::mock(builder), + CircuitBuilderStage::Keygen => RangeCircuitBuilder::keygen(builder), + CircuitBuilderStage::Prover => { + RangeCircuitBuilder::prover(builder, break_points.unwrap()) + } + }; + let inner = RangeWithInstanceCircuitBuilder::new(circuit, assigned_instances); + Self { inner, previous_instances, as_proof } + } + + pub fn public( + stage: CircuitBuilderStage, + break_points: Option, + lookup_bits: usize, + params: &ParamsKZG, + snarks: impl IntoIterator, + has_prev_accumulator: bool, + ) -> Self + where + AS: for<'a> Halo2KzgAccumulationScheme<'a>, + { + let mut private = Self::new::(stage, break_points, lookup_bits, params, snarks); + private.expose_previous_instances(has_prev_accumulator); + private + } + + // this function is for convenience + /// `params` should be the universal trusted setup to be used for the aggregation circuit, not the one used to generate the previous snarks, although we assume both use the same generator g[0] + pub fn keygen(params: &ParamsKZG, snarks: impl IntoIterator) -> Self + where + AS: for<'a> Halo2KzgAccumulationScheme<'a>, + { + let lookup_bits = params.k() as usize - 1; // almost always we just use the max lookup bits possible, which is k - 1 because of blinding factors + let circuit = + Self::new::(CircuitBuilderStage::Keygen, None, lookup_bits, params, snarks); + circuit.config(params.k(), Some(10)); + set_var("LOOKUP_BITS", lookup_bits.to_string()); + circuit + } + + // this function is for convenience + pub fn prover( + params: &ParamsKZG, + snarks: impl IntoIterator, + break_points: MultiPhaseThreadBreakPoints, + ) -> Self + where + AS: for<'a> Halo2KzgAccumulationScheme<'a>, + { + let lookup_bits: usize = var("LOOKUP_BITS").expect("LOOKUP_BITS not set").parse().unwrap(); + let circuit = Self::new::( + CircuitBuilderStage::Prover, + Some(break_points), + lookup_bits, + params, + snarks, + ); + let minimum_rows = var("MINIMUM_ROWS").map(|s| s.parse().unwrap_or(10)).unwrap_or(10); + circuit.config(params.k(), Some(minimum_rows)); + set_var("LOOKUP_BITS", lookup_bits.to_string()); + circuit + } + + /// Re-expose the previous public instances of aggregated snarks again. + /// If `hash_prev_accumulator` is true, then we assume all aggregated snarks were themselves + /// aggregation snarks, and we exclude the old accumulators from the public input. + pub fn expose_previous_instances(&mut self, has_prev_accumulator: bool) { + let start = (has_prev_accumulator as usize) * 4 * LIMBS; + for prev in self.previous_instances.iter() { + self.inner.assigned_instances.extend_from_slice(&prev[start..]); + } + } + + pub fn as_proof(&self) -> &[u8] { + &self.as_proof[..] + } + + pub fn config(&self, k: u32, minimum_rows: Option) -> FlexGateConfigParams { + self.inner.config(k, minimum_rows) + } + + pub fn break_points(&self) -> MultiPhaseThreadBreakPoints { + self.inner.break_points() + } + + pub fn instance_count(&self) -> usize { + self.inner.instance_count() + } + + pub fn instance(&self) -> Vec { + self.inner.instance() + } +} + +impl CircuitExt for RangeWithInstanceCircuitBuilder { + fn num_instance(&self) -> Vec { + vec![self.instance_count()] + } + + fn instances(&self) -> Vec> { + vec![self.instance()] + } + + fn selectors(config: &Self::Config) -> Vec { + config.range.gate.basic_gates[0].iter().map(|gate| gate.q_enable).collect() + } +} + +impl Circuit for AggregationCircuit { + type Config = RangeWithInstanceConfig; + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + unimplemented!() + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + RangeWithInstanceCircuitBuilder::configure(meta) + } + + fn synthesize( + &self, + config: Self::Config, + layouter: impl Layouter, + ) -> Result<(), plonk::Error> { + self.inner.synthesize(config, layouter) + } +} + +impl CircuitExt for AggregationCircuit { + fn num_instance(&self) -> Vec { + self.inner.num_instance() + } + + fn instances(&self) -> Vec> { + self.inner.instances() + } + + fn accumulator_indices() -> Option> { + Some((0..4 * LIMBS).map(|idx| (0, idx)).collect()) + } + + fn selectors(config: &Self::Config) -> Vec { + RangeWithInstanceCircuitBuilder::selectors(config) + } +} + +pub fn load_verify_circuit_degree() -> u32 { + let path = std::env::var("VERIFY_CONFIG") + .unwrap_or_else(|_| "./configs/verify_circuit.config".to_string()); + let params: AggregationConfigParams = serde_json::from_reader( + File::open(path.as_str()).unwrap_or_else(|_| panic!("{path} does not exist")), + ) + .unwrap(); + params.degree +} diff --git a/snark-verifier-sdk/src/aggregation/aggregation_circuit.rs b/snark-verifier-sdk/src/aggregation/aggregation_circuit.rs new file mode 100644 index 00000000..2ebecd33 --- /dev/null +++ b/snark-verifier-sdk/src/aggregation/aggregation_circuit.rs @@ -0,0 +1,207 @@ +use std::fs::File; + +use halo2_base::{ + halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner, Value}, + halo2curves::bn256::{Bn256, Fr}, + plonk::{self, Circuit, ConstraintSystem, Selector}, + poly::{commitment::ParamsProver, kzg::commitment::ParamsKZG}, + }, + Context, ContextParams, +}; +use itertools::Itertools; +use rand::Rng; +use snark_verifier::{ + loader::native::NativeLoader, + pcs::{kzg::KzgAccumulator, AccumulationSchemeProver}, + util::arithmetic::fe_to_limbs, + verifier::PlonkVerifier, +}; + +use crate::{ + aggregation::{ + aggregate, + config::{AggregationConfig, AggregationConfigParams}, + flatten_accumulator, POSEIDON_SPEC, + }, + types::{Halo2Loader, KzgAs, KzgBDFG, PoseidonTranscript, Shplonk, Svk}, + CircuitExt, Snark, SnarkWitness, BITS, LIMBS, +}; + +/// Aggregation circuit that does not re-expose any public inputs from aggregated snarks +/// +/// This is mostly a reference implementation. In practice one will probably need to re-implement the circuit for one's particular use case with specific instance logic. +#[derive(Clone)] +pub struct AggregationCircuit { + pub(crate) svk: Svk, + pub(crate) snarks: Vec, + // the public instances from previous snarks that were aggregated, now collected as PRIVATE assigned values + // the user can optionally append these to `inner.assigned_instances` to expose them + pub(crate) instances: Vec, + // accumulation scheme proof, private input + pub(crate) as_proof: Value>, +} + +impl AggregationCircuit { + pub fn new( + params: &ParamsKZG, + snarks: impl IntoIterator, + rng: impl Rng + Send, + ) -> Self { + let svk = params.get_g()[0].into(); + let snarks = snarks.into_iter().collect_vec(); + + // TODO: this is all redundant calculation to get the public output + // Halo2 should just be able to expose public output to instance column directly + let mut transcript_read = + PoseidonTranscript::::from_spec(&[], POSEIDON_SPEC.clone()); + let accumulators = snarks + .iter() + .flat_map(|snark| { + transcript_read.new_stream(snark.proof.as_slice()); + let proof = Shplonk::read_proof( + &svk, + &snark.protocol, + &snark.instances, + &mut transcript_read, + ); + Shplonk::succinct_verify(&svk, &snark.protocol, &snark.instances, &proof) + }) + .collect_vec(); + + let (accumulator, as_proof) = { + let mut transcript_write = PoseidonTranscript::>::from_spec( + vec![], + POSEIDON_SPEC.clone(), + ); + // We always use SHPLONK for accumulation scheme when aggregating proofs + let accumulator = + KzgAs::create_proof(&Default::default(), &accumulators, &mut transcript_write, rng) + .unwrap(); + (accumulator, transcript_write.finalize()) + }; + + let KzgAccumulator { lhs, rhs } = accumulator; + let instances = [lhs.x, lhs.y, rhs.x, rhs.y].map(fe_to_limbs::<_, _, LIMBS, BITS>).concat(); + + Self { + svk, + snarks: snarks.into_iter().map_into().collect(), + instances, + as_proof: Value::known(as_proof), + } + } + + pub fn instance(&self) -> Vec { + self.instances.clone() + } + + pub fn succinct_verifying_key(&self) -> &Svk { + &self.svk + } + + pub fn snarks(&self) -> &[SnarkWitness] { + &self.snarks + } + + pub fn as_proof(&self) -> Value<&[u8]> { + self.as_proof.as_ref().map(Vec::as_slice) + } +} + +impl CircuitExt for AggregationCircuit { + fn num_instance(&self) -> Vec { + // [..lhs, ..rhs] + vec![4 * LIMBS] + } + + fn instances(&self) -> Vec> { + vec![self.instance()] + } + + fn accumulator_indices() -> Option> { + Some((0..4 * LIMBS).map(|idx| (0, idx)).collect()) + } + + fn selectors(config: &Self::Config) -> Vec { + config.gate().basic_gates[0].iter().map(|gate| gate.q_enable).collect() + } +} + +impl Circuit for AggregationCircuit { + type Config = AggregationConfig; + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self { + svk: self.svk, + snarks: self.snarks.iter().map(SnarkWitness::without_witnesses).collect(), + instances: Vec::new(), + as_proof: Value::unknown(), + } + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + let path = std::env::var("VERIFY_CONFIG") + .unwrap_or_else(|_| "configs/verify_circuit.config".to_owned()); + let params: AggregationConfigParams = serde_json::from_reader( + File::open(path.as_str()).unwrap_or_else(|_| panic!("{path:?} does not exist")), + ) + .unwrap(); + + AggregationConfig::configure(meta, params) + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), plonk::Error> { + #[cfg(feature = "display")] + let witness_time = start_timer!(|| "synthesize | Aggregation Circuit"); + config.range().load_lookup_table(&mut layouter).expect("load range lookup table"); + let mut first_pass = halo2_base::SKIP_FIRST_PASS; + let mut instances = vec![]; + layouter + .assign_region( + || "", + |region| { + if first_pass { + first_pass = false; + return Ok(()); + } + let ctx = Context::new( + region, + ContextParams { + max_rows: config.gate().max_rows, + num_context_ids: 1, + fixed_columns: config.gate().constants.clone(), + }, + ); + + let ecc_chip = config.ecc_chip(); + let loader = Halo2Loader::new(ecc_chip, ctx); + let (_, acc) = + aggregate::(&self.svk, &loader, &self.snarks, self.as_proof()); + + instances.extend( + flatten_accumulator(acc).iter().map(|assigned| assigned.cell().clone()), + ); + + config.range().finalize(&mut loader.ctx_mut()); + #[cfg(feature = "display")] + loader.ctx_mut().print_stats(&["Range"]); + Ok(()) + }, + ) + .unwrap(); + + // Expose instances + for (i, cell) in instances.into_iter().enumerate() { + layouter.constrain_instance(cell, config.instance, i)?; + } + #[cfg(feature = "display")] + end_timer!(witness_time); + Ok(()) + } +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/aggregation/config.rs b/snark-verifier-sdk/src/aggregation/config.rs new file mode 100644 index 00000000..4b751a14 --- /dev/null +++ b/snark-verifier-sdk/src/aggregation/config.rs @@ -0,0 +1,79 @@ +use halo2_base::{ + halo2_proofs::{ + halo2curves::bn256::{Fq, Fr, G1Affine}, + plonk::{Column, ConstraintSystem, Instance}, + }, + utils::modulus, +}; +use snark_verifier::loader::halo2::halo2_ecc::{ + ecc::{BaseFieldEccChip, EccChip}, + fields::fp::{FpConfig, FpStrategy}, +}; + +use crate::{BITS, LIMBS}; + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] +/// Parameters for aggregation circuit configs. +pub struct AggregationConfigParams { + pub strategy: FpStrategy, + pub degree: u32, + pub num_advice: Vec, + pub num_lookup_advice: Vec, + pub num_fixed: usize, + pub lookup_bits: usize, + pub limb_bits: usize, + pub num_limbs: usize, +} + +#[derive(Clone, Debug)] +/// Configurations for aggregation circuit +pub struct AggregationConfig { + /// Non-native field chip configurations + pub base_field_config: FpConfig, + /// Instance for public input + pub instance: Column, +} + +impl AggregationConfig { + /// Build a configuration from parameters. + pub fn configure(meta: &mut ConstraintSystem, params: AggregationConfigParams) -> Self { + assert!( + params.limb_bits == BITS && params.num_limbs == LIMBS, + "For now we fix limb_bits = {}, otherwise change code", + BITS + ); + let base_field_config = FpConfig::configure( + meta, + params.strategy, + ¶ms.num_advice, + ¶ms.num_lookup_advice, + params.num_fixed, + params.lookup_bits, + BITS, + LIMBS, + modulus::(), + 0, + params.degree as usize, + ); + + let instance = meta.instance_column(); + meta.enable_equality(instance); + + Self { base_field_config, instance } + } + + /// Range gate configuration + pub fn range(&self) -> &halo2_base::gates::range::RangeConfig { + &self.base_field_config.range + } + + /// Flex gate configuration + pub fn gate(&self) -> &halo2_base::gates::flex_gate::FlexGateConfig { + &self.base_field_config.range.gate + } + + /// Ecc gate configuration + pub fn ecc_chip(&self) -> BaseFieldEccChip { + EccChip::construct(self.base_field_config.clone()) + } +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/aggregation/multi_aggregation_circuit.rs b/snark-verifier-sdk/src/aggregation/multi_aggregation_circuit.rs new file mode 100644 index 00000000..5fdf9ea5 --- /dev/null +++ b/snark-verifier-sdk/src/aggregation/multi_aggregation_circuit.rs @@ -0,0 +1,163 @@ +#![allow(clippy::clone_on_copy)] +use crate::{ + aggregation::{aggregate, flatten_accumulator}, + types::Halo2Loader, + CircuitExt, Snark, LIMBS, +}; +#[cfg(feature = "display")] +use ark_std::end_timer; +#[cfg(feature = "display")] +use ark_std::start_timer; +use halo2_base::utils::value_to_option; +use halo2_base::{ + halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner}, + halo2curves::bn256::{Bn256, Fr}, + plonk::{self, Circuit, Selector}, + poly::kzg::commitment::ParamsKZG, + }, + Context, ContextParams, +}; +use itertools::Itertools; +use rand::Rng; +use snark_verifier::pcs::kzg::{Bdfg21, Kzg}; + +use super::{aggregation_circuit::AggregationCircuit, config::AggregationConfig}; + +/// This circuit takes multiple SNARKs and passes through all of their instances except the old accumulators. +/// +/// * If `has_prev_accumulator = true`, we assume all SNARKs are of aggregation circuits with old accumulators +/// only in the first instance column. +/// * Otherwise if `has_prev_accumulator = false`, then all previous instances are passed through. +#[derive(Clone)] +pub struct PublicAggregationCircuit { + pub aggregation: AggregationCircuit, + pub has_prev_accumulator: bool, +} + +impl PublicAggregationCircuit { + pub fn new( + params: &ParamsKZG, + snarks: Vec, + has_prev_accumulator: bool, + rng: &mut (impl Rng + Send), + ) -> Self { + Self { aggregation: AggregationCircuit::new(params, snarks, rng), has_prev_accumulator } + } +} + +impl CircuitExt for PublicAggregationCircuit { + fn num_instance(&self) -> Vec { + let prev_num = self + .aggregation + .snarks + .iter() + .map(|snark| snark.instances.iter().map(|instance| instance.len()).sum::()) + .sum::() + - self.aggregation.snarks.len() * 4 * LIMBS * usize::from(self.has_prev_accumulator); + vec![4 * LIMBS + prev_num] + } + + fn instances(&self) -> Vec> { + let start_idx = 4 * LIMBS * usize::from(self.has_prev_accumulator); + let instance = self + .aggregation + .instances + .iter() + .cloned() + .chain(self.aggregation.snarks.iter().flat_map(|snark| { + snark.instances.iter().enumerate().flat_map(|(i, instance)| { + instance[usize::from(i == 0) * start_idx..] + .iter() + .map(|v| value_to_option(*v).unwrap()) + }) + })) + .collect_vec(); + vec![instance] + } + + fn accumulator_indices() -> Option> { + Some((0..4 * LIMBS).map(|idx| (0, idx)).collect()) + } + + fn selectors(config: &Self::Config) -> Vec { + AggregationCircuit::selectors(config) + } +} + +impl Circuit for PublicAggregationCircuit { + type Config = AggregationConfig; + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self { + aggregation: self.aggregation.without_witnesses(), + has_prev_accumulator: self.has_prev_accumulator, + } + } + + fn configure(meta: &mut plonk::ConstraintSystem) -> Self::Config { + AggregationCircuit::configure(meta) + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), plonk::Error> { + #[cfg(feature = "display")] + let witness_time = start_timer!(|| { "synthesize | EVM verifier" }); + config.range().load_lookup_table(&mut layouter).expect("load range lookup table"); + let mut first_pass = halo2_base::SKIP_FIRST_PASS; + let mut instances = vec![]; + layouter + .assign_region( + || "", + |region| { + if first_pass { + first_pass = false; + return Ok(()); + } + let ctx = Context::new( + region, + ContextParams { + max_rows: config.gate().max_rows, + num_context_ids: 1, + fixed_columns: config.gate().constants.clone(), + }, + ); + + let ecc_chip = config.ecc_chip(); + let loader = Halo2Loader::new(ecc_chip, ctx); + let (prev_instances, acc) = aggregate::>( + &self.aggregation.svk, + &loader, + &self.aggregation.snarks, + self.aggregation.as_proof(), + ); + + // accumulator + instances.extend(flatten_accumulator(acc).iter().map(|a| a.cell().clone())); + // prev instances except accumulators + let start_idx = 4 * LIMBS * usize::from(self.has_prev_accumulator); + for prev_instance in prev_instances { + instances + .extend(prev_instance[start_idx..].iter().map(|a| a.cell().clone())); + } + + config.range().finalize(&mut loader.ctx_mut()); + #[cfg(feature = "display")] + loader.ctx_mut().print_stats(&["Range"]); + Ok(()) + }, + ) + .unwrap(); + // Expose instances + for (i, cell) in instances.into_iter().enumerate() { + layouter.constrain_instance(cell, config.instance, i)?; + } + #[cfg(feature = "display")] + end_timer!(witness_time); + Ok(()) + } +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/circuit_ext.rs b/snark-verifier-sdk/src/circuit_ext.rs new file mode 100644 index 00000000..1cb602c0 --- /dev/null +++ b/snark-verifier-sdk/src/circuit_ext.rs @@ -0,0 +1,28 @@ +use halo2_base::halo2_proofs::{ + arithmetic::Field, + plonk::{Circuit, Selector}, +}; + +/// Circuit Extension trait that exposes related APIs. +pub trait CircuitExt: Circuit { + /// Return the number of instances of the circuit. + /// This may depend on extra circuit parameters but NOT on private witnesses. + fn num_instance(&self) -> Vec { + vec![] + } + + /// Expose the instance for the circuit + fn instances(&self) -> Vec> { + vec![] + } + + /// The indices of the accumulator + fn accumulator_indices() -> Option> { + None + } + + /// Output the simple selector columns (before selector compression) of the circuit + fn selectors(_: &Self::Config) -> Vec { + vec![] + } +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/evm.rs b/snark-verifier-sdk/src/evm.rs index d3b745fb..0b24db4c 100644 --- a/snark-verifier-sdk/src/evm.rs +++ b/snark-verifier-sdk/src/evm.rs @@ -185,4 +185,4 @@ pub fn write_calldata(instances: &[Vec], proof: &[u8], path: &Path) -> io::R let calldata = hex::encode(calldata); fs::write(path, &calldata)?; Ok(calldata) -} +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/evm_api.rs b/snark-verifier-sdk/src/evm_api.rs new file mode 100644 index 00000000..c3fbbb17 --- /dev/null +++ b/snark-verifier-sdk/src/evm_api.rs @@ -0,0 +1,211 @@ +use crate::{circuit_ext::CircuitExt, types::Plonk}; + +#[cfg(feature = "display")] +use ark_std::end_timer; +#[cfg(feature = "display")] +use ark_std::start_timer; +use ethereum_types::Address; +use halo2_base::halo2_proofs::{ + halo2curves::bn256::{Bn256, Fq, Fr, G1Affine}, + plonk::{create_proof, verify_proof, Circuit, ProvingKey, VerifyingKey}, + poly::{ + commitment::{ParamsProver, Prover, Verifier}, + kzg::{ + commitment::{KZGCommitmentScheme, ParamsKZG}, + msm::DualMSM, + multiopen::{ProverGWC, ProverSHPLONK, VerifierGWC, VerifierSHPLONK}, + strategy::{AccumulatorStrategy, GuardKZG}, + }, + VerificationStrategy, + }, + transcript::{TranscriptReadBuffer, TranscriptWriterBuffer}, +}; +use itertools::Itertools; +use rand::Rng; +pub use snark_verifier::loader::evm::encode_calldata; +use snark_verifier::{ + loader::evm::{compile_yul, EvmLoader, ExecutorBuilder}, + pcs::{ + kzg::{Bdfg21, Gwc19, Kzg, KzgAccumulator, KzgDecidingKey, KzgSuccinctVerifyingKey}, + Decider, MultiOpenScheme, PolynomialCommitmentScheme, + }, + system::halo2::{compile, transcript::evm::EvmTranscript, Config}, + verifier::PlonkVerifier, +}; +use std::{fs, path::Path, rc::Rc}; + + +pub trait EvmKzgAccumulationScheme = PolynomialCommitmentScheme< + G1Affine, + Rc, + VerifyingKey = KzgSuccinctVerifyingKey, + Output = KzgAccumulator>, + > + AccumulationScheme< + G1Affine, + Rc, + VerifyingKey = KzgAsVerifyingKey, + Accumulator = KzgAccumulator>, + > + AccumulationDecider, DecidingKey = KzgDecidingKey>; + + +/// Generates a proof for evm verification using either SHPLONK or GWC proving method. Uses Keccak for Fiat-Shamir. +pub fn gen_evm_proof<'params, C, P, V>( + params: &'params ParamsKZG, + pk: &'params ProvingKey, + circuit: C, + instances: Vec>, + rng: &mut (impl Rng + Send), +) -> Vec +where + C: Circuit, + P: Prover<'params, KZGCommitmentScheme>, + V: Verifier< + 'params, + KZGCommitmentScheme, + Guard = GuardKZG<'params, Bn256>, + MSMAccumulator = DualMSM<'params, Bn256>, + >, +{ + /* + #[cfg(debug_assertions)] + { + use halo2_base::halo2_proofs::{dev::MockProver, poly::commitment::Params}; + MockProver::run(params.k(), &circuit, instances.clone()).unwrap().assert_satisfied_par(); + } + */ + + let instances = instances.iter().map(|instances| instances.as_slice()).collect_vec(); + + #[cfg(feature = "display")] + let proof_time = start_timer!(|| "Create EVM proof"); + let proof = { + let mut transcript = TranscriptWriterBuffer::<_, G1Affine, _>::init(Vec::new()); + create_proof::, P, _, _, EvmTranscript<_, _, _, _>, _>( + params, + pk, + &[circuit], + &[instances.as_slice()], + rng, + &mut transcript, + ) + .unwrap(); + transcript.finalize() + }; + #[cfg(feature = "display")] + end_timer!(proof_time); + + let accept = { + let mut transcript = TranscriptReadBuffer::<_, G1Affine, _>::init(proof.as_slice()); + VerificationStrategy::<_, V>::finalize( + verify_proof::<_, V, _, EvmTranscript<_, _, _, _>, _>( + params.verifier_params(), + pk.get_vk(), + AccumulatorStrategy::new(params.verifier_params()), + &[instances.as_slice()], + &mut transcript, + ) + .unwrap(), + ) + }; + assert!(accept); + + proof +} + +pub fn gen_evm_proof_gwc<'params, C: Circuit>( + params: &'params ParamsKZG, + pk: &'params ProvingKey, + circuit: C, + instances: Vec>, + rng: &mut (impl Rng + Send), +) -> Vec { + gen_evm_proof::, VerifierGWC<_>>(params, pk, circuit, instances, rng) +} + +pub fn gen_evm_proof_shplonk<'params, C: Circuit>( + params: &'params ParamsKZG, + pk: &'params ProvingKey, + circuit: C, + instances: Vec>, + rng: &mut (impl Rng + Send), +) -> Vec { + gen_evm_proof::, VerifierSHPLONK<_>>(params, pk, circuit, instances, rng) +} + +pub fn gen_evm_verifier( + params: &ParamsKZG, + vk: &VerifyingKey, + num_instance: Vec, + path: Option<&Path>, +) -> Vec +where + C: CircuitExt, + AS: EvmKzgAccumulationScheme, +{ + let svk = params.get_g()[0].into(); + let dk = (params.g2(), params.s_g2()).into(); + let protocol = compile( + params, + vk, + Config::kzg() + .with_num_instance(num_instance.clone()) + .with_accumulator_indices(C::accumulator_indices()), + ); + + let loader = EvmLoader::new::(); + let protocol = protocol.loaded(&loader); + let mut transcript = EvmTranscript::<_, Rc, _, _>::new(&loader); + + let instances = transcript.load_instances(num_instance); + let proof = Plonk::::read_proof(&svk, &protocol, &instances, &mut transcript); + Plonk::::verify(&svk, &dk, &protocol, &instances, &proof); + + let yul_code = loader.yul_code(); + let byte_code = compile_yul(&yul_code); + if let Some(path) = path { + path.parent().and_then(|dir| fs::create_dir_all(dir).ok()).unwrap(); + fs::write(path, yul_code).unwrap(); + } + byte_code +} + +pub fn gen_evm_verifier_gwc>( + params: &ParamsKZG, + vk: &VerifyingKey, + num_instance: Vec, + path: Option<&Path>, +) -> Vec { + gen_evm_verifier::>(params, vk, num_instance, path) +} + +pub fn gen_evm_verifier_shplonk>( + params: &ParamsKZG, + vk: &VerifyingKey, + num_instance: Vec, + path: Option<&Path>, +) -> Vec { + gen_evm_verifier::>(params, vk, num_instance, path) +} + +pub fn evm_verify(deployment_code: Vec, instances: Vec>, proof: Vec) { + let success = verify_evm_proof(deployment_code, instances, proof); + + assert!(success); +} + +pub fn verify_evm_proof(deployment_code: Vec, instances: Vec>, proof: Vec) -> bool { + let calldata = encode_calldata(&instances, &proof); + verify_evm_calldata(deployment_code, calldata) +} + +pub fn verify_evm_calldata(deployment_code: Vec, calldata: Vec) -> bool { + let mut evm = ExecutorBuilder::default().with_gas_limit(u64::MAX.into()).build(); + + let caller = Address::from_low_u64_be(0xfe); + let verifier = evm.deploy(caller, deployment_code.into(), 0.into()).address.unwrap(); + let result = evm.call_raw(caller, verifier, calldata.into(), 0.into()); + + log::info!("gas used: {}", result.gas_used); + + !result.reverted +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/file_io.rs b/snark-verifier-sdk/src/file_io.rs new file mode 100644 index 00000000..bdc707cb --- /dev/null +++ b/snark-verifier-sdk/src/file_io.rs @@ -0,0 +1,82 @@ +use std::{ + fs::{write, File}, + io::{BufReader, BufWriter}, + path::Path, +}; + +use halo2_base::halo2_proofs::{ + halo2curves::bn256::{Fr, G1Affine}, + plonk::{Circuit, ProvingKey}, + SerdeFormat, +}; +use itertools::Itertools; +use snark_verifier::loader::evm::encode_calldata; + +use crate::Snark; + +/// Read instances from the disk +pub fn read_instances(path: impl AsRef) -> Result>, bincode::Error> { + let f = File::open(path)?; + let reader = BufReader::new(f); + let instances: Vec> = bincode::deserialize_from(reader)?; + instances + .into_iter() + .map(|instance_column| { + instance_column + .iter() + .map(|bytes| { + Option::from(Fr::from_bytes(bytes)).ok_or(Box::new(bincode::ErrorKind::Custom( + "Invalid finite field point".to_owned(), + ))) + }) + .collect::, _>>() + }) + .collect() +} + +/// Write instances to the disk +pub fn write_instances(instances: &[&[Fr]], path: impl AsRef) { + let instances: Vec> = instances + .iter() + .map(|instance_column| instance_column.iter().map(|x| x.to_bytes()).collect_vec()) + .collect_vec(); + let f = BufWriter::new(File::create(path).unwrap()); + bincode::serialize_into(f, &instances).unwrap(); +} + +/// Read proving key from the disk +pub fn read_pk>(path: &Path) -> std::io::Result> { + let f = File::open(path)?; + #[cfg(feature = "display")] + let read_time = start_timer!(|| format!("Reading pkey from {path:?}")); + + // BufReader is indeed MUCH faster than Read + let mut bufreader = BufReader::new(f); + // But it's even faster to load the whole file into memory first and then process, + // HOWEVER this requires twice as much memory to initialize + // let initial_buffer_size = f.metadata().map(|m| m.len() as usize + 1).unwrap_or(0); + // let mut bufreader = Vec::with_capacity(initial_buffer_size); + // f.read_to_end(&mut bufreader)?; + let pk = ProvingKey::read::<_, C>(&mut bufreader, SerdeFormat::RawBytesUnchecked).unwrap(); + + #[cfg(feature = "display")] + end_timer!(read_time); + + Ok(pk) +} + +/// Tries to deserialize a SNARK from the specified `path` using `bincode`. +/// +/// WARNING: The user must keep track of whether the SNARK was generated using the GWC or SHPLONK multi-open scheme. +pub fn read_snark(path: impl AsRef) -> Result { + let f = File::open(path).map_err(Box::::from)?; + bincode::deserialize_from(f) +} + +/// Write the calldata to disk +pub fn write_calldata(instances: &[Vec], proof: &[u8], path: &Path) -> std::io::Result { + let calldata = encode_calldata(instances, proof); + let calldata = hex::encode(calldata); + write(path, &calldata)?; + Ok(calldata) +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/halo2.rs b/snark-verifier-sdk/src/halo2.rs index aa5f8810..52c87407 100644 --- a/snark-verifier-sdk/src/halo2.rs +++ b/snark-verifier-sdk/src/halo2.rs @@ -299,6 +299,7 @@ where impl> Circuit for CsProxy { type Config = C::Config; type FloorPlanner = C::FloorPlanner; + type Params = (); fn without_witnesses(&self) -> Self { CsProxy(PhantomData) diff --git a/snark-verifier-sdk/src/halo2_api.rs b/snark-verifier-sdk/src/halo2_api.rs new file mode 100644 index 00000000..2a444b92 --- /dev/null +++ b/snark-verifier-sdk/src/halo2_api.rs @@ -0,0 +1,322 @@ +use std::{ + fs::{self, File}, + io::BufWriter, + path::Path, +}; + +use crate::{ + circuit_ext::CircuitExt, + file_io::{read_pk, read_snark}, + read_instances, + types::{PoseidonTranscript, POSEIDON_SPEC}, + write_instances, Snark, +}; + +#[cfg(feature = "display")] +use ark_std::end_timer; +#[cfg(feature = "display")] +use ark_std::start_timer; +use halo2_base::halo2_proofs::{ + halo2curves::bn256::{Bn256, Fr, G1Affine}, + plonk::{create_proof, keygen_pk, keygen_vk, verify_proof, Circuit, ProvingKey, VerifyingKey}, + poly::{ + commitment::{ParamsProver, Prover, Verifier}, + kzg::{ + commitment::{KZGCommitmentScheme, ParamsKZG}, + msm::DualMSM, + multiopen::{ProverGWC, ProverSHPLONK, VerifierGWC, VerifierSHPLONK}, + strategy::{AccumulatorStrategy, GuardKZG, SingleStrategy}, + }, + VerificationStrategy, + }, + transcript::TranscriptReadBuffer, + SerdeFormat, {self}, +}; +use itertools::Itertools; +use rand::Rng; +use snark_verifier::{ + loader::native::NativeLoader, + system::halo2::{compile, Config}, +}; + +#[allow(clippy::let_and_return)] +pub fn gen_pk>( + params: &ParamsKZG, // TODO: read pk without params + circuit: &C, + path: Option<&Path>, +) -> ProvingKey { + if let Some(path) = path { + if let Ok(pk) = read_pk::(path) { + return pk; + } + } + #[cfg(feature = "display")] + let pk_time = start_timer!(|| "Generating vkey & pkey"); + + let vk = keygen_vk(params, circuit).unwrap(); + let pk = keygen_pk(params, vk, circuit).unwrap(); + + #[cfg(feature = "display")] + end_timer!(pk_time); + + if let Some(path) = path { + #[cfg(feature = "display")] + let write_time = start_timer!(|| format!("Writing pkey to {path:?}")); + + path.parent().and_then(|dir| fs::create_dir_all(dir).ok()).unwrap(); + let mut f = BufWriter::new(File::create(path).unwrap()); + pk.write(&mut f, SerdeFormat::RawBytesUnchecked).unwrap(); + + #[cfg(feature = "display")] + end_timer!(write_time); + } + pk +} + +/// Generates a native proof using either SHPLONK or GWC proving method. Uses Poseidon for Fiat-Shamir. +/// +/// Caches the instances and proof if `path = Some(instance_path, proof_path)` is specified. +pub fn gen_proof<'params, C, P, V>( + // TODO: pass Option<&'params ParamsKZG> but hard to get lifetimes to work with `Cow` + params: &'params ParamsKZG, + pk: &ProvingKey, + circuit: C, + instances: Vec>, + rng: &mut (impl Rng + Send), + path: Option<(&Path, &Path)>, +) -> Vec +where + C: Circuit, + P: Prover<'params, KZGCommitmentScheme>, + V: Verifier< + 'params, + KZGCommitmentScheme, + Guard = GuardKZG<'params, Bn256>, + MSMAccumulator = DualMSM<'params, Bn256>, + >, +{ + /* + #[cfg(debug_assertions)] + { + use halo2_proofs::poly::commitment::Params; + halo2_proofs::dev::MockProver::run(params.k(), &circuit, instances.clone()) + .unwrap() + .assert_satisfied_par(); + } + */ + + if let Some((instance_path, proof_path)) = path { + let cached_instances = read_instances(instance_path); + if matches!(cached_instances, Ok(tmp) if tmp == instances) && proof_path.exists() { + #[cfg(feature = "display")] + let read_time = start_timer!(|| format!("Reading proof from {proof_path:?}")); + + let proof = fs::read(proof_path).unwrap(); + + #[cfg(feature = "display")] + end_timer!(read_time); + return proof; + } + } + + let instances = instances.iter().map(Vec::as_slice).collect_vec(); + + #[cfg(feature = "display")] + let proof_time = start_timer!(|| "Create proof"); + + let mut transcript = + PoseidonTranscript::>::from_spec(vec![], POSEIDON_SPEC.clone()); + create_proof::<_, P, _, _, _, _>(params, pk, &[circuit], &[&instances], rng, &mut transcript) + .unwrap(); + let proof = transcript.finalize(); + + #[cfg(feature = "display")] + end_timer!(proof_time); + + if let Some((instance_path, proof_path)) = path { + write_instances(&instances, instance_path); + fs::write(proof_path, &proof).unwrap(); + } + + debug_assert!({ + let mut transcript_read = PoseidonTranscript::::new(proof.as_slice()); + VerificationStrategy::<_, V>::finalize( + verify_proof::<_, V, _, _, _>( + params.verifier_params(), + pk.get_vk(), + AccumulatorStrategy::new(params.verifier_params()), + &[instances.as_slice()], + &mut transcript_read, + ) + .unwrap(), + ) + }); + + proof +} + +/// Generates a native proof using original Plonk (GWC '19) multi-open scheme. Uses Poseidon for Fiat-Shamir. +/// +/// Caches the instances and proof if `path = Some(instance_path, proof_path)` is specified. +pub fn gen_proof_gwc>( + params: &ParamsKZG, + pk: &ProvingKey, + circuit: C, + instances: Vec>, + rng: &mut (impl Rng + Send), + path: Option<(&Path, &Path)>, +) -> Vec { + gen_proof::, VerifierGWC<_>>(params, pk, circuit, instances, rng, path) +} + +/// Generates a native proof using SHPLONK multi-open scheme. Uses Poseidon for Fiat-Shamir. +/// +/// Caches the instances and proof if `path` is specified. +pub fn gen_proof_shplonk>( + params: &ParamsKZG, + pk: &ProvingKey, + circuit: C, + instances: Vec>, + rng: &mut (impl Rng + Send), + path: Option<(&Path, &Path)>, +) -> Vec { + gen_proof::, VerifierSHPLONK<_>>(params, pk, circuit, instances, rng, path) +} + +/// Generates a SNARK using either SHPLONK or GWC multi-open scheme. Uses Poseidon for Fiat-Shamir. +/// +/// Tries to first deserialize from / later serialize the entire SNARK into `path` if specified. +/// Serialization is done using `bincode`. +pub fn gen_snark<'params, ConcreteCircuit, P, V>( + params: &'params ParamsKZG, + pk: &ProvingKey, + circuit: ConcreteCircuit, + rng: &mut (impl Rng + Send), + path: Option>, +) -> Snark +where + ConcreteCircuit: CircuitExt, + P: Prover<'params, KZGCommitmentScheme>, + V: Verifier< + 'params, + KZGCommitmentScheme, + Guard = GuardKZG<'params, Bn256>, + MSMAccumulator = DualMSM<'params, Bn256>, + >, +{ + if let Some(path) = &path { + if let Ok(snark) = read_snark(path) { + return snark; + } + } + let protocol = compile( + params, + pk.get_vk(), + Config::kzg() + .with_num_instance(circuit.num_instance()) + .with_accumulator_indices(ConcreteCircuit::accumulator_indices()), + ); + + let instances = circuit.instances(); + let proof = + gen_proof::(params, pk, circuit, instances.clone(), rng, None); + + let snark = Snark::new(protocol, instances, proof); + if let Some(path) = &path { + let f = File::create(path).unwrap(); + #[cfg(feature = "display")] + let write_time = start_timer!(|| "Write SNARK"); + bincode::serialize_into(f, &snark).unwrap(); + #[cfg(feature = "display")] + end_timer!(write_time); + } + snark +} + +/// Generates a SNARK using GWC multi-open scheme. Uses Poseidon for Fiat-Shamir. +/// +/// Tries to first deserialize from / later serialize the entire SNARK into `path` if specified. +/// Serialization is done using `bincode`. +pub fn gen_snark_gwc>( + params: &ParamsKZG, + pk: &ProvingKey, + circuit: ConcreteCircuit, + rng: &mut (impl Rng + Send), + path: Option>, +) -> Snark { + gen_snark::, VerifierGWC<_>>(params, pk, circuit, rng, path) +} + +/// Generates a SNARK using SHPLONK multi-open scheme. Uses Poseidon for Fiat-Shamir. +/// +/// Tries to first deserialize from / later serialize the entire SNARK into `path` if specified. +/// Serialization is done using `bincode`. +pub fn gen_snark_shplonk>( + params: &ParamsKZG, + pk: &ProvingKey, + circuit: ConcreteCircuit, + rng: &mut (impl Rng + Send), + path: Option>, +) -> Snark { + gen_snark::, VerifierSHPLONK<_>>( + params, pk, circuit, rng, path, + ) +} + +/// Verifies a native proof using either SHPLONK or GWC proving method. Uses Poseidon for Fiat-Shamir. +/// +pub fn verify_snark<'params, ConcreteCircuit, V>( + verifier_params: &'params ParamsKZG, + snark: Snark, + vk: &VerifyingKey, +) -> bool +where + ConcreteCircuit: CircuitExt, + V: Verifier< + 'params, + KZGCommitmentScheme, + Guard = GuardKZG<'params, Bn256>, + MSMAccumulator = DualMSM<'params, Bn256>, + >, +{ + let mut transcript: PoseidonTranscript<_, _> = + TranscriptReadBuffer::<_, G1Affine, _>::init(snark.proof.as_slice()); + let strategy = SingleStrategy::new(verifier_params); + let instance_slice = snark.instances.iter().map(|x| &x[..]).collect::>(); + match verify_proof::<_, V, _, _, _>( + verifier_params, + vk, + strategy, + &[instance_slice.as_slice()], + &mut transcript, + ) { + Ok(_p) => true, + Err(_e) => false, + } +} + +/// Verifies a native proof using SHPLONK proving method. Uses Poseidon for Fiat-Shamir. +/// +pub fn verify_snark_shplonk( + verifier_params: &ParamsKZG, + snark: Snark, + vk: &VerifyingKey, +) -> bool +where + ConcreteCircuit: CircuitExt, +{ + verify_snark::>(verifier_params, snark, vk) +} + +/// Verifies a native proof using GWC proving method. Uses Poseidon for Fiat-Shamir. +/// +pub fn verify_snark_gwc( + verifier_params: &ParamsKZG, + snark: Snark, + vk: &VerifyingKey, +) -> bool +where + ConcreteCircuit: CircuitExt, +{ + verify_snark::>(verifier_params, snark, vk) +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/param.rs b/snark-verifier-sdk/src/param.rs new file mode 100644 index 00000000..de3d0194 --- /dev/null +++ b/snark-verifier-sdk/src/param.rs @@ -0,0 +1,10 @@ +/// Number of limbs for non-native field decomposition +pub const LIMBS: usize = 3; +/// Number of bits for each limb. +pub const BITS: usize = 88; + +// Poseidon parameters +pub(crate) const T: usize = 5; +pub(crate) const RATE: usize = 4; +pub(crate) const R_F: usize = 8; +pub(crate) const R_P: usize = 60; \ No newline at end of file diff --git a/snark-verifier-sdk/src/snark.rs b/snark-verifier-sdk/src/snark.rs new file mode 100644 index 00000000..fdb5c351 --- /dev/null +++ b/snark-verifier-sdk/src/snark.rs @@ -0,0 +1,68 @@ +use halo2_base::halo2_proofs; +use halo2_proofs::{ + circuit::Value, + halo2curves::bn256::{Fr, G1Affine}, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use snark_verifier::PlonkProtocol; + +mod mock; + +pub use mock::gen_dummy_snark; + +/// A Snark struct is all one may need to generate witnesses for an aggregation circuit. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Snark { + pub protocol: PlonkProtocol, + pub instances: Vec>, + pub proof: Vec, +} + +impl Snark { + pub fn new(protocol: PlonkProtocol, instances: Vec>, proof: Vec) -> Self { + Self { protocol, instances, proof } + } +} + +impl From for SnarkWitness { + fn from(snark: Snark) -> Self { + Self { + protocol: snark.protocol, + instances: snark + .instances + .into_iter() + .map(|instances| instances.into_iter().map(Value::known).collect_vec()) + .collect(), + proof: Value::known(snark.proof), + } + } +} + +/// A SnarkWitness struct is a snark converted to witness. +#[derive(Clone, Debug)] +pub struct SnarkWitness { + pub protocol: PlonkProtocol, + pub instances: Vec>>, + pub proof: Value>, +} + +impl SnarkWitness { + /// Initialize an empty SnarkWitness with a same struct as self. + pub fn without_witnesses(&self) -> Self { + SnarkWitness { + protocol: self.protocol.clone(), + instances: self + .instances + .iter() + .map(|instances| vec![Value::unknown(); instances.len()]) + .collect(), + proof: Value::unknown(), + } + } + + /// Expose the proof of the witness. + pub fn proof(&self) -> Value<&[u8]> { + self.proof.as_ref().map(Vec::as_slice) + } +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/snark/mock.rs b/snark-verifier-sdk/src/snark/mock.rs new file mode 100644 index 00000000..edfc4d4b --- /dev/null +++ b/snark-verifier-sdk/src/snark/mock.rs @@ -0,0 +1,108 @@ +//! Mock Snark +use crate::{circuit_ext::CircuitExt, types::PoseidonTranscript}; + +use super::Snark; +#[cfg(feature = "display")] +use ark_std::end_timer; +#[cfg(feature = "display")] +use ark_std::start_timer; +use halo2_base::halo2_proofs::{self}; +use halo2_proofs::{ + circuit::Layouter, + halo2curves::{ + bn256::{Bn256, Fr, G1Affine}, + group::ff::Field, + }, + plonk::{keygen_vk, Circuit, ConstraintSystem, Error, VerifyingKey}, + poly::kzg::commitment::ParamsKZG, +}; +use snark_verifier::{ + cost::CostEstimation, + loader::native::NativeLoader, + pcs::{ + MultiOpenScheme, {self}, + }, + system::halo2::{compile, Config}, + util::transcript::TranscriptWrite, + verifier::PlonkProof, +}; +use std::marker::PhantomData; + +struct CsProxy(PhantomData<(F, C)>); + +impl> Circuit for CsProxy { + type Config = C::Config; + type FloorPlanner = C::FloorPlanner; + + fn without_witnesses(&self) -> Self { + CsProxy(PhantomData) + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + C::configure(meta) + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + // when `C` has simple selectors, we tell `CsProxy` not to over-optimize the selectors (e.g., compressing them all into one) by turning all selectors on in the first row + // currently this only works if all simple selector columns are used in the actual circuit and there are overlaps amongst all enabled selectors (i.e., the actual circuit will not optimize constraint system further) + layouter.assign_region( + || "", + |mut region| { + for q in C::selectors(&config).iter() { + q.enable(&mut region, 0)?; + } + Ok(()) + }, + )?; + Ok(()) + } +} + +/// Generate a Snark for a ConcreteCircuit +pub fn gen_dummy_snark( + params: &ParamsKZG, + vk: Option<&VerifyingKey>, + num_instance: Vec, +) -> Snark +where + ConcreteCircuit: CircuitExt, + MOS: MultiOpenScheme + + CostEstimation>>, +{ + let dummy_vk = vk + .is_none() + .then(|| keygen_vk(params, &CsProxy::(PhantomData)).unwrap()); + let protocol = compile( + params, + vk.or(dummy_vk.as_ref()).unwrap(), + Config::kzg() + .with_num_instance(num_instance.clone()) + .with_accumulator_indices(ConcreteCircuit::accumulator_indices()), + ); + let instances = num_instance.into_iter().map(|n| vec![Fr::default(); n]).collect(); + let proof = { + let mut transcript = PoseidonTranscript::::new(Vec::new()); + for _ in 0..protocol + .num_witness + .iter() + .chain(Some(&protocol.quotient.num_chunk())) + .sum::() + { + transcript.write_ec_point(G1Affine::default()).unwrap(); + } + for _ in 0..protocol.evaluations.len() { + transcript.write_scalar(Fr::default()).unwrap(); + } + let queries = PlonkProof::::empty_queries(&protocol); + for _ in 0..MOS::estimate_cost(&queries).num_commitment { + transcript.write_ec_point(G1Affine::default()).unwrap(); + } + transcript.finalize() + }; + + Snark::new(protocol, instances, proof) +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/snarks.rs b/snark-verifier-sdk/src/snarks.rs new file mode 100644 index 00000000..0ccfeb02 --- /dev/null +++ b/snark-verifier-sdk/src/snarks.rs @@ -0,0 +1,68 @@ +use halo2_base::halo2_proofs; +use halo2_proofs::{ + circuit::Value, + halo2curves::bn256::{Fr, G1Affine}, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use snark_verifier::Protocol; + +mod mock; + +pub use mock::gen_dummy_snark; + +/// A Snark struct is all one may need to generate witnesses for an aggregation circuit. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Snark { + pub protocol: Protocol, + pub instances: Vec>, + pub proof: Vec, +} + +impl Snark { + pub fn new(protocol: Protocol, instances: Vec>, proof: Vec) -> Self { + Self { protocol, instances, proof } + } +} + +impl From for SnarkWitness { + fn from(snark: Snark) -> Self { + Self { + protocol: snark.protocol, + instances: snark + .instances + .into_iter() + .map(|instances| instances.into_iter().map(Value::known).collect_vec()) + .collect(), + proof: Value::known(snark.proof), + } + } +} + +/// A SnarkWitness struct is a snark converted to witness. +#[derive(Clone, Debug)] +pub struct SnarkWitness { + pub protocol: Protocol, + pub instances: Vec>>, + pub proof: Value>, +} + +impl SnarkWitness { + /// Initialize an empty SnarkWitness with a same struct as self. + pub fn without_witnesses(&self) -> Self { + SnarkWitness { + protocol: self.protocol.clone(), + instances: self + .instances + .iter() + .map(|instances| vec![Value::unknown(); instances.len()]) + .collect(), + proof: Value::unknown(), + } + } + + /// Expose the proof of the witness. + pub fn proof(&self) -> Value<&[u8]> { + self.proof.as_ref().map(Vec::as_slice) + } +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/tests/evm_verifier.rs b/snark-verifier-sdk/src/tests/evm_verifier.rs new file mode 100644 index 00000000..047bb8bc --- /dev/null +++ b/snark-verifier-sdk/src/tests/evm_verifier.rs @@ -0,0 +1,34 @@ +use super::TestCircuit1; +use crate::{ + evm_api::{evm_verify, gen_evm_proof_shplonk, gen_evm_verifier}, + halo2_api::gen_pk, + CircuitExt, +}; +use ark_std::test_rng; +use halo2_base::halo2_proofs; +use halo2_proofs::halo2curves::bn256::Bn256; +use snark_verifier::{ + loader::halo2::halo2_ecc::halo2_base::utils::fs::gen_srs, + pcs::kzg::{Bdfg21, Kzg}, +}; + +#[test] +fn test_evm_verification() { + std::env::set_var("VERIFY_CONFIG", "./configs/verify_circuit.config"); + + let mut rng = test_rng(); + let params = gen_srs(8); + + let circuit = TestCircuit1::rand(&mut rng); + let pk = gen_pk(¶ms, &circuit, None); + let deployment_code = gen_evm_verifier::>( + ¶ms, + pk.get_vk(), + circuit.num_instance(), + None, + ); + + let instances = circuit.instances(); + let proof = gen_evm_proof_shplonk(¶ms, &pk, circuit.clone(), instances.clone(), &mut rng); + evm_verify(deployment_code.clone(), circuit.instances(), proof) +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/tests/mod.rs b/snark-verifier-sdk/src/tests/mod.rs new file mode 100644 index 00000000..090ff9c4 --- /dev/null +++ b/snark-verifier-sdk/src/tests/mod.rs @@ -0,0 +1,58 @@ +use halo2_base::halo2_proofs; +use halo2_proofs::{ + halo2curves::bn256::Fr, + plonk::{Advice, Column, ConstraintSystem, Fixed, Instance}, + poly::Rotation, +}; +use test_circuit_1::TestCircuit1; +use test_circuit_2::TestCircuit2; + +mod evm_verifier; +mod single_layer_aggregation; +mod test_circuit_1; +mod test_circuit_2; +mod two_layer_aggregation; + +#[derive(Clone, Copy)] +pub struct StandardPlonkConfig { + a: Column, + b: Column, + c: Column, + q_a: Column, + q_b: Column, + q_c: Column, + q_ab: Column, + constant: Column, + #[allow(dead_code)] + instance: Column, +} + +impl StandardPlonkConfig { + fn configure(meta: &mut ConstraintSystem) -> Self { + let [a, b, c] = [(); 3].map(|_| meta.advice_column()); + let [q_a, q_b, q_c, q_ab, constant] = [(); 5].map(|_| meta.fixed_column()); + let instance = meta.instance_column(); + + [a, b, c].map(|column| meta.enable_equality(column)); + + meta.create_gate( + "q_a·a + q_b·b + q_c·c + q_ab·a·b + constant + instance = 0", + |meta| { + let [a, b, c] = [a, b, c].map(|column| meta.query_advice(column, Rotation::cur())); + let [q_a, q_b, q_c, q_ab, constant] = [q_a, q_b, q_c, q_ab, constant] + .map(|column| meta.query_fixed(column, Rotation::cur())); + let instance = meta.query_instance(instance, Rotation::cur()); + Some( + q_a * a.clone() + + q_b * b.clone() + + q_c * c + + q_ab * a * b + + constant + + instance, + ) + }, + ); + + StandardPlonkConfig { a, b, c, q_a, q_b, q_c, q_ab, constant, instance } + } +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/tests/single_layer_aggregation.rs b/snark-verifier-sdk/src/tests/single_layer_aggregation.rs new file mode 100644 index 00000000..ead5a77b --- /dev/null +++ b/snark-verifier-sdk/src/tests/single_layer_aggregation.rs @@ -0,0 +1,91 @@ +use super::{TestCircuit1, TestCircuit2}; +use crate::{ + aggregation::aggregation_circuit::AggregationCircuit, + evm_api::{evm_verify, gen_evm_proof_shplonk, gen_evm_verifier}, + halo2_api::{gen_pk, gen_snark_shplonk}, + CircuitExt, +}; +use ark_std::test_rng; +use halo2_base::halo2_proofs; +use halo2_proofs::{halo2curves::bn256::Bn256, poly::commitment::Params}; +use snark_verifier::{ + loader::halo2::halo2_ecc::halo2_base::utils::fs::gen_srs, + pcs::kzg::{Bdfg21, Kzg}, +}; +use std::path::Path; + +#[test] +fn test_shplonk_then_sphplonk_with_evm_verification() { + std::env::set_var("VERIFY_CONFIG", "./configs/example_evm_accumulator.config"); + let k = 8; + let k_agg = 21; + + let mut rng = test_rng(); + let params_outer = gen_srs(k_agg); + let params_inner = { + let mut params = params_outer.clone(); + params.downsize(k); + params + }; + + // Proof for circuit 1 + let circuit_1 = TestCircuit1::rand(&mut rng); + let pk_inner_1 = gen_pk(¶ms_inner, &circuit_1, Some(Path::new("data/inner_1.pkey"))); + let snarks_1 = gen_snark_shplonk( + ¶ms_inner, + &pk_inner_1, + circuit_1.clone(), + &mut rng, + Some(Path::new("data/inner_1.snark")), + ); + println!("finished snark generation for circuit 1"); + + // Another Proof for circuit 1 + let circuit_2 = TestCircuit1::rand(&mut rng); + let pk_inner_2 = gen_pk(¶ms_inner, &circuit_2, Some(Path::new("data/inner_2.pkey"))); + let snarks_2 = gen_snark_shplonk( + ¶ms_inner, + &pk_inner_2, + circuit_1.clone(), + &mut rng, + Some(Path::new("data/inner_2.snark")), + ); + println!("finished snark generation for circuit 1"); + + // Proof for circuit 2 + let circuit_3 = TestCircuit2::rand(&mut rng); + let pk_inner_3 = gen_pk(¶ms_inner, &circuit_1, Some(Path::new("data/inner_3.pkey"))); + let snarks_3 = gen_snark_shplonk( + ¶ms_inner, + &pk_inner_3, + circuit_3.clone(), + &mut rng, + Some(Path::new("data/inner_3.snark")), + ); + println!("finished snark generation for circuit 1"); + + // aggregation circuit + let snarks = vec![snarks_1, snarks_2, snarks_3]; + let agg_circuit = AggregationCircuit::new(¶ms_outer, snarks, &mut rng); + let pk_outer = gen_pk(¶ms_outer, &agg_circuit, Some(Path::new("data/outer.pkey"))); + println!("finished outer pk generation"); + let instances = agg_circuit.instances(); + let proof = gen_evm_proof_shplonk( + ¶ms_outer, + &pk_outer, + agg_circuit.clone(), + instances.clone(), + &mut rng, + ); + println!("finished aggregation generation"); + + let deployment_code = gen_evm_verifier::>( + ¶ms_outer, + pk_outer.get_vk(), + agg_circuit.num_instance(), + Some(Path::new("data/single_layer_recur.sol")), + ); + + println!("finished bytecode generation"); + evm_verify(deployment_code, instances, proof) +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/tests/test_circuit_1.rs b/snark-verifier-sdk/src/tests/test_circuit_1.rs new file mode 100644 index 00000000..36d55680 --- /dev/null +++ b/snark-verifier-sdk/src/tests/test_circuit_1.rs @@ -0,0 +1,73 @@ +//! A dummy circuit for testing. +use super::StandardPlonkConfig; +use crate::CircuitExt; +use halo2_base::halo2_proofs; +use halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner, Value}, + halo2curves::bn256::Fr, + plonk::{Circuit, ConstraintSystem, Error}, +}; +use rand::RngCore; + +#[derive(Clone, Default)] +pub struct TestCircuit1(Fr); + +impl TestCircuit1 { + pub fn rand(mut rng: R) -> Self { + Self(Fr::from(rng.next_u32() as u64)) + } +} + +impl CircuitExt for TestCircuit1 { + fn num_instance(&self) -> Vec { + vec![1] + } + + fn instances(&self) -> Vec> { + vec![vec![self.0]] + } +} + +impl Circuit for TestCircuit1 { + type Config = StandardPlonkConfig; + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self::default() + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + meta.set_minimum_degree(4); + StandardPlonkConfig::configure(meta) + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + layouter.assign_region( + || "", + |mut region| { + region.assign_advice(|| "", config.a, 0, || Value::known(self.0))?; + region.assign_fixed(|| "", config.q_a, 0, || Value::known(-Fr::one()))?; + region.assign_advice(|| "", config.a, 1, || Value::known(-Fr::from(5)))?; + for (idx, column) in + (1..).zip([config.q_a, config.q_b, config.q_c, config.q_ab, config.constant]) + { + region.assign_fixed( + || "", + column, + 1, + || Value::known(Fr::from(2 * idx as u64)), + )?; + } + let a = region.assign_advice(|| "", config.a, 2, || Value::known(Fr::one()))?; + a.copy_advice(|| "", &mut region, config.b, 3)?; + a.copy_advice(|| "", &mut region, config.c, 4)?; + + Ok(()) + }, + ) + } +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/tests/test_circuit_2.rs b/snark-verifier-sdk/src/tests/test_circuit_2.rs new file mode 100644 index 00000000..4fb23dad --- /dev/null +++ b/snark-verifier-sdk/src/tests/test_circuit_2.rs @@ -0,0 +1,68 @@ +//! Another dummy circuit for testing. +use super::StandardPlonkConfig; +use crate::CircuitExt; +use halo2_base::halo2_proofs; +use halo2_proofs::{ + circuit::{Layouter, SimpleFloorPlanner, Value}, + halo2curves::bn256::Fr, + plonk::{Circuit, ConstraintSystem, Error}, +}; +use rand::RngCore; + +#[derive(Clone, Default)] +pub struct TestCircuit2(Fr); + +impl TestCircuit2 { + pub fn rand(mut rng: R) -> Self { + Self(Fr::from(rng.next_u32() as u64)) + } +} + +impl CircuitExt for TestCircuit2 { + fn num_instance(&self) -> Vec { + vec![1] + } + + fn instances(&self) -> Vec> { + vec![vec![self.0]] + } +} + +impl Circuit for TestCircuit2 { + type Config = StandardPlonkConfig; + type FloorPlanner = SimpleFloorPlanner; + + fn without_witnesses(&self) -> Self { + Self::default() + } + + fn configure(meta: &mut ConstraintSystem) -> Self::Config { + meta.set_minimum_degree(4); + StandardPlonkConfig::configure(meta) + } + + fn synthesize( + &self, + config: Self::Config, + mut layouter: impl Layouter, + ) -> Result<(), Error> { + layouter.assign_region( + || "", + |mut region| { + region.assign_advice(|| "", config.a, 0, || Value::known(self.0))?; + region.assign_fixed(|| "", config.q_a, 0, || Value::known(-Fr::one()))?; + region.assign_advice(|| "", config.a, 1, || Value::known(-Fr::from(5u64)))?; + for (idx, column) in + (1..).zip([config.q_a, config.q_b, config.q_c, config.q_ab, config.constant]) + { + region.assign_fixed(|| "", column, 1, || Value::known(Fr::from(idx as u64)))?; + } + let a = region.assign_advice(|| "", config.a, 2, || Value::known(Fr::one()))?; + a.copy_advice(|| "", &mut region, config.b, 3)?; + a.copy_advice(|| "", &mut region, config.c, 4)?; + + Ok(()) + }, + ) + } +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/tests/two_layer_aggregation.rs b/snark-verifier-sdk/src/tests/two_layer_aggregation.rs new file mode 100644 index 00000000..14637b2b --- /dev/null +++ b/snark-verifier-sdk/src/tests/two_layer_aggregation.rs @@ -0,0 +1,79 @@ +use super::TestCircuit1; +use crate::{ + aggregation::aggregation_circuit::AggregationCircuit, + evm_api::{evm_verify, gen_evm_proof_shplonk, gen_evm_verifier}, + halo2_api::{gen_pk, gen_snark_shplonk}, + CircuitExt, +}; +use ark_std::test_rng; +use halo2_base::halo2_proofs; +use halo2_proofs::{halo2curves::bn256::Bn256, poly::commitment::Params}; +use snark_verifier::{ + loader::halo2::halo2_ecc::halo2_base::utils::fs::gen_srs, + pcs::kzg::{Bdfg21, Kzg}, +}; +use std::path::Path; + +#[test] +fn test_two_layer_aggregation_evm_verification() { + std::env::set_var("VERIFY_CONFIG", "./configs/example_evm_accumulator.config"); + let k = 8; + let k_agg = 21; + + let mut rng = test_rng(); + let params_outer = gen_srs(k_agg); + let params_inner = { + let mut params = params_outer.clone(); + params.downsize(k); + params + }; + + // layer 1 snarks + let circuit = TestCircuit1::rand(&mut rng); + let pk_inner = gen_pk(¶ms_inner, &circuit, None); + let snarks = (0..3) + .map(|i| { + gen_snark_shplonk( + ¶ms_inner, + &pk_inner, + circuit.clone(), + &mut rng, + Some(Path::new(&format!("data/inner_{}.snark", i).to_string())), + ) + }) + .collect::>(); + println!("finished snark generation"); + + // layer 2, first aggregation + let first_agg_circuit = AggregationCircuit::new(¶ms_outer, snarks, &mut rng); + let pk_outer = gen_pk(¶ms_outer, &first_agg_circuit, None); + println!("finished outer pk generation"); + let first_agg_proof = gen_snark_shplonk( + ¶ms_outer, + &pk_outer, + first_agg_circuit.clone(), + &mut rng, + Some(Path::new("data/outer.snark")), + ); + println!("finished outer proof generation"); + + // layer 3, second aggregation + let second_agg_circuit = AggregationCircuit::new(¶ms_outer, [first_agg_proof], &mut rng); + let pk_agg = gen_pk(¶ms_outer, &second_agg_circuit, None); + + let deployment_code = gen_evm_verifier::>( + ¶ms_outer, + pk_agg.get_vk(), + second_agg_circuit.num_instance(), + Some(Path::new("data/two_layer_recur.sol")), + ); + let proof = gen_evm_proof_shplonk( + ¶ms_outer, + &pk_agg, + second_agg_circuit.clone(), + second_agg_circuit.instances().clone(), + &mut rng, + ); + println!("finished bytecode generation"); + evm_verify(deployment_code, second_agg_circuit.instances(), proof) +} \ No newline at end of file diff --git a/snark-verifier-sdk/src/types.rs b/snark-verifier-sdk/src/types.rs new file mode 100644 index 00000000..e0220499 --- /dev/null +++ b/snark-verifier-sdk/src/types.rs @@ -0,0 +1,52 @@ +//! This module concretize generic types with Bn256 curve and BDFG KZG scheme. + +use super::{BITS, LIMBS}; +use halo2_base::halo2_proofs::halo2curves::bn256::{Bn256, Fr, G1Affine}; +use lazy_static::lazy_static; +use snark_verifier::{ + loader::halo2::{halo2_ecc::ecc::BaseFieldEccChip as EccChip, Halo2Loader as Loader}, + pcs::kzg::{ + Bdfg21, Kzg, KzgAs as KzgAccumulationScheme, KzgSuccinctVerifyingKey, LimbsEncoding, + }, + verifier, PoseidonSpec, +}; + +use crate::param::{RATE, R_F, R_P, T}; + +lazy_static! { + pub static ref POSEIDON_SPEC: PoseidonSpec = PoseidonSpec::new(R_F, R_P); +} + +/// Transcript instantiated with Poseidon +pub type PoseidonTranscript = + snark_verifier::system::halo2::transcript::halo2::PoseidonTranscript< + G1Affine, + L, + S, + T, + RATE, + R_F, + R_P, + >; + +/// Plonk configured with AS. +/// AS is either `Kzg` or `Kzg` +pub type PlonkVerifier = verifier::plonk::PlonkVerifier>; + +/// KZG instantiated with BDFG21 +pub type KzgBDFG = Kzg; + +/// Accumulator scheme build from KZG over BDFG21 scheme +pub type KzgAs = KzgAccumulationScheme; + +/// SHPlonk +pub type Shplonk = Plonk; + +/// KZG succinct verifying key. +pub type Svk = KzgSuccinctVerifyingKey; + +/// Non-native arithmetic chip +pub type BaseFieldEccChip = EccChip; + +/// Halo2 loader +pub type Halo2Loader<'a> = Loader<'a, G1Affine, BaseFieldEccChip>; \ No newline at end of file diff --git a/snark-verifier/Cargo.toml b/snark-verifier/Cargo.toml index 72894ebf..61f39e2c 100644 --- a/snark-verifier/Cargo.toml +++ b/snark-verifier/Cargo.toml @@ -12,10 +12,11 @@ num-traits = "0.2.15" hex = "0.4" rand = "0.8" serde = { version = "1.0", features = ["derive"] } -pairing = { version = "0.23" } +ff = { version = "0.13" } +# pairing = { version = "0.23" } # Use halo2-base as non-optional dependency because it re-exports halo2_proofs, halo2curves, and poseidon, using different repos based on feature flag "halo2-axiom" or "halo2-pse" -halo2-base = { git = "https://github.com/axiom-crypto/halo2-lib.git", branch = "community-edition", default-features = false } +halo2-base = { git = "https://github.com/scroll-tech/halo2-lib.git", branch = "sync-halo2-lib-0.4.0", default-features = false } # parallel rayon = { version = "1.7", optional = true } @@ -25,7 +26,7 @@ sha3 = { version = "0.10", optional = true } revm = { version = "3.3.0", optional = true } # loader_halo2 -halo2-ecc = { git = "https://github.com/axiom-crypto/halo2-lib.git", branch = "community-edition", default-features = false, optional = true } +halo2-ecc = { git = "https://github.com/scroll-tech/halo2-lib.git", branch = "sync-halo2-lib-0.4.0", default-features = false, optional = true } [dev-dependencies] ark-std = { version = "0.3.0", features = ["print-trace"] } @@ -38,7 +39,7 @@ crossterm = { version = "0.25" } tui = { version = "0.19", default-features = false, features = ["crossterm"] } [features] -default = ["loader_evm", "loader_halo2", "halo2-axiom", "display"] +default = ["loader_evm", "loader_halo2", "halo2-pse", "display"] display = ["halo2-base/display", "halo2-ecc?/display"] loader_evm = ["dep:sha3", "dep:revm"] loader_halo2 = ["halo2-ecc"] diff --git a/snark-verifier/examples/evm-verifier-with-accumulator.rs b/snark-verifier/examples/evm-verifier-with-accumulator.rs index 304b7395..6301492b 100644 --- a/snark-verifier/examples/evm-verifier-with-accumulator.rs +++ b/snark-verifier/examples/evm-verifier-with-accumulator.rs @@ -114,6 +114,7 @@ mod application { impl Circuit for StandardPlonk { type Config = StandardPlonkConfig; type FloorPlanner = SimpleFloorPlanner; + type Params = (); fn without_witnesses(&self) -> Self { Self::default() diff --git a/snark-verifier/examples/evm-verifier.rs b/snark-verifier/examples/evm-verifier.rs index d541528a..9c60bd42 100644 --- a/snark-verifier/examples/evm-verifier.rs +++ b/snark-verifier/examples/evm-verifier.rs @@ -94,6 +94,7 @@ impl StandardPlonk { impl Circuit for StandardPlonk { type Config = StandardPlonkConfig; type FloorPlanner = SimpleFloorPlanner; + type Params = (); fn without_witnesses(&self) -> Self { Self::default() diff --git a/snark-verifier/examples/recursion.rs b/snark-verifier/examples/recursion.rs index da34cb82..7fdc73eb 100644 --- a/snark-verifier/examples/recursion.rs +++ b/snark-verifier/examples/recursion.rs @@ -291,6 +291,7 @@ mod application { impl Circuit for Square { type Config = Selector; type FloorPlanner = SimpleFloorPlanner; + type Params = (); fn without_witnesses(&self) -> Self { Self::default() diff --git a/snark-verifier/src/loader/evm/util/executor.rs b/snark-verifier/src/loader/evm/util/executor.rs index e2c5bb2c..cb808a0e 100644 --- a/snark-verifier/src/loader/evm/util/executor.rs +++ b/snark-verifier/src/loader/evm/util/executor.rs @@ -6,10 +6,7 @@ use revm::{ /// Deploy contract and then call with calldata. /// Returns gas_used of call to deployed contract if both transactions are successful. pub fn deploy_and_call(deployment_code: Vec, calldata: Vec) -> Result { - let mut evm = EVM { - env: Default::default(), - db: Some(InMemoryDB::default()), - }; + let mut evm = EVM { env: Default::default(), db: Some(InMemoryDB::default()) }; evm.env.tx = TxEnv { gas_limit: u64::MAX, diff --git a/snark-verifier/src/pcs/kzg/accumulation.rs b/snark-verifier/src/pcs/kzg/accumulation.rs index d71e366e..49dcaaf2 100644 --- a/snark-verifier/src/pcs/kzg/accumulation.rs +++ b/snark-verifier/src/pcs/kzg/accumulation.rs @@ -19,7 +19,7 @@ pub struct KzgAs(PhantomData<(M, MOS)>); impl AccumulationScheme for KzgAs where M: MultiMillerLoop, - M::G1Affine: CurveAffine, + M::G1Affine: CurveAffine, L: Loader, MOS: Clone + Debug, { @@ -140,7 +140,7 @@ where impl AccumulationSchemeProver for KzgAs where M: MultiMillerLoop, - M::G1Affine: CurveAffine, + M::G1Affine: CurveAffine, MOS: Clone + Debug, { type ProvingKey = KzgAsProvingKey; @@ -165,7 +165,7 @@ where let blind = pk .zk() .then(|| { - let s = M::Fr::random(rng); + let s = M::Scalar::random(rng); let (g, s_g) = pk.0.unwrap(); let lhs = (s_g * s).to_affine(); let rhs = (g * s).to_affine(); diff --git a/snark-verifier/src/pcs/kzg/decider.rs b/snark-verifier/src/pcs/kzg/decider.rs index d55e0a57..7a5916e4 100644 --- a/snark-verifier/src/pcs/kzg/decider.rs +++ b/snark-verifier/src/pcs/kzg/decider.rs @@ -28,7 +28,7 @@ impl KzgDecidingKey { impl From<(M::G1Affine, M::G2Affine, M::G2Affine)> for KzgDecidingKey where - M::G1Affine: CurveAffine, + M::G1Affine: CurveAffine, { fn from((g1, g2, s_g2): (M::G1Affine, M::G2Affine, M::G2Affine)) -> KzgDecidingKey { KzgDecidingKey::new(g1, g2, s_g2) @@ -59,7 +59,7 @@ mod native { impl AccumulationDecider for KzgAs where M: MultiMillerLoop, - M::G1Affine: CurveAffine, + M::G1Affine: CurveAffine, MOS: Clone + Debug, { type DecidingKey = KzgDecidingKey; @@ -110,9 +110,9 @@ mod evm { impl AccumulationDecider> for KzgAs where M: MultiMillerLoop, - M::G1Affine: CurveAffine, - M::G2Affine: CurveAffine, - M::Fr: PrimeField, + M::G1Affine: CurveAffine, + M::G2Affine: CurveAffine, + M::Scalar: PrimeField, MOS: Clone + Debug, { type DecidingKey = KzgDecidingKey; @@ -161,7 +161,7 @@ mod evm { loader.code_mut().runtime_append(code); let challenge = loader.scalar(Value::Memory(challenge_ptr)); - let powers_of_challenge = LoadedScalar::::powers(&challenge, lhs.len()); + let powers_of_challenge = LoadedScalar::::powers(&challenge, lhs.len()); let [lhs, rhs] = [lhs, rhs].map(|msms| { msms.iter() .zip(powers_of_challenge.iter()) diff --git a/snark-verifier/src/pcs/kzg/multiopen/bdfg21.rs b/snark-verifier/src/pcs/kzg/multiopen/bdfg21.rs index e13e09cc..8f282a8b 100644 --- a/snark-verifier/src/pcs/kzg/multiopen/bdfg21.rs +++ b/snark-verifier/src/pcs/kzg/multiopen/bdfg21.rs @@ -27,8 +27,8 @@ pub struct Bdfg21; impl PolynomialCommitmentScheme for KzgAs where M: MultiMillerLoop, - M::G1Affine: CurveAffine, - M::Fr: Ord, + M::G1Affine: CurveAffine, + M::Scalar: Ord, L: Loader, { type VerifyingKey = KzgSuccinctVerifyingKey; diff --git a/snark-verifier/src/pcs/kzg/multiopen/gwc19.rs b/snark-verifier/src/pcs/kzg/multiopen/gwc19.rs index e8114d09..293b37a2 100644 --- a/snark-verifier/src/pcs/kzg/multiopen/gwc19.rs +++ b/snark-verifier/src/pcs/kzg/multiopen/gwc19.rs @@ -23,7 +23,7 @@ pub struct Gwc19; impl PolynomialCommitmentScheme for KzgAs where M: MultiMillerLoop, - M::G1Affine: CurveAffine, + M::G1Affine: CurveAffine, L: Loader, { type VerifyingKey = KzgSuccinctVerifyingKey; diff --git a/snark-verifier/src/system/halo2/transcript.rs b/snark-verifier/src/system/halo2/transcript.rs index 10da3a22..8d162280 100644 --- a/snark-verifier/src/system/halo2/transcript.rs +++ b/snark-verifier/src/system/halo2/transcript.rs @@ -10,7 +10,7 @@ use crate::{ Error, }; use halo2_proofs::transcript::{Blake2bRead, Blake2bWrite, Challenge255}; -use pairing::group::ff::FromUniformBytes; +use ff::FromUniformBytes; use std::io::{Read, Write}; #[cfg(feature = "loader_evm")] diff --git a/snark-verifier/src/util/arithmetic.rs b/snark-verifier/src/util/arithmetic.rs index c34daef8..255a9d14 100644 --- a/snark-verifier/src/util/arithmetic.rs +++ b/snark-verifier/src/util/arithmetic.rs @@ -12,7 +12,7 @@ pub use halo2_curves::{ }; use num_bigint::BigUint; use num_traits::One; -pub use pairing::MillerLoopResult; +pub use halo2_curves::pairing::MillerLoopResult; use serde::{Deserialize, Serialize}; use std::{ cmp::Ordering, @@ -22,9 +22,9 @@ use std::{ }; /// [`halo2_curves::pairing::MultiMillerLoop`] with [`std::fmt::Debug`]. -pub trait MultiMillerLoop: pairing::MultiMillerLoop + Debug {} +pub trait MultiMillerLoop: halo2_curves::pairing::MultiMillerLoop + Debug {} -impl MultiMillerLoop for M {} +impl MultiMillerLoop for M {} /// Trait for fields that can implement Poseidon hash pub trait FieldExt: PrimeField + FromUniformBytes<64> + Ord {}