diff --git a/simapp/app.go b/simapp/app.go index 7d62f2e84..e3771e2fc 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -339,7 +339,7 @@ func NewSimApp( app.mm.RegisterInvariants(&app.CrisisKeeper) app.mm.RegisterRoutes(app.Router(), app.QueryRouter(), encodingConfig.Amino) - app.configurator = module.NewConfigurator(app.MsgServiceRouter(), app.GRPCQueryRouter()) + app.configurator = module.NewConfigurator(app.appCodec, app.MsgServiceRouter(), app.GRPCQueryRouter()) app.mm.RegisterServices(app.configurator) // add test gRPC service for testing gRPC queries in isolation @@ -536,22 +536,6 @@ func (app *SimApp) RegisterTendermintService(clientCtx client.Context) { tmservice.RegisterTendermintService(app.BaseApp.GRPCQueryRouter(), clientCtx, app.interfaceRegistry) } -// RunMigrations performs in-place store migrations for all modules. This -// function MUST be only called by x/upgrade UpgradeHandler. -// -// `migrateFromVersions` is a map of moduleName to fromVersion (unit64), where -// fromVersion denotes the version from which we should migrate the module, the -// target version being the module's latest ConsensusVersion. -// -// Example: -// cfg := module.NewConfigurator(...) -// app.UpgradeKeeper.SetUpgradeHandler("store-migration", func(ctx sdk.Context, plan upgradetypes.Plan, vm module.VersionMap) { -// return app.RunMigrations(ctx, vm) -// }) -func (app *SimApp) RunMigrations(ctx sdk.Context, migrateFromVersions module.VersionMap) (module.VersionMap, error) { - return app.mm.RunMigrations(ctx, app.configurator, migrateFromVersions) -} - // RegisterSwaggerAPI registers swagger route with API Server func RegisterSwaggerAPI(ctx client.Context, rtr *mux.Router) { statikFS, err := fs.New() diff --git a/simapp/app_test.go b/simapp/app_test.go index c4aa9885b..2f9c5e2c3 100644 --- a/simapp/app_test.go +++ b/simapp/app_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" @@ -12,11 +13,13 @@ import ( dbm "github.com/tendermint/tm-db" "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/tests/mocks" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/vesting" "github.com/cosmos/cosmos-sdk/x/authz" + "github.com/cosmos/cosmos-sdk/x/bank" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/capability" "github.com/cosmos/cosmos-sdk/x/crisis" @@ -80,7 +83,7 @@ func TestRunMigrations(t *testing.T) { bApp.SetCommitMultiStoreTracer(nil) bApp.SetInterfaceRegistry(encCfg.InterfaceRegistry) app.BaseApp = bApp - app.configurator = module.NewConfigurator(app.MsgServiceRouter(), app.GRPCQueryRouter()) + app.configurator = module.NewConfigurator(app.appCodec, app.MsgServiceRouter(), app.GRPCQueryRouter()) // We register all modules on the Configurator, except x/bank. x/bank will // serve as the test subject on which we run the migration tests. @@ -160,8 +163,8 @@ func TestRunMigrations(t *testing.T) { // Run migrations only for bank. That's why we put the initial // version for bank as 1, and for all other modules, we put as // their latest ConsensusVersion. - _, err = app.RunMigrations( - app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}), + _, err = app.mm.RunMigrations( + app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}), app.configurator, module.VersionMap{ "bank": 1, "auth": auth.AppModule{}.ConsensusVersion(), @@ -185,12 +188,58 @@ func TestRunMigrations(t *testing.T) { require.EqualError(t, err, tc.expRunErrMsg) } else { require.NoError(t, err) + // Make sure bank's migration is called. require.Equal(t, tc.expCalled, called) } }) } } +func TestInitGenesisOnMigration(t *testing.T) { + db := dbm.NewMemDB() + encCfg := MakeTestEncodingConfig() + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + app := NewSimApp(logger, db, nil, true, map[int64]bool{}, DefaultNodeHome, 0, encCfg, EmptyAppOptions{}) + ctx := app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}) + + // Create a mock module. This module will serve as the new module we're + // adding during a migration. + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + mockModule := mocks.NewMockAppModule(mockCtrl) + mockDefaultGenesis := json.RawMessage(`{"key": "value"}`) + mockModule.EXPECT().DefaultGenesis(gomock.Eq(app.appCodec)).Times(1).Return(mockDefaultGenesis) + mockModule.EXPECT().InitGenesis(gomock.Eq(ctx), gomock.Eq(app.appCodec), gomock.Eq(mockDefaultGenesis)).Times(1).Return(nil) + + app.mm.Modules["mock"] = mockModule + + // Run migrations only for "mock" module. That's why we put the initial + // version for bank as 0 (to run its InitGenesis), and for all other + // modules, we put their latest ConsensusVersion to skip migrations. + _, err := app.mm.RunMigrations(ctx, app.configurator, + module.VersionMap{ + "mock": 0, + "bank": bank.AppModule{}.ConsensusVersion(), + "auth": auth.AppModule{}.ConsensusVersion(), + "authz": authz.AppModule{}.ConsensusVersion(), + "staking": staking.AppModule{}.ConsensusVersion(), + "mint": mint.AppModule{}.ConsensusVersion(), + "distribution": distribution.AppModule{}.ConsensusVersion(), + "slashing": slashing.AppModule{}.ConsensusVersion(), + "gov": gov.AppModule{}.ConsensusVersion(), + "params": params.AppModule{}.ConsensusVersion(), + "upgrade": upgrade.AppModule{}.ConsensusVersion(), + "vesting": vesting.AppModule{}.ConsensusVersion(), + "feegrant": feegrant.AppModule{}.ConsensusVersion(), + "evidence": evidence.AppModule{}.ConsensusVersion(), + "crisis": crisis.AppModule{}.ConsensusVersion(), + "genutil": genutil.AppModule{}.ConsensusVersion(), + "capability": capability.AppModule{}.ConsensusVersion(), + }, + ) + require.NoError(t, err) +} + func TestUpgradeStateOnGenesis(t *testing.T) { encCfg := MakeTestEncodingConfig() db := dbm.NewMemDB() diff --git a/types/module/configurator.go b/types/module/configurator.go index 0e766df2a..f1e16f7e7 100644 --- a/types/module/configurator.go +++ b/types/module/configurator.go @@ -3,6 +3,7 @@ package module import ( "github.com/gogo/protobuf/grpc" + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) @@ -34,6 +35,7 @@ type Configurator interface { } type configurator struct { + cdc codec.Marshaler msgServer grpc.Server queryServer grpc.Server @@ -42,8 +44,9 @@ type configurator struct { } // NewConfigurator returns a new Configurator instance -func NewConfigurator(msgServer grpc.Server, queryServer grpc.Server) Configurator { +func NewConfigurator(cdc codec.Marshaler, msgServer grpc.Server, queryServer grpc.Server) Configurator { return configurator{ + cdc: cdc, msgServer: msgServer, queryServer: queryServer, migrations: map[string]map[uint64]MigrationHandler{}, diff --git a/types/module/module.go b/types/module/module.go index d123a05d1..df62ea8f0 100644 --- a/types/module/module.go +++ b/types/module/module.go @@ -337,7 +337,51 @@ type MigrationHandler func(sdk.Context) error // version from which we should perform the migration for each module. type VersionMap map[string]uint64 -// RunMigrations performs in-place store migrations for all modules. +// RunMigrations performs in-place store migrations for all modules. This +// function MUST be called insde an x/upgrade UpgradeHandler. +// +// Recall that in an upgrade handler, the `fromVM` VersionMap is retrieved from +// x/upgrade's store, and the function needs to return the target VersionMap +// that will in turn be persisted to the x/upgrade's store. In general, +// returning RunMigrations should be enough: +// +// Example: +// cfg := module.NewConfigurator(...) +// app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx sdk.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { +// return app.mm.RunMigrations(ctx, cfg, fromVM) +// }) +// +// Internally, RunMigrations will perform the following steps: +// - create an `updatedVM` VersionMap of module with their latest ConsensusVersion +// - make a diff of `fromVM` and `udpatedVM`, and for each module: +// - if the module's `fromVM` version is less than its `updatedVM` version, +// then run in-place store migrations for that module between those versions. +// - if the module's `fromVM` is 0 (which means that it's a new module, +// because it was not in the previous x/upgrade's store), then run +// `InitGenesis` on that module. +// - return the `updatedVM` to be persisted in the x/upgrade's store. +// +// As an app developer, if you wish to skip running InitGenesis for your new +// module "foo", you need to manually pass a `fromVM` argument to this function +// foo's module version set to its latest ConsensusVersion. That way, the diff +// between the function's `fromVM` and `udpatedVM` will be empty, hence not +// running anything for foo. +// +// Example: +// cfg := module.NewConfigurator(...) +// app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx sdk.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { +// // Assume "foo" is a new module. +// // `fromVM` is fetched from existing x/upgrade store. Since foo didn't exist +// // before this upgrade, `fromVM["foo"] == 0`, and RunMigration will by default +// // run InitGenesis on foo. +// // To skip running foo's InitGenesis, you need set `fromVM`'s foo to its latest +// // consensus version: +// fromVM["foo"] = foo.AppModule{}.ConsensusVersion() +// +// return app.mm.RunMigrations(ctx, cfg, fromVM) +// }) +// +// Please also refer to docs/core/upgrade.md for more information. func (m Manager) RunMigrations(ctx sdk.Context, cfg Configurator, fromVM VersionMap) (VersionMap, error) { c, ok := cfg.(configurator) if !ok { @@ -349,13 +393,34 @@ func (m Manager) RunMigrations(ctx sdk.Context, cfg Configurator, fromVM Version fromVersion := fromVM[moduleName] toVersion := module.ConsensusVersion() - // only run migrations when the from version is > 0 - // from version will be 0 when a new module is added and migrations shouldn't be run in this case + // Only run migrations when the fromVersion is > 0, or run InitGenesis + // if fromVersion == 0. + // + // fromVersion will be 0 in two cases: + // 1. If a new module is added. In this case we run InitGenesis with an + // empty genesis state. + // 2. If the app developer is running in-place store migrations for the + // first time. In this case, it is the app developer's responsibility + // to set their module's fromVersions to a version that suits them. if fromVersion > 0 { err := c.runModuleMigrations(ctx, moduleName, fromVersion, toVersion) if err != nil { return nil, err } + } else { + cfgtor, ok := cfg.(configurator) + if !ok { + // Currently, the only implementator of Configurator (the interface) + // is configurator (the struct). + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "expected %T, got %T", configurator{}, cfg) + } + + moduleValUpdates := module.InitGenesis(ctx, cfgtor.cdc, module.DefaultGenesis(cfgtor.cdc)) + // The module manager assumes only one module will update the + // validator set, and that it will not be by a new module. + if len(moduleValUpdates) > 0 { + return nil, sdkerrors.Wrapf(sdkerrors.ErrLogic, "validator InitGenesis updates already set by a previous module") + } } updatedVM[moduleName] = toVersion diff --git a/types/module/module_test.go b/types/module/module_test.go index 630c57619..9f7a21f5c 100644 --- a/types/module/module_test.go +++ b/types/module/module_test.go @@ -179,7 +179,9 @@ func TestManager_RegisterQueryServices(t *testing.T) { msgRouter := mocks.NewMockServer(mockCtrl) queryRouter := mocks.NewMockServer(mockCtrl) - cfg := module.NewConfigurator(msgRouter, queryRouter) + interfaceRegistry := types.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(interfaceRegistry) + cfg := module.NewConfigurator(cdc, msgRouter, queryRouter) mockAppModule1.EXPECT().RegisterServices(cfg).Times(1) mockAppModule2.EXPECT().RegisterServices(cfg).Times(1) diff --git a/x/upgrade/types/handler.go b/x/upgrade/types/handler.go index 0dccaefeb..0543b6bbe 100644 --- a/x/upgrade/types/handler.go +++ b/x/upgrade/types/handler.go @@ -5,5 +5,22 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" ) -// UpgradeHandler specifies the type of function that is called when an upgrade is applied -type UpgradeHandler func(ctx sdk.Context, plan Plan, vm module.VersionMap) (module.VersionMap, error) +// UpgradeHandler specifies the type of function that is called when an upgrade +// is applied. +// +// `fromVM` is a VersionMap of moduleName to fromVersion (unit64), where +// fromVersion denotes the version from which we should migrate the module, the +// target version being the module's latest version in the return VersionMap, +// let's call it `toVM`. +// +// `fromVM` is retrieved from x/upgrade's store, whereas `toVM` is chosen +// arbitrarily by the app developer (and persisted to x/upgrade's store right +// after the upgrade handler runs). In general, `toVM` should map all modules +// to their latest ConsensusVersion so that x/upgrade can track each module's +// latest ConsensusVersion; `fromVM` can be left as-is, but can also be +// modified inside the upgrade handler, e.g. to skip running InitGenesis or +// migrations for certain modules when calling the `module.Manager#RunMigrations` +// function. +// +// Please also refer to docs/core/upgrade.md for more information. +type UpgradeHandler func(ctx sdk.Context, plan Plan, fromVM module.VersionMap) (module.VersionMap, error)