github.com/516108736/tendermint@v0.36.0/light/rpc/client.go (about)

     1  package rpc
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"time"
     9  
    10  	"github.com/gogo/protobuf/proto"
    11  
    12  	abci "github.com/tendermint/tendermint/abci/types"
    13  	"github.com/tendermint/tendermint/crypto/merkle"
    14  	tmbytes "github.com/tendermint/tendermint/libs/bytes"
    15  	tmmath "github.com/tendermint/tendermint/libs/math"
    16  	service "github.com/tendermint/tendermint/libs/service"
    17  	rpcclient "github.com/tendermint/tendermint/rpc/client"
    18  	ctypes "github.com/tendermint/tendermint/rpc/core/types"
    19  	rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
    20  	"github.com/tendermint/tendermint/types"
    21  )
    22  
    23  var errNegOrZeroHeight = errors.New("negative or zero height")
    24  
    25  // KeyPathFunc builds a merkle path out of the given path and key.
    26  type KeyPathFunc func(path string, key []byte) (merkle.KeyPath, error)
    27  
    28  // LightClient is an interface that contains functionality needed by Client from the light client.
    29  //go:generate mockery --case underscore --name LightClient
    30  type LightClient interface {
    31  	ChainID() string
    32  	Update(ctx context.Context, now time.Time) (*types.LightBlock, error)
    33  	VerifyLightBlockAtHeight(ctx context.Context, height int64, now time.Time) (*types.LightBlock, error)
    34  	TrustedLightBlock(height int64) (*types.LightBlock, error)
    35  }
    36  
    37  // Client is an RPC client, which uses light#Client to verify data (if it can
    38  // be proved!). merkle.DefaultProofRuntime is used to verify values returned by
    39  // ABCIQuery.
    40  type Client struct {
    41  	service.BaseService
    42  
    43  	next rpcclient.Client
    44  	lc   LightClient
    45  	// Proof runtime used to verify values returned by ABCIQuery
    46  	prt       *merkle.ProofRuntime
    47  	keyPathFn KeyPathFunc
    48  }
    49  
    50  var _ rpcclient.Client = (*Client)(nil)
    51  
    52  // Option allow you to tweak Client.
    53  type Option func(*Client)
    54  
    55  // KeyPathFn option can be used to set a function, which parses a given path
    56  // and builds the merkle path for the prover. It must be provided if you want
    57  // to call ABCIQuery or ABCIQueryWithOptions.
    58  func KeyPathFn(fn KeyPathFunc) Option {
    59  	return func(c *Client) {
    60  		c.keyPathFn = fn
    61  	}
    62  }
    63  
    64  // NewClient returns a new client.
    65  func NewClient(next rpcclient.Client, lc LightClient, opts ...Option) *Client {
    66  	c := &Client{
    67  		next: next,
    68  		lc:   lc,
    69  		prt:  merkle.DefaultProofRuntime(),
    70  	}
    71  	c.BaseService = *service.NewBaseService(nil, "Client", c)
    72  	for _, o := range opts {
    73  		o(c)
    74  	}
    75  	return c
    76  }
    77  
    78  func (c *Client) OnStart() error {
    79  	if !c.next.IsRunning() {
    80  		return c.next.Start()
    81  	}
    82  	return nil
    83  }
    84  
    85  func (c *Client) OnStop() {
    86  	if c.next.IsRunning() {
    87  		if err := c.next.Stop(); err != nil {
    88  			c.Logger.Error("Error stopping on next", "err", err)
    89  		}
    90  	}
    91  }
    92  
    93  func (c *Client) Status(ctx context.Context) (*ctypes.ResultStatus, error) {
    94  	return c.next.Status(ctx)
    95  }
    96  
    97  func (c *Client) ABCIInfo(ctx context.Context) (*ctypes.ResultABCIInfo, error) {
    98  	return c.next.ABCIInfo(ctx)
    99  }
   100  
   101  // ABCIQuery requests proof by default.
   102  func (c *Client) ABCIQuery(ctx context.Context, path string, data tmbytes.HexBytes) (*ctypes.ResultABCIQuery, error) {
   103  	return c.ABCIQueryWithOptions(ctx, path, data, rpcclient.DefaultABCIQueryOptions)
   104  }
   105  
   106  // ABCIQueryWithOptions returns an error if opts.Prove is false.
   107  func (c *Client) ABCIQueryWithOptions(ctx context.Context, path string, data tmbytes.HexBytes,
   108  	opts rpcclient.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) {
   109  
   110  	// always request the proof
   111  	opts.Prove = true
   112  
   113  	res, err := c.next.ABCIQueryWithOptions(ctx, path, data, opts)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	resp := res.Response
   118  
   119  	// Validate the response.
   120  	if resp.IsErr() {
   121  		return nil, fmt.Errorf("err response code: %v", resp.Code)
   122  	}
   123  	if len(resp.Key) == 0 {
   124  		return nil, errors.New("empty key")
   125  	}
   126  	if resp.ProofOps == nil || len(resp.ProofOps.Ops) == 0 {
   127  		return nil, errors.New("no proof ops")
   128  	}
   129  	if resp.Height <= 0 {
   130  		return nil, errNegOrZeroHeight
   131  	}
   132  
   133  	// Update the light client if we're behind.
   134  	// NOTE: AppHash for height H is in header H+1.
   135  	nextHeight := resp.Height + 1
   136  	l, err := c.updateLightClientIfNeededTo(ctx, &nextHeight)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	// Validate the value proof against the trusted header.
   142  	if resp.Value != nil {
   143  		// 1) build a Merkle key path from path and resp.Key
   144  		if c.keyPathFn == nil {
   145  			return nil, errors.New("please configure Client with KeyPathFn option")
   146  		}
   147  
   148  		kp, err := c.keyPathFn(path, resp.Key)
   149  		if err != nil {
   150  			return nil, fmt.Errorf("can't build merkle key path: %w", err)
   151  		}
   152  
   153  		// 2) verify value
   154  		err = c.prt.VerifyValue(resp.ProofOps, l.AppHash, kp.String(), resp.Value)
   155  		if err != nil {
   156  			return nil, fmt.Errorf("verify value proof: %w", err)
   157  		}
   158  	} else { // OR validate the absence proof against the trusted header.
   159  		err = c.prt.VerifyAbsence(resp.ProofOps, l.AppHash, string(resp.Key))
   160  		if err != nil {
   161  			return nil, fmt.Errorf("verify absence proof: %w", err)
   162  		}
   163  	}
   164  
   165  	return &ctypes.ResultABCIQuery{Response: resp}, nil
   166  }
   167  
   168  func (c *Client) BroadcastTxCommit(ctx context.Context, tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) {
   169  	return c.next.BroadcastTxCommit(ctx, tx)
   170  }
   171  
   172  func (c *Client) BroadcastTxAsync(ctx context.Context, tx types.Tx) (*ctypes.ResultBroadcastTx, error) {
   173  	return c.next.BroadcastTxAsync(ctx, tx)
   174  }
   175  
   176  func (c *Client) BroadcastTxSync(ctx context.Context, tx types.Tx) (*ctypes.ResultBroadcastTx, error) {
   177  	return c.next.BroadcastTxSync(ctx, tx)
   178  }
   179  
   180  func (c *Client) UnconfirmedTxs(ctx context.Context, limit *int) (*ctypes.ResultUnconfirmedTxs, error) {
   181  	return c.next.UnconfirmedTxs(ctx, limit)
   182  }
   183  
   184  func (c *Client) NumUnconfirmedTxs(ctx context.Context) (*ctypes.ResultUnconfirmedTxs, error) {
   185  	return c.next.NumUnconfirmedTxs(ctx)
   186  }
   187  
   188  func (c *Client) CheckTx(ctx context.Context, tx types.Tx) (*ctypes.ResultCheckTx, error) {
   189  	return c.next.CheckTx(ctx, tx)
   190  }
   191  
   192  func (c *Client) NetInfo(ctx context.Context) (*ctypes.ResultNetInfo, error) {
   193  	return c.next.NetInfo(ctx)
   194  }
   195  
   196  func (c *Client) DumpConsensusState(ctx context.Context) (*ctypes.ResultDumpConsensusState, error) {
   197  	return c.next.DumpConsensusState(ctx)
   198  }
   199  
   200  func (c *Client) ConsensusState(ctx context.Context) (*ctypes.ResultConsensusState, error) {
   201  	return c.next.ConsensusState(ctx)
   202  }
   203  
   204  func (c *Client) ConsensusParams(ctx context.Context, height *int64) (*ctypes.ResultConsensusParams, error) {
   205  	res, err := c.next.ConsensusParams(ctx, height)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	// Validate res.
   211  	if err := types.ValidateConsensusParams(res.ConsensusParams); err != nil {
   212  		return nil, err
   213  	}
   214  	if res.BlockHeight <= 0 {
   215  		return nil, errNegOrZeroHeight
   216  	}
   217  
   218  	// Update the light client if we're behind.
   219  	l, err := c.updateLightClientIfNeededTo(ctx, &res.BlockHeight)
   220  	if err != nil {
   221  		return nil, err
   222  	}
   223  
   224  	// Verify hash.
   225  	if cH, tH := types.HashConsensusParams(res.ConsensusParams), l.ConsensusHash; !bytes.Equal(cH, tH) {
   226  		return nil, fmt.Errorf("params hash %X does not match trusted hash %X",
   227  			cH, tH)
   228  	}
   229  
   230  	return res, nil
   231  }
   232  
   233  func (c *Client) Health(ctx context.Context) (*ctypes.ResultHealth, error) {
   234  	return c.next.Health(ctx)
   235  }
   236  
   237  // BlockchainInfo calls rpcclient#BlockchainInfo and then verifies every header
   238  // returned.
   239  func (c *Client) BlockchainInfo(ctx context.Context, minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) {
   240  	res, err := c.next.BlockchainInfo(ctx, minHeight, maxHeight)
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  
   245  	// Validate res.
   246  	for i, meta := range res.BlockMetas {
   247  		if meta == nil {
   248  			return nil, fmt.Errorf("nil block meta %d", i)
   249  		}
   250  		if err := meta.ValidateBasic(); err != nil {
   251  			return nil, fmt.Errorf("invalid block meta %d: %w", i, err)
   252  		}
   253  	}
   254  
   255  	// Update the light client if we're behind.
   256  	if len(res.BlockMetas) > 0 {
   257  		lastHeight := res.BlockMetas[len(res.BlockMetas)-1].Header.Height
   258  		if _, err := c.updateLightClientIfNeededTo(ctx, &lastHeight); err != nil {
   259  			return nil, err
   260  		}
   261  	}
   262  
   263  	// Verify each of the BlockMetas.
   264  	for _, meta := range res.BlockMetas {
   265  		h, err := c.lc.TrustedLightBlock(meta.Header.Height)
   266  		if err != nil {
   267  			return nil, fmt.Errorf("trusted header %d: %w", meta.Header.Height, err)
   268  		}
   269  		if bmH, tH := meta.Header.Hash(), h.Hash(); !bytes.Equal(bmH, tH) {
   270  			return nil, fmt.Errorf("block meta header %X does not match with trusted header %X",
   271  				bmH, tH)
   272  		}
   273  	}
   274  
   275  	return res, nil
   276  }
   277  
   278  func (c *Client) Genesis(ctx context.Context) (*ctypes.ResultGenesis, error) {
   279  	return c.next.Genesis(ctx)
   280  }
   281  
   282  // Block calls rpcclient#Block and then verifies the result.
   283  func (c *Client) Block(ctx context.Context, height *int64) (*ctypes.ResultBlock, error) {
   284  	res, err := c.next.Block(ctx, height)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  
   289  	// Validate res.
   290  	if err := res.BlockID.ValidateBasic(); err != nil {
   291  		return nil, err
   292  	}
   293  	if err := res.Block.ValidateBasic(); err != nil {
   294  		return nil, err
   295  	}
   296  	if bmH, bH := res.BlockID.Hash, res.Block.Hash(); !bytes.Equal(bmH, bH) {
   297  		return nil, fmt.Errorf("blockID %X does not match with block %X",
   298  			bmH, bH)
   299  	}
   300  
   301  	// Update the light client if we're behind.
   302  	l, err := c.updateLightClientIfNeededTo(ctx, &res.Block.Height)
   303  	if err != nil {
   304  		return nil, err
   305  	}
   306  
   307  	// Verify block.
   308  	if bH, tH := res.Block.Hash(), l.Hash(); !bytes.Equal(bH, tH) {
   309  		return nil, fmt.Errorf("block header %X does not match with trusted header %X",
   310  			bH, tH)
   311  	}
   312  
   313  	return res, nil
   314  }
   315  
   316  // BlockByHash calls rpcclient#BlockByHash and then verifies the result.
   317  func (c *Client) BlockByHash(ctx context.Context, hash []byte) (*ctypes.ResultBlock, error) {
   318  	res, err := c.next.BlockByHash(ctx, hash)
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  
   323  	// Validate res.
   324  	if err := res.BlockID.ValidateBasic(); err != nil {
   325  		return nil, err
   326  	}
   327  	if err := res.Block.ValidateBasic(); err != nil {
   328  		return nil, err
   329  	}
   330  	if bmH, bH := res.BlockID.Hash, res.Block.Hash(); !bytes.Equal(bmH, bH) {
   331  		return nil, fmt.Errorf("blockID %X does not match with block %X",
   332  			bmH, bH)
   333  	}
   334  
   335  	// Update the light client if we're behind.
   336  	l, err := c.updateLightClientIfNeededTo(ctx, &res.Block.Height)
   337  	if err != nil {
   338  		return nil, err
   339  	}
   340  
   341  	// Verify block.
   342  	if bH, tH := res.Block.Hash(), l.Hash(); !bytes.Equal(bH, tH) {
   343  		return nil, fmt.Errorf("block header %X does not match with trusted header %X",
   344  			bH, tH)
   345  	}
   346  
   347  	return res, nil
   348  }
   349  
   350  // BlockResults returns the block results for the given height. If no height is
   351  // provided, the results of the block preceding the latest are returned.
   352  func (c *Client) BlockResults(ctx context.Context, height *int64) (*ctypes.ResultBlockResults, error) {
   353  	var h int64
   354  	if height == nil {
   355  		res, err := c.next.Status(ctx)
   356  		if err != nil {
   357  			return nil, fmt.Errorf("can't get latest height: %w", err)
   358  		}
   359  		// Can't return the latest block results here because we won't be able to
   360  		// prove them. Return the results for the previous block instead.
   361  		h = res.SyncInfo.LatestBlockHeight - 1
   362  	} else {
   363  		h = *height
   364  	}
   365  
   366  	res, err := c.next.BlockResults(ctx, &h)
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  
   371  	// Validate res.
   372  	if res.Height <= 0 {
   373  		return nil, errNegOrZeroHeight
   374  	}
   375  
   376  	// Update the light client if we're behind.
   377  	nextHeight := h + 1
   378  	trustedBlock, err := c.updateLightClientIfNeededTo(ctx, &nextHeight)
   379  	if err != nil {
   380  		return nil, err
   381  	}
   382  
   383  	// proto-encode BeginBlock events
   384  	bbeBytes, err := proto.Marshal(&abci.ResponseBeginBlock{
   385  		Events: res.BeginBlockEvents,
   386  	})
   387  	if err != nil {
   388  		return nil, err
   389  	}
   390  
   391  	// Build a Merkle tree of proto-encoded DeliverTx results and get a hash.
   392  	results := types.NewResults(res.TxsResults)
   393  
   394  	// proto-encode EndBlock events.
   395  	ebeBytes, err := proto.Marshal(&abci.ResponseEndBlock{
   396  		Events: res.EndBlockEvents,
   397  	})
   398  	if err != nil {
   399  		return nil, err
   400  	}
   401  
   402  	// Build a Merkle tree out of the above 3 binary slices.
   403  	rH := merkle.HashFromByteSlices([][]byte{bbeBytes, results.Hash(), ebeBytes})
   404  
   405  	// Verify block results.
   406  	if !bytes.Equal(rH, trustedBlock.LastResultsHash) {
   407  		return nil, fmt.Errorf("last results %X does not match with trusted last results %X",
   408  			rH, trustedBlock.LastResultsHash)
   409  	}
   410  
   411  	return res, nil
   412  }
   413  
   414  func (c *Client) Commit(ctx context.Context, height *int64) (*ctypes.ResultCommit, error) {
   415  	// Update the light client if we're behind and retrieve the light block at the requested height
   416  	// or at the latest height if no height is provided.
   417  	l, err := c.updateLightClientIfNeededTo(ctx, height)
   418  	if err != nil {
   419  		return nil, err
   420  	}
   421  
   422  	return &ctypes.ResultCommit{
   423  		SignedHeader:    *l.SignedHeader,
   424  		CanonicalCommit: true,
   425  	}, nil
   426  }
   427  
   428  // Tx calls rpcclient#Tx method and then verifies the proof if such was
   429  // requested.
   430  func (c *Client) Tx(ctx context.Context, hash []byte, prove bool) (*ctypes.ResultTx, error) {
   431  	res, err := c.next.Tx(ctx, hash, prove)
   432  	if err != nil || !prove {
   433  		return res, err
   434  	}
   435  
   436  	// Validate res.
   437  	if res.Height <= 0 {
   438  		return nil, errNegOrZeroHeight
   439  	}
   440  
   441  	// Update the light client if we're behind.
   442  	l, err := c.updateLightClientIfNeededTo(ctx, &res.Height)
   443  	if err != nil {
   444  		return nil, err
   445  	}
   446  
   447  	// Validate the proof.
   448  	return res, res.Proof.Validate(l.DataHash)
   449  }
   450  
   451  func (c *Client) TxSearch(ctx context.Context, query string, prove bool, page, perPage *int, orderBy string) (
   452  	*ctypes.ResultTxSearch, error) {
   453  	return c.next.TxSearch(ctx, query, prove, page, perPage, orderBy)
   454  }
   455  
   456  // Validators fetches and verifies validators.
   457  func (c *Client) Validators(ctx context.Context, height *int64, pagePtr, perPagePtr *int) (*ctypes.ResultValidators,
   458  	error) {
   459  	// Update the light client if we're behind and retrieve the light block at the requested height
   460  	// or at the latest height if no height is provided.
   461  	l, err := c.updateLightClientIfNeededTo(ctx, height)
   462  	if err != nil {
   463  		return nil, err
   464  	}
   465  
   466  	totalCount := len(l.ValidatorSet.Validators)
   467  	perPage := validatePerPage(perPagePtr)
   468  	page, err := validatePage(pagePtr, perPage, totalCount)
   469  	if err != nil {
   470  		return nil, err
   471  	}
   472  
   473  	skipCount := validateSkipCount(page, perPage)
   474  
   475  	v := l.ValidatorSet.Validators[skipCount : skipCount+tmmath.MinInt(perPage, totalCount-skipCount)]
   476  
   477  	return &ctypes.ResultValidators{
   478  		BlockHeight: l.Height,
   479  		Validators:  v,
   480  		Count:       len(v),
   481  		Total:       totalCount}, nil
   482  }
   483  
   484  func (c *Client) BroadcastEvidence(ctx context.Context, ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) {
   485  	return c.next.BroadcastEvidence(ctx, ev)
   486  }
   487  
   488  func (c *Client) Subscribe(ctx context.Context, subscriber, query string,
   489  	outCapacity ...int) (out <-chan ctypes.ResultEvent, err error) {
   490  	return c.next.Subscribe(ctx, subscriber, query, outCapacity...)
   491  }
   492  
   493  func (c *Client) Unsubscribe(ctx context.Context, subscriber, query string) error {
   494  	return c.next.Unsubscribe(ctx, subscriber, query)
   495  }
   496  
   497  func (c *Client) UnsubscribeAll(ctx context.Context, subscriber string) error {
   498  	return c.next.UnsubscribeAll(ctx, subscriber)
   499  }
   500  
   501  func (c *Client) updateLightClientIfNeededTo(ctx context.Context, height *int64) (*types.LightBlock, error) {
   502  	var (
   503  		l   *types.LightBlock
   504  		err error
   505  	)
   506  	if height == nil {
   507  		l, err = c.lc.Update(ctx, time.Now())
   508  	} else {
   509  		l, err = c.lc.VerifyLightBlockAtHeight(ctx, *height, time.Now())
   510  	}
   511  	if err != nil {
   512  		return nil, fmt.Errorf("failed to update light client to %d: %w", height, err)
   513  	}
   514  	return l, nil
   515  }
   516  
   517  func (c *Client) RegisterOpDecoder(typ string, dec merkle.OpDecoder) {
   518  	c.prt.RegisterOpDecoder(typ, dec)
   519  }
   520  
   521  // SubscribeWS subscribes for events using the given query and remote address as
   522  // a subscriber, but does not verify responses (UNSAFE)!
   523  // TODO: verify data
   524  func (c *Client) SubscribeWS(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, error) {
   525  	out, err := c.next.Subscribe(context.Background(), ctx.RemoteAddr(), query)
   526  	if err != nil {
   527  		return nil, err
   528  	}
   529  
   530  	go func() {
   531  		for {
   532  			select {
   533  			case resultEvent := <-out:
   534  				// We should have a switch here that performs a validation
   535  				// depending on the event's type.
   536  				ctx.WSConn.TryWriteRPCResponse(
   537  					rpctypes.NewRPCSuccessResponse(
   538  						rpctypes.JSONRPCStringID(fmt.Sprintf("%v#event", ctx.JSONReq.ID)),
   539  						resultEvent,
   540  					))
   541  			case <-c.Quit():
   542  				return
   543  			}
   544  		}
   545  	}()
   546  
   547  	return &ctypes.ResultSubscribe{}, nil
   548  }
   549  
   550  // UnsubscribeWS calls original client's Unsubscribe using remote address as a
   551  // subscriber.
   552  func (c *Client) UnsubscribeWS(ctx *rpctypes.Context, query string) (*ctypes.ResultUnsubscribe, error) {
   553  	err := c.next.Unsubscribe(context.Background(), ctx.RemoteAddr(), query)
   554  	if err != nil {
   555  		return nil, err
   556  	}
   557  	return &ctypes.ResultUnsubscribe{}, nil
   558  }
   559  
   560  // UnsubscribeAllWS calls original client's UnsubscribeAll using remote address
   561  // as a subscriber.
   562  func (c *Client) UnsubscribeAllWS(ctx *rpctypes.Context) (*ctypes.ResultUnsubscribe, error) {
   563  	err := c.next.UnsubscribeAll(context.Background(), ctx.RemoteAddr())
   564  	if err != nil {
   565  		return nil, err
   566  	}
   567  	return &ctypes.ResultUnsubscribe{}, nil
   568  }
   569  
   570  // XXX: Copied from rpc/core/env.go
   571  const (
   572  	// see README
   573  	defaultPerPage = 30
   574  	maxPerPage     = 100
   575  )
   576  
   577  func validatePage(pagePtr *int, perPage, totalCount int) (int, error) {
   578  	if perPage < 1 {
   579  		panic(fmt.Sprintf("zero or negative perPage: %d", perPage))
   580  	}
   581  
   582  	if pagePtr == nil { // no page parameter
   583  		return 1, nil
   584  	}
   585  
   586  	pages := ((totalCount - 1) / perPage) + 1
   587  	if pages == 0 {
   588  		pages = 1 // one page (even if it's empty)
   589  	}
   590  	page := *pagePtr
   591  	if page <= 0 || page > pages {
   592  		return 1, fmt.Errorf("page should be within [1, %d] range, given %d", pages, page)
   593  	}
   594  
   595  	return page, nil
   596  }
   597  
   598  func validatePerPage(perPagePtr *int) int {
   599  	if perPagePtr == nil { // no per_page parameter
   600  		return defaultPerPage
   601  	}
   602  
   603  	perPage := *perPagePtr
   604  	if perPage < 1 {
   605  		return defaultPerPage
   606  	} else if perPage > maxPerPage {
   607  		return maxPerPage
   608  	}
   609  	return perPage
   610  }
   611  
   612  func validateSkipCount(page, perPage int) int {
   613  	skipCount := (page - 1) * perPage
   614  	if skipCount < 0 {
   615  		return 0
   616  	}
   617  
   618  	return skipCount
   619  }