SSZ

Simple Serialize

SimpleSerialize (SSZ)

Table of contents

Constants

Name Value Description
BYTES_PER_CHUNK 32 Number of bytes per chunk.
BYTES_PER_LENGTH_OFFSET 4 Number of bytes per serialized length offset.
BITS_PER_BYTE 8 Number of bits per byte.

Typing

Basic types

Composite types

Note: Both Vector[boolean, N] and Bitvector[N] are valid, yet distinct due to their different serialization requirements. Similarly, both List[boolean, N] and Bitlist[N] are valid, yet distinct. Generally Bitvector[N]/Bitlist[N] are preferred because of their serialization efficiencies.

Variable-size and fixed-size

We recursively define “variable-size” types to be lists, unions, Bitlist and all types that contain a variable-size type. All other types are said to be “fixed-size”.

Aliases

For convenience we alias:

Default values

Assuming a helper function default(type) which returns the default value for type, we can recursively define the default value for all types.

Type Default Value
uintN 0
boolean False
Container [default(type) for type in container]
Vector[type, N] [default(type)] * N
Bitvector[N] [False] * N
List[type, N] []
Bitlist[N] []
Union[type_0, type_1, ...] default(type_0)

is_zero

An SSZ object is called zeroed (and thus, is_zero(object) returns true) if it is equal to the default value for that type.

Illegal types

Serialization

We recursively define the serialize function which consumes an object value (of the type specified) and returns a bytestring of type bytes.

Note: In the function definitions below (serialize, hash_tree_root, is_variable_size, etc.) objects implicitly carry their type.

uintN

assert N in [8, 16, 32, 64, 128, 256]
return value.to_bytes(N // BITS_PER_BYTE, "little")

boolean

assert value in (True, False)
return b"\x01" if value is True else b"\x00"

null

return b""

Bitvector[N]

array = [0] * ((N + 7) // 8)
for i in range(N):
    array[i // 8] |= value[i] << (i % 8)
return bytes(array)

Bitlist[N]

Note that from the offset coding, the length (in bytes) of the bitlist is known. An additional 1 bit is added to the end, at index e where e is the length of the bitlist (not the limit), so that the length in bits will also be known.

array = [0] * ((len(value) // 8) + 1)
for i in range(len(value)):
    array[i // 8] |= value[i] << (i % 8)
array[len(value) // 8] |= 1 << (len(value) % 8)
return bytes(array)

Vectors, containers, lists, unions

# Recursively serialize
fixed_parts = [serialize(element) if not is_variable_size(element) else None for element in value]
variable_parts = [serialize(element) if is_variable_size(element) else b"" for element in value]

# Compute and check lengths
fixed_lengths = [len(part) if part != None else BYTES_PER_LENGTH_OFFSET for part in fixed_parts]
variable_lengths = [len(part) for part in variable_parts]
assert sum(fixed_lengths + variable_lengths) < 2**(BYTES_PER_LENGTH_OFFSET * BITS_PER_BYTE)

# Interleave offsets of variable-size parts with fixed-size parts
variable_offsets = [serialize(uint32(sum(fixed_lengths + variable_lengths[:i]))) for i in range(len(value))]
fixed_parts = [part if part != None else variable_offsets[i] for i, part in enumerate(fixed_parts)]

# Return the concatenation of the fixed-size parts (offsets interleaved) with the variable-size parts
return b"".join(fixed_parts + variable_parts)

If value is a union type:

Define value as an object that has properties value.value with the contained value, and value.type_index which indexes the type.

serialized_bytes = serialize(value.value)
serialized_type_index = value.type_index.to_bytes(BYTES_PER_LENGTH_OFFSET, "little")
return serialized_type_index + serialized_bytes

Deserialization

Because serialization is an injective function (i.e. two distinct objects of the same type will serialize to different values) any bytestring has at most one object it could deserialize to.

Deserialization can be implemented using a recursive algorithm. The deserialization of basic objects is easy, and from there we can find a simple recursive algorithm for all fixed-size objects. For variable-size objects we have to do one of the following depending on what kind of object it is:

Note that deserialization requires hardening against invalid inputs. A non-exhaustive list:

Efficient algorithms for computing this object can be found in the implementations.

Merkleization

We first define helper functions:

We now define Merkleization hash_tree_root(value) of an object value recursively:

Summaries and expansions

Let A be an object derived from another object B by replacing some of the (possibly nested) values of B by their hash_tree_root. We say A is a “summary” of B, and that B is an “expansion” of A. Notice hash_tree_root(A) == hash_tree_root(B).

We similarly define “summary types” and “expansion types”. For example, BeaconBlock is an expansion type of BeaconBlockHeader. Notice that objects expand to at most one object of a given expansion type. For example, BeaconBlockHeader objects uniquely expand to BeaconBlock objects.

Implementations

See https://github.com/ethereum/eth2.0-specs/issues/2138 for a list of current known implementations.

SSZ SimpleSerialize