Files
tendermint/light/provider/http/http.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

338 lines
10 KiB
Go

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"
"github.com/tendermint/tendermint/rpc/coretypes"
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
"github.com/tendermint/tendermint/types"
)
var defaultOptions = Options{
MaxRetryAttempts: 5,
Timeout: 5 * time.Second,
NoBlockThreshold: 5,
NoResponseThreshold: 5,
}
// http provider uses an RPC client to obtain the necessary information.
type http struct {
chainID string
client rpcclient.RemoteClient
// httt provider heuristics
// The provider tracks the amount of times that the
// client doesn't respond. If this exceeds the threshold
// then the provider will return an unreliable provider error
noResponseThreshold uint16
noResponseCount uint16
// The provider tracks the amount of time the client
// doesn't have a block. If this exceeds the threshold
// then the provider will return an unreliable provider error
noBlockThreshold uint16
noBlockCount uint16
// In a single request, the provider attempts multiple times
// with exponential backoff to reach the client. If this
// exceeds the maxRetry attempts, this result in a ErrNoResponse
maxRetryAttempts uint16
}
type Options struct {
// 0 means no retries
MaxRetryAttempts uint16
// 0 means no timeout.
Timeout time.Duration
// The amount of requests that a client doesn't have the block
// for before the provider deems the client unreliable
NoBlockThreshold uint16
// The amount of requests that a client doesn't respond to
// before the provider deems the client unreliable
NoResponseThreshold uint16
}
// 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,
noResponseThreshold: options.NoResponseThreshold,
noBlockThreshold: options.NoBlockThreshold,
}
}
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
}
if height != 0 && sh.Height != height {
return nil, provider.ErrBadLightBlock{
Reason: fmt.Errorf("height %d responded doesn't match height %d requested", sh.Height, height),
}
}
if sh.Header == nil {
return nil, provider.ErrBadLightBlock{
Reason: errors.New("returned header is nil unexpectedly"),
}
}
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.
attempt := uint16(0)
for {
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() {
// if we have exceeded retry attempts then return a no response error
if attempt == p.maxRetryAttempts {
return nil, p.noResponse()
}
attempt++
// request timed out: we wait and try again with exponential backoff
time.Sleep(backoffTimeout(attempt))
continue
}
return nil, provider.ErrBadLightBlock{Reason: e}
case *rpctypes.RPCError:
// process the rpc error and return the corresponding error to the light client
return nil, p.parseRPCError(e)
default:
// check if the error stems from the context
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
// If we don't know the error then by default we return an unreliable provider error and
// terminate the connection with the peer.
return nil, provider.ErrUnreliableProvider{Reason: e.Error()}
}
// 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 := uint16(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:
// check if the request timed out
if e.Timeout() {
// we wait and try again with exponential backoff
time.Sleep(backoffTimeout(attempt))
continue
}
// check if the connection was refused or dropped
if strings.Contains(e.Error(), "connection refused") {
return nil, provider.ErrConnectionClosed
}
// else, as a catch all, we return the error as a bad light block response
return nil, provider.ErrBadLightBlock{Reason: e}
case *rpctypes.RPCError:
// process the rpc error and return the corresponding error to the light client
return nil, p.parseRPCError(e)
default:
// check if the error stems from the context
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
// If we don't know the error then by default we return an unreliable provider error and
// terminate the connection with the peer.
return nil, provider.ErrUnreliableProvider{Reason: e.Error()}
}
}
return nil, p.noResponse()
}
func (p *http) noResponse() error {
p.noResponseCount++
if p.noResponseCount > p.noResponseThreshold {
return provider.ErrUnreliableProvider{
Reason: fmt.Sprintf("failed to respond after %d attempts", p.noResponseCount),
}
}
return provider.ErrNoResponse
}
func (p *http) noBlock(e error) error {
p.noBlockCount++
if p.noBlockCount > p.noBlockThreshold {
return provider.ErrUnreliableProvider{
Reason: fmt.Sprintf("failed to provide a block after %d attempts", p.noBlockCount),
}
}
return e
}
// parseRPCError process the error and return the corresponding error to the light clent
// NOTE: When an error is sent over the wire it gets "flattened" hence we are unable to use error
// checking functions like errors.Is() to unwrap the error.
func (p *http) parseRPCError(e *rpctypes.RPCError) error {
switch {
// 1) check if the error indicates that the peer doesn't have the block
case strings.Contains(e.Data, coretypes.ErrHeightNotAvailable.Error()):
return p.noBlock(provider.ErrLightBlockNotFound)
// 2) check if the height requested is too high
case strings.Contains(e.Data, coretypes.ErrHeightExceedsChainHead.Error()):
return p.noBlock(provider.ErrHeightTooHigh)
// 3) check if the provider closed the connection
case strings.Contains(e.Data, "connection refused"):
return provider.ErrConnectionClosed
// 4) else return a generic error
default:
return provider.ErrBadLightBlock{Reason: e}
}
}
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
}