ed25519: add 128-bit NAF path (#27)

* ed25519: add 128-bit NAF path

Add Scalar::non_adjacent_form_128 and use it in the serial and vector triple-base verifier paths for scalars known to fit in 128 bits. The helper computes only the HEEA-readable digit range instead of producing a full 256-entry NAF array for each scalar.

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.740 us, with 95% CI 19.686..19.789 us.

- master measured 20.051 us, with 95% CI 19.938..20.134 us, so this branch was about 1.55% faster in that run.

* Use 128-bit NAF in triple-base paths

---------

Co-authored-by: zz-sol <allaboutshop10@163.com>
This commit is contained in:
Edvard Fagerholm 2026-06-24 10:47:31 +03:00 committed by GitHub
parent 11a5cf38fb
commit 0a54ccaf04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 93 additions and 8 deletions

View file

@ -91,10 +91,10 @@ pub(crate) fn mul_128_128_256_prechecked(
// The serial backend keeps b_lo at width 5 even when table_B is the larger
// precomputed width-8 basepoint table. The vector backend uses width 8 in
// that configuration.
let a1_naf = a1.non_adjacent_form(DYNAMIC_NAF_WINDOW);
let a2_naf = a2.non_adjacent_form(DYNAMIC_NAF_WINDOW);
let b_lo_naf = b_lo.non_adjacent_form(B_LO_NAF_WINDOW);
let b_hi_naf = b_hi.non_adjacent_form(DYNAMIC_NAF_WINDOW);
let a1_naf = a1.non_adjacent_form_128(DYNAMIC_NAF_WINDOW);
let a2_naf = a2.non_adjacent_form_128(DYNAMIC_NAF_WINDOW);
let b_lo_naf = b_lo.non_adjacent_form_128(B_LO_NAF_WINDOW);
let b_hi_naf = b_hi.non_adjacent_form_128(DYNAMIC_NAF_WINDOW);
// Find starting index - check all NAFs up to bit 127
// (with potential carry to bit 128 or 129)

View file

@ -102,10 +102,10 @@ pub mod spec {
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(DYNAMIC_NAF_WINDOW);
let a2_naf = a2.non_adjacent_form(DYNAMIC_NAF_WINDOW);
let b_lo_naf = b_lo.non_adjacent_form(B_LO_NAF_WINDOW);
let b_hi_naf = b_hi.non_adjacent_form(DYNAMIC_NAF_WINDOW);
let a1_naf = a1.non_adjacent_form_128(DYNAMIC_NAF_WINDOW);
let a2_naf = a2.non_adjacent_form_128(DYNAMIC_NAF_WINDOW);
let b_lo_naf = b_lo.non_adjacent_form_128(B_LO_NAF_WINDOW);
let b_hi_naf = b_hi.non_adjacent_form_128(DYNAMIC_NAF_WINDOW);
// Find starting index - check all NAFs up to bit 127
// (with potential carry to bit 128 or 129)

View file

@ -152,6 +152,8 @@ use crate::traits::HEEADecomposition;
mod heea;
pub(crate) use heea::HEEA_MAX_INDEX;
pub(crate) const NAF_128_SIZE: usize = HEEA_MAX_INDEX + 1;
/// An `UnpackedScalar` represents an element of the field GF(l), optimized for speed.
///
/// This is pinned to the 64-bit serial scalar backend.
@ -1077,6 +1079,55 @@ impl Scalar {
naf
}
/// Compute a width-\\(w\\) non-adjacent form for scalars known to fit in 128 bits.
pub(crate) fn non_adjacent_form_128(&self, w: usize) -> [i8; NAF_128_SIZE] {
// required by the NAF definition
debug_assert!(w >= 2);
// required so that the NAF digits fit in i8
debug_assert!(w <= 8);
debug_assert!(self.bytes[16..32].iter().all(|&b| b == 0));
let mut naf = [0i8; NAF_128_SIZE];
let mut x_u64 = [0u64; 3];
read_le_u64_into(&self.bytes[..16], &mut x_u64[0..2]);
let width = 1u64 << w;
let window_mask = width - 1;
let mut pos = 0;
let mut carry = 0;
while pos < HEEA_MAX_INDEX {
let u64_idx = pos / 64;
let bit_idx = pos % 64;
let bit_buf: u64 = if bit_idx < 64 - w {
x_u64[u64_idx] >> bit_idx
} else {
(x_u64[u64_idx] >> bit_idx) | (x_u64[1 + u64_idx] << (64 - bit_idx))
};
let window = carry + (bit_buf & window_mask);
if window & 1 == 0 {
pos += 1;
continue;
}
if window < width / 2 {
carry = 0;
naf[pos] = window as i8;
} else {
carry = 1;
naf[pos] = (window as i8).wrapping_sub(width as i8);
}
pos += w;
}
debug_assert_eq!(carry, 0);
naf
}
/// Write this scalar in radix 16, with coefficients in \\([-8,8)\\),
/// i.e., compute \\(a\_i\\) such that
/// $$
@ -1693,6 +1744,40 @@ pub(crate) mod test {
}
}
#[test]
fn non_adjacent_form_128_matches_generic() {
let mut high_bit = [0u8; 32];
high_bit[15] = 0x80;
let cases = [
Scalar::ZERO,
Scalar::ONE,
Scalar::from(u64::MAX),
Scalar::from(0xfedc_ba98_7654_3210_0123_4567_89ab_cdefu128),
Scalar::from(u128::MAX),
Scalar { bytes: high_bit },
];
for scalar in cases {
for w in 2..=8 {
let generic = scalar.non_adjacent_form(w);
let naf_128 = scalar.non_adjacent_form_128(w);
for i in 0..=HEEA_MAX_INDEX {
assert_eq!(
naf_128[i], generic[i],
"NAF mismatch at index {i} for width {w}"
);
}
assert!(
generic[NAF_128_SIZE..].iter().all(|&digit| digit == 0),
"generic NAF has non-zero digits above 128-bit range for width {w}"
);
}
}
}
#[cfg(feature = "rand_core")]
fn non_adjacent_form_iter(w: usize, x: &Scalar) {
let naf = x.non_adjacent_form(w);