SSZ

SSZ (Simple Serialize) was coined by Vitalik Buterin in 2018 as a simpler replacement for RLP in the Beacon Chain proposal.

Justin Drake, Ben Edgington, and the research team iterated in the early eth2.0-specs repo; Drake’s PR #71 is the first time
clients saw the SimpleSerialize name in code. The work later migrated into the current consensus-specs/ssz folder and has stayed there ever since.

The official spec shows how every basic type is defined. The excerpt below is taken straight from the GitHub file that client teams track:

ssz/simple-serialize.md
assert N in [8, 16, 32, 64, 128, 256]
return value.to_bytes(N // BITS_PER_BYTE, "little")
 
assert value in (True, False)
return b"\x01" if value is True else b"\x00"

SSZ vs RLP

While both SSZ and RLP ultimately serialize data into bytes, their approaches and goals differ significantly. SSZ is designed around strict typing and fixed layouts to enable fast hashing and simple proofs, whereas RLP focuses on flexible, self-describing length-prefixed encodings optimized for general-purpose data serialization.

RLP encodes each field with length prefixes, recursively nesting lists and byte arrays.

SSZ encodes fixed-size fields sequentially, followed by 4-byte offsets pointing to variable-size fields in the variable section.

This typed offset scheme enables constant-time access to variable fields and efficient hashing.

Encoding Multiple Bytes

For multi-byte strings, RLP adds a length prefix to indicate the payload size, while SSZ encodes the bytes directly according to its type definition. Adjust the slider below to see how each encoding handles strings of different lengths.

Encoding Multiple Bytes
HEX("abcdefghijklm")=0x6162636465666768696a6b6c6d
PBS("abcdefghijklm")=
0x0d6162636465666768696a6b6c6d
RLP("abcdefghijklm")=
0x8d6162636465666768696a6b6c6d
SSZ("abcdefghijklm")=0x6162636465666768696a6b6c6d
Key Differences
RLP: Adds a length prefix (8d) indicating total payload size. Total: 14 bytes
SSZ: Simply concatenates bytes without prefix. Total: 13 bytes
SSZ saves 1 byte(s) by omitting the length prefix, but requires the type definition (List[bytes, 32]) to know the structure.

Encoding Bytes Sequence

When encoding a sequence of bytes (a list), RLP wraps the entire list with a length prefix, and each element may also have its own prefix. SSZ simply concatenates the elements without any prefixes, relying on the type definition to know the structure. Try entering different values below to compare the encodings side-by-side.

Encoding Bytes Sequence
Input Comma-separated values, Max 6 items. Example: [1, 2, "hello", "world"]
data preview: sequence · 3 items
[01B,'x'1B,'build'5B]
RLP
RLP([0, "x", "build"]) =
0xc80x000x780x850x620x750x690x6c0x64
c8 - list prefix (0xc0 + 8 = length 8 bytes)
00 - element 1 = 0
78 - element 2 = x
856275696c64 - element 3 = build
SSZ (List[bytes, 6])
SSZ([0, "x", "build"]) =
0x000x780x620x750x690x6c0x64
00 - element 1 = 0 (no prefix)
78 - element 2 = "x" (no prefix)
62 75 69 6c 64 - element 3 = "build" (no prefix)
Key Differences
RLP: Adds a length prefix (c8) indicating total payload size. Total: 9 bytes
SSZ: Simply concatenates bytes without prefix. Total: 7 bytes
SSZ saves 2 byte(s) by omitting the length prefix, but requires the type definition (List[bytes, 6]) to know the structure.

Ethereum uses RLP primarily for the execution layer, encoding transactions and state data, where flexibility is key. SSZ is the backbone of the consensus layer, chosen for its deterministic layouts and suitability for light-client proofs.

Layout

Unlike RLP, SSZ does not encode type information in the byte stream. The layout (type definition) is separate from the encoded data and must be known in advance by both encoder and decoder.

Where type definitions are stored:

  • Consensus specifications: The Ethereum consensus specs repository defines all SSZ types used in the Beacon Chain (e.g., BeaconBlock, Attestation, Validator).
  • Client code: Each client implementation (Prysm, Lighthouse, Teku, etc.) includes these type definitions in their codebase.
  • Protocol documentation: The official SSZ spec documents the encoding rules, while consensus specs define the specific types.

Why this design?

  • Efficiency: No overhead from encoding type metadata in every message.
  • Determinism: All clients use identical type definitions, ensuring identical encodings.
  • Verification: Light clients can verify data without knowing the full type, using Merkle proofs that rely on fixed layouts.

When you see List[uint8, 8] or Vector[uint16, 4], these are part of the type definition, not the encoded bytes. The bytes themselves contain only the data, arranged according to the predefined layout.

Primitives

  • Unsigned ints are little-endian and fixed width: uint8 → 1 byte, uint16 → 2 bytes, uint32 → 4 bytes.
  • Booleans still use a byte so hashes stay aligned (0x00 for false, 0x01 for true).
Scalar → bytes
Input 2 bytes, little-endian
Encoded Bytes 2 bytes
0x010x00

Switch the type selector, nudge the value, and notice how the least-significant byte always appears first.

Vectors

Vectors are arrays with the length baked into the type. Because each element has the same size, the encoder simply places them back-to-back. Nothing else—no headers, no lengths.

Vector[uint16, 4]
Input 4 slots, each 16 bits → 2 bytes
Encoded Bytes Total: 8 bytes
0x010x000x020x000x030x000x040x00

Changing any slot only touches its two bytes. The total stays at 8 bytes because 4 * uint16 never changes.

Lists

Lists trade strictness for flexibility. Elements are still written consecutively, but the container that owns the list must keep track of where those bytes live. For a list of basic types we can infer the length from the byte count because one element = one byte.

List[uint8, 8]
Input Comma-separated bytes, Max 8 items. Example: [0, 1, 2]
data preview: sequence · 3 items
[101B,421B,2551B]
Encoded Bytes Total: 3 bytes
0x0a0x2a0xff

Try entering more than eight numbers—the extra entries are ignored because the max length is part of the type.

Containers

A container feels like a struct with named fields. SSZ splits the encoding into two areas:

  1. Fixed section – immediate encodings of every fixed-size field plus a 4-byte offset for each variable-size field.
  2. Variable section – the actual bytes for lists or other variable members, laid out in the same order as declared.
Container offsets
Fixed Fields Fixed-size fields encoded sequentially
Variable Fields Variable-size fields with offsets
data preview: sequence · 3 items
[51B,81B,131B]

List[uint8, 6]

data preview: sequence · 2 items
[10242B,20482B]

List[uint16, 3]

Offsets Pointers to variable section
payload bytes
Offset 13 bytes
Length: 3 bytes
proof words
Offset 16 bytes
Length: 4 bytes
Fixed section 13 bytes
0x010x000x000x030x000x0d0x000x000x000x100x000x000x00
Variable section 7 bytes
0x050x080x0d0x000x040x000x08
Full Encoding Fixed + Variable sections
0x010x000x000x030x000x0d0x000x000x000x100x000x000x000x050x080x0d0x000x040x000x08

The demo container has version, flag, count, and two lists. As you extend a list:

  • The offset stored in the fixed section jumps because it points to the new start index.
  • The variable section grows by exactly the number of bytes you added.
  • The highlighted full encoding shows which bytes belong to fixed data, offsets, or variable payloads.

References