diff --git a/snark-verifier-sdk/Cargo.toml b/snark-verifier-sdk/Cargo.toml index 899c2cbb..3ab5d50d 100644 --- a/snark-verifier-sdk/Cargo.toml +++ b/snark-verifier-sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "snark-verifier-sdk" -version = "0.1.6" +version = "0.1.7" edition = "2021" [dependencies] @@ -22,14 +22,10 @@ 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 } +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 } +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 } @@ -45,13 +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-axiom", "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/range_check.rs b/snark-verifier-sdk/examples/range_check.rs index 2beb5a78..3d75c2cd 100644 --- a/snark-verifier-sdk/examples/range_check.rs +++ b/snark-verifier-sdk/examples/range_check.rs @@ -14,7 +14,7 @@ use snark_verifier_sdk::{ Snark, }; -fn generate_circuit(k: u32) -> Snark { +fn generate_circuit(k: u32, fill: bool) -> Snark { let lookup_bits = k as usize - 1; let circuit_params = BaseCircuitParams { k: k as usize, @@ -30,20 +30,31 @@ fn generate_circuit(k: u32) -> Snark { let ctx = builder.main(0); let x = ctx.load_witness(Fr::from(14)); - range.range_check(ctx, x, 2 * lookup_bits + 1); - range.gate().add(ctx, x, x); + if fill { + for _ in 0..2 << k { + range.gate().add(ctx, x, x); + } + } let params = gen_srs(k); // do not call calculate_params, we want to use fixed params let pk = gen_pk(¶ms, &builder, None); + // std::fs::remove_file(Path::new("examples/app.pk")).ok(); + // let _pk = gen_pk(¶ms, &builder, Some(Path::new("examples/app.pk"))); + // let pk = read_pk::>( + // Path::new("examples/app.pk"), + // builder.config_params.clone(), + // ) + // .unwrap(); + // std::fs::remove_file(Path::new("examples/app.pk")).ok(); // builder now has break_point set gen_snark_shplonk(¶ms, &pk, builder, None::<&str>) } fn main() { - let dummy_snark = generate_circuit(9); + let dummy_snark = generate_circuit(9, false); - let k = 14u32; + let k = 16u32; let lookup_bits = k as usize - 1; let params = gen_srs(k); let mut agg_circuit = AggregationCircuit::new::( @@ -57,10 +68,14 @@ fn main() { let start0 = start_timer!(|| "gen vk & pk"); let pk = gen_pk(¶ms, &agg_circuit, None); + // std::fs::remove_file(Path::new("examples/agg.pk")).ok(); + // let _pk = gen_pk(¶ms, &agg_circuit, Some(Path::new("examples/agg.pk"))); end_timer!(start0); + // let pk = read_pk::(Path::new("examples/agg.pk"), agg_config).unwrap(); + // std::fs::remove_file(Path::new("examples/agg.pk")).ok(); let break_points = agg_circuit.break_points(); - let snarks = (10..16).map(generate_circuit).collect_vec(); + let snarks = (10..16).map(|k| generate_circuit(k, true)).collect_vec(); for (i, snark) in snarks.into_iter().enumerate() { let agg_circuit = AggregationCircuit::new::( CircuitBuilderStage::Prover, diff --git a/snark-verifier-sdk/src/halo2/aggregation.rs b/snark-verifier-sdk/src/halo2/aggregation.rs index 690a64fc..ed940729 100644 --- a/snark-verifier-sdk/src/halo2/aggregation.rs +++ b/snark-verifier-sdk/src/halo2/aggregation.rs @@ -6,7 +6,8 @@ use halo2_base::{ circuit::{ builder::BaseCircuitBuilder, BaseCircuitParams, BaseConfig, CircuitBuilderStage, }, - flex_gate::MultiPhaseThreadBreakPoints, + flex_gate::{threads::SinglePhaseCoreManager, MultiPhaseThreadBreakPoints}, + RangeChip, }, halo2_proofs::{ circuit::{Layouter, SimpleFloorPlanner}, @@ -25,13 +26,14 @@ use snark_verifier::util::arithmetic::fe_to_limbs; use snark_verifier::{ loader::{ self, - halo2::halo2_ecc::{self, bn254::FpChip}, + halo2::halo2_ecc::{self, bigint::ProperCrtUint, bn254::FpChip}, native::NativeLoader, }, pcs::{ kzg::{KzgAccumulator, KzgAsProvingKey, KzgAsVerifyingKey, KzgSuccinctVerifyingKey}, AccumulationScheme, AccumulationSchemeProver, PolynomialCommitmentScheme, }, + system::halo2::transcript::halo2::TranscriptObject, verifier::SnarkVerifier, }; use std::{fs::File, mem, path::Path, rc::Rc}; @@ -56,6 +58,8 @@ pub struct SnarkAggregationWitness<'a> { /// This returns the assigned `preprocessed` and `transcript_initial_state` values as a vector of assigned values, one for each aggregated snark. /// These can then be exposed as public instances. pub preprocessed: Vec, + /// The proof transcript, as loaded scalars and elliptic curve points, for each SNARK that was aggregated. + pub proof_transcripts: Vec>>>>, } /// Different possible stages of universality the aggregation circuit can support @@ -132,9 +136,9 @@ where ); let preprocessed_as_witness = universality.preprocessed_as_witness(); - let mut accumulators = snarks + let (proof_transcripts, accumulators): (Vec<_>, Vec<_>) = snarks .iter() - .flat_map(|snark: &Snark| { + .map(|snark: &Snark| { let protocol = if preprocessed_as_witness { // always load `domain.n` as witness if vkey is witness snark.protocol.loaded_preprocessed_as_witness(loader, universality.k_as_witness()) @@ -184,10 +188,21 @@ where previous_instances.push( instances.into_iter().flatten().map(|scalar| scalar.into_assigned()).collect(), ); - - accumulator + let proof_transcript = transcript.loaded_stream.clone(); + debug_assert_eq!( + snark.proof().len(), + proof_transcript + .iter() + .map(|t| match t { + TranscriptObject::Scalar(_) => 32, + TranscriptObject::EcPoint(_) => 32, + }) + .sum::() + ); + (proof_transcript, accumulator) }) - .collect_vec(); + .unzip(); + let mut accumulators = accumulators.into_iter().flatten().collect_vec(); let accumulator = if accumulators.len() > 1 { transcript.new_stream(as_proof); @@ -207,6 +222,7 @@ where previous_instances, accumulator, preprocessed: preprocessed_witnesses, + proof_transcripts, } } @@ -314,6 +330,145 @@ pub trait Halo2KzgAccumulationScheme<'a> = PolynomialCommitmentScheme< VerifyingKey = KzgAsVerifyingKey, > + AccumulationSchemeProver>; +/// **Private** witnesses that form the output of [aggregate_snarks]. +/// Same as [SnarkAggregationWitness] except that we flatten `accumulator` into a vector of field elements. +#[derive(Clone, Debug)] +pub struct SnarkAggregationOutput { + pub previous_instances: Vec>>, + pub accumulator: Vec>, + /// This returns the assigned `preprocessed` and `transcript_initial_state` values as a vector of assigned values, one for each aggregated snark. + /// These can then be exposed as public instances. + pub preprocessed: Vec, + /// The proof transcript, as loaded scalars and elliptic curve points, for each SNARK that was aggregated. + pub proof_transcripts: Vec>, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Debug)] +pub enum AssignedTranscriptObject { + Scalar(AssignedValue), + EcPoint(halo2_ecc::ecc::EcPoint>), +} + +/// Given snarks, this populates the circuit builder with the virtual cells and constraints necessary to verify all the snarks. +/// +/// ## Notes +/// - This function does _not_ expose any public instances. +/// - `svk` is the generator of the KZG trusted setup, usually gotten via `params.get_g()[0]` +/// (avoids having to pass `params` into function just to get generator) +/// +/// ## Universality +/// - If `universality` is not `None`, then the verifying keys of each snark in `snarks` is loaded as a witness in the circuit. +/// - Moreover, if `universality` is `Full`, then the number of rows `n` of each snark in `snarks` is also loaded as a witness. In this case the generator `omega` of the order `n` multiplicative subgroup of `F` is also loaded as a witness. +/// - By default, these witnesses are _private_ and returned in `self.preprocessed_digests +/// - 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) +/// +/// ## Assumptions +/// - `pool` and `range` reference the same `SharedCopyConstraintManager`. +pub fn aggregate_snarks( + pool: &mut SinglePhaseCoreManager, + range: &RangeChip, + svk: Svk, // gotten by params.get_g()[0].into() + snarks: impl IntoIterator, + universality: VerifierUniversality, +) -> SnarkAggregationOutput +where + AS: for<'a> Halo2KzgAccumulationScheme<'a>, +{ + 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 halo2loader + let fp_chip = FpChip::::new(range, BITS, LIMBS); + let ecc_chip = BaseFieldEccChip::new(&fp_chip); + // `pool` needs to be owned by loader. + // We put it back later (below), so it should have same effect as just mutating `pool`. + let tmp_pool = mem::take(pool); + // range_chip has shared reference to LookupAnyManager, with shared CopyConstraintManager + // pool has shared reference to CopyConstraintManager + let loader = Halo2Loader::new(ecc_chip, tmp_pool); + + // run witness and copy constraint generation + let SnarkAggregationWitness { + previous_instances, + accumulator, + preprocessed, + proof_transcripts, + } = aggregate::(&svk, &loader, &snarks, as_proof.as_slice(), universality); + let lhs = accumulator.lhs.assigned(); + let rhs = accumulator.rhs.assigned(); + let accumulator = lhs + .x() + .limbs() + .iter() + .chain(lhs.y().limbs().iter()) + .chain(rhs.x().limbs().iter()) + .chain(rhs.y().limbs().iter()) + .copied() + .collect_vec(); + let proof_transcripts = proof_transcripts + .into_iter() + .map(|transcript| { + transcript + .into_iter() + .map(|obj| match obj { + TranscriptObject::Scalar(scalar) => { + AssignedTranscriptObject::Scalar(scalar.into_assigned()) + } + TranscriptObject::EcPoint(point) => { + AssignedTranscriptObject::EcPoint(point.into_assigned()) + } + }) + .collect() + }) + .collect(); + + #[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(accumulator.iter()) { + assert_eq!(lhs, rhs.value()); + } + } + // put back `pool` into `builder` + *pool = loader.take_ctx(); + SnarkAggregationOutput { previous_instances, accumulator, preprocessed, proof_transcripts } +} + impl AggregationCircuit { /// Given snarks, this creates `BaseCircuitBuilder` and populates the circuit builder with the virtual cells and constraints necessary to verify all the snarks. /// @@ -339,77 +494,10 @@ impl AggregationCircuit { 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()) - }; - let mut builder = BaseCircuitBuilder::from_stage(stage).use_params(config_params.into()); - // create halo2loader let range = builder.range_chip(); - let fp_chip = FpChip::::new(&range, BITS, LIMBS); - let ecc_chip = BaseFieldEccChip::new(&fp_chip); - // Take the phase 0 pool from `builder`; it needs to be owned by loader. - // We put it back later (below), so it should have same effect as just mutating `builder.pool(0)`. - let pool = mem::take(builder.pool(0)); - // range_chip has shared reference to LookupAnyManager, with shared CopyConstraintManager - // pool has shared reference to CopyConstraintManager - let loader = Halo2Loader::new(ecc_chip, pool); - - // run witness and copy constraint generation - let SnarkAggregationWitness { previous_instances, accumulator, preprocessed } = - aggregate::(&svk, &loader, &snarks, as_proof.as_slice(), universality); - let lhs = accumulator.lhs.assigned(); - let rhs = accumulator.rhs.assigned(); - let accumulator = 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(accumulator.iter()) { - assert_eq!(lhs, rhs.value()); - } - } - // put back `pool` into `builder` - *builder.pool(0) = loader.take_ctx(); + let SnarkAggregationOutput { previous_instances, accumulator, preprocessed, .. } = + aggregate_snarks::(builder.pool(0), &range, svk, snarks, universality); assert_eq!( builder.assigned_instances.len(), 1, diff --git a/snark-verifier/Cargo.toml b/snark-verifier/Cargo.toml index a9072edd..85726802 100644 --- a/snark-verifier/Cargo.toml +++ b/snark-verifier/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "snark-verifier" -version = "0.1.6" +version = "0.1.7" edition = "2021" [dependencies] diff --git a/snark-verifier/src/loader/evm/code.rs b/snark-verifier/src/loader/evm/code.rs index 2634ba2d..193b66e9 100644 --- a/snark-verifier/src/loader/evm/code.rs +++ b/snark-verifier/src/loader/evm/code.rs @@ -21,7 +21,7 @@ impl SolidityAssemblyCode { " // SPDX-License-Identifier: MIT -pragma solidity 0.8.19; +pragma solidity >=0.8.19 <0.8.21; contract Halo2Verifier {{ fallback(bytes calldata) external returns (bytes memory) {{ diff --git a/snark-verifier/src/system/halo2.rs b/snark-verifier/src/system/halo2.rs index 74824361..d62ab26f 100644 --- a/snark-verifier/src/system/halo2.rs +++ b/snark-verifier/src/system/halo2.rs @@ -716,7 +716,9 @@ impl Transcript for MockTranscript } } -fn transcript_initial_state(vk: &VerifyingKey) -> C::Scalar { +/// Returns the transcript initial state of the [VerifyingKey]. +/// Roundabout way to do it because [VerifyingKey] doesn't expose the field. +pub fn transcript_initial_state(vk: &VerifyingKey) -> C::Scalar { let mut transcript = MockTranscript::default(); vk.hash_into(&mut transcript).unwrap(); transcript.0 diff --git a/snark-verifier/src/system/halo2/transcript/halo2.rs b/snark-verifier/src/system/halo2/transcript/halo2.rs index 8a0ce6d4..ab62b088 100644 --- a/snark-verifier/src/system/halo2/transcript/halo2.rs +++ b/snark-verifier/src/system/halo2/transcript/halo2.rs @@ -35,6 +35,19 @@ where ) -> Result, Error>; } +/// A way to keep track of what gets read in the transcript. +#[derive(Clone, Debug)] +pub enum TranscriptObject +where + C: CurveAffine, + L: Loader, +{ + /// Scalar + Scalar(L::LoadedScalar), + /// Elliptic curve point + EcPoint(L::LoadedEcPoint), +} + #[derive(Debug)] /// Transcript for verifier in [`halo2_proofs`] circuit using poseidon hasher. /// Currently It assumes the elliptic curve scalar field is same as native @@ -53,6 +66,10 @@ pub struct PoseidonTranscript< { loader: L, stream: S, + /// Only relevant for Halo2 loader: as elements from `stream` are read, they are assigned as witnesses. + /// The loaded witnesses are pushed to `loaded_stream`. This way at the end we have the entire proof transcript + /// as loaded witnesses. + pub loaded_stream: Vec>, buf: Poseidon>::LoadedScalar, T, RATE>, } @@ -70,7 +87,7 @@ where C::Scalar: FieldExt, { let buf = Poseidon::new::(loader); - Self { loader: loader.clone(), stream, buf } + Self { loader: loader.clone(), stream, buf, loaded_stream: vec![] } } /// Initialize [`PoseidonTranscript`] from a precomputed spec of round constants and MDS matrix because computing the constants is expensive. @@ -80,12 +97,13 @@ where spec: OptimizedPoseidonSpec, ) -> Self { let buf = Poseidon::from_spec(loader, spec); - Self { loader: loader.clone(), stream, buf } + Self { loader: loader.clone(), stream, buf, loaded_stream: vec![] } } /// Clear the buffer and set the stream to a new one. Effectively the same as starting from a new transcript. pub fn new_stream(&mut self, stream: R) { self.buf.clear(); + self.loaded_stream.clear(); self.stream = stream; } } @@ -148,6 +166,7 @@ where C::Scalar::from_repr(data).unwrap() }; let scalar = self.loader.assign_scalar(scalar); + self.loaded_stream.push(TranscriptObject::Scalar(scalar.clone())); self.common_scalar(&scalar)?; Ok(scalar) } @@ -159,6 +178,7 @@ where C::from_bytes(&compressed).unwrap() }; let ec_point = self.loader.assign_ec_point(ec_point); + self.loaded_stream.push(TranscriptObject::EcPoint(ec_point.clone())); self.common_ec_point(&ec_point)?; Ok(ec_point) } @@ -177,17 +197,24 @@ impl(&NativeLoader), + loaded_stream: vec![], } } /// Initialize [`PoseidonTranscript`] from a precomputed spec of round constants and MDS matrix because computing the constants is expensive. pub fn from_spec(stream: S, spec: OptimizedPoseidonSpec) -> Self { - Self { loader: NativeLoader, stream, buf: Poseidon::from_spec(&NativeLoader, spec) } + Self { + loader: NativeLoader, + stream, + buf: Poseidon::from_spec(&NativeLoader, spec), + loaded_stream: vec![], + } } /// Clear the buffer and set the stream to a new one. Effectively the same as starting from a new transcript. pub fn new_stream(&mut self, stream: S) { self.buf.clear(); + self.loaded_stream.clear(); self.stream = stream; } } @@ -198,6 +225,7 @@ impl