Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
72e17f7
Add on-demand hint ecall (host-computed)
jotabulacios Jul 24, 2026
d01d145
Add HINT prover table for the hint ecall
jotabulacios Jul 24, 2026
c378f56
Add hint ecall guest tests and test programs
jotabulacios Jul 24, 2026
3b599b3
Route ecsm inverses and sqrt through hint ecall
jotabulacios Jul 24, 2026
e8f421c
Make the hint ecall ABI big-endian
jotabulacios Jul 27, 2026
9f83982
Validate the Hint ecall operand addresses
jotabulacios Jul 27, 2026
eac970b
Verify hints by difference instead of byte compare
jotabulacios Jul 27, 2026
8fffbfb
Bind HINT writes to x12 and range-check bytes
jotabulacios Jul 29, 2026
f417360
Fix hint doc placement and guest cargo config
jotabulacios Jul 29, 2026
2f30acf
Return the affine point (x, y) from ECSM
jotabulacios Jul 28, 2026
ca01856
Make ECSM affine y-return sound
jotabulacios Jul 28, 2026
cd603c2
Select ECSM x-only vs affine ecall via IS_AFFINE
jotabulacios Jul 28, 2026
729647b
Merge branch 'main' into perf/ecsm-affine-selector
jotabulacios Jul 29, 2026
287b2f5
Force yR < p in the ECSM chip
jotabulacios Jul 30, 2026
5d04947
Drop stale affine docs and dead entry point
jotabulacios Jul 30, 2026
5743caa
Verify hints with a mandatory software fallback
jotabulacios Jul 30, 2026
ad21c37
Constrain the HINT multiplicity column as boolean
jotabulacios Jul 30, 2026
515a921
Drop BENCH-ONLY labels from the hint ecall
jotabulacios Jul 30, 2026
da14fc8
Test that IS_BIT rejects a non-boolean HINT mu
jotabulacios Jul 30, 2026
71ba613
Merge branch 'main' into feat/hint-ecall
jotabulacios Jul 30, 2026
17b874f
Merge remote-tracking branch 'origin/feat/hint-ecall' into perf/ecsm-…
jotabulacios Jul 30, 2026
56a1717
Merge branch 'main' into feat/hint-ecall
jotabulacios Jul 30, 2026
2e0c16d
solve conflicts
jotabulacios Jul 31, 2026
c75121a
Run ethrex-crypto host tests in CI
jotabulacios Jul 31, 2026
3b1a012
Add software fallback and test seam to field_inv
jotabulacios Jul 31, 2026
32f9cd1
GPU parity-check the HINT table
jotabulacios Jul 31, 2026
be9066b
Move HINT syscall off the FEXT_FMA numberD
jotabulacios Jul 31, 2026
95b62de
Merge feat/hint-ecall into perf/ecsm-affine-selector
jotabulacios Jul 31, 2026
7d0e1ba
Test the affine ECSM operand ranges instead of their distance
jotabulacios Jul 31, 2026
58ad49f
Fix stale ECSM affine docs, counts and the new guest's lock
jotabulacios Jul 31, 2026
5aa7d0c
Cover the IS_AFFINE selector and the yR < p check
jotabulacios Jul 31, 2026
9ce5c1b
Widen the ECSM error wording to the affine path
jotabulacios Jul 31, 2026
c2283bf
Bind and range-check the HINT ecall operands
jotabulacios Jul 31, 2026
64a22ff
lint
jotabulacios Jul 31, 2026
847954b
Merge main into feat/hint-ecall (pick up the real-block benchmark)
MauroToscano Aug 1, 2026
abd3a8b
Merge feat/hint-ecall (now including main) into the selector branch
MauroToscano Aug 1, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/pr_main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ jobs:
- name: Run syscalls host tests (keccak differential vs sha3)
run: make test-syscalls

- name: Run ethrex-crypto host tests (hint verify-then-fallback + ecrecover)
run: make test-ethrex-crypto

# "Test" is a required check — keep this name to avoid branch protection changes.
# This gate job passes only when CLI, executor, disk-spill, and prover tests succeed.
test:
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ ASM_LDFLAGS ?= -fuse-ld=lld -nostdlib -Wl,-e,main
# Custom RV64IM target spec location
RV64_TARGET_SPEC=$(CURDIR)/executor/programs/riscv64im-lambda-vm-elf.json

.PHONY: test prepare-sysroot
.PHONY: test test-syscalls test-ethrex-crypto prepare-sysroot

# The guard checks for include/stdlib.h (not just the include/ dir) so that a PARTIAL
# sysroot — directories present but missing the C standard library headers — is detected
Expand Down Expand Up @@ -512,7 +512,12 @@ check-ethrex-fixture-checksums:
test-syscalls:
cd syscalls && cargo test

test: compile-programs test-syscalls
# ethrex-crypto is a detached workspace (excluded from the root members), so a
# root `cargo test` never runs it. Run it explicitly, like test-syscalls.
test-ethrex-crypto:
cd crypto/ethrex-crypto && cargo test

test: compile-programs test-syscalls test-ethrex-crypto
cargo test

# === Quick test shortcuts ===
Expand Down
1 change: 1 addition & 0 deletions bench_vs/lambda/recursion/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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