EEVM.Crypto.ECDSA (eevm v0.1.0)

Copy Markdown View Source

ECDSA over the short-Weierstrass curves y² = x³ + ax + b (mod p) that Ethereum uses, in pure Elixir:

  • :secp256k1 for ECRECOVER (0x01) and for transaction and EIP-7702 authorization signatures: public-key recovery (recover/5) and deterministic signing (sign/3).
  • :p256 (NIST P-256, secp256r1) for P256VERIFY (0x100, EIP-7951): verify/5.

Both curves share one implementation of SEC 1 v2 §4.1. Points are computed in Jacobian coordinates {X, Y, Z}, which stand for the affine point (X / Z², Y / Z³), so adding and doubling need no field inversion; the point at infinity is :infinity. Scalar multiplication recodes scalars in width-w NAF, so about one bit in six or nine costs an addition. Each base point gets a table of its odd multiples; the base point G's table is wide and built at compile time. u₁·G + u₂·Q shares one chain of doublings between both products (Shamir's trick). On secp256k1 the GLV endomorphism splits each scalar into two halves of about 128 bits, which halves the doublings.

Hashes and private keys are 32-byte big-endian binaries; r, s and coordinates are integers.

This code is not constant-time. eevm verifies and recovers public signatures; sign/3 serves tests, tooling and examples, not keys that must resist timing side channels.

Summary

Types

A supported curve.

An affine point {x, y}.

Functions

Returns the order n of curve's base point.

Returns the public key d·G of the 32-byte private key d, or {:error, :invalid_private_key} unless 0 < d < n.

Recovers the public key that produced the signature (r, s) over the 32-byte hash (SEC 1 §4.1.6).

Signs the 32-byte hash with the 32-byte private_key.

Verifies the signature (r, s) over the 32-byte hash against public_key (SEC 1 §4.1.4).

Types

curve()

@type curve() :: :secp256k1 | :p256

A supported curve.

point()

@type point() :: {non_neg_integer(), non_neg_integer()}

An affine point {x, y}.

Functions

order(curve)

@spec order(curve()) :: pos_integer()

Returns the order n of curve's base point.

Example

iex> EEVM.Crypto.ECDSA.order(:secp256k1)
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

public_key(curve, arg)

@spec public_key(curve(), <<_::256>>) ::
  {:ok, point()} | {:error, :invalid_private_key}

Returns the public key d·G of the 32-byte private key d, or {:error, :invalid_private_key} unless 0 < d < n.

Example

iex> {:ok, {x, _y}} = EEVM.Crypto.ECDSA.public_key(:secp256k1, <<1::256>>)
iex> x == 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
true

recover(curve, arg, r, s, recovery_id)

@spec recover(curve(), <<_::256>>, non_neg_integer(), non_neg_integer(), 0..3) ::
  {:ok, point()} | {:error, :invalid_signature}

Recovers the public key that produced the signature (r, s) over the 32-byte hash (SEC 1 §4.1.6).

recovery_id picks the ephemeral point R: bit 0 is the parity of its y, and bit 1 says its x is r + n rather than r. Any s in [1, n) is accepted; enforcing EIP-2's low s is the caller's job.

Returns {:error, :invalid_signature} when r or s is out of range, no curve point has that x, or the result is the point at infinity.

sign(curve, hash, private_key)

@spec sign(curve(), <<_::256>>, <<_::256>>) ::
  {:ok, {pos_integer(), pos_integer(), 0..3}} | {:error, :invalid_private_key}

Signs the 32-byte hash with the 32-byte private_key.

The nonce is deterministic (RFC 6979 with HMAC-SHA256), and s is normalised to the low half of [1, n) (EIP-2), flipping the recovery id to match, as libsecp256k1 does. Returns {:ok, {r, s, recovery_id}}, or {:error, :invalid_private_key} unless 0 < d < n.

verify(curve, arg, r, s, public_key)

@spec verify(curve(), <<_::256>>, non_neg_integer(), non_neg_integer(), point()) ::
  boolean()

Verifies the signature (r, s) over the 32-byte hash against public_key (SEC 1 §4.1.4).

False when r or s is outside (0, n), public_key is not on the curve (which rules out (0, 0)), or R' = u₁·G + u₂·Q is the point at infinity; otherwise true when R'.x mod n == r. This matches EIP-7951.