From 26d8b6b7869d0c1ee4cb6de27d7c9779b380c6b5 Mon Sep 17 00:00:00 2001 From: Jun Song <87601811+syjn99@users.noreply.github.com> Date: Mon, 25 Aug 2025 23:29:26 +0900 Subject: [PATCH] Initialize SSZ-QL package with support for fixed-size types (#15588) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- changelog/syjn99_initialize-ssz-ql.md | 3 + encoding/ssz/query/BUILD.bazel | 30 +++ encoding/ssz/query/analyzer.go | 242 ++++++++++++++++++++ encoding/ssz/query/analyzer_test.go | 17 ++ encoding/ssz/query/container.go | 11 + encoding/ssz/query/path.go | 31 +++ encoding/ssz/query/path_test.go | 53 +++++ encoding/ssz/query/query.go | 37 +++ encoding/ssz/query/query_test.go | 200 ++++++++++++++++ encoding/ssz/query/ssz_info.go | 110 +++++++++ encoding/ssz/query/ssz_type.go | 53 +++++ encoding/ssz/query/testutil/BUILD.bazel | 18 ++ encoding/ssz/query/testutil/runner.go | 38 ++++ encoding/ssz/query/testutil/type.go | 13 ++ encoding/ssz/query/testutil/util.go | 49 ++++ proto/ssz_query/BUILD.bazel | 68 ++++++ proto/ssz_query/ssz_query.pb.go | 289 ++++++++++++++++++++++++ proto/ssz_query/ssz_query.proto | 43 ++++ proto/ssz_query/ssz_query.ssz.go | 238 +++++++++++++++++++ 19 files changed, 1543 insertions(+) create mode 100644 changelog/syjn99_initialize-ssz-ql.md create mode 100644 encoding/ssz/query/BUILD.bazel create mode 100644 encoding/ssz/query/analyzer.go create mode 100644 encoding/ssz/query/analyzer_test.go create mode 100644 encoding/ssz/query/container.go create mode 100644 encoding/ssz/query/path.go create mode 100644 encoding/ssz/query/path_test.go create mode 100644 encoding/ssz/query/query.go create mode 100644 encoding/ssz/query/query_test.go create mode 100644 encoding/ssz/query/ssz_info.go create mode 100644 encoding/ssz/query/ssz_type.go create mode 100644 encoding/ssz/query/testutil/BUILD.bazel create mode 100644 encoding/ssz/query/testutil/runner.go create mode 100644 encoding/ssz/query/testutil/type.go create mode 100644 encoding/ssz/query/testutil/util.go create mode 100644 proto/ssz_query/BUILD.bazel create mode 100755 proto/ssz_query/ssz_query.pb.go create mode 100644 proto/ssz_query/ssz_query.proto create mode 100644 proto/ssz_query/ssz_query.ssz.go diff --git a/changelog/syjn99_initialize-ssz-ql.md b/changelog/syjn99_initialize-ssz-ql.md new file mode 100644 index 0000000000..2d3448b448 --- /dev/null +++ b/changelog/syjn99_initialize-ssz-ql.md @@ -0,0 +1,3 @@ +### Added + +- Initialize package for SSZ Query Language. diff --git a/encoding/ssz/query/BUILD.bazel b/encoding/ssz/query/BUILD.bazel new file mode 100644 index 0000000000..82ce958d19 --- /dev/null +++ b/encoding/ssz/query/BUILD.bazel @@ -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", + ], +) diff --git a/encoding/ssz/query/analyzer.go b/encoding/ssz/query/analyzer.go new file mode 100644 index 0000000000..82865e8b57 --- /dev/null +++ b/encoding/ssz/query/analyzer.go @@ -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 +} diff --git a/encoding/ssz/query/analyzer_test.go b/encoding/ssz/query/analyzer_test.go new file mode 100644 index 0000000000..9580481232 --- /dev/null +++ b/encoding/ssz/query/analyzer_test.go @@ -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") +} diff --git a/encoding/ssz/query/container.go b/encoding/ssz/query/container.go new file mode 100644 index 0000000000..1f16d36b2c --- /dev/null +++ b/encoding/ssz/query/container.go @@ -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 +} diff --git a/encoding/ssz/query/path.go b/encoding/ssz/query/path.go new file mode 100644 index 0000000000..4fd976828d --- /dev/null +++ b/encoding/ssz/query/path.go @@ -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 +} diff --git a/encoding/ssz/query/path_test.go b/encoding/ssz/query/path_test.go new file mode 100644 index 0000000000..0594459d38 --- /dev/null +++ b/encoding/ssz/query/path_test.go @@ -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") + }) + } +} diff --git a/encoding/ssz/query/query.go b/encoding/ssz/query/query.go new file mode 100644 index 0000000000..12028fdc4c --- /dev/null +++ b/encoding/ssz/query/query.go @@ -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 +} diff --git a/encoding/ssz/query/query_test.go b/encoding/ssz/query/query_test.go new file mode 100644 index 0000000000..e954ee75ed --- /dev/null +++ b/encoding/ssz/query/query_test.go @@ -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, + }, + }, + } +} diff --git a/encoding/ssz/query/ssz_info.go b/encoding/ssz/query/ssz_info.go new file mode 100644 index 0000000000..54827b8830 --- /dev/null +++ b/encoding/ssz/query/ssz_info.go @@ -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 "" + } + 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") + } + } +} diff --git a/encoding/ssz/query/ssz_type.go b/encoding/ssz/query/ssz_type.go new file mode 100644 index 0000000000..fe6195cc8f --- /dev/null +++ b/encoding/ssz/query/ssz_type.go @@ -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) + } +} diff --git a/encoding/ssz/query/testutil/BUILD.bazel b/encoding/ssz/query/testutil/BUILD.bazel new file mode 100644 index 0000000000..d97421eaa9 --- /dev/null +++ b/encoding/ssz/query/testutil/BUILD.bazel @@ -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", + ], +) diff --git a/encoding/ssz/query/testutil/runner.go b/encoding/ssz/query/testutil/runner.go new file mode 100644 index 0000000000..bc3db90547 --- /dev/null +++ b/encoding/ssz/query/testutil/runner.go @@ -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") + }) + } + }) +} diff --git a/encoding/ssz/query/testutil/type.go b/encoding/ssz/query/testutil/type.go new file mode 100644 index 0000000000..645fd7d24c --- /dev/null +++ b/encoding/ssz/query/testutil/type.go @@ -0,0 +1,13 @@ +package testutil + +type PathTest struct { + Path string + Expected any +} + +type TestSpec struct { + Name string + Type any + Instance any + PathTests []PathTest +} diff --git a/encoding/ssz/query/testutil/util.go b/encoding/ssz/query/testutil/util.go new file mode 100644 index 0000000000..039e6e8ebd --- /dev/null +++ b/encoding/ssz/query/testutil/util.go @@ -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) + } +} diff --git a/proto/ssz_query/BUILD.bazel b/proto/ssz_query/BUILD.bazel new file mode 100644 index 0000000000..1df73ce8ff --- /dev/null +++ b/proto/ssz_query/BUILD.bazel @@ -0,0 +1,68 @@ +load("@rules_proto//proto:defs.bzl", "proto_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") +load("//proto:ssz_proto_library.bzl", "ssz_proto_files") +load("//tools:ssz.bzl", "SSZ_DEPS", "ssz_gen_marshal") + +# gazelle:ignore + +proto_library( + name = "proto", + srcs = ["ssz_query.proto"], + visibility = ["//visibility:public"], + deps = [ + "//proto/eth/ext:proto", + ], +) + +go_proto_library( + name = "go_proto", + compilers = [ + "@com_github_prysmaticlabs_protoc_gen_go_cast//:go_cast_grpc", + ], + importpath = "github.com/OffchainLabs/prysm/v6/proto/ssz_query", + proto = ":proto", + visibility = ["//visibility:public"], + deps = [ + "//proto/eth/ext:go_default_library", + "@com_github_golang_protobuf//proto:go_default_library", + "@org_golang_google_protobuf//reflect/protoreflect:go_default_library", + "@org_golang_google_protobuf//runtime/protoimpl:go_default_library", + ], +) + +# SSZ generation for test proto messages +ssz_gen_marshal( + name = "ssz_generated", + out = "ssz_query.ssz.go", + go_proto = ":go_proto", + objs = [ + "FixedTestContainer", + "FixedNestedContainer", + ], +) + +go_library( + name = "go_default_library", + srcs = [ + ":ssz_generated", # keep + ], + embed = [":go_proto"], + importpath = "github.com/OffchainLabs/prysm/v6/proto/ssz_query", + visibility = ["//visibility:public"], + deps = SSZ_DEPS + [ + "//proto/eth/ext:go_default_library", + "@com_github_golang_protobuf//proto:go_default_library", + "@com_github_prysmaticlabs_go_bitfield//:go_default_library", + ], +) + +ssz_proto_files( + name = "ssz_proto_files", + srcs = ["ssz_query.proto"], + config = select({ + "//conditions:default": "mainnet", + "//proto:ssz_mainnet": "mainnet", + "//proto:ssz_minimal": "minimal", + }), +) diff --git a/proto/ssz_query/ssz_query.pb.go b/proto/ssz_query/ssz_query.pb.go new file mode 100755 index 0000000000..dac481abe3 --- /dev/null +++ b/proto/ssz_query/ssz_query.pb.go @@ -0,0 +1,289 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.33.0 +// protoc v3.21.7 +// source: proto/ssz_query/ssz_query.proto + +package ssz_query + +import ( + reflect "reflect" + sync "sync" + + _ "github.com/OffchainLabs/prysm/v6/proto/eth/ext" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type FixedNestedContainer struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value1 uint64 `protobuf:"varint,1,opt,name=value1,proto3" json:"value1,omitempty"` + Value2 []byte `protobuf:"bytes,2,opt,name=value2,proto3" json:"value2,omitempty" ssz-size:"32"` +} + +func (x *FixedNestedContainer) Reset() { + *x = FixedNestedContainer{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_ssz_query_ssz_query_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FixedNestedContainer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FixedNestedContainer) ProtoMessage() {} + +func (x *FixedNestedContainer) ProtoReflect() protoreflect.Message { + mi := &file_proto_ssz_query_ssz_query_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FixedNestedContainer.ProtoReflect.Descriptor instead. +func (*FixedNestedContainer) Descriptor() ([]byte, []int) { + return file_proto_ssz_query_ssz_query_proto_rawDescGZIP(), []int{0} +} + +func (x *FixedNestedContainer) GetValue1() uint64 { + if x != nil { + return x.Value1 + } + return 0 +} + +func (x *FixedNestedContainer) GetValue2() []byte { + if x != nil { + return x.Value2 + } + return nil +} + +type FixedTestContainer struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FieldUint32 uint32 `protobuf:"varint,3,opt,name=field_uint32,json=fieldUint32,proto3" json:"field_uint32,omitempty"` + FieldUint64 uint64 `protobuf:"varint,4,opt,name=field_uint64,json=fieldUint64,proto3" json:"field_uint64,omitempty"` + FieldBool bool `protobuf:"varint,5,opt,name=field_bool,json=fieldBool,proto3" json:"field_bool,omitempty"` + FieldBytes32 []byte `protobuf:"bytes,8,opt,name=field_bytes32,json=fieldBytes32,proto3" json:"field_bytes32,omitempty" ssz-size:"32"` + Nested *FixedNestedContainer `protobuf:"bytes,9,opt,name=nested,proto3" json:"nested,omitempty"` + VectorField []uint64 `protobuf:"varint,10,rep,packed,name=vector_field,json=vectorField,proto3" json:"vector_field,omitempty" ssz-size:"24"` + TrailingField []byte `protobuf:"bytes,11,opt,name=trailing_field,json=trailingField,proto3" json:"trailing_field,omitempty" ssz-size:"56"` +} + +func (x *FixedTestContainer) Reset() { + *x = FixedTestContainer{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_ssz_query_ssz_query_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FixedTestContainer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FixedTestContainer) ProtoMessage() {} + +func (x *FixedTestContainer) ProtoReflect() protoreflect.Message { + mi := &file_proto_ssz_query_ssz_query_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FixedTestContainer.ProtoReflect.Descriptor instead. +func (*FixedTestContainer) Descriptor() ([]byte, []int) { + return file_proto_ssz_query_ssz_query_proto_rawDescGZIP(), []int{1} +} + +func (x *FixedTestContainer) GetFieldUint32() uint32 { + if x != nil { + return x.FieldUint32 + } + return 0 +} + +func (x *FixedTestContainer) GetFieldUint64() uint64 { + if x != nil { + return x.FieldUint64 + } + return 0 +} + +func (x *FixedTestContainer) GetFieldBool() bool { + if x != nil { + return x.FieldBool + } + return false +} + +func (x *FixedTestContainer) GetFieldBytes32() []byte { + if x != nil { + return x.FieldBytes32 + } + return nil +} + +func (x *FixedTestContainer) GetNested() *FixedNestedContainer { + if x != nil { + return x.Nested + } + return nil +} + +func (x *FixedTestContainer) GetVectorField() []uint64 { + if x != nil { + return x.VectorField + } + return nil +} + +func (x *FixedTestContainer) GetTrailingField() []byte { + if x != nil { + return x.TrailingField + } + return nil +} + +var File_proto_ssz_query_ssz_query_proto protoreflect.FileDescriptor + +var file_proto_ssz_query_ssz_query_proto_rawDesc = []byte{ + 0x0a, 0x1f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x73, 0x7a, 0x5f, 0x71, 0x75, 0x65, 0x72, + 0x79, 0x2f, 0x73, 0x73, 0x7a, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x09, 0x73, 0x73, 0x7a, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x1a, 0x1b, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x74, 0x68, 0x2f, 0x65, 0x78, 0x74, 0x2f, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x4e, 0x0a, 0x14, 0x46, 0x69, 0x78, + 0x65, 0x64, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x31, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x31, 0x12, 0x1e, 0x0a, 0x06, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x06, 0x8a, 0xb5, 0x18, 0x02, 0x33, + 0x32, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x22, 0xb9, 0x02, 0x0a, 0x12, 0x46, 0x69, + 0x78, 0x65, 0x64, 0x54, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x55, 0x69, 0x6e, + 0x74, 0x33, 0x32, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x75, 0x69, 0x6e, + 0x74, 0x36, 0x34, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x66, 0x69, 0x65, 0x6c, 0x64, + 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x12, 0x1d, 0x0a, 0x0a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, + 0x62, 0x6f, 0x6f, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x66, 0x69, 0x65, 0x6c, + 0x64, 0x42, 0x6f, 0x6f, 0x6c, 0x12, 0x2b, 0x0a, 0x0d, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x62, + 0x79, 0x74, 0x65, 0x73, 0x33, 0x32, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x06, 0x8a, 0xb5, + 0x18, 0x02, 0x33, 0x32, 0x52, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, + 0x33, 0x32, 0x12, 0x37, 0x0a, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x73, 0x73, 0x7a, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x46, + 0x69, 0x78, 0x65, 0x64, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x52, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x12, 0x29, 0x0a, 0x0c, 0x76, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x0a, 0x20, 0x03, 0x28, + 0x04, 0x42, 0x06, 0x8a, 0xb5, 0x18, 0x02, 0x32, 0x34, 0x52, 0x0b, 0x76, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x2d, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x69, + 0x6e, 0x67, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x06, + 0x8a, 0xb5, 0x18, 0x02, 0x35, 0x36, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x69, 0x6e, 0x67, + 0x46, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x32, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, + 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x36, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, + 0x73, 0x73, 0x7a, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_proto_ssz_query_ssz_query_proto_rawDescOnce sync.Once + file_proto_ssz_query_ssz_query_proto_rawDescData = file_proto_ssz_query_ssz_query_proto_rawDesc +) + +func file_proto_ssz_query_ssz_query_proto_rawDescGZIP() []byte { + file_proto_ssz_query_ssz_query_proto_rawDescOnce.Do(func() { + file_proto_ssz_query_ssz_query_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_ssz_query_ssz_query_proto_rawDescData) + }) + return file_proto_ssz_query_ssz_query_proto_rawDescData +} + +var file_proto_ssz_query_ssz_query_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_proto_ssz_query_ssz_query_proto_goTypes = []interface{}{ + (*FixedNestedContainer)(nil), // 0: ssz_query.FixedNestedContainer + (*FixedTestContainer)(nil), // 1: ssz_query.FixedTestContainer +} +var file_proto_ssz_query_ssz_query_proto_depIdxs = []int32{ + 0, // 0: ssz_query.FixedTestContainer.nested:type_name -> ssz_query.FixedNestedContainer + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_proto_ssz_query_ssz_query_proto_init() } +func file_proto_ssz_query_ssz_query_proto_init() { + if File_proto_ssz_query_ssz_query_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proto_ssz_query_ssz_query_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FixedNestedContainer); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_ssz_query_ssz_query_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FixedTestContainer); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_proto_ssz_query_ssz_query_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proto_ssz_query_ssz_query_proto_goTypes, + DependencyIndexes: file_proto_ssz_query_ssz_query_proto_depIdxs, + MessageInfos: file_proto_ssz_query_ssz_query_proto_msgTypes, + }.Build() + File_proto_ssz_query_ssz_query_proto = out.File + file_proto_ssz_query_ssz_query_proto_rawDesc = nil + file_proto_ssz_query_ssz_query_proto_goTypes = nil + file_proto_ssz_query_ssz_query_proto_depIdxs = nil +} diff --git a/proto/ssz_query/ssz_query.proto b/proto/ssz_query/ssz_query.proto new file mode 100644 index 0000000000..e6d3ae2369 --- /dev/null +++ b/proto/ssz_query/ssz_query.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package ssz_query; + +import "proto/eth/ext/options.proto"; + +option go_package = "github.com/OffchainLabs/prysm/v6/proto/ssz_query"; + + +// ===== FIXED-SIZE TEST CONTAINERS ===== +// These containers are designed to test SSZ query functionality with comprehensive coverage +// of all fixed-size SSZ types according to the SSZ specification. + +// FixedNestedContainer - nested container for testing nested field access +// Tests: nested container navigation, field offset calculations within nested structures +message FixedNestedContainer { + uint64 value1 = 1; // Test: uint64 basic type, offset calculation in nested context + bytes value2 = 2 [ (ethereum.eth.ext.ssz_size) = "32" ]; // Test: fixed-size bytes in nested container +} + +// FixedTestContainer - comprehensive fixed-size container for SSZ query testing +// Tests: All basic fixed-size SSZ types, nested containers, vectors, offset/length calculations +// Total size: 333 bytes (4+8+1+32+40+192+56) +message FixedTestContainer { + // Basic integer types - test different integer sizes and their SSZ serialization + uint32 field_uint32 = 3; // Test: uint32 basic type, offset: 0 + uint64 field_uint64 = 4; // Test: uint64 basic type, offset: 4 + + // Boolean type - test boolean serialization (1 byte in SSZ) + bool field_bool = 5; // Test: boolean basic type, offset: 12 + + // Fixed-size bytes - test byte array + bytes field_bytes32 = 8 [ (ethereum.eth.ext.ssz_size) = "32" ]; // Test: 32-byte array, offset: 13 + + // Nested container - test container nesting and field access + FixedNestedContainer nested = 9; // Test: nested container navigation (8+32=40 bytes), offset: 45 + + // Vector type - test fixed-size array of basic elements + repeated uint64 vector_field = 10 [ (ethereum.eth.ext.ssz_size) = "24" ]; // Test: Vector[24] of uint64 (24*8=192 bytes), offset: 85 + + // Additional bytes field - test field ordering and offset calculation + bytes trailing_field = 11 [ (ethereum.eth.ext.ssz_size) = "56" ]; // Test: trailing field after vector, offset: 277 +} diff --git a/proto/ssz_query/ssz_query.ssz.go b/proto/ssz_query/ssz_query.ssz.go new file mode 100644 index 0000000000..c5ec4a7595 --- /dev/null +++ b/proto/ssz_query/ssz_query.ssz.go @@ -0,0 +1,238 @@ +// Code generated by fastssz. DO NOT EDIT. +package ssz_query + +import ( + ssz "github.com/prysmaticlabs/fastssz" +) + +// MarshalSSZ ssz marshals the FixedNestedContainer object +func (f *FixedNestedContainer) MarshalSSZ() ([]byte, error) { + return ssz.MarshalSSZ(f) +} + +// MarshalSSZTo ssz marshals the FixedNestedContainer object to a target array +func (f *FixedNestedContainer) MarshalSSZTo(buf []byte) (dst []byte, err error) { + dst = buf + + // Field (0) 'Value1' + dst = ssz.MarshalUint64(dst, f.Value1) + + // Field (1) 'Value2' + if size := len(f.Value2); size != 32 { + err = ssz.ErrBytesLengthFn("--.Value2", size, 32) + return + } + dst = append(dst, f.Value2...) + + return +} + +// UnmarshalSSZ ssz unmarshals the FixedNestedContainer object +func (f *FixedNestedContainer) UnmarshalSSZ(buf []byte) error { + var err error + size := uint64(len(buf)) + if size != 40 { + return ssz.ErrSize + } + + // Field (0) 'Value1' + f.Value1 = ssz.UnmarshallUint64(buf[0:8]) + + // Field (1) 'Value2' + if cap(f.Value2) == 0 { + f.Value2 = make([]byte, 0, len(buf[8:40])) + } + f.Value2 = append(f.Value2, buf[8:40]...) + + return err +} + +// SizeSSZ returns the ssz encoded size in bytes for the FixedNestedContainer object +func (f *FixedNestedContainer) SizeSSZ() (size int) { + size = 40 + return +} + +// HashTreeRoot ssz hashes the FixedNestedContainer object +func (f *FixedNestedContainer) HashTreeRoot() ([32]byte, error) { + return ssz.HashWithDefaultHasher(f) +} + +// HashTreeRootWith ssz hashes the FixedNestedContainer object with a hasher +func (f *FixedNestedContainer) HashTreeRootWith(hh *ssz.Hasher) (err error) { + indx := hh.Index() + + // Field (0) 'Value1' + hh.PutUint64(f.Value1) + + // Field (1) 'Value2' + if size := len(f.Value2); size != 32 { + err = ssz.ErrBytesLengthFn("--.Value2", size, 32) + return + } + hh.PutBytes(f.Value2) + + hh.Merkleize(indx) + return +} + +// MarshalSSZ ssz marshals the FixedTestContainer object +func (f *FixedTestContainer) MarshalSSZ() ([]byte, error) { + return ssz.MarshalSSZ(f) +} + +// MarshalSSZTo ssz marshals the FixedTestContainer object to a target array +func (f *FixedTestContainer) MarshalSSZTo(buf []byte) (dst []byte, err error) { + dst = buf + + // Field (0) 'FieldUint32' + dst = ssz.MarshalUint32(dst, f.FieldUint32) + + // Field (1) 'FieldUint64' + dst = ssz.MarshalUint64(dst, f.FieldUint64) + + // Field (2) 'FieldBool' + dst = ssz.MarshalBool(dst, f.FieldBool) + + // Field (3) 'FieldBytes32' + if size := len(f.FieldBytes32); size != 32 { + err = ssz.ErrBytesLengthFn("--.FieldBytes32", size, 32) + return + } + dst = append(dst, f.FieldBytes32...) + + // Field (4) 'Nested' + if f.Nested == nil { + f.Nested = new(FixedNestedContainer) + } + if dst, err = f.Nested.MarshalSSZTo(dst); err != nil { + return + } + + // Field (5) 'VectorField' + if size := len(f.VectorField); size != 24 { + err = ssz.ErrVectorLengthFn("--.VectorField", size, 24) + return + } + for ii := 0; ii < 24; ii++ { + dst = ssz.MarshalUint64(dst, f.VectorField[ii]) + } + + // Field (6) 'TrailingField' + if size := len(f.TrailingField); size != 56 { + err = ssz.ErrBytesLengthFn("--.TrailingField", size, 56) + return + } + dst = append(dst, f.TrailingField...) + + return +} + +// UnmarshalSSZ ssz unmarshals the FixedTestContainer object +func (f *FixedTestContainer) UnmarshalSSZ(buf []byte) error { + var err error + size := uint64(len(buf)) + if size != 333 { + return ssz.ErrSize + } + + // Field (0) 'FieldUint32' + f.FieldUint32 = ssz.UnmarshallUint32(buf[0:4]) + + // Field (1) 'FieldUint64' + f.FieldUint64 = ssz.UnmarshallUint64(buf[4:12]) + + // Field (2) 'FieldBool' + f.FieldBool, err = ssz.DecodeBool(buf[12:13]) + if err != nil { + return err + } + + // Field (3) 'FieldBytes32' + if cap(f.FieldBytes32) == 0 { + f.FieldBytes32 = make([]byte, 0, len(buf[13:45])) + } + f.FieldBytes32 = append(f.FieldBytes32, buf[13:45]...) + + // Field (4) 'Nested' + if f.Nested == nil { + f.Nested = new(FixedNestedContainer) + } + if err = f.Nested.UnmarshalSSZ(buf[45:85]); err != nil { + return err + } + + // Field (5) 'VectorField' + f.VectorField = ssz.ExtendUint64(f.VectorField, 24) + for ii := 0; ii < 24; ii++ { + f.VectorField[ii] = ssz.UnmarshallUint64(buf[85:277][ii*8 : (ii+1)*8]) + } + + // Field (6) 'TrailingField' + if cap(f.TrailingField) == 0 { + f.TrailingField = make([]byte, 0, len(buf[277:333])) + } + f.TrailingField = append(f.TrailingField, buf[277:333]...) + + return err +} + +// SizeSSZ returns the ssz encoded size in bytes for the FixedTestContainer object +func (f *FixedTestContainer) SizeSSZ() (size int) { + size = 333 + return +} + +// HashTreeRoot ssz hashes the FixedTestContainer object +func (f *FixedTestContainer) HashTreeRoot() ([32]byte, error) { + return ssz.HashWithDefaultHasher(f) +} + +// HashTreeRootWith ssz hashes the FixedTestContainer object with a hasher +func (f *FixedTestContainer) HashTreeRootWith(hh *ssz.Hasher) (err error) { + indx := hh.Index() + + // Field (0) 'FieldUint32' + hh.PutUint32(f.FieldUint32) + + // Field (1) 'FieldUint64' + hh.PutUint64(f.FieldUint64) + + // Field (2) 'FieldBool' + hh.PutBool(f.FieldBool) + + // Field (3) 'FieldBytes32' + if size := len(f.FieldBytes32); size != 32 { + err = ssz.ErrBytesLengthFn("--.FieldBytes32", size, 32) + return + } + hh.PutBytes(f.FieldBytes32) + + // Field (4) 'Nested' + if err = f.Nested.HashTreeRootWith(hh); err != nil { + return + } + + // Field (5) 'VectorField' + { + if size := len(f.VectorField); size != 24 { + err = ssz.ErrVectorLengthFn("--.VectorField", size, 24) + return + } + subIndx := hh.Index() + for _, i := range f.VectorField { + hh.AppendUint64(i) + } + hh.Merkleize(subIndx) + } + + // Field (6) 'TrailingField' + if size := len(f.TrailingField); size != 56 { + err = ssz.ErrBytesLengthFn("--.TrailingField", size, 56) + return + } + hh.PutBytes(f.TrailingField) + + hh.Merkleize(indx) + return +}