diff --git a/.pending/features/sdk/3981-Add-support-to-gracefully-halt-a-node-at-a-given-height b/.pending/features/sdk/3981-Add-support-to-gracefully-halt-a-node-at-a-given-height new file mode 100644 index 000000000000..2e6e3332ef01 --- /dev/null +++ b/.pending/features/sdk/3981-Add-support-to-gracefully-halt-a-node-at-a-given-height @@ -0,0 +1,2 @@ +#3981 Add support to gracefully halt a node at a given height +via the node's `halt-height` config or CLI value. diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index b244a98b77c1..87cd86b0cdba 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -3,6 +3,7 @@ package baseapp import ( "fmt" "io" + "os" "reflect" "runtime/debug" "strings" @@ -81,6 +82,9 @@ type BaseApp struct { // flag for sealing options and parameters to a BaseApp sealed bool + + // height at which to halt the chain and gracefully shutdown + haltHeight uint64 } var _ abci.Application = (*BaseApp)(nil) @@ -230,6 +234,10 @@ func (app *BaseApp) setMinGasPrices(gasPrices sdk.DecCoins) { app.minGasPrices = gasPrices } +func (app *BaseApp) setHaltHeight(height uint64) { + app.haltHeight = height +} + // Router returns the router of the BaseApp. func (app *BaseApp) Router() Router { if app.sealed { @@ -885,7 +893,13 @@ func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBloc return } -// Commit implements the ABCI interface. +// Commit implements the ABCI interface. It will commit all state that exists in +// the deliver state's multi-store and includes the resulting commit ID in the +// returned abci.ResponseCommit. Commit will set the check state based on the +// latest header and reset the deliver state. Also, if a non-zero halt height is +// defined in config, Commit will execute a deferred function call to check +// against that height and gracefully halt if it matches the latest committed +// height. func (app *BaseApp) Commit() (res abci.ResponseCommit) { header := app.deliverState.ctx.BlockHeader() @@ -896,13 +910,20 @@ func (app *BaseApp) Commit() (res abci.ResponseCommit) { // Reset the Check state to the latest committed. // - // NOTE: safe because Tendermint holds a lock on the mempool for Commit. - // Use the header from this latest block. + // NOTE: This is safe because Tendermint holds a lock on the mempool for + // Commit. Use the header from this latest block. app.setCheckState(header) // empty/reset the deliver state app.deliverState = nil + defer func() { + if app.haltHeight > 0 && uint64(header.Height) == app.haltHeight { + app.logger.Info("halting node per configuration", "height", app.haltHeight) + os.Exit(0) + } + }() + return abci.ResponseCommit{ Data: commitID.Hash, } diff --git a/baseapp/options.go b/baseapp/options.go index a40d6eba568c..d78f59958a7f 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -28,6 +28,11 @@ func SetMinGasPrices(gasPricesStr string) func(*BaseApp) { return func(bap *BaseApp) { bap.setMinGasPrices(gasPrices) } } +// SetHaltHeight returns a BaseApp option function that sets the halt height. +func SetHaltHeight(height uint64) func(*BaseApp) { + return func(bap *BaseApp) { bap.setHaltHeight(height) } +} + func (app *BaseApp) SetName(name string) { if app.sealed { panic("SetName() on sealed BaseApp") diff --git a/cmd/gaia/cmd/gaiad/main.go b/cmd/gaia/cmd/gaiad/main.go index 216b8df63c0b..4f636d305e0f 100644 --- a/cmd/gaia/cmd/gaiad/main.go +++ b/cmd/gaia/cmd/gaiad/main.go @@ -70,6 +70,7 @@ func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer) abci.Application logger, db, traceStore, true, invCheckPeriod, baseapp.SetPruning(store.NewPruningOptionsFromString(viper.GetString("pruning"))), baseapp.SetMinGasPrices(viper.GetString(server.FlagMinGasPrices)), + baseapp.SetHaltHeight(uint64(viper.GetInt(server.FlagHaltHeight))), ) } diff --git a/cmd/gaia/init/testnet.go b/cmd/gaia/init/testnet.go index fc59827a6cd6..5e359a5569d2 100644 --- a/cmd/gaia/init/testnet.go +++ b/cmd/gaia/init/testnet.go @@ -236,6 +236,8 @@ func initTestnet(config *tmconfig.Config, cdc *codec.Codec) error { return err } + // TODO: Rename config file to server.toml as it's not particular to Gaia + // (REF: https://github.com/cosmos/cosmos-sdk/issues/4125). gaiaConfigFilePath := filepath.Join(nodeDir, "config/gaiad.toml") srvconfig.WriteConfigFile(gaiaConfigFilePath, gaiaConfig) } diff --git a/docs/cosmos-hub/validators/validator-setup.md b/docs/cosmos-hub/validators/validator-setup.md index 0087e8f8e8ae..25f543917341 100644 --- a/docs/cosmos-hub/validators/validator-setup.md +++ b/docs/cosmos-hub/validators/validator-setup.md @@ -165,6 +165,15 @@ You should now see your validator in one of the Cosmos Hub explorers. You are lo To be in the validator set, you need to have more total voting power than the 100th validator. ::: +## Halting Your Validator + +When attempting to perform routine maintenance or planning for an upcoming coordinated +upgrade, it can be useful to have your validator systematically and gracefully halt. +You can achieve this by either setting the `halt-height` to the height at which +you want your node to shutdown or by passing the `--halt-height` flag to `gaiad`. +The node will shutdown with a zero exit code at that given height after committing +the block. + ## Common Problems ### Problem #1: My validator has `voting_power: 0` diff --git a/server/config/config.go b/server/config/config.go index 11ff53488573..1507999345fe 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -17,6 +17,10 @@ type BaseConfig struct { // transaction. A transaction's fees must meet the minimum of any denomination // specified in this config (e.g. 0.25token1;0.0001token2). MinGasPrices string `mapstructure:"minimum-gas-prices"` + + // HaltHeight contains a non-zero height at which a node will gracefully halt + // and shutdown that can be used to assist upgrades and testing. + HaltHeight uint64 `mapstructure:"halt-height"` } // Config defines the server's top level configuration @@ -56,6 +60,7 @@ func DefaultConfig() *Config { return &Config{ BaseConfig{ MinGasPrices: defaultMinGasPrices, + HaltHeight: 0, }, } } diff --git a/server/config/toml.go b/server/config/toml.go index 31e6ea9bcf47..7f585b981fd6 100644 --- a/server/config/toml.go +++ b/server/config/toml.go @@ -17,6 +17,10 @@ const defaultConfigTemplate = `# This is a TOML config file. # transaction. A transaction's fees must meet the minimum of any denomination # specified in this config (e.g. 0.25token1;0.0001token2). minimum-gas-prices = "{{ .BaseConfig.MinGasPrices }}" + +# HaltHeight contains a non-zero height at which a node will gracefully halt +# and shutdown that can be used to assist upgrades and testing. +halt-height = {{ .BaseConfig.HaltHeight }} ` var configTemplate *template.Template diff --git a/server/start.go b/server/start.go index 4b976c822bca..189a2c866899 100644 --- a/server/start.go +++ b/server/start.go @@ -23,6 +23,7 @@ const ( flagTraceStore = "trace-store" flagPruning = "pruning" FlagMinGasPrices = "minimum-gas-prices" + FlagHaltHeight = "halt-height" ) // StartCmd runs the service passed in, either stand-alone or in-process with @@ -53,6 +54,7 @@ func StartCmd(ctx *Context, appCreator AppCreator) *cobra.Command { FlagMinGasPrices, "", "Minimum gas prices to accept for transactions; Any fee in a tx must meet this minimum (e.g. 0.01photino;0.0001stake)", ) + cmd.Flags().Uint64(FlagHaltHeight, 0, "Height at which to gracefully halt the chain and shutdown the node") // add support for all Tendermint-specific command line options tcmd.AddNodeFlags(cmd) diff --git a/server/util.go b/server/util.go index 94859bab0ac0..2528e0a71574 100644 --- a/server/util.go +++ b/server/util.go @@ -105,7 +105,10 @@ func interceptLoadConfig() (conf *cfg.Config, err error) { conf, err = tcmd.ParseConfig() // NOTE: ParseConfig() creates dir/files as necessary. } - // create a default gaia config file if it does not exist + // create a default Gaia config file if it does not exist + // + // TODO: Rename config file to server.toml as it's not particular to Gaia + // (REF: https://github.com/cosmos/cosmos-sdk/issues/4125). gaiaConfigFilePath := filepath.Join(rootDir, "config/gaiad.toml") if _, err := os.Stat(gaiaConfigFilePath); os.IsNotExist(err) { gaiaConf, _ := config.ParseConfig()