You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

245 lines
7.2 KiB

package http
import (
"context"
"errors"
"fmt"
"math/rand"
"net/url"
"strings"
"time"
"github.com/tendermint/tendermint/light/provider"
rpcclient "github.com/tendermint/tendermint/rpc/client"
rpchttp "github.com/tendermint/tendermint/rpc/client/http"
ctypes "github.com/tendermint/tendermint/rpc/core/types"
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
"github.com/tendermint/tendermint/types"
)
var defaultOptions = Options{
MaxRetryAttempts: 5,
Timeout: 3 * time.Second,
}
// http provider uses an RPC client to obtain the necessary information.
type http struct {
chainID string
client rpcclient.RemoteClient
maxRetryAttempts int
}
type Options struct {
// -1 means no limit
MaxRetryAttempts int
// 0 means no timeout.
Timeout time.Duration
}
// New creates a HTTP provider, which is using the rpchttp.HTTP client under
// the hood. If no scheme is provided in the remote URL, http will be used by
// default. The 5s timeout is used for all requests.
func New(chainID, remote string) (provider.Provider, error) {
return NewWithOptions(chainID, remote, defaultOptions)
}
// NewWithOptions is an extension to creating a new http provider that allows the addition
// of a specified timeout and maxRetryAttempts
func NewWithOptions(chainID, remote string, options Options) (provider.Provider, error) {
// Ensure URL scheme is set (default HTTP) when not provided.
if !strings.Contains(remote, "://") {
remote = "http://" + remote
}
httpClient, err := rpchttp.NewWithTimeout(remote, options.Timeout)
if err != nil {
return nil, err
}
return NewWithClientAndOptions(chainID, httpClient, options), nil
}
func NewWithClient(chainID string, client rpcclient.RemoteClient) provider.Provider {
return NewWithClientAndOptions(chainID, client, defaultOptions)
}
// NewWithClient allows you to provide a custom client.
func NewWithClientAndOptions(chainID string, client rpcclient.RemoteClient, options Options) provider.Provider {
return &http{
client: client,
chainID: chainID,
maxRetryAttempts: options.MaxRetryAttempts,
}
}
func (p *http) String() string {
return fmt.Sprintf("http{%s}", p.client.Remote())
}
// LightBlock fetches a LightBlock at the given height and checks the
// chainID matches.
func (p *http) LightBlock(ctx context.Context, height int64) (*types.LightBlock, error) {
h, err := validateHeight(height)
if err != nil {
return nil, provider.ErrBadLightBlock{Reason: err}
}
sh, err := p.signedHeader(ctx, h)
if err != nil {
return nil, err
}
vs, err := p.validatorSet(ctx, &sh.Height)
if err != nil {
return nil, err
}
lb := &types.LightBlock{
SignedHeader: sh,
ValidatorSet: vs,
}
err = lb.ValidateBasic(p.chainID)
if err != nil {
return nil, provider.ErrBadLightBlock{Reason: err}
}
return lb, nil
}
// ReportEvidence calls `/broadcast_evidence` endpoint.
func (p *http) ReportEvidence(ctx context.Context, ev types.Evidence) error {
_, err := p.client.BroadcastEvidence(ctx, ev)
return err
}
func (p *http) validatorSet(ctx context.Context, height *int64) (*types.ValidatorSet, error) {
// Since the malicious node could report a massive number of pages, making us
// spend a considerable time iterating, we restrict the number of pages here.
// => 10000 validators max
const maxPages = 100
var (
perPage = 100
vals = []*types.Validator{}
page = 1
total = -1
)
for len(vals) != total && page <= maxPages {
// create another for loop to control retries. If p.maxRetryAttempts
// is negative we will keep repeating.
for attempt := 0; attempt != p.maxRetryAttempts+1; attempt++ {
res, err := p.client.Validators(ctx, height, &page, &perPage)
switch e := err.(type) {
case nil: // success!! Now we validate the response
if len(res.Validators) == 0 {
return nil, provider.ErrBadLightBlock{
Reason: fmt.Errorf("validator set is empty (height: %d, page: %d, per_page: %d)",
height, page, perPage),
}
}
if res.Total <= 0 {
return nil, provider.ErrBadLightBlock{
Reason: fmt.Errorf("total number of vals is <= 0: %d (height: %d, page: %d, per_page: %d)",
res.Total, height, page, perPage),
}
}
case *url.Error:
if e.Timeout() {
// request timed out: we wait and try again with exponential backoff
time.Sleep(backoffTimeout(uint16(attempt)))
continue
}
return nil, provider.ErrBadLightBlock{Reason: e}
case *rpctypes.RPCError:
// check if the error indicates that the peer doesn't have the block
if strings.Contains(e.Data, ctypes.ErrHeightNotAvailable.Error()) ||
strings.Contains(e.Data, ctypes.ErrHeightExceedsChainHead.Error()) {
return nil, provider.ErrLightBlockNotFound
}
return nil, provider.ErrBadLightBlock{Reason: e}
default:
// If we don't know the error then by default we return a bad light block error and
// terminate the connection with the peer.
return nil, provider.ErrBadLightBlock{Reason: e}
}
// update the total and increment the page index so we can fetch the
// next page of validators if need be
total = res.Total
vals = append(vals, res.Validators...)
page++
break
}
}
valSet, err := types.ValidatorSetFromExistingValidators(vals)
if err != nil {
return nil, provider.ErrBadLightBlock{Reason: err}
}
return valSet, nil
}
func (p *http) signedHeader(ctx context.Context, height *int64) (*types.SignedHeader, error) {
// create a for loop to control retries. If p.maxRetryAttempts
// is negative we will keep repeating.
for attempt := 0; attempt != p.maxRetryAttempts+1; attempt++ {
commit, err := p.client.Commit(ctx, height)
switch e := err.(type) {
case nil: // success!!
return &commit.SignedHeader, nil
case *url.Error:
if e.Timeout() {
// we wait and try again with exponential backoff
time.Sleep(backoffTimeout(uint16(attempt)))
continue
}
return nil, provider.ErrBadLightBlock{Reason: e}
case *rpctypes.RPCError:
// Check if we got something other than internal error. This shouldn't happen unless the RPC module
// or light client has been tampered with. If we do get this error, stop the connection with the
// peer and return an error
if e.Code != -32603 {
return nil, provider.ErrBadLightBlock{Reason: errors.New(e.Data)}
}
// check if the error indicates that the peer doesn't have the block
if strings.Contains(err.Error(), ctypes.ErrHeightNotAvailable.Error()) ||
strings.Contains(err.Error(), ctypes.ErrHeightExceedsChainHead.Error()) {
return nil, provider.ErrLightBlockNotFound
}
default:
// If we don't know the error then by default we return a bad light block error and
// terminate the connection with the peer.
return nil, provider.ErrBadLightBlock{Reason: e}
}
}
return nil, provider.ErrNoResponse
}
func validateHeight(height int64) (*int64, error) {
if height < 0 {
return nil, fmt.Errorf("expected height >= 0, got height %d", height)
}
h := &height
if height == 0 {
h = nil
}
return h, nil
}
// exponential backoff (with jitter)
// 0.5s -> 2s -> 4.5s -> 8s -> 12.5 with 1s variation
func backoffTimeout(attempt uint16) time.Duration {
// nolint:gosec // G404: Use of weak random number generator
return time.Duration(500*attempt*attempt)*time.Millisecond + time.Duration(rand.Intn(1000))*time.Millisecond
}