Use NetworkSchedule config to determine max blobs at epoch (#15714)

Co-authored-by: Kasey Kirkham <kasey@users.noreply.github.com>
This commit is contained in:
kasey
2025-10-07 23:02:05 -05:00
committed by GitHub
parent 0d742c6f88
commit 71f05b597f
56 changed files with 874 additions and 684 deletions

View File

@@ -82,6 +82,7 @@ go_test(
"//genesis:go_default_library",
"//io/file:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//runtime/version:go_default_library",
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",

View File

@@ -5,7 +5,6 @@ import (
"encoding/binary"
"fmt"
"math"
"slices"
"sort"
"strings"
"sync"
@@ -384,10 +383,12 @@ func (b *BeaconChainConfig) ApplyOptions(opts ...Option) {
}
}
// TODO: this needs to be able to return an error
// InitializeForkSchedule initializes the schedules forks baked into the config.
// InitializeForkSchedule initializes the scheduled forks and BPOs baked into the config.
func (b *BeaconChainConfig) InitializeForkSchedule() {
// Reset Fork Version Schedule.
// TODO: this needs to be able to return an error. The network schedule code has
// to implement weird fallbacks when it is not initialized properly, it would be better
// if the beacon node could crash if there isn't a valid fork schedule
// at the return of this function.
b.ForkVersionSchedule = configForkSchedule(b)
b.ForkVersionNames = configForkNames(b)
b.forkSchedule = initForkSchedule(b)
@@ -439,16 +440,18 @@ func (ns *NetworkSchedule) epochIdx(epoch primitives.Epoch) int {
return -1
}
func (ns *NetworkSchedule) safeIndex(idx int) NetworkScheduleEntry {
if idx < 0 || len(ns.entries) == 0 {
return genesisNetworkScheduleEntry()
}
if idx >= len(ns.entries) {
return ns.entries[len(ns.entries)-1]
}
return ns.entries[idx]
}
func (ns *NetworkSchedule) Next(epoch primitives.Epoch) NetworkScheduleEntry {
lastIdx := len(ns.entries) - 1
idx := ns.epochIdx(epoch)
if idx < 0 {
return ns.entries[0]
}
if idx == lastIdx {
return ns.entries[lastIdx]
}
return ns.entries[idx+1]
return ns.safeIndex(ns.epochIdx(epoch) + 1)
}
func (ns *NetworkSchedule) LastEntry() NetworkScheduleEntry {
@@ -457,38 +460,21 @@ func (ns *NetworkSchedule) LastEntry() NetworkScheduleEntry {
return ns.entries[i]
}
}
return ns.entries[0]
return genesisNetworkScheduleEntry()
}
// LastFork is the last full fork (this is used by e2e testing)
func (ns *NetworkSchedule) LastFork() NetworkScheduleEntry {
for i := len(ns.entries) - 1; i >= 0; i-- {
if ns.entries[i].isFork {
if ns.entries[i].isFork && ns.entries[i].Epoch != BeaconConfig().FarFutureEpoch {
return ns.entries[i]
}
}
return ns.entries[0]
return genesisNetworkScheduleEntry()
}
func (ns *NetworkSchedule) ForEpoch(epoch primitives.Epoch) NetworkScheduleEntry {
idx := ns.epochIdx(epoch)
if idx < 0 {
return ns.entries[0]
}
if idx >= len(ns.entries)-1 {
return ns.entries[len(ns.entries)-1]
}
return ns.entries[idx]
}
func (ns *NetworkSchedule) activatedAt(epoch primitives.Epoch) (*NetworkScheduleEntry, bool) {
ns.mu.RLock()
defer ns.mu.RUnlock()
if ns.byEpoch == nil {
return nil, false
}
entry, ok := ns.byEpoch[epoch]
return entry, ok
func (ns *NetworkSchedule) forEpoch(epoch primitives.Epoch) NetworkScheduleEntry {
return ns.safeIndex(ns.epochIdx(epoch))
}
func (ns *NetworkSchedule) merge(other *NetworkSchedule) *NetworkSchedule {
@@ -497,10 +483,15 @@ func (ns *NetworkSchedule) merge(other *NetworkSchedule) *NetworkSchedule {
merged = append(merged, other.entries...)
sort.Slice(merged, func(i, j int) bool {
if merged[i].Epoch == merged[j].Epoch {
if merged[i].VersionEnum == merged[j].VersionEnum {
return merged[i].isFork
// This can happen for 2 reasons:
// 1) both entries are forks in a test setup (eg starting genesis at a later fork)
// - break tie by version enum
// 2) one entry is a fork, the other is a BPO change
// - break tie by putting the fork first
if merged[i].isFork && merged[j].isFork {
return merged[i].VersionEnum < merged[j].VersionEnum
}
return merged[i].VersionEnum < merged[j].VersionEnum
return merged[i].isFork
}
return merged[i].Epoch < merged[j].Epoch
})
@@ -702,52 +693,12 @@ func (b *BeaconChainConfig) TargetBlobsPerBlock(slot primitives.Slot) int {
// MaxBlobsPerBlock returns the maximum number of blobs per block for the given slot.
func (b *BeaconChainConfig) MaxBlobsPerBlock(slot primitives.Slot) int {
epoch := primitives.Epoch(slot.DivSlot(b.SlotsPerEpoch))
if len(b.BlobSchedule) > 0 {
if !slices.IsSortedFunc(b.BlobSchedule, func(a, b BlobScheduleEntry) int {
return int(a.Epoch - b.Epoch)
}) {
slices.SortFunc(b.BlobSchedule, func(a, b BlobScheduleEntry) int {
return int(a.Epoch - b.Epoch)
})
}
for i := len(b.BlobSchedule) - 1; i >= 0; i-- {
if epoch >= b.BlobSchedule[i].Epoch {
return int(b.BlobSchedule[i].MaxBlobsPerBlock)
}
}
}
if epoch >= b.ElectraForkEpoch {
return b.DeprecatedMaxBlobsPerBlockElectra
}
return b.DeprecatedMaxBlobsPerBlock
return b.MaxBlobsPerBlockAtEpoch(epoch)
}
// MaxBlobsPerBlockAtEpoch returns the maximum number of blobs per block for the given epoch
func (b *BeaconChainConfig) MaxBlobsPerBlockAtEpoch(epoch primitives.Epoch) int {
if len(b.BlobSchedule) > 0 {
if !slices.IsSortedFunc(b.BlobSchedule, func(a, b BlobScheduleEntry) int {
return int(a.Epoch - b.Epoch)
}) {
slices.SortFunc(b.BlobSchedule, func(a, b BlobScheduleEntry) int {
return int(a.Epoch - b.Epoch)
})
}
for i := len(b.BlobSchedule) - 1; i >= 0; i-- {
if epoch >= b.BlobSchedule[i].Epoch {
return int(b.BlobSchedule[i].MaxBlobsPerBlock)
}
}
}
if epoch >= b.ElectraForkEpoch {
return b.DeprecatedMaxBlobsPerBlockElectra
}
return b.DeprecatedMaxBlobsPerBlock
return int(b.networkSchedule.forEpoch(epoch).MaxBlobsPerBlock)
}
// DenebEnabled centralizes the check to determine if code paths that are specific to deneb should be allowed to execute.

View File

@@ -10,6 +10,7 @@ import (
"github.com/OffchainLabs/prysm/v6/config/params"
"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v6/genesis"
"github.com/OffchainLabs/prysm/v6/runtime/version"
"github.com/OffchainLabs/prysm/v6/testing/require"
"github.com/ethereum/go-ethereum/common/hexutil"
)
@@ -109,75 +110,80 @@ func TestConfigGenesisValidatorRoot(t *testing.T) {
require.Equal(t, params.BeaconConfig().GenesisValidatorsRoot, genesis.ValidatorsRoot())
}
func TestMaxBlobsPerBlock(t *testing.T) {
t.Run("Before all forks and no BlobSchedule", func(t *testing.T) {
cfg := params.MainnetConfig()
cfg.BlobSchedule = nil
cfg.ElectraForkEpoch = 100
cfg.FuluForkEpoch = 200
require.Equal(t, cfg.MaxBlobsPerBlock(0), cfg.DeprecatedMaxBlobsPerBlock)
})
func TestMaxBlobsJumbled(t *testing.T) {
params.SetActiveTestCleanup(t, params.MainnetBeaconConfig)
cfg := params.MainnetConfig()
cfg.FuluForkEpoch = cfg.ElectraForkEpoch + 4098*2
electraMaxBlobs := uint64(cfg.DeprecatedMaxBlobsPerBlockElectra)
offsets := []primitives.Epoch{cfg.FuluForkEpoch}
for _, offset := range []primitives.Epoch{320, 640, 960, 1080} {
offsets = append(offsets, cfg.FuluForkEpoch+offset)
}
maxBlobs := map[primitives.Epoch]uint64{
cfg.FuluForkEpoch: electraMaxBlobs,
offsets[0]: electraMaxBlobs + 3,
offsets[1]: electraMaxBlobs + 6,
offsets[2]: electraMaxBlobs + 9,
offsets[3]: electraMaxBlobs + 12,
}
schedule := make([]params.BlobScheduleEntry, 0, len(maxBlobs))
for _, epoch := range offsets[1:] {
schedule = append(schedule, params.BlobScheduleEntry{Epoch: epoch, MaxBlobsPerBlock: maxBlobs[epoch]})
}
cfg.BlobSchedule = schedule
cfg.InitializeForkSchedule()
for i := 1; i < len(cfg.BlobSchedule); i++ {
beforeEpoch, epoch := cfg.BlobSchedule[i-1].Epoch, cfg.BlobSchedule[i].Epoch
before, after := maxBlobs[beforeEpoch], maxBlobs[epoch]
require.Equal(t, before, uint64(cfg.MaxBlobsPerBlockAtEpoch(epoch-1)))
require.Equal(t, after, uint64(cfg.MaxBlobsPerBlockAtEpoch(epoch)))
beforeSlot, err := cfg.SlotsPerEpoch.SafeMul(uint64(beforeEpoch))
require.NoError(t, err)
afterSlot, err := cfg.SlotsPerEpoch.SafeMul(uint64(epoch))
require.NoError(t, err)
require.Equal(t, before, uint64(cfg.MaxBlobsPerBlock(beforeSlot)))
require.Equal(t, after, uint64(cfg.MaxBlobsPerBlock(afterSlot)))
}
t.Run("Uses latest matching BlobSchedule entry", func(t *testing.T) {
cfg := params.MainnetConfig()
cfg.BlobSchedule = []params.BlobScheduleEntry{
{Epoch: 5, MaxBlobsPerBlock: 7},
{Epoch: 10, MaxBlobsPerBlock: 11},
}
slot := 11 * cfg.SlotsPerEpoch
require.Equal(t, cfg.MaxBlobsPerBlock(slot), 11)
})
require.Equal(t, electraMaxBlobs, uint64(cfg.MaxBlobsPerBlockAtEpoch(cfg.FuluForkEpoch-1)))
require.Equal(t, electraMaxBlobs, uint64(cfg.MaxBlobsPerBlockAtEpoch(cfg.ElectraForkEpoch)))
require.Equal(t, cfg.DeprecatedMaxBlobsPerBlock, cfg.MaxBlobsPerBlockAtEpoch(cfg.ElectraForkEpoch-1))
require.Equal(t, cfg.DeprecatedMaxBlobsPerBlock, cfg.MaxBlobsPerBlockAtEpoch(cfg.DenebForkEpoch))
preBlobEpochs := []primitives.Epoch{cfg.DenebForkEpoch - 1, cfg.CapellaForkEpoch, cfg.BellatrixForkEpoch, cfg.AltairForkEpoch, 0}
for _, epoch := range preBlobEpochs {
require.Equal(t, 0, cfg.MaxBlobsPerBlockAtEpoch(epoch))
}
}
t.Run("Uses earlier matching BlobSchedule entry", func(t *testing.T) {
cfg := params.MainnetConfig()
cfg.BlobSchedule = []params.BlobScheduleEntry{
{Epoch: 5, MaxBlobsPerBlock: 7},
{Epoch: 10, MaxBlobsPerBlock: 11},
}
slot := 6 * cfg.SlotsPerEpoch
require.Equal(t, cfg.MaxBlobsPerBlock(slot), 7)
})
func TestFirstBPOAtFork(t *testing.T) {
params.SetActiveTestCleanup(t, params.MainnetBeaconConfig)
cfg := params.MainnetConfig()
cfg.FuluForkEpoch = cfg.ElectraForkEpoch + 4096*2
electraMaxBlobs := uint64(cfg.DeprecatedMaxBlobsPerBlockElectra)
cfg.BlobSchedule = []params.BlobScheduleEntry{
{Epoch: cfg.FuluForkEpoch, MaxBlobsPerBlock: electraMaxBlobs + 1},
{Epoch: cfg.FuluForkEpoch + 1, MaxBlobsPerBlock: electraMaxBlobs + 2},
}
cfg.InitializeForkSchedule()
require.Equal(t, electraMaxBlobs, uint64(cfg.MaxBlobsPerBlockAtEpoch(cfg.FuluForkEpoch-1)))
require.Equal(t, electraMaxBlobs+1, uint64(cfg.MaxBlobsPerBlockAtEpoch(cfg.FuluForkEpoch)))
require.Equal(t, electraMaxBlobs+2, uint64(cfg.MaxBlobsPerBlockAtEpoch(cfg.FuluForkEpoch+2)))
}
t.Run("Before first BlobSchedule entry falls back to fork logic", func(t *testing.T) {
cfg := params.MainnetConfig()
cfg.FuluForkEpoch = 1
cfg.BlobSchedule = []params.BlobScheduleEntry{
{Epoch: 5, MaxBlobsPerBlock: 7},
}
slot := primitives.Slot(2) // Epoch 0
require.Equal(t, cfg.MaxBlobsPerBlock(slot), cfg.DeprecatedMaxBlobsPerBlock)
})
t.Run("Unsorted BlobSchedule still picks latest matching entry", func(t *testing.T) {
cfg := params.MainnetConfig()
cfg.BlobSchedule = []params.BlobScheduleEntry{
{Epoch: 10, MaxBlobsPerBlock: 11},
{Epoch: 5, MaxBlobsPerBlock: 7},
}
slot := 11 * cfg.SlotsPerEpoch
require.Equal(t, cfg.MaxBlobsPerBlock(slot), 11)
})
t.Run("Unsorted BlobSchedule picks earlier matching entry correctly", func(t *testing.T) {
cfg := params.MainnetConfig()
cfg.BlobSchedule = []params.BlobScheduleEntry{
{Epoch: 10, MaxBlobsPerBlock: 11},
{Epoch: 5, MaxBlobsPerBlock: 7},
}
slot := 6 * cfg.SlotsPerEpoch
require.Equal(t, cfg.MaxBlobsPerBlock(slot), 7)
})
t.Run("Unsorted BlobSchedule falls back to fork logic when epoch is before all entries", func(t *testing.T) {
cfg := params.MainnetConfig()
cfg.ElectraForkEpoch = 2
cfg.BlobSchedule = []params.BlobScheduleEntry{
{Epoch: 10, MaxBlobsPerBlock: 11},
{Epoch: 5, MaxBlobsPerBlock: 7},
}
slot := primitives.Slot(1) // Epoch 0
require.Equal(t, cfg.MaxBlobsPerBlock(slot), cfg.DeprecatedMaxBlobsPerBlock)
})
func TestMaxBlobsNoSchedule(t *testing.T) {
params.SetActiveTestCleanup(t, params.MainnetBeaconConfig)
cfg := params.MainnetConfig()
electraMaxBlobs := uint64(cfg.DeprecatedMaxBlobsPerBlockElectra)
cfg.BlobSchedule = nil
cfg.InitializeForkSchedule()
require.Equal(t, electraMaxBlobs, uint64(cfg.MaxBlobsPerBlockAtEpoch(cfg.FuluForkEpoch-1)))
require.Equal(t, electraMaxBlobs, uint64(cfg.MaxBlobsPerBlockAtEpoch(cfg.ElectraForkEpoch)))
require.Equal(t, cfg.DeprecatedMaxBlobsPerBlock, cfg.MaxBlobsPerBlockAtEpoch(cfg.ElectraForkEpoch-1))
require.Equal(t, cfg.DeprecatedMaxBlobsPerBlock, cfg.MaxBlobsPerBlockAtEpoch(cfg.DenebForkEpoch))
preBlobEpochs := []primitives.Epoch{cfg.DenebForkEpoch - 1, cfg.CapellaForkEpoch, cfg.BellatrixForkEpoch, cfg.AltairForkEpoch, 0}
for _, epoch := range preBlobEpochs {
require.Equal(t, 0, cfg.MaxBlobsPerBlockAtEpoch(epoch))
}
}
func Test_TargetBlobCount(t *testing.T) {
@@ -287,3 +293,15 @@ func TestFarFuturePrepareFilter(t *testing.T) {
entry := params.GetNetworkScheduleEntry(oldElectra)
require.Equal(t, [4]byte(params.BeaconConfig().DenebForkVersion), entry.ForkVersion)
}
func TestMaxBlobsOverrideEpoch(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
require.Equal(t, 0, cfg.MaxBlobsPerBlockAtEpoch(0))
params.SetGenesisFork(t, cfg, version.Deneb)
require.Equal(t, cfg.DeprecatedMaxBlobsPerBlock, cfg.MaxBlobsPerBlockAtEpoch(0))
params.SetGenesisFork(t, cfg, version.Electra)
require.Equal(t, cfg.DeprecatedMaxBlobsPerBlockElectra, cfg.MaxBlobsPerBlockAtEpoch(0))
params.SetGenesisFork(t, cfg, version.Fulu)
require.Equal(t, cfg.DeprecatedMaxBlobsPerBlockElectra, cfg.MaxBlobsPerBlockAtEpoch(0))
}

View File

@@ -4,19 +4,14 @@ import (
fieldparams "github.com/OffchainLabs/prysm/v6/config/fieldparams"
"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v6/runtime/version"
"github.com/pkg/errors"
)
// DigestChangesAfter checks if an allotted fork is in the following epoch.
func DigestChangesAfter(e primitives.Epoch) bool {
_, ok := BeaconConfig().networkSchedule.activatedAt(e + 1)
return ok
}
// ForkDigestUsingConfig retrieves the fork digest from the current schedule determined
// by the provided epoch.
func ForkDigestUsingConfig(epoch primitives.Epoch, cfg *BeaconChainConfig) [4]byte {
entry := cfg.networkSchedule.ForEpoch(epoch)
entry := cfg.networkSchedule.forEpoch(epoch)
return entry.ForkDigest
}
@@ -42,10 +37,10 @@ func Fork(epoch primitives.Epoch) (*ethpb.Fork, error) {
}
func ForkFromConfig(cfg *BeaconChainConfig, epoch primitives.Epoch) *ethpb.Fork {
current := cfg.networkSchedule.ForEpoch(epoch)
current := cfg.networkSchedule.forEpoch(epoch)
previous := current
if current.Epoch > 0 {
previous = cfg.networkSchedule.ForEpoch(current.Epoch - 1)
previous = cfg.networkSchedule.forEpoch(current.Epoch - 1)
}
return &ethpb.Fork{
PreviousVersion: previous.ForkVersion[:],
@@ -102,11 +97,17 @@ func LastForkEpoch() primitives.Epoch {
}
func LastNetworkScheduleEntry() NetworkScheduleEntry {
lastIdx := len(BeaconConfig().networkSchedule.entries) - 1
return BeaconConfig().networkSchedule.entries[lastIdx]
return BeaconConfig().networkSchedule.LastEntry()
}
func GetNetworkScheduleEntry(epoch primitives.Epoch) NetworkScheduleEntry {
entry := BeaconConfig().networkSchedule.ForEpoch(epoch)
entry := BeaconConfig().networkSchedule.forEpoch(epoch)
return entry
}
func genesisNetworkScheduleEntry() NetworkScheduleEntry {
b := BeaconConfig()
// TODO: note this has a zero digest, but we would never hit this fallback condition on
// a properly initialized fork schedule.
return NetworkScheduleEntry{Epoch: b.GenesisEpoch, isFork: true, ForkVersion: to4(b.GenesisForkVersion), VersionEnum: version.Phase0}
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/OffchainLabs/prysm/v6/config/params"
"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v6/testing/assert"
"github.com/OffchainLabs/prysm/v6/testing/require"
)
@@ -93,15 +92,6 @@ func TestRetrieveForkDataFromDigest(t *testing.T) {
require.Equal(t, params.BeaconConfig().AltairForkEpoch, epoch)
}
func TestIsForkNextEpoch(t *testing.T) {
// at
assert.Equal(t, false, params.DigestChangesAfter(params.BeaconConfig().ElectraForkEpoch))
// just before
assert.Equal(t, true, params.DigestChangesAfter(params.BeaconConfig().ElectraForkEpoch-1))
// just after
assert.Equal(t, false, params.DigestChangesAfter(params.BeaconConfig().ElectraForkEpoch+1))
}
func TestNextForkData(t *testing.T) {
params.SetupTestConfigCleanup(t)
params.BeaconConfig().InitializeForkSchedule()
@@ -163,7 +153,9 @@ func TestNextForkData(t *testing.T) {
func TestLastForkEpoch(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
require.Equal(t, cfg.ElectraForkEpoch, params.LastForkEpoch())
if cfg.FuluForkEpoch == cfg.FarFutureEpoch {
require.Equal(t, cfg.ElectraForkEpoch, params.LastForkEpoch())
}
}
func TestForkFromConfig_UsesPassedConfig(t *testing.T) {

View File

@@ -2,12 +2,41 @@ package params
import (
"testing"
"github.com/OffchainLabs/prysm/v6/runtime/version"
)
const (
EnvNameOverrideAccept = "PRYSM_API_OVERRIDE_ACCEPT"
)
func SetGenesisFork(t *testing.T, cfg *BeaconChainConfig, fork int) {
setGenesisUpdateEpochs(cfg, fork)
OverrideBeaconConfig(cfg)
}
func setGenesisUpdateEpochs(b *BeaconChainConfig, fork int) {
switch fork {
case version.Fulu:
b.FuluForkEpoch = 0
setGenesisUpdateEpochs(b, version.Electra)
case version.Electra:
b.ElectraForkEpoch = 0
setGenesisUpdateEpochs(b, version.Deneb)
case version.Deneb:
b.DenebForkEpoch = 0
setGenesisUpdateEpochs(b, version.Capella)
case version.Capella:
b.CapellaForkEpoch = 0
setGenesisUpdateEpochs(b, version.Bellatrix)
case version.Bellatrix:
b.BellatrixForkEpoch = 0
setGenesisUpdateEpochs(b, version.Altair)
case version.Altair:
b.AltairForkEpoch = 0
}
}
// SetupTestConfigCleanup preserves configurations allowing to modify them within tests without any
// restrictions, everything is restored after the test.
func SetupTestConfigCleanup(t testing.TB) {