From 64d0ebef0e6c930a12881a3f32e8ebfbb4af592c Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Fri, 28 Aug 2026 10:48:14 +0900 Subject: [PATCH 1/2] make some doc touches --- ed25519-verify/src/config.rs | 38 ++++++++++--------- ed25519-verify/src/error.rs | 10 ++--- ed25519-verify/src/verifier.rs | 68 +++++++++++++--------------------- 3 files changed, 50 insertions(+), 66 deletions(-) diff --git a/ed25519-verify/src/config.rs b/ed25519-verify/src/config.rs index 6e56310..e55fd8e 100644 --- a/ed25519-verify/src/config.rs +++ b/ed25519-verify/src/config.rs @@ -25,27 +25,30 @@ /// are public so callers can compose arbitrary combinations, typically by /// starting from a preset and overriding a single knob: /// -/// Canonical `S` (`S < L`) is enforced for every criteria set and so has no -/// corresponding field: the curve back-end converts scalars through -/// `Scalar::from_canonical_bytes` and rejects anything out of range before any -/// group operation runs. -/// /// ``` /// use solana_ed25519_verify::VerificationCriteria; /// -/// let strict_s = VerificationCriteria { +/// let custom = VerificationCriteria { /// reject_small_order_a: true, /// ..VerificationCriteria::zip215() /// }; /// ``` +/// +/// Canonical `S` (`S < L`) has no corresponding field. Every profile worth +/// targeting requires it — accepting `S >= L` reintroduces signature +/// malleability — and the multiscalar-mul syscall enforces it regardless, +/// converting scalars through `Scalar::from_canonical_bytes` and rejecting +/// out-of-range values before any group operation runs. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct VerificationCriteria { - /// Use the cofactored verification equation `[8](S·B − H·A) == [8]R`. + /// Use the cofactored verification equation + /// `[8](S·B − H·A − R) == identity`. /// - /// When `false`, the cofactorless equation `S·B − H·A == R` is used, which - /// rejects mixed-order points that the cofactored equation tolerates. The - /// cofactored form costs one extra multiplication by the cofactor 8, which - /// the verifier performs as three `sol_curve_group_op` additions. + /// When `false`, the cofactorless equation `S·B − H·A − R == identity` is + /// used, which rejects mixed-order points that the cofactored equation + /// tolerates. The cofactored form costs one extra multiplication by the + /// cofactor 8, which the verifier performs as three `sol_curve_group_op` + /// additions. pub cofactored: bool, /// Reject public keys whose compressed `y`-coordinate is `>= p` (a /// non-canonical encoding of a reduced point). @@ -87,13 +90,12 @@ impl VerificationCriteria { /// The criteria enforced by `ed25519_dalek::VerifyingKey::verify_strict`. /// - /// Cofactorless verification with canonical `R` and small-order rejection - /// for both `A` and `R`. Mirrors ed25519-dalek 2.x exactly, including the - /// detail that a non-canonically encoded public key `A` is *not* rejected - /// — dalek's `VerifyingKey::from_bytes` decompresses `A` (reducing `y` - /// modulo `p`) without a canonicity check, and `verify_strict` only - /// re-encodes and compares `R`. Every signature this preset accepts is - /// accepted by dalek's `verify_strict`, and vice versa. + /// Cofactorless, with canonical `R` and small-order rejection for both `A` + /// and `R`. Note that a non-canonically encoded `A` is *not* rejected: + /// dalek's `VerifyingKey::from_bytes` decompresses `A` (reducing `y` modulo + /// `p`) without a canonicity check, and `verify_strict` only re-encodes and + /// compares `R`. The match is exact in both directions, cross-checked in + /// the test suite. pub const fn dalek_verify_strict() -> Self { Self { cofactored: false, diff --git a/ed25519-verify/src/error.rs b/ed25519-verify/src/error.rs index 7db1d8c..e86999e 100644 --- a/ed25519-verify/src/error.rs +++ b/ed25519-verify/src/error.rs @@ -31,12 +31,10 @@ pub enum Ed25519VerifyError { /// The public key does not decode to a valid Edwards curve point, or the /// signature's `S` scalar is not canonical (`S >= L`). /// - /// These two causes are not distinguished: telling them apart would - /// require either a canonical-`S` check duplicating work the curve - /// back-end already performs internally, or a public-key decode check - /// ahead of the syscall that computes the verification equation. Both - /// would cost compute units on every signature to add precision that - /// only benefits the malformed-input case. + /// These two causes are not distinguished: the syscall that consumes both + /// reports only overall success or failure. Telling them apart would mean an + /// explicit `S < L` comparison, or decoding `A` ahead of the syscall — compute + /// units spent on every signature for precision that only helps malformed input. InvalidEncoding, /// The public key, signature, and message all decoded successfully, but /// the signature does not satisfy the verification equation. diff --git a/ed25519-verify/src/verifier.rs b/ed25519-verify/src/verifier.rs index 4aa03ec..4f30465 100644 --- a/ed25519-verify/src/verifier.rs +++ b/ed25519-verify/src/verifier.rs @@ -16,9 +16,10 @@ use { /// Stateless, zero-allocation Ed25519 verifier. /// -/// The verification behavior is selected by [`VerificationCriteria`]. A verifier -/// created with [`Ed25519Verifier::new`] uses the [`VerificationCriteria::zip215`] -/// preset, matching this crate's historical behavior. +/// Behavior is selected by [`VerificationCriteria`]; [`Ed25519Verifier::new`] +/// uses the [ZIP-215] preset. +/// +/// [ZIP-215]: VerificationCriteria::zip215 #[derive(Debug, Clone, Copy)] pub struct Ed25519Verifier { criteria: VerificationCriteria, @@ -50,14 +51,11 @@ impl Ed25519Verifier { self.criteria } - /// Verifies one Ed25519 signature according to the configured criteria. + /// Verifies one Ed25519 signature against the configured criteria. /// - /// The core relation is `S*B - H(R || A || M)*A == R`. Depending on - /// [`VerificationCriteria::cofactored`], the check is performed either - /// cofactored — `[8](S*B - H*A - R) == identity`, matching the - /// ed25519-zebra batch verification shape — or cofactorless — - /// `S*B - H*A - R == identity`. The canonical-encoding and - /// small-order rejections are applied first per the configured knobs. + /// Checks `S*B - H(R || A || M)*A - R == identity`, multiplied by the + /// cofactor 8 when [`VerificationCriteria::cofactored`] is set. + /// Canonical-encoding and small-order rejections run first. pub fn verify_signature( &self, signature: &[u8; SIGNATURE_SERIALIZED_SIZE], @@ -68,13 +66,8 @@ impl Ed25519Verifier { let r_bytes: &[u8; 32] = r_bytes.try_into().unwrap(); let s_bytes: &[u8; 32] = s_bytes.try_into().unwrap(); - // `require_canonical_s` is deliberately not checked because - // `multiscalar_multiply_edwards` converts `PodScalar` through - // `Scalar::from_canonical_bytes` and returns `None` on a non-canonical - // scalar, which maps to `InvalidEncoding` below. Re-checking it - // in-program duplicates work the curve backend already performs, and - // would not let us distinguish it from a malformed public key anyway - // (see `Ed25519VerifyError::InvalidEncoding`). + // `S < L` is enforced by the multiscalar-mul syscall, not here; see + // `VerificationCriteria` for why there is no knob. if self.criteria.require_canonical_a && !scalar::is_canonical_point_encoding(public_key) { return Err(Ed25519VerifyError::NonCanonicalPublicKey); @@ -95,22 +88,19 @@ impl Ed25519Verifier { let challenge = compute_challenge(r_bytes, public_key, message); - // S*(-B) + H*A = -(S*B - H*A), so this yields the negation of the value - // the verification equation compares against R. + // `S*(-B) + H*A` is `-(S*B - H*A)`, the negation of the value the + // verification equation compares against `R`. let neg_lhs = multiscalar_multiply_edwards( &[PodScalar(*s_bytes), PodScalar(challenge)], &[ED25519_BASEPOINT_NEGATED_COMPRESSED, public_key_point], ) .ok_or(Ed25519VerifyError::InvalidEncoding)?; - // Flip the sign bit back to recover the encoding of `S*B - H*A`, then - // compare against `R` as supplied. `neg_lhs` is canonical, so the flip - // yields the canonical encoding of `lhs` (except when `lhs` has x = 0, - // where it yields negative zero — which can only miss, never falsely - // match). A match therefore implies `R == lhs` as points, so `lhs - R` - // is the identity and both the cofactorless and the cofactored equation - // hold. Deciding here skips the `subtract_edwards` syscall on the path - // every honestly generated signature follows. + // Flipping the sign bit recovers the left-hand side's encoding. + // `neg_lhs` is canonical, so the flip is too — except at `x = 0`, where + // it yields negative zero, which can only miss, never falsely match. A + // byte match implies `R == lhs`, satisfying both equations, and skips + // `subtract_edwards` on the happy path. let mut lhs_bytes = neg_lhs.0; lhs_bytes[31] ^= 0x80; if lhs_bytes == *r_bytes { @@ -118,30 +108,24 @@ impl Ed25519Verifier { } let lhs = PodEdwardsPoint(lhs_bytes); - // `lhs` is always a valid point by construction (see above), so a - // `None` here can only mean `r_point` — built directly from the - // caller-supplied `r_bytes` — failed to decode. + // `lhs` is valid by construction, so a `None` here means `r_point`, + // built from caller-supplied bytes, failed to decode. let difference = subtract_edwards(&lhs, &r_point).ok_or(Ed25519VerifyError::InvalidEncoding)?; - // An exact-identity difference satisfies both the cofactorless and the - // cofactored equation, so accept it without the cofactor multiplication. - // This is the common case for honestly generated (prime-order) signatures, - // so it saves the `multiply_by_8` syscalls on the hot path. + // Exact identity satisfies both equations, so accept before paying for + // the cofactor multiplication. if difference == EDWARDS_IDENTITY_COMPRESSED { return Ok(()); } - // Cofactorless verification requires an exact identity, which is now ruled - // out. Cofactored verification additionally accepts a difference that - // clears to identity once multiplied by the cofactor 8 (the mixed-order - // points that ZIP-215 tolerates). + // Cofactorless requires exact identity, now ruled out; cofactored also + // accepts a difference that clears to identity under multiplication by + // 8 — the mixed-order points ZIP-215 tolerates. if !self.criteria.cofactored { return Err(Ed25519VerifyError::SignatureMismatch); } - // `difference` is a point returned by `subtract_edwards` above, so it - // is always a valid encoding; `multiply_by_8` failing here is not - // expected to be reachable in practice. `InvalidEncoding` is used - // defensively in case it is. + // `difference` came from `subtract_edwards`, so `None` should be + // unreachable; `InvalidEncoding` is defensive. if multiply_by_8(&difference).ok_or(Ed25519VerifyError::InvalidEncoding)? != EDWARDS_IDENTITY_COMPRESSED { From 38323146dc52132eb937b3ebea58c97caff4df86 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Fri, 28 Aug 2026 12:13:30 +0900 Subject: [PATCH 2/2] reduce scalar reduction from byte-wise to limb-wise arithmetic --- README.md | 36 +++++---- ed25519-verify/src/constants.rs | 74 +++++++++++++++++-- ed25519-verify/src/scalar.rs | 125 ++++++++++++++++++++++++-------- 3 files changed, 182 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 64aa54f..e32fbe7 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ let instruction = verify(&ID, &public_key, &signature, message); ### Constraints - **Verification criteria.** The program always applies [ZIP-215]: the - cofactored equation `[8](S·B − H(R‖A‖M)·A) == [8]R`. + cofactored equation `[8](S·B − H(R‖A‖M)·A − R) == identity`. Small-order and non-canonical points are accepted. Programs needing a different variant (e.g. `verify_strict`) should depend on the `solana-ed25519-verify` library directly (see @@ -78,18 +78,22 @@ non-canonical encodings, and small-order rejection (see Henry de Valence's [It's 255:19AM]). The `solana-ed25519-verify` crate exposes these as independent knobs via `VerificationCriteria`: -| Knob | Effect when enabled | Extra syscalls | -| ---------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | -| `cofactored` | Use `[8](S·B − H·A − R) == identity` instead of the cofactorless `S·B − H·A − R == identity` | +3 `sol_curve_group_op` (multiply-by-8 as three doublings) | -| `require_canonical_a` | Reject public keys whose `y`-coordinate is `≥ p` | none | -| `require_canonical_r` | Reject signature `R` whose `y`-coordinate is `≥ p` | none | -| `reject_small_order_a` | Reject small-order (torsion) public keys | +3 `sol_curve_group_op` | -| `reject_small_order_r` | Reject small-order signature `R` values | +3 `sol_curve_group_op` | +| Knob | Effect when enabled | Extra syscalls | +| ---------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| `cofactored` | Use `[8](S·B − H·A − R) == identity` instead of the cofactorless `S·B − H·A − R == identity` | +3 `sol_curve_group_op` (1,419 CU), fallback only | +| `require_canonical_a` | Reject public keys whose `y`-coordinate is `≥ p` | none | +| `require_canonical_r` | Reject signature `R` whose `y`-coordinate is `≥ p` | none | +| `reject_small_order_a` | Reject small-order (torsion) public keys | +3 `sol_curve_group_op` (1,419 CU) | +| `reject_small_order_r` | Reject small-order signature `R` values | +3 `sol_curve_group_op` (1,419 CU) | -Canonical `S` (`S < L`) is enforced for every criteria set and so has no knob: -`sol_curve_multiscalar_mul` converts scalars through -`Scalar::from_canonical_bytes` and rejects anything out of range before any -group operation runs. +The cofactor multiplication is only reached when a signature fails the exact +comparison first, so it costs nothing on the path a valid signature follows. + +Canonical `S` (`S < L`) has no knob. Every profile worth targeting requires it — +accepting `S ≥ L` reintroduces signature malleability — and +`sol_curve_multiscalar_mul` enforces it regardless, converting scalars through +`Scalar::from_canonical_bytes` and rejecting out-of-range values before any group +operation runs. ```rust use solana_ed25519_verify::{Ed25519Verifier, VerificationCriteria}; @@ -145,9 +149,11 @@ Solana-specific type. | `InvalidEncoding` | `A` doesn't decode to a valid point, or `S` is non-canonical (`S ≥ L`) | | `SignatureMismatch` | Every input decoded successfully, but the equation doesn't hold | -`InvalidEncoding` intentionally does not distinguish a malformed public key -from a non-canonical `S` scalar: the curve syscall that consumes both reports -only overall success or failure, with no further detail attached. +`InvalidEncoding` does not distinguish a malformed public key from a +non-canonical `S` scalar: the syscall that consumes both reports only overall +success or failure. Telling them apart would mean an explicit `S < L` comparison, +or decoding `A` ahead of the syscall — compute units spent on every signature for +precision that only helps malformed input. The on-chain program collapses all of these to `ProgramError::InvalidInstructionData` — see [Constraints](#constraints). diff --git a/ed25519-verify/src/constants.rs b/ed25519-verify/src/constants.rs index 88195d2..764f9e9 100644 --- a/ed25519-verify/src/constants.rs +++ b/ed25519-verify/src/constants.rs @@ -16,6 +16,35 @@ pub(crate) const BASEPOINT_ORDER: [u8; 32] = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, ]; +/// Reinterprets a 32-byte little-endian integer as four little-endian 64-bit +/// limbs. +/// +/// Evaluated at compile time so that limb constants are derived from their byte +/// counterparts rather than transcribed by hand a second time. +const fn to_le_limbs(bytes: [u8; 32]) -> [u64; 4] { + let mut limbs = [0u64; 4]; + let mut limb_index = 0; + + while limb_index < 4 { + let mut limb = 0u64; + let mut byte_index = 0; + while byte_index < 8 { + limb |= (bytes[limb_index * 8 + byte_index] as u64) << (byte_index * 8); + byte_index += 1; + } + limbs[limb_index] = limb; + limb_index += 1; + } + + limbs +} + +/// [`BASEPOINT_ORDER`] as little-endian 64-bit limbs. +/// +/// Used by the limb-wise reduction in [`crate::scalar`], where a 64-bit borrow +/// chain replaces a byte-wise one. +pub(crate) const BASEPOINT_ORDER_LIMBS: [u64; 4] = to_le_limbs(BASEPOINT_ORDER); + /// Field modulus `p = 2^255 - 19` in little-endian form. pub(crate) const FIELD_MODULUS: [u8; 32] = [ 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, @@ -45,14 +74,16 @@ pub(crate) const EDWARDS_IDENTITY_COMPRESSED: PodEdwardsPoint = #[cfg(test)] mod tests { - use {super::*, curve25519_dalek::traits::Identity}; + use { + super::*, + curve25519_dalek::{scalar::Scalar, traits::Identity}, + }; - // These two constants are hand-transcribed byte literals with no - // compiler check that they encode what the doc comments claim. - // Cross-checking them against `curve25519-dalek`'s own constants (an - // independent implementation, not just a second copy of the same - // literal) catches a transcription error immediately instead of relying - // on review. + // These constants are hand-transcribed byte literals with no compiler check + // that they encode what the doc comments claim. Cross-checking them against + // `curve25519-dalek` (an independent implementation, not just a second copy + // of the same literal) catches a transcription error immediately instead of + // relying on review. #[test] fn negated_basepoint_constant_matches_curve25519_dalek() { @@ -73,4 +104,33 @@ mod tests { assert_eq!(EDWARDS_IDENTITY_COMPRESSED_BYTES, expected); } + + #[test] + fn basepoint_order_matches_curve25519_dalek() { + // dalek does not export `L` directly, but `from_canonical_bytes` accepts + // exactly the values below it. `L - 1` being accepted and `L` itself + // being rejected pins the constant to dalek's group order. + let mut order_minus_one = BASEPOINT_ORDER; + order_minus_one[0] -= 1; + + assert!(Scalar::from_canonical_bytes(order_minus_one) + .into_option() + .is_some()); + assert!(Scalar::from_canonical_bytes(BASEPOINT_ORDER) + .into_option() + .is_none()); + } + + #[test] + fn basepoint_order_limbs_round_trip_to_bytes() { + // Rebuilds the byte constant from the limbs using `to_le_bytes` rather + // than the shift loop in `to_le_limbs`, so the two directions are not + // the same code checked against itself. + let mut bytes = [0u8; 32]; + for (chunk, limb) in bytes.chunks_exact_mut(8).zip(BASEPOINT_ORDER_LIMBS) { + chunk.copy_from_slice(&limb.to_le_bytes()); + } + + assert_eq!(bytes, BASEPOINT_ORDER); + } } diff --git a/ed25519-verify/src/scalar.rs b/ed25519-verify/src/scalar.rs index ce32667..3e835e0 100644 --- a/ed25519-verify/src/scalar.rs +++ b/ed25519-verify/src/scalar.rs @@ -1,7 +1,7 @@ //! Small scalar- and field-element helpers needed to assemble Ed25519 //! verification around syscalls. -use crate::constants::{BASEPOINT_ORDER, FIELD_MODULUS}; +use crate::constants::{BASEPOINT_ORDER_LIMBS, FIELD_MODULUS}; /// Returns `true` if `encoding` is a canonical compressed Edwards point. /// @@ -17,49 +17,66 @@ pub(crate) fn is_canonical_point_encoding(encoding: &[u8; 32]) -> bool { } /// Reduces a 64-byte little-endian integer modulo the ed25519 base point order. +/// +/// Bit-serial long division: shift the remainder left, shift in the next bit of +/// the dividend, and subtract `L` whenever the result reaches it. The remainder +/// is carried as 64-bit limbs rather than bytes, so each step touches four words +/// instead of thirty-two. pub(crate) fn reduce_wide(wide: &[u8; 64]) -> [u8; 32] { - let mut remainder = [0u8; 32]; + let mut remainder = [0u64; 4]; for bit_index in (0..512).rev() { shl1(&mut remainder); - if (wide[bit_index / 8] >> (bit_index % 8)) & 1 == 1 { - remainder[0] |= 1; - } - if !cmp_le(&remainder, &BASEPOINT_ORDER).is_lt() { - sub_assign(&mut remainder, &BASEPOINT_ORDER); - } + remainder[0] |= u64::from((wide[bit_index / 8] >> (bit_index % 8)) & 1); + conditional_sub_order(&mut remainder); } - remainder + let mut reduced = [0u8; 32]; + for (chunk, limb) in reduced.chunks_exact_mut(8).zip(remainder) { + chunk.copy_from_slice(&limb.to_le_bytes()); + } + reduced } -fn shl1(value: &mut [u8; 32]) { - let mut carry = 0u8; - for byte in value { - let next_carry = *byte >> 7; - *byte = (*byte << 1) | carry; +/// Doubles `value` in place, discarding the bit shifted out of the top. +/// +/// The caller keeps `value < L < 2^253` between iterations, so the discarded bit +/// is always zero. +fn shl1(value: &mut [u64; 4]) { + let mut carry = 0u64; + for limb in value { + let next_carry = *limb >> 63; + *limb = (*limb << 1) | carry; carry = next_carry; } } -fn sub_assign(left: &mut [u8; 32], right: &[u8; 32]) { - let mut borrow = 0u16; - for (left_byte, right_byte) in left.iter_mut().zip(right) { - let minuend = u16::from(*left_byte); - let subtrahend = u16::from(*right_byte) + borrow; - if minuend >= subtrahend { - *left_byte = (minuend - subtrahend) as u8; - borrow = 0; - } else { - *left_byte = (minuend + 256 - subtrahend) as u8; - borrow = 1; - } +/// Subtracts `L` from `value` when `value >= L`, without branching on the input. +fn conditional_sub_order(value: &mut [u64; 4]) { + let mut difference = [0u64; 4]; + let mut borrow = 0u64; + + for index in 0..4 { + let (partial, borrow_from_order) = + value[index].overflowing_sub(BASEPOINT_ORDER_LIMBS[index]); + let (limb, borrow_from_carry) = partial.overflowing_sub(borrow); + difference[index] = limb; + borrow = u64::from(borrow_from_order | borrow_from_carry); + } + + // A borrow out of the top limb means `value < L`, so the difference is + // discarded. `mask` is all-ones exactly when the subtraction should apply. + let mask = borrow.wrapping_sub(1); + for index in 0..4 { + value[index] = (value[index] & !mask) | (difference[index] & mask); } } pub(crate) fn cmp_le(left: &[u8; 32], right: &[u8; 32]) -> core::cmp::Ordering { - for (left_byte, right_byte) in left.iter().zip(right).rev() { - match left_byte.cmp(right_byte) { + for index in (0..4).rev() { + let left_limb = u64::from_le_bytes(left[index * 8..index * 8 + 8].try_into().unwrap()); + let right_limb = u64::from_le_bytes(right[index * 8..index * 8 + 8].try_into().unwrap()); + match left_limb.cmp(&right_limb) { core::cmp::Ordering::Equal => {} ordering => return ordering, } @@ -69,13 +86,59 @@ pub(crate) fn cmp_le(left: &[u8; 32], right: &[u8; 32]) -> core::cmp::Ordering { #[cfg(test)] mod tests { - use super::*; + use {super::*, crate::constants::BASEPOINT_ORDER}; + + fn wide_from_low_32(low: &[u8; 32]) -> [u8; 64] { + let mut wide = [0u8; 64]; + wide[..32].copy_from_slice(low); + wide + } #[test] fn reduces_group_order_to_zero() { + assert_eq!(reduce_wide(&wide_from_low_32(&BASEPOINT_ORDER)), [0; 32]); + } + + #[test] + fn reduces_order_boundaries() { + // L - 1 is already reduced and must pass through unchanged. + let mut order_minus_one = BASEPOINT_ORDER; + order_minus_one[0] -= 1; + assert_eq!( + reduce_wide(&wide_from_low_32(&order_minus_one)), + order_minus_one + ); + + // L + 1 must come back as 1. + let mut order_plus_one = BASEPOINT_ORDER; + order_plus_one[0] += 1; + let mut one = [0u8; 32]; + one[0] = 1; + assert_eq!(reduce_wide(&wide_from_low_32(&order_plus_one)), one); + } + + // `reduce_wide` is hand-rolled modular arithmetic with a branch-free carry + // chain, so it is cross-checked against curve25519-dalek's own wide + // reduction rather than against a second copy of the same reasoning. + #[test] + fn matches_curve25519_dalek_wide_reduction() { let mut wide = [0u8; 64]; - wide[..32].copy_from_slice(&BASEPOINT_ORDER); - assert_eq!(reduce_wide(&wide), [0; 32]); + + for round in 0..32u32 { + for (index, byte) in wide.iter_mut().enumerate() { + *byte = (index as u32).wrapping_mul(31).wrapping_add(round * 7) as u8; + } + + let expected = + curve25519_dalek::scalar::Scalar::from_bytes_mod_order_wide(&wide).to_bytes(); + assert_eq!(reduce_wide(&wide), expected, "round {round}"); + } + + // Saturated input exercises the widest possible quotient. + let saturated = [0xff; 64]; + let expected = + curve25519_dalek::scalar::Scalar::from_bytes_mod_order_wide(&saturated).to_bytes(); + assert_eq!(reduce_wide(&saturated), expected); } #[test]