diff --git a/app/app.go b/app/app.go index 21aee50983..904ca403f6 100644 --- a/app/app.go +++ b/app/app.go @@ -2,11 +2,13 @@ package app import ( "encoding/json" + "fmt" "io" "math/big" "net/http" "os" + "github.com/cosmos/cosmos-sdk/x/auth/ante" distrclient "github.com/cosmos/cosmos-sdk/x/distribution/client" govclient "github.com/cosmos/cosmos-sdk/x/gov/client" paramsclient "github.com/cosmos/cosmos-sdk/x/params/client" @@ -103,6 +105,8 @@ import ( ibchost "github.com/cosmos/ibc-go/v5/modules/core/24-host" ibckeeper "github.com/cosmos/ibc-go/v5/modules/core/keeper" + regenupgrades "github.com/regen-network/regen-ledger/v4/app/upgrades" + v5 "github.com/regen-network/regen-ledger/v4/app/upgrades/v5" "github.com/regen-network/regen-ledger/x/data" datamodule "github.com/regen-network/regen-ledger/x/data/module" "github.com/regen-network/regen-ledger/x/ecocredit" @@ -176,6 +180,8 @@ var ( return perms }() + + upgrades = []regenupgrades.Upgrade{v5.Upgrade} ) func init() { @@ -317,7 +323,6 @@ func NewRegenApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest // set the governance module account as the authority for conducting upgrades app.UpgradeKeeper = upgradekeeper.NewKeeper(skipUpgradeHeights, keys[upgradetypes.StoreKey], appCodec, homePath, app.BaseApp, authtypes.NewModuleAddress(govtypes.ModuleName).String()) - app.registerUpgradeHandlers() // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks @@ -530,6 +535,9 @@ func NewRegenApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest app.ModuleManager.RegisterRoutes(app.Router(), app.QueryRouter(), encodingConfig.Amino) app.ModuleManager.RegisterInvariants(&app.CrisisKeeper) + app.setUpgradeStoreLoaders() + app.setUpgradeHandlers() + // create the simulation manager and define the order of the modules for deterministic simulations // // NOTE: this is not required apps that don't use the simulator for fuzz testing @@ -566,7 +574,15 @@ func NewRegenApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest app.SetBeginBlocker(app.BeginBlocker) app.SetEndBlocker(app.EndBlocker) - anteHandler, err := app.setCustomAnteHandler(encodingConfig.TxConfig) + anteHandler, err := ante.NewAnteHandler( + ante.HandlerOptions{ + AccountKeeper: app.AccountKeeper, + BankKeeper: app.BankKeeper, + SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), + FeegrantKeeper: app.FeeGrantKeeper, + SigGasConsumer: ante.DefaultSigVerificationGasConsumer, + }, + ) if err != nil { panic(err) } @@ -708,6 +724,32 @@ func (app *RegenApp) RegisterTendermintService(clientCtx client.Context) { ) } +func (app *RegenApp) setUpgradeStoreLoaders() { + upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk() + if err != nil { + panic(fmt.Sprintf("failed to read upgrade info from disk %s", err)) + } + + if app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { + return + } + + for _, u := range upgrades { + if upgradeInfo.Name == u.UpgradeName { + app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &u.StoreUpgrades)) + } + } +} + +func (app *RegenApp) setUpgradeHandlers() { + for _, u := range upgrades { + app.UpgradeKeeper.SetUpgradeHandler( + u.UpgradeName, + u.CreateUpgradeHandler(app.ModuleManager, app.configurator), + ) + } +} + // RegisterSwaggerAPI registers swagger route with API Server func RegisterSwaggerAPI(_ client.Context, rtr *mux.Router) { statikFS, err := fs.New() diff --git a/app/appconfig.go b/app/appconfig.go deleted file mode 100644 index 2005530d5a..0000000000 --- a/app/appconfig.go +++ /dev/null @@ -1,91 +0,0 @@ -package app - -import ( - "fmt" - - "github.com/cosmos/cosmos-sdk/client" - storetypes "github.com/cosmos/cosmos-sdk/store/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/module" - "github.com/cosmos/cosmos-sdk/x/auth/ante" - "github.com/cosmos/cosmos-sdk/x/group" - upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" - ica "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts" - icacontrollertypes "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/controller/types" - icahosttypes "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/host/types" - icatypes "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/types" - - "github.com/regen-network/regen-ledger/x/data" - "github.com/regen-network/regen-ledger/x/ecocredit" -) - -func (app *RegenApp) registerUpgradeHandlers() { - upgradeName := "v5.0" - app.UpgradeKeeper.SetUpgradeHandler(upgradeName, - func(ctx sdk.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { - // set regen module consensus version - fromVM[ecocredit.ModuleName] = 2 - fromVM[data.ModuleName] = 1 - app.UpgradeKeeper.SetModuleVersionMap(ctx, fromVM) - - // manually set the ICA params - // the ICA module's default genesis has host and controller enabled. - // we want these to be enabled via gov param change. - - // Add Interchain Accounts host module - // set the ICS27 consensus version so InitGenesis is not run - fromVM[icatypes.ModuleName] = app.ModuleManager.Modules[icatypes.ModuleName].ConsensusVersion() - - // create ICS27 Controller submodule params, controller module not enabled. - controllerParams := icacontrollertypes.Params{ControllerEnabled: false} - - // create ICS27 Host submodule params, host module not enabled. - hostParams := icahosttypes.Params{ - HostEnabled: false, - AllowMessages: []string{}, - } - - mod, found := app.ModuleManager.Modules[icatypes.ModuleName] - if !found { - panic(fmt.Sprintf("module %s is not in the module manager", icatypes.ModuleName)) - } - - icaMod, ok := mod.(ica.AppModule) - if !ok { - panic(fmt.Sprintf("expected module %s to be type %T, got %T", icatypes.ModuleName, ica.AppModule{}, mod)) - } - icaMod.InitModule(ctx, controllerParams, hostParams) - - // transfer module consensus version has been bumped to 2 - return app.ModuleManager.RunMigrations(ctx, app.configurator, fromVM) - }) - - upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk() - if err != nil { - panic(err) - } - - if upgradeInfo.Name == upgradeName && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { - storeUpgrades := storetypes.StoreUpgrades{ - Added: []string{ - group.ModuleName, - icahosttypes.StoreKey, - }, - } - - // configure store loader that checks if version == upgradeHeight and applies store upgrades - app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades)) - } -} - -func (app *RegenApp) setCustomAnteHandler(cfg client.TxConfig) (sdk.AnteHandler, error) { - return ante.NewAnteHandler( - ante.HandlerOptions{ - AccountKeeper: app.AccountKeeper, - BankKeeper: app.BankKeeper, - SignModeHandler: cfg.SignModeHandler(), - FeegrantKeeper: app.FeeGrantKeeper, - SigGasConsumer: ante.DefaultSigVerificationGasConsumer, - }, - ) -} diff --git a/app/app_test.go b/app/testsuite/app_test.go similarity index 56% rename from app/app_test.go rename to app/testsuite/app_test.go index 24aa0e871a..fbd8624f31 100644 --- a/app/app_test.go +++ b/app/testsuite/app_test.go @@ -1,4 +1,4 @@ -package app +package testsuite import ( "os" @@ -8,36 +8,33 @@ import ( dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/libs/log" + + "github.com/regen-network/regen-ledger/v4/app" ) func TestSimAppExportAndBlockedAddrs(t *testing.T) { - encCfg := MakeEncodingConfig() + encCfg := app.MakeEncodingConfig() db := dbm.NewMemDB() logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) - app := NewAppWithCustomOptions(t, false, SetupOptions{ + regenApp := NewAppWithCustomOptions(t, false, SetupOptions{ Logger: logger, DB: db, InvCheckPeriod: 0, EncConfig: encCfg, - HomePath: DefaultNodeHome, + HomePath: app.DefaultNodeHome, SkipUpgradeHeights: map[int64]bool{}, AppOpts: EmptyAppOptions{}, }) - for acc := range maccPerms { - require.Equal(t, true, app.BankKeeper.BlockedAddr(app.AccountKeeper.GetModuleAddress(acc)), + for acc := range app.GetMaccPerms() { + require.Equal(t, true, regenApp.BankKeeper.BlockedAddr(regenApp.AccountKeeper.GetModuleAddress(acc)), "ensure that all module account addresses are properly blocked in bank keeper") } - app.Commit() + regenApp.Commit() // Making a new app object with the db, so that initchain hasn't been called - app2 := NewRegenApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, DefaultNodeHome, 0, encCfg, EmptyAppOptions{}) + app2 := app.NewRegenApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, app.DefaultNodeHome, 0, encCfg, EmptyAppOptions{}) _, err := app2.ExportAppStateAndValidators(false, []string{}) require.NoError(t, err, "ExportAppStateAndValidators should not have an error") } - -func TestGetMaccPerms(t *testing.T) { - dup := GetMaccPerms() - require.Equal(t, maccPerms, dup, "duplicated module account permissions differed from actual module account permissions") -} diff --git a/app/test_helpers.go b/app/testsuite/helpers.go similarity index 80% rename from app/test_helpers.go rename to app/testsuite/helpers.go index 1c2523688c..1b8617a183 100644 --- a/app/test_helpers.go +++ b/app/testsuite/helpers.go @@ -1,4 +1,4 @@ -package app +package testsuite import ( "testing" @@ -7,6 +7,8 @@ import ( "github.com/stretchr/testify/require" dbm "github.com/tendermint/tm-db" + "github.com/tendermint/tendermint/crypto/ed25519" + abci "github.com/tendermint/tendermint/abci/types" tmjson "github.com/tendermint/tendermint/libs/json" "github.com/tendermint/tendermint/libs/log" @@ -22,6 +24,8 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/regen-network/regen-ledger/v4/app" ) // DefaultConsensusParams defines the default Tendermint consensus params used in @@ -50,12 +54,24 @@ type SetupOptions struct { InvCheckPeriod uint HomePath string SkipUpgradeHeights map[int64]bool - EncConfig EncodingConfig + EncConfig app.EncodingConfig AppOpts types.AppOptions } +func DefaultOptions() SetupOptions { + return SetupOptions{ + Logger: log.NewNopLogger(), + DB: dbm.NewMemDB(), + InvCheckPeriod: 5, + HomePath: app.DefaultNodeHome, + SkipUpgradeHeights: map[int64]bool{}, + EncConfig: app.MakeEncodingConfig(), + AppOpts: EmptyAppOptions{}, + } +} + // NewAppWithCustomOptions initializes a new RegenApp with custom options. -func NewAppWithCustomOptions(t *testing.T, isCheckTx bool, options SetupOptions) *RegenApp { +func NewAppWithCustomOptions(t *testing.T, isCheckTx bool, options SetupOptions) *app.RegenApp { t.Helper() privVal := mock.NewPV() @@ -73,9 +89,9 @@ func NewAppWithCustomOptions(t *testing.T, isCheckTx bool, options SetupOptions) Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100000000000000))), } - app := NewRegenApp(options.Logger, options.DB, nil, true, options.SkipUpgradeHeights, options.HomePath, options.InvCheckPeriod, options.EncConfig, options.AppOpts) - genesisState := NewDefaultGenesisState(app.appCodec) - genesisState = genesisStateWithValSet(t, app, genesisState, valSet, []authtypes.GenesisAccount{acc}, balance) + regenApp := app.NewRegenApp(options.Logger, options.DB, nil, true, options.SkipUpgradeHeights, options.HomePath, options.InvCheckPeriod, options.EncConfig, options.AppOpts) + genesisState := app.NewDefaultGenesisState(regenApp.AppCodec()) + genesisState = genesisStateWithValSet(t, regenApp, genesisState, valSet, []authtypes.GenesisAccount{acc}, balance) if !isCheckTx { // init chain must be called to stop deliverState from being nil @@ -83,7 +99,7 @@ func NewAppWithCustomOptions(t *testing.T, isCheckTx bool, options SetupOptions) require.NoError(t, err) // Initialize the chain - app.InitChain( + regenApp.InitChain( abci.RequestInitChain{ Validators: []abci.ValidatorUpdate{}, ConsensusParams: DefaultConsensusParams, @@ -92,14 +108,14 @@ func NewAppWithCustomOptions(t *testing.T, isCheckTx bool, options SetupOptions) ) } - return app + return regenApp } func genesisStateWithValSet(t *testing.T, - app *RegenApp, genesisState GenesisState, + app *app.RegenApp, genesisState app.GenesisState, valSet *tmtypes.ValidatorSet, genAccs []authtypes.GenesisAccount, balances ...banktypes.Balance, -) GenesisState { +) app.GenesisState { // set genesis accounts authGenesis := authtypes.NewGenesisState(authtypes.DefaultParams(), genAccs) genesisState[authtypes.ModuleName] = app.AppCodec().MustMarshalJSON(authGenesis) @@ -166,3 +182,14 @@ type EmptyAppOptions struct{} func (ao EmptyAppOptions) Get(o string) interface{} { return nil } + +// CreateRandomAccounts is a function return a list of randomly generated AccAddresses +func CreateRandomAccounts(numAccts int) []sdk.AccAddress { + testAddrs := make([]sdk.AccAddress, numAccts) + for i := 0; i < numAccts; i++ { + pk := ed25519.GenPrivKey().PubKey() + testAddrs[i] = sdk.AccAddress(pk.Address()) + } + + return testAddrs +} diff --git a/app/testsuite/network_config.go b/app/testsuite/network_config.go index 199bdbd915..fcd643937d 100644 --- a/app/testsuite/network_config.go +++ b/app/testsuite/network_config.go @@ -25,7 +25,7 @@ func NewRegenAppConstructor(encCfg app.EncodingConfig) network.AppConstructor { return app.NewRegenApp( val.Ctx.Logger, dbm.NewMemDB(), nil, true, make(map[int64]bool), val.Ctx.Config.RootDir, 0, encCfg, - app.EmptyAppOptions{}, + EmptyAppOptions{}, baseapp.SetPruning(pruningtypes.NewPruningOptionsFromString(val.AppConfig.Pruning)), baseapp.SetMinGasPrices(val.AppConfig.MinGasPrices), ) diff --git a/app/testsuite/suite.go b/app/testsuite/suite.go new file mode 100644 index 0000000000..abc8c4113f --- /dev/null +++ b/app/testsuite/suite.go @@ -0,0 +1,34 @@ +package testsuite + +import ( + "time" + + "github.com/stretchr/testify/suite" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + tmtypes "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/regen-network/regen-ledger/v4/app" +) + +type UpgradeTestSuite struct { + suite.Suite + + App *app.RegenApp + Ctx sdk.Context + QueryHelper *baseapp.QueryServiceTestHelper + TestAccs []sdk.AccAddress +} + +// Setup sets up basic environment for suite (App, Ctx, and test accounts) +func (s *UpgradeTestSuite) Setup() { + s.App = NewAppWithCustomOptions(s.T(), false, DefaultOptions()) + s.Ctx = s.App.BaseApp.NewContext(false, tmtypes.Header{Height: 1, ChainID: "regen-1", Time: time.Now().UTC()}) + s.QueryHelper = &baseapp.QueryServiceTestHelper{ + GRPCQueryRouter: s.App.GRPCQueryRouter(), + Ctx: s.Ctx, + } + + s.TestAccs = CreateRandomAccounts(3) +} diff --git a/app/upgrades/upgrade.go b/app/upgrades/upgrade.go new file mode 100644 index 0000000000..07e6ca8155 --- /dev/null +++ b/app/upgrades/upgrade.go @@ -0,0 +1,22 @@ +package upgrades + +import ( + "github.com/cosmos/cosmos-sdk/store/types" + "github.com/cosmos/cosmos-sdk/types/module" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" +) + +// Upgrade defines a struct containing necessary fields that a SoftwareUpgradeProposal +// must have written, in order for the state migration to go smoothly. +// An upgrade must implement this struct, and then set it in the app.go. +// The app.go will then define the handler. +type Upgrade struct { + // Upgrade version name, for the upgrade handler, e.g. `v7` + UpgradeName string + + // CreateUpgradeHandler defines the function that creates an upgrade handler + CreateUpgradeHandler func(*module.Manager, module.Configurator) upgradetypes.UpgradeHandler + + // Store upgrades, should be used for any new modules introduced, new modules deleted, or store names renamed. + StoreUpgrades types.StoreUpgrades +} diff --git a/app/upgrades/v5/upgrade.go b/app/upgrades/v5/upgrade.go new file mode 100644 index 0000000000..5a03bf97e0 --- /dev/null +++ b/app/upgrades/v5/upgrade.go @@ -0,0 +1,72 @@ +package v5 + +import ( + "fmt" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/group" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + ica "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts" + icacontrollertypes "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/controller/types" + icahosttypes "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/host/types" + icatypes "github.com/cosmos/ibc-go/v5/modules/apps/27-interchain-accounts/types" + + "github.com/regen-network/regen-ledger/v4/app/upgrades" + "github.com/regen-network/regen-ledger/x/data" + "github.com/regen-network/regen-ledger/x/ecocredit" +) + +const Name = "v5" + +var Upgrade = upgrades.Upgrade{ + UpgradeName: Name, + CreateUpgradeHandler: func(mm *module.Manager, cfg module.Configurator) upgradetypes.UpgradeHandler { + return func(ctx sdk.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + // set regen module consensus version + fromVM[ecocredit.ModuleName] = 2 + fromVM[data.ModuleName] = 1 + + // save oldIcaVersion, so we can skip icahost.InitModule in longer term tests. + oldIcaVersion := fromVM[icatypes.ModuleName] + + // Add Interchain Accounts host module + // set the ICS27 consensus version so InitGenesis is not run + fromVM[icatypes.ModuleName] = mm.Modules[icatypes.ModuleName].ConsensusVersion() + + // create ICS27 Controller submodule params, controller module not enabled. + controllerParams := icacontrollertypes.Params{ControllerEnabled: false} + + // create ICS27 Host submodule params, host module not enabled. + hostParams := icahosttypes.Params{ + HostEnabled: false, + AllowMessages: []string{}, + } + + mod, found := mm.Modules[icatypes.ModuleName] + if !found { + panic(fmt.Sprintf("module %s is not in the module manager", icatypes.ModuleName)) + } + + icaMod, ok := mod.(ica.AppModule) + if !ok { + panic(fmt.Sprintf("expected module %s to be type %T, got %T", icatypes.ModuleName, ica.AppModule{}, mod)) + } + + // skip InitModule in upgrade tests after the upgrade has gone through. + if oldIcaVersion != fromVM[icatypes.ModuleName] { + icaMod.InitModule(ctx, controllerParams, hostParams) + } + + // transfer module consensus version has been bumped to 2 + return mm.RunMigrations(ctx, cfg, fromVM) + } + }, + StoreUpgrades: storetypes.StoreUpgrades{ + Added: []string{ + group.ModuleName, + icahosttypes.StoreKey, + }, + }, +} diff --git a/app/upgrades/v5/upgrade_test.go b/app/upgrades/v5/upgrade_test.go new file mode 100644 index 0000000000..7d69f2f0f4 --- /dev/null +++ b/app/upgrades/v5/upgrade_test.go @@ -0,0 +1,50 @@ +package v5_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + sdk "github.com/cosmos/cosmos-sdk/types" + + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/regen-network/regen-ledger/v4/app/testsuite" + "github.com/regen-network/regen-ledger/x/ecocredit" + ecocreditv1 "github.com/regen-network/regen-ledger/x/ecocredit/base/types/v1" +) + +type UpgradeTestSuite struct { + testsuite.UpgradeTestSuite +} + +func TestUpgrade(t *testing.T) { + suite.Run(t, new(UpgradeTestSuite)) +} + +const dummyUpgradeHeight = 5 + +func (suite *UpgradeTestSuite) TestV5Upgrade() { + suite.Setup() + + suite.Ctx = suite.Ctx.WithBlockHeight(dummyUpgradeHeight - 1) + plan := upgradetypes.Plan{Name: "v5", Height: dummyUpgradeHeight} + err := suite.App.UpgradeKeeper.ScheduleUpgrade(suite.Ctx, plan) + suite.Require().NoError(err) + _, exists := suite.App.UpgradeKeeper.GetUpgradePlan(suite.Ctx) + suite.Require().True(exists) + + // force the app to have params so migration does not fail + ss, ok := suite.App.ParamsKeeper.GetSubspace(ecocredit.ModuleName) + assert.True(suite.T(), ok) + fees := sdk.NewCoins(sdk.NewInt64Coin("uregen", 10)) + ss.SetParamSet(suite.Ctx, &ecocreditv1.Params{CreditClassFee: fees, BasketFee: fees}) + + suite.Ctx = suite.Ctx.WithBlockHeight(dummyUpgradeHeight) + suite.Require().NotPanics(func() { + beginBlockRequest := abci.RequestBeginBlock{} + suite.App.BeginBlocker(suite.Ctx, beginBlockRequest) + }) +}