diff --git a/w3f-plonk-common/src/kzg_acc.rs b/w3f-plonk-common/src/kzg_acc.rs index d49af79..8b487e7 100644 --- a/w3f-plonk-common/src/kzg_acc.rs +++ b/w3f-plonk-common/src/kzg_acc.rs @@ -4,11 +4,10 @@ use crate::{ColumnsCommited, ColumnsEvaluated, Proof}; use ark_ec::pairing::Pairing; use ark_ec::{CurveGroup, VariableBaseMSM}; use ark_ff::{PrimeField, Zero}; -use ark_std::iterable::Iterable; use ark_std::rand::Rng; use w3f_pcs::pcs::kzg::params::KzgVerifierKey; use w3f_pcs::pcs::kzg::{AccumulatedOpening, KZG}; -use w3f_pcs::pcs::{Commitment, PCS}; +use w3f_pcs::pcs::PCS; // Aggregates opennings for KZG commitments. // Somewhat similar to https://eprint.iacr.org/2020/499.pdf, section 8. diff --git a/w3f-plonk-common/src/verifier.rs b/w3f-plonk-common/src/verifier.rs index 7df1928..ec5de7b 100644 --- a/w3f-plonk-common/src/verifier.rs +++ b/w3f-plonk-common/src/verifier.rs @@ -14,7 +14,7 @@ pub struct PlonkVerifier, T: PlonkTranscript> { pub pcs_vk: CS::VK, // Transcript, // initialized with the public parameters and the commitments to the precommitted columns. - transcript_prelude: T, + pub transcript_prelude: T, } impl, T: PlonkTranscript> PlonkVerifier { diff --git a/w3f-ring-proof/src/lib.rs b/w3f-ring-proof/src/lib.rs index 239d20c..de24e5a 100644 --- a/w3f-ring-proof/src/lib.rs +++ b/w3f-ring-proof/src/lib.rs @@ -51,7 +51,7 @@ impl ArkTranscript { #[cfg(test)] mod tests { use ark_bls12_381::Bls12_381; - use ark_ec::CurveGroup; + use ark_ec::{AffineRepr, CurveGroup}; use ark_ed_on_bls12_381_bandersnatch::{BandersnatchConfig, EdwardsAffine, Fq, Fr}; use ark_std::ops::Mul; use ark_std::rand::Rng; @@ -67,7 +67,26 @@ mod tests { use super::*; - fn _test_ring_proof>( + impl> Clone for VerifierKey { + fn clone(&self) -> Self { + Self { + pcs_raw_vk: self.pcs_raw_vk.clone(), + fixed_columns_committed: self.fixed_columns_committed.clone(), + } + } + } + + impl, G: AffineRepr> Clone for ProverKey { + fn clone(&self) -> Self { + Self { + pcs_ck: self.pcs_ck.clone(), + fixed_columns: self.fixed_columns.clone(), + verifier_key: self.verifier_key.clone(), + } + } + } + + fn _test_ring_proof + Clone>( domain_size: usize, batch_size: usize, ) -> ( @@ -153,11 +172,29 @@ mod tests { (pcs_params, piop_params) } - #[test] // cargo test test_ring_proof_kzg --release --features="print-trace" -- --show-output + // + // Batch vs sequential verification times (ms): + // + // | proofs | sequential | batch | speedup | + // |--------|------------|--------|---------| + // | 1 | 3.032 | 2.790 | 1.09x | + // | 2 | 6.425 | 3.218 | 2.00x | + // | 4 | 11.968 | 5.122 | 2.34x | + // | 8 | 23.922 | 6.487 | 3.69x | + // | 16 | 47.773 | 10.002 | 4.78x | + // | 32 | 95.570 | 16.601 | 5.76x | + // | 64 | 210.959 | 29.484 | 7.15x | + // | 128 | 422.217 | 52.170 | 8.09x | + // | 256 | 762.874 | 85.164 | 8.96x | + // + // Sequential verification scales linearly with proof count. + // Batch verification scales sub-linearly. + #[test] fn test_ring_proof_kzg() { - let (verifier, claims) = _test_ring_proof::>(2usize.pow(10), 10); - let t_verify_batch = start_timer!(|| "Verify Batch KZG"); + let batch_size: usize = 16; + let (verifier, claims) = _test_ring_proof::>(2usize.pow(10), batch_size); + let t_verify_batch = start_timer!(|| format!("Verify Batch KZG (batch={batch_size})")); let (blinded_pks, proofs) = claims.into_iter().unzip(); assert!(verifier.verify_batch_kzg(proofs, blinded_pks)); end_timer!(t_verify_batch); diff --git a/w3f-ring-proof/src/piop/mod.rs b/w3f-ring-proof/src/piop/mod.rs index 29cd41a..c1e02d0 100644 --- a/w3f-ring-proof/src/piop/mod.rs +++ b/w3f-ring-proof/src/piop/mod.rs @@ -127,14 +127,13 @@ impl> FixedColumns { } // #[derive(CanonicalSerialize, CanonicalDeserialize)] -#[derive(Clone)] pub struct ProverKey, G: AffineRepr> { pub(crate) pcs_ck: CS::CK, pub(crate) fixed_columns: FixedColumns, pub(crate) verifier_key: VerifierKey, // used in the Fiat-Shamir transform } -#[derive(Clone, Debug, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] +#[derive(Debug, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)] pub struct VerifierKey> { pub(crate) pcs_raw_vk: ::RVK, pub(crate) fixed_columns_committed: FixedColumnsCommitted, diff --git a/w3f-ring-proof/src/ring_verifier.rs b/w3f-ring-proof/src/ring_verifier.rs index 4f38cac..0098259 100644 --- a/w3f-ring-proof/src/ring_verifier.rs +++ b/w3f-ring-proof/src/ring_verifier.rs @@ -2,12 +2,13 @@ use ark_ec::pairing::Pairing; use ark_ec::twisted_edwards::{Affine, TECurveConfig}; use ark_ec::CurveGroup; use ark_ff::PrimeField; +use ark_std::rand::RngCore; use w3f_pcs::pcs::kzg::KZG; use w3f_pcs::pcs::{RawVerifierKey, PCS}; use w3f_plonk_common::kzg_acc::KzgAccumulator; use w3f_plonk_common::piop::VerifierPiop; use w3f_plonk_common::transcript::PlonkTranscript; -use w3f_plonk_common::verifier::PlonkVerifier; +use w3f_plonk_common::verifier::{Challenges, PlonkVerifier}; use crate::piop::params::PiopParams; use crate::piop::{FixedColumnsCommitted, PiopVerifier, VerifierKey}; @@ -89,40 +90,132 @@ where } } -impl RingVerifier, Jubjub, T> +/// Accumulating batch verifier for ring proofs using KZG polynomial commitment scheme. +pub struct KzgBatchVerifier where E: Pairing, - Jubjub: TECurveConfig, + J: TECurveConfig, T: PlonkTranscript>, { - // Verifies a batch of proofs against the same ring. - pub fn verify_batch_kzg( + pub acc: KzgAccumulator, + pub verifier: RingVerifier, J, T>, +} + +/// A ring proof that has been preprocessed for batch verification. +pub struct PreparedBatchItem +where + E: Pairing, + J: TECurveConfig, +{ + piop: PiopVerifier as PCS>::C, Affine>, + proof: RingProof>, + challenges: Challenges, + entropy: [u8; 32], +} + +impl KzgBatchVerifier +where + E: Pairing, + J: TECurveConfig, + T: PlonkTranscript>, +{ + /// Prepares a ring proof for batch verification without accumulating it. + /// + /// Returns a `PreparedBatchItem` that can later be passed to `push_prepared`. + /// + /// This method is independent of the accumulator state, so multiple proofs can be + /// prepared in parallel (e.g., using `rayon`). Each prepared item is in the order + /// of a few KB, so for large batches you may want to prepare and push incrementally + /// rather than holding all prepared items in memory at once. + pub fn prepare( &self, + proof: RingProof>, + result: Affine, + ) -> PreparedBatchItem { + let (challenges, mut rng) = self.verifier.plonk_verifier.restore_challenges( + &result, + &proof, + // '1' accounts for the quotient polynomial that is aggregated together with the columns + PiopVerifier:: as PCS<_>>::C, Affine>::N_COLUMNS + 1, + PiopVerifier:: as PCS<_>>::C, Affine>::N_CONSTRAINTS, + ); + let seed = self.verifier.piop_params.seed; + let seed_plus_result = (seed + result).into_affine(); + let domain_at_zeta = self.verifier.piop_params.domain.evaluate(challenges.zeta); + let piop = PiopVerifier::<_, _, Affine>::init( + domain_at_zeta, + self.verifier.fixed_columns_committed.clone(), + proof.column_commitments.clone(), + proof.columns_at_zeta.clone(), + (seed.x, seed.y), + (seed_plus_result.x, seed_plus_result.y), + ); + + // Pick some entropy from plonk verifier for later usage + let mut entropy = [0_u8; 32]; + rng.fill_bytes(&mut entropy); + + PreparedBatchItem { + piop, + proof, + challenges, + entropy, + } + } + + /// Accumulates a previously prepared proof into the batch. + /// + /// This is the second step of the two-phase batch verification workflow: + /// 1. `prepare` - can be parallelized across multiple proofs + /// 2. `push_prepared` - must be called sequentially (mutates the accumulator) + /// + /// For simpler usage where parallelism isn't needed, use `push` instead. + pub fn push_prepared(&mut self, item: PreparedBatchItem) { + let mut ts = self.verifier.plonk_verifier.transcript_prelude.clone(); + ts._add_serializable(b"batch-entropy", &item.entropy); + self.acc + .accumulate(item.piop, item.proof, item.challenges, &mut ts.to_rng()); + } + + /// Adds a ring proof to the batch, preparing and accumulating it immediately. + /// + /// The proof's pairing equation is aggregated into the internal accumulator. + /// Call `verify` after pushing all proofs to perform the batched verification. + pub fn push(&mut self, proof: RingProof>, result: Affine) { + let item = self.prepare(proof, result); + self.push_prepared(item); + } + + /// Verifies all accumulated proofs in a single batched pairing check. + pub fn verify(&self) -> bool { + self.acc.verify() + } +} + +impl RingVerifier, J, T> +where + E: Pairing, + J: TECurveConfig, + T: PlonkTranscript>, +{ + /// Build a new batch verifier. + pub fn kzg_batch_verifier(self) -> KzgBatchVerifier { + KzgBatchVerifier { + acc: KzgAccumulator::::new(self.plonk_verifier.pcs_vk.clone()), + verifier: self, + } + } + + /// Verifies a batch of proofs against the same ring. + pub fn verify_batch_kzg( + self, proofs: Vec>>, - results: Vec>, + results: Vec>, ) -> bool { - let mut acc = KzgAccumulator::::new(self.plonk_verifier.pcs_vk.clone()); + let mut batch = self.kzg_batch_verifier(); for (proof, result) in proofs.into_iter().zip(results) { - let (challenges, mut rng) = self.plonk_verifier.restore_challenges( - &result, - &proof, - // '1' accounts for the quotient polynomial that is aggregated together with the columns - PiopVerifier:: as PCS<_>>::C, Affine>::N_COLUMNS + 1, - PiopVerifier:: as PCS<_>>::C, Affine>::N_CONSTRAINTS, - ); - let seed = self.piop_params.seed; - let seed_plus_result = (seed + result).into_affine(); - let domain_at_zeta = self.piop_params.domain.evaluate(challenges.zeta); - let piop = PiopVerifier::<_, _, Affine>::init( - domain_at_zeta, - self.fixed_columns_committed.clone(), - proof.column_commitments.clone(), - proof.columns_at_zeta.clone(), - (seed.x, seed.y), - (seed_plus_result.x, seed_plus_result.y), - ); - acc.accumulate(piop, proof, challenges, &mut rng); + batch.push(proof, result); } - acc.verify() + batch.verify() } }