Skip to content

Commit

Permalink
core: do not trigger rewind behaviour when modifying pre-genesis hard…
Browse files Browse the repository at this point in the history
…fork times / blocks (#332)

* skeetch out a test for config change pre genesis

* expand test to multiple test cases

* thread genesisTimestamp down to ChainConfig.CheckCompatible

* add failing test cases

* refactor test

* add all optimism forks to checkCompatible

This is brittle in general and longer term we need a solution that requires less boiler plate.

* fix: pregenesis fork changes are not incompatible

* fix integration test

* lint

* remove genesis number from checkCompatible

it should always be 0

* allow for nil genesis in SetupGenesisBlockWithOverride

* move test to its own file

* use shorter test names

* remove duplicate test case

* reorder struct fields to reduce diff

* CheckCompatible accepts a *uint64 genesisTimestamp

This allows us to handle the case where the genesis itself (and therefore the genesis timestamp) is undefined. We handle that by declaring all time stamps as POST genesis, so they can't pass the compatibilty check via the new escape hatch.

* remove optimism test

* remove newline
  • Loading branch information
geoknee authored Jun 14, 2024
1 parent 7c28198 commit a34e174
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 16 deletions.
6 changes: 5 additions & 1 deletion core/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,11 @@ func SetupGenesisBlockWithOverride(db ethdb.Database, triedb *triedb.Database, g
if head == nil {
return newcfg, stored, errors.New("missing head header")
}
compatErr := storedcfg.CheckCompatible(newcfg, head.Number.Uint64(), head.Time)
var genesisTimestamp *uint64
if genesis != nil {
genesisTimestamp = &genesis.Timestamp
}
compatErr := storedcfg.CheckCompatible(newcfg, head.Number.Uint64(), head.Time, genesisTimestamp)
if compatErr != nil && ((head.Number.Uint64() != 0 && compatErr.RewindToBlock != 0) || (head.Time != 0 && compatErr.RewindToTime != 0)) {
return newcfg, stored, compatErr
}
Expand Down
45 changes: 35 additions & 10 deletions params/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -713,15 +713,15 @@ func (c *ChainConfig) IsOptimismPreBedrock(num *big.Int) bool {

// CheckCompatible checks whether scheduled fork transitions have been imported
// with a mismatching chain configuration.
func (c *ChainConfig) CheckCompatible(newcfg *ChainConfig, height uint64, time uint64) *ConfigCompatError {
func (c *ChainConfig) CheckCompatible(newcfg *ChainConfig, height, time uint64, genesisTimestamp *uint64) *ConfigCompatError {
var (
bhead = new(big.Int).SetUint64(height)
btime = time
)
// Iterate checkCompatible to find the lowest conflict.
var lasterr *ConfigCompatError
for {
err := c.checkCompatible(newcfg, bhead, btime)
err := c.checkCompatible(newcfg, bhead, btime, genesisTimestamp)
if err == nil || (lasterr != nil && err.RewindToBlock == lasterr.RewindToBlock && err.RewindToTime == lasterr.RewindToTime) {
break
}
Expand Down Expand Up @@ -804,7 +804,7 @@ func (c *ChainConfig) CheckConfigForkOrder() error {
return nil
}

func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, headNumber *big.Int, headTimestamp uint64) *ConfigCompatError {
func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, headNumber *big.Int, headTimestamp uint64, genesisTimestamp *uint64) *ConfigCompatError {
if isForkBlockIncompatible(c.HomesteadBlock, newcfg.HomesteadBlock, headNumber) {
return newBlockCompatError("Homestead fork block", c.HomesteadBlock, newcfg.HomesteadBlock)
}
Expand Down Expand Up @@ -860,18 +860,36 @@ func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, headNumber *big.Int,
if isForkBlockIncompatible(c.MergeNetsplitBlock, newcfg.MergeNetsplitBlock, headNumber) {
return newBlockCompatError("Merge netsplit fork block", c.MergeNetsplitBlock, newcfg.MergeNetsplitBlock)
}
if isForkTimestampIncompatible(c.ShanghaiTime, newcfg.ShanghaiTime, headTimestamp) {
if isForkTimestampIncompatible(c.ShanghaiTime, newcfg.ShanghaiTime, headTimestamp, genesisTimestamp) {
return newTimestampCompatError("Shanghai fork timestamp", c.ShanghaiTime, newcfg.ShanghaiTime)
}
if isForkTimestampIncompatible(c.CancunTime, newcfg.CancunTime, headTimestamp) {
if isForkTimestampIncompatible(c.CancunTime, newcfg.CancunTime, headTimestamp, genesisTimestamp) {
return newTimestampCompatError("Cancun fork timestamp", c.CancunTime, newcfg.CancunTime)
}
if isForkTimestampIncompatible(c.PragueTime, newcfg.PragueTime, headTimestamp) {
if isForkTimestampIncompatible(c.PragueTime, newcfg.PragueTime, headTimestamp, genesisTimestamp) {
return newTimestampCompatError("Prague fork timestamp", c.PragueTime, newcfg.PragueTime)
}
if isForkTimestampIncompatible(c.VerkleTime, newcfg.VerkleTime, headTimestamp) {
if isForkTimestampIncompatible(c.VerkleTime, newcfg.VerkleTime, headTimestamp, genesisTimestamp) {
return newTimestampCompatError("Verkle fork timestamp", c.VerkleTime, newcfg.VerkleTime)
}
if isForkBlockIncompatible(c.BedrockBlock, newcfg.BedrockBlock, headNumber) {
return newBlockCompatError("Bedrock fork block", c.BedrockBlock, newcfg.BedrockBlock)
}
if isForkTimestampIncompatible(c.RegolithTime, newcfg.RegolithTime, headTimestamp, genesisTimestamp) {
return newTimestampCompatError("Regolith fork timestamp", c.RegolithTime, newcfg.RegolithTime)
}
if isForkTimestampIncompatible(c.CanyonTime, newcfg.CanyonTime, headTimestamp, genesisTimestamp) {
return newTimestampCompatError("Canyon fork timestamp", c.CanyonTime, newcfg.CanyonTime)
}
if isForkTimestampIncompatible(c.EcotoneTime, newcfg.EcotoneTime, headTimestamp, genesisTimestamp) {
return newTimestampCompatError("Ecotone fork timestamp", c.EcotoneTime, newcfg.EcotoneTime)
}
if isForkTimestampIncompatible(c.FjordTime, newcfg.FjordTime, headTimestamp, genesisTimestamp) {
return newTimestampCompatError("Fjord fork timestamp", c.FjordTime, newcfg.FjordTime)
}
if isForkTimestampIncompatible(c.InteropTime, newcfg.InteropTime, headTimestamp, genesisTimestamp) {
return newTimestampCompatError("Interop fork timestamp", c.InteropTime, newcfg.InteropTime)
}
return nil
}

Expand Down Expand Up @@ -913,7 +931,7 @@ func (c *ChainConfig) LatestFork(time uint64) forks.Fork {
}

// isForkBlockIncompatible returns true if a fork scheduled at block s1 cannot be
// rescheduled to block s2 because head is already past the fork.
// rescheduled to block s2 because head is already past the fork and the fork was scheduled after genesis
func isForkBlockIncompatible(s1, s2, head *big.Int) bool {
return (isBlockForked(s1, head) || isBlockForked(s2, head)) && !configBlockEqual(s1, s2)
}
Expand All @@ -940,8 +958,15 @@ func configBlockEqual(x, y *big.Int) bool {

// isForkTimestampIncompatible returns true if a fork scheduled at timestamp s1
// cannot be rescheduled to timestamp s2 because head is already past the fork.
func isForkTimestampIncompatible(s1, s2 *uint64, head uint64) bool {
return (isTimestampForked(s1, head) || isTimestampForked(s2, head)) && !configTimestampEqual(s1, s2)
func isForkTimestampIncompatible(s1, s2 *uint64, head uint64, genesis *uint64) bool {
return (isTimestampForked(s1, head) || isTimestampForked(s2, head)) && !configTimestampEqual(s1, s2) && !isTimestampPreGenesis(s1, genesis) && !isTimestampPreGenesis(s2, genesis)
}

func isTimestampPreGenesis(s, genesis *uint64) bool {
if s == nil || genesis == nil {
return false
}
return *s < *genesis
}

// isTimestampForked returns whether a fork scheduled at timestamp s is active
Expand Down
46 changes: 41 additions & 5 deletions params/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package params

import (
"fmt"
"math/big"
"reflect"
"testing"
Expand All @@ -31,6 +32,8 @@ func TestCheckCompatible(t *testing.T) {
headBlock uint64
headTimestamp uint64
wantErr *ConfigCompatError

genesisTimestamp *uint64
}
tests := []test{
{stored: AllEthashProtocolChanges, new: AllEthashProtocolChanges, headBlock: 0, headTimestamp: 0, wantErr: nil},
Expand Down Expand Up @@ -109,13 +112,46 @@ func TestCheckCompatible(t *testing.T) {
RewindToTime: 9,
},
},
{
stored: &ChainConfig{CanyonTime: newUint64(10)},
new: &ChainConfig{CanyonTime: newUint64(20)},
headTimestamp: 25,
genesisTimestamp: newUint64(2),
wantErr: &ConfigCompatError{
What: "Canyon fork timestamp",
StoredTime: newUint64(10),
NewTime: newUint64(20),
RewindToTime: 9,
},
},
{
stored: &ChainConfig{CanyonTime: newUint64(10)},
new: &ChainConfig{CanyonTime: newUint64(20)},
headTimestamp: 25,
genesisTimestamp: nil,
wantErr: &ConfigCompatError{
What: "Canyon fork timestamp",
StoredTime: newUint64(10),
NewTime: newUint64(20),
RewindToTime: 9,
},
},
{
stored: &ChainConfig{CanyonTime: newUint64(10)},
new: &ChainConfig{CanyonTime: newUint64(20)},
headTimestamp: 25,
genesisTimestamp: newUint64(24),
wantErr: nil,
},
}

for _, test := range tests {
err := test.stored.CheckCompatible(test.new, test.headBlock, test.headTimestamp)
if !reflect.DeepEqual(err, test.wantErr) {
t.Errorf("error mismatch:\nstored: %v\nnew: %v\nheadBlock: %v\nheadTimestamp: %v\nerr: %v\nwant: %v", test.stored, test.new, test.headBlock, test.headTimestamp, err, test.wantErr)
}
for i, test := range tests {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
err := test.stored.CheckCompatible(test.new, test.headBlock, test.headTimestamp, test.genesisTimestamp)
if !reflect.DeepEqual(err, test.wantErr) {
t.Errorf("error mismatch:\nstored: %v\nnew: %v\nheadBlock: %v\nheadTimestamp: %v\nerr: %v\nwant: %v", test.stored, test.new, test.headBlock, test.headTimestamp, err, test.wantErr)
}
})
}
}

Expand Down

0 comments on commit a34e174

Please sign in to comment.