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  }