-
Notifications
You must be signed in to change notification settings - Fork 886
Add freeze mode for historical EVM RPC #3910
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ package config | |
|
|
||
| import ( | ||
| "bytes" | ||
| "math" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
@@ -242,6 +243,35 @@ func TestValidateBasic(t *testing.T) { | |
| }, | ||
| expectErr: true, | ||
| }, | ||
| { | ||
| name: "freeze height above maximum int64", | ||
| setupCfg: func() *Config { | ||
| cfg := DefaultConfig() | ||
| cfg.FreezeHeight = uint64(math.MaxInt64) + 1 | ||
| return cfg | ||
| }, | ||
| expectErr: true, | ||
| }, | ||
| { | ||
| name: "freeze and halt heights", | ||
| setupCfg: func() *Config { | ||
| cfg := DefaultConfig() | ||
| cfg.FreezeHeight = 100 | ||
| cfg.HaltHeight = 100 | ||
| return cfg | ||
| }, | ||
| expectErr: true, | ||
| }, | ||
| { | ||
| name: "freeze height and halt time", | ||
| setupCfg: func() *Config { | ||
| cfg := DefaultConfig() | ||
| cfg.FreezeHeight = 100 | ||
| cfg.HaltTime = 100 | ||
| return cfg | ||
| }, | ||
| expectErr: true, | ||
| }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit - add a negative freeze test |
||
| } | ||
|
|
||
| for _, tt := range tests { | ||
|
|
@@ -257,6 +287,14 @@ func TestValidateBasic(t *testing.T) { | |
| } | ||
| } | ||
|
|
||
| func TestGetConfigRejectsNegativeFreezeHeight(t *testing.T) { | ||
| v := seedViperWithDefaultConfig(t) | ||
| v.Set("freeze-height", -1) | ||
|
|
||
| _, err := GetConfig(v) | ||
| require.Error(t, err) | ||
| } | ||
|
|
||
| func TestGetMinGasPrices(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,4 +10,5 @@ | |
| "compaction-interval" | ||
| "concurrency-workers" | ||
| "occ-enabled" | ||
| "freeze-height" | ||
| # keys with a target of their own | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,7 @@ const ( | |
| flagCPUProfile = "cpu-profile" | ||
| FlagMinGasPrices = "minimum-gas-prices" | ||
| FlagHaltHeight = "halt-height" | ||
| FlagFreezeHeight = "freeze-height" | ||
| FlagHaltTime = "halt-time" | ||
| FlagInterBlockCache = "inter-block-cache" | ||
| FlagUnsafeSkipUpgrades = "unsafe-skip-upgrades" | ||
|
|
@@ -102,6 +103,8 @@ the ABCI Commit phase, the node will check if the current block height is greate | |
| the halt-height or if the current block time is greater than or equal to the halt-time. If so, the | ||
| node will attempt to gracefully shutdown and the block will not be committed. In addition, the node | ||
| will not be able to commit subsequent blocks. | ||
| The '--freeze-height' flag instead keeps the process and RPC servers running while preventing block | ||
| sync and consensus from executing the block at the configured height or advancing beyond it. | ||
| For profiling and benchmarking purposes, CPU profiling can be enabled via the '--cpu-profile' flag | ||
| which accepts a path for the resulting pprof file. | ||
| The node may be started in a 'query only' mode where only the gRPC and JSON HTTP | ||
|
|
@@ -208,6 +211,7 @@ func addStartNodeFlags(cmd *cobra.Command, defaultNodeHome string) { | |
| cmd.Flags().String(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().IntSlice(FlagUnsafeSkipUpgrades, []int{}, "Skip a set of upgrade heights to continue the old binary") | ||
| cmd.Flags().Uint64(FlagHaltHeight, 0, "Block height at which to gracefully halt the chain and shutdown the node") | ||
| cmd.Flags().Uint64(FlagFreezeHeight, 0, "Block height to stop before executing while continuing to serve RPC") | ||
| cmd.Flags().Uint64(FlagHaltTime, 0, "Minimum block time (in Unix seconds) at which to gracefully halt the chain and shutdown the node") | ||
| cmd.Flags().Bool(FlagInterBlockCache, true, "Enable inter-block caching") | ||
| cmd.Flags().String(flagCPUProfile, "", "Enable CPU profiling and write to the provided file") | ||
|
|
@@ -304,6 +308,13 @@ func startInProcess( | |
| if err != nil { | ||
| return err | ||
| } | ||
| if err := config.ValidateFreeze(); err != nil { | ||
| return err | ||
| } | ||
| gRPCOnly := ctx.Viper.GetBool(flagGRPCOnly) | ||
| if gRPCOnly && config.FreezeHeight > 0 { | ||
| return errors.New("freeze-height cannot be used with grpc-only mode") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. grpc-only does not start the EVM rpc nor consensus, so for one there is nothing to freeze (bc there is no consensus) and for the other there is nothing to query from |
||
| } | ||
|
|
||
| if err := config.ValidateBasic(ctx.Config); err != nil { | ||
| logger.Error("WARNING: The minimum-gas-prices config in app.toml is set to the empty string. " + | ||
|
|
@@ -318,8 +329,6 @@ func startInProcess( | |
| } | ||
| }() | ||
|
|
||
| gRPCOnly := ctx.Viper.GetBool(flagGRPCOnly) | ||
|
|
||
| var restartMtx sync.Mutex | ||
| restartCh := make(chan struct{}) | ||
| restartEvent := func() { | ||
|
|
@@ -359,6 +368,7 @@ func startInProcess( | |
| gen, | ||
| tracerProviderOptions, | ||
| tmtypes.DefaultConsensusPolicy(), | ||
| node.WithFreezeHeight(config.FreezeHeight), | ||
| ) | ||
| if err != nil { | ||
| return fmt.Errorf("error creating node: %w", err) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -108,6 +108,7 @@ type SyncerConfig struct { | |
| EventBus *eventbus.EventBus | ||
| RestartEvent func() | ||
| SelfRemediationConfig *config.SelfRemediationConfig | ||
| FreezeHeight uint64 | ||
| } | ||
|
|
||
| // Reactor owns the blocksync channel and always-on query serving path, while | ||
|
|
@@ -154,6 +155,7 @@ type syncController struct { | |
| blocksBehindThreshold uint64 | ||
| blocksBehindCheckInterval time.Duration | ||
| restartCooldownSeconds uint64 | ||
| freezeHeight uint64 | ||
|
|
||
| // blocksyncReady fires when the active sync routines should begin processing | ||
| // work, either during OnStart or later via SwitchToBlockSync. | ||
|
|
@@ -186,6 +188,7 @@ func NewReactor( | |
| blocksBehindThreshold: cfg.SelfRemediationConfig.BlocksBehindThreshold, | ||
| blocksBehindCheckInterval: time.Duration(cfg.SelfRemediationConfig.BlocksBehindCheckIntervalSeconds) * time.Second, //nolint:gosec // validated in config.ValidateBasic against MaxInt64 | ||
| restartCooldownSeconds: cfg.SelfRemediationConfig.RestartCooldownSeconds, | ||
| freezeHeight: cfg.FreezeHeight, | ||
| blocksyncReady: utils.NewAtomicSend(utils.None[blocksyncResult]()), | ||
| startInBlockSync: cfg.BlockSync, | ||
| } | ||
|
|
@@ -375,6 +378,9 @@ func (s *syncController) run(ctx context.Context) error { | |
| if r, ok := s.consReactor.Get(); ok { | ||
| logger.Info("switching to consensus reactor", "height", handoff.height, "blocks_synced", handoff.blocksSynced, "state_synced", handoff.stateSynced, "max_peer_height", handoff.maxPeerHeight) | ||
| r.SwitchToConsensus(handoff.state, handoff.blocksSynced > 0 || handoff.stateSynced) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Switch to consensus means we still write to wal right? not a huge issue but worth gating any state writes in freeze mode in case it results in hands on involvement to bring the node out of freeze mode. Operationally, we want the freeze mode to basically be noop for consensus.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added a check to only write to WAL if not frozen |
||
| if s.shouldFreeze(handoff.state) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the handoff state is not already in freeze boundary? IIUC, that means:
We can avoid this by checking the freeze boundary inside
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good catch. I made auto remediation also respect the freeze height which I believe would prevent the restart |
||
| return nil | ||
| } | ||
| s.autoRestartIfBehind(ctx, pool) | ||
| } | ||
| return nil | ||
|
|
@@ -462,6 +468,10 @@ func (s *syncController) requestRoutine(ctx context.Context, pool *BlockPool) er | |
| // | ||
| // NOTE: Don't sleep in the FOR_LOOP or otherwise slow it down! | ||
| func (s *syncController) poolRoutine(ctx context.Context, pool *BlockPool, initialState sm.State, stateSynced bool) (consensusHandoff, error) { | ||
| if handoff, frozen := s.frozenHandoff(pool, initialState, 0, stateSynced); frozen { | ||
| return handoff, nil | ||
| } | ||
|
|
||
| var ( | ||
| trySyncTicker = time.NewTicker(trySyncIntervalMS * time.Millisecond) | ||
| switchToConsensusTicker = time.NewTicker(switchToConsensusIntervalSeconds * time.Second) | ||
|
|
@@ -581,6 +591,9 @@ func (s *syncController) poolRoutine(ctx context.Context, pool *BlockPool, initi | |
|
|
||
| consensus.Global.RecordConsMetrics(first) | ||
| blocksSynced++ | ||
| if handoff, frozen := s.frozenHandoff(pool, state, blocksSynced, stateSynced); frozen { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] This is the call site that matters operationally — stopping an in-flight sync exactly one block before the boundary — and it is untested. A test that applies a block and asserts the handoff carries |
||
| return handoff, nil | ||
| } | ||
|
|
||
| if blocksSynced%100 == 0 { | ||
| lastRate = 0.9*lastRate + 0.1*(100/time.Since(lastHundred).Seconds()) | ||
|
|
@@ -596,6 +609,26 @@ func (s *syncController) poolRoutine(ctx context.Context, pool *BlockPool, initi | |
| } | ||
| } | ||
|
|
||
| func (s *syncController) frozenHandoff(pool *BlockPool, state sm.State, blocksSynced uint64, stateSynced bool) (consensusHandoff, bool) { | ||
| if !s.shouldFreeze(state) { | ||
| return consensusHandoff{}, false | ||
| } | ||
| height, _, _ := pool.GetStatus() | ||
| logger.Info("Block sync stopped before configured freeze height", "last_block_height", state.LastBlockHeight, "freeze_height", s.freezeHeight) | ||
| return consensusHandoff{ | ||
| state: state, | ||
| blocksSynced: blocksSynced, | ||
| stateSynced: stateSynced, | ||
| height: height, | ||
| maxPeerHeight: pool.MaxPeerHeight(), | ||
| }, true | ||
| } | ||
|
|
||
| func (s *syncController) shouldFreeze(state sm.State) bool { | ||
| height := startHeightForState(state) | ||
| return s.freezeHeight > 0 && height >= 0 && uint64(height) >= s.freezeHeight //nolint:gosec // negative heights are rejected first. | ||
| } | ||
|
|
||
| // autoRestartIfBehind will check if the node is behind the max peer height by | ||
| // a certain threshold. If it is, the node will attempt to restart itself. | ||
| // TODO(gprusak): this should be a sub task of the consensus reactor instead. | ||
|
|
@@ -612,6 +645,10 @@ func (s *syncController) autoRestartIfBehind(ctx context.Context, pool *BlockPoo | |
| select { | ||
| case <-time.After(s.blocksBehindCheckInterval): | ||
| selfHeight := s.store.Height() | ||
| if s.freezeHeight > 0 && selfHeight >= 0 && uint64(selfHeight) >= s.freezeHeight-1 { //nolint:gosec // negative heights are rejected first. | ||
| logger.Info("Auto remediation stopped at configured freeze height", "selfHeight", selfHeight, "freeze_height", s.freezeHeight) | ||
| return | ||
| } | ||
| maxPeerHeight := pool.MaxPeerHeight() | ||
| threshold := int64(s.blocksBehindThreshold) //nolint:gosec // validated in config.ValidateBasic against MaxInt64 | ||
| behindHeight := maxPeerHeight - selfHeight | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] This adds two more rejection conditions to
ValidateBasic, which makesFuzzConfigValidateBasic's doc comment stale — it opens "pins the two conditions that reject an otherwise parseable app.toml" and enumerates only min-gas-prices and pruning-everything + snapshots (config_fuzz_test.go:693). That fuzz surface also never variesfreeze-height, so the new conditions sit outside it; they are pinned only by theTestValidateBasictable rows.In a package where these comments are the characterization record, worth updating the count/enumeration (and ideally threading
freeze-height/halt-heightthrough the fuzz inputs).