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