Skip to content
Open
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ Cargo.lock
bin
obj


# Simple Task Master - User tasks are git-ignored
.simple-task-master
.simple-task-master/lock
.claude
specs/
8 changes: 8 additions & 0 deletions banderwagon/src/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ impl Element {
Self(point)
}

/// Try to deserialize from uncompressed bytes, returning an error on failure
pub fn try_from_bytes_uncompressed(
bytes: [u8; 64],
) -> Result<Self, ark_serialize::SerializationError> {
let point = EdwardsProjective::deserialize_uncompressed_unchecked(&bytes[..])?;
Ok(Self(point))
}

pub fn from_bytes(bytes: &[u8]) -> Option<Element> {
// Switch from big endian to little endian, as arkworks library uses little endian
let mut bytes = bytes.to_vec();
Expand Down
5 changes: 2 additions & 3 deletions banderwagon/src/trait_impls/serialize.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::Element;
use ark_ec::CurveGroup;
use ark_ed_on_bls12_381_bandersnatch::EdwardsProjective;
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, SerializationError, Valid};
impl CanonicalSerialize for Element {
Expand All @@ -13,7 +12,7 @@ impl CanonicalSerialize for Element {
writer.write_all(&self.to_bytes())?;
Ok(())
}
ark_serialize::Compress::No => self.0.into_affine().serialize_uncompressed(writer),
ark_serialize::Compress::No => self.0.serialize_uncompressed(writer),
}
}

Expand Down Expand Up @@ -56,7 +55,7 @@ impl CanonicalDeserialize for Element {
}
}
ark_serialize::Compress::No => {
let point = EdwardsProjective::deserialize_uncompressed(reader)?;
let point = EdwardsProjective::deserialize_uncompressed_unchecked(reader)?;
Ok(Element(point))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ internal static unsafe partial class NativeMethods





[DllImport(__DllName, EntryPoint = "context_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern Context* context_new();

Expand Down
1 change: 1 addition & 0 deletions ffi_interface/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ banderwagon = { path = "../banderwagon" }
ipa-multipoint = { path = "../ipa-multipoint" }
verkle-spec = { path = "../verkle-spec" }
hex = "*"
rayon = "1.8.0"
verkle-trie = { path = "../verkle-trie" }
170 changes: 169 additions & 1 deletion ffi_interface/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use ipa_multipoint::crs::CRS;
use ipa_multipoint::lagrange_basis::PrecomputedWeights;
use ipa_multipoint::multiproof::{MultiPoint, MultiPointProof, ProverQuery, VerifierQuery};
use ipa_multipoint::transcript::Transcript;
use rayon::prelude::*;
pub use serialization::{fr_from_le_bytes, fr_to_le_bytes};
use verkle_trie::proof::golang_proof_format::{bytes32_to_element, hex_to_bytes32, VerkleProofGo};

Expand Down Expand Up @@ -258,12 +259,50 @@ pub fn hash_commitment(commitment: CommitmentBytes) -> ScalarBytes {
// TODO: this is actually a bottleneck for the average workflow before doing this.
fr_to_le_bytes(Element::from_bytes_unchecked_uncompressed(commitment).map_to_scalar_field())
}

/// Minimum number of commitments before using parallel processing.
/// Below this threshold, sequential processing is faster due to thread pool overhead.
const PARALLEL_HASH_THRESHOLD: usize = 100;

/// Hashes a vector of commitments.
///
/// This is more efficient than repeatedly calling `hash_commitment`
/// This is more efficient than repeatedly calling `hash_commitment`.
/// For batches of 100 or more commitments, parallel processing is automatically used.
///
/// Returns a vector of `Scalar`s representing the hash of each commitment
pub fn hash_commitments(commitments: &[CommitmentBytes]) -> Vec<ScalarBytes> {
if commitments.len() < PARALLEL_HASH_THRESHOLD {
// Sequential for small batches
hash_commitments_sequential(commitments)
} else {
// Parallel for large batches
hash_commitments_impl_parallel(commitments)
}
}

/// Hashes commitments with explicit parallelism control.
///
/// Use this when you need to control whether parallel processing is used,
/// regardless of the batch size.
///
/// # Arguments
/// * `commitments` - The commitments to hash
/// * `use_parallel` - If true, use parallel processing; if false, use sequential
///
/// Returns a vector of `Scalar`s representing the hash of each commitment
pub fn hash_commitments_parallel(
commitments: &[CommitmentBytes],
use_parallel: bool,
) -> Vec<ScalarBytes> {
if use_parallel {
hash_commitments_impl_parallel(commitments)
} else {
hash_commitments_sequential(commitments)
}
}

/// Sequential implementation of commitment hashing using batch_map_to_scalar_field
fn hash_commitments_sequential(commitments: &[CommitmentBytes]) -> Vec<ScalarBytes> {
let elements = commitments
.iter()
.map(|commitment| Element::from_bytes_unchecked_uncompressed(*commitment))
Expand All @@ -275,6 +314,14 @@ pub fn hash_commitments(commitments: &[CommitmentBytes]) -> Vec<ScalarBytes> {
.collect()
}

/// Parallel implementation of commitment hashing using rayon
fn hash_commitments_impl_parallel(commitments: &[CommitmentBytes]) -> Vec<ScalarBytes> {
commitments
.par_iter()
.map(|commitment| hash_commitment(*commitment))
.collect()
}

/// Receives a tuple (C_i, f_i(X), z_i, y_i)
///
/// Where C_i is a commitment to f_i(X) serialized as 32 bytes
Expand Down Expand Up @@ -746,3 +793,124 @@ mod prover_verifier_test {
assert!(verified);
}
}

#[cfg(test)]
mod parallel_hash_tests {
use super::*;
use ipa_multipoint::committer::Committer;

fn create_test_commitments(count: usize) -> Vec<CommitmentBytes> {
let context = Context::new();
(0..count)
.map(|i| {
// Create a unique commitment for each index
let scalar = banderwagon::Fr::from(i as u128 + 1);
let element = context.committer.scalar_mul(scalar, 0);
element.to_bytes_uncompressed()
})
.collect()
}

#[test]
fn test_parallel_hash_determinism() {
// Test that parallel hashing produces the same results as sequential
let commitments = create_test_commitments(200);

let sequential = hash_commitments_parallel(&commitments, false);
let parallel = hash_commitments_parallel(&commitments, true);

assert_eq!(
sequential, parallel,
"Parallel and sequential hashing should produce identical results"
);
}

#[test]
fn test_parallel_hash_threshold_behavior() {
// Below threshold (< 100): should use sequential
let small_batch = create_test_commitments(50);
let result1 = hash_commitments(&small_batch);
let result2 = hash_commitments_sequential(&small_batch);
assert_eq!(
result1, result2,
"Small batch should use sequential implementation"
);

// At or above threshold (>= 100): should use parallel
let large_batch = create_test_commitments(150);
let result3 = hash_commitments(&large_batch);
let result4 = hash_commitments_impl_parallel(&large_batch);
assert_eq!(
result3, result4,
"Large batch should use parallel implementation"
);
}

#[test]
fn test_parallel_hash_explicit_control() {
let commitments = create_test_commitments(50);

// Force parallel even with small batch
let parallel_result = hash_commitments_parallel(&commitments, true);
let sequential_result = hash_commitments_parallel(&commitments, false);

assert_eq!(
parallel_result, sequential_result,
"Explicit parallel control should still produce same results"
);
}

#[test]
fn test_parallel_hash_empty_input() {
let empty: Vec<CommitmentBytes> = vec![];

let sequential = hash_commitments_parallel(&empty, false);
let parallel = hash_commitments_parallel(&empty, true);

assert!(sequential.is_empty());
assert!(parallel.is_empty());
}

#[test]
fn test_parallel_hash_single_item() {
let single = create_test_commitments(1);

let sequential = hash_commitments_parallel(&single, false);
let parallel = hash_commitments_parallel(&single, true);

assert_eq!(sequential.len(), 1);
assert_eq!(parallel.len(), 1);
assert_eq!(sequential[0], parallel[0]);
}

#[test]
fn test_parallel_hash_large_batch() {
// Test with a large batch to verify parallel processing works correctly
let large_batch = create_test_commitments(500);

let sequential = hash_commitments_parallel(&large_batch, false);
let parallel = hash_commitments_parallel(&large_batch, true);

assert_eq!(sequential.len(), 500);
assert_eq!(parallel.len(), 500);
assert_eq!(
sequential, parallel,
"Large batch parallel hashing should match sequential"
);
}

#[test]
fn test_hash_commitments_matches_individual() {
// Verify that batch hashing produces same results as individual hashing
let commitments = create_test_commitments(10);

let batch_results = hash_commitments(&commitments);
let individual_results: Vec<ScalarBytes> =
commitments.iter().map(|c| hash_commitment(*c)).collect();

assert_eq!(
batch_results, individual_results,
"Batch hashing should produce same results as individual hashing"
);
}
}
1 change: 1 addition & 0 deletions ipa-multipoint/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ itertools = "0.10.1"
sha2 = "0.9.8"
rayon = "1.8.0"
hex = "0.4.3"
thiserror = "1.0"

[dev-dependencies]
criterion = "0.5.1"
Expand Down
10 changes: 8 additions & 2 deletions ipa-multipoint/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ This library uses the banderwagon prime group (https://hackmd.io/@6iQDuIePQjyYBq

## Efficiency

- Parallelism is not being used
- The `MultiPoint::open()` function uses Rayon for parallel processing of large batches (>100 grouped queries)
- Parallel paths are used for query aggregation, division mapping, and polynomial scaling
- For small batches, sequential processing is used to avoid thread pool overhead
- We have not modified pippenger to take benefit of the GLV endomorphism

## API
Expand Down Expand Up @@ -78,4 +80,8 @@ New benchmark on banderwagon subgroup: Apple M1 Pro 16GB RAM



*These benchmarks are tentative because on one hand, the machine being used may not be the what the average user uses, while on the other hand, we have not optimised the verifier algorithm to remove `bH` , the pippenger algorithm does not take into consideration GLV and we are not using rayon to parallelise.*
*These benchmarks are tentative because on one hand, the machine being used may not be the what the average user uses, while on the other hand, we have not optimised the verifier algorithm to remove `bH` and the pippenger algorithm does not take into consideration GLV.*

### Parallel Performance

The multiproof prover now uses Rayon parallelization for large batches (>100 grouped queries). For batches of 16K+ queries, expect 4-8x speedup on multi-core systems. The parallel threshold of 100 queries balances parallelization overhead against performance gains. You can control the number of threads using the `RAYON_NUM_THREADS` environment variable.
Loading