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 }