Merge PR #4233: Add upgrade module

This commit is contained in:
Aaron Craelius 2019-11-08 09:40:56 -05:00 committed by Alexander Bezobchuk
parent 95ddc242ad
commit d81d46192a
36 changed files with 1760 additions and 143 deletions

View File

@ -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.

View File

@ -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
)

View File

@ -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
}

View File

@ -28,9 +28,6 @@ func NormalizeProposalType(proposalType string) string {
case "Text", "text":
return types.ProposalTypeText
case "SoftwareUpgrade", "software_upgrade":
return types.ProposalTypeSoftwareUpgrade
default:
return ""
}

View File

@ -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),

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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) {

View File

@ -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

View File

@ -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"
)

View File

@ -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())
}

View File

@ -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},

View File

@ -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

43
x/upgrade/abci.go Normal file
View File

@ -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)
}
}

190
x/upgrade/abci_test.go Normal file
View File

@ -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))
}

45
x/upgrade/alias.go Normal file
View File

@ -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
)

View File

@ -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
},
}
}

167
x/upgrade/client/cli/tx.go Normal file
View File

@ -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
}

View File

@ -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)

View File

@ -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)
}
}

119
x/upgrade/client/rest/tx.go Normal file
View File

@ -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})
}
}

113
x/upgrade/doc.go Normal file
View File

@ -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 "<Name>" NEEDED at height <NNNN>: <Info>
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

View File

@ -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)
}

35
x/upgrade/handler.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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, &params)
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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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}
}

View File

@ -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)
}

View File

@ -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)
})
}
}

View File

@ -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)
}

View File

@ -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())
})
}
}

View File

@ -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}
}

134
x/upgrade/module.go Normal file
View File

@ -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{}
}