diff --git a/w3f-ring-proof/src/lib.rs b/w3f-ring-proof/src/lib.rs index 9770ad0..d2e355e 100644 --- a/w3f-ring-proof/src/lib.rs +++ b/w3f-ring-proof/src/lib.rs @@ -12,6 +12,7 @@ use w3f_plonk_common::Proof; pub use crate::piop::{params::PiopParams, FixedColumnsCommitted, ProverKey, VerifierKey}; use crate::piop::{RingCommitments, RingEvaluations}; +pub mod multi_ring_batch_verifier; mod piop; pub mod ring; pub mod ring_prover; @@ -185,4 +186,79 @@ mod tests { fn test_ring_proof_id() { _test_ring_proof::(2usize.pow(10), 1); } + + #[test] + fn test_multi_ring_batch_verify_kzg() { + let rng = &mut test_rng(); + let domain_size = 2usize.pow(9); + let proofs_per_ring = 4; + + let (pcs_params, piop_params) = setup::<_, KZG>(rng, domain_size); + + // Ring A + let keyset_size_a = piop_params.keyset_part_size; + let pks_a = random_vec::(keyset_size_a, rng); + let (prover_key_a, verifier_key_a) = + index::<_, KZG, _>(&pcs_params, &piop_params, &pks_a); + + // Ring B (smaller keyset) + let keyset_size_b = piop_params.keyset_part_size / 2; + let pks_b = random_vec::(keyset_size_b, rng); + let (prover_key_b, verifier_key_b) = + index::<_, KZG, _>(&pcs_params, &piop_params, &pks_b); + + let mut generate_claims = |prover_key: &ProverKey, EdwardsAffine>, + pks: &[EdwardsAffine], + keyset_size: usize| { + (0..proofs_per_ring) + .map(|_| { + let prover_idx = rng.gen_range(0..keyset_size); + let prover = RingProver::init( + prover_key.clone(), + piop_params.clone(), + prover_idx, + ArkTranscript::new(b"w3f-ring-proof-test"), + ); + let blinding_factor = Fr::rand(rng); + let blinded_pk = + (pks[prover_idx] + piop_params.h.mul(blinding_factor)).into_affine(); + let proof = prover.prove(blinding_factor); + (blinded_pk, proof) + }) + .collect::>() + }; + + let claims_a = generate_claims(&prover_key_a, &pks_a, keyset_size_a); + let claims_b = generate_claims(&prover_key_b, &pks_b, keyset_size_b); + + let verifier_a = RingVerifier::init( + verifier_key_a, + piop_params.clone(), + ArkTranscript::new(b"w3f-ring-proof-test"), + ); + let verifier_b = RingVerifier::init( + verifier_key_b, + piop_params, + ArkTranscript::new(b"w3f-ring-proof-test"), + ); + + // Sanity: individual verification + for (result, proof) in &claims_a { + assert!(verifier_a.verify(proof.clone(), *result)); + } + for (result, proof) in &claims_b { + assert!(verifier_b.verify(proof.clone(), *result)); + } + + // Multi-ring batch verification + use crate::multi_ring_batch_verifier::MultiRingBatchVerifier; + let mut batch = MultiRingBatchVerifier::new(verifier_a.pcs_vk().clone()); + for (result, proof) in claims_a { + batch.push(&verifier_a, proof, result); + } + for (result, proof) in claims_b { + batch.push(&verifier_b, proof, result); + } + assert!(batch.verify()); + } } diff --git a/w3f-ring-proof/src/multi_ring_batch_verifier.rs b/w3f-ring-proof/src/multi_ring_batch_verifier.rs new file mode 100644 index 0000000..3d048f5 --- /dev/null +++ b/w3f-ring-proof/src/multi_ring_batch_verifier.rs @@ -0,0 +1,131 @@ +use ark_ec::pairing::Pairing; +use ark_ec::twisted_edwards::{Affine, TECurveConfig}; +use ark_ec::CurveGroup; +use ark_std::rand::RngCore; +use w3f_pcs::pcs::kzg::params::KzgVerifierKey; +use w3f_pcs::pcs::kzg::KZG; +use w3f_pcs::pcs::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::Challenges; + +use crate::piop::PiopVerifier; +use crate::ring_verifier::RingVerifier; +use crate::RingProof; + +/// A ring proof preprocessed for multi-ring batch verification. +/// +/// Holds a reference to the `RingVerifier` that was used during preparation, +/// so that `push_prepared` can access the correct ring's transcript prelude. +pub struct PreparedMultiRingItem<'a, E, J, T> +where + E: Pairing, + J: TECurveConfig, + T: PlonkTranscript>, +{ + verifier: &'a RingVerifier, J, T>, + piop: PiopVerifier as PCS>::C, Affine>, + proof: RingProof>, + challenges: Challenges, + entropy: [u8; 32], +} + +/// Accumulating batch verifier for ring proofs across multiple rings. +/// +/// Unlike `KzgBatchVerifier` which is tied to a single ring, +/// this verifier can accumulate proofs from different rings (keysets) +/// into a single batched pairing check. +/// +/// All rings must share the same KZG SRS (same `KzgVerifierKey`). +pub struct MultiRingBatchVerifier { + acc: KzgAccumulator, +} + +impl MultiRingBatchVerifier { + /// Creates a new multi-ring batch verifier. + pub fn new(kzg_vk: KzgVerifierKey) -> Self { + Self { + acc: KzgAccumulator::::new(kzg_vk), + } + } + + /// Prepares a ring proof for batch verification without accumulating it. + /// + /// The returned item holds a reference to the `verifier` and is independent + /// of the accumulator state, so multiple proofs (even from different rings) + /// can be prepared in parallel. + pub fn prepare<'a, J, T>( + verifier: &'a RingVerifier, J, T>, + proof: RingProof>, + result: Affine, + ) -> PreparedMultiRingItem<'a, E, J, T> + where + J: TECurveConfig, + T: PlonkTranscript>, + { + let (challenges, mut rng) = verifier.plonk_verifier.restore_challenges( + &result, + &proof, + PiopVerifier:: as PCS<_>>::C, Affine>::N_COLUMNS + 1, + PiopVerifier:: as PCS<_>>::C, Affine>::N_CONSTRAINTS, + ); + let seed = verifier.piop_params.seed; + let seed_plus_result = (seed + result).into_affine(); + let domain_at_zeta = verifier.piop_params.domain.evaluate(challenges.zeta); + let piop = PiopVerifier::<_, _, Affine>::init( + domain_at_zeta, + 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), + ); + + let mut entropy = [0_u8; 32]; + rng.fill_bytes(&mut entropy); + + PreparedMultiRingItem { + verifier, + 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) + pub fn push_prepared(&mut self, item: PreparedMultiRingItem<'_, E, J, T>) + where + J: TECurveConfig, + T: PlonkTranscript>, + { + let mut ts = item.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. + pub fn push( + &mut self, + verifier: &RingVerifier, J, T>, + proof: RingProof>, + result: Affine, + ) where + J: TECurveConfig, + T: PlonkTranscript>, + { + let item = Self::prepare(verifier, proof, result); + self.push_prepared(item); + } + + /// Verifies all accumulated proofs in a single batched pairing check. + pub fn verify(&self) -> bool { + self.acc.verify() + } +} diff --git a/w3f-ring-proof/src/ring_verifier.rs b/w3f-ring-proof/src/ring_verifier.rs index 0cdd562..9765b11 100644 --- a/w3f-ring-proof/src/ring_verifier.rs +++ b/w3f-ring-proof/src/ring_verifier.rs @@ -22,9 +22,9 @@ where Jubjub: TECurveConfig, T: PlonkTranscript, { - piop_params: PiopParams, - fixed_columns_committed: FixedColumnsCommitted, - plonk_verifier: PlonkVerifier, + pub(crate) piop_params: PiopParams, + pub(crate) fixed_columns_committed: FixedColumnsCommitted, + pub(crate) plonk_verifier: PlonkVerifier, } impl RingVerifier @@ -76,6 +76,10 @@ where &self.piop_params } + pub fn pcs_vk(&self) -> &CS::VK { + &self.plonk_verifier.pcs_vk + } + pub fn verify_batch( &self, proofs: Vec>,