Skip to main content

Try it Live

Run Signature examples in the interactive playground

BrandedSignature

Branded Uint8Array type for cryptographic signatures with algorithm metadata.

Type Definition

import type { brand } from 'tevm/brand';

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

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

Properties

brand

readonly [brand]: "Signature"
Type brand for runtime type checking using symbol branding. Visibility: Non-enumerable Writable: false Configurable: false

algorithm

readonly algorithm: SignatureAlgorithm
Signature algorithm identifier. Values:
  • "secp256k1" - Bitcoin/Ethereum ECDSA
  • "p256" - NIST P-256 ECDSA
  • "ed25519" - EdDSA on Curve25519
Visibility: Enumerable Writable: false Configurable: false

v

readonly v?: number
Recovery ID for secp256k1 signatures (optional). Values:
  • 27 - First recovery attempt (standard Ethereum)
  • 28 - Second recovery attempt
  • undefined - Not secp256k1 or no recovery ID
Visibility: Enumerable only if defined Writable: false Configurable: false

Byte Structure

ECDSA (secp256k1, p256)

Length: 64 bytes

Layout:
[0-31]   r component (32 bytes)
[32-63]  s component (32 bytes)

Metadata: algorithm, v (optional)

Ed25519

Length: 64 bytes

Layout:
[0-63]  signature (64 bytes)

Metadata: algorithm = 'ed25519'

Metadata Storage

Properties are defined using Object.defineProperties():
import { brand } from 'tevm/brand';

Object.defineProperties(bytes, {
  [brand]: {
    value: "Signature",
    writable: false,
    enumerable: false,
    configurable: false,
  },
  algorithm: {
    value: "secp256k1",
    writable: false,
    enumerable: true,
    configurable: false,
  },
  v: {
    value: 27,
    writable: false,
    enumerable: v !== undefined,
    configurable: false,
  },
});

Type Guards

is

function is(value: unknown): value is BrandedSignature
Runtime type guard.
if (Signature.is(value)) {
  // value is BrandedSignature
  console.log(value.algorithm);
}
Checks:
  • Is Uint8Array
  • Has [brand] === "Signature"
  • Has algorithm property

Algorithm-Specific Checks

function isSecp256k1(sig: BrandedSignature): boolean {
  return sig.algorithm === 'secp256k1';
}

function isP256(sig: BrandedSignature): boolean {
  return sig.algorithm === 'p256';
}

function isEd25519(sig: BrandedSignature): boolean {
  return sig.algorithm === 'ed25519';
}

function isECDSA(sig: BrandedSignature): boolean {
  return sig.algorithm === 'secp256k1' || sig.algorithm === 'p256';
}

Examples

Creating Branded Signatures

// secp256k1 with recovery ID
const sig1 = Signature.fromSecp256k1(r, s, 27);
console.log(sig1.algorithm); // "secp256k1"
console.log(sig1.v); // 27
console.log(sig1[brand]); // "Signature"

// P-256 without recovery ID
const sig2 = Signature.fromP256(r, s);
console.log(sig2.algorithm); // "p256"
console.log(sig2.v); // undefined

// Ed25519
const sig3 = Signature.fromEd25519(sigBytes);
console.log(sig3.algorithm); // "ed25519"
console.log(sig3.length); // 64

Accessing Metadata

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

// Algorithm
switch (sig.algorithm) {
  case 'secp256k1':
    console.log('Bitcoin/Ethereum signature');
    break;
  case 'p256':
    console.log('NIST P-256 signature');
    break;
  case 'ed25519':
    console.log('Ed25519 signature');
    break;
}

// Recovery ID
if (sig.v !== undefined) {
  console.log(`Recovery ID: ${sig.v}`);
}

// Brand check
console.log(sig[brand]); // "Signature"

Preserving Metadata

// Metadata preserved through operations
const sig = Signature.fromSecp256k1(r, s, 27);
const normalized = Signature.normalize(sig);

console.log(normalized.algorithm); // "secp256k1"
console.log(normalized.v); // 28 (flipped if normalized)

// Metadata lost on byte operations
const bytes = sig.slice(); // Plain Uint8Array
console.log(bytes.algorithm); // undefined

Type Safety

function processSignature(sig: BrandedSignature) {
  // Type-safe access to algorithm
  if (sig.algorithm === 'secp256k1') {
    // Can safely access v
    const recoveryId = sig.v;
    console.log(`Recovery ID: ${recoveryId}`);
  }

  // Algorithm determines valid operations
  if (sig.algorithm === 'ed25519') {
    // DER encoding not supported
    // const der = Signature.toDER(sig); // Throws error
  } else {
    // ECDSA supports DER
    const der = Signature.toDER(sig);
  }
}

Comparison with Plain Uint8Array

BrandedSignature

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

// Advantages:
console.log(branded.algorithm); // "secp256k1" (known algorithm)
console.log(branded.v); // 27 (known recovery ID)
console.log(branded.length); // 64 (guaranteed size)

// Type safety:
function verify(sig: BrandedSignature) {
  // Compiler knows sig has algorithm property
}

Plain Uint8Array

const plain = Bytes64();

// Disadvantages:
console.log(plain.algorithm); // undefined (unknown algorithm)
// Need external tracking of algorithm
// Need external tracking of recovery ID
// Need validation of length

// No type safety:
function verify(sig: Uint8Array) {
  // Need to determine algorithm manually
}

Serialization

JSON

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

// Metadata lost in JSON
const json = JSON.stringify(sig);
// {"0":123,"1":45,...} (array of numbers)

// Need custom serialization for metadata
const serialized = JSON.stringify({
  bytes: Array(sig),
  algorithm: sig.algorithm,
  v: sig.v,
});

// Deserialize
const data = JSON.parse(serialized);
const restored = Signature.fromSecp256k1(
  new Uint8Array(data.bytes.slice(0, 32)),
  new Uint8Array(data.bytes.slice(32, 64)),
  data.v
);

Binary

// Raw bytes lose metadata
const sig = Signature.fromSecp256k1(r, s, 27);
const bytes = sig; // Still BrandedSignature

// Slice creates plain Uint8Array
const plain = sig.slice();
console.log(plain.algorithm); // undefined

// Use toBytes to explicitly strip metadata
const stripped = Signature.toBytes(sig);
console.log(stripped.algorithm); // undefined

Performance

Memory Overhead

BrandedSignature has minimal overhead:
  • Base: 64 bytes (signature data)
  • Metadata: 3 property descriptors (~48 bytes)
  • Total: ~112 bytes

Runtime Cost

  • Property access: O(1) (native object properties)
  • Type checking: O(1) (simple property checks)
  • Creation: Minimal overhead vs plain Uint8Array

Optimization

// Efficient: metadata stored in object properties
const sig = Signature.fromSecp256k1(r, s, 27);
console.log(sig.algorithm); // Fast property access

// Inefficient: external metadata map
const metadataMap = new Map();
metadataMap.set(plainBytes, { algorithm: 'secp256k1', v: 27 });

Immutability

All properties are readonly:
const sig = Signature.fromSecp256k1(r, s, 27);

// These throw in strict mode:
sig.algorithm = 'p256'; // Error
sig.v = 28; // Error
sig[brand] = 'Foo'; // Error

// Bytes are mutable (Uint8Array behavior):
sig[0] = 99; // Allowed (mutates signature bytes)

// Use toBytes for immutability guarantee:
const bytes = Signature.toBytes(sig);

Compatibility

Uint8Array Methods

BrandedSignature supports all Uint8Array methods:
const sig = Signature.fromSecp256k1(r, s, 27);

// Array methods work
sig.slice(0, 32); // Get r component (plain Uint8Array)
sig.subarray(32, 64); // Get s component (plain Uint8Array)
sig.forEach(byte => console.log(byte));

// Note: slice/subarray return plain Uint8Array (lose metadata)
const r = sig.slice(0, 32);
console.log(r.algorithm); // undefined

Type Narrowing

function process(value: Uint8Array | BrandedSignature) {
  if (Signature.is(value)) {
    // value is BrandedSignature
    console.log(value.algorithm);
  } else {
    // value is plain Uint8Array
    // Need to determine algorithm externally
  }
}

Design Rationale

Why Branded Types?

  1. Type Safety: Compile-time guarantees about signature format
  2. Self-Describing: Algorithm embedded in data
  3. API Simplicity: No need to pass algorithm separately
  4. Runtime Validation: Type guards enable safe operations

Why Not Classes?

// Branded type (current):
const sig: BrandedSignature = Signature.fromSecp256k1(r, s, 27);
sig instanceof Uint8Array; // true
sig.slice(0, 32); // Works

// Class-based alternative:
class SignatureClass {
  constructor(bytes, algorithm, v) { ... }
}
const sig = new SignatureClass(bytes, 'secp256k1', 27);
sig instanceof Uint8Array; // false
sig.slice(0, 32); // Doesn't work
Branded types maintain Uint8Array compatibility while adding type safety.

See Also