From d015de4394ef08c0f3d649ec074f02008ddb7513 Mon Sep 17 00:00:00 2001 From: Skalman Date: Tue, 3 Mar 2026 12:45:21 -0500 Subject: [PATCH 1/2] Add MultiRingBatchVerifier for cross-ring batch verification Introduces a new MultiRingBatchVerifier that can accumulate ring proofs from different rings (keysets) into a single batched KZG pairing check. The prepared item holds a reference to the originating RingVerifier, following the same two-phase prepare/push pattern as KzgBatchVerifier. Co-Authored-By: Claude Opus 4.6 --- w3f-ring-proof/src/lib.rs | 122 ++++++++++++++++ .../src/multi_ring_batch_verifier.rs | 131 ++++++++++++++++++ w3f-ring-proof/src/ring_verifier.rs | 10 +- 3 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 w3f-ring-proof/src/multi_ring_batch_verifier.rs diff --git a/w3f-ring-proof/src/lib.rs b/w3f-ring-proof/src/lib.rs index 9770ad0..2f911ea 100644 --- a/w3f-ring-proof/src/lib.rs +++ b/w3f-ring-proof/src/lib.rs @@ -16,6 +16,7 @@ mod piop; pub mod ring; pub mod ring_prover; pub mod ring_verifier; +pub mod multi_ring_batch_verifier; pub type RingProof = Proof>::C>, RingEvaluations>; @@ -185,4 +186,125 @@ 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()); + } + + #[test] + fn test_multi_ring_batch_verify_kzg_wrong_ring_fails() { + let rng = &mut test_rng(); + let domain_size = 2usize.pow(9); + + 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, _) = + index::<_, KZG, _>(&pcs_params, &piop_params, &pks_a); + + // Ring B (different keyset) + let keyset_size_b = piop_params.keyset_part_size / 2; + let pks_b = random_vec::(keyset_size_b, rng); + let (_, verifier_key_b) = + index::<_, KZG, _>(&pcs_params, &piop_params, &pks_b); + + // Generate a proof from ring A + let prover_idx = rng.gen_range(0..keyset_size_a); + let prover = RingProver::init( + prover_key_a, + piop_params.clone(), + prover_idx, + ArkTranscript::new(b"w3f-ring-proof-test"), + ); + let blinding_factor = Fr::rand(rng); + let blinded_pk = + (pks_a[prover_idx] + piop_params.h.mul(blinding_factor)).into_affine(); + let proof = prover.prove(blinding_factor); + + // Verify with ring B's verifier — should fail + let verifier_b = RingVerifier::init( + verifier_key_b, + piop_params, + ArkTranscript::new(b"w3f-ring-proof-test"), + ); + + use crate::multi_ring_batch_verifier::MultiRingBatchVerifier; + let mut batch = MultiRingBatchVerifier::new(verifier_b.pcs_vk().clone()); + batch.push(&verifier_b, proof, blinded_pk); + 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..d7750bc --- /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() + } +} \ No newline at end of file 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>, From 67d542d139898a56eb0766d7733dd5b4d054b748 Mon Sep 17 00:00:00 2001 From: Sergey Vasilyev Date: Thu, 5 Mar 2026 09:38:24 +0300 Subject: [PATCH 2/2] test dropped + fmt --- w3f-ring-proof/src/lib.rs | 88 +++++-------------- .../src/multi_ring_batch_verifier.rs | 2 +- 2 files changed, 22 insertions(+), 68 deletions(-) diff --git a/w3f-ring-proof/src/lib.rs b/w3f-ring-proof/src/lib.rs index 2f911ea..d2e355e 100644 --- a/w3f-ring-proof/src/lib.rs +++ b/w3f-ring-proof/src/lib.rs @@ -12,11 +12,11 @@ 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; pub mod ring_verifier; -pub mod multi_ring_batch_verifier; pub type RingProof = Proof>::C>, RingEvaluations>; @@ -207,27 +207,26 @@ mod tests { 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 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); @@ -262,49 +261,4 @@ mod tests { } assert!(batch.verify()); } - - #[test] - fn test_multi_ring_batch_verify_kzg_wrong_ring_fails() { - let rng = &mut test_rng(); - let domain_size = 2usize.pow(9); - - 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, _) = - index::<_, KZG, _>(&pcs_params, &piop_params, &pks_a); - - // Ring B (different keyset) - let keyset_size_b = piop_params.keyset_part_size / 2; - let pks_b = random_vec::(keyset_size_b, rng); - let (_, verifier_key_b) = - index::<_, KZG, _>(&pcs_params, &piop_params, &pks_b); - - // Generate a proof from ring A - let prover_idx = rng.gen_range(0..keyset_size_a); - let prover = RingProver::init( - prover_key_a, - piop_params.clone(), - prover_idx, - ArkTranscript::new(b"w3f-ring-proof-test"), - ); - let blinding_factor = Fr::rand(rng); - let blinded_pk = - (pks_a[prover_idx] + piop_params.h.mul(blinding_factor)).into_affine(); - let proof = prover.prove(blinding_factor); - - // Verify with ring B's verifier — should fail - let verifier_b = RingVerifier::init( - verifier_key_b, - piop_params, - ArkTranscript::new(b"w3f-ring-proof-test"), - ); - - use crate::multi_ring_batch_verifier::MultiRingBatchVerifier; - let mut batch = MultiRingBatchVerifier::new(verifier_b.pcs_vk().clone()); - batch.push(&verifier_b, proof, blinded_pk); - 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 index d7750bc..3d048f5 100644 --- a/w3f-ring-proof/src/multi_ring_batch_verifier.rs +++ b/w3f-ring-proof/src/multi_ring_batch_verifier.rs @@ -128,4 +128,4 @@ impl MultiRingBatchVerifier { pub fn verify(&self) -> bool { self.acc.verify() } -} \ No newline at end of file +}