Dynamic SSZ is a Go library for SSZ (Simple Serialize) encoding, decoding, and hash tree root computation. It supports runtime-configurable field sizes, making it suitable for Ethereum consensus types that vary across network presets.
go get github.com/pk910/dynamic-sszFor the code generation CLI tool:
go install github.com/pk910/dynamic-ssz/dynssz-gen@latestpackage main
import (
"fmt"
dynssz "github.com/pk910/dynamic-ssz"
)
// Define a structure with SSZ annotations
type BeaconBlockHeader struct {
Slot uint64
ProposerIndex uint64
ParentRoot [32]byte
StateRoot [32]byte
BodyRoot [32]byte
}
func main() {
// Create a DynSsz instance (the central entry point)
ds := dynssz.NewDynSsz(nil)
header := &BeaconBlockHeader{
Slot: 12345,
ProposerIndex: 42,
}
// Serialize to SSZ
data, err := ds.MarshalSSZ(header)
if err != nil {
panic(err)
}
fmt.Printf("Encoded: %d bytes\n", len(data))
// Deserialize from SSZ
var decoded BeaconBlockHeader
err = ds.UnmarshalSSZ(&decoded, data)
if err != nil {
panic(err)
}
fmt.Printf("Decoded slot: %d\n", decoded.Slot)
// Compute hash tree root
root, err := ds.HashTreeRoot(header)
if err != nil {
panic(err)
}
fmt.Printf("Root: %x\n", root)
}All SSZ operations go through a DynSsz instance. Create one with NewDynSsz(), passing optional specification values and options:
// Basic instance (no dynamic specs)
ds := dynssz.NewDynSsz(nil)
// With spec values for dynamic field sizes
specs := map[string]any{
"VALIDATOR_REGISTRY_LIMIT": uint64(1099511627776),
"MAX_ATTESTATIONS": uint64(128),
}
ds := dynssz.NewDynSsz(specs)
// With options
ds := dynssz.NewDynSsz(specs,
dynssz.WithExtendedTypes(), // enable non-standard types
dynssz.WithVerbose(), // enable debug logging
)Reuse the same DynSsz instance across your application - it caches type information for performance.
SSZ encoding is controlled through Go struct tags:
ssz-size:"N"- Fixed size for byte slices and strings (makes them SSZ vectors)ssz-max:"N"- Maximum size for dynamic lists (required for hash tree root security)ssz-type:"T"- Explicit SSZ type (e.g.,"bitvector","uint256","progressive-list")ssz-bitsize:"N"- Bit-level size for bitvectors (enables padding validation)
type Example struct {
Hash []byte `ssz-size:"32"` // fixed 32-byte vector
Items []uint64 `ssz-max:"1024"` // list with max 1024 elements
Bits [8]byte `ssz-type:"bitvector"` // bitvector
}The dynssz-* tags reference spec values that are resolved at runtime. They work alongside the static ssz-* tags:
type State struct {
// ssz-max provides the static fallback, dynssz-max overrides it at runtime
Validators []Validator `ssz-max:"1099511627776" dynssz-max:"VALIDATOR_REGISTRY_LIMIT"`
}When a dynssz-* expression resolves successfully, it overrides the corresponding ssz-* value. If the expression cannot be resolved (e.g., the spec value was not provided), the ssz-* value is used as a fallback. This lets the same type definitions work across different Ethereum presets.
Expressions support arithmetic operators: dynssz-max:"MAX_COMMITTEES_PER_SLOT*SLOTS_PER_EPOCH"
See SSZ Annotations for the complete tag reference.
size, err := ds.SizeSSZ(header)
if err != nil {
panic(err)
}
fmt.Printf("Serialized size: %d bytes\n", size)For performance-critical paths, reuse a buffer across multiple operations:
buf := make([]byte, 0, 1024)
for _, block := range blocks {
buf, err = ds.MarshalSSZTo(block, buf[:0])
if err != nil {
panic(err)
}
// process buf...
}err := ds.ValidateType(reflect.TypeOf(MyStruct{}))
if err != nil {
fmt.Printf("Type validation failed: %v\n", err)
}type Data struct {
Values [10]uint32 // vector of 10 elements
}Lists require ssz-max (or dynssz-max) for hash tree root security:
type Data struct {
Items []uint64 `ssz-max:"100"`
}Byte slices and strings are variable-length by default and need ssz-size or ssz-max:
type Data struct {
Hash []byte `ssz-size:"32"` // fixed 32 bytes (vector)
Payload []byte `ssz-max:"2048"` // variable up to 2048 bytes (list)
Name string `ssz-size:"64"` // fixed 64 bytes, null-padded
}type Flags struct {
FixedBits [32]byte `ssz-type:"bitvector"` // 256-bit bitvector
DynamicBits []byte `ssz-type:"bitlist" ssz-max:"2048"` // bitlist, max 2048 bits
}For bitlists, ssz-max specifies the maximum number of bits, not bytes. This matches the SSZ specification.
type Header struct {
Slot uint64
StateRoot [32]byte
}
type Block struct {
Header Header
Body *BlockBody // pointers are followed; nil pointers are initialized on unmarshal
}import "github.com/holiman/uint256"
type Account struct {
Balance *uint256.Int // automatically detected as uint256
}- Supported Types - Complete type reference
- SSZ Annotations - All struct tags and dynamic expressions
- Code Generation - Performance optimization
- API Reference - Full public interface
See the examples/ directory:
basic/- Encoding/decoding with go-eth2-client typescodegen/- Code generation setupcustom-types/- Dynamic expressions and spec valuesversioned-blocks/- Ethereum fork handlingprogressive-merkleization/- EIP-7916/7495 features