From 92ba48aae09a44113084fba0dee4f2ba94da90e8 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:11:48 +0000 Subject: [PATCH 1/9] feat(node): add --halt-height flag for coordinated chain upgrades Add a -halt-height flag that stops the node after committing a specific block height. This enables coordinated chain upgrades where all validators stop at the same height before upgrading the binary. Usage: gnoland start -halt-height 50000 The node will process blocks normally until it commits block 50000, then gracefully shut down. Set to 0 (default) to run indefinitely. Implementation: - Add haltHeight field to ConsensusState with SetHaltHeight setter - Add WithHaltHeight node Option (same pattern as WithEarlyStart) - Check height in finalizeCommit after block is committed - Register -halt-height flag in gnoland start command --- gno.land/cmd/gnoland/start.go | 11 +++++++++++ tm2/pkg/bft/consensus/state.go | 17 +++++++++++++++++ tm2/pkg/bft/node/node.go | 18 +++++++++++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index dba40f94fc6..da3f48bc39b 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -58,6 +58,7 @@ type startCfg struct { logLevel string logFormat string earlyStart bool + haltHeight int64 } func newStartCmd(io commands.IO) *commands.Command { @@ -171,6 +172,13 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { false, "[experimental] start RPC and P2P before genesis time, deferring only consensus", ) + + fs.Int64Var( + &c.haltHeight, + "halt-height", + 0, + "halt the node after committing this block height (0 = run indefinitely)", + ) } func execStart(ctx context.Context, c *startCfg, io commands.IO) error { @@ -274,6 +282,9 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error { if c.earlyStart { opts = append(opts, node.WithEarlyStart()) } + if c.haltHeight > 0 { + opts = append(opts, node.WithHaltHeight(c.haltHeight)) + } gnoNode, err := node.DefaultNewNode(cfg, genesisPath, evsw, logger, opts...) if err != nil { return fmt.Errorf("unable to create the Gnoland node, %w", err) diff --git a/tm2/pkg/bft/consensus/state.go b/tm2/pkg/bft/consensus/state.go index 9fc068f49a1..642aa7047f1 100644 --- a/tm2/pkg/bft/consensus/state.go +++ b/tm2/pkg/bft/consensus/state.go @@ -138,6 +138,9 @@ type ConsensusState struct { doPrevote func(height int64, round int) setProposal func(proposal *types.Proposal) error + // if non-zero, the node will stop after committing this height + haltHeight int64 + // closed when we finish shutting down done chan struct{} } @@ -195,6 +198,12 @@ func (cs *ConsensusState) SetLogger(l *slog.Logger) { cs.timeoutTicker.SetLogger(l) } +// SetHaltHeight sets the height at which the node will stop after committing. +// If set to 0, the node runs indefinitely. +func (cs *ConsensusState) SetHaltHeight(height int64) { + cs.haltHeight = height +} + // SetEventSwitch sets event bus. func (cs *ConsensusState) SetEventSwitch(evsw events.EventSwitch) { cs.evsw = evsw @@ -1402,6 +1411,14 @@ func (cs *ConsensusState) finalizeCommit(height int64) { // Log the telemetry cs.logTelemetry(block) + + // Check if we should halt at this height + if cs.haltHeight > 0 && height >= cs.haltHeight { + cs.Logger.Info("Halt height reached, shutting down", "height", height, "halt_height", cs.haltHeight) + if err := osm.Kill(); err != nil { + cs.Logger.Error("Failed to halt node", "err", err) + } + } } // ----------------------------------------------------------------------------- diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index 172276a11bb..7601d17a4ee 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -155,6 +155,15 @@ func WithEarlyStart() Option { } } +// WithHaltHeight configures the node to stop after committing the given block height. +// This is useful for coordinated chain upgrades where all validators need to stop +// at the same height before upgrading the binary. +func WithHaltHeight(height int64) Option { + return func(n *Node) { + n.haltHeight = height + } +} + // ------------------------------------------------------------------------------ // Node is the highest level interface to a full Tendermint node. @@ -190,7 +199,8 @@ type Node struct { eventStoreService *eventstore.Service firstBlockSignal <-chan struct{} - earlyStart bool // start RPC+P2P before genesis time, defer only consensus + earlyStart bool // start RPC+P2P before genesis time, defer only consensus + haltHeight int64 // if non-zero, halt after committing this block height } func initDBs(config *cfg.Config, dbProvider DBProvider) (blockStore *store.BlockStore, stateDB dbm.DB, err error) { @@ -577,6 +587,12 @@ func NewNode(config *cfg.Config, option(node) } + // Apply halt height to consensus state after options are processed + if node.haltHeight > 0 { + node.consensusState.SetHaltHeight(node.haltHeight) + logger.Info("Halt height configured", "height", node.haltHeight) + } + return node, nil } From 6eedad88ef69100f6cb35be573462d285aafb19d Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:27:48 +0000 Subject: [PATCH 2/9] test(consensus): add TestSetHaltHeight unit test --- tm2/pkg/bft/consensus/state_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tm2/pkg/bft/consensus/state_test.go b/tm2/pkg/bft/consensus/state_test.go index c947875171b..0b4b7c1d655 100644 --- a/tm2/pkg/bft/consensus/state_test.go +++ b/tm2/pkg/bft/consensus/state_test.go @@ -1807,3 +1807,26 @@ func TestStateOutputVoteStats(t *testing.T) { func subscribe(evsw events.EventSwitch, protoevent events.Event) <-chan events.Event { return events.SubscribeToEvent(evsw, testSubscriber, protoevent) } + +func TestSetHaltHeight(t *testing.T) { + t.Helper() + + cs := &ConsensusState{} + + // Default should be 0 (no halt) + if cs.haltHeight != 0 { + t.Fatalf("expected default haltHeight to be 0, got %d", cs.haltHeight) + } + + // Set halt height + cs.SetHaltHeight(100) + if cs.haltHeight != 100 { + t.Fatalf("expected haltHeight to be 100, got %d", cs.haltHeight) + } + + // Set to 0 to disable + cs.SetHaltHeight(0) + if cs.haltHeight != 0 { + t.Fatalf("expected haltHeight to be 0, got %d", cs.haltHeight) + } +} From 88dade8c85dcb016de853514aae260b5d7079b1e Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:58:45 +0000 Subject: [PATCH 3/9] feat: support halt_height in config.toml (settable via gnoland config set) --- gno.land/cmd/gnoland/start.go | 2 +- tm2/pkg/bft/config/config.go | 4 ++++ tm2/pkg/bft/node/node.go | 13 +++++++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 5f5138debe7..a2ff03090ba 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -177,7 +177,7 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { &c.haltHeight, "halt-height", 0, - "halt the node after committing this block height (0 = run indefinitely)", + "halt the node after committing this block height (0 = run indefinitely; overrides config halt_height)", ) } 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/bft/node/node.go b/tm2/pkg/bft/node/node.go index 7601d17a4ee..57dd1d9beb5 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -587,10 +587,15 @@ func NewNode(config *cfg.Config, option(node) } - // Apply halt height to consensus state after options are processed - if node.haltHeight > 0 { - node.consensusState.SetHaltHeight(node.haltHeight) - logger.Info("Halt height configured", "height", node.haltHeight) + // Apply halt height: CLI option overrides config file + haltHeight := node.haltHeight + if haltHeight == 0 { + haltHeight = config.BaseConfig.HaltHeight + } + if haltHeight > 0 { + node.haltHeight = haltHeight + node.consensusState.SetHaltHeight(haltHeight) + logger.Info("Halt height configured", "height", haltHeight) } return node, nil From b6abb2420a8067b1f5169d5ee4cab02dc13557b6 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 1 Apr 2026 21:00:49 +0000 Subject: [PATCH 4/9] refactor: remove --halt-height flag, use config-only (gnoland config set halt_height N) --- gno.land/cmd/gnoland/start.go | 10 ---------- tm2/pkg/bft/node/node.go | 25 +++++-------------------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index a2ff03090ba..10f74ae1e03 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -58,7 +58,6 @@ type startCfg struct { logLevel string logFormat string earlyStart bool - haltHeight int64 } func newStartCmd(io commands.IO) *commands.Command { @@ -173,12 +172,6 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { "[experimental] start RPC and P2P before genesis time, deferring only consensus", ) - fs.Int64Var( - &c.haltHeight, - "halt-height", - 0, - "halt the node after committing this block height (0 = run indefinitely; overrides config halt_height)", - ) } func execStart(ctx context.Context, c *startCfg, io commands.IO) error { @@ -282,9 +275,6 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error { if c.earlyStart { opts = append(opts, node.WithEarlyStart()) } - if c.haltHeight > 0 { - opts = append(opts, node.WithHaltHeight(c.haltHeight)) - } gnoNode, err := node.DefaultNewNode(cfg, genesisPath, evsw, logger, opts...) if err != nil { return fmt.Errorf("unable to create the Gnoland node, %w", err) diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index 57dd1d9beb5..a47873c58d7 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -155,15 +155,6 @@ func WithEarlyStart() Option { } } -// WithHaltHeight configures the node to stop after committing the given block height. -// This is useful for coordinated chain upgrades where all validators need to stop -// at the same height before upgrading the binary. -func WithHaltHeight(height int64) Option { - return func(n *Node) { - n.haltHeight = height - } -} - // ------------------------------------------------------------------------------ // Node is the highest level interface to a full Tendermint node. @@ -199,8 +190,7 @@ type Node struct { eventStoreService *eventstore.Service firstBlockSignal <-chan struct{} - earlyStart bool // start RPC+P2P before genesis time, defer only consensus - haltHeight int64 // if non-zero, halt after committing this block height + earlyStart bool // start RPC+P2P before genesis time, defer only consensus } func initDBs(config *cfg.Config, dbProvider DBProvider) (blockStore *store.BlockStore, stateDB dbm.DB, err error) { @@ -587,15 +577,10 @@ func NewNode(config *cfg.Config, option(node) } - // Apply halt height: CLI option overrides config file - haltHeight := node.haltHeight - if haltHeight == 0 { - haltHeight = config.BaseConfig.HaltHeight - } - if haltHeight > 0 { - node.haltHeight = haltHeight - node.consensusState.SetHaltHeight(haltHeight) - logger.Info("Halt height configured", "height", haltHeight) + // Apply halt height from config + if config.BaseConfig.HaltHeight > 0 { + node.consensusState.SetHaltHeight(config.BaseConfig.HaltHeight) + logger.Info("Halt height configured", "height", config.BaseConfig.HaltHeight) } return node, nil From 45c5ac8b9f1772eaef4ebc787af356d90769c5c0 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 1 Apr 2026 21:06:45 +0000 Subject: [PATCH 5/9] fix: remove trailing newline in RegisterFlags --- gno.land/cmd/gnoland/start.go | 1 - 1 file changed, 1 deletion(-) diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 10f74ae1e03..678b9fbff63 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -171,7 +171,6 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { false, "[experimental] start RPC and P2P before genesis time, deferring only consensus", ) - } func execStart(ctx context.Context, c *startCfg, io commands.IO) error { From f77bcd9e2731e587ba77f976b6cc3127c6a87858 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:14:15 +0000 Subject: [PATCH 6/9] refactor: move halt-height to baseapp, fix commit-before-halt bug Move halt logic from consensus state.go to the existing baseapp.go mechanism. Fix the original bug where state was not committed before halt. Replace signal-based halt with osm.Kill() to avoid async non-determinism (cosmos/cosmos-sdk#16638). halt_height is set via config.toml (gnoland config set halt_height N). --- gno.land/cmd/gnoland/start.go | 9 +++++ tm2/pkg/bft/consensus/state.go | 16 --------- tm2/pkg/bft/consensus/state_test.go | 22 ------------ tm2/pkg/bft/node/node.go | 6 ---- tm2/pkg/sdk/baseapp.go | 53 +++++++++-------------------- 5 files changed, 26 insertions(+), 80 deletions(-) diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 678b9fbff63..80fea6937d6 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -17,6 +17,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/sdk" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/events" @@ -269,6 +270,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/consensus/state.go b/tm2/pkg/bft/consensus/state.go index 52db392fe95..5ef5ae69b64 100644 --- a/tm2/pkg/bft/consensus/state.go +++ b/tm2/pkg/bft/consensus/state.go @@ -138,9 +138,6 @@ type ConsensusState struct { doPrevote func(height int64, round int) setProposal func(proposal *types.Proposal) error - // if non-zero, the node will stop after committing this height - haltHeight int64 - // closed when we finish shutting down done chan struct{} } @@ -198,12 +195,6 @@ func (cs *ConsensusState) SetLogger(l *slog.Logger) { cs.timeoutTicker.SetLogger(l) } -// SetHaltHeight sets the height at which the node will stop after committing. -// If set to 0, the node runs indefinitely. -func (cs *ConsensusState) SetHaltHeight(height int64) { - cs.haltHeight = height -} - // SetEventSwitch sets event bus. func (cs *ConsensusState) SetEventSwitch(evsw events.EventSwitch) { cs.evsw = evsw @@ -1412,13 +1403,6 @@ func (cs *ConsensusState) finalizeCommit(height int64) { // Log the telemetry cs.logTelemetry(block) - // Check if we should halt at this height - if cs.haltHeight > 0 && height >= cs.haltHeight { - cs.Logger.Info("Halt height reached, shutting down", "height", height, "halt_height", cs.haltHeight) - if err := osm.Kill(); err != nil { - cs.Logger.Error("Failed to halt node", "err", err) - } - } } // ----------------------------------------------------------------------------- diff --git a/tm2/pkg/bft/consensus/state_test.go b/tm2/pkg/bft/consensus/state_test.go index 0b4b7c1d655..751324a4240 100644 --- a/tm2/pkg/bft/consensus/state_test.go +++ b/tm2/pkg/bft/consensus/state_test.go @@ -1808,25 +1808,3 @@ func subscribe(evsw events.EventSwitch, protoevent events.Event) <-chan events.E return events.SubscribeToEvent(evsw, testSubscriber, protoevent) } -func TestSetHaltHeight(t *testing.T) { - t.Helper() - - cs := &ConsensusState{} - - // Default should be 0 (no halt) - if cs.haltHeight != 0 { - t.Fatalf("expected default haltHeight to be 0, got %d", cs.haltHeight) - } - - // Set halt height - cs.SetHaltHeight(100) - if cs.haltHeight != 100 { - t.Fatalf("expected haltHeight to be 100, got %d", cs.haltHeight) - } - - // Set to 0 to disable - cs.SetHaltHeight(0) - if cs.haltHeight != 0 { - t.Fatalf("expected haltHeight to be 0, got %d", cs.haltHeight) - } -} diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index a47873c58d7..172276a11bb 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -577,12 +577,6 @@ func NewNode(config *cfg.Config, option(node) } - // Apply halt height from config - if config.BaseConfig.HaltHeight > 0 { - node.consensusState.SetHaltHeight(config.BaseConfig.HaltHeight) - logger.Info("Halt height configured", "height", config.BaseConfig.HaltHeight) - } - return node, nil } diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index b8594735b4d..b1383bdc899 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -7,13 +7,13 @@ import ( "runtime/debug" "sort" "strings" - "syscall" "github.com/gnolang/gno/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" bft "github.com/gnolang/gno/tm2/pkg/bft/types" dbm "github.com/gnolang/gno/tm2/pkg/db" "github.com/gnolang/gno/tm2/pkg/errors" + osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" ) @@ -901,24 +901,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. @@ -946,29 +928,28 @@ func (app *BaseApp) Commit() (res abci.ResponseCommit) { // return. res.Data = commitID.Hash + + // Check if we should halt after this commit. + if app.haltHeight > 0 && uint64(header.GetHeight()) >= app.haltHeight { + app.logger.Info("halt height reached, shutting down", "height", header.GetHeight(), "halt_height", app.haltHeight) + app.halt() + } + return } -// halt attempts to gracefully shutdown the node via SIGINT and SIGTERM falling -// back on os.Exit if both fail. +// halt attempts to gracefully shutdown the node via osm.Kill(), +// falling back on os.Exit if that fails. 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 - } + if err := osm.Kill(); err != nil { + app.logger.Error("failed to halt node", "err", err) + os.Exit(0) } +} - // 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 { From 9feeab3bd9497c8cbad6d4416fd38be20c159abc Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:43:33 +0000 Subject: [PATCH 7/9] refactor: move halt check to BeginBlock and use panic instead of signals --- tm2/pkg/sdk/baseapp.go | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 7a80fc94c2f..d345b84f81e 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -3,7 +3,6 @@ package sdk import ( "fmt" "log/slog" - "os" "runtime/debug" "sort" "strings" @@ -13,7 +12,6 @@ import ( bft "github.com/gnolang/gno/tm2/pkg/bft/types" dbm "github.com/gnolang/gno/tm2/pkg/db" "github.com/gnolang/gno/tm2/pkg/errors" - osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" ) @@ -526,6 +524,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. @@ -912,24 +917,9 @@ func (app *BaseApp) Commit() (res abci.ResponseCommit) { // return. res.Data = commitID.Hash - // Check if we should halt after this commit. - if app.haltHeight > 0 && uint64(header.GetHeight()) >= app.haltHeight { - app.logger.Info("halt height reached, shutting down", "height", header.GetHeight(), "halt_height", app.haltHeight) - app.halt() - } - return } -// halt attempts to gracefully shutdown the node via osm.Kill(), -// falling back on os.Exit if that fails. -func (app *BaseApp) halt() { - if err := osm.Kill(); err != nil { - app.logger.Error("failed to halt node", "err", err) - 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 From c6ac2158047125576befc7895b9be086f5639009 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:01:18 +0000 Subject: [PATCH 8/9] fix: resolve lint issues (unused haltTime, formatting, import order) --- gno.land/cmd/gnoland/start.go | 3 +-- tm2/pkg/bft/consensus/state.go | 1 - tm2/pkg/bft/consensus/state_test.go | 1 - tm2/pkg/sdk/baseapp.go | 3 --- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 80fea6937d6..5c015c79033 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -17,13 +17,12 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" "github.com/gnolang/gno/tm2/pkg/bft/privval" - "github.com/gnolang/gno/tm2/pkg/sdk" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" "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" diff --git a/tm2/pkg/bft/consensus/state.go b/tm2/pkg/bft/consensus/state.go index e3f831b0205..8c1e0a456ba 100644 --- a/tm2/pkg/bft/consensus/state.go +++ b/tm2/pkg/bft/consensus/state.go @@ -1408,7 +1408,6 @@ func (cs *ConsensusState) finalizeCommit(height int64) { // Log the telemetry cs.logTelemetry(block) - } // ----------------------------------------------------------------------------- diff --git a/tm2/pkg/bft/consensus/state_test.go b/tm2/pkg/bft/consensus/state_test.go index 751324a4240..c947875171b 100644 --- a/tm2/pkg/bft/consensus/state_test.go +++ b/tm2/pkg/bft/consensus/state_test.go @@ -1807,4 +1807,3 @@ func TestStateOutputVoteStats(t *testing.T) { func subscribe(evsw events.EventSwitch, protoevent events.Event) <-chan events.Event { return events.SubscribeToEvent(evsw, testSubscriber, protoevent) } - diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index d345b84f81e..8084e834a82 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -66,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 } From 3237d120e02775772b17bb4926e0a6992f380cba Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:52:34 +0000 Subject: [PATCH 9/9] test: add unit tests for halt height behavior --- tm2/pkg/sdk/baseapp_test.go | 78 +++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) 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) +}