diff --git a/core/state_processor.go b/core/state_processor.go index 2634f83640..8ddc8c4f0d 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -37,6 +37,7 @@ import ( "github.com/ava-labs/subnet-evm/params" "github.com/ava-labs/subnet-evm/precompile/contract" "github.com/ava-labs/subnet-evm/precompile/modules" + "github.com/ava-labs/subnet-evm/stateupgrade" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" @@ -80,8 +81,8 @@ func (p *StateProcessor) Process(block *types.Block, parent *types.Header, state timestamp = new(big.Int).SetUint64(header.Time) ) - // Configure any stateful precompiles that should go into effect during this block. - err := ApplyPrecompileActivations(p.config, new(big.Int).SetUint64(parent.Time), block, statedb) + // Configure any upgrades that should go into effect during this block. + err := ApplyUpgrades(p.config, new(big.Int).SetUint64(parent.Time), block, statedb) if err != nil { log.Error("failed to configure precompiles processing block", "hash", block.Hash(), "number", block.NumberU64(), "timestamp", block.Time(), "err", err) return nil, nil, 0, err @@ -174,10 +175,8 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo // ApplyPrecompileActivations checks if any of the precompiles specified by the chain config are enabled or disabled by the block // transition from [parentTimestamp] to the timestamp set in [blockContext]. If this is the case, it calls [Configure] // to apply the necessary state transitions for the upgrade. -// This function is called: -// - within genesis setup to configure the starting state for precompiles enabled at genesis, -// - during block processing to update the state before processing the given block. -// - during block producing to apply the precompile upgrades before producing the block. +// This function is called within genesis setup to configure the starting state for precompiles enabled at genesis. +// In block processing and building, ApplyUpgrades is called instead which also applies state upgrades. func ApplyPrecompileActivations(c *params.ChainConfig, parentTimestamp *big.Int, blockContext contract.BlockContext, statedb *state.StateDB) error { blockTimestamp := blockContext.Timestamp() // Note: RegisteredModules returns precompiles sorted by module addresses. @@ -218,3 +217,30 @@ func ApplyPrecompileActivations(c *params.ChainConfig, parentTimestamp *big.Int, } return nil } + +// applyStateUpgrades checks if any of the state upgrades specified by the chain config are activated by the block +// transition from [parentTimestamp] to the timestamp set in [header]. If this is the case, it calls [Configure] +// to apply the necessary state transitions for the upgrade. +func applyStateUpgrades(c *params.ChainConfig, parentTimestamp *big.Int, blockContext contract.BlockContext, statedb *state.StateDB) error { + // Apply state upgrades + for _, upgrade := range c.GetActivatingStateUpgrades(parentTimestamp, blockContext.Timestamp(), c.StateUpgrades) { + log.Info("Applying state upgrade", "blockNumber", blockContext.Number(), "upgrade", upgrade) + if err := stateupgrade.Configure(&upgrade, c, statedb, blockContext); err != nil { + return fmt.Errorf("could not configure state upgrade: %w", err) + } + } + return nil +} + +// ApplyUpgrades checks if any of the precompile or state upgrades specified by the chain config are activated by the block +// transition from [parentTimestamp] to the timestamp set in [header]. If this is the case, it calls [Configure] +// to apply the necessary state transitions for the upgrade. +// This function is called: +// - in block processing to update the state when processing a block. +// - in the miner to apply the state upgrades when producing a block. +func ApplyUpgrades(c *params.ChainConfig, parentTimestamp *big.Int, blockContext contract.BlockContext, statedb *state.StateDB) error { + if err := ApplyPrecompileActivations(c, parentTimestamp, blockContext, statedb); err != nil { + return err + } + return applyStateUpgrades(c, parentTimestamp, blockContext, statedb) +} diff --git a/go.mod b/go.mod index e4bae89122..524fbdfb94 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.19 require ( github.com/VictoriaMetrics/fastcache v1.10.0 - github.com/ava-labs/avalanchego v1.9.10 + github.com/ava-labs/avalanchego v1.9.11 github.com/cespare/cp v0.1.0 github.com/davecgh/go-spew v1.1.1 github.com/deckarep/golang-set v1.8.0 diff --git a/go.sum b/go.sum index cfcf45fdc5..e471048922 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0= -github.com/ava-labs/avalanchego v1.9.10 h1:IQYUruncY3yuKLwfbGXGslydTJQcjzLMtZuW8g5wQOY= -github.com/ava-labs/avalanchego v1.9.10/go.mod h1:nNc+4JCIJMaEt2xRmeMVAUyQwDIap7RvnMrfWD2Tpo8= +github.com/ava-labs/avalanchego v1.9.11 h1:5hXHJMvErfaolWD7Hw9gZaVylck2shBaV/2NTHA0BfA= +github.com/ava-labs/avalanchego v1.9.11/go.mod h1:nNc+4JCIJMaEt2xRmeMVAUyQwDIap7RvnMrfWD2Tpo8= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= diff --git a/miner/worker.go b/miner/worker.go index 26a9c31168..041ca12a90 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -185,8 +185,8 @@ func (w *worker) commitNewWork() (*types.Block, error) { if err != nil { return nil, fmt.Errorf("failed to create new current environment: %w", err) } - // Configure any stateful precompiles that should go into effect during this block. - err = core.ApplyPrecompileActivations(w.chainConfig, new(big.Int).SetUint64(parent.Time()), types.NewBlockWithHeader(header), env.state) + // Configure any upgrades that should go into effect during this block. + err = core.ApplyUpgrades(w.chainConfig, new(big.Int).SetUint64(parent.Time()), types.NewBlockWithHeader(header), env.state) if err != nil { log.Error("failed to configure precompiles mining new block", "parent", parent.Hash(), "number", header.Number, "timestamp", header.Time, "err", err) return nil, err diff --git a/params/config.go b/params/config.go index af5010f3a2..b583ec00dd 100644 --- a/params/config.go +++ b/params/config.go @@ -142,6 +142,9 @@ type UpgradeConfig struct { // forks must be present or upgradeBytes will be rejected. NetworkUpgrades *NetworkUpgrades `json:"networkUpgrades,omitempty"` + // Config for modifying state as a network upgrade. + StateUpgrades []StateUpgrade `json:"stateUpgrades,omitempty"` + // Config for enabling and disabling precompiles as network upgrades. PrecompileUpgrades []PrecompileUpgrade `json:"precompileUpgrades,omitempty"` } @@ -325,7 +328,7 @@ func (c *ChainConfig) IsSubnetEVM(blockTimestamp *big.Int) bool { // IsPrecompileEnabled returns whether precompile with [address] is enabled at [blockTimestamp]. func (c *ChainConfig) IsPrecompileEnabled(address common.Address, blockTimestamp *big.Int) bool { - config := c.GetActivePrecompileConfig(address, blockTimestamp) + config := c.getActivePrecompileConfig(address, blockTimestamp) return config != nil && !config.IsDisabled() } @@ -359,6 +362,11 @@ func (c *ChainConfig) Verify() error { return fmt.Errorf("invalid precompile upgrades: %w", err) } + // Verify the state upgrades are internally consistent given the existing chainConfig. + if err := c.verifyStateUpgrades(); err != nil { + return fmt.Errorf("invalid state upgrades: %w", err) + } + return nil } @@ -493,6 +501,11 @@ func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, lastHeight *big.Int, return err } + // Check that the state upgrades on the new config are compatible with the existing state upgrade config. + if err := c.CheckStateUpgradesCompatible(newcfg.StateUpgrades, lastTimestamp); err != nil { + return err + } + // TODO verify that the fee config is fully compatible between [c] and [newcfg]. return nil } @@ -598,7 +611,7 @@ func (c *ChainConfig) AvalancheRules(blockNum, blockTimestamp *big.Int) Rules { // Initialize the stateful precompiles that should be enabled at [blockTimestamp]. rules.ActivePrecompiles = make(map[common.Address]precompileconfig.Config) for _, module := range modules.RegisteredModules() { - if config := c.GetActivePrecompileConfig(module.Address, blockTimestamp); config != nil && !config.IsDisabled() { + if config := c.getActivePrecompileConfig(module.Address, blockTimestamp); config != nil && !config.IsDisabled() { rules.ActivePrecompiles[module.Address] = config } } diff --git a/params/precompile_config_test.go b/params/precompile_config_test.go index ec81ecaf36..82054ec024 100644 --- a/params/precompile_config_test.go +++ b/params/precompile_config_test.go @@ -179,7 +179,7 @@ func TestVerifyPrecompileUpgrades(t *testing.T) { Config: txallowlist.NewDisableConfig(big.NewInt(1)), }, }, - expectedError: " config block timestamp (1) <= previous timestamp (1) of same key", + expectedError: "config block timestamp (1) <= previous timestamp (1) of same key", }, } for _, tt := range tests { @@ -269,16 +269,16 @@ func TestGetPrecompileConfig(t *testing.T) { deployerallowlist.ConfigKey: deployerallowlist.NewConfig(big.NewInt(10), nil, nil), } - deployerConfig := config.GetActivePrecompileConfig(deployerallowlist.ContractAddress, big.NewInt(0)) + deployerConfig := config.getActivePrecompileConfig(deployerallowlist.ContractAddress, big.NewInt(0)) require.Nil(deployerConfig) - deployerConfig = config.GetActivePrecompileConfig(deployerallowlist.ContractAddress, big.NewInt(10)) + deployerConfig = config.getActivePrecompileConfig(deployerallowlist.ContractAddress, big.NewInt(10)) require.NotNil(deployerConfig) - deployerConfig = config.GetActivePrecompileConfig(deployerallowlist.ContractAddress, big.NewInt(11)) + deployerConfig = config.getActivePrecompileConfig(deployerallowlist.ContractAddress, big.NewInt(11)) require.NotNil(deployerConfig) - txAllowListConfig := config.GetActivePrecompileConfig(txallowlist.ContractAddress, big.NewInt(0)) + txAllowListConfig := config.getActivePrecompileConfig(txallowlist.ContractAddress, big.NewInt(0)) require.Nil(txAllowListConfig) } diff --git a/params/precompile_upgrade.go b/params/precompile_upgrade.go index c3c072ed87..e8346174bf 100644 --- a/params/precompile_upgrade.go +++ b/params/precompile_upgrade.go @@ -149,9 +149,29 @@ func (c *ChainConfig) verifyPrecompileUpgrades() error { return nil } -// GetActivePrecompileConfig returns the most recent precompile config corresponding to [address]. +// verifyStateUpgrades checks [c.StateUpgrades] is well formed: +// - the specified blockTimestamps must monotonically increase +func (c *ChainConfig) verifyStateUpgrades() error { + var previousUpgradeTimestamp *big.Int + for i, upgrade := range c.StateUpgrades { + upgradeTimestamp := upgrade.BlockTimestamp + // Verify the upgrade's timestamp is greater than 0 (to avoid confusion with genesis). + if upgradeTimestamp.Cmp(common.Big0) <= 0 { + return fmt.Errorf("StateUpgrade[%d]: config block timestamp (%v) must be greater than 0", i, upgradeTimestamp) + } + + // Verify specified timestamps are strictly monotonically increasing. + if previousUpgradeTimestamp != nil && upgradeTimestamp.Cmp(previousUpgradeTimestamp) <= 0 { + return fmt.Errorf("StateUpgrade[%d]: config block timestamp (%v) <= previous timestamp (%v)", i, upgradeTimestamp, previousUpgradeTimestamp) + } + previousUpgradeTimestamp = upgradeTimestamp + } + return nil +} + +// getActivePrecompileConfig returns the most recent precompile config corresponding to [address]. // If none have occurred, returns nil. -func (c *ChainConfig) GetActivePrecompileConfig(address common.Address, blockTimestamp *big.Int) precompileconfig.Config { +func (c *ChainConfig) getActivePrecompileConfig(address common.Address, blockTimestamp *big.Int) precompileconfig.Config { configs := c.GetActivatingPrecompileConfigs(address, nil, blockTimestamp, c.PrecompileUpgrades) if len(configs) == 0 { return nil @@ -159,8 +179,8 @@ func (c *ChainConfig) GetActivePrecompileConfig(address common.Address, blockTim return configs[len(configs)-1] // return the most recent config } -// GetActivatingPrecompileConfigs returns all upgrades configured to activate during the state transition from a block with timestamp [from] -// to a block with timestamp [to]. +// GetActivatingPrecompileConfigs returns all precompile upgrades configured to activate during the +// state transition from a block with timestamp [from] to a block with timestamp [to]. func (c *ChainConfig) GetActivatingPrecompileConfigs(address common.Address, from *big.Int, to *big.Int, upgrades []PrecompileUpgrade) []precompileconfig.Config { // Get key from address. module, ok := modules.GetPrecompileModuleByAddress(address) @@ -188,6 +208,18 @@ func (c *ChainConfig) GetActivatingPrecompileConfigs(address common.Address, fro return configs } +// GetActivatingStateUpgrades returns all state upgrades configured to activate during the +// state transition from a block with timestamp [from] to a block with timestamp [to]. +func (c *ChainConfig) GetActivatingStateUpgrades(from *big.Int, to *big.Int, upgrades []StateUpgrade) []StateUpgrade { + activating := make([]StateUpgrade, 0) + for _, upgrade := range upgrades { + if utils.IsForkTransition(upgrade.BlockTimestamp, from, to) { + activating = append(activating, upgrade) + } + } + return activating +} + // CheckPrecompilesCompatible checks if [precompileUpgrades] are compatible with [c] at [headTimestamp]. // Returns a ConfigCompatError if upgrades already activated at [headTimestamp] are missing from // [precompileUpgrades]. Upgrades not already activated may be modified or absent from [precompileUpgrades]. @@ -246,11 +278,49 @@ func (c *ChainConfig) checkPrecompileCompatible(address common.Address, precompi return nil } +// CheckStateUpgradesCompatible checks if [stateUpgrades] are compatible with [c] at [headTimestamp]. +func (c *ChainConfig) CheckStateUpgradesCompatible(stateUpgrades []StateUpgrade, lastTimestamp *big.Int) *ConfigCompatError { + // All active upgrades (from nil to [lastTimestamp]) must match. + activeUpgrades := c.GetActivatingStateUpgrades(nil, lastTimestamp, c.StateUpgrades) + newUpgrades := c.GetActivatingStateUpgrades(nil, lastTimestamp, stateUpgrades) + + // Check activated upgrades are still present. + for i, upgrade := range activeUpgrades { + if len(newUpgrades) <= i { + // missing upgrade + return newCompatError( + fmt.Sprintf("missing StateUpgrade[%d]", i), + upgrade.BlockTimestamp, + nil, + ) + } + // All upgrades that have activated must be identical. + if !upgrade.Equal(&newUpgrades[i]) { + return newCompatError( + fmt.Sprintf("StateUpgrade[%d]", i), + upgrade.BlockTimestamp, + newUpgrades[i].BlockTimestamp, + ) + } + } + // then, make sure newUpgrades does not have additional upgrades + // that are already activated. (cannot perform retroactive upgrade) + if len(newUpgrades) > len(activeUpgrades) { + return newCompatError( + fmt.Sprintf("cannot retroactively enable StateUpgrade[%d]", len(activeUpgrades)), + nil, + newUpgrades[len(activeUpgrades)].BlockTimestamp, // this indexes to the first element in newUpgrades after the end of activeUpgrades + ) + } + + return nil +} + // EnabledStatefulPrecompiles returns current stateful precompile configs that are enabled at [blockTimestamp]. func (c *ChainConfig) EnabledStatefulPrecompiles(blockTimestamp *big.Int) Precompiles { statefulPrecompileConfigs := make(Precompiles) for _, module := range modules.RegisteredModules() { - if config := c.GetActivePrecompileConfig(module.Address, blockTimestamp); config != nil && !config.IsDisabled() { + if config := c.getActivePrecompileConfig(module.Address, blockTimestamp); config != nil && !config.IsDisabled() { statefulPrecompileConfigs[module.ConfigKey] = config } } diff --git a/params/precompile_upgrade_test.go b/params/precompile_upgrade_test.go index 54078aad3b..17ef49a3c5 100644 --- a/params/precompile_upgrade_test.go +++ b/params/precompile_upgrade_test.go @@ -78,13 +78,7 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { deployerallowlist.ConfigKey: deployerallowlist.NewConfig(big.NewInt(10), admins, nil), } - type test struct { - configs []*UpgradeConfig - startTimestamps []*big.Int - expectedErrorString string - } - - tests := map[string]test{ + tests := map[string]upgradeCompatibilityTest{ "disable and re-enable": { startTimestamps: []*big.Int{big.NewInt(5)}, configs: []*UpgradeConfig{ @@ -252,32 +246,39 @@ func TestCheckCompatibleUpgradeConfigs(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { - // make a local copy of the chainConfig - chainConfig := chainConfig + tt.run(t, chainConfig) + }) + } +} - // apply all the upgrade bytes specified in order - for i, upgrade := range tt.configs { - newCfg := chainConfig - newCfg.UpgradeConfig = *upgrade +type upgradeCompatibilityTest struct { + configs []*UpgradeConfig + startTimestamps []*big.Int + expectedErrorString string +} - err := chainConfig.checkCompatible(&newCfg, nil, tt.startTimestamps[i]) +func (tt *upgradeCompatibilityTest) run(t *testing.T, chainConfig ChainConfig) { + // apply all the upgrade bytes specified in order + for i, upgrade := range tt.configs { + newCfg := chainConfig + newCfg.UpgradeConfig = *upgrade - // if this is not the final upgradeBytes, continue applying - // the next upgradeBytes. (only check the result on the last apply) - if i != len(tt.configs)-1 { - if err != nil { - t.Fatalf("expecting ApplyUpgradeBytes call %d to return nil, got %s", i+1, err) - } - chainConfig = newCfg - continue - } + err := chainConfig.checkCompatible(&newCfg, nil, tt.startTimestamps[i]) - if tt.expectedErrorString != "" { - require.ErrorContains(t, err, tt.expectedErrorString) - } else { - require.Nil(t, err) - } + // if this is not the final upgradeBytes, continue applying + // the next upgradeBytes. (only check the result on the last apply) + if i != len(tt.configs)-1 { + if err != nil { + t.Fatalf("expecting checkCompatible call %d to return nil, got %s", i+1, err) } - }) + chainConfig = newCfg + continue + } + + if tt.expectedErrorString != "" { + require.ErrorContains(t, err, tt.expectedErrorString) + } else { + require.Nil(t, err) + } } } diff --git a/params/state_upgrade.go b/params/state_upgrade.go new file mode 100644 index 0000000000..c03ad0d637 --- /dev/null +++ b/params/state_upgrade.go @@ -0,0 +1,34 @@ +// (c) 2023 Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package params + +import ( + "math/big" + "reflect" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" +) + +// StateUpgrade describes the modifications to be made to the state during +// a state upgrade. +type StateUpgrade struct { + BlockTimestamp *big.Int `json:"blockTimestamp,omitempty"` + + // map from account address to the modification to be made to the account. + StateUpgradeAccounts map[common.Address]StateUpgradeAccount `json:"accounts"` +} + +// StateUpgradeAccount describes the modifications to be made to an account during +// a state upgrade. +type StateUpgradeAccount struct { + Code hexutil.Bytes `json:"code,omitempty"` + Storage map[common.Hash]common.Hash `json:"storage,omitempty"` + BalanceChange *math.HexOrDecimal256 `json:"balanceChange,omitempty"` +} + +func (s *StateUpgrade) Equal(other *StateUpgrade) bool { + return reflect.DeepEqual(s, other) +} diff --git a/params/stateupgrade_config_test.go b/params/stateupgrade_config_test.go new file mode 100644 index 0000000000..4a7a85fe81 --- /dev/null +++ b/params/stateupgrade_config_test.go @@ -0,0 +1,186 @@ +// (c) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package params + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/stretchr/testify/require" +) + +func TestVerifyStateUpgrades(t *testing.T) { + modifiedAccounts := map[common.Address]StateUpgradeAccount{ + {1}: { + BalanceChange: (*math.HexOrDecimal256)(common.Big1), + }, + } + tests := []struct { + name string + upgrades []StateUpgrade + expectedError string + }{ + { + name: "valid upgrade", + upgrades: []StateUpgrade{ + {BlockTimestamp: common.Big1, StateUpgradeAccounts: modifiedAccounts}, + {BlockTimestamp: common.Big2, StateUpgradeAccounts: modifiedAccounts}, + }, + }, + { + name: "upgrade block timestamp is not strictly increasing", + upgrades: []StateUpgrade{ + {BlockTimestamp: common.Big1, StateUpgradeAccounts: modifiedAccounts}, + {BlockTimestamp: common.Big1, StateUpgradeAccounts: modifiedAccounts}, + }, + expectedError: "config block timestamp (1) <= previous timestamp (1)", + }, + { + name: "upgrade block timestamp decreases", + upgrades: []StateUpgrade{ + {BlockTimestamp: common.Big2, StateUpgradeAccounts: modifiedAccounts}, + {BlockTimestamp: common.Big1, StateUpgradeAccounts: modifiedAccounts}, + }, + expectedError: "config block timestamp (1) <= previous timestamp (2)", + }, + { + name: "upgrade block timestamp is zero", + upgrades: []StateUpgrade{ + {BlockTimestamp: common.Big0, StateUpgradeAccounts: modifiedAccounts}, + }, + expectedError: "config block timestamp (0) must be greater than 0", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + baseConfig := *SubnetEVMDefaultChainConfig + config := &baseConfig + config.StateUpgrades = tt.upgrades + + err := config.Verify() + if tt.expectedError == "" { + require.NoError(err) + } else { + require.ErrorContains(err, tt.expectedError) + } + }) + } +} + +func TestCheckCompatibleStateUpgradeConfigs(t *testing.T) { + chainConfig := *TestChainConfig + stateUpgrade := map[common.Address]StateUpgradeAccount{ + {1}: {BalanceChange: (*math.HexOrDecimal256)(common.Big1)}, + } + differentStateUpgrade := map[common.Address]StateUpgradeAccount{ + {2}: {BalanceChange: (*math.HexOrDecimal256)(common.Big1)}, + } + + tests := map[string]upgradeCompatibilityTest{ + "reschedule upgrade before it happens": { + startTimestamps: []*big.Int{big.NewInt(5), big.NewInt(6)}, + configs: []*UpgradeConfig{ + { + StateUpgrades: []StateUpgrade{ + {BlockTimestamp: big.NewInt(6), StateUpgradeAccounts: stateUpgrade}, + }, + }, + { + StateUpgrades: []StateUpgrade{ + {BlockTimestamp: big.NewInt(6), StateUpgradeAccounts: stateUpgrade}, + }, + }, + }, + }, + "modify upgrade after it happens not allowed": { + expectedErrorString: "mismatching StateUpgrade", + startTimestamps: []*big.Int{big.NewInt(5), big.NewInt(8)}, + configs: []*UpgradeConfig{ + { + StateUpgrades: []StateUpgrade{ + {BlockTimestamp: big.NewInt(6), StateUpgradeAccounts: stateUpgrade}, + {BlockTimestamp: big.NewInt(7), StateUpgradeAccounts: stateUpgrade}, + }, + }, + { + StateUpgrades: []StateUpgrade{ + {BlockTimestamp: big.NewInt(6), StateUpgradeAccounts: stateUpgrade}, + {BlockTimestamp: big.NewInt(7), StateUpgradeAccounts: differentStateUpgrade}, + }, + }, + }, + }, + "cancel upgrade before it happens": { + startTimestamps: []*big.Int{big.NewInt(5), big.NewInt(6)}, + configs: []*UpgradeConfig{ + { + StateUpgrades: []StateUpgrade{ + {BlockTimestamp: big.NewInt(6), StateUpgradeAccounts: stateUpgrade}, + {BlockTimestamp: big.NewInt(7), StateUpgradeAccounts: stateUpgrade}, + }, + }, + { + StateUpgrades: []StateUpgrade{ + {BlockTimestamp: big.NewInt(6), StateUpgradeAccounts: stateUpgrade}, + }, + }, + }, + }, + "retroactively enabling upgrades is not allowed": { + expectedErrorString: "cannot retroactively enable StateUpgrade", + startTimestamps: []*big.Int{big.NewInt(6)}, + configs: []*UpgradeConfig{ + { + StateUpgrades: []StateUpgrade{ + {BlockTimestamp: big.NewInt(5), StateUpgradeAccounts: stateUpgrade}, + }, + }, + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tt.run(t, chainConfig) + }) + } +} + +func TestUnmarshalStateUpgradeJSON(t *testing.T) { + jsonBytes := []byte( + `{ + "stateUpgrades": [ + { + "blockTimestamp": 1677608400, + "accounts": { + "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC": { + "balanceChange": "100" + } + } + } + ] + }`, + ) + + upgradeConfig := UpgradeConfig{ + StateUpgrades: []StateUpgrade{ + { + BlockTimestamp: big.NewInt(1677608400), + StateUpgradeAccounts: map[common.Address]StateUpgradeAccount{ + common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"): { + BalanceChange: (*math.HexOrDecimal256)(big.NewInt(100)), + }, + }, + }, + }, + } + var unmarshaledConfig UpgradeConfig + err := json.Unmarshal(jsonBytes, &unmarshaledConfig) + require.NoError(t, err) + require.Equal(t, upgradeConfig, unmarshaledConfig) +} diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index 29a6a52937..36fcb2157d 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -17,17 +17,6 @@ import ( "testing" "time" - "github.com/ava-labs/subnet-evm/commontype" - "github.com/ava-labs/subnet-evm/internal/ethapi" - "github.com/ava-labs/subnet-evm/metrics" - "github.com/ava-labs/subnet-evm/plugin/evm/message" - "github.com/ava-labs/subnet-evm/precompile/allowlist" - "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" - "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" - "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" - "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" - "github.com/ava-labs/subnet-evm/trie" - "github.com/ava-labs/subnet-evm/vmerrs" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" @@ -41,6 +30,7 @@ import ( "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/snowman" + commonEng "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/snow/validators" avalancheConstants "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/formatting" @@ -48,18 +38,26 @@ import ( "github.com/ava-labs/avalanchego/version" "github.com/ava-labs/avalanchego/vms/components/chain" - engCommon "github.com/ava-labs/avalanchego/snow/engine/common" - + "github.com/ava-labs/subnet-evm/accounts/abi" + accountKeystore "github.com/ava-labs/subnet-evm/accounts/keystore" + "github.com/ava-labs/subnet-evm/commontype" "github.com/ava-labs/subnet-evm/consensus/dummy" "github.com/ava-labs/subnet-evm/constants" "github.com/ava-labs/subnet-evm/core" "github.com/ava-labs/subnet-evm/core/types" "github.com/ava-labs/subnet-evm/eth" + "github.com/ava-labs/subnet-evm/internal/ethapi" + "github.com/ava-labs/subnet-evm/metrics" "github.com/ava-labs/subnet-evm/params" + "github.com/ava-labs/subnet-evm/plugin/evm/message" + "github.com/ava-labs/subnet-evm/precompile/allowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/deployerallowlist" + "github.com/ava-labs/subnet-evm/precompile/contracts/feemanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/rewardmanager" + "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" "github.com/ava-labs/subnet-evm/rpc" - - "github.com/ava-labs/subnet-evm/accounts/abi" - accountKeystore "github.com/ava-labs/subnet-evm/accounts/keystore" + "github.com/ava-labs/subnet-evm/trie" + "github.com/ava-labs/subnet-evm/vmerrs" ) var ( @@ -150,7 +148,7 @@ func setupGenesis(t *testing.T, ) (*snow.Context, manager.Manager, []byte, - chan engCommon.Message, + chan commonEng.Message, ) { if len(genesisJSON) == 0 { genesisJSON = genesisJSONLatest @@ -178,7 +176,7 @@ func setupGenesis(t *testing.T, } ctx.Keystore = userKeystore.NewBlockchainKeyStore(ctx.ChainID) - issuer := make(chan engCommon.Message, 1) + issuer := make(chan commonEng.Message, 1) prefixedDBManager := baseDBManager.NewPrefixDBManager([]byte{1}) return ctx, prefixedDBManager, genesisBytes, issuer } @@ -192,16 +190,16 @@ func GenesisVM(t *testing.T, genesisJSON string, configJSON string, upgradeJSON string, -) (chan engCommon.Message, +) (chan commonEng.Message, *VM, manager.Manager, - *engCommon.SenderTest, + *commonEng.SenderTest, ) { vm := &VM{} ctx, dbManager, genesisBytes, issuer := setupGenesis(t, genesisJSON) - appSender := &engCommon.SenderTest{T: t} + appSender := &commonEng.SenderTest{T: t} appSender.CantSendAppGossip = true appSender.SendAppGossipF = func(context.Context, []byte) error { return nil } - if err := vm.Initialize( + err := vm.Initialize( context.Background(), ctx, dbManager, @@ -209,11 +207,10 @@ func GenesisVM(t *testing.T, []byte(upgradeJSON), []byte(configJSON), issuer, - []*engCommon.Fx{}, + []*commonEng.Fx{}, appSender, - ); err != nil { - t.Fatal(err) - } + ) + require.NoError(t, err, "error initializing GenesisVM") if finishBootstrapping { require.NoError(t, vm.SetState(context.Background(), snow.Bootstrapping)) @@ -345,7 +342,7 @@ func TestVMUpgrades(t *testing.T) { } } -func issueAndAccept(t *testing.T, issuer <-chan engCommon.Message, vm *VM) snowman.Block { +func issueAndAccept(t *testing.T, issuer <-chan commonEng.Message, vm *VM) snowman.Block { t.Helper() <-issuer @@ -416,7 +413,7 @@ func TestSubnetEVMUpgradeRequiredAtGenesis(t *testing.T) { []byte(""), []byte(test.configJSON), issuer, - []*engCommon.Fx{}, + []*commonEng.Fx{}, nil, ) @@ -534,7 +531,7 @@ func TestBuildEthTxBlock(t *testing.T) { []byte(""), []byte("{\"pruning-enabled\":true}"), issuer, - []*engCommon.Fx{}, + []*commonEng.Fx{}, nil, ); err != nil { t.Fatal(err) @@ -2031,7 +2028,7 @@ func TestConfigureLogLevel(t *testing.T) { t.Run(test.name, func(t *testing.T) { vm := &VM{} ctx, dbManager, genesisBytes, issuer := setupGenesis(t, test.genesisJSON) - appSender := &engCommon.SenderTest{T: t} + appSender := &commonEng.SenderTest{T: t} appSender.CantSendAppGossip = true appSender.SendAppGossipF = func(context.Context, []byte) error { return nil } err := vm.Initialize( @@ -2042,7 +2039,7 @@ func TestConfigureLogLevel(t *testing.T) { []byte(""), []byte(test.logConfig), issuer, - []*engCommon.Fx{}, + []*commonEng.Fx{}, appSender, ) if len(test.expectedErr) == 0 && err != nil { @@ -2992,12 +2989,12 @@ func TestSkipChainConfigCheckCompatible(t *testing.T) { require.NoError(t, err) // this will not be allowed - err = reinitVM.Initialize(context.Background(), vm.ctx, dbManager, genesisWithUpgradeBytes, []byte{}, []byte{}, issuer, []*engCommon.Fx{}, appSender) + err = reinitVM.Initialize(context.Background(), vm.ctx, dbManager, genesisWithUpgradeBytes, []byte{}, []byte{}, issuer, []*commonEng.Fx{}, appSender) require.ErrorContains(t, err, "mismatching SubnetEVM fork block timestamp in database") // try again with skip-upgrade-check config := []byte("{\"skip-upgrade-check\": true}") - err = reinitVM.Initialize(context.Background(), vm.ctx, dbManager, genesisWithUpgradeBytes, []byte{}, config, issuer, []*engCommon.Fx{}, appSender) + err = reinitVM.Initialize(context.Background(), vm.ctx, dbManager, genesisWithUpgradeBytes, []byte{}, config, issuer, []*commonEng.Fx{}, appSender) require.NoError(t, err) require.NoError(t, reinitVM.Shutdown(context.Background())) } diff --git a/plugin/evm/vm_upgrade_bytes_test.go b/plugin/evm/vm_upgrade_bytes_test.go index 4880f0e348..9124945487 100644 --- a/plugin/evm/vm_upgrade_bytes_test.go +++ b/plugin/evm/vm_upgrade_bytes_test.go @@ -7,12 +7,13 @@ import ( "context" "encoding/json" "errors" + "fmt" "math/big" "testing" "time" "github.com/ava-labs/avalanchego/snow" - "github.com/ava-labs/avalanchego/snow/engine/common" + commonEng "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/vms/components/chain" "github.com/ava-labs/subnet-evm/core" "github.com/ava-labs/subnet-evm/core/types" @@ -20,7 +21,12 @@ import ( "github.com/ava-labs/subnet-evm/params" "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" "github.com/ava-labs/subnet-evm/vmerrs" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestVMUpgradeBytesPrecompile(t *testing.T) { @@ -83,7 +89,7 @@ func TestVMUpgradeBytesPrecompile(t *testing.T) { // restart the vm ctx := NewContext() if err := vm.Initialize( - context.Background(), ctx, dbManager, []byte(genesisJSONSubnetEVM), upgradeBytesJSON, []byte{}, issuer, []*common.Fx{}, appSender, + context.Background(), ctx, dbManager, []byte(genesisJSONSubnetEVM), upgradeBytesJSON, []byte{}, issuer, []*commonEng.Fx{}, appSender, ); err != nil { t.Fatal(err) } @@ -192,7 +198,7 @@ func TestVMUpgradeBytesNetworkUpgrades(t *testing.T) { } // VM should not start again without proper upgrade bytes. - err = vm.Initialize(context.Background(), vm.ctx, dbManager, []byte(genesisJSONPreSubnetEVM), []byte{}, []byte{}, issuer, []*common.Fx{}, appSender) + err = vm.Initialize(context.Background(), vm.ctx, dbManager, []byte(genesisJSONPreSubnetEVM), []byte{}, []byte{}, issuer, []*commonEng.Fx{}, appSender) assert.ErrorContains(t, err, "mismatching SubnetEVM fork block timestamp in database") // VM should not start if fork is moved back @@ -201,7 +207,7 @@ func TestVMUpgradeBytesNetworkUpgrades(t *testing.T) { if err != nil { t.Fatalf("could not marshal upgradeConfig to json: %s", err) } - err = vm.Initialize(context.Background(), vm.ctx, dbManager, []byte(genesisJSONPreSubnetEVM), upgradeBytesJSON, []byte{}, issuer, []*common.Fx{}, appSender) + err = vm.Initialize(context.Background(), vm.ctx, dbManager, []byte(genesisJSONPreSubnetEVM), upgradeBytesJSON, []byte{}, issuer, []*commonEng.Fx{}, appSender) assert.ErrorContains(t, err, "mismatching SubnetEVM fork block timestamp in database") // VM should not start if fork is moved forward @@ -210,7 +216,7 @@ func TestVMUpgradeBytesNetworkUpgrades(t *testing.T) { if err != nil { t.Fatalf("could not marshal upgradeConfig to json: %s", err) } - err = vm.Initialize(context.Background(), vm.ctx, dbManager, []byte(genesisJSONPreSubnetEVM), upgradeBytesJSON, []byte{}, issuer, []*common.Fx{}, appSender) + err = vm.Initialize(context.Background(), vm.ctx, dbManager, []byte(genesisJSONPreSubnetEVM), upgradeBytesJSON, []byte{}, issuer, []*commonEng.Fx{}, appSender) assert.ErrorContains(t, err, "mismatching SubnetEVM fork block timestamp in database") } @@ -270,3 +276,113 @@ func TestVMUpgradeBytesNetworkUpgradesWithGenesis(t *testing.T) { t.Fatal(err) } } + +func mustMarshal(t *testing.T, v interface{}) string { + b, err := json.Marshal(v) + require.NoError(t, err) + return string(b) +} + +func TestVMStateUpgrade(t *testing.T) { + // modify genesis to add a key to the state + genesis := &core.Genesis{} + err := json.Unmarshal([]byte(genesisJSONSubnetEVM), genesis) + require.NoError(t, err) + genesisAccount, ok := genesis.Alloc[testEthAddrs[0]] + require.True(t, ok) + storageKey := common.HexToHash("0x1234") + genesisAccount.Storage = map[common.Hash]common.Hash{storageKey: common.HexToHash("0x5555")} + genesisCode, err := hexutil.Decode("0xabcd") + require.NoError(t, err) + genesisAccount.Code = genesisCode + genesisAccount.Nonce = 2 // set to a non-zero value to test that it is preserved + genesis.Alloc[testEthAddrs[0]] = genesisAccount // have to assign this back to the map for changes to take effect. + genesisStr := mustMarshal(t, genesis) + + upgradedCodeStr := "0xdeadbeef" // this code will be applied during the upgrade + upgradedCode, err := hexutil.Decode(upgradedCodeStr) + // This modification will be applied to an existing account + genesisAccountUpgrade := ¶ms.StateUpgradeAccount{ + BalanceChange: (*math.HexOrDecimal256)(big.NewInt(100)), + Storage: map[common.Hash]common.Hash{storageKey: {}}, + Code: upgradedCode, + } + + // This modification will be applied to a new account + newAccount := common.Address{42} + require.NoError(t, err) + newAccountUpgrade := ¶ms.StateUpgradeAccount{ + BalanceChange: (*math.HexOrDecimal256)(big.NewInt(100)), + Storage: map[common.Hash]common.Hash{storageKey: common.HexToHash("0x6666")}, + Code: upgradedCode, + } + + upgradeTimestamp := time.Unix(10, 0) // arbitrary timestamp to perform the network upgrade + upgradeBytesJSON := fmt.Sprintf( + `{ + "stateUpgrades": [ + { + "blockTimestamp": %d, + "accounts": { + "%s": %s, + "%s": %s + } + } + ] + }`, + upgradeTimestamp.Unix(), + testEthAddrs[0].Hex(), + mustMarshal(t, genesisAccountUpgrade), + newAccount.Hex(), + mustMarshal(t, newAccountUpgrade), + ) + require.Contains(t, upgradeBytesJSON, upgradedCodeStr) + + // initialize the VM with these upgrade bytes + issuer, vm, _, _ := GenesisVM(t, true, genesisStr, "", upgradeBytesJSON) + defer func() { require.NoError(t, vm.Shutdown(context.Background())) }() + + // Verify the new account doesn't exist yet + genesisState, err := vm.blockChain.State() + require.NoError(t, err) + require.Equal(t, common.Big0, genesisState.GetBalance(newAccount)) + + // Advance the chain to the upgrade time + vm.clock.Set(upgradeTimestamp) + + // Submit a successful (unrelated) transaction, so we can build a block + // in this tx, testEthAddrs[1] sends 1 wei to itself. + tx0 := types.NewTransaction(uint64(0), testEthAddrs[1], big.NewInt(1), 21000, big.NewInt(testMinGasPrice), nil) + signedTx0, err := types.SignTx(tx0, types.NewEIP155Signer(vm.chainConfig.ChainID), testKeys[1]) + require.NoError(t, err) + + errs := vm.txPool.AddRemotesSync([]*types.Transaction{signedTx0}) + require.NoError(t, errs[0], "Failed to add tx") + + blk := issueAndAccept(t, issuer, vm) + require.NotNil(t, blk) + require.EqualValues(t, 1, blk.Height()) + + // Verify the state upgrade was applied + state, err := vm.blockChain.State() + require.NoError(t, err) + + // Existing account + expectedGenesisAccountBalance := new(big.Int).Add( + genesisAccount.Balance, + (*big.Int)(genesisAccountUpgrade.BalanceChange), + ) + require.Equal(t, state.GetBalance(testEthAddrs[0]), expectedGenesisAccountBalance) + require.Equal(t, state.GetState(testEthAddrs[0], storageKey), genesisAccountUpgrade.Storage[storageKey]) + require.Equal(t, state.GetCode(testEthAddrs[0]), upgradedCode) + require.Equal(t, state.GetCodeHash(testEthAddrs[0]), crypto.Keccak256Hash(upgradedCode)) + require.Equal(t, state.GetNonce(testEthAddrs[0]), genesisAccount.Nonce) // Nonce should be preserved since it was non-zero + + // New account + expectedNewAccountBalance := newAccountUpgrade.BalanceChange + require.Equal(t, state.GetBalance(newAccount), (*big.Int)(expectedNewAccountBalance)) + require.Equal(t, state.GetCode(newAccount), upgradedCode) + require.Equal(t, state.GetCodeHash(newAccount), crypto.Keccak256Hash(upgradedCode)) + require.Equal(t, state.GetNonce(newAccount), uint64(1)) // Nonce should be set to 1 when code is set if nonce was 0 + require.Equal(t, state.GetState(newAccount, storageKey), newAccountUpgrade.Storage[storageKey]) +} diff --git a/precompile/contract/interfaces.go b/precompile/contract/interfaces.go index a8402bed75..b9c431272d 100644 --- a/precompile/contract/interfaces.go +++ b/precompile/contract/interfaces.go @@ -34,14 +34,11 @@ type StateDB interface { GetState(common.Address, common.Hash) common.Hash SetState(common.Address, common.Hash, common.Hash) - SetCode(common.Address, []byte) - SetNonce(common.Address, uint64) GetNonce(common.Address) uint64 GetBalance(common.Address) *big.Int AddBalance(common.Address, *big.Int) - SubBalance(common.Address, *big.Int) CreateAccount(common.Address) Exist(common.Address) bool diff --git a/scripts/versions.sh b/scripts/versions.sh index 43df281829..926aca9df0 100644 --- a/scripts/versions.sh +++ b/scripts/versions.sh @@ -3,7 +3,7 @@ # Set up the versions to be used - populate ENV variables only if they are not already populated SUBNET_EVM_VERSION=${SUBNET_EVM_VERSION:-'v0.4.12'} # Don't export them as they're used in the context of other calls -AVALANCHEGO_VERSION=${AVALANCHE_VERSION:-'v1.9.10'} +AVALANCHEGO_VERSION=${AVALANCHE_VERSION:-'v1.9.11'} GINKGO_VERSION=${GINKGO_VERSION:-'v2.2.0'} # This won't be used, but it's here to make code syncs easier diff --git a/stateupgrade/interfaces.go b/stateupgrade/interfaces.go new file mode 100644 index 0000000000..f667980487 --- /dev/null +++ b/stateupgrade/interfaces.go @@ -0,0 +1,35 @@ +// (c) 2023 Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package stateupgrade + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +// StateDB is the interface for accessing EVM state in state upgrades +type StateDB interface { + SetState(common.Address, common.Hash, common.Hash) + SetCode(common.Address, []byte) + AddBalance(common.Address, *big.Int) + + GetNonce(common.Address) uint64 + SetNonce(common.Address, uint64) + + CreateAccount(common.Address) + Exist(common.Address) bool +} + +// ChainContext defines an interface that provides information to a state upgrade +// about the chain configuration. +type ChainContext interface { + IsEIP158(num *big.Int) bool +} + +// BlockContext defines an interface that provides information to a state upgrade +// about the block that activates the upgrade. +type BlockContext interface { + Number() *big.Int +} diff --git a/stateupgrade/state_upgrade.go b/stateupgrade/state_upgrade.go new file mode 100644 index 0000000000..682e7cd493 --- /dev/null +++ b/stateupgrade/state_upgrade.go @@ -0,0 +1,46 @@ +// (c) 2023 Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package stateupgrade + +import ( + "math/big" + + "github.com/ava-labs/subnet-evm/params" + "github.com/ethereum/go-ethereum/common" +) + +// Configure applies the state upgrade to the state. +func Configure(stateUpgrade *params.StateUpgrade, chainConfig ChainContext, state StateDB, blockContext BlockContext) error { + isEIP158 := chainConfig.IsEIP158(blockContext.Number()) + for account, upgrade := range stateUpgrade.StateUpgradeAccounts { + if err := upgradeAccount(account, upgrade, state, isEIP158); err != nil { + return err + } + } + return nil +} + +// upgradeAccount applies the state upgrade to the given account. +func upgradeAccount(account common.Address, upgrade params.StateUpgradeAccount, state StateDB, isEIP158 bool) error { + // Create the account if it does not exist + if !state.Exist(account) { + state.CreateAccount(account) + } + + if upgrade.BalanceChange != nil { + state.AddBalance(account, (*big.Int)(upgrade.BalanceChange)) + } + if len(upgrade.Code) != 0 { + // if the nonce is 0, set the nonce to 1 as we would when deploying a contract at + // the address. + if isEIP158 && state.GetNonce(account) == 0 { + state.SetNonce(account, 1) + } + state.SetCode(account, upgrade.Code) + } + for key, value := range upgrade.Storage { + state.SetState(account, key, value) + } + return nil +}