Refactor fork schedules (#15490)

* overhaul fork schedule management for bpos

* Unify log

* Radek's comments

* Use arg config to determine previous epoch, with regression test

* Remove unnecessary NewClock. @potuz feedback

* Continuation of previous commit: Remove unnecessary NewClock. @potuz feedback

* Remove VerifyBlockHeaderSignatureUsingCurrentFork

* cosmetic changes

* Remove unnecessary copy. entryWithForkDigest passes by value, not by pointer so it shold be fine

* Reuse ErrInvalidTopic from p2p package

* Unskip TestServer_GetBeaconConfig

* Resolve TODO about forkwatcher in local mode

* remove Copy()

---------

Co-authored-by: Kasey <kasey@users.noreply.github.com>
Co-authored-by: terence tsao <terence@prysmaticlabs.com>
Co-authored-by: rkapka <radoslaw.kapka@gmail.com>
Co-authored-by: Preston Van Loon <preston@pvl.dev>
This commit is contained in:
kasey
2025-08-11 09:08:53 -07:00
committed by GitHub
parent f7f992c256
commit 3da40ecd9c
117 changed files with 1469 additions and 2426 deletions

View File

@@ -1,39 +0,0 @@
load("@prysm//tools/go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"errors.go",
"fork.go",
"ordered.go",
],
importpath = "github.com/OffchainLabs/prysm/v6/network/forks",
visibility = ["//visibility:public"],
deps = [
"//beacon-chain/core/signing:go_default_library",
"//config/fieldparams:go_default_library",
"//config/params:go_default_library",
"//consensus-types/primitives:go_default_library",
"//encoding/bytesutil:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//time/slots:go_default_library",
"@com_github_pkg_errors//:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"fork_test.go",
"ordered_test.go",
],
embed = [":go_default_library"],
deps = [
"//beacon-chain/core/signing:go_default_library",
"//config/params:go_default_library",
"//consensus-types/primitives:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
],
)

View File

@@ -1,10 +0,0 @@
package forks
import "github.com/pkg/errors"
// ErrVersionNotFound indicates the config package couldn't determine the version for an epoch using the fork schedule.
var ErrVersionNotFound = errors.New("could not find an entry in the fork schedule")
// ErrNoPreviousVersion indicates that a version prior to the given version could not be found, because the given version
// is the first one in the list
var ErrNoPreviousVersion = errors.New("no previous version")

View File

@@ -1,202 +0,0 @@
// Package forks contains useful helpers for Ethereum consensus fork-related functionality.
package forks
import (
"bytes"
"math"
"sort"
"time"
"github.com/OffchainLabs/prysm/v6/beacon-chain/core/signing"
"github.com/OffchainLabs/prysm/v6/config/params"
"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v6/encoding/bytesutil"
ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v6/time/slots"
"github.com/pkg/errors"
)
// IsForkNextEpoch checks if an allotted fork is in the following epoch.
func IsForkNextEpoch(genesisTime time.Time, genesisValidatorsRoot []byte) (bool, error) {
if genesisTime.IsZero() {
return false, errors.New("genesis time is not set")
}
if len(genesisValidatorsRoot) == 0 {
return false, errors.New("genesis validators root is not set")
}
currentSlot := slots.CurrentSlot(genesisTime)
currentEpoch := slots.ToEpoch(currentSlot)
fSchedule := params.BeaconConfig().ForkVersionSchedule
scheduledForks := SortedForkVersions(fSchedule)
isForkEpoch := false
for _, forkVersion := range scheduledForks {
epoch := fSchedule[forkVersion]
if currentEpoch+1 == epoch {
isForkEpoch = true
break
}
}
return isForkEpoch, nil
}
// ForkDigestFromEpoch retrieves the fork digest from the current schedule determined
// by the provided epoch.
func ForkDigestFromEpoch(currentEpoch primitives.Epoch, genesisValidatorsRoot []byte) ([4]byte, error) {
if len(genesisValidatorsRoot) == 0 {
return [4]byte{}, errors.New("genesis validators root is not set")
}
forkData, err := Fork(currentEpoch)
if err != nil {
return [4]byte{}, err
}
return signing.ComputeForkDigest(forkData.CurrentVersion, genesisValidatorsRoot)
}
// CreateForkDigest creates a fork digest from a genesis time and genesis
// validators root, utilizing the current slot to determine
// the active fork version in the node.
func CreateForkDigest(
genesisTime time.Time,
genesisValidatorsRoot []byte,
) ([4]byte, error) {
if genesisTime.IsZero() {
return [4]byte{}, errors.New("genesis time is not set")
}
if len(genesisValidatorsRoot) == 0 {
return [4]byte{}, errors.New("genesis validators root is not set")
}
currentSlot := slots.CurrentSlot(genesisTime)
currentEpoch := slots.ToEpoch(currentSlot)
forkData, err := Fork(currentEpoch)
if err != nil {
return [4]byte{}, err
}
digest, err := signing.ComputeForkDigest(forkData.CurrentVersion, genesisValidatorsRoot)
if err != nil {
return [4]byte{}, err
}
return digest, nil
}
// Fork given a target epoch,
// returns the active fork version during this epoch.
func Fork(
targetEpoch primitives.Epoch,
) (*ethpb.Fork, error) {
currentForkVersion := bytesutil.ToBytes4(params.BeaconConfig().GenesisForkVersion)
previousForkVersion := bytesutil.ToBytes4(params.BeaconConfig().GenesisForkVersion)
fSchedule := params.BeaconConfig().ForkVersionSchedule
sortedForkVersions := SortedForkVersions(fSchedule)
forkEpoch := primitives.Epoch(0)
for _, forkVersion := range sortedForkVersions {
epoch, ok := fSchedule[forkVersion]
if !ok {
return nil, errors.Errorf("fork version %x doesn't exist in schedule", forkVersion)
}
if targetEpoch >= epoch {
previousForkVersion = currentForkVersion
currentForkVersion = forkVersion
forkEpoch = epoch
}
}
return &ethpb.Fork{
PreviousVersion: previousForkVersion[:],
CurrentVersion: currentForkVersion[:],
Epoch: forkEpoch,
}, nil
}
// RetrieveForkDataFromDigest performs the inverse, where it tries to determine the fork version
// and epoch from a provided digest by looping through our current fork schedule.
func RetrieveForkDataFromDigest(digest [4]byte, genesisValidatorsRoot []byte) ([4]byte, primitives.Epoch, error) {
fSchedule := params.BeaconConfig().ForkVersionSchedule
for v, e := range fSchedule {
rDigest, err := signing.ComputeForkDigest(v[:], genesisValidatorsRoot)
if err != nil {
return [4]byte{}, 0, err
}
if rDigest == digest {
return v, e, nil
}
}
return [4]byte{}, 0, errors.Errorf("no fork exists for a digest of %#x", digest)
}
// NextForkData retrieves the next fork data according to the
// provided current epoch.
func NextForkData(currEpoch primitives.Epoch) ([4]byte, primitives.Epoch, error) {
fSchedule := params.BeaconConfig().ForkVersionSchedule
sortedForkVersions := SortedForkVersions(fSchedule)
nextForkEpoch := primitives.Epoch(math.MaxUint64)
var nextForkVersion [4]byte
for _, forkVersion := range sortedForkVersions {
epoch, ok := fSchedule[forkVersion]
if !ok {
return [4]byte{}, 0, errors.Errorf("fork version %x doesn't exist in schedule", forkVersion)
}
// If we get an epoch larger than out current epoch
// we set this as our next fork epoch and exit the
// loop.
if epoch > currEpoch {
nextForkEpoch = epoch
nextForkVersion = forkVersion
break
}
// In the event the retrieved epoch is less than
// our current epoch, we mark the previous
// fork's version as the next fork version.
if epoch <= currEpoch {
// The next fork version is updated to
// always include the most current fork version.
nextForkVersion = forkVersion
}
}
return nextForkVersion, nextForkEpoch, nil
}
// SortedForkVersions sorts the provided fork schedule in ascending order
// by epoch.
func SortedForkVersions(forkSchedule map[[4]byte]primitives.Epoch) [][4]byte {
sortedVersions := make([][4]byte, len(forkSchedule))
i := 0
for k := range forkSchedule {
sortedVersions[i] = k
i++
}
sort.Slice(sortedVersions, func(a, b int) bool {
// va == "version" a, ie the [4]byte version id
va, vb := sortedVersions[a], sortedVersions[b]
// ea == "epoch" a, ie the types.Epoch corresponding to va
ea, eb := forkSchedule[va], forkSchedule[vb]
// Try to sort by epochs first, which works fine when epochs are all distinct.
// in the case of testnets starting from a given fork, all epochs leading to the fork will be zero.
if ea != eb {
return ea < eb
}
// If the epochs are equal, break the tie with a lexicographic comparison of the fork version bytes.
// eg 2 versions both with a fork epoch of 0, 0x00000000 would come before 0x01000000.
// sort.Slice takes a 'less' func, ie `return a < b`, and when va < vb, bytes.Compare will return -1
return bytes.Compare(va[:], vb[:]) < 0
})
return sortedVersions
}
// LastForkEpoch returns the last valid fork epoch that exists in our
// fork schedule.
func LastForkEpoch() primitives.Epoch {
fSchedule := params.BeaconConfig().ForkVersionSchedule
sortedForkVersions := SortedForkVersions(fSchedule)
lastValidEpoch := primitives.Epoch(0)
numOfVersions := len(sortedForkVersions)
for i := numOfVersions - 1; i >= 0; i-- {
v := sortedForkVersions[i]
fEpoch := fSchedule[v]
if fEpoch != math.MaxUint64 {
lastValidEpoch = fEpoch
break
}
}
return lastValidEpoch
}

View File

@@ -1,485 +0,0 @@
package forks
import (
"math"
"reflect"
"testing"
"time"
"github.com/OffchainLabs/prysm/v6/beacon-chain/core/signing"
"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"
)
func TestFork(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
tests := []struct {
name string
targetEpoch primitives.Epoch
want *ethpb.Fork
wantErr bool
setConfg func()
}{
{
name: "no fork",
targetEpoch: 0,
want: &ethpb.Fork{
Epoch: 0,
CurrentVersion: []byte{'A', 'B', 'C', 'D'},
PreviousVersion: []byte{'A', 'B', 'C', 'D'},
},
wantErr: false,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "genesis fork",
targetEpoch: 0,
want: &ethpb.Fork{
Epoch: 0,
CurrentVersion: []byte{'A', 'B', 'C', 'D'},
PreviousVersion: []byte{'A', 'B', 'C', 'D'},
},
wantErr: false,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "altair pre-fork",
targetEpoch: 0,
want: &ethpb.Fork{
Epoch: 0,
CurrentVersion: []byte{'A', 'B', 'C', 'D'},
PreviousVersion: []byte{'A', 'B', 'C', 'D'},
},
wantErr: false,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.AltairForkVersion = []byte{'A', 'B', 'C', 'F'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "altair on fork",
targetEpoch: 10,
want: &ethpb.Fork{
Epoch: 10,
CurrentVersion: []byte{'A', 'B', 'C', 'F'},
PreviousVersion: []byte{'A', 'B', 'C', 'D'},
},
wantErr: false,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.AltairForkVersion = []byte{'A', 'B', 'C', 'F'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "altair post fork",
targetEpoch: 10,
want: &ethpb.Fork{
Epoch: 10,
CurrentVersion: []byte{'A', 'B', 'C', 'F'},
PreviousVersion: []byte{'A', 'B', 'C', 'D'},
},
wantErr: false,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.AltairForkVersion = []byte{'A', 'B', 'C', 'F'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "3 forks, pre-fork",
targetEpoch: 20,
want: &ethpb.Fork{
Epoch: 10,
CurrentVersion: []byte{'A', 'B', 'C', 'F'},
PreviousVersion: []byte{'A', 'B', 'C', 'D'},
},
wantErr: false,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
{'A', 'B', 'C', 'Z'}: 100,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "3 forks, on fork",
targetEpoch: 100,
want: &ethpb.Fork{
Epoch: 100,
CurrentVersion: []byte{'A', 'B', 'C', 'Z'},
PreviousVersion: []byte{'A', 'B', 'C', 'F'},
},
wantErr: false,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
{'A', 'B', 'C', 'Z'}: 100,
}
params.OverrideBeaconConfig(cfg)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setConfg()
got, err := Fork(tt.targetEpoch)
if (err != nil) != tt.wantErr {
t.Errorf("Fork() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Fork() got = %v, want %v", got, tt.want)
}
})
}
}
func TestRetrieveForkDataFromDigest(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.GenesisEpoch = 0
cfg.AltairForkVersion = []byte{'A', 'B', 'C', 'F'}
cfg.AltairForkEpoch = 10
cfg.BellatrixForkVersion = []byte{'A', 'B', 'C', 'Z'}
cfg.BellatrixForkEpoch = 100
cfg.InitializeForkSchedule()
params.OverrideBeaconConfig(cfg)
genValRoot := [32]byte{'A', 'B', 'C', 'D'}
digest, err := signing.ComputeForkDigest([]byte{'A', 'B', 'C', 'F'}, genValRoot[:])
assert.NoError(t, err)
version, epoch, err := RetrieveForkDataFromDigest(digest, genValRoot[:])
assert.NoError(t, err)
assert.Equal(t, [4]byte{'A', 'B', 'C', 'F'}, version)
assert.Equal(t, epoch, primitives.Epoch(10))
digest, err = signing.ComputeForkDigest([]byte{'A', 'B', 'C', 'Z'}, genValRoot[:])
assert.NoError(t, err)
version, epoch, err = RetrieveForkDataFromDigest(digest, genValRoot[:])
assert.NoError(t, err)
assert.Equal(t, [4]byte{'A', 'B', 'C', 'Z'}, version)
assert.Equal(t, epoch, primitives.Epoch(100))
}
func TestIsForkNextEpoch(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
{'A', 'B', 'C', 'Z'}: 100,
}
params.OverrideBeaconConfig(cfg)
genTimeCreator := func(epoch primitives.Epoch) time.Time {
return time.Now().Add(-time.Duration(uint64(params.BeaconConfig().SlotsPerEpoch)*uint64(epoch)*params.BeaconConfig().SecondsPerSlot) * time.Second)
}
// Is at Fork Epoch
genesisTime := genTimeCreator(10)
genRoot := [32]byte{'A'}
isFork, err := IsForkNextEpoch(genesisTime, genRoot[:])
assert.NoError(t, err)
assert.Equal(t, false, isFork)
// Is right before fork epoch
genesisTime = genTimeCreator(9)
isFork, err = IsForkNextEpoch(genesisTime, genRoot[:])
assert.NoError(t, err)
assert.Equal(t, true, isFork)
// Is at fork epoch
genesisTime = genTimeCreator(100)
isFork, err = IsForkNextEpoch(genesisTime, genRoot[:])
assert.NoError(t, err)
assert.Equal(t, false, isFork)
genesisTime = genTimeCreator(99)
// Is right before fork epoch.
isFork, err = IsForkNextEpoch(genesisTime, genRoot[:])
assert.NoError(t, err)
assert.Equal(t, true, isFork)
}
func TestNextForkData(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
tests := []struct {
name string
setConfg func()
currEpoch primitives.Epoch
wantedForkVersion [4]byte
wantedEpoch primitives.Epoch
}{
{
name: "genesis fork",
currEpoch: 0,
wantedForkVersion: [4]byte{'A', 'B', 'C', 'D'},
wantedEpoch: math.MaxUint64,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "altair pre-fork",
currEpoch: 5,
wantedForkVersion: [4]byte{'A', 'B', 'C', 'F'},
wantedEpoch: 10,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.AltairForkVersion = []byte{'A', 'B', 'C', 'F'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "altair on fork",
currEpoch: 10,
wantedForkVersion: [4]byte{'A', 'B', 'C', 'F'},
wantedEpoch: math.MaxUint64,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.AltairForkVersion = []byte{'A', 'B', 'C', 'F'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "altair post fork",
currEpoch: 20,
wantedForkVersion: [4]byte{'A', 'B', 'C', 'F'},
wantedEpoch: math.MaxUint64,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.AltairForkVersion = []byte{'A', 'B', 'C', 'F'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "3 forks, pre-fork, 1st fork",
currEpoch: 5,
wantedForkVersion: [4]byte{'A', 'B', 'C', 'F'},
wantedEpoch: 10,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
{'A', 'B', 'C', 'Z'}: 100,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "3 forks, pre-fork, 2nd fork",
currEpoch: 50,
wantedForkVersion: [4]byte{'A', 'B', 'C', 'Z'},
wantedEpoch: 100,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
{'A', 'B', 'C', 'Z'}: 100,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "3 forks, on fork",
currEpoch: 100,
wantedForkVersion: [4]byte{'A', 'B', 'C', 'Z'},
wantedEpoch: math.MaxUint64,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
{'A', 'B', 'C', 'Z'}: 100,
}
params.OverrideBeaconConfig(cfg)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setConfg()
fVersion, fEpoch, err := NextForkData(tt.currEpoch)
assert.NoError(t, err)
if fVersion != tt.wantedForkVersion {
t.Errorf("NextForkData() fork version = %v, want %v", fVersion, tt.wantedForkVersion)
}
if fEpoch != tt.wantedEpoch {
t.Errorf("NextForkData() fork epoch = %v, want %v", fEpoch, tt.wantedEpoch)
}
})
}
}
func TestLastForkEpoch(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
tests := []struct {
name string
setConfg func()
wantedEpoch primitives.Epoch
}{
{
name: "no schedule",
wantedEpoch: 0,
setConfg: func() {
cfg = cfg.Copy()
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "genesis fork",
wantedEpoch: 0,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "altair post fork",
wantedEpoch: 10,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.AltairForkVersion = []byte{'A', 'B', 'C', 'F'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "3 forks, 1 valid fork",
wantedEpoch: 5,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 5,
{'A', 'B', 'C', 'F'}: math.MaxUint64,
{'A', 'B', 'C', 'Z'}: math.MaxUint64,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "3 forks, 2 valid ones",
wantedEpoch: 10,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
{'A', 'B', 'C', 'Z'}: math.MaxUint64,
}
params.OverrideBeaconConfig(cfg)
},
},
{
name: "3 forks",
wantedEpoch: 100,
setConfg: func() {
cfg = cfg.Copy()
cfg.GenesisForkVersion = []byte{'A', 'B', 'C', 'D'}
cfg.ForkVersionSchedule = map[[4]byte]primitives.Epoch{
{'A', 'B', 'C', 'D'}: 0,
{'A', 'B', 'C', 'F'}: 10,
{'A', 'B', 'C', 'Z'}: 100,
}
params.OverrideBeaconConfig(cfg)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setConfg()
fEpoch := LastForkEpoch()
if fEpoch != tt.wantedEpoch {
t.Errorf("LastForkEpoch() fork epoch = %v, want %v", fEpoch, tt.wantedEpoch)
}
})
}
}

View File

@@ -1,125 +0,0 @@
package forks
import (
"bytes"
"sort"
"strings"
fieldparams "github.com/OffchainLabs/prysm/v6/config/fieldparams"
"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/pkg/errors"
)
// ForkScheduleEntry is a Version+Epoch tuple for sorted storage in an OrderedSchedule
type ForkScheduleEntry struct {
Version [fieldparams.VersionLength]byte
Epoch primitives.Epoch
Name string
}
// OrderedSchedule provides a type that can be used to sort the fork schedule and find the Version
// the chain should be at for a given epoch (via VersionForEpoch) or name (via VersionForName).
type OrderedSchedule []ForkScheduleEntry
// Len implements the Len method of sort.Interface
func (o OrderedSchedule) Len() int { return len(o) }
// Swap implements the Swap method of sort.Interface
func (o OrderedSchedule) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
// Less implements the Less method of sort.Interface
func (o OrderedSchedule) Less(i, j int) bool {
if o[i].Epoch == o[j].Epoch {
return bytes.Compare(o[i].Version[:], o[j].Version[:]) < 0
}
return o[i].Epoch < o[j].Epoch
}
// VersionForEpoch finds the Version with the highest epoch <= the given epoch
func (o OrderedSchedule) VersionForEpoch(epoch primitives.Epoch) ([fieldparams.VersionLength]byte, error) {
for i := len(o) - 1; i >= 0; i-- {
if o[i].Epoch <= epoch {
return o[i].Version, nil
}
}
return [fieldparams.VersionLength]byte{}, errors.Wrapf(ErrVersionNotFound, "no epoch in list <= %d", epoch)
}
// VersionForName finds the Version corresponding to the lowercase version of the provided name.
func (o OrderedSchedule) VersionForName(name string) ([fieldparams.VersionLength]byte, error) {
lower := strings.ToLower(name)
for _, e := range o {
if e.Name == lower {
return e.Version, nil
}
}
return [4]byte{}, errors.Wrapf(ErrVersionNotFound, "no version with name %s", lower)
}
func (o OrderedSchedule) ForkFromVersion(version [fieldparams.VersionLength]byte) (*ethpb.Fork, error) {
for i := range o {
e := o[i]
if e.Version == version {
f := &ethpb.Fork{Epoch: e.Epoch, CurrentVersion: version[:], PreviousVersion: version[:]}
if i > 0 {
f.PreviousVersion = o[i-1].Version[:]
}
return f, nil
}
}
return nil, errors.Wrapf(ErrVersionNotFound, "could not determine fork for version %#x", version)
}
func (o OrderedSchedule) Previous(version [fieldparams.VersionLength]byte) ([fieldparams.VersionLength]byte, error) {
for i := len(o) - 1; i >= 0; i-- {
if o[i].Version == version {
if i-1 >= 0 {
return o[i-1].Version, nil
} else {
return [fieldparams.VersionLength]byte{}, errors.Wrapf(ErrNoPreviousVersion, "%#x is the first version", version)
}
}
}
return [fieldparams.VersionLength]byte{}, errors.Wrapf(ErrVersionNotFound, "no version in list == %#x", version)
}
// NewOrderedSchedule Converts fork version maps into a list of Version+Epoch+Name values, ordered by Epoch from lowest to highest.
// See docs for OrderedSchedule for more detail on what you can do with this type.
func NewOrderedSchedule(b *params.BeaconChainConfig) OrderedSchedule {
ofs := make(OrderedSchedule, 0)
for version, epoch := range b.ForkVersionSchedule {
fse := ForkScheduleEntry{
Version: version,
Epoch: epoch,
Name: b.ForkVersionNames[version],
}
ofs = append(ofs, fse)
}
sort.Sort(ofs)
return ofs
}
// ForkForEpochFromConfig returns the fork data for the given epoch from the provided config.
func ForkForEpochFromConfig(cfg *params.BeaconChainConfig, epoch primitives.Epoch) (*ethpb.Fork, error) {
os := NewOrderedSchedule(cfg)
currentVersion, err := os.VersionForEpoch(epoch)
if err != nil {
return nil, err
}
prevVersion, err := os.Previous(currentVersion)
if err != nil {
if !errors.Is(err, ErrNoPreviousVersion) {
return nil, err
}
// use same version for both in the case of genesis
prevVersion = currentVersion
}
forkEpoch := cfg.ForkVersionSchedule[currentVersion]
return &ethpb.Fork{
PreviousVersion: prevVersion[:],
CurrentVersion: currentVersion[:],
Epoch: forkEpoch,
}, nil
}

View File

@@ -1,169 +0,0 @@
package forks
import (
"math"
"testing"
"github.com/OffchainLabs/prysm/v6/config/params"
"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v6/testing/require"
)
func TestOrderedConfigSchedule(t *testing.T) {
params.SetupTestConfigCleanup(t)
for _, cfg := range params.All() {
t.Run(cfg.ConfigName, func(t *testing.T) {
prevVersion := [4]byte{0, 0, 0, 0}
// epoch 0 is genesis, and it's a uint so can't make it -1
// so we use a pointer to detect the boundary condition and skip it
var prevEpoch *primitives.Epoch
for _, fse := range NewOrderedSchedule(cfg) {
// copy loop variable so we can take the address of fields
f := fse
if prevEpoch == nil {
prevEpoch = &f.Epoch
prevVersion = f.Version
continue
}
if *prevEpoch > f.Epoch {
t.Errorf("Epochs out of order! %#x/%d before %#x/%d", f.Version, f.Epoch, prevVersion, prevEpoch)
}
prevEpoch = &f.Epoch
prevVersion = f.Version
}
})
}
bc := testForkVersionBCC()
ofs := NewOrderedSchedule(bc)
for i := range ofs {
if ofs[i].Epoch != primitives.Epoch(math.Pow(2, float64(i))) {
t.Errorf("expected %dth element of list w/ epoch=%d, got=%d. list=%v", i, primitives.Epoch(2^i), ofs[i].Epoch, ofs)
}
}
}
func TestVersionForEpoch(t *testing.T) {
bc := testForkVersionBCC()
ofs := NewOrderedSchedule(bc)
testCases := []struct {
name string
version [4]byte
epoch primitives.Epoch
err error
}{
{
name: "found between versions",
version: [4]byte{2, 1, 2, 3},
epoch: primitives.Epoch(7),
},
{
name: "found at end",
version: [4]byte{4, 1, 2, 3},
epoch: primitives.Epoch(100),
},
{
name: "found at start",
version: [4]byte{0, 1, 2, 3},
epoch: primitives.Epoch(1),
},
{
name: "found at boundary",
version: [4]byte{1, 1, 2, 3},
epoch: primitives.Epoch(2),
},
{
name: "not found before",
epoch: primitives.Epoch(0),
err: ErrVersionNotFound,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
v, err := ofs.VersionForEpoch(tc.epoch)
if tc.err == nil {
require.NoError(t, err)
} else {
require.ErrorIs(t, err, tc.err)
}
require.Equal(t, tc.version, v)
})
}
}
func TestVersionForName(t *testing.T) {
bc := testForkVersionBCC()
ofs := NewOrderedSchedule(bc)
testCases := []struct {
testName string
version [4]byte
versionName string
err error
}{
{
testName: "found",
version: [4]byte{2, 1, 2, 3},
versionName: "third",
},
{
testName: "found lowercase",
version: [4]byte{4, 1, 2, 3},
versionName: "FiFtH",
},
{
testName: "not found",
versionName: "nonexistent",
err: ErrVersionNotFound,
},
}
for _, tc := range testCases {
t.Run(tc.testName, func(t *testing.T) {
v, err := ofs.VersionForName(tc.versionName)
if tc.err == nil {
require.NoError(t, err)
} else {
require.ErrorIs(t, err, tc.err)
}
require.Equal(t, tc.version, v)
})
}
}
func testForkVersionBCC() *params.BeaconChainConfig {
return &params.BeaconChainConfig{
ForkVersionSchedule: map[[4]byte]primitives.Epoch{
{1, 1, 2, 3}: primitives.Epoch(2),
{0, 1, 2, 3}: primitives.Epoch(1),
{4, 1, 2, 3}: primitives.Epoch(16),
{3, 1, 2, 3}: primitives.Epoch(8),
{2, 1, 2, 3}: primitives.Epoch(4),
},
ForkVersionNames: map[[4]byte]string{
{1, 1, 2, 3}: "second",
{0, 1, 2, 3}: "first",
{4, 1, 2, 3}: "fifth",
{3, 1, 2, 3}: "fourth",
{2, 1, 2, 3}: "third",
},
}
}
func TestPrevious(t *testing.T) {
cfg := testForkVersionBCC()
os := NewOrderedSchedule(cfg)
unreal := [4]byte{255, 255, 255, 255}
_, err := os.Previous(unreal)
require.ErrorIs(t, err, ErrVersionNotFound)
// first element has no previous, should return appropriate error
_, err = os.Previous(os[0].Version)
require.ErrorIs(t, err, ErrNoPreviousVersion)
// work up the list from the second element to the last, make sure each result matches the previous element
// this test of course relies on TestOrderedConfigSchedule to be correct!
prev := os[0].Version
for i := 1; i < len(os); i++ {
p, err := os.Previous(os[i].Version)
require.NoError(t, err)
require.Equal(t, prev, p)
prev = os[i].Version
}
}