Skip to content
2 changes: 1 addition & 1 deletion w3f-plonk-common/src/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub struct PlonkVerifier<F: PrimeField, CS: PCS<F>, T: PlonkTranscript<F, CS>> {
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<F: PrimeField, CS: PCS<F>, T: PlonkTranscript<F, CS>> PlonkVerifier<F, CS, T> {
Expand Down
47 changes: 42 additions & 5 deletions w3f-ring-proof/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -67,7 +67,26 @@ mod tests {

use super::*;

fn _test_ring_proof<CS: PCS<Fq>>(
impl<F: PrimeField, CS: PCS<F>> Clone for VerifierKey<F, CS> {
fn clone(&self) -> Self {
Self {
pcs_raw_vk: self.pcs_raw_vk.clone(),
fixed_columns_committed: self.fixed_columns_committed.clone(),
}
}
}

impl<F: PrimeField, CS: PCS<F>, G: AffineRepr<BaseField = F>> Clone for ProverKey<F, CS, G> {
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<CS: PCS<Fq> + Clone>(
domain_size: usize,
batch_size: usize,
) -> (
Expand Down Expand Up @@ -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::<KZG<Bls12_381>>(2usize.pow(10), 10);
let t_verify_batch = start_timer!(|| "Verify Batch KZG");
let batch_size: usize = 16;
let (verifier, claims) = _test_ring_proof::<KZG<Bls12_381>>(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);
Expand Down
3 changes: 1 addition & 2 deletions w3f-ring-proof/src/piop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,13 @@ impl<F: PrimeField, G: AffineRepr<BaseField = F>> FixedColumns<F, G> {
}

// #[derive(CanonicalSerialize, CanonicalDeserialize)]
#[derive(Clone)]
pub struct ProverKey<F: PrimeField, CS: PCS<F>, G: AffineRepr<BaseField = F>> {
pub(crate) pcs_ck: CS::CK,
pub(crate) fixed_columns: FixedColumns<F, G>,
pub(crate) verifier_key: VerifierKey<F, CS>, // used in the Fiat-Shamir transform
}

#[derive(Clone, Debug, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
#[derive(Debug, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
pub struct VerifierKey<F: PrimeField, CS: PCS<F>> {
pub(crate) pcs_raw_vk: <CS::Params as PcsParams>::RVK,
pub(crate) fixed_columns_committed: FixedColumnsCommitted<F, CS::C>,
Expand Down
147 changes: 120 additions & 27 deletions w3f-ring-proof/src/ring_verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -89,40 +90,132 @@ where
}
}

impl<E, Jubjub, T> RingVerifier<E::ScalarField, KZG<E>, Jubjub, T>
/// Accumulating batch verifier for ring proofs using KZG polynomial commitment scheme.
pub struct KzgBatchVerifier<E, J, T = ArkTranscript>
where
E: Pairing,
Jubjub: TECurveConfig<BaseField = E::ScalarField>,
J: TECurveConfig<BaseField = E::ScalarField>,
T: PlonkTranscript<E::ScalarField, KZG<E>>,
{
// Verifies a batch of proofs against the same ring.
pub fn verify_batch_kzg(
pub acc: KzgAccumulator<E>,
pub verifier: RingVerifier<E::ScalarField, KZG<E>, J, T>,
}

/// A ring proof that has been preprocessed for batch verification.
pub struct PreparedBatchItem<E, J>
where
E: Pairing,
J: TECurveConfig<BaseField = E::ScalarField>,
{
piop: PiopVerifier<E::ScalarField, <KZG<E> as PCS<E::ScalarField>>::C, Affine<J>>,
proof: RingProof<E::ScalarField, KZG<E>>,
challenges: Challenges<E::ScalarField>,
entropy: [u8; 32],
}

impl<E, J, T> KzgBatchVerifier<E, J, T>
where
E: Pairing,
J: TECurveConfig<BaseField = E::ScalarField>,
T: PlonkTranscript<E::ScalarField, KZG<E>>,
{
/// 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<E::ScalarField, KZG<E>>,
result: Affine<J>,
) -> PreparedBatchItem<E, J> {
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::<E::ScalarField, <KZG<E> as PCS<_>>::C, Affine<J>>::N_COLUMNS + 1,
PiopVerifier::<E::ScalarField, <KZG<E> as PCS<_>>::C, Affine<J>>::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<J>>::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<E, J>) {
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<E::ScalarField, KZG<E>>, result: Affine<J>) {
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<E, J, T> RingVerifier<E::ScalarField, KZG<E>, J, T>
where
E: Pairing,
J: TECurveConfig<BaseField = E::ScalarField>,
T: PlonkTranscript<E::ScalarField, KZG<E>>,
{
/// Build a new batch verifier.
pub fn kzg_batch_verifier(self) -> KzgBatchVerifier<E, J, T> {
KzgBatchVerifier {
acc: KzgAccumulator::<E>::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<RingProof<E::ScalarField, KZG<E>>>,
results: Vec<Affine<Jubjub>>,
results: Vec<Affine<J>>,
) -> bool {
let mut acc = KzgAccumulator::<E>::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::<E::ScalarField, <KZG<E> as PCS<_>>::C, Affine<Jubjub>>::N_COLUMNS + 1,
PiopVerifier::<E::ScalarField, <KZG<E> as PCS<_>>::C, Affine<Jubjub>>::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<Jubjub>>::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()
}
}
Loading