github.com/lazyledger/lazyledger-core@v0.35.0-dev.0.20210613111200-4c651f053571/light/provider/http/http.go (about) 1 package http 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "math/rand" 8 "regexp" 9 "strings" 10 "time" 11 12 "github.com/lazyledger/lazyledger-core/light/provider" 13 rpcclient "github.com/lazyledger/lazyledger-core/rpc/client" 14 rpchttp "github.com/lazyledger/lazyledger-core/rpc/client/http" 15 "github.com/lazyledger/lazyledger-core/types" 16 ) 17 18 // This is very brittle, see: https://github.com/tendermint/tendermint/issues/4740 19 var ( 20 regexpMissingHeight = regexp.MustCompile(`height \d+ (must be less than or equal to|is not available)`) 21 maxRetryAttempts = 10 22 ) 23 24 // http provider uses an RPC client to obtain the necessary information. 25 type http struct { 26 chainID string 27 client rpcclient.RemoteClient 28 } 29 30 // New creates a HTTP provider, which is using the rpchttp.HTTP client under 31 // the hood. If no scheme is provided in the remote URL, http will be used by 32 // default. 33 func New(chainID, remote string) (provider.Provider, error) { 34 // Ensure URL scheme is set (default HTTP) when not provided. 35 if !strings.Contains(remote, "://") { 36 remote = "http://" + remote 37 } 38 39 httpClient, err := rpchttp.New(remote, "/websocket") 40 if err != nil { 41 return nil, err 42 } 43 44 return NewWithClient(chainID, httpClient), nil 45 } 46 47 // NewWithClient allows you to provide a custom client. 48 func NewWithClient(chainID string, client rpcclient.RemoteClient) provider.Provider { 49 return &http{ 50 client: client, 51 chainID: chainID, 52 } 53 } 54 55 func (p *http) String() string { 56 return fmt.Sprintf("http{%s}", p.client.Remote()) 57 } 58 59 // LightBlock fetches a LightBlock at the given height and checks the 60 // chainID matches. 61 func (p *http) LightBlock(ctx context.Context, height int64) (*types.LightBlock, error) { 62 h, err := validateHeight(height) 63 if err != nil { 64 return nil, provider.ErrBadLightBlock{Reason: err} 65 } 66 67 sh, err := p.signedHeader(ctx, h) 68 if err != nil { 69 return nil, err 70 } 71 72 vs, err := p.validatorSet(ctx, h) 73 if err != nil { 74 return nil, err 75 } 76 77 lb := &types.LightBlock{ 78 SignedHeader: sh, 79 ValidatorSet: vs, 80 } 81 82 err = lb.ValidateBasic(p.chainID) 83 if err != nil { 84 return nil, provider.ErrBadLightBlock{Reason: err} 85 } 86 87 return lb, nil 88 } 89 90 func (p *http) DASLightBlock(ctx context.Context, height int64) (*types.LightBlock, error) { 91 h, err := validateHeight(height) 92 if err != nil { 93 return nil, provider.ErrBadLightBlock{Reason: err} 94 } 95 96 lb, err := p.LightBlock(ctx, height) 97 if err != nil { 98 return nil, err 99 } 100 // additionally get DAHeader from rpc: 101 dah, err := p.daHeader(ctx, h) 102 if err != nil { 103 return nil, provider.ErrBadLightBlock{Reason: err} 104 } 105 // do LightBlock and DAHeader match hashes? 106 if !bytes.Equal(dah.Hash(), lb.DataHash) { 107 return nil, provider.ErrBadLightBlock{ 108 Reason: fmt.Errorf("mismatching data hashes, dah.Hash(): %X != lb.DataHash: %X\ndah: %v", 109 dah.Hash(), 110 lb.DataHash, 111 dah), 112 } 113 } 114 lb.DataAvailabilityHeader = dah 115 116 return lb, nil 117 } 118 119 // ReportEvidence calls `/broadcast_evidence` endpoint. 120 func (p *http) ReportEvidence(ctx context.Context, ev types.Evidence) error { 121 _, err := p.client.BroadcastEvidence(ctx, ev) 122 return err 123 } 124 125 func (p *http) validatorSet(ctx context.Context, height *int64) (*types.ValidatorSet, error) { 126 var ( 127 maxPerPage = 100 128 vals = []*types.Validator{} 129 page = 1 130 ) 131 132 for len(vals)%maxPerPage == 0 { 133 for attempt := 1; attempt <= maxRetryAttempts; attempt++ { 134 res, err := p.client.Validators(ctx, height, &page, &maxPerPage) 135 if err != nil { 136 // TODO: standardize errors on the RPC side 137 if regexpMissingHeight.MatchString(err.Error()) { 138 return nil, provider.ErrLightBlockNotFound 139 } 140 // if we have exceeded retry attempts then return no response error 141 if attempt == maxRetryAttempts { 142 return nil, provider.ErrNoResponse 143 } 144 // else we wait and try again with exponential backoff 145 time.Sleep(backoffTimeout(uint16(attempt))) 146 continue 147 } 148 if len(res.Validators) == 0 { // no more validators left 149 valSet, err := types.ValidatorSetFromExistingValidators(vals) 150 if err != nil { 151 return nil, provider.ErrBadLightBlock{Reason: err} 152 } 153 return valSet, nil 154 } 155 vals = append(vals, res.Validators...) 156 page++ 157 break 158 } 159 } 160 valSet, err := types.ValidatorSetFromExistingValidators(vals) 161 if err != nil { 162 return nil, provider.ErrBadLightBlock{Reason: err} 163 } 164 return valSet, nil 165 } 166 167 func (p *http) signedHeader(ctx context.Context, height *int64) (*types.SignedHeader, error) { 168 for attempt := 1; attempt <= maxRetryAttempts; attempt++ { 169 commit, err := p.client.Commit(ctx, height) 170 if err != nil { 171 // TODO: standardize errors on the RPC side 172 if regexpMissingHeight.MatchString(err.Error()) { 173 return nil, provider.ErrLightBlockNotFound 174 } 175 // we wait and try again with exponential backoff 176 time.Sleep(backoffTimeout(uint16(attempt))) 177 continue 178 } 179 return &commit.SignedHeader, nil 180 } 181 return nil, provider.ErrNoResponse 182 } 183 184 func (p *http) daHeader(ctx context.Context, height *int64) (*types.DataAvailabilityHeader, error) { 185 for attempt := 1; attempt <= maxRetryAttempts; attempt++ { 186 daHeaderRes, err := p.client.DataAvailabilityHeader(ctx, height) 187 if err != nil { 188 if regexpMissingHeight.MatchString(err.Error()) { 189 return nil, provider.ErrDAHeaderNotFound 190 } 191 // we wait and try again with exponential backoff 192 time.Sleep(backoffTimeout(uint16(attempt))) 193 continue 194 } 195 return &daHeaderRes.DataAvailabilityHeader, nil 196 } 197 return nil, provider.ErrNoResponse 198 } 199 200 func validateHeight(height int64) (*int64, error) { 201 if height < 0 { 202 return nil, fmt.Errorf("expected height >= 0, got height %d", height) 203 } 204 205 h := &height 206 if height == 0 { 207 h = nil 208 } 209 return h, nil 210 } 211 212 // exponential backoff (with jitter) 213 // 0.5s -> 2s -> 4.5s -> 8s -> 12.5 with 1s variation 214 func backoffTimeout(attempt uint16) time.Duration { 215 // nolint:gosec // G404: Use of weak random number generator 216 return time.Duration(500*attempt*attempt)*time.Millisecond + time.Duration(rand.Intn(1000))*time.Millisecond 217 }