Documentation
¶
Overview ¶
Package dynssz provides dynamic SSZ encoding and decoding with runtime reflection support.
Package dynssz provides dynamic SSZ encoding and decoding with runtime reflection support.
Index ¶
- func SetGlobalSpecs(specs map[string]any)
- type CallOption
- type CompatibleUnion
- type DynSsz
- func (d *DynSsz) GetTree(source any, opts ...CallOption) (*treeproof.Node, error)
- func (d *DynSsz) GetTypeCache() *ssztypes.TypeCache
- func (d *DynSsz) HashTreeRoot(source any, opts ...CallOption) ([32]byte, error)
- func (d *DynSsz) HashTreeRootWith(source any, hh sszutils.HashWalker, opts ...CallOption) error
- func (d *DynSsz) LoadSpecSet(key reflect.Type) []uint64
- func (d *DynSsz) MarshalSSZ(source any, opts ...CallOption) ([]byte, error)
- func (d *DynSsz) MarshalSSZTo(source any, buf []byte, opts ...CallOption) ([]byte, error)
- func (d *DynSsz) MarshalSSZWriter(source any, w io.Writer, opts ...CallOption) error
- func (d *DynSsz) ResolveSpecValue(name string) (bool, uint64, error)
- func (d *DynSsz) SizeSSZ(source any, opts ...CallOption) (int, error)
- func (d *DynSsz) StoreSpecSet(key reflect.Type, set []uint64) []uint64
- func (d *DynSsz) UnmarshalSSZ(target any, ssz []byte, opts ...CallOption) error
- func (d *DynSsz) UnmarshalSSZReader(target any, r io.Reader, size int, opts ...CallOption) error
- func (d *DynSsz) ValidateType(t reflect.Type, opts ...CallOption) error
- type DynSszOption
- func WithAsyncHashing(workers int) DynSszOption
- func WithExtendedTypes() DynSszOption
- func WithLogCb(logCb func(format string, args ...any)) DynSszOption
- func WithMaxNestingDepth(depth int) DynSszOption
- func WithMaxStreamSize(size int) DynSszOption
- func WithNoDelegation() DynSszOption
- func WithNoFastHash() DynSszOption
- func WithNoFastSsz() DynSszOption
- func WithStreamReaderBufferSize(size int) DynSszOption
- func WithStreamWriterBufferSize(size int) DynSszOption
- func WithVerbose() DynSszOption
- type DynSszOptions
- type None
- type TypeWrapper
- type Union
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func SetGlobalSpecs ¶ added in v1.0.2
SetGlobalSpecs replaces the global DynSsz instance with a new one configured with the given specification values. Safe for concurrent use. A reference obtained from GetGlobalDynSsz earlier keeps the instance it got — its specs and caches stay a consistent snapshot, unaffected by the swap.
Types ¶
type CallOption ¶ added in v1.3.0
type CallOption func(*callConfig)
CallOption is a functional option for per-call configuration of MarshalSSZ, UnmarshalSSZ, and HashTreeRoot operations. These options allow runtime customization of SSZ encoding behavior without modifying the DynSsz instance.
func WithStreamSizeLimit ¶ added in v1.4.0
func WithStreamSizeLimit(size int) CallOption
WithStreamSizeLimit bounds one unknown-length UnmarshalSSZReader decode (size < 0). It overrides WithMaxStreamSize for that call; a non-positive value keeps the instance default. A declared size is its own bound and is not affected. The other operations ignore this option.
func WithViewDescriptor ¶ added in v1.3.0
func WithViewDescriptor(view any) CallOption
WithViewDescriptor specifies a view descriptor for fork-dependent SSZ schemas.
The view descriptor defines the SSZ layout (field order, tags, sizes) while the actual data is read from/written to the runtime object. This enables a single runtime type to support multiple SSZ representations for different forks.
The view parameter must be a struct or pointer to struct. Its fields are mapped to the runtime type's fields by name. The view's field types may differ from the runtime type's field types to support nested view descriptors.
When no view descriptor is provided, the runtime type itself is used as the schema.
Example usage:
// Define a view descriptor for Altair fork
type BodyAltairView struct {
RandaoReveal [96]byte
SyncAggregate SyncAggregateAltairView // Nested view type
}
// Marshal with the Altair view
data, err := ds.MarshalSSZ(body, dynssz.WithViewDescriptor(&BodyAltairView{}))
// Unmarshal with the Altair view
err = ds.UnmarshalSSZ(&body, data, dynssz.WithViewDescriptor(&BodyAltairView{}))
// Compute hash tree root with the Altair view
root, err := ds.HashTreeRoot(body, dynssz.WithViewDescriptor(&BodyAltairView{}))
Note: The view descriptor value itself is not used for data storage; only its type information is used to determine the SSZ schema. You can pass a nil pointer of the view type: WithViewDescriptor((*BodyAltairView)(nil))
type CompatibleUnion ¶ added in v1.0.2
CompatibleUnion represents a union type that can hold one of several possible types. It uses Go generics where T is a descriptor struct that defines the union's possible types. The descriptor struct is never instantiated but provides type information through its fields.
EIP-8016 expects the variants to share a compatible merkleization: the same tree shape, with containers carrying the same field names in the same order, collections the same capacity, and only byte/uint8 differing among basics. That is a rule for designing the schema; the library does not verify it (a check may be added later). Diverging variants serialize and hash — the selector is mixed into the root — but no conforming implementation will accept the type.
The union stores: - unionType: uint8 field index indicating which variant is active - data: interface{} holding the actual value
Usage:
type UnionExecutionPayload = dynssz.CompatibleUnion[struct {
ExecutionPayload
ExecutionPayloadWithBlobs
}]
type BlockWithPayload struct {
Slot uint64
ExecutionData UnionExecutionPayload
}
block := BlockWithPayload{
Slot: 123,
ExecutionData: UnionExecutionPayload{
Variant: 1,
Data: ExecutionPayload{
...
},
},
}
func NewCompatibleUnion ¶ added in v1.0.2
func NewCompatibleUnion[T any](variantIndex uint8, data interface{}) (*CompatibleUnion[T], error)
NewCompatibleUnion creates a new CompatibleUnion with the specified variant selector and data. Selectors follow the descriptor struct's field order starting at 1, or the fields' ssz-index tags when present (valid range 1..127 per EIP-8016).
The values are stored verbatim: the selector, the descriptor shape and the data type are validated by the marshal, unmarshal, size and hash operations, not here. Data must be exactly the declared variant type (not a pointer to it). The returned error is always nil and remains for API stability.
func (*CompatibleUnion[T]) GetDescriptorType ¶ added in v1.0.2
func (u *CompatibleUnion[T]) GetDescriptorType() reflect.Type
GetDescriptorType returns the reflect.Type of the descriptor struct T. This allows external code to access the descriptor type information.
type DynSsz ¶
type DynSsz struct {
// contains filtered or unexported fields
}
DynSsz is a dynamic SSZ encoder/decoder that uses runtime reflection to handle dynamic field sizes. It provides flexible SSZ encoding/decoding for any Go data structures that can adapt to different specifications through dynamic field sizing. While commonly used with Ethereum data structures and presets (mainnet, minimal, custom), it works with any SSZ-compatible types.
The instance maintains caches for type descriptors and specification values to optimize performance. It's recommended to reuse the same DynSsz instance across operations to benefit from caching.
Key features:
- Hybrid approach: automatically uses fastssz for static types, reflection for dynamic types
- Type caching: reduces overhead for repeated operations on the same types
- Specification support: handles dynamic field sizes based on runtime specifications
- Thread-safe: can be safely used from multiple goroutines
Example usage:
specs := map[string]any{
"SLOTS_PER_HISTORICAL_ROOT": uint64(8192),
"SYNC_COMMITTEE_SIZE": uint64(512),
}
ds := dynssz.NewDynSsz(specs)
// Marshal
data, err := ds.MarshalSSZ(myStruct)
// Unmarshal
err = ds.UnmarshalSSZ(&myStruct, data)
// Hash tree root
root, err := ds.HashTreeRoot(myStruct)
func GetGlobalDynSsz ¶ added in v1.0.2
func GetGlobalDynSsz() *DynSsz
GetGlobalDynSsz returns the global DynSsz instance, creating one with default settings if none exists. Safe for concurrent use.
func NewDynSsz ¶
func NewDynSsz(specs map[string]any, options ...DynSszOption) *DynSsz
NewDynSsz creates a new instance of the DynSsz encoder/decoder.
The specs map contains dynamic properties and configurations that control SSZ serialization and deserialization. These specifications allow the library to handle different configurations by defining dynamic field sizes at runtime. While commonly used with Ethereum presets (mainnet, minimal, custom), they can define any dynamic sizing parameters for your data structures.
For non-Ethereum use cases, you can define any specifications relevant to your data structures.
The library supports mathematical expressions in dynssz-size tags that reference these specification values, enabling complex dynamic sizing behavior.
Parameters:
- specs: A map of specification names to their values. Can be nil for default behavior.
Returns:
- *DynSsz: A new DynSsz instance ready for encoding/decoding operations
Example:
// Ethereum mainnet specifications
specs := map[string]any{
"SLOTS_PER_HISTORICAL_ROOT": uint64(8192),
"SYNC_COMMITTEE_SIZE": uint64(512),
}
ds := dynssz.NewDynSsz(specs)
// Custom application specifications
customSpecs := map[string]any{
"MAX_ITEMS": uint64(1000),
"BUFFER_SIZE": uint64(4096),
"CUSTOM_ARRAY_LENGTH": uint64(256),
}
dsCustom := dynssz.NewDynSsz(customSpecs)
func (*DynSsz) GetTree ¶ added in v1.1.1
GetTree builds and returns the complete Merkle tree for the given value.
This method constructs a full Merkle tree representation of the SSZ-encoded structure, which is useful for proof generation, debugging, and understanding the internal tree structure. The returned tree can be used to generate Merkle proofs for any field or value within the structure.
The tree construction follows the same SSZ merkleization rules as HashTreeRoot, but instead of returning just the root hash, it provides access to the complete tree with all intermediate nodes. This enables:
- Generating Merkle proofs for specific fields using tree.Prove(index)
- Debugging tree structure with tree.Show(maxDepth)
- Understanding how different fields map to generalized indices
- Analyzing the progressive vs binary tree structures
Parameters:
- source: Any Go value to be converted to a Merkle tree. Must be SSZ-compatible.
Returns:
- *treeproof.Node: The root node of the complete Merkle tree
- error: An error if tree construction fails due to unsupported types or encoding errors
The returned tree supports:
- Navigation: Use Get(index) to fetch nodes by generalized index
- Proof generation: Use Prove(index) to generate Merkle proofs
- Debugging: Use Show(maxDepth) to visualize the tree structure
- Multi-proofs: Use ProveMulti(indices) for efficient batch proofs
Example:
// Build tree for a beacon block
tree, err := ds.GetTree(beaconBlock)
if err != nil {
log.Fatal("Failed to build tree:", err)
}
// Show tree structure (limited to 3 levels deep)
tree.Show(3)
// Generate proof for a specific field at generalized index 25
proof, err := tree.Prove(25)
if err != nil {
log.Fatal("Failed to generate proof:", err)
}
// Verify the proof against the tree root
isValid, err := treeproof.VerifyProof(tree.Hash(), proof)
Note: For progressive containers (with ssz-index tags), the tree structure will be progressive rather than binary, which affects the generalized indices of fields.
On success the returned tree is fully hashed: every branch node already holds its cached hash, so the tree is read-only and safe to share across goroutines for concurrent Prove/ProveMulti/Hash/Value calls. On error the tree is returned alongside it but may be only partially finalized: every hash cached in it is valid, and a later Finalize resumes from those caches, but it must not be shared across goroutines until one succeeds.
func (*DynSsz) GetTypeCache ¶ added in v1.0.0
GetTypeCache returns the type cache for the DynSsz instance.
The type cache stores computed type descriptors for types used in encoding/decoding operations. Type descriptors contain optimized information about how to serialize/deserialize specific types, including field offsets, size information, and whether fastssz can be used.
This method is primarily useful for debugging, performance analysis, or advanced use cases where you need to inspect or manage the cached type information.
Returns:
- *TypeCache: The type cache instance containing all cached type descriptors
Example:
ds := dynssz.NewDynSsz(specs)
cache := ds.GetTypeCache()
// Inspect cached types
types := cache.GetAllTypes()
fmt.Printf("Cache contains %d types\n", len(types))
func (*DynSsz) HashTreeRoot ¶ added in v0.0.6
func (d *DynSsz) HashTreeRoot(source any, opts ...CallOption) ([32]byte, error)
HashTreeRoot computes the hash tree root of the given source object according to SSZ specifications.
The hash tree root is a cryptographic commitment to the entire data structure, used extensively in Ethereum's consensus layer for creating Merkle proofs and maintaining state roots. This method implements the SSZ hash tree root algorithm, which recursively hashes all fields and combines them using binary Merkle trees.
For optimal performance, the method uses a hasher pool to reuse hasher instances across calls. When NoFastHash is false (default), it uses the optimized gohashtree implementation. For types without dynamic fields, it automatically delegates to fastssz's HashTreeRoot method when available.
Parameters:
- source: Any Go value for which to compute the hash tree root
Returns:
- [32]byte: The computed hash tree root
- error: An error if the computation fails due to unsupported types or hashing errors
The method handles all SSZ-supported types including:
- Basic types (bool, uint8, uint16, uint32, uint64)
- Fixed-size and variable-size arrays
- Structs with nested fields
- Slices with proper limit handling
- Bitlists with maximum size constraints
Example:
block := &phase0.BeaconBlock{
Slot: 12345,
ProposerIndex: 42,
// ... other fields
}
root, err := ds.HashTreeRoot(block)
if err != nil {
log.Fatal("Failed to compute root:", err)
}
fmt.Printf("Block root: %x\n", root)
func (*DynSsz) HashTreeRootWith ¶ added in v1.1.1
func (d *DynSsz) HashTreeRootWith(source any, hh sszutils.HashWalker, opts ...CallOption) error
HashTreeRootWith computes the hash tree root of the given source object according to SSZ specifications.
This method is similar to HashTreeRoot, but allows for custom hasher instances to be used. It dynamically handles hashing for types with both static and dynamic field sizes, automatically using fastssz for optimal performance when applicable.
Parameters:
- source: Any Go value for which to compute the hash tree root
- hh: The HashWalker instance to use for hashing. It may be a clean walker or an in-progress one; the source is merkleized as a subtree at the walker's current position. On a non-nil error the walker is left in an indeterminate state (unbalanced scopes / partial buffer); the caller must reset or discard it before reusing it, otherwise a subsequent root is corrupted. (The pooled HashTreeRoot entry point handles this automatically.)
Returns:
- error: An error if the computation fails due to unsupported types or hashing errors
The method handles all SSZ-supported types including:
- Basic types (bool, uint8, uint16, uint32, uint64)
- Fixed-size and variable-size arrays
- Structs with nested fields
- Slices with proper limit handling
- Bitlists with maximum size constraints
Example:
block := &phase0.BeaconBlock{
Slot: 12345,
ProposerIndex: 42,
// ... other fields
}
hh := &hasher.Hasher{}
err := ds.HashTreeRootWith(block, hh)
if err != nil {
log.Fatal("Failed to compute root:", err)
}
fmt.Printf("Block root: %x\n", hh.HashRoot())
func (*DynSsz) LoadSpecSet ¶ added in v1.4.0
LoadSpecSet returns the spec set cached for the generated type key, or nil when no method of that type has resolved one under this instance yet.
func (*DynSsz) MarshalSSZ ¶
func (d *DynSsz) MarshalSSZ(source any, opts ...CallOption) ([]byte, error)
MarshalSSZ serializes the given source into its SSZ (Simple Serialize) representation.
This method dynamically handles the serialization of Go types to SSZ format, supporting both static and dynamic field sizes. For types without dynamic specifications, it automatically uses fastssz for optimal performance. For types with dynamic field sizes (based on runtime specifications), it uses reflection-based processing.
The method allocates a new byte slice for the result. For high-performance scenarios with frequent allocations, consider using MarshalSSZTo with a pre-allocated buffer.
Parameters:
- source: Any Go value to be serialized. Must be a type supported by SSZ encoding.
Returns:
- []byte: The SSZ-encoded data as a new byte slice
- error: An error if serialization fails due to unsupported types, encoding errors, or size mismatches
Supported types include:
- Basic types: bool, uint8, uint16, uint32, uint64
- Arrays and slices of supported types
- Structs with appropriate SSZ tags
- Pointers to supported types
- Types implementing any of the fastssz marshalling interfaces
Example:
header := &phase0.BeaconBlockHeader{
Slot: 12345,
ProposerIndex: 42,
// ... other fields
}
data, err := ds.MarshalSSZ(header)
if err != nil {
log.Fatal("Failed to marshal:", err)
}
fmt.Printf("Encoded %d bytes\n", len(data))
func (*DynSsz) MarshalSSZTo ¶
MarshalSSZTo serializes the given source into its SSZ (Simple Serialize) representation and writes the output to the provided buffer.
This method provides direct control over the output buffer, enabling performance optimizations such as buffer reuse across multiple serialization operations. Like MarshalSSZ, it dynamically handles serialization for types with both static and dynamic field sizes, automatically using fastssz when possible for optimal performance.
The method appends the serialized data to the provided buffer, which allows for efficient concatenation of multiple serialized objects without additional allocations.
Parameters:
- source: Any Go value to be serialized. Must be a type supported by SSZ encoding.
- buf: Pre-allocated byte slice where the serialized data will be appended. Can be nil or empty.
Returns:
- []byte: The updated buffer containing the original data plus the newly serialized data
- error: An error if serialization fails due to unsupported types, encoding errors, or size mismatches
Example:
buf := make([]byte, 0, 1024) // Pre-allocate with expected capacity
for _, block := range blocks {
buf, err = ds.MarshalSSZTo(block, buf)
if err != nil {
log.Fatal("Failed to marshal block:", err)
}
}
fmt.Printf("Serialized %d blocks into %d bytes\n", len(blocks), len(buf))
func (*DynSsz) MarshalSSZWriter ¶ added in v1.2.0
MarshalSSZWriter serializes the given source into its SSZ representation and writes it directly to an io.Writer.
This method provides memory-efficient streaming serialization for SSZ encoding, particularly beneficial for large data structures that would be expensive to buffer entirely in memory. Unlike MarshalSSZ which returns a complete byte slice, this method writes data incrementally to the provided writer, enabling direct output to files, network connections, or other I/O destinations.
The implementation employs several optimizations:
- Internal buffering (default 1KB) to reduce system call overhead for small writes
- Automatic delegation to regular MarshalSSZ for structures smaller than the buffer size
- Seamless integration with fastssz for types without dynamic fields
A stream cannot be seeked to patch offsets in, so the offsets of a value's dynamic children are computed from their sizes before the children are written. Each nesting level sizes its own children, so a deeply nested value is sized once per level it is nested in; on consensus-shaped values the cost is on par with MarshalSSZ.
Parameters:
- source: Any Go value to be serialized. Must be a type supported by SSZ encoding.
- w: The io.Writer destination for the SSZ-encoded output. Common writers include:
- os.File for file output
- net.Conn for network transmission
- bytes.Buffer for in-memory buffering
- Any custom io.Writer implementation
Returns:
- error: An error if serialization fails due to:
- Type validation errors
- I/O write failures
- Size calculation errors for dynamic fields
- Unsupported type structures
On error the writer may already hold part of the encoding. Bytes are written as they are produced, so whatever was encoded before the failure is gone -- and how much that is depends on the value, the buffer size and whether the type has generated code, which reaches the writer sooner. A write failure leaves a partial encoding regardless of any of that.
So a failed call leaves the stream in an unusable state: discard it, or reset it to where it was, rather than writing anything further. A peer reading it has no way to tell a truncated encoding from a complete one. When the reader must see all of it or none, encode with MarshalSSZ and write the result.
Example usage:
// Write directly to a file
file, err := os.Create("beacon_state.ssz")
if err != nil {
log.Fatal(err)
}
defer file.Close()
err = ds.MarshalSSZWriter(state, file)
if err != nil {
log.Fatal("Failed to write state:", err)
}
// Stream over network
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
err = ds.MarshalSSZWriter(block, conn)
func (*DynSsz) ResolveSpecValue ¶ added in v1.0.2
ResolveSpecValue resolves a dynamic specification value by name. The name can be a simple identifier (e.g., "MAX_VALIDATORS_PER_COMMITTEE") or an integer arithmetic expression referencing spec values. Results are cached for subsequent lookups.
Expressions support + - * / % and parentheses over unsigned integer literals and spec identifiers. They are evaluated as exact rationals (full precision across the whole uint64 range, independent of evaluation order) and rounded up to the next whole unit once at the end, since partial bytes/bits cannot be serialized. Modulo requires integer operands. A negative or out-of-range result, division/modulo by zero, or anything beyond this subset is an error.
An identifier or parenthesized group followed by :N resolves on its own the way a size tag resolves against its static fallback: it takes its value rounded up to a whole unit, or N when it is undefined or zero (with :0 it is simply zero then). It binds tighter than the arithmetic operators, so "(A/8):4*8+B:2" is ((A/8):4)*8+(B:2).
Returns whether the value was resolved, the uint64 value, and any parse error. If the name references undefined spec values, resolved will be false with no error.
func (*DynSsz) SizeSSZ ¶
func (d *DynSsz) SizeSSZ(source any, opts ...CallOption) (int, error)
SizeSSZ calculates the size of the given source object when serialized using SSZ encoding.
This method is useful for pre-allocating buffers with the exact size needed for serialization, avoiding unnecessary allocations and resizing. It dynamically evaluates the size based on the actual values in the source object, accurately handling variable-length fields such as slices and dynamic arrays.
For types without dynamic fields, the size is calculated using the optimized fastssz SizeSSZ method when available. For types with dynamic fields, it traverses the entire structure to compute the exact serialized size.
Parameters:
- source: Any Go value whose SSZ-encoded size needs to be calculated
Returns:
- int: The exact number of bytes that would be produced by MarshalSSZ for this source
- error: An error if the size calculation fails due to unsupported types or invalid data
For a value that encodes, the size is exact: it is the length MarshalSSZ produces, and both engines agree on it.
For a value that does not encode -- a union holding data that does not match its selector, or a list past its limit, say -- the number means nothing, and what comes back depends on which engine ran. A type with generated code sizes itself through DynamicSizer, which returns a bare int and so cannot report the problem: it answers with whatever the shape adds up to, a plausible number that no encoding of the value will ever match, or with 0 where the shape itself is refused. The reflection engine returns an error instead.
So use this to size a buffer, not to decide whether a value is encodable. MarshalSSZ rejects such a value in either engine.
Example:
state := &phase0.BeaconState{
// ... populated state fields
}
size, err := ds.SizeSSZ(state)
if err != nil {
log.Fatal("Failed to calculate size:", err)
}
// Pre-allocate buffer with exact size
buf := make([]byte, 0, size)
buf, err = ds.MarshalSSZTo(state, buf)
func (*DynSsz) StoreSpecSet ¶ added in v1.4.0
StoreSpecSet caches set for the generated type key. The spec values of an instance never change, so two callers that resolved the set at the same time hold equal sets; the first one stored is the one every caller shares.
func (*DynSsz) UnmarshalSSZ ¶
func (d *DynSsz) UnmarshalSSZ(target any, ssz []byte, opts ...CallOption) error
UnmarshalSSZ decodes the given SSZ-encoded data into the target object.
This method is the counterpart to MarshalSSZ, reconstructing Go values from their SSZ representation. It dynamically handles decoding for types with both static and dynamic field sizes, automatically using fastssz for optimal performance when applicable.
The target must be a pointer to a value of the appropriate type. The method will allocate memory for slices and initialize pointer fields as needed during decoding.
Parameters:
- target: A pointer to the Go value where the decoded data will be stored. Must be a pointer.
- ssz: The SSZ-encoded data to decode
Returns:
- error: An error if decoding fails due to:
- Invalid SSZ format
- Type mismatches between the data and target
- Insufficient or excess data
- Unsupported types
The method ensures that all bytes in the ssz parameter are consumed during decoding. If there are leftover bytes, an error is returned indicating incomplete consumption.
Example:
var header phase0.BeaconBlockHeader
err := ds.UnmarshalSSZ(&header, encodedData)
if err != nil {
log.Fatal("Failed to unmarshal:", err)
}
fmt.Printf("Decoded header for slot %d\n", header.Slot)
func (*DynSsz) UnmarshalSSZReader ¶ added in v1.2.0
UnmarshalSSZReader decodes SSZ-encoded data from an io.Reader directly into the target object.
This method implements memory-efficient streaming deserialization for SSZ data, reading incrementally from any io.Reader source. Unlike UnmarshalSSZ which requires the complete data in memory as a byte slice, this method processes data in chunks, making it ideal for large files, network streams, or memory-constrained environments.
The implementation handles SSZ's offset-based encoding for dynamic fields by:
- Reading offsets to determine field boundaries for variable-length data
- Using limited readers to enforce exact byte consumption per field
- Processing static fields directly from the stream
- Dynamically allocating slices based on discovered sizes
Parameters:
- target: A pointer to the Go value where decoded data will be stored. Must be a pointer to a type compatible with SSZ decoding. The method will allocate memory for slices and initialize pointer fields as needed during decoding.
- r: An io.Reader source containing the SSZ-encoded data. Common readers include:
- os.File for file input
- net.Conn for network reception
- bytes.Reader for in-memory data
- Any custom io.Reader implementation
- size: The expected total size of the SSZ data in bytes. A negative size enables "unknown size" mode: the payload is consumed to EOF without being materialised, so the memory savings of streaming still apply.
A non-negative size is trusted input. It is the extent every region is measured against and the most that is read from r, and the decoder sizes its allocations from it before the bytes arrive; it is not subject to WithMaxStreamSize. It must therefore come from a source you control, such as the stat() result of a local file or a length your own protocol has already validated, never from an untrusted remote peer. A size taken from untrusted framing lets the peer choose how much memory the decode commits before sending anything: the heap reserved for a list is the declared element count times the Go element size, so a few bytes carrying a declared 64 MiB list of pointer elements reserve 512 MiB up front and hold it for as long as the peer keeps the connection open. For input that cannot be trusted pass a negative size and bound the decode with WithMaxStreamSize or WithStreamSizeLimit instead.
Unknown-size mode is possible because SSZ is self-delimiting for every region except the trailing one, so the missing length only ever affects the last dynamic child at each nesting level. It is bounded by WithMaxStreamSize, or by WithStreamSizeLimit when the call passes one. That is a wire-byte allowance, not a deadline, cancellation mechanism, or decoded-object heap limit. Use the smallest application-specific cap your schema permits.
EOF is the message boundary in unknown-size mode. A raw connection is safe only when it carries one SSZ payload and EOF unambiguously ends that payload. Long-lived network readers need protocol framing instead; every network reader also needs a deadline or cancellation mechanism, since a peer can stay below the byte allowance while withholding EOF forever. io.Reader itself has no context support, so cancellation usually closes the reader/connection.
Two caveats apply to unknown-size mode:
- Errors are less precise. Checks that a known length would catch up front — a truncated fixed section, an offset past the end, a misaligned list — instead surface as ErrUnexpectedEOF when the read runs out. The accept/reject decision is unchanged; only the diagnostic differs.
- Types whose SSZ methods were produced by dynamic-ssz v1.3.2 or earlier must be regenerated before decoding without a size. Older generated decoders size the trailing region from the remaining-length estimate, so they reject valid input or allocate up to WithMaxStreamSize before failing. Passing an explicit size keeps working with them.
Returns:
- error: An error if decoding fails due to:
- I/O read failures
- Invalid SSZ format or structure
- Type mismatches between data and target
- Unexpected EOF or excess data
- Size constraint violations
The method ensures strict compliance with SSZ specifications, validating that:
- All expected bytes are consumed (when size is specified)
- Dynamic field offsets are valid and properly ordered
- Field boundaries are respected
- No data is left unread
Example usage:
// Read from file
file, err := os.Open("beacon_state.ssz")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Get file size for exact reading
info, _ := file.Stat()
var state phase0.BeaconState
err = ds.UnmarshalSSZReader(&state, file, info.Size())
if err != nil {
log.Fatal("Failed to read state:", err)
}
// Read one EOF-framed payload from a connection. A deadline is required:
// WithMaxStreamSize limits bytes, not how long the peer may withhold EOF.
conn, _ := net.Dial("tcp", "localhost:8080")
defer conn.Close()
_ = conn.SetReadDeadline(time.Now().Add(15 * time.Second))
var block phase0.BeaconBlock
err = ds.UnmarshalSSZReader(&block, conn, -1)
func (*DynSsz) ValidateType ¶ added in v1.0.2
func (d *DynSsz) ValidateType(t reflect.Type, opts ...CallOption) error
ValidateType validates whether a given type is compatible with SSZ encoding/decoding.
This method performs a comprehensive analysis of the provided type to determine if it can be successfully serialized and deserialized according to SSZ specifications. It recursively validates all nested types within structs, arrays, and slices, ensuring complete compatibility throughout the type hierarchy.
The validation process checks for:
- Supported primitive types (bool, uint8, uint16, uint32, uint64)
- Valid composite types (arrays, slices, structs)
- Proper SSZ tags on slice fields (ssz-size, ssz-max, dynssz-size, dynssz-max)
- Correct tag syntax and values
- No unsupported types (maps, channels, functions, multi-level pointers; signed integers and floats need WithExtendedTypes)
When a view descriptor is provided via WithViewDescriptor option, the method validates:
- The schema type (view descriptor) is compatible with SSZ
- Schema fields can be mapped to corresponding runtime fields by name
- Field type compatibility between schema and runtime types
This method is particularly useful for:
- Pre-validation before attempting marshalling/unmarshalling operations
- Development-time type checking to catch errors early
- Runtime validation of dynamically constructed types
- Ensuring type compatibility when integrating with external systems
- Validating view descriptor compatibility with runtime types
Parameters:
- t: The reflect.Type to validate for SSZ compatibility
- opts: Optional CallOptions, including WithViewDescriptor for fork-dependent validation
Returns:
- error: nil if the type is valid for SSZ encoding/decoding, or a descriptive error explaining why the type is incompatible. The error message includes details about the specific field or type that caused the validation failure.
A type can be valid for encoding yet refuse a hash tree root: a list or bitlist without an ssz-max limit encodes, but has no standard SSZ hash tree root unless extended types hash it as an unbounded list. ValidateType answers the encoding question.
Example usage:
type MyStruct struct {
ValidField uint64
InvalidField map[string]int // maps cannot be serialized
}
err := ds.ValidateType(reflect.TypeOf(MyStruct{}))
if err != nil {
log.Fatal("Type validation failed:", err)
}
// With view descriptor validation:
err = ds.ValidateType(reflect.TypeOf(RuntimeBody{}), WithViewDescriptor(&AltairBodyView{}))
The method validates at the type level without requiring an instance of the type, making it suitable for early validation scenarios. For performance-critical paths, validation results can be cached as type compatibility doesn't change at runtime.
type DynSszOption ¶ added in v1.2.0
type DynSszOption func(*DynSszOptions)
DynSszOption is a functional option for configuring a DynSsz instance.
func WithAsyncHashing ¶ added in v1.4.0
func WithAsyncHashing(workers int) DynSszOption
WithAsyncHashing gates this instance's HashTreeRoot computations into background subtree reduction: large subtrees hash on worker goroutines while the walker keeps serializing, which speeds up large lists (such as a beacon state's validator registry) severalfold. Roots are identical to synchronous hashing.
workers > 0 also configures the process-wide worker limit (shared by all instances, see hasher.EnableAsyncHashing); workers == 0 only gates this instance in and leaves the process-wide configuration untouched. HashTreeRootWith is unaffected: the caller owns that hasher and gates it explicitly via its SetAsyncHashing method.
func WithExtendedTypes ¶ added in v1.2.2
func WithExtendedTypes() DynSszOption
WithExtendedTypes creates an option to enable extended type support.
When this option is enabled, dynssz will support nun-specified types like signed integers, floating point numbers, big integers and more. Generated SSZ code is incompatible with other SSZ libraries like fastssz.
Note: this gate only applies to reflection-based processing. Generated code opts into extended types at generation time (dynssz-gen's with-extended-types flag / the codegen WithExtendedTypes option) and is not re-gated at runtime: a type whose generated methods use extended types will marshal/unmarshal them regardless of this option on the consuming DynSsz instance.
func WithLogCb ¶ added in v1.2.0
func WithLogCb(logCb func(format string, args ...any)) DynSszOption
WithLogCb sets a custom logging callback for debug output during SSZ operations.
func WithMaxNestingDepth ¶ added in v1.4.0
func WithMaxNestingDepth(depth int) DynSszOption
WithMaxNestingDepth bounds how deeply a value may nest while being encoded, decoded or hashed. Defaults to 1024 if not set or set to a non-positive value.
The bound exists because stack exhaustion is fatal in Go: the runtime aborts the process and recover() cannot contain it, so a server could not isolate the failure to the request that caused it. Exceeding the bound returns sszutils.ErrMaxDepthExceeded instead.
Only a recursive type -- one whose cycle closes through a variable-length field -- can nest to a depth the input controls, so the count advances only through the types that lie on such a cycle: one level per cycle member descended into, with the outermost value itself costing nothing. A trip around a cycle costs as many levels as the cycle has structural members (a type wrapper adds none). Every other type bottoms out at a depth fixed by its own structure and is unaffected, as is the cost of encoding it.
Generated code counts by the same rule with its own bound, fixed at generation time (see the code generator's WithRecursionDepth option), so both engines accept and reject at identical nesting depths. The one caveat is a chain of distinct cycles spanning several packages: generated code restarts its count where such a chain crosses a package boundary (the depth-carrying methods are unexported), while the reflection walk counts it as one run. Each side stays bounded either way.
func WithMaxStreamSize ¶ added in v1.4.0
func WithMaxStreamSize(size int) DynSszOption
WithMaxStreamSize sets the upper bound on the total size of an SSZ payload decoded by UnmarshalSSZReader without a size (size < 0): the decode stops at the bound. A declared size is its own bound and is not subject to it: the decode sizes its allocations from that declaration before the bytes arrive, so setting this alongside a declared size does nothing. Pass a negative size to put the decode under this bound instead. Defaults to sszutils.DefaultMaxStreamSize (512 MiB) if not set or set to a non-positive value. WithStreamSizeLimit overrides it for a single call.
The bound is deliberate and cannot be disabled: an unknown-length decode would otherwise let an endless input drive unbounded wire buffering. It also doubles as the remaining-length estimate reported to decode paths that have not been taught about regions of unknown extent.
This is not a deadline, cancellation mechanism, or decoded-object heap limit. Network callers must impose their own lifetime bound, and should choose the smallest maximum their protocol and schema permit.
func WithNoDelegation ¶ added in v1.4.0
func WithNoDelegation() DynSszOption
WithNoDelegation disables delegation to a type's own generated Dynamic* SSZ methods (MarshalSSZDyn, UnmarshalSSZDyn, HashTreeRootWith and friends, including their DynamicView* variants). Fastssz-style methods are governed separately by WithNoFastSsz for a type without a dynamic surface. A type that carries the complete dynamic surface is walked by reflection even when it also has static methods: a -legacy generation's static methods forward to the global instance's specs, and only the method set tells them from real static bodies.
This differs from WithNoFastSsz, which only disables the legacy fastssz fallback: WithNoFastSsz leaves generated dynamic methods in charge, whereas WithNoDelegation bypasses them as well. Combine both to run entirely on reflection, which is primarily useful for differential testing a type's generated code against the reflection implementation.
Custom types (ssz-type:"custom" and types with no reflection representation) always delegate to their own methods regardless of this option, since the reflection engine cannot serialize them.
func WithNoFastHash ¶ added in v1.2.0
func WithNoFastHash() DynSszOption
WithNoFastHash disables the accelerated hashtree hashing library, falling back to the native Go sha256 implementation.
func WithNoFastSsz ¶ added in v1.2.0
func WithNoFastSsz() DynSszOption
WithNoFastSsz disables fastssz fallback for types that implement fastssz interfaces, forcing all operations through reflection-based encoding.
func WithStreamReaderBufferSize ¶ added in v1.3.0
func WithStreamReaderBufferSize(size int) DynSszOption
WithStreamReaderBufferSize sets the maximum internal buffer size for the streaming SSZ decoder used by UnmarshalSSZReader. Defaults to 2KB if not set.
func WithStreamWriterBufferSize ¶ added in v1.3.0
func WithStreamWriterBufferSize(size int) DynSszOption
WithStreamWriterBufferSize sets the internal buffer size for the streaming SSZ encoder used by MarshalSSZWriter. Defaults to 2KB if not set.
func WithVerbose ¶ added in v1.2.0
func WithVerbose() DynSszOption
WithVerbose enables verbose debug logging during SSZ operations.
type DynSszOptions ¶ added in v1.2.0
type DynSszOptions struct {
NoFastSsz bool
NoDelegation bool
NoFastHash bool
AsyncHashing bool
AsyncHashingWorkers int
ExtendedTypes bool
Verbose bool
LogCb func(format string, args ...any)
StreamWriterBufferSize int
StreamReaderBufferSize int
MaxStreamSize int
MaxNestingDepth int
}
DynSszOptions holds the configuration options for a DynSsz instance.
type None ¶ added in v1.4.0
type None struct{}
None marks the empty option of a classic SSZ Union. Declared as the FIRST field of a Union descriptor struct, it makes selector 0 the None variant:
type MaybePayload = dynssz.Union[struct {
None dynssz.None // selector 0: no value
Full ExecutionPayload
}]
A None-valued union serializes as the single byte 0x00 and hashes as mix_in_selector(Bytes32(), 0). The SSZ spec only allows None as the first option, and a union declaring it must offer at least one other variant.
type TypeWrapper ¶ added in v1.0.2
type TypeWrapper[D, T any] struct { Data T }
TypeWrapper represents a wrapper type that can provide SSZ annotations for non-struct types. It uses Go generics where D is a WrapperDescriptor struct that must have exactly 1 field, and T is the actual value type. The descriptor struct is never instantiated but provides type information with annotations.
The wrapper stores: - data: the actual value of type T
Usage:
type ByteSliceDescriptor struct {
Data []byte `ssz-size:"32"`
}
type WrappedByteSlice = dynssz.TypeWrapper[ByteSliceDescriptor, []byte]
// Use in a struct or standalone
wrapped := WrappedByteSlice{}
wrapped.Set([]byte{1, 2, 3, 4})
data := wrapped.Get() // returns []byte
func NewTypeWrapper ¶ added in v1.0.2
func NewTypeWrapper[D, T any](data T) (*TypeWrapper[D, T], error)
NewTypeWrapper creates a new TypeWrapper with the specified data.
func (*TypeWrapper[D, T]) Get ¶ added in v1.0.2
func (w *TypeWrapper[D, T]) Get() T
Get returns the wrapped value.
func (*TypeWrapper[D, T]) GetDescriptorType ¶ added in v1.0.2
func (w *TypeWrapper[D, T]) GetDescriptorType() reflect.Type
GetDescriptorType returns the reflect.Type of the descriptor struct D. This allows external code to access the descriptor type information.
func (*TypeWrapper[D, T]) Set ¶ added in v1.0.2
func (w *TypeWrapper[D, T]) Set(value T)
Set sets the wrapped value.
type Union ¶ added in v1.4.0
Union represents a classic SSZ union: an ordered set of variants addressed by 0-based positional selectors, following the consensus-spec Union rules. T is a descriptor struct whose fields define the variants in order; it is never instantiated and only provides type information. Selectors above 127 are reserved by the spec and rejected.
The union stores the active selector and the value. A union whose descriptor declares dynssz.None as its first field represents the empty option as {Variant: 0, Data: nil}.
Variants carry no merkleization expectations (CompatibleUnion's EIP-8016 compatibility rule does not apply here) and the selector is assigned by position alone (ssz-index tags are not allowed).
func NewUnion ¶ added in v1.4.0
NewUnion creates a new Union with the specified variant selector and data. Selectors are the 0-based positions of the descriptor struct's fields; when the descriptor declares dynssz.None first, selector 0 with nil data is the empty option.
The values are stored verbatim: the selector, the descriptor shape and the data type are validated by the marshal, unmarshal, size and hash operations, not here. Data must be exactly the declared variant type (not a pointer to it). The returned error is always nil and remains for API stability.
func (*Union[T]) GetDescriptorType ¶ added in v1.4.0
GetDescriptorType returns the reflect.Type of the descriptor struct T. This allows external code to access the descriptor type information.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package codegen provides code generation for dynamic SSZ types.
|
Package codegen provides code generation for dynamic SSZ types. |
|
Package main implements the dynssz-gen command.
|
Package main implements the dynssz-gen command. |
|
testpkg
Package testpkg is a fixture for dynssz-gen coverage tests.
|
Package testpkg is a fixture for dynssz-gen coverage tests. |
|
testpkg/extra
Package extra registers an annotation for a type declared in viewfix/sub with a key testpkg registers too.
|
Package extra registers an annotation for a type declared in viewfix/sub with a key testpkg registers too. |
|
testpkg/third
Package third registers an annotation for a type declared in another package (viewfix/sub) that it neither declares nor generates; testpkg imports it, so the registration applies wherever testpkg's code runs.
|
Package third registers an annotation for a type declared in another package (viewfix/sub) that it neither declares nor generates; testpkg imports it, so the registration applies wherever testpkg's code runs. |
|
testpkg/viewfix
Package viewfix is a dynssz-gen coverage fixture for external view-type loading.
|
Package viewfix is a dynssz-gen coverage fixture for external view-type loading. |
|
testpkg/viewfix/sub
Package sub holds view types for the viewfix.Base fixture.
|
Package sub holds view types for the viewfix.Base fixture. |
|
Package reflection provides runtime reflection-based SSZ encoding, decoding, and hash tree root computation.
|
Package reflection provides runtime reflection-based SSZ encoding, decoding, and hash tree root computation. |
|
Package ssztypes provides SSZ type descriptors, caching, and struct tag parsing for the dynamic-ssz library.
|
Package ssztypes provides SSZ type descriptors, caching, and struct tag parsing for the dynamic-ssz library. |
|
Package sszutils provides shared interfaces, encoding/decoding primitives, and utility functions for SSZ (Simple Serialize) operations.
|
Package sszutils provides shared interfaces, encoding/decoding primitives, and utility functions for SSZ (Simple Serialize) operations. |
|
test
module
|
|
|
tests
|
|
|
fuzz/cmd/fuzz
command
Command fuzz runs the SSZ fuzzer against generated corpus types.
|
Command fuzz runs the SSZ fuzzer against generated corpus types. |
|
fuzz/cmd/generate
command
Command generate creates random SSZ-compatible Go types, runs codegen on them, and produces a compilable corpus for the fuzzer.
|
Command generate creates random SSZ-compatible Go types, runs codegen on them, and produces a compilable corpus for the fuzzer. |
|
fuzz/corpus
Package corpus contains generated SSZ types for fuzz testing.
|
Package corpus contains generated SSZ types for fuzz testing. |
|
fuzz/engine
Package engine implements the core fuzzing logic for comparing SSZ reflection-based and codegen-based implementations.
|
Package engine implements the core fuzzing logic for comparing SSZ reflection-based and codegen-based implementations. |
|
fuzz/typegen
Package typegen generates random SSZ-compatible Go struct definitions for fuzz testing.
|
Package typegen generates random SSZ-compatible Go struct definitions for fuzz testing. |
|
fuzz/walker
Package walker drives both sszutils.HashWalker implementations over the same generated call sequence and compares what they build.
|
Package walker drives both sszutils.HashWalker implementations over the same generated call sequence and compares what they build. |
|
promomatrix
Package promomatrix holds a generated matrix of embedding shapes.
|
Package promomatrix holds a generated matrix of embedding shapes. |
|
sizeguard
Package sizeguard holds the matrix of every construct the code generator forms or bounds a size from, against every generated path that forms one.
|
Package sizeguard holds the matrix of every construct the code generator forms or bounds a size from, against every generated path that forms one. |
|
sszspecs
Package sszspecs runs the official ethereum/ssz-specs vectors against dynamic-ssz.
|
Package sszspecs runs the official ethereum/ssz-specs vectors against dynamic-ssz. |
|
sszspecs/descriptor
Package descriptor reads the type descriptors the official SSZ vectors carry.
|
Package descriptor reads the type descriptors the official SSZ vectors carry. |
|
sszspecs/gen
command
Command gen writes the Go types the official SSZ vectors declare.
|
Command gen writes the Go types the official SSZ vectors declare. |
|
Package treeproof provides Merkle tree construction and proof generation for SSZ structures.
|
Package treeproof provides Merkle tree construction and proof generation for SSZ structures. |