From 7d804887982a3ff0422be9e467588e1d35ce084b Mon Sep 17 00:00:00 2001 From: Edvard Fagerholm Date: Mon, 8 Jun 2026 20:02:22 +0300 Subject: [PATCH] ed25519: avoid split-scalar canonical checks (#24) The triple-base verifier splits b into zero-extended 128-bit halves, so b_lo and b_hi are already canonical. Add a crate-private unchecked constructor and use it for that internal AVX2 path. Benchmark notes: - Ran this repository's Criterion benchmark program, benches/bench.rs, filtering to Single Verification, pinned to CPU 4 with 1s warmup, 2s measurement, and sample size 10. - local_verify_zebra estimate was 19.996 us, with 95% CI 19.862..20.077 us. - master measured 20.051 us, with 95% CI 19.938..20.134 us, so this branch was about 0.27% faster in that run. --- .../src/backend/vector/scalar_mul/vartime_triple_base.rs | 4 ++-- curve25519/solana-ed25519/src/scalar.rs | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/curve25519/solana-ed25519/src/backend/vector/scalar_mul/vartime_triple_base.rs b/curve25519/solana-ed25519/src/backend/vector/scalar_mul/vartime_triple_base.rs index 4508051..7dcc5d1 100644 --- a/curve25519/solana-ed25519/src/backend/vector/scalar_mul/vartime_triple_base.rs +++ b/curve25519/solana-ed25519/src/backend/vector/scalar_mul/vartime_triple_base.rs @@ -78,8 +78,8 @@ pub mod spec { b_lo_bytes[..16].copy_from_slice(&b_bytes[..16]); b_hi_bytes[..16].copy_from_slice(&b_bytes[16..]); - let b_lo = Scalar::from_canonical_bytes(b_lo_bytes).unwrap(); - let b_hi = Scalar::from_canonical_bytes(b_hi_bytes).unwrap(); + let b_lo = Scalar::from_canonical_bytes_unchecked(b_lo_bytes); + let b_hi = Scalar::from_canonical_bytes_unchecked(b_hi_bytes); // Compute NAF representations (all scalars are now ~128 bits) let a1_naf = a1.non_adjacent_form(5); diff --git a/curve25519/solana-ed25519/src/scalar.rs b/curve25519/solana-ed25519/src/scalar.rs index 56d4b3c..8be511e 100644 --- a/curve25519/solana-ed25519/src/scalar.rs +++ b/curve25519/solana-ed25519/src/scalar.rs @@ -232,6 +232,13 @@ impl Scalar { CtOption::new(candidate, high_bit_unset & candidate.is_canonical()) } + /// Construct a `Scalar` from bytes that are known to be canonical. + #[inline] + #[cfg(target_arch = "x86_64")] + pub(crate) const fn from_canonical_bytes_unchecked(bytes: [u8; 32]) -> Scalar { + Scalar { bytes } + } + /// Construct a `Scalar` from the low 255 bits of a 256-bit integer. This breaks the invariant /// that scalars are always reduced. Scalar-scalar arithmetic, i.e., addition, subtraction, /// multiplication, **does not work** on scalars produced from this function. You may only use