Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions examples/gno.land/r/sys/params/halt.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package params

import (
"strconv"

"chain"
prms "sys/params"

"gno.land/r/gov/dao"
)

const (
nodeModulePrefix = "node"
haltHeightKey = "halt_height"
haltMinVersionKey = "halt_min_version"
)

// NewSetHaltRequest creates a GovDAO proposal to halt all chain nodes at the given block height.
// Once approved and executed, nodes will gracefully stop after committing the specified block,
// enabling coordinated chain upgrades.
//
// minVersion, if non-empty, sets the minimum binary version required to resume after the halt.
// Nodes will refuse to restart unless their version satisfies the minimum requirement,
// preventing old binaries from accidentally resuming a chain halted for an upgrade.
// Example: minVersion="chain/gnoland1.1" prevents gnoland1.0 from resuming.
//
// Use height=0 to cancel a previously scheduled halt.
func NewSetHaltRequest(height int64, minVersion string) dao.ProposalRequest {
callback := func(cur realm) error {
prms.SetSysParamInt64(nodeModulePrefix, "p", haltHeightKey, height)
prms.SetSysParamString(nodeModulePrefix, "p", haltMinVersionKey, minVersion)
chain.Emit("set_halt",
"height", strconv.FormatInt(height, 10),
"min_version", minVersion,
)
return nil
}

var desc string
if height == 0 {
desc = "Cancel the scheduled chain halt and clear the minimum version requirement."
} else {
desc = "Halt the chain at block " + strconv.FormatInt(height, 10) + "."
if minVersion != "" {
desc += " Requires binary version >= " + minVersion + " to resume."
}
}

e := dao.NewSimpleExecutor(callback, "")
return dao.NewProposalRequest("Set node halt height", desc, e)
}
21 changes: 21 additions & 0 deletions examples/gno.land/r/sys/params/params_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,24 @@ func TestNewStringPropRequest(t *testing.T) {
t.Errorf("executor shouldn't be nil")
}
}

func TestNewSetHaltRequest(t *testing.T) {
pr := NewSetHaltRequest(100_000, "chain/gnoland1.1")
if pr.Title() == "" {
t.Errorf("proposal title shouldn't be empty")
}
}

func TestNewSetHaltRequestNoVersion(t *testing.T) {
pr := NewSetHaltRequest(100_000, "")
if pr.Title() == "" {
t.Errorf("proposal title shouldn't be empty")
}
}

func TestNewSetHaltRequestCancel(t *testing.T) {
pr := NewSetHaltRequest(0, "")
if pr.Title() == "" {
t.Errorf("proposal title shouldn't be empty")
}
}
1 change: 1 addition & 0 deletions gno.land/cmd/gnoland/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error {
cfg.Application,
evsw,
logger,
cfg.BaseConfig.SkipUpgradeHeight,
)
if err != nil {
return fmt.Errorf("unable to create the Gnoland app, %w", err)
Expand Down
36 changes: 34 additions & 2 deletions gno.land/pkg/gnoland/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type AppOptions struct {
EventSwitch events.EventSwitch // required
VMOutput io.Writer // optional
SkipGenesisSigVerification bool // default to verify genesis transactions
SkipUpgradeHeight int64 // if set, skip the halt_min_version check at this height
InitChainerConfig // options related to InitChainer
MinGasPrices string // optional
PruneStrategy types.PruneStrategy
Expand Down Expand Up @@ -115,6 +116,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) {
prmk.Register(auth.ModuleName, acck)
prmk.Register(bank.ModuleName, bankk)
prmk.Register(vm.ModuleName, vmk)
prmk.Register("node", nodeParamsKeeper{})

// Set InitChainer
icc := cfg.InitChainerConfig
Expand Down Expand Up @@ -213,6 +215,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) {
acck,
gpk,
vmk,
prmk,
baseApp,
),
)
Expand All @@ -233,6 +236,11 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) {
vmk.Initialize(cfg.Logger, ms)
ms.MultiWrite() // XXX why was't this needed?

// Verify node startup constraints set by governance halt proposals.
if err := checkNodeStartupParams(prmk, baseApp.GetCacheMultiStore(), baseApp.LastBlockHeight(), cfg.SkipUpgradeHeight); err != nil {
return nil, err
}

return baseApp, nil
}

Expand All @@ -258,6 +266,7 @@ func NewApp(
appCfg *sdkCfg.AppConfig,
evsw events.EventSwitch,
logger *slog.Logger,
skipUpgradeHeight int64,
) (abci.Application, error) {
var err error

Expand All @@ -270,6 +279,7 @@ func NewApp(
},
MinGasPrices: appCfg.MinGasPrices,
SkipGenesisSigVerification: genesisCfg.SkipSigVerification,
SkipUpgradeHeight: skipUpgradeHeight,
PruneStrategy: appCfg.PruneStrategy,
}
if genesisCfg.SkipFailingTxs {
Expand Down Expand Up @@ -475,22 +485,26 @@ type endBlockerApp interface {

// Logger returns the logger reference
Logger() *slog.Logger

// SetHaltHeight sets the block height at which the node will halt.
SetHaltHeight(uint64)
}

// EndBlocker defines the logic executed after every block.
// Currently, it parses events that happened during execution to calculate
// validator set changes
// validator set changes, and checks for a governance-requested chain halt.
func EndBlocker(
collector *collector[validatorUpdate],
acck auth.AccountKeeperI,
gpk auth.GasPriceKeeperI,
vmk vm.VMKeeperI,
prmk params.ParamsKeeperI,
app endBlockerApp,
) func(
ctx sdk.Context,
req abci.RequestEndBlock,
) abci.ResponseEndBlock {
return func(ctx sdk.Context, _ abci.RequestEndBlock) abci.ResponseEndBlock {
return func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock {
// set the auth params value in the ctx. The EndBlocker will use InitialGasPrice in
// the params to calculate the updated gas price.
if acck != nil {
Expand All @@ -500,6 +514,24 @@ func EndBlocker(
auth.EndBlocker(ctx, gpk)
}

// Check if GovDAO has requested a halt at this height.
// Use == (not >=) so we only trigger once: at the exact halt height.
// SetHaltHeight causes BeginBlock of the *next* block to panic, ensuring
// this block is fully committed before the node stops.
// On restart, req.Height > halt_height, so == never re-fires — no infinite loop.
if prmk != nil {
var haltHeight int64
prmk.GetInt64(ctx, nodeParamHaltHeight, &haltHeight)
if haltHeight > 0 && req.Height == haltHeight {
app.Logger().Info(
"GovDAO halt height reached, will halt after this block",
"height", req.Height,
"halt_height", haltHeight,
)
app.SetHaltHeight(uint64(haltHeight))
}
}

// Check if there was a valset change
if len(collector.getEvents()) == 0 {
// No valset updates
Expand Down
Loading
Loading