Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 17 additions & 21 deletions curve25519/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@ 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)

---

## Crates

| 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 |

---

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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";
Expand All @@ -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");
```

---
Expand All @@ -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
```

---
Expand Down
22 changes: 13 additions & 9 deletions curve25519/solana-ed25519/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand All @@ -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.

Expand Down Expand Up @@ -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 ℓ)
```

---
Expand All @@ -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. |
Expand Down
4 changes: 4 additions & 0 deletions curve25519/solana-ed25519/README_zebra.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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"))]
Expand Down
6 changes: 3 additions & 3 deletions curve25519/solana-ed25519/src/ed_sigs/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion curve25519/solana-ed25519/src/ed_sigs/tests/decoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
2 changes: 1 addition & 1 deletion curve25519/solana-ed25519/src/ed_sigs/tests/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
4 changes: 2 additions & 2 deletions curve25519/solana-ed25519/src/ed_sigs/tests/heea.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand Down
55 changes: 29 additions & 26 deletions curve25519/solana-ed25519/src/ed_sigs/verification_key.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
///
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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)]
Expand Down
Loading
Loading