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