Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
process IDs can repeat) never write the same temporary file. A write that
fails removes its temporary file. On Unix, the directory is synced after
the file is published, so the published file survives a power loss.
- The prepared-point cache (`cache` feature) no longer pairs against the
wrong points. The setup multi-pairings used the cached prepared points
for any input of no greater length, without checking that the input was
the cached points. So a cache built from another setup, or from a larger
setup generated separately, gave wrong pairings. An input longer than
the cache panicked. The cache now keeps the points it prepared. It serves
an input only when the input is a prefix of those points, and otherwise
the pairing prepares the input itself. `init_cache` replaces a cache that
does not cover its input, instead of keeping any cache at least as large.
Under `cargo test`, the cache tests no longer break the proofs that later
tests verify in the same process.

### Added

- `PreparedCache::g1_prepared_for` and `PreparedCache::g2_prepared_for`
return the prepared form of a prefix of the cached points.
- `PreparedCache::g1_len` and `PreparedCache::g2_len` return the number of
cached points.

### Changed

- **Breaking:** all of `PreparedCache`'s fields are private, including
`g1_prepared` and `g2_prepared`. Read prepared points through
`g1_prepared_for` and `g2_prepared_for`, which check that the input is a
prefix of the cached points, and read the cache size through `g1_len` and
`g2_len`. Code outside the crate can no longer construct a
`PreparedCache` with a struct literal. `init_cache` builds it.

## [0.4.2] - 2026-08-20

Expand Down
84 changes: 63 additions & 21 deletions src/backends/arkworks/ark_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,72 @@
//! across multiple pairing operations. Prepared points skip the affine conversion
//! and preprocessing steps, providing ~20-30% speedup for repeated pairings.
//!
//! The cache supports smart re-initialization: if a larger setup is needed,
//! the cache is automatically replaced. Smaller or equal setups reuse the existing cache.
//! The cache remembers the points it prepared. A lookup returns prepared
//! points only for a prefix of those points, so a stale cache, or one built
//! from another setup, falls back to preparing the caller's points instead
//! of pairing against the wrong ones.

use super::ark_group::{ArkG1, ArkG2};
use ark_bn254::{Bn254, G1Affine, G2Affine};
use ark_ec::pairing::Pairing;
use rayon::prelude::*;
use std::sync::{Arc, RwLock};

/// Global cache for prepared points
/// Global cache for prepared points.
///
/// The prepared points are reachable only through
/// [`g1_prepared_for`](Self::g1_prepared_for) and
/// [`g2_prepared_for`](Self::g2_prepared_for), which check that the caller's
/// points are the ones that were prepared.
#[derive(Debug, Clone)]
pub struct PreparedCache {
/// Prepared G1 points for efficient pairing operations
pub g1_prepared: Vec<<Bn254 as Pairing>::G1Prepared>,
/// Prepared G2 points for efficient pairing operations
pub g2_prepared: Vec<<Bn254 as Pairing>::G2Prepared>,
g1_prepared: Vec<<Bn254 as Pairing>::G1Prepared>,
g2_prepared: Vec<<Bn254 as Pairing>::G2Prepared>,
g1: Vec<ArkG1>,
g2: Vec<ArkG2>,
}

impl PreparedCache {
fn covers(&self, g1: &[ArkG1], g2: &[ArkG2]) -> bool {
is_prefix(&self.g1, g1) && is_prefix(&self.g2, g2)
}

/// The number of cached G1 points.
pub fn g1_len(&self) -> usize {
self.g1.len()
}

/// The number of cached G2 points.
pub fn g2_len(&self) -> usize {
self.g2.len()
}

/// The prepared form of `g1`, if `g1` is a prefix of the cached G1 points.
pub fn g1_prepared_for(&self, g1: &[ArkG1]) -> Option<&[<Bn254 as Pairing>::G1Prepared]> {
is_prefix(&self.g1, g1).then(|| &self.g1_prepared[..g1.len()])
}

/// The prepared form of `g2`, if `g2` is a prefix of the cached G2 points.
pub fn g2_prepared_for(&self, g2: &[ArkG2]) -> Option<&[<Bn254 as Pairing>::G2Prepared]> {
is_prefix(&self.g2, g2).then(|| &self.g2_prepared[..g2.len()])
}
}

/// Whether `points` is a prefix of `cached`. Comparing projective points
/// costs a few field multiplications each, far less than preparing them.
fn is_prefix<P: PartialEq + Sync>(cached: &[P], points: &[P]) -> bool {
cached
.get(..points.len())
.is_some_and(|cached| cached.par_iter().zip(points).all(|(a, b)| a == b))
}

static CACHE: RwLock<Option<Arc<PreparedCache>>> = RwLock::new(None);

/// Initialize the global cache with G1 and G2 vectors.
///
/// This function implements smart re-initialization:
/// - If the cache doesn't exist, it creates one
/// - If the cache exists but is too small, it replaces it with a larger one
/// - If the cache exists and is large enough, it does nothing (reuses existing)
///
/// This allows multiple proofs with different setup sizes to run in the same process.
/// If `g1_vec` and `g2_vec` are prefixes of the cached points, this does
/// nothing. Otherwise it replaces the cache, whether the cache is smaller or
/// was built from other points.
///
/// # Arguments
/// * `g1_vec` - Vector of G1 points to prepare and cache
Expand All @@ -52,10 +90,11 @@ pub fn init_cache(g1_vec: &[ArkG1], g2_vec: &[ArkG2]) {
// Fast path: check if existing cache is sufficient (read lock only)
{
let read_guard = CACHE.read().unwrap();
if let Some(ref cache) = *read_guard {
if cache.g1_prepared.len() >= g1_vec.len() && cache.g2_prepared.len() >= g2_vec.len() {
return; // Existing cache is large enough
}
if read_guard
.as_ref()
.is_some_and(|cache| cache.covers(g1_vec, g2_vec))
{
return;
}
}

Expand Down Expand Up @@ -85,15 +124,18 @@ pub fn init_cache(g1_vec: &[ArkG1], g2_vec: &[ArkG2]) {
let mut write_guard = CACHE.write().unwrap();

// Double-check after acquiring the write lock (another thread may have initialized).
if let Some(ref cache) = *write_guard {
if cache.g1_prepared.len() >= g1_vec.len() && cache.g2_prepared.len() >= g2_vec.len() {
return;
}
if write_guard
.as_ref()
.is_some_and(|cache| cache.covers(g1_vec, g2_vec))
{
return;
}

*write_guard = Some(Arc::new(PreparedCache {
g1_prepared,
g2_prepared,
g1: g1_vec.to_vec(),
g2: g2_vec.to_vec(),
}));
}

Expand Down
25 changes: 14 additions & 11 deletions src/backends/arkworks/ark_pairing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ mod pairing_helpers {
#[cfg(feature = "cache")]
{
if let Some(cache) = crate::backends::arkworks::ark_cache::get_prepared_cache() {
return multi_pair_with_prepared(ps_prep, &cache.g2_prepared[..qs.len()]);
if let Some(qs_prep) = cache.g2_prepared_for(qs) {
return multi_pair_with_prepared(ps_prep, qs_prep);
}
}
}

Expand Down Expand Up @@ -108,12 +110,9 @@ mod pairing_helpers {
#[cfg(feature = "cache")]
{
if let Some(cache) = crate::backends::arkworks::ark_cache::get_prepared_cache() {
let ps_prep: Vec<_> = ps
.iter()
.enumerate()
.map(|(i, _)| cache.g1_prepared[i].clone())
.collect();
return multi_pair_with_prepared(ps_prep, &qs_prep);
if let Some(ps_prep) = cache.g1_prepared_for(ps) {
return multi_pair_with_prepared(ps_prep.to_vec(), &qs_prep);
}
}
}

Expand Down Expand Up @@ -190,6 +189,8 @@ mod pairing_helpers {

#[cfg(feature = "cache")]
let cache = crate::backends::arkworks::ark_cache::get_prepared_cache();
#[cfg(feature = "cache")]
let cached_qs = cache.as_deref().and_then(|c| c.g2_prepared_for(qs));

let combined = ps
.par_chunks(chunk_size)
Expand All @@ -207,8 +208,8 @@ mod pairing_helpers {
.collect();

#[cfg(feature = "cache")]
let qs_prep: Vec<<Bn254 as Pairing>::G2Prepared> = if let Some(ref c) = cache {
c.g2_prepared[start_idx..end_idx].to_vec()
let qs_prep: Vec<<Bn254 as Pairing>::G2Prepared> = if let Some(cached) = cached_qs {
cached[start_idx..end_idx].to_vec()
} else {
use ark_bn254::G2Affine;
qs[start_idx..end_idx]
Expand Down Expand Up @@ -254,6 +255,8 @@ mod pairing_helpers {

#[cfg(feature = "cache")]
let cache = crate::backends::arkworks::ark_cache::get_prepared_cache();
#[cfg(feature = "cache")]
let cached_ps = cache.as_deref().and_then(|c| c.g1_prepared_for(ps));

let combined = qs
.par_chunks(chunk_size)
Expand All @@ -271,8 +274,8 @@ mod pairing_helpers {
.collect();

#[cfg(feature = "cache")]
let ps_prep: Vec<<Bn254 as Pairing>::G1Prepared> = if let Some(ref c) = cache {
c.g1_prepared[start_idx..end_idx].to_vec()
let ps_prep: Vec<<Bn254 as Pairing>::G1Prepared> = if let Some(cached) = cached_ps {
cached[start_idx..end_idx].to_vec()
} else {
use ark_bn254::G1Affine;
ps[start_idx..end_idx]
Expand Down
Loading
Loading