package http
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"regexp"
|
|
"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/types"
|
|
)
|
|
|
|
// This is very brittle, see: https://github.com/tendermint/tendermint/issues/4740
|
|
var (
|
|
regexpMissingHeight = regexp.MustCompile(`height \d+ (must be less than or equal to|is not available)`)
|
|
maxRetryAttempts = 10
|
|
)
|
|
|
|
// http provider uses an RPC client to obtain the necessary information.
|
|
type http struct {
|
|
chainID string
|
|
client rpcclient.RemoteClient
|
|
}
|
|
|
|
// 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.
|
|
func New(chainID, remote string) (provider.Provider, error) {
|
|
// Ensure URL scheme is set (default HTTP) when not provided.
|
|
if !strings.Contains(remote, "://") {
|
|
remote = "http://" + remote
|
|
}
|
|
|
|
httpClient, err := rpchttp.New(remote, "/websocket")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return NewWithClient(chainID, httpClient), nil
|
|
}
|
|
|
|
// NewWithClient allows you to provide a custom client.
|
|
func NewWithClient(chainID string, client rpcclient.RemoteClient) provider.Provider {
|
|
return &http{
|
|
client: client,
|
|
chainID: chainID,
|
|
}
|
|
}
|
|
|
|
// ChainID returns a chainID this provider was configured with.
|
|
func (p *http) ChainID() string {
|
|
return p.chainID
|
|
}
|
|
|
|
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, h)
|
|
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) {
|
|
var (
|
|
maxPerPage = 100
|
|
vals = []*types.Validator{}
|
|
page = 1
|
|
)
|
|
|
|
for len(vals)%maxPerPage == 0 {
|
|
for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
|
|
res, err := p.client.Validators(ctx, height, &page, &maxPerPage)
|
|
if err != nil {
|
|
// TODO: standardize errors on the RPC side
|
|
if regexpMissingHeight.MatchString(err.Error()) {
|
|
return nil, provider.ErrLightBlockNotFound
|
|
}
|
|
// if we have exceeded retry attempts then return no response error
|
|
if attempt == maxRetryAttempts {
|
|
return nil, provider.ErrNoResponse
|
|
}
|
|
// else we wait and try again with exponential backoff
|
|
time.Sleep(backoffTimeout(uint16(attempt)))
|
|
continue
|
|
}
|
|
if len(res.Validators) == 0 { // no more validators left
|
|
valSet, err := types.ValidatorSetFromExistingValidators(vals)
|
|
if err != nil {
|
|
return nil, provider.ErrBadLightBlock{Reason: err}
|
|
}
|
|
return valSet, nil
|
|
}
|
|
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) {
|
|
for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
|
|
commit, err := p.client.Commit(ctx, height)
|
|
if err != nil {
|
|
// TODO: standardize errors on the RPC side
|
|
if regexpMissingHeight.MatchString(err.Error()) {
|
|
return nil, provider.ErrLightBlockNotFound
|
|
}
|
|
// we wait and try again with exponential backoff
|
|
time.Sleep(backoffTimeout(uint16(attempt)))
|
|
continue
|
|
}
|
|
return &commit.SignedHeader, nil
|
|
}
|
|
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
|
|
}
|