From 51d5743e2dc68df719c8aea5370ef282a8573051 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Mon, 29 Apr 2024 12:27:31 -0700 Subject: [PATCH] feat(zetacore): add develop store upgrade tracker --- app/app.go | 1 + app/setup_handlers.go | 108 ++++++++++++++++++++++-------------- app/upgrade_tracker.go | 94 +++++++++++++++++++++++++++++++ app/upgrade_tracker_test.go | 86 ++++++++++++++++++++++++++++ 4 files changed, 247 insertions(+), 42 deletions(-) create mode 100644 app/upgrade_tracker.go create mode 100644 app/upgrade_tracker_test.go diff --git a/app/app.go b/app/app.go index 195dd14cf5..ced457edff 100644 --- a/app/app.go +++ b/app/app.go @@ -815,6 +815,7 @@ func (app *App) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.Res if err := tmjson.Unmarshal(req.AppStateBytes, &genesisState); err != nil { panic(err) } + app.UpgradeKeeper.SetModuleVersionMap(ctx, app.mm.GetVersionMap()) return app.mm.InitGenesis(ctx, app.appCodec, genesisState) } diff --git a/app/setup_handlers.go b/app/setup_handlers.go index 3c13cac2ff..bc6dadfe3b 100644 --- a/app/setup_handlers.go +++ b/app/setup_handlers.go @@ -1,13 +1,16 @@ package app import ( + "os" + + "golang.org/x/exp/slices" + "github.com/cosmos/cosmos-sdk/baseapp" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" consensustypes "github.com/cosmos/cosmos-sdk/x/consensus/types" crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" @@ -17,12 +20,11 @@ import ( slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/cosmos/cosmos-sdk/x/upgrade/types" - ibctransfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" - ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" - "github.com/zeta-chain/zetacore/pkg/constant" emissionstypes "github.com/zeta-chain/zetacore/x/emissions/types" ) +const releaseVersion = "v17" + func SetupHandlers(app *App) { // Set param key table for params module migration for _, subspace := range app.ParamsKeeper.GetSubspaces() { @@ -52,26 +54,66 @@ func SetupHandlers(app *App) { } } baseAppLegacySS := app.ParamsKeeper.Subspace(baseapp.Paramspace).WithKeyTable(paramstypes.ConsensusParamsKeyTable()) - app.UpgradeKeeper.SetUpgradeHandler(constant.Version, func(ctx sdk.Context, plan types.Plan, vm module.VersionMap) (module.VersionMap, error) { - app.Logger().Info("Running upgrade handler for " + constant.Version) - // Migrate Tendermint consensus parameters from x/params module to a dedicated x/consensus module. - baseapp.MigrateParams(ctx, baseAppLegacySS, &app.ConsensusParamsKeeper) - // Updated version map to the latest consensus versions from each module - for m, mb := range app.mm.Modules { - if module, ok := mb.(module.HasConsensusVersion); ok { - vm[m] = module.ConsensusVersion() - } - } + needsForcedMigration := []string{ + authtypes.ModuleName, + banktypes.ModuleName, + stakingtypes.ModuleName, + distrtypes.ModuleName, + slashingtypes.ModuleName, + govtypes.ModuleName, + crisistypes.ModuleName, + emissionstypes.ModuleName, + } + allUpgrades := upgradeTracker{ + upgrades: []upgradeTrackerItem{ + { + index: 1714664193, + storeUpgrade: &storetypes.StoreUpgrades{ + Added: []string{consensustypes.ModuleName, crisistypes.ModuleName}, + }, + upgradeHandler: func(ctx sdk.Context, vm module.VersionMap) (module.VersionMap, error) { + // Migrate Tendermint consensus parameters from x/params module to a dedicated x/consensus module + // https://docs.cosmos.network/main/build/migrations/upgrading#xconsensus + baseapp.MigrateParams(ctx, baseAppLegacySS, &app.ConsensusParamsKeeper) - VersionMigrator{v: vm}.TriggerMigration(authtypes.ModuleName) - VersionMigrator{v: vm}.TriggerMigration(banktypes.ModuleName) - VersionMigrator{v: vm}.TriggerMigration(stakingtypes.ModuleName) - VersionMigrator{v: vm}.TriggerMigration(distrtypes.ModuleName) - VersionMigrator{v: vm}.TriggerMigration(slashingtypes.ModuleName) - VersionMigrator{v: vm}.TriggerMigration(govtypes.ModuleName) - VersionMigrator{v: vm}.TriggerMigration(crisistypes.ModuleName) + // empty version map happens when upgrading from old versions which did not correctly call + // app.UpgradeKeeper.SetModuleVersionMap(ctx, app.mm.GetVersionMap()) in InitChainer. + // we must populate the version map if we detect this scenario + // + // this will only happen on the first upgrade. mainnet and testnet will not require this condition. + if len(vm) == 0 { + for m, mb := range app.mm.Modules { + if module, ok := mb.(module.HasConsensusVersion); ok { + if slices.Contains(needsForcedMigration, m) { + vm[m] = module.ConsensusVersion() - 1 + } else { + vm[m] = module.ConsensusVersion() + } + } + } + } + return vm, nil + }, + }, + }, + } - VersionMigrator{v: vm}.TriggerMigration(emissionstypes.ModuleName) + _, useDevelopTracker := os.LookupEnv("ZETACORED_USE_DEVELOP_UPGRADE_TRACKER") + upgradeHandlerFns, storeUpgrades, err := allUpgrades.getUpgrades(useDevelopTracker) + if err != nil { + panic(err) + } + + app.UpgradeKeeper.SetUpgradeHandler(releaseVersion, func(ctx sdk.Context, plan types.Plan, vm module.VersionMap) (module.VersionMap, error) { + app.Logger().Info("Running upgrade handler for " + releaseVersion) + + var err error + for _, upgradeHandler := range upgradeHandlerFns { + vm, err = upgradeHandler(ctx, vm) + if err != nil { + return vm, err + } + } return app.mm.RunMigrations(ctx, app.configurator, vm) }) @@ -80,29 +122,11 @@ func SetupHandlers(app *App) { if err != nil { panic(err) } - if upgradeInfo.Name == constant.Version && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { - storeUpgrades := storetypes.StoreUpgrades{ - Added: []string{ - consensustypes.ModuleName, - crisistypes.ModuleName, - capabilitytypes.ModuleName, - ibcexported.ModuleName, - ibctransfertypes.ModuleName, - }, - } + if upgradeInfo.Name == releaseVersion && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { // Use upgrade store loader for the initial loading of all stores when app starts, // it checks if version == upgradeHeight and applies store upgrades before loading the stores, // so that new stores start with the correct version (the current height of chain), // instead the default which is the latest version that store last committed i.e 0 for new stores. - app.SetStoreLoader(types.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades)) + app.SetStoreLoader(types.UpgradeStoreLoader(upgradeInfo.Height, storeUpgrades)) } } - -type VersionMigrator struct { - v module.VersionMap -} - -func (v VersionMigrator) TriggerMigration(moduleName string) module.VersionMap { - v.v[moduleName] = v.v[moduleName] - 1 - return v.v -} diff --git a/app/upgrade_tracker.go b/app/upgrade_tracker.go new file mode 100644 index 0000000000..1dec261ea5 --- /dev/null +++ b/app/upgrade_tracker.go @@ -0,0 +1,94 @@ +package app + +import ( + "fmt" + "os" + "path" + "strconv" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" +) + +type upgradeHandlerFn func(ctx sdk.Context, vm module.VersionMap) (module.VersionMap, error) + +type upgradeTrackerItem struct { + // Monotonically increasing index to order and track migrations. Typically the current unix epoch timestamp. + index int64 + // Function that will run during the SetUpgradeHandler callback. The VersionMap must always be returned. + upgradeHandler upgradeHandlerFn + // StoreUpgrades that will be provided to UpgradeStoreLoader + storeUpgrade *storetypes.StoreUpgrades +} + +// upgradeTracker allows us to track needed upgrades/migrations across both release and develop builds +type upgradeTracker struct { + upgrades []upgradeTrackerItem + stateFileDir string +} + +func (t upgradeTracker) getDevelopUpgrades() ([]upgradeHandlerFn, *storetypes.StoreUpgrades, error) { + neededUpgrades := &storetypes.StoreUpgrades{} + neededUpgradeHandlers := []upgradeHandlerFn{} + stateFilePath := path.Join(t.stateFileDir, "developupgradetracker") + + currentIndex := int64(0) + if stateFileContents, err := os.ReadFile(stateFilePath); err == nil { // #nosec G304 -- stateFilePath is not user controllable + currentIndex, err = strconv.ParseInt(string(stateFileContents), 10, 64) + if err != nil { + return nil, nil, fmt.Errorf("unable to decode upgrade tracker: %w", err) + } + } else { + fmt.Printf("unable to load upgrade tracker: %v\n", err) + } + + maxIndex := currentIndex + for _, item := range t.upgrades { + index := item.index + upgrade := item.storeUpgrade + versionModifier := item.upgradeHandler + if index <= currentIndex { + continue + } + if versionModifier != nil { + neededUpgradeHandlers = append(neededUpgradeHandlers, versionModifier) + } + if upgrade != nil { + neededUpgrades.Added = append(neededUpgrades.Added, upgrade.Added...) + neededUpgrades.Deleted = append(neededUpgrades.Deleted, upgrade.Deleted...) + neededUpgrades.Renamed = append(neededUpgrades.Renamed, upgrade.Renamed...) + } + maxIndex = index + } + err := os.WriteFile(stateFilePath, []byte(strconv.FormatInt(maxIndex, 10)), 0o600) + if err != nil { + return nil, nil, fmt.Errorf("unable to write upgrade state file: %w", err) + } + return neededUpgradeHandlers, neededUpgrades, nil +} + +func (t upgradeTracker) mergeAllUpgrades() ([]upgradeHandlerFn, *storetypes.StoreUpgrades, error) { + upgrades := &storetypes.StoreUpgrades{} + upgradeHandlers := []upgradeHandlerFn{} + for _, item := range t.upgrades { + upgrade := item.storeUpgrade + versionModifier := item.upgradeHandler + if versionModifier != nil { + upgradeHandlers = append(upgradeHandlers, versionModifier) + } + if upgrade != nil { + upgrades.Added = append(upgrades.Added, upgrade.Added...) + upgrades.Deleted = append(upgrades.Deleted, upgrade.Deleted...) + upgrades.Renamed = append(upgrades.Renamed, upgrade.Renamed...) + } + } + return upgradeHandlers, upgrades, nil +} + +func (t upgradeTracker) getUpgrades(isDevelop bool) ([]upgradeHandlerFn, *storetypes.StoreUpgrades, error) { + if isDevelop { + return t.getDevelopUpgrades() + } + return t.mergeAllUpgrades() +} diff --git a/app/upgrade_tracker_test.go b/app/upgrade_tracker_test.go new file mode 100644 index 0000000000..c8138d452b --- /dev/null +++ b/app/upgrade_tracker_test.go @@ -0,0 +1,86 @@ +package app + +import ( + "os" + "testing" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/stretchr/testify/require" + authoritytypes "github.com/zeta-chain/zetacore/x/authority/types" + lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" +) + +func TestUpgradeTracker(t *testing.T) { + r := require.New(t) + + tmpdir, err := os.MkdirTemp("", "storeupgradetracker-*") + r.NoError(err) + + allUpgrades := upgradeTracker{ + upgrades: []upgradeTrackerItem{ + { + index: 1000, + storeUpgrade: &storetypes.StoreUpgrades{ + Added: []string{authoritytypes.ModuleName}, + }, + }, + { + index: 2000, + storeUpgrade: &storetypes.StoreUpgrades{ + Added: []string{lightclienttypes.ModuleName}, + }, + upgradeHandler: func(ctx sdk.Context, vm module.VersionMap) (module.VersionMap, error) { + return vm, nil + }, + }, + { + index: 3000, + upgradeHandler: func(ctx sdk.Context, vm module.VersionMap) (module.VersionMap, error) { + return vm, nil + }, + }, + }, + stateFileDir: tmpdir, + } + + upgradeHandlers, storeUpgrades, err := allUpgrades.mergeAllUpgrades() + r.NoError(err) + r.Len(storeUpgrades.Added, 2) + r.Len(storeUpgrades.Renamed, 0) + r.Len(storeUpgrades.Deleted, 0) + r.Len(upgradeHandlers, 2) + + // should return all migrations on first call + upgradeHandlers, storeUpgrades, err = allUpgrades.getDevelopUpgrades() + r.NoError(err) + r.Len(storeUpgrades.Added, 2) + r.Len(storeUpgrades.Renamed, 0) + r.Len(storeUpgrades.Deleted, 0) + r.Len(upgradeHandlers, 2) + + // should return no upgrades on second call + upgradeHandlers, storeUpgrades, err = allUpgrades.getDevelopUpgrades() + r.NoError(err) + r.Len(storeUpgrades.Added, 0) + r.Len(storeUpgrades.Renamed, 0) + r.Len(storeUpgrades.Deleted, 0) + r.Len(upgradeHandlers, 0) + + // now add a upgrade and ensure that it gets run without running + // the other upgrades + allUpgrades.upgrades = append(allUpgrades.upgrades, upgradeTrackerItem{ + index: 4000, + storeUpgrade: &storetypes.StoreUpgrades{ + Deleted: []string{"example"}, + }, + }) + + upgradeHandlers, storeUpgrades, err = allUpgrades.getDevelopUpgrades() + r.NoError(err) + r.Len(storeUpgrades.Added, 0) + r.Len(storeUpgrades.Renamed, 0) + r.Len(storeUpgrades.Deleted, 1) + r.Len(upgradeHandlers, 0) +}