-
Notifications
You must be signed in to change notification settings - Fork 106
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(zetacore): add upgrade tracker (#2095)
* feat(zetacore): add develop store upgrade tracker * add ibc upgrade * fix gosec annotation? * feedback - directly call from upgrade handlers - add test coverage for bad state * ibc crosschain migration * review feedback * fix version
- Loading branch information
Showing
4 changed files
with
285 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
// 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) | ||
} | ||
} 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 { | ||
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) | ||
} | ||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |