Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [#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 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.
* [#4009](https://github.com/sei-protocol/sei-chain/pull/4009) Bound `/store/*/subspace` ABCI queries with pair/byte caps, empty-prefix rejection, SS-path concurrency limits, and context-aware iteration to prevent memory-exhaustion DoS.

### Upgrade guide
* **IBC core removal.** Removes the retired IBC core source, protobufs, light clients, CLI, and simulation support. Retired IBC stores remain mounted but are omitted from `export-genesis`; preserve the state database or use v6.6 freeze nodes for historical IBC data.
Expand All @@ -43,6 +44,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [#3958](https://github.com/sei-protocol/sei-chain/pull/3958) **Feegrant removal.** Removes feegrant execution, module APIs, and the unreleased feegrant EVM precompile. The feegrant store remains mounted for historical state access. Transactions with a fee granter different from the payer are rejected.
* **WebSocket frame size default drops from 10 MiB to 5 MiB.** Before this release, :8546 used a hardcoded 10 MiB frame cap. Both HTTP and WebSocket now share `[evm].max_request_body_bytes`, whose default is 5 MiB (`5242880`). WS clients that send frames in the 5-10 MiB range (large `eth_sendRawTransaction` batches, wide filter payloads, etc.) will be disconnected after upgrade unless the limit is raised. **Operators who relied on the old 10 MiB WS cap should set `max_request_body_bytes = 10485760` in `app.toml` before upgrading.** This also raises the HTTP body limit to 10 MiB. The exported `DefaultWebsocketMaxMessageSize` constant was removed; use the config knob instead.
* [#3984](https://github.com/sei-protocol/sei-chain/pull/3984) **ABCI/gRPC pagination is now capped by default.** Untrusted callers requesting `limit` above 1000, `offset` above 10000, or a scan that exceeds 11000 total iterations now get `InvalidArgument` (over-cap) or a partial page with `next_key` (budget exhausted) instead of the previously unbounded scan. Clients that page with large limits/offsets, or trusted internal indexers, should either follow `next_key` for resumption or be added to the new `[query] trusted-cidrs` allowlist (or set `[query] disable-limits = true`) before upgrading.
* [#4009](https://github.com/sei-protocol/sei-chain/pull/4009) **`/store/*/subspace` scans are now capped.** Wide prefix scans that previously returned unbounded KV pairs now fail with `subspace result exceeds limit` once they would exceed the default caps of 1,000 pairs or 4 MiB of accumulated key+value bytes. Empty prefixes are rejected. Indexers and tooling that issue wide `/subspace` queries must narrow their prefixes, shard by sub-prefix, or raise `[state-commit] sc-subspace-max-pairs` and `sc-subspace-max-bytes` before upgrading. Values `<= 0` resolve to these defaults; there is no unlimited setting.
* [#3927](https://github.com/sei-protocol/sei-chain/pull/3927) **Legacy Sei JSON-RPC and CLI removal.** Removes `sei_associate`, `sei_getBlockByHash`, `sei_getBlockByHashExcludeTraceFail`, `sei_getBlockTransactionCountByHash`, `sei_getBlockTransactionCountByNumber`, `sei_getEvmTx`, `sei_getFilterChanges`, `sei_getFilterLogs`, `sei_getLogs`, `sei_getTransactionByBlockHashAndIndex`, `sei_getTransactionByBlockNumberAndIndex`, `sei_getTransactionByHash`, `sei_getTransactionCount`, `sei_getTransactionErrorByHash`, `sei_getTransactionReceiptExcludeTraceFail`, `sei_getVMError`, `sei_newBlockFilter`, `sei_newFilter`, `sei_sign`, and `sei_uninstallFilter`. Use standard `eth_*` methods for EVM-originated data and `seid tx evm native-associate <custom-message> -y` for address association. There is no block- or filter-level replacement for discovering Cosmos-originated synthetic logs; clients that know the synthetic transaction hash can enable `sei_getTransactionReceipt`.

## v6.6
Expand Down
12 changes: 9 additions & 3 deletions app/config_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ var scKeys = []configtest.KeySpec{
{Key: FlagSCHistoricalProofMaxInFlight, Path: "HistoricalProofMaxInFlight", Cast: configtest.CastInt},
{Key: FlagSCHistoricalProofRateLimit, Path: "HistoricalProofRateLimit", Cast: configtest.CastFloat64},
{Key: FlagSCHistoricalProofBurst, Path: "HistoricalProofBurst", Cast: configtest.CastInt},
{Key: FlagSCSubspaceQueryMaxInFlight, Path: "SubspaceQueryMaxInFlight", Cast: configtest.CastInt},
{Key: FlagSCSubspaceMaxPairs, Path: "SubspaceMaxPairs", Cast: configtest.CastInt},
{Key: FlagSCSubspaceMaxBytes, Path: "SubspaceMaxBytes", Cast: configtest.CastInt},
{
Key: FlagSCHashLoggerEnable, Path: "HashLogger.Enable", Cast: configtest.CastBool,
Why: "default true; guarded so an absent key does not silently turn hash logging off",
Expand Down Expand Up @@ -238,15 +241,18 @@ func FuzzParseSCConfigs(f *testing.F) {
seeds.AddRow(uint(4), fuzzing.KindInt64, "", int64(-1), false) // negative into an unchecked unsigned cast: resolves 0
seeds.AddRow(uint(7), fuzzing.KindFloat64, "", int64(2), false) // prefetch threshold as a float
seeds.AddRow(uint(1), fuzzing.KindString, "/var/lib/sei/sc", int64(0), false)
seeds.AddRow(uint(13), fuzzing.KindString, "not-a-bool", int64(0), false) // unchecked: resolves false, no error
seeds.AddRow(uint(15), fuzzing.KindInt64, "", int64(0), false) // explicit 0 taken verbatim
seeds.AddRow(uint(16), fuzzing.KindString, "not-a-bool", int64(0), false) // unchecked: resolves false, no error
seeds.AddRow(uint(18), fuzzing.KindInt64, "", int64(0), false) // explicit 0 taken verbatim

// Two rows default to their cast's zero, which is also what the malformed seed resolves
// to on an unchecked read, so neither of the per-row seeds above moves the field off the
// value an absent key produces. Each gets one value that converts to something else,
// which is what holds the reader to the key name rather than only to the cast.
seeds.AddRow(uint(9), fuzzing.KindBool, "", int64(0), true) // flatkv read/write metrics on; the default is off
seeds.AddRow(uint(15), fuzzing.KindInt64, "", int64(100000), false) // block-count retention on; the default is 0, meaning disabled
seeds.AddRow(uint(18), fuzzing.KindInt64, "", int64(100000), false) // block-count retention on; the default is 0, meaning disabled
seeds.AddRow(uint(13), fuzzing.KindInt64, "", int64(5), false) // subspace max inflight above default 2
seeds.AddRow(uint(14), fuzzing.KindInt64, "", int64(500), false) // subspace max pairs below default 1000
seeds.AddRow(uint(15), fuzzing.KindInt64, "", int64(1024), false) // subspace max bytes below default 4 MiB

configtest.CheckEveryRowHasADiscriminatingSeed(f, "state-commit", readSC, scKeys, seeds,
scKeysWithTargetsOfTheirOwn...)
Expand Down
6 changes: 6 additions & 0 deletions app/config_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ type stateCommitSchema struct {
HistoricalProofMaxInFlight int `mapstructure:"sc-historical-proof-max-inflight"`
HistoricalProofRateLimit float64 `mapstructure:"sc-historical-proof-rate-limit"`
HistoricalProofBurst int `mapstructure:"sc-historical-proof-burst"`
SubspaceQueryMaxInFlight int `mapstructure:"sc-subspace-query-max-inflight"`
SubspaceMaxPairs int `mapstructure:"sc-subspace-max-pairs"`
SubspaceMaxBytes int `mapstructure:"sc-subspace-max-bytes"`
WriteMode string `mapstructure:"sc-write-mode"`
WriteModeEnableAuto bool `mapstructure:"sc-write-mode-enable-auto"`
HashLoggerEnable bool `mapstructure:"sc-hash-logger-enable"`
Expand Down Expand Up @@ -195,6 +198,9 @@ func stateCommitDefaults(registry.Mode) any {
HistoricalProofMaxInFlight: live.HistoricalProofMaxInFlight,
HistoricalProofRateLimit: live.HistoricalProofRateLimit,
HistoricalProofBurst: live.HistoricalProofBurst,
SubspaceQueryMaxInFlight: live.SubspaceQueryMaxInFlight,
SubspaceMaxPairs: live.SubspaceMaxPairs,
SubspaceMaxBytes: live.SubspaceMaxBytes,
WriteMode: string(live.WriteMode),
WriteModeEnableAuto: live.WriteModeEnableAuto,
HashLoggerEnable: live.HashLogger.Enable,
Expand Down
3 changes: 3 additions & 0 deletions app/config_register_agreement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ func readerValues(t *testing.T) map[string]string {
FlagSCHistoricalProofMaxInFlight: fmt.Sprint(sc.HistoricalProofMaxInFlight),
FlagSCHistoricalProofRateLimit: fmt.Sprint(sc.HistoricalProofRateLimit),
FlagSCHistoricalProofBurst: fmt.Sprint(sc.HistoricalProofBurst),
FlagSCSubspaceQueryMaxInFlight: fmt.Sprint(sc.SubspaceQueryMaxInFlight),
FlagSCSubspaceMaxPairs: fmt.Sprint(sc.SubspaceMaxPairs),
FlagSCSubspaceMaxBytes: fmt.Sprint(sc.SubspaceMaxBytes),
FlagSCWriteMode: fmt.Sprint(sc.WriteMode),
FlagSCWriteModeEnableAuto: fmt.Sprint(sc.WriteModeEnableAuto),
FlagSCHashLoggerEnable: fmt.Sprint(sc.HashLogger.Enable),
Expand Down
7 changes: 5 additions & 2 deletions app/config_register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ func TestStateStoreResolvesItsOtherValuesTheSameForEveryMode(t *testing.T) {

// TestStateCommitDeclaresEveryKeyItsReaderResolves holds the schema against the read-site record.
//
// Twenty keys: the seventeen the record holds as rows, and three it names beside them because each has a
// target of its own. The four keys under this section's flat key-value name that only the Cosmos server's
// Twenty-three keys: the twenty the record holds as rows, and three it names beside them because each has
// a target of its own. The four keys under this section's flat key-value name that only the Cosmos server's
// reader resolves are not among them, and are not this section's to declare.
func TestStateCommitDeclaresEveryKeyItsReaderResolves(t *testing.T) {
requireDeclares(t, StateCommitSectionName, manifestKeys(scKeys,
Expand Down Expand Up @@ -189,6 +189,9 @@ func TestStateCommitResolvesTheModuleDeclaredValues(t *testing.T) {
FlagSCHistoricalProofMaxInFlight: live.HistoricalProofMaxInFlight,
FlagSCHistoricalProofRateLimit: live.HistoricalProofRateLimit,
FlagSCHistoricalProofBurst: live.HistoricalProofBurst,
FlagSCSubspaceQueryMaxInFlight: live.SubspaceQueryMaxInFlight,
FlagSCSubspaceMaxPairs: live.SubspaceMaxPairs,
FlagSCSubspaceMaxBytes: live.SubspaceMaxBytes,
FlagSCWriteMode: string(live.WriteMode),
FlagSCWriteModeEnableAuto: live.WriteModeEnableAuto,
FlagSCHashLoggerEnable: live.HashLogger.Enable,
Expand Down
12 changes: 12 additions & 0 deletions app/seidb.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const (
FlagSCHistoricalProofMaxInFlight = "state-commit.sc-historical-proof-max-inflight"
FlagSCHistoricalProofRateLimit = "state-commit.sc-historical-proof-rate-limit"
FlagSCHistoricalProofBurst = "state-commit.sc-historical-proof-burst"
FlagSCSubspaceQueryMaxInFlight = "state-commit.sc-subspace-query-max-inflight"
FlagSCSubspaceMaxPairs = "state-commit.sc-subspace-max-pairs"
FlagSCSubspaceMaxBytes = "state-commit.sc-subspace-max-bytes"
FlagSCWriteMode = "state-commit.sc-write-mode"
FlagSCWriteModeEnableAuto = "state-commit.sc-write-mode-enable-auto"
FlagSCFlatKVReadWriteMetrics = "state-commit.flatkv.enable-read-write-metrics"
Expand Down Expand Up @@ -165,6 +168,15 @@ func parseSCConfigs(appOpts servertypes.AppOptions) config.StateCommitConfig {
if v := appOpts.Get(FlagSCHistoricalProofBurst); v != nil {
scConfig.HistoricalProofBurst = cast.ToInt(v)
}
if v := appOpts.Get(FlagSCSubspaceQueryMaxInFlight); v != nil {
scConfig.SubspaceQueryMaxInFlight = cast.ToInt(v)
}
if v := appOpts.Get(FlagSCSubspaceMaxPairs); v != nil {
scConfig.SubspaceMaxPairs = cast.ToInt(v)
}
if v := appOpts.Get(FlagSCSubspaceMaxBytes); v != nil {
scConfig.SubspaceMaxBytes = cast.ToInt(v)
}

// Hash logger. Guard each read with v != nil so an absent app.toml entry preserves the default
// (notably Enable, which defaults to true) instead of clobbering it to the zero value.
Expand Down
3 changes: 3 additions & 0 deletions app/testdata/state-commit.golden
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ FlatKVConfig.LtHashThreadsPerCore = float64(1)
HistoricalProofMaxInFlight = int(1)
HistoricalProofRateLimit = float64(1)
HistoricalProofBurst = int(1)
SubspaceQueryMaxInFlight = int(2)
SubspaceMaxPairs = int(1000)
SubspaceMaxBytes = int(4194304)
HashLogger.Enable = bool(true)
HashLogger.Directory = string("")
HashLogger.BlocksToRetain = uint(0)
Expand Down
3 changes: 3 additions & 0 deletions app/testdata/state-commit.keys.golden
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
"state-commit.sc-historical-proof-max-inflight"
"state-commit.sc-historical-proof-rate-limit"
"state-commit.sc-historical-proof-burst"
"state-commit.sc-subspace-query-max-inflight"
"state-commit.sc-subspace-max-pairs"
"state-commit.sc-subspace-max-bytes"
"state-commit.sc-hash-logger-enable"
"state-commit.sc-hash-logger-directory"
"state-commit.sc-hash-logger-blocks-to-retain"
Expand Down
3 changes: 3 additions & 0 deletions sei-cosmos/server/config/testdata/server_config.golden
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ StateCommit.FlatKVConfig.LtHashThreadsPerCore = float64(1)
StateCommit.HistoricalProofMaxInFlight = int(1)
StateCommit.HistoricalProofRateLimit = float64(1)
StateCommit.HistoricalProofBurst = int(1)
StateCommit.SubspaceQueryMaxInFlight = int(2)
StateCommit.SubspaceMaxPairs = int(1000)
StateCommit.SubspaceMaxBytes = int(4194304)
StateCommit.HashLogger.Enable = bool(true)
StateCommit.HashLogger.Directory = string("")
StateCommit.HashLogger.BlocksToRetain = uint(0)
Expand Down
3 changes: 3 additions & 0 deletions sei-cosmos/store/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ const StoreCodespace = "store"

var (
ErrInvalidProof = sdkerrors.Register(StoreCodespace, 2, "invalid proof")

// ErrSubspaceCapExceeded is returned when a /subspace scan would exceed configured limits.
ErrSubspaceCapExceeded = sdkerrors.Register(StoreCodespace, 3, "subspace result exceeds limit")
)
34 changes: 11 additions & 23 deletions sei-cosmos/storev2/commitment/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ package commitment
import (
"bytes"
"context"
"fmt"
"io"

"cosmossdk.io/errors"
"github.com/sei-protocol/sei-chain/sei-cosmos/store/cachekv"
"github.com/sei-protocol/sei-chain/sei-cosmos/store/tracekv"
"github.com/sei-protocol/sei-chain/sei-cosmos/store/types"
"github.com/sei-protocol/sei-chain/sei-cosmos/storev2/query"
sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors"
"github.com/sei-protocol/sei-chain/sei-cosmos/types/kv"
seidbproto "github.com/sei-protocol/sei-chain/sei-db/proto"
sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types"
abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types"
Expand All @@ -25,13 +24,15 @@ var (

// Store Implements types.KVStore and CommitKVStore.
type Store struct {
tree sctypes.CommitKVStore
changeSet seidbproto.ChangeSet
tree sctypes.CommitKVStore
changeSet seidbproto.ChangeSet
subspaceLimits query.Limits
}

func NewStore(tree sctypes.CommitKVStore) *Store {
func NewStore(tree sctypes.CommitKVStore, subspaceLimits query.Limits) *Store {
return &Store{
tree: tree,
tree: tree,
subspaceLimits: subspaceLimits,
}
}

Expand Down Expand Up @@ -133,7 +134,7 @@ func (st *Store) HasPendingChanges() bool {
return len(st.changeSet.Pairs) > 0
}

func (st *Store) Query(_ context.Context, req abci.RequestQuery) (res abci.ResponseQuery) {
func (st *Store) Query(ctx context.Context, req abci.RequestQuery) (res abci.ResponseQuery) {
if req.Height > 0 && req.Height != st.tree.Version() {
return sdkerrors.QueryResult(errors.Wrap(sdkerrors.ErrInvalidHeight, "invalid height"))
}
Expand All @@ -152,24 +153,11 @@ func (st *Store) Query(_ context.Context, req abci.RequestQuery) (res abci.Respo
op := types.NewIavlCommitmentOp(res.Key, commitmentProof)
res.ProofOps = &crypto.ProofOps{Ops: []crypto.ProofOp{op.ProofOp()}}
case "/subspace":
pairs := kv.Pairs{
Pairs: make([]kv.Pair, 0),
}

subspace := req.Data
res.Key = subspace

iterator := types.KVStorePrefixIterator(st, subspace)
for ; iterator.Valid(); iterator.Next() {
pairs.Pairs = append(pairs.Pairs, kv.Pair{Key: iterator.Key(), Value: iterator.Value()})
}
_ = iterator.Close()

bz, err := pairs.Marshal()
res.Key = req.Data
bz, err := query.ScanSubspace(ctx, st, req.Data, st.subspaceLimits)
if err != nil {
panic(fmt.Errorf("failed to marshal KV pairs: %w", err))
return sdkerrors.QueryResult(err)
}

res.Value = bz
default:
return sdkerrors.QueryResult(errors.Wrapf(sdkerrors.ErrUnknownRequest, "unexpected query path: %v", req.Path))
Expand Down
44 changes: 42 additions & 2 deletions sei-cosmos/storev2/commitment/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,52 @@ import (
"testing"

"github.com/sei-protocol/sei-chain/sei-cosmos/store/types"
"github.com/sei-protocol/sei-chain/sei-cosmos/storev2/query"
"github.com/sei-protocol/sei-chain/sei-db/state_db/sc/memiavl"
abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types"
"github.com/stretchr/testify/require"
)

func TestLastCommitID(t *testing.T) {
tree := memiavl.New(100)
store := NewStore(tree)
tree := memiavl.New(0)
store := NewStore(tree, query.Limits{})
require.Equal(t, types.CommitID{Hash: tree.RootHash()}, store.LastCommitID())
}

func TestQuerySubspace_EmptyPrefixRejected(t *testing.T) {
tree := memiavl.New(0)
store := NewStore(tree, query.Limits{})

resp := store.Query(t.Context(), abci.RequestQuery{Path: "/subspace"})
require.NotEqualValues(t, 0, resp.Code)
require.Contains(t, resp.Log, "subspace prefix must not be empty")
}

func TestQuerySubspace_NarrowPrefixSucceeds(t *testing.T) {
tree := memiavl.New(0)
tree.Set([]byte("ab1"), []byte("v1"))
tree.Set([]byte("ab2"), []byte("v2"))
tree.Set([]byte("xy1"), []byte("v3"))
store := NewStore(tree, query.Limits{MaxPairs: 10, MaxBytes: query.DefaultMaxSubspaceBytes})

resp := store.Query(t.Context(), abci.RequestQuery{
Path: "/subspace",
Data: []byte("ab"),
})
require.EqualValues(t, 0, resp.Code)
require.NotEmpty(t, resp.Value)
}

func TestQueryKey_UnaffectedBySubspaceLimits(t *testing.T) {
tree := memiavl.New(0)
key := []byte("k")
tree.Set(key, []byte("v"))
store := NewStore(tree, query.Limits{MaxPairs: 1, MaxBytes: 1})

resp := store.Query(t.Context(), abci.RequestQuery{
Path: "/key",
Data: key,
})
require.EqualValues(t, 0, resp.Code)
require.Equal(t, []byte("v"), resp.Value)
}
Loading
Loading