Skip to main content

Try it Live

Run Signature examples in the interactive playground

Signature

Unified cryptographic signature primitive supporting multiple algorithms (secp256k1, P-256, Ed25519).
New to ECDSA signatures? Start with Fundamentals to learn about secp256k1, signature components (r, s, v), signing/verification, malleability prevention, and common use cases.

Overview

Signature provides algorithm-agnostic signature handling with automatic metadata tracking. Supports ECDSA (secp256k1, P-256) and EdDSA (Ed25519) with format conversions (compact, DER). Key Features:
  • Multi-algorithm support (secp256k1, P-256, Ed25519)
  • Format conversions (compact, DER, RSV)
  • Signature validation and canonicalization
  • Public key recovery (secp256k1 only)
  • Zero-copy operations where possible
  • WASM acceleration available

Documentation

Core Operations

  • constructors - Create signatures from components, DER, compact formats, or algorithm-specific constructors
  • conversions - Convert between signature formats: compact, DER, RSV, and raw bytes
  • validation - Validate signatures, check canonicality, and prevent malleability attacks
  • recovery - Recover public keys and addresses from secp256k1 signatures using recovery ID
  • utilities - Extract components, compare signatures, and type guards

Formats

  • formats - Compare signature formats: compact (64B), DER (variable), RSV, and EIP-2098
  • eip-2098 - Compact 64-byte format with embedded recovery ID for gas savings

Advanced

  • usage-patterns - Real-world examples: Ethereum transactions, EIP-712, personal_sign
  • wasm - WASM-accelerated signature operations for improved performance
  • branded-signature - Type definition and metadata structure

Signature Format Comparison

FormatSizeRecoveryUse Case
Compact64 bytes❌ NoStandard ECDSA storage
Compact+V65 bytes✅ YesEthereum transactions
DER~70-72 bytes❌ NoBitcoin, X.509 certificates
EIP-209864 bytes✅ Yes (embedded)Gas-optimized Ethereum
See Signature Formats for detailed comparison.

Quick Reference

Quick Start

import { Signature, Hex } from 'tevm';

// Create from transaction components
const sig = Signature.fromSecp256k1(
  Hex.toBytes('0x1234...'), // r (32 bytes)
  Hex.toBytes('0x5678...'), // s (32 bytes)
  27                         // v (recovery ID)
);

// Or use universal constructor
const sig2 = Signature({
  r: rBytes,
  s: sBytes,
  v: 27,
  algorithm: 'secp256k1'
});

// Ensure canonical form (prevent malleability)
const canonical = Signature.normalize(sig);
console.log(Signature.isCanonical(canonical)); // true

// Convert between formats
const compact = Signature.toCompact(sig);    // 65 bytes (r + s + v)
const der = Signature.toDER(sig);            // ~70 bytes (ASN.1 encoded)

// Extract components
const r = Signature.getR(sig);  // 32 bytes
const s = Signature.getS(sig);  // 32 bytes
const v = Signature.getV(sig);  // 27 or 28

Effect Schema

import { SignatureSchema } from '@tevm/voltaire/Signature/effect'

// From compact bytes (64)
const bytes = new Uint8Array(64)
const sig = SignatureSchema.fromBytes(bytes)
sig.toHex()

Security Considerations

Signature Malleability

ECDSA signatures are malleable: both (r, s) and (r, -s mod n) are valid. Always normalize to prevent attacks:
// Wrong: Accept any signature
const address = Secp256k1.recoverAddress(sig, messageHash);

// Right: Normalize first
const canonical = Signature.normalize(sig);
const address = Secp256k1.recoverAddress(canonical, messageHash);
Standards:
  • Bitcoin BIP-62: Requires canonical low-s signatures
  • Ethereum: Enforces low-s in consensus rules
  • Best Practice: Always normalize before verification or storage

Recovery ID Validation

const v = Signature.getV(sig);

// Validate recovery ID range
if (v !== undefined && v !== 27 && v !== 28) {
  // Handle EIP-155 chain-specific v values
  const chainId = Math.floor((v - 35) / 2);
  const yParity = (v - 35) % 2;
  const standardV = 27 + yParity;
  // Create new signature with standard v...
}

Replay Attack Prevention

Ethereum uses EIP-155 to prevent cross-chain replay attacks:
// EIP-155 v encoding: v = chainId * 2 + 35 + yParity
const chainId = 1; // Mainnet
const yParity = v === 27 ? 0 : 1;
const eip155V = chainId * 2 + 35 + yParity; // 37 or 38

API Reference

Constructors

See Constructors for detailed documentation.
  • Signature.from(value) - Universal constructor
  • Signature.fromSecp256k1(r, s, v?) - secp256k1 ECDSA
  • Signature.fromP256(r, s) - P-256 ECDSA
  • Signature.fromEd25519(signature) - Ed25519
  • Signature.fromCompact(bytes, algorithm) - Compact format
  • Signature.fromDER(der, algorithm, v?) - DER encoding

Conversions

See Conversions for detailed documentation.
  • Signature.toBytes(signature) - Raw bytes (strips metadata)
  • Signature.toCompact(signature) - Compact format (64 or 65 bytes)
  • Signature.toDER(signature) - DER encoding (~70 bytes)

Validation

See Validation for detailed documentation.
  • Signature.isCanonical(signature) - Check if s ≤ n/2
  • Signature.normalize(signature) - Convert to canonical form

Component Extraction

See Utilities for detailed documentation.
  • Signature.getAlgorithm(signature) - Get algorithm
  • Signature.getR(signature) - Extract r component (32 bytes)
  • Signature.getS(signature) - Extract s component (32 bytes)
  • Signature.getV(signature) - Get recovery ID

Comparison

  • Signature.equals(a, b) - Compare signatures
  • Signature.is(value) - Type guard

Types

type SignatureAlgorithm = "secp256k1" | "p256" | "ed25519";

type BrandedSignature = Uint8Array & {
  readonly __tag: "Signature";
  readonly algorithm: SignatureAlgorithm;
  readonly v?: number; // Recovery ID for secp256k1 (27 or 28)
};

Constants

ECDSA_SIZE = 64;           // r + s
ECDSA_WITH_V_SIZE = 65;    // r + s + v
ED25519_SIZE = 64;         // Ed25519 signature
COMPONENT_SIZE = 32;       // r or s component
RECOVERY_ID_MIN = 27;      // Ethereum v value
RECOVERY_ID_MAX = 28;      // Ethereum v value

Errors

  • SignatureError - Base error class
  • InvalidSignatureLengthError - Invalid byte length
  • InvalidSignatureFormatError - Unsupported format
  • InvalidAlgorithmError - Invalid or unsupported algorithm
  • NonCanonicalSignatureError - Signature not canonical
  • InvalidDERError - DER encoding/decoding error

Structure

ECDSA (secp256k1, P-256)

Bytes: [r (32 bytes)][s (32 bytes)]
Metadata: algorithm, v (optional for secp256k1)

Ed25519

Bytes: [signature (64 bytes)]
Metadata: algorithm = 'ed25519'

Recovery ID (v)

  • 27: First recovery attempt (Ethereum standard)
  • 28: Second recovery attempt
  • Only applies to secp256k1
  • Not stored in signature bytes (metadata only)

Quick Start

import { Signature, Hex } from 'tevm';

// Create secp256k1 signature
const sig = Signature.fromSecp256k1(rBytes, sBytes, 27);

// Universal constructor
const sig2 = Signature({ r: rBytes, s: sBytes, v: 27 });

// Check algorithm
console.log(Signature.getAlgorithm(sig)); // "secp256k1"

// Normalize to canonical form
const canonical = Signature.normalize(sig);
console.log(Signature.isCanonical(canonical)); // true

// Convert to DER
const der = Signature.toDER(sig);

// Parse from DER
const parsed = Signature.fromDER(der, 'secp256k1', 27);

// Compare signatures
console.log(Signature.equals(sig, parsed)); // true

Complete Example

import { Signature, Address, Hash, Hex } from 'tevm';
import { Secp256k1, keccak256 } from 'tevm/crypto';

// 1. Parse Ethereum transaction signature
const tx = {
  r: '0x1234...',
  s: '0x5678...',
  v: 27
};

const sig = Signature.fromSecp256k1(
  Hex.toBytes(tx.r),
  Hex.toBytes(tx.s),
  tx.v
);

// 2. Validate and normalize (prevent malleability)
if (!Signature.isCanonical(sig)) {
  console.log('Non-canonical signature detected, normalizing...');
  sig = Signature.normalize(sig);
}

// 3. Verify signature structure
console.log('Algorithm:', Signature.getAlgorithm(sig)); // "secp256k1"
console.log('Recovery ID:', Signature.getV(sig));        // 27 or 28
console.log('Canonical:', Signature.isCanonical(sig));   // true

// 4. Recover signer address
const txHash = Hash(keccak256(encodedTxData));
const signer = Secp256k1.recoverAddress(sig, txHash);
console.log('Signer:', Address.toString(signer)); // "0x..."

// 5. Convert to different formats
const compact = Signature.toCompact(sig);  // 65 bytes (r + s + v)
const der = Signature.toDER(sig);          // ~70 bytes (DER encoded)

// 6. Round-trip verification
const parsed = Signature.fromDER(der, 'secp256k1', tx.v);
console.log('Equal:', Signature.equals(sig, parsed)); // true

External References