Skip to main content

Overview

SSZ (Simple Serialize) is the serialization format used by Ethereum’s consensus layer (Beacon Chain). It provides efficient, deterministic encoding with Merkle tree support for proofs.

Features

  • Basic type encoding (uint8, uint16, uint32, uint64, uint256, bool)
  • Variable-length types (vectors, lists, bitvectors, bitlists)
  • Container (struct) encoding
  • Hash tree root computation for Merkle proofs
  • Full Zig implementation with TypeScript bindings

Basic Types

Encoding

import { Ssz } from '@voltaire/primitives';

// Encode unsigned integers
const uint8 = Ssz.encodeBasic(42, 'uint8');        // [42]
const uint16 = Ssz.encodeBasic(0x1234, 'uint16');  // [0x34, 0x12] (little-endian)
const uint32 = Ssz.encodeBasic(0x12345678, 'uint32');
const uint64 = Ssz.encodeBasic(0x123n, 'uint64');
const uint256 = Ssz.encodeBasic(0xDEADBEEFn, 'uint256');

// Encode boolean
const bool = Ssz.encodeBasic(true, 'bool');        // [1]

Decoding

// Decode unsigned integers
const value8 = Ssz.decodeBasic(bytes, 'uint8');    // number
const value16 = Ssz.decodeBasic(bytes, 'uint16');  // number
const value32 = Ssz.decodeBasic(bytes, 'uint32');  // number
const value64 = Ssz.decodeBasic(bytes, 'uint64');  // bigint
const value256 = Ssz.decodeBasic(bytes, 'uint256'); // bigint

// Decode boolean
const flag = Ssz.decodeBasic(bytes, 'bool');       // boolean

Merkleization

Hash Tree Root

Compute Merkle tree root for proofs:
// Hash tree root of data
const data = new Uint8Array([1, 2, 3, 4, 5]);
const root = await Ssz.hashTreeRoot(data);
// Returns: 32-byte hash

// Empty data returns zero hash
const zeroRoot = await Ssz.hashTreeRoot(new Uint8Array(0));

Properties

  • Deterministic: Same input always produces same root
  • 32-byte output: Compatible with Merkle proofs
  • SHA256-based: Uses standard cryptographic hash

Examples

Validator Deposit

// Encode deposit data
const pubkey = Ssz.encodeBasic(validatorPubkey, 'uint256');
const withdrawalCredentials = Ssz.encodeBasic(credentials, 'uint256');
const amount = Ssz.encodeBasic(32000000000n, 'uint64'); // 32 ETH in gwei
const signature = Ssz.encodeBasic(sig, 'uint256');

// Compute deposit root
const depositData = new Uint8Array([
  ...pubkey,
  ...withdrawalCredentials,
  ...amount,
  ...signature
]);
const depositRoot = await Ssz.hashTreeRoot(depositData);

Beacon Block Header

// Encode block header fields
const slot = Ssz.encodeBasic(12345n, 'uint64');
const proposerIndex = Ssz.encodeBasic(67890n, 'uint64');
const parentRoot = new Uint8Array(32); // 32-byte hash
const stateRoot = new Uint8Array(32);
const bodyRoot = new Uint8Array(32);

// Combine and hash
const headerData = new Uint8Array([
  ...slot,
  ...proposerIndex,
  ...parentRoot,
  ...stateRoot,
  ...bodyRoot
]);
const headerRoot = await Ssz.hashTreeRoot(headerData);

Sync Committee

// Encode sync committee data
const aggregatePubkey = new Uint8Array(48); // BLS pubkey
const slot = Ssz.encodeBasic(100n, 'uint64');

const data = new Uint8Array([
  ...aggregatePubkey,
  ...slot
]);
const root = await Ssz.hashTreeRoot(data);

Zig API

Basic Types

const primitives = @import("primitives");
const Ssz = primitives.Ssz;

// Encode
const uint8_bytes = Ssz.encodeUint8(42);
const uint16_bytes = Ssz.encodeUint16(0x1234);
const uint32_bytes = Ssz.encodeUint32(0x12345678);
const uint64_bytes = Ssz.encodeUint64(0x123456789ABCDEF0);
const bool_bytes = Ssz.encodeBool(true);

// Decode
const value = try Ssz.decodeUint32(&bytes);
const flag = try Ssz.decodeBool(&bytes);

Variable Types

// Vector (fixed-length)
const items = [_]u8{ 1, 2, 3, 4 };
const encoded = try Ssz.encodeVector(allocator, u8, &items);
defer allocator.free(encoded);

// List (variable-length with max)
const list = try Ssz.encodeList(allocator, u8, &items, 10);
defer allocator.free(list);

// Bitvector
const bits = [_]bool{ true, false, true, true };
const bitvec = try Ssz.encodeBitvector(allocator, &bits);
defer allocator.free(bitvec);

// Bitlist
const bitlist = try Ssz.encodeBitlist(allocator, &bits, 100);
defer allocator.free(bitlist);

Container Encoding

const MyStruct = struct {
    a: u8,
    b: u16,
    c: u32,
};

const value = MyStruct{
    .a = 1,
    .b = 0x0203,
    .c = 0x04050607,
};

const encoded = try Ssz.encodeContainer(allocator, MyStruct, value);
defer allocator.free(encoded);

Hash Tree Root

// Hash tree root of data
const data = [_]u8{ 1, 2, 3, 4, 5 };
const root = try Ssz.hashTreeRoot(allocator, &data);
// root: [32]u8

// Basic type hash tree root (no allocation)
const root_basic = Ssz.hashTreeRootBasic(u64, 12345);

Specification

SSZ follows the Ethereum consensus-specs:

Basic Types

  • Boolean: 1 byte (0 = false, 1 = true)
  • Unsigned integers: Little-endian encoding
    • uint8: 1 byte
    • uint16: 2 bytes
    • uint32: 4 bytes
    • uint64: 8 bytes
    • uint256: 32 bytes

Variable Types

  • Vector: Fixed-length homogeneous collection
  • List: Variable-length with maximum size
  • Bitvector: Fixed-length collection of bits
  • Bitlist: Variable-length collection of bits with sentinel

Container Encoding

  1. Fixed part: All fixed-size fields + offsets for variable fields
  2. Variable part: Concatenated variable-size fields

Merkleization

  1. Chunk data into 32-byte leaves
  2. Pad to next power of 2
  3. Build binary Merkle tree bottom-up using SHA256
  4. Return root hash

Performance

TypeScript

  • Basic types: ~10-50 ns per operation
  • Hash tree root: ~100-500 μs depending on data size
  • Web Crypto API used for SHA256

Zig

  • Basic types: ~5-20 ns per operation
  • Hash tree root: ~50-200 μs with optimized hashing
  • Zero-copy decoding where possible

Limitations

Current implementation:
  • ✅ Basic types (all unsigned integers, bool)
  • ✅ Variable types (vector, list, bitvector, bitlist)
  • ✅ Container encoding (fixed fields only)
  • ✅ Hash tree root computation
  • ⚠️ Variable fields in containers (partial support)
  • ❌ Union types (not yet implemented)
  • ❌ Full consensus-specs test vectors (in progress)

See Also