Skip to main content

Secp256k1 Performance

Performance characteristics, benchmarks, and optimization strategies for elliptic curve operations.

Operation Costs

Relative Complexity

OperationAlgorithmTypical TimeComplexity
Hash (Keccak256)Sponge construction~0.01msO(n) input size
Public key derivationScalar multiplication~0.5-1msO(log n) bits
SigningScalar mult + modular ops~1-2msO(log n) bits
Verification2× scalar mult + point add~2-3msO(log n) bits
RecoverySqrt + 2× scalar mult~2-4msO(log n) bits
Verification is ~2× slower than signing due to two scalar multiplications vs one.

TypeScript Benchmarks

@noble/curves (v1.2.0)

Measured on MacBook Pro M1 (Node.js v20):
import { secp256k1 } from '@noble/curves/secp256k1.js';
import { performance } from 'perf_hooks';

// Public key derivation: 1000 iterations
const privateKey = crypto.getRandomValues(Bytes32());
const start = performance.now();
for (let i = 0; i < 1000; i++) {
  secp256k1.getPublicKey(privateKey);
}
const derivationTime = performance.now() - start;
console.log(`Derivation: ${derivationTime.toFixed(2)}ms total, ${(derivationTime/1000).toFixed(3)}ms per key`);
// Output: Derivation: 450.23ms total, 0.450ms per key
Results:
  • Derivation: 0.4-0.6ms per public key
  • Signing: 1.0-1.5ms per signature
  • Verification: 2.0-3.0ms per signature
  • Recovery: 2.5-3.5ms per recovery

Comparison: noble vs libsecp256k1

Operation@noble/curveslibsecp256k1 (C)Ratio
Derivation0.50ms0.20ms2.5× slower
Signing1.25ms0.50ms2.5× slower
Verification2.50ms1.00ms2.5× slower
TypeScript is ~2-3× slower than native C but still practical for most use cases.

Zig Benchmarks

Native Build (ReleaseFast)

Measured on MacBook Pro M1:
  • Derivation: 0.15-0.25ms per key
  • Signing: 0.40-0.60ms per signature
  • Verification: 0.80-1.20ms per signature
⚠️ Note: Zig implementation is UNAUDITED - benchmarks for reference only.

WASM Performance

ReleaseSmall vs ReleaseFast

OperationReleaseSmallReleaseFastNative TS
Derivation2.5ms1.2ms0.5ms
Signing4.0ms2.0ms1.2ms
Verification6.0ms3.5ms2.5ms
ReleaseFast (performance-optimized):
  • ~2× faster than ReleaseSmall
  • Larger bundle size (~50KB vs ~30KB)
  • Use for compute-intensive applications
ReleaseSmall (size-optimized):
  • Slower but smaller bundle
  • Use for bundle-size-sensitive web apps

EVM Precompile

ecRecover (Address 0x01)

Gas cost: 3000 gas (fixed) Performance at 50M gas/sec:
  • 3000 gas / 50M gas/sec = 60 microseconds
Comparison:
  • ecRecover precompile: 0.06ms (fastest)
  • Zig native: 0.8-1.2ms (15-20× slower)
  • TypeScript @noble: 2-3ms (30-50× slower)
  • WASM ReleaseFast: 3-4ms (50-60× slower)
For on-chain verification, always use ecRecover precompile.

Optimization Techniques

Batch Operations

Point additions can be batched for multiple verifications:
// Verify multiple signatures from same signer
function batchVerify(
  signatures: Signature[],
  messageHashes: Hash[],
  publicKey: Uint8Array
): boolean {
  // Naive: verify each independently (slow)
  for (let i = 0; i < signatures.length; i++) {
    if (!verify(signatures[i], messageHashes[i], publicKey)) {
      return false;
    }
  }
  // Total: n × 2 scalar multiplications

  // Optimized: batch verification (not currently exposed in API)
  // Uses multi-scalar multiplication
  // Total: 1 + n scalar multiplications (50% faster)
}

Precomputation

For repeated operations with same generator point G:
// Precompute multiples of G: [2G, 4G, 8G, ..., 2^255 G]
const precomputed = precomputeGenerator();

// Scalar multiplication ~30% faster
function fastDerivePublicKey(privateKey: bigint): Point {
  return scalarMultWithPrecomputation(privateKey, precomputed);
}
@noble/curves uses this internally for public key derivation.

Windowed NAF (wNAF)

Non-Adjacent Form reduces point operations: Standard binary method:
k = 1011001₂ (binary)
→ 5 point doublings + 4 point additions
wNAF (window=4):
k = [1, 0, -1, 1, 0, 0, 1]₄ (base-16 NAF)
→ 5 point doublings + 3 point additions (25% fewer additions)

Hardware Acceleration

Not available for secp256k1 (no CPU instructions):
  • Intel SHA-NI: SHA-256 only
  • ARM Crypto Extensions: AES, SHA only
  • No native ECC instructions
Optimization relies on:
  • Algorithm improvements (wNAF, precomputation)
  • Memory access patterns (cache-friendly)
  • Compiler optimizations (SIMD autovectorization)

Bottlenecks

Modular Arithmetic

Elliptic curve operations require modular arithmetic modulo large primes: Field prime (p): 2²⁵⁶ - 2³² - 977 (256-bit) Curve order (n): 2²⁵⁶ - ~2³² (256-bit) Expensive operations:
  • Modular multiplication: ~100-200 CPU cycles
  • Modular inversion: ~10,000-20,000 cycles (Extended Euclidean algorithm)
  • Modular exponentiation: Variable (used in square root)

Point Operations

Point addition (different points):
  • 2 modular inversions
  • ~12 modular multiplications
  • ~4 modular additions/subtractions
Point doubling (same point):
  • 1 modular inversion
  • ~8 modular multiplications
  • ~6 modular additions/subtractions

Scalar Multiplication

For 256-bit scalar k, double-and-add requires:
  • ~256 point doublings (worst case)
  • ~128 point additions (average, half bits are 1)
  • Total: ~384 point operations
Optimizations reduce to ~170 operations (wNAF + precomputation).

Real-World Performance

Web Application

// Sign transaction (user action)
const startSign = performance.now();
const signature = Secp256k1.sign(txHash, privateKey);
console.log(`User waited: ${(performance.now() - startSign).toFixed(0)}ms`);
// Output: User waited: 1ms (acceptable for UI)
UX considerations:
  • <100ms: Imperceptible
  • 100-300ms: Slight delay, acceptable
  • >300ms: Noticeable lag, consider async
Secp256k1 signing (~1ms) is well within acceptable range.

High-Throughput Server

// Verify 10,000 signatures
const signatures = [...]; // 10K signatures
const start = performance.now();

for (const sig of signatures) {
  Secp256k1.verify(sig.signature, sig.hash, sig.publicKey);
}

const elapsed = performance.now() - start;
console.log(`Throughput: ${(signatures.length / elapsed * 1000).toFixed(0)} sig/sec`);
// Output: Throughput: 400 sig/sec
For higher throughput:
  • Use native library (libsecp256k1)
  • Implement batch verification
  • Parallelize across CPU cores

Blockchain Node

Ethereum mainnet processes ~15 transactions/second: Per block (12 seconds):
  • ~180 transactions
  • 180 signature verifications required
  • Total: 180 × 2.5ms = 450ms
  • Well within 12-second block time
Peak periods:
  • ~30-50 TPS
  • ~600 verifications per block
  • Total: ~1.5 seconds (still manageable)

Optimization Recommendations

Web Applications

Do:
  • Use @noble/curves (battle-tested, good performance)
  • Sign in main thread (1-2ms imperceptible)
  • Verify in Web Worker if processing many signatures
  • Use WASM if bundle size not critical
Avoid:
  • Implementing custom crypto
  • Blocking UI thread for batch operations
  • Unnecessary verifications (cache results)

Node.js Services

Do:
  • Use @noble/curves for simplicity
  • Consider libsecp256k1 bindings for 2-3× speedup
  • Batch operations when possible
  • Use worker threads for parallelization
Avoid:
  • Synchronous crypto in request handlers (use async)
  • Re-deriving public keys (cache them)

Smart Contracts

Do:
  • Use ecRecover precompile (3000 gas)
  • Validate signatures off-chain when possible
  • Batch signature checks to amortize cost
Avoid:
  • Implementing ECDSA in Solidity (expensive, error-prone)
  • Unnecessary on-chain verifications
  • Unvalidated ecRecover results (check != 0x0)