Initialize SSZ-QL package with support for fixed-size types (#15588)

* Add basic PathElement

* Add ssz_type.go

* Add basic sszInfo

* Add containerInfo

* Add basic analyzer without analyzing list/vector

* Add analyzer for homogeneous collection types

* Add offset/length calculator

* Add testutil package in encoding/ssz/query

* Add first round trip test for IndexedAttestationElectra

* Go mod tidy

* Add Print function for debugging purpose

* Add changelog

* Add testonly flag for testutil package & Nit for nogo

* Apply reviews from Radek

* Replace fastssz with prysmaticlabs one

* Add proto/ssz_query package for testing purpose

* Update encoding/ssz/query tests to decouple with beacon types

* Use require.* instead of assert.*

* Fix import name for proto ssz_query package

* Remove uint8/uint16 and some byte arrays in FixedTestContainer

* Add newline for files

* Fix comment about byte array in ssz_query.proto

---------

Co-authored-by: Radosław Kapka <rkapka@wp.pl>
This commit is contained in:
Jun Song
2025-08-25 23:29:26 +09:00
committed by GitHub
parent 92c359456e
commit 26d8b6b786
19 changed files with 1543 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
load("@prysm//tools/go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"analyzer.go",
"container.go",
"path.go",
"query.go",
"ssz_info.go",
"ssz_type.go",
],
importpath = "github.com/OffchainLabs/prysm/v6/encoding/ssz/query",
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = [
"analyzer_test.go",
"path_test.go",
"query_test.go",
],
deps = [
":go_default_library",
"//encoding/ssz/query/testutil:go_default_library",
"//proto/ssz_query:go_default_library",
"//testing/require:go_default_library",
],
)

View File

@@ -0,0 +1,242 @@
package query
import (
"fmt"
"reflect"
"strconv"
"strings"
)
const (
offsetBytes = 4
// sszMaxTag specifies the maximum capacity of a variable-sized collection, like an SSZ List.
sszMaxTag = "ssz-max"
// sszSizeTag specifies the length of a fixed-sized collection, like an SSZ Vector.
// A wildcard ('?') indicates that the dimension is variable-sized (a List).
sszSizeTag = "ssz-size"
)
// AnalyzeObject analyzes given object and returns its SSZ information.
func AnalyzeObject(obj any) (*sszInfo, error) {
value := dereferencePointer(obj)
info, err := analyzeType(value.Type(), nil)
if err != nil {
return nil, fmt.Errorf("could not analyze type %s: %w", value.Type().Name(), err)
}
return info, nil
}
// analyzeType is an entry point that inspects a reflect.Type and computes its SSZ layout information.
func analyzeType(typ reflect.Type, tag *reflect.StructTag) (*sszInfo, error) {
switch typ.Kind() {
// Basic types (e.g., uintN where N is 8, 16, 32, 64)
// NOTE: uint128 and uint256 are represented as []byte in Go,
// so we handle them as slices. See `analyzeHomogeneousColType`.
case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Bool:
return analyzeBasicType(typ)
case reflect.Slice:
return analyzeHomogeneousColType(typ, tag)
case reflect.Struct:
return analyzeContainerType(typ)
case reflect.Ptr:
// Dereference pointer types.
return analyzeType(typ.Elem(), tag)
default:
return nil, fmt.Errorf("unsupported type %v for SSZ calculation", typ.Kind())
}
}
// analyzeBasicType analyzes SSZ basic types (uintN, bool) and returns its info.
func analyzeBasicType(typ reflect.Type) (*sszInfo, error) {
sszInfo := &sszInfo{
typ: typ,
// Every basic type is fixed-size and not variable.
isVariable: false,
}
switch typ.Kind() {
case reflect.Uint64:
sszInfo.sszType = UintN
sszInfo.fixedSize = 8
case reflect.Uint32:
sszInfo.sszType = UintN
sszInfo.fixedSize = 4
case reflect.Uint16:
sszInfo.sszType = UintN
sszInfo.fixedSize = 2
case reflect.Uint8:
sszInfo.sszType = UintN
sszInfo.fixedSize = 1
case reflect.Bool:
sszInfo.sszType = Boolean
sszInfo.fixedSize = 1
default:
return nil, fmt.Errorf("unsupported basic type %v for SSZ calculation", typ.Kind())
}
return sszInfo, nil
}
// analyzeHomogeneousColType analyzes homogeneous collection types (e.g., List, Vector, Bitlist, Bitvector) and returns its SSZ info.
func analyzeHomogeneousColType(typ reflect.Type, tag *reflect.StructTag) (*sszInfo, error) {
if typ.Kind() != reflect.Slice {
return nil, fmt.Errorf("can only analyze slice types, got %v", typ.Kind())
}
if tag == nil {
return nil, fmt.Errorf("tag is required for slice types")
}
elementInfo, err := analyzeType(typ.Elem(), nil)
if err != nil {
return nil, fmt.Errorf("could not analyze element type for homogeneous collection: %w", err)
}
// 1. Check if the type is List/Bitlist by checking `ssz-max` tag.
sszMax := tag.Get(sszMaxTag)
if sszMax != "" {
dims := strings.Split(sszMax, ",")
if len(dims) > 1 {
return nil, fmt.Errorf("multi-dimensional lists are not supported, got %d dimensions", len(dims))
}
limit, err := strconv.ParseUint(dims[0], 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid ssz-max tag (%s): %w", sszMax, err)
}
return analyzeListType(typ, elementInfo, limit)
}
// 2. Handle Vector/Bitvector type.
sszSize := tag.Get(sszSizeTag)
dims := strings.Split(sszSize, ",")
if len(dims) > 1 {
return nil, fmt.Errorf("multi-dimensional vectors are not supported, got %d dimensions", len(dims))
}
length, err := strconv.ParseUint(dims[0], 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid ssz-size tag (%s): %w", sszSize, err)
}
return analyzeVectorType(typ, elementInfo, length)
}
// analyzeListType analyzes SSZ List type and returns its SSZ info.
func analyzeListType(typ reflect.Type, elementInfo *sszInfo, limit uint64) (*sszInfo, error) {
if elementInfo == nil {
return nil, fmt.Errorf("element info is required for List")
}
return &sszInfo{
sszType: List,
typ: typ,
fixedSize: offsetBytes,
isVariable: true,
}, nil
}
// analyzeVectorType analyzes SSZ Vector type and returns its SSZ info.
func analyzeVectorType(typ reflect.Type, elementInfo *sszInfo, length uint64) (*sszInfo, error) {
if elementInfo == nil {
return nil, fmt.Errorf("element info is required for Vector")
}
return &sszInfo{
sszType: Vector,
typ: typ,
fixedSize: length * elementInfo.Size(),
isVariable: false,
}, nil
}
// analyzeContainerType analyzes SSZ Container type and returns its SSZ info.
func analyzeContainerType(typ reflect.Type) (*sszInfo, error) {
if typ.Kind() != reflect.Struct {
return nil, fmt.Errorf("can only analyze struct types, got %v", typ.Kind())
}
sszInfo := &sszInfo{
sszType: Container,
typ: typ,
containerInfo: make(map[string]*fieldInfo),
}
var currentOffset uint64
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
// Protobuf-generated structs contain private fields we must skip.
// e.g., state, sizeCache, unknownFields, etc.
if !field.IsExported() {
continue
}
// The JSON tag contains the field name in the first part.
// e.g., "attesting_indices,omitempty" -> "attesting_indices".
jsonTag := field.Tag.Get("json")
if jsonTag == "" {
return nil, fmt.Errorf("field %s has no JSON tag", field.Name)
}
// NOTE: `fieldName` is a string with `snake_case` format (following consensus specs).
fieldName := strings.Split(jsonTag, ",")[0]
if fieldName == "" {
return nil, fmt.Errorf("field %s has an empty JSON tag", field.Name)
}
// Analyze each field so that we can complete full SSZ information.
info, err := analyzeType(field.Type, &field.Tag)
if err != nil {
return nil, fmt.Errorf("could not analyze type for field %s: %w", fieldName, err)
}
// If one of the fields is variable-sized,
// the entire struct is considered variable-sized.
if info.isVariable {
sszInfo.isVariable = true
}
// Store nested struct info.
sszInfo.containerInfo[fieldName] = &fieldInfo{
sszInfo: info,
offset: currentOffset,
}
// Update the current offset based on the field's fixed size.
currentOffset += info.fixedSize
}
sszInfo.fixedSize = currentOffset
return sszInfo, nil
}
// dereferencePointer dereferences a pointer to get the underlying value using reflection.
func dereferencePointer(obj any) reflect.Value {
value := reflect.ValueOf(obj)
if value.Kind() == reflect.Ptr {
if value.IsNil() {
// If we encounter a nil pointer before the end of the path, we can still proceed
// by analyzing the type, not the value.
value = reflect.New(value.Type().Elem()).Elem()
} else {
value = value.Elem()
}
}
return value
}

View File

@@ -0,0 +1,17 @@
package query_test
import (
"testing"
"github.com/OffchainLabs/prysm/v6/encoding/ssz/query"
sszquerypb "github.com/OffchainLabs/prysm/v6/proto/ssz_query"
"github.com/OffchainLabs/prysm/v6/testing/require"
)
func TestAnalyzeSSZInfo(t *testing.T) {
info, err := query.AnalyzeObject(&sszquerypb.FixedTestContainer{})
require.NoError(t, err)
require.NotNil(t, info, "Expected non-nil SSZ info")
require.Equal(t, uint64(333), info.FixedSize(), "Expected fixed size to be 333")
}

View File

@@ -0,0 +1,11 @@
package query
// containerInfo maps a field's JSON name to its sszInfo for nested Containers.
type containerInfo = map[string]*fieldInfo
type fieldInfo struct {
// sszInfo contains the SSZ information of the field.
sszInfo *sszInfo
// offset is the offset of the field within the parent struct.
offset uint64
}

View File

@@ -0,0 +1,31 @@
package query
import (
"errors"
"strings"
)
// PathElement represents a single element in a path.
type PathElement struct {
Name string
}
func ParsePath(rawPath string) ([]PathElement, error) {
// We use dot notation, so we split the path by '.'.
rawElements := strings.Split(rawPath, ".")
if len(rawElements) == 0 {
return nil, errors.New("empty path provided")
}
if rawElements[0] == "" {
// Remove leading dot if present
rawElements = rawElements[1:]
}
var path []PathElement
for _, elem := range rawElements {
path = append(path, PathElement{Name: elem})
}
return path, nil
}

View File

@@ -0,0 +1,53 @@
package query_test
import (
"testing"
"github.com/OffchainLabs/prysm/v6/encoding/ssz/query"
"github.com/OffchainLabs/prysm/v6/testing/require"
)
func TestParsePath(t *testing.T) {
tests := []struct {
name string
path string
expected []query.PathElement
wantErr bool
}{
{
name: "simple nested path",
path: "data.target.root",
expected: []query.PathElement{
{Name: "data"},
{Name: "target"},
{Name: "root"},
},
wantErr: false,
},
{
name: "simple nested path with leading dot",
path: ".data.target.root",
expected: []query.PathElement{
{Name: "data"},
{Name: "target"},
{Name: "root"},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parsedPath, err := query.ParsePath(tt.path)
if tt.wantErr {
require.NotNil(t, err, "Expected error but got none")
return
}
require.NoError(t, err)
require.Equal(t, len(tt.expected), len(parsedPath), "Expected %d path elements, got %d", len(tt.expected), len(parsedPath))
require.DeepEqual(t, tt.expected, parsedPath, "Parsed path does not match expected path")
})
}
}

View File

@@ -0,0 +1,37 @@
package query
import "fmt"
func CalculateOffsetAndLength(sszInfo *sszInfo, path []PathElement) (*sszInfo, uint64, uint64, error) {
if sszInfo == nil {
return nil, 0, 0, fmt.Errorf("sszInfo is nil")
}
if len(path) == 0 {
return nil, 0, 0, fmt.Errorf("path is empty")
}
walk := sszInfo
currentOffset := uint64(0)
for _, elem := range path {
fieldInfos, err := walk.ContainerInfo()
if err != nil {
return nil, 0, 0, fmt.Errorf("could not get field infos: %w", err)
}
fieldInfo, exists := fieldInfos[elem.Name]
if !exists {
return nil, 0, 0, fmt.Errorf("field %s not found in fieldInfos", elem.Name)
}
currentOffset += fieldInfo.offset
walk = fieldInfo.sszInfo
}
if walk.isVariable {
return nil, 0, 0, fmt.Errorf("cannot calculate length for variable-sized type")
}
return walk, currentOffset, walk.Size(), nil
}

View File

@@ -0,0 +1,200 @@
package query_test
import (
"math"
"testing"
"github.com/OffchainLabs/prysm/v6/encoding/ssz/query"
"github.com/OffchainLabs/prysm/v6/encoding/ssz/query/testutil"
"github.com/OffchainLabs/prysm/v6/proto/ssz_query"
sszquerypb "github.com/OffchainLabs/prysm/v6/proto/ssz_query"
"github.com/OffchainLabs/prysm/v6/testing/require"
)
func TestCalculateOffsetAndLength(t *testing.T) {
tests := []struct {
name string
path string
expectedOffset uint64
expectedLength uint64
}{
// Basic integer types
{
name: "field_uint32",
path: ".field_uint32",
expectedOffset: 0,
expectedLength: 4,
},
{
name: "field_uint64",
path: ".field_uint64",
expectedOffset: 4,
expectedLength: 8,
},
// Boolean type
{
name: "field_bool",
path: ".field_bool",
expectedOffset: 12,
expectedLength: 1,
},
// Fixed-size bytes
{
name: "field_bytes32",
path: ".field_bytes32",
expectedOffset: 13,
expectedLength: 32,
},
// Nested container
{
name: "nested container",
path: ".nested",
expectedOffset: 45,
expectedLength: 40,
},
{
name: "nested value1",
path: ".nested.value1",
expectedOffset: 45,
expectedLength: 8,
},
{
name: "nested value2",
path: ".nested.value2",
expectedOffset: 53,
expectedLength: 32,
},
// Vector field
{
name: "vector field",
path: ".vector_field",
expectedOffset: 85,
expectedLength: 192, // 24 * 8 bytes
},
// Trailing field
{
name: "trailing_field",
path: ".trailing_field",
expectedOffset: 277,
expectedLength: 56,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path, err := query.ParsePath(tt.path)
require.NoError(t, err)
info, err := query.AnalyzeObject(&sszquerypb.FixedTestContainer{})
require.NoError(t, err)
_, offset, length, err := query.CalculateOffsetAndLength(info, path)
require.NoError(t, err)
require.Equal(t, tt.expectedOffset, offset, "Expected offset to be %d", tt.expectedOffset)
require.Equal(t, tt.expectedLength, length, "Expected length to be %d", tt.expectedLength)
})
}
}
func TestRoundTripSszInfo(t *testing.T) {
specs := []testutil.TestSpec{
getFixedTestContainerSpec(),
}
for _, spec := range specs {
testutil.RunStructTest(t, spec)
}
}
func createFixedTestContainer() any {
fieldBytes32 := make([]byte, 32)
for i := range fieldBytes32 {
fieldBytes32[i] = byte(i + 24)
}
nestedValue2 := make([]byte, 32)
for i := range nestedValue2 {
nestedValue2[i] = byte(i + 56)
}
trailingField := make([]byte, 56)
for i := range trailingField {
trailingField[i] = byte(i + 88)
}
return &ssz_query.FixedTestContainer{
// Basic types
FieldUint32: math.MaxUint32,
FieldUint64: math.MaxUint64,
FieldBool: true,
// Fixed-size bytes
FieldBytes32: fieldBytes32,
// Nested container
Nested: &sszquerypb.FixedNestedContainer{
Value1: 123,
Value2: nestedValue2,
},
// Vector field
VectorField: []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24},
// Trailing field
TrailingField: trailingField,
}
}
func getFixedTestContainerSpec() testutil.TestSpec {
testContainer := createFixedTestContainer().(*sszquerypb.FixedTestContainer)
return testutil.TestSpec{
Name: "FixedTestContainer",
Type: sszquerypb.FixedTestContainer{},
Instance: testContainer,
PathTests: []testutil.PathTest{
// Basic types
{
Path: ".field_uint32",
Expected: testContainer.FieldUint32,
},
{
Path: ".field_uint64",
Expected: testContainer.FieldUint64,
},
{
Path: ".field_bool",
Expected: testContainer.FieldBool,
},
// Fixed-size bytes
{
Path: ".field_bytes32",
Expected: testContainer.FieldBytes32,
},
// Nested container
{
Path: ".nested",
Expected: testContainer.Nested,
},
{
Path: ".nested.value1",
Expected: testContainer.Nested.Value1,
},
{
Path: ".nested.value2",
Expected: testContainer.Nested.Value2,
},
// Vector field
{
Path: ".vector_field",
Expected: testContainer.VectorField,
},
// Trailing field
{
Path: ".trailing_field",
Expected: testContainer.TrailingField,
},
},
}
}

View File

@@ -0,0 +1,110 @@
package query
import (
"fmt"
"reflect"
"sort"
"strings"
)
// sszInfo holds the all necessary data for analyzing SSZ data types.
type sszInfo struct {
// Type of the SSZ structure (Basic, Container, List, etc.).
sszType SSZType
// Type in Go. Need this for unmarshaling.
typ reflect.Type
// isVariable is true if the struct contains any variable-size fields.
isVariable bool
// fixedSize is the total size of the struct's fixed part.
fixedSize uint64
// For Container types.
containerInfo containerInfo
}
func (info *sszInfo) FixedSize() uint64 {
if info == nil {
return 0
}
return info.fixedSize
}
func (info *sszInfo) Size() uint64 {
if info == nil {
return 0
}
// Easy case: if the type is not variable, we can return the fixed size.
if !info.isVariable {
return info.fixedSize
}
// NOTE: Handle variable-sized types.
return 0
}
func (info *sszInfo) ContainerInfo() (containerInfo, error) {
if info == nil {
return nil, fmt.Errorf("sszInfo is nil")
}
if info.sszType != Container {
return nil, fmt.Errorf("sszInfo is not a Container type, got %s", info.sszType)
}
if info.containerInfo == nil {
return nil, fmt.Errorf("sszInfo.containerInfo is nil")
}
return info.containerInfo, nil
}
// Print returns a string representation of the sszInfo, which is useful for debugging.
func (info *sszInfo) Print() string {
if info == nil {
return "<nil>"
}
var builder strings.Builder
printRecursive(info, &builder, "")
return builder.String()
}
func printRecursive(info *sszInfo, builder *strings.Builder, prefix string) {
var sizeDesc string
if info.isVariable {
sizeDesc = "Variable-size"
} else {
sizeDesc = "Fixed-size"
}
switch info.sszType {
case Container:
builder.WriteString(fmt.Sprintf("%s: %s (%s / fixed size: %d, total size: %d)\n", info.sszType, info.typ.Name(), sizeDesc, info.FixedSize(), info.Size()))
default:
builder.WriteString(fmt.Sprintf("%s (%s / size: %d)\n", info.sszType, sizeDesc, info.Size()))
}
keys := make([]string, 0, len(info.containerInfo))
for k := range info.containerInfo {
keys = append(keys, k)
}
sort.Strings(keys)
for i, key := range keys {
connector := "├─"
nextPrefix := prefix + "│ "
if i == len(keys)-1 {
connector = "└─"
nextPrefix = prefix + " "
}
builder.WriteString(fmt.Sprintf("%s%s %s (offset: %d) ", prefix, connector, key, info.containerInfo[key].offset))
if nestedInfo := info.containerInfo[key].sszInfo; nestedInfo != nil {
printRecursive(nestedInfo, builder, nextPrefix)
} else {
builder.WriteString("\n")
}
}
}

View File

@@ -0,0 +1,53 @@
package query
import "fmt"
// SSZType represents the type supported by SSZ.
// https://github.com/ethereum/consensus-specs/blob/master/ssz/simple-serialize.md#typing
type SSZType int
// SSZ type constants.
const (
// Basic types
UintN SSZType = iota
Byte
Boolean
// Composite types
Container
Vector
List
Bitvector
Bitlist
// Added in EIP-7916
ProgressiveList
Union
)
func (t SSZType) String() string {
switch t {
case UintN:
return "UintN"
case Byte:
return "Byte"
case Boolean:
return "Boolean"
case Container:
return "Container"
case Vector:
return "Vector"
case List:
return "List"
case Bitvector:
return "Bitvector"
case Bitlist:
return "Bitlist"
case ProgressiveList:
return "ProgressiveList"
case Union:
return "Union"
default:
return fmt.Sprintf("Unknown(%d)", t)
}
}

View File

@@ -0,0 +1,18 @@
load("@prysm//tools/go:def.bzl", "go_library")
go_library(
name = "go_default_library",
testonly = True,
srcs = [
"runner.go",
"type.go",
"util.go",
],
importpath = "github.com/OffchainLabs/prysm/v6/encoding/ssz/query/testutil",
visibility = ["//visibility:public"],
deps = [
"//encoding/ssz/query:go_default_library",
"//testing/require:go_default_library",
"@com_github_prysmaticlabs_fastssz//:go_default_library",
],
)

View File

@@ -0,0 +1,38 @@
package testutil
import (
"testing"
"github.com/OffchainLabs/prysm/v6/encoding/ssz/query"
"github.com/OffchainLabs/prysm/v6/testing/require"
ssz "github.com/prysmaticlabs/fastssz"
)
func RunStructTest(t *testing.T, spec TestSpec) {
t.Run(spec.Name, func(t *testing.T) {
info, err := query.AnalyzeObject(spec.Type)
require.NoError(t, err)
testInstance := spec.Instance
marshaller, ok := testInstance.(ssz.Marshaler)
require.Equal(t, true, ok, "Test instance must implement ssz.Marshaler, got %T", testInstance)
marshalledData, err := marshaller.MarshalSSZ()
require.NoError(t, err)
for _, pathTest := range spec.PathTests {
t.Run(pathTest.Path, func(t *testing.T) {
path, err := query.ParsePath(pathTest.Path)
require.NoError(t, err)
_, offset, length, err := query.CalculateOffsetAndLength(info, path)
require.NoError(t, err)
expectedRawBytes := marshalledData[offset : offset+length]
rawBytes, err := marshalAny(pathTest.Expected)
require.NoError(t, err, "Marshalling expected value should not return an error")
require.DeepEqual(t, expectedRawBytes, rawBytes, "Extracted value should match expected")
})
}
})
}

View File

@@ -0,0 +1,13 @@
package testutil
type PathTest struct {
Path string
Expected any
}
type TestSpec struct {
Name string
Type any
Instance any
PathTests []PathTest
}

View File

@@ -0,0 +1,49 @@
package testutil
import (
"fmt"
"reflect"
ssz "github.com/prysmaticlabs/fastssz"
)
// marshalAny marshals any value into SSZ format.
func marshalAny(value any) ([]byte, error) {
// First check if it implements ssz.Marshaler (this catches custom types like primitives.Epoch)
if marshaler, ok := value.(ssz.Marshaler); ok {
return marshaler.MarshalSSZ()
}
// Handle custom type aliases by checking if they're based on primitive types
valueType := reflect.TypeOf(value)
if valueType.PkgPath() != "" {
switch valueType.Kind() {
case reflect.Uint64:
return ssz.MarshalUint64(make([]byte, 0), reflect.ValueOf(value).Uint()), nil
case reflect.Uint32:
return ssz.MarshalUint32(make([]byte, 0), uint32(reflect.ValueOf(value).Uint())), nil
case reflect.Bool:
return ssz.MarshalBool(make([]byte, 0), reflect.ValueOf(value).Bool()), nil
}
}
switch v := value.(type) {
case []byte:
return v, nil
case []uint64:
buf := make([]byte, 0, len(v)*8)
for _, val := range v {
buf = ssz.MarshalUint64(buf, val)
}
return buf, nil
case uint64:
return ssz.MarshalUint64(make([]byte, 0), v), nil
case uint32:
return ssz.MarshalUint32(make([]byte, 0), v), nil
case bool:
return ssz.MarshalBool(make([]byte, 0), v), nil
default:
return nil, fmt.Errorf("unsupported type for SSZ marshalling: %T", value)
}
}