diff --git a/params/config2.go b/params/config2.go index 4fefcad717..f53438d032 100644 --- a/params/config2.go +++ b/params/config2.go @@ -24,6 +24,7 @@ import ( "maps" "math/big" "slices" + "strconv" "strings" "github.com/ethereum/go-ethereum/params/forks" @@ -43,7 +44,6 @@ func NewConfig2(activations Activations, param ...ParamValue) *Config2 { activation: maps.Clone(activations), param: make(map[int]any, len(param)), } - cfg.activation[forks.Frontier] = 0 for _, pv := range param { 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. func (cfg *Config2) Active(f forks.Fork, block, timestamp uint64) bool { + if f == forks.Frontier { + return true + } activation, ok := cfg.activation[f] if f.BlockBased() { 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. func (cfg *Config2) ActiveAtBlock(f forks.Fork, block *big.Int) bool { + if f == forks.Frontier { + return true + } if !f.BlockBased() { panic(fmt.Sprintf("fork %v has time-based scheduling", f)) } @@ -96,19 +102,38 @@ func (cfg *Config2) SetParam(param ...ParamValue) *Config2 { return cpy } -// LatestFork returns the latest time-based fork that would be active for the given time. -func (cfg *Config2) LatestFork(time uint64) forks.Fork { - var active []forks.Fork - for f := range forks.All() { - if f.BlockBased() { - continue - } - if a, ok := cfg.activation[f]; ok && a <= time { - active = append(active, f) - } +// String encodes the config in a readable way. +func (cfg *Config2) String() string { + paramList := slices.Sorted(maps.Keys(cfg.param)) + forkList := make([]forkActivation, 0, len(cfg.activation)) + for f, a := range cfg.activation { + forkList = append(forkList, forkActivation{f, a}) } + 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. @@ -146,9 +171,7 @@ func (cfg *Config2) UnmarshalJSON(input []byte) error { return fmt.Errorf("expected JSON object for chain configuration") } // Now we're in the object. - newcfg := Config2{ - activation: make(Activations), - } + newcfg := Config2{activation: Activations{}} for { tok, err = dec.Token() if tok == json.Delim('}') { @@ -182,6 +205,9 @@ func (cfg *Config2) decodeActivation(key string, dec *json.Decoder) error { if !ok || !f.BlockBased() { 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 { f, ok = forks.ForkByConfigName(name) if !ok || f.BlockBased() { @@ -215,6 +241,9 @@ func (cfg *Config2) Validate() error { } for dep := range f.DirectDependencies() { + if dep == forks.Frontier { + continue + } switch { // Non-optional forks must all be present in the chain config up to the last defined fork. case !cfg.Scheduled(dep) && cfg.Scheduled(f): @@ -238,7 +267,7 @@ func (cfg *Config2) Validate() error { v, isSet := cfg.param[id] if !isSet { 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 } @@ -260,8 +289,6 @@ func (cfg *Config2) Validate() error { // point where the new config can be applied safely. func (cfg *Config2) CheckCompatible(newcfg *Config2, blocknum uint64, time uint64) *ConfigCompatError { // 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 for f := range forks.All() { 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}) } } - slices.SortFunc(forkList, func(f1, f2 forkActivation) int { - 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) - } - }) + slices.SortFunc(forkList, forkActivation.compare) // Iterate checkCompatible to find the lowest conflict. var lasterr *ConfigCompatError @@ -387,3 +403,18 @@ type forkActivation struct { fork forks.Fork 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) + } +} diff --git a/params/config2_test.go b/params/config2_test.go index ad00865a33..55a47000c0 100644 --- a/params/config2_test.go +++ b/params/config2_test.go @@ -18,15 +18,46 @@ package params_test import ( "math/big" + "path/filepath" "reflect" "testing" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params/forks" "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) { type test struct { stored, new *params.Config2 diff --git a/params/testdata/invalid-forkorder.json b/params/testdata/invalid-forkorder.json new file mode 100644 index 0000000000..8f2a5d4683 --- /dev/null +++ b/params/testdata/invalid-forkorder.json @@ -0,0 +1,7 @@ +{ + "config": { + "homesteadBlock": 10, + "eip150Block": 8 + }, + "error": "unsupported fork ordering: Homestead enabled at block 10, but TangerineWhistle enabled at block 8" +} diff --git a/params/testdata/invalid-missingChainID.json b/params/testdata/invalid-missingChainID.json new file mode 100644 index 0000000000..46343adf78 --- /dev/null +++ b/params/testdata/invalid-missingChainID.json @@ -0,0 +1,8 @@ +{ + "config": { + "homesteadBlock": 7, + "eip150Block": 8, + "eip155Block": 9 + }, + "error": "required chain parameter \"chainId\" is not set" +}