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

fix!: invalid return value from keeper GetLastCompleteUpgrade - breaking change version #11800

Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### State Machine Breaking

* (x/upgrade) [\#11800](https://github.com/cosmos/cosmos-sdk/pull/11800) Fix `GetLastCompleteUpgrade` to properly return the latest upgrade.
* [\#10564](https://github.com/cosmos/cosmos-sdk/pull/10564) Fix bug when updating allowance inside AllowedMsgAllowance
* (x/auth)[\#9596](https://github.com/cosmos/cosmos-sdk/pull/9596) Enable creating periodic vesting accounts with a transactions instead of requiring them to be created in genesis.
* (x/bank) [\#9611](https://github.com/cosmos/cosmos-sdk/pull/9611) Introduce a new index to act as a reverse index between a denomination and address allowing to query for
Expand Down
44 changes: 28 additions & 16 deletions x/upgrade/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
storetypes "github.com/cosmos/cosmos-sdk/store/types"

"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/internal/conv"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
Expand Down Expand Up @@ -239,28 +238,43 @@ func (k Keeper) GetUpgradedConsensusState(ctx sdk.Context, lastHeight int64) ([]
func (k Keeper) GetLastCompletedUpgrade(ctx sdk.Context) (string, int64) {
iter := sdk.KVStoreReversePrefixIterator(ctx.KVStore(k.storeKey), []byte{types.DoneByte})
defer iter.Close()

if iter.Valid() {
return parseDoneKey(iter.Key()), int64(binary.BigEndian.Uint64(iter.Value()))
return parseDoneKey(iter.Key())
}

return "", 0
}

// parseDoneKey - split upgrade name from the done key
func parseDoneKey(key []byte) string {
kv.AssertKeyAtLeastLength(key, 2)
return string(key[1:])
// parseDoneKey - split upgrade name and height from the done key
func parseDoneKey(key []byte) (string, int64) {
// 1 byte for the DoneByte + 8 bytes height + at least 1 byte for the name
kv.AssertKeyAtLeastLength(key, 10)
height := binary.BigEndian.Uint64(key[1:9])
return string(key[9:]), int64(height)
}

// encodeDoneKey - concatenate DoneByte, height and upgrade name to form the done key
func encodeDoneKey(name string, height int64) []byte {
key := make([]byte, 9+len(name)) // 9 = donebyte + uint64 len
key[0] = types.DoneByte
binary.BigEndian.PutUint64(key[1:9], uint64(height))
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
copy(key[9:], name)
return key
}

// GetDoneHeight returns the height at which the given upgrade was executed
func (k Keeper) GetDoneHeight(ctx sdk.Context, name string) int64 {
store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte{types.DoneByte})
bz := store.Get(conv.UnsafeStrToBytes(name))
if len(bz) == 0 {
return 0
}
iter := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), []byte{types.DoneByte})
defer iter.Close()

return int64(binary.BigEndian.Uint64(bz))
for ; iter.Valid(); iter.Next() {
upgradeName, height := parseDoneKey(iter.Key())
if upgradeName == name {
return height
}
}
return 0
}

// ClearIBCState clears any planned IBC state
Expand Down Expand Up @@ -303,10 +317,8 @@ func (k Keeper) GetUpgradePlan(ctx sdk.Context) (plan types.Plan, havePlan bool)

// setDone marks this upgrade name as being done so the name can't be reused accidentally
func (k Keeper) setDone(ctx sdk.Context, name string) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte{types.DoneByte})
bz := make([]byte, 8)
binary.BigEndian.PutUint64(bz, uint64(ctx.BlockHeight()))
store.Set([]byte(name), bz)
store := ctx.KVStore(k.storeKey)
store.Set(encodeDoneKey(name, ctx.BlockHeight()), []byte{1})
}

// HasHandler returns true iff there is a handler registered for this name
Expand Down
36 changes: 36 additions & 0 deletions x/upgrade/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,42 @@ func (s *KeeperTestSuite) TestLastCompletedUpgrade() {
require.Equal(int64(15), height)
}

// This test ensures that `GetLastDoneUpgrade` always returns the last upgrade according to the block height
// it was executed at, rather than using an ordering based on upgrade names.
func (s *KeeperTestSuite) TestLastCompletedUpgradeOrdering() {
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
keeper := s.app.UpgradeKeeper
require := s.Require()

// apply first upgrade
keeper.SetUpgradeHandler("test-v0.9", func(_ sdk.Context, _ types.Plan, vm module.VersionMap) (module.VersionMap, error) {
return vm, nil
})

keeper.ApplyUpgrade(s.ctx, types.Plan{
Name: "test-v0.9",
Height: 10,
})

name, height := keeper.GetLastCompletedUpgrade(s.ctx)
require.Equal("test-v0.9", name)
require.Equal(int64(10), height)

// apply second upgrade
keeper.SetUpgradeHandler("test-v0.10", func(_ sdk.Context, _ types.Plan, vm module.VersionMap) (module.VersionMap, error) {
return vm, nil
})

newCtx := s.ctx.WithBlockHeight(15)
keeper.ApplyUpgrade(newCtx, types.Plan{
Name: "test-v0.10",
Height: 15,
})

name, height = keeper.GetLastCompletedUpgrade(newCtx)
require.Equal("test-v0.10", name)
require.Equal(int64(15), height)
}

func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}
43 changes: 43 additions & 0 deletions x/upgrade/keeper/migrations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package keeper

import (
"encoding/binary"

"github.com/cosmos/cosmos-sdk/store/prefix"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/upgrade/types"
)

// Migrator is a struct for handling in-place store migrations.
type Migrator struct {
keeper Keeper
}

// NewMigrator returns a new Migrator.
func NewMigrator(keeper Keeper) Migrator {
return Migrator{keeper: keeper}
}

// Migrate1to2 migrates from version 1 to 2.
func (m Migrator) Migrate1to2(ctx sdk.Context) error {
return migrateDoneUpgradeKeys(ctx, m.keeper.storeKey)
}

func migrateDoneUpgradeKeys(ctx sdk.Context, storeKey storetypes.StoreKey) error {
store := ctx.KVStore(storeKey)
oldDoneStore := prefix.NewStore(store, []byte{types.DoneByte})
oldDoneStoreIter := oldDoneStore.Iterator(nil, nil)
defer oldDoneStoreIter.Close()

for ; oldDoneStoreIter.Valid(); oldDoneStoreIter.Next() {
oldKey := oldDoneStoreIter.Key()
upgradeName := string(oldKey)
upgradeHeight := int64(binary.BigEndian.Uint64(oldDoneStoreIter.Value()))
newKey := encodeDoneKey(upgradeName, upgradeHeight)

store.Set(newKey, []byte{1})
oldDoneStore.Delete(oldKey)
ValarDragon marked this conversation as resolved.
Show resolved Hide resolved
}
return nil
}
59 changes: 59 additions & 0 deletions x/upgrade/keeper/migrations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package keeper

import (
"encoding/binary"
"testing"

"github.com/cosmos/cosmos-sdk/testutil"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/upgrade/types"
"github.com/stretchr/testify/require"
)

type storedUpgrade struct {
name string
height int64
}

func encodeOldDoneKey(upgrade storedUpgrade) []byte {
return append([]byte{types.DoneByte}, []byte(upgrade.name)...)
}

func TestMigrateDoneUpgradeKeys(t *testing.T) {
upgradeKey := sdk.NewKVStoreKey("upgrade")
ctx := testutil.DefaultContext(upgradeKey, sdk.NewTransientStoreKey("transient_test"))
store := ctx.KVStore(upgradeKey)

testCases := []struct {
name string
upgrades []storedUpgrade
}{
{
name: "valid upgrades",
upgrades: []storedUpgrade{
{name: "some-other-upgrade", height: 1},
{name: "test02", height: 2},
{name: "test01", height: 3},
},
},
}

for _, tc := range testCases {
for _, upgrade := range tc.upgrades {
bz := make([]byte, 8)
binary.BigEndian.PutUint64(bz, uint64(upgrade.height))
oldKey := encodeOldDoneKey(upgrade)
store.Set(oldKey, bz)
}

err := migrateDoneUpgradeKeys(ctx, upgradeKey)
require.NoError(t, err)

for _, upgrade := range tc.upgrades {
newKey := encodeDoneKey(upgrade.name, upgrade.height)
oldKey := encodeOldDoneKey(upgrade)
require.Nil(t, store.Get(oldKey))
require.Equal(t, []byte{1}, store.Get(newKey))
}
}
}
9 changes: 8 additions & 1 deletion x/upgrade/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ func init() {
types.RegisterLegacyAminoCodec(codec.NewLegacyAmino())
}

const (
consensusVersion uint64 = 2
)

var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
Expand Down Expand Up @@ -95,6 +99,9 @@ func (am AppModule) LegacyQuerierHandler(legacyQuerierCdc *codec.LegacyAmino) sd
func (am AppModule) RegisterServices(cfg module.Configurator) {
types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper))
types.RegisterQueryServer(cfg.QueryServer(), am.keeper)

m := keeper.NewMigrator(am.keeper)
cfg.RegisterMigration(types.ModuleName, 1, m.Migrate1to2)
}

// InitGenesis is ignored, no sense in serializing future upgrades
Expand All @@ -118,7 +125,7 @@ func (am AppModule) ExportGenesis(_ sdk.Context, cdc codec.JSONCodec) json.RawMe
}

// ConsensusVersion implements AppModule/ConsensusVersion.
func (AppModule) ConsensusVersion() uint64 { return 1 }
func (AppModule) ConsensusVersion() uint64 { return consensusVersion }

// BeginBlock calls the upgrade module hooks
//
Expand Down