mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-08 23:18:15 -05:00
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:
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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")
|
||||
@@ -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 ðpb.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
|
||||
}
|
||||
@@ -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: ðpb.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: ðpb.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: ðpb.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: ðpb.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: ðpb.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: ðpb.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: ðpb.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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 := ðpb.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 ðpb.Fork{
|
||||
PreviousVersion: prevVersion[:],
|
||||
CurrentVersion: currentVersion[:],
|
||||
Epoch: forkEpoch,
|
||||
}, nil
|
||||
}
|
||||
@@ -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 ¶ms.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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user