github.com/lazyledger/lazyledger-core@v0.35.0-dev.0.20210613111200-4c651f053571/light/provider/http/http.go (about)

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