Dynamic SSZ includes a code generation tool (dynssz-gen) that generates optimized SSZ methods for your types, eliminating reflection overhead and improving performance.
Always use ds.MarshalSSZ(), ds.UnmarshalSSZ(), ds.HashTreeRoot(), etc. as your entry points - even when code generation is active. The DynSsz runtime automatically detects and delegates to generated methods (like MarshalSSZDyn, UnmarshalSSZDyn) when they are present on the type.
Do not call generated methods directly from your application code. This creates a circular dependency problem: if you delete the generated file to regenerate it, the direct references to generated methods break compilation, and the code generator cannot analyze your package. By routing everything through DynSsz, your code compiles with or without the generated file - the library simply falls back to reflection when generated methods are absent.
go install github.com/pk910/dynamic-ssz/dynssz-gen@latestVerify installation:
dynssz-gen -helpGenerate SSZ methods for specific types:
dynssz-gen -package /path/to/package -types BeaconBlock,BeaconState -output generated_ssz.go| Flag | Description | Default |
|---|---|---|
-package |
Go package path to analyze | Required |
-types |
Comma-separated list of types to generate (see extended format below) | Required |
-output |
Output file path | Required if types don't specify files |
-v |
Verbose output | false |
-legacy |
Generate legacy compatibility methods | false |
-without-dynamic-expressions |
Generate only legacy methods, disable dynamic methods | false |
-without-fastssz |
Generate code without using fast ssz generated methods | false |
-with-streaming |
Generate streaming encoder/decoder functions | false |
-with-extended-types |
Enable support for non-standard extended types (signed ints, floats, big.Int, optionals) | false |
The -types flag supports an extended format for specifying output files and view types:
TypeName[:output.go][:views=View1;View2;...][:viewonly]
| Part | Description | Required |
|---|---|---|
TypeName |
Name of the type to generate methods for | Yes |
:output.go |
Output file for this specific type | No (uses -output flag) |
:views=... |
Semicolon-separated list of view types for fork support | No |
:viewonly |
Only generate view-aware methods, not standard methods | No |
View type references can be:
- Local types:
Phase0View - External package types:
github.com/myproject/views.Phase0View
Examples:
# Simple type to default output
-types "BeaconBlock"
# Type to specific file
-types "BeaconBlock:block_ssz.go"
# Type with local view types
-types "BeaconBlock:block_ssz.go:views=Phase0BlockView;AltairBlockView"
# Type with external view types
-types "BeaconBlock:block_ssz.go:views=github.com/views.Phase0View"
# View-only mode (no standard methods)
-types "BeaconBlock:block_ssz.go:views=Phase0View:viewonly"
# Multiple types with mixed configurations
-types "BeaconBlock:block_ssz.go:views=Phase0BlockView,BeaconState:state_ssz.go"# In your project directory
dynssz-gen -package . -types Block,Transaction,Validator -output ssz_generated.go# Each type to its own file
dynssz-gen -package . -types Block:block_ssz.go,Transaction:tx_ssz.go,Validator:validator_ssz.go# Some types to separate files, others to default output
dynssz-gen -package . -types Block:block_ssz.go,Transaction:tx_ssz.go,Validator,ValidatorData -output validators_ssz.goGenerate SSZ methods that support multiple schemas (views) for fork handling:
# Data type with views - generates both standard and view-aware methods
dynssz-gen -package . \
-types "BeaconBlockBody:body_ssz.go:views=Phase0BeaconBlockBodyView;AltairBeaconBlockBodyView" \
-output default.go
# View-only mode - only generates view-aware methods (useful when standard methods already exist)
dynssz-gen -package . \
-types "BeaconBlockBody:body_ssz.go:views=Phase0View;AltairView:viewonly" \
-output default.go
# Views from external packages
dynssz-gen -package . \
-types "BeaconState:state_ssz.go:views=github.com/myproject/views.Phase0StateView" \
-output default.goSee SSZ Views for detailed documentation on view support.
dynssz-gen -package github.com/myproject/types -types BeaconBlock -output beacon_ssz.godynssz-gen -v -package . -types State,Block -output ssz_methods.goFor compatibility with existing fastssz code:
dynssz-gen -legacy -package . -types BeaconState -output legacy_ssz.goFor maximum performance with default preset:
dynssz-gen -without-dynamic-expressions -package . -types BeaconState -output static_ssz.goFor maximum flexibility:
dynssz-gen -legacy -package . -types BeaconState -output full_ssz.goFor memory-efficient streaming to/from io.Reader/io.Writer:
dynssz-gen -with-streaming -package . -types BeaconState -output streaming_ssz.gopackage main
import (
"github.com/pk910/dynamic-ssz/codegen"
dynssz "github.com/pk910/dynamic-ssz"
)
func generateSSZ() error {
// Create code generator
codeGen := codegen.NewCodeGenerator(dynssz.NewDynSsz(nil))
// Add types to single file
codeGen.BuildFile("generated_ssz.go",
codegen.WithReflectType(reflect.TypeOf(BeaconBlock{})),
codegen.WithReflectType(reflect.TypeOf(BeaconState{})),
)
// Generate code
return codeGen.Generate()
}func generateSSZMultipleFiles() error {
codeGen := codegen.NewCodeGenerator(dynssz.NewDynSsz(nil))
// Block types to one file
codeGen.BuildFile("block_ssz.go",
codegen.WithReflectType(reflect.TypeOf(BeaconBlock{})),
codegen.WithReflectType(reflect.TypeOf(BeaconBlockBody{})),
)
// State types to another file
codeGen.BuildFile("state_ssz.go",
codegen.WithReflectType(reflect.TypeOf(BeaconState{})),
codegen.WithReflectType(reflect.TypeOf(Validator{})),
)
// Cross-references between types are handled automatically
return codeGen.Generate()
}import "github.com/pk910/dynamic-ssz/codegen"
// Create generator with options
codeGen := codegen.NewCodeGenerator(dynSsz)
// Build file with options
codeGen.BuildFile("output.go",
// Skip marshal method generation
codegen.WithNoMarshalSSZ(),
// Skip unmarshal method generation
codegen.WithNoUnmarshalSSZ(),
// Skip size calculation
codegen.WithNoSizeSSZ(),
// Skip hash tree root generation
codegen.WithNoHashTreeRoot(),
// Generate legacy methods (adds MarshalSSZ, UnmarshalSSZ, etc.)
codegen.WithCreateLegacyFn(),
// Skip dynamic expressions (generates only static legacy methods)
codegen.WithoutDynamicExpressions(),
// Generate streaming encoder function (MarshalSSZEncoder)
codegen.WithCreateEncoderFn(),
// Generate streaming decoder function (UnmarshalSSZDecoder)
codegen.WithCreateDecoderFn(),
// Add type to generate
codegen.WithReflectType(reflect.TypeOf(MyType{})),
)import "github.com/pk910/dynamic-ssz/codegen"
codeGen := codegen.NewCodeGenerator(dynSsz)
// Generate type with view support (data + views mode)
codeGen.BuildFile("output.go",
codegen.WithReflectType(
reflect.TypeOf(BeaconBlockBody{}),
// Add view types using reflect.Type
codegen.WithReflectViewTypes(
reflect.TypeOf(Phase0BeaconBlockBodyView{}),
reflect.TypeOf(AltairBeaconBlockBodyView{}),
reflect.TypeOf(BellatrixBeaconBlockBodyView{}),
),
),
)
// Generate type in view-only mode (only view methods, no standard methods)
codeGen.BuildFile("views_only.go",
codegen.WithReflectType(
reflect.TypeOf(BeaconBlockBody{}),
codegen.WithReflectViewTypes(
reflect.TypeOf(Phase0BeaconBlockBodyView{}),
),
codegen.WithViewOnly(), // Only generate view-aware methods
),
)
// Using go/types (for external packages)
codeGen.BuildFile("output.go",
codegen.WithGoTypesType(
goTypesType,
codegen.WithGoTypesViewTypes(phase0ViewType, altairViewType),
),
)
codeGen.Generate()See SSZ Views for detailed documentation on view concepts and usage.
The code generator produces different methods depending on the flags used:
By default, the generator creates dynamic methods that accept specification values:
func (b *BeaconBlock) MarshalSSZDyn(ds sszutils.DynamicSpecs, buf []byte) (dst []byte, err error) {
// Generated marshaling with dynamic expression support
}func (b *BeaconBlock) UnmarshalSSZDyn(ds sszutils.DynamicSpecs, buf []byte) (err error) {
// Generated unmarshaling with dynamic expression support
}func (b *BeaconBlock) SizeSSZDyn(ds sszutils.DynamicSpecs) (size int) {
// Size calculation with dynamic expression support
}func (b *BeaconBlock) HashTreeRootWithDyn(ds sszutils.DynamicSpecs, hh sszutils.HashWalker) error {
// Hash tree root with dynamic expression support
}When using -legacy flag, additional fastssz-compatible methods are generated:
func (b *BeaconBlock) MarshalSSZ() ([]byte, error) {
return b.MarshalSSZDyn(dynssz.GetGlobalDynSsz(), nil)
}
func (b *BeaconBlock) MarshalSSZTo(buf []byte) ([]byte, error) {
return b.MarshalSSZDyn(dynssz.GetGlobalDynSsz(), buf)
}func (b *BeaconBlock) UnmarshalSSZ(buf []byte) error {
return b.UnmarshalSSZDyn(dynssz.GetGlobalDynSsz(), buf)
}func (b *BeaconBlock) SizeSSZ() int {
return b.SizeSSZDyn(dynssz.GetGlobalDynSsz())
}func (b *BeaconBlock) HashTreeRoot() ([32]byte, error) {
// Optimized merkle root calculation using global specs
}When using -without-dynamic-expressions, only static legacy methods are generated (no *Dyn methods):
func (b *BeaconBlock) MarshalSSZ() ([]byte, error) {
// Static marshaling for default preset only
}
func (b *BeaconBlock) MarshalSSZTo(buf []byte) ([]byte, error) {
// Static marshaling to buffer
}
func (b *BeaconBlock) UnmarshalSSZ(buf []byte) error {
// Static unmarshaling
}
func (b *BeaconBlock) SizeSSZ() int {
// Static size calculation
}
func (b *BeaconBlock) HashTreeRoot() ([32]byte, error) {
// Static hash tree root
}Use case: Generate optimized code for the default preset while falling back to reflection for other presets.
Add generation directives to your code:
//go:generate dynssz-gen -package . -types BeaconBlock,BeaconState -output generated_ssz.go
package types
type BeaconBlock struct {
Slot uint64
ProposerIndex uint64
ParentRoot [32]byte
StateRoot [32]byte
Body BeaconBlockBody
}Run generation:
go generate ./....PHONY: generate-ssz
generate-ssz:
dynssz-gen -package . -types Block,State,Transaction -output ssz_generated.go
.PHONY: build
build: generate-ssz
go build ./...GitHub Actions example:
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Install dynssz-gen
run: go install github.com/pk910/dynamic-ssz/dynssz-gen@latest
- name: Generate SSZ code
run: dynssz-gen -package . -types BeaconBlock,BeaconState -output generated_ssz.go
- name: Build
run: go build ./...
- name: Test
run: go test ./...The generator supports all Dynamic SSZ types and annotations:
- All unsigned integers:
uint8,uint16,uint32,uint64 - Boolean:
bool - Fixed arrays:
[N]T - Slices:
[]T - Byte arrays:
[]byte,[N]byte - Strings:
string
The code generator supports generating SSZ methods for non-struct types (named slices, arrays, etc.) using sszutils.Annotate[T]() to register SSZ annotations. This solves the problem that Go does not allow struct tags on type definitions.
import "github.com/pk910/dynamic-ssz/sszutils"
type Blobs []*Blob
var _ = sszutils.Annotate[Blobs](`ssz-max:"4096"`)
type BlobKZGs [][48]byte
var _ = sszutils.Annotate[BlobKZGs](`ssz-max:"4096" dynssz-max:"MAX_BLOB_COMMITMENTS_PER_BLOCK"`)The tag string uses the same key:"value" syntax as struct tags. All SSZ annotations are supported (ssz-size, ssz-max, ssz-type, dynssz-size, dynssz-max, etc.).
Generate code for these types the same way as struct types:
dynssz-gen -package . -types Blobs -output blobs_ssz.goThe generated code uses the registered annotations:
func (t *Blobs) MarshalSSZTo(buf []byte) (dst []byte, err error) {
dst = buf
vlen := len(*t)
if vlen > 4096 {
return nil, sszutils.ErrListLengthFn(vlen, 4096)
}
// ...
}Unlike struct field tags, Annotate[T]() works with both the code generator and the runtime reflection path, ensuring consistent behavior across both paths.
See SSZ Annotations for full syntax details.
- Large integers: byte arrays or uint64 arrays with
ssz-type:"uint256" - Bitfields
- Progressive types
- Unions:
CompatibleUnion[T] - Type wrappers:
TypeWrapper[D, T]
All SSZ annotations are supported:
ssz-sizessz-maxssz-typessz-bitsize(bit-level sizing for bitvectors)dynssz-sizedynssz-maxdynssz-bitsize(dynamic bit-level sizing for bitvectors)ssz-index
Generated code eliminates reflection overhead, pre-calculates sizes, and reduces allocations. See the benchmark repository for real performance comparisons across SSZ libraries.
The code generator automatically handles cross-references between types when they're generated together. This prevents massive code duplication.
How it works:
- When generating multiple types, the generator analyzes all types first
- If a type references another type being generated, it calls the generated method on that type
- If the generator doesn't know about a referenced type, it duplicates the entire marshaling logic inline
- Cross-references work across files when types are generated in the same batch
Example:
type BeaconBlock struct {
Body BeaconBlockBody // Reference to another type
}
type BeaconBlockBody struct {
Attestations []Attestation `ssz-max:"128"`
}With cross-reference detection (generating together):
dynssz-gen -package . -types BeaconBlock,BeaconBlockBody -output beacon_ssz.goGenerated code calls the method:
func (b *BeaconBlock) MarshalSSZDyn(ds sszutils.DynamicSpecs, buf []byte) ([]byte, error) {
// ... other fields
if dst, err = b.Body.MarshalSSZDyn(ds, dst); err != nil {
return dst, err
}
// ...
}Without cross-reference detection (generating separately):
dynssz-gen -package . -types BeaconBlock -output block_ssz.go # BeaconBlockBody not includedGenerated code duplicates the entire BeaconBlockBody marshaling logic inline, leading to massive code duplication.
Multiple Files with Cross-References:
# These types can reference each other without code duplication
dynssz-gen -package . -types BeaconBlock:block_ssz.go,BeaconBlockBody:block_ssz.go,Attestation:attestation_ssz.goThe generator recognizes custom SSZ interfaces:
type CustomType struct {
// If type implements MarshalSSZ/UnmarshalSSZ,
// generator calls those methods
}
func (c *CustomType) MarshalSSZ() ([]byte, error) {
// Custom implementation
}By default, dynamic methods support runtime specification values:
type State struct {
Validators []Validator `dynssz-max:"VALIDATOR_REGISTRY_LIMIT"`
}
// Generated dynamic method
func (s *State) MarshalSSZDyn(ds sszutils.DynamicSpecs, buf []byte) ([]byte, error) {
maxValidators := ds.GetValue("VALIDATOR_REGISTRY_LIMIT")
// ... use dynamic value for different presets
}With -without-dynamic-expressions, expressions are resolved at generation time:
// Generated static method (assuming default preset values)
func (s *State) MarshalSSZ() ([]byte, error) {
// Hard-coded limits for maximum performance
// Falls back to reflection for non-default presets
}dynssz-gen -package . -types BeaconBlock -output block_ssz.goGenerates: MarshalSSZDyn, UnmarshalSSZDyn, SizeSSZDyn, HashTreeRootWithDyn
dynssz-gen -legacy -package . -types BeaconBlock -output block_ssz.goGenerates: All dynamic methods + MarshalSSZ, UnmarshalSSZ, SizeSSZ, HashTreeRoot, MarshalSSZTo
dynssz-gen -without-dynamic-expressions -package . -types BeaconBlock -output block_ssz.goGenerates: Only MarshalSSZ, UnmarshalSSZ, SizeSSZ, HashTreeRoot, MarshalSSZTo (no *Dyn methods)
Key Point: -without-dynamic-expressions is especially useful when you want to generate optimized code for the default preset and fall back to reflection-based Dynamic SSZ for other presets.
When using the views= type specification, view-aware methods are generated that allow the same runtime type to be serialized with different SSZ schemas:
func (b *BeaconBlock) MarshalSSZDynView(view any) func(ds sszutils.DynamicSpecs, buf []byte) ([]byte, error) {
// Returns a marshal function for the specified view type, or nil if not supported
switch view.(type) {
case *Phase0BeaconBlockView:
return b.marshalSSZDynPhase0BeaconBlockView
case *AltairBeaconBlockView:
return b.marshalSSZDynAltairBeaconBlockView
}
return nil
}func (b *BeaconBlock) UnmarshalSSZDynView(view any) func(ds sszutils.DynamicSpecs, buf []byte) error {
// Returns an unmarshal function for the specified view type, or nil if not supported
}func (b *BeaconBlock) SizeSSZDynView(view any) func(ds sszutils.DynamicSpecs) int {
// Returns a size function for the specified view type, or nil if not supported
}func (b *BeaconBlock) HashTreeRootWithDynView(view any) func(ds sszutils.DynamicSpecs, hh sszutils.HashWalker) error {
// Returns a hash function for the specified view type, or nil if not supported
}These methods implement the DynamicViewMarshaler, DynamicViewUnmarshaler, DynamicViewSizer, and DynamicViewHashRoot interfaces from sszutils. The Dynamic SSZ runtime automatically uses these methods when a view descriptor is provided via WithViewDescriptor().
Note: View methods return nil if the view type is not recognized, causing Dynamic SSZ to fall back to reflection-based processing.
See SSZ Views for detailed documentation on view usage.
When using -with-streaming flag, streaming-capable methods are generated:
func (b *BeaconBlock) MarshalSSZEncoder(ds sszutils.DynamicSpecs, encoder sszutils.Encoder) error {
// Streaming-compatible encoding that works with both buffer and stream encoders
}func (b *BeaconBlock) UnmarshalSSZDecoder(ds sszutils.DynamicSpecs, decoder sszutils.Decoder) error {
// Streaming-compatible decoding that works with both buffer and stream decoders
}These methods enable efficient streaming I/O by implementing the DynamicEncoder and DynamicDecoder interfaces. They are automatically used by MarshalSSZWriter and UnmarshalSSZReader when available.
Use case: Generate streaming methods for types that need memory-efficient encoding/decoding to/from io.Reader/io.Writer.
Performance note: Streaming trades memory for CPU time. Expect ~2x CPU overhead for unmarshal and ~1.3x for marshal operations, as streams cannot seek back to update offsets. See Streaming Support for details.
Ensure the type is exported and in the correct package:
# Specify correct package path
dynssz-gen -package github.com/myproject/types -types MyType -output output.goIf types reference each other, generate them together to avoid code duplication:
# Problem: Generating types separately causes massive code duplication
dynssz-gen -package . -types Block -output block_ssz.go # Duplicates Transaction marshaling code inline
dynssz-gen -package . -types Transaction -output tx_ssz.go
# Solution: Generate related types together
dynssz-gen -package . -types Block,Transaction -output types_ssz.go
# Or use per-type files but generate in single command
dynssz-gen -package . -types Block:block_ssz.go,Transaction:tx_ssz.goCheck that your type follows SSZ rules:
- No interfaces (except with
ssz-type) - No channels or functions
- Dynamic lists should have
ssz-maxtags (highly recommended for security)
If generated code differs from runtime behavior:
-
Regenerate with verbose mode:
dynssz-gen -v -package . -types MyType -output debug_ssz.go -
Check for custom interfaces that might affect generation
-
Verify all tags are correctly specified
If generated code doesn't compile:
- Ensure all imported types are available
- Check for circular dependencies
- Verify generated imports are correct
Do commit generated files to your repository:
This ensures:
- Code builds without requiring code generation
- No external tool dependencies for builds
- Consistent builds across environments
- Faster CI/build processes
Use consistent output file naming:
# Good patterns
types_ssz.go
generated_ssz.go
beacon_ssz_generated.goSet up automated regeneration:
# Pre-commit hook
#!/bin/sh
go generate ./...
git add *_ssz.goGenerate only needed methods:
codeGen.BuildFile("output.go",
codegen.WithNoUnmarshalSSZ(), // Only marshal and size
codegen.WithNoHashTreeRoot(),
codegen.WithReflectType(reflect.TypeOf(MyType{})),
)When generating multiple files, ensure proper cross-references:
# Generate all related types together for proper cross-references
dynssz-gen -package . -types Block:block_ssz.go,Transaction:tx_ssz.go,Header:block_ssz.go
# Or generate all at once
dynssz-gen -package . -types Block,Transaction,Header,Validator -output all_ssz.go# Generate all beacon chain types together for cross-references
dynssz-gen -package . -types BeaconBlock,BeaconState,Validator,Attestation,BeaconBlockBody -output consensus_types_ssz.go# Generate to separate files while maintaining cross-references
dynssz-gen -package . -types \
BeaconBlock:block_ssz.go,BeaconBlockBody:block_ssz.go,\
BeaconState:state_ssz.go,Validator:state_ssz.go,\
Attestation:attestation_ssz.go,AttestationData:attestation_ssz.go//go:generate dynssz-gen -package . -types Message,Header,Payload -output protocol_ssz.go
package protocol
type Message struct {
Header Header
Payload Payload `ssz-max:"65536"`
Sig [96]byte
}
type Header struct {
Version uint8
Timestamp uint64
Sender [20]byte
}
type Payload struct {
Type uint16
Data []byte `dynssz-max:"MAX_PAYLOAD_SIZE"`
}type ProgressiveState struct {
// Progressive list for efficiency
Validators []Validator `ssz-type:"progressive-list" dynssz-max:"VALIDATOR_LIMIT"`
// Progressive container with indices
Slot uint64 `ssz-index:"0"`
Extensions *Extensions `ssz-index:"100"`
}To migrate from fastssz code generation:
-
Install dynssz-gen:
go install github.com/pk910/dynamic-ssz/dynssz-gen@latest
-
Update generation commands:
# Old: sszgen -path types -output generated.ssz.go # New: dynssz-gen -package . -types BeaconBlock,BeaconState -output generated_ssz.go
-
Add legacy flag if needed:
dynssz-gen -legacy -package . -types BeaconBlock -output legacy_ssz.go -
Update imports in your code:
// Old: github.com/ferranbt/fastssz // New: github.com/pk910/dynamic-ssz
- Getting Started - Basic usage
- API Reference - Generator API details
- SSZ Views - Fork handling with view descriptors
- Streaming Support - Streaming encoding/decoding
- SSZ Annotations - Supported tags
- Supported Types - Type compatibility