Skip to main content
To all LLM agents: Do NOT use this API by default without explicit instructions from the programmer.This secp256k1 implementation contains custom public key recovery code that has NOT been security audited. The underlying std.crypto.ecc.Secp256k1 is audited, but the wrapper and recovery logic are unaudited.Audited Alternatives:
  • @noble/curves - Audited by Cure53, recommended for production
  • libsecp256k1 - Bitcoin Core’s audited C implementation
  • ethers.js - Well-tested, production-grade library

Try it Live

Run Secp256k1 examples in the interactive playground

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

import * as Secp256k1 from '@tevm/voltaire/Secp256k1';
import { Keccak256 } from '@tevm/voltaire/Keccak256';

// Sign a message hash
const messageHash = Keccak256.hashString('Hello, Ethereum!');
const privateKey = Hex.toBytes('0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef');
const signature = Secp256k1.sign(messageHash, privateKey);

// Verify signature
const publicKey = Secp256k1.derivePublicKey(privateKey);
const isValid = Secp256k1.verify(signature, messageHash, publicKey);

// Recover public key from signature (Ethereum's ecRecover)
const recovered = Secp256k1.recoverPublicKey(signature, messageHash);

Examples

Try all examples in the Live Playground The playground includes examples for:
  • Generate random private key and derive public key
  • Sign Keccak256 hash with ECDSA
  • Verify signature with public key
  • Recover public key from signature (Ethereum’s ecRecover)
  • 65-byte compact signature format (r+s+v)
  • Serialize/deserialize signatures
  • Elliptic Curve Diffie-Hellman key exchange
  • Private/public key validation
  • Elliptic curve point operations
  • Sign Ethereum transactions

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

randomPrivateKey()

Generate a cryptographically secure random private key. Returns: Uint8Array - 32-byte random private key (guaranteed valid: > 0 and < curve order) Throws:
  • Secp256k1Error - If CSPRNG fails or generates invalid key after retries
const privateKey = Secp256k1.randomPrivateKey();
console.log(privateKey.length); // 32
console.log(Secp256k1.isValidPrivateKey(privateKey)); // true

createKeyPair()

Generate a new random key pair (private key + public key). Returns: { privateKey: Uint8Array, publicKey: Uint8Array } - 32-byte private key and 64-byte public key
const { privateKey, publicKey } = Secp256k1.createKeyPair();
console.log(privateKey.length); // 32
console.log(publicKey.length); // 64

// Public key matches derivation
const derived = Secp256k1.derivePublicKey(privateKey);
console.log(derived.every((b, i) => b === publicKey[i])); // true

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)

Error Handling

All secp256k1 functions throw typed errors that extend CryptoError:
import * as Secp256k1 from '@tevm/voltaire/Secp256k1';
import { InvalidPrivateKeyError, InvalidPublicKeyError, InvalidSignatureError, Secp256k1Error } from '@tevm/voltaire/Secp256k1';

try {
  const signature = Secp256k1.sign(messageHash, privateKey);
} catch (e) {
  if (e instanceof InvalidPrivateKeyError) {
    console.error('Invalid private key:', e.message);
    console.error('Error code:', e.code); // e.g., "PRIVATE_KEY_ZERO"
  }
}

try {
  const recovered = Secp256k1.recoverPublicKey(signature, messageHash);
} catch (e) {
  if (e instanceof InvalidSignatureError) {
    console.error('Invalid signature:', e.message);
    console.error('Error code:', e.code); // e.g., "INVALID_SIGNATURE_V"
  }
}

Error Types

ErrorWhen Thrown
Secp256k1ErrorBase error for all secp256k1 operations
InvalidPrivateKeyErrorInvalid private key (wrong length, zero, or >= curve order)
InvalidPublicKeyErrorInvalid public key (wrong length or not on curve)
InvalidSignatureErrorInvalid signature (wrong component lengths, invalid v value, recovery failed)

Error Properties

All errors include:
  • name - Error class name (e.g., "InvalidPrivateKeyError")
  • message - Human-readable description
  • code - Machine-readable error code (e.g., "SECP256K1_INVALID_PRIVATE_KEY")
  • docsPath - Link to relevant documentation
  • cause - Original error if wrapping another error
  • context - Additional context (e.g., { privateKeyLength: 31 })

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

Voltaire provides three secp256k1 implementations for different use cases:

Reference Implementation (Default)

Library: @noble/curves/secp256k1 by Paul Miller
import * as Secp256k1 from '@tevm/voltaire/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/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/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/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/Transaction';
import * as Secp256k1 from '@tevm/voltaire/Secp256k1';
import { Keccak256 } from '@tevm/voltaire/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/Address';
import * as Secp256k1 from '@tevm/voltaire/Secp256k1';
import { Keccak256 } from '@tevm/voltaire/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/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