github.com/0chain/gosdk@v1.17.11/zcncore/transaction_query_mobile.go (about) 1 //go:build mobile 2 // +build mobile 3 4 package zcncore 5 6 import ( 7 "context" 8 "encoding/json" 9 "errors" 10 stderrors "errors" 11 "math/rand" 12 "net/http" 13 "strconv" 14 "strings" 15 "time" 16 17 thrown "github.com/0chain/errors" 18 "github.com/0chain/gosdk/core/resty" 19 "github.com/0chain/gosdk/core/util" 20 ) 21 22 var ( 23 ErrNoAvailableSharders = errors.New("zcn: no available sharders") 24 ErrNoEnoughSharders = errors.New("zcn: sharders is not enough") 25 ErrNoEnoughOnlineSharders = errors.New("zcn: online sharders is not enough") 26 ErrInvalidNumSharder = errors.New("zcn: number of sharders is invalid") 27 ErrNoOnlineSharders = errors.New("zcn: no any online sharder") 28 ErrSharderOffline = errors.New("zcn: sharder is offline") 29 ErrInvalidConsensus = errors.New("zcn: invalid consensus") 30 ErrTransactionNotFound = errors.New("zcn: transaction not found") 31 ErrTransactionNotConfirmed = errors.New("zcn: transaction not confirmed") 32 ) 33 34 const ( 35 SharderEndpointHealthCheck = "/_health_check" 36 ) 37 38 type QueryResult struct { 39 Content []byte 40 StatusCode int 41 Error error 42 } 43 44 // queryResultHandle handle query response, return true if it is a consensus-result 45 type queryResultHandle func(result QueryResult) bool 46 47 type transactionQuery struct { 48 max int 49 sharders []string 50 51 selected map[string]interface{} 52 offline map[string]interface{} 53 } 54 55 func (tq *transactionQuery) Reset() { 56 tq.selected = make(map[string]interface{}) 57 tq.offline = make(map[string]interface{}) 58 } 59 60 // validate validate data and input 61 func (tq *transactionQuery) validate(num int) error { 62 if tq == nil || tq.max == 0 { 63 return ErrNoAvailableSharders 64 } 65 66 if num < 1 { 67 return ErrInvalidNumSharder 68 } 69 70 if num > tq.max { 71 return ErrNoEnoughSharders 72 } 73 74 if num > (tq.max - len(tq.offline)) { 75 return ErrNoEnoughOnlineSharders 76 } 77 78 return nil 79 80 } 81 82 // buildUrl build url with host and parts 83 func (tq *transactionQuery) buildUrl(host string, parts ...string) string { 84 var sb strings.Builder 85 86 sb.WriteString(strings.TrimSuffix(host, "/")) 87 88 for _, it := range parts { 89 sb.WriteString(it) 90 } 91 92 return sb.String() 93 } 94 95 // checkHealth check health 96 func (tq *transactionQuery) checkHealth(ctx context.Context, host string) error { 97 98 _, ok := tq.offline[host] 99 if ok { 100 return ErrSharderOffline 101 } 102 103 // check health 104 r := resty.New() 105 requestUrl := tq.buildUrl(host, SharderEndpointHealthCheck) 106 logging.Info("zcn: check health ", requestUrl) 107 r.DoGet(ctx, requestUrl) 108 r.Then(func(req *http.Request, resp *http.Response, respBody []byte, cf context.CancelFunc, err error) error { 109 if err != nil { 110 return err 111 } 112 113 // 5xx: it is a server error, not client error 114 if resp.StatusCode >= http.StatusInternalServerError { 115 return thrown.Throw(ErrSharderOffline, resp.Status) 116 } 117 118 return nil 119 }) 120 errs := r.Wait() 121 122 if len(errs) > 0 { 123 tq.offline[host] = true 124 125 if len(tq.offline) >= tq.max { 126 return ErrNoOnlineSharders 127 } 128 } 129 130 return nil 131 } 132 133 // randOne random one health sharder 134 func (tq *transactionQuery) randOne(ctx context.Context) (string, error) { 135 136 randGen := rand.New(rand.NewSource(time.Now().UnixNano())) 137 for { 138 139 // reset selected if all sharders were selected 140 if len(tq.selected) >= tq.max { 141 tq.selected = make(map[string]interface{}) 142 } 143 144 i := randGen.Intn(len(tq.sharders)) 145 host := tq.sharders[i] 146 147 _, ok := tq.selected[host] 148 149 // it was selected, try next 150 if ok { 151 continue 152 } 153 154 tq.selected[host] = true 155 156 err := tq.checkHealth(ctx, host) 157 158 if err != nil { 159 if errors.Is(err, ErrNoOnlineSharders) { 160 return "", err 161 } 162 163 // it is offline, try next one 164 continue 165 } 166 167 return host, nil 168 } 169 } 170 171 func newTransactionQuery(sharders []string) (*transactionQuery, error) { 172 173 if len(sharders) == 0 { 174 return nil, ErrNoAvailableSharders 175 } 176 177 tq := &transactionQuery{ 178 max: len(sharders), 179 sharders: sharders, 180 } 181 tq.selected = make(map[string]interface{}) 182 tq.offline = make(map[string]interface{}) 183 184 return tq, nil 185 } 186 187 // fromAll query transaction from all sharders whatever it is selected or offline in previous queires, and return consensus result 188 func (tq *transactionQuery) fromAll(query string, handle queryResultHandle, timeout RequestTimeout) error { 189 if tq == nil || tq.max == 0 { 190 return ErrNoAvailableSharders 191 } 192 193 ctx, cancel := makeTimeoutContext(timeout) 194 defer cancel() 195 196 urls := make([]string, 0, tq.max) 197 for _, host := range tq.sharders { 198 urls = append(urls, tq.buildUrl(host, query)) 199 } 200 201 r := resty.New() 202 r.DoGet(ctx, urls...). 203 Then(func(req *http.Request, resp *http.Response, respBody []byte, cf context.CancelFunc, err error) error { 204 res := QueryResult{ 205 Content: respBody, 206 Error: err, 207 StatusCode: http.StatusBadRequest, 208 } 209 210 if resp != nil { 211 res.StatusCode = resp.StatusCode 212 213 logging.Debug(req.URL.String() + " " + resp.Status) 214 logging.Debug(string(respBody)) 215 } else { 216 logging.Debug(req.URL.String()) 217 218 } 219 220 if handle != nil { 221 if handle(res) { 222 223 cf() 224 } 225 } 226 227 return nil 228 }) 229 230 r.Wait() 231 232 return nil 233 } 234 235 // fromAny query transaction from any sharder that is not selected in previous queires. use any used sharder if there is not any unused sharder 236 func (tq *transactionQuery) fromAny(query string, timeout RequestTimeout) (QueryResult, error) { 237 res := QueryResult{ 238 StatusCode: http.StatusBadRequest, 239 } 240 241 ctx, cancel := makeTimeoutContext(timeout) 242 defer cancel() 243 244 err := tq.validate(1) 245 246 if err != nil { 247 return res, err 248 } 249 250 host, err := tq.randOne(ctx) 251 252 if err != nil { 253 return res, err 254 } 255 256 r := resty.New() 257 requestUrl := tq.buildUrl(host, query) 258 259 logging.Debug("GET", requestUrl) 260 261 r.DoGet(ctx, requestUrl). 262 Then(func(req *http.Request, resp *http.Response, respBody []byte, cf context.CancelFunc, err error) error { 263 res.Error = err 264 if err != nil { 265 return err 266 } 267 268 res.Content = respBody 269 logging.Debug(string(respBody)) 270 271 if resp != nil { 272 res.StatusCode = resp.StatusCode 273 } 274 275 return nil 276 }) 277 278 errs := r.Wait() 279 280 if len(errs) > 0 { 281 return res, errs[0] 282 } 283 284 return res, nil 285 286 } 287 288 func (tq *transactionQuery) getInfo(query string, timeout RequestTimeout) (*QueryResult, error) { 289 290 consensuses := make(map[int]int) 291 var maxConsensus int 292 var consensusesResp QueryResult 293 // {host}{query} 294 295 err := tq.fromAll(query, 296 func(qr QueryResult) bool { 297 //ignore response if it is network error 298 if qr.StatusCode >= 500 { 299 return false 300 } 301 302 consensuses[qr.StatusCode]++ 303 if consensuses[qr.StatusCode] >= maxConsensus { 304 maxConsensus = consensuses[qr.StatusCode] 305 consensusesResp = qr 306 } 307 308 return false 309 310 }, timeout) 311 312 if err != nil { 313 return nil, err 314 } 315 316 if maxConsensus == 0 { 317 return nil, stderrors.New("zcn: query not found") 318 } 319 320 rate := float32(maxConsensus*100) / float32(tq.max) 321 if rate < consensusThresh { 322 return nil, ErrInvalidConsensus 323 } 324 325 if consensusesResp.StatusCode != http.StatusOK { 326 return nil, stderrors.New(string(consensusesResp.Content)) 327 } 328 329 return &consensusesResp, nil 330 } 331 332 func (tq *transactionQuery) getConsensusConfirmation(numSharders int, txnHash string, timeout RequestTimeout) (*blockHeader, map[string]json.RawMessage, *blockHeader, error) { 333 var maxConfirmation int 334 txnConfirmations := make(map[string]int) 335 var confirmationBlockHeader *blockHeader 336 var confirmationBlock map[string]json.RawMessage 337 var lfbBlockHeader *blockHeader 338 maxLfbBlockHeader := int(0) 339 lfbBlockHeaders := make(map[string]int) 340 341 // {host}/v1/transaction/get/confirmation?hash={txnHash}&content=lfb 342 err := tq.fromAll(tq.buildUrl("", TXN_VERIFY_URL, txnHash, "&content=lfb"), 343 func(qr QueryResult) bool { 344 if qr.StatusCode != http.StatusOK { 345 return false 346 } 347 348 var cfmBlock map[string]json.RawMessage 349 err := json.Unmarshal([]byte(qr.Content), &cfmBlock) 350 if err != nil { 351 logging.Error("txn confirmation parse error", err) 352 return false 353 } 354 355 // parse `confirmation` section as block header 356 cfmBlockHeader, err := getBlockHeaderFromTransactionConfirmation(txnHash, cfmBlock) 357 if err != nil { 358 logging.Error("txn confirmation parse header error", err) 359 360 // parse `latest_finalized_block` section 361 if lfbRaw, ok := cfmBlock["latest_finalized_block"]; ok { 362 var lfb blockHeader 363 err := json.Unmarshal([]byte(lfbRaw), &lfb) 364 if err != nil { 365 logging.Error("round info parse error.", err) 366 return false 367 } 368 369 lfbBlockHeaders[lfb.Hash]++ 370 if lfbBlockHeaders[lfb.Hash] > maxLfbBlockHeader { 371 maxLfbBlockHeader = lfbBlockHeaders[lfb.Hash] 372 lfbBlockHeader = &lfb 373 } 374 } 375 376 return false 377 } 378 379 txnConfirmations[cfmBlockHeader.Hash]++ 380 if txnConfirmations[cfmBlockHeader.Hash] > maxConfirmation { 381 maxConfirmation = txnConfirmations[cfmBlockHeader.Hash] 382 383 if maxConfirmation >= numSharders { 384 confirmationBlockHeader = cfmBlockHeader 385 confirmationBlock = cfmBlock 386 387 // it is consensus by enough sharders, and latest_finalized_block is valid 388 // return true to cancel other requests 389 return true 390 } 391 } 392 393 return false 394 395 }, timeout) 396 397 if err != nil { 398 return nil, nil, lfbBlockHeader, err 399 } 400 401 if maxConfirmation == 0 { 402 return nil, nil, lfbBlockHeader, stderrors.New("zcn: transaction not found") 403 } 404 405 if maxConfirmation < numSharders { 406 return nil, nil, lfbBlockHeader, ErrInvalidConsensus 407 } 408 409 return confirmationBlockHeader, confirmationBlock, lfbBlockHeader, nil 410 } 411 412 // getFastConfirmation get txn confirmation from a random online sharder 413 func (tq *transactionQuery) getFastConfirmation(txnHash string, timeout RequestTimeout) (*blockHeader, map[string]json.RawMessage, *blockHeader, error) { 414 var confirmationBlockHeader *blockHeader 415 var confirmationBlock map[string]json.RawMessage 416 var lfbBlockHeader blockHeader 417 418 // {host}/v1/transaction/get/confirmation?hash={txnHash}&content=lfb 419 result, err := tq.fromAny(tq.buildUrl("", TXN_VERIFY_URL, txnHash, "&content=lfb"), timeout) 420 if err != nil { 421 return nil, nil, nil, err 422 } 423 424 if result.StatusCode == http.StatusOK { 425 426 err = json.Unmarshal(result.Content, &confirmationBlock) 427 if err != nil { 428 logging.Error("txn confirmation parse error", err) 429 return nil, nil, nil, err 430 } 431 432 // parse `confirmation` section as block header 433 confirmationBlockHeader, err = getBlockHeaderFromTransactionConfirmation(txnHash, confirmationBlock) 434 if err == nil { 435 return confirmationBlockHeader, confirmationBlock, nil, nil 436 } 437 438 logging.Error("txn confirmation parse header error", err) 439 440 // parse `latest_finalized_block` section 441 lfbRaw, ok := confirmationBlock["latest_finalized_block"] 442 if !ok { 443 return confirmationBlockHeader, confirmationBlock, nil, err 444 } 445 446 err = json.Unmarshal([]byte(lfbRaw), &lfbBlockHeader) 447 if err == nil { 448 return confirmationBlockHeader, confirmationBlock, &lfbBlockHeader, ErrTransactionNotConfirmed 449 } 450 451 logging.Error("round info parse error.", err) 452 return nil, nil, nil, err 453 454 } 455 456 return nil, nil, nil, thrown.Throw(ErrTransactionNotFound, strconv.Itoa(result.StatusCode)) 457 } 458 459 func GetInfoFromSharders(urlSuffix string, op int, cb GetInfoCallback) { 460 461 tq, err := newTransactionQuery(util.Shuffle(Sharders.Healthy())) 462 if err != nil { 463 cb.OnInfoAvailable(op, StatusError, "", err.Error()) 464 return 465 } 466 467 qr, err := tq.getInfo(urlSuffix, nil) 468 if err != nil { 469 cb.OnInfoAvailable(op, StatusError, "", err.Error()) 470 return 471 } 472 473 cb.OnInfoAvailable(op, StatusSuccess, string(qr.Content), "") 474 } 475 476 func GetInfoFromAnySharder(urlSuffix string, op int, cb GetInfoCallback) { 477 478 tq, err := newTransactionQuery(util.Shuffle(Sharders.Healthy())) 479 if err != nil { 480 cb.OnInfoAvailable(op, StatusError, "", err.Error()) 481 return 482 } 483 484 qr, err := tq.fromAny(urlSuffix, nil) 485 if err != nil { 486 cb.OnInfoAvailable(op, StatusError, "", err.Error()) 487 return 488 } 489 490 cb.OnInfoAvailable(op, StatusSuccess, string(qr.Content), "") 491 }