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