Skip to content
Open
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
10 changes: 9 additions & 1 deletion crypto/ecsm/src/curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,19 @@ fn schedule(k: &BigUint) -> Vec<(u8, u8, u8)> {
/// multiplication. Needs no step list or slopes, so it skips all witness work.
/// `k` must be in `[1, N)` (guaranteed by `prepare`).
pub fn scalar_mul_affine_x(k: &BigUint, g: &AffinePoint) -> BigUint {
scalar_mul_affine(k, g).x
}

/// Executor fast path: the full affine point `k·g`, so the `ecsm_mul_affine` syscall can
/// hand `y` back to the guest. `g` is whatever point the caller prepared — the even-`y` lift
/// of `xG` on the x-only path, the caller's own input point on the affine one — and `k·g`'s
/// y matches the ECDAS-constrained `y_r` either way.
pub fn scalar_mul_affine(k: &BigUint, g: &AffinePoint) -> AffinePoint {
let scalar = Option::<Scalar>::from(Scalar::from_repr(be32(k).into()))
.expect("ECSM: scalar k must be < N");
let g_proj = ProjectivePoint::from(to_k256_affine(g));
let r = (g_proj * scalar).to_affine();
from_k256_affine(&r).x
from_k256_affine(&r)
}

/// Jacobian doubling (dbl-2009-l) for `y² = x³ + 7`: on `(X:Y:Z)` with
Expand Down
61 changes: 54 additions & 7 deletions crypto/ecsm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ mod tests;
use num_bigint::BigUint;

pub use curve::{AffinePoint, recover_y_canonical, replay_double_and_add};
pub use witness::{EcdasStep, EcsmWitness, compute_witness};
pub use witness::{EcdasStep, EcsmWitness, compute_witness, compute_witness_with_y};

/// secp256k1 curve coefficient `b`.
pub const B: u64 = 7;
Expand Down Expand Up @@ -65,11 +65,14 @@ pub enum EcsmError {
ScalarIsZero,
/// `k >= N`: outside the valid scalar range `[1, N)`.
ScalarOutOfRange,
/// `x^3 + b` is not a quadratic residue, so `xG` is not a valid x-coordinate.
/// The input point is not on the curve: on the x-only path `x³ + b` is not a quadratic
/// residue, so `xG` is not a valid x-coordinate; on the affine path the caller's own
/// `yG` fails `yG² ≡ xG³ + b`.
NotOnCurve,
/// `xG >= p`: not a canonical field element. Reducing it silently would
/// diverge from the prover, whose `xR < p` range check makes a non-canonical
/// input unprovable (with `k = 1` the input is echoed back as `xR`).
/// A coordinate is `>= p`, so it is not a canonical field element — `xG` on either path,
/// `yG` on the affine one. Reducing it silently would diverge from the prover, whose
/// `xR < p` / `yR < p` range checks make a non-canonical input unprovable (with `k = 1`
/// the x-only input is echoed back as `xR`).
CoordinateOutOfRange,
}

Expand All @@ -78,8 +81,8 @@ impl core::fmt::Display for EcsmError {
match self {
EcsmError::ScalarIsZero => write!(f, "ECSM scalar k must be non-zero"),
EcsmError::ScalarOutOfRange => write!(f, "ECSM scalar k must be < N"),
EcsmError::NotOnCurve => write!(f, "ECSM xG is not a valid curve x-coordinate"),
EcsmError::CoordinateOutOfRange => write!(f, "ECSM xG must be < p"),
EcsmError::NotOnCurve => write!(f, "ECSM input point is not on the curve"),
EcsmError::CoordinateOutOfRange => write!(f, "ECSM coordinates must be < p"),
}
}
}
Expand Down Expand Up @@ -119,6 +122,50 @@ pub(crate) fn prepare(
Ok((k, AffinePoint { x: xg, y: yg }))
}

/// Like [`prepare`] but takes an explicit `yG` (the caller's full input point) instead of
/// lifting `xG` to the canonical even root. Validates `0 < k < N`, `xG < p`, `yG < p`, and
/// that `(xG, yG)` is on the curve (`yG² ≡ xG³ + b mod p`). Used by the affine path so the
/// returned `yR` matches the caller's actual point (no parity convention / guest-side sign
/// flip). `yG`'s value is pinned in the prover by a memory read of the caller's input.
pub(crate) fn prepare_with_y(
k_le: &[u8; 32],
xg_le: &[u8; 32],
yg_le: &[u8; 32],
) -> Result<(BigUint, AffinePoint), EcsmError> {
let k = BigUint::from_bytes_le(k_le);
if k == BigUint::from(0u8) {
return Err(EcsmError::ScalarIsZero);
}
if k >= n() {
return Err(EcsmError::ScalarOutOfRange);
}
let p = p();
let xg = BigUint::from_bytes_le(xg_le);
let yg = BigUint::from_bytes_le(yg_le);
if xg >= p || yg >= p {
return Err(EcsmError::CoordinateOutOfRange);
}
// On-curve: yG² ≡ xG³ + b (mod p).
let lhs = (&yg * &yg) % &p;
let rhs = (&xg * &xg % &p * &xg + BigUint::from(B)) % &p;
if lhs != rhs {
return Err(EcsmError::NotOnCurve);
}
Ok((k, AffinePoint { x: xg, y: yg }))
}

/// Affine entry point with an explicit input `yG`: both coordinates of `k·(xG, yG)` as
/// little-endian 32-byte values. The executor writes `xR` then `yR` back (64-byte output).
pub fn scalar_mul_xy_with_y(
k_le: &[u8; 32],
xg_le: &[u8; 32],
yg_le: &[u8; 32],
) -> Result<([u8; 32], [u8; 32]), EcsmError> {
let (k, g) = prepare_with_y(k_le, xg_le, yg_le)?;
let r = curve::scalar_mul_affine(&k, &g);
Ok((to_le_32(&r.x), to_le_32(&r.y)))
}

/// Computes the x-coordinate of `k·G` over secp256k1, given `k` and `xG` as little-endian
/// 32-byte values. This is the executor's entry point — it writes the returned bytes back
/// to guest memory at `addr_xR`.
Expand Down
28 changes: 27 additions & 1 deletion crypto/ecsm/src/witness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use num_traits::{Signed, Zero};
use rayon::prelude::*;

use crate::curve::{StepPts, replay_double_and_add};
use crate::{B, EcsmError, P_BYTES, R_BYTES, n, p, prepare, to_le_32};
use crate::{B, EcsmError, P_BYTES, R_BYTES, n, p, prepare, prepare_with_y, to_le_32};

/// Full ECSM-chip witness for one scalar multiplication (one ECSM row).
#[derive(Debug, Clone)]
Expand All @@ -47,6 +47,11 @@ pub struct EcsmWitness {
pub k_sub_n: [u8; 32],
/// `(xR - p) mod 2^256`
pub x_r_sub_p: [u8; 32],
/// `(yR - p) mod 2^256`. Forces `yR < p`: without it the byte range checks only
/// bound `yR < 2^256`, and the quotient columns absorb a multiple of `p`, so a
/// witness could publish `yR + p` for any `yR < 2^256 - p` (~2^32) — points with
/// such a tiny `y` are constructible, since `3 | p-1` makes cubing 3-to-1.
pub y_r_sub_p: [u8; 32],
/// position of the most significant set bit of `k`
pub len_k: u8,
pub x_r: [u8; 32],
Expand Down Expand Up @@ -280,7 +285,26 @@ fn shifted_quotient(relation: &str, numerator: &BigInt, p_big: &BigInt, r_big: &
/// little-endian 32-byte values. This is the prover's entry point.
pub fn compute_witness(k_le: &[u8; 32], xg_le: &[u8; 32]) -> Result<EcsmWitness, EcsmError> {
let (k, g) = prepare(k_le, xg_le)?;
compute_witness_inner(k_le, k, g)
}

/// Like [`compute_witness`] but with an explicit input `yG` (the caller's full point),
/// validated on-curve by [`prepare_with_y`]. The affine path uses this so the witnessed
/// `yG`/`yR` match the caller's actual point rather than the canonical even lift.
pub fn compute_witness_with_y(
k_le: &[u8; 32],
xg_le: &[u8; 32],
yg_le: &[u8; 32],
) -> Result<EcsmWitness, EcsmError> {
let (k, g) = prepare_with_y(k_le, xg_le, yg_le)?;
compute_witness_inner(k_le, k, g)
}

fn compute_witness_inner(
k_le: &[u8; 32],
k: BigUint,
g: crate::curve::AffinePoint,
) -> Result<EcsmWitness, EcsmError> {
let p_big = BigInt::from(p());
let r_big = BigInt::from(BigUint::from_bytes_le(&R_BYTES)); // r = 3p

Expand Down Expand Up @@ -328,6 +352,7 @@ pub fn compute_witness(k_le: &[u8; 32], xg_le: &[u8; 32]) -> Result<EcsmWitness,
let x_r = to_le_32(&result.x);
let y_r = to_le_32(&result.y);
let x_r_sub_p = to_le_32(&((&two_256 + &result.x) - p()));
let y_r_sub_p = to_le_32(&((&two_256 + &result.y) - p()));

// Steps are independent witnesses (each builds its own λ/quotient/carry data
// from one StepPts), so they parallelize freely when rayon is available.
Expand All @@ -354,6 +379,7 @@ pub fn compute_witness(k_le: &[u8; 32], xg_le: &[u8; 32]) -> Result<EcsmWitness,
x_g_sub_p,
k_sub_n,
x_r_sub_p,
y_r_sub_p,
len_k,
x_r,
y_r,
Expand Down
140 changes: 56 additions & 84 deletions crypto/ethrex-crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,10 @@ fn ecsm_ecrecover(sig: &[u8; 64], recid: u8, msg: &[u8; 32]) -> Result<[u8; 64],

/// ECSM-accelerated 2-term linear combination `k1·P1 + k2·P2`.
///
/// On riscv64 this reconstructs the full affine result from four x-only ECSM
/// queries (see [`lincomb2_with_oracle`]); on other targets, and whenever a
/// guard trips (degenerate input or oracle inconsistency), it returns `None`
/// so the caller uses the pure-Rust `ProjectivePoint::lincomb`.
/// On riscv64 this uses two affine ECSM queries (the precompile returns `(x, y)`,
/// see [`lincomb2_with_oracle`]) instead of four x-only queries plus chord-law
/// y-reconstruction; on other targets, and whenever a guard trips, it returns
/// `None` so the caller uses the pure-Rust `ProjectivePoint::lincomb`.
#[cfg(target_arch = "riscv64")]
fn ecsm_lincomb2(
a1: &AffinePoint,
Expand All @@ -309,27 +309,41 @@ fn ecsm_lincomb2(
None
}

/// x-only scalar-mul oracle backed by the ECSM precompile: computes `x(k·P)`
/// for the curve point P whose x-coordinate is passed in. `x` must be the
/// x-coordinate of a curve point and `k` in `(0, N)` (N = curve order) —
/// guaranteed by the guards in [`lincomb2_with_oracle`]. Values cross the ABI
/// as 32-byte little-endian; `x_le` and `k_le` are distinct stack arrays so
/// the executor's `|addr_x_le − addr_k_le| ≥ 32` assumption holds by
/// construction.
/// AFFINE oracle backed by the ECSM precompile: computes the full point `k·(x, y)` for the
/// caller's actual input point `(x, y)`. Returns `(xR, yR)` as normalized field elements —
/// no parity convention or sign flip, because the precompile receives the real `y` and the
/// prover pins it by a memory read. `(x, y)` must be a curve point and `k` in `(0, N)`.
/// Values cross the ABI as 32-byte little-endian; `input` is a 64-byte `[xG‖yG]` buffer,
/// `out` a 64-byte `[xR‖yR]` buffer and `k_le` a distinct 32-byte array, so the two
/// operand ranges are disjoint as the executor requires (it tests the ranges, not their
/// distance, so either stack layout is fine).
#[cfg(target_arch = "riscv64")]
fn ecsm_oracle(x: &FieldElement, k: &Scalar) -> Option<FieldElement> {
fn ecsm_oracle(
x: &FieldElement,
y: &FieldElement,
k: &Scalar,
) -> Option<(FieldElement, FieldElement)> {
let x_be = x.to_bytes();
let y_be = y.to_bytes();
let k_be = k.to_bytes();
let mut x_le = [0u8; 32];
let mut input = [0u8; 64];
let mut k_le = [0u8; 32];
for i in 0..32 {
x_le[i] = x_be[31 - i];
input[i] = x_be[31 - i];
input[32 + i] = y_be[31 - i];
k_le[i] = k_be[31 - i];
}
let mut xr_le = [0u8; 32];
lambda_vm_syscalls::syscalls::ecsm_mul(&mut xr_le, &x_le, &k_le);
xr_le.reverse();
Option::from(FieldElement::from_bytes(&xr_le.into()))
let mut out = [0u8; 64];
lambda_vm_syscalls::syscalls::ecsm_mul_affine(&mut out, &input, &k_le);
let mut xr_be = [0u8; 32];
let mut yr_be = [0u8; 32];
for i in 0..32 {
xr_be[i] = out[31 - i];
yr_be[i] = out[32 + 31 - i];
}
let xr = Option::<FieldElement>::from(FieldElement::from_bytes(&xr_be.into()))?;
let yr = Option::<FieldElement>::from(FieldElement::from_bytes(&yr_be.into()))?;
Some((xr.normalize(), yr.normalize()))
}

/// Base-field inverse `x⁻¹ mod p`.
Expand Down Expand Up @@ -381,18 +395,20 @@ where
Option::from(x.invert())
}

/// Computes `k1·P1 + k2·P2` from four x-only oracle queries, or `None` if any
/// degenerate-configuration guard trips.
/// Computes `k1·P1 + k2·P2` from two affine oracle queries, or `None` if a
/// degenerate configuration trips a guard.
///
/// The lambda-vm ECSM precompile returns only `x(k·P)`. For `A = k1·P1` with
/// `P1 = (xp, yp)` fully known, query `xa = x(k1·P1)` and `xc = x((k1+1)·P1)`.
/// The chord-addition law gives `λ² = xc + xa + xp =: t` and `ya = yp + λ·dx`
/// with `dx = xa − xp`; substituting into `ya² = xa³ + b` makes λ *linear*:
/// `λ = (xa³ − xp³ − t·dx²) / (2·yp·dx)`. The wrong sign `−ya` would force
/// `x((k1−1)·P1) = xc`, i.e. `k1 ≡ 0` or `2·k1 ≡ 0 (mod n)`, excluded by the
/// scalar guards. x-only queries are parity-invariant (`x(k·P) = x(k·(−P))`),
/// so the precompile's canonical-y lift never matters. Same for `B = k2·P2`,
/// then `Q = A + B` is one affine addition. All three inversions are batched.
/// The affine ECSM ecall returns the full point, so `A = k1·P1` and `B = k2·P2`
/// each cost one query and `Q = A + B` is a single chord addition — one field
/// inversion, for `1/(xb − xa)`. `dx = 0` covers both degenerate cases at once
/// (two curve points share an x only when they are equal or negatives), so the
/// caller falls back to the software `lincomb` there.
///
/// The x-only predecessor needed a second query `x((k+1)·P)` per point to solve
/// for `y` through the chord-addition law, which is what made `k1 = 1` and
/// `k1 = N−1` degenerate; with `y` supplied by the chip those scalars are
/// ordinary. secp256k1 has cofactor 1 and prime `N`, so `k·P ≠ O` for every
/// `k ∈ (0, N)` and no further scalar guard is needed.
///
/// Generic over the oracle so unit tests can substitute a software stand-in.
#[cfg(any(target_arch = "riscv64", test))]
Expand All @@ -404,45 +420,33 @@ fn lincomb2_with_oracle<O>(
oracle: O,
) -> Option<AffinePoint>
where
O: Fn(&FieldElement, &Scalar) -> Option<FieldElement>,
O: Fn(&FieldElement, &FieldElement, &Scalar) -> Option<(FieldElement, FieldElement)>,
{
// Inputs are affine already (the ecrecover path lifts them from known Z=1
// points), so no projective→affine inversion is needed here.
if bool::from(a1.is_identity()) || bool::from(a2.is_identity()) {
return None;
}
if scalar_near_edge(k1) || scalar_near_edge(k2) {
if bool::from(k1.is_zero()) || bool::from(k2.is_zero()) {
return None;
}

let (x1, y1) = affine_xy(a1)?;
let (x2, y2) = affine_xy(a2)?;

let xa = oracle(&x1, k1)?;
let xc1 = oracle(&x1, &(*k1 + Scalar::ONE))?;
let xb = oracle(&x2, k2)?;
let xc2 = oracle(&x2, &(*k2 + Scalar::ONE))?;
// The oracle receives the full point (x, y) and returns k·(x, y) directly — no parity
// convention or sign flip, since the precompile gets the real y (pinned in the prover
// by a memory read).
let (xa, ya) = oracle(&x1, &y1, k1)?;
let (xb, yb) = oracle(&x2, &y2, k2)?;

let dx1 = (xa - x1).normalize();
let dx2 = (xb - x2).normalize();
// Q = A + B via one chord addition (A ≠ ±B ⇒ dxq ≠ 0). One field inversion.
let dxq = (xb - xa).normalize();
if bool::from(dx1.is_zero()) || bool::from(dx2.is_zero()) || bool::from(dxq.is_zero()) {
if bool::from(dxq.is_zero()) {
return None;
}

// One shared inversion for the two λ denominators and the final chord.
let den1 = y1.double() * dx1;
let den2 = y2.double() * dx2;
let inv = field_inv(&(den1 * den2 * dxq))?;
let inv_den1 = inv * den2 * dxq;
let inv_den2 = inv * den1 * dxq;
let inv_dxq = inv * den1 * den2;

let ya = solve_y(&x1, &y1, &xa, &xc1, &dx1, &inv_den1)?;
let yb = solve_y(&x2, &y2, &xb, &xc2, &dx2, &inv_den2)?;

// Q = A + B, with A ≠ ±B ensured by dxq ≠ 0.
let lq = (yb - ya) * inv_dxq;
let inv_dxq = field_inv(&dxq)?;
let lq = ((yb - ya) * inv_dxq).normalize();
let xq = (lq.square() - xa - xb).normalize();
let yq = (lq * (xa - xq) - ya).normalize();

Expand All @@ -453,38 +457,6 @@ where
point_from_xy(&xq, &yq)
}

/// Recovers `y(k·P)` from `xa = x(k·P)` and `xc = x((k+1)·P)`.
/// Returns `None` if `xc` is inconsistent with the computed `lambda`
/// (oracle misbehavior); degeneracy guards are in [`lincomb2_with_oracle`].
#[cfg(any(target_arch = "riscv64", test))]
fn solve_y(
xp: &FieldElement,
yp: &FieldElement,
xa: &FieldElement,
xc: &FieldElement,
dx: &FieldElement,
inv_den: &FieldElement,
) -> Option<FieldElement> {
let t = *xc + xa + xp;
let xa3 = xa.square() * xa;
let xp3 = xp.square() * xp;
let lambda = (xa3 - xp3 - t * dx.square()) * inv_den;
if lambda.square().normalize() != t.normalize() {
return None;
}
Some((*yp + lambda * dx).normalize())
}

/// `k ∈ {0, 1, n−1}`: fast early-exit before oracle calls.
/// k=0: invalid ecall scalar. k=1: dx=0. k=n-1: k+1 wraps to 0 mod n.
#[cfg(any(target_arch = "riscv64", test))]
fn scalar_near_edge(k: &Scalar) -> bool {
use k256::elliptic_curve::subtle::ConstantTimeEq;
bool::from(k.is_zero())
|| bool::from(k.ct_eq(&Scalar::ONE))
|| bool::from(k.ct_eq(&(-Scalar::ONE)))
}

/// Affine `(x, y)` of a non-identity point as field elements, via its SEC1
/// uncompressed encoding (k256 keeps `AffinePoint`'s coordinate fields private).
#[cfg(any(target_arch = "riscv64", test))]
Expand Down
Loading
Loading