Skip to main content

Try it Live

Run Secp256k1 examples in the interactive playground
This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.

Examples

Secp256k1 Signing

Sign message hashes with secp256k1 using deterministic ECDSA (RFC 6979). Ethereum uses secp256k1 signatures for all transaction authorization and authentication.

Overview

ECDSA (Elliptic Curve Digital Signature Algorithm) signing computes a signature (r, s, v) from:
  • Message hash (32 bytes) - Keccak256 of transaction or message
  • Private key (32 bytes) - Secret scalar (0 < key < curve order)
The signature proves knowledge of the private key without revealing it. RFC 6979 deterministic nonce generation ensures identical signatures for same message+key pairs, preventing catastrophic nonce reuse.

API

sign(messageHash, privateKey)

Sign a 32-byte message hash with deterministic ECDSA. Parameters:
  • messageHash (HashType) - 32-byte hash to sign
  • privateKey (Uint8Array) - 32-byte private key (0 < key < n)
Returns: BrandedSignature
{
  r: Uint8Array,  // 32 bytes
  s: Uint8Array,  // 32 bytes (low-s enforced)
  v: number       // 27 or 28 (Ethereum format)
}
Throws:
  • InvalidPrivateKeyError - Key wrong length, zero, or >= curve order
  • Secp256k1Error - Signing operation failed
Example:
import * as Secp256k1 from '@tevm/voltaire/Secp256k1';
import { Keccak256 } from '@tevm/voltaire/Keccak256';

const privateKey = Bytes32();
crypto.getRandomValues(privateKey);

const message = Keccak256.hashString('Hello, Ethereum!');
const signature = Secp256k1.sign(message, privateKey);

console.log(signature.v); // 27 or 28
console.log(signature.r.length); // 32
console.log(signature.s.length); // 32

Algorithm Details

ECDSA Signature Generation

  1. Hash message: e = hash(message) (typically Keccak256)
  2. Generate nonce (RFC 6979): k = HMAC_DRBG(private_key, message_hash) (deterministic)
  3. Calculate point: R = k * G (scalar multiplication of generator)
  4. Compute r: r = R.x mod n (x-coordinate of R)
  5. Compute s: s = k^-1 * (e + r * private_key) mod n
  6. Normalize s: If s > n/2, set s = n - s (low-s malleability fix)
  7. Calculate v: Recovery ID (0 or 1) + 27 for Ethereum compatibility

RFC 6979 Deterministic Nonces

Why deterministic? Random nonce generation is dangerous:
  • Nonce reuse with different messages leaks the private key
  • Weak randomness (bad RNG) enables key recovery attacks
  • Implementation bugs in random generation are common
RFC 6979 derives nonces deterministically:
k = HMAC_DRBG(key: private_key, data: message_hash, hash: SHA-256)
Benefits:
  • No RNG required - Eliminates entropy source vulnerabilities
  • Reproducible - Same message + key = same signature (testable)
  • Secure - Nonce is cryptographically derived from secrets
  • Standard - RFC 6979 widely adopted (Bitcoin, Ethereum, etc.)
Critical: Never implement custom nonce generation. Use RFC 6979.

Signature Components

r (32 bytes)

The x-coordinate of the ephemeral public key R = k * G:
  • Must be in range [1, n-1] where n is curve order
  • Derived from deterministic nonce k
  • Acts as a commitment to the nonce

s (32 bytes)

The proof that binds the signature to the private key:
  • Computed as s = k^-1 * (e + r * private_key) mod n
  • Must be in range [1, n-1]
  • Low-s enforced: If s > n/2, signature uses n - s instead
  • Low-s prevents signature malleability (BIP 62, EIP-2)

v (recovery ID)

Ethereum-specific value for public key recovery:
  • Standard: 0 or 1 (which of two possible y-coordinates)
  • Ethereum: 27 or 28 (v = recovery_id + 27)
  • EIP-155 (replay protection): v = chain_id * 2 + 35 + recovery_id
  • Enables ecRecover precompile to extract public key from signature

Security Considerations

Critical Warnings

⚠️ NEVER reuse nonces: Reusing k with different messages leaks the private key. RFC 6979 prevents this - do not override. ⚠️ Validate private keys: Keys must be 32 bytes and satisfy 0 < key < n. Zero keys and keys >= n are invalid. ⚠️ Use cryptographically secure random: For private key generation, use crypto.getRandomValues() or similar CSPRNG. Never use Math.random(). ⚠️ Protect private keys: Store keys in secure hardware (HSM, Secure Enclave) when possible. Never log or transmit unencrypted keys. ⚠️ Hash before signing: Sign hashes, not raw messages. Ethereum signs Keccak256 hashes of RLP-encoded transactions.

Low-s Malleability

ECDSA signatures have an inherent malleability: if (r, s) is valid, so is (r, n - s). Both verify correctly but produce different signature bytes. Problem: Malleability enables:
  • Transaction replay with modified signature
  • Smart contract exploit via signature verification bypass
  • Blockchain state inconsistency
Solution: Enforce low-s (s ≤ n/2):
if (s > CURVE_ORDER / 2n) {
  s = CURVE_ORDER - s;
  v ^= 1; // Flip recovery ID
}
Ethereum requires low-s (BIP 62, EIP-2). Our implementation automatically normalizes.

Side-Channel Resistance

Timing attacks can leak private keys through:
  • Non-constant-time modular arithmetic - Branch timing leaks bit values
  • Cache timing - Memory access patterns reveal secrets
  • Power analysis - CPU power consumption correlates with operations
Mitigations:
  • TypeScript: Uses @noble/curves (constant-time operations)
  • Zig: ⚠️ NOT constant-time - Custom implementation unaudited
  • Production: Use audited libraries or hardware wallets

Test Vectors

RFC 6979 Deterministic Signatures

// Private key = 1
const privateKey = Bytes32();
privateKey[31] = 1;

// Message hash (SHA-256 of "hello world")
const messageHash = Hash(
  sha256(new TextEncoder().encode("hello world"))
);

// Sign twice
const sig1 = Secp256k1.sign(messageHash, privateKey);
const sig2 = Secp256k1.sign(messageHash, privateKey);

// Deterministic: identical signatures
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);

Different Messages

const privateKey = Bytes32();
privateKey[31] = 1;

const msg1 = Keccak256.hashString("message 1");
const msg2 = Keccak256.hashString("message 2");

const sig1 = Secp256k1.sign(msg1, privateKey);
const sig2 = Secp256k1.sign(msg2, privateKey);

// Different messages = different signatures
assert(!sig1.r.every((byte, i) => byte === sig2.r[i]));

Edge Cases

// Minimum valid private key
const minKey = Bytes32();
minKey[31] = 1;
const sig1 = Secp256k1.sign(messageHash, minKey); // Valid

// Maximum valid private key (n - 1)
const maxKey = new Uint8Array([
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
  0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b,
  0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, 0x41, 0x40,
]);
const sig2 = Secp256k1.sign(messageHash, maxKey); // Valid

// Zero key (invalid)
const zeroKey = Bytes32();
expect(() => Secp256k1.sign(messageHash, zeroKey)).toThrow();

// Key >= n (invalid)
const invalidKey = new Uint8Array([
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
  0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b,
  0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, 0x41, 0x41,
]);
expect(() => Secp256k1.sign(messageHash, invalidKey)).toThrow();

Implementation Notes

TypeScript

Uses @noble/curves/secp256k1:
  • Audit status: Multiple security audits, production-ready
  • RFC 6979: Built-in deterministic nonces
  • Low-s: Automatic normalization
  • Constant-time: Side-channel resistant
  • Size: ~20KB minified (tree-shakeable)

Zig

Custom implementation in src/crypto/secp256k1.zig:
  • ⚠️ UNAUDITED - Not security reviewed
  • ⚠️ NOT constant-time - Vulnerable to timing attacks
  • ⚠️ Educational only - Do not use in production
  • Implements basic ECDSA with RFC 6979
For production Zig/FFI use, wrap TypeScript implementation via WASM.