From f195fbc0972f713474b3798311593c86a3aa9d8a Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 26 Mar 2019 14:26:40 -0400 Subject: [PATCH 01/62] Add upgrade module --- x/upgrade/client/cli/query.go | 37 +++++++++ x/upgrade/client/rest/rest.go | 40 ++++++++++ x/upgrade/codec.go | 10 +++ x/upgrade/doc.go | 48 +++++++++++ x/upgrade/keeper.go | 137 ++++++++++++++++++++++++++++++++ x/upgrade/keeper_test.go | 145 ++++++++++++++++++++++++++++++++++ x/upgrade/types.go | 45 +++++++++++ 7 files changed, 462 insertions(+) create mode 100644 x/upgrade/client/cli/query.go create mode 100644 x/upgrade/client/rest/rest.go create mode 100644 x/upgrade/codec.go create mode 100644 x/upgrade/doc.go create mode 100644 x/upgrade/keeper.go create mode 100644 x/upgrade/keeper_test.go create mode 100644 x/upgrade/types.go diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go new file mode 100644 index 000000000000..5694b6c83299 --- /dev/null +++ b/x/upgrade/client/cli/query.go @@ -0,0 +1,37 @@ +package cli + +import ( + "fmt" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/upgrade" + "github.com/spf13/cobra" +) + +// GetQueryCmd creates a query sub-command for the upgrade module using cmdName as the name of the sub-command. +func GetQueryCmd(cmdName string, storeName string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: cmdName, + Short: "get upgrade plan (if one exists)", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + res, err := cliCtx.QueryStore([]byte(upgrade.PlanKey), storeName) + if err != nil { + return err + } + + if len(res) == 0 { + return fmt.Errorf("no upgrade scheduled") + } + + var plan upgrade.Plan + err = cdc.UnmarshalBinaryBare(res, &plan) + if err != nil { + return err + } + return cliCtx.PrintOutput(plan) + }, + } +} diff --git a/x/upgrade/client/rest/rest.go b/x/upgrade/client/rest/rest.go new file mode 100644 index 000000000000..00b4c23cb8a3 --- /dev/null +++ b/x/upgrade/client/rest/rest.go @@ -0,0 +1,40 @@ +package rest + +import ( + "fmt" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/upgrade" + "github.com/gorilla/mux" + "net/http" +) + +// RegisterRoutes registers REST routes for the upgrade module under the path specified by routeName. +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, routeName string, storeName string) { + r.HandleFunc(fmt.Sprintf("/%s", routeName), getUpgradePlanHandler(cdc, cliCtx, storeName)).Methods("GET") +} + +func getUpgradePlanHandler(cdc *codec.Codec, cliCtx context.CLIContext, storeName string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, request *http.Request) { + res, err := cliCtx.QueryStore([]byte(upgrade.PlanKey), storeName) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + if len(res) == 0 { + http.NotFound(w, request) + return + } + + var plan upgrade.Plan + err = cdc.UnmarshalBinaryBare(res, &plan) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + rest.PostProcessResponse(w, cdc, plan, cliCtx.Indent) + } +} diff --git a/x/upgrade/codec.go b/x/upgrade/codec.go new file mode 100644 index 000000000000..91974577879f --- /dev/null +++ b/x/upgrade/codec.go @@ -0,0 +1,10 @@ +package upgrade + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +// RegisterCodec registers concrete types on the Amino codec +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(Plan{}, "upgrade/Plan", nil) +} diff --git a/x/upgrade/doc.go b/x/upgrade/doc.go new file mode 100644 index 000000000000..fff0a704e46b --- /dev/null +++ b/x/upgrade/doc.go @@ -0,0 +1,48 @@ +/* +Package upgrade provides a Cosmos SDK module that can be used for smoothly upgrading a live Cosmos chain to a +new software version. It accomplishes this by providing a BeginBlocker hook that prevents the blockchain state +machine from proceeding once a pre-defined upgrade block time or height has been reached. The module does not prescribe +anything regarding how governance decides to do an upgrade, but just the mechanism for coordinating the upgrade safely. +Without software support for upgrades, upgrading a live chain is risky because all of the validators need to pause +their state machines at exactly the same point in the process. If this is not done correctly, there can be state +inconsistencies which are hard to recover from. + +Integrating With An App + +Setup an upgrade Keeper for the app and then define a BeginBlocker that calls the upgrade +keeper's BeginBlocker method: + func (app *myApp) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { + app.upgradeKeeper.BeginBlocker(ctx, req) + return abci.ResponseBeginBlock{} + } + +The app must then integrate the upgrade keeper with its governance module as appropriate. The governance module +should call ScheduleUpgrade to schedule an upgrade and ClearUpgradePlan to cancel a pending upgrade. + +Performing Upgrades + +Upgrades can be scheduled at either a predefined block height or time. Once this block height or time is reached, the +existing software will cease to process ABCI messages and a new version with code that handles the upgrade must be deployed. +All upgrades are coordinated by a unique upgrade name that cannot be reused on the same blockchain. In order for the upgrade +module to know that the upgrade has been safely applied, a handler with the name of the upgrade must be installed. +Here is an example handler for an upgrade named "my-fancy-upgrade": + app.upgradeKeeper.SetUpgradeHandler("my-fancy-upgrade", func(ctx sdk.Context, plan upgrade.Plan) { + // Perform any migrations of the state store needed for this upgrade + }) + +This upgrade handler performs the dual function of alerting the upgrade module that the named upgrade has been applied, +as well as providing the opportunity for the upgraded software to perform any necessary state migrations. + +Default and Custom Shutdown Behavior + +Before "crashing" the ABCI state machine in the BeginBlocker method, the upgrade module will log an error +that looks like: + UPGRADE "" NEEDED at height : +where Name are Info are the values of the respective fields on the upgrade Plan. + +The default method for "crashing" the state machine is to panic with an error which will stop the state machine +but usually not exit the process. This behavior can be overridden using the keeper's SetDoShutdowner method. A custom +shutdown method could perform some other steps such as alerting another running process that an upgrade is needed, +as well as crashing the program by another method such as os.Exit(). +*/ +package upgrade diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go new file mode 100644 index 000000000000..bdd67faccd02 --- /dev/null +++ b/x/upgrade/keeper.go @@ -0,0 +1,137 @@ +package upgrade + +import ( + "fmt" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +// Keeper of the upgrade module +type Keeper struct { + storeKey sdk.StoreKey + cdc *codec.Codec + doShutdowner func(sdk.Context, Plan) + upgradeHandlers map[string]Handler +} + +const ( + // PlanKey specifies the key under which an upgrade plan is stored in the store + PlanKey = "plan" +) + +// NewKeeper constructs an upgrade keeper +func NewKeeper(storeKey sdk.StoreKey, cdc *codec.Codec) Keeper { + return Keeper{ + storeKey: storeKey, + cdc: cdc, + upgradeHandlers: map[string]Handler{}, + } +} + +// SetUpgradeHandler sets an UpgradeHandler for the upgrade specified by name. This handler will be called when the upgrade +// with this name is applied. In order for an upgrade with the given name to proceed, a handler for this upgrade +// must be set even if it is a no-op function. +func (keeper Keeper) SetUpgradeHandler(name string, upgradeHandler Handler) { + keeper.upgradeHandlers[name] = upgradeHandler +} + +// ScheduleUpgrade schedules an upgrade based on the specified plan +func (keeper Keeper) ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error { + err := plan.ValidateBasic() + if err != nil { + return err + } + if !plan.Time.IsZero() { + if !plan.Time.After(ctx.BlockHeader().Time) { + return sdk.ErrUnknownRequest("Upgrade cannot be scheduled in the past") + } + if plan.Height != 0 { + return sdk.ErrUnknownRequest("Only one of Time or Height should be specified") + } + } else { + if plan.Height <= ctx.BlockHeight() { + return sdk.ErrUnknownRequest("Upgrade cannot be scheduled in the past") + } + } + store := ctx.KVStore(keeper.storeKey) + if store.Has(upgradeDoneKey(plan.Name)) { + return sdk.ErrUnknownRequest(fmt.Sprintf("Upgrade with name %s has already been completed", plan.Name)) + } + bz := keeper.cdc.MustMarshalBinaryBare(plan) + store.Set([]byte(PlanKey), bz) + return nil +} + +// ClearUpgradePlan clears any schedule upgrade +func (keeper Keeper) ClearUpgradePlan(ctx sdk.Context) { + store := ctx.KVStore(keeper.storeKey) + store.Delete([]byte(PlanKey)) +} + +// ValidateBasic does basic validation of a Plan +func (plan Plan) ValidateBasic() sdk.Error { + if len(plan.Name) == 0 { + return sdk.ErrUnknownRequest("Name cannot be empty") + + } + return nil +} + +// GetUpgradePlan returns the currently scheduled Plan if any, setting havePlan to true if there is a scheduled +// upgrade or false if there is none +func (keeper Keeper) GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get([]byte(PlanKey)) + if bz == nil { + return plan, false + } + keeper.cdc.MustUnmarshalBinaryBare(bz, &plan) + return plan, true +} + +// SetDoShutdowner sets a custom shutdown function for the upgrade module. This shutdown +// function will be called during the BeginBlock method when an upgrade is required +// instead of panic'ing which is the default behavior +func (keeper *Keeper) SetDoShutdowner(doShutdowner func(ctx sdk.Context, plan Plan)) { + keeper.doShutdowner = doShutdowner +} + +func upgradeDoneKey(name string) []byte { + return []byte(fmt.Sprintf("done/%s", name)) +} + +// BeginBlocker should be called inside the BeginBlocker method of any app using the upgrade module +func (keeper *Keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) { + blockTime := ctx.BlockHeader().Time + blockHeight := ctx.BlockHeight() + + plan, havePlan := keeper.GetUpgradePlan(ctx) + if !havePlan { + return + } + + upgradeTime := plan.Time + upgradeHeight := plan.Height + if (!upgradeTime.IsZero() && !blockTime.Before(upgradeTime)) || upgradeHeight <= blockHeight { + handler, ok := keeper.upgradeHandlers[plan.Name] + if ok { + // We have an upgrade handler for this upgrade name, so apply the upgrade + ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at height %d", plan.Name, blockHeight)) + handler(ctx, plan) + keeper.ClearUpgradePlan(ctx) + // Mark this upgrade name as being done so the name can't be reused accidentally + store := ctx.KVStore(keeper.storeKey) + store.Set(upgradeDoneKey(plan.Name), []byte("1")) + } else { + // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown + ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", plan.Name, blockHeight, plan.Info)) + doShutdowner := keeper.doShutdowner + if doShutdowner != nil { + doShutdowner(ctx, plan) + } else { + panic("UPGRADE REQUIRED!") + } + } + } +} diff --git a/x/upgrade/keeper_test.go b/x/upgrade/keeper_test.go new file mode 100644 index 000000000000..50333d90cc9b --- /dev/null +++ b/x/upgrade/keeper_test.go @@ -0,0 +1,145 @@ +package upgrade + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/libs/log" + "testing" + "time" +) + +type TestSuite struct { + suite.Suite + keeper Keeper + ctx sdk.Context + cms store.CommitMultiStore +} + +func (s *TestSuite) SetupTest() { + db := dbm.NewMemDB() + s.cms = store.NewCommitMultiStore(db) + key := sdk.NewKVStoreKey("upgrade") + cdc := codec.New() + RegisterCodec(cdc) + s.keeper = NewKeeper(key, cdc) + s.cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db) + _ = s.cms.LoadLatestVersion() + s.ctx = sdk.NewContext(s.cms, abci.Header{Height: 10, Time: time.Now()}, false, log.NewNopLogger()) +} + +func (s *TestSuite) TestRequireName() { + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{}) + s.Require().NotNil(err) + s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) +} + +func (s *TestSuite) TestRequireFutureTime() { + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: s.ctx.BlockHeader().Time}) + s.Require().NotNil(err) + s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) +} + +func (s *TestSuite) TestRequireFutureBlock() { + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight()}) + s.Require().NotNil(err) + s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) +} + +func (s *TestSuite) TestCantSetBothTimeAndHeight() { + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now(), Height: s.ctx.BlockHeight() + 1}) + s.Require().NotNil(err) + s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) +} + +func (s *TestSuite) TestDoTimeUpgrade() { + s.T().Log("Verify can schedule an upgrade") + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) + s.Require().Nil(err) + + s.VerifyDoUpgrade() +} + +func (s *TestSuite) TestDoHeightUpgrade() { + s.T().Log("Verify can schedule an upgrade") + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}) + s.Require().Nil(err) + + s.VerifyDoUpgrade() +} + +func (s *TestSuite) VerifyDoUpgrade() { + s.T().Log("Verify that a panic happens at the upgrade time/height") + newCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 1, Time: time.Now()}, false, log.NewNopLogger()) + req := abci.RequestBeginBlock{Header: newCtx.BlockHeader()} + s.Require().Panics(func() { + s.keeper.BeginBlocker(newCtx, req) + }) + + s.T().Log("Verify that the upgrade can be successfully applied with a handler") + s.keeper.SetUpgradeHandler("test", func(ctx sdk.Context, plan Plan) {}) + s.Require().NotPanics(func() { + s.keeper.BeginBlocker(newCtx, req) + }) + + s.VerifyCleared(newCtx) +} + +func (s *TestSuite) VerifyCleared(newCtx sdk.Context) { + s.T().Log("Verify that the upgrade plan has been cleared") + _, havePlan := s.keeper.GetUpgradePlan(newCtx) + s.Require().False(havePlan) +} + +func (s *TestSuite) TestCanClear() { + s.T().Log("Verify upgrade is scheduled") + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) + s.Require().Nil(err) + + s.keeper.ClearUpgradePlan(s.ctx) + + s.VerifyCleared(s.ctx) +} + +func (s *TestSuite) TestCantApplySameUpgradeTwice() { + s.TestDoTimeUpgrade() + s.T().Log("Verify an upgrade named \"test\" can't be scheduled twice") + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) + s.Require().NotNil(err) + s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) +} + +func (s *TestSuite) TestDoShutdowner() { + s.T().Log("Set a custom DoShutdowner") + shutdownerCalled := false + s.keeper.SetDoShutdowner(func(ctx sdk.Context, plan Plan) { + shutdownerCalled = true + }) + + s.T().Log("Run an upgrade and verify that the custom shutdowner was called and no panic happened") + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) + s.Require().Nil(err) + + header := abci.Header{Height: s.ctx.BlockHeight() + 1, Time: time.Now()} + newCtx := sdk.NewContext(s.cms, header, false, log.NewNopLogger()) + req := abci.RequestBeginBlock{Header: header} + s.Require().NotPanics(func() { + s.keeper.BeginBlocker(newCtx, req) + }) + s.Require().True(shutdownerCalled) +} + +func (s *TestSuite) TestNoSpuriousUpgrades() { + s.T().Log("Verify that no upgrade panic is triggered in the BeginBlocker when we haven't scheduled an upgrade") + req := abci.RequestBeginBlock{Header: s.ctx.BlockHeader()} + s.Require().NotPanics(func() { + s.keeper.BeginBlocker(s.ctx, req) + }) +} + +func TestTestSuite(t *testing.T) { + suite.Run(t, new(TestSuite)) +} diff --git a/x/upgrade/types.go b/x/upgrade/types.go new file mode 100644 index 000000000000..fa9b2a45f30e --- /dev/null +++ b/x/upgrade/types.go @@ -0,0 +1,45 @@ +package upgrade + +import ( + "fmt" + "time" +) +import sdk "github.com/cosmos/cosmos-sdk/types" + +// Plan specifies information about a planned upgrade and when it should occur +type Plan struct { + // Sets the name for the upgrade. This name will be used by the upgraded version of the software to apply any + // special "on-upgrade" commands during the first BeginBlock method after the upgrade is applied. It is also used + // to detect whether a software version can handle a given upgrade. If no upgrade handler with this name has been + // set in the software, it will be assumed that the software is out-of-date when the upgrade Time or Height + // is reached and the software will exit. + Name string `json:"name,omitempty"` + + // The time after which the upgrade must be performed. + // Leave set to its zero value to use a pre-defined Height instead. + Time time.Time `json:"time,omitempty"` + + // The height at which the upgrade must be performed. + // Only used if Time is not set. + Height int64 `json:"height,omitempty"` + + // Any application specific upgrade info to be included on-chain + // such as a git commit that validators could automatically upgrade to + Info string `json:"info,omitempty"` +} + +// Handler specifies the type of function that is called when an upgrade is applied +type Handler func(ctx sdk.Context, plan Plan) + +func (plan Plan) String() string { + var whenStr string + if !plan.Time.IsZero() { + whenStr = fmt.Sprintf("Time: %s", plan.Time.Format(time.RFC3339)) + } else { + whenStr = fmt.Sprintf("Height: %d", plan.Height) + } + return fmt.Sprintf(`Upgrade Plan + Name: %s + %s + Info: %s`, plan.Name, whenStr, plan.Info) +} From 41cc4af2e1a935d31bf91e617afef3c55ee32e91 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 26 Mar 2019 14:53:32 -0400 Subject: [PATCH 02/62] Added pending changelog entry --- .pending/features/sdk/Add-upgrade-module-t | 1 + 1 file changed, 1 insertion(+) create mode 100644 .pending/features/sdk/Add-upgrade-module-t diff --git a/.pending/features/sdk/Add-upgrade-module-t b/.pending/features/sdk/Add-upgrade-module-t new file mode 100644 index 000000000000..2d5bd773a98e --- /dev/null +++ b/.pending/features/sdk/Add-upgrade-module-t @@ -0,0 +1 @@ +#3979 Add upgrade module that coordinates software upgrades of live chains. \ No newline at end of file From cada0e469d813f793ea15d48d86efa403e4b6fa8 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 28 Mar 2019 12:45:44 -0400 Subject: [PATCH 03/62] Add cache behavior to upgrade Keeper, bring package test coverage to 100% --- x/upgrade/keeper.go | 116 ++++++++++++++++++++++++++------------- x/upgrade/keeper_test.go | 44 ++++++++++++--- 2 files changed, 115 insertions(+), 45 deletions(-) diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index bdd67faccd02..d50fe0c0ea6e 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -8,11 +8,47 @@ import ( ) // Keeper of the upgrade module -type Keeper struct { +type Keeper interface { + // ScheduleUpgrade schedules an upgrade based on the specified plan + ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error + + // SetUpgradeHandler sets an UpgradeHandler for the upgrade specified by name. This handler will be called when the upgrade + // with this name is applied. In order for an upgrade with the given name to proceed, a handler for this upgrade + // must be set even if it is a no-op function. + SetUpgradeHandler(name string, upgradeHandler Handler) + + // ClearUpgradePlan clears any schedule upgrade + ClearUpgradePlan(ctx sdk.Context) + + // GetUpgradePlan returns the currently scheduled Plan if any, setting havePlan to true if there is a scheduled + // upgrade or false if there is none + GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) + + // SetWillUpgrader sets a custom function to be run whenever an upgrade is scheduled. This + // can be used to notify the node that an upgrade will be happen in the future so that it + // can download any software ahead of time in the background. + // It does not indicate that an upgrade is happening now and should just be used for preparation, + // not the actual upgrade. + SetWillUpgrader(willUpgrader func(ctx sdk.Context, plan Plan)) + + // SetOnUpgrader sets a custom function to be called right before the chain halts and the + // upgrade needs to be applied. This can be used to initiate an automatic upgrade process. + SetOnUpgrader(onUpgrader func(ctx sdk.Context, plan Plan)) + + // BeginBlocker should be called inside the BeginBlocker method of any app using the upgrade module. Scheduled upgrade + // plans are cached in memory so the overhead of this method is trivial. + BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) +} + +type keeper struct { storeKey sdk.StoreKey cdc *codec.Codec - doShutdowner func(sdk.Context, Plan) upgradeHandlers map[string]Handler + haveCache bool + haveCachedPlan bool + plan Plan + willUpgrader func(ctx sdk.Context, plan Plan) + onUpgrader func(ctx sdk.Context, plan Plan) } const ( @@ -22,22 +58,26 @@ const ( // NewKeeper constructs an upgrade keeper func NewKeeper(storeKey sdk.StoreKey, cdc *codec.Codec) Keeper { - return Keeper{ + return &keeper{ storeKey: storeKey, cdc: cdc, upgradeHandlers: map[string]Handler{}, } } -// SetUpgradeHandler sets an UpgradeHandler for the upgrade specified by name. This handler will be called when the upgrade -// with this name is applied. In order for an upgrade with the given name to proceed, a handler for this upgrade -// must be set even if it is a no-op function. -func (keeper Keeper) SetUpgradeHandler(name string, upgradeHandler Handler) { +func (keeper *keeper) SetUpgradeHandler(name string, upgradeHandler Handler) { keeper.upgradeHandlers[name] = upgradeHandler } -// ScheduleUpgrade schedules an upgrade based on the specified plan -func (keeper Keeper) ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error { +func (keeper *keeper) SetWillUpgrader(willUpgrader func(ctx sdk.Context, plan Plan)) { + keeper.willUpgrader = willUpgrader +} + +func (keeper *keeper) SetOnUpgrader(onUpgrader func(ctx sdk.Context, plan Plan)) { + keeper.onUpgrader = onUpgrader +} + +func (keeper *keeper) ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error { err := plan.ValidateBasic() if err != nil { return err @@ -59,13 +99,14 @@ func (keeper Keeper) ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error { return sdk.ErrUnknownRequest(fmt.Sprintf("Upgrade with name %s has already been completed", plan.Name)) } bz := keeper.cdc.MustMarshalBinaryBare(plan) + keeper.haveCache = false store.Set([]byte(PlanKey), bz) return nil } -// ClearUpgradePlan clears any schedule upgrade -func (keeper Keeper) ClearUpgradePlan(ctx sdk.Context) { +func (keeper *keeper) ClearUpgradePlan(ctx sdk.Context) { store := ctx.KVStore(keeper.storeKey) + keeper.haveCache = false store.Delete([]byte(PlanKey)) } @@ -78,9 +119,7 @@ func (plan Plan) ValidateBasic() sdk.Error { return nil } -// GetUpgradePlan returns the currently scheduled Plan if any, setting havePlan to true if there is a scheduled -// upgrade or false if there is none -func (keeper Keeper) GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) { +func (keeper *keeper) GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) { store := ctx.KVStore(keeper.storeKey) bz := store.Get([]byte(PlanKey)) if bz == nil { @@ -90,48 +129,51 @@ func (keeper Keeper) GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) return plan, true } -// SetDoShutdowner sets a custom shutdown function for the upgrade module. This shutdown -// function will be called during the BeginBlock method when an upgrade is required -// instead of panic'ing which is the default behavior -func (keeper *Keeper) SetDoShutdowner(doShutdowner func(ctx sdk.Context, plan Plan)) { - keeper.doShutdowner = doShutdowner -} - func upgradeDoneKey(name string) []byte { return []byte(fmt.Sprintf("done/%s", name)) } -// BeginBlocker should be called inside the BeginBlocker method of any app using the upgrade module -func (keeper *Keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) { +func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) { blockTime := ctx.BlockHeader().Time blockHeight := ctx.BlockHeight() - plan, havePlan := keeper.GetUpgradePlan(ctx) - if !havePlan { + if !keeper.haveCache { + plan, found := keeper.GetUpgradePlan(ctx) + keeper.haveCachedPlan = found + keeper.plan = plan + keeper.haveCache = true + if found { + willUpgrader := keeper.willUpgrader + if willUpgrader != nil { + willUpgrader(ctx, keeper.plan) + } + } + } + + if !keeper.haveCachedPlan { return } - upgradeTime := plan.Time - upgradeHeight := plan.Height + upgradeTime := keeper.plan.Time + upgradeHeight := keeper.plan.Height if (!upgradeTime.IsZero() && !blockTime.Before(upgradeTime)) || upgradeHeight <= blockHeight { - handler, ok := keeper.upgradeHandlers[plan.Name] + handler, ok := keeper.upgradeHandlers[keeper.plan.Name] if ok { // We have an upgrade handler for this upgrade name, so apply the upgrade - ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at height %d", plan.Name, blockHeight)) - handler(ctx, plan) + ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at height %d", keeper.plan.Name, blockHeight)) + handler(ctx, keeper.plan) keeper.ClearUpgradePlan(ctx) // Mark this upgrade name as being done so the name can't be reused accidentally store := ctx.KVStore(keeper.storeKey) - store.Set(upgradeDoneKey(plan.Name), []byte("1")) + store.Set(upgradeDoneKey(keeper.plan.Name), []byte("1")) } else { // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown - ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", plan.Name, blockHeight, plan.Info)) - doShutdowner := keeper.doShutdowner - if doShutdowner != nil { - doShutdowner(ctx, plan) - } else { - panic("UPGRADE REQUIRED!") + ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", keeper.plan.Name, blockHeight, keeper.plan.Info)) + onUpgrader := keeper.onUpgrader + if onUpgrader != nil { + onUpgrader(ctx, keeper.plan) } + panic("UPGRADE REQUIRED!") } } } diff --git a/x/upgrade/keeper_test.go b/x/upgrade/keeper_test.go index 50333d90cc9b..73dd673dabec 100644 --- a/x/upgrade/keeper_test.go +++ b/x/upgrade/keeper_test.go @@ -113,23 +113,38 @@ func (s *TestSuite) TestCantApplySameUpgradeTwice() { } func (s *TestSuite) TestDoShutdowner() { - s.T().Log("Set a custom DoShutdowner") - shutdownerCalled := false - s.keeper.SetDoShutdowner(func(ctx sdk.Context, plan Plan) { - shutdownerCalled = true + s.T().Log("Set custom WillUpgrader and OnUpgrader") + willUpgraderCalled := false + onUpgraderCalled := false + s.keeper.SetWillUpgrader(func(ctx sdk.Context, plan Plan) { + willUpgraderCalled = true + }) + s.keeper.SetOnUpgrader(func(ctx sdk.Context, plan Plan) { + onUpgraderCalled = true }) - s.T().Log("Run an upgrade and verify that the custom shutdowner was called and no panic happened") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) + s.T().Log("Run an upgrade and verify that the custom WillUpgrader and OnUpgrader's are called") + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight() + 2}) s.Require().Nil(err) - header := abci.Header{Height: s.ctx.BlockHeight() + 1, Time: time.Now()} + header := abci.Header{Height: s.ctx.BlockHeight() + 1} newCtx := sdk.NewContext(s.cms, header, false, log.NewNopLogger()) req := abci.RequestBeginBlock{Header: header} s.Require().NotPanics(func() { s.keeper.BeginBlocker(newCtx, req) }) - s.Require().True(shutdownerCalled) + s.Require().True(willUpgraderCalled) + s.Require().False(onUpgraderCalled) + + willUpgraderCalled = false + header = abci.Header{Height: s.ctx.BlockHeight() + 2} + newCtx = sdk.NewContext(s.cms, header, false, log.NewNopLogger()) + req = abci.RequestBeginBlock{Header: header} + s.Require().Panics(func() { + s.keeper.BeginBlocker(newCtx, req) + }) + s.Require().True(onUpgraderCalled) + s.Require().False(willUpgraderCalled) } func (s *TestSuite) TestNoSpuriousUpgrades() { @@ -140,6 +155,19 @@ func (s *TestSuite) TestNoSpuriousUpgrades() { }) } +func (s *TestSuite) TestPlanStringer() { + t, err := time.Parse(time.RFC3339, "2020-01-01T00:00:00Z") + s.Require().Nil(err) + s.Require().Equal(`Upgrade Plan + Name: test + Time: 2020-01-01T00:00:00Z + Info: `, Plan{Name: "test", Time: t}.String()) + s.Require().Equal(`Upgrade Plan + Name: test + Height: 100 + Info: `, Plan{Name: "test", Height: 100}.String()) +} + func TestTestSuite(t *testing.T) { suite.Run(t, new(TestSuite)) } From b88a9a72179c37b3ed6b9269389636348a9ada04 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Wed, 17 Apr 2019 17:01:03 -0400 Subject: [PATCH 04/62] Add the ability to retrieve the height of an applied upgrade and cleanup CLI --- x/upgrade/client/cli/query.go | 37 ++++++++++++++++++++++-- x/upgrade/client/module_client.go | 47 +++++++++++++++++++++++++++++++ x/upgrade/keeper.go | 8 ++++-- 3 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 x/upgrade/client/module_client.go diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go index 5694b6c83299..da146801c216 100644 --- a/x/upgrade/client/cli/query.go +++ b/x/upgrade/client/cli/query.go @@ -8,10 +8,10 @@ import ( "github.com/spf13/cobra" ) -// GetQueryCmd creates a query sub-command for the upgrade module using cmdName as the name of the sub-command. -func GetQueryCmd(cmdName string, storeName string, cdc *codec.Codec) *cobra.Command { +// GetPlanCmd returns the query upgrade plan command +func GetPlanCmd(storeName string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: cmdName, + Use: "plan", Short: "get upgrade plan (if one exists)", Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { @@ -35,3 +35,34 @@ func GetQueryCmd(cmdName string, storeName string, cdc *codec.Codec) *cobra.Comm }, } } + +// GetAppliedHeightCmd returns the height at which a completed upgrade was applied +func GetAppliedHeightCmd(storeName string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "applied-height [upgrade-name]", + Short: "get the height at which a completed upgrade was applied", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + name := args[0] + + res, err := cliCtx.QueryStore(upgrade.DoneHeightKey(name), storeName) + if err != nil { + return err + } + + if len(res) == 0 { + return fmt.Errorf("no upgrade found") + } + + var height int64 + err = cdc.UnmarshalBinaryBare(res, &height) + if err != nil { + return err + } + fmt.Println(height) + return nil + }, + } +} diff --git a/x/upgrade/client/module_client.go b/x/upgrade/client/module_client.go new file mode 100644 index 000000000000..4027a31270e8 --- /dev/null +++ b/x/upgrade/client/module_client.go @@ -0,0 +1,47 @@ +package client + +import ( + "github.com/spf13/cobra" + "github.com/tendermint/go-amino" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/x/upgrade/client/cli" +) + +// ModuleClient exports all client functionality from this module +type ModuleClient struct { + storeKey string + cdc *amino.Codec +} + +// NewModuleClient returns an upgrade ModuleClient +func NewModuleClient(storeKey string, cdc *amino.Codec) ModuleClient { + return ModuleClient{storeKey, cdc} +} + +// GetQueryCmd returns the cli query commands for this module +func (mc ModuleClient) GetQueryCmd() *cobra.Command { + queryCmd := &cobra.Command{ + Use: "upgrade", + Short: "Querying commands for the upgrade module", + } + queryCmd.AddCommand(client.GetCommands( + cli.GetPlanCmd(mc.storeKey, mc.cdc), + cli.GetAppliedHeightCmd(mc.storeKey, mc.cdc), + )...) + + return queryCmd + +} + +// GetTxCmd returns the transaction commands for this module +func (mc ModuleClient) GetTxCmd() *cobra.Command { + txCmd := &cobra.Command{ + Use: "upgrade", + Short: "Upgrade transaction subcommands", + } + + txCmd.AddCommand(client.PostCommands()...) + + return txCmd +} diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index d50fe0c0ea6e..3691891e010e 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -95,7 +95,7 @@ func (keeper *keeper) ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error { } } store := ctx.KVStore(keeper.storeKey) - if store.Has(upgradeDoneKey(plan.Name)) { + if store.Has(DoneHeightKey(plan.Name)) { return sdk.ErrUnknownRequest(fmt.Sprintf("Upgrade with name %s has already been completed", plan.Name)) } bz := keeper.cdc.MustMarshalBinaryBare(plan) @@ -129,7 +129,8 @@ func (keeper *keeper) GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) return plan, true } -func upgradeDoneKey(name string) []byte { +// DoneHeightKey returns a key that points to the height at which a past upgrade was applied +func DoneHeightKey(name string) []byte { return []byte(fmt.Sprintf("done/%s", name)) } @@ -165,7 +166,8 @@ func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) keeper.ClearUpgradePlan(ctx) // Mark this upgrade name as being done so the name can't be reused accidentally store := ctx.KVStore(keeper.storeKey) - store.Set(upgradeDoneKey(keeper.plan.Name), []byte("1")) + bz := keeper.cdc.MustMarshalBinaryBare(blockHeight) + store.Set(DoneHeightKey(keeper.plan.Name), bz) } else { // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", keeper.plan.Name, blockHeight, keeper.plan.Info)) From e3faef508f0d3d501c5ecb0bd0e9a57dd7eafe30 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 18 Apr 2019 09:47:56 -0400 Subject: [PATCH 05/62] Update upgrade module docs --- x/gov/client/utils/utils.go | 3 - x/gov/types/codec.go | 1 - x/gov/types/msgs.go | 6 -- x/gov/types/msgs_test.go | 1 - x/upgrade/codec.go | 9 +++ x/upgrade/doc.go | 20 +++++-- x/upgrade/genesis.go | 33 +++++++++++ x/upgrade/module.go | 108 ++++++++++++++++++++++++++++++++++ x/upgrade/proposal.go | 74 +++++++++++++++++++++++ x/upgrade/proposal_handler.go | 36 ++++++++++++ 10 files changed, 274 insertions(+), 17 deletions(-) create mode 100644 x/upgrade/genesis.go create mode 100644 x/upgrade/module.go create mode 100644 x/upgrade/proposal.go create mode 100644 x/upgrade/proposal_handler.go diff --git a/x/gov/client/utils/utils.go b/x/gov/client/utils/utils.go index e23aa3ca1e85..b5640a39bcd7 100644 --- a/x/gov/client/utils/utils.go +++ b/x/gov/client/utils/utils.go @@ -28,9 +28,6 @@ func NormalizeProposalType(proposalType string) string { case "Text", "text": return types.ProposalTypeText - case "SoftwareUpgrade", "software_upgrade": - return types.ProposalTypeSoftwareUpgrade - default: return "" } diff --git a/x/gov/types/codec.go b/x/gov/types/codec.go index 32d4900785af..fc3de0e3f30c 100644 --- a/x/gov/types/codec.go +++ b/x/gov/types/codec.go @@ -17,7 +17,6 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(MsgVote{}, "cosmos-sdk/MsgVote", nil) cdc.RegisterConcrete(TextProposal{}, "cosmos-sdk/TextProposal", nil) - cdc.RegisterConcrete(SoftwareUpgradeProposal{}, "cosmos-sdk/SoftwareUpgradeProposal", nil) } // RegisterProposalTypeCodec registers an external proposal content type defined diff --git a/x/gov/types/msgs.go b/x/gov/types/msgs.go index aead430073ed..658c1149cd70 100644 --- a/x/gov/types/msgs.go +++ b/x/gov/types/msgs.go @@ -39,12 +39,6 @@ func (msg MsgSubmitProposal) ValidateBasic() sdk.Error { if msg.Content == nil { return ErrInvalidProposalContent(DefaultCodespace, "missing content") } - if msg.Content.ProposalType() == ProposalTypeSoftwareUpgrade { - // Disable software upgrade proposals as they are currently equivalent - // to text proposals. Re-enable once a valid software upgrade proposal - // handler is implemented. - return ErrInvalidProposalType(DefaultCodespace, msg.Content.ProposalType()) - } if msg.Proposer.Empty() { return sdk.ErrInvalidAddress(msg.Proposer.String()) } diff --git a/x/gov/types/msgs_test.go b/x/gov/types/msgs_test.go index 7ca97c432f4e..54bb3fa48412 100644 --- a/x/gov/types/msgs_test.go +++ b/x/gov/types/msgs_test.go @@ -35,7 +35,6 @@ func TestMsgSubmitProposal(t *testing.T) { {"Test Proposal", "the purpose of this proposal is to test", ProposalTypeText, addrs[0], coinsPos, true}, {"", "the purpose of this proposal is to test", ProposalTypeText, addrs[0], coinsPos, false}, {"Test Proposal", "", ProposalTypeText, addrs[0], coinsPos, false}, - {"Test Proposal", "the purpose of this proposal is to test", ProposalTypeSoftwareUpgrade, addrs[0], coinsPos, false}, {"Test Proposal", "the purpose of this proposal is to test", ProposalTypeText, sdk.AccAddress{}, coinsPos, false}, {"Test Proposal", "the purpose of this proposal is to test", ProposalTypeText, addrs[0], coinsZero, true}, {"Test Proposal", "the purpose of this proposal is to test", ProposalTypeText, addrs[0], coinsMulti, true}, diff --git a/x/upgrade/codec.go b/x/upgrade/codec.go index 91974577879f..aa94fefb2396 100644 --- a/x/upgrade/codec.go +++ b/x/upgrade/codec.go @@ -7,4 +7,13 @@ import ( // RegisterCodec registers concrete types on the Amino codec func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(Plan{}, "upgrade/Plan", nil) + cdc.RegisterConcrete(SoftwareUpgradeProposal{}, "upgrade/SoftwareUpgradeProposal", nil) + cdc.RegisterConcrete(CancelSoftwareUpgradeProposal{}, "upgrade/CancelSoftwareUpgradeProposal", nil) +} + +// module codec +var moduleCdc = codec.New() + +func init() { + RegisterCodec(moduleCdc) } diff --git a/x/upgrade/doc.go b/x/upgrade/doc.go index fff0a704e46b..dd8907ca5bb4 100644 --- a/x/upgrade/doc.go +++ b/x/upgrade/doc.go @@ -33,16 +33,24 @@ Here is an example handler for an upgrade named "my-fancy-upgrade": This upgrade handler performs the dual function of alerting the upgrade module that the named upgrade has been applied, as well as providing the opportunity for the upgraded software to perform any necessary state migrations. -Default and Custom Shutdown Behavior +Halt Behavior -Before "crashing" the ABCI state machine in the BeginBlocker method, the upgrade module will log an error +Before halting the ABCI state machine in the BeginBlocker method, the upgrade module will log an error that looks like: UPGRADE "" NEEDED at height : where Name are Info are the values of the respective fields on the upgrade Plan. -The default method for "crashing" the state machine is to panic with an error which will stop the state machine -but usually not exit the process. This behavior can be overridden using the keeper's SetDoShutdowner method. A custom -shutdown method could perform some other steps such as alerting another running process that an upgrade is needed, -as well as crashing the program by another method such as os.Exit(). +To perform the actual halt of the blockchain, the upgrade keeper simply panic's which prevents the ABCI state machine +from proceeding but doesn't actually exit the process. Exiting the process can cause issues for other nodes that start +to lose connectivity with the exiting nodes, thus this module prefers to just halt but not exit. + +Will Upgrade and On Upgrade Callbacks + +The upgrade keeper has two methods for setting callbacks - SetWillUpgrader and SetOnUpgrader. The will upgrade +callback will be called in the BeginBlocker of the first block after an upgrade is scheduled. The on upgrade callback +will be called in the BeginBlocker at the block where an upgrade is needed right before the state machine is halted. +The will upgrade callback can be used to notify some external process that an upgrade is needed so that it can +prepare binaries, etc. The on upgrade callback can notify some external process to actually begin the upgrade process. + */ package upgrade diff --git a/x/upgrade/genesis.go b/x/upgrade/genesis.go new file mode 100644 index 000000000000..59646ceb1207 --- /dev/null +++ b/x/upgrade/genesis.go @@ -0,0 +1,33 @@ +package upgrade + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// GenesisState - upgrade genesis state +type GenesisState struct { +} + +// NewGenesisState creates a new GenesisState object +func NewGenesisState() GenesisState { + return GenesisState{} +} + +// DefaultGenesisState creates a default GenesisState object +func DefaultGenesisState() GenesisState { + return GenesisState{} +} + +// new crisis genesis +func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { +} + +// ExportGenesis returns a GenesisState for a given context and keeper. +func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { + return NewGenesisState() +} + +// ValidateGenesis - placeholder function +func ValidateGenesis(data GenesisState) error { + return nil +} diff --git a/x/upgrade/module.go b/x/upgrade/module.go new file mode 100644 index 000000000000..3e1666f9fd02 --- /dev/null +++ b/x/upgrade/module.go @@ -0,0 +1,108 @@ +package upgrade + +import ( + "encoding/json" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +var ( + _ sdk.AppModule = AppModule{} + _ sdk.AppModuleBasic = AppModuleBasic{} +) + +const ( + ModuleName = "upgrade" + StoreKey = ModuleName +) + +// app module basics object +type AppModuleBasic struct{} + +var _ sdk.AppModuleBasic = AppModuleBasic{} + +// module name +func (AppModuleBasic) Name() string { + return ModuleName +} + +// register module codec +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { + RegisterCodec(cdc) +} + +// default genesis state +func (AppModuleBasic) DefaultGenesis() json.RawMessage { + return moduleCdc.MustMarshalJSON(DefaultGenesisState()) +} + +// module validate genesis +func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + var data GenesisState + err := moduleCdc.UnmarshalJSON(bz, &data) + if err != nil { + return err + } + return ValidateGenesis(data) +} + +//___________________________ +// app module +type AppModule struct { + AppModuleBasic + keeper Keeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper Keeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + } +} + +// module name +func (AppModule) Name() string { + return ModuleName +} + +// register invariants +func (AppModule) RegisterInvariants(_ sdk.InvariantRouter) {} + +// module message route name +func (AppModule) Route() string { return "" } + +// module handler +func (am AppModule) NewHandler() sdk.Handler { return nil } + +// module querier route name +func (AppModule) QuerierRoute() string { return "" } + +// module querier +func (am AppModule) NewQuerierHandler() sdk.Querier { return nil } + +// module init-genesis +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState GenesisState + moduleCdc.MustUnmarshalJSON(data, &genesisState) + InitGenesis(ctx, am.keeper, genesisState) + return []abci.ValidatorUpdate{} +} + +// module export genesis +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + gs := ExportGenesis(ctx, am.keeper) + return moduleCdc.MustMarshalJSON(gs) +} + +// module begin-block +func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) sdk.Tags { + return sdk.EmptyTags() +} + +// module end-block +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) ([]abci.ValidatorUpdate, sdk.Tags) { + return []abci.ValidatorUpdate{}, sdk.EmptyTags() +} diff --git a/x/upgrade/proposal.go b/x/upgrade/proposal.go new file mode 100644 index 000000000000..80e5671ca5f7 --- /dev/null +++ b/x/upgrade/proposal.go @@ -0,0 +1,74 @@ +package upgrade + +import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" +) + +const ( + ProposalTypeSoftwareUpgrade string = "SoftwareUpgrade" + ProposalTypeCancelSoftwareUpgrade string = "CancelSoftwareUpgrade" + DefaultCodespace sdk.CodespaceType = "upgrade" +) + +// Software Upgrade Proposals +type SoftwareUpgradeProposal struct { + Title string `json:"title"` + Description string `json:"description"` + Plan Plan `json:"plan"` +} + +func NewSoftwareUpgradeProposal(title, description string, plan Plan) gov.Content { + return SoftwareUpgradeProposal{title, description, plan} +} + +// Implements Proposal Interface +var _ gov.Content = SoftwareUpgradeProposal{} + +// nolint +func (sup SoftwareUpgradeProposal) GetTitle() string { return sup.Title } +func (sup SoftwareUpgradeProposal) GetDescription() string { return sup.Description } +func (sup SoftwareUpgradeProposal) ProposalRoute() string { return RouterKey } +func (sup SoftwareUpgradeProposal) ProposalType() string { return ProposalTypeSoftwareUpgrade } +func (sup SoftwareUpgradeProposal) ValidateBasic() sdk.Error { + return gov.ValidateAbstract(DefaultCodespace, sup) +} + +func (sup SoftwareUpgradeProposal) String() string { + return fmt.Sprintf(`Software Upgrade Proposal: + Title: %s + Description: %s +`, sup.Title, sup.Description) +} + +// Cancel Software Upgrade Proposals +type CancelSoftwareUpgradeProposal struct { + Title string `json:"title"` + Description string `json:"description"` +} + +func NewCancelSoftwareUpgradeProposal(title, description string) gov.Content { + return CancelSoftwareUpgradeProposal{title, description} +} + +// Implements Proposal Interface +var _ gov.Content = CancelSoftwareUpgradeProposal{} + +// nolint +func (sup CancelSoftwareUpgradeProposal) GetTitle() string { return sup.Title } +func (sup CancelSoftwareUpgradeProposal) GetDescription() string { return sup.Description } +func (sup CancelSoftwareUpgradeProposal) ProposalRoute() string { return RouterKey } +func (sup CancelSoftwareUpgradeProposal) ProposalType() string { + return ProposalTypeCancelSoftwareUpgrade +} +func (sup CancelSoftwareUpgradeProposal) ValidateBasic() sdk.Error { + return gov.ValidateAbstract(DefaultCodespace, sup) +} + +func (sup CancelSoftwareUpgradeProposal) String() string { + return fmt.Sprintf(`Cancel Software Upgrade Proposal: + Title: %s + Description: %s +`, sup.Title, sup.Description) +} diff --git a/x/upgrade/proposal_handler.go b/x/upgrade/proposal_handler.go new file mode 100644 index 000000000000..a245993100e2 --- /dev/null +++ b/x/upgrade/proposal_handler.go @@ -0,0 +1,36 @@ +package upgrade + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +const ( + RouterKey = ModuleName +) + +func NewSoftwareUpgradeProposalHandler(k Keeper) govtypes.Handler { + return func(ctx sdk.Context, content govtypes.Content) sdk.Error { + switch c := content.(type) { + case SoftwareUpgradeProposal: + return handleSoftwareUpgradeProposal(ctx, k, c) + case CancelSoftwareUpgradeProposal: + return handleCancelSoftwareUpgradeProposal(ctx, k, c) + + default: + errMsg := fmt.Sprintf("unrecognized software upgrade proposal content type: %T", c) + return sdk.ErrUnknownRequest(errMsg) + } + } +} + +func handleSoftwareUpgradeProposal(ctx sdk.Context, k Keeper, p SoftwareUpgradeProposal) sdk.Error { + return k.ScheduleUpgrade(ctx, p.Plan) +} + +func handleCancelSoftwareUpgradeProposal(ctx sdk.Context, k Keeper, p CancelSoftwareUpgradeProposal) sdk.Error { + k.ClearUpgradePlan(ctx) + return nil +} From 90b9e181e4ca2b0f48734ae74605ea16e6ae825c Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 3 Jun 2019 16:29:00 -0400 Subject: [PATCH 06/62] add GetCmdSubmitProposal for upgrade module --- x/upgrade/client/cli/tx.go | 121 +++++++++++++++++++++++++++++++++++++ x/upgrade/codec.go | 6 +- x/upgrade/keeper.go | 2 +- x/upgrade/module.go | 3 +- x/upgrade/proposal.go | 7 +++ 5 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 x/upgrade/client/cli/tx.go diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go new file mode 100644 index 000000000000..ac67d1aeb8f0 --- /dev/null +++ b/x/upgrade/client/cli/tx.go @@ -0,0 +1,121 @@ +package cli + +import ( + "fmt" + "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + "strings" + time2 "time" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/utils" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/upgrade" +) + +const ( + TimeFormat = "2006-01-02T15:04:05Z" +) + +// GetCmdSubmitProposal implements a command handler for submitting a software upgrade proposal transaction. +func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "software-upgrade --upgrade-name [name] (--upgrade-height [height] | --upgrade-time [time]) (--upgrade-info [info])", + Args: cobra.ExactArgs(0), + Short: "Submit a software upgrade proposal", + Long: strings.TrimSpace( + fmt.Sprintf(`Submit a software upgrade along with an initial deposit. +`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := authtxb.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext(). + WithCodec(cdc). + WithAccountDecoder(cdc) + + from := cliCtx.GetFromAddress() + + depositStr, err := cmd.Flags().GetString("deposit") + if err != nil { + return err + } + + deposit, err := sdk.ParseCoins(depositStr) + if err != nil { + return err + } + + title, err := cmd.Flags().GetString("title") + if err != nil { + return err + } + + description, err := cmd.Flags().GetString("description") + if err != nil { + return err + } + + name, err := cmd.Flags().GetString("upgrade-name") + if err != nil { + return err + } + if len(name) == 0 { + name = title + } + + height, err := cmd.Flags().GetInt64("upgrade-height") + if err != nil { + return err + } + + timeStr, err := cmd.Flags().GetString("upgrade-time") + if err != nil { + return err + } + + if height != 0 { + if len(timeStr) != 0 { + return fmt.Errorf("only one of --upgrade-time or --upgrade-height should be specified") + } + } + + var time time2.Time + if len(timeStr) != 0 { + time, err = time2.Parse(TimeFormat, timeStr) + if err != nil { + return err + } + } + + info, err := cmd.Flags().GetString("upgrade-info") + if err != nil { + return err + } + + content := upgrade.NewSoftwareUpgradeProposal(title, description, + upgrade.Plan{Name: name, Time: time, Height: height, Info: info}) + + msg := gov.NewMsgSubmitProposal(content, deposit, from) + if err := msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + cmd.Flags().String(cli.FlagTitle, "", "title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "deposit of proposal") + cmd.Flags().String("upgrade-name", "", "The name of the upgrade (if not specified title will be used)") + cmd.Flags().Int64("upgrade-height", 0, "The height at which the upgrade must happen (not to be used together with --upgrade-time)") + cmd.Flags().String("upgrade-time", "", fmt.Sprintf("The time at which the upgrade must happen (ex. %s) (not to be used together with --upgrade-height)", TimeFormat)) + cmd.Flags().String("upgrade-info", "", "Optional info for the planned upgrade such as commit hash, etc.") + + return cmd +} diff --git a/x/upgrade/codec.go b/x/upgrade/codec.go index aa94fefb2396..e9f9c6af4aff 100644 --- a/x/upgrade/codec.go +++ b/x/upgrade/codec.go @@ -6,9 +6,9 @@ import ( // RegisterCodec registers concrete types on the Amino codec func RegisterCodec(cdc *codec.Codec) { - cdc.RegisterConcrete(Plan{}, "upgrade/Plan", nil) - cdc.RegisterConcrete(SoftwareUpgradeProposal{}, "upgrade/SoftwareUpgradeProposal", nil) - cdc.RegisterConcrete(CancelSoftwareUpgradeProposal{}, "upgrade/CancelSoftwareUpgradeProposal", nil) + cdc.RegisterConcrete(Plan{}, "cosmos-sdk/Plan", nil) + cdc.RegisterConcrete(SoftwareUpgradeProposal{}, "cosmos-sdk/SoftwareUpgradeProposal", nil) + cdc.RegisterConcrete(CancelSoftwareUpgradeProposal{}, "cosmos-sdk/CancelSoftwareUpgradeProposal", nil) } // module codec diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index 3691891e010e..11ad1eced9a2 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -157,7 +157,7 @@ func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) upgradeTime := keeper.plan.Time upgradeHeight := keeper.plan.Height - if (!upgradeTime.IsZero() && !blockTime.Before(upgradeTime)) || upgradeHeight <= blockHeight { + if (!upgradeTime.IsZero() && !blockTime.Before(upgradeTime)) || (upgradeHeight > 0 && upgradeHeight <= blockHeight) { handler, ok := keeper.upgradeHandlers[keeper.plan.Name] if ok { // We have an upgrade handler for this upgrade name, so apply the upgrade diff --git a/x/upgrade/module.go b/x/upgrade/module.go index 3e1666f9fd02..09c766745cf8 100644 --- a/x/upgrade/module.go +++ b/x/upgrade/module.go @@ -98,7 +98,8 @@ func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { } // module begin-block -func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) sdk.Tags { +func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) sdk.Tags { + am.keeper.BeginBlocker(ctx, req) return sdk.EmptyTags() } diff --git a/x/upgrade/proposal.go b/x/upgrade/proposal.go index 80e5671ca5f7..8960e0d4464c 100644 --- a/x/upgrade/proposal.go +++ b/x/upgrade/proposal.go @@ -26,6 +26,13 @@ func NewSoftwareUpgradeProposal(title, description string, plan Plan) gov.Conten // Implements Proposal Interface var _ gov.Content = SoftwareUpgradeProposal{} +func init() { + gov.RegisterProposalType(ProposalTypeSoftwareUpgrade) + gov.RegisterProposalTypeCodec(SoftwareUpgradeProposal{}, "cosmos-sdk/SoftwareUpgradeProposal") + gov.RegisterProposalType(ProposalTypeCancelSoftwareUpgrade) + gov.RegisterProposalTypeCodec(CancelSoftwareUpgradeProposal{}, "cosmos-sdk/CancelSoftwareUpgradeProposal") +} + // nolint func (sup SoftwareUpgradeProposal) GetTitle() string { return sup.Title } func (sup SoftwareUpgradeProposal) GetDescription() string { return sup.Description } From 1f0bf382521b8c2623d9eaaaf43d90fe0d1283ac Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 3 Jun 2019 22:56:12 -0400 Subject: [PATCH 07/62] Add calls to optional prepare-upgrade and do-upgrade scripts from the upgrade module --- x/upgrade/doc.go | 5 ++- x/upgrade/keeper.go | 95 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/x/upgrade/doc.go b/x/upgrade/doc.go index dd8907ca5bb4..93ba26598d10 100644 --- a/x/upgrade/doc.go +++ b/x/upgrade/doc.go @@ -46,7 +46,10 @@ to lose connectivity with the exiting nodes, thus this module prefers to just ha Will Upgrade and On Upgrade Callbacks -The upgrade keeper has two methods for setting callbacks - SetWillUpgrader and SetOnUpgrader. The will upgrade +The upgrade keeper has two methods for setting callbacks - SetWillUpgrader and SetOnUpgrader. Custom callbacks can be +configured or the default ones will be used. DefaultWillUpgrader and DefaultOnUpgrader will call scripts +called prepare-upgrade and do-upgrade respectively in the config directory of the running daemon if such files exist with +the JSON-serialized upgrade plan as the first argument and the current block height as the second argument. The will upgrade callback will be called in the BeginBlocker of the first block after an upgrade is scheduled. The on upgrade callback will be called in the BeginBlocker at the block where an upgrade is needed right before the state machine is halted. The will upgrade callback can be used to notify some external process that an upgrade is needed so that it can diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index 11ad1eced9a2..aa7848a17638 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -1,10 +1,16 @@ package upgrade import ( + "encoding/json" "fmt" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/spf13/viper" abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/cli" + "os" + "os/exec" + "path/filepath" ) // Keeper of the upgrade module @@ -138,16 +144,15 @@ func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) blockTime := ctx.BlockHeader().Time blockHeight := ctx.BlockHeight() + justRetrievedFromCache := false + if !keeper.haveCache { plan, found := keeper.GetUpgradePlan(ctx) keeper.haveCachedPlan = found keeper.plan = plan keeper.haveCache = true if found { - willUpgrader := keeper.willUpgrader - if willUpgrader != nil { - willUpgrader(ctx, keeper.plan) - } + justRetrievedFromCache = true } } @@ -171,11 +176,87 @@ func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) } else { // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", keeper.plan.Name, blockHeight, keeper.plan.Info)) - onUpgrader := keeper.onUpgrader - if onUpgrader != nil { - onUpgrader(ctx, keeper.plan) + if keeper.onUpgrader != nil { + keeper.onUpgrader(ctx, keeper.plan) + } else { + DefaultOnUpgrader(ctx, keeper.plan) } panic("UPGRADE REQUIRED!") } + } else if justRetrievedFromCache { + // In this case we are notifying the system that an upgrade is planned but not scheduled to happen yet + if keeper.willUpgrader != nil { + keeper.willUpgrader(ctx, keeper.plan) + } else { + DefaultWillUpgrader(ctx, keeper.plan) + } + } +} + + +// DefaultWillUpgrader asynchronously runs a script called prepare-upgrade from $COSMOS_HOME/config if such a script exists, +// with plan serialized to JSON as the first argument and the current block height as the second argument. +// The environment variable $COSMOS_HOME will be set to the home directory of the daemon. +func DefaultWillUpgrader(ctx sdk.Context, plan Plan) { + CallUpgradeScript(ctx, plan, "prepare-upgrade", true) +} + +// DefaultOnUpgrader synchronously runs a script called do-upgrade from $COSMOS_HOME/config if such a script exists, +// with plan serialized to JSON as the first argument and the current block height as the second argument. +// The environment variable $COSMOS_HOME will be set to the home directory of the daemon. +func DefaultOnUpgrader(ctx sdk.Context, plan Plan) { + CallUpgradeScript(ctx, plan, "do-upgrade", false) +} + +// CallUpgradeScript runs a script called script from $COSMOS_HOME/config if such a script exists, +// with plan serialized to JSON as the first argument and the current block height as the second argument. +// The environment variable $COSMOS_HOME will be set to the home directory of the daemon. +// If async is true, the command will be run in a separate go-routine. +func CallUpgradeScript(ctx sdk.Context, plan Plan, script string, async bool) { + f := func() { + home := viper.GetString(cli.HomeFlag) + file := filepath.Join(home, "config", script) + ctx.Logger().Info(fmt.Sprintf("Looking for upgrade script %s", file)) + if _, err := os.Stat(file); err == nil { + ctx.Logger().Info(fmt.Sprintf("Applying upgrade script %s", file)) + err = os.Setenv("COSMOS_HOME", home) + if err != nil { + ctx.Logger().Error(fmt.Sprintf("Error setting env var COSMOS_HOME: %v", err)) + } + + + planJson, err := json.Marshal(plan) + if err != nil { + ctx.Logger().Error(fmt.Sprintf("Error marshaling upgrade plan to JSON: %v", err)) + } + cmd := exec.Command(file, string(planJson), fmt.Sprintf("%d", ctx.BlockHeight())) + cmd.Stdout = logWriter{ctx, script, false} + cmd.Stderr = logWriter{ctx, script, false} + err = cmd.Run() + if err != nil { + ctx.Logger().Error(fmt.Sprintf("Error running script %s: %v", file, err)) + } + } + } + if async { + go f() + } else { + f() + } +} + +type logWriter struct { + sdk.Context + script string + err bool +} + +func (w logWriter) Write(p []byte) (n int, err error) { + s := fmt.Sprintf("script %s: %s", w.script, p) + if w.err { + w.Logger().Error(s) + } else { + w.Logger().Info(s) } + return len(p), nil } From eaaf5a29b7e7d9cce927fc885e6876fccbcefcc1 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 4 Jun 2019 15:24:58 -0400 Subject: [PATCH 08/62] Add GetCmdSubmitCancelUpgradeProposal --- x/upgrade/client/cli/tx.go | 63 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go index ac67d1aeb8f0..e2bdc3f6c567 100644 --- a/x/upgrade/client/cli/tx.go +++ b/x/upgrade/client/cli/tx.go @@ -21,10 +21,10 @@ const ( TimeFormat = "2006-01-02T15:04:05Z" ) -// GetCmdSubmitProposal implements a command handler for submitting a software upgrade proposal transaction. -func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { +// GetCmdSubmitUpgradeProposal implements a command handler for submitting a software upgrade proposal transaction. +func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ - Use: "software-upgrade --upgrade-name [name] (--upgrade-height [height] | --upgrade-time [time]) (--upgrade-info [info])", + Use: "software-upgrade --upgrade-name [name] (--upgrade-height [height] | --upgrade-time [time]) (--upgrade-info [info]) [flags]", Args: cobra.ExactArgs(0), Short: "Submit a software upgrade proposal", Long: strings.TrimSpace( @@ -119,3 +119,60 @@ func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { return cmd } + +// GetCmdSubmitCancelUpgradeProposal implements a command handler for submitting a software upgrade cancel proposal transaction. +func GetCmdSubmitCancelUpgradeProposal(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "cancel-software-upgrade [flags]", + Args: cobra.ExactArgs(0), + Short: "Submit a software upgrade proposal", + Long: strings.TrimSpace( + fmt.Sprintf(`Submit a software upgrade along with an initial deposit. +`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := authtxb.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext(). + WithCodec(cdc). + WithAccountDecoder(cdc) + + from := cliCtx.GetFromAddress() + + depositStr, err := cmd.Flags().GetString("deposit") + if err != nil { + return err + } + + deposit, err := sdk.ParseCoins(depositStr) + if err != nil { + return err + } + + title, err := cmd.Flags().GetString("title") + if err != nil { + return err + } + + description, err := cmd.Flags().GetString("description") + if err != nil { + return err + } + + content := upgrade.NewCancelSoftwareUpgradeProposal(title, description) + + msg := gov.NewMsgSubmitProposal(content, deposit, from) + if err := msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + cmd.Flags().String(cli.FlagTitle, "", "title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "deposit of proposal") + + return cmd +} From 5fc7efbd5ad629dfb58cc5d5bed3ba80328142c3 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Tue, 4 Jun 2019 16:08:34 -0400 Subject: [PATCH 09/62] Disable cache usage for upgrade module and will upgrader because of inconsistent behavior --- x/upgrade/keeper.go | 52 +++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index aa7848a17638..2de0140fd4ee 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -51,8 +51,8 @@ type keeper struct { cdc *codec.Codec upgradeHandlers map[string]Handler haveCache bool - haveCachedPlan bool - plan Plan + //haveCachedPlan bool + //plan Plan willUpgrader func(ctx sdk.Context, plan Plan) onUpgrader func(ctx sdk.Context, plan Plan) } @@ -144,53 +144,45 @@ func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) blockTime := ctx.BlockHeader().Time blockHeight := ctx.BlockHeight() - justRetrievedFromCache := false + plan, found := keeper.GetUpgradePlan(ctx) - if !keeper.haveCache { - plan, found := keeper.GetUpgradePlan(ctx) - keeper.haveCachedPlan = found - keeper.plan = plan - keeper.haveCache = true - if found { - justRetrievedFromCache = true - } - } - - if !keeper.haveCachedPlan { + if !found { return } - upgradeTime := keeper.plan.Time - upgradeHeight := keeper.plan.Height + upgradeTime := plan.Time + upgradeHeight := plan.Height if (!upgradeTime.IsZero() && !blockTime.Before(upgradeTime)) || (upgradeHeight > 0 && upgradeHeight <= blockHeight) { - handler, ok := keeper.upgradeHandlers[keeper.plan.Name] + handler, ok := keeper.upgradeHandlers[plan.Name] if ok { // We have an upgrade handler for this upgrade name, so apply the upgrade - ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at height %d", keeper.plan.Name, blockHeight)) - handler(ctx, keeper.plan) + ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at height %d", plan.Name, blockHeight)) + handler(ctx, plan) keeper.ClearUpgradePlan(ctx) // Mark this upgrade name as being done so the name can't be reused accidentally store := ctx.KVStore(keeper.storeKey) bz := keeper.cdc.MustMarshalBinaryBare(blockHeight) - store.Set(DoneHeightKey(keeper.plan.Name), bz) + store.Set(DoneHeightKey(plan.Name), bz) } else { // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown - ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", keeper.plan.Name, blockHeight, keeper.plan.Info)) + ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", plan.Name, blockHeight, plan.Info)) if keeper.onUpgrader != nil { - keeper.onUpgrader(ctx, keeper.plan) + keeper.onUpgrader(ctx, plan) } else { - DefaultOnUpgrader(ctx, keeper.plan) + DefaultOnUpgrader(ctx, plan) } panic("UPGRADE REQUIRED!") } - } else if justRetrievedFromCache { - // In this case we are notifying the system that an upgrade is planned but not scheduled to happen yet - if keeper.willUpgrader != nil { - keeper.willUpgrader(ctx, keeper.plan) - } else { - DefaultWillUpgrader(ctx, keeper.plan) - } } + // TODO will upgraded is disabled for now because of inconsistent behavior + //else if justRetrievedFromCache { + // // In this case we are notifying the system that an upgrade is planned but not scheduled to happen yet + // if keeper.willUpgrader != nil { + // keeper.willUpgrader(ctx, keeper.plan) + // } else { + // DefaultWillUpgrader(ctx, keeper.plan) + // } + //} } From 9702017293d66fe1df55d3e5200b933f359ecb35 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 8 Jul 2019 16:39:32 +0200 Subject: [PATCH 10/62] Explain general workflow of upgrade module from user perspective --- x/upgrade/doc.go | 26 ++++++++++++++++++++++++++ x/upgrade/keeper.go | 39 ++++----------------------------------- x/upgrade/keeper_test.go | 16 +++++----------- 3 files changed, 35 insertions(+), 46 deletions(-) diff --git a/x/upgrade/doc.go b/x/upgrade/doc.go index 93ba26598d10..26747e7a84db 100644 --- a/x/upgrade/doc.go +++ b/x/upgrade/doc.go @@ -7,6 +7,30 @@ Without software support for upgrades, upgrading a live chain is risky because a their state machines at exactly the same point in the process. If this is not done correctly, there can be state inconsistencies which are hard to recover from. +General Workflow + +Let's assume we are running v0.34.0 of our software in our testnet and want to upgrade to v0.36.0. +How would this look in practice? First of all, we want to finalize the v0.36.0 release candidate +and there install a specially named upgrade handler (eg. "testnet-v2" or even "v0.36.0"). Once +this code is public, we can have a governance vote to approve this upgrade at some future blocktime +or blockheight (known as an upgrade.Plan). The v0.34.0 code will not know of this handler, but will +continue to run until block 200000, when the plan kicks in at BeginBlock. It will check for existence +of the handler, and finding it missing, know that it is running the obsolete software, and kill itself. +Shortly before killing itself, it will check if there is a script in `/config/do-upgrade` +and run it if present. + +Generally the gaiad/regend/etc binary will restart on crash, but then will execute this BeginBlocker +again and crash, causing a restart loop. Either the operator can manually install the new software, +or you can make use of the `do-upgrade` script to eg. dump state to json (backup), download new binary +(from a location I trust - script written by operator), install binary. + +When the binary restarts with the upgraded version (here v0.36.0), it will detect we have registered the +"testnet-v2" upgrade handler in the code, and realize it is the new version. It then will run the script +and *migrate the database in-place*. Once finished, it marks the upgrade as done, and continues processing +the rest of the block as normal. Once 2/3 of the voting power has upgraded, the blockchain will immediately +resume the consensus mechanism. If the majority of operators add a custom `do-upgrade` script, this should +be a matter of minutes and not even require them to be awake at that time. + Integrating With An App Setup an upgrade Keeper for the app and then define a BeginBlocker that calls the upgrade @@ -55,5 +79,7 @@ will be called in the BeginBlocker at the block where an upgrade is needed right The will upgrade callback can be used to notify some external process that an upgrade is needed so that it can prepare binaries, etc. The on upgrade callback can notify some external process to actually begin the upgrade process. +BUG(aaronc): will upgrade callbacks are temporarily disabled + */ package upgrade diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index 2de0140fd4ee..c9aa294af033 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -3,14 +3,15 @@ package upgrade import ( "encoding/json" "fmt" + "os" + "os/exec" + "path/filepath" + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/spf13/viper" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/cli" - "os" - "os/exec" - "path/filepath" ) // Keeper of the upgrade module @@ -30,13 +31,6 @@ type Keeper interface { // upgrade or false if there is none GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) - // SetWillUpgrader sets a custom function to be run whenever an upgrade is scheduled. This - // can be used to notify the node that an upgrade will be happen in the future so that it - // can download any software ahead of time in the background. - // It does not indicate that an upgrade is happening now and should just be used for preparation, - // not the actual upgrade. - SetWillUpgrader(willUpgrader func(ctx sdk.Context, plan Plan)) - // SetOnUpgrader sets a custom function to be called right before the chain halts and the // upgrade needs to be applied. This can be used to initiate an automatic upgrade process. SetOnUpgrader(onUpgrader func(ctx sdk.Context, plan Plan)) @@ -51,9 +45,6 @@ type keeper struct { cdc *codec.Codec upgradeHandlers map[string]Handler haveCache bool - //haveCachedPlan bool - //plan Plan - willUpgrader func(ctx sdk.Context, plan Plan) onUpgrader func(ctx sdk.Context, plan Plan) } @@ -75,10 +66,6 @@ func (keeper *keeper) SetUpgradeHandler(name string, upgradeHandler Handler) { keeper.upgradeHandlers[name] = upgradeHandler } -func (keeper *keeper) SetWillUpgrader(willUpgrader func(ctx sdk.Context, plan Plan)) { - keeper.willUpgrader = willUpgrader -} - func (keeper *keeper) SetOnUpgrader(onUpgrader func(ctx sdk.Context, plan Plan)) { keeper.onUpgrader = onUpgrader } @@ -174,23 +161,6 @@ func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) panic("UPGRADE REQUIRED!") } } - // TODO will upgraded is disabled for now because of inconsistent behavior - //else if justRetrievedFromCache { - // // In this case we are notifying the system that an upgrade is planned but not scheduled to happen yet - // if keeper.willUpgrader != nil { - // keeper.willUpgrader(ctx, keeper.plan) - // } else { - // DefaultWillUpgrader(ctx, keeper.plan) - // } - //} -} - - -// DefaultWillUpgrader asynchronously runs a script called prepare-upgrade from $COSMOS_HOME/config if such a script exists, -// with plan serialized to JSON as the first argument and the current block height as the second argument. -// The environment variable $COSMOS_HOME will be set to the home directory of the daemon. -func DefaultWillUpgrader(ctx sdk.Context, plan Plan) { - CallUpgradeScript(ctx, plan, "prepare-upgrade", true) } // DefaultOnUpgrader synchronously runs a script called do-upgrade from $COSMOS_HOME/config if such a script exists, @@ -216,7 +186,6 @@ func CallUpgradeScript(ctx sdk.Context, plan Plan, script string, async bool) { ctx.Logger().Error(fmt.Sprintf("Error setting env var COSMOS_HOME: %v", err)) } - planJson, err := json.Marshal(plan) if err != nil { ctx.Logger().Error(fmt.Sprintf("Error marshaling upgrade plan to JSON: %v", err)) diff --git a/x/upgrade/keeper_test.go b/x/upgrade/keeper_test.go index 73dd673dabec..5c2555df40f4 100644 --- a/x/upgrade/keeper_test.go +++ b/x/upgrade/keeper_test.go @@ -1,6 +1,9 @@ package upgrade import ( + "testing" + "time" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" @@ -8,8 +11,6 @@ import ( abci "github.com/tendermint/tendermint/abci/types" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" - "testing" - "time" ) type TestSuite struct { @@ -112,18 +113,14 @@ func (s *TestSuite) TestCantApplySameUpgradeTwice() { s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) } -func (s *TestSuite) TestDoShutdowner() { +func (s *TestSuite) TestCustomCallbacks() { s.T().Log("Set custom WillUpgrader and OnUpgrader") - willUpgraderCalled := false onUpgraderCalled := false - s.keeper.SetWillUpgrader(func(ctx sdk.Context, plan Plan) { - willUpgraderCalled = true - }) s.keeper.SetOnUpgrader(func(ctx sdk.Context, plan Plan) { onUpgraderCalled = true }) - s.T().Log("Run an upgrade and verify that the custom WillUpgrader and OnUpgrader's are called") + s.T().Log("Run an upgrade and verify that the custom OnUpgrader is called") err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight() + 2}) s.Require().Nil(err) @@ -133,10 +130,8 @@ func (s *TestSuite) TestDoShutdowner() { s.Require().NotPanics(func() { s.keeper.BeginBlocker(newCtx, req) }) - s.Require().True(willUpgraderCalled) s.Require().False(onUpgraderCalled) - willUpgraderCalled = false header = abci.Header{Height: s.ctx.BlockHeight() + 2} newCtx = sdk.NewContext(s.cms, header, false, log.NewNopLogger()) req = abci.RequestBeginBlock{Header: header} @@ -144,7 +139,6 @@ func (s *TestSuite) TestDoShutdowner() { s.keeper.BeginBlocker(newCtx, req) }) s.Require().True(onUpgraderCalled) - s.Require().False(willUpgraderCalled) } func (s *TestSuite) TestNoSpuriousUpgrades() { From 5c494cdeaa2ea1f902ff4ffbbc9f8eda501bec6e Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 8 Jul 2019 18:18:27 +0200 Subject: [PATCH 11/62] Rename OnUpgrader -> OnShutdowner --- x/upgrade/keeper.go | 20 ++++++++++---------- x/upgrade/keeper_test.go | 14 +++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index c9aa294af033..2085e0614e36 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -31,9 +31,9 @@ type Keeper interface { // upgrade or false if there is none GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) - // SetOnUpgrader sets a custom function to be called right before the chain halts and the + // SetOnShutdowner sets a custom function to be called right before the chain halts and the // upgrade needs to be applied. This can be used to initiate an automatic upgrade process. - SetOnUpgrader(onUpgrader func(ctx sdk.Context, plan Plan)) + SetOnShutdowner(OnShutdowner func(ctx sdk.Context, plan Plan)) // BeginBlocker should be called inside the BeginBlocker method of any app using the upgrade module. Scheduled upgrade // plans are cached in memory so the overhead of this method is trivial. @@ -45,7 +45,7 @@ type keeper struct { cdc *codec.Codec upgradeHandlers map[string]Handler haveCache bool - onUpgrader func(ctx sdk.Context, plan Plan) + onShutdowner func(ctx sdk.Context, plan Plan) } const ( @@ -66,8 +66,8 @@ func (keeper *keeper) SetUpgradeHandler(name string, upgradeHandler Handler) { keeper.upgradeHandlers[name] = upgradeHandler } -func (keeper *keeper) SetOnUpgrader(onUpgrader func(ctx sdk.Context, plan Plan)) { - keeper.onUpgrader = onUpgrader +func (keeper *keeper) SetOnShutdowner(OnShutdowner func(ctx sdk.Context, plan Plan)) { + keeper.onShutdowner = OnShutdowner } func (keeper *keeper) ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error { @@ -153,20 +153,20 @@ func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) } else { // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", plan.Name, blockHeight, plan.Info)) - if keeper.onUpgrader != nil { - keeper.onUpgrader(ctx, plan) + if keeper.onShutdowner != nil { + keeper.onShutdowner(ctx, plan) } else { - DefaultOnUpgrader(ctx, plan) + DefaultOnShutdowner(ctx, plan) } panic("UPGRADE REQUIRED!") } } } -// DefaultOnUpgrader synchronously runs a script called do-upgrade from $COSMOS_HOME/config if such a script exists, +// DefaultOnShutdowner synchronously runs a script called do-upgrade from $COSMOS_HOME/config if such a script exists, // with plan serialized to JSON as the first argument and the current block height as the second argument. // The environment variable $COSMOS_HOME will be set to the home directory of the daemon. -func DefaultOnUpgrader(ctx sdk.Context, plan Plan) { +func DefaultOnShutdowner(ctx sdk.Context, plan Plan) { CallUpgradeScript(ctx, plan, "do-upgrade", false) } diff --git a/x/upgrade/keeper_test.go b/x/upgrade/keeper_test.go index 5c2555df40f4..fc6be54d81e6 100644 --- a/x/upgrade/keeper_test.go +++ b/x/upgrade/keeper_test.go @@ -114,13 +114,13 @@ func (s *TestSuite) TestCantApplySameUpgradeTwice() { } func (s *TestSuite) TestCustomCallbacks() { - s.T().Log("Set custom WillUpgrader and OnUpgrader") - onUpgraderCalled := false - s.keeper.SetOnUpgrader(func(ctx sdk.Context, plan Plan) { - onUpgraderCalled = true + s.T().Log("Set custom OnShutdowner") + onShutdownerCalled := false + s.keeper.SetOnShutdowner(func(ctx sdk.Context, plan Plan) { + onShutdownerCalled = true }) - s.T().Log("Run an upgrade and verify that the custom OnUpgrader is called") + s.T().Log("Run an upgrade and verify that the custom OnShutdowner is called") err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight() + 2}) s.Require().Nil(err) @@ -130,7 +130,7 @@ func (s *TestSuite) TestCustomCallbacks() { s.Require().NotPanics(func() { s.keeper.BeginBlocker(newCtx, req) }) - s.Require().False(onUpgraderCalled) + s.Require().False(onShutdownerCalled) header = abci.Header{Height: s.ctx.BlockHeight() + 2} newCtx = sdk.NewContext(s.cms, header, false, log.NewNopLogger()) @@ -138,7 +138,7 @@ func (s *TestSuite) TestCustomCallbacks() { s.Require().Panics(func() { s.keeper.BeginBlocker(newCtx, req) }) - s.Require().True(onUpgraderCalled) + s.Require().True(onShutdownerCalled) } func (s *TestSuite) TestNoSpuriousUpgrades() { From 7625f0cb6ca233e47a327691e8c2a8a02bc5156b Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 8 Jul 2019 18:53:40 +0200 Subject: [PATCH 12/62] Upgrade client imports to v0.36.0 --- x/mint/internal/keeper/test_common.go | 89 ++++++++++++++ x/supply/internal/keeper/test_common.go | 107 +++++++++++++++++ x/upgrade/client/cli/query.go | 6 +- x/upgrade/client/cli/tx.go | 21 ++-- x/upgrade/client/rest/rest.go | 5 +- x/upgrade/codec.go | 7 -- x/upgrade/keeper.go | 5 + x/upgrade/module.go | 109 ----------------- x/upgrade/module/module.go | 149 ++++++++++++++++++++++++ 9 files changed, 369 insertions(+), 129 deletions(-) create mode 100644 x/mint/internal/keeper/test_common.go create mode 100644 x/supply/internal/keeper/test_common.go delete mode 100644 x/upgrade/module.go create mode 100644 x/upgrade/module/module.go diff --git a/x/mint/internal/keeper/test_common.go b/x/mint/internal/keeper/test_common.go new file mode 100644 index 000000000000..f4f0265ce55b --- /dev/null +++ b/x/mint/internal/keeper/test_common.go @@ -0,0 +1,89 @@ +// nolint:deadcode unused +package keeper + +import ( + "os" + "testing" + "time" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + + "github.com/stretchr/testify/require" + + dbm "github.com/tendermint/tendermint/libs/db" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/mint/internal/types" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/supply" +) + +type testInput struct { + ctx sdk.Context + cdc *codec.Codec + mintKeeper Keeper +} + +func newTestInput(t *testing.T) testInput { + db := dbm.NewMemDB() + + keyAcc := sdk.NewKVStoreKey(auth.StoreKey) + keySupply := sdk.NewKVStoreKey(supply.StoreKey) + keyStaking := sdk.NewKVStoreKey(staking.StoreKey) + tkeyStaking := sdk.NewTransientStoreKey(staking.TStoreKey) + keyParams := sdk.NewKVStoreKey(params.StoreKey) + tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey) + keyMint := sdk.NewKVStoreKey(types.StoreKey) + + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(keyAcc, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(tkeyStaking, sdk.StoreTypeTransient, nil) + ms.MountStoreWithDB(keyStaking, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keySupply, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyMint, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db) + err := ms.LoadLatestVersion() + require.Nil(t, err) + + ctx := sdk.NewContext(ms, abci.Header{Time: time.Unix(0, 0)}, false, log.NewTMLogger(os.Stdout)) + + paramsKeeper := params.NewKeeper(types.ModuleCdc, keyParams, tkeyParams, params.DefaultCodespace) + accountKeeper := auth.NewAccountKeeper(types.ModuleCdc, keyAcc, paramsKeeper.Subspace(auth.DefaultParamspace), auth.ProtoBaseAccount) + bankKeeper := bank.NewBaseKeeper(accountKeeper, paramsKeeper.Subspace(bank.DefaultParamspace), bank.DefaultCodespace) + maccPerms := map[string][]string{ + auth.FeeCollectorName: {supply.Basic}, + types.ModuleName: {supply.Minter}, + staking.NotBondedPoolName: {supply.Burner, supply.Staking}, + staking.BondedPoolName: {supply.Burner, supply.Staking}, + } + supplyKeeper := supply.NewKeeper(types.ModuleCdc, keySupply, accountKeeper, bankKeeper, supply.DefaultCodespace, maccPerms) + supplyKeeper.SetSupply(ctx, supply.NewSupply(sdk.Coins{})) + + stakingKeeper := staking.NewKeeper( + types.ModuleCdc, keyStaking, tkeyStaking, supplyKeeper, paramsKeeper.Subspace(staking.DefaultParamspace), staking.DefaultCodespace, + ) + mintKeeper := NewKeeper(types.ModuleCdc, keyMint, paramsKeeper.Subspace(types.DefaultParamspace), &stakingKeeper, supplyKeeper, auth.FeeCollectorName) + + // set module accounts + feeCollectorAcc := supply.NewEmptyModuleAccount(auth.FeeCollectorName, supply.Basic) + minterAcc := supply.NewEmptyModuleAccount(types.ModuleName, supply.Minter) + notBondedPool := supply.NewEmptyModuleAccount(staking.NotBondedPoolName, supply.Burner) + bondPool := supply.NewEmptyModuleAccount(staking.BondedPoolName, supply.Burner) + + supplyKeeper.SetModuleAccount(ctx, feeCollectorAcc) + supplyKeeper.SetModuleAccount(ctx, minterAcc) + supplyKeeper.SetModuleAccount(ctx, notBondedPool) + supplyKeeper.SetModuleAccount(ctx, bondPool) + + mintKeeper.SetParams(ctx, types.DefaultParams()) + mintKeeper.SetMinter(ctx, types.DefaultInitialMinter()) + + return testInput{ctx, types.ModuleCdc, mintKeeper} +} diff --git a/x/supply/internal/keeper/test_common.go b/x/supply/internal/keeper/test_common.go new file mode 100644 index 000000000000..0831801b41a3 --- /dev/null +++ b/x/supply/internal/keeper/test_common.go @@ -0,0 +1,107 @@ +package keeper + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/secp256k1" + dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/libs/log" + + abci "github.com/tendermint/tendermint/abci/types" + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/supply/internal/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// nolint: deadcode unused +var ( + multiPerm = "multiple permissions account" + randomPerm = "random permission" +) + +// nolint: deadcode unused +// create a codec used only for testing +func makeTestCodec() *codec.Codec { + var cdc = codec.New() + + bank.RegisterCodec(cdc) + auth.RegisterCodec(cdc) + types.RegisterCodec(cdc) + sdk.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + + return cdc +} + +// nolint: deadcode unused +func createTestInput(t *testing.T, isCheckTx bool, initPower int64, nAccs int64) (sdk.Context, auth.AccountKeeper, Keeper) { + + keyAcc := sdk.NewKVStoreKey(auth.StoreKey) + keyParams := sdk.NewKVStoreKey(params.StoreKey) + tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey) + keySupply := sdk.NewKVStoreKey(types.StoreKey) + + db := dbm.NewMemDB() + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(keyAcc, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keySupply, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db) + err := ms.LoadLatestVersion() + require.Nil(t, err) + + ctx := sdk.NewContext(ms, abci.Header{ChainID: "supply-chain"}, isCheckTx, log.NewNopLogger()) + ctx = ctx.WithConsensusParams( + &abci.ConsensusParams{ + Validator: &abci.ValidatorParams{ + PubKeyTypes: []string{tmtypes.ABCIPubKeyTypeEd25519}, + }, + }, + ) + cdc := makeTestCodec() + + pk := params.NewKeeper(cdc, keyParams, tkeyParams, params.DefaultCodespace) + ak := auth.NewAccountKeeper(cdc, keyAcc, pk.Subspace(auth.DefaultParamspace), auth.ProtoBaseAccount) + bk := bank.NewBaseKeeper(ak, pk.Subspace(bank.DefaultParamspace), bank.DefaultCodespace) + + valTokens := sdk.TokensFromConsensusPower(initPower) + + initialCoins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, valTokens)) + createTestAccs(ctx, int(nAccs), initialCoins, &ak) + + maccPerms := map[string][]string{ + types.Basic: {types.Basic}, + types.Minter: {types.Minter}, + types.Burner: {types.Burner}, + multiPerm: {types.Basic, types.Minter, types.Burner}, + randomPerm: {"random"}, + } + keeper := NewKeeper(cdc, keySupply, ak, bk, DefaultCodespace, maccPerms) + totalSupply := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, valTokens.MulRaw(nAccs))) + keeper.SetSupply(ctx, types.NewSupply(totalSupply)) + + return ctx, ak, keeper +} + +// nolint: unparam deadcode unused +func createTestAccs(ctx sdk.Context, numAccs int, initialCoins sdk.Coins, ak *auth.AccountKeeper) (accs []auth.Account) { + for i := 0; i < numAccs; i++ { + privKey := secp256k1.GenPrivKey() + pubKey := privKey.PubKey() + addr := sdk.AccAddress(pubKey.Address()) + acc := auth.NewBaseAccountWithAddress(addr) + acc.Coins = initialCoins + acc.PubKey = pubKey + acc.AccountNumber = uint64(i) + ak.SetAccount(ctx, &acc) + } + return +} diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go index da146801c216..00c37d8372f3 100644 --- a/x/upgrade/client/cli/query.go +++ b/x/upgrade/client/cli/query.go @@ -17,7 +17,8 @@ func GetPlanCmd(storeName string, cdc *codec.Codec) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) - res, err := cliCtx.QueryStore([]byte(upgrade.PlanKey), storeName) + // ignore height for now + res, _, err := cliCtx.QueryStore([]byte(upgrade.PlanKey), storeName) if err != nil { return err } @@ -47,7 +48,8 @@ func GetAppliedHeightCmd(storeName string, cdc *codec.Codec) *cobra.Command { name := args[0] - res, err := cliCtx.QueryStore(upgrade.DoneHeightKey(name), storeName) + // ignore height for now + res, _, err := cliCtx.QueryStore(upgrade.DoneHeightKey(name), storeName) if err != nil { return err } diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go index e2bdc3f6c567..88bf0b795b0d 100644 --- a/x/upgrade/client/cli/tx.go +++ b/x/upgrade/client/cli/tx.go @@ -2,17 +2,18 @@ package cli import ( "fmt" - "github.com/cosmos/cosmos-sdk/x/gov/client/cli" "strings" time2 "time" + "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + "github.com/spf13/cobra" "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/client/utils" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/upgrade" ) @@ -33,10 +34,11 @@ func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { ), ), RunE: func(cmd *cobra.Command, args []string) error { - txBldr := authtxb.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) cliCtx := context.NewCLIContext(). - WithCodec(cdc). - WithAccountDecoder(cdc) + WithCodec(cdc) + // removed due to #4588 (verifyt his didn't break anything) + // WithAccountDecoder(cdc) from := cliCtx.GetFromAddress() @@ -132,10 +134,11 @@ func GetCmdSubmitCancelUpgradeProposal(cdc *codec.Codec) *cobra.Command { ), ), RunE: func(cmd *cobra.Command, args []string) error { - txBldr := authtxb.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) cliCtx := context.NewCLIContext(). - WithCodec(cdc). - WithAccountDecoder(cdc) + WithCodec(cdc) + // removed due to #4588 (verifyt his didn't break anything) + // WithAccountDecoder(cdc) from := cliCtx.GetFromAddress() diff --git a/x/upgrade/client/rest/rest.go b/x/upgrade/client/rest/rest.go index 00b4c23cb8a3..99188bc1feea 100644 --- a/x/upgrade/client/rest/rest.go +++ b/x/upgrade/client/rest/rest.go @@ -17,7 +17,8 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, func getUpgradePlanHandler(cdc *codec.Codec, cliCtx context.CLIContext, storeName string) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, request *http.Request) { - res, err := cliCtx.QueryStore([]byte(upgrade.PlanKey), storeName) + // ignore height for now + res, _, err := cliCtx.QueryStore([]byte(upgrade.PlanKey), storeName) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return @@ -35,6 +36,6 @@ func getUpgradePlanHandler(cdc *codec.Codec, cliCtx context.CLIContext, storeNam return } - rest.PostProcessResponse(w, cdc, plan, cliCtx.Indent) + rest.PostProcessResponse(w, cliCtx, plan) } } diff --git a/x/upgrade/codec.go b/x/upgrade/codec.go index e9f9c6af4aff..db9e6a4d2c9b 100644 --- a/x/upgrade/codec.go +++ b/x/upgrade/codec.go @@ -10,10 +10,3 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(SoftwareUpgradeProposal{}, "cosmos-sdk/SoftwareUpgradeProposal", nil) cdc.RegisterConcrete(CancelSoftwareUpgradeProposal{}, "cosmos-sdk/CancelSoftwareUpgradeProposal", nil) } - -// module codec -var moduleCdc = codec.New() - -func init() { - RegisterCodec(moduleCdc) -} diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index 2085e0614e36..104467a8f3d7 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -14,6 +14,11 @@ import ( "github.com/tendermint/tendermint/libs/cli" ) +const ( + ModuleName = "upgrade" + StoreKey = ModuleName +) + // Keeper of the upgrade module type Keeper interface { // ScheduleUpgrade schedules an upgrade based on the specified plan diff --git a/x/upgrade/module.go b/x/upgrade/module.go deleted file mode 100644 index 09c766745cf8..000000000000 --- a/x/upgrade/module.go +++ /dev/null @@ -1,109 +0,0 @@ -package upgrade - -import ( - "encoding/json" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - abci "github.com/tendermint/tendermint/abci/types" -) - -var ( - _ sdk.AppModule = AppModule{} - _ sdk.AppModuleBasic = AppModuleBasic{} -) - -const ( - ModuleName = "upgrade" - StoreKey = ModuleName -) - -// app module basics object -type AppModuleBasic struct{} - -var _ sdk.AppModuleBasic = AppModuleBasic{} - -// module name -func (AppModuleBasic) Name() string { - return ModuleName -} - -// register module codec -func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { - RegisterCodec(cdc) -} - -// default genesis state -func (AppModuleBasic) DefaultGenesis() json.RawMessage { - return moduleCdc.MustMarshalJSON(DefaultGenesisState()) -} - -// module validate genesis -func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { - var data GenesisState - err := moduleCdc.UnmarshalJSON(bz, &data) - if err != nil { - return err - } - return ValidateGenesis(data) -} - -//___________________________ -// app module -type AppModule struct { - AppModuleBasic - keeper Keeper -} - -// NewAppModule creates a new AppModule object -func NewAppModule(keeper Keeper) AppModule { - return AppModule{ - AppModuleBasic: AppModuleBasic{}, - keeper: keeper, - } -} - -// module name -func (AppModule) Name() string { - return ModuleName -} - -// register invariants -func (AppModule) RegisterInvariants(_ sdk.InvariantRouter) {} - -// module message route name -func (AppModule) Route() string { return "" } - -// module handler -func (am AppModule) NewHandler() sdk.Handler { return nil } - -// module querier route name -func (AppModule) QuerierRoute() string { return "" } - -// module querier -func (am AppModule) NewQuerierHandler() sdk.Querier { return nil } - -// module init-genesis -func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { - var genesisState GenesisState - moduleCdc.MustUnmarshalJSON(data, &genesisState) - InitGenesis(ctx, am.keeper, genesisState) - return []abci.ValidatorUpdate{} -} - -// module export genesis -func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { - gs := ExportGenesis(ctx, am.keeper) - return moduleCdc.MustMarshalJSON(gs) -} - -// module begin-block -func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) sdk.Tags { - am.keeper.BeginBlocker(ctx, req) - return sdk.EmptyTags() -} - -// module end-block -func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) ([]abci.ValidatorUpdate, sdk.Tags) { - return []abci.ValidatorUpdate{}, sdk.EmptyTags() -} diff --git a/x/upgrade/module/module.go b/x/upgrade/module/module.go new file mode 100644 index 000000000000..28d5a3c4791e --- /dev/null +++ b/x/upgrade/module/module.go @@ -0,0 +1,149 @@ +package upgrade + +import ( + "encoding/json" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/upgrade" + "github.com/cosmos/cosmos-sdk/x/upgrade/client/cli" + "github.com/cosmos/cosmos-sdk/x/upgrade/client/rest" + abci "github.com/tendermint/tendermint/abci/types" +) + +// module codec +var moduleCdc = codec.New() + +func init() { + upgrade.RegisterCodec(moduleCdc) +} + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// app module basics object +type AppModuleBasic struct{} + +// module name +func (AppModuleBasic) Name() string { + return upgrade.ModuleName +} + +// register module codec +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { + upgrade.RegisterCodec(cdc) +} + +// default genesis state +func (AppModuleBasic) DefaultGenesis() json.RawMessage { + return moduleCdc.MustMarshalJSON(upgrade.DefaultGenesisState()) +} + +// module validate genesis +func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + var data upgrade.GenesisState + err := moduleCdc.UnmarshalJSON(bz, &data) + if err != nil { + return err + } + return upgrade.ValidateGenesis(data) +} + +func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, r *mux.Router) { + // TODO: is ModuleName the proper route??? + rest.RegisterRoutes(ctx, r, moduleCdc, upgrade.ModuleName, upgrade.StoreKey) +} + +// GetQueryCmd returns the cli query commands for this module +func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { + queryCmd := &cobra.Command{ + Use: "upgrade", + Short: "Querying commands for the upgrade module", + } + queryCmd.AddCommand(client.GetCommands( + cli.GetPlanCmd(upgrade.StoreKey, cdc), + cli.GetAppliedHeightCmd(upgrade.StoreKey, cdc), + )...) + + return queryCmd + +} + +// GetTxCmd returns the transaction commands for this module +func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { + txCmd := &cobra.Command{ + Use: "upgrade", + Short: "Upgrade transaction subcommands", + } + + txCmd.AddCommand(client.PostCommands()...) + + return txCmd +} + +//___________________________ +// app module +type AppModule struct { + AppModuleBasic + keeper upgrade.Keeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper upgrade.Keeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + } +} + +// module name +func (AppModule) Name() string { + return upgrade.ModuleName +} + +// register invariants +func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// module message route name +func (AppModule) Route() string { return "" } + +// module handler +func (am AppModule) NewHandler() sdk.Handler { return nil } + +// module querier route name +func (AppModule) QuerierRoute() string { return "" } + +// module querier +func (am AppModule) NewQuerierHandler() sdk.Querier { return nil } + +// module init-genesis +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState upgrade.GenesisState + moduleCdc.MustUnmarshalJSON(data, &genesisState) + upgrade.InitGenesis(ctx, am.keeper, genesisState) + return []abci.ValidatorUpdate{} +} + +// module export genesis +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + gs := upgrade.ExportGenesis(ctx, am.keeper) + return moduleCdc.MustMarshalJSON(gs) +} + +// module begin-block +func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { + am.keeper.BeginBlocker(ctx, req) +} + +// module end-block +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} From 1d318fe78b7a962db1d02459561fbaf5f4d28847 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 27 Aug 2019 13:42:41 +0200 Subject: [PATCH 13/62] Remove upgrade script callbacks - not to be used --- x/upgrade/keeper.go | 63 ---------------------------------------- x/upgrade/keeper_test.go | 28 ------------------ 2 files changed, 91 deletions(-) diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index 104467a8f3d7..e565bedfa96a 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -1,17 +1,11 @@ package upgrade import ( - "encoding/json" "fmt" - "os" - "os/exec" - "path/filepath" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/spf13/viper" abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/cli" ) const ( @@ -36,10 +30,6 @@ type Keeper interface { // upgrade or false if there is none GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) - // SetOnShutdowner sets a custom function to be called right before the chain halts and the - // upgrade needs to be applied. This can be used to initiate an automatic upgrade process. - SetOnShutdowner(OnShutdowner func(ctx sdk.Context, plan Plan)) - // BeginBlocker should be called inside the BeginBlocker method of any app using the upgrade module. Scheduled upgrade // plans are cached in memory so the overhead of this method is trivial. BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) @@ -50,7 +40,6 @@ type keeper struct { cdc *codec.Codec upgradeHandlers map[string]Handler haveCache bool - onShutdowner func(ctx sdk.Context, plan Plan) } const ( @@ -71,10 +60,6 @@ func (keeper *keeper) SetUpgradeHandler(name string, upgradeHandler Handler) { keeper.upgradeHandlers[name] = upgradeHandler } -func (keeper *keeper) SetOnShutdowner(OnShutdowner func(ctx sdk.Context, plan Plan)) { - keeper.onShutdowner = OnShutdowner -} - func (keeper *keeper) ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error { err := plan.ValidateBasic() if err != nil { @@ -158,59 +143,11 @@ func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) } else { // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", plan.Name, blockHeight, plan.Info)) - if keeper.onShutdowner != nil { - keeper.onShutdowner(ctx, plan) - } else { - DefaultOnShutdowner(ctx, plan) - } panic("UPGRADE REQUIRED!") } } } -// DefaultOnShutdowner synchronously runs a script called do-upgrade from $COSMOS_HOME/config if such a script exists, -// with plan serialized to JSON as the first argument and the current block height as the second argument. -// The environment variable $COSMOS_HOME will be set to the home directory of the daemon. -func DefaultOnShutdowner(ctx sdk.Context, plan Plan) { - CallUpgradeScript(ctx, plan, "do-upgrade", false) -} - -// CallUpgradeScript runs a script called script from $COSMOS_HOME/config if such a script exists, -// with plan serialized to JSON as the first argument and the current block height as the second argument. -// The environment variable $COSMOS_HOME will be set to the home directory of the daemon. -// If async is true, the command will be run in a separate go-routine. -func CallUpgradeScript(ctx sdk.Context, plan Plan, script string, async bool) { - f := func() { - home := viper.GetString(cli.HomeFlag) - file := filepath.Join(home, "config", script) - ctx.Logger().Info(fmt.Sprintf("Looking for upgrade script %s", file)) - if _, err := os.Stat(file); err == nil { - ctx.Logger().Info(fmt.Sprintf("Applying upgrade script %s", file)) - err = os.Setenv("COSMOS_HOME", home) - if err != nil { - ctx.Logger().Error(fmt.Sprintf("Error setting env var COSMOS_HOME: %v", err)) - } - - planJson, err := json.Marshal(plan) - if err != nil { - ctx.Logger().Error(fmt.Sprintf("Error marshaling upgrade plan to JSON: %v", err)) - } - cmd := exec.Command(file, string(planJson), fmt.Sprintf("%d", ctx.BlockHeight())) - cmd.Stdout = logWriter{ctx, script, false} - cmd.Stderr = logWriter{ctx, script, false} - err = cmd.Run() - if err != nil { - ctx.Logger().Error(fmt.Sprintf("Error running script %s: %v", file, err)) - } - } - } - if async { - go f() - } else { - f() - } -} - type logWriter struct { sdk.Context script string diff --git a/x/upgrade/keeper_test.go b/x/upgrade/keeper_test.go index fc6be54d81e6..978de13111f8 100644 --- a/x/upgrade/keeper_test.go +++ b/x/upgrade/keeper_test.go @@ -113,34 +113,6 @@ func (s *TestSuite) TestCantApplySameUpgradeTwice() { s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) } -func (s *TestSuite) TestCustomCallbacks() { - s.T().Log("Set custom OnShutdowner") - onShutdownerCalled := false - s.keeper.SetOnShutdowner(func(ctx sdk.Context, plan Plan) { - onShutdownerCalled = true - }) - - s.T().Log("Run an upgrade and verify that the custom OnShutdowner is called") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight() + 2}) - s.Require().Nil(err) - - header := abci.Header{Height: s.ctx.BlockHeight() + 1} - newCtx := sdk.NewContext(s.cms, header, false, log.NewNopLogger()) - req := abci.RequestBeginBlock{Header: header} - s.Require().NotPanics(func() { - s.keeper.BeginBlocker(newCtx, req) - }) - s.Require().False(onShutdownerCalled) - - header = abci.Header{Height: s.ctx.BlockHeight() + 2} - newCtx = sdk.NewContext(s.cms, header, false, log.NewNopLogger()) - req = abci.RequestBeginBlock{Header: header} - s.Require().Panics(func() { - s.keeper.BeginBlocker(newCtx, req) - }) - s.Require().True(onShutdownerCalled) -} - func (s *TestSuite) TestNoSpuriousUpgrades() { s.T().Log("Verify that no upgrade panic is triggered in the BeginBlocker when we haven't scheduled an upgrade") req := abci.RequestBeginBlock{Header: s.ctx.BlockHeader()} From 057968ea7f2a77fb46ea21cf70fd8eed5b1f4afa Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 27 Aug 2019 13:52:52 +0200 Subject: [PATCH 14/62] Abort in BeginBlock if we have too new upgrade handlers --- x/upgrade/keeper.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index e565bedfa96a..60a2a4df6a3c 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -145,7 +145,17 @@ func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", plan.Name, blockHeight, plan.Info)) panic("UPGRADE REQUIRED!") } + } else { + // enforce that we are not ahead of the game... if we didn't just upgrade, all registered handlers should be in done + store := ctx.KVStore(keeper.storeKey) + for name := range keeper.upgradeHandlers { + if !store.Has(DoneHeightKey(name)) { + ctx.Logger().Error(fmt.Sprintf("UNKOWN UPGRADE \"%s\" - in binary but not executed on chain", name)) + panic("BINARY NEWER THAN CHAIN!") + } + } } + } type logWriter struct { From 3e38495409749aed31c5702bea4e29d74491acd0 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 27 Aug 2019 14:04:17 +0200 Subject: [PATCH 15/62] Test early abort --- x/upgrade/keeper.go | 15 ++++++--------- x/upgrade/keeper_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index 60a2a4df6a3c..8ba4727907cb 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -122,7 +122,6 @@ func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) blockHeight := ctx.BlockHeight() plan, found := keeper.GetUpgradePlan(ctx) - if !found { return } @@ -146,16 +145,14 @@ func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) panic("UPGRADE REQUIRED!") } } else { - // enforce that we are not ahead of the game... if we didn't just upgrade, all registered handlers should be in done - store := ctx.KVStore(keeper.storeKey) - for name := range keeper.upgradeHandlers { - if !store.Has(DoneHeightKey(name)) { - ctx.Logger().Error(fmt.Sprintf("UNKOWN UPGRADE \"%s\" - in binary but not executed on chain", name)) - panic("BINARY NEWER THAN CHAIN!") - } + // if we have a pending upgrade, but it is not yet time, make sure we did not + // set the handler already + _, ok := keeper.upgradeHandlers[plan.Name] + if ok { + ctx.Logger().Error(fmt.Sprintf("UNKOWN UPGRADE \"%s\" - in binary but not executed on chain", plan.Name)) + panic("BINARY UPDATED BEFORE TRIGGER!") } } - } type logWriter struct { diff --git a/x/upgrade/keeper_test.go b/x/upgrade/keeper_test.go index 978de13111f8..ddf190a54a21 100644 --- a/x/upgrade/keeper_test.go +++ b/x/upgrade/keeper_test.go @@ -89,6 +89,38 @@ func (s *TestSuite) VerifyDoUpgrade() { s.VerifyCleared(newCtx) } +func (s *TestSuite) TestHaltIfTooNew() { + s.T().Log("Verify that we don't panic with registered plan not in database at all") + var called int + s.keeper.SetUpgradeHandler("future", func(ctx sdk.Context, plan Plan) { called++ }) + + newCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 1, Time: time.Now()}, false, log.NewNopLogger()) + req := abci.RequestBeginBlock{Header: newCtx.BlockHeader()} + s.Require().NotPanics(func() { + s.keeper.BeginBlocker(newCtx, req) + }) + s.Require().Equal(0, called) + + s.T().Log("Verify we panic if we have a registered handler ahead of time") + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "future", Height: s.ctx.BlockHeight() + 3}) + s.Require().NoError(err) + s.Require().Panics(func() { + s.keeper.BeginBlocker(newCtx, req) + }) + s.Require().Equal(0, called) + + s.T().Log("Verify we no longer panic if the plan is on time") + + futCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 3, Time: time.Now()}, false, log.NewNopLogger()) + req = abci.RequestBeginBlock{Header: futCtx.BlockHeader()} + s.Require().NotPanics(func() { + s.keeper.BeginBlocker(futCtx, req) + }) + s.Require().Equal(1, called) + + s.VerifyCleared(futCtx) +} + func (s *TestSuite) VerifyCleared(newCtx sdk.Context) { s.T().Log("Verify that the upgrade plan has been cleared") _, havePlan := s.keeper.GetUpgradePlan(newCtx) From 3fa26ea4b38c7af7d7ef62e15ce7adbdffb7b787 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 29 Aug 2019 18:59:22 +0200 Subject: [PATCH 16/62] Fix compile errors after rebase --- x/mint/internal/keeper/test_common.go | 89 -------------------- x/supply/internal/keeper/test_common.go | 107 ------------------------ x/upgrade/keeper_test.go | 2 +- 3 files changed, 1 insertion(+), 197 deletions(-) delete mode 100644 x/mint/internal/keeper/test_common.go delete mode 100644 x/supply/internal/keeper/test_common.go diff --git a/x/mint/internal/keeper/test_common.go b/x/mint/internal/keeper/test_common.go deleted file mode 100644 index f4f0265ce55b..000000000000 --- a/x/mint/internal/keeper/test_common.go +++ /dev/null @@ -1,89 +0,0 @@ -// nolint:deadcode unused -package keeper - -import ( - "os" - "testing" - "time" - - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/log" - - "github.com/stretchr/testify/require" - - dbm "github.com/tendermint/tendermint/libs/db" - - "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/store" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/cosmos/cosmos-sdk/x/bank" - "github.com/cosmos/cosmos-sdk/x/mint/internal/types" - "github.com/cosmos/cosmos-sdk/x/params" - "github.com/cosmos/cosmos-sdk/x/staking" - "github.com/cosmos/cosmos-sdk/x/supply" -) - -type testInput struct { - ctx sdk.Context - cdc *codec.Codec - mintKeeper Keeper -} - -func newTestInput(t *testing.T) testInput { - db := dbm.NewMemDB() - - keyAcc := sdk.NewKVStoreKey(auth.StoreKey) - keySupply := sdk.NewKVStoreKey(supply.StoreKey) - keyStaking := sdk.NewKVStoreKey(staking.StoreKey) - tkeyStaking := sdk.NewTransientStoreKey(staking.TStoreKey) - keyParams := sdk.NewKVStoreKey(params.StoreKey) - tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey) - keyMint := sdk.NewKVStoreKey(types.StoreKey) - - ms := store.NewCommitMultiStore(db) - ms.MountStoreWithDB(keyAcc, sdk.StoreTypeIAVL, db) - ms.MountStoreWithDB(tkeyStaking, sdk.StoreTypeTransient, nil) - ms.MountStoreWithDB(keyStaking, sdk.StoreTypeIAVL, db) - ms.MountStoreWithDB(keySupply, sdk.StoreTypeIAVL, db) - ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) - ms.MountStoreWithDB(keyMint, sdk.StoreTypeIAVL, db) - ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db) - err := ms.LoadLatestVersion() - require.Nil(t, err) - - ctx := sdk.NewContext(ms, abci.Header{Time: time.Unix(0, 0)}, false, log.NewTMLogger(os.Stdout)) - - paramsKeeper := params.NewKeeper(types.ModuleCdc, keyParams, tkeyParams, params.DefaultCodespace) - accountKeeper := auth.NewAccountKeeper(types.ModuleCdc, keyAcc, paramsKeeper.Subspace(auth.DefaultParamspace), auth.ProtoBaseAccount) - bankKeeper := bank.NewBaseKeeper(accountKeeper, paramsKeeper.Subspace(bank.DefaultParamspace), bank.DefaultCodespace) - maccPerms := map[string][]string{ - auth.FeeCollectorName: {supply.Basic}, - types.ModuleName: {supply.Minter}, - staking.NotBondedPoolName: {supply.Burner, supply.Staking}, - staking.BondedPoolName: {supply.Burner, supply.Staking}, - } - supplyKeeper := supply.NewKeeper(types.ModuleCdc, keySupply, accountKeeper, bankKeeper, supply.DefaultCodespace, maccPerms) - supplyKeeper.SetSupply(ctx, supply.NewSupply(sdk.Coins{})) - - stakingKeeper := staking.NewKeeper( - types.ModuleCdc, keyStaking, tkeyStaking, supplyKeeper, paramsKeeper.Subspace(staking.DefaultParamspace), staking.DefaultCodespace, - ) - mintKeeper := NewKeeper(types.ModuleCdc, keyMint, paramsKeeper.Subspace(types.DefaultParamspace), &stakingKeeper, supplyKeeper, auth.FeeCollectorName) - - // set module accounts - feeCollectorAcc := supply.NewEmptyModuleAccount(auth.FeeCollectorName, supply.Basic) - minterAcc := supply.NewEmptyModuleAccount(types.ModuleName, supply.Minter) - notBondedPool := supply.NewEmptyModuleAccount(staking.NotBondedPoolName, supply.Burner) - bondPool := supply.NewEmptyModuleAccount(staking.BondedPoolName, supply.Burner) - - supplyKeeper.SetModuleAccount(ctx, feeCollectorAcc) - supplyKeeper.SetModuleAccount(ctx, minterAcc) - supplyKeeper.SetModuleAccount(ctx, notBondedPool) - supplyKeeper.SetModuleAccount(ctx, bondPool) - - mintKeeper.SetParams(ctx, types.DefaultParams()) - mintKeeper.SetMinter(ctx, types.DefaultInitialMinter()) - - return testInput{ctx, types.ModuleCdc, mintKeeper} -} diff --git a/x/supply/internal/keeper/test_common.go b/x/supply/internal/keeper/test_common.go deleted file mode 100644 index 0831801b41a3..000000000000 --- a/x/supply/internal/keeper/test_common.go +++ /dev/null @@ -1,107 +0,0 @@ -package keeper - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/crypto/secp256k1" - dbm "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/libs/log" - - abci "github.com/tendermint/tendermint/abci/types" - tmtypes "github.com/tendermint/tendermint/types" - - "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/store" - "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/cosmos/cosmos-sdk/x/bank" - "github.com/cosmos/cosmos-sdk/x/params" - "github.com/cosmos/cosmos-sdk/x/supply/internal/types" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// nolint: deadcode unused -var ( - multiPerm = "multiple permissions account" - randomPerm = "random permission" -) - -// nolint: deadcode unused -// create a codec used only for testing -func makeTestCodec() *codec.Codec { - var cdc = codec.New() - - bank.RegisterCodec(cdc) - auth.RegisterCodec(cdc) - types.RegisterCodec(cdc) - sdk.RegisterCodec(cdc) - codec.RegisterCrypto(cdc) - - return cdc -} - -// nolint: deadcode unused -func createTestInput(t *testing.T, isCheckTx bool, initPower int64, nAccs int64) (sdk.Context, auth.AccountKeeper, Keeper) { - - keyAcc := sdk.NewKVStoreKey(auth.StoreKey) - keyParams := sdk.NewKVStoreKey(params.StoreKey) - tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey) - keySupply := sdk.NewKVStoreKey(types.StoreKey) - - db := dbm.NewMemDB() - ms := store.NewCommitMultiStore(db) - ms.MountStoreWithDB(keyAcc, sdk.StoreTypeIAVL, db) - ms.MountStoreWithDB(keySupply, sdk.StoreTypeIAVL, db) - ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) - ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db) - err := ms.LoadLatestVersion() - require.Nil(t, err) - - ctx := sdk.NewContext(ms, abci.Header{ChainID: "supply-chain"}, isCheckTx, log.NewNopLogger()) - ctx = ctx.WithConsensusParams( - &abci.ConsensusParams{ - Validator: &abci.ValidatorParams{ - PubKeyTypes: []string{tmtypes.ABCIPubKeyTypeEd25519}, - }, - }, - ) - cdc := makeTestCodec() - - pk := params.NewKeeper(cdc, keyParams, tkeyParams, params.DefaultCodespace) - ak := auth.NewAccountKeeper(cdc, keyAcc, pk.Subspace(auth.DefaultParamspace), auth.ProtoBaseAccount) - bk := bank.NewBaseKeeper(ak, pk.Subspace(bank.DefaultParamspace), bank.DefaultCodespace) - - valTokens := sdk.TokensFromConsensusPower(initPower) - - initialCoins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, valTokens)) - createTestAccs(ctx, int(nAccs), initialCoins, &ak) - - maccPerms := map[string][]string{ - types.Basic: {types.Basic}, - types.Minter: {types.Minter}, - types.Burner: {types.Burner}, - multiPerm: {types.Basic, types.Minter, types.Burner}, - randomPerm: {"random"}, - } - keeper := NewKeeper(cdc, keySupply, ak, bk, DefaultCodespace, maccPerms) - totalSupply := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, valTokens.MulRaw(nAccs))) - keeper.SetSupply(ctx, types.NewSupply(totalSupply)) - - return ctx, ak, keeper -} - -// nolint: unparam deadcode unused -func createTestAccs(ctx sdk.Context, numAccs int, initialCoins sdk.Coins, ak *auth.AccountKeeper) (accs []auth.Account) { - for i := 0; i < numAccs; i++ { - privKey := secp256k1.GenPrivKey() - pubKey := privKey.PubKey() - addr := sdk.AccAddress(pubKey.Address()) - acc := auth.NewBaseAccountWithAddress(addr) - acc.Coins = initialCoins - acc.PubKey = pubKey - acc.AccountNumber = uint64(i) - ak.SetAccount(ctx, &acc) - } - return -} diff --git a/x/upgrade/keeper_test.go b/x/upgrade/keeper_test.go index ddf190a54a21..2c694a5f0e6f 100644 --- a/x/upgrade/keeper_test.go +++ b/x/upgrade/keeper_test.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/suite" abci "github.com/tendermint/tendermint/abci/types" - dbm "github.com/tendermint/tendermint/libs/db" + dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/libs/log" ) From 33f8d41c078ce009415bbabd17c6aa5164601dde Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 29 Aug 2019 19:06:34 +0200 Subject: [PATCH 17/62] Removed all traces of "informational" software upgrade proposal --- x/gov/alias.go | 3 --- x/gov/client/rest/rest.go | 2 +- x/gov/legacy/v0_34/types.go | 13 --------- x/gov/legacy/v0_36/migrate.go | 2 -- x/gov/legacy/v0_36/types.go | 27 ------------------- x/gov/test_common.go | 2 +- x/gov/types/proposal.go | 51 ++--------------------------------- 7 files changed, 4 insertions(+), 96 deletions(-) diff --git a/x/gov/alias.go b/x/gov/alias.go index c8942f63fc7e..17de3bb684ce 100644 --- a/x/gov/alias.go +++ b/x/gov/alias.go @@ -41,7 +41,6 @@ const ( StatusRejected = types.StatusRejected StatusFailed = types.StatusFailed ProposalTypeText = types.ProposalTypeText - ProposalTypeSoftwareUpgrade = types.ProposalTypeSoftwareUpgrade QueryParams = types.QueryParams QueryProposals = types.QueryProposals QueryProposal = types.QueryProposal @@ -111,7 +110,6 @@ var ( ProposalStatusFromString = types.ProposalStatusFromString ValidProposalStatus = types.ValidProposalStatus NewTextProposal = types.NewTextProposal - NewSoftwareUpgradeProposal = types.NewSoftwareUpgradeProposal RegisterProposalType = types.RegisterProposalType ContentFromProposalType = types.ContentFromProposalType IsValidProposalType = types.IsValidProposalType @@ -160,7 +158,6 @@ type ( ProposalQueue = types.ProposalQueue ProposalStatus = types.ProposalStatus TextProposal = types.TextProposal - SoftwareUpgradeProposal = types.SoftwareUpgradeProposal QueryProposalParams = types.QueryProposalParams QueryDepositParams = types.QueryDepositParams QueryVoteParams = types.QueryVoteParams diff --git a/x/gov/client/rest/rest.go b/x/gov/client/rest/rest.go index cfded3eeeecb..eca37e364416 100644 --- a/x/gov/client/rest/rest.go +++ b/x/gov/client/rest/rest.go @@ -39,7 +39,7 @@ type PostProposalReq struct { BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` Title string `json:"title" yaml:"title"` // Title of the proposal Description string `json:"description" yaml:"description"` // Description of the proposal - ProposalType string `json:"proposal_type" yaml:"proposal_type"` // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal} + ProposalType string `json:"proposal_type" yaml:"proposal_type"` // Type of proposal. Initial set {PlainTextProposal } Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"` // Address of the proposer InitialDeposit sdk.Coins `json:"initial_deposit" yaml:"initial_deposit"` // Coins to add to the proposal's deposit } diff --git a/x/gov/legacy/v0_34/types.go b/x/gov/legacy/v0_34/types.go index cbc139b01e3c..23e5eebc5e47 100644 --- a/x/gov/legacy/v0_34/types.go +++ b/x/gov/legacy/v0_34/types.go @@ -13,7 +13,6 @@ import ( var ( _ ProposalContent = TextProposal{} - _ ProposalContent = SoftwareUpgradeProposal{} ) const ( @@ -35,14 +34,9 @@ const ( ProposalTypeNil ProposalKind = 0x00 ProposalTypeText ProposalKind = 0x01 ProposalTypeParameterChange ProposalKind = 0x02 - ProposalTypeSoftwareUpgrade ProposalKind = 0x03 ) type ( - SoftwareUpgradeProposal struct { - TextProposal - } - ProposalQueue []uint64 ProposalKind byte @@ -142,8 +136,6 @@ func (tp TextProposal) GetTitle() string { return tp.Title } func (tp TextProposal) GetDescription() string { return tp.Description } func (tp TextProposal) ProposalType() ProposalKind { return ProposalTypeText } -func (sup SoftwareUpgradeProposal) ProposalType() ProposalKind { return ProposalTypeSoftwareUpgrade } - // ProposalStatusToString turns a string into a ProposalStatus func ProposalStatusFromString(str string) (ProposalStatus, error) { switch str { @@ -290,8 +282,6 @@ func ProposalTypeFromString(str string) (ProposalKind, error) { return ProposalTypeText, nil case "ParameterChange": return ProposalTypeParameterChange, nil - case "SoftwareUpgrade": - return ProposalTypeSoftwareUpgrade, nil default: return ProposalKind(0xff), fmt.Errorf("'%s' is not a valid proposal type", str) } @@ -331,8 +321,6 @@ func (pt ProposalKind) String() string { return "Text" case ProposalTypeParameterChange: return "ParameterChange" - case ProposalTypeSoftwareUpgrade: - return "SoftwareUpgrade" default: return "" } @@ -341,5 +329,4 @@ func (pt ProposalKind) String() string { func RegisterCodec(cdc *codec.Codec) { cdc.RegisterInterface((*ProposalContent)(nil), nil) cdc.RegisterConcrete(TextProposal{}, "gov/TextProposal", nil) - cdc.RegisterConcrete(SoftwareUpgradeProposal{}, "gov/SoftwareUpgradeProposal", nil) } diff --git a/x/gov/legacy/v0_36/migrate.go b/x/gov/legacy/v0_36/migrate.go index 8bc892bd81ef..6b55a53ea462 100644 --- a/x/gov/legacy/v0_36/migrate.go +++ b/x/gov/legacy/v0_36/migrate.go @@ -43,8 +43,6 @@ func migrateContent(proposalContent v034gov.ProposalContent) (content Content) { switch proposalContent.ProposalType() { case v034gov.ProposalTypeText: return NewTextProposal(proposalContent.GetTitle(), proposalContent.GetDescription()) - case v034gov.ProposalTypeSoftwareUpgrade: - return NewSoftwareUpgradeProposal(proposalContent.GetTitle(), proposalContent.GetDescription()) default: return nil } diff --git a/x/gov/legacy/v0_36/types.go b/x/gov/legacy/v0_36/types.go index 64d30a6527fc..abc34fbef763 100644 --- a/x/gov/legacy/v0_36/types.go +++ b/x/gov/legacy/v0_36/types.go @@ -19,7 +19,6 @@ const ( DefaultCodespace sdk.CodespaceType = "gov" ProposalTypeText string = "Text" - ProposalTypeSoftwareUpgrade string = "SoftwareUpgrade" MaxDescriptionLength int = 5000 MaxTitleLength int = 140 @@ -29,7 +28,6 @@ const ( var ( _ Content = TextProposal{} - _ Content = SoftwareUpgradeProposal{} ) type ( @@ -41,11 +39,6 @@ type ( Description string `json:"description"` } - SoftwareUpgradeProposal struct { - Title string `json:"title"` - Description string `json:"description"` - } - Content interface { GetTitle() string GetDescription() string @@ -114,25 +107,6 @@ func (tp TextProposal) String() string { `, tp.Title, tp.Description) } -func NewSoftwareUpgradeProposal(title, description string) Content { - return SoftwareUpgradeProposal{title, description} -} - -func (sup SoftwareUpgradeProposal) GetTitle() string { return sup.Title } -func (sup SoftwareUpgradeProposal) GetDescription() string { return sup.Description } -func (sup SoftwareUpgradeProposal) ProposalRoute() string { return RouterKey } -func (sup SoftwareUpgradeProposal) ProposalType() string { return ProposalTypeSoftwareUpgrade } -func (sup SoftwareUpgradeProposal) ValidateBasic() sdk.Error { - return ValidateAbstract(DefaultCodespace, sup) -} - -func (sup SoftwareUpgradeProposal) String() string { - return fmt.Sprintf(`Software Upgrade Proposal: - Title: %s - Description: %s -`, sup.Title, sup.Description) -} - func ErrInvalidProposalContent(cs sdk.CodespaceType, msg string) sdk.Error { return sdk.NewError(cs, CodeInvalidContent, fmt.Sprintf("invalid proposal content: %s", msg)) } @@ -160,5 +134,4 @@ func ValidateAbstract(codespace sdk.CodespaceType, c Content) sdk.Error { func RegisterCodec(cdc *codec.Codec) { cdc.RegisterInterface((*Content)(nil), nil) cdc.RegisterConcrete(TextProposal{}, "cosmos-sdk/TextProposal", nil) - cdc.RegisterConcrete(SoftwareUpgradeProposal{}, "cosmos-sdk/SoftwareUpgradeProposal", nil) } diff --git a/x/gov/test_common.go b/x/gov/test_common.go index b309068bef79..fecaf471f059 100644 --- a/x/gov/test_common.go +++ b/x/gov/test_common.go @@ -195,7 +195,7 @@ const contextKeyBadProposal = "contextKeyBadProposal" // for the key contextKeyBadProposal or if the value is false. func badProposalHandler(ctx sdk.Context, c types.Content) sdk.Error { switch c.ProposalType() { - case types.ProposalTypeText, types.ProposalTypeSoftwareUpgrade: + case types.ProposalTypeText: v := ctx.Value(contextKeyBadProposal) if v == nil || !v.(bool) { diff --git a/x/gov/types/proposal.go b/x/gov/types/proposal.go index 0a1a544460c8..0ac7b59f653f 100644 --- a/x/gov/types/proposal.go +++ b/x/gov/types/proposal.go @@ -202,7 +202,6 @@ func (status ProposalStatus) Format(s fmt.State, verb rune) { // Proposal types const ( ProposalTypeText string = "Text" - ProposalTypeSoftwareUpgrade string = "SoftwareUpgrade" ) // TextProposal defines a standard text proposal whose changes need to be @@ -243,52 +242,9 @@ func (tp TextProposal) String() string { `, tp.Title, tp.Description) } -// SoftwareUpgradeProposal defines a proposal for upgrading the network nodes -// without the need of manually halting at a given height -// -// TODO: We have to add fields for SUP specific arguments e.g. commit hash, -// upgrade date, etc. -type SoftwareUpgradeProposal struct { - Title string `json:"title" yaml:"title"` - Description string `json:"description" yaml:"description"` -} - -// NewSoftwareUpgradeProposal creates a software upgrade proposal Content -func NewSoftwareUpgradeProposal(title, description string) Content { - return SoftwareUpgradeProposal{title, description} -} - -// Implements Content Interface -var _ Content = SoftwareUpgradeProposal{} - -// GetTitle returns the proposal title -func (sup SoftwareUpgradeProposal) GetTitle() string { return sup.Title } - -// GetDescription returns the proposal description -func (sup SoftwareUpgradeProposal) GetDescription() string { return sup.Description } - -// ProposalRoute returns the proposal router key -func (sup SoftwareUpgradeProposal) ProposalRoute() string { return RouterKey } - -// ProposalType is "SoftwareUpgrade" -func (sup SoftwareUpgradeProposal) ProposalType() string { return ProposalTypeSoftwareUpgrade } - -// ValidateBasic validates the content's title and description of the proposal -func (sup SoftwareUpgradeProposal) ValidateBasic() sdk.Error { - return ValidateAbstract(DefaultCodespace, sup) -} - -// String implements Stringer interface -func (sup SoftwareUpgradeProposal) String() string { - return fmt.Sprintf(`Software Upgrade Proposal: - Title: %s - Description: %s -`, sup.Title, sup.Description) -} var validProposalTypes = map[string]struct{}{ ProposalTypeText: {}, - ProposalTypeSoftwareUpgrade: {}, } // RegisterProposalType registers a proposal type. It will panic if the type is @@ -307,9 +263,6 @@ func ContentFromProposalType(title, desc, ty string) Content { case ProposalTypeText: return NewTextProposal(title, desc) - case ProposalTypeSoftwareUpgrade: - return NewSoftwareUpgradeProposal(title, desc) - default: return nil } @@ -325,12 +278,12 @@ func IsValidProposalType(ty string) bool { } // ProposalHandler implements the Handler interface for governance module-based -// proposals (ie. TextProposal and SoftwareUpgradeProposal). Since these are +// proposals (ie. TextProposal ). Since these are // merely signaling mechanisms at the moment and do not affect state, it // performs a no-op. func ProposalHandler(_ sdk.Context, c Content) sdk.Error { switch c.ProposalType() { - case ProposalTypeText, ProposalTypeSoftwareUpgrade: + case ProposalTypeText: // both proposal types do not change state so this performs a no-op return nil From 585f242b10697a123a373e7bf8985ba6e06cf9b1 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 20 Sep 2019 11:34:16 +0200 Subject: [PATCH 18/62] Fix go lint issues --- x/upgrade/client/cli/query.go | 1 + x/upgrade/client/rest/rest.go | 3 ++- x/upgrade/keeper.go | 24 +++--------------------- x/upgrade/keeper_test.go | 2 +- x/upgrade/proposal.go | 1 + 5 files changed, 8 insertions(+), 23 deletions(-) diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go index 00c37d8372f3..77b146371b45 100644 --- a/x/upgrade/client/cli/query.go +++ b/x/upgrade/client/cli/query.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/x/upgrade" diff --git a/x/upgrade/client/rest/rest.go b/x/upgrade/client/rest/rest.go index 99188bc1feea..16508e17728d 100644 --- a/x/upgrade/client/rest/rest.go +++ b/x/upgrade/client/rest/rest.go @@ -2,12 +2,13 @@ package rest import ( "fmt" + "net/http" + "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/types/rest" "github.com/cosmos/cosmos-sdk/x/upgrade" "github.com/gorilla/mux" - "net/http" ) // RegisterRoutes registers REST routes for the upgrade module under the path specified by routeName. diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index 8ba4727907cb..2857102db7bc 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -72,10 +72,8 @@ func (keeper *keeper) ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error { if plan.Height != 0 { return sdk.ErrUnknownRequest("Only one of Time or Height should be specified") } - } else { - if plan.Height <= ctx.BlockHeight() { - return sdk.ErrUnknownRequest("Upgrade cannot be scheduled in the past") - } + } else if plan.Height <= ctx.BlockHeight() { + return sdk.ErrUnknownRequest("Upgrade cannot be scheduled in the past") } store := ctx.KVStore(keeper.storeKey) if store.Has(DoneHeightKey(plan.Name)) { @@ -149,24 +147,8 @@ func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) // set the handler already _, ok := keeper.upgradeHandlers[plan.Name] if ok { - ctx.Logger().Error(fmt.Sprintf("UNKOWN UPGRADE \"%s\" - in binary but not executed on chain", plan.Name)) + ctx.Logger().Error(fmt.Sprintf("UNKNOWN UPGRADE \"%s\" - in binary but not executed on chain", plan.Name)) panic("BINARY UPDATED BEFORE TRIGGER!") } } } - -type logWriter struct { - sdk.Context - script string - err bool -} - -func (w logWriter) Write(p []byte) (n int, err error) { - s := fmt.Sprintf("script %s: %s", w.script, p) - if w.err { - w.Logger().Error(s) - } else { - w.Logger().Info(s) - } - return len(p), nil -} diff --git a/x/upgrade/keeper_test.go b/x/upgrade/keeper_test.go index 2c694a5f0e6f..e2ba4b093529 100644 --- a/x/upgrade/keeper_test.go +++ b/x/upgrade/keeper_test.go @@ -9,8 +9,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/suite" abci "github.com/tendermint/tendermint/abci/types" - dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" ) type TestSuite struct { diff --git a/x/upgrade/proposal.go b/x/upgrade/proposal.go index 8960e0d4464c..d9043e5fcc3e 100644 --- a/x/upgrade/proposal.go +++ b/x/upgrade/proposal.go @@ -2,6 +2,7 @@ package upgrade import ( "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/gov" ) From 8d1788fcf86af88e68a93949801bb2166527fa2f Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 30 Sep 2019 15:57:53 +0200 Subject: [PATCH 19/62] Address some PR comments --- x/upgrade/client/cli/query.go | 7 +++-- x/upgrade/client/cli/tx.go | 56 +++++++++++++++++------------------ 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go index 77b146371b45..3ebcaa24165c 100644 --- a/x/upgrade/client/cli/query.go +++ b/x/upgrade/client/cli/query.go @@ -14,6 +14,7 @@ func GetPlanCmd(storeName string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "plan", Short: "get upgrade plan (if one exists)", + Long: "This gets the currently scheduled upgrade plan", Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -42,8 +43,10 @@ func GetPlanCmd(storeName string, cdc *codec.Codec) *cobra.Command { func GetAppliedHeightCmd(storeName string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "applied-height [upgrade-name]", - Short: "get the height at which a completed upgrade was applied", - Args: cobra.ExactArgs(1), + Short: "height at which a completed upgrade was applied", + Long: "If upgrade-name was previously executed on the chain, this returns the height at which it was applied.\n" + + "This helps a client determine which binary was valid over a given range of blocks.", + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go index 88bf0b795b0d..5eb01f1e588e 100644 --- a/x/upgrade/client/cli/tx.go +++ b/x/upgrade/client/cli/tx.go @@ -3,7 +3,7 @@ package cli import ( "fmt" "strings" - time2 "time" + "time" "github.com/cosmos/cosmos-sdk/x/gov/client/cli" @@ -19,7 +19,13 @@ import ( ) const ( + // TimeFormat specifies ISO UTC format for submitting the upgrade-time for a new upgrade proposal TimeFormat = "2006-01-02T15:04:05Z" + + FlagUpgradeName = "upgrade-name" + FlagUpgradeHeight = "upgrade-height" + FlagUpgradeTime = "upgrade-time" + FlagUpgradeInfo = "upgrade-info" ) // GetCmdSubmitUpgradeProposal implements a command handler for submitting a software upgrade proposal transaction. @@ -28,21 +34,15 @@ func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { Use: "software-upgrade --upgrade-name [name] (--upgrade-height [height] | --upgrade-time [time]) (--upgrade-info [info]) [flags]", Args: cobra.ExactArgs(0), Short: "Submit a software upgrade proposal", - Long: strings.TrimSpace( - fmt.Sprintf(`Submit a software upgrade along with an initial deposit. -`, - ), - ), + Long: "Submit a software upgrade along with an initial deposit.\n" + + "Please specify a unique name and height OR time for the upgrade to take effect.\n" + + "You may include info to reference a binary download link, in a format compatible with: https://github.com/regen-network/cosmosd", RunE: func(cmd *cobra.Command, args []string) error { txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) - cliCtx := context.NewCLIContext(). - WithCodec(cdc) - // removed due to #4588 (verifyt his didn't break anything) - // WithAccountDecoder(cdc) - + cliCtx := context.NewCLIContext().WithCodec(cdc) from := cliCtx.GetFromAddress() - depositStr, err := cmd.Flags().GetString("deposit") + depositStr, err := cmd.Flags().GetString(cli.FlagDeposit) if err != nil { return err } @@ -52,17 +52,17 @@ func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { return err } - title, err := cmd.Flags().GetString("title") + title, err := cmd.Flags().GetString(cli.FlagTitle) if err != nil { return err } - description, err := cmd.Flags().GetString("description") + description, err := cmd.Flags().GetString(cli.FlagDescription) if err != nil { return err } - name, err := cmd.Flags().GetString("upgrade-name") + name, err := cmd.Flags().GetString(FlagUpgradeName) if err != nil { return err } @@ -70,12 +70,12 @@ func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { name = title } - height, err := cmd.Flags().GetInt64("upgrade-height") + height, err := cmd.Flags().GetInt64(FlagUpgradeHeight) if err != nil { return err } - timeStr, err := cmd.Flags().GetString("upgrade-time") + timeStr, err := cmd.Flags().GetString(FlagUpgradeTime) if err != nil { return err } @@ -86,21 +86,21 @@ func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { } } - var time time2.Time + var upgradeTime time.Time if len(timeStr) != 0 { - time, err = time2.Parse(TimeFormat, timeStr) + upgradeTime, err = time.Parse(TimeFormat, timeStr) if err != nil { return err } } - info, err := cmd.Flags().GetString("upgrade-info") + info, err := cmd.Flags().GetString(FlagUpgradeInfo) if err != nil { return err } content := upgrade.NewSoftwareUpgradeProposal(title, description, - upgrade.Plan{Name: name, Time: time, Height: height, Info: info}) + upgrade.Plan{Name: name, Time: upgradeTime, Height: height, Info: info}) msg := gov.NewMsgSubmitProposal(content, deposit, from) if err := msg.ValidateBasic(); err != nil { @@ -114,10 +114,10 @@ func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { cmd.Flags().String(cli.FlagTitle, "", "title of proposal") cmd.Flags().String(cli.FlagDescription, "", "description of proposal") cmd.Flags().String(cli.FlagDeposit, "", "deposit of proposal") - cmd.Flags().String("upgrade-name", "", "The name of the upgrade (if not specified title will be used)") - cmd.Flags().Int64("upgrade-height", 0, "The height at which the upgrade must happen (not to be used together with --upgrade-time)") - cmd.Flags().String("upgrade-time", "", fmt.Sprintf("The time at which the upgrade must happen (ex. %s) (not to be used together with --upgrade-height)", TimeFormat)) - cmd.Flags().String("upgrade-info", "", "Optional info for the planned upgrade such as commit hash, etc.") + cmd.Flags().String(FlagUpgradeName, "", "The name of the upgrade (if not specified title will be used)") + cmd.Flags().Int64(FlagUpgradeHeight, 0, "The height at which the upgrade must happen (not to be used together with --upgrade-time)") + cmd.Flags().String(FlagUpgradeTime, "", fmt.Sprintf("The time at which the upgrade must happen (ex. %s) (not to be used together with --upgrade-height)", TimeFormat)) + cmd.Flags().String(FlagUpgradeInfo, "", "Optional info for the planned upgrade such as commit hash, etc.") return cmd } @@ -142,7 +142,7 @@ func GetCmdSubmitCancelUpgradeProposal(cdc *codec.Codec) *cobra.Command { from := cliCtx.GetFromAddress() - depositStr, err := cmd.Flags().GetString("deposit") + depositStr, err := cmd.Flags().GetString(cli.FlagDeposit) if err != nil { return err } @@ -152,12 +152,12 @@ func GetCmdSubmitCancelUpgradeProposal(cdc *codec.Codec) *cobra.Command { return err } - title, err := cmd.Flags().GetString("title") + title, err := cmd.Flags().GetString(cli.FlagTitle) if err != nil { return err } - description, err := cmd.Flags().GetString("description") + description, err := cmd.Flags().GetString(cli.FlagDescription) if err != nil { return err } From a75e5e63b3faca3b8ace55baabf9d1bb2012496f Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 30 Sep 2019 16:48:38 +0200 Subject: [PATCH 20/62] goimports --- x/upgrade/client/module_client.go | 2 +- x/upgrade/types.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x/upgrade/client/module_client.go b/x/upgrade/client/module_client.go index 4027a31270e8..7c00eb9f1160 100644 --- a/x/upgrade/client/module_client.go +++ b/x/upgrade/client/module_client.go @@ -2,7 +2,7 @@ package client import ( "github.com/spf13/cobra" - "github.com/tendermint/go-amino" + amino "github.com/tendermint/go-amino" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/x/upgrade/client/cli" diff --git a/x/upgrade/types.go b/x/upgrade/types.go index fa9b2a45f30e..cbb897efa467 100644 --- a/x/upgrade/types.go +++ b/x/upgrade/types.go @@ -3,8 +3,9 @@ package upgrade import ( "fmt" "time" + + sdk "github.com/cosmos/cosmos-sdk/types" ) -import sdk "github.com/cosmos/cosmos-sdk/types" // Plan specifies information about a planned upgrade and when it should occur type Plan struct { From b76bdb4cfa7cddaabaa33b262574be1ad1eefdba Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 30 Sep 2019 17:01:34 +0200 Subject: [PATCH 21/62] Updated doc.go, address golint errors --- x/upgrade/client/cli/tx.go | 109 ++++++++++++++++++---------------- x/upgrade/client/rest/rest.go | 5 +- x/upgrade/doc.go | 21 +++---- x/upgrade/keeper.go | 1 - x/upgrade/module/module.go | 6 +- 5 files changed, 69 insertions(+), 73 deletions(-) diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go index 5eb01f1e588e..f528fdd769f2 100644 --- a/x/upgrade/client/cli/tx.go +++ b/x/upgrade/client/cli/tx.go @@ -28,6 +28,59 @@ const ( FlagUpgradeInfo = "upgrade-info" ) +func parseSubmitArgs(cmd *cobra.Command, cdc *codec.Codec) (gov.Content, error) { + title, err := cmd.Flags().GetString(cli.FlagTitle) + if err != nil { + return nil, err + } + + description, err := cmd.Flags().GetString(cli.FlagDescription) + if err != nil { + return nil, err + } + + name, err := cmd.Flags().GetString(FlagUpgradeName) + if err != nil { + return nil, err + } + if len(name) == 0 { + name = title + } + + height, err := cmd.Flags().GetInt64(FlagUpgradeHeight) + if err != nil { + return nil, err + } + + timeStr, err := cmd.Flags().GetString(FlagUpgradeTime) + if err != nil { + return nil, err + } + + if height != 0 { + if len(timeStr) != 0 { + return nil, fmt.Errorf("only one of --upgrade-time or --upgrade-height should be specified") + } + } + + var upgradeTime time.Time + if len(timeStr) != 0 { + upgradeTime, err = time.Parse(TimeFormat, timeStr) + if err != nil { + return nil, err + } + } + + info, err := cmd.Flags().GetString(FlagUpgradeInfo) + if err != nil { + return nil, err + } + + content := upgrade.NewSoftwareUpgradeProposal(title, description, + upgrade.Plan{Name: name, Time: upgradeTime, Height: height, Info: info}) + return content, nil +} + // GetCmdSubmitUpgradeProposal implements a command handler for submitting a software upgrade proposal transaction. func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ @@ -38,6 +91,11 @@ func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { "Please specify a unique name and height OR time for the upgrade to take effect.\n" + "You may include info to reference a binary download link, in a format compatible with: https://github.com/regen-network/cosmosd", RunE: func(cmd *cobra.Command, args []string) error { + content, err := parseSubmitArgs(cmd, cdc) + if err != nil { + return err + } + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) cliCtx := context.NewCLIContext().WithCodec(cdc) from := cliCtx.GetFromAddress() @@ -46,62 +104,11 @@ func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { if err != nil { return err } - deposit, err := sdk.ParseCoins(depositStr) if err != nil { return err } - title, err := cmd.Flags().GetString(cli.FlagTitle) - if err != nil { - return err - } - - description, err := cmd.Flags().GetString(cli.FlagDescription) - if err != nil { - return err - } - - name, err := cmd.Flags().GetString(FlagUpgradeName) - if err != nil { - return err - } - if len(name) == 0 { - name = title - } - - height, err := cmd.Flags().GetInt64(FlagUpgradeHeight) - if err != nil { - return err - } - - timeStr, err := cmd.Flags().GetString(FlagUpgradeTime) - if err != nil { - return err - } - - if height != 0 { - if len(timeStr) != 0 { - return fmt.Errorf("only one of --upgrade-time or --upgrade-height should be specified") - } - } - - var upgradeTime time.Time - if len(timeStr) != 0 { - upgradeTime, err = time.Parse(TimeFormat, timeStr) - if err != nil { - return err - } - } - - info, err := cmd.Flags().GetString(FlagUpgradeInfo) - if err != nil { - return err - } - - content := upgrade.NewSoftwareUpgradeProposal(title, description, - upgrade.Plan{Name: name, Time: upgradeTime, Height: height, Info: info}) - msg := gov.NewMsgSubmitProposal(content, deposit, from) if err := msg.ValidateBasic(); err != nil { return err diff --git a/x/upgrade/client/rest/rest.go b/x/upgrade/client/rest/rest.go index 16508e17728d..19743c91b6ce 100644 --- a/x/upgrade/client/rest/rest.go +++ b/x/upgrade/client/rest/rest.go @@ -1,7 +1,6 @@ package rest import ( - "fmt" "net/http" "github.com/cosmos/cosmos-sdk/client/context" @@ -12,8 +11,8 @@ import ( ) // RegisterRoutes registers REST routes for the upgrade module under the path specified by routeName. -func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, routeName string, storeName string) { - r.HandleFunc(fmt.Sprintf("/%s", routeName), getUpgradePlanHandler(cdc, cliCtx, storeName)).Methods("GET") +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, storeName string) { + r.HandleFunc("/"+upgrade.ModuleName, getUpgradePlanHandler(cdc, cliCtx, storeName)).Methods("GET") } func getUpgradePlanHandler(cdc *codec.Codec, cliCtx context.CLIContext, storeName string) func(http.ResponseWriter, *http.Request) { diff --git a/x/upgrade/doc.go b/x/upgrade/doc.go index 26747e7a84db..5c8f1ccb15be 100644 --- a/x/upgrade/doc.go +++ b/x/upgrade/doc.go @@ -68,18 +68,13 @@ To perform the actual halt of the blockchain, the upgrade keeper simply panic's from proceeding but doesn't actually exit the process. Exiting the process can cause issues for other nodes that start to lose connectivity with the exiting nodes, thus this module prefers to just halt but not exit. -Will Upgrade and On Upgrade Callbacks - -The upgrade keeper has two methods for setting callbacks - SetWillUpgrader and SetOnUpgrader. Custom callbacks can be -configured or the default ones will be used. DefaultWillUpgrader and DefaultOnUpgrader will call scripts -called prepare-upgrade and do-upgrade respectively in the config directory of the running daemon if such files exist with -the JSON-serialized upgrade plan as the first argument and the current block height as the second argument. The will upgrade -callback will be called in the BeginBlocker of the first block after an upgrade is scheduled. The on upgrade callback -will be called in the BeginBlocker at the block where an upgrade is needed right before the state machine is halted. -The will upgrade callback can be used to notify some external process that an upgrade is needed so that it can -prepare binaries, etc. The on upgrade callback can notify some external process to actually begin the upgrade process. - -BUG(aaronc): will upgrade callbacks are temporarily disabled - +Automation and Plan.Info + +We have deprecated calling out to scripts, instead with propose https://github.com/regen-network/cosmosd +as a model for a watcher daemon that can launch gaiad as a subprocess and then read the upgrade log message +to swap binaries as needed. You can pass in information into Plan.Info according to the format +specified here https://github.com/regen-network/cosmosd/blob/master/README.md#auto-download . +This will allow a properly configured cosmsod daemon to auto-download new binaries and auto-upgrade. +As noted there, this is intended more for full nodes than validators. */ package upgrade diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index 2857102db7bc..ddb0d8d41a42 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -95,7 +95,6 @@ func (keeper *keeper) ClearUpgradePlan(ctx sdk.Context) { func (plan Plan) ValidateBasic() sdk.Error { if len(plan.Name) == 0 { return sdk.ErrUnknownRequest("Name cannot be empty") - } return nil } diff --git a/x/upgrade/module/module.go b/x/upgrade/module/module.go index 28d5a3c4791e..7713ee87b618 100644 --- a/x/upgrade/module/module.go +++ b/x/upgrade/module/module.go @@ -58,8 +58,7 @@ func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { } func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, r *mux.Router) { - // TODO: is ModuleName the proper route??? - rest.RegisterRoutes(ctx, r, moduleCdc, upgrade.ModuleName, upgrade.StoreKey) + rest.RegisterRoutes(ctx, r, moduleCdc, upgrade.StoreKey) } // GetQueryCmd returns the cli query commands for this module @@ -83,13 +82,10 @@ func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { Use: "upgrade", Short: "Upgrade transaction subcommands", } - txCmd.AddCommand(client.PostCommands()...) - return txCmd } -//___________________________ // app module type AppModule struct { AppModuleBasic From 1a018a2b1ef452fd82ca3abe88517583a7c13606 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 30 Sep 2019 17:06:42 +0200 Subject: [PATCH 22/62] Some more PR cleanup --- x/upgrade/client/cli/tx.go | 6 +----- x/upgrade/client/module_client.go | 2 -- x/upgrade/proposal.go | 10 +++++----- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go index f528fdd769f2..df74f2d0e37c 100644 --- a/x/upgrade/client/cli/tx.go +++ b/x/upgrade/client/cli/tx.go @@ -142,11 +142,7 @@ func GetCmdSubmitCancelUpgradeProposal(cdc *codec.Codec) *cobra.Command { ), RunE: func(cmd *cobra.Command, args []string) error { txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) - cliCtx := context.NewCLIContext(). - WithCodec(cdc) - // removed due to #4588 (verifyt his didn't break anything) - // WithAccountDecoder(cdc) - + cliCtx := context.NewCLIContext().WithCodec(cdc) from := cliCtx.GetFromAddress() depositStr, err := cmd.Flags().GetString(cli.FlagDeposit) diff --git a/x/upgrade/client/module_client.go b/x/upgrade/client/module_client.go index 7c00eb9f1160..1b96d3d0185a 100644 --- a/x/upgrade/client/module_client.go +++ b/x/upgrade/client/module_client.go @@ -31,7 +31,6 @@ func (mc ModuleClient) GetQueryCmd() *cobra.Command { )...) return queryCmd - } // GetTxCmd returns the transaction commands for this module @@ -40,7 +39,6 @@ func (mc ModuleClient) GetTxCmd() *cobra.Command { Use: "upgrade", Short: "Upgrade transaction subcommands", } - txCmd.AddCommand(client.PostCommands()...) return txCmd diff --git a/x/upgrade/proposal.go b/x/upgrade/proposal.go index d9043e5fcc3e..0a95674bd2f1 100644 --- a/x/upgrade/proposal.go +++ b/x/upgrade/proposal.go @@ -15,9 +15,9 @@ const ( // Software Upgrade Proposals type SoftwareUpgradeProposal struct { - Title string `json:"title"` - Description string `json:"description"` - Plan Plan `json:"plan"` + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + Plan Plan `json:"plan" yaml:"plan"` } func NewSoftwareUpgradeProposal(title, description string, plan Plan) gov.Content { @@ -52,8 +52,8 @@ func (sup SoftwareUpgradeProposal) String() string { // Cancel Software Upgrade Proposals type CancelSoftwareUpgradeProposal struct { - Title string `json:"title"` - Description string `json:"description"` + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` } func NewCancelSoftwareUpgradeProposal(title, description string) gov.Content { From 5e509a08dec198bdb2bd7303eff78b9976bc9b1e Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 30 Sep 2019 19:28:24 +0200 Subject: [PATCH 23/62] Cleanup CHANGELOG --- .pending/features/sdk/Add-upgrade-module-t | 1 - CHANGELOG.md | 4 ++++ x/upgrade/module/module.go | 1 - 3 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 .pending/features/sdk/Add-upgrade-module-t diff --git a/.pending/features/sdk/Add-upgrade-module-t b/.pending/features/sdk/Add-upgrade-module-t deleted file mode 100644 index 2d5bd773a98e..000000000000 --- a/.pending/features/sdk/Add-upgrade-module-t +++ /dev/null @@ -1 +0,0 @@ -#3979 Add upgrade module that coordinates software upgrades of live chains. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ac24ddc3bbc..2e7e4afe5e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,9 +77,13 @@ the following [issue](https://github.com/keybase/go-keychain/issues/47) with the you encounter this issue, you must upgrade your xcode command line tools to version >= `10.2`. You can upgrade via: `sudo rm -rf /Library/Developer/CommandLineTools; xcode-select --install`. Verify the correct version via: `pkgutil --pkg-info=com.apple.pkg.CLTools_Executables`. +<<<<<<< HEAD * (keys) [\#5097](https://github.com/cosmos/cosmos-sdk/pull/5097) New `keys migrate` command to assist users migrate their keys to the new keyring. +======= +* (modules) [\#4233](https://github.com/cosmos/cosmos-sdk/pull/4233) Add upgrade module that coordinates software upgrades of live chains. +>>>>>>> Cleanup CHANGELOG ### Improvements diff --git a/x/upgrade/module/module.go b/x/upgrade/module/module.go index 7713ee87b618..236a3a54f30d 100644 --- a/x/upgrade/module/module.go +++ b/x/upgrade/module/module.go @@ -73,7 +73,6 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { )...) return queryCmd - } // GetTxCmd returns the transaction commands for this module From 7b19ead16a0143e19d0bc695dd89f3596ae7b589 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 30 Sep 2019 19:34:35 +0200 Subject: [PATCH 24/62] Change PlanKey/DoneKey to bytes --- x/upgrade/client/cli/query.go | 2 +- x/upgrade/client/rest/rest.go | 2 +- x/upgrade/keeper.go | 28 ++++++++++++++++++---------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go index 3ebcaa24165c..0efec33f74bb 100644 --- a/x/upgrade/client/cli/query.go +++ b/x/upgrade/client/cli/query.go @@ -20,7 +20,7 @@ func GetPlanCmd(storeName string, cdc *codec.Codec) *cobra.Command { cliCtx := context.NewCLIContext().WithCodec(cdc) // ignore height for now - res, _, err := cliCtx.QueryStore([]byte(upgrade.PlanKey), storeName) + res, _, err := cliCtx.QueryStore(upgrade.PlanKey(), storeName) if err != nil { return err } diff --git a/x/upgrade/client/rest/rest.go b/x/upgrade/client/rest/rest.go index 19743c91b6ce..91910c8cbb5c 100644 --- a/x/upgrade/client/rest/rest.go +++ b/x/upgrade/client/rest/rest.go @@ -18,7 +18,7 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, func getUpgradePlanHandler(cdc *codec.Codec, cliCtx context.CLIContext, storeName string) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, request *http.Request) { // ignore height for now - res, _, err := cliCtx.QueryStore([]byte(upgrade.PlanKey), storeName) + res, _, err := cliCtx.QueryStore(upgrade.PlanKey(), storeName) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index ddb0d8d41a42..62359503e1d7 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -43,10 +43,23 @@ type keeper struct { } const ( - // PlanKey specifies the key under which an upgrade plan is stored in the store - PlanKey = "plan" + // PlanByte specifies the Byte under which a pending upgrade plan is stored in the store + PlanByte = 0x0 + // DoneByte is a prefix for to look up completed upgrade plan by name + DoneByte = 0x1 ) +// PlanKey is the key under which the current plan is saved +// We store PlanByte as a const to keep it immutible (unlike a []byte) +func PlanKey() []byte { + return []byte{PlanByte} +} + +// DoneHeightKey returns a key that points to the height at which a past upgrade was applied +func DoneHeightKey(name string) []byte { + return append([]byte{DoneByte}, []byte(name)...) +} + // NewKeeper constructs an upgrade keeper func NewKeeper(storeKey sdk.StoreKey, cdc *codec.Codec) Keeper { return &keeper{ @@ -81,14 +94,14 @@ func (keeper *keeper) ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error { } bz := keeper.cdc.MustMarshalBinaryBare(plan) keeper.haveCache = false - store.Set([]byte(PlanKey), bz) + store.Set(PlanKey(), bz) return nil } func (keeper *keeper) ClearUpgradePlan(ctx sdk.Context) { store := ctx.KVStore(keeper.storeKey) keeper.haveCache = false - store.Delete([]byte(PlanKey)) + store.Delete(PlanKey()) } // ValidateBasic does basic validation of a Plan @@ -101,7 +114,7 @@ func (plan Plan) ValidateBasic() sdk.Error { func (keeper *keeper) GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) { store := ctx.KVStore(keeper.storeKey) - bz := store.Get([]byte(PlanKey)) + bz := store.Get(PlanKey()) if bz == nil { return plan, false } @@ -109,11 +122,6 @@ func (keeper *keeper) GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) return plan, true } -// DoneHeightKey returns a key that points to the height at which a past upgrade was applied -func DoneHeightKey(name string) []byte { - return []byte(fmt.Sprintf("done/%s", name)) -} - func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) { blockTime := ctx.BlockHeader().Time blockHeight := ctx.BlockHeight() From 07ca03eb2064b67125494d3a491680690cf7c27f Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 30 Sep 2019 19:44:28 +0200 Subject: [PATCH 25/62] Store DoneHeight in BigEndian not amino var-int --- x/upgrade/client/cli/query.go | 10 +++++----- x/upgrade/keeper.go | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go index 0efec33f74bb..1c3231e3e623 100644 --- a/x/upgrade/client/cli/query.go +++ b/x/upgrade/client/cli/query.go @@ -1,6 +1,7 @@ package cli import ( + "encoding/binary" "fmt" "github.com/cosmos/cosmos-sdk/client/context" @@ -61,12 +62,11 @@ func GetAppliedHeightCmd(storeName string, cdc *codec.Codec) *cobra.Command { if len(res) == 0 { return fmt.Errorf("no upgrade found") } - - var height int64 - err = cdc.UnmarshalBinaryBare(res, &height) - if err != nil { - return err + if len(res) != 8 { + return fmt.Errorf("unknown format for applied-upgrade") } + + height := int64(binary.BigEndian.Uint64(res)) fmt.Println(height) return nil }, diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index 62359503e1d7..a2ff6a5b4443 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -1,6 +1,7 @@ package upgrade import ( + "encoding/binary" "fmt" "github.com/cosmos/cosmos-sdk/codec" @@ -9,8 +10,10 @@ import ( ) const ( + // ModuleName is the name of this module ModuleName = "upgrade" - StoreKey = ModuleName + // StoreKey is the prefix under which we store this module's data + StoreKey = ModuleName ) // Keeper of the upgrade module @@ -122,6 +125,14 @@ func (keeper *keeper) GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) return plan, true } +// Mark this upgrade name as being done so the name can't be reused accidentally +func (keeper *keeper) setDone(ctx sdk.Context, name string) { + store := ctx.KVStore(keeper.storeKey) + bz := make([]byte, 8) + binary.BigEndian.PutUint64(bz, uint64(ctx.BlockHeight())) + store.Set(DoneHeightKey(name), bz) +} + func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) { blockTime := ctx.BlockHeader().Time blockHeight := ctx.BlockHeight() @@ -140,10 +151,7 @@ func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at height %d", plan.Name, blockHeight)) handler(ctx, plan) keeper.ClearUpgradePlan(ctx) - // Mark this upgrade name as being done so the name can't be reused accidentally - store := ctx.KVStore(keeper.storeKey) - bz := keeper.cdc.MustMarshalBinaryBare(blockHeight) - store.Set(DoneHeightKey(plan.Name), bz) + keeper.setDone(ctx, plan.Name) } else { // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", plan.Name, blockHeight, plan.Info)) From 1967997f5e995f1b67f0679f858e485b567225d1 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 30 Sep 2019 19:46:57 +0200 Subject: [PATCH 26/62] Add godoc --- x/upgrade/keeper.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go index a2ff6a5b4443..5dea58e459cf 100644 --- a/x/upgrade/keeper.go +++ b/x/upgrade/keeper.go @@ -72,10 +72,14 @@ func NewKeeper(storeKey sdk.StoreKey, cdc *codec.Codec) Keeper { } } +// SetUpgradeHandler sets an UpgradeHandler for the upgrade specified by name. This handler will be called when the upgrade +// with this name is applied. In order for an upgrade with the given name to proceed, a handler for this upgrade +// must be set even if it is a no-op function. func (keeper *keeper) SetUpgradeHandler(name string, upgradeHandler Handler) { keeper.upgradeHandlers[name] = upgradeHandler } +// ScheduleUpgrade schedules an upgrade based on the specified plan func (keeper *keeper) ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error { err := plan.ValidateBasic() if err != nil { @@ -101,6 +105,7 @@ func (keeper *keeper) ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error { return nil } +// ClearUpgradePlan clears any schedule upgrade func (keeper *keeper) ClearUpgradePlan(ctx sdk.Context) { store := ctx.KVStore(keeper.storeKey) keeper.haveCache = false @@ -115,6 +120,8 @@ func (plan Plan) ValidateBasic() sdk.Error { return nil } +// GetUpgradePlan returns the currently scheduled Plan if any, setting havePlan to true if there is a scheduled +// upgrade or false if there is none func (keeper *keeper) GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) { store := ctx.KVStore(keeper.storeKey) bz := store.Get(PlanKey()) @@ -125,7 +132,7 @@ func (keeper *keeper) GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) return plan, true } -// Mark this upgrade name as being done so the name can't be reused accidentally +// setDone marks this upgrade name as being done so the name can't be reused accidentally func (keeper *keeper) setDone(ctx sdk.Context, name string) { store := ctx.KVStore(keeper.storeKey) bz := make([]byte, 8) @@ -133,6 +140,8 @@ func (keeper *keeper) setDone(ctx sdk.Context, name string) { store.Set(DoneHeightKey(name), bz) } +// BeginBlocker should be called inside the BeginBlocker method of any app using the upgrade module. Scheduled upgrade +// plans are cached in memory so the overhead of this method is trivial. func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) { blockTime := ctx.BlockHeader().Time blockHeight := ctx.BlockHeight() From af95b8c8ad26be970fce7a2e04515954d82bb969 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 1 Oct 2019 11:33:47 +0200 Subject: [PATCH 27/62] Refactor 1: create internal/types --- x/upgrade/abci.go | 1 + x/upgrade/client/cli/query.go | 74 ------- x/upgrade/client/cli/tx.go | 184 ------------------ x/upgrade/client/module_client.go | 45 ----- x/upgrade/client/rest/rest.go | 41 ---- x/upgrade/genesis.go | 33 ---- x/upgrade/{ => internal/types}/codec.go | 2 +- x/upgrade/internal/types/keys.go | 33 ++++ .../{types.go => internal/types/plan.go} | 9 +- x/upgrade/{ => internal/types}/proposal.go | 2 +- x/upgrade/keeper.go | 178 ----------------- x/upgrade/keeper_test.go | 171 ---------------- x/upgrade/module/module.go | 144 -------------- x/upgrade/proposal_handler.go | 36 ---- 14 files changed, 41 insertions(+), 912 deletions(-) create mode 100644 x/upgrade/abci.go delete mode 100644 x/upgrade/client/cli/query.go delete mode 100644 x/upgrade/client/cli/tx.go delete mode 100644 x/upgrade/client/module_client.go delete mode 100644 x/upgrade/client/rest/rest.go delete mode 100644 x/upgrade/genesis.go rename x/upgrade/{ => internal/types}/codec.go (96%) create mode 100644 x/upgrade/internal/types/keys.go rename x/upgrade/{types.go => internal/types/plan.go} (98%) rename x/upgrade/{ => internal/types}/proposal.go (99%) delete mode 100644 x/upgrade/keeper.go delete mode 100644 x/upgrade/keeper_test.go delete mode 100644 x/upgrade/module/module.go delete mode 100644 x/upgrade/proposal_handler.go diff --git a/x/upgrade/abci.go b/x/upgrade/abci.go new file mode 100644 index 000000000000..8088f98b1d64 --- /dev/null +++ b/x/upgrade/abci.go @@ -0,0 +1 @@ +package upgrade diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go deleted file mode 100644 index 1c3231e3e623..000000000000 --- a/x/upgrade/client/cli/query.go +++ /dev/null @@ -1,74 +0,0 @@ -package cli - -import ( - "encoding/binary" - "fmt" - - "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/x/upgrade" - "github.com/spf13/cobra" -) - -// GetPlanCmd returns the query upgrade plan command -func GetPlanCmd(storeName string, cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ - Use: "plan", - Short: "get upgrade plan (if one exists)", - Long: "This gets the currently scheduled upgrade plan", - Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - cliCtx := context.NewCLIContext().WithCodec(cdc) - - // ignore height for now - res, _, err := cliCtx.QueryStore(upgrade.PlanKey(), storeName) - if err != nil { - return err - } - - if len(res) == 0 { - return fmt.Errorf("no upgrade scheduled") - } - - var plan upgrade.Plan - err = cdc.UnmarshalBinaryBare(res, &plan) - if err != nil { - return err - } - return cliCtx.PrintOutput(plan) - }, - } -} - -// GetAppliedHeightCmd returns the height at which a completed upgrade was applied -func GetAppliedHeightCmd(storeName string, cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ - Use: "applied-height [upgrade-name]", - Short: "height at which a completed upgrade was applied", - Long: "If upgrade-name was previously executed on the chain, this returns the height at which it was applied.\n" + - "This helps a client determine which binary was valid over a given range of blocks.", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - cliCtx := context.NewCLIContext().WithCodec(cdc) - - name := args[0] - - // ignore height for now - res, _, err := cliCtx.QueryStore(upgrade.DoneHeightKey(name), storeName) - if err != nil { - return err - } - - if len(res) == 0 { - return fmt.Errorf("no upgrade found") - } - if len(res) != 8 { - return fmt.Errorf("unknown format for applied-upgrade") - } - - height := int64(binary.BigEndian.Uint64(res)) - fmt.Println(height) - return nil - }, - } -} diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go deleted file mode 100644 index df74f2d0e37c..000000000000 --- a/x/upgrade/client/cli/tx.go +++ /dev/null @@ -1,184 +0,0 @@ -package cli - -import ( - "fmt" - "strings" - "time" - - "github.com/cosmos/cosmos-sdk/x/gov/client/cli" - - "github.com/spf13/cobra" - - "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - "github.com/cosmos/cosmos-sdk/x/auth/client/utils" - "github.com/cosmos/cosmos-sdk/x/gov" - "github.com/cosmos/cosmos-sdk/x/upgrade" -) - -const ( - // TimeFormat specifies ISO UTC format for submitting the upgrade-time for a new upgrade proposal - TimeFormat = "2006-01-02T15:04:05Z" - - FlagUpgradeName = "upgrade-name" - FlagUpgradeHeight = "upgrade-height" - FlagUpgradeTime = "upgrade-time" - FlagUpgradeInfo = "upgrade-info" -) - -func parseSubmitArgs(cmd *cobra.Command, cdc *codec.Codec) (gov.Content, error) { - title, err := cmd.Flags().GetString(cli.FlagTitle) - if err != nil { - return nil, err - } - - description, err := cmd.Flags().GetString(cli.FlagDescription) - if err != nil { - return nil, err - } - - name, err := cmd.Flags().GetString(FlagUpgradeName) - if err != nil { - return nil, err - } - if len(name) == 0 { - name = title - } - - height, err := cmd.Flags().GetInt64(FlagUpgradeHeight) - if err != nil { - return nil, err - } - - timeStr, err := cmd.Flags().GetString(FlagUpgradeTime) - if err != nil { - return nil, err - } - - if height != 0 { - if len(timeStr) != 0 { - return nil, fmt.Errorf("only one of --upgrade-time or --upgrade-height should be specified") - } - } - - var upgradeTime time.Time - if len(timeStr) != 0 { - upgradeTime, err = time.Parse(TimeFormat, timeStr) - if err != nil { - return nil, err - } - } - - info, err := cmd.Flags().GetString(FlagUpgradeInfo) - if err != nil { - return nil, err - } - - content := upgrade.NewSoftwareUpgradeProposal(title, description, - upgrade.Plan{Name: name, Time: upgradeTime, Height: height, Info: info}) - return content, nil -} - -// GetCmdSubmitUpgradeProposal implements a command handler for submitting a software upgrade proposal transaction. -func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { - cmd := &cobra.Command{ - Use: "software-upgrade --upgrade-name [name] (--upgrade-height [height] | --upgrade-time [time]) (--upgrade-info [info]) [flags]", - Args: cobra.ExactArgs(0), - Short: "Submit a software upgrade proposal", - Long: "Submit a software upgrade along with an initial deposit.\n" + - "Please specify a unique name and height OR time for the upgrade to take effect.\n" + - "You may include info to reference a binary download link, in a format compatible with: https://github.com/regen-network/cosmosd", - RunE: func(cmd *cobra.Command, args []string) error { - content, err := parseSubmitArgs(cmd, cdc) - if err != nil { - return err - } - - txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) - cliCtx := context.NewCLIContext().WithCodec(cdc) - from := cliCtx.GetFromAddress() - - depositStr, err := cmd.Flags().GetString(cli.FlagDeposit) - if err != nil { - return err - } - deposit, err := sdk.ParseCoins(depositStr) - if err != nil { - return err - } - - msg := gov.NewMsgSubmitProposal(content, deposit, from) - if err := msg.ValidateBasic(); err != nil { - return err - } - - return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) - }, - } - - cmd.Flags().String(cli.FlagTitle, "", "title of proposal") - cmd.Flags().String(cli.FlagDescription, "", "description of proposal") - cmd.Flags().String(cli.FlagDeposit, "", "deposit of proposal") - cmd.Flags().String(FlagUpgradeName, "", "The name of the upgrade (if not specified title will be used)") - cmd.Flags().Int64(FlagUpgradeHeight, 0, "The height at which the upgrade must happen (not to be used together with --upgrade-time)") - cmd.Flags().String(FlagUpgradeTime, "", fmt.Sprintf("The time at which the upgrade must happen (ex. %s) (not to be used together with --upgrade-height)", TimeFormat)) - cmd.Flags().String(FlagUpgradeInfo, "", "Optional info for the planned upgrade such as commit hash, etc.") - - return cmd -} - -// GetCmdSubmitCancelUpgradeProposal implements a command handler for submitting a software upgrade cancel proposal transaction. -func GetCmdSubmitCancelUpgradeProposal(cdc *codec.Codec) *cobra.Command { - cmd := &cobra.Command{ - Use: "cancel-software-upgrade [flags]", - Args: cobra.ExactArgs(0), - Short: "Submit a software upgrade proposal", - Long: strings.TrimSpace( - fmt.Sprintf(`Submit a software upgrade along with an initial deposit. -`, - ), - ), - RunE: func(cmd *cobra.Command, args []string) error { - txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) - cliCtx := context.NewCLIContext().WithCodec(cdc) - from := cliCtx.GetFromAddress() - - depositStr, err := cmd.Flags().GetString(cli.FlagDeposit) - if err != nil { - return err - } - - deposit, err := sdk.ParseCoins(depositStr) - if err != nil { - return err - } - - title, err := cmd.Flags().GetString(cli.FlagTitle) - if err != nil { - return err - } - - description, err := cmd.Flags().GetString(cli.FlagDescription) - if err != nil { - return err - } - - content := upgrade.NewCancelSoftwareUpgradeProposal(title, description) - - msg := gov.NewMsgSubmitProposal(content, deposit, from) - if err := msg.ValidateBasic(); err != nil { - return err - } - - return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) - }, - } - - cmd.Flags().String(cli.FlagTitle, "", "title of proposal") - cmd.Flags().String(cli.FlagDescription, "", "description of proposal") - cmd.Flags().String(cli.FlagDeposit, "", "deposit of proposal") - - return cmd -} diff --git a/x/upgrade/client/module_client.go b/x/upgrade/client/module_client.go deleted file mode 100644 index 1b96d3d0185a..000000000000 --- a/x/upgrade/client/module_client.go +++ /dev/null @@ -1,45 +0,0 @@ -package client - -import ( - "github.com/spf13/cobra" - amino "github.com/tendermint/go-amino" - - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/x/upgrade/client/cli" -) - -// ModuleClient exports all client functionality from this module -type ModuleClient struct { - storeKey string - cdc *amino.Codec -} - -// NewModuleClient returns an upgrade ModuleClient -func NewModuleClient(storeKey string, cdc *amino.Codec) ModuleClient { - return ModuleClient{storeKey, cdc} -} - -// GetQueryCmd returns the cli query commands for this module -func (mc ModuleClient) GetQueryCmd() *cobra.Command { - queryCmd := &cobra.Command{ - Use: "upgrade", - Short: "Querying commands for the upgrade module", - } - queryCmd.AddCommand(client.GetCommands( - cli.GetPlanCmd(mc.storeKey, mc.cdc), - cli.GetAppliedHeightCmd(mc.storeKey, mc.cdc), - )...) - - return queryCmd -} - -// GetTxCmd returns the transaction commands for this module -func (mc ModuleClient) GetTxCmd() *cobra.Command { - txCmd := &cobra.Command{ - Use: "upgrade", - Short: "Upgrade transaction subcommands", - } - txCmd.AddCommand(client.PostCommands()...) - - return txCmd -} diff --git a/x/upgrade/client/rest/rest.go b/x/upgrade/client/rest/rest.go deleted file mode 100644 index 91910c8cbb5c..000000000000 --- a/x/upgrade/client/rest/rest.go +++ /dev/null @@ -1,41 +0,0 @@ -package rest - -import ( - "net/http" - - "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/types/rest" - "github.com/cosmos/cosmos-sdk/x/upgrade" - "github.com/gorilla/mux" -) - -// RegisterRoutes registers REST routes for the upgrade module under the path specified by routeName. -func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, storeName string) { - r.HandleFunc("/"+upgrade.ModuleName, getUpgradePlanHandler(cdc, cliCtx, storeName)).Methods("GET") -} - -func getUpgradePlanHandler(cdc *codec.Codec, cliCtx context.CLIContext, storeName string) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, request *http.Request) { - // ignore height for now - res, _, err := cliCtx.QueryStore(upgrade.PlanKey(), storeName) - if err != nil { - rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } - - if len(res) == 0 { - http.NotFound(w, request) - return - } - - var plan upgrade.Plan - err = cdc.UnmarshalBinaryBare(res, &plan) - if err != nil { - rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } - - rest.PostProcessResponse(w, cliCtx, plan) - } -} diff --git a/x/upgrade/genesis.go b/x/upgrade/genesis.go deleted file mode 100644 index 59646ceb1207..000000000000 --- a/x/upgrade/genesis.go +++ /dev/null @@ -1,33 +0,0 @@ -package upgrade - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// GenesisState - upgrade genesis state -type GenesisState struct { -} - -// NewGenesisState creates a new GenesisState object -func NewGenesisState() GenesisState { - return GenesisState{} -} - -// DefaultGenesisState creates a default GenesisState object -func DefaultGenesisState() GenesisState { - return GenesisState{} -} - -// new crisis genesis -func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { -} - -// ExportGenesis returns a GenesisState for a given context and keeper. -func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { - return NewGenesisState() -} - -// ValidateGenesis - placeholder function -func ValidateGenesis(data GenesisState) error { - return nil -} diff --git a/x/upgrade/codec.go b/x/upgrade/internal/types/codec.go similarity index 96% rename from x/upgrade/codec.go rename to x/upgrade/internal/types/codec.go index db9e6a4d2c9b..4a8b07cfbb24 100644 --- a/x/upgrade/codec.go +++ b/x/upgrade/internal/types/codec.go @@ -1,4 +1,4 @@ -package upgrade +package types import ( "github.com/cosmos/cosmos-sdk/codec" diff --git a/x/upgrade/internal/types/keys.go b/x/upgrade/internal/types/keys.go new file mode 100644 index 000000000000..a5f4860dfb83 --- /dev/null +++ b/x/upgrade/internal/types/keys.go @@ -0,0 +1,33 @@ +package types + +const ( + // ModuleName is the name of this module + ModuleName = "upgrade" + + // RouterKey is used to route governance proposals + RouterKey = ModuleName + + // StoreKey is the prefix under which we store this module's data + StoreKey = ModuleName + + // QuerierKey is used to handle abci_query requests + QuerierKey = ModuleName +) + +const ( + // PlanByte specifies the Byte under which a pending upgrade plan is stored in the store + PlanByte = 0x0 + // DoneByte is a prefix for to look up completed upgrade plan by name + DoneByte = 0x1 +) + +// PlanKey is the key under which the current plan is saved +// We store PlanByte as a const to keep it immutible (unlike a []byte) +func PlanKey() []byte { + return []byte{PlanByte} +} + +// DoneHeightKey returns a key that points to the height at which a past upgrade was applied +func DoneHeightKey(name string) []byte { + return append([]byte{DoneByte}, []byte(name)...) +} diff --git a/x/upgrade/types.go b/x/upgrade/internal/types/plan.go similarity index 98% rename from x/upgrade/types.go rename to x/upgrade/internal/types/plan.go index cbb897efa467..698d0901d05e 100644 --- a/x/upgrade/types.go +++ b/x/upgrade/internal/types/plan.go @@ -1,4 +1,4 @@ -package upgrade +package types import ( "fmt" @@ -7,6 +7,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +// TODO!! +// Handler specifies the type of function that is called when an upgrade is applied +type Handler func(ctx sdk.Context, plan Plan) + // Plan specifies information about a planned upgrade and when it should occur type Plan struct { // Sets the name for the upgrade. This name will be used by the upgraded version of the software to apply any @@ -29,9 +33,6 @@ type Plan struct { Info string `json:"info,omitempty"` } -// Handler specifies the type of function that is called when an upgrade is applied -type Handler func(ctx sdk.Context, plan Plan) - func (plan Plan) String() string { var whenStr string if !plan.Time.IsZero() { diff --git a/x/upgrade/proposal.go b/x/upgrade/internal/types/proposal.go similarity index 99% rename from x/upgrade/proposal.go rename to x/upgrade/internal/types/proposal.go index 0a95674bd2f1..11b242252aa9 100644 --- a/x/upgrade/proposal.go +++ b/x/upgrade/internal/types/proposal.go @@ -1,4 +1,4 @@ -package upgrade +package types import ( "fmt" diff --git a/x/upgrade/keeper.go b/x/upgrade/keeper.go deleted file mode 100644 index 5dea58e459cf..000000000000 --- a/x/upgrade/keeper.go +++ /dev/null @@ -1,178 +0,0 @@ -package upgrade - -import ( - "encoding/binary" - "fmt" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - abci "github.com/tendermint/tendermint/abci/types" -) - -const ( - // ModuleName is the name of this module - ModuleName = "upgrade" - // StoreKey is the prefix under which we store this module's data - StoreKey = ModuleName -) - -// Keeper of the upgrade module -type Keeper interface { - // ScheduleUpgrade schedules an upgrade based on the specified plan - ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error - - // SetUpgradeHandler sets an UpgradeHandler for the upgrade specified by name. This handler will be called when the upgrade - // with this name is applied. In order for an upgrade with the given name to proceed, a handler for this upgrade - // must be set even if it is a no-op function. - SetUpgradeHandler(name string, upgradeHandler Handler) - - // ClearUpgradePlan clears any schedule upgrade - ClearUpgradePlan(ctx sdk.Context) - - // GetUpgradePlan returns the currently scheduled Plan if any, setting havePlan to true if there is a scheduled - // upgrade or false if there is none - GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) - - // BeginBlocker should be called inside the BeginBlocker method of any app using the upgrade module. Scheduled upgrade - // plans are cached in memory so the overhead of this method is trivial. - BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) -} - -type keeper struct { - storeKey sdk.StoreKey - cdc *codec.Codec - upgradeHandlers map[string]Handler - haveCache bool -} - -const ( - // PlanByte specifies the Byte under which a pending upgrade plan is stored in the store - PlanByte = 0x0 - // DoneByte is a prefix for to look up completed upgrade plan by name - DoneByte = 0x1 -) - -// PlanKey is the key under which the current plan is saved -// We store PlanByte as a const to keep it immutible (unlike a []byte) -func PlanKey() []byte { - return []byte{PlanByte} -} - -// DoneHeightKey returns a key that points to the height at which a past upgrade was applied -func DoneHeightKey(name string) []byte { - return append([]byte{DoneByte}, []byte(name)...) -} - -// NewKeeper constructs an upgrade keeper -func NewKeeper(storeKey sdk.StoreKey, cdc *codec.Codec) Keeper { - return &keeper{ - storeKey: storeKey, - cdc: cdc, - upgradeHandlers: map[string]Handler{}, - } -} - -// SetUpgradeHandler sets an UpgradeHandler for the upgrade specified by name. This handler will be called when the upgrade -// with this name is applied. In order for an upgrade with the given name to proceed, a handler for this upgrade -// must be set even if it is a no-op function. -func (keeper *keeper) SetUpgradeHandler(name string, upgradeHandler Handler) { - keeper.upgradeHandlers[name] = upgradeHandler -} - -// ScheduleUpgrade schedules an upgrade based on the specified plan -func (keeper *keeper) ScheduleUpgrade(ctx sdk.Context, plan Plan) sdk.Error { - err := plan.ValidateBasic() - if err != nil { - return err - } - if !plan.Time.IsZero() { - if !plan.Time.After(ctx.BlockHeader().Time) { - return sdk.ErrUnknownRequest("Upgrade cannot be scheduled in the past") - } - if plan.Height != 0 { - return sdk.ErrUnknownRequest("Only one of Time or Height should be specified") - } - } else if plan.Height <= ctx.BlockHeight() { - return sdk.ErrUnknownRequest("Upgrade cannot be scheduled in the past") - } - store := ctx.KVStore(keeper.storeKey) - if store.Has(DoneHeightKey(plan.Name)) { - return sdk.ErrUnknownRequest(fmt.Sprintf("Upgrade with name %s has already been completed", plan.Name)) - } - bz := keeper.cdc.MustMarshalBinaryBare(plan) - keeper.haveCache = false - store.Set(PlanKey(), bz) - return nil -} - -// ClearUpgradePlan clears any schedule upgrade -func (keeper *keeper) ClearUpgradePlan(ctx sdk.Context) { - store := ctx.KVStore(keeper.storeKey) - keeper.haveCache = false - store.Delete(PlanKey()) -} - -// ValidateBasic does basic validation of a Plan -func (plan Plan) ValidateBasic() sdk.Error { - if len(plan.Name) == 0 { - return sdk.ErrUnknownRequest("Name cannot be empty") - } - return nil -} - -// GetUpgradePlan returns the currently scheduled Plan if any, setting havePlan to true if there is a scheduled -// upgrade or false if there is none -func (keeper *keeper) GetUpgradePlan(ctx sdk.Context) (plan Plan, havePlan bool) { - store := ctx.KVStore(keeper.storeKey) - bz := store.Get(PlanKey()) - if bz == nil { - return plan, false - } - keeper.cdc.MustUnmarshalBinaryBare(bz, &plan) - return plan, true -} - -// setDone marks this upgrade name as being done so the name can't be reused accidentally -func (keeper *keeper) setDone(ctx sdk.Context, name string) { - store := ctx.KVStore(keeper.storeKey) - bz := make([]byte, 8) - binary.BigEndian.PutUint64(bz, uint64(ctx.BlockHeight())) - store.Set(DoneHeightKey(name), bz) -} - -// BeginBlocker should be called inside the BeginBlocker method of any app using the upgrade module. Scheduled upgrade -// plans are cached in memory so the overhead of this method is trivial. -func (keeper *keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) { - blockTime := ctx.BlockHeader().Time - blockHeight := ctx.BlockHeight() - - plan, found := keeper.GetUpgradePlan(ctx) - if !found { - return - } - - upgradeTime := plan.Time - upgradeHeight := plan.Height - if (!upgradeTime.IsZero() && !blockTime.Before(upgradeTime)) || (upgradeHeight > 0 && upgradeHeight <= blockHeight) { - handler, ok := keeper.upgradeHandlers[plan.Name] - if ok { - // We have an upgrade handler for this upgrade name, so apply the upgrade - ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at height %d", plan.Name, blockHeight)) - handler(ctx, plan) - keeper.ClearUpgradePlan(ctx) - keeper.setDone(ctx, plan.Name) - } else { - // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown - ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", plan.Name, blockHeight, plan.Info)) - panic("UPGRADE REQUIRED!") - } - } else { - // if we have a pending upgrade, but it is not yet time, make sure we did not - // set the handler already - _, ok := keeper.upgradeHandlers[plan.Name] - if ok { - ctx.Logger().Error(fmt.Sprintf("UNKNOWN UPGRADE \"%s\" - in binary but not executed on chain", plan.Name)) - panic("BINARY UPDATED BEFORE TRIGGER!") - } - } -} diff --git a/x/upgrade/keeper_test.go b/x/upgrade/keeper_test.go deleted file mode 100644 index e2ba4b093529..000000000000 --- a/x/upgrade/keeper_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package upgrade - -import ( - "testing" - "time" - - "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/store" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/suite" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/log" - dbm "github.com/tendermint/tm-db" -) - -type TestSuite struct { - suite.Suite - keeper Keeper - ctx sdk.Context - cms store.CommitMultiStore -} - -func (s *TestSuite) SetupTest() { - db := dbm.NewMemDB() - s.cms = store.NewCommitMultiStore(db) - key := sdk.NewKVStoreKey("upgrade") - cdc := codec.New() - RegisterCodec(cdc) - s.keeper = NewKeeper(key, cdc) - s.cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db) - _ = s.cms.LoadLatestVersion() - s.ctx = sdk.NewContext(s.cms, abci.Header{Height: 10, Time: time.Now()}, false, log.NewNopLogger()) -} - -func (s *TestSuite) TestRequireName() { - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{}) - s.Require().NotNil(err) - s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) -} - -func (s *TestSuite) TestRequireFutureTime() { - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: s.ctx.BlockHeader().Time}) - s.Require().NotNil(err) - s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) -} - -func (s *TestSuite) TestRequireFutureBlock() { - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight()}) - s.Require().NotNil(err) - s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) -} - -func (s *TestSuite) TestCantSetBothTimeAndHeight() { - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now(), Height: s.ctx.BlockHeight() + 1}) - s.Require().NotNil(err) - s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) -} - -func (s *TestSuite) TestDoTimeUpgrade() { - s.T().Log("Verify can schedule an upgrade") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) - s.Require().Nil(err) - - s.VerifyDoUpgrade() -} - -func (s *TestSuite) TestDoHeightUpgrade() { - s.T().Log("Verify can schedule an upgrade") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}) - s.Require().Nil(err) - - s.VerifyDoUpgrade() -} - -func (s *TestSuite) VerifyDoUpgrade() { - s.T().Log("Verify that a panic happens at the upgrade time/height") - newCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 1, Time: time.Now()}, false, log.NewNopLogger()) - req := abci.RequestBeginBlock{Header: newCtx.BlockHeader()} - s.Require().Panics(func() { - s.keeper.BeginBlocker(newCtx, req) - }) - - s.T().Log("Verify that the upgrade can be successfully applied with a handler") - s.keeper.SetUpgradeHandler("test", func(ctx sdk.Context, plan Plan) {}) - s.Require().NotPanics(func() { - s.keeper.BeginBlocker(newCtx, req) - }) - - s.VerifyCleared(newCtx) -} - -func (s *TestSuite) TestHaltIfTooNew() { - s.T().Log("Verify that we don't panic with registered plan not in database at all") - var called int - s.keeper.SetUpgradeHandler("future", func(ctx sdk.Context, plan Plan) { called++ }) - - newCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 1, Time: time.Now()}, false, log.NewNopLogger()) - req := abci.RequestBeginBlock{Header: newCtx.BlockHeader()} - s.Require().NotPanics(func() { - s.keeper.BeginBlocker(newCtx, req) - }) - s.Require().Equal(0, called) - - s.T().Log("Verify we panic if we have a registered handler ahead of time") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "future", Height: s.ctx.BlockHeight() + 3}) - s.Require().NoError(err) - s.Require().Panics(func() { - s.keeper.BeginBlocker(newCtx, req) - }) - s.Require().Equal(0, called) - - s.T().Log("Verify we no longer panic if the plan is on time") - - futCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 3, Time: time.Now()}, false, log.NewNopLogger()) - req = abci.RequestBeginBlock{Header: futCtx.BlockHeader()} - s.Require().NotPanics(func() { - s.keeper.BeginBlocker(futCtx, req) - }) - s.Require().Equal(1, called) - - s.VerifyCleared(futCtx) -} - -func (s *TestSuite) VerifyCleared(newCtx sdk.Context) { - s.T().Log("Verify that the upgrade plan has been cleared") - _, havePlan := s.keeper.GetUpgradePlan(newCtx) - s.Require().False(havePlan) -} - -func (s *TestSuite) TestCanClear() { - s.T().Log("Verify upgrade is scheduled") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) - s.Require().Nil(err) - - s.keeper.ClearUpgradePlan(s.ctx) - - s.VerifyCleared(s.ctx) -} - -func (s *TestSuite) TestCantApplySameUpgradeTwice() { - s.TestDoTimeUpgrade() - s.T().Log("Verify an upgrade named \"test\" can't be scheduled twice") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) - s.Require().NotNil(err) - s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) -} - -func (s *TestSuite) TestNoSpuriousUpgrades() { - s.T().Log("Verify that no upgrade panic is triggered in the BeginBlocker when we haven't scheduled an upgrade") - req := abci.RequestBeginBlock{Header: s.ctx.BlockHeader()} - s.Require().NotPanics(func() { - s.keeper.BeginBlocker(s.ctx, req) - }) -} - -func (s *TestSuite) TestPlanStringer() { - t, err := time.Parse(time.RFC3339, "2020-01-01T00:00:00Z") - s.Require().Nil(err) - s.Require().Equal(`Upgrade Plan - Name: test - Time: 2020-01-01T00:00:00Z - Info: `, Plan{Name: "test", Time: t}.String()) - s.Require().Equal(`Upgrade Plan - Name: test - Height: 100 - Info: `, Plan{Name: "test", Height: 100}.String()) -} - -func TestTestSuite(t *testing.T) { - suite.Run(t, new(TestSuite)) -} diff --git a/x/upgrade/module/module.go b/x/upgrade/module/module.go deleted file mode 100644 index 236a3a54f30d..000000000000 --- a/x/upgrade/module/module.go +++ /dev/null @@ -1,144 +0,0 @@ -package upgrade - -import ( - "encoding/json" - - "github.com/gorilla/mux" - "github.com/spf13/cobra" - - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/module" - "github.com/cosmos/cosmos-sdk/x/upgrade" - "github.com/cosmos/cosmos-sdk/x/upgrade/client/cli" - "github.com/cosmos/cosmos-sdk/x/upgrade/client/rest" - abci "github.com/tendermint/tendermint/abci/types" -) - -// module codec -var moduleCdc = codec.New() - -func init() { - upgrade.RegisterCodec(moduleCdc) -} - -var ( - _ module.AppModule = AppModule{} - _ module.AppModuleBasic = AppModuleBasic{} -) - -// app module basics object -type AppModuleBasic struct{} - -// module name -func (AppModuleBasic) Name() string { - return upgrade.ModuleName -} - -// register module codec -func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { - upgrade.RegisterCodec(cdc) -} - -// default genesis state -func (AppModuleBasic) DefaultGenesis() json.RawMessage { - return moduleCdc.MustMarshalJSON(upgrade.DefaultGenesisState()) -} - -// module validate genesis -func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { - var data upgrade.GenesisState - err := moduleCdc.UnmarshalJSON(bz, &data) - if err != nil { - return err - } - return upgrade.ValidateGenesis(data) -} - -func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, r *mux.Router) { - rest.RegisterRoutes(ctx, r, moduleCdc, upgrade.StoreKey) -} - -// GetQueryCmd returns the cli query commands for this module -func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { - queryCmd := &cobra.Command{ - Use: "upgrade", - Short: "Querying commands for the upgrade module", - } - queryCmd.AddCommand(client.GetCommands( - cli.GetPlanCmd(upgrade.StoreKey, cdc), - cli.GetAppliedHeightCmd(upgrade.StoreKey, cdc), - )...) - - return queryCmd -} - -// GetTxCmd returns the transaction commands for this module -func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { - txCmd := &cobra.Command{ - Use: "upgrade", - Short: "Upgrade transaction subcommands", - } - txCmd.AddCommand(client.PostCommands()...) - return txCmd -} - -// app module -type AppModule struct { - AppModuleBasic - keeper upgrade.Keeper -} - -// NewAppModule creates a new AppModule object -func NewAppModule(keeper upgrade.Keeper) AppModule { - return AppModule{ - AppModuleBasic: AppModuleBasic{}, - keeper: keeper, - } -} - -// module name -func (AppModule) Name() string { - return upgrade.ModuleName -} - -// register invariants -func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} - -// module message route name -func (AppModule) Route() string { return "" } - -// module handler -func (am AppModule) NewHandler() sdk.Handler { return nil } - -// module querier route name -func (AppModule) QuerierRoute() string { return "" } - -// module querier -func (am AppModule) NewQuerierHandler() sdk.Querier { return nil } - -// module init-genesis -func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { - var genesisState upgrade.GenesisState - moduleCdc.MustUnmarshalJSON(data, &genesisState) - upgrade.InitGenesis(ctx, am.keeper, genesisState) - return []abci.ValidatorUpdate{} -} - -// module export genesis -func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { - gs := upgrade.ExportGenesis(ctx, am.keeper) - return moduleCdc.MustMarshalJSON(gs) -} - -// module begin-block -func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { - am.keeper.BeginBlocker(ctx, req) -} - -// module end-block -func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { - return []abci.ValidatorUpdate{} -} diff --git a/x/upgrade/proposal_handler.go b/x/upgrade/proposal_handler.go deleted file mode 100644 index a245993100e2..000000000000 --- a/x/upgrade/proposal_handler.go +++ /dev/null @@ -1,36 +0,0 @@ -package upgrade - -import ( - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" -) - -const ( - RouterKey = ModuleName -) - -func NewSoftwareUpgradeProposalHandler(k Keeper) govtypes.Handler { - return func(ctx sdk.Context, content govtypes.Content) sdk.Error { - switch c := content.(type) { - case SoftwareUpgradeProposal: - return handleSoftwareUpgradeProposal(ctx, k, c) - case CancelSoftwareUpgradeProposal: - return handleCancelSoftwareUpgradeProposal(ctx, k, c) - - default: - errMsg := fmt.Sprintf("unrecognized software upgrade proposal content type: %T", c) - return sdk.ErrUnknownRequest(errMsg) - } - } -} - -func handleSoftwareUpgradeProposal(ctx sdk.Context, k Keeper, p SoftwareUpgradeProposal) sdk.Error { - return k.ScheduleUpgrade(ctx, p.Plan) -} - -func handleCancelSoftwareUpgradeProposal(ctx sdk.Context, k Keeper, p CancelSoftwareUpgradeProposal) sdk.Error { - k.ClearUpgradePlan(ctx) - return nil -} From 2133c2448279c9aaf74ec67f73910add9f9b8e97 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 1 Oct 2019 11:46:49 +0200 Subject: [PATCH 28/62] Refactor 2: Add Keeper, clean up internal/types --- x/upgrade/exported/keeper.go | 29 ++++ x/upgrade/internal/keeper/keeper.go | 127 +++++++++++++++++ x/upgrade/internal/keeper/keeper_test.go | 172 +++++++++++++++++++++++ x/upgrade/internal/types/handler.go | 8 ++ x/upgrade/internal/types/plan.go | 12 +- 5 files changed, 344 insertions(+), 4 deletions(-) create mode 100644 x/upgrade/exported/keeper.go create mode 100644 x/upgrade/internal/keeper/keeper.go create mode 100644 x/upgrade/internal/keeper/keeper_test.go create mode 100644 x/upgrade/internal/types/handler.go diff --git a/x/upgrade/exported/keeper.go b/x/upgrade/exported/keeper.go new file mode 100644 index 000000000000..3c752a9f8df8 --- /dev/null +++ b/x/upgrade/exported/keeper.go @@ -0,0 +1,29 @@ +package exported + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +// Keeper of the upgrade module +type Keeper interface { + // ScheduleUpgrade schedules an upgrade based on the specified plan + ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error + + // SetUpgradeHandler sets an UpgradeHandler for the upgrade specified by name. This handler will be called when the upgrade + // with this name is applied. In order for an upgrade with the given name to proceed, a handler for this upgrade + // must be set even if it is a no-op function. + SetUpgradeHandler(name string, upgradeHandler types.UpgradeHandler) + + // ClearUpgradePlan clears any schedule upgrade + ClearUpgradePlan(ctx sdk.Context) + + // GetUpgradePlan returns the currently scheduled Plan if any, setting havePlan to true if there is a scheduled + // upgrade or false if there is none + GetUpgradePlan(ctx sdk.Context) (plan types.Plan, havePlan bool) + + // BeginBlocker should be called inside the BeginBlocker method of any app using the upgrade module. Scheduled upgrade + // plans are cached in memory so the overhead of this method is trivial. + BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) +} diff --git a/x/upgrade/internal/keeper/keeper.go b/x/upgrade/internal/keeper/keeper.go new file mode 100644 index 000000000000..2215fcf59d40 --- /dev/null +++ b/x/upgrade/internal/keeper/keeper.go @@ -0,0 +1,127 @@ +package keeper + +import ( + "encoding/binary" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/upgrade/exported" + "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +type Keeper struct { + storeKey sdk.StoreKey + cdc *codec.Codec + upgradeHandlers map[string]types.UpgradeHandler + haveCache bool +} + +var _ exported.Keeper = (*Keeper)(nil) + +// NewKeeper constructs an upgrade Keeper +func NewKeeper(storeKey sdk.StoreKey, cdc *codec.Codec) *Keeper { + return &Keeper{ + storeKey: storeKey, + cdc: cdc, + upgradeHandlers: map[string]types.UpgradeHandler{}, + } +} + +// SetUpgradeHandler sets an UpgradeHandler for the upgrade specified by name. This handler will be called when the upgrade +// with this name is applied. In order for an upgrade with the given name to proceed, a handler for this upgrade +// must be set even if it is a no-op function. +func (k *Keeper) SetUpgradeHandler(name string, upgradeHandler types.UpgradeHandler) { + k.upgradeHandlers[name] = upgradeHandler +} + +// ScheduleUpgrade schedules an upgrade based on the specified plan +func (k *Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error { + err := plan.ValidateBasic() + if err != nil { + return err + } + if !plan.Time.IsZero() { + if !plan.Time.After(ctx.BlockHeader().Time) { + return sdk.ErrUnknownRequest("Upgrade cannot be scheduled in the past") + } + if plan.Height != 0 { + return sdk.ErrUnknownRequest("Only one of Time or Height should be specified") + } + } else if plan.Height <= ctx.BlockHeight() { + return sdk.ErrUnknownRequest("Upgrade cannot be scheduled in the past") + } + store := ctx.KVStore(k.storeKey) + if store.Has(types.DoneHeightKey(plan.Name)) { + return sdk.ErrUnknownRequest(fmt.Sprintf("Upgrade with name %s has already been completed", plan.Name)) + } + bz := k.cdc.MustMarshalBinaryBare(plan) + k.haveCache = false + store.Set(types.PlanKey(), bz) + return nil +} + +// ClearUpgradePlan clears any schedule upgrade +func (k *Keeper) ClearUpgradePlan(ctx sdk.Context) { + store := ctx.KVStore(k.storeKey) + k.haveCache = false + store.Delete(types.PlanKey()) +} + +// GetUpgradePlan returns the currently scheduled Plan if any, setting havePlan to true if there is a scheduled +// upgrade or false if there is none +func (k *Keeper) GetUpgradePlan(ctx sdk.Context) (plan types.Plan, havePlan bool) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.PlanKey()) + if bz == nil { + return plan, false + } + k.cdc.MustUnmarshalBinaryBare(bz, &plan) + return plan, true +} + +// setDone marks this upgrade name as being done so the name can't be reused accidentally +func (k *Keeper) setDone(ctx sdk.Context, name string) { + store := ctx.KVStore(k.storeKey) + bz := make([]byte, 8) + binary.BigEndian.PutUint64(bz, uint64(ctx.BlockHeight())) + store.Set(types.DoneHeightKey(name), bz) +} + +// BeginBlocker should be called inside the BeginBlocker method of any app using the upgrade module. Scheduled upgrade +// plans are cached in memory so the overhead of this method is trivial. +func (k *Keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) { + blockTime := ctx.BlockHeader().Time + blockHeight := ctx.BlockHeight() + + plan, found := k.GetUpgradePlan(ctx) + if !found { + return + } + + upgradeTime := plan.Time + upgradeHeight := plan.Height + if (!upgradeTime.IsZero() && !blockTime.Before(upgradeTime)) || (upgradeHeight > 0 && upgradeHeight <= blockHeight) { + handler, ok := k.upgradeHandlers[plan.Name] + if ok { + // We have an upgrade handler for this upgrade name, so apply the upgrade + ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at height %d", plan.Name, blockHeight)) + handler(ctx, plan) + k.ClearUpgradePlan(ctx) + k.setDone(ctx, plan.Name) + } else { + // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown + ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", plan.Name, blockHeight, plan.Info)) + panic("UPGRADE REQUIRED!") + } + } else { + // if we have a pending upgrade, but it is not yet time, make sure we did not + // set the handler already + _, ok := k.upgradeHandlers[plan.Name] + if ok { + ctx.Logger().Error(fmt.Sprintf("UNKNOWN UPGRADE \"%s\" - in binary but not executed on chain", plan.Name)) + panic("BINARY UPDATED BEFORE TRIGGER!") + } + } +} diff --git a/x/upgrade/internal/keeper/keeper_test.go b/x/upgrade/internal/keeper/keeper_test.go new file mode 100644 index 000000000000..4ee2b86a626d --- /dev/null +++ b/x/upgrade/internal/keeper/keeper_test.go @@ -0,0 +1,172 @@ +package keeper + +import ( + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" +) + +type TestSuite struct { + suite.Suite + keeper *Keeper + ctx sdk.Context + cms store.CommitMultiStore +} + +func (s *TestSuite) SetupTest() { + db := dbm.NewMemDB() + s.cms = store.NewCommitMultiStore(db) + key := sdk.NewKVStoreKey("upgrade") + cdc := codec.New() + types.RegisterCodec(cdc) + s.keeper = NewKeeper(key, cdc) + s.cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db) + _ = s.cms.LoadLatestVersion() + s.ctx = sdk.NewContext(s.cms, abci.Header{Height: 10, Time: time.Now()}, false, log.NewNopLogger()) +} + +func (s *TestSuite) TestRequireName() { + err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{}) + s.Require().NotNil(err) + s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) +} + +func (s *TestSuite) TestRequireFutureTime() { + err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Time: s.ctx.BlockHeader().Time}) + s.Require().NotNil(err) + s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) +} + +func (s *TestSuite) TestRequireFutureBlock() { + err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Height: s.ctx.BlockHeight()}) + s.Require().NotNil(err) + s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) +} + +func (s *TestSuite) TestCantSetBothTimeAndHeight() { + err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Time: time.Now(), Height: s.ctx.BlockHeight() + 1}) + s.Require().NotNil(err) + s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) +} + +func (s *TestSuite) TestDoTimeUpgrade() { + s.T().Log("Verify can schedule an upgrade") + err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Time: time.Now()}) + s.Require().Nil(err) + + s.VerifyDoUpgrade() +} + +func (s *TestSuite) TestDoHeightUpgrade() { + s.T().Log("Verify can schedule an upgrade") + err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}) + s.Require().Nil(err) + + s.VerifyDoUpgrade() +} + +func (s *TestSuite) VerifyDoUpgrade() { + s.T().Log("Verify that a panic happens at the upgrade time/height") + newCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 1, Time: time.Now()}, false, log.NewNopLogger()) + req := abci.RequestBeginBlock{Header: newCtx.BlockHeader()} + s.Require().Panics(func() { + s.keeper.BeginBlocker(newCtx, req) + }) + + s.T().Log("Verify that the upgrade can be successfully applied with a handler") + s.keeper.SetUpgradeHandler("test", func(ctx sdk.Context, plan types.Plan) {}) + s.Require().NotPanics(func() { + s.keeper.BeginBlocker(newCtx, req) + }) + + s.VerifyCleared(newCtx) +} + +func (s *TestSuite) TestHaltIfTooNew() { + s.T().Log("Verify that we don't panic with registered plan not in database at all") + var called int + s.keeper.SetUpgradeHandler("future", func(ctx sdk.Context, plan types.Plan) { called++ }) + + newCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 1, Time: time.Now()}, false, log.NewNopLogger()) + req := abci.RequestBeginBlock{Header: newCtx.BlockHeader()} + s.Require().NotPanics(func() { + s.keeper.BeginBlocker(newCtx, req) + }) + s.Require().Equal(0, called) + + s.T().Log("Verify we panic if we have a registered handler ahead of time") + err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "future", Height: s.ctx.BlockHeight() + 3}) + s.Require().NoError(err) + s.Require().Panics(func() { + s.keeper.BeginBlocker(newCtx, req) + }) + s.Require().Equal(0, called) + + s.T().Log("Verify we no longer panic if the plan is on time") + + futCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 3, Time: time.Now()}, false, log.NewNopLogger()) + req = abci.RequestBeginBlock{Header: futCtx.BlockHeader()} + s.Require().NotPanics(func() { + s.keeper.BeginBlocker(futCtx, req) + }) + s.Require().Equal(1, called) + + s.VerifyCleared(futCtx) +} + +func (s *TestSuite) VerifyCleared(newCtx sdk.Context) { + s.T().Log("Verify that the upgrade plan has been cleared") + _, havePlan := s.keeper.GetUpgradePlan(newCtx) + s.Require().False(havePlan) +} + +func (s *TestSuite) TestCanClear() { + s.T().Log("Verify upgrade is scheduled") + err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Time: time.Now()}) + s.Require().Nil(err) + + s.keeper.ClearUpgradePlan(s.ctx) + + s.VerifyCleared(s.ctx) +} + +func (s *TestSuite) TestCantApplySameUpgradeTwice() { + s.TestDoTimeUpgrade() + s.T().Log("Verify an upgrade named \"test\" can't be scheduled twice") + err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Time: time.Now()}) + s.Require().NotNil(err) + s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) +} + +func (s *TestSuite) TestNoSpuriousUpgrades() { + s.T().Log("Verify that no upgrade panic is triggered in the BeginBlocker when we haven't scheduled an upgrade") + req := abci.RequestBeginBlock{Header: s.ctx.BlockHeader()} + s.Require().NotPanics(func() { + s.keeper.BeginBlocker(s.ctx, req) + }) +} + +func (s *TestSuite) TestPlanStringer() { + t, err := time.Parse(time.RFC3339, "2020-01-01T00:00:00Z") + s.Require().Nil(err) + s.Require().Equal(`Upgrade Plan + Name: test + Time: 2020-01-01T00:00:00Z + Info: `, types.Plan{Name: "test", Time: t}.String()) + s.Require().Equal(`Upgrade Plan + Name: test + Height: 100 + Info: `, types.Plan{Name: "test", Height: 100}.String()) +} + +func TestTestSuite(t *testing.T) { + suite.Run(t, new(TestSuite)) +} diff --git a/x/upgrade/internal/types/handler.go b/x/upgrade/internal/types/handler.go new file mode 100644 index 000000000000..44e50cff112f --- /dev/null +++ b/x/upgrade/internal/types/handler.go @@ -0,0 +1,8 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// UpgradeHandler specifies the type of function that is called when an upgrade is applied +type UpgradeHandler func(ctx sdk.Context, plan Plan) diff --git a/x/upgrade/internal/types/plan.go b/x/upgrade/internal/types/plan.go index 698d0901d05e..4fd6ff270999 100644 --- a/x/upgrade/internal/types/plan.go +++ b/x/upgrade/internal/types/plan.go @@ -7,10 +7,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// TODO!! -// Handler specifies the type of function that is called when an upgrade is applied -type Handler func(ctx sdk.Context, plan Plan) - // Plan specifies information about a planned upgrade and when it should occur type Plan struct { // Sets the name for the upgrade. This name will be used by the upgraded version of the software to apply any @@ -45,3 +41,11 @@ func (plan Plan) String() string { %s Info: %s`, plan.Name, whenStr, plan.Info) } + +// ValidateBasic does basic validation of a Plan +func (plan Plan) ValidateBasic() sdk.Error { + if len(plan.Name) == 0 { + return sdk.ErrUnknownRequest("Name cannot be empty") + } + return nil +} From 1553d75cd08881dc3f25410238d83d199173caf3 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 1 Oct 2019 13:27:01 +0200 Subject: [PATCH 29/62] Refactor 3: Add cli and rest client --- x/upgrade/client/cli/query.go | 74 ++++++++++++ x/upgrade/client/cli/tx.go | 184 ++++++++++++++++++++++++++++++ x/upgrade/client/module_client.go | 45 ++++++++ x/upgrade/client/rest/query.go | 41 +++++++ 4 files changed, 344 insertions(+) create mode 100644 x/upgrade/client/cli/query.go create mode 100644 x/upgrade/client/cli/tx.go create mode 100644 x/upgrade/client/module_client.go create mode 100644 x/upgrade/client/rest/query.go diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go new file mode 100644 index 000000000000..d3ebbc3f77b5 --- /dev/null +++ b/x/upgrade/client/cli/query.go @@ -0,0 +1,74 @@ +package cli + +import ( + "encoding/binary" + "fmt" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + upgrade "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" + "github.com/spf13/cobra" +) + +// GetPlanCmd returns the query upgrade plan command +func GetPlanCmd(storeName string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "plan", + Short: "get upgrade plan (if one exists)", + Long: "This gets the currently scheduled upgrade plan", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // ignore height for now + res, _, err := cliCtx.QueryStore(upgrade.PlanKey(), storeName) + if err != nil { + return err + } + + if len(res) == 0 { + return fmt.Errorf("no upgrade scheduled") + } + + var plan upgrade.Plan + err = cdc.UnmarshalBinaryBare(res, &plan) + if err != nil { + return err + } + return cliCtx.PrintOutput(plan) + }, + } +} + +// GetAppliedHeightCmd returns the height at which a completed upgrade was applied +func GetAppliedHeightCmd(storeName string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "applied-height [upgrade-name]", + Short: "height at which a completed upgrade was applied", + Long: "If upgrade-name was previously executed on the chain, this returns the height at which it was applied.\n" + + "This helps a client determine which binary was valid over a given range of blocks.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + name := args[0] + + // ignore height for now + res, _, err := cliCtx.QueryStore(upgrade.DoneHeightKey(name), storeName) + if err != nil { + return err + } + + if len(res) == 0 { + return fmt.Errorf("no upgrade found") + } + if len(res) != 8 { + return fmt.Errorf("unknown format for applied-upgrade") + } + + height := int64(binary.BigEndian.Uint64(res)) + fmt.Println(height) + return nil + }, + } +} diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go new file mode 100644 index 000000000000..d6f8d9c3fe14 --- /dev/null +++ b/x/upgrade/client/cli/tx.go @@ -0,0 +1,184 @@ +package cli + +import ( + "fmt" + "strings" + "time" + + "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/cosmos/cosmos-sdk/x/gov" + upgrade "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" +) + +const ( + // TimeFormat specifies ISO UTC format for submitting the upgrade-time for a new upgrade proposal + TimeFormat = "2006-01-02T15:04:05Z" + + FlagUpgradeName = "upgrade-name" + FlagUpgradeHeight = "upgrade-height" + FlagUpgradeTime = "upgrade-time" + FlagUpgradeInfo = "upgrade-info" +) + +func parseSubmitArgs(cmd *cobra.Command, cdc *codec.Codec) (gov.Content, error) { + title, err := cmd.Flags().GetString(cli.FlagTitle) + if err != nil { + return nil, err + } + + description, err := cmd.Flags().GetString(cli.FlagDescription) + if err != nil { + return nil, err + } + + name, err := cmd.Flags().GetString(FlagUpgradeName) + if err != nil { + return nil, err + } + if len(name) == 0 { + name = title + } + + height, err := cmd.Flags().GetInt64(FlagUpgradeHeight) + if err != nil { + return nil, err + } + + timeStr, err := cmd.Flags().GetString(FlagUpgradeTime) + if err != nil { + return nil, err + } + + if height != 0 { + if len(timeStr) != 0 { + return nil, fmt.Errorf("only one of --upgrade-time or --upgrade-height should be specified") + } + } + + var upgradeTime time.Time + if len(timeStr) != 0 { + upgradeTime, err = time.Parse(TimeFormat, timeStr) + if err != nil { + return nil, err + } + } + + info, err := cmd.Flags().GetString(FlagUpgradeInfo) + if err != nil { + return nil, err + } + + content := upgrade.NewSoftwareUpgradeProposal(title, description, + upgrade.Plan{Name: name, Time: upgradeTime, Height: height, Info: info}) + return content, nil +} + +// GetCmdSubmitUpgradeProposal implements a command handler for submitting a software upgrade proposal transaction. +func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "software-upgrade --upgrade-name [name] (--upgrade-height [height] | --upgrade-time [time]) (--upgrade-info [info]) [flags]", + Args: cobra.ExactArgs(0), + Short: "Submit a software upgrade proposal", + Long: "Submit a software upgrade along with an initial deposit.\n" + + "Please specify a unique name and height OR time for the upgrade to take effect.\n" + + "You may include info to reference a binary download link, in a format compatible with: https://github.com/regen-network/cosmosd", + RunE: func(cmd *cobra.Command, args []string) error { + content, err := parseSubmitArgs(cmd, cdc) + if err != nil { + return err + } + + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext().WithCodec(cdc) + from := cliCtx.GetFromAddress() + + depositStr, err := cmd.Flags().GetString(cli.FlagDeposit) + if err != nil { + return err + } + deposit, err := sdk.ParseCoins(depositStr) + if err != nil { + return err + } + + msg := gov.NewMsgSubmitProposal(content, deposit, from) + if err := msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + cmd.Flags().String(cli.FlagTitle, "", "title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "deposit of proposal") + cmd.Flags().String(FlagUpgradeName, "", "The name of the upgrade (if not specified title will be used)") + cmd.Flags().Int64(FlagUpgradeHeight, 0, "The height at which the upgrade must happen (not to be used together with --upgrade-time)") + cmd.Flags().String(FlagUpgradeTime, "", fmt.Sprintf("The time at which the upgrade must happen (ex. %s) (not to be used together with --upgrade-height)", TimeFormat)) + cmd.Flags().String(FlagUpgradeInfo, "", "Optional info for the planned upgrade such as commit hash, etc.") + + return cmd +} + +// GetCmdSubmitCancelUpgradeProposal implements a command handler for submitting a software upgrade cancel proposal transaction. +func GetCmdSubmitCancelUpgradeProposal(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "cancel-software-upgrade [flags]", + Args: cobra.ExactArgs(0), + Short: "Submit a software upgrade proposal", + Long: strings.TrimSpace( + fmt.Sprintf(`Submit a software upgrade along with an initial deposit. +`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext().WithCodec(cdc) + from := cliCtx.GetFromAddress() + + depositStr, err := cmd.Flags().GetString(cli.FlagDeposit) + if err != nil { + return err + } + + deposit, err := sdk.ParseCoins(depositStr) + if err != nil { + return err + } + + title, err := cmd.Flags().GetString(cli.FlagTitle) + if err != nil { + return err + } + + description, err := cmd.Flags().GetString(cli.FlagDescription) + if err != nil { + return err + } + + content := upgrade.NewCancelSoftwareUpgradeProposal(title, description) + + msg := gov.NewMsgSubmitProposal(content, deposit, from) + if err := msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + cmd.Flags().String(cli.FlagTitle, "", "title of proposal") + cmd.Flags().String(cli.FlagDescription, "", "description of proposal") + cmd.Flags().String(cli.FlagDeposit, "", "deposit of proposal") + + return cmd +} diff --git a/x/upgrade/client/module_client.go b/x/upgrade/client/module_client.go new file mode 100644 index 000000000000..1b96d3d0185a --- /dev/null +++ b/x/upgrade/client/module_client.go @@ -0,0 +1,45 @@ +package client + +import ( + "github.com/spf13/cobra" + amino "github.com/tendermint/go-amino" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/x/upgrade/client/cli" +) + +// ModuleClient exports all client functionality from this module +type ModuleClient struct { + storeKey string + cdc *amino.Codec +} + +// NewModuleClient returns an upgrade ModuleClient +func NewModuleClient(storeKey string, cdc *amino.Codec) ModuleClient { + return ModuleClient{storeKey, cdc} +} + +// GetQueryCmd returns the cli query commands for this module +func (mc ModuleClient) GetQueryCmd() *cobra.Command { + queryCmd := &cobra.Command{ + Use: "upgrade", + Short: "Querying commands for the upgrade module", + } + queryCmd.AddCommand(client.GetCommands( + cli.GetPlanCmd(mc.storeKey, mc.cdc), + cli.GetAppliedHeightCmd(mc.storeKey, mc.cdc), + )...) + + return queryCmd +} + +// GetTxCmd returns the transaction commands for this module +func (mc ModuleClient) GetTxCmd() *cobra.Command { + txCmd := &cobra.Command{ + Use: "upgrade", + Short: "Upgrade transaction subcommands", + } + txCmd.AddCommand(client.PostCommands()...) + + return txCmd +} diff --git a/x/upgrade/client/rest/query.go b/x/upgrade/client/rest/query.go new file mode 100644 index 000000000000..f70f5ec041c9 --- /dev/null +++ b/x/upgrade/client/rest/query.go @@ -0,0 +1,41 @@ +package rest + +import ( + "net/http" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/rest" + upgrade "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" + "github.com/gorilla/mux" +) + +// RegisterRoutes registers REST routes for the upgrade module under the path specified by routeName. +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, storeName string) { + r.HandleFunc("/"+upgrade.ModuleName, getUpgradePlanHandler(cdc, cliCtx, storeName)).Methods("GET") +} + +func getUpgradePlanHandler(cdc *codec.Codec, cliCtx context.CLIContext, storeName string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, request *http.Request) { + // ignore height for now + res, _, err := cliCtx.QueryStore(upgrade.PlanKey(), storeName) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + if len(res) == 0 { + http.NotFound(w, request) + return + } + + var plan upgrade.Plan + err = cdc.UnmarshalBinaryBare(res, &plan) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + rest.PostProcessResponse(w, cliCtx, plan) + } +} From fcbaa4eb7bc42343e52d0667eee90ecabfefb2b8 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 1 Oct 2019 13:27:38 +0200 Subject: [PATCH 30/62] Refactor 4: Top level module and helpers --- x/upgrade/alias.go | 41 +++++++++++++ x/upgrade/handler.go | 32 +++++++++++ x/upgrade/module.go | 134 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 x/upgrade/alias.go create mode 100644 x/upgrade/handler.go create mode 100644 x/upgrade/module.go diff --git a/x/upgrade/alias.go b/x/upgrade/alias.go new file mode 100644 index 000000000000..4018b7087bbd --- /dev/null +++ b/x/upgrade/alias.go @@ -0,0 +1,41 @@ +// nolint +// autogenerated code using github.com/rigelrozanski/multitool +// aliases generated for the following subdirectories: +// ALIASGEN: github.com/cosmos/cosmos-sdk/x/upgrade/internal/types/ +// ALIASGEN: github.com/cosmos/cosmos-sdk/x/upgrade/internal/keeper/ +package upgrade + +import ( + "github.com/cosmos/cosmos-sdk/x/upgrade/internal/keeper" + "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" +) + +const ( + ModuleName = types.ModuleName + RouterKey = types.RouterKey + StoreKey = types.StoreKey + QuerierKey = types.QuerierKey + PlanByte = types.PlanByte + DoneByte = types.DoneByte + ProposalTypeSoftwareUpgrade = types.ProposalTypeSoftwareUpgrade + ProposalTypeCancelSoftwareUpgrade = types.ProposalTypeCancelSoftwareUpgrade + DefaultCodespace = types.DefaultCodespace +) + +var ( + // functions aliases + RegisterCodec = types.RegisterCodec + PlanKey = types.PlanKey + DoneHeightKey = types.DoneHeightKey + NewSoftwareUpgradeProposal = types.NewSoftwareUpgradeProposal + NewCancelSoftwareUpgradeProposal = types.NewCancelSoftwareUpgradeProposal + NewKeeper = keeper.NewKeeper +) + +type ( + UpgradeHandler = types.UpgradeHandler + Plan = types.Plan + SoftwareUpgradeProposal = types.SoftwareUpgradeProposal + CancelSoftwareUpgradeProposal = types.CancelSoftwareUpgradeProposal + Keeper = keeper.Keeper +) diff --git a/x/upgrade/handler.go b/x/upgrade/handler.go new file mode 100644 index 000000000000..5f8e70baf161 --- /dev/null +++ b/x/upgrade/handler.go @@ -0,0 +1,32 @@ +package upgrade + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +func NewSoftwareUpgradeProposalHandler(k Keeper) govtypes.Handler { + return func(ctx sdk.Context, content govtypes.Content) sdk.Error { + switch c := content.(type) { + case SoftwareUpgradeProposal: + return handleSoftwareUpgradeProposal(ctx, k, c) + case CancelSoftwareUpgradeProposal: + return handleCancelSoftwareUpgradeProposal(ctx, k, c) + + default: + errMsg := fmt.Sprintf("unrecognized software upgrade proposal content type: %T", c) + return sdk.ErrUnknownRequest(errMsg) + } + } +} + +func handleSoftwareUpgradeProposal(ctx sdk.Context, k Keeper, p SoftwareUpgradeProposal) sdk.Error { + return k.ScheduleUpgrade(ctx, p.Plan) +} + +func handleCancelSoftwareUpgradeProposal(ctx sdk.Context, k Keeper, p CancelSoftwareUpgradeProposal) sdk.Error { + k.ClearUpgradePlan(ctx) + return nil +} diff --git a/x/upgrade/module.go b/x/upgrade/module.go new file mode 100644 index 000000000000..fa591b817652 --- /dev/null +++ b/x/upgrade/module.go @@ -0,0 +1,134 @@ +package upgrade + +import ( + "encoding/json" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/upgrade/client/cli" + "github.com/cosmos/cosmos-sdk/x/upgrade/client/rest" + abci "github.com/tendermint/tendermint/abci/types" +) + +// module codec +var moduleCdc = codec.New() + +func init() { + RegisterCodec(moduleCdc) +} + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// AppModuleBasic implements the sdk.AppModuleBasic interface +type AppModuleBasic struct{} + +// Name returns the ModuleName +func (AppModuleBasic) Name() string { + return ModuleName +} + +// RegisterCodec registers the upgrade types on the amino codec +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { + RegisterCodec(cdc) +} + +// RegisterRESTRoutes registers all REST query handlers +func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, r *mux.Router) { + rest.RegisterRoutes(ctx, r, moduleCdc, StoreKey) +} + +// GetQueryCmd returns the cli query commands for this module +func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { + queryCmd := &cobra.Command{ + Use: "upgrade", + Short: "Querying commands for the upgrade module", + } + queryCmd.AddCommand(client.GetCommands( + cli.GetPlanCmd(StoreKey, cdc), + cli.GetAppliedHeightCmd(StoreKey, cdc), + )...) + + return queryCmd +} + +// GetTxCmd returns the transaction commands for this module +func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { + txCmd := &cobra.Command{ + Use: "upgrade", + Short: "Upgrade transaction subcommands", + } + txCmd.AddCommand(client.PostCommands()...) + return txCmd +} + +// AppModule implements the sdk.AppModule interface +type AppModule struct { + AppModuleBasic + keeper Keeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper Keeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + } +} + +// RegisterInvariants does nothing, there are no invariants to enforce +func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// Route is empty, as we do not handle Messages (just proposals) +func (AppModule) Route() string { return "" } + +// NewHandler is empty, as we do not handle Messages (just proposals) +func (am AppModule) NewHandler() sdk.Handler { return nil } + +// QuerierRoute returns the route we respond to for abci queries +func (AppModule) QuerierRoute() string { return QuerierKey } + +// NewQuerierHandler registers a query handler to respond to the module-specific queries +func (am AppModule) NewQuerierHandler() sdk.Querier { + // TODO + return nil +} + +// InitGenesis is ignored, no sense in serializing future upgrades +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} + +// DefaultGenesis is an empty object +func (AppModuleBasic) DefaultGenesis() json.RawMessage { + return []byte("{}") +} + +// ValidateGenesis is always successful, as we ignore the value +func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + return nil +} + +// ExportGenesis is always empty, as InitGenesis does nothing either +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + return am.DefaultGenesis() +} + +// BeginBlock calls the upgrade module hooks +func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { + am.keeper.BeginBlocker(ctx, req) + // BeginBlock(am.keeper, ctx, req) +} + +// EndBlock does nothing +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} From 17ab81ce266674363ca51535cf0eb0a0d33e6b85 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 1 Oct 2019 13:29:25 +0200 Subject: [PATCH 31/62] Move BeginBlock logic from Keeper into abci.go --- x/upgrade/abci.go | 39 +++++++++++++++ .../keeper/keeper_test.go => abci_test.go} | 43 +++++++++-------- x/upgrade/exported/keeper.go | 9 ++-- x/upgrade/internal/keeper/keeper.go | 47 +++++-------------- x/upgrade/internal/types/plan.go | 37 ++++++++++++--- x/upgrade/module.go | 7 ++- 6 files changed, 111 insertions(+), 71 deletions(-) rename x/upgrade/{internal/keeper/keeper_test.go => abci_test.go} (75%) diff --git a/x/upgrade/abci.go b/x/upgrade/abci.go index 8088f98b1d64..060c1449ea38 100644 --- a/x/upgrade/abci.go +++ b/x/upgrade/abci.go @@ -1 +1,40 @@ package upgrade + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +// BeginBlock will check if there is a scheduled plan and if it is ready to be executed. +// If it is ready, it will execute it if the handler is installed, and panic/abort otherwise. +// If the plan is not ready, it will ensure the handler is not registered too early (and abort otherwise). +// +// The prupose is to ensure the binary is switch EXACTLY at the desired block, and to allow +// a migration to be executed if needed upon this switch (migration defined in the new binary) +func BeginBlock(k *Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { + plan, found := k.GetUpgradePlan(ctx) + if !found { + return + } + + if plan.ShouldExecute(ctx) { + if k.HasHandler(plan.Name) { + // We have an upgrade handler for this upgrade name, so apply the upgrade + ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at %s", plan.Name, plan.DueDate())) + k.ApplyUpgrade(ctx, plan) + } else { + // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown + ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at %s: %s", plan.Name, plan.DueDate(), plan.Info)) + panic("UPGRADE REQUIRED!") + } + } else { + // if we have a pending upgrade, but it is not yet time, make sure we did not + // set the handler already + if k.HasHandler(plan.Name) { + ctx.Logger().Error(fmt.Sprintf("UNKNOWN UPGRADE \"%s\" - in binary but not executed on chain", plan.Name)) + panic("BINARY UPDATED BEFORE TRIGGER!") + } + } +} diff --git a/x/upgrade/internal/keeper/keeper_test.go b/x/upgrade/abci_test.go similarity index 75% rename from x/upgrade/internal/keeper/keeper_test.go rename to x/upgrade/abci_test.go index 4ee2b86a626d..c5d53529ea5b 100644 --- a/x/upgrade/internal/keeper/keeper_test.go +++ b/x/upgrade/abci_test.go @@ -1,4 +1,4 @@ -package keeper +package upgrade import ( "testing" @@ -7,7 +7,6 @@ import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" "github.com/stretchr/testify/suite" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" @@ -26,7 +25,7 @@ func (s *TestSuite) SetupTest() { s.cms = store.NewCommitMultiStore(db) key := sdk.NewKVStoreKey("upgrade") cdc := codec.New() - types.RegisterCodec(cdc) + RegisterCodec(cdc) s.keeper = NewKeeper(key, cdc) s.cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db) _ = s.cms.LoadLatestVersion() @@ -34,32 +33,32 @@ func (s *TestSuite) SetupTest() { } func (s *TestSuite) TestRequireName() { - err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{}) + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{}) s.Require().NotNil(err) s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) } func (s *TestSuite) TestRequireFutureTime() { - err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Time: s.ctx.BlockHeader().Time}) + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: s.ctx.BlockHeader().Time}) s.Require().NotNil(err) s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) } func (s *TestSuite) TestRequireFutureBlock() { - err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Height: s.ctx.BlockHeight()}) + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight()}) s.Require().NotNil(err) s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) } func (s *TestSuite) TestCantSetBothTimeAndHeight() { - err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Time: time.Now(), Height: s.ctx.BlockHeight() + 1}) + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now(), Height: s.ctx.BlockHeight() + 1}) s.Require().NotNil(err) s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) } func (s *TestSuite) TestDoTimeUpgrade() { s.T().Log("Verify can schedule an upgrade") - err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Time: time.Now()}) + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) s.Require().Nil(err) s.VerifyDoUpgrade() @@ -67,7 +66,7 @@ func (s *TestSuite) TestDoTimeUpgrade() { func (s *TestSuite) TestDoHeightUpgrade() { s.T().Log("Verify can schedule an upgrade") - err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}) + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}) s.Require().Nil(err) s.VerifyDoUpgrade() @@ -78,13 +77,13 @@ func (s *TestSuite) VerifyDoUpgrade() { newCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 1, Time: time.Now()}, false, log.NewNopLogger()) req := abci.RequestBeginBlock{Header: newCtx.BlockHeader()} s.Require().Panics(func() { - s.keeper.BeginBlocker(newCtx, req) + BeginBlock(s.keeper, newCtx, req) }) s.T().Log("Verify that the upgrade can be successfully applied with a handler") - s.keeper.SetUpgradeHandler("test", func(ctx sdk.Context, plan types.Plan) {}) + s.keeper.SetUpgradeHandler("test", func(ctx sdk.Context, plan Plan) {}) s.Require().NotPanics(func() { - s.keeper.BeginBlocker(newCtx, req) + BeginBlock(s.keeper, newCtx, req) }) s.VerifyCleared(newCtx) @@ -93,20 +92,20 @@ func (s *TestSuite) VerifyDoUpgrade() { func (s *TestSuite) TestHaltIfTooNew() { s.T().Log("Verify that we don't panic with registered plan not in database at all") var called int - s.keeper.SetUpgradeHandler("future", func(ctx sdk.Context, plan types.Plan) { called++ }) + s.keeper.SetUpgradeHandler("future", func(ctx sdk.Context, plan Plan) { called++ }) newCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 1, Time: time.Now()}, false, log.NewNopLogger()) req := abci.RequestBeginBlock{Header: newCtx.BlockHeader()} s.Require().NotPanics(func() { - s.keeper.BeginBlocker(newCtx, req) + BeginBlock(s.keeper, newCtx, req) }) s.Require().Equal(0, called) s.T().Log("Verify we panic if we have a registered handler ahead of time") - err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "future", Height: s.ctx.BlockHeight() + 3}) + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "future", Height: s.ctx.BlockHeight() + 3}) s.Require().NoError(err) s.Require().Panics(func() { - s.keeper.BeginBlocker(newCtx, req) + BeginBlock(s.keeper, newCtx, req) }) s.Require().Equal(0, called) @@ -115,7 +114,7 @@ func (s *TestSuite) TestHaltIfTooNew() { futCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 3, Time: time.Now()}, false, log.NewNopLogger()) req = abci.RequestBeginBlock{Header: futCtx.BlockHeader()} s.Require().NotPanics(func() { - s.keeper.BeginBlocker(futCtx, req) + BeginBlock(s.keeper, futCtx, req) }) s.Require().Equal(1, called) @@ -130,7 +129,7 @@ func (s *TestSuite) VerifyCleared(newCtx sdk.Context) { func (s *TestSuite) TestCanClear() { s.T().Log("Verify upgrade is scheduled") - err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Time: time.Now()}) + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) s.Require().Nil(err) s.keeper.ClearUpgradePlan(s.ctx) @@ -141,7 +140,7 @@ func (s *TestSuite) TestCanClear() { func (s *TestSuite) TestCantApplySameUpgradeTwice() { s.TestDoTimeUpgrade() s.T().Log("Verify an upgrade named \"test\" can't be scheduled twice") - err := s.keeper.ScheduleUpgrade(s.ctx, types.Plan{Name: "test", Time: time.Now()}) + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) s.Require().NotNil(err) s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) } @@ -150,7 +149,7 @@ func (s *TestSuite) TestNoSpuriousUpgrades() { s.T().Log("Verify that no upgrade panic is triggered in the BeginBlocker when we haven't scheduled an upgrade") req := abci.RequestBeginBlock{Header: s.ctx.BlockHeader()} s.Require().NotPanics(func() { - s.keeper.BeginBlocker(s.ctx, req) + BeginBlock(s.keeper, s.ctx, req) }) } @@ -160,11 +159,11 @@ func (s *TestSuite) TestPlanStringer() { s.Require().Equal(`Upgrade Plan Name: test Time: 2020-01-01T00:00:00Z - Info: `, types.Plan{Name: "test", Time: t}.String()) + Info: `, Plan{Name: "test", Time: t}.String()) s.Require().Equal(`Upgrade Plan Name: test Height: 100 - Info: `, types.Plan{Name: "test", Height: 100}.String()) + Info: `, Plan{Name: "test", Height: 100}.String()) } func TestTestSuite(t *testing.T) { diff --git a/x/upgrade/exported/keeper.go b/x/upgrade/exported/keeper.go index 3c752a9f8df8..88cf6a6edb36 100644 --- a/x/upgrade/exported/keeper.go +++ b/x/upgrade/exported/keeper.go @@ -3,7 +3,6 @@ package exported import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" - abci "github.com/tendermint/tendermint/abci/types" ) // Keeper of the upgrade module @@ -23,7 +22,9 @@ type Keeper interface { // upgrade or false if there is none GetUpgradePlan(ctx sdk.Context) (plan types.Plan, havePlan bool) - // BeginBlocker should be called inside the BeginBlocker method of any app using the upgrade module. Scheduled upgrade - // plans are cached in memory so the overhead of this method is trivial. - BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) + // HasHandler returns true iff there is a handler registered for this name + HasHandler(name string) bool + + // ApplyUpgrade will execute the handler associated with the Plan and mark the plan as done. + ApplyUpgrade(ctx sdk.Context, plan types.Plan) } diff --git a/x/upgrade/internal/keeper/keeper.go b/x/upgrade/internal/keeper/keeper.go index 2215fcf59d40..9413b0c081f4 100644 --- a/x/upgrade/internal/keeper/keeper.go +++ b/x/upgrade/internal/keeper/keeper.go @@ -8,7 +8,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/upgrade/exported" "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" - abci "github.com/tendermint/tendermint/abci/types" ) type Keeper struct { @@ -89,39 +88,19 @@ func (k *Keeper) setDone(ctx sdk.Context, name string) { store.Set(types.DoneHeightKey(name), bz) } -// BeginBlocker should be called inside the BeginBlocker method of any app using the upgrade module. Scheduled upgrade -// plans are cached in memory so the overhead of this method is trivial. -func (k *Keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) { - blockTime := ctx.BlockHeader().Time - blockHeight := ctx.BlockHeight() - - plan, found := k.GetUpgradePlan(ctx) - if !found { - return - } +// HasHandler returns true iff there is a handler registered for this name +func (k *Keeper) HasHandler(name string) bool { + _, ok := k.upgradeHandlers[name] + return ok +} - upgradeTime := plan.Time - upgradeHeight := plan.Height - if (!upgradeTime.IsZero() && !blockTime.Before(upgradeTime)) || (upgradeHeight > 0 && upgradeHeight <= blockHeight) { - handler, ok := k.upgradeHandlers[plan.Name] - if ok { - // We have an upgrade handler for this upgrade name, so apply the upgrade - ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at height %d", plan.Name, blockHeight)) - handler(ctx, plan) - k.ClearUpgradePlan(ctx) - k.setDone(ctx, plan.Name) - } else { - // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown - ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at height %d: %s", plan.Name, blockHeight, plan.Info)) - panic("UPGRADE REQUIRED!") - } - } else { - // if we have a pending upgrade, but it is not yet time, make sure we did not - // set the handler already - _, ok := k.upgradeHandlers[plan.Name] - if ok { - ctx.Logger().Error(fmt.Sprintf("UNKNOWN UPGRADE \"%s\" - in binary but not executed on chain", plan.Name)) - panic("BINARY UPDATED BEFORE TRIGGER!") - } +// ApplyUpgrade will execute the handler associated with the Plan and mark the plan as done. +func (k *Keeper) ApplyUpgrade(ctx sdk.Context, plan types.Plan) { + handler := k.upgradeHandlers[plan.Name] + if handler == nil { + panic("ApplyUpgrade should never be called without first checking HasHandler") } + handler(ctx, plan) + k.ClearUpgradePlan(ctx) + k.setDone(ctx, plan.Name) } diff --git a/x/upgrade/internal/types/plan.go b/x/upgrade/internal/types/plan.go index 4fd6ff270999..66491007d35f 100644 --- a/x/upgrade/internal/types/plan.go +++ b/x/upgrade/internal/types/plan.go @@ -29,23 +29,46 @@ type Plan struct { Info string `json:"info,omitempty"` } -func (plan Plan) String() string { +func (p Plan) String() string { var whenStr string - if !plan.Time.IsZero() { - whenStr = fmt.Sprintf("Time: %s", plan.Time.Format(time.RFC3339)) + if !p.Time.IsZero() { + whenStr = fmt.Sprintf("Time: %s", p.Time.Format(time.RFC3339)) } else { - whenStr = fmt.Sprintf("Height: %d", plan.Height) + whenStr = fmt.Sprintf("Height: %d", p.Height) } return fmt.Sprintf(`Upgrade Plan Name: %s %s - Info: %s`, plan.Name, whenStr, plan.Info) + Info: %s`, p.Name, whenStr, p.Info) } // ValidateBasic does basic validation of a Plan -func (plan Plan) ValidateBasic() sdk.Error { - if len(plan.Name) == 0 { +func (p Plan) ValidateBasic() sdk.Error { + if len(p.Name) == 0 { return sdk.ErrUnknownRequest("Name cannot be empty") } return nil } + +// ShouldExecute returns true if the Plan is ready to execute given the current context +func (p Plan) ShouldExecute(ctx sdk.Context) bool { + if !p.Time.IsZero() { + return !ctx.BlockTime().Before(p.Time) + } + if p.Height > 0 { + return p.Height <= ctx.BlockHeight() + } + return false +} + +// DueDate is a string representation of when this plan is due to be executed +func (p Plan) DueDate() string { + if !p.Time.IsZero() { + return fmt.Sprintf("time: %s", p.Time.UTC().Format(time.RFC3339)) + } + if p.Height > 0 { + return fmt.Sprintf("height: %d", p.Height) + } + return "" + +} diff --git a/x/upgrade/module.go b/x/upgrade/module.go index fa591b817652..a3119e6cdc17 100644 --- a/x/upgrade/module.go +++ b/x/upgrade/module.go @@ -73,11 +73,11 @@ func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { // AppModule implements the sdk.AppModule interface type AppModule struct { AppModuleBasic - keeper Keeper + keeper *Keeper } // NewAppModule creates a new AppModule object -func NewAppModule(keeper Keeper) AppModule { +func NewAppModule(keeper *Keeper) AppModule { return AppModule{ AppModuleBasic: AppModuleBasic{}, keeper: keeper, @@ -124,8 +124,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { // BeginBlock calls the upgrade module hooks func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { - am.keeper.BeginBlocker(ctx, req) - // BeginBlock(am.keeper, ctx, req) + BeginBlock(am.keeper, ctx, req) } // EndBlock does nothing From ee7cc3c5a119c2d9c9141eedebf2ef324017249a Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 1 Oct 2019 14:00:33 +0200 Subject: [PATCH 32/62] Resolved linter issues --- x/gov/alias.go | 54 ++++++++++++++++++------------------- x/gov/legacy/v0_36/types.go | 2 +- x/gov/types/proposal.go | 5 ++-- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/x/gov/alias.go b/x/gov/alias.go index 17de3bb684ce..0ca57b7a39fc 100644 --- a/x/gov/alias.go +++ b/x/gov/alias.go @@ -140,31 +140,31 @@ var ( ) type ( - Keeper = keeper.Keeper - Content = types.Content - Handler = types.Handler - Deposit = types.Deposit - Deposits = types.Deposits - GenesisState = types.GenesisState - MsgSubmitProposal = types.MsgSubmitProposal - MsgDeposit = types.MsgDeposit - MsgVote = types.MsgVote - DepositParams = types.DepositParams - TallyParams = types.TallyParams - VotingParams = types.VotingParams - Params = types.Params - Proposal = types.Proposal - Proposals = types.Proposals - ProposalQueue = types.ProposalQueue - ProposalStatus = types.ProposalStatus - TextProposal = types.TextProposal - QueryProposalParams = types.QueryProposalParams - QueryDepositParams = types.QueryDepositParams - QueryVoteParams = types.QueryVoteParams - QueryProposalsParams = types.QueryProposalsParams - ValidatorGovInfo = types.ValidatorGovInfo - TallyResult = types.TallyResult - Vote = types.Vote - Votes = types.Votes - VoteOption = types.VoteOption + Keeper = keeper.Keeper + Content = types.Content + Handler = types.Handler + Deposit = types.Deposit + Deposits = types.Deposits + GenesisState = types.GenesisState + MsgSubmitProposal = types.MsgSubmitProposal + MsgDeposit = types.MsgDeposit + MsgVote = types.MsgVote + DepositParams = types.DepositParams + TallyParams = types.TallyParams + VotingParams = types.VotingParams + Params = types.Params + Proposal = types.Proposal + Proposals = types.Proposals + ProposalQueue = types.ProposalQueue + ProposalStatus = types.ProposalStatus + TextProposal = types.TextProposal + QueryProposalParams = types.QueryProposalParams + QueryDepositParams = types.QueryDepositParams + QueryVoteParams = types.QueryVoteParams + QueryProposalsParams = types.QueryProposalsParams + ValidatorGovInfo = types.ValidatorGovInfo + TallyResult = types.TallyResult + Vote = types.Vote + Votes = types.Votes + VoteOption = types.VoteOption ) diff --git a/x/gov/legacy/v0_36/types.go b/x/gov/legacy/v0_36/types.go index abc34fbef763..f18cddac1c50 100644 --- a/x/gov/legacy/v0_36/types.go +++ b/x/gov/legacy/v0_36/types.go @@ -18,7 +18,7 @@ const ( DefaultCodespace sdk.CodespaceType = "gov" - ProposalTypeText string = "Text" + ProposalTypeText string = "Text" MaxDescriptionLength int = 5000 MaxTitleLength int = 140 diff --git a/x/gov/types/proposal.go b/x/gov/types/proposal.go index 0ac7b59f653f..a02f7b54d6b9 100644 --- a/x/gov/types/proposal.go +++ b/x/gov/types/proposal.go @@ -201,7 +201,7 @@ func (status ProposalStatus) Format(s fmt.State, verb rune) { // Proposal types const ( - ProposalTypeText string = "Text" + ProposalTypeText string = "Text" ) // TextProposal defines a standard text proposal whose changes need to be @@ -242,9 +242,8 @@ func (tp TextProposal) String() string { `, tp.Title, tp.Description) } - var validProposalTypes = map[string]struct{}{ - ProposalTypeText: {}, + ProposalTypeText: {}, } // RegisterProposalType registers a proposal type. It will panic if the type is From cf3acea03c30981aa95e0346d297cf5368b40528 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 1 Oct 2019 18:00:13 +0200 Subject: [PATCH 33/62] Update flow according to PR --- CHANGELOG.md | 4 ---- x/upgrade/abci.go | 24 ++++++++++++------------ x/upgrade/client/cli/tx.go | 8 ++++---- x/upgrade/handler.go | 3 +++ x/upgrade/internal/keeper/keeper.go | 14 ++++++++++---- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e7e4afe5e1b..feaa910b72ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,13 +77,9 @@ the following [issue](https://github.com/keybase/go-keychain/issues/47) with the you encounter this issue, you must upgrade your xcode command line tools to version >= `10.2`. You can upgrade via: `sudo rm -rf /Library/Developer/CommandLineTools; xcode-select --install`. Verify the correct version via: `pkgutil --pkg-info=com.apple.pkg.CLTools_Executables`. -<<<<<<< HEAD * (keys) [\#5097](https://github.com/cosmos/cosmos-sdk/pull/5097) New `keys migrate` command to assist users migrate their keys to the new keyring. - -======= * (modules) [\#4233](https://github.com/cosmos/cosmos-sdk/pull/4233) Add upgrade module that coordinates software upgrades of live chains. ->>>>>>> Cleanup CHANGELOG ### Improvements diff --git a/x/upgrade/abci.go b/x/upgrade/abci.go index 060c1449ea38..9b4e5871494f 100644 --- a/x/upgrade/abci.go +++ b/x/upgrade/abci.go @@ -20,21 +20,21 @@ func BeginBlock(k *Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { } if plan.ShouldExecute(ctx) { - if k.HasHandler(plan.Name) { - // We have an upgrade handler for this upgrade name, so apply the upgrade - ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at %s", plan.Name, plan.DueDate())) - k.ApplyUpgrade(ctx, plan) - } else { + if !k.HasHandler(plan.Name) { // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at %s: %s", plan.Name, plan.DueDate(), plan.Info)) panic("UPGRADE REQUIRED!") } - } else { - // if we have a pending upgrade, but it is not yet time, make sure we did not - // set the handler already - if k.HasHandler(plan.Name) { - ctx.Logger().Error(fmt.Sprintf("UNKNOWN UPGRADE \"%s\" - in binary but not executed on chain", plan.Name)) - panic("BINARY UPDATED BEFORE TRIGGER!") - } + // We have an upgrade handler for this upgrade name, so apply the upgrade + ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at %s", plan.Name, plan.DueDate())) + k.ApplyUpgrade(ctx, plan) + return + } + + // if we have a pending upgrade, but it is not yet time, make sure we did not + // set the handler already + if k.HasHandler(plan.Name) { + ctx.Logger().Error(fmt.Sprintf("UNKNOWN UPGRADE \"%s\" - in binary but not executed on chain", plan.Name)) + panic("BINARY UPDATED BEFORE TRIGGER!") } } diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go index d6f8d9c3fe14..0daf0307d544 100644 --- a/x/upgrade/client/cli/tx.go +++ b/x/upgrade/client/cli/tx.go @@ -28,7 +28,7 @@ const ( FlagUpgradeInfo = "upgrade-info" ) -func parseSubmitArgs(cmd *cobra.Command, cdc *codec.Codec) (gov.Content, error) { +func parseArgsToContent(cmd *cobra.Command, cdc *codec.Codec) (gov.Content, error) { title, err := cmd.Flags().GetString(cli.FlagTitle) if err != nil { return nil, err @@ -76,8 +76,8 @@ func parseSubmitArgs(cmd *cobra.Command, cdc *codec.Codec) (gov.Content, error) return nil, err } - content := upgrade.NewSoftwareUpgradeProposal(title, description, - upgrade.Plan{Name: name, Time: upgradeTime, Height: height, Info: info}) + plan := upgrade.Plan{Name: name, Time: upgradeTime, Height: height, Info: info} + content := upgrade.NewSoftwareUpgradeProposal(title, description, plan) return content, nil } @@ -91,7 +91,7 @@ func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { "Please specify a unique name and height OR time for the upgrade to take effect.\n" + "You may include info to reference a binary download link, in a format compatible with: https://github.com/regen-network/cosmosd", RunE: func(cmd *cobra.Command, args []string) error { - content, err := parseSubmitArgs(cmd, cdc) + content, err := parseArgsToContent(cmd, cdc) if err != nil { return err } diff --git a/x/upgrade/handler.go b/x/upgrade/handler.go index 5f8e70baf161..2ae2cb09976a 100644 --- a/x/upgrade/handler.go +++ b/x/upgrade/handler.go @@ -7,6 +7,9 @@ import ( govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" ) +// NewSoftwareUpgradeProposalHandler creates a governance handler to manage new proposal types. +// It enables SoftwareUpgradeProposal to propose an Upgrade, and CancelSoftwareUpgradeProposal +// to abort a previously voted upgrade. func NewSoftwareUpgradeProposalHandler(k Keeper) govtypes.Handler { return func(ctx sdk.Context, content govtypes.Content) sdk.Error { switch c := content.(type) { diff --git a/x/upgrade/internal/keeper/keeper.go b/x/upgrade/internal/keeper/keeper.go index 9413b0c081f4..c4765334abd7 100644 --- a/x/upgrade/internal/keeper/keeper.go +++ b/x/upgrade/internal/keeper/keeper.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/upgrade/exported" "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" + "github.com/tendermint/tendermint/libs/log" ) type Keeper struct { @@ -43,17 +44,17 @@ func (k *Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error { } if !plan.Time.IsZero() { if !plan.Time.After(ctx.BlockHeader().Time) { - return sdk.ErrUnknownRequest("Upgrade cannot be scheduled in the past") + return sdk.ErrUnknownRequest("upgrade cannot be scheduled in the past") } if plan.Height != 0 { - return sdk.ErrUnknownRequest("Only one of Time or Height should be specified") + return sdk.ErrUnknownRequest("only one of Time or Height should be specified") } } else if plan.Height <= ctx.BlockHeight() { - return sdk.ErrUnknownRequest("Upgrade cannot be scheduled in the past") + return sdk.ErrUnknownRequest("upgrade cannot be scheduled in the past") } store := ctx.KVStore(k.storeKey) if store.Has(types.DoneHeightKey(plan.Name)) { - return sdk.ErrUnknownRequest(fmt.Sprintf("Upgrade with name %s has already been completed", plan.Name)) + return sdk.ErrUnknownRequest(fmt.Sprintf("upgrade with name %s has already been completed", plan.Name)) } bz := k.cdc.MustMarshalBinaryBare(plan) k.haveCache = false @@ -68,6 +69,11 @@ func (k *Keeper) ClearUpgradePlan(ctx sdk.Context) { store.Delete(types.PlanKey()) } +// Logger returns a module-specific logger. +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} + // GetUpgradePlan returns the currently scheduled Plan if any, setting havePlan to true if there is a scheduled // upgrade or false if there is none func (k *Keeper) GetUpgradePlan(ctx sdk.Context) (plan types.Plan, havePlan bool) { From bfe520ca137544c6951e0fd98af3bff2abf3cd33 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 7 Oct 2019 18:16:31 +0200 Subject: [PATCH 34/62] Updated from PR comments --- x/upgrade/abci.go | 5 +++-- x/upgrade/internal/types/keys.go | 2 +- x/upgrade/internal/types/plan.go | 14 ++++++++++---- x/upgrade/module.go | 2 ++ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/x/upgrade/abci.go b/x/upgrade/abci.go index 9b4e5871494f..d95617f81338 100644 --- a/x/upgrade/abci.go +++ b/x/upgrade/abci.go @@ -22,11 +22,12 @@ func BeginBlock(k *Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { if plan.ShouldExecute(ctx) { if !k.HasHandler(plan.Name) { // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown - ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at %s: %s", plan.Name, plan.DueDate(), plan.Info)) + ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at %s: %s", plan.Name, plan.DueAt(), plan.Info)) panic("UPGRADE REQUIRED!") } // We have an upgrade handler for this upgrade name, so apply the upgrade - ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at %s", plan.Name, plan.DueDate())) + ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at %s", plan.Name, plan.DueAt())) + ctx = ctx.WithBlockGasMeter(sdk.NewInfiniteGasMeter()) k.ApplyUpgrade(ctx, plan) return } diff --git a/x/upgrade/internal/types/keys.go b/x/upgrade/internal/types/keys.go index a5f4860dfb83..75f64525dbc0 100644 --- a/x/upgrade/internal/types/keys.go +++ b/x/upgrade/internal/types/keys.go @@ -22,7 +22,7 @@ const ( ) // PlanKey is the key under which the current plan is saved -// We store PlanByte as a const to keep it immutible (unlike a []byte) +// We store PlanByte as a const to keep it immutable (unlike a []byte) func PlanKey() []byte { return []byte{PlanByte} } diff --git a/x/upgrade/internal/types/plan.go b/x/upgrade/internal/types/plan.go index 66491007d35f..a1976a1b64d1 100644 --- a/x/upgrade/internal/types/plan.go +++ b/x/upgrade/internal/types/plan.go @@ -45,7 +45,13 @@ func (p Plan) String() string { // ValidateBasic does basic validation of a Plan func (p Plan) ValidateBasic() sdk.Error { if len(p.Name) == 0 { - return sdk.ErrUnknownRequest("Name cannot be empty") + return sdk.ErrUnknownRequest("name cannot be empty") + } + if p.Time.IsZero() && p.Height == 0 { + return sdk.ErrUnknownRequest("must set either time or height") + } + if !p.Time.IsZero() && p.Height != 0 { + return sdk.ErrUnknownRequest("cannot set both time and height") } return nil } @@ -61,14 +67,14 @@ func (p Plan) ShouldExecute(ctx sdk.Context) bool { return false } -// DueDate is a string representation of when this plan is due to be executed -func (p Plan) DueDate() string { +// DueAt is a string representation of when this plan is due to be executed +func (p Plan) DueAt() string { if !p.Time.IsZero() { return fmt.Sprintf("time: %s", p.Time.UTC().Format(time.RFC3339)) } if p.Height > 0 { return fmt.Sprintf("height: %d", p.Height) } - return "" + return "" } diff --git a/x/upgrade/module.go b/x/upgrade/module.go index a3119e6cdc17..4d294ee24370 100644 --- a/x/upgrade/module.go +++ b/x/upgrade/module.go @@ -123,6 +123,8 @@ func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { } // BeginBlock calls the upgrade module hooks +// +// CONTRACT: this is registered in BeginBlocker *before* all other modules' BeginBlock functions func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { BeginBlock(am.keeper, ctx, req) } From 2b7396a204e0ca574cc527db76c6d5dcefeb8044 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 7 Oct 2019 18:24:45 +0200 Subject: [PATCH 35/62] Ensure ScheduleUpgrade does not overwrite --- x/upgrade/abci_test.go | 19 +++++++++++++++++++ x/upgrade/internal/keeper/keeper.go | 15 ++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/x/upgrade/abci_test.go b/x/upgrade/abci_test.go index c5d53529ea5b..20912e03a8dc 100644 --- a/x/upgrade/abci_test.go +++ b/x/upgrade/abci_test.go @@ -72,6 +72,25 @@ func (s *TestSuite) TestDoHeightUpgrade() { s.VerifyDoUpgrade() } +func (s *TestSuite) CannotOverwriteScheduleUpgrade() { + s.T().Log("Cannot overwrite a scheduled upgrade") + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}) + s.Require().Nil(err) + err = s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test2", Height: s.ctx.BlockHeight() + 2}) + s.Require().NotNil(err) +} + +func (s *TestSuite) CanCancelAndReschedule() { + s.T().Log("Can clear plan and reschedule") + err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "bad_test", Height: s.ctx.BlockHeight() + 10}) + s.Require().Nil(err) + s.keeper.ClearUpgradePlan(s.ctx) + err = s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}) + s.Require().NotNil(err) + + s.VerifyDoUpgrade() +} + func (s *TestSuite) VerifyDoUpgrade() { s.T().Log("Verify that a panic happens at the upgrade time/height") newCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 1, Time: time.Now()}, false, log.NewNopLogger()) diff --git a/x/upgrade/internal/keeper/keeper.go b/x/upgrade/internal/keeper/keeper.go index c4765334abd7..c6eb58e8c3e0 100644 --- a/x/upgrade/internal/keeper/keeper.go +++ b/x/upgrade/internal/keeper/keeper.go @@ -15,7 +15,6 @@ type Keeper struct { storeKey sdk.StoreKey cdc *codec.Codec upgradeHandlers map[string]types.UpgradeHandler - haveCache bool } var _ exported.Keeper = (*Keeper)(nil) @@ -36,7 +35,9 @@ func (k *Keeper) SetUpgradeHandler(name string, upgradeHandler types.UpgradeHand k.upgradeHandlers[name] = upgradeHandler } -// ScheduleUpgrade schedules an upgrade based on the specified plan +// ScheduleUpgrade schedules an upgrade based on the specified plan. +// It fails to schedule if there is another Plan already scheduled, +// you must first ClearUpgradePlan. func (k *Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error { err := plan.ValidateBasic() if err != nil { @@ -46,18 +47,19 @@ func (k *Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error { if !plan.Time.After(ctx.BlockHeader().Time) { return sdk.ErrUnknownRequest("upgrade cannot be scheduled in the past") } - if plan.Height != 0 { - return sdk.ErrUnknownRequest("only one of Time or Height should be specified") - } } else if plan.Height <= ctx.BlockHeight() { return sdk.ErrUnknownRequest("upgrade cannot be scheduled in the past") } + store := ctx.KVStore(k.storeKey) if store.Has(types.DoneHeightKey(plan.Name)) { return sdk.ErrUnknownRequest(fmt.Sprintf("upgrade with name %s has already been completed", plan.Name)) } + if p, ok := k.GetUpgradePlan(ctx); ok { + return sdk.ErrUnknownRequest(fmt.Sprintf("another upgrade is already scehduled: %s", p.Name)) + } + bz := k.cdc.MustMarshalBinaryBare(plan) - k.haveCache = false store.Set(types.PlanKey(), bz) return nil } @@ -65,7 +67,6 @@ func (k *Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error { // ClearUpgradePlan clears any schedule upgrade func (k *Keeper) ClearUpgradePlan(ctx sdk.Context) { store := ctx.KVStore(k.storeKey) - k.haveCache = false store.Delete(types.PlanKey()) } From a4242a317d851d0ac06e377b4fbdd4993d52faf3 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 7 Oct 2019 19:13:03 +0200 Subject: [PATCH 36/62] Register Querier, use .Query and .QueryWithData --- x/upgrade/alias.go | 5 +++ x/upgrade/client/cli/query.go | 14 ++++--- x/upgrade/client/rest/query.go | 49 +++++++++++++++++++---- x/upgrade/internal/keeper/keeper.go | 9 +++++ x/upgrade/internal/keeper/querier.go | 58 ++++++++++++++++++++++++++++ x/upgrade/internal/types/querier.go | 18 +++++++++ x/upgrade/module.go | 5 +-- 7 files changed, 142 insertions(+), 16 deletions(-) create mode 100644 x/upgrade/internal/keeper/querier.go create mode 100644 x/upgrade/internal/types/querier.go diff --git a/x/upgrade/alias.go b/x/upgrade/alias.go index 4018b7087bbd..89da07e7fc20 100644 --- a/x/upgrade/alias.go +++ b/x/upgrade/alias.go @@ -20,6 +20,8 @@ const ( ProposalTypeSoftwareUpgrade = types.ProposalTypeSoftwareUpgrade ProposalTypeCancelSoftwareUpgrade = types.ProposalTypeCancelSoftwareUpgrade DefaultCodespace = types.DefaultCodespace + QueryCurrent = types.QueryCurrent + QueryApplied = types.QueryApplied ) var ( @@ -29,7 +31,9 @@ var ( DoneHeightKey = types.DoneHeightKey NewSoftwareUpgradeProposal = types.NewSoftwareUpgradeProposal NewCancelSoftwareUpgradeProposal = types.NewCancelSoftwareUpgradeProposal + NewQueryAppliedParams = types.NewQueryAppliedParams NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier ) type ( @@ -37,5 +41,6 @@ type ( Plan = types.Plan SoftwareUpgradeProposal = types.SoftwareUpgradeProposal CancelSoftwareUpgradeProposal = types.CancelSoftwareUpgradeProposal + QueryAppliedParams = types.QueryAppliedParams Keeper = keeper.Keeper ) diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go index d3ebbc3f77b5..b8313844d296 100644 --- a/x/upgrade/client/cli/query.go +++ b/x/upgrade/client/cli/query.go @@ -21,7 +21,7 @@ func GetPlanCmd(storeName string, cdc *codec.Codec) *cobra.Command { cliCtx := context.NewCLIContext().WithCodec(cdc) // ignore height for now - res, _, err := cliCtx.QueryStore(upgrade.PlanKey(), storeName) + res, _, err := cliCtx.Query(fmt.Sprintf("custom/%s/%s", upgrade.QuerierKey, upgrade.QueryCurrent)) if err != nil { return err } @@ -52,9 +52,13 @@ func GetAppliedHeightCmd(storeName string, cdc *codec.Codec) *cobra.Command { cliCtx := context.NewCLIContext().WithCodec(cdc) name := args[0] + params := upgrade.NewQueryAppliedParams(name) + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + return err + } - // ignore height for now - res, _, err := cliCtx.QueryStore(upgrade.DoneHeightKey(name), storeName) + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", upgrade.QuerierKey, upgrade.QueryApplied), bz) if err != nil { return err } @@ -66,8 +70,8 @@ func GetAppliedHeightCmd(storeName string, cdc *codec.Codec) *cobra.Command { return fmt.Errorf("unknown format for applied-upgrade") } - height := int64(binary.BigEndian.Uint64(res)) - fmt.Println(height) + applied := int64(binary.BigEndian.Uint64(res)) + fmt.Println(applied) return nil }, } diff --git a/x/upgrade/client/rest/query.go b/x/upgrade/client/rest/query.go index f70f5ec041c9..3afe6385f81f 100644 --- a/x/upgrade/client/rest/query.go +++ b/x/upgrade/client/rest/query.go @@ -1,36 +1,38 @@ package rest import ( + "encoding/binary" + "fmt" "net/http" + "github.com/gorilla/mux" + "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/types/rest" upgrade "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" - "github.com/gorilla/mux" ) // RegisterRoutes registers REST routes for the upgrade module under the path specified by routeName. -func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, storeName string) { - r.HandleFunc("/"+upgrade.ModuleName, getUpgradePlanHandler(cdc, cliCtx, storeName)).Methods("GET") +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc("/upgrade/current", getCurrentPlanHandler(cliCtx)).Methods("GET") + r.HandleFunc("/upgrade/applied/{name}", getDonePlanHandler(cliCtx)).Methods("GET") } -func getUpgradePlanHandler(cdc *codec.Codec, cliCtx context.CLIContext, storeName string) func(http.ResponseWriter, *http.Request) { +func getCurrentPlanHandler(cliCtx context.CLIContext) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, request *http.Request) { // ignore height for now - res, _, err := cliCtx.QueryStore(upgrade.PlanKey(), storeName) + res, _, err := cliCtx.Query(fmt.Sprintf("custom/%s/%s", upgrade.QuerierKey, upgrade.QueryCurrent)) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - if len(res) == 0 { http.NotFound(w, request) return } var plan upgrade.Plan - err = cdc.UnmarshalBinaryBare(res, &plan) + err = cliCtx.Codec.UnmarshalBinaryBare(res, &plan) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return @@ -39,3 +41,34 @@ func getUpgradePlanHandler(cdc *codec.Codec, cliCtx context.CLIContext, storeNam rest.PostProcessResponse(w, cliCtx, plan) } } + +func getDonePlanHandler(cliCtx context.CLIContext) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + + params := upgrade.NewQueryAppliedParams(name) + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", upgrade.QuerierKey, upgrade.QueryApplied), bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + if len(res) == 0 { + http.NotFound(w, r) + return + } + if len(res) != 8 { + rest.WriteErrorResponse(w, http.StatusInternalServerError, "unknown format for applied-upgrade") + } + + applied := int64(binary.BigEndian.Uint64(res)) + fmt.Println(applied) + rest.PostProcessResponse(w, cliCtx, applied) + } +} diff --git a/x/upgrade/internal/keeper/keeper.go b/x/upgrade/internal/keeper/keeper.go index c6eb58e8c3e0..0f42dad97057 100644 --- a/x/upgrade/internal/keeper/keeper.go +++ b/x/upgrade/internal/keeper/keeper.go @@ -64,6 +64,15 @@ func (k *Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error { return nil } +func (k *Keeper) getDoneHeight(ctx sdk.Context, name string) int64 { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.DoneHeightKey(name)) + if len(bz) == 0 { + return 0 + } + return int64(binary.BigEndian.Uint64(bz)) +} + // ClearUpgradePlan clears any schedule upgrade func (k *Keeper) ClearUpgradePlan(ctx sdk.Context) { store := ctx.KVStore(k.storeKey) diff --git a/x/upgrade/internal/keeper/querier.go b/x/upgrade/internal/keeper/querier.go new file mode 100644 index 000000000000..a167655c5fc2 --- /dev/null +++ b/x/upgrade/internal/keeper/querier.go @@ -0,0 +1,58 @@ +package keeper + +import ( + "encoding/binary" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +// NewQuerier creates a querier for upgrade cli and REST endpoints +func NewQuerier(k *Keeper) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) { + switch path[0] { + + case types.QueryCurrent: + return queryCurrent(ctx, req, k) + + case types.QueryApplied: + return queryApplied(ctx, req, k) + + default: + return nil, sdk.ErrUnknownRequest("unknown supply query endpoint") + } + } +} + +func queryCurrent(ctx sdk.Context, req abci.RequestQuery, k *Keeper) ([]byte, sdk.Error) { + plan, has := k.GetUpgradePlan(ctx) + if !has { + // empty data - client can respond Not Found + return nil, nil + } + res, err := k.cdc.MarshalJSON(&plan) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("failed to JSON marshal result: %s", err.Error())) + } + return res, nil +} + +func queryApplied(ctx sdk.Context, req abci.RequestQuery, k *Keeper) ([]byte, sdk.Error) { + var params types.QueryAppliedParams + + err := k.cdc.UnmarshalJSON(req.Data, ¶ms) + if err != nil { + return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err)) + } + + applied := k.getDoneHeight(ctx, params.Name) + if applied == 0 { + // empty data - client can respond Not Found + return nil, nil + } + bz := make([]byte, 8) + binary.BigEndian.PutUint64(bz, uint64(applied)) + return bz, nil +} diff --git a/x/upgrade/internal/types/querier.go b/x/upgrade/internal/types/querier.go new file mode 100644 index 000000000000..94c247f80c20 --- /dev/null +++ b/x/upgrade/internal/types/querier.go @@ -0,0 +1,18 @@ +package types + +// query endpoints supported by the upgrade Querier +const ( + QueryCurrent = "current" + QueryApplied = "applied" +) + +// QueryAppliedParams is passed as data with QueryApplied +type QueryAppliedParams struct { + Name string +} + +// NewQueryAppliedParams creates a new instance to query +// if a named plan was applied +func NewQueryAppliedParams(name string) QueryAppliedParams { + return QueryAppliedParams{Name: name} +} diff --git a/x/upgrade/module.go b/x/upgrade/module.go index 4d294ee24370..455cefbc1346 100644 --- a/x/upgrade/module.go +++ b/x/upgrade/module.go @@ -43,7 +43,7 @@ func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { // RegisterRESTRoutes registers all REST query handlers func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, r *mux.Router) { - rest.RegisterRoutes(ctx, r, moduleCdc, StoreKey) + rest.RegisterRoutes(ctx, r) } // GetQueryCmd returns the cli query commands for this module @@ -98,8 +98,7 @@ func (AppModule) QuerierRoute() string { return QuerierKey } // NewQuerierHandler registers a query handler to respond to the module-specific queries func (am AppModule) NewQuerierHandler() sdk.Querier { - // TODO - return nil + return NewQuerier(am.keeper) } // InitGenesis is ignored, no sense in serializing future upgrades From 48421748638f287eecb9ea06e19a99dad7e08bbc Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 8 Oct 2019 14:57:20 +0200 Subject: [PATCH 37/62] Add rest handler for upgrade transactions --- x/upgrade/client/cli/tx.go | 2 +- x/upgrade/client/rest/query.go | 1 + x/upgrade/client/rest/tx.go | 110 +++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 x/upgrade/client/rest/tx.go diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go index 0daf0307d544..ac77cb44c5fd 100644 --- a/x/upgrade/client/cli/tx.go +++ b/x/upgrade/client/cli/tx.go @@ -136,7 +136,7 @@ func GetCmdSubmitCancelUpgradeProposal(cdc *codec.Codec) *cobra.Command { Args: cobra.ExactArgs(0), Short: "Submit a software upgrade proposal", Long: strings.TrimSpace( - fmt.Sprintf(`Submit a software upgrade along with an initial deposit. + fmt.Sprintf(`Cancel a software upgrade along with an initial deposit. `, ), ), diff --git a/x/upgrade/client/rest/query.go b/x/upgrade/client/rest/query.go index 3afe6385f81f..bf9bdd341070 100644 --- a/x/upgrade/client/rest/query.go +++ b/x/upgrade/client/rest/query.go @@ -16,6 +16,7 @@ import ( func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { r.HandleFunc("/upgrade/current", getCurrentPlanHandler(cliCtx)).Methods("GET") r.HandleFunc("/upgrade/applied/{name}", getDonePlanHandler(cliCtx)).Methods("GET") + registerTxRoutes(cliCtx, r) } func getCurrentPlanHandler(cliCtx context.CLIContext) func(http.ResponseWriter, *http.Request) { diff --git a/x/upgrade/client/rest/tx.go b/x/upgrade/client/rest/tx.go new file mode 100644 index 000000000000..3afa9a95b4af --- /dev/null +++ b/x/upgrade/client/rest/tx.go @@ -0,0 +1,110 @@ +package rest + +import ( + "net/http" + "time" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" +) + +func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc("/upgrade/plan", postPlanHandler(cliCtx)).Methods("POST") + r.HandleFunc("/upgrade/cancel", cancelPlanHandler(cliCtx)).Methods("POST") +} + +// PlanRequest defines a proposal for a new upgrade plan. +type PlanRequest struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + UpgradeName string `json:"upgrade_name" yaml:"upgrade_name"` + UpgradeHeight int64 `json:"upgrade_height" yaml:"upgrade_height"` + UpgradeTime string `json:"upgrade_time" yaml:"upgrade_time"` + UpgradeInfo string `json:"upgrade_info" yaml:"upgrade_info"` +} + +// CancelRequest defines a proposal to cancel a current plan. +type CancelRequest struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` +} + +func postPlanHandler(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req PlanRequest + + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + fromAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + var t time.Time + if req.UpgradeTime != "" { + t, err = time.Parse(time.RFC3339, req.UpgradeTime) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + } + + plan := types.Plan{Name: req.UpgradeName, Time: t, Height: req.UpgradeHeight, Info: req.UpgradeInfo} + content := types.NewSoftwareUpgradeProposal(req.Title, req.Description, plan) + msg := gov.NewMsgSubmitProposal(content, req.Deposit, fromAddr) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} + +func cancelPlanHandler(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req CancelRequest + + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + fromAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + content := types.NewCancelSoftwareUpgradeProposal(req.Title, req.Description) + msg := gov.NewMsgSubmitProposal(content, req.Deposit, fromAddr) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} From 8eeaed02be10c99404163c41290672d9c8dc2788 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 8 Oct 2019 15:01:59 +0200 Subject: [PATCH 38/62] Allow overwrite scheduled plan, as requested by @aaronc --- x/upgrade/abci_test.go | 15 +++------------ x/upgrade/internal/keeper/keeper.go | 7 ++----- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/x/upgrade/abci_test.go b/x/upgrade/abci_test.go index 20912e03a8dc..d8124471cd81 100644 --- a/x/upgrade/abci_test.go +++ b/x/upgrade/abci_test.go @@ -72,21 +72,12 @@ func (s *TestSuite) TestDoHeightUpgrade() { s.VerifyDoUpgrade() } -func (s *TestSuite) CannotOverwriteScheduleUpgrade() { - s.T().Log("Cannot overwrite a scheduled upgrade") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}) - s.Require().Nil(err) - err = s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test2", Height: s.ctx.BlockHeight() + 2}) - s.Require().NotNil(err) -} - -func (s *TestSuite) CanCancelAndReschedule() { - s.T().Log("Can clear plan and reschedule") +func (s *TestSuite) TestCanOverwriteScheduleUpgrade() { + s.T().Log("Can overwrite plan") err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "bad_test", Height: s.ctx.BlockHeight() + 10}) s.Require().Nil(err) - s.keeper.ClearUpgradePlan(s.ctx) err = s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}) - s.Require().NotNil(err) + s.Require().Nil(err) s.VerifyDoUpgrade() } diff --git a/x/upgrade/internal/keeper/keeper.go b/x/upgrade/internal/keeper/keeper.go index 0f42dad97057..a93248ea5737 100644 --- a/x/upgrade/internal/keeper/keeper.go +++ b/x/upgrade/internal/keeper/keeper.go @@ -36,8 +36,8 @@ func (k *Keeper) SetUpgradeHandler(name string, upgradeHandler types.UpgradeHand } // ScheduleUpgrade schedules an upgrade based on the specified plan. -// It fails to schedule if there is another Plan already scheduled, -// you must first ClearUpgradePlan. +// It there is another Plan already scheduled, it will overwrite it +// (implicitly cancelling the current plan) func (k *Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error { err := plan.ValidateBasic() if err != nil { @@ -55,9 +55,6 @@ func (k *Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error { if store.Has(types.DoneHeightKey(plan.Name)) { return sdk.ErrUnknownRequest(fmt.Sprintf("upgrade with name %s has already been completed", plan.Name)) } - if p, ok := k.GetUpgradePlan(ctx); ok { - return sdk.ErrUnknownRequest(fmt.Sprintf("another upgrade is already scehduled: %s", p.Name)) - } bz := k.cdc.MustMarshalBinaryBare(plan) store.Set(types.PlanKey(), bz) From ac312d8b2e415627602b399f336f963354402589 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 8 Oct 2019 15:50:09 +0200 Subject: [PATCH 39/62] Add some tests --- x/upgrade/internal/types/plan.go | 20 +-- x/upgrade/internal/types/plan_test.go | 183 ++++++++++++++++++++++ x/upgrade/internal/types/proposal.go | 3 + x/upgrade/internal/types/proposal_test.go | 75 +++++++++ 4 files changed, 269 insertions(+), 12 deletions(-) create mode 100644 x/upgrade/internal/types/plan_test.go create mode 100644 x/upgrade/internal/types/proposal_test.go diff --git a/x/upgrade/internal/types/plan.go b/x/upgrade/internal/types/plan.go index a1976a1b64d1..24fd28b72005 100644 --- a/x/upgrade/internal/types/plan.go +++ b/x/upgrade/internal/types/plan.go @@ -2,6 +2,7 @@ package types import ( "fmt" + "strings" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -30,16 +31,12 @@ type Plan struct { } func (p Plan) String() string { - var whenStr string - if !p.Time.IsZero() { - whenStr = fmt.Sprintf("Time: %s", p.Time.Format(time.RFC3339)) - } else { - whenStr = fmt.Sprintf("Height: %d", p.Height) - } + due := p.DueAt() + dueUp := strings.ToUpper(due[0:1]) + due[1:] return fmt.Sprintf(`Upgrade Plan Name: %s %s - Info: %s`, p.Name, whenStr, p.Info) + Info: %s`, p.Name, dueUp, p.Info) } // ValidateBasic does basic validation of a Plan @@ -47,6 +44,9 @@ func (p Plan) ValidateBasic() sdk.Error { if len(p.Name) == 0 { return sdk.ErrUnknownRequest("name cannot be empty") } + if p.Height < 0 { + return sdk.ErrUnknownRequest("height cannot be negative") + } if p.Time.IsZero() && p.Height == 0 { return sdk.ErrUnknownRequest("must set either time or height") } @@ -72,9 +72,5 @@ func (p Plan) DueAt() string { if !p.Time.IsZero() { return fmt.Sprintf("time: %s", p.Time.UTC().Format(time.RFC3339)) } - if p.Height > 0 { - return fmt.Sprintf("height: %d", p.Height) - } - return "" - + return fmt.Sprintf("height: %d", p.Height) } diff --git a/x/upgrade/internal/types/plan_test.go b/x/upgrade/internal/types/plan_test.go new file mode 100644 index 000000000000..999a8f3be6d0 --- /dev/null +++ b/x/upgrade/internal/types/plan_test.go @@ -0,0 +1,183 @@ +package types + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" +) + +func mustParseTime(s string) time.Time { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + panic(err) + } + return t +} + +func TestPlanString(t *testing.T) { + cases := map[string]struct { + p Plan + expect string + }{ + "with time": { + p: Plan{ + Name: "due_time", + Info: "https://foo.bar", + Time: mustParseTime("2019-07-08T11:33:55Z"), + }, + expect: "Upgrade Plan\n Name: due_time\n Time: 2019-07-08T11:33:55Z\n Info: https://foo.bar", + }, + "with height": { + p: Plan{ + Name: "by height", + Info: "https://foo.bar/baz", + Height: 7890, + }, + expect: "Upgrade Plan\n Name: by height\n Height: 7890\n Info: https://foo.bar/baz", + }, + "neither": { + p: Plan{ + Name: "almost-empty", + }, + expect: "Upgrade Plan\n Name: almost-empty\n Height: 0\n Info: ", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + s := tc.p.String() + require.Equal(t, tc.expect, s) + }) + } +} + +func TestPlanValid(t *testing.T) { + cases := map[string]struct { + p Plan + valid bool + }{ + "proper": { + p: Plan{ + Name: "all-good", + Info: "some text here", + Time: mustParseTime("2019-07-08T11:33:55Z"), + }, + valid: true, + }, + "proper by height": { + p: Plan{ + Name: "all-good", + Height: 123450000, + }, + valid: true, + }, + "no name": { + p: Plan{ + Height: 123450000, + }, + }, + "no due at": { + p: Plan{ + Name: "missing", + Info: "important", + }, + }, + "negative height": { + p: Plan{ + Name: "minus", + Height: -12345, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := tc.p.ValidateBasic() + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } + +} + +func TestshouldExecute(t *testing.T) { + cases := map[string]struct { + p Plan + ctxTime time.Time + ctxHeight int64 + expected bool + }{ + "past time": { + p: Plan{ + Name: "do-good", + Info: "some text here", + Time: mustParseTime("2019-07-08T11:33:55Z"), + }, + ctxTime: mustParseTime("2019-07-08T11:32:00Z"), + ctxHeight: 100000, + expected: false, + }, + "on time": { + p: Plan{ + Name: "do-good", + Time: mustParseTime("2019-07-08T11:33:55Z"), + }, + ctxTime: mustParseTime("2019-07-08T11:33:55Z"), + ctxHeight: 100000, + expected: true, + }, + "future time": { + p: Plan{ + Name: "do-good", + Time: mustParseTime("2019-07-08T11:33:55Z"), + }, + ctxTime: mustParseTime("2019-07-08T11:33:57Z"), + ctxHeight: 100000, + expected: true, + }, + "past height": { + p: Plan{ + Name: "do-good", + Height: 1234, + }, + ctxTime: mustParseTime("2019-07-08T11:32:00Z"), + ctxHeight: 1000, + expected: false, + }, + "on height": { + p: Plan{ + Name: "do-good", + Height: 1234, + }, + ctxTime: mustParseTime("2019-07-08T11:32:00Z"), + ctxHeight: 1234, + expected: true, + }, + "future height": { + p: Plan{ + Name: "do-good", + Height: 1234, + }, + ctxTime: mustParseTime("2019-07-08T11:32:00Z"), + ctxHeight: 1235, + expected: true, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + ctx := sdk.NewContext(nil, abci.Header{Height: tc.ctxHeight, Time: tc.ctxTime}, false, log.NewNopLogger()) + should := tc.p.ShouldExecute(ctx) + assert.Equal(t, tc.expected, should) + }) + } +} diff --git a/x/upgrade/internal/types/proposal.go b/x/upgrade/internal/types/proposal.go index 11b242252aa9..3dc54f494f60 100644 --- a/x/upgrade/internal/types/proposal.go +++ b/x/upgrade/internal/types/proposal.go @@ -40,6 +40,9 @@ func (sup SoftwareUpgradeProposal) GetDescription() string { return sup.Descript func (sup SoftwareUpgradeProposal) ProposalRoute() string { return RouterKey } func (sup SoftwareUpgradeProposal) ProposalType() string { return ProposalTypeSoftwareUpgrade } func (sup SoftwareUpgradeProposal) ValidateBasic() sdk.Error { + if err := sup.Plan.ValidateBasic(); err != nil { + return err + } return gov.ValidateAbstract(DefaultCodespace, sup) } diff --git a/x/upgrade/internal/types/proposal_test.go b/x/upgrade/internal/types/proposal_test.go new file mode 100644 index 000000000000..b44357e13f4e --- /dev/null +++ b/x/upgrade/internal/types/proposal_test.go @@ -0,0 +1,75 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/gov" +) + +type ProposalWrapper struct { + Prop gov.Content +} + +func TestContentAccessors(t *testing.T) { + cases := map[string]struct { + p gov.Content + title string + desc string + typ string + str string + }{ + "upgrade": { + p: NewSoftwareUpgradeProposal("Title", "desc", Plan{ + Name: "due_time", + Info: "https://foo.bar", + Time: mustParseTime("2019-07-08T11:33:55Z"), + }), + title: "Title", + desc: "desc", + typ: "SoftwareUpgrade", + str: "Software Upgrade Proposal:\n Title: Title\n Description: desc\n", + }, + "cancel": { + p: NewCancelSoftwareUpgradeProposal("Cancel", "bad idea"), + title: "Cancel", + desc: "bad idea", + typ: "CancelSoftwareUpgrade", + str: "Cancel Software Upgrade Proposal:\n Title: Cancel\n Description: bad idea\n", + }, + } + + cdc := codec.New() + gov.RegisterCodec(cdc) + RegisterCodec(cdc) + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.title, tc.p.GetTitle()) + assert.Equal(t, tc.desc, tc.p.GetDescription()) + assert.Equal(t, tc.typ, tc.p.ProposalType()) + assert.Equal(t, "upgrade", tc.p.ProposalRoute()) + assert.Equal(t, tc.str, tc.p.String()) + + // try to encode and decode type to ensure codec works + wrap := ProposalWrapper{tc.p} + bz, err := cdc.MarshalBinaryBare(&wrap) + require.NoError(t, err) + unwrap := ProposalWrapper{} + err = cdc.UnmarshalBinaryBare(bz, &unwrap) + require.NoError(t, err) + + // all methods should look the same + assert.Equal(t, tc.title, unwrap.Prop.GetTitle()) + assert.Equal(t, tc.desc, unwrap.Prop.GetDescription()) + assert.Equal(t, tc.typ, unwrap.Prop.ProposalType()) + assert.Equal(t, "upgrade", unwrap.Prop.ProposalRoute()) + assert.Equal(t, tc.str, unwrap.Prop.String()) + + }) + + } +} From 08f48d9d62bf359b05a22dfabd1673d9d327d4b1 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 9 Oct 2019 11:27:00 +0200 Subject: [PATCH 40/62] Simply cli tx arguments. Thanks Jack --- x/upgrade/client/cli/query.go | 7 +++++-- x/upgrade/client/cli/tx.go | 27 +++++++++------------------ 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go index b8313844d296..d02e01632373 100644 --- a/x/upgrade/client/cli/query.go +++ b/x/upgrade/client/cli/query.go @@ -40,6 +40,10 @@ func GetPlanCmd(storeName string, cdc *codec.Codec) *cobra.Command { } } +type appliedHeight struct { + Applied int64 `json:"applied" yaml:"applied"` +} + // GetAppliedHeightCmd returns the height at which a completed upgrade was applied func GetAppliedHeightCmd(storeName string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ @@ -71,8 +75,7 @@ func GetAppliedHeightCmd(storeName string, cdc *codec.Codec) *cobra.Command { } applied := int64(binary.BigEndian.Uint64(res)) - fmt.Println(applied) - return nil + return cliCtx.PrintOutput(appliedHeight{applied}) }, } } diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go index ac77cb44c5fd..62d5f9f63ca9 100644 --- a/x/upgrade/client/cli/tx.go +++ b/x/upgrade/client/cli/tx.go @@ -19,16 +19,15 @@ import ( ) const ( - // TimeFormat specifies ISO UTC format for submitting the upgrade-time for a new upgrade proposal + // TimeFormat specifies ISO UTC format for submitting the time for a new upgrade proposal TimeFormat = "2006-01-02T15:04:05Z" - FlagUpgradeName = "upgrade-name" - FlagUpgradeHeight = "upgrade-height" - FlagUpgradeTime = "upgrade-time" - FlagUpgradeInfo = "upgrade-info" + FlagUpgradeHeight = "height" + FlagUpgradeTime = "time" + FlagUpgradeInfo = "info" ) -func parseArgsToContent(cmd *cobra.Command, cdc *codec.Codec) (gov.Content, error) { +func parseArgsToContent(cmd *cobra.Command, name string) (gov.Content, error) { title, err := cmd.Flags().GetString(cli.FlagTitle) if err != nil { return nil, err @@ -39,14 +38,6 @@ func parseArgsToContent(cmd *cobra.Command, cdc *codec.Codec) (gov.Content, erro return nil, err } - name, err := cmd.Flags().GetString(FlagUpgradeName) - if err != nil { - return nil, err - } - if len(name) == 0 { - name = title - } - height, err := cmd.Flags().GetInt64(FlagUpgradeHeight) if err != nil { return nil, err @@ -84,14 +75,15 @@ func parseArgsToContent(cmd *cobra.Command, cdc *codec.Codec) (gov.Content, erro // GetCmdSubmitUpgradeProposal implements a command handler for submitting a software upgrade proposal transaction. func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ - Use: "software-upgrade --upgrade-name [name] (--upgrade-height [height] | --upgrade-time [time]) (--upgrade-info [info]) [flags]", - Args: cobra.ExactArgs(0), + Use: "software-upgrade [name] (--upgrade-height [height] | --upgrade-time [time]) (--upgrade-info [info]) [flags]", + Args: cobra.ExactArgs(1), Short: "Submit a software upgrade proposal", Long: "Submit a software upgrade along with an initial deposit.\n" + "Please specify a unique name and height OR time for the upgrade to take effect.\n" + "You may include info to reference a binary download link, in a format compatible with: https://github.com/regen-network/cosmosd", RunE: func(cmd *cobra.Command, args []string) error { - content, err := parseArgsToContent(cmd, cdc) + name := args[0] + content, err := parseArgsToContent(cmd, name) if err != nil { return err } @@ -121,7 +113,6 @@ func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { cmd.Flags().String(cli.FlagTitle, "", "title of proposal") cmd.Flags().String(cli.FlagDescription, "", "description of proposal") cmd.Flags().String(cli.FlagDeposit, "", "deposit of proposal") - cmd.Flags().String(FlagUpgradeName, "", "The name of the upgrade (if not specified title will be used)") cmd.Flags().Int64(FlagUpgradeHeight, 0, "The height at which the upgrade must happen (not to be used together with --upgrade-time)") cmd.Flags().String(FlagUpgradeTime, "", fmt.Sprintf("The time at which the upgrade must happen (ex. %s) (not to be used together with --upgrade-height)", TimeFormat)) cmd.Flags().String(FlagUpgradeInfo, "", "Optional info for the planned upgrade such as commit hash, etc.") From 4bf257ccb19b010cd73a56480c0ecddbd87640de Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 9 Oct 2019 11:37:21 +0200 Subject: [PATCH 41/62] Fix lint error --- x/upgrade/internal/types/plan_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/upgrade/internal/types/plan_test.go b/x/upgrade/internal/types/plan_test.go index 999a8f3be6d0..470dd265f747 100644 --- a/x/upgrade/internal/types/plan_test.go +++ b/x/upgrade/internal/types/plan_test.go @@ -109,7 +109,7 @@ func TestPlanValid(t *testing.T) { } -func TestshouldExecute(t *testing.T) { +func TestShouldExecute(t *testing.T) { cases := map[string]struct { p Plan ctxTime time.Time From 69a2907abd5f8d46a18c8a87fb094cccdadf8498 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 9 Oct 2019 11:41:37 +0200 Subject: [PATCH 42/62] Added proposaltype event attribute to gov/submit-proposal --- x/gov/handler.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x/gov/handler.go b/x/gov/handler.go index cd116264c359..c3ce2f3e80e5 100644 --- a/x/gov/handler.go +++ b/x/gov/handler.go @@ -7,6 +7,9 @@ import ( "github.com/cosmos/cosmos-sdk/x/gov/types" ) +// AttributeKeyProposalType is used to flag events on the type of the proposal +const AttributeKeyProposalType = "proposaltype" + // NewHandler creates an sdk.Handler for all the gov type messages func NewHandler(keeper Keeper) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { @@ -45,6 +48,7 @@ func handleMsgSubmitProposal(ctx sdk.Context, keeper Keeper, msg MsgSubmitPropos sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), sdk.NewAttribute(sdk.AttributeKeySender, msg.Proposer.String()), + sdk.NewAttribute(AttributeKeyProposalType, msg.Content.ProposalType()), ), ) From 8c75855b878ca811733cc2da5c08edd18311908a Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 9 Oct 2019 19:05:54 +0200 Subject: [PATCH 43/62] Improved gov event emitting --- x/gov/handler.go | 15 ++++++--------- x/gov/types/events.go | 1 + 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/x/gov/handler.go b/x/gov/handler.go index c3ce2f3e80e5..f10c7b36ffc1 100644 --- a/x/gov/handler.go +++ b/x/gov/handler.go @@ -7,9 +7,6 @@ import ( "github.com/cosmos/cosmos-sdk/x/gov/types" ) -// AttributeKeyProposalType is used to flag events on the type of the proposal -const AttributeKeyProposalType = "proposaltype" - // NewHandler creates an sdk.Handler for all the gov type messages func NewHandler(keeper Keeper) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { @@ -48,18 +45,18 @@ func handleMsgSubmitProposal(ctx sdk.Context, keeper Keeper, msg MsgSubmitPropos sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), sdk.NewAttribute(sdk.AttributeKeySender, msg.Proposer.String()), - sdk.NewAttribute(AttributeKeyProposalType, msg.Content.ProposalType()), ), ) + submitAttrs := []sdk.Attribute{sdk.NewAttribute(types.AttributeKeyProposalType, msg.Content.ProposalType())} if votingStarted { - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeSubmitProposal, - sdk.NewAttribute(types.AttributeKeyVotingPeriodStart, fmt.Sprintf("%d", proposal.ProposalID)), - ), + submitAttrs = append(submitAttrs, + sdk.NewAttribute(types.AttributeKeyVotingPeriodStart, fmt.Sprintf("%d", proposal.ProposalID)), ) } + ctx.EventManager().EmitEvent( + sdk.NewEvent(types.EventTypeSubmitProposal, submitAttrs...), + ) return sdk.Result{ Data: GetProposalIDBytes(proposal.ProposalID), diff --git a/x/gov/types/events.go b/x/gov/types/events.go index 383078459506..19f8857f3a1c 100644 --- a/x/gov/types/events.go +++ b/x/gov/types/events.go @@ -17,4 +17,5 @@ const ( AttributeValueProposalPassed = "proposal_passed" // met vote quorum AttributeValueProposalRejected = "proposal_rejected" // didn't meet vote quorum AttributeValueProposalFailed = "proposal_failed" // error on proposal handler + AttributeKeyProposalType = "proposal_type" ) From f67260782fbef95d36849b2bab6c2a956972851f Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 17 Oct 2019 11:10:42 +0200 Subject: [PATCH 44/62] Fix CHANGELOG --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40192fee4bbf..1f55091b3b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,9 +97,7 @@ upgrade via: `sudo rm -rf /Library/Developer/CommandLineTools; xcode-select --in correct version via: `pkgutil --pkg-info=com.apple.pkg.CLTools_Executables`. * (keys) [\#5097](https://github.com/cosmos/cosmos-sdk/pull/5097) New `keys migrate` command to assist users migrate their keys to the new keyring. -<<<<<<< HEAD * (modules) [\#4233](https://github.com/cosmos/cosmos-sdk/pull/4233) Add upgrade module that coordinates software upgrades of live chains. -======= * [\#4486](https://github.com/cosmos/cosmos-sdk/issues/4486) Introduce new `PeriodicVestingAccount` vesting account type that allows for arbitrary vesting periods. * (x/auth) [\#5006](https://github.com/cosmos/cosmos-sdk/pull/5006) Modular `AnteHandler` via composable decorators: @@ -125,7 +123,6 @@ that allows for arbitrary vesting periods. * `SigVerificationDecorator`: Verify each signature is valid, return if there is an error. * `ValidateSigCountDecorator`: Validate the number of signatures in tx based on app-parameters. * `IncrementSequenceDecorator`: Increments the account sequence for each signer to prevent replay attacks. ->>>>>>> origin/master ### Improvements From e25925353fe24cdeb12b2a38710f692e6410fb49 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 24 Oct 2019 14:51:05 +0200 Subject: [PATCH 45/62] Addressed most PR comments --- x/gov/handler.go | 8 +++----- x/upgrade/abci.go | 12 +++++++----- x/upgrade/client/cli/query.go | 2 +- x/upgrade/client/cli/tx.go | 13 +++---------- x/upgrade/doc.go | 27 ++++++++++++++------------- x/upgrade/internal/keeper/keeper.go | 2 +- 6 files changed, 29 insertions(+), 35 deletions(-) diff --git a/x/gov/handler.go b/x/gov/handler.go index f10c7b36ffc1..51065fc5b24e 100644 --- a/x/gov/handler.go +++ b/x/gov/handler.go @@ -48,15 +48,13 @@ func handleMsgSubmitProposal(ctx sdk.Context, keeper Keeper, msg MsgSubmitPropos ), ) - submitAttrs := []sdk.Attribute{sdk.NewAttribute(types.AttributeKeyProposalType, msg.Content.ProposalType())} + submitEvent := sdk.NewEvent(types.EventTypeSubmitProposal, sdk.NewAttribute(types.AttributeKeyProposalType, msg.Content.ProposalType())) if votingStarted { - submitAttrs = append(submitAttrs, + submitEvent.AppendAttributes( sdk.NewAttribute(types.AttributeKeyVotingPeriodStart, fmt.Sprintf("%d", proposal.ProposalID)), ) } - ctx.EventManager().EmitEvent( - sdk.NewEvent(types.EventTypeSubmitProposal, submitAttrs...), - ) + ctx.EventManager().EmitEvent(submitEvent) return sdk.Result{ Data: GetProposalIDBytes(proposal.ProposalID), diff --git a/x/upgrade/abci.go b/x/upgrade/abci.go index d95617f81338..7cff1ed15ddf 100644 --- a/x/upgrade/abci.go +++ b/x/upgrade/abci.go @@ -21,12 +21,13 @@ func BeginBlock(k *Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { if plan.ShouldExecute(ctx) { if !k.HasHandler(plan.Name) { + upgradeMsg := fmt.Sprintf("UPGRADE \"%s\" NEEDED at %s: %s", plan.Name, plan.DueAt(), plan.Info) // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown - ctx.Logger().Error(fmt.Sprintf("UPGRADE \"%s\" NEEDED at %s: %s", plan.Name, plan.DueAt(), plan.Info)) - panic("UPGRADE REQUIRED!") + ctx.Logger().Error(upgradeMsg) + panic(upgradeMsg) } // We have an upgrade handler for this upgrade name, so apply the upgrade - ctx.Logger().Info(fmt.Sprintf("Applying upgrade \"%s\" at %s", plan.Name, plan.DueAt())) + ctx.Logger().Info(fmt.Sprintf("applying upgrade \"%s\" at %s", plan.Name, plan.DueAt())) ctx = ctx.WithBlockGasMeter(sdk.NewInfiniteGasMeter()) k.ApplyUpgrade(ctx, plan) return @@ -35,7 +36,8 @@ func BeginBlock(k *Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { // if we have a pending upgrade, but it is not yet time, make sure we did not // set the handler already if k.HasHandler(plan.Name) { - ctx.Logger().Error(fmt.Sprintf("UNKNOWN UPGRADE \"%s\" - in binary but not executed on chain", plan.Name)) - panic("BINARY UPDATED BEFORE TRIGGER!") + downgradeMsg := fmt.Sprintf("BINARY UPDATED BEFORE TRIGGER! UPGRADE \"%s\" - in binary but not executed on chain", plan.Name) + ctx.Logger().Error(downgradeMsg) + panic(downgradeMsg) } } diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go index d02e01632373..77b6f146cdd8 100644 --- a/x/upgrade/client/cli/query.go +++ b/x/upgrade/client/cli/query.go @@ -15,7 +15,7 @@ func GetPlanCmd(storeName string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "plan", Short: "get upgrade plan (if one exists)", - Long: "This gets the currently scheduled upgrade plan", + Long: "Gets the currently scheduled upgrade plan, if one exists", Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go index 62d5f9f63ca9..e49dbf5eb2c4 100644 --- a/x/upgrade/client/cli/tx.go +++ b/x/upgrade/client/cli/tx.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "strings" "time" "github.com/cosmos/cosmos-sdk/x/gov/client/cli" @@ -48,10 +47,8 @@ func parseArgsToContent(cmd *cobra.Command, name string) (gov.Content, error) { return nil, err } - if height != 0 { - if len(timeStr) != 0 { - return nil, fmt.Errorf("only one of --upgrade-time or --upgrade-height should be specified") - } + if height != 0 && len(timeStr) != 0 { + return nil, fmt.Errorf("only one of --upgrade-time or --upgrade-height should be specified") } var upgradeTime time.Time @@ -126,11 +123,7 @@ func GetCmdSubmitCancelUpgradeProposal(cdc *codec.Codec) *cobra.Command { Use: "cancel-software-upgrade [flags]", Args: cobra.ExactArgs(0), Short: "Submit a software upgrade proposal", - Long: strings.TrimSpace( - fmt.Sprintf(`Cancel a software upgrade along with an initial deposit. -`, - ), - ), + Long: "Cancel a software upgrade along with an initial deposit.", RunE: func(cmd *cobra.Command, args []string) error { txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) cliCtx := context.NewCLIContext().WithCodec(cdc) diff --git a/x/upgrade/doc.go b/x/upgrade/doc.go index 5c8f1ccb15be..33beba3aecc6 100644 --- a/x/upgrade/doc.go +++ b/x/upgrade/doc.go @@ -12,20 +12,20 @@ General Workflow Let's assume we are running v0.34.0 of our software in our testnet and want to upgrade to v0.36.0. How would this look in practice? First of all, we want to finalize the v0.36.0 release candidate and there install a specially named upgrade handler (eg. "testnet-v2" or even "v0.36.0"). Once -this code is public, we can have a governance vote to approve this upgrade at some future blocktime -or blockheight (known as an upgrade.Plan). The v0.34.0 code will not know of this handler, but will -continue to run until block 200000, when the plan kicks in at BeginBlock. It will check for existence -of the handler, and finding it missing, know that it is running the obsolete software, and kill itself. -Shortly before killing itself, it will check if there is a script in `/config/do-upgrade` -and run it if present. - -Generally the gaiad/regend/etc binary will restart on crash, but then will execute this BeginBlocker -again and crash, causing a restart loop. Either the operator can manually install the new software, -or you can make use of the `do-upgrade` script to eg. dump state to json (backup), download new binary -(from a location I trust - script written by operator), install binary. +this code is public, we can have a governance vote to approve this upgrade at some future block time +or block height (e.g. 200000). This is known as an upgrade.Plan. The v0.34.0 code will not know of this +handler, but will continue to run until block 200000, when the plan kicks in at BeginBlock. It will check +for existence of the handler, and finding it missing, know that it is running the obsolete software, +and gracefully exit. + +Generally the application binary will restart on exit, but then will execute this BeginBlocker +again and exit, causing a restart loop. Either the operator can manually install the new software, +or you can make use of an external watcher daemon to possibly download and then switch binaries, +also potentially doing a backup. An example of such a daemon is https://github.com/regen-network/cosmosd/ +described below under "Automation". When the binary restarts with the upgraded version (here v0.36.0), it will detect we have registered the -"testnet-v2" upgrade handler in the code, and realize it is the new version. It then will run the script +"testnet-v2" upgrade handler in the code, and realize it is the new version. It then will run the upgrade handler and *migrate the database in-place*. Once finished, it marks the upgrade as done, and continues processing the rest of the block as normal. Once 2/3 of the voting power has upgraded, the blockchain will immediately resume the consensus mechanism. If the majority of operators add a custom `do-upgrade` script, this should @@ -64,7 +64,7 @@ that looks like: UPGRADE "" NEEDED at height : where Name are Info are the values of the respective fields on the upgrade Plan. -To perform the actual halt of the blockchain, the upgrade keeper simply panic's which prevents the ABCI state machine +To perform the actual halt of the blockchain, the upgrade keeper simply panics which prevents the ABCI state machine from proceeding but doesn't actually exit the process. Exiting the process can cause issues for other nodes that start to lose connectivity with the exiting nodes, thus this module prefers to just halt but not exit. @@ -76,5 +76,6 @@ to swap binaries as needed. You can pass in information into Plan.Info according specified here https://github.com/regen-network/cosmosd/blob/master/README.md#auto-download . This will allow a properly configured cosmsod daemon to auto-download new binaries and auto-upgrade. As noted there, this is intended more for full nodes than validators. + */ package upgrade diff --git a/x/upgrade/internal/keeper/keeper.go b/x/upgrade/internal/keeper/keeper.go index a93248ea5737..da550b6805df 100644 --- a/x/upgrade/internal/keeper/keeper.go +++ b/x/upgrade/internal/keeper/keeper.go @@ -36,7 +36,7 @@ func (k *Keeper) SetUpgradeHandler(name string, upgradeHandler types.UpgradeHand } // ScheduleUpgrade schedules an upgrade based on the specified plan. -// It there is another Plan already scheduled, it will overwrite it +// If there is another Plan already scheduled, it will overwrite it // (implicitly cancelling the current plan) func (k *Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error { err := plan.ValidateBasic() From 8dee72a1aecf69e49fc35c742ccb5d42bb4d82aa Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 24 Oct 2019 15:06:24 +0200 Subject: [PATCH 46/62] Fix scopelint errors --- x/upgrade/internal/types/plan_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x/upgrade/internal/types/plan_test.go b/x/upgrade/internal/types/plan_test.go index 470dd265f747..6720667f9832 100644 --- a/x/upgrade/internal/types/plan_test.go +++ b/x/upgrade/internal/types/plan_test.go @@ -50,6 +50,7 @@ func TestPlanString(t *testing.T) { } for name, tc := range cases { + tc := tc // copy to local variable for scopelint t.Run(name, func(t *testing.T) { s := tc.p.String() require.Equal(t, tc.expect, s) @@ -174,6 +175,7 @@ func TestShouldExecute(t *testing.T) { } for name, tc := range cases { + tc := tc // copy to local variable for scopelint t.Run(name, func(t *testing.T) { ctx := sdk.NewContext(nil, abci.Header{Height: tc.ctxHeight, Time: tc.ctxTime}, false, log.NewNopLogger()) should := tc.p.ShouldExecute(ctx) From b3a7de79b5c9f1442bec58b5202f0c81c0bc605d Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 24 Oct 2019 16:51:37 +0200 Subject: [PATCH 47/62] More test coverage on abci interface - handler and module --- x/upgrade/abci_test.go | 55 ++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/x/upgrade/abci_test.go b/x/upgrade/abci_test.go index d8124471cd81..b7e64d202bef 100644 --- a/x/upgrade/abci_test.go +++ b/x/upgrade/abci_test.go @@ -7,6 +7,8 @@ import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/gov" "github.com/stretchr/testify/suite" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" @@ -15,9 +17,12 @@ import ( type TestSuite struct { suite.Suite - keeper *Keeper - ctx sdk.Context - cms store.CommitMultiStore + keeper *Keeper + querier sdk.Querier + handler gov.Handler + module module.AppModule + ctx sdk.Context + cms store.CommitMultiStore } func (s *TestSuite) SetupTest() { @@ -27,38 +32,41 @@ func (s *TestSuite) SetupTest() { cdc := codec.New() RegisterCodec(cdc) s.keeper = NewKeeper(key, cdc) + s.handler = NewSoftwareUpgradeProposalHandler(*s.keeper) + s.querier = NewQuerier(s.keeper) + s.module = NewAppModule(s.keeper) s.cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db) _ = s.cms.LoadLatestVersion() s.ctx = sdk.NewContext(s.cms, abci.Header{Height: 10, Time: time.Now()}, false, log.NewNopLogger()) } func (s *TestSuite) TestRequireName() { - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{}) + err := s.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{}}) s.Require().NotNil(err) s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) } func (s *TestSuite) TestRequireFutureTime() { - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: s.ctx.BlockHeader().Time}) + err := s.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "test", Time: s.ctx.BlockHeader().Time}}) s.Require().NotNil(err) s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) } func (s *TestSuite) TestRequireFutureBlock() { - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight()}) + err := s.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "test", Height: s.ctx.BlockHeight()}}) s.Require().NotNil(err) s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) } func (s *TestSuite) TestCantSetBothTimeAndHeight() { - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now(), Height: s.ctx.BlockHeight() + 1}) + err := s.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "test", Time: time.Now(), Height: s.ctx.BlockHeight() + 1}}) s.Require().NotNil(err) s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) } func (s *TestSuite) TestDoTimeUpgrade() { s.T().Log("Verify can schedule an upgrade") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) + err := s.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "test", Time: time.Now()}}) s.Require().Nil(err) s.VerifyDoUpgrade() @@ -66,7 +74,7 @@ func (s *TestSuite) TestDoTimeUpgrade() { func (s *TestSuite) TestDoHeightUpgrade() { s.T().Log("Verify can schedule an upgrade") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}) + err := s.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}}) s.Require().Nil(err) s.VerifyDoUpgrade() @@ -74,9 +82,9 @@ func (s *TestSuite) TestDoHeightUpgrade() { func (s *TestSuite) TestCanOverwriteScheduleUpgrade() { s.T().Log("Can overwrite plan") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "bad_test", Height: s.ctx.BlockHeight() + 10}) + err := s.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "bad_test", Height: s.ctx.BlockHeight() + 10}}) s.Require().Nil(err) - err = s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}) + err = s.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}}) s.Require().Nil(err) s.VerifyDoUpgrade() @@ -87,13 +95,13 @@ func (s *TestSuite) VerifyDoUpgrade() { newCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 1, Time: time.Now()}, false, log.NewNopLogger()) req := abci.RequestBeginBlock{Header: newCtx.BlockHeader()} s.Require().Panics(func() { - BeginBlock(s.keeper, newCtx, req) + s.module.BeginBlock(newCtx, req) }) s.T().Log("Verify that the upgrade can be successfully applied with a handler") s.keeper.SetUpgradeHandler("test", func(ctx sdk.Context, plan Plan) {}) s.Require().NotPanics(func() { - BeginBlock(s.keeper, newCtx, req) + s.module.BeginBlock(newCtx, req) }) s.VerifyCleared(newCtx) @@ -107,15 +115,15 @@ func (s *TestSuite) TestHaltIfTooNew() { newCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 1, Time: time.Now()}, false, log.NewNopLogger()) req := abci.RequestBeginBlock{Header: newCtx.BlockHeader()} s.Require().NotPanics(func() { - BeginBlock(s.keeper, newCtx, req) + s.module.BeginBlock(newCtx, req) }) s.Require().Equal(0, called) s.T().Log("Verify we panic if we have a registered handler ahead of time") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "future", Height: s.ctx.BlockHeight() + 3}) + err := s.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "future", Height: s.ctx.BlockHeight() + 3}}) s.Require().NoError(err) s.Require().Panics(func() { - BeginBlock(s.keeper, newCtx, req) + s.module.BeginBlock(newCtx, req) }) s.Require().Equal(0, called) @@ -124,7 +132,7 @@ func (s *TestSuite) TestHaltIfTooNew() { futCtx := sdk.NewContext(s.cms, abci.Header{Height: s.ctx.BlockHeight() + 3, Time: time.Now()}, false, log.NewNopLogger()) req = abci.RequestBeginBlock{Header: futCtx.BlockHeader()} s.Require().NotPanics(func() { - BeginBlock(s.keeper, futCtx, req) + s.module.BeginBlock(futCtx, req) }) s.Require().Equal(1, called) @@ -133,16 +141,17 @@ func (s *TestSuite) TestHaltIfTooNew() { func (s *TestSuite) VerifyCleared(newCtx sdk.Context) { s.T().Log("Verify that the upgrade plan has been cleared") - _, havePlan := s.keeper.GetUpgradePlan(newCtx) - s.Require().False(havePlan) + bz, err := s.querier(newCtx, []string{QueryCurrent}, abci.RequestQuery{}) + s.Require().NoError(err) + s.Require().Nil(bz) } func (s *TestSuite) TestCanClear() { s.T().Log("Verify upgrade is scheduled") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) + err := s.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "test", Time: time.Now()}}) s.Require().Nil(err) - s.keeper.ClearUpgradePlan(s.ctx) + s.handler(s.ctx, CancelSoftwareUpgradeProposal{Title: "cancel"}) s.VerifyCleared(s.ctx) } @@ -150,7 +159,7 @@ func (s *TestSuite) TestCanClear() { func (s *TestSuite) TestCantApplySameUpgradeTwice() { s.TestDoTimeUpgrade() s.T().Log("Verify an upgrade named \"test\" can't be scheduled twice") - err := s.keeper.ScheduleUpgrade(s.ctx, Plan{Name: "test", Time: time.Now()}) + err := s.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "test", Time: time.Now()}}) s.Require().NotNil(err) s.Require().Equal(sdk.CodeUnknownRequest, err.Code()) } @@ -159,7 +168,7 @@ func (s *TestSuite) TestNoSpuriousUpgrades() { s.T().Log("Verify that no upgrade panic is triggered in the BeginBlocker when we haven't scheduled an upgrade") req := abci.RequestBeginBlock{Header: s.ctx.BlockHeader()} s.Require().NotPanics(func() { - BeginBlock(s.keeper, s.ctx, req) + s.module.BeginBlock(s.ctx, req) }) } From 9652ec5a504c12019ce62e8b90d7fd8676d4610f Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 24 Oct 2019 16:56:44 +0200 Subject: [PATCH 48/62] Becoming convinced scopelinter hates table tests --- x/upgrade/internal/types/plan_test.go | 1 + x/upgrade/internal/types/proposal_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/x/upgrade/internal/types/plan_test.go b/x/upgrade/internal/types/plan_test.go index 6720667f9832..d8e7e18d7fa3 100644 --- a/x/upgrade/internal/types/plan_test.go +++ b/x/upgrade/internal/types/plan_test.go @@ -98,6 +98,7 @@ func TestPlanValid(t *testing.T) { } for name, tc := range cases { + tc := tc // copy to local variable for scopelint t.Run(name, func(t *testing.T) { err := tc.p.ValidateBasic() if tc.valid { diff --git a/x/upgrade/internal/types/proposal_test.go b/x/upgrade/internal/types/proposal_test.go index b44357e13f4e..02d204a16722 100644 --- a/x/upgrade/internal/types/proposal_test.go +++ b/x/upgrade/internal/types/proposal_test.go @@ -47,6 +47,7 @@ func TestContentAccessors(t *testing.T) { RegisterCodec(cdc) for name, tc := range cases { + tc := tc // copy to local variable for scopelint t.Run(name, func(t *testing.T) { assert.Equal(t, tc.title, tc.p.GetTitle()) assert.Equal(t, tc.desc, tc.p.GetDescription()) From f039c591872ea490650df172506ca7f5f705feba Mon Sep 17 00:00:00 2001 From: Sahith Reddy Narahari Date: Thu, 31 Oct 2019 12:43:52 +0530 Subject: [PATCH 49/62] added proposal handler --- x/upgrade/client/module_client.go | 45 ---------------------------- x/upgrade/client/proposal_handler.go | 9 ++++++ x/upgrade/client/rest/tx.go | 8 +++++ 3 files changed, 17 insertions(+), 45 deletions(-) delete mode 100644 x/upgrade/client/module_client.go create mode 100644 x/upgrade/client/proposal_handler.go diff --git a/x/upgrade/client/module_client.go b/x/upgrade/client/module_client.go deleted file mode 100644 index 1b96d3d0185a..000000000000 --- a/x/upgrade/client/module_client.go +++ /dev/null @@ -1,45 +0,0 @@ -package client - -import ( - "github.com/spf13/cobra" - amino "github.com/tendermint/go-amino" - - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/x/upgrade/client/cli" -) - -// ModuleClient exports all client functionality from this module -type ModuleClient struct { - storeKey string - cdc *amino.Codec -} - -// NewModuleClient returns an upgrade ModuleClient -func NewModuleClient(storeKey string, cdc *amino.Codec) ModuleClient { - return ModuleClient{storeKey, cdc} -} - -// GetQueryCmd returns the cli query commands for this module -func (mc ModuleClient) GetQueryCmd() *cobra.Command { - queryCmd := &cobra.Command{ - Use: "upgrade", - Short: "Querying commands for the upgrade module", - } - queryCmd.AddCommand(client.GetCommands( - cli.GetPlanCmd(mc.storeKey, mc.cdc), - cli.GetAppliedHeightCmd(mc.storeKey, mc.cdc), - )...) - - return queryCmd -} - -// GetTxCmd returns the transaction commands for this module -func (mc ModuleClient) GetTxCmd() *cobra.Command { - txCmd := &cobra.Command{ - Use: "upgrade", - Short: "Upgrade transaction subcommands", - } - txCmd.AddCommand(client.PostCommands()...) - - return txCmd -} diff --git a/x/upgrade/client/proposal_handler.go b/x/upgrade/client/proposal_handler.go new file mode 100644 index 000000000000..314c8ac587a5 --- /dev/null +++ b/x/upgrade/client/proposal_handler.go @@ -0,0 +1,9 @@ +package client + +import ( + govclient "github.com/cosmos/cosmos-sdk/x/gov/client" + "github.com/cosmos/cosmos-sdk/x/upgrade/client/cli" + "github.com/cosmos/cosmos-sdk/x/upgrade/client/rest" +) + +var ProposalHandler = govclient.NewProposalHandler(cli.GetCmdSubmitUpgradeProposal, rest.ProposalRESTHandler) diff --git a/x/upgrade/client/rest/tx.go b/x/upgrade/client/rest/tx.go index 3afa9a95b4af..02d1f5cd5528 100644 --- a/x/upgrade/client/rest/tx.go +++ b/x/upgrade/client/rest/tx.go @@ -6,6 +6,8 @@ import ( "github.com/gorilla/mux" + govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" + "github.com/cosmos/cosmos-sdk/client/context" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/rest" @@ -39,6 +41,12 @@ type CancelRequest struct { Deposit sdk.Coins `json:"deposit" yaml:"deposit"` } +func ProposalRESTHandler(cliCtx context.CLIContext) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "param_change", + Handler: postPlanHandler(cliCtx), + } +} func postPlanHandler(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req PlanRequest From 85ad699c4e2bf4539de632f8f900c763e305fd73 Mon Sep 17 00:00:00 2001 From: Sahith Reddy Narahari Date: Thu, 31 Oct 2019 13:53:32 +0530 Subject: [PATCH 50/62] renamed beginblock --- x/upgrade/abci.go | 2 +- x/upgrade/module.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x/upgrade/abci.go b/x/upgrade/abci.go index 7cff1ed15ddf..b5aab83336cd 100644 --- a/x/upgrade/abci.go +++ b/x/upgrade/abci.go @@ -13,7 +13,7 @@ import ( // // The prupose is to ensure the binary is switch EXACTLY at the desired block, and to allow // a migration to be executed if needed upon this switch (migration defined in the new binary) -func BeginBlock(k *Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { +func BeginBlocker(k *Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { plan, found := k.GetUpgradePlan(ctx) if !found { return diff --git a/x/upgrade/module.go b/x/upgrade/module.go index 455cefbc1346..6c83cf42653e 100644 --- a/x/upgrade/module.go +++ b/x/upgrade/module.go @@ -125,7 +125,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { // // CONTRACT: this is registered in BeginBlocker *before* all other modules' BeginBlock functions func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { - BeginBlock(am.keeper, ctx, req) + BeginBlocker(am.keeper, ctx, req) } // EndBlock does nothing From 86184fa1e8dea689ca7bf2a05845e7002c67c813 Mon Sep 17 00:00:00 2001 From: Sahith Reddy Narahari Date: Thu, 31 Oct 2019 16:09:46 +0530 Subject: [PATCH 51/62] changed height flag name --- x/upgrade/client/cli/tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go index e49dbf5eb2c4..e5586a940e77 100644 --- a/x/upgrade/client/cli/tx.go +++ b/x/upgrade/client/cli/tx.go @@ -21,7 +21,7 @@ const ( // TimeFormat specifies ISO UTC format for submitting the time for a new upgrade proposal TimeFormat = "2006-01-02T15:04:05Z" - FlagUpgradeHeight = "height" + FlagUpgradeHeight = "upgrade-height" FlagUpgradeTime = "time" FlagUpgradeInfo = "info" ) From c1233ffa2290c450598d417ce3c6dd5f9999c4f0 Mon Sep 17 00:00:00 2001 From: Sahith Reddy Narahari Date: Thu, 31 Oct 2019 17:21:32 +0530 Subject: [PATCH 52/62] added tstore key --- x/upgrade/alias.go | 1 + x/upgrade/internal/types/keys.go | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/x/upgrade/alias.go b/x/upgrade/alias.go index 89da07e7fc20..4c31a0029bc5 100644 --- a/x/upgrade/alias.go +++ b/x/upgrade/alias.go @@ -14,6 +14,7 @@ const ( ModuleName = types.ModuleName RouterKey = types.RouterKey StoreKey = types.StoreKey + TStoreKey = types.TStoreKey QuerierKey = types.QuerierKey PlanByte = types.PlanByte DoneByte = types.DoneByte diff --git a/x/upgrade/internal/types/keys.go b/x/upgrade/internal/types/keys.go index 75f64525dbc0..1ea3e6c64005 100644 --- a/x/upgrade/internal/types/keys.go +++ b/x/upgrade/internal/types/keys.go @@ -10,8 +10,12 @@ const ( // StoreKey is the prefix under which we store this module's data StoreKey = ModuleName + TStoreKey = "transient_" + ModuleName + // QuerierKey is used to handle abci_query requests QuerierKey = ModuleName + + ) const ( From ca1d0fb7b181c92e4f81fdfe42a02a8cfba4e17e Mon Sep 17 00:00:00 2001 From: "kaustubh.kapatral" Date: Thu, 31 Oct 2019 20:51:13 +0530 Subject: [PATCH 53/62] removed pointers --- x/upgrade/abci.go | 2 +- x/upgrade/abci_test.go | 2 +- x/upgrade/internal/keeper/keeper.go | 22 +++++++++++----------- x/upgrade/internal/keeper/querier.go | 6 +++--- x/upgrade/module.go | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/x/upgrade/abci.go b/x/upgrade/abci.go index b5aab83336cd..6de30b330218 100644 --- a/x/upgrade/abci.go +++ b/x/upgrade/abci.go @@ -13,7 +13,7 @@ import ( // // The prupose is to ensure the binary is switch EXACTLY at the desired block, and to allow // a migration to be executed if needed upon this switch (migration defined in the new binary) -func BeginBlocker(k *Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { +func BeginBlocker(k Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { plan, found := k.GetUpgradePlan(ctx) if !found { return diff --git a/x/upgrade/abci_test.go b/x/upgrade/abci_test.go index b7e64d202bef..41c5dfcf017f 100644 --- a/x/upgrade/abci_test.go +++ b/x/upgrade/abci_test.go @@ -17,7 +17,7 @@ import ( type TestSuite struct { suite.Suite - keeper *Keeper + keeper Keeper querier sdk.Querier handler gov.Handler module module.AppModule diff --git a/x/upgrade/internal/keeper/keeper.go b/x/upgrade/internal/keeper/keeper.go index da550b6805df..40cf52080f9c 100644 --- a/x/upgrade/internal/keeper/keeper.go +++ b/x/upgrade/internal/keeper/keeper.go @@ -17,11 +17,11 @@ type Keeper struct { upgradeHandlers map[string]types.UpgradeHandler } -var _ exported.Keeper = (*Keeper)(nil) +var _ exported.Keeper = (Keeper)(nil) // NewKeeper constructs an upgrade Keeper -func NewKeeper(storeKey sdk.StoreKey, cdc *codec.Codec) *Keeper { - return &Keeper{ +func NewKeeper(storeKey sdk.StoreKey, cdc *codec.Codec) Keeper { + return Keeper{ storeKey: storeKey, cdc: cdc, upgradeHandlers: map[string]types.UpgradeHandler{}, @@ -31,14 +31,14 @@ func NewKeeper(storeKey sdk.StoreKey, cdc *codec.Codec) *Keeper { // SetUpgradeHandler sets an UpgradeHandler for the upgrade specified by name. This handler will be called when the upgrade // with this name is applied. In order for an upgrade with the given name to proceed, a handler for this upgrade // must be set even if it is a no-op function. -func (k *Keeper) SetUpgradeHandler(name string, upgradeHandler types.UpgradeHandler) { +func (k Keeper) SetUpgradeHandler(name string, upgradeHandler types.UpgradeHandler) { k.upgradeHandlers[name] = upgradeHandler } // ScheduleUpgrade schedules an upgrade based on the specified plan. // If there is another Plan already scheduled, it will overwrite it // (implicitly cancelling the current plan) -func (k *Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error { +func (k Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error { err := plan.ValidateBasic() if err != nil { return err @@ -61,7 +61,7 @@ func (k *Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error { return nil } -func (k *Keeper) getDoneHeight(ctx sdk.Context, name string) int64 { +func (k Keeper) getDoneHeight(ctx sdk.Context, name string) int64 { store := ctx.KVStore(k.storeKey) bz := store.Get(types.DoneHeightKey(name)) if len(bz) == 0 { @@ -71,7 +71,7 @@ func (k *Keeper) getDoneHeight(ctx sdk.Context, name string) int64 { } // ClearUpgradePlan clears any schedule upgrade -func (k *Keeper) ClearUpgradePlan(ctx sdk.Context) { +func (k Keeper) ClearUpgradePlan(ctx sdk.Context) { store := ctx.KVStore(k.storeKey) store.Delete(types.PlanKey()) } @@ -83,7 +83,7 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger { // GetUpgradePlan returns the currently scheduled Plan if any, setting havePlan to true if there is a scheduled // upgrade or false if there is none -func (k *Keeper) GetUpgradePlan(ctx sdk.Context) (plan types.Plan, havePlan bool) { +func (k Keeper) GetUpgradePlan(ctx sdk.Context) (plan types.Plan, havePlan bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(types.PlanKey()) if bz == nil { @@ -94,7 +94,7 @@ func (k *Keeper) GetUpgradePlan(ctx sdk.Context) (plan types.Plan, havePlan bool } // setDone marks this upgrade name as being done so the name can't be reused accidentally -func (k *Keeper) setDone(ctx sdk.Context, name string) { +func (k Keeper) setDone(ctx sdk.Context, name string) { store := ctx.KVStore(k.storeKey) bz := make([]byte, 8) binary.BigEndian.PutUint64(bz, uint64(ctx.BlockHeight())) @@ -102,13 +102,13 @@ func (k *Keeper) setDone(ctx sdk.Context, name string) { } // HasHandler returns true iff there is a handler registered for this name -func (k *Keeper) HasHandler(name string) bool { +func (k Keeper) HasHandler(name string) bool { _, ok := k.upgradeHandlers[name] return ok } // ApplyUpgrade will execute the handler associated with the Plan and mark the plan as done. -func (k *Keeper) ApplyUpgrade(ctx sdk.Context, plan types.Plan) { +func (k Keeper) ApplyUpgrade(ctx sdk.Context, plan types.Plan) { handler := k.upgradeHandlers[plan.Name] if handler == nil { panic("ApplyUpgrade should never be called without first checking HasHandler") diff --git a/x/upgrade/internal/keeper/querier.go b/x/upgrade/internal/keeper/querier.go index a167655c5fc2..291f7bb9d049 100644 --- a/x/upgrade/internal/keeper/querier.go +++ b/x/upgrade/internal/keeper/querier.go @@ -10,7 +10,7 @@ import ( ) // NewQuerier creates a querier for upgrade cli and REST endpoints -func NewQuerier(k *Keeper) sdk.Querier { +func NewQuerier(k Keeper) sdk.Querier { return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) { switch path[0] { @@ -26,7 +26,7 @@ func NewQuerier(k *Keeper) sdk.Querier { } } -func queryCurrent(ctx sdk.Context, req abci.RequestQuery, k *Keeper) ([]byte, sdk.Error) { +func queryCurrent(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, sdk.Error) { plan, has := k.GetUpgradePlan(ctx) if !has { // empty data - client can respond Not Found @@ -39,7 +39,7 @@ func queryCurrent(ctx sdk.Context, req abci.RequestQuery, k *Keeper) ([]byte, sd return res, nil } -func queryApplied(ctx sdk.Context, req abci.RequestQuery, k *Keeper) ([]byte, sdk.Error) { +func queryApplied(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, sdk.Error) { var params types.QueryAppliedParams err := k.cdc.UnmarshalJSON(req.Data, ¶ms) diff --git a/x/upgrade/module.go b/x/upgrade/module.go index 6c83cf42653e..a365cd1a36c2 100644 --- a/x/upgrade/module.go +++ b/x/upgrade/module.go @@ -73,11 +73,11 @@ func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { // AppModule implements the sdk.AppModule interface type AppModule struct { AppModuleBasic - keeper *Keeper + keeper Keeper } // NewAppModule creates a new AppModule object -func NewAppModule(keeper *Keeper) AppModule { +func NewAppModule(keeper Keeper) AppModule { return AppModule{ AppModuleBasic: AppModuleBasic{}, keeper: keeper, From 05adda85758e95def4072f850b6e1efb3bb1c0b3 Mon Sep 17 00:00:00 2001 From: "kaustubh.kapatral" Date: Thu, 31 Oct 2019 20:54:52 +0530 Subject: [PATCH 54/62] modified keeper --- x/upgrade/abci.go | 4 ++-- x/upgrade/alias.go | 1 - x/upgrade/client/cli/tx.go | 1 - x/upgrade/internal/keeper/keeper.go | 2 -- x/upgrade/internal/types/keys.go | 2 -- 5 files changed, 2 insertions(+), 8 deletions(-) diff --git a/x/upgrade/abci.go b/x/upgrade/abci.go index 6de30b330218..c0c7a8ae10fd 100644 --- a/x/upgrade/abci.go +++ b/x/upgrade/abci.go @@ -3,8 +3,9 @@ package upgrade import ( "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" abci "github.com/tendermint/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" ) // BeginBlock will check if there is a scheduled plan and if it is ready to be executed. @@ -18,7 +19,6 @@ func BeginBlocker(k Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { if !found { return } - if plan.ShouldExecute(ctx) { if !k.HasHandler(plan.Name) { upgradeMsg := fmt.Sprintf("UPGRADE \"%s\" NEEDED at %s: %s", plan.Name, plan.DueAt(), plan.Info) diff --git a/x/upgrade/alias.go b/x/upgrade/alias.go index 4c31a0029bc5..89da07e7fc20 100644 --- a/x/upgrade/alias.go +++ b/x/upgrade/alias.go @@ -14,7 +14,6 @@ const ( ModuleName = types.ModuleName RouterKey = types.RouterKey StoreKey = types.StoreKey - TStoreKey = types.TStoreKey QuerierKey = types.QuerierKey PlanByte = types.PlanByte DoneByte = types.DoneByte diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go index e5586a940e77..b4c34e23129d 100644 --- a/x/upgrade/client/cli/tx.go +++ b/x/upgrade/client/cli/tx.go @@ -102,7 +102,6 @@ func GetCmdSubmitUpgradeProposal(cdc *codec.Codec) *cobra.Command { if err := msg.ValidateBasic(); err != nil { return err } - return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) }, } diff --git a/x/upgrade/internal/keeper/keeper.go b/x/upgrade/internal/keeper/keeper.go index 40cf52080f9c..03ac46e4c4be 100644 --- a/x/upgrade/internal/keeper/keeper.go +++ b/x/upgrade/internal/keeper/keeper.go @@ -6,7 +6,6 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/upgrade/exported" "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" "github.com/tendermint/tendermint/libs/log" ) @@ -17,7 +16,6 @@ type Keeper struct { upgradeHandlers map[string]types.UpgradeHandler } -var _ exported.Keeper = (Keeper)(nil) // NewKeeper constructs an upgrade Keeper func NewKeeper(storeKey sdk.StoreKey, cdc *codec.Codec) Keeper { diff --git a/x/upgrade/internal/types/keys.go b/x/upgrade/internal/types/keys.go index 1ea3e6c64005..104605bb05fb 100644 --- a/x/upgrade/internal/types/keys.go +++ b/x/upgrade/internal/types/keys.go @@ -10,8 +10,6 @@ const ( // StoreKey is the prefix under which we store this module's data StoreKey = ModuleName - TStoreKey = "transient_" + ModuleName - // QuerierKey is used to handle abci_query requests QuerierKey = ModuleName From 9a016d3082fb454901d747e4905a49cbd751d95c Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 1 Nov 2019 11:28:16 +0100 Subject: [PATCH 55/62] Update test --- x/upgrade/abci_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/upgrade/abci_test.go b/x/upgrade/abci_test.go index 41c5dfcf017f..1c599e612645 100644 --- a/x/upgrade/abci_test.go +++ b/x/upgrade/abci_test.go @@ -32,7 +32,7 @@ func (s *TestSuite) SetupTest() { cdc := codec.New() RegisterCodec(cdc) s.keeper = NewKeeper(key, cdc) - s.handler = NewSoftwareUpgradeProposalHandler(*s.keeper) + s.handler = NewSoftwareUpgradeProposalHandler(s.keeper) s.querier = NewQuerier(s.keeper) s.module = NewAppModule(s.keeper) s.cms.MountStoreWithDB(key, sdk.StoreTypeIAVL, db) From 7c0bba7996811edbbbb1539584318fb4c470edf1 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 5 Nov 2019 12:15:37 +0100 Subject: [PATCH 56/62] Addressed PR comments --- x/gov/handler.go | 2 +- x/upgrade/doc.go | 46 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/x/gov/handler.go b/x/gov/handler.go index 51065fc5b24e..47efe4473cd4 100644 --- a/x/gov/handler.go +++ b/x/gov/handler.go @@ -50,7 +50,7 @@ func handleMsgSubmitProposal(ctx sdk.Context, keeper Keeper, msg MsgSubmitPropos submitEvent := sdk.NewEvent(types.EventTypeSubmitProposal, sdk.NewAttribute(types.AttributeKeyProposalType, msg.Content.ProposalType())) if votingStarted { - submitEvent.AppendAttributes( + submitEvent = submitEvent.AppendAttributes( sdk.NewAttribute(types.AttributeKeyVotingPeriodStart, fmt.Sprintf("%d", proposal.ProposalID)), ) } diff --git a/x/upgrade/doc.go b/x/upgrade/doc.go index 33beba3aecc6..160580d5c79c 100644 --- a/x/upgrade/doc.go +++ b/x/upgrade/doc.go @@ -9,11 +9,20 @@ inconsistencies which are hard to recover from. General Workflow -Let's assume we are running v0.34.0 of our software in our testnet and want to upgrade to v0.36.0. -How would this look in practice? First of all, we want to finalize the v0.36.0 release candidate -and there install a specially named upgrade handler (eg. "testnet-v2" or even "v0.36.0"). Once -this code is public, we can have a governance vote to approve this upgrade at some future block time -or block height (e.g. 200000). This is known as an upgrade.Plan. The v0.34.0 code will not know of this +Let's assume we are running v0.38.0 of our software in our testnet and want to upgrade to v0.40.0. +How would this look in practice? First of all, we want to finalize the v0.40.0 release candidate +and there install a specially named upgrade handler (eg. "testnet-v2" or even "v0.40.0"). An upgrade +handler should be defined in a new version of the software to define what migrations +to run to migrate from the older version of the software. Naturally, this is app-specific rather +than module specific, and must be defined in `app.go`, even if it imports logic from various +modules to perform the actions. You can register them with `upgradeKeeper.SetUpgradeHandler` +during the app initialization (before starting the abci server), and they serve not only to +perform a migration, but also to identify if this is the old or new version (eg. presence of +a handler registered for the named upgrade). + +Once the release candidate along with an appropriate upgrade handler is frozen, +we can have a governance vote to approve this upgrade at some future block time +or block height (e.g. 200000). This is known as an upgrade.Plan. The v0.38.0 code will not know of this handler, but will continue to run until block 200000, when the plan kicks in at BeginBlock. It will check for existence of the handler, and finding it missing, know that it is running the obsolete software, and gracefully exit. @@ -24,7 +33,7 @@ or you can make use of an external watcher daemon to possibly download and then also potentially doing a backup. An example of such a daemon is https://github.com/regen-network/cosmosd/ described below under "Automation". -When the binary restarts with the upgraded version (here v0.36.0), it will detect we have registered the +When the binary restarts with the upgraded version (here v0.40.0), it will detect we have registered the "testnet-v2" upgrade handler in the code, and realize it is the new version. It then will run the upgrade handler and *migrate the database in-place*. Once finished, it marks the upgrade as done, and continues processing the rest of the block as normal. Once 2/3 of the voting power has upgraded, the blockchain will immediately @@ -55,7 +64,9 @@ Here is an example handler for an upgrade named "my-fancy-upgrade": }) This upgrade handler performs the dual function of alerting the upgrade module that the named upgrade has been applied, -as well as providing the opportunity for the upgraded software to perform any necessary state migrations. +as well as providing the opportunity for the upgraded software to perform any necessary state migrations. Both the halt +(with the old binary) and applying the migration (with the new binary) are enforced in the state machine. Actually +switching the binaries is an ops task and not handled inside the sdk / abci app. Halt Behavior @@ -77,5 +88,26 @@ specified here https://github.com/regen-network/cosmosd/blob/master/README.md#au This will allow a properly configured cosmsod daemon to auto-download new binaries and auto-upgrade. As noted there, this is intended more for full nodes than validators. +Cancelling Upgrades + +There are two ways to cancel a planned upgrade - with on-chain governance or off-chain social consensus. +For the first one, there is a CancelSoftwareUpgrade proposal type, which can be voted on and will +remove the scheduled upgrade plan. Of course this requires that the upgrade was known to be a bad idea +well before the upgrade itself, to allow time for a vote. If you want to allow such a possibility, you +should set the upgrade height to be 2 * (votingperiod + depositperiod) + (safety delta) from the beginning of +the first upgrade proposal. Safety delta is the time available from the success of an upgrade proposal +and the realization it was a bad idea (due to external testing). You can also start a CancelSoftwareUpgrade +proposal while the original SoftwareUpgrade proposal is still being voted upon, as long as the voting +period ends after the SoftwareUpgrade proposal. + +However, let's assume that we don't realize the upgrade has a bug until shortly before it will occur +(or while we try it out - hitting some panic in the migration). It would seem the blockchain is stuck, +but we need to allow an escape for social consensus to overrule the planned upgrade. To do so, we are +adding a --unsafe-skip-upgrade flag to the start command, which will cause the node to mark the upgrade +as done upon hiting the planned upgrade height, without halting and without actually performing a migration. +If over two-thirds run their nodes with this flag on the old binary, it will allow the chain to continue through +the upgrade with a manual override. (This must be well-documented for anyone syncing from genesis later on). + +(Skip-upgrade flag is in a WIP PR - will update this text when merged ^^) */ package upgrade From d8a9c42fcf46e72de014b9481c97c39b122ed13d Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 5 Nov 2019 12:26:03 +0100 Subject: [PATCH 57/62] Return header for query applied-height --- x/upgrade/client/cli/query.go | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go index 77b6f146cdd8..04b8b85a697a 100644 --- a/x/upgrade/client/cli/query.go +++ b/x/upgrade/client/cli/query.go @@ -40,17 +40,13 @@ func GetPlanCmd(storeName string, cdc *codec.Codec) *cobra.Command { } } -type appliedHeight struct { - Applied int64 `json:"applied" yaml:"applied"` -} - // GetAppliedHeightCmd returns the height at which a completed upgrade was applied func GetAppliedHeightCmd(storeName string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "applied-height [upgrade-name]", - Short: "height at which a completed upgrade was applied", - Long: "If upgrade-name was previously executed on the chain, this returns the height at which it was applied.\n" + - "This helps a client determine which binary was valid over a given range of blocks.", + Use: "applied [upgrade-name]", + Short: "block header for height at which a completed upgrade was applied", + Long: "If upgrade-name was previously executed on the chain, this returns the header for the block at which it was applied.\n" + + "This helps a client determine which binary was valid over a given range of blocks, as well as more context to understand past migrations.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -73,9 +69,28 @@ func GetAppliedHeightCmd(storeName string, cdc *codec.Codec) *cobra.Command { if len(res) != 8 { return fmt.Errorf("unknown format for applied-upgrade") } - applied := int64(binary.BigEndian.Uint64(res)) - return cliCtx.PrintOutput(appliedHeight{applied}) + + // we got the height, now let's return the headers + node, err := cliCtx.GetNode() + if err != nil { + return err + } + headers, err := node.BlockchainInfo(applied, applied) + if err != nil { + return err + } + if len(headers.BlockMetas) == 0 { + return fmt.Errorf("No headers returned for height %d", applied) + } + + // always output json as Header is unreable in toml ([]byte is a long list of numbers) + bz, err = cdc.MarshalJSONIndent(headers.BlockMetas[0], "", " ") + if err != nil { + return err + } + fmt.Println(string(bz)) + return nil }, } } From 9c681115897e550a567ed3b94be27cdfd9b575b7 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 5 Nov 2019 12:27:27 +0100 Subject: [PATCH 58/62] Fix query plan json/binary parsing --- x/upgrade/client/cli/query.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go index 04b8b85a697a..79d9914ef11b 100644 --- a/x/upgrade/client/cli/query.go +++ b/x/upgrade/client/cli/query.go @@ -31,7 +31,7 @@ func GetPlanCmd(storeName string, cdc *codec.Codec) *cobra.Command { } var plan upgrade.Plan - err = cdc.UnmarshalBinaryBare(res, &plan) + err = cdc.UnmarshalJSON(res, &plan) if err != nil { return err } From e874d64fb88daf3ca95b55a279ba07d9c85bb7d9 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 5 Nov 2019 12:30:10 +0100 Subject: [PATCH 59/62] Fixed linter issues --- x/upgrade/internal/keeper/keeper.go | 1 - x/upgrade/internal/types/keys.go | 2 -- 2 files changed, 3 deletions(-) diff --git a/x/upgrade/internal/keeper/keeper.go b/x/upgrade/internal/keeper/keeper.go index 03ac46e4c4be..869b3850b677 100644 --- a/x/upgrade/internal/keeper/keeper.go +++ b/x/upgrade/internal/keeper/keeper.go @@ -16,7 +16,6 @@ type Keeper struct { upgradeHandlers map[string]types.UpgradeHandler } - // NewKeeper constructs an upgrade Keeper func NewKeeper(storeKey sdk.StoreKey, cdc *codec.Codec) Keeper { return Keeper{ diff --git a/x/upgrade/internal/types/keys.go b/x/upgrade/internal/types/keys.go index 104605bb05fb..75f64525dbc0 100644 --- a/x/upgrade/internal/types/keys.go +++ b/x/upgrade/internal/types/keys.go @@ -12,8 +12,6 @@ const ( // QuerierKey is used to handle abci_query requests QuerierKey = ModuleName - - ) const ( From afc0e76dcaaa557062c245330379eee0e5ca6c11 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 5 Nov 2019 12:38:22 +0100 Subject: [PATCH 60/62] Use prefix store --- x/upgrade/alias.go | 5 ++--- x/upgrade/internal/keeper/keeper.go | 13 +++++++------ x/upgrade/internal/types/keys.go | 5 ----- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/x/upgrade/alias.go b/x/upgrade/alias.go index 89da07e7fc20..b7324f2c3e41 100644 --- a/x/upgrade/alias.go +++ b/x/upgrade/alias.go @@ -1,8 +1,8 @@ // nolint // autogenerated code using github.com/rigelrozanski/multitool // aliases generated for the following subdirectories: -// ALIASGEN: github.com/cosmos/cosmos-sdk/x/upgrade/internal/types/ -// ALIASGEN: github.com/cosmos/cosmos-sdk/x/upgrade/internal/keeper/ +// ALIASGEN: github.com/cosmos/cosmos-sdk/x/upgrade/internal/types +// ALIASGEN: github.com/cosmos/cosmos-sdk/x/upgrade/internal/keeper package upgrade import ( @@ -28,7 +28,6 @@ var ( // functions aliases RegisterCodec = types.RegisterCodec PlanKey = types.PlanKey - DoneHeightKey = types.DoneHeightKey NewSoftwareUpgradeProposal = types.NewSoftwareUpgradeProposal NewCancelSoftwareUpgradeProposal = types.NewCancelSoftwareUpgradeProposal NewQueryAppliedParams = types.NewQueryAppliedParams diff --git a/x/upgrade/internal/keeper/keeper.go b/x/upgrade/internal/keeper/keeper.go index 869b3850b677..11c50a0e5abd 100644 --- a/x/upgrade/internal/keeper/keeper.go +++ b/x/upgrade/internal/keeper/keeper.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" "github.com/tendermint/tendermint/libs/log" @@ -48,19 +49,19 @@ func (k Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) sdk.Error { return sdk.ErrUnknownRequest("upgrade cannot be scheduled in the past") } - store := ctx.KVStore(k.storeKey) - if store.Has(types.DoneHeightKey(plan.Name)) { + if k.getDoneHeight(ctx, plan.Name) != 0 { return sdk.ErrUnknownRequest(fmt.Sprintf("upgrade with name %s has already been completed", plan.Name)) } bz := k.cdc.MustMarshalBinaryBare(plan) + store := ctx.KVStore(k.storeKey) store.Set(types.PlanKey(), bz) return nil } func (k Keeper) getDoneHeight(ctx sdk.Context, name string) int64 { - store := ctx.KVStore(k.storeKey) - bz := store.Get(types.DoneHeightKey(name)) + store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte{types.DoneByte}) + bz := store.Get([]byte(name)) if len(bz) == 0 { return 0 } @@ -92,10 +93,10 @@ func (k Keeper) GetUpgradePlan(ctx sdk.Context) (plan types.Plan, havePlan bool) // setDone marks this upgrade name as being done so the name can't be reused accidentally func (k Keeper) setDone(ctx sdk.Context, name string) { - store := ctx.KVStore(k.storeKey) + store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte{types.DoneByte}) bz := make([]byte, 8) binary.BigEndian.PutUint64(bz, uint64(ctx.BlockHeight())) - store.Set(types.DoneHeightKey(name), bz) + store.Set([]byte(name), bz) } // HasHandler returns true iff there is a handler registered for this name diff --git a/x/upgrade/internal/types/keys.go b/x/upgrade/internal/types/keys.go index 75f64525dbc0..cda224623b2b 100644 --- a/x/upgrade/internal/types/keys.go +++ b/x/upgrade/internal/types/keys.go @@ -26,8 +26,3 @@ const ( func PlanKey() []byte { return []byte{PlanByte} } - -// DoneHeightKey returns a key that points to the height at which a past upgrade was applied -func DoneHeightKey(name string) []byte { - return append([]byte{DoneByte}, []byte(name)...) -} From 73304609a9f2dde60438d87ba5aa7f99bb20f8f6 Mon Sep 17 00:00:00 2001 From: Sahith Reddy Narahari Date: Mon, 4 Nov 2019 12:40:08 +0530 Subject: [PATCH 61/62] Renamed subroute in rest --- x/upgrade/client/rest/tx.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x/upgrade/client/rest/tx.go b/x/upgrade/client/rest/tx.go index 02d1f5cd5528..acdec064328d 100644 --- a/x/upgrade/client/rest/tx.go +++ b/x/upgrade/client/rest/tx.go @@ -43,10 +43,11 @@ type CancelRequest struct { func ProposalRESTHandler(cliCtx context.CLIContext) govrest.ProposalRESTHandler { return govrest.ProposalRESTHandler{ - SubRoute: "param_change", + SubRoute: "upgrade", Handler: postPlanHandler(cliCtx), } } + func postPlanHandler(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req PlanRequest From e0fe9be5d58e9e6cf6e984e52eb5457217b8fd30 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 5 Nov 2019 13:05:15 +0100 Subject: [PATCH 62/62] Better error message --- x/upgrade/client/cli/query.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/upgrade/client/cli/query.go b/x/upgrade/client/cli/query.go index 79d9914ef11b..bf7d2163fa44 100644 --- a/x/upgrade/client/cli/query.go +++ b/x/upgrade/client/cli/query.go @@ -81,7 +81,7 @@ func GetAppliedHeightCmd(storeName string, cdc *codec.Codec) *cobra.Command { return err } if len(headers.BlockMetas) == 0 { - return fmt.Errorf("No headers returned for height %d", applied) + return fmt.Errorf("no headers returned for height %d", applied) } // always output json as Header is unreable in toml ([]byte is a long list of numbers)