github.com/0chain/gosdk@v1.17.11/core/transaction/utils.go (about)

     1  package transaction
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"math"
     8  	"net/http"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/0chain/common/core/encryption"
    14  	"github.com/0chain/errors"
    15  	"github.com/0chain/gosdk/core/conf"
    16  	"github.com/0chain/gosdk/core/resty"
    17  	"github.com/0chain/gosdk/core/util"
    18  )
    19  
    20  const retriesCount = 30
    21  
    22  type OptimisticVerifier struct {
    23  	allSharders []string
    24  	sharders    []string
    25  	options     []resty.Option
    26  }
    27  
    28  func NewOptimisticVerifier(sharders []string) *OptimisticVerifier {
    29  	//initialize resty
    30  	header := map[string]string{
    31  		"Content-Type":                "application/json; charset=utf-8",
    32  		"Access-Control-Allow-Origin": "*",
    33  	}
    34  
    35  	transport := createTransport(resty.DefaultDialTimeout)
    36  
    37  	options := []resty.Option{
    38  		resty.WithRetry(resty.DefaultRetry),
    39  		resty.WithHeader(header),
    40  		resty.WithTransport(transport),
    41  	}
    42  
    43  	return &OptimisticVerifier{
    44  		allSharders: sharders,
    45  		options:     options,
    46  	}
    47  }
    48  
    49  func (v *OptimisticVerifier) VerifyTransactionOptimistic(txnHash string) (*Transaction, error) {
    50  	cfg, err := conf.GetClientConfig()
    51  	if err != nil {
    52  
    53  		return nil, err
    54  	}
    55  
    56  	//refresh sharders
    57  	v.sharders = v.allSharders
    58  
    59  	//amount of sharders to query
    60  	minNumConfirmation := int(math.Ceil(float64(cfg.MinConfirmation*len(v.sharders)) / 100))
    61  	if minNumConfirmation > len(v.sharders) {
    62  		return nil, errors.New("verify_optimistic", "wrong number of min_confirmations")
    63  	}
    64  	shuffled := util.Shuffle(v.sharders)[:minNumConfirmation]
    65  
    66  	//prepare urls for confirmation request
    67  	urls := make([]string, 0, len(shuffled))
    68  	mappedSharders := make(map[string]string)
    69  	for _, sharder := range shuffled {
    70  		url := fmt.Sprintf("%v/%v%v", sharder, TXN_VERIFY_URL, txnHash)
    71  		urls = append(urls, url)
    72  		mappedSharders[url] = sharder
    73  	}
    74  
    75  	var url string
    76  	var chain []*RoundBlockHeader
    77  	var txn *Transaction
    78  	r := resty.New(v.options...).Then(func(req *http.Request, resp *http.Response, respBody []byte, cf context.CancelFunc, err error) error {
    79  		if err != nil { //network issue
    80  			return err
    81  		}
    82  
    83  		if resp.StatusCode != 200 {
    84  			return errors.Throw(ErrInvalidRequest, strconv.Itoa(resp.StatusCode)+": "+resp.Status)
    85  		}
    86  
    87  		//parse response
    88  		var objmap map[string]json.RawMessage
    89  		err = json.Unmarshal(respBody, &objmap)
    90  		if err != nil {
    91  			return err
    92  		}
    93  		txnRawJSON, ok := objmap["txn"]
    94  		// txn data is found, success
    95  		if !ok {
    96  			return errors.New("handle_response", "bad transaction response")
    97  		}
    98  		merklePathRawJSON, ok := objmap["merkle_tree_path"]
    99  		if !ok {
   100  			return errors.New("handle_response", "bad merkle_tree_path response")
   101  		}
   102  
   103  		txn = &Transaction{}
   104  		err = json.Unmarshal(txnRawJSON, txn)
   105  		if err != nil {
   106  			return err
   107  		}
   108  
   109  		b := &RoundBlockHeader{}
   110  		err = json.Unmarshal(respBody, b)
   111  		if err != nil {
   112  			return err
   113  		}
   114  		err = validateBlockHash(b)
   115  		if err != nil {
   116  			return err
   117  		}
   118  
   119  		err = verifyMerklePath(merklePathRawJSON, txn.Hash, b.MerkleTreeRoot)
   120  		if err != nil {
   121  			return err
   122  		}
   123  
   124  		url = req.URL.String()
   125  		chain = append(chain, b)
   126  		return nil
   127  	})
   128  
   129  	retries := 0
   130  	ticker := time.NewTicker(time.Second)
   131  L:
   132  	//loop query confirmation
   133  	for retries < retriesCount {
   134  		<-ticker.C
   135  		retries++
   136  		r.DoGet(context.TODO(), urls...)
   137  		//need single valid confirmation
   138  		errs := r.First()
   139  		if len(errs) == 0 {
   140  			break L
   141  		}
   142  	}
   143  
   144  	if len(chain) == 0 {
   145  		return nil, errors.Newf("verify", "can't get confirmation after %v retries", retriesCount)
   146  	}
   147  
   148  	//remove current sharder from the list to avoid building chain with it
   149  	toDelete := mappedSharders[url]
   150  	for i, s := range v.sharders {
   151  		if s == toDelete {
   152  			v.sharders = append(v.sharders[:i], v.sharders[i+1:]...)
   153  			break
   154  		}
   155  	}
   156  
   157  	err = v.checkConfirmation(chain)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	return txn, err
   163  }
   164  
   165  func (v *OptimisticVerifier) checkConfirmation(chain []*RoundBlockHeader) error {
   166  	cfg, err := conf.GetClientConfig()
   167  	if err != nil {
   168  
   169  		return err
   170  	}
   171  
   172  	//build blockchain starting from confirmation block
   173  	curRound := chain[0].Round
   174  	rb := resty.New(v.options...).Then(func(req *http.Request, resp *http.Response, respBody []byte, cf context.CancelFunc, err error) error {
   175  		if err != nil { //network issue
   176  			return err
   177  		}
   178  
   179  		if resp.StatusCode != 200 {
   180  			return errors.Throw(ErrInvalidRequest, strconv.Itoa(resp.StatusCode)+": "+resp.Status)
   181  		}
   182  
   183  		curBlock := &Block{}
   184  		err = json.Unmarshal(respBody, &curBlock)
   185  		if err != nil {
   186  			return err
   187  		}
   188  
   189  		//get tail block and check that current extends it
   190  		prevBlock := chain[len(chain)-1]
   191  		if prevBlock.Hash == curBlock.PrevHash && prevBlock.Round+1 == curBlock.Round {
   192  			blockHeader := &RoundBlockHeader{
   193  				Version:               curBlock.Version,
   194  				CreationDate:          curBlock.CreationDate,
   195  				Hash:                  curBlock.Hash,
   196  				PreviousBlockHash:     curBlock.PrevHash,
   197  				MinerID:               curBlock.MinerID,
   198  				Round:                 curBlock.Round,
   199  				RoundRandomSeed:       curBlock.RoundRandomSeed,
   200  				MerkleTreeRoot:        curBlock.MerkleTreeRoot,
   201  				StateChangesCount:     curBlock.StateChangesCount,
   202  				StateHash:             curBlock.StateHash,
   203  				ReceiptMerkleTreeRoot: curBlock.ReceiptMerkleTreeRoot,
   204  				NumberOfTxns:          int64(curBlock.NumTxns),
   205  			}
   206  			err = validateBlockHash(blockHeader)
   207  			if err != nil {
   208  				return err
   209  			}
   210  
   211  			chain = append(chain, blockHeader)
   212  			return nil
   213  		}
   214  		return errors.New("get_block", "wrong block")
   215  	})
   216  
   217  	//query for blocks until ConfirmationChainLength is built or every sharder is queried
   218  	for len(chain) < cfg.ConfirmationChainLength && len(v.sharders) > 0 {
   219  		//for every new block create sharder list to query
   220  		rand := util.NewRand(len(v.sharders))
   221  		//iterate through all sharders sequentially to get next block
   222  		for {
   223  			next, err := rand.Next()
   224  			if err != nil {
   225  				return errors.New("get_round_block", "can't get round block, blockchain might be stuck")
   226  			}
   227  
   228  			cur := v.sharders[next]
   229  			burl := fmt.Sprintf("%v/%v%v", cur, BLOCK_BY_ROUND_URL, curRound+1)
   230  			rb.DoGet(context.TODO(), burl)
   231  
   232  			wait := rb.Wait()
   233  			if len(wait) != 0 {
   234  				continue
   235  			}
   236  			//exclude sharder if it gave block, we do it to avoid building blockchain from single sharder
   237  			v.sharders = append(v.sharders[:next], v.sharders[next+1:]...)
   238  			curRound++
   239  			break
   240  		}
   241  	}
   242  
   243  	return nil
   244  }
   245  
   246  func verifyMerklePath(merklePathRawJSON json.RawMessage, txnHash string, merkleRoot string) error {
   247  	merklePath := &util.MTPath{}
   248  	err := json.Unmarshal(merklePathRawJSON, merklePath)
   249  	if err != nil {
   250  		return err
   251  	}
   252  	if !util.VerifyMerklePath(txnHash, merklePath, merkleRoot) {
   253  		return errors.New("handle_response", "invalid merkle path")
   254  	}
   255  	return nil
   256  }
   257  
   258  func validateBlockHash(b *RoundBlockHeader) error {
   259  	hashBuilder := strings.Builder{}
   260  	hashBuilder.WriteString(b.MinerID)
   261  	hashBuilder.WriteString(":")
   262  	hashBuilder.WriteString(b.PreviousBlockHash)
   263  	hashBuilder.WriteString(":")
   264  	hashBuilder.WriteString(strconv.FormatInt(b.CreationDate, 10))
   265  	hashBuilder.WriteString(":")
   266  	hashBuilder.WriteString(strconv.FormatInt(b.Round, 10))
   267  	hashBuilder.WriteString(":")
   268  	hashBuilder.WriteString(strconv.FormatInt(b.RoundRandomSeed, 10))
   269  	hashBuilder.WriteString(":")
   270  	hashBuilder.WriteString(strconv.Itoa(b.StateChangesCount))
   271  	hashBuilder.WriteString(":")
   272  	hashBuilder.WriteString(b.MerkleTreeRoot)
   273  	hashBuilder.WriteString(":")
   274  	hashBuilder.WriteString(b.ReceiptMerkleTreeRoot)
   275  	//todo handling of magic block here
   276  	hash := encryption.Hash(hashBuilder.String())
   277  	if hash != b.Hash {
   278  		return errors.New("handle_response", "invalid block hash")
   279  	}
   280  	return nil
   281  }
   282  
   283  // VerifyTransaction query transaction status from sharders, and verify it by mininal confirmation
   284  func VerifyTransaction(txnHash string, sharders []string) (*Transaction, error) {
   285  	cfg, err := conf.GetClientConfig()
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  
   290  	if cfg.VerifyOptimistic {
   291  		ov := NewOptimisticVerifier(sharders)
   292  		return ov.VerifyTransactionOptimistic(txnHash)
   293  	} else {
   294  		return VerifyTransactionTrusted(txnHash, sharders)
   295  	}
   296  }
   297  
   298  // VerifyTransaction query transaction status from sharders, and verify it by mininal confirmation
   299  func VerifyTransactionTrusted(txnHash string, sharders []string) (*Transaction, error) {
   300  
   301  	cfg, err := conf.GetClientConfig()
   302  	if err != nil {
   303  
   304  		return nil, err
   305  	}
   306  
   307  	numSharders := len(sharders)
   308  
   309  	if numSharders == 0 {
   310  		return nil, ErrNoAvailableSharder
   311  	}
   312  
   313  	minNumConfirmation := int(math.Ceil(float64(cfg.MinConfirmation*numSharders) / 100))
   314  
   315  	rand := util.NewRand(numSharders)
   316  
   317  	selectedSharders := make([]string, 0, minNumConfirmation+1)
   318  
   319  	// random pick minNumConfirmation+1 first
   320  	for i := 0; i <= minNumConfirmation; i++ {
   321  		n, err := rand.Next()
   322  
   323  		if err != nil {
   324  			break
   325  		}
   326  
   327  		selectedSharders = append(selectedSharders, sharders[n])
   328  	}
   329  
   330  	numSuccess := 0
   331  
   332  	var retTxn *Transaction
   333  
   334  	//leave first item for ErrTooLessConfirmation
   335  	var msgList = make([]string, 1, numSharders)
   336  
   337  	urls := make([]string, 0, len(selectedSharders))
   338  
   339  	for _, sharder := range selectedSharders {
   340  		urls = append(urls, fmt.Sprintf("%v/%v%v", sharder, TXN_VERIFY_URL, txnHash))
   341  	}
   342  
   343  	header := map[string]string{
   344  		"Content-Type":                "application/json; charset=utf-8",
   345  		"Access-Control-Allow-Origin": "*",
   346  	}
   347  
   348  	transport := createTransport(resty.DefaultDialTimeout)
   349  
   350  	options := []resty.Option{
   351  		resty.WithRetry(resty.DefaultRetry),
   352  		resty.WithHeader(header),
   353  		resty.WithTransport(transport),
   354  	}
   355  
   356  	r := resty.New(options...).
   357  		Then(func(req *http.Request, resp *http.Response, respBody []byte, cf context.CancelFunc, err error) error {
   358  			url := req.URL.String()
   359  
   360  			if err != nil { //network issue
   361  				msgList = append(msgList, err.Error())
   362  				return err
   363  			}
   364  
   365  			if resp.StatusCode != 200 {
   366  				msgList = append(msgList, url+": ["+strconv.Itoa(resp.StatusCode)+"] "+string(respBody))
   367  				return errors.Throw(ErrInvalidRequest, strconv.Itoa(resp.StatusCode)+": "+resp.Status)
   368  			}
   369  
   370  			var objmap map[string]json.RawMessage
   371  			err = json.Unmarshal(respBody, &objmap)
   372  			if err != nil {
   373  				msgList = append(msgList, "json: "+string(respBody))
   374  				return err
   375  			}
   376  			txnRawJSON, ok := objmap["txn"]
   377  
   378  			// txn data is found, success
   379  			if ok {
   380  				txn := &Transaction{}
   381  				err = json.Unmarshal(txnRawJSON, txn)
   382  				if err != nil {
   383  					msgList = append(msgList, "json: "+string(txnRawJSON))
   384  					return err
   385  				}
   386  				if len(txn.Signature) > 0 {
   387  					retTxn = txn
   388  				}
   389  				numSuccess++
   390  
   391  			} else {
   392  				// txn data is not found, but get block_hash, success
   393  				if _, ok := objmap["block_hash"]; ok {
   394  					numSuccess++
   395  				} else {
   396  					// txn and block_hash
   397  					msgList = append(msgList, fmt.Sprintf("Sharder does not have the block summary with url: %s, contents: %s", url, string(respBody)))
   398  				}
   399  
   400  			}
   401  
   402  			return nil
   403  		})
   404  
   405  	for {
   406  		r.DoGet(context.TODO(), urls...)
   407  
   408  		r.Wait()
   409  
   410  		if numSuccess >= minNumConfirmation {
   411  			break
   412  		}
   413  
   414  		// pick one more sharder to query transaction
   415  		n, err := rand.Next()
   416  
   417  		if errors.Is(err, util.ErrNoItem) {
   418  			break
   419  		}
   420  
   421  		urls = []string{fmt.Sprintf("%v/%v%v", sharders[n], TXN_VERIFY_URL, txnHash)}
   422  
   423  	}
   424  
   425  	if numSuccess > 0 && numSuccess >= minNumConfirmation {
   426  		if retTxn == nil {
   427  			return nil, errors.Throw(ErrNoTxnDetail, strings.Join(msgList, "\r\n"))
   428  		}
   429  		return retTxn, nil
   430  	}
   431  
   432  	msgList[0] = fmt.Sprintf("min_confirmation is %v%%, but got %v/%v sharders", cfg.MinConfirmation, numSuccess, numSharders)
   433  	return nil, errors.Throw(ErrTooLessConfirmation, strings.Join(msgList, "\r\n"))
   434  
   435  }