mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 15:37:56 -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/forks:go_default_library",
|
||||||
"//network/httputil:go_default_library",
|
"//network/httputil:go_default_library",
|
||||||
"@com_github_ethereum_go_ethereum//common/hexutil: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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"github.com/OffchainLabs/prysm/v6/network/forks"
|
"github.com/OffchainLabs/prysm/v6/network/forks"
|
||||||
"github.com/OffchainLabs/prysm/v6/network/httputil"
|
"github.com/OffchainLabs/prysm/v6/network/httputil"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetDepositContract retrieves deposit contract address and genesis fork version.
|
// 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})
|
httputil.WriteJson(w, &structs.GetSpecResponse{Data: data})
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareConfigSpec() (map[string]string, error) {
|
func convertValueForJSON(v reflect.Value, tag string) interface{} {
|
||||||
data := make(map[string]string)
|
// 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()
|
config := *params.BeaconConfig()
|
||||||
|
|
||||||
t := reflect.TypeOf(config)
|
t := reflect.TypeOf(config)
|
||||||
v := reflect.ValueOf(config)
|
v := reflect.ValueOf(config)
|
||||||
|
|
||||||
for i := 0; i < t.NumField(); i++ {
|
for i := 0; i < t.NumField(); i++ {
|
||||||
tField := t.Field(i)
|
tField := t.Field(i)
|
||||||
_, isSpecField := tField.Tag.Lookup("spec")
|
_, isSpec := tField.Tag.Lookup("spec")
|
||||||
if !isSpecField {
|
if !isSpec {
|
||||||
// Field should not be returned from API.
|
continue
|
||||||
|
}
|
||||||
|
if shouldSkip(tField) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
tagValue := strings.ToUpper(tField.Tag.Get("yaml"))
|
tag := strings.ToUpper(tField.Tag.Get("yaml"))
|
||||||
vField := v.Field(i)
|
val := v.Field(i)
|
||||||
switch vField.Kind() {
|
data[tag] = convertValueForJSON(val, tag)
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
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/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -200,7 +201,7 @@ func TestGetSpec(t *testing.T) {
|
|||||||
data, ok := resp.Data.(map[string]interface{})
|
data, ok := resp.Data.(map[string]interface{})
|
||||||
require.Equal(t, true, ok)
|
require.Equal(t, true, ok)
|
||||||
|
|
||||||
assert.Equal(t, 175, len(data))
|
assert.Equal(t, 176, len(data))
|
||||||
for k, v := range data {
|
for k, v := range data {
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
switch k {
|
switch k {
|
||||||
@@ -577,6 +578,11 @@ func TestGetSpec(t *testing.T) {
|
|||||||
assert.Equal(t, "102", v)
|
assert.Equal(t, "102", v)
|
||||||
case "BLOB_SIDECAR_SUBNET_COUNT_ELECTRA":
|
case "BLOB_SIDECAR_SUBNET_COUNT_ELECTRA":
|
||||||
assert.Equal(t, "103", v)
|
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:
|
default:
|
||||||
t.Errorf("Incorrect key: %s", k)
|
t.Errorf("Incorrect key: %s", k)
|
||||||
}
|
}
|
||||||
@@ -637,3 +643,86 @@ func TestForkSchedule_Ok(t *testing.T) {
|
|||||||
assert.Equal(t, os.Len(), len(resp.Data))
|
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.
|
NodeIdBits uint64 `yaml:"NODE_ID_BITS" spec:"true"` // NodeIdBits defines the bit length of a node id.
|
||||||
|
|
||||||
// Blobs Values
|
// 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_MaxBlobsPerBlock defines the max blobs that could exist in a block.
|
||||||
// Deprecated: This field is no longer supported. Avoid using it.
|
// Deprecated: This field is no longer supported. Avoid using it.
|
||||||
@@ -336,8 +336,8 @@ func (b *BeaconChainConfig) ExecutionRequestLimits() enginev1.ExecutionRequestLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BlobScheduleEntry struct {
|
type BlobScheduleEntry struct {
|
||||||
Epoch primitives.Epoch `yaml:"EPOCH"`
|
Epoch primitives.Epoch `yaml:"EPOCH" json:"EPOCH"`
|
||||||
MaxBlobsPerBlock uint64 `yaml:"MAX_BLOBS_PER_BLOCK"`
|
MaxBlobsPerBlock uint64 `yaml:"MAX_BLOBS_PER_BLOCK" json:"MAX_BLOBS_PER_BLOCK"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitializeForkSchedule initializes the schedules forks baked into the config.
|
// InitializeForkSchedule initializes the schedules forks baked into the config.
|
||||||
|
|||||||
Reference in New Issue
Block a user