mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-08 23:18:15 -05:00
* Move ssz_query objects into testing folder (ensuring test objects only used in test environment) * Add containers for response * Export sszInfo * Add QueryBeaconState/Block * Add comments and few refactor * Fix merge conflict issues * Return 500 when calculate offset fails * Add test for QueryBeaconState * Add test for QueryBeaconBlock * Changelog :) * Rename `QuerySSZRequest` to `SSZQueryRequest` * Fix middleware hooks for RPC to accept JSON from client and return SSZ * Convert to `SSZObject` directly from proto * Move marshalling/calculating hash tree root part after `CalculateOffsetAndLength` * Make nogo happy * Add informing comment for using proto unsafe conversion --------- Co-authored-by: Radosław Kapka <rkapka@wp.pl>
466 lines
13 KiB
Go
466 lines
13 KiB
Go
package query
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
const offsetBytes = 4
|
|
|
|
// AnalyzeObject analyzes given object and returns its SSZ information.
|
|
func AnalyzeObject(obj SSZObject) (*SszInfo, error) {
|
|
value := reflect.ValueOf(obj)
|
|
|
|
info, err := analyzeType(value, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not analyze type %s: %w", value.Type().Name(), err)
|
|
}
|
|
|
|
// Populate variable-length information using the actual value.
|
|
err = PopulateVariableLengthInfo(info, value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not populate variable length info for 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 reflect.Value) error {
|
|
if sszInfo == nil {
|
|
return errors.New("sszInfo is nil")
|
|
}
|
|
|
|
if !value.IsValid() {
|
|
return errors.New("value is invalid")
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
if value.Kind() != reflect.Slice {
|
|
return fmt.Errorf("expected slice for List type, got %v", value.Kind())
|
|
}
|
|
|
|
length := value.Len()
|
|
|
|
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, value.Index(i)); 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)
|
|
}
|
|
|
|
return nil
|
|
|
|
// In Bitlist case, we have to set the actual length of the bitlist.
|
|
case Bitlist:
|
|
bitlistInfo, err := sszInfo.BitlistInfo()
|
|
if err != nil {
|
|
return fmt.Errorf("could not get bitlist info: %w", err)
|
|
}
|
|
|
|
if bitlistInfo == nil {
|
|
return errors.New("bitlistInfo is nil")
|
|
}
|
|
|
|
if err := bitlistInfo.SetLengthFromBytes(value.Bytes()); err != nil {
|
|
return fmt.Errorf("could not set bitlist length from bytes: %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)
|
|
}
|
|
|
|
if containerInfo == nil {
|
|
return errors.New("containerInfo is nil")
|
|
}
|
|
|
|
// Dereference first in case value is a pointer.
|
|
derefValue := dereferencePointer(value)
|
|
if derefValue.Kind() != reflect.Struct {
|
|
return fmt.Errorf("expected struct for Container type, got %v", derefValue.Kind())
|
|
}
|
|
|
|
// Reset the pointer to the new value.
|
|
sszInfo.source = castToSSZObject(derefValue)
|
|
|
|
// Start with the end offset of this Container.
|
|
currentOffset := containerInfo.fixedOffset
|
|
|
|
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
|
|
}
|
|
|
|
// Recursively populate variable-sized fields.
|
|
fieldValue := derefValue.FieldByName(fieldInfo.goFieldName)
|
|
if err := PopulateVariableLengthInfo(childSszInfo, fieldValue); 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 listInfo, err := childSszInfo.ListInfo(); err == nil && listInfo != nil {
|
|
if listInfo.element.isVariable {
|
|
currentOffset += listInfo.Length() * offsetBytes
|
|
}
|
|
}
|
|
|
|
// Set the actual offset for variable-sized fields.
|
|
fieldInfo.offset = currentOffset
|
|
|
|
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.Value and computes its SSZ layout information.
|
|
func analyzeType(value reflect.Value, tag *reflect.StructTag) (*SszInfo, error) {
|
|
switch value.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(value)
|
|
|
|
case reflect.Slice:
|
|
return analyzeHomogeneousColType(value, tag)
|
|
|
|
case reflect.Struct:
|
|
return analyzeContainerType(value)
|
|
|
|
case reflect.Pointer:
|
|
derefValue := dereferencePointer(value)
|
|
return analyzeType(derefValue, tag)
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unsupported type %v for SSZ calculation", value.Kind())
|
|
}
|
|
}
|
|
|
|
// analyzeBasicType analyzes SSZ basic types (uintN, bool) and returns its info.
|
|
func analyzeBasicType(value reflect.Value) (*SszInfo, error) {
|
|
var sszType SSZType
|
|
|
|
switch value.Kind() {
|
|
case reflect.Uint64:
|
|
sszType = Uint64
|
|
case reflect.Uint32:
|
|
sszType = Uint32
|
|
case reflect.Uint16:
|
|
sszType = Uint16
|
|
case reflect.Uint8:
|
|
sszType = Uint8
|
|
case reflect.Bool:
|
|
sszType = Boolean
|
|
default:
|
|
return nil, fmt.Errorf("unsupported basic type %v for SSZ calculation", value.Kind())
|
|
}
|
|
|
|
sszInfo := &SszInfo{
|
|
sszType: sszType,
|
|
typ: value.Type(),
|
|
|
|
// Every basic type is fixed-size and not variable.
|
|
isVariable: false,
|
|
}
|
|
|
|
return sszInfo, nil
|
|
}
|
|
|
|
// analyzeHomogeneousColType analyzes homogeneous collection types (e.g., List, Vector, Bitlist, Bitvector) and returns its SSZ info.
|
|
func analyzeHomogeneousColType(value reflect.Value, tag *reflect.StructTag) (*SszInfo, error) {
|
|
if value.Kind() != reflect.Slice {
|
|
return nil, fmt.Errorf("can only analyze slice types, got %v", value.Kind())
|
|
}
|
|
|
|
// Parse the first dimension from the tag and get remaining tag for element
|
|
sszDimension, remainingTag, err := ParseSSZTag(tag)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse SSZ tag: %w", err)
|
|
}
|
|
if sszDimension == nil {
|
|
return nil, errors.New("ssz tag is required for slice types")
|
|
}
|
|
|
|
// NOTE: Elem() won't panic because value is guaranteed to be a slice here.
|
|
elementType := value.Type().Elem()
|
|
// Analyze element type with remaining dimensions
|
|
// Note that it is enough to analyze by a zero value,
|
|
// as the actual value with variable-sized type will be populated later.
|
|
elementInfo, err := analyzeType(reflect.New(elementType), remainingTag)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not analyze element type for homogeneous collection: %w", err)
|
|
}
|
|
|
|
// 1. Handle List/Bitlist type
|
|
if sszDimension.IsList() {
|
|
limit, err := sszDimension.GetListLimit()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get list limit: %w", err)
|
|
}
|
|
|
|
return analyzeListType(value, elementInfo, limit, sszDimension.isBitfield)
|
|
}
|
|
|
|
// 2. Handle Vector/Bitvector type
|
|
if sszDimension.IsVector() {
|
|
length, err := sszDimension.GetVectorLength()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get vector length: %w", err)
|
|
}
|
|
|
|
return analyzeVectorType(value, elementInfo, length, sszDimension.isBitfield)
|
|
}
|
|
|
|
// Parsing ssz tag doesn't provide enough information to determine the collection type,
|
|
// return an error.
|
|
return nil, errors.New("could not determine collection type from tags")
|
|
}
|
|
|
|
// analyzeListType analyzes SSZ List/Bitlist type and returns its SSZ info.
|
|
func analyzeListType(value reflect.Value, elementInfo *SszInfo, limit uint64, isBitfield bool) (*SszInfo, error) {
|
|
if isBitfield {
|
|
return &SszInfo{
|
|
sszType: Bitlist,
|
|
typ: value.Type(),
|
|
|
|
isVariable: true,
|
|
|
|
bitlistInfo: &bitlistInfo{
|
|
limit: limit,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
if elementInfo == nil {
|
|
return nil, errors.New("element info is required for List")
|
|
}
|
|
|
|
return &SszInfo{
|
|
sszType: List,
|
|
typ: value.Type(),
|
|
|
|
isVariable: true,
|
|
|
|
listInfo: &listInfo{
|
|
limit: limit,
|
|
element: elementInfo,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// analyzeVectorType analyzes SSZ Vector/Bitvector type and returns its SSZ info.
|
|
func analyzeVectorType(value reflect.Value, elementInfo *SszInfo, length uint64, isBitfield bool) (*SszInfo, error) {
|
|
if isBitfield {
|
|
return &SszInfo{
|
|
sszType: Bitvector,
|
|
typ: value.Type(),
|
|
|
|
isVariable: false,
|
|
|
|
bitvectorInfo: &bitvectorInfo{
|
|
length: length * 8, // length in bits
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
if elementInfo == nil {
|
|
return nil, errors.New("element info is required for Vector/Bitvector")
|
|
}
|
|
|
|
// Validate the given length.
|
|
// https://github.com/ethereum/consensus-specs/blob/master/ssz/simple-serialize.md#illegal-types
|
|
if length == 0 {
|
|
return nil, fmt.Errorf("vector length must be greater than 0, got %d", length)
|
|
}
|
|
|
|
return &SszInfo{
|
|
sszType: Vector,
|
|
typ: value.Type(),
|
|
|
|
isVariable: false,
|
|
|
|
vectorInfo: &vectorInfo{
|
|
length: length,
|
|
element: elementInfo,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// analyzeContainerType analyzes SSZ Container type and returns its SSZ info.
|
|
func analyzeContainerType(value reflect.Value) (*SszInfo, error) {
|
|
if value.Kind() != reflect.Struct {
|
|
return nil, fmt.Errorf("can only analyze struct types, got %v", value.Kind())
|
|
}
|
|
|
|
containerTyp := value.Type()
|
|
fields := make(map[string]*fieldInfo)
|
|
order := make([]string, 0)
|
|
|
|
isVariable := false
|
|
var currentOffset uint64
|
|
|
|
for i := 0; i < value.NumField(); i++ {
|
|
structFieldInfo := containerTyp.Field(i)
|
|
|
|
// Protobuf-generated structs contain private fields we must skip.
|
|
// e.g., state, sizeCache, unknownFields, etc.
|
|
if !structFieldInfo.IsExported() {
|
|
continue
|
|
}
|
|
|
|
tag := structFieldInfo.Tag
|
|
goFieldName := structFieldInfo.Name
|
|
fieldName, err := parseFieldNameFromTag(tag)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse field name from tag for field %s: %w", goFieldName, err)
|
|
}
|
|
|
|
// Analyze each field so that we can complete full SSZ information.
|
|
info, err := analyzeType(value.Field(i), &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: goFieldName,
|
|
}
|
|
// 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.
|
|
isVariable = true
|
|
currentOffset += offsetBytes
|
|
} else {
|
|
currentOffset += info.Size()
|
|
}
|
|
}
|
|
|
|
return &SszInfo{
|
|
sszType: Container,
|
|
typ: containerTyp,
|
|
source: castToSSZObject(value),
|
|
|
|
isVariable: isVariable,
|
|
|
|
containerInfo: &containerInfo{
|
|
fields: fields,
|
|
order: order,
|
|
fixedOffset: currentOffset,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// dereferencePointer dereferences a pointer to get the underlying value using reflection.
|
|
func dereferencePointer(value reflect.Value) reflect.Value {
|
|
derefValue := value
|
|
|
|
if value.IsValid() && value.Kind() == reflect.Pointer {
|
|
if value.IsNil() {
|
|
// Create a zero value if the pointer is nil.
|
|
derefValue = reflect.New(value.Type().Elem()).Elem()
|
|
} else {
|
|
derefValue = value.Elem()
|
|
}
|
|
}
|
|
|
|
return derefValue
|
|
}
|
|
|
|
// castToSSZObject attempts to cast a reflect.Value to the SSZObject interface.
|
|
// If failed, it returns nil.
|
|
func castToSSZObject(value reflect.Value) SSZObject {
|
|
if !value.IsValid() {
|
|
return nil
|
|
}
|
|
|
|
// SSZObject is only implemented by struct types.
|
|
if value.Kind() != reflect.Struct {
|
|
return nil
|
|
}
|
|
|
|
// To cast to SSZObject, we need the addressable value.
|
|
if !value.CanAddr() {
|
|
return nil
|
|
}
|
|
|
|
if sszObj, ok := value.Addr().Interface().(SSZObject); ok {
|
|
return sszObj
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseFieldNameFromTag extracts the field name (`snake_case` format)
|
|
// from a struct tag by looking for the json tag.
|
|
// The JSON tag contains the field name in the first part.
|
|
// e.g., "attesting_indices,omitempty" -> "attesting_indices".
|
|
func parseFieldNameFromTag(tag reflect.StructTag) (string, error) {
|
|
jsonTag := tag.Get("json")
|
|
if jsonTag == "" {
|
|
return "", errors.New("no JSON tag found")
|
|
}
|
|
|
|
substrings := strings.Split(jsonTag, ",")
|
|
if len(substrings) == 0 {
|
|
return "", errors.New("invalid JSON tag format")
|
|
}
|
|
|
|
fieldName := strings.TrimSpace(substrings[0])
|
|
if fieldName == "" {
|
|
return "", errors.New("empty field name")
|
|
}
|
|
|
|
return fieldName, nil
|
|
}
|