params: add validation tests and remove LatestFork

This commit is contained in:
Felix Lange 2025-07-24 11:46:13 +02:00
parent 57c395f59a
commit 13d14f5ab8
4 changed files with 107 additions and 30 deletions

View file

@ -24,6 +24,7 @@ import (
"maps" "maps"
"math/big" "math/big"
"slices" "slices"
"strconv"
"strings" "strings"
"github.com/ethereum/go-ethereum/params/forks" "github.com/ethereum/go-ethereum/params/forks"
@ -43,7 +44,6 @@ func NewConfig2(activations Activations, param ...ParamValue) *Config2 {
activation: maps.Clone(activations), activation: maps.Clone(activations),
param: make(map[int]any, len(param)), param: make(map[int]any, len(param)),
} }
cfg.activation[forks.Frontier] = 0
for _, pv := range param { for _, pv := range param {
cfg.param[pv.id] = pv.value cfg.param[pv.id] = pv.value
} }
@ -52,6 +52,9 @@ func NewConfig2(activations Activations, param ...ParamValue) *Config2 {
// Active reports whether the given fork is active for a block number/time. // Active reports whether the given fork is active for a block number/time.
func (cfg *Config2) Active(f forks.Fork, block, timestamp uint64) bool { func (cfg *Config2) Active(f forks.Fork, block, timestamp uint64) bool {
if f == forks.Frontier {
return true
}
activation, ok := cfg.activation[f] activation, ok := cfg.activation[f]
if f.BlockBased() { if f.BlockBased() {
return ok && block >= activation return ok && block >= activation
@ -61,6 +64,9 @@ func (cfg *Config2) Active(f forks.Fork, block, timestamp uint64) bool {
// ActiveAtBlock reports whether the given fork is active for a block number/time. // ActiveAtBlock reports whether the given fork is active for a block number/time.
func (cfg *Config2) ActiveAtBlock(f forks.Fork, block *big.Int) bool { func (cfg *Config2) ActiveAtBlock(f forks.Fork, block *big.Int) bool {
if f == forks.Frontier {
return true
}
if !f.BlockBased() { if !f.BlockBased() {
panic(fmt.Sprintf("fork %v has time-based scheduling", f)) panic(fmt.Sprintf("fork %v has time-based scheduling", f))
} }
@ -96,19 +102,38 @@ func (cfg *Config2) SetParam(param ...ParamValue) *Config2 {
return cpy return cpy
} }
// LatestFork returns the latest time-based fork that would be active for the given time. // String encodes the config in a readable way.
func (cfg *Config2) LatestFork(time uint64) forks.Fork { func (cfg *Config2) String() string {
var active []forks.Fork paramList := slices.Sorted(maps.Keys(cfg.param))
for f := range forks.All() { forkList := make([]forkActivation, 0, len(cfg.activation))
if f.BlockBased() { for f, a := range cfg.activation {
continue forkList = append(forkList, forkActivation{f, a})
}
if a, ok := cfg.activation[f]; ok && a <= time {
active = append(active, f)
}
} }
slices.SortFunc(forkList, forkActivation.compare)
return forks.Paris var out strings.Builder
var sp bool
writeSp := func() {
if sp {
out.WriteString(" ")
}
sp = true
}
out.WriteRune('[')
for _, fa := range forkList {
writeSp()
out.WriteString(fa.fork.ConfigName())
out.WriteString(":")
out.WriteString(strconv.FormatUint(fa.activation, 10))
}
for _, p := range paramList {
writeSp()
out.WriteString(paramRegistry[p].name)
out.WriteString(":")
fmt.Fprintf(&out, "%v", cfg.param[p])
}
out.WriteRune(']')
return out.String()
} }
// MarshalJSON encodes the config as JSON. // MarshalJSON encodes the config as JSON.
@ -146,9 +171,7 @@ func (cfg *Config2) UnmarshalJSON(input []byte) error {
return fmt.Errorf("expected JSON object for chain configuration") return fmt.Errorf("expected JSON object for chain configuration")
} }
// Now we're in the object. // Now we're in the object.
newcfg := Config2{ newcfg := Config2{activation: Activations{}}
activation: make(Activations),
}
for { for {
tok, err = dec.Token() tok, err = dec.Token()
if tok == json.Delim('}') { if tok == json.Delim('}') {
@ -182,6 +205,9 @@ func (cfg *Config2) decodeActivation(key string, dec *json.Decoder) error {
if !ok || !f.BlockBased() { if !ok || !f.BlockBased() {
return fmt.Errorf("unknown block-based fork %q", name) return fmt.Errorf("unknown block-based fork %q", name)
} }
if f == forks.Frontier {
return fmt.Errorf("frontier fork cannot be scheduled")
}
} else if name, ok = strings.CutSuffix(key, "Time"); ok { } else if name, ok = strings.CutSuffix(key, "Time"); ok {
f, ok = forks.ForkByConfigName(name) f, ok = forks.ForkByConfigName(name)
if !ok || f.BlockBased() { if !ok || f.BlockBased() {
@ -215,6 +241,9 @@ func (cfg *Config2) Validate() error {
} }
for dep := range f.DirectDependencies() { for dep := range f.DirectDependencies() {
if dep == forks.Frontier {
continue
}
switch { switch {
// Non-optional forks must all be present in the chain config up to the last defined fork. // Non-optional forks must all be present in the chain config up to the last defined fork.
case !cfg.Scheduled(dep) && cfg.Scheduled(f): case !cfg.Scheduled(dep) && cfg.Scheduled(f):
@ -238,7 +267,7 @@ func (cfg *Config2) Validate() error {
v, isSet := cfg.param[id] v, isSet := cfg.param[id]
if !isSet { if !isSet {
if !info.optional { if !info.optional {
return fmt.Errorf("required chain parameter %s is not set", info.name) return fmt.Errorf("required chain parameter %q is not set", info.name)
} }
v = info.defaultValue v = info.defaultValue
} }
@ -260,8 +289,6 @@ func (cfg *Config2) Validate() error {
// point where the new config can be applied safely. // point where the new config can be applied safely.
func (cfg *Config2) CheckCompatible(newcfg *Config2, blocknum uint64, time uint64) *ConfigCompatError { func (cfg *Config2) CheckCompatible(newcfg *Config2, blocknum uint64, time uint64) *ConfigCompatError {
// Gather forks which are active in either config, and sort them by activation. // Gather forks which are active in either config, and sort them by activation.
// Here we assume block-based forks activate before time-based ones.
// For forks with equal activation, the ordering is based on fork name to ensure a consistent result.
var forkList []forkActivation var forkList []forkActivation
for f := range forks.All() { for f := range forks.All() {
a, ok := minActivation(f, cfg, newcfg) a, ok := minActivation(f, cfg, newcfg)
@ -269,18 +296,7 @@ func (cfg *Config2) CheckCompatible(newcfg *Config2, blocknum uint64, time uint6
forkList = append(forkList, forkActivation{f, a}) forkList = append(forkList, forkActivation{f, a})
} }
} }
slices.SortFunc(forkList, func(f1, f2 forkActivation) int { slices.SortFunc(forkList, forkActivation.compare)
switch {
case f1.fork.BlockBased() && !f2.fork.BlockBased():
return -1
case !f2.fork.BlockBased() && f2.fork.BlockBased():
return 1
case f1.activation == f2.activation:
return strings.Compare(f1.fork.String(), f2.fork.String())
default:
return cmp.Compare(f1.activation, f2.activation)
}
})
// Iterate checkCompatible to find the lowest conflict. // Iterate checkCompatible to find the lowest conflict.
var lasterr *ConfigCompatError var lasterr *ConfigCompatError
@ -387,3 +403,18 @@ type forkActivation struct {
fork forks.Fork fork forks.Fork
activation uint64 activation uint64
} }
func (fa forkActivation) compare(other forkActivation) int {
// Here we assume block-based forks activate before time-based ones. For forks with
// equal activation, the ordering is based on fork name to ensure a consistent result.
switch {
case fa.fork.BlockBased() && !other.fork.BlockBased():
return -1
case !fa.fork.BlockBased() && other.fork.BlockBased():
return 1
case fa.activation == other.activation:
return strings.Compare(fa.fork.String(), other.fork.String())
default:
return cmp.Compare(fa.activation, other.activation)
}
}

View file

@ -18,15 +18,46 @@ package params_test
import ( import (
"math/big" "math/big"
"path/filepath"
"reflect" "reflect"
"testing" "testing"
"time" "time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/params/forks" "github.com/ethereum/go-ethereum/params/forks"
"github.com/ethereum/go-ethereum/params/presets" "github.com/ethereum/go-ethereum/params/presets"
) )
func TestConfigValidateErrors(t *testing.T) {
files, err := filepath.Glob("testdata/invalid-*.json")
if err != nil {
t.Fatal(err)
}
type test struct {
Config params.Config2 `json:"config"`
Error string `json:"error"`
}
for _, f := range files {
name := filepath.Base(f)
t.Run(name, func(t *testing.T) {
var test test
if err := common.LoadJSON(f, &test); err != nil {
t.Fatal(err)
}
err := test.Config.Validate()
if err == nil {
t.Fatal("expected validation error, got none")
}
if err.Error() != test.Error {
t.Fatal("wrong error:\n got:", err.Error(), "want:", test.Error)
}
})
}
}
func TestCheckCompatible2(t *testing.T) { func TestCheckCompatible2(t *testing.T) {
type test struct { type test struct {
stored, new *params.Config2 stored, new *params.Config2

View file

@ -0,0 +1,7 @@
{
"config": {
"homesteadBlock": 10,
"eip150Block": 8
},
"error": "unsupported fork ordering: Homestead enabled at block 10, but TangerineWhistle enabled at block 8"
}

View file

@ -0,0 +1,8 @@
{
"config": {
"homesteadBlock": 7,
"eip150Block": 8,
"eip155Block": 9
},
"error": "required chain parameter \"chainId\" is not set"
}