From 349556c6d99b4a4253d6835df843044ef18aa796 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 20 Apr 2020 16:38:34 +0400 Subject: [PATCH] lite2/rpc: verify block results and validators (#4703) Closes: #4695 Verify /block_results and /validators responses from an HTTP client using the light client. Added count and total to /validators response. Refs #3113 --- CHANGELOG_PENDING.md | 1 + lite2/rpc/client.go | 104 +++++++++++++++++++++++++++++------- rpc/client/rpc_test.go | 2 + rpc/core/consensus.go | 12 +++-- rpc/core/types/responses.go | 6 ++- rpc/swagger/swagger.yaml | 6 +++ 6 files changed, 107 insertions(+), 24 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 8dce40822..cd23e2ce2 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -42,6 +42,7 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermi - [tools] \#4615 Allow developers to use Docker to generate proto stubs, via `make proto-gen-docker`. - [p2p] \#4621(https://github.com/tendermint/tendermint/pull/4621) ban peers when messages are unsolicited or too frequent (@cmwaters) - [evidence] [\#4632](https://github.com/tendermint/tendermint/pull/4632) Inbound evidence checked if already existing (@cmwaters) +- [rpc] [\#4703](https://github.com/tendermint/tendermint/pull/4703) Add `count` and `total` to `/validators` response (@melekes) ### BUG FIXES: diff --git a/lite2/rpc/client.go b/lite2/rpc/client.go index da42a747a..9155220c9 100644 --- a/lite2/rpc/client.go +++ b/lite2/rpc/client.go @@ -3,12 +3,11 @@ package rpc import ( "bytes" "context" + "errors" "fmt" "strings" "time" - "github.com/pkg/errors" - "github.com/tendermint/tendermint/crypto/merkle" tmbytes "github.com/tendermint/tendermint/libs/bytes" service "github.com/tendermint/tendermint/libs/service" @@ -19,6 +18,8 @@ import ( "github.com/tendermint/tendermint/types" ) +var errNegOrZeroHeight = errors.New("negative or zero height") + // Client is an RPC client, which uses lite#Client to verify data (if it can be // proved!). type Client struct { @@ -80,13 +81,13 @@ func (c *Client) ABCIQueryWithOptions(path string, data tmbytes.HexBytes, // Validate the response. if resp.IsErr() { - return nil, errors.Errorf("err response code: %v", resp.Code) + return nil, fmt.Errorf("err response code: %v", resp.Code) } if len(resp.Key) == 0 || resp.Proof == nil { return nil, errors.New("empty tree") } if resp.Height <= 0 { - return nil, errors.New("negative or zero height") + return nil, errNegOrZeroHeight } // Update the light client if we're behind. @@ -109,7 +110,7 @@ func (c *Client) ABCIQueryWithOptions(path string, data tmbytes.HexBytes, kp = kp.AppendKey(resp.Key, merkle.KeyEncodingURL) err = c.prt.VerifyValue(resp.Proof, h.AppHash, kp.String(), resp.Value) if err != nil { - return nil, errors.Wrap(err, "verify value proof") + return nil, fmt.Errorf("verify value proof: %w", err) } return &ctypes.ResultABCIQuery{Response: resp}, nil } @@ -118,7 +119,7 @@ func (c *Client) ABCIQueryWithOptions(path string, data tmbytes.HexBytes, // XXX How do we encode the key into a string... err = c.prt.VerifyAbsence(resp.Proof, h.AppHash, string(resp.Key)) if err != nil { - return nil, errors.Wrap(err, "verify absence proof") + return nil, fmt.Errorf("verify absence proof: %w", err) } return &ctypes.ResultABCIQuery{Response: resp}, nil } @@ -161,6 +162,14 @@ func (c *Client) ConsensusParams(height *int64) (*ctypes.ResultConsensusParams, return nil, err } + // Validate res. + if err := res.ConsensusParams.Validate(); err != nil { + return nil, err + } + if res.BlockHeight <= 0 { + return nil, errNegOrZeroHeight + } + // Update the light client if we're behind. h, err := c.updateLiteClientIfNeededTo(res.BlockHeight) if err != nil { @@ -189,12 +198,12 @@ func (c *Client) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlock } // Validate res. - for _, meta := range res.BlockMetas { + for i, meta := range res.BlockMetas { if meta == nil { - return nil, errors.New("nil BlockMeta") + return nil, fmt.Errorf("nil block meta %d", i) } if err := meta.ValidateBasic(); err != nil { - return nil, errors.Wrap(err, "invalid BlockMeta") + return nil, fmt.Errorf("invalid block meta %d: %w", i, err) } } @@ -210,10 +219,10 @@ func (c *Client) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlock for _, meta := range res.BlockMetas { h, err := c.lc.TrustedHeader(meta.Header.Height) if err != nil { - return nil, errors.Wrapf(err, "TrustedHeader(%d)", meta.Header.Height) + 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, errors.Errorf("BlockMeta#Header %X does not match with trusted header %X", + return nil, fmt.Errorf("block meta header %X does not match with trusted header %X", bmH, tH) } } @@ -240,7 +249,7 @@ func (c *Client) Block(height *int64) (*ctypes.ResultBlock, error) { return nil, err } if bmH, bH := res.BlockID.Hash, res.Block.Hash(); !bytes.Equal(bmH, bH) { - return nil, errors.Errorf("BlockID %X does not match with Block %X", + return nil, fmt.Errorf("blockID %X does not match with block %X", bmH, bH) } @@ -252,7 +261,7 @@ func (c *Client) Block(height *int64) (*ctypes.ResultBlock, error) { // Verify block. if bH, tH := res.Block.Hash(), h.Hash(); !bytes.Equal(bH, tH) { - return nil, errors.Errorf("Block#Header %X does not match with trusted header %X", + return nil, fmt.Errorf("block header %X does not match with trusted header %X", bH, tH) } @@ -260,7 +269,30 @@ func (c *Client) Block(height *int64) (*ctypes.ResultBlock, error) { } func (c *Client) BlockResults(height *int64) (*ctypes.ResultBlockResults, error) { - return c.next.BlockResults(height) + res, err := c.next.BlockResults(height) + if err != nil { + return nil, err + } + + // Validate res. + if res.Height <= 0 { + return nil, errNegOrZeroHeight + } + + // Update the light client if we're behind. + h, err := c.updateLiteClientIfNeededTo(res.Height + 1) + if err != nil { + return nil, err + } + + // Verify block results. + results := types.NewResults(res.TxsResults) + if rH, tH := results.Hash(), h.LastResultsHash; !bytes.Equal(rH, tH) { + return nil, fmt.Errorf("last results %X does not match with trusted last results %X", + rH, tH) + } + + return res, nil } func (c *Client) Commit(height *int64) (*ctypes.ResultCommit, error) { @@ -273,6 +305,9 @@ func (c *Client) Commit(height *int64) (*ctypes.ResultCommit, error) { if err := res.SignedHeader.ValidateBasic(c.lc.ChainID()); err != nil { return nil, err } + if res.Height <= 0 { + return nil, errNegOrZeroHeight + } // Update the light client if we're behind. h, err := c.updateLiteClientIfNeededTo(res.Height) @@ -282,7 +317,7 @@ func (c *Client) Commit(height *int64) (*ctypes.ResultCommit, error) { // Verify commit. if rH, tH := res.Hash(), h.Hash(); !bytes.Equal(rH, tH) { - return nil, errors.Errorf("header %X does not match with trusted header %X", + return nil, fmt.Errorf("header %X does not match with trusted header %X", rH, tH) } @@ -299,7 +334,7 @@ func (c *Client) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) { // Validate res. if res.Height <= 0 { - return nil, errors.Errorf("invalid ResultTx: %v", res) + return nil, errNegOrZeroHeight } // Update the light client if we're behind. @@ -317,8 +352,36 @@ func (c *Client) TxSearch(query string, prove bool, page, perPage int, orderBy s return c.next.TxSearch(query, prove, page, perPage, orderBy) } +// Validators fetches and verifies validators. +// +// WARNING: only full validator sets are verified (when length of validators is +// less than +perPage+. +perPage+ default is 30, max is 100). func (c *Client) Validators(height *int64, page, perPage int) (*ctypes.ResultValidators, error) { - return c.next.Validators(height, page, perPage) + res, err := c.next.Validators(height, page, perPage) + if err != nil { + return nil, err + } + + // Validate res. + if res.BlockHeight <= 0 { + return nil, errNegOrZeroHeight + } + + // Update the light client if we're behind. + h, err := c.updateLiteClientIfNeededTo(res.BlockHeight) + if err != nil { + return nil, err + } + + // Verify validators. + if res.Count <= res.Total { + if rH, tH := types.NewValidatorSet(res.Validators).Hash(), h.ValidatorsHash; !bytes.Equal(rH, tH) { + return nil, fmt.Errorf("validators %X does not match with trusted validators %X", + rH, tH) + } + } + + return res, nil } func (c *Client) BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { @@ -340,7 +403,10 @@ func (c *Client) UnsubscribeAll(ctx context.Context, subscriber string) error { func (c *Client) updateLiteClientIfNeededTo(height int64) (*types.SignedHeader, error) { h, err := c.lc.VerifyHeaderAtHeight(height, time.Now()) - return h, errors.Wrapf(err, "failed to update light client to %d", height) + if err != nil { + return nil, fmt.Errorf("failed to update light client to %d: %w", height, err) + } + return h, nil } func (c *Client) RegisterOpDecoder(typ string, dec merkle.OpDecoder) { @@ -399,7 +465,7 @@ func (c *Client) UnsubscribeAllWS(ctx *rpctypes.Context) (*ctypes.ResultUnsubscr func parseQueryStorePath(path string) (storeName string, err error) { if !strings.HasPrefix(path, "/") { - return "", fmt.Errorf("expected path to start with /") + return "", errors.New("expected path to start with /") } paths := strings.SplitN(path[1:], "/", 3) diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index 62b9f23ef..3f9962774 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -176,6 +176,8 @@ func TestGenesisAndValidators(t *testing.T) { vals, err := c.Validators(nil, 0, 0) require.Nil(t, err, "%d: %+v", i, err) require.Equal(t, 1, len(vals.Validators)) + require.Equal(t, 1, vals.Count) + require.Equal(t, 1, vals.Total) val := vals.Validators[0] // make sure the current set is also the genesis set diff --git a/rpc/core/consensus.go b/rpc/core/consensus.go index 8ea2dde4f..10717c8c6 100644 --- a/rpc/core/consensus.go +++ b/rpc/core/consensus.go @@ -10,9 +10,11 @@ import ( ) // Validators gets the validator set at the given block height. -// If no height is provided, it will fetch the current validator set. -// Note the validators are sorted by their address - this is the canonical -// order for the validators in the set as used in computing their Merkle root. +// +// If no height is provided, it will fetch the current validator set. Note the +// validators are sorted by their address - this is the canonical order for the +// validators in the set as used in computing their Merkle root. +// // More: https://docs.tendermint.com/master/rpc/#/Info/validators func Validators(ctx *rpctypes.Context, heightPtr *int64, page, perPage int) (*ctypes.ResultValidators, error) { // The latest validator that we know is the @@ -41,7 +43,9 @@ func Validators(ctx *rpctypes.Context, heightPtr *int64, page, perPage int) (*ct return &ctypes.ResultValidators{ BlockHeight: height, - Validators: v}, nil + Validators: v, + Count: len(v), + Total: totalCount}, nil } // DumpConsensusState dumps consensus state. diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index 18b2109ed..e5b7b9819 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -122,10 +122,14 @@ type Peer struct { RemoteIP string `json:"remote_ip"` } -// Validators for a height +// Validators for a height. type ResultValidators struct { BlockHeight int64 `json:"block_height"` Validators []*types.Validator `json:"validators"` + // Count of actual validators in this result + Count int `json:"count"` + // Total number of validators + Total int `json:"total"` } // ConsensusParams for given height diff --git a/rpc/swagger/swagger.yaml b/rpc/swagger/swagger.yaml index 000ea972c..f2631c2e9 100644 --- a/rpc/swagger/swagger.yaml +++ b/rpc/swagger/swagger.yaml @@ -1922,6 +1922,12 @@ components: proposer_priority: type: "string" example: "13769415" + count: + type: "number" + example: 1 + total: + type: "number" + example: 25 type: "object" GenesisResponse: type: object