mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 15:37:56 -05:00
* Add VariableTestContainer in ssz_query.proto * Add listInfo * Use errors.New for making an error with a static string literal * Add listInfo field when analyzing the List type * Persist the field order in the container * Add actualOffset and goFieldName at fieldInfo * Add PopulateFromValue function & update test runner * Handle slice of ssz object for marshalling * Add CalculateOffsetAndLength test * Add comments for better doc * Changelog :) * Apply reviews from Radek * Remove actualOffset and update offset field instead * Add Nested container of variable-sized for testing nested path * Fix offset adding logics: for variable-sized field, always add 4 instead of its fixed size * Fix multiple import issue --------- Co-authored-by: Radosław Kapka <rkapka@wp.pl>
341 lines
9.4 KiB
Go
341 lines
9.4 KiB
Go
package query
|
|
|
|
import (
|
|
"errors"
|
|
"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
|
|
}
|
|
|
|
// PopulateVariableLengthInfo populates runtime information for SSZ fields of variable-sized types.
|
|
// This function updates the sszInfo structure with actual lengths and offsets that can only
|
|
// be determined at runtime for variable-sized items like Lists and variable-sized Container fields.
|
|
func PopulateVariableLengthInfo(sszInfo *sszInfo, value any) error {
|
|
if sszInfo == nil {
|
|
return errors.New("sszInfo is nil")
|
|
}
|
|
|
|
if value == nil {
|
|
return errors.New("value is nil")
|
|
}
|
|
|
|
// Short circuit: If the type is fixed-sized, we don't need to fill in the info.
|
|
if !sszInfo.isVariable {
|
|
return nil
|
|
}
|
|
|
|
switch sszInfo.sszType {
|
|
// In List case, we have to set the actual length of the list.
|
|
case List:
|
|
listInfo, err := sszInfo.ListInfo()
|
|
if err != nil {
|
|
return fmt.Errorf("could not get list info: %w", err)
|
|
}
|
|
|
|
if listInfo == nil {
|
|
return errors.New("listInfo is nil")
|
|
}
|
|
|
|
val := reflect.ValueOf(value)
|
|
if val.Kind() != reflect.Slice {
|
|
return fmt.Errorf("expected slice for List type, got %v", val.Kind())
|
|
}
|
|
|
|
length := uint64(val.Len())
|
|
if err := listInfo.SetLength(length); err != nil {
|
|
return fmt.Errorf("could not set list length: %w", err)
|
|
}
|
|
|
|
return nil
|
|
// In Container case, we need to recursively populate variable-sized fields.
|
|
case Container:
|
|
containerInfo, err := sszInfo.ContainerInfo()
|
|
if err != nil {
|
|
return fmt.Errorf("could not get container info: %w", err)
|
|
}
|
|
|
|
// Dereference first in case value is a pointer.
|
|
derefValue := dereferencePointer(value)
|
|
|
|
// Start with the fixed size of this Container.
|
|
currentOffset := sszInfo.FixedSize()
|
|
|
|
for _, fieldName := range containerInfo.order {
|
|
fieldInfo := containerInfo.fields[fieldName]
|
|
childSszInfo := fieldInfo.sszInfo
|
|
if childSszInfo == nil {
|
|
return fmt.Errorf("sszInfo is nil for field %s", fieldName)
|
|
}
|
|
|
|
// Skip fixed-size fields.
|
|
if !childSszInfo.isVariable {
|
|
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)
|
|
}
|
|
|
|
currentOffset += childSszInfo.Size()
|
|
}
|
|
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unsupported SSZ type (%s) for variable size info", sszInfo.sszType)
|
|
}
|
|
}
|
|
|
|
// 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, errors.New("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, errors.New("element info is required for List")
|
|
}
|
|
|
|
return &sszInfo{
|
|
sszType: List,
|
|
typ: typ,
|
|
|
|
fixedSize: offsetBytes,
|
|
isVariable: true,
|
|
|
|
listInfo: &listInfo{
|
|
limit: limit,
|
|
element: elementInfo,
|
|
},
|
|
}, 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, errors.New("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())
|
|
}
|
|
|
|
fields := make(map[string]*fieldInfo)
|
|
order := make([]string, 0, typ.NumField())
|
|
|
|
sszInfo := &sszInfo{
|
|
sszType: Container,
|
|
typ: typ,
|
|
}
|
|
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)
|
|
}
|
|
|
|
// Store nested struct info.
|
|
fields[fieldName] = &fieldInfo{
|
|
sszInfo: info,
|
|
offset: currentOffset,
|
|
goFieldName: field.Name,
|
|
}
|
|
// Persist order
|
|
order = append(order, fieldName)
|
|
|
|
// Update the current offset depending on whether the field is variable-sized.
|
|
if info.isVariable {
|
|
// If one of the fields is variable-sized,
|
|
// the entire struct is considered variable-sized.
|
|
sszInfo.isVariable = true
|
|
currentOffset += offsetBytes
|
|
} else {
|
|
currentOffset += info.fixedSize
|
|
}
|
|
}
|
|
|
|
sszInfo.fixedSize = currentOffset
|
|
sszInfo.containerInfo = &containerInfo{
|
|
fields: fields,
|
|
order: order,
|
|
}
|
|
|
|
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
|
|
}
|