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