Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: do not trigger rewind behaviour when modifying pre-genesis hardfork times / blocks #332

Merged
merged 18 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions core/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
package core

import (
"bytes"
"errors"
"fmt"
"strings"

"math/big"
"math/rand"
"os"
Expand All @@ -38,9 +41,12 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/tracers/logger"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slog"
)

// So we can deterministically seed different blockchains
Expand Down Expand Up @@ -4326,3 +4332,75 @@ func TestEIP3651(t *testing.T) {
t.Fatalf("sender balance incorrect: expected %d, got %d", expected, actual)
}
}

func TestRewindOnConfigChange(t *testing.T) {
geoknee marked this conversation as resolved.
Show resolved Hide resolved
genesisTime := uint64(12)

type testCase struct {
name string
override1 func(*params.ChainConfig)
override2 func(*params.ChainConfig)
expectChainRewind bool
}

tcs := []testCase{
{
name: fmt.Sprintf("CanyonTime changes from 10 to 0 (genesis time is %d)", genesisTime),
geoknee marked this conversation as resolved.
Show resolved Hide resolved
override1: func(c *params.ChainConfig) { c.CanyonTime = uint64ptr(10) },
override2: func(c *params.ChainConfig) { c.CanyonTime = uint64ptr(0) },
expectChainRewind: false,
},
{
name: fmt.Sprintf("RegolithTime changes from 10 to 0 (genesis time is %d)", genesisTime),
override1: func(c *params.ChainConfig) { c.RegolithTime = uint64ptr(10) },
override2: func(c *params.ChainConfig) { c.RegolithTime = uint64ptr(0) },
expectChainRewind: false,
},
{
name: fmt.Sprintf("ShanghaiTime changes from 10 to 0 (genesis time is %d)", genesisTime),
override1: func(c *params.ChainConfig) { c.ShanghaiTime = uint64ptr(10) },
override2: func(c *params.ChainConfig) { c.ShanghaiTime = uint64ptr(0) },
expectChainRewind: false,
},
}

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
var genesis = &Genesis{
BaseFee: big.NewInt(params.InitialBaseFee),
Config: params.AllEthashProtocolChanges,
Timestamp: genesisTime,
}

// Prepare initial config for chain
tc.override1(genesis.Config)
db := rawdb.NewMemoryDatabase()
cc := DefaultCacheConfigWithScheme(rawdb.PathScheme)

// Start blockchain once to store config in DB
blockchain, _ := NewBlockChain(db, cc, genesis, nil, ethash.NewFaker(), vm.Config{}, nil, nil)

// Stop chain after 1 second
<-time.After(1 * time.Second)
geoknee marked this conversation as resolved.
Show resolved Hide resolved
blockchain.Stop()

// Setup a buffer to capture logs
logBuffer := bytes.Buffer{}
log.SetDefault(log.NewLogger(slog.NewTextHandler(&logBuffer, nil)))

// Restart chain with modified genesis config
tc.override2(genesis.Config)
blockchain, _ = NewBlockChain(db, cc, genesis, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
<-time.After(1 * time.Second)
sebastianst marked this conversation as resolved.
Show resolved Hide resolved
blockchain.Stop()

// Inspect logs and assert on contents
rewindTriggered := strings.Contains(logBuffer.String(), "Rewinding chain to upgrade configuration")
if tc.expectChainRewind {
require.True(t, rewindTriggered, "Required log line indicating chain rewind, but did not find one")
} else {
require.False(t, rewindTriggered, "Required NO log line indicating chain rewind, but found one")
}
})
}
}
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)
genesisTimestamp := uint64(0)
if genesis != nil {
genesisTimestamp = genesis.Timestamp
}
compatErr := storedcfg.CheckCompatible(newcfg, head.Number.Uint64(), head.Time, genesisTimestamp)
sebastianst marked this conversation as resolved.
Show resolved Hide resolved
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, 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, genesis uint64) bool {
return (isTimestampForked(s1, head) || isTimestampForked(s2, head)) && !configTimestampEqual(s1, s2) && !isTimestampPreGenesis(s1, genesis) && !isTimestampPreGenesis(s2, genesis)
}

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

// isTimestampForked returns whether a fork scheduled at timestamp s is active
Expand Down
52 changes: 43 additions & 9 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 @@ -27,10 +28,11 @@ import (

func TestCheckCompatible(t *testing.T) {
type test struct {
stored, new *ChainConfig
headBlock uint64
headTimestamp uint64
wantErr *ConfigCompatError
stored, new *ChainConfig
headBlock,
headTimestamp,
genesisTimestamp uint64
wantErr *ConfigCompatError
sebastianst marked this conversation as resolved.
Show resolved Hide resolved
}
tests := []test{
{stored: AllEthashProtocolChanges, new: AllEthashProtocolChanges, headBlock: 0, headTimestamp: 0, wantErr: nil},
Expand Down Expand Up @@ -109,13 +111,45 @@ func TestCheckCompatible(t *testing.T) {
RewindToTime: 9,
},
},
{
stored: &ChainConfig{ShanghaiTime: newUint64(10)},
new: &ChainConfig{ShanghaiTime: newUint64(20)},
headTimestamp: 25,
wantErr: &ConfigCompatError{
What: "Shanghai fork timestamp",
StoredTime: newUint64(10),
NewTime: newUint64(20),
RewindToTime: 9,
},
},
sebastianst marked this conversation as resolved.
Show resolved Hide resolved
{
stored: &ChainConfig{CanyonTime: newUint64(10)},
new: &ChainConfig{CanyonTime: newUint64(20)},
headTimestamp: 25,
genesisTimestamp: 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: 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
Loading