From 07bdb2fc9372bd372b0b2415bafb830fbe2daf7e Mon Sep 17 00:00:00 2001 From: "Masih H. Derkani" Date: Mon, 24 Aug 2026 15:20:29 +0100 Subject: [PATCH 1/4] Disable mempool traffic in freeze mode Freeze mode prevents the node from executing future blocks, so transactions accepted through RPC or P2P cannot be committed. Continuing to validate, retain, and gossip them wastes application and network resources and leaves stale transactions in the mempool. Keep the internal mempool required by block execution, but do not expose it to RPC or start the mempool reactor when freeze mode is enabled. --- sei-tendermint/node/freeze_test.go | 32 ++++++++++++++++++++++++++++++ sei-tendermint/node/node.go | 14 +++++++------ sei-tendermint/node/node_test.go | 3 ++- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/sei-tendermint/node/freeze_test.go b/sei-tendermint/node/freeze_test.go index 12125881cf..94a0b58501 100644 --- a/sei-tendermint/node/freeze_test.go +++ b/sei-tendermint/node/freeze_test.go @@ -3,6 +3,9 @@ package node import ( "math" "testing" + + "github.com/sei-protocol/sei-chain/sei-tendermint/config" + mempoolreactor "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool/reactor" ) func TestValidateFreezeHeight(t *testing.T) { @@ -39,3 +42,32 @@ func TestWithFreezeHeight(t *testing.T) { t.Fatalf("freeze height = %d, want %d", got, height) } } + +func TestFreezeModeDisablesMempoolTraffic(t *testing.T) { + cfg, err := config.ResetTestRoot(t.TempDir(), "freeze_mempool_test") + if err != nil { + t.Fatal(err) + } + nodeService, err := newLocalNodeService(t.Context(), cfg, WithFreezeHeight(2)) + if err != nil { + t.Fatal(err) + } + node := nodeService.(*nodeImpl) + t.Cleanup(func() { + _ = node.shutdownOps() + _ = node.blockStore.Close() + _ = node.stateStore.Close() + }) + + if !node.mempool.IsPresent() { + t.Fatal("internal mempool is unavailable") + } + if node.rpcEnv.Mempool.IsPresent() { + t.Fatal("RPC mempool is available in freeze mode") + } + for _, nodeService := range node.services { + if _, ok := nodeService.(*mempoolreactor.Reactor); ok { + t.Fatal("mempool reactor is enabled in freeze mode") + } + } +} diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index 0ac292db3a..6b27ac04e8 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -332,13 +332,15 @@ func makeNode( if !gigaEnabled { mp := mempool.NewTxMempool(cfg.Mempool.ToMempoolConfig(), proxyApp, sm.TxConstraintsFetcherFromStore(stateStore)) node.mempool = utils.Some(mp) - node.rpcEnv.Mempool = utils.Some(mp) - mpReactor, err := mempoolreactor.NewReactor(cfg.Mempool, mp, router) - if err != nil { - return nil, fmt.Errorf("mempoolreactor.NewReactor(): %w", err) + if opts.freezeHeight == 0 { + node.rpcEnv.Mempool = utils.Some(mp) + mpReactor, err := mempoolreactor.NewReactor(cfg.Mempool, mp, router) + if err != nil { + return nil, fmt.Errorf("mempoolreactor.NewReactor(): %w", err) + } + mpReactor.MarkReadyToStart() + node.services = append(node.services, mpReactor) } - mpReactor.MarkReadyToStart() - node.services = append(node.services, mpReactor) // make block executor for consensus and blockchain reactors to execute blocks blockExec := sm.NewBlockExecutor( diff --git a/sei-tendermint/node/node_test.go b/sei-tendermint/node/node_test.go index bdfd1db87a..c34707e0fc 100644 --- a/sei-tendermint/node/node_test.go +++ b/sei-tendermint/node/node_test.go @@ -40,7 +40,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) -func newLocalNodeService(ctx context.Context, cfg *config.Config) (service.Service, error) { +func newLocalNodeService(ctx context.Context, cfg *config.Config, nodeOptions ...Option) (service.Service, error) { app := kvstore.NewApplication() app.SetValidators(utils.OrPanic1(types.GenesisDocFromFile(cfg.GenesisFile())).ValidatorUpdates()) return New( @@ -51,6 +51,7 @@ func newLocalNodeService(ctx context.Context, cfg *config.Config) (service.Servi nil, nil, types.DefaultConsensusPolicy(), + nodeOptions..., ) } From fda4ee52dbb17375b954e2babd41edb79519ea12 Mon Sep 17 00:00:00 2001 From: "Masih H. Derkani" Date: Mon, 24 Aug 2026 16:26:50 +0100 Subject: [PATCH 2/4] Address AI review comments --- sei-tendermint/internal/rpc/core/env.go | 3 ++ sei-tendermint/internal/rpc/core/mempool.go | 16 ++++++ sei-tendermint/node/freeze_test.go | 56 ++++++++++++++++++--- sei-tendermint/node/node.go | 6 ++- sei-tendermint/node/setup.go | 41 ++++++++------- 5 files changed, 96 insertions(+), 26 deletions(-) diff --git a/sei-tendermint/internal/rpc/core/env.go b/sei-tendermint/internal/rpc/core/env.go index 5a2cc310db..62ba03bfed 100644 --- a/sei-tendermint/internal/rpc/core/env.go +++ b/sei-tendermint/internal/rpc/core/env.go @@ -93,6 +93,9 @@ type Environment struct { Config config.RPCConfig + // TxBroadcastDisabled rejects transaction submission while preserving mempool reads. + TxBroadcastDisabled bool + // cache of chunked genesis data. genChunks []string } diff --git a/sei-tendermint/internal/rpc/core/mempool.go b/sei-tendermint/internal/rpc/core/mempool.go index 13a7cc82e4..17f2167f56 100644 --- a/sei-tendermint/internal/rpc/core/mempool.go +++ b/sei-tendermint/internal/rpc/core/mempool.go @@ -50,6 +50,9 @@ func (env *Environment) EvmTxByHash(hash common.Hash) (types.Tx, bool) { // https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_async // Deprecated and should be removed in 0.37 func (env *Environment) BroadcastTxAsync(ctx context.Context, req *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) { + if err := env.requireTxBroadcast(); err != nil { + return nil, err + } if giga, ok := env.gigaRouter().Get(); ok { v, ok := giga.Mempool().Get() if !ok { @@ -76,6 +79,9 @@ func (env *Environment) BroadcastTxSync(ctx context.Context, req *coretypes.Requ // DeliverTx result. // More: https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_sync func (env *Environment) BroadcastTx(ctx context.Context, req *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) { + if err := env.requireTxBroadcast(); err != nil { + return nil, err + } if giga, ok := env.gigaRouter().Get(); ok { v, ok := giga.Mempool().Get() if !ok { @@ -113,6 +119,9 @@ func (env *Environment) BroadcastTx(ctx context.Context, req *coretypes.RequestB // BroadcastTxCommit returns with the responses from CheckTx and DeliverTx. // More: https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_commit func (env *Environment) BroadcastTxCommit(ctx context.Context, req *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTxCommit, error) { + if err := env.requireTxBroadcast(); err != nil { + return nil, err + } if timeout := env.Config.TimeoutBroadcastTxCommit; timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) @@ -202,6 +211,13 @@ func (env *Environment) broadcastTxCommitFromCheckTx(ctx context.Context, req *c } } +func (env *Environment) requireTxBroadcast() error { + if env.TxBroadcastDisabled { + return errors.New("transaction broadcast is disabled in freeze mode") + } + return nil +} + // UnconfirmedTxs gets unconfirmed transactions from the mempool in order of priority // More: https://docs.tendermint.com/master/rpc/#/Info/unconfirmed_txs func (env *Environment) UnconfirmedTxs(ctx context.Context, req *coretypes.RequestUnconfirmedTxs) (*coretypes.ResultUnconfirmedTxs, error) { diff --git a/sei-tendermint/node/freeze_test.go b/sei-tendermint/node/freeze_test.go index 94a0b58501..284b7fbfa1 100644 --- a/sei-tendermint/node/freeze_test.go +++ b/sei-tendermint/node/freeze_test.go @@ -1,11 +1,16 @@ package node import ( + "fmt" "math" + "slices" "testing" "github.com/sei-protocol/sei-chain/sei-tendermint/config" mempoolreactor "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool/reactor" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/tcp" + "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" + "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) func TestValidateFreezeHeight(t *testing.T) { @@ -48,26 +53,63 @@ func TestFreezeModeDisablesMempoolTraffic(t *testing.T) { if err != nil { t.Fatal(err) } + cfg.RPC.ListenAddress = fmt.Sprintf("tcp://%s", tcp.TestReserveAddr()) nodeService, err := newLocalNodeService(t.Context(), cfg, WithFreezeHeight(2)) if err != nil { t.Fatal(err) } node := nodeService.(*nodeImpl) - t.Cleanup(func() { - _ = node.shutdownOps() - _ = node.blockStore.Close() - _ = node.stateStore.Close() - }) if !node.mempool.IsPresent() { t.Fatal("internal mempool is unavailable") } - if node.rpcEnv.Mempool.IsPresent() { - t.Fatal("RPC mempool is available in freeze mode") + if !node.rpcEnv.Mempool.IsPresent() { + t.Fatal("RPC mempool reads are unavailable in freeze mode") + } + if !node.rpcEnv.TxBroadcastDisabled { + t.Fatal("RPC transaction broadcast is enabled in freeze mode") + } + txRequest := &coretypes.RequestBroadcastTx{Tx: types.Tx{1}} + for name, broadcast := range map[string]func() error{ + "async": func() error { + _, err := node.rpcEnv.BroadcastTxAsync(t.Context(), txRequest) + return err + }, + "sync": func() error { + _, err := node.rpcEnv.BroadcastTxSync(t.Context(), txRequest) + return err + }, + "default": func() error { + _, err := node.rpcEnv.BroadcastTx(t.Context(), txRequest) + return err + }, + "commit": func() error { + _, err := node.rpcEnv.BroadcastTxCommit(t.Context(), txRequest) + return err + }, + } { + if err := broadcast(); err == nil { + t.Fatalf("%s RPC transaction broadcast succeeded in freeze mode", name) + } + } + if pending, err := node.rpcEnv.UnconfirmedTxs(t.Context(), &coretypes.RequestUnconfirmedTxs{}); err != nil { + t.Fatalf("reading unconfirmed transactions: %v", err) + } else if pending.Total != 0 { + t.Fatalf("unconfirmed transaction total = %d, want 0", pending.Total) } for _, nodeService := range node.services { if _, ok := nodeService.(*mempoolreactor.Reactor); ok { t.Fatal("mempool reactor is enabled in freeze mode") } } + if err := node.Start(t.Context()); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + node.Stop() + node.Wait() + }) + if slices.Contains(node.NodeInfo().Channels, byte(mempoolreactor.MempoolChannel)) { + t.Fatal("mempool channel is advertised in freeze mode") + } } diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index 6b27ac04e8..b25231b5b4 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -273,6 +273,8 @@ func makeNode( EventBus: eventBus, EventLog: eventLogOpt, Config: *cfg.RPC, + + TxBroadcastDisabled: opts.freezeHeight > 0, }, } @@ -332,8 +334,8 @@ func makeNode( if !gigaEnabled { mp := mempool.NewTxMempool(cfg.Mempool.ToMempoolConfig(), proxyApp, sm.TxConstraintsFetcherFromStore(stateStore)) node.mempool = utils.Some(mp) + node.rpcEnv.Mempool = utils.Some(mp) if opts.freezeHeight == 0 { - node.rpcEnv.Mempool = utils.Some(mp) mpReactor, err := mempoolreactor.NewReactor(cfg.Mempool, mp, router) if err != nil { return nil, fmt.Errorf("mempoolreactor.NewReactor(): %w", err) @@ -564,7 +566,7 @@ func (n *nodeImpl) OnStart(ctx context.Context) (err error) { // TODO: Fetch and provide real options and do proper p2p bootstrapping. // TODO: Use a persistent peer database. - n.nodeInfo, err = makeNodeInfo(n.config, n.nodeKey, n.eventSinks, n.genesisDoc, state.Version.Consensus) + n.nodeInfo, err = makeNodeInfo(n.config, n.nodeKey, n.eventSinks, n.genesisDoc, state.Version.Consensus, n.freezeHeight == 0) if err != nil { return err } diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index 092f4c3e16..fba83ea8dc 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -633,6 +633,7 @@ func makeNodeInfo( eventSinks []indexer.EventSink, genDoc *types.GenesisDoc, versionInfo version.Consensus, + mempoolP2PEnabled bool, ) (types.NodeInfo, error) { txIndexerStatus := "off" @@ -641,29 +642,35 @@ func makeNodeInfo( txIndexerStatus = "on" } + channels := []byte{ + byte(blocksync.BlockSyncChannel), + byte(consensus.StateChannel), + byte(consensus.DataChannel), + byte(consensus.VoteChannel), + byte(consensus.VoteSetBitsChannel), + } + if mempoolP2PEnabled { + channels = append(channels, byte(mempoolreactor.MempoolChannel)) + } + channels = append(channels, + byte(evidence.EvidenceChannel), + byte(statesync.SnapshotChannel), + byte(statesync.ChunkChannel), + byte(statesync.LightBlockChannel), + byte(statesync.ParamsChannel), + ) + nodeInfo := types.NodeInfo{ ProtocolVersion: types.ProtocolVersion{ P2P: version.P2PProtocol, // global Block: versionInfo.Block, App: versionInfo.App, }, - NodeID: nodeKey.ID(), - Network: genDoc.ChainID, - Version: version.TMVersion, - Channels: []byte{ - byte(blocksync.BlockSyncChannel), - byte(consensus.StateChannel), - byte(consensus.DataChannel), - byte(consensus.VoteChannel), - byte(consensus.VoteSetBitsChannel), - byte(mempoolreactor.MempoolChannel), - byte(evidence.EvidenceChannel), - byte(statesync.SnapshotChannel), - byte(statesync.ChunkChannel), - byte(statesync.LightBlockChannel), - byte(statesync.ParamsChannel), - }, - Moniker: cfg.Moniker, + NodeID: nodeKey.ID(), + Network: genDoc.ChainID, + Version: version.TMVersion, + Channels: channels, + Moniker: cfg.Moniker, Other: types.NodeInfoOther{ TxIndex: txIndexerStatus, RPCAddress: cfg.RPC.ListenAddress, From 202e7d0fcfc2a97cbb7157788e96aec4aa621d2b Mon Sep 17 00:00:00 2001 From: "Masih H. Derkani" Date: Tue, 25 Aug 2026 11:51:15 +0100 Subject: [PATCH 3/4] Fix issue pointed out by AI and update docs --- CHANGELOG.md | 1 + sei-cosmos/server/config/config.go | 5 +++-- sei-cosmos/server/config/toml.go | 5 +++-- sei-cosmos/server/start.go | 7 ++++--- sei-tendermint/node/freeze_test.go | 23 +++++++++++++++++++++++ sei-tendermint/node/node.go | 28 +++++++++++++++------------- sei-tendermint/node/public.go | 19 ++++++++++++++++--- 7 files changed, 65 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c24987da88..4a1a112594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Improvements * [#3818](https://github.com/sei-protocol/sei-chain/pull/3818) feat(evmrpc): extend HTTP admission control (`max_request_body_bytes`, `max_concurrent_request_bytes`, `ws_admission_timeout`) to the WebSocket plane (:8546). WS oversize frames close with WebSocket close code 1009; budget-wait timeouts return JSON-RPC error `-32005` before the connection closes. `evmrpc_requests_rejected_total` gains a `protocol` label (`http` / `ws`). * [#3984](https://github.com/sei-protocol/sei-chain/pull/3984) feat(query): origin-aware pagination limits for ABCI queries. Untrusted callers on the ABCI/gRPC query path get configurable `max-limit`, `max-offset`, and flat `max-iterations` (defaults: 1000 / 10000 / 11000); requests above the caps are rejected upfront, and an exhausted iteration budget returns a partial page with `next_key` instead of failing. Trusted origins (new `[query] trusted-cidrs`) and the `[query] disable-limits` kill switch bypass the caps; the consensus/EVM precompile path is unaffected. +* [#3990](https://github.com/sei-protocol/sei-chain/pull/3990) Freeze mode is limited to full nodes and disables transaction submission, mempool gossip, and state sync from startup while preserving query RPC and mempool-backed reads. Frozen and Autobahn nodes no longer advertise the unused mempool P2P channel. ### Upgrade guide * **IBC transfer removal.** Removes ICS-20 execution, module APIs, CLI commands, CosmWasm transfer messages, transfer codecs, and transfer keeper integration, including the IBC EVM precompile's keeper injection. The transfer store and module account remain materialized for state compatibility. Transfer queries, historical transfer transaction decoding, and pre-v6.7 IBC precompile tracing must be served by v6.6 freeze nodes; v6.7 nodes do not provide them. diff --git a/sei-cosmos/server/config/config.go b/sei-cosmos/server/config/config.go index 3cecd080c6..23941ec3e6 100644 --- a/sei-cosmos/server/config/config.go +++ b/sei-cosmos/server/config/config.go @@ -114,8 +114,9 @@ type BaseConfig struct { // Note: Commitment of state will be attempted on the corresponding block. HaltHeight uint64 `mapstructure:"halt-height"` - // FreezeHeight contains a non-zero block height at which the node stops - // before executing the block while continuing to serve RPC. + // FreezeHeight contains the first block height a full node must not execute. + // Query RPC remains available, while transaction submission and mempool gossip + // are disabled from startup. FreezeHeight uint64 `mapstructure:"freeze-height"` // HaltTime contains a non-zero minimum block time (in Unix seconds) at which diff --git a/sei-cosmos/server/config/toml.go b/sei-cosmos/server/config/toml.go index 5b6a4165e8..cd6865eae7 100644 --- a/sei-cosmos/server/config/toml.go +++ b/sei-cosmos/server/config/toml.go @@ -41,8 +41,9 @@ occ-enabled = {{ .BaseConfig.OccEnabled }} # Note: Commitment of state will be attempted on the corresponding block. halt-height = {{ .BaseConfig.HaltHeight }} -# FreezeHeight contains a non-zero block height at which the node stops before -# executing the block while continuing to serve RPC. +# FreezeHeight contains the first block height a full node must not execute. +# Query RPC remains available, while transaction submission and mempool gossip +# are disabled from startup. freeze-height = {{ .BaseConfig.FreezeHeight }} # HaltTime contains a non-zero minimum block time (in Unix seconds) at which diff --git a/sei-cosmos/server/start.go b/sei-cosmos/server/start.go index 98cae88149..9f24305373 100644 --- a/sei-cosmos/server/start.go +++ b/sei-cosmos/server/start.go @@ -103,8 +103,9 @@ 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. +The '--freeze-height' flag puts a full node in read-only freeze mode. Query RPC remains available, +but transaction submission, mempool gossip, and state sync are disabled from startup. Block sync +and consensus stop before executing the configured height. 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 @@ -211,7 +212,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(FlagFreezeHeight, 0, "Block height at which a full node stops executing while continuing to serve query 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") diff --git a/sei-tendermint/node/freeze_test.go b/sei-tendermint/node/freeze_test.go index 284b7fbfa1..40cd4c917d 100644 --- a/sei-tendermint/node/freeze_test.go +++ b/sei-tendermint/node/freeze_test.go @@ -48,11 +48,34 @@ func TestWithFreezeHeight(t *testing.T) { } } +func TestValidateFreezeMode(t *testing.T) { + for _, tc := range []struct { + name string + mode string + freezeHeight uint64 + wantErr bool + }{ + {name: "disabled validator", mode: config.ModeValidator}, + {name: "full node", mode: config.ModeFull, freezeHeight: 10}, + {name: "validator", mode: config.ModeValidator, freezeHeight: 10, wantErr: true}, + {name: "seed", mode: config.ModeSeed, freezeHeight: 10, wantErr: true}, + {name: "unknown mode handled by node mode validation", mode: "unknown", freezeHeight: 10}, + } { + t.Run(tc.name, func(t *testing.T) { + err := validateFreezeMode(tc.mode, tc.freezeHeight) + if (err != nil) != tc.wantErr { + t.Fatalf("validateFreezeMode() error = %v, wantErr %t", err, tc.wantErr) + } + }) + } +} + func TestFreezeModeDisablesMempoolTraffic(t *testing.T) { cfg, err := config.ResetTestRoot(t.TempDir(), "freeze_mempool_test") if err != nil { t.Fatal(err) } + cfg.Mode = config.ModeFull cfg.RPC.ListenAddress = fmt.Sprintf("tcp://%s", tcp.TestReserveAddr()) nodeService, err := newLocalNodeService(t.Context(), cfg, WithFreezeHeight(2)) if err != nil { diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index b25231b5b4..091fd07734 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -133,18 +133,19 @@ type nodeImpl struct { nodeKey types.NodeKey // our node privkey // services - eventSinks []indexer.EventSink - initialState sm.State - stateStore sm.Store - blockStore *store.BlockStore // store the blockchain to disk - mempool utils.Option[*mempool.TxMempool] - evPool utils.Option[*evidence.Pool] - indexerService *indexer.Service - services []service.Service - rpcListeners []net.Listener // rpc servers - shutdownOps closer - rpcEnv *rpccore.Environment - prometheusSrv utils.Option[*http.Server] + eventSinks []indexer.EventSink + initialState sm.State + stateStore sm.Store + blockStore *store.BlockStore // store the blockchain to disk + mempool utils.Option[*mempool.TxMempool] + mempoolP2PEnabled bool + evPool utils.Option[*evidence.Pool] + indexerService *indexer.Service + services []service.Service + rpcListeners []net.Listener // rpc servers + shutdownOps closer + rpcEnv *rpccore.Environment + prometheusSrv utils.Option[*http.Server] } // makeNode returns a new, ready to go, Tendermint Node. @@ -342,6 +343,7 @@ func makeNode( } mpReactor.MarkReadyToStart() node.services = append(node.services, mpReactor) + node.mempoolP2PEnabled = true } // make block executor for consensus and blockchain reactors to execute blocks @@ -566,7 +568,7 @@ func (n *nodeImpl) OnStart(ctx context.Context) (err error) { // TODO: Fetch and provide real options and do proper p2p bootstrapping. // TODO: Use a persistent peer database. - n.nodeInfo, err = makeNodeInfo(n.config, n.nodeKey, n.eventSinks, n.genesisDoc, state.Version.Consensus, n.freezeHeight == 0) + n.nodeInfo, err = makeNodeInfo(n.config, n.nodeKey, n.eventSinks, n.genesisDoc, state.Version.Consensus, n.mempoolP2PEnabled) if err != nil { return err } diff --git a/sei-tendermint/node/public.go b/sei-tendermint/node/public.go index 6529b654db..7f48ab460f 100644 --- a/sei-tendermint/node/public.go +++ b/sei-tendermint/node/public.go @@ -54,6 +54,10 @@ func New( if err := validateNodeSetupConfig(conf); err != nil { return nil, err } + opts := resolveOptions(nodeOptions...) + if err := validateFreezeMode(conf.Mode, opts.freezeHeight); err != nil { + return nil, err + } app = prepareApplication(conf, app) proxyApp := proxy.New(app) nodeKey, err := tmtypes.LoadOrGenNodeKey(conf.NodeKeyFile()) @@ -90,9 +94,6 @@ func New( nodeOptions..., ) case config.ModeSeed: - if resolveOptions(nodeOptions...).freezeHeight > 0 { - return nil, fmt.Errorf("freeze height is not supported in seed mode") - } return makeSeedNode( conf, config.DefaultDBProvider, @@ -104,6 +105,18 @@ func New( } } +func validateFreezeMode(mode string, freezeHeight uint64) error { + if freezeHeight == 0 || mode == config.ModeFull { + return nil + } + switch mode { + case config.ModeValidator, config.ModeSeed: + return fmt.Errorf("freeze height is not supported in %s mode", mode) + default: + return nil + } +} + func validateNodeSetupConfig(conf *config.Config) error { if conf.MockApp && conf.AutobahnConfigFile == "" { return fmt.Errorf("mock-app requires autobahn-config-file") From 0cd4a124992f3da915f9adc776dc0f52a36e3d44 Mon Sep 17 00:00:00 2001 From: "Masih H. Derkani" Date: Tue, 25 Aug 2026 13:08:34 +0100 Subject: [PATCH 4/4] Rephrase to RPC read-only to include evidence broadcast --- CHANGELOG.md | 2 +- sei-cosmos/server/config/config.go | 4 ++-- sei-cosmos/server/config/toml.go | 4 ++-- sei-cosmos/server/start.go | 4 ++-- sei-tendermint/internal/rpc/core/env.go | 19 ++++++++++++++++--- sei-tendermint/internal/rpc/core/evidence.go | 3 +++ sei-tendermint/internal/rpc/core/mempool.go | 13 +++---------- sei-tendermint/node/freeze_test.go | 13 +++++++++---- sei-tendermint/node/node.go | 2 +- 9 files changed, 39 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a1a112594..fad75f3fef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Improvements * [#3818](https://github.com/sei-protocol/sei-chain/pull/3818) feat(evmrpc): extend HTTP admission control (`max_request_body_bytes`, `max_concurrent_request_bytes`, `ws_admission_timeout`) to the WebSocket plane (:8546). WS oversize frames close with WebSocket close code 1009; budget-wait timeouts return JSON-RPC error `-32005` before the connection closes. `evmrpc_requests_rejected_total` gains a `protocol` label (`http` / `ws`). * [#3984](https://github.com/sei-protocol/sei-chain/pull/3984) feat(query): origin-aware pagination limits for ABCI queries. Untrusted callers on the ABCI/gRPC query path get configurable `max-limit`, `max-offset`, and flat `max-iterations` (defaults: 1000 / 10000 / 11000); requests above the caps are rejected upfront, and an exhausted iteration budget returns a partial page with `next_key` instead of failing. Trusted origins (new `[query] trusted-cidrs`) and the `[query] disable-limits` kill switch bypass the caps; the consensus/EVM precompile path is unaffected. -* [#3990](https://github.com/sei-protocol/sei-chain/pull/3990) Freeze mode is limited to full nodes and disables transaction submission, mempool gossip, and state sync from startup while preserving query RPC and mempool-backed reads. Frozen and Autobahn nodes no longer advertise the unused mempool P2P channel. +* [#3990](https://github.com/sei-protocol/sei-chain/pull/3990) Freeze mode is limited to full nodes and disables transaction and evidence submission, mempool gossip, and state sync from startup while preserving query RPC and mempool-backed reads. Frozen and Autobahn nodes no longer advertise the unused mempool P2P channel. ### Upgrade guide * **IBC transfer removal.** Removes ICS-20 execution, module APIs, CLI commands, CosmWasm transfer messages, transfer codecs, and transfer keeper integration, including the IBC EVM precompile's keeper injection. The transfer store and module account remain materialized for state compatibility. Transfer queries, historical transfer transaction decoding, and pre-v6.7 IBC precompile tracing must be served by v6.6 freeze nodes; v6.7 nodes do not provide them. diff --git a/sei-cosmos/server/config/config.go b/sei-cosmos/server/config/config.go index 23941ec3e6..ee732b85c9 100644 --- a/sei-cosmos/server/config/config.go +++ b/sei-cosmos/server/config/config.go @@ -115,8 +115,8 @@ type BaseConfig struct { HaltHeight uint64 `mapstructure:"halt-height"` // FreezeHeight contains the first block height a full node must not execute. - // Query RPC remains available, while transaction submission and mempool gossip - // are disabled from startup. + // Query RPC remains available, while transaction and evidence submission, + // mempool gossip, and state sync are disabled from startup. FreezeHeight uint64 `mapstructure:"freeze-height"` // HaltTime contains a non-zero minimum block time (in Unix seconds) at which diff --git a/sei-cosmos/server/config/toml.go b/sei-cosmos/server/config/toml.go index cd6865eae7..eb7d7f4ea0 100644 --- a/sei-cosmos/server/config/toml.go +++ b/sei-cosmos/server/config/toml.go @@ -42,8 +42,8 @@ occ-enabled = {{ .BaseConfig.OccEnabled }} halt-height = {{ .BaseConfig.HaltHeight }} # FreezeHeight contains the first block height a full node must not execute. -# Query RPC remains available, while transaction submission and mempool gossip -# are disabled from startup. +# Query RPC remains available, while transaction and evidence submission, +# mempool gossip, and state sync are disabled from startup. freeze-height = {{ .BaseConfig.FreezeHeight }} # HaltTime contains a non-zero minimum block time (in Unix seconds) at which diff --git a/sei-cosmos/server/start.go b/sei-cosmos/server/start.go index 9f24305373..ff9e416b75 100644 --- a/sei-cosmos/server/start.go +++ b/sei-cosmos/server/start.go @@ -104,8 +104,8 @@ the halt-height or if the current block time is greater than or equal to the hal 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 puts a full node in read-only freeze mode. Query RPC remains available, -but transaction submission, mempool gossip, and state sync are disabled from startup. Block sync -and consensus stop before executing the configured height. +but transaction and evidence submission, mempool gossip, and state sync are disabled from startup. +Block sync and consensus stop before executing the configured height. 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 diff --git a/sei-tendermint/internal/rpc/core/env.go b/sei-tendermint/internal/rpc/core/env.go index 62ba03bfed..0b6e2560d9 100644 --- a/sei-tendermint/internal/rpc/core/env.go +++ b/sei-tendermint/internal/rpc/core/env.go @@ -3,6 +3,7 @@ package core import ( "context" "encoding/base64" + "errors" "fmt" "net" "net/http" @@ -48,7 +49,12 @@ const ( genesisChunkSize = 16 * 1024 * 1024 // 16 ) -var logger = seilog.NewLogger("tendermint", "internal", "rpc", "core") +var ( + logger = seilog.NewLogger("tendermint", "internal", "rpc", "core") + + // ErrReadOnly indicates that an RPC operation would mutate a read-only node. + ErrReadOnly = errors.New("RPC writes are disabled in freeze mode") +) //---------------------------------------------- // These interfaces are used by RPC and must be thread safe @@ -93,8 +99,8 @@ type Environment struct { Config config.RPCConfig - // TxBroadcastDisabled rejects transaction submission while preserving mempool reads. - TxBroadcastDisabled bool + // ReadOnly rejects RPC writes while preserving query and mempool reads. + ReadOnly bool // cache of chunked genesis data. genChunks []string @@ -233,6 +239,13 @@ func (env *Environment) requireMempool() (*mempool.TxMempool, error) { return nil, fmt.Errorf("mempool is not available") } +func (env *Environment) requireWritable() error { + if env.ReadOnly { + return ErrReadOnly + } + return nil +} + func (env *Environment) requireEventLog() (*eventlog.Log, error) { if lg, ok := env.EventLog.Get(); ok { return lg, nil diff --git a/sei-tendermint/internal/rpc/core/evidence.go b/sei-tendermint/internal/rpc/core/evidence.go index 66015b110f..4de2928eb6 100644 --- a/sei-tendermint/internal/rpc/core/evidence.go +++ b/sei-tendermint/internal/rpc/core/evidence.go @@ -10,6 +10,9 @@ import ( // BroadcastEvidence broadcasts evidence of the misbehavior. // More: https://docs.tendermint.com/master/rpc/#/Evidence/broadcast_evidence func (env *Environment) BroadcastEvidence(ctx context.Context, req *coretypes.RequestBroadcastEvidence) (*coretypes.ResultBroadcastEvidence, error) { + if err := env.requireWritable(); err != nil { + return nil, err + } if req.Evidence == nil { return nil, fmt.Errorf("%w: no evidence was provided", coretypes.ErrInvalidRequest) } diff --git a/sei-tendermint/internal/rpc/core/mempool.go b/sei-tendermint/internal/rpc/core/mempool.go index 17f2167f56..aa31bd5950 100644 --- a/sei-tendermint/internal/rpc/core/mempool.go +++ b/sei-tendermint/internal/rpc/core/mempool.go @@ -50,7 +50,7 @@ func (env *Environment) EvmTxByHash(hash common.Hash) (types.Tx, bool) { // https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_async // Deprecated and should be removed in 0.37 func (env *Environment) BroadcastTxAsync(ctx context.Context, req *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) { - if err := env.requireTxBroadcast(); err != nil { + if err := env.requireWritable(); err != nil { return nil, err } if giga, ok := env.gigaRouter().Get(); ok { @@ -79,7 +79,7 @@ func (env *Environment) BroadcastTxSync(ctx context.Context, req *coretypes.Requ // DeliverTx result. // More: https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_sync func (env *Environment) BroadcastTx(ctx context.Context, req *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) { - if err := env.requireTxBroadcast(); err != nil { + if err := env.requireWritable(); err != nil { return nil, err } if giga, ok := env.gigaRouter().Get(); ok { @@ -119,7 +119,7 @@ func (env *Environment) BroadcastTx(ctx context.Context, req *coretypes.RequestB // BroadcastTxCommit returns with the responses from CheckTx and DeliverTx. // More: https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_commit func (env *Environment) BroadcastTxCommit(ctx context.Context, req *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTxCommit, error) { - if err := env.requireTxBroadcast(); err != nil { + if err := env.requireWritable(); err != nil { return nil, err } if timeout := env.Config.TimeoutBroadcastTxCommit; timeout > 0 { @@ -211,13 +211,6 @@ func (env *Environment) broadcastTxCommitFromCheckTx(ctx context.Context, req *c } } -func (env *Environment) requireTxBroadcast() error { - if env.TxBroadcastDisabled { - return errors.New("transaction broadcast is disabled in freeze mode") - } - return nil -} - // UnconfirmedTxs gets unconfirmed transactions from the mempool in order of priority // More: https://docs.tendermint.com/master/rpc/#/Info/unconfirmed_txs func (env *Environment) UnconfirmedTxs(ctx context.Context, req *coretypes.RequestUnconfirmedTxs) (*coretypes.ResultUnconfirmedTxs, error) { diff --git a/sei-tendermint/node/freeze_test.go b/sei-tendermint/node/freeze_test.go index 40cd4c917d..587549a9fd 100644 --- a/sei-tendermint/node/freeze_test.go +++ b/sei-tendermint/node/freeze_test.go @@ -1,6 +1,7 @@ package node import ( + "errors" "fmt" "math" "slices" @@ -8,6 +9,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/config" mempoolreactor "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool/reactor" + rpccore "github.com/sei-protocol/sei-chain/sei-tendermint/internal/rpc/core" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/tcp" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" "github.com/sei-protocol/sei-chain/sei-tendermint/types" @@ -89,8 +91,8 @@ func TestFreezeModeDisablesMempoolTraffic(t *testing.T) { if !node.rpcEnv.Mempool.IsPresent() { t.Fatal("RPC mempool reads are unavailable in freeze mode") } - if !node.rpcEnv.TxBroadcastDisabled { - t.Fatal("RPC transaction broadcast is enabled in freeze mode") + if !node.rpcEnv.ReadOnly { + t.Fatal("RPC writes are enabled in freeze mode") } txRequest := &coretypes.RequestBroadcastTx{Tx: types.Tx{1}} for name, broadcast := range map[string]func() error{ @@ -111,10 +113,13 @@ func TestFreezeModeDisablesMempoolTraffic(t *testing.T) { return err }, } { - if err := broadcast(); err == nil { - t.Fatalf("%s RPC transaction broadcast succeeded in freeze mode", name) + if err := broadcast(); !errors.Is(err, rpccore.ErrReadOnly) { + t.Fatalf("%s RPC transaction broadcast error = %v, want ErrReadOnly", name, err) } } + if _, err := node.rpcEnv.BroadcastEvidence(t.Context(), &coretypes.RequestBroadcastEvidence{}); !errors.Is(err, rpccore.ErrReadOnly) { + t.Fatalf("RPC evidence broadcast error = %v, want ErrReadOnly", err) + } if pending, err := node.rpcEnv.UnconfirmedTxs(t.Context(), &coretypes.RequestUnconfirmedTxs{}); err != nil { t.Fatalf("reading unconfirmed transactions: %v", err) } else if pending.Total != 0 { diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index 091fd07734..bfc6ba3f86 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -275,7 +275,7 @@ func makeNode( EventLog: eventLogOpt, Config: *cfg.RPC, - TxBroadcastDisabled: opts.freezeHeight > 0, + ReadOnly: opts.freezeHeight > 0, }, }