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