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.goFor larger configurations (many types, per-type options, per-type flag overrides), use a YAML config file instead of long CLI strings:
dynssz-gen --config gen.yamlSee Code Generator Config File for the format reference.
| Flag | Description | Default |
|---|---|---|
-config |
Path to a YAML config file (see config reference) | — |
-package |
Go package path to analyze | Required (unless supplied by config) |
-types |
Comma-separated list of types to generate (see extended format below) | Required (unless supplied by config) |
-output |
Output file path | Required if types don't specify files |
-header |
Custom header comment template for generated files. {hash} and {version} placeholders are substituted. The first line should match ^// Code generated .* DO NOT EDIT\.$ — a warning is printed otherwise, since tooling relies on that convention to recognize generated files. |
dynamic-ssz default header |
-v |
Verbose output | false |
-legacy |
Generate legacy compatibility methods | false |
-without-dynamic-expressions |
Generate only legacy methods, disable dynamic methods. See Freezing spec values. | 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 |
-recursion-depth |
Nesting depth at which the generated methods reject a recursive value; see Recursion depth | 0 (the default depth of 1024) |
-remove |
Move the configured output files aside before loading the package, so generated code from an earlier run cannot block the analysis when the types changed; they are deleted once generation succeeds and restored if it fails, a panic included. A stash left behind by an interrupted run (<output>.dynssz-gen.orig) is never overwritten: restore or delete it first |
off |
When both --config and CLI flags are provided, any CLI flag that is
explicitly passed overrides the config file's value. See the
config file docs for
precedence details.
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.goGeneration succeeds but prints a warning for a type whose encoding is legal yet outside the SSZ spec, so the mismatch is visible rather than silently baked into the generated methods:
warning: mypkg.Attestation.Bits has no ssz-max, so its hash tree root uses a
limit derived from the value rather than the type: it will not match another
implementation
A list or bitlist with no ssz-max has no spec-defined hash tree root, so
generating one at all requires -with-extended-types (see
supported-types.md). The programmatic
API exposes the same list through CodeGenerator.Warnings().
-without-dynamic-expressions bakes the static tag values into the generated
buffer methods. Those methods take no spec set, so they are correct only for a
DynSsz whose spec values match the static ones.
Streaming methods are unaffected: an encoder or decoder method is always handed
a DynSsz, so there is no expression-less form of it and it keeps resolving
spec values. A type generated with both flags therefore has methods that
disagree when the specs are not the defaults, which is by design -- the library
entrypoint picks the ones valid for the configured spec values, and never
serves a baked result to a DynSsz that resolves them differently. Calling the
generated methods directly is what makes the distinction visible, and there the
buffer methods carry the static contract.
Do not use this flag on a type that contains a type generated without it when that inner type uses spec expressions. See the known limitation in Delegation.
A streaming encoder that has to know a child's size before writing it sizes a
spec-carrying child inline, with the spec values it writes it with, instead of
through the child's static SizeSSZ. A spec-carrying child on a recursive
cycle cannot be inlined, so that combination is refused at generation time:
generate the type with dynamic expressions, or without streaming.
package 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).GetTypeCache())
// 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()
}The generator analyzes types on caches of its own and never writes to the cache it is given; passing a live instance's cache only inherits its extended-types setting. The instance keeps resolving spec values as before, and the generated code carries the spec expressions rather than the instance's values.
func generateSSZMultipleFiles() error {
codeGen := codegen.NewCodeGenerator(dynssz.NewDynSsz(nil).GetTypeCache())
// 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.GetTypeCache())
// 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{})),
)Reflect API limit: a type added with WithReflectType that references a static type from another package with its own generated methods and an ssz-static:"true" annotation is sized once, at generation time, with the type cache's specs. When that type's width depends on a spec value (a bitvector sized by dynssz-size, for instance), the generated code bakes that width, so it is only correct under the preset the generator ran with. Use dynssz-gen for such types: it reads the width from the referenced type's sizer at run time.
import "github.com/pk910/dynamic-ssz/codegen"
codeGen := codegen.NewCodeGenerator(dynSsz.GetTypeCache())
// 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
}A generated method reaches a nested type that has SSZ methods of its own
through them. A child with a registered static surface and no spec expressions
is reached through its static method on every path; otherwise a buffer method
calls the child's *Dyn method and a streaming method calls the child's
streaming method. Code generated with -without-dynamic-expressions reaches
every child through its static methods only. The full order and the effect of
the options are in Method Delegation.
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
}Whether a type additionally carries the static fastssz method set
(MarshalSSZTo, HashTreeRoot, HashTreeRootWith) can depend on the
composition of the generation batch: a type whose nested types are generated in
the same batch delegates to their preset-capable *Dyn methods and then omits
its own static twins. The *Dyn methods and the ds.* entry points are always
present — use those rather than relying on the static method set.
The members of a recursive cycle are generated in one run. A run inlines the
members it does not generate itself, so its code counts the whole cycle, and
a hand-written member owns its own recursion safety. A member that already
carries generated methods from an earlier run is delegated to and never
traversed, so a run that meets such a member on a cycle with a type it
generates fails with an error naming both types: regenerate the whole cycle
together (-remove clears the earlier output first). The same rule holds at
run time, in the reflection engine and in generation through the Go API:
describing a type without generated methods that lies on a cycle with a
generated type fails with the same error.
When using -without-dynamic-expressions, only static methods are generated
(no *Dyn methods): MarshalSSZTo, UnmarshalSSZ, SizeSSZ, HashTreeRoot
and HashTreeRootWith, plus the streaming pair with -with-streaming. The
allocating MarshalSSZ wrapper is a legacy method and only comes with
-legacy.
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
A value's own SSZ hash methods are always called where the value sits. A list
or vector of basic values (up to 16 bytes; a wrapper around one counts as the
wrapped value) opens a packed scope on the hash walker
(sszutils.TreeTypePacked): inside it the walker's Put* methods append the
value's packed bytes instead of a padded chunk, so a method declared on such an
element type (type Slot uint64 with generated methods, or a hand-written
hh.PutUint64(...)) packs the value with its neighbours as the SSZ
specification requires, and the same method leaves a whole chunk as a field or
root. A generated method of a large uint held as words ([2]uint64 or
[4]uint64 declared uint128 or uint256, or a wrapper around one) leaves
the value's packed bytes everywhere, and the caller pads them to a chunk
outside a packed scope, as the engines and the generated standalone
HashTreeRoot methods do. A method that merkleizes a leaf of its own inside a packed scope shifts
the elements that follow; the engines do not check this, and both walkers
produce the same shifted root. A method that only
returns a root (HashTreeRoot()) contributes the packed prefix of that root,
which for a basic value is the value itself.
A ssz-type:"custom" value always delegates. Outside a packed scope the engine
pads the hasher to the next 32-byte chunk after the delegate returns, so a hash
method may either leave a complete leaf or append only the packed bytes of its
value. A generated or fastssz composite type is not padded: its method leaves
exactly one root. Inside a list or vector, a custom type whose declared ssz-size is one a
basic type could have (a power of two up to 16 bytes, without a size
expression) stands in for that basic type: the scope is packed and its hash
method is held to the rule above. Any other size gets a leaf of its own, as a
composite element does.
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:
Union[T](classic spec) andCompatibleUnion[T](EIP-8016) - 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. The
expressions a type's methods use are resolved together by one generated
buildDynSSZSpecSet method; each dynamic method fetches the resolved set on
entry through sszutils.GetCachedSpecSet, and a DynSsz instance keeps the
set per type, so the expressions are resolved once per instance rather than on
every call:
type State struct {
Validators []Validator `dynssz-max:"VALIDATOR_REGISTRY_LIMIT"`
}
// Generated once per type
func (t *State) buildDynSSZSpecSet(ds sszutils.DynamicSpecs) ([]uint64, error) {
exprs := make([]uint64, 1)
var err error
if exprs[0], err = sszutils.ResolveSpecValueWithDefault(ds, "VALIDATOR_REGISTRY_LIMIT", 1099511627776); err != nil {
return nil, err
}
return exprs, nil
}
// Generated dynamic method
func (t *State) MarshalSSZDyn(ds sszutils.DynamicSpecs, buf []byte) ([]byte, error) {
exprs, err := sszutils.GetCachedSpecSet[State](ds, t.buildDynSSZSpecSet)
if err != nil {
return nil, err
}
expr0 := exprs[0]
// ... use dynamic value for different presets
}A DynamicSpecs implementation other than DynSsz caches nothing: the set is
built on every call through it.
With -without-dynamic-expressions, the expressions are dropped and the static
ssz-* values take their place. Generation never consults spec values of its
own — the generated code resolves expressions against the specs it runs under,
so what a generating machine has loaded cannot reach the output:
// Generated static method (assuming default preset values)
func (s *State) MarshalSSZTo(buf []byte) ([]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
- Code Generator Config File - YAML config file format
- 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