Files
tendermint/light/rpc/client.go
M. J. Fromberger cf7537ea5f cleanup: Reduce and normalize import path aliasing. (#6975)
The code in the Tendermint repository makes heavy use of import aliasing.
This is made necessary by our extensive reuse of common base package names, and
by repetition of similar names across different subdirectories.

Unfortunately we have not been very consistent about which packages we alias in
various circumstances, and the aliases we use vary. In the spirit of the advice
in the style guide and https://github.com/golang/go/wiki/CodeReviewComments#imports,
his change makes an effort to clean up and normalize import aliasing.

This change makes no API or behavioral changes. It is a pure cleanup intended
o help make the code more readable to developers (including myself) trying to
understand what is being imported where.

Only unexported names have been modified, and the changes were generated and
applied mechanically with gofmt -r and comby, respecting the lexical and
syntactic rules of Go.  Even so, I did not fix every inconsistency. Where the
changes would be too disruptive, I left it alone.

The principles I followed in this cleanup are:

- Remove aliases that restate the package name.
- Remove aliases where the base package name is unambiguous.
- Move overly-terse abbreviations from the import to the usage site.
- Fix lexical issues (remove underscores, remove capitalization).
- Fix import groupings to more closely match the style guide.
- Group blank (side-effecting) imports and ensure they are commented.
- Add aliases to multiple imports with the same base package name.
2021-09-23 07:52:07 -07:00

664 lines
19 KiB
Go

package rpc
import (
"bytes"
"context"
"errors"
"fmt"
"regexp"
"time"
"github.com/gogo/protobuf/proto"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto/merkle"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
tmmath "github.com/tendermint/tendermint/libs/math"
service "github.com/tendermint/tendermint/libs/service"
rpcclient "github.com/tendermint/tendermint/rpc/client"
"github.com/tendermint/tendermint/rpc/coretypes"
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
"github.com/tendermint/tendermint/types"
)
// KeyPathFunc builds a merkle path out of the given path and key.
type KeyPathFunc func(path string, key []byte) (merkle.KeyPath, error)
// LightClient is an interface that contains functionality needed by Client from the light client.
//go:generate ../../scripts/mockery_generate.sh LightClient
type LightClient interface {
ChainID() string
Update(ctx context.Context, now time.Time) (*types.LightBlock, error)
VerifyLightBlockAtHeight(ctx context.Context, height int64, now time.Time) (*types.LightBlock, error)
TrustedLightBlock(height int64) (*types.LightBlock, error)
}
var _ rpcclient.Client = (*Client)(nil)
// Client is an RPC client, which uses light#Client to verify data (if it can
// be proved). Note, merkle.DefaultProofRuntime is used to verify values
// returned by ABCI#Query.
type Client struct {
service.BaseService
next rpcclient.Client
lc LightClient
// proof runtime used to verify values returned by ABCIQuery
prt *merkle.ProofRuntime
keyPathFn KeyPathFunc
}
var _ rpcclient.Client = (*Client)(nil)
// Option allow you to tweak Client.
type Option func(*Client)
// KeyPathFn option can be used to set a function, which parses a given path
// and builds the merkle path for the prover. It must be provided if you want
// to call ABCIQuery or ABCIQueryWithOptions.
func KeyPathFn(fn KeyPathFunc) Option {
return func(c *Client) {
c.keyPathFn = fn
}
}
// DefaultMerkleKeyPathFn creates a function used to generate merkle key paths
// from a path string and a key. This is the default used by the cosmos SDK.
// This merkle key paths are required when verifying /abci_query calls
func DefaultMerkleKeyPathFn() KeyPathFunc {
// regexp for extracting store name from /abci_query path
storeNameRegexp := regexp.MustCompile(`\/store\/(.+)\/key`)
return func(path string, key []byte) (merkle.KeyPath, error) {
matches := storeNameRegexp.FindStringSubmatch(path)
if len(matches) != 2 {
return nil, fmt.Errorf("can't find store name in %s using %s", path, storeNameRegexp)
}
storeName := matches[1]
kp := merkle.KeyPath{}
kp = kp.AppendKey([]byte(storeName), merkle.KeyEncodingURL)
kp = kp.AppendKey(key, merkle.KeyEncodingURL)
return kp, nil
}
}
// NewClient returns a new client.
func NewClient(next rpcclient.Client, lc LightClient, opts ...Option) *Client {
c := &Client{
next: next,
lc: lc,
prt: merkle.DefaultProofRuntime(),
}
c.BaseService = *service.NewBaseService(nil, "Client", c)
for _, o := range opts {
o(c)
}
return c
}
func (c *Client) OnStart() error {
if !c.next.IsRunning() {
return c.next.Start()
}
return nil
}
func (c *Client) OnStop() {
if c.next.IsRunning() {
if err := c.next.Stop(); err != nil {
c.Logger.Error("Error stopping on next", "err", err)
}
}
}
func (c *Client) Status(ctx context.Context) (*coretypes.ResultStatus, error) {
return c.next.Status(ctx)
}
func (c *Client) ABCIInfo(ctx context.Context) (*coretypes.ResultABCIInfo, error) {
return c.next.ABCIInfo(ctx)
}
// ABCIQuery requests proof by default.
func (c *Client) ABCIQuery(ctx context.Context, path string, data tmbytes.HexBytes) (*coretypes.ResultABCIQuery, error) { //nolint:lll
return c.ABCIQueryWithOptions(ctx, path, data, rpcclient.DefaultABCIQueryOptions)
}
// ABCIQueryWithOptions returns an error if opts.Prove is false.
func (c *Client) ABCIQueryWithOptions(ctx context.Context, path string, data tmbytes.HexBytes,
opts rpcclient.ABCIQueryOptions) (*coretypes.ResultABCIQuery, error) {
// always request the proof
opts.Prove = true
res, err := c.next.ABCIQueryWithOptions(ctx, path, data, opts)
if err != nil {
return nil, err
}
resp := res.Response
// Validate the response.
if resp.IsErr() {
return nil, fmt.Errorf("err response code: %v", resp.Code)
}
if len(resp.Key) == 0 {
return nil, errors.New("empty key")
}
if resp.ProofOps == nil || len(resp.ProofOps.Ops) == 0 {
return nil, errors.New("no proof ops")
}
if resp.Height <= 0 {
return nil, coretypes.ErrZeroOrNegativeHeight
}
// Update the light client if we're behind.
// NOTE: AppHash for height H is in header H+1.
nextHeight := resp.Height + 1
l, err := c.updateLightClientIfNeededTo(ctx, &nextHeight)
if err != nil {
return nil, err
}
// Validate the value proof against the trusted header.
if resp.Value != nil {
// 1) build a Merkle key path from path and resp.Key
if c.keyPathFn == nil {
return nil, errors.New("please configure Client with KeyPathFn option")
}
kp, err := c.keyPathFn(path, resp.Key)
if err != nil {
return nil, fmt.Errorf("can't build merkle key path: %w", err)
}
// 2) verify value
err = c.prt.VerifyValue(resp.ProofOps, l.AppHash, kp.String(), resp.Value)
if err != nil {
return nil, fmt.Errorf("verify value proof: %w", err)
}
} else { // OR validate the absence proof against the trusted header.
err = c.prt.VerifyAbsence(resp.ProofOps, l.AppHash, string(resp.Key))
if err != nil {
return nil, fmt.Errorf("verify absence proof: %w", err)
}
}
return &coretypes.ResultABCIQuery{Response: resp}, nil
}
func (c *Client) BroadcastTxCommit(ctx context.Context, tx types.Tx) (*coretypes.ResultBroadcastTxCommit, error) {
return c.next.BroadcastTxCommit(ctx, tx)
}
func (c *Client) BroadcastTxAsync(ctx context.Context, tx types.Tx) (*coretypes.ResultBroadcastTx, error) {
return c.next.BroadcastTxAsync(ctx, tx)
}
func (c *Client) BroadcastTxSync(ctx context.Context, tx types.Tx) (*coretypes.ResultBroadcastTx, error) {
return c.next.BroadcastTxSync(ctx, tx)
}
func (c *Client) UnconfirmedTxs(ctx context.Context, limit *int) (*coretypes.ResultUnconfirmedTxs, error) {
return c.next.UnconfirmedTxs(ctx, limit)
}
func (c *Client) NumUnconfirmedTxs(ctx context.Context) (*coretypes.ResultUnconfirmedTxs, error) {
return c.next.NumUnconfirmedTxs(ctx)
}
func (c *Client) CheckTx(ctx context.Context, tx types.Tx) (*coretypes.ResultCheckTx, error) {
return c.next.CheckTx(ctx, tx)
}
func (c *Client) NetInfo(ctx context.Context) (*coretypes.ResultNetInfo, error) {
return c.next.NetInfo(ctx)
}
func (c *Client) DumpConsensusState(ctx context.Context) (*coretypes.ResultDumpConsensusState, error) {
return c.next.DumpConsensusState(ctx)
}
func (c *Client) ConsensusState(ctx context.Context) (*coretypes.ResultConsensusState, error) {
return c.next.ConsensusState(ctx)
}
func (c *Client) ConsensusParams(ctx context.Context, height *int64) (*coretypes.ResultConsensusParams, error) {
res, err := c.next.ConsensusParams(ctx, height)
if err != nil {
return nil, err
}
// Validate res.
if err := res.ConsensusParams.ValidateConsensusParams(); err != nil {
return nil, err
}
if res.BlockHeight <= 0 {
return nil, coretypes.ErrZeroOrNegativeHeight
}
// Update the light client if we're behind.
l, err := c.updateLightClientIfNeededTo(ctx, &res.BlockHeight)
if err != nil {
return nil, err
}
// Verify hash.
if cH, tH := res.ConsensusParams.HashConsensusParams(), l.ConsensusHash; !bytes.Equal(cH, tH) {
return nil, fmt.Errorf("params hash %X does not match trusted hash %X",
cH, tH)
}
return res, nil
}
func (c *Client) Health(ctx context.Context) (*coretypes.ResultHealth, error) {
return c.next.Health(ctx)
}
// BlockchainInfo calls rpcclient#BlockchainInfo and then verifies every header
// returned.
func (c *Client) BlockchainInfo(ctx context.Context, minHeight, maxHeight int64) (*coretypes.ResultBlockchainInfo, error) { //nolint:lll
res, err := c.next.BlockchainInfo(ctx, minHeight, maxHeight)
if err != nil {
return nil, err
}
// Validate res.
for i, meta := range res.BlockMetas {
if meta == nil {
return nil, fmt.Errorf("nil block meta %d", i)
}
if err := meta.ValidateBasic(); err != nil {
return nil, fmt.Errorf("invalid block meta %d: %w", i, err)
}
}
// Update the light client if we're behind.
if len(res.BlockMetas) > 0 {
lastHeight := res.BlockMetas[len(res.BlockMetas)-1].Header.Height
if _, err := c.updateLightClientIfNeededTo(ctx, &lastHeight); err != nil {
return nil, err
}
}
// Verify each of the BlockMetas.
for _, meta := range res.BlockMetas {
h, err := c.lc.TrustedLightBlock(meta.Header.Height)
if err != nil {
return nil, fmt.Errorf("trusted header %d: %w", meta.Header.Height, err)
}
if bmH, tH := meta.Header.Hash(), h.Hash(); !bytes.Equal(bmH, tH) {
return nil, fmt.Errorf("block meta header %X does not match with trusted header %X",
bmH, tH)
}
}
return res, nil
}
func (c *Client) Genesis(ctx context.Context) (*coretypes.ResultGenesis, error) {
return c.next.Genesis(ctx)
}
func (c *Client) GenesisChunked(ctx context.Context, id uint) (*coretypes.ResultGenesisChunk, error) {
return c.next.GenesisChunked(ctx, id)
}
// Block calls rpcclient#Block and then verifies the result.
func (c *Client) Block(ctx context.Context, height *int64) (*coretypes.ResultBlock, error) {
res, err := c.next.Block(ctx, height)
if err != nil {
return nil, err
}
// Validate res.
if err := res.BlockID.ValidateBasic(); err != nil {
return nil, err
}
if err := res.Block.ValidateBasic(); err != nil {
return nil, err
}
if bmH, bH := res.BlockID.Hash, res.Block.Hash(); !bytes.Equal(bmH, bH) {
return nil, fmt.Errorf("blockID %X does not match with block %X",
bmH, bH)
}
// Update the light client if we're behind.
l, err := c.updateLightClientIfNeededTo(ctx, &res.Block.Height)
if err != nil {
return nil, err
}
// Verify block.
if bH, tH := res.Block.Hash(), l.Hash(); !bytes.Equal(bH, tH) {
return nil, fmt.Errorf("block header %X does not match with trusted header %X",
bH, tH)
}
return res, nil
}
// BlockByHash calls rpcclient#BlockByHash and then verifies the result.
func (c *Client) BlockByHash(ctx context.Context, hash tmbytes.HexBytes) (*coretypes.ResultBlock, error) {
res, err := c.next.BlockByHash(ctx, hash)
if err != nil {
return nil, err
}
// Validate res.
if err := res.BlockID.ValidateBasic(); err != nil {
return nil, err
}
if err := res.Block.ValidateBasic(); err != nil {
return nil, err
}
if bmH, bH := res.BlockID.Hash, res.Block.Hash(); !bytes.Equal(bmH, bH) {
return nil, fmt.Errorf("blockID %X does not match with block %X",
bmH, bH)
}
// Update the light client if we're behind.
l, err := c.updateLightClientIfNeededTo(ctx, &res.Block.Height)
if err != nil {
return nil, err
}
// Verify block.
if bH, tH := res.Block.Hash(), l.Hash(); !bytes.Equal(bH, tH) {
return nil, fmt.Errorf("block header %X does not match with trusted header %X",
bH, tH)
}
return res, nil
}
// BlockResults returns the block results for the given height. If no height is
// provided, the results of the block preceding the latest are returned.
func (c *Client) BlockResults(ctx context.Context, height *int64) (*coretypes.ResultBlockResults, error) {
var h int64
if height == nil {
res, err := c.next.Status(ctx)
if err != nil {
return nil, fmt.Errorf("can't get latest height: %w", err)
}
// Can't return the latest block results here because we won't be able to
// prove them. Return the results for the previous block instead.
h = res.SyncInfo.LatestBlockHeight - 1
} else {
h = *height
}
res, err := c.next.BlockResults(ctx, &h)
if err != nil {
return nil, err
}
// Validate res.
if res.Height <= 0 {
return nil, coretypes.ErrZeroOrNegativeHeight
}
// Update the light client if we're behind.
nextHeight := h + 1
trustedBlock, err := c.updateLightClientIfNeededTo(ctx, &nextHeight)
if err != nil {
return nil, err
}
// proto-encode BeginBlock events
bbeBytes, err := proto.Marshal(&abci.ResponseBeginBlock{
Events: res.BeginBlockEvents,
})
if err != nil {
return nil, err
}
// Build a Merkle tree of proto-encoded DeliverTx results and get a hash.
results := types.NewResults(res.TxsResults)
// proto-encode EndBlock events.
ebeBytes, err := proto.Marshal(&abci.ResponseEndBlock{
Events: res.EndBlockEvents,
})
if err != nil {
return nil, err
}
// Build a Merkle tree out of the above 3 binary slices.
rH := merkle.HashFromByteSlices([][]byte{bbeBytes, results.Hash(), ebeBytes})
// Verify block results.
if !bytes.Equal(rH, trustedBlock.LastResultsHash) {
return nil, fmt.Errorf("last results %X does not match with trusted last results %X",
rH, trustedBlock.LastResultsHash)
}
return res, nil
}
func (c *Client) Commit(ctx context.Context, height *int64) (*coretypes.ResultCommit, error) {
// Update the light client if we're behind and retrieve the light block at the requested height
// or at the latest height if no height is provided.
l, err := c.updateLightClientIfNeededTo(ctx, height)
if err != nil {
return nil, err
}
return &coretypes.ResultCommit{
SignedHeader: *l.SignedHeader,
CanonicalCommit: true,
}, nil
}
// Tx calls rpcclient#Tx method and then verifies the proof if such was
// requested.
func (c *Client) Tx(ctx context.Context, hash tmbytes.HexBytes, prove bool) (*coretypes.ResultTx, error) {
res, err := c.next.Tx(ctx, hash, prove)
if err != nil || !prove {
return res, err
}
// Validate res.
if res.Height <= 0 {
return nil, coretypes.ErrZeroOrNegativeHeight
}
// Update the light client if we're behind.
l, err := c.updateLightClientIfNeededTo(ctx, &res.Height)
if err != nil {
return nil, err
}
// Validate the proof.
return res, res.Proof.Validate(l.DataHash)
}
func (c *Client) TxSearch(
ctx context.Context,
query string,
prove bool,
page, perPage *int,
orderBy string,
) (*coretypes.ResultTxSearch, error) {
return c.next.TxSearch(ctx, query, prove, page, perPage, orderBy)
}
func (c *Client) BlockSearch(
ctx context.Context,
query string,
page, perPage *int,
orderBy string,
) (*coretypes.ResultBlockSearch, error) {
return c.next.BlockSearch(ctx, query, page, perPage, orderBy)
}
// Validators fetches and verifies validators.
func (c *Client) Validators(
ctx context.Context,
height *int64,
pagePtr, perPagePtr *int,
) (*coretypes.ResultValidators, error) {
// Update the light client if we're behind and retrieve the light block at the
// requested height or at the latest height if no height is provided.
l, err := c.updateLightClientIfNeededTo(ctx, height)
if err != nil {
return nil, err
}
totalCount := len(l.ValidatorSet.Validators)
perPage := validatePerPage(perPagePtr)
page, err := validatePage(pagePtr, perPage, totalCount)
if err != nil {
return nil, err
}
skipCount := validateSkipCount(page, perPage)
v := l.ValidatorSet.Validators[skipCount : skipCount+tmmath.MinInt(perPage, totalCount-skipCount)]
return &coretypes.ResultValidators{
BlockHeight: l.Height,
Validators: v,
Count: len(v),
Total: totalCount}, nil
}
func (c *Client) BroadcastEvidence(ctx context.Context, ev types.Evidence) (*coretypes.ResultBroadcastEvidence, error) {
return c.next.BroadcastEvidence(ctx, ev)
}
func (c *Client) Subscribe(ctx context.Context, subscriber, query string,
outCapacity ...int) (out <-chan coretypes.ResultEvent, err error) {
return c.next.Subscribe(ctx, subscriber, query, outCapacity...)
}
func (c *Client) Unsubscribe(ctx context.Context, subscriber, query string) error {
return c.next.Unsubscribe(ctx, subscriber, query)
}
func (c *Client) UnsubscribeAll(ctx context.Context, subscriber string) error {
return c.next.UnsubscribeAll(ctx, subscriber)
}
func (c *Client) updateLightClientIfNeededTo(ctx context.Context, height *int64) (*types.LightBlock, error) {
var (
l *types.LightBlock
err error
)
if height == nil {
l, err = c.lc.Update(ctx, time.Now())
} else {
l, err = c.lc.VerifyLightBlockAtHeight(ctx, *height, time.Now())
}
if err != nil {
return nil, fmt.Errorf("failed to update light client: %w", err)
}
return l, nil
}
func (c *Client) RegisterOpDecoder(typ string, dec merkle.OpDecoder) {
c.prt.RegisterOpDecoder(typ, dec)
}
// SubscribeWS subscribes for events using the given query and remote address as
// a subscriber, but does not verify responses (UNSAFE)!
// TODO: verify data
func (c *Client) SubscribeWS(ctx *rpctypes.Context, query string) (*coretypes.ResultSubscribe, error) {
out, err := c.next.Subscribe(context.Background(), ctx.RemoteAddr(), query)
if err != nil {
return nil, err
}
go func() {
for {
select {
case resultEvent := <-out:
// We should have a switch here that performs a validation
// depending on the event's type.
ctx.WSConn.TryWriteRPCResponse(
rpctypes.NewRPCSuccessResponse(
rpctypes.JSONRPCStringID(fmt.Sprintf("%v#event", ctx.JSONReq.ID)),
resultEvent,
))
case <-c.Quit():
return
}
}
}()
return &coretypes.ResultSubscribe{}, nil
}
// UnsubscribeWS calls original client's Unsubscribe using remote address as a
// subscriber.
func (c *Client) UnsubscribeWS(ctx *rpctypes.Context, query string) (*coretypes.ResultUnsubscribe, error) {
err := c.next.Unsubscribe(context.Background(), ctx.RemoteAddr(), query)
if err != nil {
return nil, err
}
return &coretypes.ResultUnsubscribe{}, nil
}
// UnsubscribeAllWS calls original client's UnsubscribeAll using remote address
// as a subscriber.
func (c *Client) UnsubscribeAllWS(ctx *rpctypes.Context) (*coretypes.ResultUnsubscribe, error) {
err := c.next.UnsubscribeAll(context.Background(), ctx.RemoteAddr())
if err != nil {
return nil, err
}
return &coretypes.ResultUnsubscribe{}, nil
}
// XXX: Copied from rpc/core/env.go
const (
// see README
defaultPerPage = 30
maxPerPage = 100
)
func validatePage(pagePtr *int, perPage, totalCount int) (int, error) {
if perPage < 1 {
panic(fmt.Errorf("%w (%d)", coretypes.ErrZeroOrNegativePerPage, perPage))
}
if pagePtr == nil { // no page parameter
return 1, nil
}
pages := ((totalCount - 1) / perPage) + 1
if pages == 0 {
pages = 1 // one page (even if it's empty)
}
page := *pagePtr
if page <= 0 || page > pages {
return 1, fmt.Errorf("%w expected range: [1, %d], given %d", coretypes.ErrPageOutOfRange, pages, page)
}
return page, nil
}
func validatePerPage(perPagePtr *int) int {
if perPagePtr == nil { // no per_page parameter
return defaultPerPage
}
perPage := *perPagePtr
if perPage < 1 {
return defaultPerPage
} else if perPage > maxPerPage {
return maxPerPage
}
return perPage
}
func validateSkipCount(page, perPage int) int {
skipCount := (page - 1) * perPage
if skipCount < 0 {
return 0
}
return skipCount
}