github.com/number571/tendermint@v0.34.11-gost/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/number571/tendermint/light/provider"
    13  	rpcclient "github.com/number571/tendermint/rpc/client"
    14  	rpchttp "github.com/number571/tendermint/rpc/client/http"
    15  	ctypes "github.com/number571/tendermint/rpc/core/types"
    16  	rpctypes "github.com/number571/tendermint/rpc/jsonrpc/types"
    17  	"github.com/number571/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  func (p *http) String() string {
   104  	return fmt.Sprintf("http{%s}", p.client.Remote())
   105  }
   106  
   107  // LightBlock fetches a LightBlock at the given height and checks the
   108  // chainID matches.
   109  func (p *http) LightBlock(ctx context.Context, height int64) (*types.LightBlock, error) {
   110  	h, err := validateHeight(height)
   111  	if err != nil {
   112  		return nil, provider.ErrBadLightBlock{Reason: err}
   113  	}
   114  
   115  	sh, err := p.signedHeader(ctx, h)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	if height != 0 && sh.Height != height {
   121  		return nil, provider.ErrBadLightBlock{
   122  			Reason: fmt.Errorf("height %d responded doesn't match height %d requested", sh.Height, height),
   123  		}
   124  	}
   125  
   126  	if sh.Header == nil {
   127  		return nil, provider.ErrBadLightBlock{
   128  			Reason: errors.New("returned header is nil unexpectedly"),
   129  		}
   130  	}
   131  
   132  	vs, err := p.validatorSet(ctx, &sh.Height)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  
   137  	lb := &types.LightBlock{
   138  		SignedHeader: sh,
   139  		ValidatorSet: vs,
   140  	}
   141  
   142  	err = lb.ValidateBasic(p.chainID)
   143  	if err != nil {
   144  		return nil, provider.ErrBadLightBlock{Reason: err}
   145  	}
   146  
   147  	return lb, nil
   148  }
   149  
   150  // ReportEvidence calls `/broadcast_evidence` endpoint.
   151  func (p *http) ReportEvidence(ctx context.Context, ev types.Evidence) error {
   152  	_, err := p.client.BroadcastEvidence(ctx, ev)
   153  	return err
   154  }
   155  
   156  func (p *http) validatorSet(ctx context.Context, height *int64) (*types.ValidatorSet, error) {
   157  	// Since the malicious node could report a massive number of pages, making us
   158  	// spend a considerable time iterating, we restrict the number of pages here.
   159  	// => 10000 validators max
   160  	const maxPages = 100
   161  
   162  	var (
   163  		perPage = 100
   164  		vals    = []*types.Validator{}
   165  		page    = 1
   166  		total   = -1
   167  	)
   168  
   169  	for len(vals) != total && page <= maxPages {
   170  		// create another for loop to control retries. If p.maxRetryAttempts
   171  		// is negative we will keep repeating.
   172  		attempt := uint16(0)
   173  		for {
   174  			res, err := p.client.Validators(ctx, height, &page, &perPage)
   175  			switch e := err.(type) {
   176  			case nil: // success!! Now we validate the response
   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  
   190  			case *url.Error:
   191  				if e.Timeout() {
   192  					// if we have exceeded retry attempts then return a no response error
   193  					if attempt == p.maxRetryAttempts {
   194  						return nil, p.noResponse()
   195  					}
   196  					attempt++
   197  					// request timed out: we wait and try again with exponential backoff
   198  					time.Sleep(backoffTimeout(attempt))
   199  					continue
   200  				}
   201  				return nil, provider.ErrBadLightBlock{Reason: e}
   202  
   203  			case *rpctypes.RPCError:
   204  				// process the rpc error and return the corresponding error to the light client
   205  				return nil, p.parseRPCError(e)
   206  
   207  			default:
   208  				// check if the error stems from the context
   209  				if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
   210  					return nil, err
   211  				}
   212  
   213  				// If we don't know the error then by default we return an unreliable provider error and
   214  				// terminate the connection with the peer.
   215  				return nil, provider.ErrUnreliableProvider{Reason: e.Error()}
   216  			}
   217  
   218  			// update the total and increment the page index so we can fetch the
   219  			// next page of validators if need be
   220  			total = res.Total
   221  			vals = append(vals, res.Validators...)
   222  			page++
   223  			break
   224  		}
   225  	}
   226  
   227  	valSet, err := types.ValidatorSetFromExistingValidators(vals)
   228  	if err != nil {
   229  		return nil, provider.ErrBadLightBlock{Reason: err}
   230  	}
   231  	return valSet, nil
   232  }
   233  
   234  func (p *http) signedHeader(ctx context.Context, height *int64) (*types.SignedHeader, error) {
   235  	// create a for loop to control retries. If p.maxRetryAttempts
   236  	// is negative we will keep repeating.
   237  	for attempt := uint16(0); attempt != p.maxRetryAttempts+1; attempt++ {
   238  		commit, err := p.client.Commit(ctx, height)
   239  		switch e := err.(type) {
   240  		case nil: // success!!
   241  			return &commit.SignedHeader, nil
   242  
   243  		case *url.Error:
   244  			// check if the request timed out
   245  			if e.Timeout() {
   246  				// we wait and try again with exponential backoff
   247  				time.Sleep(backoffTimeout(attempt))
   248  				continue
   249  			}
   250  
   251  			// check if the connection was refused or dropped
   252  			if strings.Contains(e.Error(), "connection refused") {
   253  				return nil, provider.ErrConnectionClosed
   254  			}
   255  
   256  			// else, as a catch all, we return the error as a bad light block response
   257  			return nil, provider.ErrBadLightBlock{Reason: e}
   258  
   259  		case *rpctypes.RPCError:
   260  			// process the rpc error and return the corresponding error to the light client
   261  			return nil, p.parseRPCError(e)
   262  
   263  		default:
   264  			// check if the error stems from the context
   265  			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
   266  				return nil, err
   267  			}
   268  
   269  			// If we don't know the error then by default we return an unreliable provider error and
   270  			// terminate the connection with the peer.
   271  			return nil, provider.ErrUnreliableProvider{Reason: e.Error()}
   272  		}
   273  	}
   274  	return nil, p.noResponse()
   275  }
   276  
   277  func (p *http) noResponse() error {
   278  	p.noResponseCount++
   279  	if p.noResponseCount > p.noResponseThreshold {
   280  		return provider.ErrUnreliableProvider{
   281  			Reason: fmt.Sprintf("failed to respond after %d attempts", p.noResponseCount),
   282  		}
   283  	}
   284  	return provider.ErrNoResponse
   285  }
   286  
   287  func (p *http) noBlock(e error) error {
   288  	p.noBlockCount++
   289  	if p.noBlockCount > p.noBlockThreshold {
   290  		return provider.ErrUnreliableProvider{
   291  			Reason: fmt.Sprintf("failed to provide a block after %d attempts", p.noBlockCount),
   292  		}
   293  	}
   294  	return e
   295  }
   296  
   297  // parseRPCError process the error and return the corresponding error to the light clent
   298  // NOTE: When an error is sent over the wire it gets "flattened" hence we are unable to use error
   299  // checking functions like errors.Is() to unwrap the error.
   300  func (p *http) parseRPCError(e *rpctypes.RPCError) error {
   301  	switch {
   302  	// 1) check if the error indicates that the peer doesn't have the block
   303  	case strings.Contains(e.Data, ctypes.ErrHeightNotAvailable.Error()):
   304  		return p.noBlock(provider.ErrLightBlockNotFound)
   305  
   306  	// 2) check if the height requested is too high
   307  	case strings.Contains(e.Data, ctypes.ErrHeightExceedsChainHead.Error()):
   308  		return p.noBlock(provider.ErrHeightTooHigh)
   309  
   310  	// 3) check if the provider closed the connection
   311  	case strings.Contains(e.Data, "connection refused"):
   312  		return provider.ErrConnectionClosed
   313  
   314  	// 4) else return a generic error
   315  	default:
   316  		return provider.ErrBadLightBlock{Reason: e}
   317  	}
   318  }
   319  
   320  func validateHeight(height int64) (*int64, error) {
   321  	if height < 0 {
   322  		return nil, fmt.Errorf("expected height >= 0, got height %d", height)
   323  	}
   324  
   325  	h := &height
   326  	if height == 0 {
   327  		h = nil
   328  	}
   329  	return h, nil
   330  }
   331  
   332  // exponential backoff (with jitter)
   333  // 0.5s -> 2s -> 4.5s -> 8s -> 12.5 with 1s variation
   334  func backoffTimeout(attempt uint16) time.Duration {
   335  	// nolint:gosec // G404: Use of weak random number generator
   336  	return time.Duration(500*attempt*attempt)*time.Millisecond + time.Duration(rand.Intn(1000))*time.Millisecond
   337  }