diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 678b9fbff63..5c015c79033 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -22,7 +22,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/events" osm "github.com/gnolang/gno/tm2/pkg/os" p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" - + "github.com/gnolang/gno/tm2/pkg/sdk" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/telemetry" "go.uber.org/zap/zapcore" @@ -269,6 +269,14 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error { return fmt.Errorf("unable to create the Gnoland app, %w", err) } + // Apply halt height from config to the application + if cfg.BaseConfig.HaltHeight > 0 { + if baseApp, ok := cfg.LocalApp.(*sdk.BaseApp); ok { + baseApp.SetHaltHeight(uint64(cfg.BaseConfig.HaltHeight)) + logger.Info("Halt height configured", "height", cfg.BaseConfig.HaltHeight) + } + } + // Create a default node, with the given setup opts := []node.Option{} if c.earlyStart { diff --git a/tm2/pkg/bft/config/config.go b/tm2/pkg/bft/config/config.go index 29734a69a72..4793860391b 100644 --- a/tm2/pkg/bft/config/config.go +++ b/tm2/pkg/bft/config/config.go @@ -324,6 +324,10 @@ type BaseConfig struct { // TCP or UNIX socket address for the profiling server to listen on ProfListenAddress string `toml:"prof_laddr" comment:"TCP or UNIX socket address for the profiling server to listen on"` + + // If non-zero, the node will halt after committing this block height. + // Useful for coordinated chain upgrades. + HaltHeight int64 `toml:"halt_height" comment:"If non-zero, the node will halt after committing this block height.\n Useful for coordinated chain upgrades."` } // DefaultBaseConfig returns a default base configuration for a Tendermint node diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index d2493977de4..8084e834a82 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -3,11 +3,9 @@ package sdk import ( "fmt" "log/slog" - "os" "runtime/debug" "sort" "strings" - "syscall" "github.com/gnolang/gno/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" @@ -68,9 +66,6 @@ type BaseApp struct { // block height at which to halt the chain and gracefully shutdown haltHeight uint64 - // minimum block time (in Unix seconds) at which to halt the chain and gracefully shutdown - haltTime uint64 - // application's version string appVersion string } @@ -526,6 +521,13 @@ func (app *BaseApp) BeginBlock(req abci.RequestBeginBlock) (res abci.ResponseBeg panic(err) } + // Check if we should halt before processing this block. + // We halt at the beginning of the block *after* haltHeight, + // so the block at haltHeight is fully committed. + if app.haltHeight > 0 && uint64(req.Header.GetHeight()) > app.haltHeight { + panic(fmt.Sprintf("halt height %d reached, node shutting down", app.haltHeight)) + } + // Initialize the DeliverTx state. If this is the first block, it should // already be initialized in InitChain. Otherwise app.deliverState will be // nil, since it is reset on Commit. @@ -884,24 +886,6 @@ func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBloc func (app *BaseApp) Commit() (res abci.ResponseCommit) { header := app.deliverState.ctx.BlockHeader() - var halt bool - - switch { - case app.haltHeight > 0 && uint64(header.GetHeight()) >= app.haltHeight: - halt = true - - case app.haltTime > 0 && header.GetTime().Unix() >= int64(app.haltTime): - halt = true - } - - if halt { - app.halt() - - // Note: State is not actually committed when halted. Logs from Tendermint - // can be ignored. - return abci.ResponseCommit{} - } - // Write the DeliverTx state which is cache-wrapped and commit the MultiStore. // The write to the DeliverTx state writes all state transitions to the root // MultiStore (app.cms) so when Commit() is called is persists those values. @@ -929,29 +913,13 @@ func (app *BaseApp) Commit() (res abci.ResponseCommit) { // return. res.Data = commitID.Hash + return } -// halt attempts to gracefully shutdown the node via SIGINT and SIGTERM falling -// back on os.Exit if both fail. -func (app *BaseApp) halt() { - app.logger.Info("halting node per configuration", "height", app.haltHeight, "time", app.haltTime) - - p, err := os.FindProcess(os.Getpid()) - if err == nil { - // attempt cascading signals in case SIGINT fails (os dependent) - sigIntErr := p.Signal(syscall.SIGINT) - sigTermErr := p.Signal(syscall.SIGTERM) - - if sigIntErr == nil || sigTermErr == nil { - return - } - } - - // Resort to exiting immediately if the process could not be found or killed - // via SIGINT/SIGTERM signals. - app.logger.Info("failed to send SIGINT/SIGTERM; exiting...") - os.Exit(0) +// SetHaltHeight sets the block height at which the node will halt after committing. +func (app *BaseApp) SetHaltHeight(height uint64) { + app.haltHeight = height } func (app *BaseApp) Close() error { diff --git a/tm2/pkg/sdk/baseapp_test.go b/tm2/pkg/sdk/baseapp_test.go index 69298b4ada1..257ce45b3b2 100644 --- a/tm2/pkg/sdk/baseapp_test.go +++ b/tm2/pkg/sdk/baseapp_test.go @@ -1273,3 +1273,81 @@ func TestGetMaximumBlockGas(t *testing.T) { app.setConsensusParams(&abci.ConsensusParams{Block: &abci.BlockParams{MaxGas: -5000000}}) require.Panics(t, func() { app.getMaximumBlockGas() }) } + +func TestHaltHeight(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + haltHeight uint64 + blockHeight int64 + shouldPanic bool + }{ + { + name: "no halt configured", + haltHeight: 0, + blockHeight: 10, + shouldPanic: false, + }, + { + name: "block before halt height", + haltHeight: 5, + blockHeight: 4, + shouldPanic: false, + }, + { + name: "block at halt height processes normally", + haltHeight: 5, + blockHeight: 5, + shouldPanic: false, + }, + { + name: "block after halt height panics", + haltHeight: 5, + blockHeight: 6, + shouldPanic: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + app := setupBaseApp(t) + app.SetHaltHeight(tt.haltHeight) + + // Process blocks up to the target height. + for h := int64(1); h < tt.blockHeight; h++ { + header := &bft.Header{ChainID: "test-chain", Height: h} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + app.Commit() + } + + // Process the target block. + header := &bft.Header{ChainID: "test-chain", Height: tt.blockHeight} + if tt.shouldPanic { + require.Panics(t, func() { + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + }) + } else { + require.NotPanics(t, func() { + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + }) + app.Commit() + } + }) + } +} + +func TestSetHaltHeight(t *testing.T) { + t.Parallel() + + app := setupBaseApp(t) + require.Equal(t, uint64(0), app.haltHeight) + + app.SetHaltHeight(100) + require.Equal(t, uint64(100), app.haltHeight) + + app.SetHaltHeight(0) + require.Equal(t, uint64(0), app.haltHeight) +}