diff --git a/cmd/geth/main.go b/cmd/geth/main.go index a94a49ea7daf..36daf10ae400 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -147,6 +147,7 @@ var ( utils.LogDebugFlag, utils.LogBacktraceAtFlag, utils.ZeroFeeAddressesFlag, + utils.UpgradeTimestampFlag, }, utils.NetworkFlags, utils.DatabaseFlags) rpcFlags = []cli.Flag{ @@ -366,6 +367,8 @@ func startNode(ctx *cli.Context, stack *node.Node, backend ethapi.Backend, isCon rpcClient := stack.Attach() ethClient := ethclient.NewClient(rpcClient) + utils.ShutdownAtUpgradeTimestamp(ctx, stack, ethClient) + go func() { // Open any wallets already attached for _, wallet := range stack.AccountManager().Wallets() { diff --git a/cmd/utils/cmd.go b/cmd/utils/cmd.go index 4b5716466556..6d222c684739 100644 --- a/cmd/utils/cmd.go +++ b/cmd/utils/cmd.go @@ -21,6 +21,7 @@ import ( "bufio" "bytes" "compress/gzip" + "context" "crypto/sha256" "errors" "fmt" @@ -40,6 +41,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/internal/debug" "github.com/ethereum/go-ethereum/internal/era" @@ -123,6 +125,42 @@ func StartNode(ctx *cli.Context, stack *node.Node, isConsole bool) { }() } +func ShutdownAtUpgradeTimestamp(ctx *cli.Context, n *node.Node, ethClient *ethclient.Client) { + upgradeTimestamp := n.Config().UpgradeTimestamp + if upgradeTimestamp == 0 { + log.Info("Upgrade timestamp is not set, skipping shutdown at upgrade timestamp") + return + } + log.Info("Starting goroutine to shutdown at upgrade timestamp", "upgradeTimestamp", upgradeTimestamp) + go func() { + headers := make(chan *types.Header) + sub, err := ethClient.SubscribeNewHead(context.Background(), headers) + if err != nil { + log.Error("ShutdownAtUpgradeTimestamp: failed to subscribe to new head", "err", err) + return + } + defer sub.Unsubscribe() + for { + select { + case <-ctx.Done(): + log.Info("ShutdownAtUpgradeTimestamp: context cancelled, exiting goroutine") + return + case header, ok := <-headers: + if !ok { + log.Error("ShutdownAtUpgradeTimestamp: subscription closed, exiting goroutine") + return + } + shutdownTimestamp := upgradeTimestamp - 200 // Timestamps are in ms, block time is 200ms + if header.Time >= shutdownTimestamp { + log.Info("Final block before upgrade has been sealed, initiating shutdown", "header_timestamp", header.Time) + n.Close() + return + } + } + } + }() +} + func monitorFreeDiskSpace(sigc chan os.Signal, path string, freeDiskSpaceCritical uint64) { if path == "" { return diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 6e82143e3565..b1dd93268c34 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -566,6 +566,12 @@ var ( Category: flags.MiscCategory, } + UpgradeTimestampFlag = &cli.Uint64Flag{ + Name: "upgrade-timestamp-ms", + Usage: "Timestamp (in unix milliseconds) at which the node will shut down for upgrade", + Category: flags.MiscCategory, + } + // RPC settings IPCDisabledFlag = &cli.BoolFlag{ Name: "ipcdisable", @@ -1405,6 +1411,9 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { if ctx.IsSet(LogDebugFlag.Name) { log.Warn("log.debug flag is deprecated") } + if ctx.IsSet(UpgradeTimestampFlag.Name) { + cfg.UpgradeTimestamp = ctx.Uint64(UpgradeTimestampFlag.Name) + } } func setSmartCard(ctx *cli.Context, cfg *node.Config) { diff --git a/node/config.go b/node/config.go index 949db887e4e4..e0a59f9da35d 100644 --- a/node/config.go +++ b/node/config.go @@ -211,6 +211,9 @@ type Config struct { EnablePersonal bool `toml:"-"` DBEngine string `toml:",omitempty"` + + // UpgradeTimestamp is approximately the timestamp where the node will shut down for upgrade. + UpgradeTimestamp uint64 `toml:",omitempty"` } // IPCEndpoint resolves an IPC endpoint based on a configured value, taking into