diff --git a/curve25519/README.md b/curve25519/README.md index 0b757f9..7a91e7c 100644 --- a/curve25519/README.md +++ b/curve25519/README.md @@ -5,8 +5,8 @@ accelerated Ed25519 signature verification via the **HEEA** (Half-Extended Eucli method and a reduced set of well-tested backends. > Original library READMEs: [README_dalek.md](README_dalek.md) (workspace) · -> [curve25519/README_dalek.md](curve25519/README_dalek.md) · -> [ed25519-heea/README_zebra.md](ed25519-heea/README_zebra.md) +> [solana-ed25519/README_dalek.md](solana-ed25519/README_dalek.md) · +> [solana-ed25519/README_zebra.md](solana-ed25519/README_zebra.md) --- @@ -14,10 +14,9 @@ method and a reduced set of well-tested backends. | Crate | Description | |---|---| -| [`curve25519`](./curve25519) | Fork of `curve25519-dalek`. Core elliptic-curve arithmetic over Curve25519, Edwards, Ristretto, and Short-Weierstrass forms, with HEEA scalar decomposition and a narrowed backend set (removed `u32` and constraint device supports). | -| [`ed25519-heea`](./ed25519-heea) | Fork of `ed25519-zebra`. ZIP-215-compliant Ed25519 with an added `verify_heea` fast-path that uses HEEA half-size scalars. | +| [`solana-ed25519`](./solana-ed25519) | Fork of `curve25519-dalek` with ZIP-215-compliant Ed25519 from `ed25519-zebra`, HEEA-accelerated `verify` / `verify_zebra`, and a narrowed backend set (removed `u32` and constraint device supports). | | [`curve25519-cuda`](./curve25519-cuda) | GPU-accelerated multi-scalar multiplication (MSM) via CUDA/SPPARK. Falls back to CPU when CUDA is unavailable. | -| [`curve25519-derive`](./curve25519-derive) | Helper proc-macro crate (`#[unsafe_target_feature]`) inherited from upstream; required to write clean SIMD code. Identical to the one in dalek 0.5.0 | +| [`curve25519-derive`](../curve25519-derive) | Helper proc-macro crate (`#[unsafe_target_feature]`) inherited from upstream; required to write clean SIMD code. Identical to the one in dalek 0.5.0 | --- @@ -30,7 +29,8 @@ HEEA (from the TCHES 2025 paper _"Accelerating EdDSA Signature Verification with Size Halving"_) transforms this into a 4-point MSM with ~128-bit scalars: ``` -τs_lo · B + τs_hi · (2¹²⁸·B) = τ·R + ρ·A +flip_h = false: τs_lo · B + τs_hi · (2¹²⁸·B) = τ·R + ρ·A +flip_h = true: τs_lo · B + τs_hi · (2¹²⁸·B) = τ·R - ρ·A ``` where `ρ` and `τ` are half-size (~127-bit) values derived from `h` via a half-extended @@ -39,10 +39,10 @@ two of the bases (`B` and `2¹²⁸B`) use precomputed tables. In practice this **~15% faster** verification compared to the standard double-scalar-multiplication path. The algorithm is implemented in: -- [`curve25519/src/scalar/heea.rs`](curve25519/src/scalar/heea.rs) – `curve25519_heea_vartime` -- [`curve25519/src/traits.rs`](curve25519/src/traits.rs) – `HEEADecomposition` trait -- [`curve25519/src/backend/serial/scalar_mul/vartime_triple_base.rs`](curve25519/src/backend/serial/scalar_mul/vartime_triple_base.rs) – optimised 128+128+256 MSM -- [`ed25519-heea/src/verification_key.rs`](ed25519-heea/src/verification_key.rs) – `VerificationKey::verify_heea` +- [`solana-ed25519/src/scalar/heea.rs`](solana-ed25519/src/scalar/heea.rs) – `curve25519_heea_vartime` +- [`solana-ed25519/src/traits.rs`](solana-ed25519/src/traits.rs) – `HEEADecomposition` trait +- [`solana-ed25519/src/backend/serial/scalar_mul/vartime_triple_base.rs`](solana-ed25519/src/backend/serial/scalar_mul/vartime_triple_base.rs) – optimised 128+128+256 MSM +- [`solana-ed25519/src/ed_sigs/verification_key.rs`](solana-ed25519/src/ed_sigs/verification_key.rs) – `VerificationKey::verify` / `VerificationKey::verify_zebra` ### Reduced Backend Set @@ -65,17 +65,13 @@ maintenance surface. If you need them, use upstream `curve25519-dalek` directly. Add the relevant crate to `Cargo.toml`: ```toml -# Core curve arithmetic with HEEA -curve25519-sol = { git = "https://github.com/zz-sol/ed25519-sol" } - -# Ed25519 signatures with fast HEEA verification -ed25519-heea = { git = "https://github.com/zz-sol/ed25519-sol" } +curve25519 = { package = "solana-ed25519", git = "https://github.com/anza-xyz/cryptography" } ``` ### Standard Ed25519 verification ```rust -use ed25519_heea::{SigningKey, VerificationKey}; +use curve25519::ed_sigs::{SigningKey, VerificationKey}; use rand::thread_rng; let msg = b"hello world"; @@ -87,11 +83,11 @@ let vk = VerificationKey::from(&sk); vk.verify(&sig, msg).expect("valid signature"); ``` -### HEEA-accelerated verification +### Explicit HEEA-accelerated verification ```rust -// Fast path: ~15% faster via half-size scalars (same result) -vk.verify_heea(&sig, msg).expect("valid signature"); +// Same ZIP-215 result as verify(), using the HEEA path explicitly. +vk.verify_zebra(&sig, msg).expect("valid signature"); ``` --- @@ -106,8 +102,8 @@ cargo build --release RUSTFLAGS='-C target-feature=+avx2' cargo build --release # Run benchmarks -cargo bench --features "rand_core" -p curve25519 -cargo bench -p ed25519-heea +cargo bench --features "rand_core" -p solana-ed25519 +cargo bench -p curve25519-cuda ``` --- diff --git a/curve25519/solana-ed25519/README.md b/curve25519/solana-ed25519/README.md index 494607e..4acb6ec 100644 --- a/curve25519/solana-ed25519/README.md +++ b/curve25519/solana-ed25519/README.md @@ -24,7 +24,8 @@ A new `HEEADecomposition` trait and implementation have been added in: Given a 256-bit hash scalar `h`, `heea_decompose` returns `(ρ, τ, flip_h)` such that: ```text -ρ ≡ ±τ·h (mod ℓ) // ρ and τ are both ≤ 128 bits +flip_h = false: ρ ≡ τ·h (mod ℓ) +flip_h = true: ρ ≡ -τ·h (mod ℓ) ``` This allows verification of `sB = R + hA` to be rewritten as a 4-point MSM over ~128-bit @@ -58,9 +59,9 @@ verification. ### `verify_zebra`: fast-path signature verification -A new method `VerificationKey::verify_zebra` sits alongside the existing `verify`. -Both accept the same arguments and produce identical results — `verify_zebra` is a -**drop-in accelerated replacement** for `verify`. +`VerificationKey::verify_zebra` is the HEEA implementation used by the default +`VerificationKey::verify` method. Both accept the same arguments and produce identical +ZIP-215 results. The HEEA method (TCHES 2025) transforms the standard 2-point MSM: @@ -71,10 +72,12 @@ The HEEA method (TCHES 2025) transforms the standard 2-point MSM: into a 4-point MSM over half-size (~128-bit) scalars: ```text -τs_lo·B + τs_hi·(2¹²⁸·B) = τ·R + ρ·A (HEEA) +flip_h = false: τs_lo·B + τs_hi·(2¹²⁸·B) = τ·R + ρ·A +flip_h = true: τs_lo·B + τs_hi·(2¹²⁸·B) = τ·R - ρ·A ``` -where `ρ ≡ ±τ·h (mod ℓ)` and `τs = τs_hi·2¹²⁸ + τs_lo`. All four scalars are ≤128 bits +where `ρ ≡ τ·h (mod ℓ)` when `flip_h` is false, `ρ ≡ -τ·h (mod ℓ)` when +`flip_h` is true, and `τs = τs_hi·2¹²⁸ + τs_lo`. All four scalars are ≤128 bits and the two basepoints (`B` and `2¹²⁸B`) use precomputed lookup tables, giving approximately **~15% faster** verification compared to the standard path. @@ -137,7 +140,8 @@ let h = Scalar::from_hash(Sha512::new().chain_update(b"some message")); // Decompose into two ~128-bit scalars let (rho, tau, flip_h) = h.heea_decompose(); -// rho ≡ ±tau·h (mod ℓ) +// flip_h == false: rho ≡ tau·h (mod ℓ) +// flip_h == true: rho ≡ -tau·h (mod ℓ) ``` --- @@ -146,10 +150,10 @@ let (rho, tau, flip_h) = h.heea_decompose(); | Feature | Default? | Description | |---|:---:|---| -| `alloc` | ✓ | Multiscalar multiplication, batch inversion, batch compress, batch Ed25519 verification. | +| `alloc` | ✓ | Multiscalar multiplication, batch inversion, batch compress, and the Ed25519 batch module. | | `zeroize` | ✓ | `Zeroize` for all scalar and point types. | | `precomputed-tables` | ✓ | Precomputed basepoint tables (~400 KB, ~4× faster basepoint mul). | -| `rand_core` | ✓ | `Scalar::random`, `RistrettoPoint::random`, `SigningKey::new`. | +| `rand_core` | ✓ | `Scalar::random`, `RistrettoPoint::random`, `SigningKey::new`, and randomized batch verification. | | `digest` | ✓ | Hash-to-curve, `Scalar::from_hash`, and Ed25519 hashing. | | `std` | | Enables `std::error::Error` impl on `ed_sigs::Error`. | | `serde` | | Serialization for all point, scalar, and key types. | diff --git a/curve25519/solana-ed25519/README_zebra.md b/curve25519/solana-ed25519/README_zebra.md index f984ec8..5ac8569 100644 --- a/curve25519/solana-ed25519/README_zebra.md +++ b/curve25519/solana-ed25519/README_zebra.md @@ -1,3 +1,7 @@ +> This is an upstream `ed25519-zebra` README snapshot retained for provenance. +> It describes the upstream crate, not this fork's current `curve25519::ed_sigs` +> API. See [README.md](README.md) for current `solana-ed25519` usage. + [![Build status](https://github.com/ZcashFoundation/ed25519-zebra/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/ZcashFoundation/ed25519-zebra/actions/workflows/main.yml?query=branch%3Amain) [![dependency status](https://deps.rs/repo/github/ZcashFoundation/ed25519-zebra/status.svg)](https://deps.rs/repo/github/ZcashFoundation/ed25519-zebra) diff --git a/curve25519/solana-ed25519/src/backend/serial/scalar_mul/vartime_triple_base.rs b/curve25519/solana-ed25519/src/backend/serial/scalar_mul/vartime_triple_base.rs index cecc6b9..3fae118 100644 --- a/curve25519/solana-ed25519/src/backend/serial/scalar_mul/vartime_triple_base.rs +++ b/curve25519/solana-ed25519/src/backend/serial/scalar_mul/vartime_triple_base.rs @@ -40,8 +40,13 @@ use crate::window::NafLookupTable5; /// # Implementation /// /// - For \\(A_1\\) and \\(A_2\\): NAF with window width 5 (8 precomputed points each) -/// - For \\(B\\): NAF with window width 8 when precomputed tables available (64 points) -/// - For \\(B'\\): NAF with window width 5 (could be optimized with precomputed table) +/// - For \\(B\\): NAF with window width 5. When precomputed tables are enabled, +/// these width-5 digits are selected from the larger width-8 basepoint table. +/// - For \\(B'\\): NAF with window width 5. +/// +/// The vector backend uses width 8 for \\(b_{lo}\\) when precomputed tables are +/// enabled. This serial backend keeps width 5 for \\(b_{lo}\\) as a +/// backend-specific performance tradeoff. /// /// The algorithm shares doublings across all four scalar multiplications, processing /// only 128 bits instead of 256, providing approximately 2x speedup over the naive approach. @@ -70,7 +75,11 @@ pub fn mul_128_128_256( let b_lo = Scalar::from_canonical_bytes(b_lo_bytes).unwrap(); let b_hi = Scalar::from_canonical_bytes(b_hi_bytes).unwrap(); - // Compute NAF representations (all scalars are now ~128 bits) + // Compute NAF representations (all scalars are now ~128 bits). + // + // 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(5); let a2_naf = a2.non_adjacent_form(5); let b_lo_naf = b_lo.non_adjacent_form(5); 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 921b533..0066c68 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 @@ -54,6 +54,10 @@ pub mod spec { /// - For \\(B\\): NAF with window width 8 when precomputed tables available (64 points), otherwise width 5 /// - For \\(B'\\): NAF with window width 5 /// + /// The serial backend keeps \\(b_{lo}\\) at width 5 even when precomputed + /// tables are enabled. This vector backend uses width 8 in that + /// configuration as a backend-specific performance tradeoff. + /// /// The algorithm shares doublings across all four scalar multiplications, processing /// only 128 bits instead of 256, providing approximately 2x speedup over the naive approach. /// @@ -88,6 +92,8 @@ pub mod spec { let a1_naf = a1.non_adjacent_form(5); let a2_naf = a2.non_adjacent_form(5); + // With precomputed tables, the vector backend uses the larger width-8 + // basepoint table for b_lo. The serial backend keeps b_lo at width 5. #[cfg(feature = "precomputed-tables")] let b_lo_naf = b_lo.non_adjacent_form(8); #[cfg(not(feature = "precomputed-tables"))] diff --git a/curve25519/solana-ed25519/src/ed_sigs/batch.rs b/curve25519/solana-ed25519/src/ed_sigs/batch.rs index 1cf5424..a43fcc9 100644 --- a/curve25519/solana-ed25519/src/ed_sigs/batch.rs +++ b/curve25519/solana-ed25519/src/ed_sigs/batch.rs @@ -104,9 +104,9 @@ impl Item { /// /// This is useful (in combination with `Item::clone`) for implementing fallback /// logic when batch verification fails. In contrast to - /// [`VerificationKey::verify`](crate::VerificationKey::verify), which requires - /// borrowing the message data, the `Item` type is unlinked from the lifetime of - /// the message. + /// [`VerificationKey::verify`](crate::ed_sigs::VerificationKey::verify), + /// which requires borrowing the message data, the `Item` type is unlinked + /// from the lifetime of the message. pub fn verify_single(self) -> Result<(), Error> { VerificationKey::try_from(self.vk_bytes) .and_then(|vk| vk.verify_zebra_prehashed(&self.sig, self.k)) diff --git a/curve25519/solana-ed25519/src/ed_sigs/tests/decoding.rs b/curve25519/solana-ed25519/src/ed_sigs/tests/decoding.rs index 4816790..cdeb41b 100644 --- a/curve25519/solana-ed25519/src/ed_sigs/tests/decoding.rs +++ b/curve25519/solana-ed25519/src/ed_sigs/tests/decoding.rs @@ -16,7 +16,7 @@ const PKCS8_V1_PEM: &str = include_str!("../../../examples/pkcs8-v1.pem"); #[cfg(feature = "pkcs8")] const PKCS8_V2_DER: &[u8] = include_bytes!("../../../examples/pkcs8-v2.der"); -/// Ed25519 PKCS#8 v1 private key encoded as PEM. +/// Ed25519 PKCS#8 v2 private key + public key encoded as PEM. #[cfg(feature = "pem")] const PKCS8_V2_PEM: &str = include_str!("../../../examples/pkcs8-v2.pem"); diff --git a/curve25519/solana-ed25519/src/ed_sigs/tests/encoding.rs b/curve25519/solana-ed25519/src/ed_sigs/tests/encoding.rs index 730e9a3..0273ef8 100644 --- a/curve25519/solana-ed25519/src/ed_sigs/tests/encoding.rs +++ b/curve25519/solana-ed25519/src/ed_sigs/tests/encoding.rs @@ -19,7 +19,7 @@ const PKCS8_V1_PEM: &str = include_str!("../../../examples/pkcs8-v1.pem"); #[cfg(feature = "pkcs8")] const PKCS8_V2_DER: &[u8] = include_bytes!("../../../examples/pkcs8-v2.der"); -/// Ed25519 PKCS#8 v1 private key encoded as PEM. +/// Ed25519 PKCS#8 v2 private key + public key encoded as PEM. #[cfg(feature = "pem")] const PKCS8_V2_PEM: &str = include_str!("../../../examples/pkcs8-v2.pem"); diff --git a/curve25519/solana-ed25519/src/ed_sigs/tests/heea.rs b/curve25519/solana-ed25519/src/ed_sigs/tests/heea.rs index fe7bb40..5633dec 100644 --- a/curve25519/solana-ed25519/src/ed_sigs/tests/heea.rs +++ b/curve25519/solana-ed25519/src/ed_sigs/tests/heea.rs @@ -8,7 +8,7 @@ use core::convert::TryFrom; use ed25519::Signature; #[test] -fn test_verify_heea_invalid_signature() { +fn test_verify_zebra_invalid_signature() { let signing_key = SigningKey::from([1u8; 32]); let verification_key = VerificationKey::from(&signing_key); @@ -37,7 +37,7 @@ fn test_verify_heea_invalid_signature() { } #[test] -fn test_verify_heea_multiple_signatures() { +fn test_verify_zebra_multiple_signatures() { for i in 0..100 { let mut seed = [0u8; 32]; seed[0] = i; diff --git a/curve25519/solana-ed25519/src/ed_sigs/verification_key.rs b/curve25519/solana-ed25519/src/ed_sigs/verification_key.rs index a059f76..6e6a361 100644 --- a/curve25519/solana-ed25519/src/ed_sigs/verification_key.rs +++ b/curve25519/solana-ed25519/src/ed_sigs/verification_key.rs @@ -1,16 +1,16 @@ // -*- mode: rust; -*- // -// This file is part of ed25519-heea, a fork of ed25519-zebra. +// This file is part of solana-ed25519's ed_sigs module, forked from ed25519-zebra. // Original ed25519-zebra code: Copyright (c) Zcash Foundation contributors // Modifications for HEEA: Copyright (c) 2025 curve25519-sol contributors // See LICENSE-APACHE and LICENSE-MIT for licensing information. // // Modifications from ed25519-zebra: -// - Added `verify_heea`, an accelerated verification path using the HEEA +// - Added `verify_zebra`, an accelerated verification path using the HEEA // scalar decomposition from curve25519-sol's `HEEADecomposition` trait. // See "Accelerating EdDSA Signature Verification with Faster Scalar Size // Halving" (TCHES 2025) for the algorithm. -// - `verify` and all ZIP-215 consensus logic are unchanged from ed25519-zebra. +// - `verify` dispatches to `verify_zebra`, preserving ZIP-215 semantics. use crate::{ edwards::{CompressedEdwardsY, EdwardsPoint}, @@ -105,12 +105,12 @@ const LEGACY_EXCLUDED_R_ENCODINGS: [[u8; 32]; 11] = [ ], ]; -/// A refinement type for `[u8; 32]` indicating that the bytes represent an -/// encoding of an Ed25519 verification key. +/// A container for the 32-byte encoded form of an Ed25519 verification key. /// -/// This is useful for representing an encoded verification key, while the -/// [`VerificationKey`] type in this library caches other decoded state used in -/// signature verification. +/// This type only checks or carries the byte length. It does not prove that the +/// bytes decompress to a valid Ed25519 verification key. Convert it to +/// [`VerificationKey`] to validate the encoded point and cache decoded state +/// used in signature verification. /// /// A `VerificationKeyBytes` can be used to verify a single signature using the /// following idiom: @@ -204,7 +204,7 @@ fn verification_key_bytes_from_spki( /// /// This type holds decompressed state used in signature verification; if the /// verification key may not be used immediately, it is probably better to use -/// [`VerificationKeyBytes`], which is a refinement type for `[u8; 32]`. +/// [`VerificationKeyBytes`], which stores only the length-checked encoded bytes. /// /// ## Zcash-specific consensus properties /// @@ -366,17 +366,17 @@ impl VerificationKey { /// This implements the algorithm from "Accelerating EdDSA Signature Verification /// with Faster Scalar Size Halving" (TCHES 2025). /// - /// The standard verification equation sB = R + hA is transformed to: - /// τsB = τR + ρA where ρ ≡ τh (mod ℓ) + /// The decomposition returns ρ and τ such that either ρ ≡ τh (mod ℓ) or + /// ρ ≡ -τh (mod ℓ). The standard verification equation sB = R + hA is + /// multiplied by τ and the sign of A is selected according to `flip_h`. /// /// Both ρ and τ are approximately half the size of h. /// /// We then decompose τs into two 128-bit scalars: /// τs = τs_hi * 2^128 + τs_lo /// - /// The verification equation becomes: - /// τs_lo B + τs_hi (2^128 B) = τR + ρA - /// which can be done via 4-variable MSM with half-size scalars. + /// The resulting equation can be checked with a 4-variable MSM with + /// half-size scalars. #[allow(non_snake_case)] pub fn verify_zebra(&self, signature: &Signature, msg: &[u8]) -> Result<(), Error> { self.verify_zebra_prehashed(signature, self.challenge_scalar(signature, msg)) @@ -388,12 +388,9 @@ impl VerificationKey { signature: &Signature, h: Scalar, ) -> Result<(), Error> { - // Generate half-size scalars ρ and τ such that ρ ≡ τh (mod ℓ) - // in order to have rho and tau approximately half the size of h - // it is possible that we compute ρ ≡ -τh (mod ℓ) - // this is indicated by `flip_h` flag being true, - // in which case we will need to negate A later - // let (rho, tau, flip_h) = crate::heea::generate_half_size_scalars(&h); + // Generate half-size scalars ρ and τ. If flip_h is false, then + // ρ ≡ τh (mod ℓ). If flip_h is true, then ρ ≡ -τh (mod ℓ), so the + // sign of A is flipped below. let (rho, tau, flip_h) = h.heea_decompose(); // Extract s from the signature @@ -405,11 +402,11 @@ impl VerificationKey { .decompress() .ok_or(Error::InvalidSignature)?; - // Standard verification checks: sB = R + hA - // Transformed verification: -τsB + τR + ρA == 0 + // Standard verification checks: sB = R + hA. // // We verify: - // [8] τs B + [8] τ (-R) + [8] ρ (-A) == 0 + // [8] τs B + [8] τ (-R) + [8] ρ A_term == 0 + // where A_term is -A when ρ ≡ τh and A when ρ ≡ -τh. // Compute τs let ts = tau * s; @@ -424,12 +421,18 @@ impl VerificationKey { } } - /// Verify a signature with exact `ed25519-dalek`-style byte-level behavior. + /// Verify a signature with dalek-style canonical-`R` byte comparison. /// /// This recomputes the expected canonical `R` encoding and compares it to the - /// signature's `R` bytes, matching dalek's ordinary verification behavior. + /// signature's `R` bytes. /// - /// Note that exact dalek-compatible behavior is incompatible with the HEEA + /// This helper also preserves this crate's legacy compatibility filters: it + /// rejects an all-zero encoded public key and the known legacy-excluded `R` + /// encodings before running the canonical-`R` comparison. Because of those + /// extra checks, this is not a byte-for-byte clone of every `ed25519-dalek` + /// release. + /// + /// Note that dalek-style canonical-`R` comparison is incompatible with the HEEA /// transformed equation because the transformed check does not preserve the /// original `R` encoding needed for the byte comparison. #[allow(non_snake_case)] diff --git a/curve25519/solana-ed25519/src/scalar.rs b/curve25519/solana-ed25519/src/scalar.rs index 8be511e..feeef75 100644 --- a/curve25519/solana-ed25519/src/scalar.rs +++ b/curve25519/solana-ed25519/src/scalar.rs @@ -537,12 +537,15 @@ impl Zeroize for Scalar { } impl HEEADecomposition for Scalar { - /// Generate half-size scalars (rho, tau) for a given hash value h + /// Generate approximately half-size non-negative scalars `(rho, tau)` for a + /// given challenge scalar `h`. /// - /// This function takes the hash value h from the signature verification equation - /// and produces two half-size scalars rho and tau such that rho = tau*h (mod ell). + /// The signed HEEA output satisfies `rho_i = tau_i * h (mod ell)`. This + /// method returns the absolute values of `rho_i` and `tau_i`, plus `flip_h` + /// to record whether exactly one of the signed values was negative. /// - /// And a flag indicating if rho is negative in its signed representation. + /// If `flip_h` is false, then `rho = tau * h (mod ell)`. If `flip_h` is + /// true, then `rho = -tau * h (mod ell)`. fn heea_decompose(&self) -> (Scalar, Scalar, bool) { // Convert h to I256 let v = self.into(); diff --git a/curve25519/solana-ed25519/src/scalar/heea.rs b/curve25519/solana-ed25519/src/scalar/heea.rs index 1c7553b..c28f774 100644 --- a/curve25519/solana-ed25519/src/scalar/heea.rs +++ b/curve25519/solana-ed25519/src/scalar/heea.rs @@ -17,7 +17,10 @@ //! This module implements Algorithm 4 (hEEA_approx_q) from the paper, which generates //! half-size scalars for faster EdDSA verification. //! -//! For verification sB = R + hA, we find rho, tau such that rho = tau*h (mod ell) +//! For verification `sB = R + hA`, this module finds signed values `rho` and +//! `tau` such that `rho == tau * h (mod ell)`. The public +//! `HEEADecomposition` implementation converts them to non-negative scalars and +//! reports whether the sign of `h` must be flipped in the transformed equation. use core::ops::Neg; use crate::constants; @@ -34,9 +37,11 @@ pub(crate) struct I256 { pub(crate) const HEEA_MAX_INDEX: usize = 129; -/// Implement curve25519_hEEA_vartime algorithm -/// Returns (rho, tau) such that rho ≡ tau * v (mod L) -/// where L is the Ed25519 group order (2^252 + 27742317777372353535851937790883648493) +/// Implement the curve25519 hEEA variable-time algorithm. +/// +/// Returns signed `(rho, tau)` such that `rho == tau * v (mod L)`, where `L` is +/// the Ed25519 group order +/// `2^252 + 27742317777372353535851937790883648493`. pub(crate) fn curve25519_heea_vartime(v: I256) -> (I256, I256) { // Get L from the existing BASEPOINT_ORDER constant let mut r0: I256 = (&constants::BASEPOINT_ORDER).into(); @@ -427,7 +432,7 @@ mod tests { #[test] #[cfg(all(feature = "rand_core", feature = "digest"))] - fn test_generate_half_size_scalars() { + fn test_heea_decompose_half_size_scalars() { use rand::thread_rng; use sha2::{Digest, Sha512}; @@ -453,10 +458,13 @@ mod tests { // Now convert to Scalars and verify the equation let (rho, tau, flip) = h.heea_decompose(); - // Verify that rho = tau * h (mod ell) + // Verify the non-negative scalar relation reported by flip. let computed_rho = tau * h; let computed_rho = if flip { -computed_rho } else { computed_rho }; - assert_eq!(rho, computed_rho, "rho should equal tau * h"); + assert_eq!( + rho, computed_rho, + "rho should equal tau * h with flip applied" + ); // Check that they are non-zero assert_ne!(rho, Scalar::ZERO, "rho should be non-zero"); diff --git a/curve25519/solana-ed25519/src/traits.rs b/curve25519/solana-ed25519/src/traits.rs index dc403fe..747b68a 100644 --- a/curve25519/solana-ed25519/src/traits.rs +++ b/curve25519/solana-ed25519/src/traits.rs @@ -422,18 +422,19 @@ pub trait VartimePrecomputedMultiscalarMul: Sized { /// A trait for decomposing scalars for the HEEA method. /// -/// The HEEA method is a technique for accelerating scalar multiplication -/// on elliptic curves by decomposing a scalar into two smaller scalars. -/// The smaller scalars then can be multiplied with multi-scalar multiplication -/// which is in general faster than single point full multiplication. +/// The HEEA method is used by EdDSA verification to replace multiplication by +/// a full-width challenge scalar with multiplications by two shorter scalars. +/// For a challenge scalar `h`, the decomposition returns non-negative scalars +/// `rho` and `tau` that satisfy either `rho == tau * h (mod ell)` or +/// `rho == -tau * h (mod ell)`. /// /// This trait provides a method for performing this decomposition. pub trait HEEADecomposition { - /// Decomposes a scalar `k` into two half-size scalars `k1` and `k2` - /// such that `k = k1 + k2 * h (mod ell)`, where `h` is a fixed constant. + /// Decomposes a scalar `h` into two approximately half-size scalars. /// - /// Returns the tuple `(k1, k2, flip_h)`, where `flip_h` is a boolean - /// indicating whether the sign of `h` was flipped during the decomposition. + /// Returns `(rho, tau, flip_h)`. If `flip_h` is false, then + /// `rho == tau * h (mod ell)`. If `flip_h` is true, then + /// `rho == -tau * h (mod ell)`. fn heea_decompose(&self) -> (Scalar, Scalar, bool); }