Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
11 changes: 8 additions & 3 deletions crypto/ecsm/src/curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,20 @@ fn schedule(k: &BigUint) -> Vec<(u8, u8, u8)> {
sched
}

/// Executor fast path: the x-coordinate of `k·g`, via k256's optimized scalar
/// Executor fast path: `k·g` in affine coordinates, via k256's optimized scalar
/// 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 {
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)
}

/// The x-coordinate of `k·g`. Thin wrapper over [`scalar_mul_affine`].
pub fn scalar_mul_affine_x(k: &BigUint, g: &AffinePoint) -> BigUint {
scalar_mul_affine(k, g).x
}

/// Jacobian doubling (dbl-2009-l) for `y² = x³ + 7`: on `(X:Y:Z)` with
Expand Down
27 changes: 24 additions & 3 deletions crypto/ecsm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,30 @@ pub(crate) fn prepare(
}

/// 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`.
/// 32-byte values.
pub fn scalar_mul_x(k_le: &[u8; 32], xg_le: &[u8; 32]) -> Result<[u8; 32], EcsmError> {
Ok(scalar_mul_full(k_le, xg_le)?.0)
}

/// The ECSM ecall's memory image: `(xR, yR, yG)`, three little-endian 32-byte values written
/// back as one contiguous 96-byte buffer.
pub type EcsmOutput = ([u8; 32], [u8; 32], [u8; 32]);

/// The executor's entry point: `(xR, yR, yG)` as little-endian 32-byte values, written back
/// to guest memory as one contiguous 96-byte buffer at `addr_xR`.
///
/// `yG` is echoed because the chip is free to witness *either* root of `xG` — the AIR only
/// binds `yG² ≡ xG³ + b`, so nothing pins the sign (see `spec/ecsm.typ`, "Two options for
/// `y_G`"). Returning `yR` alone would therefore be ambiguous: it is the y of `k·(xG, yG)`
/// for whichever root the prover chose, which is `±y(k·P)` for the caller's own point `P`.
/// Echoing `yG` resolves it caller-side at no cost: the caller compares `yG` against its own
/// base point's `y`, so a flipped root just flips the sign it applies to `yR`. That keeps the
/// root a free choice for the prover, exactly as the spec's aside argues, while still handing
/// back a usable y. The comparison is safe on bytes because the chip range-checks `yG < p`
/// and `yR < p` (`OverflowKind::YgLtP` / `YrLtP`): without those the prover could publish the
/// second representative `y + p`, which agrees mod `p` but carries the opposite parity.
pub fn scalar_mul_full(k_le: &[u8; 32], xg_le: &[u8; 32]) -> Result<EcsmOutput, EcsmError> {
let (k, g) = prepare(k_le, xg_le)?;
Ok(to_le_32(&curve::scalar_mul_affine_x(&k, &g)))
let r = curve::scalar_mul_affine(&k, &g);
Ok((to_le_32(&r.x), to_le_32(&r.y), to_le_32(&g.y)))
}
29 changes: 28 additions & 1 deletion crypto/ecsm/src/tests/witness_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use num_bigint::BigUint;

use crate::witness::compute_witness;
use crate::{n, scalar_mul_x, to_le_32};
use crate::{n, scalar_mul_full, scalar_mul_x, to_le_32};

fn gx_le() -> [u8; 32] {
let gx = BigUint::parse_bytes(
Expand Down Expand Up @@ -59,3 +59,30 @@ fn witness_works_near_curve_order() {
assert_eq!(w.x_r, gx); // (N-1)·G = -G shares x with G
assert_eq!(w.len_k, 255);
}

/// The executor writes `scalar_mul_full`'s bytes into guest memory while the prover writes the
/// witness columns, and the MEMW bus asserts the two images are the same claim. Publishing
/// `yR` and `yG` put two more values under that coupling — the executor takes them from k256's
/// affine scalar multiplication, the witness from its own double-and-add replay — so the two
/// disagreeing anywhere (a sign, a representative) would surface only as an unbalanced bus on
/// whichever scalar hit it. Pin the agreement directly instead.
#[test]
fn witness_matches_the_executor_output_image() {
let gx = gx_le();
let scalars = [
BigUint::from(1u8),
BigUint::from(5u8),
BigUint::from(0xABCDEFu64),
(n() - BigUint::from(1u8)) / BigUint::from(2u8),
n() - BigUint::from(1u8),
];
for k_big in scalars {
let k = to_le_32(&k_big);
let w = compute_witness(&k, &gx).expect("witness");
let (x_r, y_r, y_g) = scalar_mul_full(&k, &gx).expect("executor output");
assert_eq!(w.x_r, x_r, "xR disagrees for k = {k_big}");
assert_eq!(w.y_r, y_r, "yR disagrees for k = {k_big}");
assert_eq!(w.y_g, y_g, "yG disagrees for k = {k_big}");
assert_eq!(y_g[0] & 1, 0, "xG is lifted to its even root on both sides");
}
}
15 changes: 15 additions & 0 deletions crypto/ecsm/src/witness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ 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`, the addend that forces `yR < p`.
pub y_r_sub_p: [u8; 32],
/// `(yG - p) mod 2^256`, the addend that forces `yG < p`.
///
/// Both are needed because `yR` and `yG` are published to guest memory. The byte range
/// checks alone bound them by `2^256`, and the quotient columns absorb a multiple of `p`,
/// so a witness could publish `y + p` for any `y < 2^256 - p` (~2^32) — and such points
/// are constructible, since `3 | p-1` makes cubing 3-to-1, so a third of small `y` have a
/// curve `x`. `y + p` carries the opposite parity, which is exactly what the caller reads
/// to resolve the root.
pub y_g_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 @@ -328,6 +339,8 @@ 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()));
let y_g_sub_p = to_le_32(&((&two_256 + &g.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 +367,8 @@ 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,
y_g_sub_p,
len_k,
x_r,
y_r,
Expand Down
Loading
Loading