diff --git a/CHANGELOG.md b/CHANGELOG.md index 670f26a4c28e..cc3a700d4213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,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. +* (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. * (baseapp) [\#5196](https://github.com/cosmos/cosmos-sdk/pull/5196) Baseapp has a new `runTxModeReCheck` to allow applications to skip expensive and unnecessary re-checking of transactions. diff --git a/x/gov/alias.go b/x/gov/alias.go index c8942f63fc7e..0ca57b7a39fc 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 @@ -142,32 +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 - SoftwareUpgradeProposal = types.SoftwareUpgradeProposal - 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/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/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/handler.go b/x/gov/handler.go index cd116264c359..47efe4473cd4 100644 --- a/x/gov/handler.go +++ b/x/gov/handler.go @@ -48,14 +48,13 @@ func handleMsgSubmitProposal(ctx sdk.Context, keeper Keeper, msg MsgSubmitPropos ), ) + submitEvent := sdk.NewEvent(types.EventTypeSubmitProposal, 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)), - ), + submitEvent = submitEvent.AppendAttributes( + sdk.NewAttribute(types.AttributeKeyVotingPeriodStart, fmt.Sprintf("%d", proposal.ProposalID)), ) } + ctx.EventManager().EmitEvent(submitEvent) return sdk.Result{ Data: GetProposalIDBytes(proposal.ProposalID), 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..f18cddac1c50 100644 --- a/x/gov/legacy/v0_36/types.go +++ b/x/gov/legacy/v0_36/types.go @@ -18,8 +18,7 @@ const ( DefaultCodespace sdk.CodespaceType = "gov" - ProposalTypeText string = "Text" - ProposalTypeSoftwareUpgrade string = "SoftwareUpgrade" + ProposalTypeText string = "Text" 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 f2372b615da4..36479f7fa0c2 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/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/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" ) diff --git a/x/gov/types/msgs.go b/x/gov/types/msgs.go index a29f2a20aafd..b5b85ca167ea 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/gov/types/proposal.go b/x/gov/types/proposal.go index 0a1a544460c8..a02f7b54d6b9 100644 --- a/x/gov/types/proposal.go +++ b/x/gov/types/proposal.go @@ -201,8 +201,7 @@ func (status ProposalStatus) Format(s fmt.State, verb rune) { // Proposal types const ( - ProposalTypeText string = "Text" - ProposalTypeSoftwareUpgrade string = "SoftwareUpgrade" + ProposalTypeText string = "Text" ) // TextProposal defines a standard text proposal whose changes need to be @@ -243,52 +242,8 @@ 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: {}, + ProposalTypeText: {}, } // RegisterProposalType registers a proposal type. It will panic if the type is @@ -307,9 +262,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 +277,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 diff --git a/x/upgrade/abci.go b/x/upgrade/abci.go new file mode 100644 index 000000000000..c0c7a8ae10fd --- /dev/null +++ b/x/upgrade/abci.go @@ -0,0 +1,43 @@ +package upgrade + +import ( + "fmt" + + 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. +// 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 BeginBlocker(k Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { + plan, found := k.GetUpgradePlan(ctx) + 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) + // We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown + 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 = ctx.WithBlockGasMeter(sdk.NewInfiniteGasMeter()) + 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) { + 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/abci_test.go b/x/upgrade/abci_test.go new file mode 100644 index 000000000000..1c599e612645 --- /dev/null +++ b/x/upgrade/abci_test.go @@ -0,0 +1,190 @@ +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/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" + dbm "github.com/tendermint/tm-db" +) + +type TestSuite struct { + suite.Suite + keeper Keeper + querier sdk.Querier + handler gov.Handler + module module.AppModule + 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.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.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.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.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.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.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: 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.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "test", Height: s.ctx.BlockHeight() + 1}}) + s.Require().Nil(err) + + s.VerifyDoUpgrade() +} + +func (s *TestSuite) TestCanOverwriteScheduleUpgrade() { + s.T().Log("Can overwrite plan") + err := s.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "bad_test", Height: s.ctx.BlockHeight() + 10}}) + s.Require().Nil(err) + err = s.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: 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.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() { + s.module.BeginBlock(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.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.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "future", Height: s.ctx.BlockHeight() + 3}}) + s.Require().NoError(err) + s.Require().Panics(func() { + s.module.BeginBlock(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.module.BeginBlock(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") + 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.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: Plan{Name: "test", Time: time.Now()}}) + s.Require().Nil(err) + + s.handler(s.ctx, CancelSoftwareUpgradeProposal{Title: "cancel"}) + + 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.handler(s.ctx, SoftwareUpgradeProposal{Title: "prop", Plan: 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.module.BeginBlock(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/alias.go b/x/upgrade/alias.go new file mode 100644 index 000000000000..b7324f2c3e41 --- /dev/null +++ b/x/upgrade/alias.go @@ -0,0 +1,45 @@ +// 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 + QueryCurrent = types.QueryCurrent + QueryApplied = types.QueryApplied +) + +var ( + // functions aliases + RegisterCodec = types.RegisterCodec + PlanKey = types.PlanKey + NewSoftwareUpgradeProposal = types.NewSoftwareUpgradeProposal + NewCancelSoftwareUpgradeProposal = types.NewCancelSoftwareUpgradeProposal + NewQueryAppliedParams = types.NewQueryAppliedParams + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier +) + +type ( + UpgradeHandler = types.UpgradeHandler + 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 new file mode 100644 index 000000000000..bf7d2163fa44 --- /dev/null +++ b/x/upgrade/client/cli/query.go @@ -0,0 +1,96 @@ +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: "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) + + // ignore height for now + res, _, err := cliCtx.Query(fmt.Sprintf("custom/%s/%s", upgrade.QuerierKey, upgrade.QueryCurrent)) + if err != nil { + return err + } + + if len(res) == 0 { + return fmt.Errorf("no upgrade scheduled") + } + + var plan upgrade.Plan + err = cdc.UnmarshalJSON(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 [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) + + name := args[0] + params := upgrade.NewQueryAppliedParams(name) + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + return err + } + + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", upgrade.QuerierKey, upgrade.QueryApplied), bz) + 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") + } + applied := int64(binary.BigEndian.Uint64(res)) + + // 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 + }, + } +} diff --git a/x/upgrade/client/cli/tx.go b/x/upgrade/client/cli/tx.go new file mode 100644 index 000000000000..b4c34e23129d --- /dev/null +++ b/x/upgrade/client/cli/tx.go @@ -0,0 +1,167 @@ +package cli + +import ( + "fmt" + "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 time for a new upgrade proposal + TimeFormat = "2006-01-02T15:04:05Z" + + FlagUpgradeHeight = "upgrade-height" + FlagUpgradeTime = "time" + FlagUpgradeInfo = "info" +) + +func parseArgsToContent(cmd *cobra.Command, name string) (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 + } + + 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 && 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 + } + + plan := upgrade.Plan{Name: name, Time: upgradeTime, Height: height, Info: info} + content := upgrade.NewSoftwareUpgradeProposal(title, description, plan) + 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 [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 { + name := args[0] + content, err := parseArgsToContent(cmd, name) + 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().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: "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) + 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/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/query.go b/x/upgrade/client/rest/query.go new file mode 100644 index 000000000000..bf9bdd341070 --- /dev/null +++ b/x/upgrade/client/rest/query.go @@ -0,0 +1,75 @@ +package rest + +import ( + "encoding/binary" + "fmt" + "net/http" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/types/rest" + upgrade "github.com/cosmos/cosmos-sdk/x/upgrade/internal/types" +) + +// RegisterRoutes registers REST routes for the upgrade module under the path specified by routeName. +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) { + return func(w http.ResponseWriter, request *http.Request) { + // ignore height for now + 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 = cliCtx.Codec.UnmarshalBinaryBare(res, &plan) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + 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/client/rest/tx.go b/x/upgrade/client/rest/tx.go new file mode 100644 index 000000000000..acdec064328d --- /dev/null +++ b/x/upgrade/client/rest/tx.go @@ -0,0 +1,119 @@ +package rest + +import ( + "net/http" + "time" + + "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" + "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 ProposalRESTHandler(cliCtx context.CLIContext) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "upgrade", + Handler: postPlanHandler(cliCtx), + } +} + +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}) + } +} diff --git a/x/upgrade/doc.go b/x/upgrade/doc.go new file mode 100644 index 000000000000..160580d5c79c --- /dev/null +++ b/x/upgrade/doc.go @@ -0,0 +1,113 @@ +/* +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. + +General Workflow + +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. + +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.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 +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 +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. 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 + +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. + +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. + +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. + +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 diff --git a/x/upgrade/exported/keeper.go b/x/upgrade/exported/keeper.go new file mode 100644 index 000000000000..88cf6a6edb36 --- /dev/null +++ b/x/upgrade/exported/keeper.go @@ -0,0 +1,30 @@ +package exported + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/upgrade/internal/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) + + // 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/handler.go b/x/upgrade/handler.go new file mode 100644 index 000000000000..2ae2cb09976a --- /dev/null +++ b/x/upgrade/handler.go @@ -0,0 +1,35 @@ +package upgrade + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + 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) { + 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/internal/keeper/keeper.go b/x/upgrade/internal/keeper/keeper.go new file mode 100644 index 000000000000..11c50a0e5abd --- /dev/null +++ b/x/upgrade/internal/keeper/keeper.go @@ -0,0 +1,117 @@ +package keeper + +import ( + "encoding/binary" + "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" +) + +type Keeper struct { + storeKey sdk.StoreKey + cdc *codec.Codec + upgradeHandlers map[string]types.UpgradeHandler +} + +// 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. +// 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() + 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") + } + } else if plan.Height <= ctx.BlockHeight() { + return sdk.ErrUnknownRequest("upgrade cannot be scheduled in the past") + } + + 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 := prefix.NewStore(ctx.KVStore(k.storeKey), []byte{types.DoneByte}) + bz := store.Get([]byte(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) + 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) { + 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 := prefix.NewStore(ctx.KVStore(k.storeKey), []byte{types.DoneByte}) + bz := make([]byte, 8) + binary.BigEndian.PutUint64(bz, uint64(ctx.BlockHeight())) + store.Set([]byte(name), bz) +} + +// 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 +} + +// 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/keeper/querier.go b/x/upgrade/internal/keeper/querier.go new file mode 100644 index 000000000000..291f7bb9d049 --- /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/codec.go b/x/upgrade/internal/types/codec.go new file mode 100644 index 000000000000..4a8b07cfbb24 --- /dev/null +++ b/x/upgrade/internal/types/codec.go @@ -0,0 +1,12 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +// RegisterCodec registers concrete types on the Amino codec +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(Plan{}, "cosmos-sdk/Plan", nil) + cdc.RegisterConcrete(SoftwareUpgradeProposal{}, "cosmos-sdk/SoftwareUpgradeProposal", nil) + cdc.RegisterConcrete(CancelSoftwareUpgradeProposal{}, "cosmos-sdk/CancelSoftwareUpgradeProposal", nil) +} 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/keys.go b/x/upgrade/internal/types/keys.go new file mode 100644 index 000000000000..cda224623b2b --- /dev/null +++ b/x/upgrade/internal/types/keys.go @@ -0,0 +1,28 @@ +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 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 new file mode 100644 index 000000000000..24fd28b72005 --- /dev/null +++ b/x/upgrade/internal/types/plan.go @@ -0,0 +1,76 @@ +package types + +import ( + "fmt" + "strings" + "time" + + 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"` +} + +func (p Plan) String() string { + due := p.DueAt() + dueUp := strings.ToUpper(due[0:1]) + due[1:] + return fmt.Sprintf(`Upgrade Plan + Name: %s + %s + Info: %s`, p.Name, dueUp, p.Info) +} + +// 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") + } + 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") + } + if !p.Time.IsZero() && p.Height != 0 { + return sdk.ErrUnknownRequest("cannot set both time and height") + } + 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 +} + +// 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)) + } + 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..d8e7e18d7fa3 --- /dev/null +++ b/x/upgrade/internal/types/plan_test.go @@ -0,0 +1,186 @@ +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 { + 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) + }) + } +} + +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 { + tc := tc // copy to local variable for scopelint + 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 { + 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) + assert.Equal(t, tc.expected, should) + }) + } +} diff --git a/x/upgrade/internal/types/proposal.go b/x/upgrade/internal/types/proposal.go new file mode 100644 index 000000000000..3dc54f494f60 --- /dev/null +++ b/x/upgrade/internal/types/proposal.go @@ -0,0 +1,85 @@ +package types + +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" yaml:"title"` + Description string `json:"description" yaml:"description"` + Plan Plan `json:"plan" yaml:"plan"` +} + +func NewSoftwareUpgradeProposal(title, description string, plan Plan) gov.Content { + return SoftwareUpgradeProposal{title, description, plan} +} + +// 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 } +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) +} + +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" yaml:"title"` + Description string `json:"description" yaml:"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/internal/types/proposal_test.go b/x/upgrade/internal/types/proposal_test.go new file mode 100644 index 000000000000..02d204a16722 --- /dev/null +++ b/x/upgrade/internal/types/proposal_test.go @@ -0,0 +1,76 @@ +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 { + 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()) + 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()) + + }) + + } +} 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 new file mode 100644 index 000000000000..a365cd1a36c2 --- /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) +} + +// 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 { + return NewQuerier(am.keeper) +} + +// 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 +// +// CONTRACT: this is registered in BeginBlocker *before* all other modules' BeginBlock functions +func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { + BeginBlocker(am.keeper, ctx, req) +} + +// EndBlock does nothing +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +}