Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
da36bbb
Updated max limit for sei_cosmos query pagination
amir-deris May 21, 2026
d5241fa
Enforced pagination limit and used IterateTotalSupply
amir-deris May 21, 2026
561eb56
Merge branch 'main' into amir/plt-361-lower-sei-cosmos-pagination-que…
amir-deris May 22, 2026
b14257e
Added pagination limit check to other entry points
amir-deris May 22, 2026
ac977b3
Added pagination verification to parsePagination
amir-deris May 22, 2026
cb374e0
Used pagination to fetch total supply in genesis and invariants
amir-deris May 22, 2026
89dd0cc
Added nosec comment
amir-deris May 22, 2026
4d427b6
Added tests
amir-deris May 22, 2026
67908cc
Fixing lint error
amir-deris May 22, 2026
d27d285
Merge branch 'main' into amir/plt-361-lower-sei-cosmos-pagination-que…
amir-deris May 23, 2026
84a9803
Reduced pagination page limit to 1000
amir-deris May 26, 2026
b1408c2
Merge branch 'main' into amir/plt-361-lower-sei-cosmos-pagination-que…
amir-deris May 26, 2026
811f3b3
Merge branch 'main' into amir/plt-361-lower-sei-cosmos-pagination-que…
amir-deris May 26, 2026
d728fdf
Added further validations and tests
amir-deris May 27, 2026
3210ac1
Merge branch 'main' into amir/plt-361-lower-sei-cosmos-pagination-que…
amir-deris May 27, 2026
c8bc04f
Added count safety check after the page is complete
amir-deris May 27, 2026
21e8e4f
Fixed query tests
amir-deris May 27, 2026
857c301
Added cap for iteration before and after page is complete
amir-deris May 27, 2026
8846f6f
Removed countTotal condition for limit, applied limit to key branch
amir-deris May 28, 2026
84b53fb
Merge branch 'main' into amir/plt-361-lower-sei-cosmos-pagination-que…
amir-deris May 28, 2026
edaaacc
Cleaned up pagination.go
amir-deris May 28, 2026
2d6c0f8
fixing tests
amir-deris May 28, 2026
6231007
Moved page complete check before scan limit check
amir-deris May 28, 2026
9785eba
Fixing max scan limit past page for pagination.go
amir-deris May 29, 2026
07cb064
Added default limit 0 to GetBlockWithTxs
amir-deris May 29, 2026
6b0b9fc
Merge branch 'main' into amir/plt-361-lower-sei-cosmos-pagination-que…
amir-deris May 29, 2026
bc5e8ce
return already assembled page if reached to scan limit and not lookin…
amir-deris May 29, 2026
a2c5e5b
Added previously removed comment
amir-deris May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 82 additions & 12 deletions sei-cosmos/types/query/filtered_pagination.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -36,27 +38,38 @@ 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 {
return nil, err
}

if len(key) != 0 {
iterator := getIterator(prefixStore, key, reverse)
defer func() { _ = iterator.Close() }()

var (
numHits uint64
nextKey []byte
numHits uint64
nextKey []byte
totalIter uint64
)

for ; iterator.Valid(); iterator.Next() {
totalIter++
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()
Expand All @@ -83,11 +96,30 @@ func FilteredPaginate(
end := offset + limit

var (
numHits uint64
nextKey []byte
numHits uint64
nextKey []byte
totalIter uint64
pageCompleteIter uint64
)

for ; iterator.Valid(); iterator.Next() {
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 numHits < end && totalIter > offset+MaxScanLimit {
Comment thread
amir-deris marked this conversation as resolved.
return nil, status.Errorf(codes.InvalidArgument,
"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 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)
}
Comment thread
amir-deris marked this conversation as resolved.
Comment thread
amir-deris marked this conversation as resolved.

if iterator.Error() != nil {
return nil, iterator.Error()
}
Expand All @@ -102,6 +134,10 @@ func FilteredPaginate(
numHits++
}

if numHits >= end {
pageCompleteIter++
}

if numHits == end+1 {
nextKey = iterator.Key()

Expand Down Expand Up @@ -150,27 +186,38 @@ 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 {
return results, nil, err
}

if len(key) != 0 {
iterator := getIterator(prefixStore, key, reverse)
defer func() { _ = iterator.Close() }()

var (
numHits uint64
nextKey []byte
numHits uint64
nextKey []byte
totalIter uint64
)

for ; iterator.Valid(); iterator.Next() {
totalIter++
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()
Expand Down Expand Up @@ -205,11 +252,30 @@ func GenericFilteredPaginate[T codec.ProtoMarshaler, F codec.ProtoMarshaler](
end := offset + limit

var (
numHits uint64
nextKey []byte
numHits uint64
nextKey []byte
totalIter uint64
pageCompleteIter uint64
)

for ; iterator.Valid(); iterator.Next() {
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 numHits < end && totalIter > offset+MaxScanLimit {
return nil, nil, status.Errorf(codes.InvalidArgument,
"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 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)
}

if iterator.Error() != nil {
return nil, nil, iterator.Error()
}
Expand All @@ -234,6 +300,10 @@ func GenericFilteredPaginate[T codec.ProtoMarshaler, F codec.ProtoMarshaler](
numHits++
}

if numHits >= end {
pageCompleteIter++
}

if numHits == end+1 {
if nextKey == nil {
nextKey = iterator.Key()
Expand Down
70 changes: 66 additions & 4 deletions sei-cosmos/types/query/filtered_pagination_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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")
Expand All @@ -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}
Expand Down Expand Up @@ -170,6 +170,68 @@ 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 (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(), "scanned more than")
}

func (s *paginationTestSuite) TestFilteredPaginateCountTotalScanLimitExceededNoHits() {
app, ctx, _ := setupTest(s.T())
kvStore := prefix.NewStore(ctx.KVStore(app.GetKey(types.StoreKey)), []byte("filteredscanlimitnohits/"))

// 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, pageReq, 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))
Expand Down
Loading
Loading