diff --git a/core/state_processor_ext.go b/core/state_processor_ext.go index ac5e3659a0..c5553491b3 100644 --- a/core/state_processor_ext.go +++ b/core/state_processor_ext.go @@ -73,16 +73,13 @@ func ApplyPrecompileActivations(c *params.ChainConfig, parentTimestamp *uint64, // 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 *uint64, blockContext contract.ConfigurationBlockContext, statedb *state.StateDB) error { +func applyStateUpgrades(c *params.ChainConfig, parentTimestamp *uint64, blockContext contract.ConfigurationBlockContext, statedb *state.StateDB) { // Apply state upgrades configExtra := params.GetExtra(c) for _, upgrade := range configExtra.GetActivatingStateUpgrades(parentTimestamp, blockContext.Timestamp(), configExtra.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) - } + stateupgrade.Configure(&upgrade, c, statedb, blockContext) } - return nil } // ApplyUpgrades checks if any of the precompile or state upgrades specified by the chain config are activated by the block @@ -95,7 +92,8 @@ func ApplyUpgrades(c *params.ChainConfig, parentTimestamp *uint64, blockContext if err := ApplyPrecompileActivations(c, parentTimestamp, blockContext, statedb); err != nil { return err } - return applyStateUpgrades(c, parentTimestamp, blockContext, statedb) + applyStateUpgrades(c, parentTimestamp, blockContext, statedb) + return nil } // BlockContext implements [contract.ConfigurationBlockContext]. diff --git a/eth/tracers/api_extra_test.go b/eth/tracers/api_extra_test.go index 75884af31b..37643988ba 100644 --- a/eth/tracers/api_extra_test.go +++ b/eth/tracers/api_extra_test.go @@ -29,6 +29,7 @@ import ( "github.com/ava-labs/subnet-evm/plugin/evm/customtypes" "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" "github.com/ava-labs/subnet-evm/rpc" + "github.com/ava-labs/subnet-evm/stateupgrade" ethparams "github.com/ava-labs/libevm/params" ) @@ -356,9 +357,9 @@ func testTraceCallWithOverridesStateUpgrade(t *testing.T, scheme string) { activateStateUpgradeBlock := uint64(2) // assumes gap is 10 sec activateStateUpgradeTime := activateStateUpgradeBlock * 10 - activateStateUpgradeConfig := extras.StateUpgrade{ + activateStateUpgradeConfig := stateupgrade.StateUpgrade{ BlockTimestamp: &activateStateUpgradeTime, - StateUpgradeAccounts: map[common.Address]extras.StateUpgradeAccount{ + StateUpgradeAccounts: map[common.Address]stateupgrade.StateUpgradeAccount{ accounts[2].addr: { // deplete all balance BalanceChange: (*math.HexOrDecimal256)(new(big.Int).Neg(big.NewInt(5 * params.Ether))), @@ -366,7 +367,7 @@ func testTraceCallWithOverridesStateUpgrade(t *testing.T, scheme string) { }, } - params.GetExtra(genesis.Config).StateUpgrades = []extras.StateUpgrade{ + params.GetExtra(genesis.Config).StateUpgrades = []stateupgrade.StateUpgrade{ activateStateUpgradeConfig, } genBlocks := 3 diff --git a/params/extras/config.go b/params/extras/config.go index 7bc9156945..e381c61072 100644 --- a/params/extras/config.go +++ b/params/extras/config.go @@ -14,6 +14,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/ava-labs/subnet-evm/commontype" + "github.com/ava-labs/subnet-evm/stateupgrade" "github.com/ava-labs/subnet-evm/utils" ethparams "github.com/ava-labs/libevm/params" @@ -87,7 +88,7 @@ type UpgradeConfig struct { NetworkUpgradeOverrides *NetworkUpgrades `json:"networkUpgradeOverrides,omitempty"` // Config for modifying state as a network upgrade. - StateUpgrades []StateUpgrade `json:"stateUpgrades,omitempty"` + StateUpgrades []stateupgrade.StateUpgrade `json:"stateUpgrades,omitempty"` // Config for enabling and disabling precompiles as network upgrades. PrecompileUpgrades []PrecompileUpgrade `json:"precompileUpgrades,omitempty"` @@ -319,7 +320,7 @@ 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 { + if err := stateupgrade.VerifyStateUpgrades(c.StateUpgrades); err != nil { return fmt.Errorf("invalid state upgrades: %w", err) } diff --git a/params/extras/config_test.go b/params/extras/config_test.go index c91508bae1..412113888b 100644 --- a/params/extras/config_test.go +++ b/params/extras/config_test.go @@ -16,6 +16,7 @@ import ( "github.com/ava-labs/subnet-evm/commontype" "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" + "github.com/ava-labs/subnet-evm/stateupgrade" ) func pointer[T any](v T) *T { return &v } @@ -61,10 +62,10 @@ $`, NetworkUpgradeOverrides: &NetworkUpgrades{ SubnetEVMTimestamp: pointer(uint64(13)), }, - StateUpgrades: []StateUpgrade{ + StateUpgrades: []stateupgrade.StateUpgrade{ { BlockTimestamp: pointer(uint64(14)), - StateUpgradeAccounts: map[common.Address]StateUpgradeAccount{ + StateUpgradeAccounts: map[common.Address]stateupgrade.StateUpgradeAccount{ {15}: { Code: []byte{16}, }, @@ -135,7 +136,7 @@ func TestChainConfigVerify(t *testing.T) { config: ChainConfig{ FeeConfig: validFeeConfig, UpgradeConfig: UpgradeConfig{ - StateUpgrades: []StateUpgrade{ + StateUpgrades: []stateupgrade.StateUpgrade{ {BlockTimestamp: nil}, }, }, diff --git a/params/extras/state_upgrade.go b/params/extras/state_upgrade.go index 530e0b8046..3b63af6f95 100644 --- a/params/extras/state_upgrade.go +++ b/params/extras/state_upgrade.go @@ -5,63 +5,16 @@ package extras import ( "fmt" - "reflect" - "github.com/ava-labs/libevm/common" - "github.com/ava-labs/libevm/common/hexutil" - "github.com/ava-labs/libevm/common/math" + "github.com/ava-labs/subnet-evm/stateupgrade" ethparams "github.com/ava-labs/libevm/params" ) -// StateUpgrade describes the modifications to be made to the state during -// a state upgrade. -type StateUpgrade struct { - BlockTimestamp *uint64 `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) -} - -// verifyStateUpgrades checks [c.StateUpgrades] is well formed: -// - the specified blockTimestamps must monotonically increase -func (c *ChainConfig) verifyStateUpgrades() error { - var previousUpgradeTimestamp *uint64 - for i, upgrade := range c.StateUpgrades { - upgradeTimestamp := upgrade.BlockTimestamp - if upgradeTimestamp == nil { - return fmt.Errorf("StateUpgrade[%d]: config block timestamp cannot be nil ", i) - } - // Verify the upgrade's timestamp is equal 0 (to avoid confusion with genesis). - if *upgradeTimestamp == 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 <= *previousUpgradeTimestamp { - return fmt.Errorf("StateUpgrade[%d]: config block timestamp (%v) <= previous timestamp (%v)", i, *upgradeTimestamp, *previousUpgradeTimestamp) - } - previousUpgradeTimestamp = upgradeTimestamp - } - return nil -} - // 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 (*ChainConfig) GetActivatingStateUpgrades(from *uint64, to uint64, upgrades []StateUpgrade) []StateUpgrade { - activating := make([]StateUpgrade, 0) +func (*ChainConfig) GetActivatingStateUpgrades(from *uint64, to uint64, upgrades []stateupgrade.StateUpgrade) []stateupgrade.StateUpgrade { + activating := make([]stateupgrade.StateUpgrade, 0) for _, upgrade := range upgrades { if IsForkTransition(upgrade.BlockTimestamp, from, to) { activating = append(activating, upgrade) @@ -71,7 +24,7 @@ func (*ChainConfig) GetActivatingStateUpgrades(from *uint64, to uint64, upgrades } // checkStateUpgradesCompatible checks if [stateUpgrades] are compatible with [c] at [headTimestamp]. -func (c *ChainConfig) checkStateUpgradesCompatible(stateUpgrades []StateUpgrade, lastTimestamp uint64) *ethparams.ConfigCompatError { +func (c *ChainConfig) checkStateUpgradesCompatible(stateUpgrades []stateupgrade.StateUpgrade, lastTimestamp uint64) *ethparams.ConfigCompatError { // All active upgrades (from nil to [lastTimestamp]) must match. activeUpgrades := c.GetActivatingStateUpgrades(nil, lastTimestamp, c.StateUpgrades) newUpgrades := c.GetActivatingStateUpgrades(nil, lastTimestamp, stateUpgrades) diff --git a/params/extras/state_upgrade_test.go b/params/extras/state_upgrade_test.go index edaecebc6c..24487b3109 100644 --- a/params/extras/state_upgrade_test.go +++ b/params/extras/state_upgrade_test.go @@ -12,76 +12,16 @@ import ( "github.com/ava-labs/libevm/common/math" "github.com/stretchr/testify/require" + "github.com/ava-labs/subnet-evm/stateupgrade" "github.com/ava-labs/subnet-evm/utils" - "github.com/ava-labs/subnet-evm/utils/utilstest" ) -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: utils.NewUint64(1), StateUpgradeAccounts: modifiedAccounts}, - {BlockTimestamp: utils.NewUint64(2), StateUpgradeAccounts: modifiedAccounts}, - }, - }, - { - name: "upgrade block timestamp is not strictly increasing", - upgrades: []StateUpgrade{ - {BlockTimestamp: utils.NewUint64(1), StateUpgradeAccounts: modifiedAccounts}, - {BlockTimestamp: utils.NewUint64(1), StateUpgradeAccounts: modifiedAccounts}, - }, - expectedError: "config block timestamp (1) <= previous timestamp (1)", - }, - { - name: "upgrade block timestamp decreases", - upgrades: []StateUpgrade{ - {BlockTimestamp: utils.NewUint64(2), StateUpgradeAccounts: modifiedAccounts}, - {BlockTimestamp: utils.NewUint64(1), StateUpgradeAccounts: modifiedAccounts}, - }, - expectedError: "config block timestamp (1) <= previous timestamp (2)", - }, - { - name: "upgrade block timestamp is zero", - upgrades: []StateUpgrade{ - {BlockTimestamp: utils.NewUint64(0), 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) - c := *TestChainConfig - config := &c - config.SnowCtx = utilstest.NewTestSnowContext(t) - config.StateUpgrades = tt.upgrades - - err := config.Verify() - if tt.expectedError == "" { - require.NoError(err) - } else { - require.ErrorContains(err, tt.expectedError) - } - }) - } -} - func TestCheckCompatibleStateUpgrades(t *testing.T) { chainConfig := *TestChainConfig - stateUpgrade := map[common.Address]StateUpgradeAccount{ + stateUpgrade := map[common.Address]stateupgrade.StateUpgradeAccount{ {1}: {BalanceChange: (*math.HexOrDecimal256)(common.Big1)}, } - differentStateUpgrade := map[common.Address]StateUpgradeAccount{ + differentStateUpgrade := map[common.Address]stateupgrade.StateUpgradeAccount{ {2}: {BalanceChange: (*math.HexOrDecimal256)(common.Big1)}, } @@ -90,12 +30,12 @@ func TestCheckCompatibleStateUpgrades(t *testing.T) { startTimestamps: []uint64{5, 6}, configs: []*UpgradeConfig{ { - StateUpgrades: []StateUpgrade{ + StateUpgrades: []stateupgrade.StateUpgrade{ {BlockTimestamp: utils.NewUint64(6), StateUpgradeAccounts: stateUpgrade}, }, }, { - StateUpgrades: []StateUpgrade{ + StateUpgrades: []stateupgrade.StateUpgrade{ {BlockTimestamp: utils.NewUint64(6), StateUpgradeAccounts: stateUpgrade}, }, }, @@ -106,13 +46,13 @@ func TestCheckCompatibleStateUpgrades(t *testing.T) { startTimestamps: []uint64{5, 8}, configs: []*UpgradeConfig{ { - StateUpgrades: []StateUpgrade{ + StateUpgrades: []stateupgrade.StateUpgrade{ {BlockTimestamp: utils.NewUint64(6), StateUpgradeAccounts: stateUpgrade}, {BlockTimestamp: utils.NewUint64(7), StateUpgradeAccounts: stateUpgrade}, }, }, { - StateUpgrades: []StateUpgrade{ + StateUpgrades: []stateupgrade.StateUpgrade{ {BlockTimestamp: utils.NewUint64(6), StateUpgradeAccounts: stateUpgrade}, {BlockTimestamp: utils.NewUint64(7), StateUpgradeAccounts: differentStateUpgrade}, }, @@ -123,13 +63,13 @@ func TestCheckCompatibleStateUpgrades(t *testing.T) { startTimestamps: []uint64{5, 6}, configs: []*UpgradeConfig{ { - StateUpgrades: []StateUpgrade{ + StateUpgrades: []stateupgrade.StateUpgrade{ {BlockTimestamp: utils.NewUint64(6), StateUpgradeAccounts: stateUpgrade}, {BlockTimestamp: utils.NewUint64(7), StateUpgradeAccounts: stateUpgrade}, }, }, { - StateUpgrades: []StateUpgrade{ + StateUpgrades: []stateupgrade.StateUpgrade{ {BlockTimestamp: utils.NewUint64(6), StateUpgradeAccounts: stateUpgrade}, }, }, @@ -140,7 +80,7 @@ func TestCheckCompatibleStateUpgrades(t *testing.T) { startTimestamps: []uint64{6}, configs: []*UpgradeConfig{ { - StateUpgrades: []StateUpgrade{ + StateUpgrades: []stateupgrade.StateUpgrade{ {BlockTimestamp: utils.NewUint64(5), StateUpgradeAccounts: stateUpgrade}, }, }, @@ -172,10 +112,10 @@ func TestUnmarshalStateUpgradeJSON(t *testing.T) { ) upgradeConfig := UpgradeConfig{ - StateUpgrades: []StateUpgrade{ + StateUpgrades: []stateupgrade.StateUpgrade{ { BlockTimestamp: utils.NewUint64(1677608400), - StateUpgradeAccounts: map[common.Address]StateUpgradeAccount{ + StateUpgradeAccounts: map[common.Address]stateupgrade.StateUpgradeAccount{ common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"): { BalanceChange: (*math.HexOrDecimal256)(big.NewInt(100)), }, diff --git a/plugin/evm/vm_upgrade_bytes_test.go b/plugin/evm/vm_upgrade_bytes_test.go index 47b36f302e..e48b3ebd07 100644 --- a/plugin/evm/vm_upgrade_bytes_test.go +++ b/plugin/evm/vm_upgrade_bytes_test.go @@ -32,6 +32,7 @@ import ( "github.com/ava-labs/subnet-evm/params/paramstest" "github.com/ava-labs/subnet-evm/plugin/evm/vmerrors" "github.com/ava-labs/subnet-evm/precompile/contracts/txallowlist" + "github.com/ava-labs/subnet-evm/stateupgrade" "github.com/ava-labs/subnet-evm/utils" commonEng "github.com/ava-labs/avalanchego/snow/engine/common" @@ -273,7 +274,7 @@ func TestVMStateUpgrade(t *testing.T) { 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 := &extras.StateUpgradeAccount{ + genesisAccountUpgrade := &stateupgrade.StateUpgradeAccount{ BalanceChange: (*math.HexOrDecimal256)(big.NewInt(100)), Storage: map[common.Hash]common.Hash{storageKey: {}}, Code: upgradedCode, @@ -282,7 +283,7 @@ func TestVMStateUpgrade(t *testing.T) { // This modification will be applied to a new account newAccount := common.Address{42} require.NoError(t, err) - newAccountUpgrade := &extras.StateUpgradeAccount{ + newAccountUpgrade := &stateupgrade.StateUpgradeAccount{ BalanceChange: (*math.HexOrDecimal256)(big.NewInt(100)), Storage: map[common.Hash]common.Hash{storageKey: common.HexToHash("0x6666")}, Code: upgradedCode, diff --git a/stateupgrade/state_upgrade.go b/stateupgrade/state_upgrade.go index 86c43e3d06..5a1182d477 100644 --- a/stateupgrade/state_upgrade.go +++ b/stateupgrade/state_upgrade.go @@ -4,27 +4,70 @@ package stateupgrade import ( + "fmt" "math/big" + "reflect" "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/common/hexutil" + "github.com/ava-labs/libevm/common/math" "github.com/holiman/uint256" - - "github.com/ava-labs/subnet-evm/params/extras" ) +// StateUpgrade describes the modifications to be made to the state during +// a state upgrade. +type StateUpgrade struct { + BlockTimestamp *uint64 `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) +} + +// VerifyStateUpgrades checks [c.StateUpgrades] is well formed: +// - the specified blockTimestamps must monotonically increase +func VerifyStateUpgrades(upgrades []StateUpgrade) error { + var previousUpgradeTimestamp *uint64 + for i, upgrade := range upgrades { + upgradeTimestamp := upgrade.BlockTimestamp + if upgradeTimestamp == nil { + return fmt.Errorf("StateUpgrade[%d]: config block timestamp cannot be nil ", i) + } + // Verify the upgrade's timestamp is equal 0 (to avoid confusion with genesis). + if *upgradeTimestamp == 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 <= *previousUpgradeTimestamp { + return fmt.Errorf("StateUpgrade[%d]: config block timestamp (%v) <= previous timestamp (%v)", i, *upgradeTimestamp, *previousUpgradeTimestamp) + } + previousUpgradeTimestamp = upgradeTimestamp + } + return nil +} + // Configure applies the state upgrade to the state. -func Configure(stateUpgrade *extras.StateUpgrade, chainConfig ChainContext, state StateDB, blockContext BlockContext) error { +func Configure(stateUpgrade *StateUpgrade, chainConfig ChainContext, state StateDB, blockContext BlockContext) { isEIP158 := chainConfig.IsEIP158(blockContext.Number()) for account, upgrade := range stateUpgrade.StateUpgradeAccounts { - if err := upgradeAccount(account, upgrade, state, isEIP158); err != nil { - return err - } + upgradeAccount(account, upgrade, state, isEIP158) } - return nil } // upgradeAccount applies the state upgrade to the given account. -func upgradeAccount(account common.Address, upgrade extras.StateUpgradeAccount, state StateDB, isEIP158 bool) error { +func upgradeAccount(account common.Address, upgrade StateUpgradeAccount, state StateDB, isEIP158 bool) { // Create the account if it does not exist if !state.Exist(account) { state.CreateAccount(account) @@ -45,5 +88,4 @@ func upgradeAccount(account common.Address, upgrade extras.StateUpgradeAccount, for key, value := range upgrade.Storage { state.SetState(account, key, value) } - return nil } diff --git a/stateupgrade/state_upgrade_test.go b/stateupgrade/state_upgrade_test.go new file mode 100644 index 0000000000..10439e6a2d --- /dev/null +++ b/stateupgrade/state_upgrade_test.go @@ -0,0 +1,70 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package stateupgrade + +import ( + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/common/math" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/subnet-evm/utils" +) + +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: utils.NewUint64(1), StateUpgradeAccounts: modifiedAccounts}, + {BlockTimestamp: utils.NewUint64(2), StateUpgradeAccounts: modifiedAccounts}, + }, + }, + { + name: "upgrade block timestamp is not strictly increasing", + upgrades: []StateUpgrade{ + {BlockTimestamp: utils.NewUint64(1), StateUpgradeAccounts: modifiedAccounts}, + {BlockTimestamp: utils.NewUint64(1), StateUpgradeAccounts: modifiedAccounts}, + }, + expectedError: "config block timestamp (1) <= previous timestamp (1)", + }, + { + name: "upgrade block timestamp decreases", + upgrades: []StateUpgrade{ + {BlockTimestamp: utils.NewUint64(2), StateUpgradeAccounts: modifiedAccounts}, + {BlockTimestamp: utils.NewUint64(1), StateUpgradeAccounts: modifiedAccounts}, + }, + expectedError: "config block timestamp (1) <= previous timestamp (2)", + }, + { + name: "upgrade block timestamp is zero", + upgrades: []StateUpgrade{ + {BlockTimestamp: utils.NewUint64(0), 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) + + err := VerifyStateUpgrades(tt.upgrades) + if tt.expectedError == "" { + require.NoError(err) + } else { + require.ErrorContains(err, tt.expectedError) + } + }) + } +}