From 29fe707143f9638dcb8933425a4124f596e68889 Mon Sep 17 00:00:00 2001 From: Jun Song <87601811+syjn99@users.noreply.github.com> Date: Fri, 26 Sep 2025 23:23:22 +0900 Subject: [PATCH] SSZ-QL: Support nested `List` type (#15725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add nested 2d list cases * Add elementSize member for listInfo to track each element's byte size * Fix misleading variable in RunStructTest * Changelog * Regen pb file * Update encoding/ssz/query/list.go Co-authored-by: Radosław Kapka * Rename elementSize into plural * Update changelog/syjn99_ssz-ql-nested-list.md --------- Co-authored-by: Radosław Kapka --- changelog/syjn99_ssz-ql-nested-list.md | 3 + encoding/ssz/query/analyzer.go | 27 +++- encoding/ssz/query/list.go | 34 +++++ encoding/ssz/query/query_test.go | 57 +++++-- encoding/ssz/query/ssz_info.go | 10 +- encoding/ssz/query/testutil/runner.go | 6 +- proto/ssz_query/ssz_query.pb.go | 58 ++++--- proto/ssz_query/ssz_query.proto | 13 +- proto/ssz_query/ssz_query.ssz.go | 204 ++++++++++++++++++++++--- 9 files changed, 355 insertions(+), 57 deletions(-) create mode 100644 changelog/syjn99_ssz-ql-nested-list.md diff --git a/changelog/syjn99_ssz-ql-nested-list.md b/changelog/syjn99_ssz-ql-nested-list.md new file mode 100644 index 0000000000..b45b0393f4 --- /dev/null +++ b/changelog/syjn99_ssz-ql-nested-list.md @@ -0,0 +1,3 @@ +### Fixed + +- SSZ-QL: Support nested `List` type (e.g., `ExecutionPayload.Transactions`) diff --git a/encoding/ssz/query/analyzer.go b/encoding/ssz/query/analyzer.go index 5bd882b802..5369d11561 100644 --- a/encoding/ssz/query/analyzer.go +++ b/encoding/ssz/query/analyzer.go @@ -60,9 +60,21 @@ func PopulateVariableLengthInfo(sszInfo *sszInfo, value any) error { if val.Kind() != reflect.Slice { return fmt.Errorf("expected slice for List type, got %v", val.Kind()) } + length := val.Len() - length := uint64(val.Len()) - if err := listInfo.SetLength(length); err != nil { + if listInfo.element.isVariable { + listInfo.elementSizes = make([]uint64, 0, length) + + // Populate nested variable-sized type element lengths recursively. + for i := range length { + if err := PopulateVariableLengthInfo(listInfo.element, val.Index(i).Interface()); err != nil { + return fmt.Errorf("could not populate nested list element at index %d: %w", i, err) + } + listInfo.elementSizes = append(listInfo.elementSizes, listInfo.element.Size()) + } + } + + if err := listInfo.SetLength(uint64(length)); err != nil { return fmt.Errorf("could not set list length: %w", err) } @@ -111,15 +123,20 @@ func PopulateVariableLengthInfo(sszInfo *sszInfo, value any) error { continue } - // Set the actual offset for variable-sized fields. - fieldInfo.offset = currentOffset - // Recursively populate variable-sized fields. fieldValue := derefValue.FieldByName(fieldInfo.goFieldName) if err := PopulateVariableLengthInfo(childSszInfo, fieldValue.Interface()); err != nil { return fmt.Errorf("could not populate from value for field %s: %w", fieldName, err) } + // Each variable-sized element needs an offset entry. + if childSszInfo.sszType == List { + currentOffset += childSszInfo.listInfo.OffsetBytes() + } + + // Set the actual offset for variable-sized fields. + fieldInfo.offset = currentOffset + currentOffset += childSszInfo.Size() } diff --git a/encoding/ssz/query/list.go b/encoding/ssz/query/list.go index ef2278d9c5..5b797a422f 100644 --- a/encoding/ssz/query/list.go +++ b/encoding/ssz/query/list.go @@ -16,6 +16,8 @@ type listInfo struct { element *sszInfo // length is the actual number of elements at runtime (0 if not set). length uint64 + // elementSizes caches each element's byte size for variable-sized type elements + elementSizes []uint64 } func (l *listInfo) Limit() uint64 { @@ -51,3 +53,35 @@ func (l *listInfo) SetLength(length uint64) error { l.length = length return nil } + +func (l *listInfo) Size() uint64 { + if l == nil { + return 0 + } + + // For fixed-sized type elements, size is multiplying length by element size. + if !l.element.isVariable { + return l.length * l.element.Size() + } + + // For variable-sized type elements, sum up the sizes of each element. + totalSize := uint64(0) + for _, sz := range l.elementSizes { + totalSize += sz + } + return totalSize +} + +// OffsetBytes returns the total number of offset bytes used for the list elements. +// Each variable-sized element uses 4 bytes to store its offset. +func (l *listInfo) OffsetBytes() uint64 { + if l == nil { + return 0 + } + + if !l.element.isVariable { + return 0 + } + + return offsetBytes * l.length +} diff --git a/encoding/ssz/query/query_test.go b/encoding/ssz/query/query_test.go index 3541274703..d67ab37bde 100644 --- a/encoding/ssz/query/query_test.go +++ b/encoding/ssz/query/query_test.go @@ -133,56 +133,73 @@ func TestCalculateOffsetAndLength(t *testing.T) { { name: "field_list_uint64", path: ".field_list_uint64", - expectedOffset: 108, // First part of variable-sized type. + expectedOffset: 112, // First part of variable-sized type. expectedLength: 40, // 5 elements * uint64 (8 bytes each) }, { name: "field_list_container", path: ".field_list_container", - expectedOffset: 148, // Second part of variable-sized type. + expectedOffset: 152, // Second part of variable-sized type. expectedLength: 120, // 3 elements * FixedNestedContainer (40 bytes each) }, { name: "field_list_bytes32", path: ".field_list_bytes32", - expectedOffset: 268, + expectedOffset: 272, expectedLength: 96, // 3 elements * 32 bytes each }, // Nested paths { name: "nested", path: ".nested", - expectedOffset: 364, + expectedOffset: 368, // Calculated with: // - Value1: 8 bytes // - field_list_uint64 offset: 4 bytes // - field_list_uint64 length: 40 bytes - expectedLength: 52, + // - nested_list_field offset: 4 bytes + // - nested_list_field length: 99 bytes + // - 3 offset pointers for each element in nested_list_field: 12 bytes + // Total: 8 + 4 + 40 + 4 + 99 + 12 = 167 bytes + expectedLength: 167, }, { name: "nested.value1", path: ".nested.value1", - expectedOffset: 364, + expectedOffset: 368, expectedLength: 8, }, { name: "nested.field_list_uint64", path: ".nested.field_list_uint64", - expectedOffset: 376, + expectedOffset: 384, expectedLength: 40, }, + { + name: "nested.nested_list_field", + path: ".nested.nested_list_field", + expectedOffset: 436, + expectedLength: 99, + }, // Bitlist field { name: "bitlist_field", path: ".bitlist_field", - expectedOffset: 416, + expectedOffset: 535, expectedLength: 33, // 32 bytes + 1 byte for length delimiter }, + // 2D bytes field + { + name: "nested_list_field", + path: ".nested_list_field", + expectedOffset: 580, + expectedLength: 99, + }, // Fixed trailing field { name: "trailing_field", path: ".trailing_field", - expectedOffset: 52, // After leading_field + 5 offset pointers + expectedOffset: 56, // After leading_field + 6 offset pointers expectedLength: 56, }, } @@ -377,6 +394,15 @@ func createVariableTestContainer() *sszquerypb.VariableTestContainer { bitlistField.SetBitAt(100, true) bitlistField.SetBitAt(255, true) + // Total size: 3 lists with lengths 32, 33, and 34 = 99 bytes + nestedListField := make([][]byte, 3) + for i := range nestedListField { + nestedListField[i] = make([]byte, (32 + i)) // Different lengths for each sub-list + for j := range nestedListField[i] { + nestedListField[i][j] = byte(j + i*16) + } + } + return &sszquerypb.VariableTestContainer{ // Fixed leading field LeadingField: leadingField, @@ -394,11 +420,15 @@ func createVariableTestContainer() *sszquerypb.VariableTestContainer { Nested: &sszquerypb.VariableNestedContainer{ Value1: 42, FieldListUint64: []uint64{1, 2, 3, 4, 5}, + NestedListField: nestedListField, }, // Bitlist field BitlistField: bitlistField, + // 2D bytes field + NestedListField: nestedListField, + // Fixed trailing field TrailingField: trailingField, } @@ -445,11 +475,20 @@ func getVariableTestContainerSpec() testutil.TestSpec { Path: ".nested.field_list_uint64", Expected: testContainer.Nested.FieldListUint64, }, + { + Path: ".nested.nested_list_field", + Expected: testContainer.Nested.NestedListField, + }, // Bitlist field { Path: ".bitlist_field", Expected: testContainer.BitlistField, }, + // 2D bytes field + { + Path: ".nested_list_field", + Expected: testContainer.NestedListField, + }, // Fixed trailing field { Path: ".trailing_field", diff --git a/encoding/ssz/query/ssz_info.go b/encoding/ssz/query/ssz_info.go index ca0c7e466c..e360013443 100644 --- a/encoding/ssz/query/ssz_info.go +++ b/encoding/ssz/query/ssz_info.go @@ -54,10 +54,7 @@ func (info *sszInfo) Size() uint64 { switch info.sszType { case List: - length := info.listInfo.length - elementSize := info.listInfo.element.Size() - - return length * elementSize + return info.listInfo.Size() case Bitlist: return info.bitlistInfo.Size() @@ -69,6 +66,11 @@ func (info *sszInfo) Size() uint64 { continue } + // Include offset bytes inside nested lists. + if fieldInfo.sszInfo.sszType == List { + size += fieldInfo.sszInfo.listInfo.OffsetBytes() + } + size += fieldInfo.sszInfo.Size() } return size diff --git a/encoding/ssz/query/testutil/runner.go b/encoding/ssz/query/testutil/runner.go index 92dbc94c13..e639dd3fc7 100644 --- a/encoding/ssz/query/testutil/runner.go +++ b/encoding/ssz/query/testutil/runner.go @@ -31,10 +31,10 @@ func RunStructTest(t *testing.T, spec TestSpec) { _, offset, length, err := query.CalculateOffsetAndLength(info, path) require.NoError(t, err) - expectedRawBytes := marshalledData[offset : offset+length] - rawBytes, err := marshalAny(pathTest.Expected) + actualRawBytes := marshalledData[offset : offset+length] + expectedRawBytes, 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") + require.DeepEqual(t, actualRawBytes, expectedRawBytes, "Extracted value should match expected") }) } }) diff --git a/proto/ssz_query/ssz_query.pb.go b/proto/ssz_query/ssz_query.pb.go index 52d1c739f5..3d273eeeb1 100755 --- a/proto/ssz_query/ssz_query.pb.go +++ b/proto/ssz_query/ssz_query.pb.go @@ -195,6 +195,7 @@ type VariableNestedContainer struct { state protoimpl.MessageState `protogen:"open.v1"` Value1 uint64 `protobuf:"varint,1,opt,name=value1,proto3" json:"value1,omitempty"` FieldListUint64 []uint64 `protobuf:"varint,2,rep,packed,name=field_list_uint64,json=fieldListUint64,proto3" json:"field_list_uint64,omitempty" ssz-max:"100"` + NestedListField [][]byte `protobuf:"bytes,3,rep,name=nested_list_field,json=nestedListField,proto3" json:"nested_list_field,omitempty" ssz-max:"100,50" ssz-size:"?,?"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -243,6 +244,13 @@ func (x *VariableNestedContainer) GetFieldListUint64() []uint64 { return nil } +func (x *VariableNestedContainer) GetNestedListField() [][]byte { + if x != nil { + return x.NestedListField + } + return nil +} + type VariableTestContainer struct { state protoimpl.MessageState `protogen:"open.v1"` LeadingField []byte `protobuf:"bytes,1,opt,name=leading_field,json=leadingField,proto3" json:"leading_field,omitempty" ssz-size:"32"` @@ -251,7 +259,8 @@ type VariableTestContainer struct { FieldListBytes32 [][]byte `protobuf:"bytes,4,rep,name=field_list_bytes32,json=fieldListBytes32,proto3" json:"field_list_bytes32,omitempty" ssz-max:"100" ssz-size:"?,32"` Nested *VariableNestedContainer `protobuf:"bytes,5,opt,name=nested,proto3" json:"nested,omitempty"` BitlistField github_com_prysmaticlabs_go_bitfield.Bitlist `protobuf:"bytes,6,opt,name=bitlist_field,json=bitlistField,proto3" json:"bitlist_field,omitempty" cast-type:"github.com/prysmaticlabs/go-bitfield.Bitlist" ssz-max:"2048"` - TrailingField []byte `protobuf:"bytes,7,opt,name=trailing_field,json=trailingField,proto3" json:"trailing_field,omitempty" ssz-size:"56"` + NestedListField [][]byte `protobuf:"bytes,7,rep,name=nested_list_field,json=nestedListField,proto3" json:"nested_list_field,omitempty" ssz-max:"100,50" ssz-size:"?,?"` + TrailingField []byte `protobuf:"bytes,8,opt,name=trailing_field,json=trailingField,proto3" json:"trailing_field,omitempty" ssz-size:"56"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -328,6 +337,13 @@ func (x *VariableTestContainer) GetBitlistField() github_com_prysmaticlabs_go_bi return github_com_prysmaticlabs_go_bitfield.Bitlist(nil) } +func (x *VariableTestContainer) GetNestedListField() [][]byte { + if x != nil { + return x.NestedListField + } + return nil +} + func (x *VariableTestContainer) GetTrailingField() []byte { if x != nil { return x.TrailingField @@ -384,14 +400,18 @@ var file_proto_ssz_query_ssz_query_proto_rawDesc = []byte{ 0x74, 0x76, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x35, 0x31, 0x32, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x2d, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x0a, 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, 0x22, 0x66, - 0x0a, 0x17, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 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, 0x33, 0x0a, 0x11, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, - 0x75, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x18, 0x02, 0x20, 0x03, 0x28, 0x04, 0x42, 0x07, 0x92, 0xb5, - 0x18, 0x03, 0x31, 0x30, 0x30, 0x52, 0x0f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4c, 0x69, 0x73, 0x74, - 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x22, 0xdf, 0x03, 0x0a, 0x15, 0x56, 0x61, 0x72, 0x69, 0x61, + 0x0d, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x69, 0x6e, 0x67, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x22, 0xa5, + 0x01, 0x0a, 0x17, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 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, 0x33, 0x0a, 0x11, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6c, 0x69, 0x73, 0x74, + 0x5f, 0x75, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x18, 0x02, 0x20, 0x03, 0x28, 0x04, 0x42, 0x07, 0x92, + 0xb5, 0x18, 0x03, 0x31, 0x30, 0x30, 0x52, 0x0f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4c, 0x69, 0x73, + 0x74, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x12, 0x3d, 0x0a, 0x11, 0x6e, 0x65, 0x73, 0x74, 0x65, + 0x64, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0c, 0x42, 0x11, 0x8a, 0xb5, 0x18, 0x03, 0x3f, 0x2c, 0x3f, 0x92, 0xb5, 0x18, 0x06, 0x31, + 0x30, 0x30, 0x2c, 0x35, 0x30, 0x52, 0x0f, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x4c, 0x69, 0x73, + 0x74, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x22, 0x9e, 0x04, 0x0a, 0x15, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x0d, 0x6c, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x06, 0x8a, 0xb5, 0x18, 0x02, 0x33, 0x32, 0x52, @@ -418,14 +438,18 @@ var file_proto_ssz_query_ssz_query_proto_rawDesc = []byte{ 0x6d, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x61, 0x74, 0x69, 0x63, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x67, 0x6f, 0x2d, 0x62, 0x69, 0x74, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x42, 0x69, 0x74, 0x6c, 0x69, 0x73, 0x74, 0x92, 0xb5, 0x18, 0x04, 0x32, 0x30, 0x34, 0x38, 0x52, 0x0c, 0x62, 0x69, 0x74, - 0x6c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x2d, 0x0a, 0x0e, 0x74, 0x72, 0x61, - 0x69, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x07, 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, + 0x6c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x3d, 0x0a, 0x11, 0x6e, 0x65, 0x73, + 0x74, 0x65, 0x64, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x07, + 0x20, 0x03, 0x28, 0x0c, 0x42, 0x11, 0x8a, 0xb5, 0x18, 0x03, 0x3f, 0x2c, 0x3f, 0x92, 0xb5, 0x18, + 0x06, 0x31, 0x30, 0x30, 0x2c, 0x35, 0x30, 0x52, 0x0f, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x4c, + 0x69, 0x73, 0x74, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x2d, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x69, + 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x08, 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 ( diff --git a/proto/ssz_query/ssz_query.proto b/proto/ssz_query/ssz_query.proto index 5902c2b120..4fe88515d7 100644 --- a/proto/ssz_query/ssz_query.proto +++ b/proto/ssz_query/ssz_query.proto @@ -65,6 +65,10 @@ message FixedTestContainer { message VariableNestedContainer { uint64 value1 = 1; repeated uint64 field_list_uint64 = 2 [ (ethereum.eth.ext.ssz_max) = "100" ]; + repeated bytes nested_list_field = 3 [ + (ethereum.eth.ext.ssz_size) = "?,?", + (ethereum.eth.ext.ssz_max) = "100,50" + ]; } // VariableTestContainer - comprehensive variable-size container for SSZ query testing @@ -96,7 +100,14 @@ message VariableTestContainer { "github.com/prysmaticlabs/go-bitfield.Bitlist" ]; + // 2D bytes list - test list of bytelists. + // e.g., ExecutionPayload.transactions + repeated bytes nested_list_field = 7 [ + (ethereum.eth.ext.ssz_size) = "?,?", + (ethereum.eth.ext.ssz_max) = "100,50" + ]; + // Fixed-size trailing field - test fixed field after variable fields // Verifies correct offset calculation after variable-size fields - bytes trailing_field = 7 [ (ethereum.eth.ext.ssz_size) = "56" ]; // Test: fixed 56-byte field at end, offset: 32 + 4 + 4 + 4 + 4 + 4 = 52 + bytes trailing_field = 8 [ (ethereum.eth.ext.ssz_size) = "56" ]; // Test: fixed 56-byte field at end, offset: 32 + 4 + 4 + 4 + 4 + 4 + 4 = 56 } diff --git a/proto/ssz_query/ssz_query.ssz.go b/proto/ssz_query/ssz_query.ssz.go index 2f20bb8c59..677d3db7a9 100644 --- a/proto/ssz_query/ssz_query.ssz.go +++ b/proto/ssz_query/ssz_query.ssz.go @@ -324,7 +324,7 @@ func (v *VariableNestedContainer) MarshalSSZ() ([]byte, error) { // MarshalSSZTo ssz marshals the VariableNestedContainer object to a target array func (v *VariableNestedContainer) MarshalSSZTo(buf []byte) (dst []byte, err error) { dst = buf - offset := int(12) + offset := int(16) // Field (0) 'Value1' dst = ssz.MarshalUint64(dst, v.Value1) @@ -333,6 +333,13 @@ func (v *VariableNestedContainer) MarshalSSZTo(buf []byte) (dst []byte, err erro dst = ssz.WriteOffset(dst, offset) offset += len(v.FieldListUint64) * 8 + // Offset (2) 'NestedListField' + dst = ssz.WriteOffset(dst, offset) + for ii := 0; ii < len(v.NestedListField); ii++ { + offset += 4 + offset += len(v.NestedListField[ii]) + } + // Field (1) 'FieldListUint64' if size := len(v.FieldListUint64); size > 100 { err = ssz.ErrListTooBigFn("--.FieldListUint64", size, 100) @@ -342,6 +349,26 @@ func (v *VariableNestedContainer) MarshalSSZTo(buf []byte) (dst []byte, err erro dst = ssz.MarshalUint64(dst, v.FieldListUint64[ii]) } + // Field (2) 'NestedListField' + if size := len(v.NestedListField); size > 100 { + err = ssz.ErrListTooBigFn("--.NestedListField", size, 100) + return + } + { + offset = 4 * len(v.NestedListField) + for ii := 0; ii < len(v.NestedListField); ii++ { + dst = ssz.WriteOffset(dst, offset) + offset += len(v.NestedListField[ii]) + } + } + for ii := 0; ii < len(v.NestedListField); ii++ { + if size := len(v.NestedListField[ii]); size > 50 { + err = ssz.ErrBytesLengthFn("--.NestedListField[ii]", size, 50) + return + } + dst = append(dst, v.NestedListField[ii]...) + } + return } @@ -349,12 +376,12 @@ func (v *VariableNestedContainer) MarshalSSZTo(buf []byte) (dst []byte, err erro func (v *VariableNestedContainer) UnmarshalSSZ(buf []byte) error { var err error size := uint64(len(buf)) - if size < 12 { + if size < 16 { return ssz.ErrSize } tail := buf - var o1 uint64 + var o1, o2 uint64 // Field (0) 'Value1' v.Value1 = ssz.UnmarshallUint64(buf[0:8]) @@ -364,13 +391,18 @@ func (v *VariableNestedContainer) UnmarshalSSZ(buf []byte) error { return ssz.ErrOffset } - if o1 != 12 { + if o1 != 16 { return ssz.ErrInvalidVariableOffset } + // Offset (2) 'NestedListField' + if o2 = ssz.ReadOffset(buf[12:16]); o2 > size || o1 > o2 { + return ssz.ErrOffset + } + // Field (1) 'FieldListUint64' { - buf = tail[o1:] + buf = tail[o1:o2] num, err := ssz.DivideInt2(len(buf), 8, 100) if err != nil { return err @@ -380,16 +412,45 @@ func (v *VariableNestedContainer) UnmarshalSSZ(buf []byte) error { v.FieldListUint64[ii] = ssz.UnmarshallUint64(buf[ii*8 : (ii+1)*8]) } } + + // Field (2) 'NestedListField' + { + buf = tail[o2:] + num, err := ssz.DecodeDynamicLength(buf, 100) + if err != nil { + return err + } + v.NestedListField = make([][]byte, num) + err = ssz.UnmarshalDynamic(buf, num, func(indx int, buf []byte) (err error) { + if len(buf) > 50 { + return ssz.ErrBytesLength + } + if cap(v.NestedListField[indx]) == 0 { + v.NestedListField[indx] = make([]byte, 0, len(buf)) + } + v.NestedListField[indx] = append(v.NestedListField[indx], buf...) + return nil + }) + if err != nil { + return err + } + } return err } // SizeSSZ returns the ssz encoded size in bytes for the VariableNestedContainer object func (v *VariableNestedContainer) SizeSSZ() (size int) { - size = 12 + size = 16 // Field (1) 'FieldListUint64' size += len(v.FieldListUint64) * 8 + // Field (2) 'NestedListField' + for ii := 0; ii < len(v.NestedListField); ii++ { + size += 4 + size += len(v.NestedListField[ii]) + } + return } @@ -421,6 +482,29 @@ func (v *VariableNestedContainer) HashTreeRootWith(hh *ssz.Hasher) (err error) { hh.MerkleizeWithMixin(subIndx, numItems, ssz.CalculateLimit(100, numItems, 8)) } + // Field (2) 'NestedListField' + { + subIndx := hh.Index() + num := uint64(len(v.NestedListField)) + if num > 100 { + err = ssz.ErrIncorrectListSize + return + } + for _, elem := range v.NestedListField { + { + elemIndx := hh.Index() + byteLen := uint64(len(elem)) + if byteLen > 50 { + err = ssz.ErrIncorrectListSize + return + } + hh.AppendBytes32(elem) + hh.MerkleizeWithMixin(elemIndx, byteLen, (50+31)/32) + } + } + hh.MerkleizeWithMixin(subIndx, num, 100) + } + hh.Merkleize(indx) return } @@ -433,7 +517,7 @@ func (v *VariableTestContainer) MarshalSSZ() ([]byte, error) { // MarshalSSZTo ssz marshals the VariableTestContainer object to a target array func (v *VariableTestContainer) MarshalSSZTo(buf []byte) (dst []byte, err error) { dst = buf - offset := int(108) + offset := int(112) // Field (0) 'LeadingField' if size := len(v.LeadingField); size != 32 { @@ -465,7 +549,14 @@ func (v *VariableTestContainer) MarshalSSZTo(buf []byte) (dst []byte, err error) dst = ssz.WriteOffset(dst, offset) offset += len(v.BitlistField) - // Field (6) 'TrailingField' + // Offset (6) 'NestedListField' + dst = ssz.WriteOffset(dst, offset) + for ii := 0; ii < len(v.NestedListField); ii++ { + offset += 4 + offset += len(v.NestedListField[ii]) + } + + // Field (7) 'TrailingField' if size := len(v.TrailingField); size != 56 { err = ssz.ErrBytesLengthFn("--.TrailingField", size, 56) return @@ -517,6 +608,26 @@ func (v *VariableTestContainer) MarshalSSZTo(buf []byte) (dst []byte, err error) } dst = append(dst, v.BitlistField...) + // Field (6) 'NestedListField' + if size := len(v.NestedListField); size > 100 { + err = ssz.ErrListTooBigFn("--.NestedListField", size, 100) + return + } + { + offset = 4 * len(v.NestedListField) + for ii := 0; ii < len(v.NestedListField); ii++ { + dst = ssz.WriteOffset(dst, offset) + offset += len(v.NestedListField[ii]) + } + } + for ii := 0; ii < len(v.NestedListField); ii++ { + if size := len(v.NestedListField[ii]); size > 50 { + err = ssz.ErrBytesLengthFn("--.NestedListField[ii]", size, 50) + return + } + dst = append(dst, v.NestedListField[ii]...) + } + return } @@ -524,12 +635,12 @@ func (v *VariableTestContainer) MarshalSSZTo(buf []byte) (dst []byte, err error) func (v *VariableTestContainer) UnmarshalSSZ(buf []byte) error { var err error size := uint64(len(buf)) - if size < 108 { + if size < 112 { return ssz.ErrSize } tail := buf - var o1, o2, o3, o4, o5 uint64 + var o1, o2, o3, o4, o5, o6 uint64 // Field (0) 'LeadingField' if cap(v.LeadingField) == 0 { @@ -542,7 +653,7 @@ func (v *VariableTestContainer) UnmarshalSSZ(buf []byte) error { return ssz.ErrOffset } - if o1 != 108 { + if o1 != 112 { return ssz.ErrInvalidVariableOffset } @@ -566,11 +677,16 @@ func (v *VariableTestContainer) UnmarshalSSZ(buf []byte) error { return ssz.ErrOffset } - // Field (6) 'TrailingField' - if cap(v.TrailingField) == 0 { - v.TrailingField = make([]byte, 0, len(buf[52:108])) + // Offset (6) 'NestedListField' + if o6 = ssz.ReadOffset(buf[52:56]); o6 > size || o5 > o6 { + return ssz.ErrOffset } - v.TrailingField = append(v.TrailingField, buf[52:108]...) + + // Field (7) 'TrailingField' + if cap(v.TrailingField) == 0 { + v.TrailingField = make([]byte, 0, len(buf[56:112])) + } + v.TrailingField = append(v.TrailingField, buf[56:112]...) // Field (1) 'FieldListUint64' { @@ -632,7 +748,7 @@ func (v *VariableTestContainer) UnmarshalSSZ(buf []byte) error { // Field (5) 'BitlistField' { - buf = tail[o5:] + buf = tail[o5:o6] if err = ssz.ValidateBitlist(buf, 2048); err != nil { return err } @@ -641,12 +757,35 @@ func (v *VariableTestContainer) UnmarshalSSZ(buf []byte) error { } v.BitlistField = append(v.BitlistField, buf...) } + + // Field (6) 'NestedListField' + { + buf = tail[o6:] + num, err := ssz.DecodeDynamicLength(buf, 100) + if err != nil { + return err + } + v.NestedListField = make([][]byte, num) + err = ssz.UnmarshalDynamic(buf, num, func(indx int, buf []byte) (err error) { + if len(buf) > 50 { + return ssz.ErrBytesLength + } + if cap(v.NestedListField[indx]) == 0 { + v.NestedListField[indx] = make([]byte, 0, len(buf)) + } + v.NestedListField[indx] = append(v.NestedListField[indx], buf...) + return nil + }) + if err != nil { + return err + } + } return err } // SizeSSZ returns the ssz encoded size in bytes for the VariableTestContainer object func (v *VariableTestContainer) SizeSSZ() (size int) { - size = 108 + size = 112 // Field (1) 'FieldListUint64' size += len(v.FieldListUint64) * 8 @@ -666,6 +805,12 @@ func (v *VariableTestContainer) SizeSSZ() (size int) { // Field (5) 'BitlistField' size += len(v.BitlistField) + // Field (6) 'NestedListField' + for ii := 0; ii < len(v.NestedListField); ii++ { + size += 4 + size += len(v.NestedListField[ii]) + } + return } @@ -748,7 +893,30 @@ func (v *VariableTestContainer) HashTreeRootWith(hh *ssz.Hasher) (err error) { } hh.PutBitlist(v.BitlistField, 2048) - // Field (6) 'TrailingField' + // Field (6) 'NestedListField' + { + subIndx := hh.Index() + num := uint64(len(v.NestedListField)) + if num > 100 { + err = ssz.ErrIncorrectListSize + return + } + for _, elem := range v.NestedListField { + { + elemIndx := hh.Index() + byteLen := uint64(len(elem)) + if byteLen > 50 { + err = ssz.ErrIncorrectListSize + return + } + hh.AppendBytes32(elem) + hh.MerkleizeWithMixin(elemIndx, byteLen, (50+31)/32) + } + } + hh.MerkleizeWithMixin(subIndx, num, 100) + } + + // Field (7) 'TrailingField' if size := len(v.TrailingField); size != 56 { err = ssz.ErrBytesLengthFn("--.TrailingField", size, 56) return