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 }