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  }