diff --git a/x/upgrade/abci.go b/x/upgrade/abci.go index c0c7a8ae10fd..01b0aae6a003 100644 --- a/x/upgrade/abci.go +++ b/x/upgrade/abci.go @@ -12,13 +12,14 @@ import ( // 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 +// The purpose 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) @@ -26,10 +27,12 @@ func BeginBlocker(k Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { 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 } diff --git a/x/upgrade/handler.go b/x/upgrade/handler.go index 2ae2cb09976a..925d4e875c9d 100644 --- a/x/upgrade/handler.go +++ b/x/upgrade/handler.go @@ -15,6 +15,7 @@ func NewSoftwareUpgradeProposalHandler(k Keeper) govtypes.Handler { switch c := content.(type) { case SoftwareUpgradeProposal: return handleSoftwareUpgradeProposal(ctx, k, c) + case CancelSoftwareUpgradeProposal: return handleCancelSoftwareUpgradeProposal(ctx, k, c) diff --git a/x/upgrade/internal/keeper/keeper.go b/x/upgrade/internal/keeper/keeper.go index 11c50a0e5abd..bb95ec6eafdb 100644 --- a/x/upgrade/internal/keeper/keeper.go +++ b/x/upgrade/internal/keeper/keeper.go @@ -37,10 +37,10 @@ func (k Keeper) SetUpgradeHandler(name string, upgradeHandler types.UpgradeHandl // 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 { + if err := plan.ValidateBasic(); 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") @@ -65,6 +65,7 @@ func (k Keeper) getDoneHeight(ctx sdk.Context, name string) int64 { if len(bz) == 0 { return 0 } + return int64(binary.BigEndian.Uint64(bz)) } @@ -87,6 +88,7 @@ func (k Keeper) GetUpgradePlan(ctx sdk.Context) (plan types.Plan, havePlan bool) if bz == nil { return plan, false } + k.cdc.MustUnmarshalBinaryBare(bz, &plan) return plan, true } @@ -111,7 +113,9 @@ func (k Keeper) ApplyUpgrade(ctx sdk.Context, plan types.Plan) { if handler == nil { panic("ApplyUpgrade should never be called without first checking HasHandler") } + handler(ctx, plan) + k.ClearUpgradePlan(ctx) k.setDone(ctx, plan.Name) } diff --git a/x/upgrade/spec/01_concepts.md b/x/upgrade/spec/01_concepts.md new file mode 100644 index 000000000000..2b93738f65af --- /dev/null +++ b/x/upgrade/spec/01_concepts.md @@ -0,0 +1,84 @@ +# Concepts + +## Plan + +The `x/upgrade` module defines a `Plan` type in which a live upgrade is scheduled +to occur. A `Plan` can be scheduled at a specific block height or time, but not both. +A `Plan` is created once a (frozen) release candidate along with an appropriate upgrade +`Handler` (see below) is agreed upon, where the `Name` of a `Plan` corresponds to a +specific `Handler`. Typically, a `Plan` is created through a governance proposal +process, where if voted upon and passed, will be scheduled. The `Info` of a `Plan` +may contain various metadata about the upgrade, typically application specific +upgrade info to be included on-chain such as a git commit that validators could +automatically upgrade to. + +### Sidecar Process + +If an operator running the application binary also runs a sidecar process to assist +in the automatic download and upgrade of a binary, the `Info` allows this process to +be seamless. Namely, the `x/upgrade` module fulfills the +[cosmosd Upgradeable Binary Specification](https://github.com/regen-network/cosmosd#upgradeable-binary-specification) +specification and `cosmosd` can optionally be used to fully automate the upgrade +process for node operators. By populating the `Info` field with the necessary information, +binaries can automatically be downloaded. See [here](https://github.com/regen-network/cosmosd#auto-download) +for more info. + +```go +type Plan struct { + Name string + Time Time + Height int64 + Info string +} +``` + +## Handler + +The `x/upgrade` module facilitates upgrading from major version X to major version Y. To +accomplish this, node operators must first upgrade their current binary to a new +binary that has a corresponding `Handler` for the new version Y. It is assumed that +this version has fully been tested and approved by the community at large. This +`Handler` defines what state migrations need to occur before the new binary Y +can successfully run the chain. Naturally, this `Handler` is application specific +and not defined on a per-module basis. Registering a `Handler` is done via +`Keeper#SetUpgradeHandler` in the application. + +```go +type UpgradeHandler func(Context, Plan) +``` + +During each `EndBlock` execution, the `x/upgrade` module checks if there exists a +`Plan` that should execute (is scheduled at that time or height). If so, the corresponding +`Handler` is executed. If the `Plan` is expected to execute but no `Handler` is registered +or if the binary was upgraded too early, the node will gracefully panic and exit. + +## Proposal + +Typically, a `Plan` is proposed and submitted through governance via a `SoftwareUpgradeProposal`. +This proposal prescribes to the standard governance process. If the proposal passes, +the `Plan`, which targets a specific `Handler`, is persisted and scheduled. The +upgrade can be delayed or hastened by updating the `Plan.Time` in a new proposal. + +```go +type SoftwareUpgradeProposal struct { + Title string + Description string + Plan Plan +} +``` + +### Cancelling Upgrade Proposals + +Upgrade proposals can be cancelled. There exists a `CancelSoftwareUpgrade` proposal +type, which can be voted on and passed 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 such a possibility is desired, the upgrade height is to be +`2 * (VotingPeriod + DepositPeriod) + (SafetyDelta)` from the beginning of the +upgrade proposal. The `SafetyDelta` is the time available from the success of an +upgrade proposal and the realization it was a bad idea (due to external social consensus). + +A `CancelSoftwareUpgrade` proposal can also be made while the original +`SoftwareUpgradeProposal` is still being voted upon, as long as the `VotingPeriod` +ends after the `SoftwareUpgradeProposal`. diff --git a/x/upgrade/spec/02_state.md b/x/upgrade/spec/02_state.md new file mode 100644 index 000000000000..ad2650053854 --- /dev/null +++ b/x/upgrade/spec/02_state.md @@ -0,0 +1,7 @@ +# State + +The internal state of the `x/upgrade` module is relatively minimal and simple. The +state only contains the currently active upgrade `Plan` (if one exists) by key +`0x0` and if a `Plan` is marked as "done" by key `0x1`. + +The `x/upgrade` module contains no genesis state. diff --git a/x/upgrade/spec/03_events.md b/x/upgrade/spec/03_events.md new file mode 100644 index 000000000000..bbfcdddfa69e --- /dev/null +++ b/x/upgrade/spec/03_events.md @@ -0,0 +1,4 @@ +# Events + +The `x/upgrade` does not emit any events by itself. Any and all proposal related +events are emitted through the `x/gov` module. diff --git a/x/upgrade/spec/README.md b/x/upgrade/spec/README.md new file mode 100644 index 000000000000..cbe7b5a3bc35 --- /dev/null +++ b/x/upgrade/spec/README.md @@ -0,0 +1,20 @@ +# Upgrade Module Specification + +## Abstract + +`x/upgrade` is an implementation of a Cosmos SDK module that facilitates smoothly +upgrading a live Cosmos chain to a new (breaking) 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. + + +1. **[Concepts](01_concepts.md)** +2. **[State](02_state.md)** +3. **[Events](03_events.md)**