github.com/0chain/gosdk@v1.17.11/zcncore/transaction_query.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 "net/http" 12 "strconv" 13 "strings" 14 "sync" 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 ErrNoAvailableMiners = errors.New("zcn: no available miners") 33 ) 34 35 const ( 36 SharderEndpointHealthCheck = "/v1/healthcheck" 37 ) 38 39 type QueryResult struct { 40 Content []byte 41 StatusCode int 42 Error error 43 } 44 45 // QueryResultHandle handle query response, return true if it is a consensus-result 46 type QueryResultHandle func(result QueryResult) bool 47 48 type TransactionQuery struct { 49 sync.RWMutex 50 max int 51 sharders []string 52 miners []string 53 numShardersToBatch int 54 55 selected map[string]interface{} 56 offline map[string]interface{} 57 } 58 59 func NewTransactionQuery(sharders []string, miners []string) (*TransactionQuery, error) { 60 61 if len(sharders) == 0 { 62 return nil, ErrNoAvailableSharders 63 } 64 65 tq := &TransactionQuery{ 66 max: len(sharders), 67 sharders: sharders, 68 numShardersToBatch: 3, 69 } 70 tq.selected = make(map[string]interface{}) 71 tq.offline = make(map[string]interface{}) 72 73 return tq, nil 74 } 75 76 func (tq *TransactionQuery) Reset() { 77 tq.selected = make(map[string]interface{}) 78 tq.offline = make(map[string]interface{}) 79 } 80 81 // validate validate data and input 82 func (tq *TransactionQuery) validate(num int) error { 83 if tq == nil || tq.max == 0 { 84 return ErrNoAvailableSharders 85 } 86 87 if num < 1 { 88 return ErrInvalidNumSharder 89 } 90 91 if num > tq.max { 92 return ErrNoEnoughSharders 93 } 94 95 if num > (tq.max - len(tq.offline)) { 96 return ErrNoEnoughOnlineSharders 97 } 98 99 return nil 100 101 } 102 103 // buildUrl build url with host and parts 104 func (tq *TransactionQuery) buildUrl(host string, parts ...string) string { 105 var sb strings.Builder 106 107 sb.WriteString(strings.TrimSuffix(host, "/")) 108 109 for _, it := range parts { 110 sb.WriteString(it) 111 } 112 113 return sb.String() 114 } 115 116 // checkSharderHealth checks the health of a sharder (denoted by host) and returns if it is healthy 117 // or ErrNoOnlineSharders if no sharders are healthy/up at the moment. 118 func (tq *TransactionQuery) checkSharderHealth(ctx context.Context, host string) error { 119 tq.RLock() 120 _, ok := tq.offline[host] 121 tq.RUnlock() 122 if ok { 123 return ErrSharderOffline 124 } 125 126 // check health 127 ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 128 defer cancel() 129 r := resty.New() 130 requestUrl := tq.buildUrl(host, SharderEndpointHealthCheck) 131 logging.Info("zcn: check health ", requestUrl) 132 r.DoGet(ctx, requestUrl) 133 r.Then(func(req *http.Request, resp *http.Response, respBody []byte, cf context.CancelFunc, err error) error { 134 if err != nil { 135 return err 136 } 137 138 // 5xx: it is a server error, not client error 139 if resp.StatusCode >= http.StatusInternalServerError { 140 return thrown.Throw(ErrSharderOffline, resp.Status) 141 } 142 143 return nil 144 }) 145 errs := r.Wait() 146 147 if len(errs) > 0 { 148 if errors.Is(errs[0], context.DeadlineExceeded) { 149 return context.DeadlineExceeded 150 } 151 tq.Lock() 152 tq.offline[host] = true 153 tq.Unlock() 154 return ErrSharderOffline 155 } 156 return nil 157 } 158 159 // getRandomSharder returns a random healthy sharder 160 func (tq *TransactionQuery) getRandomSharder(ctx context.Context) (string, error) { 161 if tq.sharders == nil || len(tq.sharders) == 0 { 162 return "", ErrNoAvailableMiners 163 } 164 165 shuffledSharders := util.Shuffle(tq.sharders) 166 167 return shuffledSharders[0], nil 168 } 169 170 //nolint:unused 171 func (tq *TransactionQuery) getRandomSharderWithHealthcheck(ctx context.Context) (string, error) { 172 ctx, cancel := context.WithCancel(ctx) 173 defer cancel() 174 shuffledSharders := util.Shuffle(tq.sharders) 175 for i := 0; i < len(shuffledSharders); i += tq.numShardersToBatch { 176 var mu sync.Mutex 177 done := false 178 errCh := make(chan error, tq.numShardersToBatch) 179 successCh := make(chan string) 180 last := i + tq.numShardersToBatch - 1 181 182 if last > len(shuffledSharders)-1 { 183 last = len(shuffledSharders) - 1 184 } 185 numShardersOffline := 0 186 for j := i; j <= last; j++ { 187 sharder := shuffledSharders[j] 188 go func(sharder string) { 189 err := tq.checkSharderHealth(ctx, sharder) 190 if err != nil { 191 errCh <- err 192 } else { 193 mu.Lock() 194 if !done { 195 successCh <- sharder 196 done = true 197 } 198 mu.Unlock() 199 } 200 }(sharder) 201 } 202 innerLoop: 203 for { 204 select { 205 case e := <-errCh: 206 switch e { 207 case ErrSharderOffline: 208 tq.RLock() 209 if len(tq.offline) >= tq.max { 210 tq.RUnlock() 211 return "", ErrNoOnlineSharders 212 } 213 tq.RUnlock() 214 numShardersOffline++ 215 if numShardersOffline >= tq.numShardersToBatch { 216 break innerLoop 217 } 218 case context.DeadlineExceeded: 219 return "", e 220 } 221 case s := <-successCh: 222 return s, nil 223 case <-ctx.Done(): 224 if ctx.Err() == context.DeadlineExceeded { 225 return "", context.DeadlineExceeded 226 } 227 } 228 } 229 } 230 return "", ErrNoOnlineSharders 231 } 232 233 //getRandomMiner returns a random miner 234 func (tq *TransactionQuery) getRandomMiner(ctx context.Context) (string, error) { 235 236 if tq.miners == nil || len(tq.miners) == 0 { 237 return "", ErrNoAvailableMiners 238 } 239 240 shuffledMiners := util.Shuffle(tq.miners) 241 242 return shuffledMiners[0], nil 243 } 244 245 // FromAll query transaction from all sharders whatever it is selected or offline in previous queires, and return consensus result 246 func (tq *TransactionQuery) FromAll(ctx context.Context, query string, handle QueryResultHandle) error { 247 if tq == nil || tq.max == 0 { 248 return ErrNoAvailableSharders 249 } 250 251 urls := make([]string, 0, tq.max) 252 for _, host := range tq.sharders { 253 urls = append(urls, tq.buildUrl(host, query)) 254 } 255 256 r := resty.New() 257 r.DoGet(ctx, urls...). 258 Then(func(req *http.Request, resp *http.Response, respBody []byte, cf context.CancelFunc, err error) error { 259 res := QueryResult{ 260 Content: respBody, 261 Error: err, 262 StatusCode: http.StatusBadRequest, 263 } 264 265 if resp != nil { 266 res.StatusCode = resp.StatusCode 267 268 logging.Debug(req.URL.String() + " " + resp.Status) 269 logging.Debug(string(respBody)) 270 } else { 271 logging.Debug(req.URL.String()) 272 273 } 274 275 if handle != nil { 276 if handle(res) { 277 278 cf() 279 } 280 } 281 282 return nil 283 }) 284 285 r.Wait() 286 287 return nil 288 } 289 290 func (tq *TransactionQuery) GetInfo(ctx context.Context, query string) (*QueryResult, error) { 291 292 consensuses := make(map[int]int) 293 var maxConsensus int 294 var consensusesResp QueryResult 295 // {host}{query} 296 err := tq.FromAll(ctx, query, 297 func(qr QueryResult) bool { 298 //ignore response if it is network error 299 if qr.StatusCode >= 500 { 300 return false 301 } 302 303 consensuses[qr.StatusCode]++ 304 if consensuses[qr.StatusCode] > maxConsensus { 305 maxConsensus = consensuses[qr.StatusCode] 306 consensusesResp = qr 307 } 308 309 // If number of 200's is equal to number of some other status codes, use 200's. 310 if qr.StatusCode == http.StatusOK && consensuses[qr.StatusCode] == maxConsensus { 311 maxConsensus = consensuses[qr.StatusCode] 312 consensusesResp = qr 313 } 314 315 return false 316 317 }) 318 319 if err != nil { 320 return nil, err 321 } 322 323 if maxConsensus == 0 { 324 return nil, stderrors.New("zcn: query not found") 325 } 326 327 rate := maxConsensus * 100 / tq.max 328 if rate < consensusThresh { 329 return nil, ErrInvalidConsensus 330 } 331 332 if consensusesResp.StatusCode != http.StatusOK { 333 return nil, stderrors.New(string(consensusesResp.Content)) 334 } 335 336 return &consensusesResp, nil 337 } 338 339 // FromAny queries transaction from any sharder that is not selected in previous queries. 340 // use any used sharder if there is not any unused sharder 341 func (tq *TransactionQuery) FromAny(ctx context.Context, query string, provider Provider) (QueryResult, error) { 342 343 res := QueryResult{ 344 StatusCode: http.StatusBadRequest, 345 } 346 347 err := tq.validate(1) 348 349 if err != nil { 350 return res, err 351 } 352 353 var host string 354 355 // host, err := tq.getRandomSharder(ctx) 356 357 switch provider { 358 case ProviderMiner: 359 host, err = tq.getRandomMiner(ctx) 360 case ProviderSharder: 361 host, err = tq.getRandomSharder(ctx) 362 } 363 364 if err != nil { 365 return res, err 366 } 367 368 r := resty.New() 369 requestUrl := tq.buildUrl(host, query) 370 371 logging.Debug("GET", requestUrl) 372 373 r.DoGet(ctx, requestUrl). 374 Then(func(req *http.Request, resp *http.Response, respBody []byte, cf context.CancelFunc, err error) error { 375 res.Error = err 376 if err != nil { 377 return err 378 } 379 380 res.Content = respBody 381 logging.Debug(string(respBody)) 382 383 if resp != nil { 384 res.StatusCode = resp.StatusCode 385 } 386 387 return nil 388 }) 389 390 errs := r.Wait() 391 392 if len(errs) > 0 { 393 return res, errs[0] 394 } 395 396 return res, nil 397 398 } 399 400 func (tq *TransactionQuery) getConsensusConfirmation(ctx context.Context, numSharders int, txnHash string) (*blockHeader, map[string]json.RawMessage, *blockHeader, error) { 401 maxConfirmation := int(0) 402 txnConfirmations := make(map[string]int) 403 var confirmationBlockHeader *blockHeader 404 var confirmationBlock map[string]json.RawMessage 405 var lfbBlockHeader *blockHeader 406 maxLfbBlockHeader := int(0) 407 lfbBlockHeaders := make(map[string]int) 408 409 // {host}/v1/transaction/get/confirmation?hash={txnHash}&content=lfb 410 err := tq.FromAll(ctx, 411 tq.buildUrl("", TXN_VERIFY_URL, txnHash, "&content=lfb"), 412 func(qr QueryResult) bool { 413 if qr.StatusCode != http.StatusOK { 414 return false 415 } 416 417 var cfmBlock map[string]json.RawMessage 418 err := json.Unmarshal([]byte(qr.Content), &cfmBlock) 419 if err != nil { 420 logging.Error("txn confirmation parse error", err) 421 return false 422 } 423 424 // parse `confirmation` section as block header 425 cfmBlockHeader, err := getBlockHeaderFromTransactionConfirmation(txnHash, cfmBlock) 426 if err != nil { 427 logging.Error("txn confirmation parse header error", err) 428 429 // parse `latest_finalized_block` section 430 if lfbRaw, ok := cfmBlock["latest_finalized_block"]; ok { 431 var lfb blockHeader 432 err := json.Unmarshal([]byte(lfbRaw), &lfb) 433 if err != nil { 434 logging.Error("round info parse error.", err) 435 return false 436 } 437 438 lfbBlockHeaders[lfb.Hash]++ 439 if lfbBlockHeaders[lfb.Hash] > maxLfbBlockHeader { 440 maxLfbBlockHeader = lfbBlockHeaders[lfb.Hash] 441 lfbBlockHeader = &lfb 442 } 443 } 444 445 return false 446 } 447 448 txnConfirmations[cfmBlockHeader.Hash]++ 449 if txnConfirmations[cfmBlockHeader.Hash] > maxConfirmation { 450 maxConfirmation = txnConfirmations[cfmBlockHeader.Hash] 451 452 if maxConfirmation >= numSharders { 453 confirmationBlockHeader = cfmBlockHeader 454 confirmationBlock = cfmBlock 455 456 // it is consensus by enough sharders, and latest_finalized_block is valid 457 // return true to cancel other requests 458 return true 459 } 460 } 461 462 return false 463 464 }) 465 466 if err != nil { 467 return nil, nil, lfbBlockHeader, err 468 } 469 470 if maxConfirmation == 0 { 471 return nil, nil, lfbBlockHeader, stderrors.New("zcn: transaction not found") 472 } 473 474 if maxConfirmation < numSharders { 475 return nil, nil, lfbBlockHeader, ErrInvalidConsensus 476 } 477 478 return confirmationBlockHeader, confirmationBlock, lfbBlockHeader, nil 479 } 480 481 // getFastConfirmation get txn confirmation from a random online sharder 482 func (tq *TransactionQuery) getFastConfirmation(ctx context.Context, txnHash string) (*blockHeader, map[string]json.RawMessage, *blockHeader, error) { 483 var confirmationBlockHeader *blockHeader 484 var confirmationBlock map[string]json.RawMessage 485 var lfbBlockHeader blockHeader 486 487 // {host}/v1/transaction/get/confirmation?hash={txnHash}&content=lfb 488 result, err := tq.FromAny(ctx, tq.buildUrl("", TXN_VERIFY_URL, txnHash, "&content=lfb"), ProviderSharder) 489 if err != nil { 490 return nil, nil, nil, err 491 } 492 493 if result.StatusCode == http.StatusOK { 494 495 err = json.Unmarshal(result.Content, &confirmationBlock) 496 if err != nil { 497 logging.Error("txn confirmation parse error", err) 498 return nil, nil, nil, err 499 } 500 501 // parse `confirmation` section as block header 502 confirmationBlockHeader, err = getBlockHeaderFromTransactionConfirmation(txnHash, confirmationBlock) 503 if err == nil { 504 return confirmationBlockHeader, confirmationBlock, nil, nil 505 } 506 507 logging.Error("txn confirmation parse header error", err) 508 509 // parse `latest_finalized_block` section 510 lfbRaw, ok := confirmationBlock["latest_finalized_block"] 511 if !ok { 512 return confirmationBlockHeader, confirmationBlock, nil, err 513 } 514 515 err = json.Unmarshal([]byte(lfbRaw), &lfbBlockHeader) 516 if err == nil { 517 return confirmationBlockHeader, confirmationBlock, &lfbBlockHeader, ErrTransactionNotConfirmed 518 } 519 520 logging.Error("round info parse error.", err) 521 return nil, nil, nil, err 522 523 } 524 525 return nil, nil, nil, thrown.Throw(ErrTransactionNotFound, strconv.Itoa(result.StatusCode)) 526 } 527 528 func GetInfoFromSharders(urlSuffix string, op int, cb GetInfoCallback) { 529 530 tq, err := NewTransactionQuery(util.Shuffle(Sharders.Healthy()), []string{}) 531 if err != nil { 532 cb.OnInfoAvailable(op, StatusError, "", err.Error()) 533 return 534 } 535 536 qr, err := tq.GetInfo(context.TODO(), urlSuffix) 537 if err != nil { 538 if qr != nil && op == OpGetMintNonce { 539 logging.Debug("OpGetMintNonce QueryResult error", "; Content = ", qr.Content, "; Error = ", qr.Error.Error(), "; StatusCode = ", qr.StatusCode) 540 cb.OnInfoAvailable(op, qr.StatusCode, "", qr.Error.Error()) 541 return 542 } 543 cb.OnInfoAvailable(op, StatusError, "", err.Error()) 544 return 545 } 546 547 cb.OnInfoAvailable(op, StatusSuccess, string(qr.Content), "") 548 } 549 550 func GetInfoFromAnySharder(urlSuffix string, op int, cb GetInfoCallback) { 551 552 tq, err := NewTransactionQuery(util.Shuffle(Sharders.Healthy()), []string{}) 553 if err != nil { 554 cb.OnInfoAvailable(op, StatusError, "", err.Error()) 555 return 556 } 557 558 qr, err := tq.FromAny(context.TODO(), urlSuffix, ProviderSharder) 559 if err != nil { 560 cb.OnInfoAvailable(op, StatusError, "", err.Error()) 561 return 562 } 563 564 cb.OnInfoAvailable(op, StatusSuccess, string(qr.Content), "") 565 } 566 567 func GetInfoFromAnyMiner(urlSuffix string, op int, cb getInfoCallback) { 568 569 tq, err := NewTransactionQuery([]string{}, util.Shuffle(_config.chain.Miners)) 570 571 if err != nil { 572 cb.OnInfoAvailable(op, StatusError, "", err.Error()) 573 return 574 } 575 qr, err := tq.FromAny(context.TODO(), urlSuffix, ProviderMiner) 576 577 if err != nil { 578 cb.OnInfoAvailable(op, StatusError, "", err.Error()) 579 return 580 } 581 cb.OnInfoAvailable(op, StatusSuccess, string(qr.Content), "") 582 } 583 584 func GetEvents(cb GetInfoCallback, filters map[string]string) (err error) { 585 if err = CheckConfig(); err != nil { 586 return 587 } 588 go GetInfoFromSharders(WithParams(GET_MINERSC_EVENTS, Params{ 589 "block_number": filters["block_number"], 590 "tx_hash": filters["tx_hash"], 591 "type": filters["type"], 592 "tag": filters["tag"], 593 }), 0, cb) 594 return 595 } 596 597 func WithParams(uri string, params Params) string { 598 return withParams(uri, params) 599 }