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 }