github.com/516108736/tendermint@v0.36.0/light/provider/http/http.go (about)

     1  package http
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math/rand"
     7  	"regexp"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/tendermint/tendermint/light/provider"
    12  	rpcclient "github.com/tendermint/tendermint/rpc/client"
    13  	rpchttp "github.com/tendermint/tendermint/rpc/client/http"
    14  	"github.com/tendermint/tendermint/types"
    15  )
    16  
    17  var (
    18  	// This is very brittle, see: https://github.com/tendermint/tendermint/issues/4740
    19  	regexpMissingHeight = regexp.MustCompile(`height \d+ (must be less than or equal to|is not available)`)
    20  
    21  	maxRetryAttempts      = 10
    22  	timeout          uint = 5 // sec.
    23  )
    24  
    25  // http provider uses an RPC client to obtain the necessary information.
    26  type http struct {
    27  	chainID string
    28  	client  rpcclient.RemoteClient
    29  }
    30  
    31  // New creates a HTTP provider, which is using the rpchttp.HTTP client under
    32  // the hood. If no scheme is provided in the remote URL, http will be used by
    33  // default. The 5s timeout is used for all requests.
    34  func New(chainID, remote string) (provider.Provider, error) {
    35  	// Ensure URL scheme is set (default HTTP) when not provided.
    36  	if !strings.Contains(remote, "://") {
    37  		remote = "http://" + remote
    38  	}
    39  
    40  	httpClient, err := rpchttp.NewWithTimeout(remote, "/websocket", timeout)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  
    45  	return NewWithClient(chainID, httpClient), nil
    46  }
    47  
    48  // NewWithClient allows you to provide a custom client.
    49  func NewWithClient(chainID string, client rpcclient.RemoteClient) provider.Provider {
    50  	return &http{
    51  		client:  client,
    52  		chainID: chainID,
    53  	}
    54  }
    55  
    56  // ChainID returns a chainID this provider was configured with.
    57  func (p *http) ChainID() string {
    58  	return p.chainID
    59  }
    60  
    61  func (p *http) String() string {
    62  	return fmt.Sprintf("http{%s}", p.client.Remote())
    63  }
    64  
    65  // LightBlock fetches a LightBlock at the given height and checks the
    66  // chainID matches.
    67  func (p *http) LightBlock(ctx context.Context, height int64) (*types.LightBlock, error) {
    68  	h, err := validateHeight(height)
    69  	if err != nil {
    70  		return nil, provider.ErrBadLightBlock{Reason: err}
    71  	}
    72  
    73  	sh, err := p.signedHeader(ctx, h)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	vs, err := p.validatorSet(ctx, &sh.Height)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	lb := &types.LightBlock{
    84  		SignedHeader: sh,
    85  		ValidatorSet: vs,
    86  	}
    87  
    88  	err = lb.ValidateBasic(p.chainID)
    89  	if err != nil {
    90  		return nil, provider.ErrBadLightBlock{Reason: err}
    91  	}
    92  
    93  	return lb, nil
    94  }
    95  
    96  // ReportEvidence calls `/broadcast_evidence` endpoint.
    97  func (p *http) ReportEvidence(ctx context.Context, ev types.Evidence) error {
    98  	_, err := p.client.BroadcastEvidence(ctx, ev)
    99  	return err
   100  }
   101  
   102  func (p *http) validatorSet(ctx context.Context, height *int64) (*types.ValidatorSet, error) {
   103  	// Since the malicious node could report a massive number of pages, making us
   104  	// spend a considerable time iterating, we restrict the number of pages here.
   105  	// => 10000 validators max
   106  	const maxPages = 100
   107  
   108  	var (
   109  		perPage = 100
   110  		vals    = []*types.Validator{}
   111  		page    = 1
   112  		total   = -1
   113  	)
   114  
   115  	for len(vals) != total && page <= maxPages {
   116  		for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
   117  			res, err := p.client.Validators(ctx, height, &page, &perPage)
   118  			if err != nil {
   119  				// TODO: standardize errors on the RPC side
   120  				if regexpMissingHeight.MatchString(err.Error()) {
   121  					return nil, provider.ErrLightBlockNotFound
   122  				}
   123  				// if we have exceeded retry attempts then return no response error
   124  				if attempt == maxRetryAttempts {
   125  					return nil, provider.ErrNoResponse
   126  				}
   127  				// else we wait and try again with exponential backoff
   128  				time.Sleep(backoffTimeout(uint16(attempt)))
   129  				continue
   130  			}
   131  
   132  			// Validate response.
   133  			if len(res.Validators) == 0 {
   134  				return nil, provider.ErrBadLightBlock{
   135  					Reason: fmt.Errorf("validator set is empty (height: %d, page: %d, per_page: %d)",
   136  						height, page, perPage),
   137  				}
   138  			}
   139  			if res.Total <= 0 {
   140  				return nil, provider.ErrBadLightBlock{
   141  					Reason: fmt.Errorf("total number of vals is <= 0: %d (height: %d, page: %d, per_page: %d)",
   142  						res.Total, height, page, perPage),
   143  				}
   144  			}
   145  
   146  			total = res.Total
   147  			vals = append(vals, res.Validators...)
   148  			page++
   149  			break
   150  		}
   151  	}
   152  
   153  	valSet, err := types.ValidatorSetFromExistingValidators(vals)
   154  	if err != nil {
   155  		return nil, provider.ErrBadLightBlock{Reason: err}
   156  	}
   157  	return valSet, nil
   158  }
   159  
   160  func (p *http) signedHeader(ctx context.Context, height *int64) (*types.SignedHeader, error) {
   161  	for attempt := 1; attempt <= maxRetryAttempts; attempt++ {
   162  		commit, err := p.client.Commit(ctx, height)
   163  		if err != nil {
   164  			// TODO: standardize errors on the RPC side
   165  			if regexpMissingHeight.MatchString(err.Error()) {
   166  				return nil, provider.ErrLightBlockNotFound
   167  			}
   168  			// we wait and try again with exponential backoff
   169  			time.Sleep(backoffTimeout(uint16(attempt)))
   170  			continue
   171  		}
   172  		return &commit.SignedHeader, nil
   173  	}
   174  	return nil, provider.ErrNoResponse
   175  }
   176  
   177  func validateHeight(height int64) (*int64, error) {
   178  	if height < 0 {
   179  		return nil, fmt.Errorf("expected height >= 0, got height %d", height)
   180  	}
   181  
   182  	h := &height
   183  	if height == 0 {
   184  		h = nil
   185  	}
   186  	return h, nil
   187  }
   188  
   189  // exponential backoff (with jitter)
   190  // 0.5s -> 2s -> 4.5s -> 8s -> 12.5 with 1s variation
   191  func backoffTimeout(attempt uint16) time.Duration {
   192  	// nolint:gosec // G404: Use of weak random number generator
   193  	return time.Duration(500*attempt*attempt)*time.Millisecond + time.Duration(rand.Intn(1000))*time.Millisecond
   194  }