cosmos / cosmjs

The Swiss Army knife to power JavaScript based client solutions ranging from Web apps/explorers over browser extensions to server-side clients like faucets/scrapers.
https://cosmos.github.io/cosmjs/
Apache License 2.0
646 stars 331 forks source link

Unable to paginate queries in StakingExtension/validatorDelegations #1179

Open iicc1 opened 2 years ago

iicc1 commented 2 years ago

Since fetching all the validator delegations is a very intensive method, trying to get all values directly usually results in an error, as already noted here https://github.com/cosmos/cosmjs/issues/1049.

I'm trying to get a paginated result with Stargate, StakingExtension, and the method validatorDelegations but I don't know what should I pass as the paginationKey parameter.

According to the docs, paginationKey should be an Uint8Array, but what should I set as my first paginationKey, when I have none? I have been trying by setting paginationKey as an object with pagination information, null, '', etc. without result. All the time it tries to get all the results directly which means that the pagination is not working and the node is not able to answer this request.

This is my code:

const { StargateClient } = require('@cosmjs/stargate')
const provider = 'https://cosmoshub-rpc.stakely.io/'
const client = await StargateClient.connect(provider)
const validatorAdress = 'cosmosvaloper16yupepagywvlk7uhpfchtwa0stu5f8cyhh54f2'

...
// Test 1
const pagination = '' // Timeout, pagination not working
// Test 2
const pagination = null // Timeout, pagination not working
// Test 3 
const pagination = {
      countTotal: false,
      key: null,
      limit: 50,
      reverse: false,
} // Timeout, pagination not working
// Test 4
const pagination = new Uint8Array() // Timeout, pagination not working

const queryData = await client.queryClient.staking.validatorDelegations(validatorAdress, pagination)
console.log(queryData )

This would be the request using the LCD, which works fine: https://cosmoshub-lcd.stakely.io/cosmos/staking/v1beta1/validators/cosmosvaloper16yupepagywvlk7uhpfchtwa0stu5f8cyhh54f2/delegations?pagination.limit=50

Thanks in advance.

iicc1 commented 2 years ago

Hello, any idea? Thanks

iicc1 commented 2 years ago

Any help would be appreciated

webmaster128 commented 2 years ago

It seems like you are not using TypeScript, which makes your life very hard you don't get errors when inserting the wrong type.

validatorDelegations takes an optional pagination key argument of type Uint8Array | undefined. You can leave it undefined for the first query. Then set value to the pagination.nextKey from the response.

iicc1 commented 2 years ago

It seems like you are not using TypeScript, which makes your life very hard you don't get errors when inserting the wrong type.

validatorDelegations takes an optional pagination key argument of type Uint8Array | undefined. You can leave it undefined for the first query. Then set value to the pagination.nextKey from the response.

Thanks for your reply. The problem is that if you set an undefined or an invalid pagination key argument, it requests the full delegators list from the RPC. Since that query is too intensive, it results in a timeout error. We would need to be able to set a pagination limit in the first request, like this LCD query does: https://cosmoshub-lcd.stakely.io/cosmos/staking/v1beta1/validators/cosmosvaloper16yupepagywvlk7uhpfchtwa0stu5f8cyhh54f2/delegations?pagination.limit=50

webmaster128 commented 2 years ago

Ah, so that means that the default limit in the backend is too large or even unlimited? If that's the case, this is a Cosmos SDK bug. The default limit should be query specific but reasonable.

The high level query client corrently does not allow changing the limit as we did not have this issue before.

iicc1 commented 2 years ago

Correct, the default limit is unlimited thus for validators with over 1000 delegators it is almost impossible to request this information via RPC

webmaster128 commented 2 years ago

This is strange. There is this code in Cosmos SDK v0.45.6:

// DefaultLimit is the default `limit` for queries
// if the `limit` is not supplied, paginate will use `DefaultLimit`
const DefaultLimit = 100

and

func FilteredPaginate(
    prefixStore types.KVStore,
    pageRequest *PageRequest,
    onResult func(key []byte, value []byte, accumulate bool) (bool, error),
) (*PageResponse, error) {

    // if the PageRequest is nil, use default PageRequest
    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 limit == 0 {
        limit = DefaultLimit

        // count total results when the limit is zero/not supplied
        countTotal = true
    }

which is used in

// ValidatorDelegations queries delegate info for given validator
func (k Querier) ValidatorDelegations(c context.Context, req *types.QueryValidatorDelegationsRequest) (*types.QueryValidatorDelegationsResponse, error) {
    if req == nil {
        return nil, status.Error(codes.InvalidArgument, "empty request")
    }

    if req.ValidatorAddr == "" {
        return nil, status.Error(codes.InvalidArgument, "validator address cannot be empty")
    }
    var delegations []types.Delegation
    ctx := sdk.UnwrapSDKContext(c)

    store := ctx.KVStore(k.storeKey)
    valStore := prefix.NewStore(store, types.DelegationKey)
    pageRes, err := query.FilteredPaginate(valStore, req.Pagination, func(key []byte, value []byte, accumulate bool) (bool, error) {
        delegation, err := types.UnmarshalDelegation(k.cdc, value)
        if err != nil {
            return false, err
        }

        valAddr, err := sdk.ValAddressFromBech32(req.ValidatorAddr)
        if err != nil {
            return false, err
        }

        if !delegation.GetValidatorAddr().Equals(valAddr) {
            return false, nil
        }

        if accumulate {
            delegations = append(delegations, delegation)
        }
        return true, nil
    })
    if err != nil {
        return nil, status.Error(codes.Internal, err.Error())
    }

    delResponses, err := DelegationsToDelegationResponses(ctx, k.Keeper, delegations)
    if err != nil {
        return nil, status.Error(codes.Internal, err.Error())
    }

    return &types.QueryValidatorDelegationsResponse{
        DelegationResponses: delResponses, Pagination: pageRes}, nil
}

But maybe thos 100 is even too much because the query implementation is inefficient?

Can you test this on your own node?

See also https://github.com/cosmos/cosmjs/issues/1209 and https://github.com/cosmos/cosmos-sdk/issues/12756

iicc1 commented 2 years ago

I have tried the query against one of our nodes that is deployed on a really powerful dedicated server and it times out.

k-yang commented 1 year ago

To provide some clarity, setting a client-side limit won't save us here because the query loops over ALL delegations on the chain and then selects the ones whose validatorAddr match the provided validatorAddr in the query. It will eventually lead to an I/O timeout if you have many delegations.

The fix here would be to add a validator + delegation index on-chain (cosmos-sdk state breaking change) or to index the delegations off-chain in a centralized database somewhere and serve them to your web app via traditional web2 means (e.g., SQL database + REST API).