Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions pkg/beacon/state/blob_schedule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package state

import (
"sort"

"github.com/attestantio/go-eth2-client/spec/phase0"
)

// BlobScheduleEntry represents a single entry in the BLOB_SCHEDULE configuration.
type BlobScheduleEntry struct {
Epoch phase0.Epoch `json:"EPOCH,string"`
MaxBlobsPerBlock uint64 `json:"MAX_BLOBS_PER_BLOCK,string"`
}

// BlobSchedule represents the BLOB_SCHEDULE configuration.
type BlobSchedule []BlobScheduleEntry

// GetMaxBlobsPerBlock returns the maximum number of blobs that can be included in a block for a given epoch.
func (bs BlobSchedule) GetMaxBlobsPerBlock(epoch phase0.Epoch) uint64 {
if len(bs) == 0 {
return 0
}

// Sort by epoch in descending order to find the most recent applicable entry.
sorted := make(BlobSchedule, len(bs))
copy(sorted, bs)

sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Epoch > sorted[j].Epoch
})

// Find the first entry where epoch >= entry.Epoch.
for _, entry := range sorted {
if epoch >= entry.Epoch {
return entry.MaxBlobsPerBlock
}
}

// If no entry is found, return the minimum value from all entries.
minBlobs := sorted[0].MaxBlobsPerBlock
for _, entry := range sorted {
if entry.MaxBlobsPerBlock < minBlobs {
minBlobs = entry.MaxBlobsPerBlock
}
}

return minBlobs
}
150 changes: 150 additions & 0 deletions pkg/beacon/state/blob_schedule_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package state

import (
"testing"

"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/assert"
)

func TestBlobSchedule_GetMaxBlobsPerBlock(t *testing.T) {
tests := []struct {
name string
schedule BlobSchedule
epoch phase0.Epoch
expectedBlob uint64
}{
{
name: "empty schedule returns 0",
schedule: BlobSchedule{},
epoch: phase0.Epoch(100),
expectedBlob: 0,
},
{
name: "epoch before any schedule entry returns minimum",
schedule: BlobSchedule{
{Epoch: phase0.Epoch(100), MaxBlobsPerBlock: 6},
{Epoch: phase0.Epoch(200), MaxBlobsPerBlock: 9},
},
epoch: phase0.Epoch(50),
expectedBlob: 6,
},
{
name: "epoch exactly matches first entry",
schedule: BlobSchedule{
{Epoch: phase0.Epoch(100), MaxBlobsPerBlock: 6},
{Epoch: phase0.Epoch(200), MaxBlobsPerBlock: 9},
},
epoch: phase0.Epoch(100),
expectedBlob: 6,
},
{
name: "epoch exactly matches second entry",
schedule: BlobSchedule{
{Epoch: phase0.Epoch(100), MaxBlobsPerBlock: 6},
{Epoch: phase0.Epoch(200), MaxBlobsPerBlock: 9},
},
epoch: phase0.Epoch(200),
expectedBlob: 9,
},
{
name: "epoch between entries uses earlier entry",
schedule: BlobSchedule{
{Epoch: phase0.Epoch(100), MaxBlobsPerBlock: 6},
{Epoch: phase0.Epoch(200), MaxBlobsPerBlock: 9},
},
epoch: phase0.Epoch(150),
expectedBlob: 6,
},
{
name: "epoch after all entries uses latest entry",
schedule: BlobSchedule{
{Epoch: phase0.Epoch(100), MaxBlobsPerBlock: 6},
{Epoch: phase0.Epoch(200), MaxBlobsPerBlock: 9},
},
epoch: phase0.Epoch(300),
expectedBlob: 9,
},
{
name: "unordered schedule entries are handled correctly",
schedule: BlobSchedule{
{Epoch: phase0.Epoch(200), MaxBlobsPerBlock: 9},
{Epoch: phase0.Epoch(100), MaxBlobsPerBlock: 6},
{Epoch: phase0.Epoch(300), MaxBlobsPerBlock: 12},
},
epoch: phase0.Epoch(250),
expectedBlob: 9,
},
{
name: "fulu testnet example - deneb epoch",
schedule: BlobSchedule{
{Epoch: phase0.Epoch(512), MaxBlobsPerBlock: 12},
{Epoch: phase0.Epoch(768), MaxBlobsPerBlock: 15},
{Epoch: phase0.Epoch(1024), MaxBlobsPerBlock: 18},
{Epoch: phase0.Epoch(1280), MaxBlobsPerBlock: 9},
{Epoch: phase0.Epoch(1584), MaxBlobsPerBlock: 20},
},
epoch: phase0.Epoch(600),
expectedBlob: 12,
},
{
name: "fulu testnet example - electra epoch",
schedule: BlobSchedule{
{Epoch: phase0.Epoch(512), MaxBlobsPerBlock: 12},
{Epoch: phase0.Epoch(768), MaxBlobsPerBlock: 15},
{Epoch: phase0.Epoch(1024), MaxBlobsPerBlock: 18},
{Epoch: phase0.Epoch(1280), MaxBlobsPerBlock: 9},
{Epoch: phase0.Epoch(1584), MaxBlobsPerBlock: 20},
},
epoch: phase0.Epoch(1300),
expectedBlob: 9,
},
{
name: "fulu testnet example - latest epoch",
schedule: BlobSchedule{
{Epoch: phase0.Epoch(512), MaxBlobsPerBlock: 12},
{Epoch: phase0.Epoch(768), MaxBlobsPerBlock: 15},
{Epoch: phase0.Epoch(1024), MaxBlobsPerBlock: 18},
{Epoch: phase0.Epoch(1280), MaxBlobsPerBlock: 9},
{Epoch: phase0.Epoch(1584), MaxBlobsPerBlock: 20},
},
epoch: phase0.Epoch(2000),
expectedBlob: 20,
},
{
name: "single entry schedule",
schedule: BlobSchedule{
{Epoch: phase0.Epoch(100), MaxBlobsPerBlock: 6},
},
epoch: phase0.Epoch(150),
expectedBlob: 6,
},
{
name: "single entry schedule before epoch",
schedule: BlobSchedule{
{Epoch: phase0.Epoch(100), MaxBlobsPerBlock: 6},
},
epoch: phase0.Epoch(50),
expectedBlob: 6,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.schedule.GetMaxBlobsPerBlock(tt.epoch)
assert.Equal(t, tt.expectedBlob, result, "Expected %d blobs for epoch %d", tt.expectedBlob, tt.epoch)
})
}
}

func TestSpec_GetMaxBlobsPerBlock(t *testing.T) {
spec := Spec{
BlobSchedule: BlobSchedule{
{Epoch: phase0.Epoch(100), MaxBlobsPerBlock: 6},
{Epoch: phase0.Epoch(200), MaxBlobsPerBlock: 9},
},
}

result := spec.GetMaxBlobsPerBlock(phase0.Epoch(150))
assert.Equal(t, uint64(6), result, "Spec.GetMaxBlobsPerBlock should delegate to BlobSchedule.GetMaxBlobsPerBlock")
}
23 changes: 22 additions & 1 deletion pkg/beacon/state/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ type Spec struct {
MinGenesisActiveValidatorCount uint64 `json:"MIN_GENESIS_ACTIVE_VALIDATOR_COUNT,string"`
Eth1FollowDistance uint64 `json:"ETH1_FOLLOW_DISTANCE,string"`

ForkEpochs ForkEpochs `json:"-"`
ForkEpochs ForkEpochs `json:"-"`
BlobSchedule BlobSchedule `json:"BLOB_SCHEDULE"`
}

// NewSpec creates a new spec instance.
Expand Down Expand Up @@ -192,6 +193,21 @@ func NewSpec(data map[string]interface{}) Spec {
})
}

if blobSchedule, exists := data["BLOB_SCHEDULE"]; exists {
if scheduleData, ok := blobSchedule.([]interface{}); ok {
spec.BlobSchedule = make(BlobSchedule, len(scheduleData))

for i, entry := range scheduleData {
if entryMap, ok := entry.(map[string]interface{}); ok {
spec.BlobSchedule[i] = BlobScheduleEntry{
Epoch: phase0.Epoch(cast.ToUint64(entryMap["EPOCH"])),
MaxBlobsPerBlock: cast.ToUint64(entryMap["MAX_BLOBS_PER_BLOCK"]),
}
}
}
}
}

return spec
}

Expand All @@ -200,6 +216,11 @@ func (s *Spec) Validate() error {
return nil
}

// GetMaxBlobsPerBlock returns the maximum number of blobs that can be included in a block for a given epoch.
func (s *Spec) GetMaxBlobsPerBlock(epoch phase0.Epoch) uint64 {
return s.BlobSchedule.GetMaxBlobsPerBlock(epoch)
}

func dataVersionFromString(name string) (sp.DataVersion, error) {
var v sp.DataVersion
if err := json.Unmarshal([]byte(fmt.Sprintf("\"%s\"", name)), &v); err != nil {
Expand Down
Loading