Skip to content
Draft
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
245 changes: 215 additions & 30 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions crypto/ecsm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ edition = "2024"
license.workspace = true

[dependencies]
num-bigint = "0.4.6"
num-traits = "0.2.19"
crypto-bigint = { version = "0.7.5", default-features = false }
# Audited secp256k1 arithmetic (host-side witness generation only; never in the
# constraint system). Used for executor scalar multiplication and for the projective
# double-and-add replay + batch inversion that builds ECDAS step witnesses efficiently.
k256 = { version = "0.13", default-features = false, features = ["arithmetic", "expose-field"] }
k256 = { version = "0.14.0-rc.14", default-features = false, features = ["arithmetic"] }

126 changes: 69 additions & 57 deletions crypto/ecsm/src/curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,36 @@
//! `k in [1, N)` (see `ecsm.typ` "Point at infinity" / ECDAS soundness argument), so the
//! affine formulas below are always well defined.

use num_bigint::BigUint;
use crypto_bigint::U256;
use crypto_bigint::modular::ConstMontyForm;

// Compile-time Montgomery parameters for secp256k1 p.
crypto_bigint::const_monty_params!(
Secp256k1Field,
U256,
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"
);

type Fp = ConstMontyForm<Secp256k1Field, 4>;

/// An affine curve point. Never the point at infinity.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AffinePoint {
pub x: BigUint,
pub y: BigUint,
pub x: U256,
pub y: U256,
}

fn fe_from_u256(v: &U256) -> Fp {
ConstMontyForm::new(v)
}

fn u256_from_fe(f: &Fp) -> U256 {
f.retrieve()
}

fn fp_invert(f: Fp) -> Option<Fp> {
// safegcd inversion; `None` for a zero input (which has no inverse).
Option::from(f.invert())
}

/// Recovers the canonical (even) `y` for a given `x` such that `y^2 = x^3 + b mod p`.
Expand All @@ -23,13 +46,14 @@ pub struct AffinePoint {
///
/// Returns `None` when `x` is not a valid curve x-coordinate (`x^3 + b` is not a quadratic
/// residue, or `x` is not a canonical field element).
pub fn recover_y_canonical(x: &BigUint) -> Option<BigUint> {
// SEC1 compressed encoding: the `0x02` prefix selects the even-`y` root, delegated to k256.
pub fn recover_y_canonical(x: &U256) -> Option<U256> {
use k256::elliptic_curve::sec1::{FromSec1Point, Sec1Point};
let x_bytes: [u8; 32] = x.to_be_bytes().into();
let mut enc = [0u8; 33];
enc[0] = 0x02;
enc[1..33].copy_from_slice(&be32(x));
let ep = EncodedPoint::from_bytes(enc).ok()?;
let affine: K256Affine = Option::from(K256Affine::from_encoded_point(&ep))?;
enc[1..33].copy_from_slice(&x_bytes);
let ep = Sec1Point::<k256::Secp256k1>::from_bytes(enc).ok()?;
let affine: K256Affine = Option::from(K256Affine::from_sec1_point(&ep))?;
Some(from_k256_affine(&affine).y)
}

Expand All @@ -47,14 +71,14 @@ pub struct StepPts {
pub r: AffinePoint,
/// Slope of this step: add => (yG-yA)/(xG-xA), double => 3xA^2/(2yA).
/// Precomputed here (batched) so the witness builder never inverts per step.
pub lambda: BigUint,
pub lambda: U256,
}

/// Bit length minus one = position of the most significant set bit (`len_k`).
/// Requires `k >= 1`.
pub fn msb_position(k: &BigUint) -> u32 {
debug_assert!(k > &BigUint::from(0u8));
(k.bits() as u32) - 1
pub fn msb_position(k: &U256) -> u32 {
debug_assert!(*k != U256::ZERO);
k.bits_vartime() - 1
}

// =========================================================================
Expand All @@ -67,54 +91,41 @@ pub fn msb_position(k: &BigUint) -> u32 {
// ~2*len_k Fermat inversions of the reference with two batched inversions.
// =========================================================================

use k256::elliptic_curve::ff::PrimeField as _;
use k256::elliptic_curve::group::Curve as _;
use k256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
use k256::{AffinePoint as K256Affine, EncodedPoint, FieldElement, ProjectivePoint, Scalar};

/// 32 big-endian bytes of a value known to fit in 256 bits (left zero-padded).
fn be32(v: &BigUint) -> [u8; 32] {
let b = v.to_bytes_be();
debug_assert!(b.len() <= 32, "value exceeds 256 bits");
let mut out = [0u8; 32];
out[32 - b.len()..].copy_from_slice(&b);
out
}

fn fe_from_biguint(v: &BigUint) -> FieldElement {
Option::from(FieldElement::from_bytes(&be32(v).into()))
.expect("ECSM: field element must be < p")
}

fn biguint_from_fe(f: &FieldElement) -> BigUint {
BigUint::from_bytes_be(&f.to_bytes())
}
use k256::elliptic_curve::sec1::{FromSec1Point, Sec1Point, ToSec1Point};
use k256::{AffinePoint as K256Affine, ProjectivePoint, Scalar};
use k256::elliptic_curve::PrimeField as _;

fn to_k256_affine(a: &AffinePoint) -> K256Affine {
let ep = EncodedPoint::from_affine_coordinates(&be32(&a.x).into(), &be32(&a.y).into(), false);
Option::from(K256Affine::from_encoded_point(&ep)).expect("ECSM: point must be on the curve")
let x_bytes: [u8; 32] = a.x.to_be_bytes().into();
let y_bytes: [u8; 32] = a.y.to_be_bytes().into();
let ep = Sec1Point::<k256::Secp256k1>::from_affine_coordinates(
<&k256::elliptic_curve::FieldBytes<k256::Secp256k1>>::from(&x_bytes),
<&k256::elliptic_curve::FieldBytes<k256::Secp256k1>>::from(&y_bytes),
false,
);
Option::from(K256Affine::from_sec1_point(&ep)).expect("ECSM: point must be on the curve")
}

fn from_k256_affine(p: &K256Affine) -> AffinePoint {
let ep = p.to_encoded_point(false);
let ep = p.to_sec1_point(false);
AffinePoint {
x: BigUint::from_bytes_be(ep.x().expect("ECSM: affine point has x")),
y: BigUint::from_bytes_be(ep.y().expect("ECSM: affine point has y")),
x: U256::from_be_slice(ep.x().expect("ECSM: affine point has x")),
y: U256::from_be_slice(ep.y().expect("ECSM: affine point has y")),
}
}

/// Montgomery's batch inversion over `FieldElement`: one real inversion total.
fn batch_invert(xs: &[FieldElement]) -> Vec<FieldElement> {
/// Montgomery's batch inversion over `Fp`: one real inversion total.
fn batch_invert(xs: &[Fp]) -> Vec<Fp> {
let n = xs.len();
let mut prefix = Vec::with_capacity(n);
let mut acc = FieldElement::ONE;
let mut acc = Fp::ONE;
for x in xs {
prefix.push(acc);
acc *= *x;
}
let mut inv =
Option::<FieldElement>::from(acc.invert()).expect("ECSM: batch denominator is nonzero");
let mut out = vec![FieldElement::ONE; n];
let mut inv = fp_invert(acc).expect("ECSM: batch denominator is nonzero");
let mut out = vec![Fp::ONE; n];
for i in (0..n).rev() {
out[i] = prefix[i] * inv;
inv *= xs[i];
Expand All @@ -125,14 +136,14 @@ fn batch_invert(xs: &[FieldElement]) -> Vec<FieldElement> {
/// The double-and-add schedule for `k`: one `(round, op, next_op)` per ECDAS row.
/// Pure bit logic (data-independent of point values), identical control flow to
/// the reference replay.
fn schedule(k: &BigUint) -> Vec<(u8, u8, u8)> {
fn schedule(k: &U256) -> Vec<(u8, u8, u8)> {
let m = msb_position(k) as i64;
let mut sched = Vec::new();
let mut round: i64 = m - 1;
let mut op: u8 = 0;
while round >= 0 {
let next_op = if op == 0 {
if k.bit(round as u64) { 1u8 } else { 0u8 }
if k.bit_vartime(round as u32) { 1u8 } else { 0u8 }
} else {
0u8
};
Expand All @@ -150,8 +161,9 @@ fn schedule(k: &BigUint) -> Vec<(u8, u8, u8)> {
/// Executor fast path: the x-coordinate of `k·g`, 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 {
let scalar = Option::<Scalar>::from(Scalar::from_repr(be32(k).into()))
pub fn scalar_mul_affine_x(k: &U256, g: &AffinePoint) -> U256 {
let k_bytes: [u8; 32] = k.to_be_bytes().into();
let scalar = Option::<Scalar>::from(Scalar::from_repr(k_bytes.into()))
.expect("ECSM: scalar k must be < N");
let g_proj = ProjectivePoint::from(to_k256_affine(g));
let r = (g_proj * scalar).to_affine();
Expand All @@ -162,7 +174,7 @@ pub fn scalar_mul_affine_x(k: &BigUint, g: &AffinePoint) -> BigUint {
/// batched inversion. Produces the identical `StepPts` sequence as the BigUint
/// reference replay (validated by the parity test in `tests::curve_tests`), but with
/// two batched inversions instead of one per double/add step.
pub fn replay_double_and_add(k: &BigUint, g: &AffinePoint) -> (Vec<StepPts>, AffinePoint) {
pub fn replay_double_and_add(k: &U256, g: &AffinePoint) -> (Vec<StepPts>, AffinePoint) {
let sched = schedule(k);
if sched.is_empty() {
return (Vec::new(), g.clone()); // k == 1: result is g, no steps
Expand Down Expand Up @@ -193,14 +205,14 @@ pub fn replay_double_and_add(k: &BigUint, g: &AffinePoint) -> (Vec<StepPts>, Aff
let r_aff: Vec<AffinePoint> = affine[n..].iter().map(from_k256_affine).collect();

// 3. batch-invert all slope denominators (add: xG-xA, double: 2yA).
let gx_fe = fe_from_biguint(&g.x);
let gy_fe = fe_from_biguint(&g.y);
let denoms: Vec<FieldElement> = (0..n)
let gx_fe = fe_from_u256(&g.x);
let gy_fe = fe_from_u256(&g.y);
let denoms: Vec<Fp> = (0..n)
.map(|i| {
if sched[i].1 == 1 {
gx_fe - fe_from_biguint(&a_aff[i].x)
gx_fe - fe_from_u256(&a_aff[i].x)
} else {
let ya = fe_from_biguint(&a_aff[i].y);
let ya = fe_from_u256(&a_aff[i].y);
ya + ya
}
})
Expand All @@ -211,10 +223,10 @@ pub fn replay_double_and_add(k: &BigUint, g: &AffinePoint) -> (Vec<StepPts>, Aff
let steps: Vec<StepPts> = (0..n)
.map(|i| {
let num = if sched[i].1 == 1 {
gy_fe - fe_from_biguint(&a_aff[i].y)
gy_fe - fe_from_u256(&a_aff[i].y)
} else {
let x2 = {
let xa = fe_from_biguint(&a_aff[i].x);
let xa = fe_from_u256(&a_aff[i].x);
xa * xa
};
x2 + x2 + x2 // 3 xA^2
Expand All @@ -226,7 +238,7 @@ pub fn replay_double_and_add(k: &BigUint, g: &AffinePoint) -> (Vec<StepPts>, Aff
op: sched[i].1,
next_op: sched[i].2,
r: r_aff[i].clone(),
lambda: biguint_from_fe(&(num * inv_denoms[i])),
lambda: u256_from_fe(&(num * inv_denoms[i])),
}
})
.collect();
Expand Down
44 changes: 21 additions & 23 deletions crypto/ecsm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//!
//! Curve point operations are delegated to the RustCrypto `k256` crate; witness generation
//! replays the schedule in `k256` projective coordinates and batch-inverts the slope
//! denominators, while `num-bigint` carries the coordinate/limb representation the trace
//! denominators, while `crypto-bigint` carries the coordinate/limb representation the trace
//! needs. All of this runs once per `ECALL`, so it is not performance critical.
//!
//! Curve: secp256k1, `y^2 = x^3 + 7 mod p`, `p = 2^256 - 2^32 - 977`, order `N`.
Expand All @@ -21,7 +21,7 @@ pub mod witness;
#[cfg(test)]
mod tests;

use num_bigint::BigUint;
use crypto_bigint::U256;

pub use curve::{AffinePoint, recover_y_canonical, replay_double_and_add};
pub use witness::{EcdasStep, EcsmWitness, compute_witness};
Expand All @@ -48,14 +48,22 @@ pub const R_BYTES: [u8; 33] = [
0x02,
];

/// The prime field modulus `p` as a `BigUint`.
pub fn p() -> BigUint {
BigUint::from_bytes_le(&P_BYTES)
/// The prime field modulus `p` as a `U256`.
pub const P: U256 =
U256::from_be_hex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F");

/// The curve group order `N` as a `U256`.
pub const N: U256 =
U256::from_be_hex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141");

/// The prime field modulus `p` as a `U256`.
pub const fn p() -> U256 {
P
}

/// The curve order `N` as a `BigUint`.
pub fn n() -> BigUint {
BigUint::from_bytes_le(&N_BYTES)
/// The curve order `N` as a `U256`.
pub const fn n() -> U256 {
N
}

/// Errors that prevent a sound ECSM witness from existing for the given inputs.
Expand Down Expand Up @@ -86,32 +94,22 @@ impl core::fmt::Display for EcsmError {

impl std::error::Error for EcsmError {}

/// Converts a `BigUint` to 32 little-endian bytes (zero-padded / truncated to 32).
pub fn to_le_32(v: &BigUint) -> [u8; 32] {
debug_assert!(v.bits() <= 256, "to_le_32: value exceeds 256 bits");
let mut bytes = v.to_bytes_le();
bytes.resize(32, 0);
let mut out = [0u8; 32];
out.copy_from_slice(&bytes[..32]);
out
}

/// Validates the scalar and recovers the generator point from `(xG, k)`.
///
/// Shared front-end for both entry points: checks `0 < k < N`, rebuilds `xG`, and recovers
/// the canonical `yG`.
pub(crate) fn prepare(
k_le: &[u8; 32],
xg_le: &[u8; 32],
) -> Result<(BigUint, AffinePoint), EcsmError> {
let k = BigUint::from_bytes_le(k_le);
if k == BigUint::from(0u8) {
) -> Result<(U256, AffinePoint), EcsmError> {
let k = U256::from_le_slice(k_le);
if k == U256::ZERO {
return Err(EcsmError::ScalarIsZero);
}
if k >= n() {
return Err(EcsmError::ScalarOutOfRange);
}
let xg = BigUint::from_bytes_le(xg_le);
let xg = U256::from_le_slice(xg_le);
if xg >= p() {
return Err(EcsmError::CoordinateOutOfRange);
}
Expand All @@ -124,5 +122,5 @@ pub(crate) fn prepare(
/// to guest memory at `addr_xR`.
pub fn scalar_mul_x(k_le: &[u8; 32], xg_le: &[u8; 32]) -> Result<[u8; 32], EcsmError> {
let (k, g) = prepare(k_le, xg_le)?;
Ok(to_le_32(&curve::scalar_mul_affine_x(&k, &g)))
Ok(curve::scalar_mul_affine_x(&k, &g).to_le_bytes().into())
}
Loading
Loading