Skip to main content

Overview

Secp256k1 is an elliptic curve digital signature algorithm (ECDSA) over the secp256k1 curve, providing asymmetric cryptography for transaction authentication. Mainnet-critical algorithm - Primary signature scheme for Ethereum transactions, message signing, and public key recovery from signatures. Curve equation: y² = x³ + 7 (mod p) Parameters:
  • Prime field: p = 2²⁵⁶ - 2³² - 977
  • Curve order: n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
  • Generator point G with coordinates (Gx, Gy)
Key operations:
  • sign(hash, privateKey) → signature - Create ECDSA signature with deterministic nonce (RFC 6979)
  • verify(signature, hash, publicKey) → boolean - Validate signature authenticity
  • recoverPublicKey(signature, hash) → publicKey - Recover signer’s public key (Ethereum’s ecRecover)
  • derivePublicKey(privateKey) → publicKey - Elliptic curve point multiplication (privateKey * G)

Quick Start

const crypto = @import("crypto");
const primitives = @import("primitives");

// Hash a message (EIP-191 personal signing typically adds prefix; this is raw keccak)
const message_hash = @import("crypto").keccak256.hash("Hello, Ethereum!"); // [32]u8

// Private key bytes
const pk = try primitives.Hex.hexToBytesFixed(32, "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");

// Sign
const sig = try crypto.secp256k1.sign(message_hash, &pk);

// Derive public key
const pub = try crypto.secp256k1.derivePublicKey(&pk);

// Verify
const ok = try crypto.secp256k1.verify(sig, message_hash, pub);

// Recover public key
const recovered = try crypto.secp256k1.recoverPublicKey(message_hash, sig);

Examples

Interactive examples in the Voltaire Playground:

API Reference

Signing

sign(messageHash, privateKey)

Sign a 32-byte message hash with a private key using deterministic ECDSA (RFC 6979). Parameters:
  • messageHash (HashType) - 32-byte Keccak256 hash to sign
  • privateKey (Uint8Array) - 32-byte private key (must be > 0 and < curve order)
Returns: BrandedSignature with components:
  • r (Uint8Array) - 32-byte signature component
  • s (Uint8Array) - 32-byte signature component (low-s enforced)
  • v (number) - Recovery ID (27 or 28 for Ethereum compatibility)
Throws:
  • InvalidPrivateKeyError - Private key invalid (wrong length, zero, or >= curve order)
  • Secp256k1Error - Signing operation failed
const signature = Secp256k1.sign(messageHash, privateKey);
console.log(signature.v); // 27 or 28
console.log(signature.r.length); // 32
console.log(signature.s.length); // 32

Verification

verify(signature, messageHash, publicKey)

Verify an ECDSA signature against a message hash and public key. Parameters:
  • signature (BrandedSignature) - Signature with r, s, v components
  • messageHash (HashType) - 32-byte message hash that was signed
  • publicKey (Uint8Array) - 64-byte uncompressed public key (x || y coordinates)
Returns: boolean - true if signature is valid, false otherwise Throws:
  • InvalidPublicKeyError - Public key wrong length
  • InvalidSignatureError - Signature components wrong length
const valid = Secp256k1.verify(signature, messageHash, publicKey);
if (valid) {
  console.log('Signature verified!');
}

recoverPublicKey(signature, messageHash)

Recover the public key from a signature and message hash. This is the core of Ethereum’s ecRecover precompile. Parameters:
  • signature (BrandedSignature) - Signature with r, s, v components
  • messageHash (HashType) - 32-byte message hash that was signed
Returns: Uint8Array - 64-byte uncompressed public key Throws:
  • InvalidSignatureError - Invalid signature format or recovery failed
const recovered = Secp256k1.recoverPublicKey(signature, messageHash);
// Use recovered key to derive Ethereum address

Key Management

derivePublicKey(privateKey)

Derive the public key from a private key using elliptic curve point multiplication (private_key * G). Parameters:
  • privateKey (Uint8Array) - 32-byte private key
Returns: Uint8Array - 64-byte uncompressed public key Throws:
  • InvalidPrivateKeyError - Invalid private key
const publicKey = Secp256k1.derivePublicKey(privateKey);
console.log(publicKey.length); // 64 (x || y, no 0x04 prefix)

isValidPrivateKey(privateKey)

Check if a byte array is a valid secp256k1 private key. Parameters:
  • privateKey (Uint8Array) - Candidate private key
Returns: boolean - true if valid (32 bytes, > 0, < curve order)
if (Secp256k1.isValidPrivateKey(privateKey)) {
  // Safe to use
}

isValidPublicKey(publicKey)

Check if a byte array is a valid secp256k1 public key. Parameters:
  • publicKey (Uint8Array) - Candidate public key
Returns: boolean - true if valid (64 bytes, point on curve)
if (Secp256k1.isValidPublicKey(publicKey)) {
  // Point is on the curve
}

isValidSignature(signature)

Check if a signature has valid r, s, v components. Parameters:
  • signature (BrandedSignature) - Candidate signature
Returns: boolean - true if valid
if (Secp256k1.isValidSignature(signature)) {
  // Signature format is correct
}

Constants

Secp256k1.CURVE_ORDER            // 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n
Secp256k1.PRIVATE_KEY_SIZE       // 32 bytes
Secp256k1.PUBLIC_KEY_SIZE        // 64 bytes (uncompressed, no prefix)
Secp256k1.SIGNATURE_COMPONENT_SIZE // 32 bytes (for r and s)

Security Considerations

Critical Warnings

⚠️ Signatures must be validated: Verify r and s are in valid range [1, n-1] where n is curve order. Invalid signature components can leak information or cause verification failures. ⚠️ Deterministic nonces prevent reuse attacks: RFC 6979 deterministic signatures eliminate nonce reuse vulnerability. Reusing a nonce with different messages leaks the private key - never implement custom nonce generation. ⚠️ Recovery ID (v parameter) for public key recovery: The v parameter (27 or 28 in Ethereum) indicates which of two possible public keys to recover from a signature. Critical for ecRecover precompile. ⚠️ Low-s enforcement: Signatures automatically use low-s values (s ≤ n/2) to prevent malleability. Both high-s and low-s signatures verify successfully, but Ethereum requires low-s. ⚠️ Use cryptographically secure random: Never use Math.random() for private key generation. Use crypto.getRandomValues() or similar CSPRNG.

Performance

Native Zig implementation provides 2-5x speedup over pure JavaScript on cryptographic operations, with negligible overhead for FFI calls.

Test Vectors

RFC 6979 Deterministic Signatures

// Private key = 1
const privateKey = Hex.toBytes('0x0000000000000000000000000000000000000000000000000000000000000001');

// Message hash (SHA-256 of "hello world")
const messageHash = sha256("hello world");

// Sign twice - should produce identical signatures
const sig1 = Secp256k1.sign(messageHash, privateKey);
const sig2 = Secp256k1.sign(messageHash, privateKey);

// Same message + key = same signature (deterministic)
assert(sig1.r.every((byte, i) => byte === sig2.r[i]));
assert(sig1.s.every((byte, i) => byte === sig2.s[i]));
assert(sig1.v === sig2.v);

Signature Recovery

const privateKey = Hex.toBytes('0x000000000000000000000000000000000000000000000000000000000000002a');
const messageHash = sha256("test recovery");

// Sign message
const signature = Secp256k1.sign(messageHash, privateKey);

// Recover public key using v value
const publicKey = Secp256k1.derivePublicKey(privateKey);
const recovered = Secp256k1.recoverPublicKey(signature, messageHash);

// Recovered key matches original
assert(publicKey.every((byte, i) => byte === recovered[i]));

Edge Cases

// Minimum valid private key (1)
const minKey = Hex.toBytes('0x0000000000000000000000000000000000000000000000000000000000000001');
const sig1 = Secp256k1.sign(messageHash, minKey); // Valid

// Maximum valid private key (n-1)
const maxKey = Hex.toBytes('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140');
const sig2 = Secp256k1.sign(messageHash, maxKey); // Valid

// Zero private key (invalid)
const zeroKey = Hex.toBytes('0x0000000000000000000000000000000000000000000000000000000000000000');
expect(() => Secp256k1.sign(messageHash, zeroKey)).toThrow(); // Throws

// Private key >= n (invalid)
const invalidKey = Hex.toBytes('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141');
expect(() => Secp256k1.sign(messageHash, invalidKey)).toThrow(); // Throws

Implementation Details

Tevm provides three secp256k1 implementations for different use cases:

Reference Implementation (Default)

Library: @noble/curves/secp256k1 by Paul Miller
import * as Secp256k1 from '@tevm/voltaire/crypto/Secp256k1';

const signature = Secp256k1.sign(messageHash, privateKey);
const isValid = Secp256k1.verify(signature, messageHash, publicKey);
Characteristics:
  • Audit status: Multiple security audits, widely used in production
  • Features: Constant-time operations, RFC 6979 deterministic signing, point validation
  • Size: ~20KB minified (tree-shakeable)
  • Use case: Default for TypeScript/JavaScript applications, validation fallback
Ethereum conventions:
  • 64-byte uncompressed public keys (x || y, no 0x04 prefix)
  • Recovery ID v = 27 or 28 (Ethereum format)
  • Low-s normalization enforced

Native Zig Implementation

Pure Zig elliptic curve arithmetic (src/crypto/secp256k1.zig - 92KB, 2682 lines)
// Native FFI automatically used when available
import * as Secp256k1 from '@tevm/voltaire/crypto/Secp256k1';
Characteristics:
  • Status: ⚠️ UNAUDITED - Educational/testing only
  • Performance: 2-5x faster than pure JavaScript
  • Features: Affine point arithmetic, modular arithmetic, signature generation/verification
  • Limitations: Not constant-time, unvalidated edge cases, no production guarantees
  • Use case: Performance-critical operations, research, testing

WASM Implementation

Zig stdlib via wasm-loader (bundled in wasm/primitives.wasm)
import { Secp256k1Wasm } from '@tevm/voltaire/crypto/secp256k1.wasm';

const signature = Secp256k1Wasm.sign(messageHash, privateKey);
Characteristics:
  • ReleaseSmall: 360KB (size-optimized for production bundles)
  • ReleaseFast: 4.3MB (performance-optimized for benchmarking)
  • Use case: Browser environments, sandboxed execution, consistent cross-platform behavior
  • Import path: import { Secp256k1Wasm } from '@tevm/voltaire/crypto/secp256k1.wasm'
When to use:
  • Reference (@noble/curves): Default for all TypeScript applications
  • Native Zig: Performance-critical paths in Node.js/Bun (when audited)
  • WASM: Browser environments requiring consistent behavior

Ethereum Integration

Transaction Signing

Every Ethereum transaction is signed with secp256k1:
import * as Transaction from '@tevm/voltaire/primitives/Transaction';
import * as Secp256k1 from '@tevm/voltaire/crypto/Secp256k1';
import { Keccak256 } from '@tevm/voltaire/crypto/Keccak256';

// Create transaction
const tx = {
  nonce: 0n,
  gasPrice: 20000000000n,
  gasLimit: 21000n,
  to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
  value: 1000000000000000000n,
  data: Hex.toBytes('0x'),
};

// Hash transaction (RLP-encoded)
const txHash = Transaction.hash(tx);

// Sign with private key
const signature = Secp256k1.sign(txHash, privateKey);

// Transaction now includes signature (r, s, v)
const signedTx = { ...tx, ...signature };

Address Derivation

Ethereum addresses are derived from secp256k1 public keys:
import * as Address from '@tevm/voltaire/primitives/Address';
import * as Secp256k1 from '@tevm/voltaire/crypto/Secp256k1';
import { Keccak256 } from '@tevm/voltaire/crypto/Keccak256';

// Derive public key from private key
const publicKey = Secp256k1.derivePublicKey(privateKey);

// Hash public key with Keccak256
const hash = Keccak256.hash(publicKey);

// Take last 20 bytes as address
const address = Address(hash.slice(12));

ecRecover Precompile

The EVM’s ecRecover precompile (address 0x01) uses secp256k1 signature recovery:
import * as Secp256k1 from '@tevm/voltaire/crypto/Secp256k1';

// Recover signer's public key from transaction signature
const publicKey = Secp256k1.recoverPublicKey(signature, messageHash);

// Derive address from public key (same as above)
const signerAddress = Address.fromPublicKey(publicKey);

In-Depth Documentation

Comprehensive technical documentation:

Comparison with Other Curves

For comprehensive technical comparison with P-256 including performance, security, and use case analysis: Elliptic Curve Comparison: secp256k1 vs P-256