From c119dcdab7a3434c64d30679cc64c1c30c7a7330 Mon Sep 17 00:00:00 2001 From: Jack Lloyd Date: Wed, 11 Feb 2026 16:34:24 -0500 Subject: [PATCH] perf(crypto): Allow passing points as references to BLS12-381 multi-scalar mul This avoids having to unnecesssarily clone points in various circumstances. --- .../crypto_lib/bls12_381/type/src/lib.rs | 78 +++++++++++++++++-- .../src/ni_dkg/fs_ni_dkg/nizk_chunking.rs | 20 ++--- .../src/ni_dkg/fs_ni_dkg/nizk_sharing.rs | 7 +- .../ni_dkg/groth20_bls12_381/transcript.rs | 12 +-- .../src/types/public_coefficients.rs | 8 +- 5 files changed, 90 insertions(+), 35 deletions(-) diff --git a/rs/crypto/internal/crypto_lib/bls12_381/type/src/lib.rs b/rs/crypto/internal/crypto_lib/bls12_381/type/src/lib.rs index 259017f4c236..a87bdf5057f2 100644 --- a/rs/crypto/internal/crypto_lib/bls12_381/type/src/lib.rs +++ b/rs/crypto/internal/crypto_lib/bls12_381/type/src/lib.rs @@ -1905,17 +1905,43 @@ macro_rules! declare_muln_vartime_dispatch_for { } } - fn muln_vartime_naive(points: &[Self], scalars: &[Scalar]) -> Self { + /// Multiscalar multiplication from references + /// + /// Same as `muln_vartime` but accepts points as a slice of references, + /// avoiding the need for callers to clone points into a contiguous `Vec`. + pub fn muln_vartime_ref(points: &[&Self], scalars: &[Scalar]) -> Self { + if points.len() == 1 { + return points[0] * &scalars[0]; + } else if points.len() == 2 { + return Self::mul2_vartime(points[0], &scalars[0], points[1], &scalars[1]); + } else if points.len() < $naive_cutoff { + Self::muln_vartime_naive(points, scalars) + } else if points.len() < $w3_cutoff { + Self::muln_vartime_window_3(points, scalars) + } else { + Self::muln_vartime_window_4(points, scalars) + } + } + + fn muln_vartime_naive>( + points: &[P], + scalars: &[Scalar], + ) -> Self { let (accum, points, scalars) = if points.len() % 2 == 0 { (Self::identity(), points, scalars) } else { - (&points[0] * &scalars[0], &points[1..], &scalars[1..]) + ( + points[0].borrow() * &scalars[0], + &points[1..], + &scalars[1..], + ) }; points .chunks(2) .zip(scalars.chunks(2)) .fold(accum, |accum, (c_p, c_s)| { - accum + Self::mul2_vartime(&c_p[0], &c_s[0], &c_p[1], &c_s[1]) + accum + + Self::mul2_vartime(c_p[0].borrow(), &c_s[0], c_p[1].borrow(), &c_s[1]) }) } } @@ -1958,17 +1984,53 @@ macro_rules! declare_muln_affine_vartime_dispatch_for { } } - fn muln_affine_vartime_naive(points: &[$affine], scalars: &[Scalar]) -> Self { + /// Multiscalar multiplication from references + /// + /// Same as `muln_affine_vartime` but accepts points as a slice of references, + /// avoiding the need for callers to clone points into a contiguous `Vec`. + pub fn muln_affine_vartime_ref(points: &[&$affine], scalars: &[Scalar]) -> Self { + if points.len() == 1 { + return points[0] * &scalars[0]; + } else if points.len() == 2 { + return Self::mul2_affine_vartime( + points[0], + &scalars[0], + points[1], + &scalars[1], + ); + } else if points.len() < $naive_cutoff { + Self::muln_affine_vartime_naive(points, scalars) + } else if points.len() < $w3_cutoff { + Self::muln_affine_vartime_window_3(points, scalars) + } else { + Self::muln_affine_vartime_window_4(points, scalars) + } + } + + fn muln_affine_vartime_naive>( + points: &[P], + scalars: &[Scalar], + ) -> Self { let (accum, points, scalars) = if points.len() % 2 == 0 { (Self::identity(), points, scalars) } else { - (&points[0] * &scalars[0], &points[1..], &scalars[1..]) + ( + points[0].borrow() * &scalars[0], + &points[1..], + &scalars[1..], + ) }; points .chunks(2) .zip(scalars.chunks(2)) .fold(accum, |accum, (c_p, c_s)| { - accum + Self::mul2_affine_vartime(&c_p[0], &c_s[0], &c_p[1], &c_s[1]) + accum + + Self::mul2_affine_vartime( + c_p[0].borrow(), + &c_s[0], + c_p[1].borrow(), + &c_s[1], + ) }) } } @@ -1978,7 +2040,7 @@ macro_rules! declare_muln_affine_vartime_dispatch_for { macro_rules! declare_pippengers_for { ( $typ:ty, $fn_name:ident, $input:ty, $window:expr_2021 ) => { impl $typ { - fn $fn_name(points: &[$input], scalars: &[Scalar]) -> Self { + fn $fn_name>(points: &[P], scalars: &[Scalar]) -> Self { // Configurable window size: can be in 1..=8 type Window = WindowInfo<$window>; @@ -2004,7 +2066,7 @@ macro_rules! declare_pippengers_for { for j in 0..count { let bucket_index = windows[j][i] as usize; if bucket_index > 0 { - buckets[bucket_index] += &points[j]; + buckets[bucket_index] += points[j].borrow(); max_bucket = std::cmp::max(max_bucket, bucket_index); } } diff --git a/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_chunking.rs b/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_chunking.rs index 92525aaca58e..b5cdd8d085aa 100644 --- a/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_chunking.rs +++ b/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_chunking.rs @@ -279,12 +279,11 @@ pub fn prove_chunking( let dd = g1.batch_mul(&delta); let yy = { - let y0_and_pk = [y0.clone()] - .iter() - .chain(&instance.public_keys) - .cloned() - .collect::>(); - G1Projective::muln_affine_vartime(&y0_and_pk, &delta).to_affine() + let y0_and_pk: Vec<_> = [&y0] + .into_iter() + .chain(instance.public_keys.iter()) + .collect(); + G1Projective::muln_affine_vartime_ref(&y0_and_pk, &delta).to_affine() }; let second_move = SecondMoveChunking::from(&z_s, &dd, &yy); @@ -414,12 +413,7 @@ pub fn verify_chunking( let cij_to_eijks: Vec = (0..NUM_ZK_REPETITIONS) .map(|k| { - let c_ij_s: Vec<_> = instance - .ciphertext_chunks - .iter() - .flatten() - .cloned() - .collect(); + let c_ij_s: Vec<_> = instance.ciphertext_chunks.iter().flatten().collect(); let e_ijk_s: Vec<_> = e .iter() .flatten() @@ -429,7 +423,7 @@ pub fn verify_chunking( return Err(ZkProofChunkingError::InvalidProof); } - Ok(G1Projective::muln_affine_vartime(&c_ij_s, &e_ijk_s) + &nizk.cc[k]) + Ok(G1Projective::muln_affine_vartime_ref(&c_ij_s, &e_ijk_s) + &nizk.cc[k]) }) .collect::, _>>()?; diff --git a/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_sharing.rs b/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_sharing.rs index 6eab3b929eb7..337c2fbc703a 100644 --- a/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_sharing.rs +++ b/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_sharing.rs @@ -321,12 +321,11 @@ pub fn verify_sharing( // The two expressions are re-arranged so that it becomes possible to compute // everything with a single multi scalar multiplication. - let instance_inputs = instance + let instance_inputs: Vec<_> = instance .combined_ciphertexts .iter() .chain(&instance.public_keys) - .cloned() - .collect::>(); + .collect(); let challenges = { let mut c = Vec::with_capacity(xpow.len() * 2); for xp in &xpow { @@ -339,7 +338,7 @@ pub fn verify_sharing( c }; - let lhs = G1Projective::muln_affine_vartime(&instance_inputs, &challenges); + let lhs = G1Projective::muln_affine_vartime_ref(&instance_inputs, &challenges); let rhs = &instance.g1_gen.mul_vartime(&nizk.z_alpha) + &nizk.yy.neg(); if lhs != rhs { diff --git a/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/groth20_bls12_381/transcript.rs b/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/groth20_bls12_381/transcript.rs index f32307107a36..c5651f92741e 100644 --- a/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/groth20_bls12_381/transcript.rs +++ b/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/groth20_bls12_381/transcript.rs @@ -211,15 +211,15 @@ fn compute_transcript( .to_vec() }; - let mut combined = vec![]; + let mut combined = Vec::with_capacity(threshold.get() as usize); for i in 0..threshold.get() as usize { - let points = individual_public_coefficients - .iter() - .map(|pts| pts.1.coefficients[i].0.clone()) - .collect::>(); + let points: Vec<_> = individual_public_coefficients + .values() + .map(|pc| &pc.coefficients[i].0) + .collect(); combined.push(crate::types::PublicKey( - G2Projective::muln_affine_vartime(&points, &coefficients).to_affine(), + G2Projective::muln_affine_vartime_ref(&points, &coefficients).to_affine(), )); } diff --git a/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/types/public_coefficients.rs b/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/types/public_coefficients.rs index 1a36b49cc913..79b55ab75495 100644 --- a/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/types/public_coefficients.rs +++ b/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/types/public_coefficients.rs @@ -82,8 +82,8 @@ impl PublicCoefficients { ) -> Result { let all_x: Vec = samples.iter().map(|(x, _)| *x).collect(); let coefficients = Self::lagrange_coefficients_at_zero(&all_x)?; - let pts: Vec<_> = samples.iter().map(|(_, pt)| pt.clone()).collect(); - Ok(G1Projective::muln_affine_vartime(&pts, &coefficients)) + let pts: Vec<_> = samples.iter().map(|(_, pt)| pt).collect(); + Ok(G1Projective::muln_affine_vartime_ref(&pts, &coefficients)) } /// Given a list of samples `(x, f(x) * g)` for a polynomial `f` in the scalar field, and a generator g of G2 returns @@ -98,8 +98,8 @@ impl PublicCoefficients { ) -> Result { let all_x: Vec = samples.iter().map(|(x, _)| *x).collect(); let coefficients = Self::lagrange_coefficients_at_zero(&all_x)?; - let pts: Vec<_> = samples.iter().map(|(_, pt)| pt.clone()).collect(); - Ok(G2Projective::muln_affine_vartime(&pts, &coefficients)) + let pts: Vec<_> = samples.iter().map(|(_, pt)| pt).collect(); + Ok(G2Projective::muln_affine_vartime_ref(&pts, &coefficients)) } /// Compute the Lagrange coefficients at x=0.