Skip to main content

Documentation Index

Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt

Use this file to discover all available pages before exploring further.

Try it Live

Run Signature examples in the interactive playground

Validation

Signature validation and canonicalization functions.

isCanonical

Check if ECDSA signature has canonical s-value.

Signature

function isCanonical(signature: BrandedSignature): boolean

Parameters

  • signature - BrandedSignature to check

Returns

  • true - If signature is canonical (s ≤ n/2) or Ed25519
  • false - If signature is non-canonical (s > n/2)

Example

const sig = Signature.fromSecp256k1(r, s, 27);

if (Signature.isCanonical(sig)) {
  console.log('Signature is canonical');
} else {
  console.log('Signature needs normalization');
  const canonical = Signature.normalize(sig);
}

// Ed25519 always canonical
const ed25519Sig = Signature.fromEd25519(bytes);
console.log(Signature.isCanonical(ed25519Sig)); // true

Canonicality Rules

ECDSA (secp256k1, P-256)

A signature is canonical if s ≤ n/2, where n is the curve order. secp256k1:
n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
n/2 = 7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
P-256:
n = FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
n/2 = 7FFFFFFF800000007FFFFFFFFFFFFFFFDE737D56D38BCF4279DCE5617E3192A8

Why Canonicality Matters

ECDSA signatures have inherent malleability: both (r, s) and (r, -s mod n) are valid for the same message and public key. Security Issues:
  • Transaction malleability attacks
  • Signature duplication
  • Replay attacks
Standards:
  • Bitcoin BIP-62: Require canonical form
  • Ethereum: Enforce low s-value
  • Most modern protocols: Mandate canonicality

Example: Non-Canonical Signature

// High s-value (non-canonical)
const sHigh = new Uint8Array([
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
  0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b,
  0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, 0x41, 0x40,
]); // s > n/2

const sig = Signature.fromSecp256k1(r, sHigh, 27);
console.log(Signature.isCanonical(sig)); // false

// Normalized (canonical)
const canonical = Signature.normalize(sig);
console.log(Signature.isCanonical(canonical)); // true

normalize

Normalize ECDSA signature to canonical form.

Signature

function normalize(signature: BrandedSignature): BrandedSignature

Parameters

  • signature - BrandedSignature to normalize

Returns

BrandedSignature with canonical s-value (s ≤ n/2) If already canonical: Returns input unchanged If Ed25519: Returns input unchanged (always canonical) If non-canonical: Returns new signature with s = n - s and flipped v

Example

const sig = Signature.fromSecp256k1(r, sHigh, 27);

// Normalize to canonical form
const canonical = Signature.normalize(sig);

console.log(Signature.isCanonical(sig)); // false
console.log(Signature.isCanonical(canonical)); // true

// v flipped when normalizing
console.log(Signature.getV(sig)); // 27
console.log(Signature.getV(canonical)); // 28

// r unchanged
const rOrig = Signature.getR(sig);
const rNorm = Signature.getR(canonical);
console.log(rOrig.every((b, i) => b === rNorm[i])); // true

// s normalized
const sOrig = Signature.getS(sig);
const sNorm = Signature.getS(canonical);
console.log(sOrig.every((b, i) => b === sNorm[i])); // false

Normalization Process

For non-canonical signature (s > n/2):
  1. Calculate s_normalized = n - s
  2. Flip recovery ID: v_new = (v === 27) ? 28 : 27
  3. Return new signature with (r, s_normalized, v_new)
// Before normalization
const sig = Signature.fromSecp256k1(r, sHigh, 27);
// r: [original r]
// s: [high s-value]
// v: 27

// After normalization
const canonical = Signature.normalize(sig);
// r: [original r] (unchanged)
// s: [n - s] (normalized)
// v: 28 (flipped)

Algorithm Details

// Pseudocode for normalization
function normalize(sig: BrandedSignature): BrandedSignature {
  // Ed25519 always canonical
  if (sig.algorithm === 'ed25519') {
    return sig;
  }

  // Already canonical
  if (isCanonical(sig)) {
    return sig;
  }

  // Extract components
  const r = getR(sig);
  const s = getS(sig);

  // Get curve order
  const n = getCurveOrder(sig.algorithm);

  // Calculate s_normalized = n - s (modular subtraction)
  const sNormalized = modularSubtract(n, s);

  // Flip v if present
  const v = sig.v !== undefined
    ? (sig.v === 27 ? 28 : 27)
    : undefined;

  // Return normalized signature
  return fromSecp256k1(r, sNormalized, v);
}

Recovery ID Flip

When normalizing, the recovery ID must flip:
// Original signature with high s
const sig1 = Signature.fromSecp256k1(r, sHigh, 27);

// Normalization flips v
const sig2 = Signature.normalize(sig1);
console.log(Signature.getV(sig2)); // 28

// Double normalization returns to original v
const sig3 = Signature.normalize(sig2);
console.log(Signature.getV(sig3)); // 27

// But s is different (both canonical)
const s2 = Signature.getS(sig2);
const s3 = Signature.getS(sig3);
console.log(s2.every((b, i) => b === s3[i])); // false
Reason: Negating s changes which of the two possible public keys is correct, so recovery ID must flip.

Use Cases

Ethereum Transaction Signing

// Sign transaction
const txHash = keccak256(encodedTx);
const sig = secp256k1.sign(txHash, privateKey);

// Ensure canonical before including in transaction
const canonical = Signature.normalize(sig);

// Only canonical signatures accepted by network
const tx = {
  ...txData,
  r: Signature.getR(canonical),
  s: Signature.getS(canonical),
  v: Signature.getV(canonical),
};

Bitcoin Transaction Validation

// Parse DER signature from transaction
const sig = Signature.fromDER(derBytes, 'secp256k1');

// Bitcoin requires canonical signatures (BIP-62)
if (!Signature.isCanonical(sig)) {
  throw new Error('Non-canonical signature rejected');
}

// Verify signature
const valid = verifySig(sig, txHash, publicKey);

Signature Verification

// Accept both canonical and non-canonical
function verifyFlexible(
  sig: BrandedSignature,
  message: Uint8Array,
  publicKey: Uint8Array
): boolean {
  // Normalize before verification
  const canonical = Signature.normalize(sig);
  return verify(canonical, message, publicKey);
}

// Strict verification (reject non-canonical)
function verifyStrict(
  sig: BrandedSignature,
  message: Uint8Array,
  publicKey: Uint8Array
): boolean {
  if (!Signature.isCanonical(sig)) {
    throw new Error('Non-canonical signature rejected');
  }
  return verify(sig, message, publicKey);
}

API Validation

// Validate signature before storing
function storeSignature(sig: BrandedSignature): void {
  // Normalize to canonical form
  const canonical = Signature.normalize(sig);

  // Store canonical version
  db.signatures.insert({
    r: Signature.getR(canonical),
    s: Signature.getS(canonical),
    v: Signature.getV(canonical),
  });
}

// Retrieve and verify is canonical
function getSignature(id: string): BrandedSignature {
  const data = db.signatures.findById(id);
  const sig = Signature.fromSecp256k1(data.r, data.s, data.v);

  // Assert stored signatures are canonical
  if (!Signature.isCanonical(sig)) {
    throw new Error('Database corruption: non-canonical signature');
  }

  return sig;
}

Validation Patterns

Pre-Normalization Check

// Check before normalizing
if (!Signature.isCanonical(sig)) {
  console.log('Signature needs normalization');
  sig = Signature.normalize(sig);
}

// Now guaranteed canonical
console.assert(Signature.isCanonical(sig));

Enforce Canonical

function ensureCanonical(sig: BrandedSignature): BrandedSignature {
  if (!Signature.isCanonical(sig)) {
    return Signature.normalize(sig);
  }
  return sig;
}

// Always returns canonical signature
const canonical = ensureCanonical(anySig);

Reject Non-Canonical

function requireCanonical(sig: BrandedSignature): void {
  if (!Signature.isCanonical(sig)) {
    throw new NonCanonicalSignatureError(
      'Signature must have low s-value'
    );
  }
}

// Throws if not canonical
requireCanonical(sig);

Security Considerations

Transaction Malleability

// Problem: Both signatures valid for same transaction
const sig1 = Signature.fromSecp256k1(r, s, 27);
const sig2 = Signature.fromSecp256k1(r, sNeg, 28); // s_neg = n - s

// Both verify correctly
verify(sig1, txHash, pubKey); // true
verify(sig2, txHash, pubKey); // true

// But produce different transaction hashes
const tx1Hash = hash(encodeTx(sig1));
const tx2Hash = hash(encodeTx(sig2));
console.log(tx1Hash !== tx2Hash); // true

// Solution: Enforce canonical signatures
const canonical = Signature.normalize(sig1);
// Only canonical signatures allowed

Signature Uniqueness

// Without canonicalization: multiple valid signatures
function getAllValidSignatures(r, s, v): BrandedSignature[] {
  const n = CURVE_ORDER;
  const sNeg = modSubtract(n, s);
  const vFlipped = v === 27 ? 28 : 27;

  return [
    Signature.fromSecp256k1(r, s, v),
    Signature.fromSecp256k1(r, sNeg, vFlipped),
  ];
}

// With canonicalization: unique signature
function getCanonicalSignature(r, s, v): BrandedSignature {
  const sig = Signature.fromSecp256k1(r, s, v);
  return Signature.normalize(sig);
}

Replay Attack Prevention

// Store signature hash to prevent replay
const canonical = Signature.normalize(sig);
const sigHash = keccak256(Signature.toBytes(canonical));

// Check if signature already used
if (await db.usedSignatures.exists(sigHash)) {
  throw new Error('Signature already used');
}

// Mark as used
await db.usedSignatures.insert(sigHash);

Performance

Canonicality Check

// Fast check: O(n) byte comparison
const isCanonical = Signature.isCanonical(sig);

// Typical performance: < 0.001ms for 32-byte comparison

Normalization

// O(n) operation: modular subtraction
const canonical = Signature.normalize(sig);

// If already canonical: O(1) (returns input)
// If needs normalization: O(n) (creates new signature)

// Typical performance: < 0.01ms

Optimization

// Avoid unnecessary normalization
if (Signature.isCanonical(sig)) {
  // Use sig directly
  return sig;
} else {
  // Normalize only if needed
  return Signature.normalize(sig);
}

// Or use normalize (checks internally)
return Signature.normalize(sig); // No-op if canonical

Testing

describe('Signature Validation', () => {
  it('detects canonical signatures', () => {
    const canonical = Signature.fromSecp256k1(r, sLow, 27);
    expect(Signature.isCanonical(canonical)).toBe(true);
  });

  it('detects non-canonical signatures', () => {
    const nonCanonical = Signature.fromSecp256k1(r, sHigh, 27);
    expect(Signature.isCanonical(nonCanonical)).toBe(false);
  });

  it('normalizes non-canonical signatures', () => {
    const sig = Signature.fromSecp256k1(r, sHigh, 27);
    const canonical = Signature.normalize(sig);

    expect(Signature.isCanonical(canonical)).toBe(true);
    expect(Signature.getV(canonical)).toBe(28); // v flipped
  });

  it('preserves canonical signatures', () => {
    const sig = Signature.fromSecp256k1(r, sLow, 27);
    const result = Signature.normalize(sig);

    expect(result).toBe(sig); // Same instance
    expect(Signature.getV(result)).toBe(27); // v unchanged
  });

  it('handles Ed25519 correctly', () => {
    const sig = Signature.fromEd25519(bytes);
    expect(Signature.isCanonical(sig)).toBe(true);
    expect(Signature.normalize(sig)).toBe(sig);
  });
});

See Also