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 58e6cf576170..5088241b507f 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 670dbc8013f0..56b0f2f86cf9 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 4c524b52fcd4..87df725f4672 100644 --- a/docs/cosmos-hub/validators/validator-setup.md +++ b/docs/cosmos-hub/validators/validator-setup.md @@ -165,6 +165,15 @@ You should also be able to see your validator on the [Explorer](https://explorec 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 2b81ce817a35..6fe6cce51669 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()