mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-08 04:54:05 -05:00
Add BLOB_SCHEDULE to eth/v1/config/spec endpoint (#15485)
* Beacon api: fix get config blob schedule * Numbers should be string instead of float * more generalized implementation for nested objects * removing unused function * fixing linting * removing redundant switch fields * adding additional log for debugging * Fix build. * adding skip function based on kasey's recommendation * fixing test --------- Co-authored-by: james-prysm <james@prysmaticlabs.com> Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com> Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com>
This commit is contained in:
@@ -12,6 +12,7 @@ go_library(
|
||||
"//network/forks:go_default_library",
|
||||
"//network/httputil:go_default_library",
|
||||
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
|
||||
"@com_github_sirupsen_logrus//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/OffchainLabs/prysm/v6/network/forks"
|
||||
"github.com/OffchainLabs/prysm/v6/network/httputil"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GetDepositContract retrieves deposit contract address and genesis fork version.
|
||||
@@ -80,39 +82,108 @@ func GetSpec(w http.ResponseWriter, r *http.Request) {
|
||||
httputil.WriteJson(w, &structs.GetSpecResponse{Data: data})
|
||||
}
|
||||
|
||||
func prepareConfigSpec() (map[string]string, error) {
|
||||
data := make(map[string]string)
|
||||
func convertValueForJSON(v reflect.Value, tag string) interface{} {
|
||||
// Unwrap pointers / interfaces
|
||||
for v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
// ===== Single byte → 0xAB =====
|
||||
case reflect.Uint8:
|
||||
return hexutil.Encode([]byte{uint8(v.Uint())})
|
||||
|
||||
// ===== Other unsigned numbers → "123" =====
|
||||
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return strconv.FormatUint(v.Uint(), 10)
|
||||
|
||||
// ===== Signed numbers → "123" =====
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return strconv.FormatInt(v.Int(), 10)
|
||||
|
||||
// ===== Raw bytes – encode to hex =====
|
||||
case reflect.Slice:
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 {
|
||||
return hexutil.Encode(v.Bytes())
|
||||
}
|
||||
fallthrough
|
||||
case reflect.Array:
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 {
|
||||
// Need a copy because v.Slice is illegal on arrays directly
|
||||
tmp := make([]byte, v.Len())
|
||||
reflect.Copy(reflect.ValueOf(tmp), v)
|
||||
return hexutil.Encode(tmp)
|
||||
}
|
||||
// Generic slice/array handling
|
||||
n := v.Len()
|
||||
out := make([]interface{}, n)
|
||||
for i := 0; i < n; i++ {
|
||||
out[i] = convertValueForJSON(v.Index(i), tag)
|
||||
}
|
||||
return out
|
||||
|
||||
// ===== Struct =====
|
||||
case reflect.Struct:
|
||||
t := v.Type()
|
||||
m := make(map[string]interface{}, v.NumField())
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
if !v.Field(i).CanInterface() {
|
||||
continue // unexported
|
||||
}
|
||||
key := f.Tag.Get("json")
|
||||
if key == "" || key == "-" {
|
||||
key = f.Name
|
||||
}
|
||||
m[key] = convertValueForJSON(v.Field(i), tag)
|
||||
}
|
||||
return m
|
||||
|
||||
// ===== Default =====
|
||||
default:
|
||||
log.WithFields(log.Fields{
|
||||
"fn": "prepareConfigSpec",
|
||||
"tag": tag,
|
||||
"kind": v.Kind().String(),
|
||||
"type": v.Type().String(),
|
||||
}).Error("Unsupported config field kind; value forwarded verbatim")
|
||||
return v.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
func prepareConfigSpec() (map[string]interface{}, error) {
|
||||
data := make(map[string]interface{})
|
||||
config := *params.BeaconConfig()
|
||||
|
||||
t := reflect.TypeOf(config)
|
||||
v := reflect.ValueOf(config)
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
tField := t.Field(i)
|
||||
_, isSpecField := tField.Tag.Lookup("spec")
|
||||
if !isSpecField {
|
||||
// Field should not be returned from API.
|
||||
_, isSpec := tField.Tag.Lookup("spec")
|
||||
if !isSpec {
|
||||
continue
|
||||
}
|
||||
if shouldSkip(tField) {
|
||||
continue
|
||||
}
|
||||
|
||||
tagValue := strings.ToUpper(tField.Tag.Get("yaml"))
|
||||
vField := v.Field(i)
|
||||
switch vField.Kind() {
|
||||
case reflect.Int:
|
||||
data[tagValue] = strconv.FormatInt(vField.Int(), 10)
|
||||
case reflect.Uint64:
|
||||
data[tagValue] = strconv.FormatUint(vField.Uint(), 10)
|
||||
case reflect.Slice:
|
||||
data[tagValue] = hexutil.Encode(vField.Bytes())
|
||||
case reflect.Array:
|
||||
data[tagValue] = hexutil.Encode(reflect.ValueOf(&config).Elem().Field(i).Slice(0, vField.Len()).Bytes())
|
||||
case reflect.String:
|
||||
data[tagValue] = vField.String()
|
||||
case reflect.Uint8:
|
||||
data[tagValue] = hexutil.Encode([]byte{uint8(vField.Uint())})
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported config field type: %s", vField.Kind().String())
|
||||
}
|
||||
tag := strings.ToUpper(tField.Tag.Get("yaml"))
|
||||
val := v.Field(i)
|
||||
data[tag] = convertValueForJSON(val, tag)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func shouldSkip(tField reflect.StructField) bool {
|
||||
// Dynamically skip blob schedule if Fulu is not yet scheduled.
|
||||
if params.BeaconConfig().FuluForkEpoch == math.MaxUint64 &&
|
||||
tField.Type == reflect.TypeOf(params.BeaconConfig().BlobSchedule) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -200,7 +201,7 @@ func TestGetSpec(t *testing.T) {
|
||||
data, ok := resp.Data.(map[string]interface{})
|
||||
require.Equal(t, true, ok)
|
||||
|
||||
assert.Equal(t, 175, len(data))
|
||||
assert.Equal(t, 176, len(data))
|
||||
for k, v := range data {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
switch k {
|
||||
@@ -577,6 +578,11 @@ func TestGetSpec(t *testing.T) {
|
||||
assert.Equal(t, "102", v)
|
||||
case "BLOB_SIDECAR_SUBNET_COUNT_ELECTRA":
|
||||
assert.Equal(t, "103", v)
|
||||
case "BLOB_SCHEDULE":
|
||||
// BLOB_SCHEDULE should be an empty slice when no schedule is defined
|
||||
blobSchedule, ok := v.([]interface{})
|
||||
assert.Equal(t, true, ok)
|
||||
assert.Equal(t, 0, len(blobSchedule))
|
||||
default:
|
||||
t.Errorf("Incorrect key: %s", k)
|
||||
}
|
||||
@@ -637,3 +643,86 @@ func TestForkSchedule_Ok(t *testing.T) {
|
||||
assert.Equal(t, os.Len(), len(resp.Data))
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSpec_BlobSchedule(t *testing.T) {
|
||||
params.SetupTestConfigCleanup(t)
|
||||
config := params.BeaconConfig().Copy()
|
||||
config.FuluForkEpoch = 1
|
||||
|
||||
// Set up a blob schedule with test data
|
||||
config.BlobSchedule = []params.BlobScheduleEntry{
|
||||
{
|
||||
Epoch: primitives.Epoch(100),
|
||||
MaxBlobsPerBlock: 6,
|
||||
},
|
||||
{
|
||||
Epoch: primitives.Epoch(200),
|
||||
MaxBlobsPerBlock: 9,
|
||||
},
|
||||
}
|
||||
params.OverrideBeaconConfig(config)
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/config/spec", nil)
|
||||
writer := httptest.NewRecorder()
|
||||
writer.Body = &bytes.Buffer{}
|
||||
|
||||
GetSpec(writer, request)
|
||||
require.Equal(t, http.StatusOK, writer.Code)
|
||||
resp := structs.GetSpecResponse{}
|
||||
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), &resp))
|
||||
data, ok := resp.Data.(map[string]interface{})
|
||||
require.Equal(t, true, ok)
|
||||
|
||||
// Verify BLOB_SCHEDULE is present and properly formatted
|
||||
blobScheduleValue, exists := data["BLOB_SCHEDULE"]
|
||||
require.Equal(t, true, exists)
|
||||
|
||||
// Verify it's a slice of maps (actual JSON object, not string)
|
||||
// The JSON unmarshaling converts it to []interface{} with map[string]interface{} entries
|
||||
blobScheduleSlice, ok := blobScheduleValue.([]interface{})
|
||||
require.Equal(t, true, ok)
|
||||
|
||||
// Convert to generic interface for easier testing
|
||||
var blobSchedule []map[string]interface{}
|
||||
for _, entry := range blobScheduleSlice {
|
||||
entryMap, ok := entry.(map[string]interface{})
|
||||
require.Equal(t, true, ok)
|
||||
blobSchedule = append(blobSchedule, entryMap)
|
||||
}
|
||||
|
||||
// Verify the blob schedule content
|
||||
require.Equal(t, 2, len(blobSchedule))
|
||||
|
||||
// Check first entry - values should be strings for consistent API output
|
||||
assert.Equal(t, "100", blobSchedule[0]["EPOCH"])
|
||||
assert.Equal(t, "6", blobSchedule[0]["MAX_BLOBS_PER_BLOCK"])
|
||||
|
||||
// Check second entry - values should be strings for consistent API output
|
||||
assert.Equal(t, "200", blobSchedule[1]["EPOCH"])
|
||||
assert.Equal(t, "9", blobSchedule[1]["MAX_BLOBS_PER_BLOCK"])
|
||||
}
|
||||
|
||||
func TestGetSpec_BlobSchedule_NotFulu(t *testing.T) {
|
||||
params.SetupTestConfigCleanup(t)
|
||||
config := params.BeaconConfig().Copy()
|
||||
// Fulu not scheduled (default: math.MaxUint64)
|
||||
config.FuluForkEpoch = math.MaxUint64
|
||||
config.BlobSchedule = []params.BlobScheduleEntry{
|
||||
{Epoch: primitives.Epoch(100), MaxBlobsPerBlock: 6},
|
||||
}
|
||||
params.OverrideBeaconConfig(config)
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/config/spec", nil)
|
||||
writer := httptest.NewRecorder()
|
||||
writer.Body = &bytes.Buffer{}
|
||||
|
||||
GetSpec(writer, request)
|
||||
require.Equal(t, http.StatusOK, writer.Code)
|
||||
resp := structs.GetSpecResponse{}
|
||||
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), &resp))
|
||||
data, ok := resp.Data.(map[string]interface{})
|
||||
require.Equal(t, true, ok)
|
||||
|
||||
_, exists := data["BLOB_SCHEDULE"]
|
||||
require.Equal(t, false, exists)
|
||||
}
|
||||
|
||||
3
changelog/add-blob-schedule-to-config-spec-endpoint.md
Normal file
3
changelog/add-blob-schedule-to-config-spec-endpoint.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Added
|
||||
|
||||
- Add BLOB_SCHEDULE field to `/eth/v1/config/spec` endpoint response to expose blob scheduling configuration for networks.
|
||||
@@ -297,7 +297,7 @@ type BeaconChainConfig struct {
|
||||
NodeIdBits uint64 `yaml:"NODE_ID_BITS" spec:"true"` // NodeIdBits defines the bit length of a node id.
|
||||
|
||||
// Blobs Values
|
||||
BlobSchedule []BlobScheduleEntry `yaml:"BLOB_SCHEDULE"`
|
||||
BlobSchedule []BlobScheduleEntry `yaml:"BLOB_SCHEDULE" spec:"true"`
|
||||
|
||||
// Deprecated_MaxBlobsPerBlock defines the max blobs that could exist in a block.
|
||||
// Deprecated: This field is no longer supported. Avoid using it.
|
||||
@@ -336,8 +336,8 @@ func (b *BeaconChainConfig) ExecutionRequestLimits() enginev1.ExecutionRequestLi
|
||||
}
|
||||
|
||||
type BlobScheduleEntry struct {
|
||||
Epoch primitives.Epoch `yaml:"EPOCH"`
|
||||
MaxBlobsPerBlock uint64 `yaml:"MAX_BLOBS_PER_BLOCK"`
|
||||
Epoch primitives.Epoch `yaml:"EPOCH" json:"EPOCH"`
|
||||
MaxBlobsPerBlock uint64 `yaml:"MAX_BLOBS_PER_BLOCK" json:"MAX_BLOBS_PER_BLOCK"`
|
||||
}
|
||||
|
||||
// InitializeForkSchedule initializes the schedules forks baked into the config.
|
||||
|
||||
Reference in New Issue
Block a user