Dynamic SSZ provides comprehensive type support through both automatic detection and explicit annotations. This guide covers all supported types, including the strict type system and progressive types.
Dynamic SSZ uses a multi-layered type detection system:
- Explicit Type Specification - via
ssz-typetag - Interface Detection - checks for SSZ marshaling interfaces
- Well-Known Types - recognizes common types (uint256.Int, go-bitfield)
- Automatic Detection - infers type from Go type system
- Go type:
bool - SSZ type:
bool - Size: 1 byte (0x00 or 0x01)
type Flags struct {
IsActive bool
}- Go types:
uint8,uint16,uint32,uint64 - SSZ types:
uint8,uint16,uint32,uint64 - Encoding: Little-endian
type Numbers struct {
Small uint8
Medium uint16
Large uint32
XLarge uint64
}type Account struct {
Balance [32]byte `ssz-type:"uint256"` // 256-bit value
Nonce [16]byte `ssz-type:"uint128"` // 128-bit value
}type Account struct {
Balance [4]uint64 `ssz-type:"uint256"` // 4 × 64-bit = 256-bit
Nonce [2]uint64 `ssz-type:"uint128"` // 2 × 64-bit = 128-bit
}import "github.com/holiman/uint256"
type Account struct {
Balance *uint256.Int // Automatically detected as uint256
}- Go type:
[N]T - SSZ type:
vector[T, N] - Elements must be of same type
type Block struct {
ParentHashes [16][32]byte // Vector of 16 hash values
}- Go type:
[]T - SSZ type:
list[T, N] - Needs
ssz-maxtag
type Transaction struct {
Data []byte `ssz-max:"1024"`
}type Hash struct {
Value [32]byte // Fixed 32-byte array
}type Data struct {
Payload []byte `ssz-max:"2048"`
}type Name struct {
First string `ssz-size:"32"`
Last string `ssz-size:"32"`
}type Permissions struct {
Flags [256]bool // Fixed-size bitvector
}When a bitvector's bit count is not a multiple of 8, the remaining bits in the last byte are padding bits. Use ssz-bitsize to specify the exact bit count and enable padding validation:
type CommitteeFlags struct {
// 12-bit bitvector stored in 2 bytes
// Bits 12-15 (padding) are validated to be zero during unmarshaling
Bits [2]byte `ssz-type:"bitvector" ssz-bitsize:"12"`
// Dynamic bit size based on spec value
DynBits []byte `ssz-type:"bitvector" ssz-bitsize:"512" dynssz-bitsize:"SYNC_COMMITTEE_SIZE"`
}Padding bit validation: According to the SSZ specification, unused bits in the last byte of a bitvector must be zero. When ssz-bitsize or dynssz-bitsize is specified, Dynamic SSZ validates these padding bits during unmarshaling and returns an error if any are non-zero.
Note: For bitlists, ssz-max specifies the maximum number of bits, not bytes. This is consistent with the SSZ specification.
type Votes struct {
Participants []bool `ssz-max:"2048"` // Maximum 2048 bits
}import bitfield "github.com/OffchainLabs/go-bitfield"
type Attestation struct {
AggregationBits bitfield.Bitlist `ssz-max:"2048"` // Maximum 2048 bits
}Structs are the primary container type:
type BeaconBlock struct {
Slot uint64
ProposerIndex uint64
ParentRoot [32]byte
StateRoot [32]byte
}type SignedBeaconBlock struct {
Message BeaconBlock
Signature [96]byte
}Pointers are treated as regular fields and are initialized if nil:
type Block struct {
Header BlockHeader
Body *BlockBody // Will be initialized if nil during unmarshaling
}Optimized merkleization for lists that grow over time:
type State struct {
Validators []Validator `ssz-type:"progressive-list"`
}Benefits:
- Efficient merkle tree updates when appending
- Reduced computation for growing lists
- Maintains backward compatibility
Optimized for participation tracking:
type Participation struct {
CurrentEpoch bitfield.Bitlist `ssz-type:"progressive-bitlist"`
}Forward-compatible containers using ssz-index:
type BeaconState struct {
// Core fields
GenesisTime uint64 `ssz-index:"0"`
Slot uint64 `ssz-index:"1"`
// Fields added in fork
NewField *uint64 `ssz-index:"5"`
}Key features:
- Explicit field ordering via
ssz-index - Backward/forward compatibility
- Pointer fields for indirection
Type-safe variant types using struct descriptor:
import dynssz "github.com/pk910/dynamic-ssz"
// Define union using struct descriptor (field order = variant index)
type PayloadUnion = dynssz.CompatibleUnion[struct {
ExecutionPayload // Variant 0
ExecutionPayloadWithBlobs // Variant 1
}]
// Use in container
type BeaconBlock struct {
Slot uint64
Payload PayloadUnion `ssz-type:"compatible-union"`
}
// Create union instance
block := BeaconBlock{
Slot: 123,
Payload: PayloadUnion{
Variant: 0, // Use ExecutionPayload
Data: ExecutionPayload{...},
},
}For applying SSZ annotations to non-struct values using a descriptor struct with one tagged field:
import dynssz "github.com/pk910/dynamic-ssz"
// Descriptor struct: exactly one field with SSZ tags, same type as T
type FixedBytesDescriptor struct {
Data []byte `ssz-size:"32"`
}
// Use wrapper in a container
type MyContainer struct {
Hash dynssz.TypeWrapper[FixedBytesDescriptor, []byte] `ssz-type:"wrapper"`
}
// Access wrapped value
container := MyContainer{}
container.Hash.Set([]byte{1, 2, 3})
value := container.Hash.Get()See Type Wrapper for detailed usage.
Types can implement custom serialization:
type CustomType struct {
data []byte
}
func (c *CustomType) MarshalSSZ() ([]byte, error) {
return c.data, nil
}
func (c *CustomType) UnmarshalSSZ(data []byte) error {
c.data = data
return nil
}
func (c *CustomType) SizeSSZ() int {
return len(c.data)
}
func (c *CustomType) HashTreeRoot() ([32]byte, error) {
// Custom merkleization
}For spec-aware marshaling:
import "github.com/pk910/dynamic-ssz/sszutils"
type DynamicType struct{}
func (d *DynamicType) MarshalSSZDyn(specs sszutils.DynamicSpecs, buf []byte) ([]byte, error) {
found, size, err := specs.ResolveSpecValue("DYNAMIC_SIZE")
if err != nil || !found {
return nil, err
}
// Use spec value for marshaling
return append(buf, make([]byte, size)...), nil
}
func (d *DynamicType) UnmarshalSSZDyn(specs sszutils.DynamicSpecs, buf []byte) error {
found, size, err := specs.ResolveSpecValue("DYNAMIC_SIZE")
if err != nil || !found {
return err
}
// Use spec value for unmarshaling
return nil
}
func (d *DynamicType) SizeSSZDyn(specs sszutils.DynamicSpecs) int {
found, size, _ := specs.ResolveSpecValue("DYNAMIC_SIZE")
if !found {
return 0
}
return int(size)
}Many types are automatically detected:
import (
"github.com/holiman/uint256"
bitfield "github.com/OffchainLabs/go-bitfield"
)
type AutoDetected struct {
// Automatically detected as uint256
Balance *uint256.Int
// Automatically detected as bitlist
Bits bitfield.Bitlist `ssz-max:"2048"`
}Use ssz-type for explicit control:
type Explicit struct {
// Force specific type
Value uint64 `ssz-type:"uint64"`
// Container type
Data MyStruct `ssz-type:"container"`
}?orauto- Let Dynamic SSZ detect the typecustom- Type implements custom interfaceswrapperortype-wrapper- Use TypeWrapper pattern
Dynamic SSZ supports complex nested structures:
type Matrix struct {
// 2D fixed array
Values [10][20]uint32
// Mixed dimensions
Data [][32]byte `ssz-max:"100"`
// Per-dimension sizing
Grid [][]uint64 `ssz-max:"100,2048"`
}Dynamic arrays should specify a maximum size for secure hash tree root computation:
// Recommended: maximum size specified
type Good struct {
Items []uint64 `ssz-max:"1000"`
}
// Discouraged: no maximum size (hash tree root has security implications)
type Risky struct {
Items []uint64
}Ensure types are SSZ-compatible:
// Valid types
type Valid struct {
Number uint64
Flag bool
Data []byte `ssz-max:"1024"`
}
// Invalid types
type Invalid struct {
Channel chan int // Channels not supported
Func func() // Functions not supported
Iface interface{} // Interfaces not supported
}- Prefer fixed-size types when possible
- Use progressive types for large, growing collections
- Implement custom interfaces for complex types
- Use TypeWrapper for reusable type patterns
- Bitvectors are more efficient than bool arrays
- Progressive lists reduce merkleization cost
- Custom types can optimize for specific patterns
type BeaconState struct {
// Fixed-size fields
GenesisTime uint64
GenesisValidatorsRoot [32]byte
Slot uint64
// Progressive list for efficiency
Validators []Validator `ssz-type:"progressive-list" ssz-max:"1099511627776" dynssz-max:"VALIDATOR_REGISTRY_LIMIT"`
// Bitlist for participation
JustificationBits bitfield.Bitvector4
// Dynamic with expression (ssz-max is fallback when spec value is unavailable)
Balances []uint64 `ssz-max:"1099511627776" dynssz-max:"VALIDATOR_REGISTRY_LIMIT"`
}type ComplexData struct {
// Multi-dimensional with per-dimension limits
Matrix [][]uint32 `ssz-max:"100,256"`
// Pointer to nested structure
Extra *struct {
Data []byte `ssz-max:"1024"`
Index uint64
}
// Union type
Operation dynssz.CompatibleUnion[struct {
Deposit
Withdrawal
}] `ssz-type:"compatible-union"`
}Dynamic SSZ also supports an extended set of types that are not part of the SSZ specification. These include signed integers (int8, int16, int32, int64), floating-point numbers (float32, float64), arbitrary-precision integers (big.Int), and optional types (pointer types annotated with ssz-type:"optional").
Extended types must be explicitly enabled with WithExtendedTypes() and are not compatible with other SSZ libraries.
See Extended Types for full documentation.
- Extended Types - Non-standard type extensions
- SSZ Annotations - Detailed tag reference
- Type Wrapper - Advanced type patterns
- API Reference - Type-related APIs