dynssz

package module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 1, 2025 License: Apache-2.0 Imports: 9 Imported by: 16

README

Dynamic SSZ (dynssz)

Dynamic SSZ (dynssz) is a Go library designed to provide flexible and dynamic SSZ encoding/decoding for any Go data structures. It stands out by using runtime reflection to handle serialization and deserialization of types with variable field sizes, enabling it to support dynamic specifications and configurations. While commonly used with Ethereum data structures and presets (mainnet, minimal, custom), it works with any SSZ-compatible types. dynssz integrates with fastssz to leverage static type information for encoding/decoding when possible, but its primary advantage lies in its ability to adapt to dynamic field sizes that are not well-suited to static code generation methods.

dynssz is designed to bridge the gap between the efficiency of static SSZ encoding/decoding and the flexibility required for handling dynamic data structures. It achieves this through a hybrid approach that combines the best of both worlds: leveraging fastssz for static types and dynamically processing types with variable sizes.

Benefits

  • Flexibility: Supports any SSZ-compatible data structures with custom and dynamic specifications, not limited to Ethereum types.
  • Hybrid Efficiency: Balances the efficiency of static processing with the flexibility of dynamic handling, optimizing performance where possible.
  • Developer-Friendly: Simplifies the handling of SSZ data for developers by abstracting the complexity of dynamic data processing.
  • General Purpose: Works with any Go types that follow SSZ serialization rules, making it suitable for various applications beyond blockchain.

Installation

To install dynssz, use the go get command:

go get github.com/pk910/dynamic-ssz

This will download and install the dynssz package into your Go workspace.

Usage

Supported Types

Dynamic SSZ supports only SSZ-compatible types as defined in the SSZ specification:

Base Types:

  • uint8, uint16, uint32, uint64 (unsigned integers)
  • bool (boolean values)
  • Fixed-size byte arrays (e.g., [32]byte)
  • string (handled as []byte)

Composite Types:

  • Arrays and slices of supported types
  • Structs containing only supported types
  • Pointers to structs (treated as optional fields)

Not Supported:

  • Signed integers (int, int8, int16, int32, int64)
  • Floating-point numbers (float32, float64)
  • Maps, channels, functions, complex numbers
  • Interfaces (except when referring to concrete SSZ-compatible types)
Struct Tag Annotations for Dynamic Encoding/Decoding

dynssz utilizes struct tag annotations to indicate how fields should be encoded/decoded, supporting both static and dynamic field sizes:

Size Tags
  • ssz-size: Defines field sizes. This tag follows the same format supported by fastssz, allowing seamless integration. Use ? to indicate dynamic length dimensions, or specify a number for fixed-size arrays/slices. Note: Fixed-size fields (those with numeric values in ssz-size) do not use ssz-max tags.

  • dynssz-size: Specifies sizes based on specification properties, extending the flexibility of dynssz to adapt to various Ethereum presets. Unlike the straightforward ssz-size, dynssz-size supports not only direct references to specification values but also simple mathematical expressions. This feature allows for dynamic calculation of field sizes based on spec values, enhancing the dynamic capabilities of dynssz.

    The dynssz-size tag can interpret and evaluate expressions involving one or multiple spec values, offering a versatile approach to defining dynamic sizes. For example:

    • A direct reference to a single spec value might look like dynssz-size:"SPEC_VALUE".
    • A simple mathematical expression based on a spec value could be dynssz-size:"(SPEC_VALUE*2)-5", enabling the size to be dynamically adjusted according to the spec value.
    • For more complex scenarios involving multiple spec values, the tag can handle expressions like dynssz-size:"(SPEC_VALUE1*SPEC_VALUE2)+SPEC_VALUE3", providing a powerful tool for defining sizes that depend on multiple dynamic specifications.

    When processing a field with a dynssz-size tag, dynssz evaluates the expression to determine the actual size. If the resolved size deviates from the default established by ssz-size, the library switches to dynamic handling for that field. This mechanism ensures that dynssz can accurately and efficiently encode or decode data structures, taking into account the intricate sizing requirements dictated by dynamic Ethereum presets.

Maximum Size Tags (Required for Dynamic Fields)
  • ssz-max: Defines the maximum number of elements for dynamic length fields. This tag is required for all dynamic length fields (slices with ? in ssz-size or no ssz-size tag) to properly calculate the hash tree root. The maximum size determines the merkle tree depth and is essential for SSZ compliance.

  • dynssz-max: Similar to dynssz-size, this tag allows specification-based maximum sizes with support for mathematical expressions. This enables dynamic adjustment of maximum bounds based on specification values.

Important: Every dynamic length field (those with ? in ssz-size or without ssz-size) must have either an ssz-max or dynssz-max tag. Without these tags, the hash tree root calculation cannot determine the appropriate merkle tree structure. Fixed-size fields (e.g., ssz-size:"32") should not have max tags.

Type Specification Tags (Strict Type System)
  • ssz-type: Explicitly specifies the SSZ type for a field, overriding automatic type detection. This strict type system allows precise control over how Go types are interpreted for SSZ encoding. Supported values include:
    • Basic types: "bool", "uint8", "uint16", "uint32", "uint64", "uint128", "uint256"
    • Composite types: "container", "list", "vector", "bitlist", "bitvector"
    • Special values: "?" or "auto" (automatic detection), "custom" (fastssz implementation)

This is particularly useful for:

  • Distinguishing bitlists from regular byte slices
  • Handling large integers (uint128, uint256) not natively supported by Go
  • Ensuring compatibility with specific SSZ implementations

The library also provides automatic type detection for well-known types like github.com/holiman/uint256.Int (detected as uint256).

Multi-dimensional Slices

dynssz supports multi-dimensional slices with different size constraints at each dimension. When using multi-dimensional arrays or slices, you can specify sizes and maximums for each dimension using comma-separated values:

// Two-dimensional byte slice: outer dynamic up to 100, inner fixed at 32 bytes
Field1 [][]byte `ssz-size:"?,32" ssz-max:"100"`

// Two-dimensional uint8 slice: both dimensions dynamic
Field2 [][]uint8 `ssz-size:"?,?" ssz-max:"64,256"`

// Mixed fixed and dynamic dimensions
Field3 [][4]byte `ssz-size:"?" ssz-max:"128" dynssz-max:"MAX_ITEMS"`

Key points for multi-dimensional fields:

  • Sizes and maximums are specified in order from outermost to innermost dimension
  • Use ? in ssz-size to indicate dynamic length dimensions
  • Each dynamic dimension requires a corresponding maximum value
  • Empty values in comma-separated lists can be used for fixed-size dimensions

Multi-dimensional slices are fully supported for all operations including hash tree root calculations, encoding, and decoding.

Fields with static sizes do not need the dynssz-size tag. Here's an example of a structure using various tag combinations:

type Example struct {
    // Fixed-size fields (no ssz-max needed)
    FixedArray     [32]byte                    // Fixed array, no tags needed
    FixedSlice     []byte    `ssz-size:"32"`   // Fixed-size slice of 32 bytes
    Fixed2D        [][]byte  `ssz-size:"4,32"` // Fixed 4x32 byte matrix
    
    // Dynamic-size fields (ssz-max required)
    DynamicSlice   []byte    `ssz-max:"1024"`                           // Dynamic slice, max 1024 bytes
    DynamicSlice2  []byte    `ssz-size:"?" ssz-max:"2048"`              // Explicit dynamic marker
    Dynamic2D      [][]byte  `ssz-size:"?,32" ssz-max:"100"`            // Dynamic outer, fixed inner
    FullyDynamic   [][]byte  `ssz-size:"?,?" ssz-max:"64,256"`          // Both dimensions dynamic
    
    // With dynamic specifications
    SpecDynamic    []uint64  `ssz-max:"1000" dynssz-max:"MAX_ITEMS"`    // Dynamic max from spec
    SpecFixed      []byte    `ssz-size:"256" dynssz-size:"BUFFER_SIZE"` // Fixed size from spec
    
    // With strict type annotations
    Bitlist        []byte    `ssz-type:"bitlist" ssz-max:"256"`         // Explicitly a bitlist
    Balance        [32]byte  `ssz-type:"uint256"`                       // Explicitly uint256
    Flags          [8]byte   `ssz-type:"bitvector"`                     // Fixed bitvector
}

// Real-world Ethereum example
type BeaconState struct {
    GenesisTime                  uint64
    GenesisValidatorsRoot        phase0.Root `ssz-size:"32"`
    Slot                         phase0.Slot
    Fork                         *phase0.Fork
    LatestBlockHeader            *phase0.BeaconBlockHeader
    BlockRoots                   []phase0.Root `ssz-size:"8192,32" dynssz-size:"SLOTS_PER_HISTORICAL_ROOT,32"`
    StateRoots                   []phase0.Root `ssz-size:"8192,32" dynssz-size:"SLOTS_PER_HISTORICAL_ROOT,32"`
    HistoricalRoots              []phase0.Root `ssz-size:"?,32" ssz-max:"16777216" dynssz-max:"HISTORICAL_ROOTS_LIMIT"`
    Validators                   []Validator   `ssz-max:"1099511627776" dynssz-max:"VALIDATOR_REGISTRY_LIMIT"`
    PreviousEpochParticipation   []byte        `ssz-max:"1099511627776" dynssz-max:"VALIDATOR_REGISTRY_LIMIT"`
    ...
}
Creating a New DynSsz Instance
import "github.com/pk910/dynamic-ssz"

// Define your dynamic specifications
// For Ethereum use case:
ethSpecs := map[string]any{
    "SYNC_COMMITTEE_SIZE": uint64(32),
    "SLOTS_PER_HISTORICAL_ROOT": uint64(8192),
    // ...
}

// For custom application use case:
customSpecs := map[string]any{
    "MAX_ITEMS": uint64(1000),
    "BUFFER_SIZE": uint64(4096),
    // ...
}

ds := dynssz.NewDynSsz(ethSpecs)
// or
ds := dynssz.NewDynSsz(customSpecs)
Marshaling an Object
data, err := ds.MarshalSSZ(myObject)
if err != nil {
    log.Fatalf("Failed to marshal SSZ: %v", err)
}
Unmarshaling an Object
err := ds.UnmarshalSSZ(&myObject, data)
if err != nil {
    log.Fatalf("Failed to unmarshal SSZ: %v", err)
}

Performance

The performance of dynssz has been benchmarked against fastssz using BeaconBlocks and BeaconStates from small kurtosis testnets, providing a consistent and comparable set of data. These benchmarks compare three scenarios: exclusively using fastssz, exclusively using dynssz, and a combined approach where dynssz defaults to fastssz for static types that do not require dynamic processing. The results highlight the balance between flexibility and speed:

Legend:

  • First number: Unmarshalling time in milliseconds.
  • Second number: Marshalling time in milliseconds.
  • Third number: Hash tree root time in milliseconds.
Mainnet Preset
BeaconBlock Decode + Encode + Hash (10,000 times)
  • fastssz only: [8 ms / 3 ms / 88 ms] success
  • dynssz only: [27 ms / 12 ms / 63 ms] success
  • dynssz + fastssz: [8 ms / 3 ms / 64 ms] success
BeaconState Decode + Encode + Hash (10,000 times)
  • fastssz only: [5849 ms / 4960 ms / 73087 ms] success
  • dynssz only: [22544 ms / 12256 ms / 40181 ms] success
  • dynssz + fastssz: [5728 ms / 4857 ms / 37191 ms] success
Minimal Preset
BeaconBlock Decode + Encode + Hash (10,000 times)
  • fastssz only: [0 ms / 0 ms / 0 ms] failed (unmarshal error)
  • dynssz only: [44 ms / 29 ms / 90 ms] success
  • dynssz + fastssz: [22 ms / 13 ms / 151 ms] success
BeaconState Decode + Encode + Hash (10,000 times)
  • fastssz only: [0 ms / 0 ms / 0 ms] failed (unmarshal error)
  • dynssz only: [796 ms / 407 ms / 1816 ms] success
  • dynssz + fastssz: [459 ms / 244 ms / 4712 ms] success

These results showcase the dynamic processing capabilities of dynssz, particularly its ability to handle data structures that fastssz cannot process due to its static nature. While dynssz introduces additional processing time, its flexibility allows it to successfully manage both mainnet and minimal presets. The combined dynssz and fastssz approach significantly improves performance while maintaining this flexibility, making it a viable solution for applications requiring dynamic SSZ processing.

Testing

The library includes comprehensive testing infrastructure:

  • Unit Tests: Fast, isolated tests for core functionality
  • Spec Tests: Ethereum consensus specification compliance tests
  • Examples: Working examples that are automatically tested
  • Performance Tests: Benchmarking and regression testing
Running Spec Tests
cd spectests
./run_tests.sh mainnet  # Run mainnet preset tests
./run_tests.sh minimal  # Run minimal preset tests

The spec tests automatically download the latest consensus spec test data and validate the library against the official Ethereum test vectors.

Internal Technical Overview

Key Components
  • Type and Value Size Calculation: The library distinguishes between type sizes (static sizes of types or -1 for dynamic types) and value sizes (the absolute size of an instance in SSZ representation), utilizing recursive functions to accurately determine these sizes based on reflection and tag annotations (ssz-size, dynssz-size).

  • Encoding/Decoding Dispatch: Central to the library's architecture are the marshalType and unmarshalType functions. These serve as entry points to the encoding and decoding processes, respectively, dynamically dispatching tasks to specialized functions based on the nature of the data (e.g., marshalStruct, unmarshalArray).

  • Dynamic Handling with Static Efficiency: For types that do not necessitate dynamic processing (neither the type nor its nested types have dynamic specifications), dynssz optimizes performance by invoking corresponding fastssz functions. This ensures minimal overhead for types compatible with static processing.

  • Size Hints and Spec Values: dynssz intelligently handles sizes through sszSizeHint structures, derived from field tag annotations. These hints inform the library whether to process data statically or dynamically, allowing for precise and efficient data serialization.

Architecture Flow
  1. Size Calculation: Upon receiving a data structure for encoding or decoding, dynssz first calculates its size. For encoding, it determines whether the structure can be processed statically or requires dynamic handling. For decoding, it assesses the expected size of the incoming SSZ data.

  2. Dynamic vs. Static Path Selection: Based on the size calculation and the presence of dynamic specifications, the library selects the appropriate processing path. Static paths leverage fastssz for efficiency, while dynamic paths use runtime reflection.

  3. Recursive Encoding/Decoding: The library recursively processes each field or element of the data structure. It dynamically navigates through nested structures, applying the correct encoding or decoding method based on the data type and size characteristics.

  4. Specialized Function Dispatch: For complex types (e.g., slices, arrays, structs), dynssz dispatches tasks to specialized functions tailored to handle specific encoding or decoding needs, ensuring accurate and efficient processing.

Contributing

We welcome contributions from the community! Please check out the CONTRIBUTING.md file for guidelines on how to contribute to dynssz.

License

dynssz is licensed under the Apache-2.0 License. See the LICENSE file for more details.

Acknowledgements

Thanks to all the contributors and the Ethereum community for providing the inspiration and foundation for this project.

Documentation

Overview

dynssz: Dynamic SSZ encoding/decoding for Ethereum with fastssz efficiency. This file is part of the dynssz package. Copyright (c) 2024 by pk910. Refer to LICENSE for more information.

Package dynssz provides dynamic SSZ (Simple Serialize) encoding and decoding for Ethereum data structures. Unlike static code generation approaches, dynssz uses runtime reflection to handle dynamic field sizes, making it suitable for various Ethereum presets beyond the mainnet. It seamlessly integrates with fastssz for optimal performance when static definitions are applicable.

Copyright (c) 2024 by pk910. See LICENSE file for details.

dynssz: Dynamic SSZ encoding/decoding for Ethereum with fastssz efficiency. This file is part of the dynssz package. Copyright (c) 2024 by pk910. Refer to LICENSE for more information.

dynssz: Dynamic SSZ encoding/decoding for Ethereum with fastssz efficiency. This file is part of the dynssz package. Copyright (c) 2024 by pk910. Refer to LICENSE for more information.

dynssz: Dynamic SSZ encoding/decoding for Ethereum with fastssz efficiency. This file is part of the dynssz package. Copyright (c) 2024 by pk910. Refer to LICENSE for more information.

dynssz: Dynamic SSZ encoding/decoding for Ethereum with fastssz efficiency. This file is part of the dynssz package. Copyright (c) 2024 by pk910. Refer to LICENSE for more information.

dynssz: Dynamic SSZ encoding/decoding for Ethereum with fastssz efficiency. This file is part of the dynssz package. Copyright (c) 2024 by pk910. Refer to LICENSE for more information.

dynssz: Dynamic SSZ encoding/decoding for Ethereum with fastssz efficiency. This file implements cached type descriptors with unsafe pointer optimization. Copyright (c) 2024 by pk910. Refer to LICENSE for more information.

dynssz: Dynamic SSZ encoding/decoding for Ethereum with fastssz efficiency. This file is part of the dynssz package. Copyright (c) 2024 by pk910. Refer to LICENSE for more information.

dynssz: Dynamic SSZ encoding/decoding for Ethereum with fastssz efficiency. This file is part of the dynssz package. Copyright (c) 2024 by pk910. Refer to LICENSE for more information.

dynssz: Dynamic SSZ encoding/decoding for Ethereum with fastssz efficiency. This file is part of the dynssz package. Copyright (c) 2024 by pk910. Refer to LICENSE for more information.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ExtractUnionDescriptorInfo added in v1.0.2

func ExtractUnionDescriptorInfo(descriptorType reflect.Type, dynssz *DynSsz) (map[uint8]UnionVariantInfo, error)

ExtractUnionDescriptorInfo extracts variant information from a union descriptor type. This function is used by the type cache to extract variant information including SSZ annotations.

func SetGlobalSpecs added in v1.0.2

func SetGlobalSpecs(specs map[string]any)

Types

type CompatibleUnion added in v1.0.2

type CompatibleUnion[T any] struct {
	Variant uint8
	Data    interface{}
}

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.

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: 0,
        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 type and data. The variantIndex corresponds to the field index in the descriptor struct T.

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 ContainerDescriptor added in v1.0.2

type ContainerDescriptor struct {
	Fields    []FieldDescriptor    // For structs
	DynFields []DynFieldDescriptor // Dynamic struct fields
}

FieldDescriptor represents a cached descriptor for a struct field

type DynFieldDescriptor added in v1.0.0

type DynFieldDescriptor struct {
	Field  *FieldDescriptor
	Offset uint32
	Index  int16 // Index of the field in the struct
}

DynFieldDescriptor represents a dynamic field descriptor for a struct

type DynSsz

type DynSsz struct {

	// NoFastSsz disables the use of fastssz for static types.
	// When true, all encoding/decoding uses reflection-based processing.
	// Generally not recommended unless you need consistent behavior across all types.
	NoFastSsz bool

	// NoFastHash disables the use of optimized hash tree root calculation.
	// When true, uses the standard hasher instead of the fast gohashtree implementation.
	NoFastHash bool

	// Verbose enables detailed logging of encoding/decoding operations.
	// Useful for debugging but impacts performance.
	Verbose bool
	// 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

func NewDynSsz

func NewDynSsz(specs map[string]any) *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) GetTypeCache added in v1.0.0

func (d *DynSsz) GetTypeCache() *TypeCache

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) ([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) MarshalSSZ

func (d *DynSsz) MarshalSSZ(source any) ([]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 fastssz.Marshaler interface

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

func (d *DynSsz) MarshalSSZTo(source any, buf []byte) ([]byte, error)

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) ResolveSpecValue added in v1.0.2

func (d *DynSsz) ResolveSpecValue(name string) (bool, uint64, error)

func (*DynSsz) SizeSSZ

func (d *DynSsz) SizeSSZ(source any) (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

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) UnmarshalSSZ

func (d *DynSsz) UnmarshalSSZ(target any, ssz []byte) 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) ValidateType added in v1.0.2

func (d *DynSsz) ValidateType(t reflect.Type) 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 (strings, maps, channels, signed integers, floats, etc.)

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

Parameters:

  • t: The reflect.Type to validate for SSZ compatibility

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.

Example usage:

type MyStruct struct {
    ValidField   uint64
    InvalidField string  // This will cause validation to fail
}

err := ds.ValidateType(reflect.TypeOf(MyStruct{}))
if err != nil {
    log.Fatal("Type validation failed:", err)
    // Output: Type validation failed: field 'InvalidField': unsupported type 'string'
}

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 FieldDescriptor added in v1.0.0

type FieldDescriptor struct {
	Name     string
	Type     *TypeDescriptor // Type descriptor
	SszIndex uint16          // SSZ index for progressive containers
}

FieldDescriptor represents a cached descriptor for a struct field

type GoTypeFlag added in v1.0.2

type GoTypeFlag uint8
const (
	GoTypeFlagIsPointer   GoTypeFlag = 1 << iota // Whether the type is a pointer type
	GoTypeFlagIsByteArray                        // Whether the type is a byte array
	GoTypeFlagIsString                           // Whether the type is a string type
)

type SszCompatFlag added in v1.0.2

type SszCompatFlag uint8

SszCompatFlag is a flag indicating whether a type implements a specific SSZ compatibility interface

const (
	SszCompatFlagFastSSZMarshaler   SszCompatFlag = 1 << iota // Whether the type implements fastssz.Marshaler
	SszCompatFlagFastSSZHasher                                // Whether the type implements fastssz.HashRoot
	SszCompatFlagHashTreeRootWith                             // Whether the type implements HashTreeRootWith
	SszCompatFlagDynamicMarshaler                             // Whether the type implements DynamicMarshaler
	SszCompatFlagDynamicUnmarshaler                           // Whether the type implements DynamicUnmarshaler
	SszCompatFlagDynamicSizer                                 // Whether the type implements DynamicSizer
	SszCompatFlagDynamicHashRoot                              // Whether the type implements DynamicHashRoot
)

type SszMaxSizeHint added in v1.0.0

type SszMaxSizeHint struct {
	Size    uint64
	NoValue bool
	Custom  bool
	Expr    string
}

type SszSizeHint added in v1.0.0

type SszSizeHint struct {
	Size    uint32
	Dynamic bool
	Custom  bool
	Expr    string
}

SszSizeHint encapsulates size information for SSZ encoding and decoding, derived from 'ssz-size' and 'dynssz-size' tag annotations. It provides detailed insights into the size attributes of fields or types, particularly noting whether sizes are fixed or dynamic, and if special specification values are applied, differing from default assumptions.

Fields:

  • size: A uint64 value indicating the statically annotated size of the type or field, as specified by 'ssz-size' tag annotations. For dynamic fields, where the size may vary depending on the instance of the data, this field is set to 0, and the dynamic flag is used to indicate its dynamic nature.
  • dynamic: A boolean flag indicating whether the field's size is dynamic, set to true for fields whose size can change or is not fixed at compile time. This determination is based on the presence of 'dynssz-size' annotations or the inherent variability of the type.
  • custom: A boolean indicating whether a non-default specification value has been applied to the type or field, typically through 'dynssz-size' annotations, suggesting a deviation from standard size expectations that might influence the encoding or decoding process.
  • expr: The dynamic expression used to calculate the size of the field, typically through 'dynssz-size' annotations.

type SszType added in v1.0.2

type SszType uint8
const (
	SszUnspecifiedType SszType = iota
	SszCustomType
	SszTypeWrapperType

	// basic types
	SszBoolType
	SszUint8Type
	SszUint16Type
	SszUint32Type
	SszUint64Type
	SszUint128Type
	SszUint256Type

	// complex types
	SszContainerType
	SszListType
	SszVectorType
	SszBitlistType
	SszBitvectorType
	SszProgressiveListType
	SszProgressiveBitlistType
	SszProgressiveContainerType
	SszCompatibleUnionType
)

type SszTypeFlag added in v1.0.2

type SszTypeFlag uint8

SszTypeFlag is a flag indicating whether a type has a specific SSZ type feature

const (
	SszTypeFlagIsDynamic      SszTypeFlag = 1 << iota // Whether the type is a dynamic type (or has nested dynamic types)
	SszTypeFlagHasLimit                               // Whether the type has a max size tag
	SszTypeFlagHasDynamicSize                         // Whether this type or any of its nested types uses dynamic spec size value that differs from the default
	SszTypeFlagHasDynamicMax                          // Whether this type or any of its nested types uses dynamic spec max value that differs from the default
	SszTypeFlagHasSizeExpr                            // Whether this type or any of its nested types uses a dynamic expression to calculate the size or max size
	SszTypeFlagHasMaxExpr                             // Whether this type or any of its nested types uses a dynamic expression to calculate the max size
)

type SszTypeHint added in v1.0.2

type SszTypeHint struct {
	Type SszType
}

type TypeCache added in v1.0.0

type TypeCache struct {
	// contains filtered or unexported fields
}

TypeCache manages cached type descriptors

func NewTypeCache added in v1.0.0

func NewTypeCache(dynssz *DynSsz) *TypeCache

NewTypeCache creates a new type cache

func (*TypeCache) GetAllTypes added in v1.0.1

func (tc *TypeCache) GetAllTypes() []reflect.Type

GetAllTypes returns a slice of all types currently cached in the TypeCache.

This method is useful for cache inspection, debugging, and understanding which types have been processed and cached during the application's lifetime. The returned slice contains the reflect.Type values in no particular order.

The method acquires a read lock to ensure thread-safe access to the cache.

Returns:

  • []reflect.Type: A slice containing all cached types

Example:

cachedTypes := cache.GetAllTypes()
fmt.Printf("TypeCache contains %d types\n", len(cachedTypes))
for _, t := range cachedTypes {
    fmt.Printf("  - %s\n", t.String())
}

func (*TypeCache) GetTypeDescriptor added in v1.0.0

func (tc *TypeCache) GetTypeDescriptor(t reflect.Type, sizeHints []SszSizeHint, maxSizeHints []SszMaxSizeHint, typeHints []SszTypeHint) (*TypeDescriptor, error)

GetTypeDescriptor returns a cached type descriptor for the given type, computing it if necessary.

This method is the primary interface for obtaining type descriptors, which contain optimized metadata about how to serialize, deserialize, and hash types according to SSZ specifications. Type descriptors are cached for performance, avoiding repeated reflection and analysis of the same types.

The method is thread-safe and ensures sequential processing to prevent duplicate computation of type descriptors when called concurrently for the same type.

Parameters:

  • t: The reflect.Type for which to obtain a descriptor
  • sizeHints: Optional size hints from parent structures' tags. Pass nil for top-level types.
  • maxSizeHints: Optional max size hints from parent structures' tags. Pass nil for top-level types.
  • typeHints: Optional type hints from parent structures' tags. Pass nil for top-level types.

Returns:

  • *TypeDescriptor: The type descriptor containing metadata for SSZ operations
  • error: An error if the type cannot be analyzed or contains unsupported features

Type descriptors are only cached when no size hints are provided (i.e., for root types). When size hints are present, the descriptor is computed dynamically to accommodate the specific constraints.

Example:

typeDesc, err := cache.GetTypeDescriptor(reflect.TypeOf(myStruct), nil, nil)
if err != nil {
    log.Fatal("Failed to get type descriptor:", err)
}
fmt.Printf("Type size: %d bytes (dynamic: %v)\n", typeDesc.Size, typeDesc.Size < 0)

func (*TypeCache) RemoveAllTypes added in v1.0.1

func (tc *TypeCache) RemoveAllTypes()

RemoveAllTypes clears all cached type descriptors from the cache.

This method is useful for:

  • Resetting the cache after configuration changes
  • Memory management in long-running applications
  • Testing scenarios requiring a clean cache state

The method acquires a write lock to ensure thread-safe clearing. After calling this method, all subsequent type descriptor requests will trigger recomputation.

Example:

// Clear cache after updating specifications
ds.UpdateSpecs(newSpecs)
cache.RemoveAllTypes()

// All types will be recomputed with new specs
desc, err := cache.GetTypeDescriptor(reflect.TypeOf(MyStruct{}), nil, nil)

func (*TypeCache) RemoveType added in v1.0.1

func (tc *TypeCache) RemoveType(t reflect.Type)

RemoveType removes a specific type from the cache.

This method is useful for cache management scenarios where you need to force recomputation of a type descriptor, such as after configuration changes or when testing different type configurations.

The method acquires a write lock to ensure thread-safe removal.

Parameters:

  • t: The reflect.Type to remove from the cache

Example:

// Remove a type to force recomputation
cache.RemoveType(reflect.TypeOf(MyStruct{}))

// Next call to GetTypeDescriptor will rebuild the descriptor
desc, err := cache.GetTypeDescriptor(reflect.TypeOf(MyStruct{}), nil, nil)

type TypeDescriptor added in v1.0.0

type TypeDescriptor struct {
	Type                   reflect.Type
	Kind                   reflect.Kind              // Go kind of the type
	Size                   uint32                    // SSZ size (-1 if dynamic)
	Len                    uint32                    // Length of array/slice
	Limit                  uint64                    // Limit of array/slice (ssz-max tag)
	ContainerDesc          *ContainerDescriptor      // For structs
	UnionVariants          map[uint8]*TypeDescriptor // Union variant types by index (for CompatibleUnion)
	ElemDesc               *TypeDescriptor           // For slices/arrays
	HashTreeRootWithMethod *reflect.Method           // Cached HashTreeRootWith method for performance
	SizeExpression         *string                   // The dynamic expression used to calculate the size of the type
	MaxExpression          *string                   // The dynamic expression used to calculate the max size of the type
	SszType                SszType                   // SSZ type of the type
	SszTypeFlags           SszTypeFlag               // SSZ type flags
	SszCompatFlags         SszCompatFlag             // SSZ compatibility flags
	GoTypeFlags            GoTypeFlag                // Additional go type flags
}

TypeDescriptor represents a cached, optimized descriptor for a type's SSZ encoding/decoding

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 UnionVariantInfo added in v1.0.2

type UnionVariantInfo struct {
	Type         reflect.Type
	SizeHints    []SszSizeHint
	MaxSizeHints []SszMaxSizeHint
	TypeHints    []SszTypeHint
}

UnionVariantInfo contains type and annotation information for a union variant

Directories

Path Synopsis
dynssz-gen module
test module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL