github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/light/provider/http/http.go (about)

     1  package http
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"math/rand"
     8  	"net/url"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/ari-anchor/sei-tendermint/light/provider"
    13  	rpcclient "github.com/ari-anchor/sei-tendermint/rpc/client"
    14  	rpchttp "github.com/ari-anchor/sei-tendermint/rpc/client/http"
    15  	"github.com/ari-anchor/sei-tendermint/rpc/coretypes"
    16  	rpctypes "github.com/ari-anchor/sei-tendermint/rpc/jsonrpc/types"
    17  	"github.com/ari-anchor/sei-tendermint/types"
    18  )
    19  
    20  var defaultOptions = Options{
    21  	MaxRetryAttempts:    5,
    22  	Timeout:             5 * time.Second,
    23  	NoBlockThreshold:    5,
    24  	NoResponseThreshold: 5,
    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  	// httt provider heuristics
    33  
    34  	// The provider tracks the amount of times that the
    35  	// client doesn't respond. If this exceeds the threshold
    36  	// then the provider will return an unreliable provider error
    37  	noResponseThreshold uint16
    38  	noResponseCount     uint16
    39  
    40  	// The provider tracks the amount of time the client
    41  	// doesn't have a block. If this exceeds the threshold
    42  	// then the provider will return an unreliable provider error
    43  	noBlockThreshold uint16
    44  	noBlockCount     uint16
    45  
    46  	// In a single request, the provider attempts multiple times
    47  	// with exponential backoff to reach the client. If this
    48  	// exceeds the maxRetry attempts, this result in a ErrNoResponse
    49  	maxRetryAttempts uint16
    50  }
    51  
    52  type Options struct {
    53  	// 0 means no retries
    54  	MaxRetryAttempts uint16
    55  	// 0 means no timeout.
    56  	Timeout time.Duration
    57  	// The amount of requests that a client doesn't have the block
    58  	// for before the provider deems the client unreliable
    59  	NoBlockThreshold uint16
    60  	// The amount of requests that a client doesn't respond to
    61  	// before the provider deems the client unreliable
    62  	NoResponseThreshold uint16
    63  }
    64  
    65  // New creates a HTTP provider, which is using the rpchttp.HTTP client under
    66  // the hood. If no scheme is provided in the remote URL, http will be used by
    67  // default. The 5s timeout is used for all requests.
    68  func New(chainID, remote string) (provider.Provider, error) {
    69  	return NewWithOptions(chainID, remote, defaultOptions)
    70  }
    71  
    72  // NewWithOptions is an extension to creating a new http provider that allows the addition
    73  // of a specified timeout and maxRetryAttempts
    74  func NewWithOptions(chainID, remote string, options Options) (provider.Provider, error) {
    75  	// Ensure URL scheme is set (default HTTP) when not provided.
    76  	if !strings.Contains(remote, "://") {
    77  		remote = "http://" + remote
    78  	}
    79  
    80  	httpClient, err := rpchttp.NewWithTimeout(remote, options.Timeout)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	return NewWithClientAndOptions(chainID, httpClient, options), nil
    86  }
    87  
    88  func NewWithClient(chainID string, client rpcclient.RemoteClient) provider.Provider {
    89  	return NewWithClientAndOptions(chainID, client, defaultOptions)
    90  }
    91  
    92  // NewWithClient allows you to provide a custom client.
    93  func NewWithClientAndOptions(chainID string, client rpcclient.RemoteClient, options Options) provider.Provider {
    94  	return &http{
    95  		client:              client,
    96  		chainID:             chainID,
    97  		maxRetryAttempts:    options.MaxRetryAttempts,
    98  		noResponseThreshold: options.NoResponseThreshold,
    99  		noBlockThreshold:    options.NoBlockThreshold,
   100  	}
   101  }
   102  
   103  // Identifies the provider with an IP in string format
   104  func (p *http) ID() string {
   105  	return fmt.Sprintf("http{%s}", p.client.Remote())
   106  }
   107  
   108  // LightBlock fetches a LightBlock at the given height and checks the
   109  // chainID matches.
   110  func (p *http) LightBlock(ctx context.Context, height int64) (*types.LightBlock, error) {
   111  	h, err := validateHeight(height)
   112  	if err != nil {
   113  		return nil, provider.ErrBadLightBlock{Reason: err}
   114  	}
   115  
   116  	sh, err := p.signedHeader(ctx, h)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	if height != 0 && sh.Height != height {
   122  		return nil, provider.ErrBadLightBlock{
   123  			Reason: fmt.Errorf("height %d responded doesn't match height %d requested", sh.Height, height),
   124  		}
   125  	}
   126  
   127  	if sh.Header == nil {
   128  		return nil, provider.ErrBadLightBlock{
   129  			Reason: errors.New("returned header is nil unexpectedly"),
   130  		}
   131  	}
   132  
   133  	vs, err := p.validatorSet(ctx, &sh.Height)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	lb := &types.LightBlock{
   139  		SignedHeader: sh,
   140  		ValidatorSet: vs,
   141  	}
   142  
   143  	err = lb.ValidateBasic(p.chainID)
   144  	if err != nil {
   145  		return nil, provider.ErrBadLightBlock{Reason: err}
   146  	}
   147  
   148  	return lb, nil
   149  }
   150  
   151  // ReportEvidence calls `/broadcast_evidence` endpoint.
   152  func (p *http) ReportEvidence(ctx context.Context, ev types.Evidence) error {
   153  	_, err := p.client.BroadcastEvidence(ctx, ev)
   154  	return err
   155  }
   156  
   157  func (p *http) validatorSet(ctx context.Context, height *int64) (*types.ValidatorSet, error) {
   158  	// Since the malicious node could report a massive number of pages, making us
   159  	// spend a considerable time iterating, we restrict the number of pages here.
   160  	// => 10000 validators max
   161  	const maxPages = 100
   162  
   163  	var (
   164  		perPage = 100
   165  		vals    = []*types.Validator{}
   166  		page    = 1
   167  		total   = -1
   168  	)
   169  
   170  	for len(vals) != total && page <= maxPages {
   171  		// create another for loop to control retries. If p.maxRetryAttempts
   172  		// is negative we will keep repeating.
   173  		attempt := uint16(0)
   174  		for {
   175  			res, err := p.client.Validators(ctx, height, &page, &perPage)
   176  			if err == nil {
   177  				if len(res.Validators) == 0 {
   178  					return nil, provider.ErrBadLightBlock{
   179  						Reason: fmt.Errorf("validator set is empty (height: %d, page: %d, per_page: %d)",
   180  							height, page, perPage),
   181  					}
   182  				}
   183  				if res.Total <= 0 {
   184  					return nil, provider.ErrBadLightBlock{
   185  						Reason: fmt.Errorf("total number of vals is <= 0: %d (height: %d, page: %d, per_page: %d)",
   186  							res.Total, height, page, perPage),
   187  					}
   188  				}
   189  			} else {
   190  				switch e := err.(type) {
   191  
   192  				case *url.Error:
   193  					if e.Timeout() {
   194  						// if we have exceeded retry attempts then return a no response error
   195  						if attempt == p.maxRetryAttempts {
   196  							return nil, p.noResponse()
   197  						}
   198  						attempt++
   199  						// request timed out: we wait and try again with exponential backoff
   200  						time.Sleep(backoffTimeout(attempt))
   201  						continue
   202  					}
   203  					return nil, provider.ErrBadLightBlock{Reason: e}
   204  
   205  				case *rpctypes.RPCError:
   206  					// process the rpc error and return the corresponding error to the light client
   207  					return nil, p.parseRPCError(e)
   208  
   209  				default:
   210  					// check if the error stems from the context
   211  					if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
   212  						return nil, err
   213  					}
   214  
   215  					// If we don't know the error then by default we return an unreliable provider error and
   216  					// terminate the connection with the peer.
   217  					return nil, provider.ErrUnreliableProvider{Reason: e}
   218  				}
   219  			}
   220  			// update the total and increment the page index so we can fetch the
   221  			// next page of validators if need be
   222  			total = res.Total
   223  			vals = append(vals, res.Validators...)
   224  			page++
   225  			break
   226  		}
   227  
   228  	}
   229  
   230  	valSet, err := types.ValidatorSetFromExistingValidators(vals)
   231  	if err != nil {
   232  		return nil, provider.ErrBadLightBlock{Reason: err}
   233  	}
   234  	return valSet, nil
   235  }
   236  
   237  func (p *http) signedHeader(ctx context.Context, height *int64) (*types.SignedHeader, error) {
   238  	// create a for loop to control retries. If p.maxRetryAttempts
   239  	// is negative we will keep repeating.
   240  	for attempt := uint16(0); attempt != p.maxRetryAttempts+1; attempt++ {
   241  		commit, err := p.client.Commit(ctx, height)
   242  		switch e := err.(type) {
   243  		case nil: // success!!
   244  			return &commit.SignedHeader, nil
   245  
   246  		case *url.Error:
   247  			// check if the request timed out
   248  			if e.Timeout() {
   249  				// we wait and try again with exponential backoff
   250  				time.Sleep(backoffTimeout(attempt))
   251  				continue
   252  			}
   253  
   254  			// check if the connection was refused or dropped
   255  			if strings.Contains(e.Error(), "connection refused") {
   256  				return nil, provider.ErrConnectionClosed
   257  			}
   258  
   259  			// else, as a catch all, we return the error as a bad light block response
   260  			return nil, provider.ErrBadLightBlock{Reason: e}
   261  
   262  		case *rpctypes.RPCError:
   263  			// process the rpc error and return the corresponding error to the light client
   264  			return nil, p.parseRPCError(e)
   265  
   266  		default:
   267  			// check if the error stems from the context
   268  			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
   269  				return nil, err
   270  			}
   271  
   272  			// If we don't know the error then by default we return an unreliable provider error and
   273  			// terminate the connection with the peer.
   274  			return nil, provider.ErrUnreliableProvider{Reason: e}
   275  		}
   276  	}
   277  	return nil, p.noResponse()
   278  }
   279  
   280  func (p *http) noResponse() error {
   281  	p.noResponseCount++
   282  	if p.noResponseCount > p.noResponseThreshold {
   283  		return provider.ErrUnreliableProvider{
   284  			Reason: fmt.Errorf("failed to respond after %d attempts", p.noResponseCount),
   285  		}
   286  	}
   287  	return provider.ErrNoResponse
   288  }
   289  
   290  func (p *http) noBlock(e error) error {
   291  	p.noBlockCount++
   292  	if p.noBlockCount > p.noBlockThreshold {
   293  		return provider.ErrUnreliableProvider{
   294  			Reason: fmt.Errorf("failed to provide a block after %d attempts", p.noBlockCount),
   295  		}
   296  	}
   297  	return e
   298  }
   299  
   300  // parseRPCError process the error and return the corresponding error to the light clent
   301  // NOTE: When an error is sent over the wire it gets "flattened" hence we are unable to use error
   302  // checking functions like errors.Is() to unwrap the error.
   303  func (p *http) parseRPCError(e *rpctypes.RPCError) error {
   304  	switch {
   305  	// 1) check if the error indicates that the peer doesn't have the block
   306  	case strings.Contains(e.Data, coretypes.ErrHeightNotAvailable.Error()):
   307  		return p.noBlock(provider.ErrLightBlockNotFound)
   308  
   309  	// 2) check if the height requested is too high
   310  	case strings.Contains(e.Data, coretypes.ErrHeightExceedsChainHead.Error()):
   311  		return p.noBlock(provider.ErrHeightTooHigh)
   312  
   313  	// 3) check if the provider closed the connection
   314  	case strings.Contains(e.Data, "connection refused"):
   315  		return provider.ErrConnectionClosed
   316  
   317  	// 4) else return a generic error
   318  	default:
   319  		return provider.ErrBadLightBlock{Reason: e}
   320  	}
   321  }
   322  
   323  func validateHeight(height int64) (*int64, error) {
   324  	if height < 0 {
   325  		return nil, fmt.Errorf("expected height >= 0, got height %d", height)
   326  	}
   327  
   328  	h := &height
   329  	if height == 0 {
   330  		h = nil
   331  	}
   332  	return h, nil
   333  }
   334  
   335  // exponential backoff (with jitter)
   336  // 0.5s -> 2s -> 4.5s -> 8s -> 12.5 with 1s variation
   337  func backoffTimeout(attempt uint16) time.Duration {
   338  	// nolint:gosec // G404: Use of weak random number generator
   339  	return time.Duration(500*attempt*attempt)*time.Millisecond + time.Duration(rand.Intn(1000))*time.Millisecond
   340  }