From da36bbb1679a3a08e891c143c998fa6732cca409 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Thu, 21 May 2026 15:47:43 -0700 Subject: [PATCH 01/21] Updated max limit for sei_cosmos query pagination --- sei-cosmos/types/query/pagination.go | 4 +--- sei-cosmos/x/bank/keeper/genesis.go | 3 ++- sei-cosmos/x/bank/keeper/invariants.go | 3 ++- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sei-cosmos/types/query/pagination.go b/sei-cosmos/types/query/pagination.go index 3492fb05a8..f41cf4a512 100644 --- a/sei-cosmos/types/query/pagination.go +++ b/sei-cosmos/types/query/pagination.go @@ -2,7 +2,6 @@ package query import ( "fmt" - "math" "github.com/sei-protocol/sei-chain/sei-cosmos/store/types" db "github.com/tendermint/tm-db" @@ -15,8 +14,7 @@ import ( const DefaultLimit = 100 // MaxLimit is the maximum limit the paginate function can handle -// which equals the maximum value that can be stored in uint64 -const MaxLimit = math.MaxUint64 +const MaxLimit = uint64(10_000) // ParsePagination validate PageRequest and returns page number & limit. func ParsePagination(pageReq *PageRequest) (page, limit int, err error) { diff --git a/sei-cosmos/x/bank/keeper/genesis.go b/sei-cosmos/x/bank/keeper/genesis.go index afdf134122..e00653bb2f 100644 --- a/sei-cosmos/x/bank/keeper/genesis.go +++ b/sei-cosmos/x/bank/keeper/genesis.go @@ -2,6 +2,7 @@ package keeper import ( "fmt" + "math" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" @@ -60,7 +61,7 @@ func (k BaseKeeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) { // ExportGenesis returns the bank module's genesis state. func (k BaseKeeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { - totalSupply, _, err := k.GetPaginatedTotalSupply(ctx, &query.PageRequest{Limit: query.MaxLimit}) + totalSupply, _, err := k.GetPaginatedTotalSupply(ctx, &query.PageRequest{Limit: math.MaxUint64}) if err != nil { panic(fmt.Errorf("unable to fetch total supply %v", err)) } diff --git a/sei-cosmos/x/bank/keeper/invariants.go b/sei-cosmos/x/bank/keeper/invariants.go index fde86feeb0..cfe10a2b08 100644 --- a/sei-cosmos/x/bank/keeper/invariants.go +++ b/sei-cosmos/x/bank/keeper/invariants.go @@ -2,6 +2,7 @@ package keeper import ( "fmt" + "math" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" @@ -13,7 +14,7 @@ func TotalSupply(k Keeper) sdk.Invariant { return func(ctx sdk.Context) (string, bool) { expectedTotal := sdk.Coins{} weiTotal := sdk.NewInt(0) - supply, _, err := k.GetPaginatedTotalSupply(ctx, &query.PageRequest{Limit: query.MaxLimit}) + supply, _, err := k.GetPaginatedTotalSupply(ctx, &query.PageRequest{Limit: math.MaxUint64}) if err != nil { return sdk.FormatInvariant(types.ModuleName, "query supply", From d5241fa826ae32ba50ba30d52c3d7155fd781534 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Thu, 21 May 2026 16:26:33 -0700 Subject: [PATCH 02/21] Enforced pagination limit and used IterateTotalSupply --- sei-cosmos/types/query/pagination.go | 22 +++++++++++++++++++++- sei-cosmos/x/bank/keeper/genesis.go | 11 +++++------ sei-cosmos/x/bank/keeper/invariants.go | 13 +++++-------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/sei-cosmos/types/query/pagination.go b/sei-cosmos/types/query/pagination.go index f41cf4a512..0fcd836586 100644 --- a/sei-cosmos/types/query/pagination.go +++ b/sei-cosmos/types/query/pagination.go @@ -42,13 +42,33 @@ func ParsePagination(pageReq *PageRequest) (page, limit int, err error) { // Paginate does pagination of all the results in the PrefixStore based on the // provided PageRequest. onResult should be used to do actual unmarshaling. +// Limits are capped at MaxLimit func Paginate( prefixStore types.KVStore, pageRequest *PageRequest, onResult func(key []byte, value []byte) error, ) (*PageResponse, error) { + if pageRequest == nil { + pageRequest = &PageRequest{} + } + + limit := pageRequest.Limit + if limit == 0 { + limit = DefaultLimit + } + if limit > MaxLimit { + return nil, status.Errorf(codes.InvalidArgument, "limit %d exceeds maximum allowed limit %d", limit, MaxLimit) + } + + return paginate(prefixStore, pageRequest, onResult) +} + +func paginate( + prefixStore types.KVStore, + pageRequest *PageRequest, + onResult func(key []byte, value []byte) error, +) (*PageResponse, error) { - // if the PageRequest is nil, use default PageRequest if pageRequest == nil { pageRequest = &PageRequest{} } diff --git a/sei-cosmos/x/bank/keeper/genesis.go b/sei-cosmos/x/bank/keeper/genesis.go index e00653bb2f..9d34e3edea 100644 --- a/sei-cosmos/x/bank/keeper/genesis.go +++ b/sei-cosmos/x/bank/keeper/genesis.go @@ -2,10 +2,8 @@ package keeper import ( "fmt" - "math" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" - "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types" ) @@ -61,10 +59,11 @@ func (k BaseKeeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) { // ExportGenesis returns the bank module's genesis state. func (k BaseKeeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { - totalSupply, _, err := k.GetPaginatedTotalSupply(ctx, &query.PageRequest{Limit: math.MaxUint64}) - if err != nil { - panic(fmt.Errorf("unable to fetch total supply %v", err)) - } + var totalSupply sdk.Coins + k.IterateTotalSupply(ctx, func(coin sdk.Coin) bool { + totalSupply = totalSupply.Add(coin) + return false + }) weiBalances := []types.WeiBalance{} k.IterateAllWeiBalances(ctx, func(aa sdk.AccAddress, i sdk.Int) bool { // Deep copy i: the iterator reuses the same sdk.Int across iterations. diff --git a/sei-cosmos/x/bank/keeper/invariants.go b/sei-cosmos/x/bank/keeper/invariants.go index cfe10a2b08..60fb96618e 100644 --- a/sei-cosmos/x/bank/keeper/invariants.go +++ b/sei-cosmos/x/bank/keeper/invariants.go @@ -2,10 +2,8 @@ package keeper import ( "fmt" - "math" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" - "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types" ) @@ -14,12 +12,11 @@ func TotalSupply(k Keeper) sdk.Invariant { return func(ctx sdk.Context) (string, bool) { expectedTotal := sdk.Coins{} weiTotal := sdk.NewInt(0) - supply, _, err := k.GetPaginatedTotalSupply(ctx, &query.PageRequest{Limit: math.MaxUint64}) - - if err != nil { - return sdk.FormatInvariant(types.ModuleName, "query supply", - fmt.Sprintf("error querying total supply %v", err)), false - } + var supply sdk.Coins + k.IterateTotalSupply(ctx, func(coin sdk.Coin) bool { + supply = supply.Add(coin) + return false + }) k.IterateAllBalances(ctx, func(_ sdk.AccAddress, balance sdk.Coin) bool { expectedTotal = expectedTotal.Add(balance) From b14257ef4ac9ca13782db9fee0aad8f8dcb20179 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Fri, 22 May 2026 11:13:50 -0700 Subject: [PATCH 03/21] Added pagination limit check to other entry points --- sei-cosmos/types/query/filtered_pagination.go | 8 ++++++++ sei-cosmos/types/query/pagination.go | 11 +++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/sei-cosmos/types/query/filtered_pagination.go b/sei-cosmos/types/query/filtered_pagination.go index 950733d552..5fe1f32018 100644 --- a/sei-cosmos/types/query/filtered_pagination.go +++ b/sei-cosmos/types/query/filtered_pagination.go @@ -43,6 +43,10 @@ func FilteredPaginate( countTotal = true } + if err := verifyPaginationLimit(limit); err != nil { + return nil, err + } + if len(key) != 0 { iterator := getIterator(prefixStore, key, reverse) defer func() { _ = iterator.Close() }() @@ -157,6 +161,10 @@ func GenericFilteredPaginate[T codec.ProtoMarshaler, F codec.ProtoMarshaler]( countTotal = true } + if err := verifyPaginationLimit(limit); err != nil { + return results, nil, err + } + if len(key) != 0 { iterator := getIterator(prefixStore, key, reverse) defer func() { _ = iterator.Close() }() diff --git a/sei-cosmos/types/query/pagination.go b/sei-cosmos/types/query/pagination.go index 0fcd836586..4795aa2cc7 100644 --- a/sei-cosmos/types/query/pagination.go +++ b/sei-cosmos/types/query/pagination.go @@ -40,6 +40,13 @@ func ParsePagination(pageReq *PageRequest) (page, limit int, err error) { return page, limit, nil } +func verifyPaginationLimit(limit uint64) error { + if limit > MaxLimit { + return status.Errorf(codes.InvalidArgument, "limit %d exceeds maximum allowed limit %d", limit, MaxLimit) + } + return nil +} + // Paginate does pagination of all the results in the PrefixStore based on the // provided PageRequest. onResult should be used to do actual unmarshaling. // Limits are capped at MaxLimit @@ -56,8 +63,8 @@ func Paginate( if limit == 0 { limit = DefaultLimit } - if limit > MaxLimit { - return nil, status.Errorf(codes.InvalidArgument, "limit %d exceeds maximum allowed limit %d", limit, MaxLimit) + if err := verifyPaginationLimit(limit); err != nil { + return nil, err } return paginate(prefixStore, pageRequest, onResult) From ac977b3d556bffc607ec0ff282c819f793942617 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Fri, 22 May 2026 14:32:00 -0700 Subject: [PATCH 04/21] Added pagination verification to parsePagination --- sei-cosmos/types/query/pagination.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sei-cosmos/types/query/pagination.go b/sei-cosmos/types/query/pagination.go index 4795aa2cc7..6118a5db2e 100644 --- a/sei-cosmos/types/query/pagination.go +++ b/sei-cosmos/types/query/pagination.go @@ -35,6 +35,10 @@ func ParsePagination(pageReq *PageRequest) (page, limit int, err error) { limit = DefaultLimit } + if limitErr := verifyPaginationLimit(uint64(limit)); limitErr != nil { + return 1, 0, limitErr + } + page = offset/limit + 1 return page, limit, nil From cb374e0cb0cfc472cf197dcf1d3b5bbdd2408ecd Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Fri, 22 May 2026 14:55:13 -0700 Subject: [PATCH 05/21] Used pagination to fetch total supply in genesis and invariants --- sei-cosmos/x/bank/keeper/genesis.go | 9 ++++----- sei-cosmos/x/bank/keeper/invariants.go | 10 +++++----- sei-cosmos/x/bank/keeper/keeper.go | 22 ++++++++++++++++++++++ 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/sei-cosmos/x/bank/keeper/genesis.go b/sei-cosmos/x/bank/keeper/genesis.go index 9d34e3edea..4f1e643a5b 100644 --- a/sei-cosmos/x/bank/keeper/genesis.go +++ b/sei-cosmos/x/bank/keeper/genesis.go @@ -59,11 +59,10 @@ func (k BaseKeeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) { // ExportGenesis returns the bank module's genesis state. func (k BaseKeeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { - var totalSupply sdk.Coins - k.IterateTotalSupply(ctx, func(coin sdk.Coin) bool { - totalSupply = totalSupply.Add(coin) - return false - }) + totalSupply, err := collectAllTotalSupply(ctx, k) + if err != nil { + panic(fmt.Errorf("unable to fetch total supply: %w", err)) + } weiBalances := []types.WeiBalance{} k.IterateAllWeiBalances(ctx, func(aa sdk.AccAddress, i sdk.Int) bool { // Deep copy i: the iterator reuses the same sdk.Int across iterations. diff --git a/sei-cosmos/x/bank/keeper/invariants.go b/sei-cosmos/x/bank/keeper/invariants.go index 60fb96618e..77084a9fb4 100644 --- a/sei-cosmos/x/bank/keeper/invariants.go +++ b/sei-cosmos/x/bank/keeper/invariants.go @@ -12,11 +12,11 @@ func TotalSupply(k Keeper) sdk.Invariant { return func(ctx sdk.Context) (string, bool) { expectedTotal := sdk.Coins{} weiTotal := sdk.NewInt(0) - var supply sdk.Coins - k.IterateTotalSupply(ctx, func(coin sdk.Coin) bool { - supply = supply.Add(coin) - return false - }) + supply, err := collectAllTotalSupply(ctx, k) + if err != nil { + return sdk.FormatInvariant(types.ModuleName, "query supply", + fmt.Sprintf("error querying total supply %v", err)), false + } k.IterateAllBalances(ctx, func(_ sdk.AccAddress, balance sdk.Coin) bool { expectedTotal = expectedTotal.Add(balance) diff --git a/sei-cosmos/x/bank/keeper/keeper.go b/sei-cosmos/x/bank/keeper/keeper.go index 33cad0af6d..e80f844f93 100644 --- a/sei-cosmos/x/bank/keeper/keeper.go +++ b/sei-cosmos/x/bank/keeper/keeper.go @@ -106,6 +106,28 @@ func (k BaseKeeper) GetPaginatedTotalSupply(ctx sdk.Context, pagination *query.P return supply, pageRes, nil } +// collectAllTotalSupply returns the full supply by paging with MaxLimit. +func collectAllTotalSupply(ctx sdk.Context, k Keeper) (sdk.Coins, error) { + totalSupply := sdk.NewCoins() + pageReq := &query.PageRequest{Limit: query.MaxLimit} + + for { + page, pageRes, err := k.GetPaginatedTotalSupply(ctx, pageReq) + if err != nil { + return nil, err + } + totalSupply = totalSupply.Add(page...) + + if pageRes == nil || len(pageRes.NextKey) == 0 { + return totalSupply, nil + } + pageReq = &query.PageRequest{ + Key: pageRes.NextKey, + Limit: query.MaxLimit, + } + } +} + // NewBaseKeeper returns a new BaseKeeper object with a given codec, dedicated // store key, an AccountKeeper implementation, and a parameter Subspace used to // store and fetch module parameters. The BaseKeeper also accepts a From 89dd0cc6cfb52c283d354e0d91d24256d829a387 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Fri, 22 May 2026 15:41:47 -0700 Subject: [PATCH 06/21] Added nosec comment --- sei-cosmos/types/query/pagination.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sei-cosmos/types/query/pagination.go b/sei-cosmos/types/query/pagination.go index 6118a5db2e..1899d6ffff 100644 --- a/sei-cosmos/types/query/pagination.go +++ b/sei-cosmos/types/query/pagination.go @@ -35,7 +35,7 @@ func ParsePagination(pageReq *PageRequest) (page, limit int, err error) { limit = DefaultLimit } - if limitErr := verifyPaginationLimit(uint64(limit)); limitErr != nil { + if limitErr := verifyPaginationLimit(uint64(limit)); limitErr != nil { // #nosec G115 -- at this point, limit is a positive int and there is no overflow issue return 1, 0, limitErr } From 4d427b6e09ac9a099182604423b2ec2c74e6e3fb Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Fri, 22 May 2026 16:09:13 -0700 Subject: [PATCH 07/21] Added tests --- .../types/query/filtered_pagination_test.go | 11 ++++++++++ sei-cosmos/types/query/pagination_test.go | 20 +++++++++++++++++++ sei-cosmos/x/bank/keeper/keeper_test.go | 6 ++++++ 3 files changed, 37 insertions(+) diff --git a/sei-cosmos/types/query/filtered_pagination_test.go b/sei-cosmos/types/query/filtered_pagination_test.go index b0b6544b0a..0550ce9046 100644 --- a/sei-cosmos/types/query/filtered_pagination_test.go +++ b/sei-cosmos/types/query/filtered_pagination_test.go @@ -170,6 +170,17 @@ func (s *paginationTestSuite) TestReverseFilteredPaginations() { } +func (s *paginationTestSuite) TestFilteredPaginateMaxLimitExceeded() { + app, ctx, _ := setupTest(s.T()) + store := ctx.KVStore(app.GetKey(types.StoreKey)) + + _, err := query.FilteredPaginate(store, &query.PageRequest{Limit: query.MaxLimit + 1}, func(_ []byte, _ []byte, _ bool) (bool, error) { + return false, nil + }) + s.Require().Error(err) + s.Require().Contains(err.Error(), "exceeds maximum allowed limit") +} + func execFilterPaginate(store sdk.KVStore, pageReq *query.PageRequest, appCodec codec.Codec) (balances sdk.Coins, res *query.PageResponse, err error) { balancesStore := prefix.NewStore(store, types.BalancesPrefix) accountStore := prefix.NewStore(balancesStore, address.MustLengthPrefix(addr1)) diff --git a/sei-cosmos/types/query/pagination_test.go b/sei-cosmos/types/query/pagination_test.go index f05055670b..b3c8bca177 100644 --- a/sei-cosmos/types/query/pagination_test.go +++ b/sei-cosmos/types/query/pagination_test.go @@ -56,6 +56,26 @@ func (s *paginationTestSuite) TestParsePagination() { s.Require().NoError(err) s.Require().Equal(page, 1) s.Require().Equal(limit, 10) + + s.T().Log("verify limit equal to MaxLimit is accepted") + pageReq = &query.PageRequest{Limit: query.MaxLimit} + _, _, err = query.ParsePagination(pageReq) + s.Require().NoError(err) + + s.T().Log("verify limit exceeding MaxLimit is rejected") + pageReq = &query.PageRequest{Limit: query.MaxLimit + 1} + _, _, err = query.ParsePagination(pageReq) + s.Require().Error(err) + s.Require().Contains(err.Error(), "exceeds maximum allowed limit") +} + +func (s *paginationTestSuite) TestPaginateMaxLimitExceeded() { + app, ctx, _ := setupTest(s.T()) + store := ctx.KVStore(app.GetKey(types.StoreKey)) + + _, err := query.Paginate(store, &query.PageRequest{Limit: query.MaxLimit + 1}, func(_, _ []byte) error { return nil }) + s.Require().Error(err) + s.Require().Contains(err.Error(), "exceeds maximum allowed limit") } func (s *paginationTestSuite) TestPagination() { diff --git a/sei-cosmos/x/bank/keeper/keeper_test.go b/sei-cosmos/x/bank/keeper/keeper_test.go index 87d8d7078f..2f9011d925 100644 --- a/sei-cosmos/x/bank/keeper/keeper_test.go +++ b/sei-cosmos/x/bank/keeper/keeper_test.go @@ -157,6 +157,12 @@ func (suite *IntegrationTestSuite) TestSendCoinsAndWei() { require.Equal(sdk.NewInt(53), keeper.GetBalance(ctx, addr3, sdk.DefaultBondDenom).Amount) } +func (suite *IntegrationTestSuite) TestGetPaginatedTotalSupplyMaxLimitExceeded() { + _, _, err := suite.app.BankKeeper.GetPaginatedTotalSupply(suite.ctx, &query.PageRequest{Limit: query.MaxLimit + 1}) + suite.Require().Error(err) + suite.Require().Contains(err.Error(), "exceeds maximum allowed limit") +} + func (suite *IntegrationTestSuite) TestSupply() { ctx := suite.ctx From 67908cc13603ba079837744f061bf62c5e193ef0 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Fri, 22 May 2026 16:59:50 -0700 Subject: [PATCH 08/21] Fixing lint error --- sei-cosmos/types/query/pagination.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sei-cosmos/types/query/pagination.go b/sei-cosmos/types/query/pagination.go index 1899d6ffff..55d7a5611a 100644 --- a/sei-cosmos/types/query/pagination.go +++ b/sei-cosmos/types/query/pagination.go @@ -35,7 +35,8 @@ func ParsePagination(pageReq *PageRequest) (page, limit int, err error) { limit = DefaultLimit } - if limitErr := verifyPaginationLimit(uint64(limit)); limitErr != nil { // #nosec G115 -- at this point, limit is a positive int and there is no overflow issue + // #nosec G115 -- limit is positive after validation above; fits in uint64 + if limitErr := verifyPaginationLimit(uint64(limit)); limitErr != nil { return 1, 0, limitErr } From 84a9803c23ecc921f094a93f04aa6a7fc83fe65d Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Tue, 26 May 2026 10:20:41 -0700 Subject: [PATCH 09/21] Reduced pagination page limit to 1000 --- sei-cosmos/types/query/pagination.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sei-cosmos/types/query/pagination.go b/sei-cosmos/types/query/pagination.go index 55d7a5611a..2f4425b54b 100644 --- a/sei-cosmos/types/query/pagination.go +++ b/sei-cosmos/types/query/pagination.go @@ -13,8 +13,8 @@ import ( // if the `limit` is not supplied, paginate will use `DefaultLimit` const DefaultLimit = 100 -// MaxLimit is the maximum limit the paginate function can handle -const MaxLimit = uint64(10_000) +// MaxLimit is the maximum limit per page the paginate function can handle +const MaxLimit = uint64(1_000) // ParsePagination validate PageRequest and returns page number & limit. func ParsePagination(pageReq *PageRequest) (page, limit int, err error) { From d728fdf83913d52035dc4d10e7d85407d6e22b2c Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Wed, 27 May 2026 07:35:45 -0700 Subject: [PATCH 10/21] Added further validations and tests --- sei-cosmos/types/query/filtered_pagination.go | 42 ++++++++++++----- .../types/query/filtered_pagination_test.go | 40 +++++++++++++++-- sei-cosmos/types/query/pagination.go | 40 ++++++++++++++--- sei-cosmos/types/query/pagination_test.go | 45 ++++++++++++++++++- sei-cosmos/x/auth/tx/service.go | 3 ++ 5 files changed, 146 insertions(+), 24 deletions(-) diff --git a/sei-cosmos/types/query/filtered_pagination.go b/sei-cosmos/types/query/filtered_pagination.go index 5fe1f32018..c084417cc0 100644 --- a/sei-cosmos/types/query/filtered_pagination.go +++ b/sei-cosmos/types/query/filtered_pagination.go @@ -5,6 +5,8 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/codec" "github.com/sei-protocol/sei-chain/sei-cosmos/store/types" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) // FilteredPaginate does pagination of all the results in the PrefixStore based on the @@ -36,14 +38,15 @@ func FilteredPaginate( return nil, fmt.Errorf("invalid request, either offset or key is expected, got both") } + if err := VerifyPaginationOffset(offset); err != nil { + return nil, err + } + if limit == 0 { limit = DefaultLimit - - // count total results when the limit is zero/not supplied - countTotal = true } - if err := verifyPaginationLimit(limit); err != nil { + if err := VerifyPaginationLimit(limit); err != nil { return nil, err } @@ -87,11 +90,18 @@ func FilteredPaginate( end := offset + limit var ( - numHits uint64 - nextKey []byte + numHits uint64 + nextKey []byte + totalIter uint64 ) for ; iterator.Valid(); iterator.Next() { + totalIter++ + if countTotal && totalIter > end+MaxScanLimit { + return nil, status.Errorf(codes.InvalidArgument, + "count_total scan exceeds maximum of %d items past the page; use key-based pagination instead", MaxScanLimit) + } + if iterator.Error() != nil { return nil, iterator.Error() } @@ -154,14 +164,15 @@ func GenericFilteredPaginate[T codec.ProtoMarshaler, F codec.ProtoMarshaler]( return results, nil, fmt.Errorf("invalid request, either offset or key is expected, got both") } + if err := VerifyPaginationOffset(offset); err != nil { + return results, nil, err + } + if limit == 0 { limit = DefaultLimit - - // count total results when the limit is zero/not supplied - countTotal = true } - if err := verifyPaginationLimit(limit); err != nil { + if err := VerifyPaginationLimit(limit); err != nil { return results, nil, err } @@ -213,11 +224,18 @@ func GenericFilteredPaginate[T codec.ProtoMarshaler, F codec.ProtoMarshaler]( end := offset + limit var ( - numHits uint64 - nextKey []byte + numHits uint64 + nextKey []byte + totalIter uint64 ) for ; iterator.Valid(); iterator.Next() { + totalIter++ + if countTotal && totalIter > end+MaxScanLimit { + return nil, nil, status.Errorf(codes.InvalidArgument, + "count_total scan exceeds maximum of %d items past the page; use key-based pagination instead", MaxScanLimit) + } + if iterator.Error() != nil { return nil, nil, iterator.Error() } diff --git a/sei-cosmos/types/query/filtered_pagination_test.go b/sei-cosmos/types/query/filtered_pagination_test.go index 0550ce9046..f2ccae5c27 100644 --- a/sei-cosmos/types/query/filtered_pagination_test.go +++ b/sei-cosmos/types/query/filtered_pagination_test.go @@ -47,7 +47,7 @@ func (s *paginationTestSuite) TestFilteredPaginations() { s.Require().NoError(err) s.Require().NotNil(res) s.Require().Equal(4, len(balances)) - s.Require().Equal(uint64(4), res.Total) + s.Require().Equal(uint64(0), res.Total) s.Require().Nil(res.NextKey) s.T().Log("verify nextKey is returned if there are more results") @@ -79,7 +79,7 @@ func (s *paginationTestSuite) TestFilteredPaginations() { s.Require().NoError(err) s.Require().NotNil(res) s.Require().Equal(4, len(balances)) - s.Require().Equal(uint64(4), res.Total) + s.Require().Equal(uint64(0), res.Total) s.T().Log("verify with offset") pageReq = &query.PageRequest{Offset: 2, Limit: 2} @@ -122,7 +122,7 @@ func (s *paginationTestSuite) TestReverseFilteredPaginations() { s.Require().NoError(err) s.Require().NotNil(res) s.Require().Equal(10, len(balns)) - s.Require().Equal(uint64(10), res.Total) + s.Require().Equal(uint64(0), res.Total) s.Require().Nil(res.NextKey) s.T().Log("verify default limit") @@ -131,7 +131,7 @@ func (s *paginationTestSuite) TestReverseFilteredPaginations() { s.Require().NoError(err) s.Require().NotNil(res) s.Require().Equal(10, len(balns)) - s.Require().Equal(uint64(10), res.Total) + s.Require().Equal(uint64(0), res.Total) s.T().Log("verify nextKey is returned if there are more results") pageReq = &query.PageRequest{Limit: 2, CountTotal: true, Reverse: true} @@ -181,6 +181,38 @@ func (s *paginationTestSuite) TestFilteredPaginateMaxLimitExceeded() { s.Require().Contains(err.Error(), "exceeds maximum allowed limit") } +func (s *paginationTestSuite) TestFilteredPaginateOffsetExceedsMax() { + app, ctx, _ := setupTest(s.T()) + kvStore := ctx.KVStore(app.GetKey(types.StoreKey)) + + _, err := query.FilteredPaginate(kvStore, &query.PageRequest{Offset: query.MaxOffset + 1}, func(_ []byte, _ []byte, _ bool) (bool, error) { + return false, nil + }) + s.Require().Error(err) + s.Require().Contains(err.Error(), "exceeds maximum allowed offset") + + _, err = query.FilteredPaginate(kvStore, &query.PageRequest{Offset: query.MaxOffset}, func(_ []byte, _ []byte, _ bool) (bool, error) { + return false, nil + }) + s.Require().NoError(err) +} + +func (s *paginationTestSuite) TestFilteredPaginateCountTotalScanLimitExceeded() { + app, ctx, _ := setupTest(s.T()) + kvStore := prefix.NewStore(ctx.KVStore(app.GetKey(types.StoreKey)), []byte("filteredscanlimit/")) + + numItems := int(query.MaxScanLimit) + 2 + for i := 0; i < numItems; i++ { + kvStore.Set([]byte(fmt.Sprintf("%08d", i)), []byte("v")) + } + + _, err := query.FilteredPaginate(kvStore, &query.PageRequest{Limit: 1, CountTotal: true}, func(_ []byte, _ []byte, _ bool) (bool, error) { + return true, nil + }) + s.Require().Error(err) + s.Require().Contains(err.Error(), "count_total scan exceeds maximum") +} + func execFilterPaginate(store sdk.KVStore, pageReq *query.PageRequest, appCodec codec.Codec) (balances sdk.Coins, res *query.PageResponse, err error) { balancesStore := prefix.NewStore(store, types.BalancesPrefix) accountStore := prefix.NewStore(balancesStore, address.MustLengthPrefix(addr1)) diff --git a/sei-cosmos/types/query/pagination.go b/sei-cosmos/types/query/pagination.go index 2f4425b54b..a6fbf8f1a1 100644 --- a/sei-cosmos/types/query/pagination.go +++ b/sei-cosmos/types/query/pagination.go @@ -16,6 +16,13 @@ const DefaultLimit = 100 // MaxLimit is the maximum limit per page the paginate function can handle const MaxLimit = uint64(1_000) +// MaxScanLimit is the maximum number of store entries the paginate function +// will iterate past the page end when count_total is requested. +const MaxScanLimit = uint64(10_000) + +// MaxOffset is the maximum offset allowed in a PageRequest. +const MaxOffset = uint64(10_000) + // ParsePagination validate PageRequest and returns page number & limit. func ParsePagination(pageReq *PageRequest) (page, limit int, err error) { offset := 0 @@ -28,6 +35,10 @@ func ParsePagination(pageReq *PageRequest) (page, limit int, err error) { if offset < 0 { return 1, 0, status.Error(codes.InvalidArgument, "offset must greater than 0") } + // #nosec G115 -- offset is non-negative after validation above; fits in uint64 + if offsetErr := VerifyPaginationOffset(uint64(offset)); offsetErr != nil { + return 1, 0, offsetErr + } if limit < 0 { return 1, 0, status.Error(codes.InvalidArgument, "limit must greater than 0") @@ -36,7 +47,7 @@ func ParsePagination(pageReq *PageRequest) (page, limit int, err error) { } // #nosec G115 -- limit is positive after validation above; fits in uint64 - if limitErr := verifyPaginationLimit(uint64(limit)); limitErr != nil { + if limitErr := VerifyPaginationLimit(uint64(limit)); limitErr != nil { return 1, 0, limitErr } @@ -45,13 +56,20 @@ func ParsePagination(pageReq *PageRequest) (page, limit int, err error) { return page, limit, nil } -func verifyPaginationLimit(limit uint64) error { +func VerifyPaginationLimit(limit uint64) error { if limit > MaxLimit { return status.Errorf(codes.InvalidArgument, "limit %d exceeds maximum allowed limit %d", limit, MaxLimit) } return nil } +func VerifyPaginationOffset(offset uint64) error { + if offset > MaxOffset { + return status.Errorf(codes.InvalidArgument, "offset %d exceeds maximum allowed offset %d", offset, MaxOffset) + } + return nil +} + // Paginate does pagination of all the results in the PrefixStore based on the // provided PageRequest. onResult should be used to do actual unmarshaling. // Limits are capped at MaxLimit @@ -68,7 +86,7 @@ func Paginate( if limit == 0 { limit = DefaultLimit } - if err := verifyPaginationLimit(limit); err != nil { + if err := VerifyPaginationLimit(limit); err != nil { return nil, err } @@ -95,11 +113,16 @@ func paginate( return nil, fmt.Errorf("invalid request, either offset or key is expected, got both") } + if err := VerifyPaginationLimit(limit); err != nil { + return nil, err + } + + if err := VerifyPaginationOffset(offset); err != nil { + return nil, err + } + if limit == 0 { limit = DefaultLimit - - // count total results when the limit is zero/not supplied - countTotal = true } if len(key) != 0 { @@ -142,6 +165,11 @@ func paginate( for ; iterator.Valid(); iterator.Next() { count++ + if countTotal && count > end+MaxScanLimit { + return nil, status.Errorf(codes.InvalidArgument, + "count_total scan exceeds maximum of %d items past the page; use key-based pagination instead", MaxScanLimit) + } + if count <= offset { continue } diff --git a/sei-cosmos/types/query/pagination_test.go b/sei-cosmos/types/query/pagination_test.go index b3c8bca177..d57388a2b4 100644 --- a/sei-cosmos/types/query/pagination_test.go +++ b/sei-cosmos/types/query/pagination_test.go @@ -15,6 +15,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/codec" "github.com/sei-protocol/sei-chain/sei-cosmos/crypto/keys/secp256k1" "github.com/sei-protocol/sei-chain/sei-cosmos/store" + "github.com/sei-protocol/sei-chain/sei-cosmos/store/prefix" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-cosmos/types/query" "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types" @@ -67,6 +68,17 @@ func (s *paginationTestSuite) TestParsePagination() { _, _, err = query.ParsePagination(pageReq) s.Require().Error(err) s.Require().Contains(err.Error(), "exceeds maximum allowed limit") + + s.T().Log("verify offset equal to MaxOffset is accepted") + pageReq = &query.PageRequest{Offset: query.MaxOffset, Limit: 1} + _, _, err = query.ParsePagination(pageReq) + s.Require().NoError(err) + + s.T().Log("verify offset exceeding MaxOffset is rejected") + pageReq = &query.PageRequest{Offset: query.MaxOffset + 1, Limit: 1} + _, _, err = query.ParsePagination(pageReq) + s.Require().Error(err) + s.Require().Contains(err.Error(), "exceeds maximum allowed offset") } func (s *paginationTestSuite) TestPaginateMaxLimitExceeded() { @@ -97,12 +109,12 @@ func (s *paginationTestSuite) TestPagination() { app.AccountKeeper.SetAccount(ctx, acc1) s.Require().NoError(apptesting.FundAccount(app.BankKeeper, ctx, addr1, balances)) - s.T().Log("verify empty page request results a max of defaultLimit records and counts total records") + s.T().Log("verify empty page request results a max of defaultLimit records without total count") pageReq := &query.PageRequest{} request := types.NewQueryAllBalancesRequest(addr1, pageReq) res, err := queryClient.AllBalances(gocontext.Background(), request) s.Require().NoError(err) - s.Require().Equal(res.Pagination.Total, uint64(numBalances)) + s.Require().Equal(res.Pagination.Total, uint64(0)) s.Require().NotNil(res.Pagination.NextKey) s.Require().LessOrEqual(res.Balances.Len(), defaultLimit) @@ -311,6 +323,35 @@ func (s *paginationTestSuite) TestReversePagination() { s.Require().Nil(res.Pagination.NextKey) } +func (s *paginationTestSuite) TestPaginateOffsetExceedsMax() { + app, ctx, _ := setupTest(s.T()) + kvStore := ctx.KVStore(app.GetKey(types.StoreKey)) + + _, err := query.Paginate(kvStore, &query.PageRequest{Offset: query.MaxOffset + 1}, func(_, _ []byte) error { return nil }) + s.Require().Error(err) + s.Require().Contains(err.Error(), "exceeds maximum allowed offset") + + _, err = query.Paginate(kvStore, &query.PageRequest{Offset: query.MaxOffset}, func(_, _ []byte) error { return nil }) + s.Require().NoError(err) +} + +func (s *paginationTestSuite) TestPaginateCountTotalScanLimitExceeded() { + app, ctx, _ := setupTest(s.T()) + // Use a dedicated prefix to isolate test data from other store entries. + kvStore := prefix.NewStore(ctx.KVStore(app.GetKey(types.StoreKey)), []byte("scanlimit/")) + + // With limit=1 and offset=0: end=1, scan cap fires when count > end+MaxScanLimit = 10,001. + // Insert 10,002 items to guarantee the cap is exceeded. + numItems := int(query.MaxScanLimit) + 2 + for i := 0; i < numItems; i++ { + kvStore.Set([]byte(fmt.Sprintf("%08d", i)), []byte("v")) + } + + _, err := query.Paginate(kvStore, &query.PageRequest{Limit: 1, CountTotal: true}, func(_, _ []byte) error { return nil }) + s.Require().Error(err) + s.Require().Contains(err.Error(), "count_total scan exceeds maximum") +} + func setupTest(t *testing.T) (*app.App, sdk.Context, codec.Codec) { a := app.Setup(t, false, false, false) ctx := a.BaseApp.NewContext(false, tmproto.Header{Height: 1}) diff --git a/sei-cosmos/x/auth/tx/service.go b/sei-cosmos/x/auth/tx/service.go index e138089018..8b604380b1 100644 --- a/sei-cosmos/x/auth/tx/service.go +++ b/sei-cosmos/x/auth/tx/service.go @@ -192,6 +192,9 @@ func (s txServer) GetBlockWithTxs(ctx context.Context, req *txtypes.GetBlockWith if req.Pagination != nil { offset = req.Pagination.Offset limit = req.Pagination.Limit + if err = pagination.VerifyPaginationLimit(limit); err != nil { + return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid pagination limit: %d. Max allowed limit is %d", limit, pagination.MaxLimit) + } } else { offset = 0 limit = pagination.DefaultLimit From c8bc04f82263d876c6e679719968b7a65365ff17 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Wed, 27 May 2026 09:34:43 -0700 Subject: [PATCH 11/21] Added count safety check after the page is complete --- sei-cosmos/types/query/filtered_pagination.go | 34 ++++++++++++------- .../types/query/filtered_pagination_test.go | 2 +- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/sei-cosmos/types/query/filtered_pagination.go b/sei-cosmos/types/query/filtered_pagination.go index c084417cc0..5862357513 100644 --- a/sei-cosmos/types/query/filtered_pagination.go +++ b/sei-cosmos/types/query/filtered_pagination.go @@ -90,16 +90,17 @@ func FilteredPaginate( end := offset + limit var ( - numHits uint64 - nextKey []byte - totalIter uint64 + numHits uint64 + nextKey []byte + pageCompleteIter uint64 ) for ; iterator.Valid(); iterator.Next() { - totalIter++ - if countTotal && totalIter > end+MaxScanLimit { + // Only enforce the scan budget after the page is fully assembled, so + // that a selective filter cannot cause the limit to fire mid-page. + if countTotal && pageCompleteIter > MaxScanLimit { return nil, status.Errorf(codes.InvalidArgument, - "count_total scan exceeds maximum of %d items past the page; use key-based pagination instead", MaxScanLimit) + "scanned more than %d entries past the end of the requested page; use key-based pagination instead", MaxScanLimit) } if iterator.Error() != nil { @@ -116,6 +117,10 @@ func FilteredPaginate( numHits++ } + if numHits >= end { + pageCompleteIter++ + } + if numHits == end+1 { nextKey = iterator.Key() @@ -224,16 +229,17 @@ func GenericFilteredPaginate[T codec.ProtoMarshaler, F codec.ProtoMarshaler]( end := offset + limit var ( - numHits uint64 - nextKey []byte - totalIter uint64 + numHits uint64 + nextKey []byte + pageCompleteIter uint64 ) for ; iterator.Valid(); iterator.Next() { - totalIter++ - if countTotal && totalIter > end+MaxScanLimit { + // Only enforce the scan budget after the page is fully assembled, so + // that a selective filter cannot cause the limit to fire mid-page. + if countTotal && pageCompleteIter > MaxScanLimit { return nil, nil, status.Errorf(codes.InvalidArgument, - "count_total scan exceeds maximum of %d items past the page; use key-based pagination instead", MaxScanLimit) + "scanned more than %d entries past the end of the requested page; use key-based pagination instead", MaxScanLimit) } if iterator.Error() != nil { @@ -260,6 +266,10 @@ func GenericFilteredPaginate[T codec.ProtoMarshaler, F codec.ProtoMarshaler]( numHits++ } + if numHits >= end { + pageCompleteIter++ + } + if numHits == end+1 { if nextKey == nil { nextKey = iterator.Key() diff --git a/sei-cosmos/types/query/filtered_pagination_test.go b/sei-cosmos/types/query/filtered_pagination_test.go index f2ccae5c27..ccd94714dd 100644 --- a/sei-cosmos/types/query/filtered_pagination_test.go +++ b/sei-cosmos/types/query/filtered_pagination_test.go @@ -210,7 +210,7 @@ func (s *paginationTestSuite) TestFilteredPaginateCountTotalScanLimitExceeded() return true, nil }) s.Require().Error(err) - s.Require().Contains(err.Error(), "count_total scan exceeds maximum") + s.Require().Contains(err.Error(), "scanned more than") } func execFilterPaginate(store sdk.KVStore, pageReq *query.PageRequest, appCodec codec.Codec) (balances sdk.Coins, res *query.PageResponse, err error) { From 21e8e4fc17ad20c078e6868ee61cf547a42ccae7 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Wed, 27 May 2026 10:41:56 -0700 Subject: [PATCH 12/21] Fixed query tests --- sei-cosmos/x/slashing/client/rest/grpc_query_test.go | 2 +- sei-cosmos/x/staking/keeper/grpc_query_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sei-cosmos/x/slashing/client/rest/grpc_query_test.go b/sei-cosmos/x/slashing/client/rest/grpc_query_test.go index bef8d69dff..2bca225b0a 100644 --- a/sei-cosmos/x/slashing/client/rest/grpc_query_test.go +++ b/sei-cosmos/x/slashing/client/rest/grpc_query_test.go @@ -57,7 +57,7 @@ func (s *IntegrationTestSuite) TestGRPCQueries() { }{ { "get signing infos (height specific)", - fmt.Sprintf("%s/cosmos/slashing/v1beta1/signing_infos", baseURL), + fmt.Sprintf("%s/cosmos/slashing/v1beta1/signing_infos?pagination.count_total=true", baseURL), map[string]string{ grpctypes.GRPCBlockHeightHeader: "1", }, diff --git a/sei-cosmos/x/staking/keeper/grpc_query_test.go b/sei-cosmos/x/staking/keeper/grpc_query_test.go index 4bbedef486..ed18b246f0 100644 --- a/sei-cosmos/x/staking/keeper/grpc_query_test.go +++ b/sei-cosmos/x/staking/keeper/grpc_query_test.go @@ -28,7 +28,7 @@ func (suite *KeeperTestSuite) TestGRPCQueryValidators() { { "empty request", func() { - req = &types.QueryValidatorsRequest{} + req = &types.QueryValidatorsRequest{Pagination: &query.PageRequest{CountTotal: true}} }, true, @@ -38,7 +38,7 @@ func (suite *KeeperTestSuite) TestGRPCQueryValidators() { { "empty status returns all the validators", func() { - req = &types.QueryValidatorsRequest{Status: ""} + req = &types.QueryValidatorsRequest{Status: "", Pagination: &query.PageRequest{CountTotal: true}} }, true, len(vals), From 857c3017cba02a3bd36dd7a2bc3b9f38ea0c7b08 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Wed, 27 May 2026 11:43:47 -0700 Subject: [PATCH 13/21] Added cap for iteration before and after page is complete --- sei-cosmos/types/query/filtered_pagination.go | 22 +++++++++++++++---- .../types/query/filtered_pagination_test.go | 18 +++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/sei-cosmos/types/query/filtered_pagination.go b/sei-cosmos/types/query/filtered_pagination.go index 5862357513..56fe14d468 100644 --- a/sei-cosmos/types/query/filtered_pagination.go +++ b/sei-cosmos/types/query/filtered_pagination.go @@ -92,12 +92,19 @@ func FilteredPaginate( var ( numHits uint64 nextKey []byte + totalIter uint64 pageCompleteIter uint64 ) for ; iterator.Valid(); iterator.Next() { - // Only enforce the scan budget after the page is fully assembled, so - // that a selective filter cannot cause the limit to fire mid-page. + totalIter++ + // Phase 1: page not yet complete — cap raw iterations to prevent full-store + // walks when the filter produces too few hits to fill the page. + if countTotal && numHits < end && totalIter > end+MaxScanLimit { + return nil, status.Errorf(codes.InvalidArgument, + "scanned more than %d entries past the end of the requested page; use key-based pagination instead", MaxScanLimit) + } + // Phase 2: page complete — cap how far past the page we scan for count_total. if countTotal && pageCompleteIter > MaxScanLimit { return nil, status.Errorf(codes.InvalidArgument, "scanned more than %d entries past the end of the requested page; use key-based pagination instead", MaxScanLimit) @@ -231,12 +238,19 @@ func GenericFilteredPaginate[T codec.ProtoMarshaler, F codec.ProtoMarshaler]( var ( numHits uint64 nextKey []byte + totalIter uint64 pageCompleteIter uint64 ) for ; iterator.Valid(); iterator.Next() { - // Only enforce the scan budget after the page is fully assembled, so - // that a selective filter cannot cause the limit to fire mid-page. + totalIter++ + // Phase 1: page not yet complete — cap raw iterations to prevent full-store + // walks when the filter produces too few hits to fill the page. + if countTotal && numHits < end && totalIter > end+MaxScanLimit { + return nil, nil, status.Errorf(codes.InvalidArgument, + "scanned more than %d entries past the end of the requested page; use key-based pagination instead", MaxScanLimit) + } + // Phase 2: page complete — cap how far past the page we scan for count_total. if countTotal && pageCompleteIter > MaxScanLimit { return nil, nil, status.Errorf(codes.InvalidArgument, "scanned more than %d entries past the end of the requested page; use key-based pagination instead", MaxScanLimit) diff --git a/sei-cosmos/types/query/filtered_pagination_test.go b/sei-cosmos/types/query/filtered_pagination_test.go index ccd94714dd..9ba60e0957 100644 --- a/sei-cosmos/types/query/filtered_pagination_test.go +++ b/sei-cosmos/types/query/filtered_pagination_test.go @@ -213,6 +213,24 @@ func (s *paginationTestSuite) TestFilteredPaginateCountTotalScanLimitExceeded() s.Require().Contains(err.Error(), "scanned more than") } +func (s *paginationTestSuite) TestFilteredPaginateCountTotalScanLimitExceededNoHits() { + app, ctx, _ := setupTest(s.T()) + kvStore := prefix.NewStore(ctx.KVStore(app.GetKey(types.StoreKey)), []byte("filteredscanlimitnohits/")) + + // end = offset + limit = 0 + 1 = 1; Phase 1 fires when totalIter > end + MaxScanLimit = 10001 + numItems := int(query.MaxScanLimit) + 2 + for i := 0; i < numItems; i++ { + kvStore.Set([]byte(fmt.Sprintf("%08d", i)), []byte("v")) + } + + // filter returns no hits — numHits never reaches end, Phase 1 guard must fire + _, err := query.FilteredPaginate(kvStore, &query.PageRequest{Limit: 1, CountTotal: true}, func(_ []byte, _ []byte, _ bool) (bool, error) { + return false, nil + }) + s.Require().Error(err) + s.Require().Contains(err.Error(), "scanned more than") +} + func execFilterPaginate(store sdk.KVStore, pageReq *query.PageRequest, appCodec codec.Codec) (balances sdk.Coins, res *query.PageResponse, err error) { balancesStore := prefix.NewStore(store, types.BalancesPrefix) accountStore := prefix.NewStore(balancesStore, address.MustLengthPrefix(addr1)) From 8846f6ff3689129bf092c6f1b5a1ad8e8a6aedcd Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Thu, 28 May 2026 14:08:45 -0700 Subject: [PATCH 14/21] Removed countTotal condition for limit, applied limit to key branch --- sei-cosmos/types/query/filtered_pagination.go | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/sei-cosmos/types/query/filtered_pagination.go b/sei-cosmos/types/query/filtered_pagination.go index 56fe14d468..fccb5fd40e 100644 --- a/sei-cosmos/types/query/filtered_pagination.go +++ b/sei-cosmos/types/query/filtered_pagination.go @@ -55,11 +55,18 @@ func FilteredPaginate( defer func() { _ = iterator.Close() }() var ( - numHits uint64 - nextKey []byte + numHits uint64 + nextKey []byte + totalIter uint64 ) for ; iterator.Valid(); iterator.Next() { + totalIter++ + if totalIter > MaxScanLimit { + return nil, status.Errorf(codes.InvalidArgument, + "scanned more than %d entries without filling the page; use a more specific key prefix or reduce limit", MaxScanLimit) + } + if numHits == limit { nextKey = iterator.Key() break @@ -100,14 +107,14 @@ func FilteredPaginate( totalIter++ // Phase 1: page not yet complete — cap raw iterations to prevent full-store // walks when the filter produces too few hits to fill the page. - if countTotal && numHits < end && totalIter > end+MaxScanLimit { + if numHits < end && totalIter > offset+MaxScanLimit { return nil, status.Errorf(codes.InvalidArgument, - "scanned more than %d entries past the end of the requested page; use key-based pagination instead", MaxScanLimit) + "scanned more than %d entries without filling the page; use key-based pagination instead", MaxScanLimit) } - // Phase 2: page complete — cap how far past the page we scan for count_total. - if countTotal && pageCompleteIter > MaxScanLimit { + // Phase 2: page complete — cap how far past the page we scan for nextKey/count_total. + if pageCompleteIter > MaxScanLimit { return nil, status.Errorf(codes.InvalidArgument, - "scanned more than %d entries past the end of the requested page; use key-based pagination instead", MaxScanLimit) + "scanned more than %d entries past the end of the page; use key-based pagination instead", MaxScanLimit) } if iterator.Error() != nil { @@ -193,11 +200,18 @@ func GenericFilteredPaginate[T codec.ProtoMarshaler, F codec.ProtoMarshaler]( defer func() { _ = iterator.Close() }() var ( - numHits uint64 - nextKey []byte + numHits uint64 + nextKey []byte + totalIter uint64 ) for ; iterator.Valid(); iterator.Next() { + totalIter++ + if totalIter > MaxScanLimit { + return nil, nil, status.Errorf(codes.InvalidArgument, + "scanned more than %d entries without filling the page; use a more specific key prefix or reduce limit", MaxScanLimit) + } + if numHits == limit { nextKey = iterator.Key() break @@ -246,14 +260,14 @@ func GenericFilteredPaginate[T codec.ProtoMarshaler, F codec.ProtoMarshaler]( totalIter++ // Phase 1: page not yet complete — cap raw iterations to prevent full-store // walks when the filter produces too few hits to fill the page. - if countTotal && numHits < end && totalIter > end+MaxScanLimit { + if numHits < end && totalIter > offset+MaxScanLimit { return nil, nil, status.Errorf(codes.InvalidArgument, - "scanned more than %d entries past the end of the requested page; use key-based pagination instead", MaxScanLimit) + "scanned more than %d entries without filling the page; use key-based pagination instead", MaxScanLimit) } - // Phase 2: page complete — cap how far past the page we scan for count_total. - if countTotal && pageCompleteIter > MaxScanLimit { + // Phase 2: page complete — cap how far past the page we scan for nextKey/count_total. + if pageCompleteIter > MaxScanLimit { return nil, nil, status.Errorf(codes.InvalidArgument, - "scanned more than %d entries past the end of the requested page; use key-based pagination instead", MaxScanLimit) + "scanned more than %d entries past the end of the page; use key-based pagination instead", MaxScanLimit) } if iterator.Error() != nil { From edaaacc10d2ea0195f69eec8f17b689968ad03f3 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Thu, 28 May 2026 14:43:24 -0700 Subject: [PATCH 15/21] Cleaned up pagination.go --- sei-cosmos/types/query/pagination.go | 45 +++++----------------------- 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/sei-cosmos/types/query/pagination.go b/sei-cosmos/types/query/pagination.go index a6fbf8f1a1..6779cf8e97 100644 --- a/sei-cosmos/types/query/pagination.go +++ b/sei-cosmos/types/query/pagination.go @@ -70,9 +70,6 @@ func VerifyPaginationOffset(offset uint64) error { return nil } -// Paginate does pagination of all the results in the PrefixStore based on the -// provided PageRequest. onResult should be used to do actual unmarshaling. -// Limits are capped at MaxLimit func Paginate( prefixStore types.KVStore, pageRequest *PageRequest, @@ -81,49 +78,23 @@ func Paginate( if pageRequest == nil { pageRequest = &PageRequest{} } - + offset := pageRequest.Offset + key := pageRequest.Key limit := pageRequest.Limit if limit == 0 { limit = DefaultLimit } - if err := VerifyPaginationLimit(limit); err != nil { - return nil, err - } - - return paginate(prefixStore, pageRequest, onResult) -} - -func paginate( - prefixStore types.KVStore, - pageRequest *PageRequest, - onResult func(key []byte, value []byte) error, -) (*PageResponse, error) { - - if pageRequest == nil { - pageRequest = &PageRequest{} - } - - offset := pageRequest.Offset - key := pageRequest.Key - limit := pageRequest.Limit - countTotal := pageRequest.CountTotal - reverse := pageRequest.Reverse - if offset > 0 && key != nil { return nil, fmt.Errorf("invalid request, either offset or key is expected, got both") } - if err := VerifyPaginationLimit(limit); err != nil { return nil, err } - if err := VerifyPaginationOffset(offset); err != nil { return nil, err } - - if limit == 0 { - limit = DefaultLimit - } + countTotal := pageRequest.CountTotal + reverse := pageRequest.Reverse if len(key) != 0 { iterator := getIterator(prefixStore, key, reverse) @@ -133,7 +104,7 @@ func paginate( var nextKey []byte for ; iterator.Valid(); iterator.Next() { - + count++ if count == limit { nextKey = iterator.Key() break @@ -145,8 +116,6 @@ func paginate( if err != nil { return nil, err } - - count++ } return &PageResponse{ @@ -165,9 +134,9 @@ func paginate( for ; iterator.Valid(); iterator.Next() { count++ - if countTotal && count > end+MaxScanLimit { + if count > offset+MaxScanLimit { return nil, status.Errorf(codes.InvalidArgument, - "count_total scan exceeds maximum of %d items past the page; use key-based pagination instead", MaxScanLimit) + "scanned more than %d entries past the end of the page; use key-based pagination instead", MaxScanLimit) } if count <= offset { From 2d6c0f8478fd842e0fef80c98c28713fce46a76f Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Thu, 28 May 2026 15:43:39 -0700 Subject: [PATCH 16/21] fixing tests --- sei-cosmos/types/query/filtered_pagination_test.go | 5 +++-- sei-cosmos/types/query/pagination.go | 2 +- sei-cosmos/types/query/pagination_test.go | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/sei-cosmos/types/query/filtered_pagination_test.go b/sei-cosmos/types/query/filtered_pagination_test.go index 9ba60e0957..4747e70ecf 100644 --- a/sei-cosmos/types/query/filtered_pagination_test.go +++ b/sei-cosmos/types/query/filtered_pagination_test.go @@ -217,14 +217,15 @@ func (s *paginationTestSuite) TestFilteredPaginateCountTotalScanLimitExceededNoH app, ctx, _ := setupTest(s.T()) kvStore := prefix.NewStore(ctx.KVStore(app.GetKey(types.StoreKey)), []byte("filteredscanlimitnohits/")) - // end = offset + limit = 0 + 1 = 1; Phase 1 fires when totalIter > end + MaxScanLimit = 10001 + // Phase 1 fires when totalIter > offset + MaxScanLimit = 10001 + pageReq := &query.PageRequest{Offset: 1, CountTotal: true} numItems := int(query.MaxScanLimit) + 2 for i := 0; i < numItems; i++ { kvStore.Set([]byte(fmt.Sprintf("%08d", i)), []byte("v")) } // filter returns no hits — numHits never reaches end, Phase 1 guard must fire - _, err := query.FilteredPaginate(kvStore, &query.PageRequest{Limit: 1, CountTotal: true}, func(_ []byte, _ []byte, _ bool) (bool, error) { + _, err := query.FilteredPaginate(kvStore, pageReq, func(_ []byte, _ []byte, _ bool) (bool, error) { return false, nil }) s.Require().Error(err) diff --git a/sei-cosmos/types/query/pagination.go b/sei-cosmos/types/query/pagination.go index 6779cf8e97..0290a837b4 100644 --- a/sei-cosmos/types/query/pagination.go +++ b/sei-cosmos/types/query/pagination.go @@ -104,7 +104,6 @@ func Paginate( var nextKey []byte for ; iterator.Valid(); iterator.Next() { - count++ if count == limit { nextKey = iterator.Key() break @@ -116,6 +115,7 @@ func Paginate( if err != nil { return nil, err } + count++ } return &PageResponse{ diff --git a/sei-cosmos/types/query/pagination_test.go b/sei-cosmos/types/query/pagination_test.go index d57388a2b4..e9c2c888bc 100644 --- a/sei-cosmos/types/query/pagination_test.go +++ b/sei-cosmos/types/query/pagination_test.go @@ -340,7 +340,7 @@ func (s *paginationTestSuite) TestPaginateCountTotalScanLimitExceeded() { // Use a dedicated prefix to isolate test data from other store entries. kvStore := prefix.NewStore(ctx.KVStore(app.GetKey(types.StoreKey)), []byte("scanlimit/")) - // With limit=1 and offset=0: end=1, scan cap fires when count > end+MaxScanLimit = 10,001. + // With offset=1, scan cap fires when count > offset+MaxScanLimit = 10,001. // Insert 10,002 items to guarantee the cap is exceeded. numItems := int(query.MaxScanLimit) + 2 for i := 0; i < numItems; i++ { @@ -349,7 +349,7 @@ func (s *paginationTestSuite) TestPaginateCountTotalScanLimitExceeded() { _, err := query.Paginate(kvStore, &query.PageRequest{Limit: 1, CountTotal: true}, func(_, _ []byte) error { return nil }) s.Require().Error(err) - s.Require().Contains(err.Error(), "count_total scan exceeds maximum") + s.Require().Contains(err.Error(), fmt.Sprintf("scanned more than %d entries", query.MaxScanLimit)) } func setupTest(t *testing.T) (*app.App, sdk.Context, codec.Codec) { From 6231007435e9b1daaa1e26e3dc08f413c4e38731 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Thu, 28 May 2026 16:22:11 -0700 Subject: [PATCH 17/21] Moved page complete check before scan limit check --- sei-cosmos/types/query/filtered_pagination.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/sei-cosmos/types/query/filtered_pagination.go b/sei-cosmos/types/query/filtered_pagination.go index fccb5fd40e..ec2a50611b 100644 --- a/sei-cosmos/types/query/filtered_pagination.go +++ b/sei-cosmos/types/query/filtered_pagination.go @@ -62,15 +62,14 @@ func FilteredPaginate( for ; iterator.Valid(); iterator.Next() { totalIter++ - if totalIter > MaxScanLimit { - return nil, status.Errorf(codes.InvalidArgument, - "scanned more than %d entries without filling the page; use a more specific key prefix or reduce limit", MaxScanLimit) - } - if numHits == limit { nextKey = iterator.Key() break } + if totalIter > MaxScanLimit { + return nil, status.Errorf(codes.InvalidArgument, + "scanned more than %d entries without filling the page; use a more specific key prefix or reduce limit", MaxScanLimit) + } if iterator.Error() != nil { return nil, iterator.Error() @@ -207,15 +206,14 @@ func GenericFilteredPaginate[T codec.ProtoMarshaler, F codec.ProtoMarshaler]( for ; iterator.Valid(); iterator.Next() { totalIter++ - if totalIter > MaxScanLimit { - return nil, nil, status.Errorf(codes.InvalidArgument, - "scanned more than %d entries without filling the page; use a more specific key prefix or reduce limit", MaxScanLimit) - } - if numHits == limit { nextKey = iterator.Key() break } + if totalIter > MaxScanLimit { + return nil, nil, status.Errorf(codes.InvalidArgument, + "scanned more than %d entries without filling the page; use a more specific key prefix or reduce limit", MaxScanLimit) + } if iterator.Error() != nil { return nil, nil, iterator.Error() From 9785eba55b43e1f689c35aebdfebf16ee1659161 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Fri, 29 May 2026 11:16:32 -0700 Subject: [PATCH 18/21] Fixing max scan limit past page for pagination.go --- sei-cosmos/types/query/pagination.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sei-cosmos/types/query/pagination.go b/sei-cosmos/types/query/pagination.go index 0290a837b4..825cfe49a7 100644 --- a/sei-cosmos/types/query/pagination.go +++ b/sei-cosmos/types/query/pagination.go @@ -134,7 +134,7 @@ func Paginate( for ; iterator.Valid(); iterator.Next() { count++ - if count > offset+MaxScanLimit { + if count > end+MaxScanLimit { return nil, status.Errorf(codes.InvalidArgument, "scanned more than %d entries past the end of the page; use key-based pagination instead", MaxScanLimit) } From 07cb06447943b5fa7dc6e71676fb96705e14fa86 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Fri, 29 May 2026 11:33:56 -0700 Subject: [PATCH 19/21] Added default limit 0 to GetBlockWithTxs --- sei-cosmos/x/auth/tx/service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sei-cosmos/x/auth/tx/service.go b/sei-cosmos/x/auth/tx/service.go index 8b604380b1..f588f82705 100644 --- a/sei-cosmos/x/auth/tx/service.go +++ b/sei-cosmos/x/auth/tx/service.go @@ -195,8 +195,8 @@ func (s txServer) GetBlockWithTxs(ctx context.Context, req *txtypes.GetBlockWith if err = pagination.VerifyPaginationLimit(limit); err != nil { return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid pagination limit: %d. Max allowed limit is %d", limit, pagination.MaxLimit) } - } else { - offset = 0 + } + if limit == 0 { limit = pagination.DefaultLimit } From bc5e8ce04b740e326363791e298c4c130312f0b3 Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Fri, 29 May 2026 13:20:33 -0700 Subject: [PATCH 20/21] return already assembled page if reached to scan limit and not looking for count total --- sei-cosmos/types/query/filtered_pagination.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sei-cosmos/types/query/filtered_pagination.go b/sei-cosmos/types/query/filtered_pagination.go index ec2a50611b..21e5c46015 100644 --- a/sei-cosmos/types/query/filtered_pagination.go +++ b/sei-cosmos/types/query/filtered_pagination.go @@ -112,6 +112,10 @@ func FilteredPaginate( } // Phase 2: page complete — cap how far past the page we scan for nextKey/count_total. if pageCompleteIter > MaxScanLimit { + if !countTotal { + // Page is already assembled; no next hit found within scan window → no next page. + break + } return nil, status.Errorf(codes.InvalidArgument, "scanned more than %d entries past the end of the page; use key-based pagination instead", MaxScanLimit) } @@ -264,6 +268,10 @@ func GenericFilteredPaginate[T codec.ProtoMarshaler, F codec.ProtoMarshaler]( } // Phase 2: page complete — cap how far past the page we scan for nextKey/count_total. if pageCompleteIter > MaxScanLimit { + if !countTotal { + // Page is already assembled; no next hit found within scan window → no next page. + break + } return nil, nil, status.Errorf(codes.InvalidArgument, "scanned more than %d entries past the end of the page; use key-based pagination instead", MaxScanLimit) } From a2c5e5b544dff2d6d1e334b19ffac26e6501cb3c Mon Sep 17 00:00:00 2001 From: Amir Deris Date: Fri, 29 May 2026 13:44:21 -0700 Subject: [PATCH 21/21] Added previously removed comment --- sei-cosmos/types/query/pagination.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sei-cosmos/types/query/pagination.go b/sei-cosmos/types/query/pagination.go index 825cfe49a7..ec6b1f9f60 100644 --- a/sei-cosmos/types/query/pagination.go +++ b/sei-cosmos/types/query/pagination.go @@ -70,6 +70,8 @@ func VerifyPaginationOffset(offset uint64) error { return nil } +// Paginate does pagination of all the results in the PrefixStore based on the +// provided PageRequest. onResult should be used to do actual unmarshaling. func Paginate( prefixStore types.KVStore, pageRequest *PageRequest,