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