fix 128bits scalar precondition (#44)

* Add prechecked optimized triple-base mul

Introduce a prechecked 128/128/256 optimized path for vartime triple-base multiplication: vartime_triple_base_mul_128_128_256 now checks whether a1 and a2 fit in 128 bits and falls back to general multiplication if not. Add vartime_triple_base_mul_128_128_256_prechecked and corresponding serial/vector backend implementations (renamed to *_prechecked). Add scalar_fits_in_128_bits helper and update callers (verification_key) to use the prechecked path. Update docs/comments and add a test to ensure full-width scalars are handled by the fallback path.

* bring back the docs

* CI
This commit is contained in:
zz-sol 2026-06-16 08:59:35 -04:00 committed by GitHub
parent cdc0688a81
commit 53383206b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 79 additions and 23 deletions

View file

@ -238,7 +238,8 @@ pub fn vartime_double_base_mul(a: &Scalar, A: &EdwardsPoint, b: &Scalar) -> Edwa
/// Compute \\(a_1 A_1 + a_2 A_2 + b B\\) in variable time, where \\(B\\) is the Ed25519 basepoint.
///
/// This function is optimized for the case where \\(a_1\\) and \\(a_2\\) are less than \\(2^{128}\\).
/// This function uses an optimized path when \\(a_1\\) and \\(a_2\\) are less than \\(2^{128}\\),
/// and falls back to general scalar multiplication otherwise.
#[allow(non_snake_case)]
pub fn vartime_triple_base_mul_128_128_256(
a1: &Scalar,
@ -246,14 +247,39 @@ pub fn vartime_triple_base_mul_128_128_256(
a2: &Scalar,
A2: &EdwardsPoint,
b: &Scalar,
) -> EdwardsPoint {
if !scalar_fits_in_128_bits(a1) || !scalar_fits_in_128_bits(a2) {
return (a1 * A1) + (a2 * A2) + EdwardsPoint::mul_base(b);
}
vartime_triple_base_mul_128_128_256_prechecked(a1, A1, a2, A2, b)
}
/// Compute \\(a_1 A_1 + a_2 A_2 + b B\\) using the optimized 128/128/256-bit path.
///
/// Callers must ensure \\(a_1\\) and \\(a_2\\) are less than \\(2^{128}\\).
#[allow(non_snake_case)]
pub(crate) fn vartime_triple_base_mul_128_128_256_prechecked(
a1: &Scalar,
A1: &EdwardsPoint,
a2: &Scalar,
A2: &EdwardsPoint,
b: &Scalar,
) -> EdwardsPoint {
match get_selected_backend() {
#[cfg(target_arch = "x86_64")]
BackendKind::Avx2 => {
vector::scalar_mul::vartime_triple_base::spec_avx2::mul_128_128_256(a1, A1, a2, A2, b)
vector::scalar_mul::vartime_triple_base::spec_avx2::mul_128_128_256_prechecked(
a1, A1, a2, A2, b,
)
}
BackendKind::Serial => {
serial::scalar_mul::vartime_triple_base::mul_128_128_256(a1, A1, a2, A2, b)
serial::scalar_mul::vartime_triple_base::mul_128_128_256_prechecked(a1, A1, a2, A2, b)
}
}
}
#[inline]
fn scalar_fits_in_128_bits(scalar: &Scalar) -> bool {
scalar.as_bytes()[16..32].iter().all(|&byte| byte == 0)
}

View file

@ -24,6 +24,12 @@ use crate::window::NafLookupTable5;
/// This function is optimized for the case where \\(a_1\\) and \\(a_2\\) are known to be less than
/// \\(2^{128}\\), while \\(b\\) is a full 256-bit scalar.
///
/// # Precondition
///
/// Callers must ensure \\(a_1\\) and \\(a_2\\) are less than \\(2^{128}\\). Use
/// `vartime_triple_base_mul_128_128_256` for a checked wrapper that falls back
/// to general scalar multiplication for full-width scalars.
///
/// # Optimization Strategy
///
/// The function decomposes the 256-bit scalar \\(b\\) as \\(b = b_{lo} + b_{hi} \cdot 2^{128}\\),
@ -50,17 +56,13 @@ use crate::window::NafLookupTable5;
///
/// The algorithm shares doublings across all four scalar multiplications, processing
/// only 128 bits instead of 256, providing approximately 2x speedup over the naive approach.
pub fn mul_128_128_256(
pub(crate) fn mul_128_128_256_prechecked(
a1: &Scalar,
A1: &EdwardsPoint,
a2: &Scalar,
A2: &EdwardsPoint,
b: &Scalar,
) -> EdwardsPoint {
// assert that a1 and a2 are less than 2^128
debug_assert!(a1.as_bytes()[16..32].iter().all(|&b| b == 0));
debug_assert!(a2.as_bytes()[16..32].iter().all(|&b| b == 0));
// Decompose b into b_lo (lower 128 bits) and b_hi (upper 128 bits)
// b = b_lo + b_hi * 2^128
let b_bytes = b.as_bytes();
@ -183,7 +185,7 @@ mod test {
let A2 = constants::ED25519_BASEPOINT_POINT * Scalar::from(3u64);
// Compute using the optimized triple-base function
let result = mul_128_128_256(&a1, &A1, &a2, &A2, &b);
let result = mul_128_128_256_prechecked(&a1, &A1, &a2, &A2, &b);
// Compute using naive addition
let expected = (a1 * A1 + a2 * A2) + b * constants::ED25519_BASEPOINT_POINT;
@ -217,7 +219,7 @@ mod test {
let A2 = constants::ED25519_BASEPOINT_POINT * Scalar::from(7u64);
// Test the optimized 128-bit version
let result_128 = mul_128_128_256(&a1, &A1, &a2, &A2, &b);
let result_128 = mul_128_128_256_prechecked(&a1, &A1, &a2, &A2, &b);
// Compute expected result
let expected = (a1 * A1 + a2 * A2) + b * constants::ED25519_BASEPOINT_POINT;
@ -234,7 +236,7 @@ mod test {
let A1 = constants::ED25519_BASEPOINT_POINT * Scalar::from(2u64);
let A2 = constants::ED25519_BASEPOINT_POINT * Scalar::from(3u64);
let result = mul_128_128_256(&a1, &A1, &a2, &A2, &b);
let result = mul_128_128_256_prechecked(&a1, &A1, &a2, &A2, &b);
let expected = a2 * A2 + b * constants::ED25519_BASEPOINT_POINT;
assert_eq!(result, expected);
@ -249,7 +251,7 @@ mod test {
let A1 = EdwardsPoint::identity();
let A2 = constants::ED25519_BASEPOINT_POINT * Scalar::from(3u64);
let result = mul_128_128_256(&a1, &A1, &a2, &A2, &b);
let result = mul_128_128_256_prechecked(&a1, &A1, &a2, &A2, &b);
let expected = a2 * A2 + b * constants::ED25519_BASEPOINT_POINT;
assert_eq!(result, expected);
@ -265,7 +267,7 @@ mod test {
let A1 = constants::ED25519_BASEPOINT_POINT * Scalar::from(11u64);
let A2 = constants::ED25519_BASEPOINT_POINT * Scalar::from(13u64);
let result_optimized = mul_128_128_256(&a1, &A1, &a2, &A2, &b);
let result_optimized = mul_128_128_256_prechecked(&a1, &A1, &a2, &A2, &b);
let result_general = (a1 * A1 + a2 * A2) + b * constants::ED25519_BASEPOINT_POINT;
assert_eq!(result_optimized, result_general);
@ -287,7 +289,7 @@ mod test {
let A1 = constants::ED25519_BASEPOINT_POINT * Scalar::from(17u64);
let A2 = constants::ED25519_BASEPOINT_POINT * Scalar::from(19u64);
let result = mul_128_128_256(&a1, &A1, &a2, &A2, &b);
let result = mul_128_128_256_prechecked(&a1, &A1, &a2, &A2, &b);
let expected = (a1 * A1 + a2 * A2) + b * constants::ED25519_BASEPOINT_POINT;
assert_eq!(result, expected);
@ -322,7 +324,7 @@ mod test {
let A2 = constants::ED25519_BASEPOINT_POINT * A2_scalar;
// Compute using the optimized triple-base function
let result_optimized = mul_128_128_256(&a1, &A1, &a2, &A2, &b);
let result_optimized = mul_128_128_256_prechecked(&a1, &A1, &a2, &A2, &b);
// Compute using raw operations: a1*A1 + a2*A2 + b*B
let expected = (a1 * A1 + a2 * A2) + b * constants::ED25519_BASEPOINT_POINT;

View file

@ -35,6 +35,12 @@ pub mod spec {
/// This function is optimized for the case where \\(a_1\\) and \\(a_2\\) are known to be less than
/// \\(2^{128}\\), while \\(b\\) is a full 256-bit scalar.
///
/// # Precondition
///
/// Callers must ensure \\(a_1\\) and \\(a_2\\) are less than \\(2^{128}\\). Use
/// `vartime_triple_base_mul_128_128_256` for a checked wrapper that falls back
/// to general scalar multiplication for full-width scalars.
///
/// # Optimization Strategy
///
/// The function decomposes the 256-bit scalar \\(b\\) as \\(b = b_{lo} + b_{hi} \cdot 2^{128}\\),
@ -63,17 +69,13 @@ pub mod spec {
///
/// This SIMD implementation uses vectorized point operations (AVX2 or AVX512-IFMA) for
/// improved performance over the serial backend.
pub fn mul_128_128_256(
pub(crate) fn mul_128_128_256_prechecked(
a1: &Scalar,
A1: &EdwardsPoint,
a2: &Scalar,
A2: &EdwardsPoint,
b: &Scalar,
) -> EdwardsPoint {
// assert that a1 and a2 are less than 2^128
debug_assert!(a1.as_bytes()[16..32].iter().all(|&b| b == 0));
debug_assert!(a2.as_bytes()[16..32].iter().all(|&b| b == 0));
// Decompose b into b_lo (lower 128 bits) and b_hi (upper 128 bits)
// b = b_lo + b_hi * 2^128
let b_bytes = b.as_bytes();

View file

@ -411,8 +411,11 @@ impl VerificationKey {
// Compute τs
let ts = tau * s;
let A = if flip_h { -self.minus_A } else { self.minus_A };
// Compute the multi-scalar multiplication
let result = EdwardsPoint::vartime_triple_scalar_mul_basepoint(&tau, &neg_R, &rho, &A, &ts);
// HEEA decomposition guarantees tau and rho fit the optimized
// 128/128/256-bit multiplication path.
let result = crate::backend::vartime_triple_base_mul_128_128_256_prechecked(
&tau, &neg_R, &rho, &A, &ts,
);
if result.mul_by_cofactor().is_identity() {
Ok(())

View file

@ -1075,7 +1075,8 @@ impl EdwardsPoint {
/// Compute \\(a_1 A_1 + a_2 A_2 + b B\\) in variable time, where \\(B\\) is the Ed25519 basepoint.
///
/// This function is optimized for the case where \\(a_1\\) and \\(a_2\\) are less than \\(2^{128}\\).
/// This function is optimized for the case where \\(a_1\\) and \\(a_2\\) are less than \\(2^{128}\\),
/// and falls back to general scalar multiplication for full-width scalars.
///
/// # Example
///
@ -2420,6 +2421,28 @@ mod test {
assert_eq!(result.compress(), DOUBLE_SCALAR_MULT_RESULT);
}
#[test]
fn triple_scalar_mul_basepoint_accepts_full_width_scalars() {
let mut a1_bytes = [0u8; 32];
a1_bytes[0] = 7;
a1_bytes[16] = 1;
let a1 = Scalar::from_canonical_bytes(a1_bytes).unwrap();
let mut a2_bytes = [0u8; 32];
a2_bytes[0] = 11;
a2_bytes[24] = 1;
let a2 = Scalar::from_canonical_bytes(a2_bytes).unwrap();
let b = B_SCALAR;
let A1 = constants::ED25519_BASEPOINT_POINT * Scalar::from(17u64);
let A2 = constants::ED25519_BASEPOINT_POINT * Scalar::from(19u64);
let result = EdwardsPoint::vartime_triple_scalar_mul_basepoint(&a1, &A1, &a2, &A2, &b);
let expected = (a1 * A1) + (a2 * A2) + EdwardsPoint::mul_base(&b);
assert_eq!(result, expected);
}
#[test]
#[cfg(feature = "alloc")]
fn multiscalar_mul_vs_ed25519py() {