diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1c89b2e3..6a212b81 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,12 +1,10 @@ -name: CI +name: Release on: pull_request: - branches: - - ["main", "release-*"] + branches: ["main", "release-*"] push: - branches: - - main + branches: ["main"] jobs: test: diff --git a/Cargo.lock b/Cargo.lock index 0ddec621..f54a549d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1120,15 +1120,14 @@ checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "halo2-axiom" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa28f319d49a0c23297d7c6792528dc31a7f2fc7d6b13a8d54836bf2a2c08c5" +version = "0.4.2" dependencies = [ "blake2b_simd", "crossbeam", "ff", "group", "halo2curves-axiom", + "itertools 0.11.0", "maybe-rayon", "pairing", "rand", @@ -1140,9 +1139,9 @@ dependencies = [ [[package]] name = "halo2-base" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eeb378fa79d4904b626da0c08dcafd974e8194928c73cdccdcdd00796441a8" +checksum = "0f2045beb3860227b7d354419f2b25a17ad05b2cd68d553dbb9cbbf35daef9d0" dependencies = [ "getset", "halo2-axiom", @@ -1162,12 +1161,12 @@ dependencies = [ [[package]] name = "halo2-ecc" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cfe18bc332c7b2b50100442ed0dd24dea87d1ca16a4eb8912d0875960b0176" +checksum = "aeba4bd49c6eb05815a9809e749f1db525b26b17f5cafb40c5d9a9a4d483f541" dependencies = [ "halo2-base", - "itertools 0.10.5", + "itertools 0.11.0", "num-bigint", "num-integer", "num-traits", @@ -1182,9 +1181,9 @@ dependencies = [ [[package]] name = "halo2curves-axiom" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d47eaec6a3040c2fbaa020716bf571b0c55025126242510f774183aff84b92e" +checksum = "4a11fd188635e09153d007ac33300d0af76972342b2f5ca3f116cba8c759f57d" dependencies = [ "blake2b_simd", "ff", @@ -2535,7 +2534,7 @@ dependencies = [ [[package]] name = "snark-verifier" -version = "0.1.6" +version = "0.1.7" dependencies = [ "ark-std 0.3.0", "crossterm 0.25.0", @@ -2562,7 +2561,7 @@ dependencies = [ [[package]] name = "snark-verifier-sdk" -version = "0.1.6" +version = "0.1.7" dependencies = [ "ark-std 0.3.0", "bincode", @@ -3215,3 +3214,11 @@ dependencies = [ "quote", "syn 2.0.38", ] + +[[patch.unused]] +name = "halo2-base" +version = "0.4.1" + +[[patch.unused]] +name = "halo2-ecc" +version = "0.4.1" diff --git a/snark-verifier-sdk/Cargo.toml b/snark-verifier-sdk/Cargo.toml index 2ba375af..ae48fee6 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" authors = ["Privacy Scaling Explorations Team", "Intrinsic Technologies"] license = "MIT" edition = "2021" @@ -24,15 +24,13 @@ 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 = { version = "=0.4.0", default-features = false } -# halo2-base = { git = "https://github.com/axiom-crypto/halo2-lib.git", branch = "release-0.4.0-rc", default-features = false } -snark-verifier = { version = "=0.1.6", path = "../snark-verifier", default-features = false } +halo2-base = { version = "=0.4.1", default-features = false } +#halo2-base = { git = "https://github.com/axiom-crypto/halo2-lib.git", branch = "main", default-features = false } +snark-verifier = { version = "=0.1.7", 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 = [ @@ -53,13 +51,7 @@ crossterm = { version = "0.25" } ratatui = { version = "0.24", 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_halo2 = ["snark-verifier/loader_halo2"] loader_evm = ["snark-verifier/loader_evm", "dep:ethereum-types"] 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.rs b/snark-verifier-sdk/src/halo2.rs index aa5f8810..ee400913 100644 --- a/snark-verifier-sdk/src/halo2.rs +++ b/snark-verifier-sdk/src/halo2.rs @@ -37,7 +37,7 @@ use snark_verifier::{ system::halo2::{compile, Config}, util::arithmetic::Rotation, util::transcript::TranscriptWrite, - verifier::plonk::PlonkProof, + verifier::plonk::{PlonkProof, PlonkProtocol}, }; use std::{ fs::{self, File}, @@ -46,6 +46,7 @@ use std::{ }; pub mod aggregation; +pub mod utils; // Poseidon parameters // We use the same ones Scroll uses for security: https://github.com/scroll-tech/poseidon-circuit/blob/714f50c7572a4ff6f2b1fa51a9604a99cd7b6c71/src/poseidon/primitives/bn256/fp.rs @@ -274,38 +275,67 @@ pub fn read_snark(path: impl AsRef) -> Result { bincode::deserialize_from(f) } +pub trait NativeKzgAccumulationScheme = PolynomialCommitmentScheme< + G1Affine, + NativeLoader, + VerifyingKey = KzgSuccinctVerifyingKey, + Output = KzgAccumulator, + > + AccumulationScheme< + G1Affine, + NativeLoader, + Accumulator = KzgAccumulator, + VerifyingKey = KzgAsVerifyingKey, + > + CostEstimation>>; + // copied from snark_verifier --example recursion pub fn gen_dummy_snark( params: &ParamsKZG, vk: Option<&VerifyingKey>, num_instance: Vec, + circuit_params: ConcreteCircuit::Params, ) -> Snark where ConcreteCircuit: CircuitExt, - AS: PolynomialCommitmentScheme< - G1Affine, - NativeLoader, - VerifyingKey = KzgSuccinctVerifyingKey, - Output = KzgAccumulator, - > + AccumulationScheme< - G1Affine, - NativeLoader, - Accumulator = KzgAccumulator, - VerifyingKey = KzgAsVerifyingKey, - > + CostEstimation>>, + ConcreteCircuit::Params: Clone, + AS: NativeKzgAccumulationScheme, { - struct CsProxy(PhantomData<(F, C)>); + #[derive(Clone)] + struct CsProxy> { + params: C::Params, + _marker: PhantomData, + } - impl> Circuit for CsProxy { + impl> CsProxy { + pub fn new(params: C::Params) -> Self { + Self { params, _marker: PhantomData } + } + } + + impl> Circuit for CsProxy + where + C::Params: Clone, + { type Config = C::Config; type FloorPlanner = C::FloorPlanner; + type Params = C::Params; fn without_witnesses(&self) -> Self { - CsProxy(PhantomData) + Self::new(self.params.clone()) } - fn configure(meta: &mut ConstraintSystem) -> Self::Config { - C::configure(meta) + fn params(&self) -> Self::Params { + self.params.clone() + } + + fn configure_with_params( + meta: &mut ConstraintSystem, + params: Self::Params, + ) -> Self::Config { + C::configure_with_params(meta, params) + } + + fn configure(_: &mut ConstraintSystem) -> Self::Config { + unreachable!("must use configure_with_params") } fn synthesize( @@ -330,15 +360,50 @@ where let dummy_vk = vk .is_none() - .then(|| keygen_vk(params, &CsProxy::(PhantomData)).unwrap()); - let protocol = compile( + .then(|| keygen_vk(params, &CsProxy::::new(circuit_params)).unwrap()); + + gen_dummy_snark_from_vk::( params, vk.or(dummy_vk.as_ref()).unwrap(), - Config::kzg() - .with_num_instance(num_instance.clone()) - .with_accumulator_indices(ConcreteCircuit::accumulator_indices()), + num_instance, + ConcreteCircuit::accumulator_indices(), + ) +} + +/// Creates a dummy snark in the correct shape corresponding to the given verifying key. +/// This dummy snark will **not** verify. +/// This snark can be used as a placeholder input into an aggregation circuit expecting a snark +/// with this verifying key. +/// +/// Note that this function does not need to know the concrete `Circuit` type. +pub fn gen_dummy_snark_from_vk( + params: &ParamsKZG, + vk: &VerifyingKey, + num_instance: Vec, + accumulator_indices: Option>, +) -> Snark +where + AS: NativeKzgAccumulationScheme, +{ + let protocol = compile( + params, + vk, + Config::kzg().with_num_instance(num_instance).with_accumulator_indices(accumulator_indices), ); - let instances = num_instance.into_iter().map(|n| vec![Fr::default(); n]).collect(); + gen_dummy_snark_from_protocol::(protocol) +} + +/// Creates a dummy snark in the correct shape corresponding to the given Plonk protocol. +/// This dummy snark will **not** verify. +/// This snark can be used as a placeholder input into an aggregation circuit expecting a snark +/// with this protocol. +/// +/// Note that this function does not need to know the concrete `Circuit` type. +pub fn gen_dummy_snark_from_protocol(protocol: PlonkProtocol) -> Snark +where + AS: NativeKzgAccumulationScheme, +{ + let instances = protocol.num_instance.iter().map(|&n| vec![Fr::default(); n]).collect(); let proof = { let mut transcript = PoseidonTranscript::::new::(Vec::new()); for _ in 0..protocol diff --git a/snark-verifier-sdk/src/halo2/aggregation.rs b/snark-verifier-sdk/src/halo2/aggregation.rs index 690a64fc..60c4b3db 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}; @@ -51,11 +53,17 @@ pub struct PreprocessedAndDomainAsWitness { #[derive(Clone, Debug)] pub struct SnarkAggregationWitness<'a> { + /// The (flattened) public instances from previous snarks that were aggregated, now collected as PRIVATE assigned values. + /// * If previous snark was from aggregation circuit, the previous instances will still contain the old KZG accumulator. + /// + /// The user can optionally append these private witnesses to `inner.assigned_instances` to expose them. pub previous_instances: Vec>>, pub accumulator: KzgAccumulator>>, /// 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 +140,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 +192,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 +226,7 @@ where previous_instances, accumulator, preprocessed: preprocessed_witnesses, + proof_transcripts, } } @@ -279,8 +299,10 @@ impl TryFrom for AggregationConfigParams { pub struct AggregationCircuit { /// Circuit builder consisting of virtual region managers pub builder: BaseCircuitBuilder, - // 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 + /// The (flattened) public instances from previous snarks that were aggregated, now collected as PRIVATE assigned values. + /// * If previous snark was from aggregation circuit, the previous instances will still contain the old KZG accumulator. + /// + /// The user can optionally append these private witnesses to `inner.assigned_instances` to expose them. #[getset(get = "pub")] previous_instances: Vec>>, /// This returns the assigned `preprocessed_digest` (vkey), optional `transcript_initial_state`, `domain.n` (optional), and `omega` (optional) values as a vector of assigned values, one for each aggregated snark. @@ -314,6 +336,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 +500,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-sdk/src/halo2/utils.rs b/snark-verifier-sdk/src/halo2/utils.rs new file mode 100644 index 00000000..b2d99de7 --- /dev/null +++ b/snark-verifier-sdk/src/halo2/utils.rs @@ -0,0 +1,126 @@ +use halo2_base::halo2_proofs::{ + halo2curves::bn256::{Bn256, Fr, G1Affine}, + plonk::{Circuit, VerifyingKey}, + poly::kzg::commitment::ParamsKZG, +}; +use rand::{rngs::StdRng, SeedableRng}; +use snark_verifier::{ + system::halo2::{compile, Config}, + verifier::plonk::PlonkProtocol, +}; + +use crate::{Snark, SHPLONK}; + +use super::{aggregation::AggregationCircuit, gen_dummy_snark_from_vk}; + +#[derive(Clone, Copy, Debug)] +pub struct AggregationDependencyIntent<'a> { + pub vk: &'a VerifyingKey, + pub num_instance: &'a [usize], + pub accumulator_indices: Option<&'a [(usize, usize)]>, + /// See [`AggregationDependencyIntentOwned::agg_vk_hash_data`]. + pub agg_vk_hash_data: Option<((usize, usize), Fr)>, +} + +#[derive(Clone, Debug)] +pub struct AggregationDependencyIntentOwned { + pub vk: VerifyingKey, + pub num_instance: Vec, + pub accumulator_indices: Option>, + /// If this dependency is itself from a universal aggregation circuit, this should contain (index, agg_vkey_hash), where `index = (i,j)` is the pair recording that the agg_vkey_hash is located at `instances[i][j]`. + pub agg_vk_hash_data: Option<((usize, usize), Fr)>, +} + +impl<'a> AggregationDependencyIntent<'a> { + /// Converts `self` into `PlonkProtocol` + pub fn compile(self, params: &ParamsKZG) -> PlonkProtocol { + compile( + params, + self.vk, + Config::kzg() + .with_num_instance(self.num_instance.to_vec()) + .with_accumulator_indices(self.accumulator_indices.map(|v| v.to_vec())), + ) + } +} + +/// This trait should be implemented on the minimal circuit configuration data necessary to +/// completely determine an aggregation circuit +/// (independent of circuit inputs or specific snarks to be aggregated). +/// This is used to generate a _dummy_ instantiation of a concrete `Circuit` type for the purposes of key generation. +/// This dummy instantiation just needs to have the correct arithmetization format, but the witnesses do not need to +/// satisfy constraints. +/// +/// This trait is specialized for aggregation circuits, which need to aggregate **dependency** snarks. +/// The aggregation circuit should only depend on the verifying key of each dependency snark. +pub trait KeygenAggregationCircuitIntent { + /// Concrete circuit type. Defaults to [`AggregationCircuit`]. + type AggregationCircuit: Circuit = AggregationCircuit; + + /// The **ordered** list of [`VerifyingKey`]s of the circuits to be aggregated. + fn intent_of_dependencies(&self) -> Vec; + + /// Builds a _dummy_ instantiation of `Self::AggregationCircuit` for the purposes of key generation. + /// Assumes that `snarks` is an ordered list of [`Snark`]s, where the `i`th snark corresponds to the `i`th [`VerifyingKey`] in `vk_of_dependencies`. + /// The `snarks` only need to have the correct witness sizes (e.g., proof length) but the + /// snarks do _not_ need to verify. + /// + /// May specify additional custom logic for building the aggregation circuit from the snarks. + fn build_keygen_circuit_from_snarks(self, snarks: Vec) -> Self::AggregationCircuit; + + /// Builds a _dummy_ instantiation of `Self::AggregationCircuit` for the purposes of key generation. + /// + /// Generates dummy snarks from the verifying keys in `vk_of_dependencies`, **assuming** that SHPLONK is + /// used for the multi-open scheme. + fn build_keygen_circuit_shplonk(self) -> Self::AggregationCircuit + where + Self: Sized, + { + let mut rng = StdRng::seed_from_u64(0u64); + let snarks = + self.intent_of_dependencies() + .into_iter() + .map( + |AggregationDependencyIntent { + vk, + num_instance, + accumulator_indices, + agg_vk_hash_data, + }| { + let k = vk.get_domain().k(); + // In KZG `gen_dummy_snark_from_vk` calls `compile`, which only uses `params` for `params.k()` so we can just use a random untrusted setup. + // Moreover since this is a dummy snark, the trusted setup shouldn't matter. + let params = ParamsKZG::setup(k, &mut rng); + let mut snark = gen_dummy_snark_from_vk::( + ¶ms, + vk, + num_instance.to_vec(), + accumulator_indices.map(|v| v.to_vec()), + ); + // We set the current agg_vk_hash in the dummy snark so that the final agg_vk_hash is correct at the end of keygen. + if let Some(((i, j), agg_vk_hash)) = agg_vk_hash_data { + assert!( + i < snark.instances.len(), + "Invalid agg_vk_hash index: ({i},{j}), num_instance: {num_instance:?}"); + assert!(j < snark.instances[i].len(), + "Invalid agg_vk_hash index: ({i},{j}), num_instance: {num_instance:?}"); + snark.instances[i][j] = agg_vk_hash; + } + snark + }, + ) + .collect(); + self.build_keygen_circuit_from_snarks(snarks) + } +} + +impl<'a> From<&'a AggregationDependencyIntentOwned> for AggregationDependencyIntent<'a> { + fn from(intent: &'a AggregationDependencyIntentOwned) -> Self { + Self { + vk: &intent.vk, + num_instance: &intent.num_instance, + accumulator_indices: intent.accumulator_indices.as_deref(), + agg_vk_hash_data: intent.agg_vk_hash_data, + } + } +} diff --git a/snark-verifier/Cargo.toml b/snark-verifier/Cargo.toml index 6e223f24..87c17546 100644 --- a/snark-verifier/Cargo.toml +++ b/snark-verifier/Cargo.toml @@ -1,13 +1,7 @@ [package] name = "snark-verifier" -version = "0.1.6" -authors = ["Privacy Scaling Explorations Team", "Intrinsic Technologies"] -license = "MIT" +version = "0.1.7" edition = "2021" -repository = "https://github.com/axiom-crypto/snark-verifier" -readme = "../README.md" -description = "Generic (S)NARK verifier for Rust, halo2, and EVM. This is a fork of PSE's version." -rust-version = "1.73.0" [dependencies] itertools = "0.11" @@ -21,10 +15,12 @@ serde = { version = "1.0", features = ["derive"] } 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 = { version = "=0.4.0", default-features = false } +halo2-base = { version = "=0.4.1", default-features = false } +# halo2-base = { git = "https://github.com/axiom-crypto/halo2-lib.git", branch = "main", default-features = false } # loader_halo2 -halo2-ecc = { version = "=0.4.0", default-features = false, optional = true } +halo2-ecc = { version = "=0.4.1", default-features = false, optional = true } +# halo2-ecc = { git = "https://github.com/axiom-crypto/halo2-lib.git", branch = "main", default-features = false, optional = true } # parallel rayon = { version = "1.8", optional = true } @@ -54,7 +50,7 @@ loader_evm = ["dep:sha3", "dep:ruint"] revm = ["loader_evm", "dep:revm"] parallel = ["dep:rayon"] # EXACTLY one of halo2-pse / halo2-axiom should always be turned on; not sure how to enforce this with Cargo -#halo2-pse = ["halo2-base/halo2-pse", "halo2-ecc?/halo2-pse"] +# halo2-pse = ["halo2-base/halo2-pse", "halo2-ecc?/halo2-pse"] halo2-axiom = ["halo2-base/halo2-axiom", "halo2-ecc?/halo2-axiom"] [[example]] diff --git a/snark-verifier/src/pcs/kzg.rs b/snark-verifier/src/pcs/kzg.rs index 387a108c..2e60ec11 100644 --- a/snark-verifier/src/pcs/kzg.rs +++ b/snark-verifier/src/pcs/kzg.rs @@ -15,9 +15,10 @@ pub use multiopen::{Bdfg21, Bdfg21Proof, Gwc19, Gwc19Proof}; #[cfg(feature = "loader_halo2")] pub use accumulator::LimbsEncodingInstructions; +use serde::{Deserialize, Serialize}; /// KZG succinct verifying key. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub struct KzgSuccinctVerifyingKey { /// Generator. pub g: C, diff --git a/snark-verifier/src/pcs/kzg/decider.rs b/snark-verifier/src/pcs/kzg/decider.rs index d55e0a57..e746fcb8 100644 --- a/snark-verifier/src/pcs/kzg/decider.rs +++ b/snark-verifier/src/pcs/kzg/decider.rs @@ -1,3 +1,5 @@ +use serde::{Deserialize, Serialize}; + use crate::{ pcs::kzg::KzgSuccinctVerifyingKey, util::arithmetic::{CurveAffine, MultiMillerLoop}, @@ -5,7 +7,11 @@ use crate::{ use std::marker::PhantomData; /// KZG deciding key. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(bound( + serialize = "M::G1Affine: Serialize, M::G2Affine: Serialize", + deserialize = "M::G1Affine: Deserialize<'de>, M::G2Affine: Deserialize<'de>" +))] pub struct KzgDecidingKey { svk: KzgSuccinctVerifyingKey, /// Generator on G2. @@ -24,6 +30,18 @@ impl KzgDecidingKey { ) -> Self { Self { svk: svk.into(), g2, s_g2, _marker: PhantomData } } + /// Succinct verifying key. + pub fn svk(&self) -> KzgSuccinctVerifyingKey { + self.svk + } + /// Generator on G2. + pub fn g2(&self) -> M::G2Affine { + self.g2 + } + /// Generator to the trusted-setup secret on G2. + pub fn s_g2(&self) -> M::G2Affine { + self.s_g2 + } } impl From<(M::G1Affine, M::G2Affine, M::G2Affine)> for KzgDecidingKey diff --git a/snark-verifier/src/system/halo2.rs b/snark-verifier/src/system/halo2.rs index 573ab845..1aee2f87 100644 --- a/snark-verifier/src/system/halo2.rs +++ b/snark-verifier/src/system/halo2.rs @@ -715,7 +715,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