Merge PR #4233: Add upgrade module
This commit is contained in:
parent
95ddc242ad
commit
d81d46192a
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -28,9 +28,6 @@ func NormalizeProposalType(proposalType string) string {
|
|||
case "Text", "text":
|
||||
return types.ProposalTypeText
|
||||
|
||||
case "SoftwareUpgrade", "software_upgrade":
|
||||
return types.ProposalTypeSoftwareUpgrade
|
||||
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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})
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
|
@ -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}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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())
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
}
|
|
@ -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{}
|
||||
}
|
Loading…
Reference in New Issue