Skip to content

Commit

Permalink
Merge pull request #600 from ava-labs/fix-incompatible-config
Browse files Browse the repository at this point in the history
Fix config handling with apricot timestamp forks
  • Loading branch information
aaronbuchwald authored Feb 3, 2022
2 parents efa5c4c + 7b4a2e4 commit 2a13494
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 64 deletions.
12 changes: 7 additions & 5 deletions core/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,14 @@ func SetupGenesisBlock(db ethdb.Database, genesis *Genesis) (*params.ChainConfig

// Check config compatibility and write the config. Compatibility errors
// are returned to the caller unless we're already at block zero.
height := rawdb.ReadHeaderNumber(db, rawdb.ReadHeadHeaderHash(db))
if height == nil {
return newcfg, fmt.Errorf("missing block number for head header hash")
headBlock := rawdb.ReadHeadBlock(db)
if headBlock == nil {
return newcfg, fmt.Errorf("missing head block")
}
compatErr := storedcfg.CheckCompatible(newcfg, *height)
if compatErr != nil && *height != 0 && compatErr.RewindTo != 0 {
height := headBlock.NumberU64()
timestamp := headBlock.Time()
compatErr := storedcfg.CheckCompatible(newcfg, height, timestamp)
if compatErr != nil && height != 0 && compatErr.RewindTo != 0 {
return newcfg, compatErr
}
rawdb.WriteChainConfig(db, stored, newcfg)
Expand Down
71 changes: 38 additions & 33 deletions core/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (
"math/big"
"reflect"
"testing"
"time"

"github.com/ava-labs/coreth/consensus/dummy"
"github.com/ava-labs/coreth/core/rawdb"
Expand All @@ -57,23 +56,22 @@ func TestGenesisBlockForTesting(t *testing.T) {
}

func TestSetupGenesis(t *testing.T) {
apricotPhase1Config := *params.TestApricotPhase1Config
apricotPhase1Config.ApricotPhase1BlockTimestamp = big.NewInt(100)
var (
customghash = common.HexToHash("0x1099a11e9e454bd3ef31d688cf21936671966407bc330f051d754b5ce401e7ed")
customg = Genesis{
Config: &params.ChainConfig{
HomesteadBlock: big.NewInt(0),
ApricotPhase1BlockTimestamp: big.NewInt(time.Date(2021, time.July, 31, 14, 0, 0, 0, time.UTC).Unix()),
},
Config: &apricotPhase1Config,
Alloc: GenesisAlloc{
{1}: {Balance: big.NewInt(1), Storage: map[common.Hash]common.Hash{{1}: {1}}},
},
}
oldcustomg = customg
)
oldcustomg.Config = &params.ChainConfig{
HomesteadBlock: big.NewInt(0),
ApricotPhase1BlockTimestamp: big.NewInt(time.Date(2021, time.March, 31, 14, 0, 0, 0, time.UTC).Unix()),
}

rollbackApricotPhase1Config := apricotPhase1Config
rollbackApricotPhase1Config.ApricotPhase1BlockTimestamp = big.NewInt(90)
oldcustomg.Config = &rollbackApricotPhase1Config
tests := []struct {
name string
fn func(ethdb.Database) (*params.ChainConfig, common.Hash, error)
Expand Down Expand Up @@ -117,46 +115,53 @@ func TestSetupGenesis(t *testing.T) {
wantConfig: customg.Config,
},
{
name: "incompatible config in DB",
name: "incompatible config for avalanche fork in DB",
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, error) {
// Commit the 'old' genesis block with ApricotPhase1 transition at 90.
// Advance to block #4, past the ApricotPhase1 transition block of customg.
genesis := oldcustomg.MustCommit(db)

bc, _ := NewBlockChain(db, DefaultCacheConfig, oldcustomg.Config, dummy.NewFullFaker(), vm.Config{}, common.Hash{})
defer bc.Stop()
blocks, _, _ := GenerateChain(oldcustomg.Config, genesis, dummy.NewFaker(), db, 4, 10, nil)

blocks, _, _ := GenerateChain(oldcustomg.Config, genesis, dummy.NewFullFaker(), db, 4, 25, nil)
bc.InsertChain(blocks)
bc.CurrentBlock()
// This should return a compatibility error.
return setupGenesisBlock(db, &customg)
},
wantHash: customghash,
wantConfig: customg.Config,
wantErr: &params.ConfigCompatError{
What: "ApricotPhase1 fork block",
StoredConfig: big.NewInt(1617199200),
NewConfig: big.NewInt(1627740000),
RewindTo: 1617199199,
What: "ApricotPhase1 fork block timestamp",
StoredConfig: big.NewInt(90),
NewConfig: big.NewInt(100),
RewindTo: 89,
},
},
}

for _, test := range tests {
db := rawdb.NewMemoryDatabase()
config, hash, err := test.fn(db)
// Check the return values.
if !reflect.DeepEqual(err, test.wantErr) {
spew := spew.ConfigState{DisablePointerAddresses: true, DisableCapacities: true}
t.Errorf("%s: returned error %#v, want %#v", test.name, spew.NewFormatter(err), spew.NewFormatter(test.wantErr))
}
if !reflect.DeepEqual(config, test.wantConfig) {
t.Errorf("%s:\nreturned %v\nwant %v", test.name, config, test.wantConfig)
}
if hash != test.wantHash {
t.Errorf("%s: returned hash %s, want %s", test.name, hash.Hex(), test.wantHash.Hex())
} else if err == nil {
// Check database content.
stored := rawdb.ReadBlock(db, test.wantHash, 0)
if stored.Hash() != test.wantHash {
t.Errorf("%s: block in DB has hash %s, want %s", test.name, stored.Hash(), test.wantHash)
t.Run(test.name, func(t *testing.T) {
db := rawdb.NewMemoryDatabase()
config, hash, err := test.fn(db)
// Check the return values.
if !reflect.DeepEqual(err, test.wantErr) {
spew := spew.ConfigState{DisablePointerAddresses: true, DisableCapacities: true}
t.Errorf("returned error %#v, want %#v", spew.NewFormatter(err), spew.NewFormatter(test.wantErr))
}
}
if !reflect.DeepEqual(config, test.wantConfig) {
t.Errorf("returned %v\nwant %v", config, test.wantConfig)
}
if hash != test.wantHash {
t.Errorf("returned hash %s, want %s", hash.Hex(), test.wantHash.Hex())
} else if err == nil {
// Check database content.
stored := rawdb.ReadBlock(db, test.wantHash, 0)
if stored.Hash() != test.wantHash {
t.Errorf("block in DB has hash %s, want %s", stored.Hash(), test.wantHash)
}
}
})
}
}
53 changes: 27 additions & 26 deletions params/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,13 +272,14 @@ func (c *ChainConfig) IsApricotPhase5(blockTimestamp *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) *ConfigCompatError {
func (c *ChainConfig) CheckCompatible(newcfg *ChainConfig, height uint64, timestamp uint64) *ConfigCompatError {
bhead := new(big.Int).SetUint64(height)
bheadTimestamp := new(big.Int).SetUint64(timestamp)

// Iterate checkCompatible to find the lowest conflict.
var lasterr *ConfigCompatError
for {
err := c.checkCompatible(newcfg, bhead)
err := c.checkCompatible(newcfg, bhead, bheadTimestamp)
if err == nil || (lasterr != nil && err.RewindTo == lasterr.RewindTo) {
break
}
Expand Down Expand Up @@ -369,61 +370,61 @@ func (c *ChainConfig) CheckConfigForkOrder() error {
return nil
}

func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, head *big.Int) *ConfigCompatError {
if isForkIncompatible(c.HomesteadBlock, newcfg.HomesteadBlock, head) {
func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, headHeight *big.Int, headTimestamp *big.Int) *ConfigCompatError {
if isForkIncompatible(c.HomesteadBlock, newcfg.HomesteadBlock, headHeight) {
return newCompatError("Homestead fork block", c.HomesteadBlock, newcfg.HomesteadBlock)
}
if isForkIncompatible(c.DAOForkBlock, newcfg.DAOForkBlock, head) {
if isForkIncompatible(c.DAOForkBlock, newcfg.DAOForkBlock, headHeight) {
return newCompatError("DAO fork block", c.DAOForkBlock, newcfg.DAOForkBlock)
}
if c.IsDAOFork(head) && c.DAOForkSupport != newcfg.DAOForkSupport {
if c.IsDAOFork(headHeight) && c.DAOForkSupport != newcfg.DAOForkSupport {
return newCompatError("DAO fork support flag", c.DAOForkBlock, newcfg.DAOForkBlock)
}
if isForkIncompatible(c.EIP150Block, newcfg.EIP150Block, head) {
if isForkIncompatible(c.EIP150Block, newcfg.EIP150Block, headHeight) {
return newCompatError("EIP150 fork block", c.EIP150Block, newcfg.EIP150Block)
}
if isForkIncompatible(c.EIP155Block, newcfg.EIP155Block, head) {
if isForkIncompatible(c.EIP155Block, newcfg.EIP155Block, headHeight) {
return newCompatError("EIP155 fork block", c.EIP155Block, newcfg.EIP155Block)
}
if isForkIncompatible(c.EIP158Block, newcfg.EIP158Block, head) {
if isForkIncompatible(c.EIP158Block, newcfg.EIP158Block, headHeight) {
return newCompatError("EIP158 fork block", c.EIP158Block, newcfg.EIP158Block)
}
if c.IsEIP158(head) && !configNumEqual(c.ChainID, newcfg.ChainID) {
if c.IsEIP158(headHeight) && !configNumEqual(c.ChainID, newcfg.ChainID) {
return newCompatError("EIP158 chain ID", c.EIP158Block, newcfg.EIP158Block)
}
if isForkIncompatible(c.ByzantiumBlock, newcfg.ByzantiumBlock, head) {
if isForkIncompatible(c.ByzantiumBlock, newcfg.ByzantiumBlock, headHeight) {
return newCompatError("Byzantium fork block", c.ByzantiumBlock, newcfg.ByzantiumBlock)
}
if isForkIncompatible(c.ConstantinopleBlock, newcfg.ConstantinopleBlock, head) {
if isForkIncompatible(c.ConstantinopleBlock, newcfg.ConstantinopleBlock, headHeight) {
return newCompatError("Constantinople fork block", c.ConstantinopleBlock, newcfg.ConstantinopleBlock)
}
if isForkIncompatible(c.PetersburgBlock, newcfg.PetersburgBlock, head) {
if isForkIncompatible(c.PetersburgBlock, newcfg.PetersburgBlock, headHeight) {
// the only case where we allow Petersburg to be set in the past is if it is equal to Constantinople
// mainly to satisfy fork ordering requirements which state that Petersburg fork be set if Constantinople fork is set
if isForkIncompatible(c.ConstantinopleBlock, newcfg.PetersburgBlock, head) {
if isForkIncompatible(c.ConstantinopleBlock, newcfg.PetersburgBlock, headHeight) {
return newCompatError("Petersburg fork block", c.PetersburgBlock, newcfg.PetersburgBlock)
}
}
if isForkIncompatible(c.IstanbulBlock, newcfg.IstanbulBlock, head) {
if isForkIncompatible(c.IstanbulBlock, newcfg.IstanbulBlock, headHeight) {
return newCompatError("Istanbul fork block", c.IstanbulBlock, newcfg.IstanbulBlock)
}
if isForkIncompatible(c.MuirGlacierBlock, newcfg.MuirGlacierBlock, head) {
if isForkIncompatible(c.MuirGlacierBlock, newcfg.MuirGlacierBlock, headHeight) {
return newCompatError("Muir Glacier fork block", c.MuirGlacierBlock, newcfg.MuirGlacierBlock)
}
if !configNumEqual(c.ApricotPhase1BlockTimestamp, newcfg.ApricotPhase1BlockTimestamp) {
return newCompatError("ApricotPhase1 fork block", c.ApricotPhase1BlockTimestamp, newcfg.ApricotPhase1BlockTimestamp)
if isForkIncompatible(c.ApricotPhase1BlockTimestamp, newcfg.ApricotPhase1BlockTimestamp, headTimestamp) {
return newCompatError("ApricotPhase1 fork block timestamp", c.ApricotPhase1BlockTimestamp, newcfg.ApricotPhase1BlockTimestamp)
}
if !configNumEqual(c.ApricotPhase2BlockTimestamp, newcfg.ApricotPhase2BlockTimestamp) {
return newCompatError("ApricotPhase2 fork block", c.ApricotPhase2BlockTimestamp, newcfg.ApricotPhase2BlockTimestamp)
if isForkIncompatible(c.ApricotPhase2BlockTimestamp, newcfg.ApricotPhase2BlockTimestamp, headTimestamp) {
return newCompatError("ApricotPhase2 fork block timestamp", c.ApricotPhase2BlockTimestamp, newcfg.ApricotPhase2BlockTimestamp)
}
if !configNumEqual(c.ApricotPhase3BlockTimestamp, newcfg.ApricotPhase3BlockTimestamp) {
return newCompatError("ApricotPhase3 fork block", c.ApricotPhase3BlockTimestamp, newcfg.ApricotPhase3BlockTimestamp)
if isForkIncompatible(c.ApricotPhase3BlockTimestamp, newcfg.ApricotPhase3BlockTimestamp, headTimestamp) {
return newCompatError("ApricotPhase3 fork block timestamp", c.ApricotPhase3BlockTimestamp, newcfg.ApricotPhase3BlockTimestamp)
}
if !configNumEqual(c.ApricotPhase4BlockTimestamp, newcfg.ApricotPhase4BlockTimestamp) {
return newCompatError("ApricotPhase4 fork block", c.ApricotPhase4BlockTimestamp, newcfg.ApricotPhase4BlockTimestamp)
if isForkIncompatible(c.ApricotPhase4BlockTimestamp, newcfg.ApricotPhase4BlockTimestamp, headTimestamp) {
return newCompatError("ApricotPhase4 fork block timestamp", c.ApricotPhase4BlockTimestamp, newcfg.ApricotPhase4BlockTimestamp)
}
if !configNumEqual(c.ApricotPhase5BlockTimestamp, newcfg.ApricotPhase5BlockTimestamp) {
return newCompatError("ApricotPhase5 fork block", c.ApricotPhase5BlockTimestamp, newcfg.ApricotPhase5BlockTimestamp)
if isForkIncompatible(c.ApricotPhase5BlockTimestamp, newcfg.ApricotPhase5BlockTimestamp, headTimestamp) {
return newCompatError("ApricotPhase5 fork block timestamp", c.ApricotPhase5BlockTimestamp, newcfg.ApricotPhase5BlockTimestamp)
}

return nil
Expand Down
138 changes: 138 additions & 0 deletions params/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// (c) 2019-2020, Ava Labs, Inc.
//
// This file is a derived work, based on the go-ethereum library whose original
// notices appear below.
//
// It is distributed under a license compatible with the licensing terms of the
// original code from which it is derived.
//
// Much love to the original authors for their work.
// **********
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package params

import (
"math/big"
"reflect"
"testing"
)

func TestCheckCompatible(t *testing.T) {
type test struct {
stored, new *ChainConfig
headHeight, headTimestamp uint64
wantErr *ConfigCompatError
}
tests := []test{
{stored: TestChainConfig, new: TestChainConfig, headHeight: 0, headTimestamp: 0, wantErr: nil},
{stored: TestChainConfig, new: TestChainConfig, headHeight: 100, headTimestamp: 1000, wantErr: nil},
{
stored: &ChainConfig{EIP150Block: big.NewInt(10)},
new: &ChainConfig{EIP150Block: big.NewInt(20)},
headHeight: 9,
headTimestamp: 90,
wantErr: nil,
},
{
stored: TestChainConfig,
new: &ChainConfig{HomesteadBlock: nil},
headHeight: 3,
headTimestamp: 30,
wantErr: &ConfigCompatError{
What: "Homestead fork block",
StoredConfig: big.NewInt(0),
NewConfig: nil,
RewindTo: 0,
},
},
{
stored: TestChainConfig,
new: &ChainConfig{HomesteadBlock: big.NewInt(1)},
headHeight: 3,
headTimestamp: 30,
wantErr: &ConfigCompatError{
What: "Homestead fork block",
StoredConfig: big.NewInt(0),
NewConfig: big.NewInt(1),
RewindTo: 0,
},
},
{
stored: &ChainConfig{HomesteadBlock: big.NewInt(30), EIP150Block: big.NewInt(10)},
new: &ChainConfig{HomesteadBlock: big.NewInt(25), EIP150Block: big.NewInt(20)},
headHeight: 25,
headTimestamp: 250,
wantErr: &ConfigCompatError{
What: "EIP150 fork block",
StoredConfig: big.NewInt(10),
NewConfig: big.NewInt(20),
RewindTo: 9,
},
},
{
stored: &ChainConfig{ConstantinopleBlock: big.NewInt(30)},
new: &ChainConfig{ConstantinopleBlock: big.NewInt(30), PetersburgBlock: big.NewInt(30)},
headHeight: 40,
headTimestamp: 400,
wantErr: nil,
},
{
stored: &ChainConfig{ConstantinopleBlock: big.NewInt(30)},
new: &ChainConfig{ConstantinopleBlock: big.NewInt(30), PetersburgBlock: big.NewInt(31)},
headHeight: 40,
headTimestamp: 400,
wantErr: &ConfigCompatError{
What: "Petersburg fork block",
StoredConfig: nil,
NewConfig: big.NewInt(31),
RewindTo: 30,
},
},
{
stored: TestChainConfig,
new: TestApricotPhase4Config,
headHeight: 0,
headTimestamp: 0,
wantErr: &ConfigCompatError{
What: "ApricotPhase5 fork block timestamp",
StoredConfig: big.NewInt(0),
NewConfig: nil,
RewindTo: 0,
},
},
{
stored: TestChainConfig,
new: TestApricotPhase4Config,
headHeight: 10,
headTimestamp: 100,
wantErr: &ConfigCompatError{
What: "ApricotPhase5 fork block timestamp",
StoredConfig: big.NewInt(0),
NewConfig: nil,
RewindTo: 0,
},
},
}

for _, test := range tests {
err := test.stored.CheckCompatible(test.new, test.headHeight, test.headTimestamp)
if !reflect.DeepEqual(err, test.wantErr) {
t.Errorf("error mismatch:\nstored: %v\nnew: %v\nheadHeight: %v\nerr: %v\nwant: %v", test.stored, test.new, test.headHeight, err, test.wantErr)
}
}
}

0 comments on commit 2a13494

Please sign in to comment.