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

feat(zetacore): add upgrade tracker #2095

Merged
merged 7 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,7 @@
if err := tmjson.Unmarshal(req.AppStateBytes, &genesisState); err != nil {
panic(err)
}
app.UpgradeKeeper.SetModuleVersionMap(ctx, app.mm.GetVersionMap())

Check warning on line 853 in app/app.go

View check run for this annotation

Codecov / codecov/patch

app/app.go#L853

Added line #L853 was not covered by tests
return app.mm.InitGenesis(ctx, app.appCodec, genesisState)
}

Expand Down
117 changes: 81 additions & 36 deletions app/setup_handlers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
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"
Expand Down Expand Up @@ -53,27 +57,87 @@
}
}
baseAppLegacySS := app.ParamsKeeper.Subspace(baseapp.Paramspace).WithKeyTable(paramstypes.ConsensusParamsKeyTable())
needsForcedMigration := []string{
authtypes.ModuleName,
skosito marked this conversation as resolved.
Show resolved Hide resolved
banktypes.ModuleName,
stakingtypes.ModuleName,
distrtypes.ModuleName,
slashingtypes.ModuleName,
govtypes.ModuleName,
crisistypes.ModuleName,
emissionstypes.ModuleName,

Check warning on line 68 in app/setup_handlers.go

View check run for this annotation

Codecov / codecov/patch

app/setup_handlers.go#L60-L68

Added lines #L60 - L68 were not covered by tests
}
allUpgrades := upgradeTracker{
gartnera marked this conversation as resolved.
Show resolved Hide resolved
upgrades: []upgradeTrackerItem{

Check warning on line 71 in app/setup_handlers.go

View check run for this annotation

Codecov / codecov/patch

app/setup_handlers.go#L70-L71

Added lines #L70 - L71 were not covered by tests
{
index: 1714664193,
storeUpgrade: &storetypes.StoreUpgrades{
Added: []string{consensustypes.ModuleName, crisistypes.ModuleName},
},
upgradeHandler: func(ctx sdk.Context, vm module.VersionMap) (module.VersionMap, error) {

Check warning on line 77 in app/setup_handlers.go

View check run for this annotation

Codecov / codecov/patch

app/setup_handlers.go#L73-L77

Added lines #L73 - L77 were not covered by tests
// 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)

Check warning on line 80 in app/setup_handlers.go

View check run for this annotation

Codecov / codecov/patch

app/setup_handlers.go#L80

Added line #L80 was not covered by tests

// 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.
gartnera marked this conversation as resolved.
Show resolved Hide resolved
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()

Check warning on line 93 in app/setup_handlers.go

View check run for this annotation

Codecov / codecov/patch

app/setup_handlers.go#L87-L93

Added lines #L87 - L93 were not covered by tests
}
}
}
}
return vm, nil

Check warning on line 98 in app/setup_handlers.go

View check run for this annotation

Codecov / codecov/patch

app/setup_handlers.go#L98

Added line #L98 was not covered by tests
},
},
{
index: 1715624665,
storeUpgrade: &storetypes.StoreUpgrades{
Added: []string{capabilitytypes.ModuleName, ibcexported.ModuleName, ibctransfertypes.ModuleName},
},
},
{
index: 1715707436,
storeUpgrade: &storetypes.StoreUpgrades{
Added: []string{ibccrosschaintypes.ModuleName},
},
},
},
stateFileDir: DefaultNodeHome,
}

var upgradeHandlerFns []upgradeHandlerFn
var storeUpgrades *storetypes.StoreUpgrades
var err error
_, useIncrementalTracker := os.LookupEnv("ZETACORED_USE_INCREMENTAL_UPGRADE_TRACKER")
if useIncrementalTracker {
upgradeHandlerFns, storeUpgrades, err = allUpgrades.getIncrementalUpgrades()
if err != nil {
panic(err)

Check warning on line 124 in app/setup_handlers.go

View check run for this annotation

Codecov / codecov/patch

app/setup_handlers.go#L117-L124

Added lines #L117 - L124 were not covered by tests
}
} else {
upgradeHandlerFns, storeUpgrades = allUpgrades.mergeAllUpgrades()

Check warning on line 127 in app/setup_handlers.go

View check run for this annotation

Codecov / codecov/patch

app/setup_handlers.go#L126-L127

Added lines #L126 - L127 were not covered by tests
}

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()

var err error
for _, upgradeHandler := range upgradeHandlerFns {
vm, err = upgradeHandler(ctx, vm)
if err != nil {
return vm, err

Check warning on line 137 in app/setup_handlers.go

View check run for this annotation

Codecov / codecov/patch

app/setup_handlers.go#L133-L137

Added lines #L133 - L137 were not covered by tests
}
}

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)

VersionMigrator{v: vm}.TriggerMigration(emissionstypes.ModuleName)

return app.mm.RunMigrations(ctx, app.configurator, vm)
})

Expand All @@ -82,29 +146,10 @@
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,
ibccrosschaintypes.ModuleName,
},
}
// 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))

Check warning on line 153 in app/setup_handlers.go

View check run for this annotation

Codecov / codecov/patch

app/setup_handlers.go#L153

Added line #L153 was not covered by tests
}
}

type VersionMigrator struct {
v module.VersionMap
}

func (v VersionMigrator) TriggerMigration(moduleName string) module.VersionMap {
v.v[moduleName] = v.v[moduleName] - 1
return v.v
}
96 changes: 96 additions & 0 deletions app/upgrade_tracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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"
)

const incrementalUpgradeTrackerStateFile = "incrementalupgradetracker"

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
gartnera marked this conversation as resolved.
Show resolved Hide resolved
// 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
// directory the incremental state file is stored
stateFileDir string
}

// getIncrementalUpgrades gets all upgrades that have not been been applied. This is typically
// used for developnet upgrades since we need to run migrations as the are committed rather than
// all at once during a release
func (t upgradeTracker) getIncrementalUpgrades() ([]upgradeHandlerFn, *storetypes.StoreUpgrades, error) {
neededUpgrades := &storetypes.StoreUpgrades{}
neededUpgradeHandlers := []upgradeHandlerFn{}
stateFilePath := path.Join(t.stateFileDir, incrementalUpgradeTrackerStateFile)

currentIndex := int64(0)
stateFileContents, err := os.ReadFile(stateFilePath) // #nosec G304 -- stateFilePath is not user controllable
if err == nil {
currentIndex, err = strconv.ParseInt(string(stateFileContents), 10, 64)
if err != nil {
return nil, nil, fmt.Errorf("unable to decode upgrade tracker: %w", err)
gartnera marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
fmt.Printf("unable to load upgrade tracker: %v\n", err)
}

maxIndex := currentIndex
for _, item := range t.upgrades {
index := item.index
upgrade := item.storeUpgrade
upgradeHandler := item.upgradeHandler
if index <= currentIndex {
skosito marked this conversation as resolved.
Show resolved Hide resolved
continue
}
if upgradeHandler != nil {
neededUpgradeHandlers = append(neededUpgradeHandlers, upgradeHandler)
}
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)

Check warning on line 73 in app/upgrade_tracker.go

View check run for this annotation

Codecov / codecov/patch

app/upgrade_tracker.go#L73

Added line #L73 was not covered by tests
}
return neededUpgradeHandlers, neededUpgrades, nil
}

// mergeAllUpgrades unconditionally merges all upgrades. Typically used to gather the
// migrations used during a release upgrade.
func (t upgradeTracker) mergeAllUpgrades() ([]upgradeHandlerFn, *storetypes.StoreUpgrades) {
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
}
107 changes: 107 additions & 0 deletions app/upgrade_tracker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package app

import (
"os"
"path"
"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)
defer os.RemoveAll(tmpdir)

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 := allUpgrades.mergeAllUpgrades()
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.getIncrementalUpgrades()
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.getIncrementalUpgrades()
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.getIncrementalUpgrades()
r.NoError(err)
r.Len(storeUpgrades.Added, 0)
r.Len(storeUpgrades.Renamed, 0)
r.Len(storeUpgrades.Deleted, 1)
r.Len(upgradeHandlers, 0)
}

func TestUpgradeTrackerBadState(t *testing.T) {
r := require.New(t)

tmpdir, err := os.MkdirTemp("", "storeupgradetracker-*")
r.NoError(err)
defer os.RemoveAll(tmpdir)

stateFilePath := path.Join(tmpdir, incrementalUpgradeTrackerStateFile)

err = os.WriteFile(stateFilePath, []byte("badstate"), 0o600)
r.NoError(err)

allUpgrades := upgradeTracker{
upgrades: []upgradeTrackerItem{},
stateFileDir: tmpdir,
}
_, _, err = allUpgrades.getIncrementalUpgrades()
r.Error(err)
}
Loading