github.com/deso-protocol/core@v1.2.9/lib/bitcoin_burner.go (about) 1 package lib 2 3 import ( 4 "bytes" 5 "encoding/hex" 6 "encoding/json" 7 "fmt" 8 "github.com/davecgh/go-spew/spew" 9 "io/ioutil" 10 "math" 11 "net/http" 12 "strings" 13 "time" 14 15 "github.com/golang/glog" 16 "github.com/pkg/errors" 17 18 "github.com/btcsuite/btcd/btcec" 19 "github.com/btcsuite/btcd/chaincfg/chainhash" 20 "github.com/btcsuite/btcd/txscript" 21 "github.com/btcsuite/btcd/wire" 22 "github.com/btcsuite/btcutil" 23 ) 24 25 // bitcoin_burner.go finds the Bitcoin UTXOs associated with a Bitcoin 26 // address and constructs a burn transaction on behalf of the user. Note that the 27 // use of an API here is strictly cosmetic, and that none of this 28 // logic is used for actually validating anything (that is all done by bitcoin_manager.go, 29 // which relies on Bitcoin peers and is fully decentralized). The user can 30 // also simply use an existing Bitcoin wallet to send Bitcoin to the burn address 31 // rather than this utility, but that is slightly less convenient than just 32 // baking this functionality in, which is why we do it. 33 // 34 // TODO: If this API doesn't work in the long run, I think the ElectrumX network would 35 // be a pretty good alternative. 36 37 type BitcoinUtxo struct { 38 TxID *chainhash.Hash 39 Index int64 40 AmountSatoshis int64 41 } 42 43 func _estimateBitcoinTxSize(numInputs int, numOutputs int) int { 44 return numInputs*180 + numOutputs*34 45 } 46 47 func EstimateBitcoinTxFee( 48 numInputs int, numOutputs int, feeRateSatoshisPerKB uint64) uint64 { 49 50 return uint64(_estimateBitcoinTxSize(numInputs, numOutputs)) * feeRateSatoshisPerKB / 1000 51 } 52 53 func CreateUnsignedBitcoinSpendTransaction( 54 spendAmountSatoshis uint64, 55 feeRateSatoshisPerKB uint64, 56 spendAddrString string, 57 recipientAddrString string, 58 params *DeSoParams, 59 utxoSource func(addr string, params *DeSoParams) ([]*BitcoinUtxo, error)) ( 60 _txn *wire.MsgTx, _totalInput uint64, _fee uint64, _err error) { 61 62 // Get the utxos for the spend key. 63 bitcoinUtxos, err := utxoSource(spendAddrString, params) 64 if err != nil { 65 return nil, 0, 0, errors.Wrapf(err, "CreateUnsignedBitcoinSpendTransaction: Problem getting "+ 66 "Bitcoin utxos for Bitcoin address %s", 67 spendAddrString) 68 } 69 70 glog.V(2).Infof("CreateUnsignedBitcoinSpendTransaction: Found %d BitcoinUtxos", len(bitcoinUtxos)) 71 72 // Create the transaction we'll be returning. 73 retTxn := &wire.MsgTx{} 74 // Set locktime to 0 since we're not using it. 75 retTxn.LockTime = 0 76 // The version seems to be 2 these days. 77 retTxn.Version = 2 78 79 // Add an output sending spendAmountSatoshis to the recipientAddress. 80 // 81 // We decode it twice in order to guarantee that we're sending to an *address* 82 // rather than to a public key. 83 recipientOutput := &wire.TxOut{} 84 recipientAddrTmp, err := btcutil.DecodeAddress( 85 recipientAddrString, params.BitcoinBtcdParams) 86 if err != nil { 87 return nil, 0, 0, errors.Wrapf(err, "CreateUnsignedBitcoinSpendTransaction: Problem decoding "+ 88 "recipient address %s", recipientAddrString) 89 } 90 recipientAddr, err := btcutil.DecodeAddress( 91 recipientAddrTmp.EncodeAddress(), params.BitcoinBtcdParams) 92 if err != nil { 93 return nil, 0, 0, errors.Wrapf(err, "CreateUnsignedBitcoinSpendTransaction: Problem decoding "+ 94 "recipient address pubkeyhash %s %s", recipientAddrString, recipientAddrTmp.EncodeAddress()) 95 } 96 recipientOutput.PkScript, err = txscript.PayToAddrScript(recipientAddr) 97 if err != nil { 98 return nil, 0, 0, errors.Wrapf(err, "CreateUnsignedBitcoinSpendTransaction: Problem adding script: %v", err) 99 } 100 recipientOutput.Value = int64(spendAmountSatoshis) 101 retTxn.TxOut = append(retTxn.TxOut, recipientOutput) 102 103 // Decode the spendAddrString. 104 // 105 // We decode it twice in order to guarantee that we're sending to an *address* 106 // rather than to a public key. 107 spendAddrTmp, err := btcutil.DecodeAddress( 108 spendAddrString, params.BitcoinBtcdParams) 109 if err != nil { 110 return nil, 0, 0, errors.Wrapf(err, "CreateUnsignedBitcoinSpendTransaction: Problem decoding "+ 111 "spend address %s", spendAddrString) 112 } 113 spendAddr, err := btcutil.DecodeAddress( 114 spendAddrTmp.EncodeAddress(), params.BitcoinBtcdParams) 115 if err != nil { 116 return nil, 0, 0, errors.Wrapf(err, "CreateUnsignedBitcoinSpendTransaction: Problem decoding "+ 117 "spend address pubkeyhash %s %s", spendAddrString, spendAddrTmp.EncodeAddress()) 118 } 119 120 // Rack up the number of utxos you need to pay the spend amount. Add each 121 // one to our transaction until we have enough to cover the spendAmount 122 // plus the fee. 123 totalInputSatoshis := uint64(0) 124 for _, bUtxo := range bitcoinUtxos { 125 totalInputSatoshis += uint64(bUtxo.AmountSatoshis) 126 127 // Construct an input corresponding to this utxo. 128 txInput := &wire.TxIn{} 129 txInput.PreviousOutPoint = *wire.NewOutPoint(bUtxo.TxID, uint32(bUtxo.Index)) 130 // Set Sequence to the max value to disable locktime and opt out of RBF. 131 txInput.Sequence = math.MaxUint32 132 // Don't set the SignatureScript yet. 133 134 // Set the input on the transaction. 135 retTxn.TxIn = append(retTxn.TxIn, txInput) 136 137 // If the total input we've accrued thus far covers the spend amount and the 138 // fee precisely, then we're done. 139 txFee := EstimateBitcoinTxFee( 140 len(retTxn.TxIn), len(retTxn.TxOut), feeRateSatoshisPerKB) 141 if totalInputSatoshis == spendAmountSatoshis+txFee { 142 break 143 } 144 145 // Since our input did not precisely equal the spend amount plus the fee, 146 // see if if the input exceeds the spend amount plus the fee after we add 147 // a change output. If it does, then we're done. Note the +1 in the function 148 // call below. 149 txFeeWithChange := EstimateBitcoinTxFee( 150 len(retTxn.TxIn), len(retTxn.TxOut)+1, feeRateSatoshisPerKB) 151 if totalInputSatoshis >= spendAmountSatoshis+txFeeWithChange { 152 // In this case we add a change output to the transaction that sends the 153 // excess Bitcoin back to the spend address. 154 changeOutput := &wire.TxOut{} 155 changeOutput.PkScript, err = txscript.PayToAddrScript(spendAddr) 156 if err != nil { 157 return nil, 0, 0, errors.Wrapf(err, "CreateUnsignedBitcoinSpendTransaction: Problem adding script: %v", err) 158 } 159 160 totalOutputSatoshis := spendAmountSatoshis + txFeeWithChange 161 changeOutput.Value = int64(totalInputSatoshis - totalOutputSatoshis) 162 // Don't append a change output if it is below the dust threshold. 163 // TODO: Is 1,000 enough for the dust threshold? 164 dustOutputSatoshis := int64(1000) 165 if changeOutput.Value > dustOutputSatoshis { 166 retTxn.TxOut = append(retTxn.TxOut, changeOutput) 167 } 168 169 break 170 } 171 } 172 173 // At this point, if the totalInputSatoshis is not greater than or equal to 174 // the spend amount plus the estimated fee then we didn't have enough input 175 // to successfully form the transaction. 176 finalFee := EstimateBitcoinTxFee( 177 len(retTxn.TxIn), len(retTxn.TxOut), feeRateSatoshisPerKB) 178 if totalInputSatoshis < spendAmountSatoshis+finalFee { 179 return nil, 0, 0, fmt.Errorf("CreateUnsignedBitcoinSpendTransaction: Total input satoshis %d is "+ 180 "not sufficient to cover the spend amount %d plus the fee %d", 181 totalInputSatoshis, spendAmountSatoshis, finalFee) 182 } 183 184 return retTxn, totalInputSatoshis, finalFee, nil 185 } 186 187 func CreateBitcoinSpendTransaction( 188 spendAmountSatoshis uint64, feeRateSatoshisPerKB uint64, 189 pubKey *btcec.PublicKey, 190 recipientAddrString string, params *DeSoParams, 191 utxoSource func(addr string, params *DeSoParams) ([]*BitcoinUtxo, error)) (_txn *wire.MsgTx, _totalInput uint64, _fee uint64, _unsignedHashes []string, _err error) { 192 193 // Convert the public key into a Bitcoin address. 194 spendAddrTmp, err := btcutil.NewAddressPubKey(pubKey.SerializeCompressed(), params.BitcoinBtcdParams) 195 spendAddrr := spendAddrTmp.AddressPubKeyHash() 196 if err != nil { 197 return nil, 0, 0, nil, errors.Wrapf(err, "CreateBitcoinSpendTransaction: Problem converting "+ 198 "pubkey to address: ") 199 } 200 spendAddrString := spendAddrr.EncodeAddress() 201 202 glog.V(2).Infof("CreateBitcoinSpendTransaction: Creating spend for "+ 203 "<from: %s, to: %s> for amount %d, feeRateSatoshisPerKB %d", 204 spendAddrString, 205 recipientAddrString, spendAmountSatoshis, feeRateSatoshisPerKB) 206 207 retTxn, totalInputSatoshis, finalFee, err := CreateUnsignedBitcoinSpendTransaction( 208 spendAmountSatoshis, feeRateSatoshisPerKB, spendAddrString, 209 recipientAddrString, params, utxoSource) 210 if err != nil { 211 return nil, 0, 0, nil, errors.Wrapf(err, "CreateBitcoinSpendTransaction: Problem "+ 212 "creating unsigned Bitcoin spend: ") 213 } 214 215 // At this point we are confident the transaction has enough input to cover its 216 // outputs. Now go through and set the input scripts. 217 // 218 // All the inputs are signing an identical output script corresponding to the 219 // spend address. 220 outputScriptToSign, err := txscript.PayToAddrScript(spendAddrr) 221 if err != nil { 222 return nil, 0, 0, nil, errors.Wrapf(err, "CreateBitcoinSpendTransaction: Problem computing "+ 223 "output script for spendAddr %v", spendAddrr) 224 } 225 226 // Calculate the unsigned hash for each input 227 // We pass these to the identity service on the frontend for the client to sign 228 unsignedHashes := []string{} 229 for ii := range retTxn.TxIn { 230 hashBytes, err := txscript.CalcSignatureHash(outputScriptToSign, txscript.SigHashAll, retTxn, ii) 231 if err != nil { 232 return nil, 0, 0, nil, errors.Wrapf(err, "CreateBitcoinSpendTransaction: Problem "+ 233 "creating signature hash for input %d", ii) 234 } 235 236 unsignedHashes = append(unsignedHashes, hex.EncodeToString(hashBytes)) 237 } 238 239 // At this point all the inputs should be signed and the total input should cover 240 // the spend amount plus the fee with any change going back to the spend address. 241 glog.V(2).Infof("CreateBitcoinSpendTransaction: Created transaction with "+ 242 "(%d inputs, %d outputs, %d total input, %d spend amount, %d change, %d fee)", 243 len(retTxn.TxIn), len(retTxn.TxOut), totalInputSatoshis, 244 spendAmountSatoshis, totalInputSatoshis-spendAmountSatoshis-finalFee, finalFee) 245 246 return retTxn, totalInputSatoshis, finalFee, unsignedHashes, nil 247 } 248 249 func IsBitcoinTestnet(params *DeSoParams) bool { 250 return params.BitcoinBtcdParams.Name == "testnet3" 251 } 252 253 // ====================================================================================== 254 // BlockCypher API code 255 // 256 // This is bascially a user experience hack. We use BlockCypher to fetch 257 // BitcoinUtxos to determine what the user is capable of spending from their address 258 // and to craft burn transactions for the user using their address and available utxos. Note 259 // that we could do this in the BitcoinManager and cut reliance on BlockCypher to make 260 // decentralized, but it would increase complexity significantly. Moreover, this 261 // piece is not critical to the protocol or to consensus, and it breaking doesn't even 262 // stop people from being able to purchase DeSo since they can always do that by sending 263 // Bitcoin to the burn address from any standard Bitcoin client after entering their 264 // seed phrase. 265 // ====================================================================================== 266 267 type BlockCypherAPIInputResponse struct { 268 PrevTxIDHex string `json:"prev_hash"` 269 Index int64 `json:"output_index"` 270 ScriptHex string `json:"script"` 271 AmountSatoshis int64 `json:"output_value"` 272 Sequence int64 `json:"sequence"` 273 Addresses []string `json:"addresses"` 274 ScriptType string `json:"script_type"` 275 Age int64 `json:"age"` 276 } 277 278 type BlockCypherAPIOutputResponse struct { 279 AmountSatoshis int64 `json:"value"` 280 ScriptHex string `json:"script"` 281 Addresses []string `json:"addresses"` 282 ScriptType string `json:"script_type"` 283 SpentBy string `json:"spent_by"` 284 } 285 286 type BlockCypherAPITxnResponse struct { 287 BlockHashHex string `json:"block_hash"` 288 BlockHeight int64 `json:"block_height"` 289 LockTime int64 `json:"lock_time"` 290 TxIDHex string `json:"hash"` 291 Inputs []*BlockCypherAPIInputResponse `json:"inputs"` 292 Outputs []*BlockCypherAPIOutputResponse `json:"outputs"` 293 Confirmations int64 `json:"confirmations"` 294 DoubleSpend bool `json:"double_spend"` 295 } 296 297 type BlockCypherAPIFullAddressResponse struct { 298 Address string `json:"address"` 299 // Balance data 300 ConfirmedBalance int64 `json:"balance"` 301 UnconfirmedBalance int64 `json:"unconfirmed_balance"` 302 FinalBalance int64 `json:"final_balance"` 303 304 // Transaction data 305 Txns []*BlockCypherAPITxnResponse `json:"txs"` 306 307 HasMore bool `json:"hasMore"` 308 309 Error string `json:"error"` 310 } 311 312 type BlockchainInfoAPIResponse struct { 313 DoubleSpend bool `json:"double_spend"` 314 } 315 316 func BlockCypherExtractBitcoinUtxosFromResponse( 317 apiData *BlockCypherAPIFullAddressResponse, addrString string, params *DeSoParams) ( 318 []*BitcoinUtxo, error) { 319 320 glog.V(2).Infof("BlockCypherExtractBitcoinUtxosFromResponse: Extracting BitcoinUtxos "+ 321 "from %d txns", len(apiData.Txns)) 322 addr, err := btcutil.DecodeAddress(addrString, params.BitcoinBtcdParams) 323 if err != nil { 324 return nil, fmt.Errorf("BlockCypherExtractBitcoinUtxosFromResponse: "+ 325 "Error decoding address %v: %v", addrString, err) 326 } 327 outputScriptForAddr, err := txscript.PayToAddrScript(addr) 328 if err != nil { 329 return nil, fmt.Errorf("BlockCypherExtractBitcoinUtxosFromResponse: "+ 330 "Error creating output script for addr %v: %v", addrString, err) 331 } 332 333 // Go through and index all of the outputs that appear as inputs. The reason 334 // we must do this is because the API only sets to SpentBy field if a transaction 335 // has at least one confirmation. So, in order to mark outputs as spent when they 336 // appear in transactions that aren't confirmed, we need to make this adjustment. 337 // 338 // We overload the BitcoinUtxo here because it's easier than defining a new struct. 339 type utxoKey struct { 340 TxIDHex string 341 Index int64 342 } 343 outputsSpentInResponseInputs := make(map[utxoKey]bool) 344 for _, txn := range apiData.Txns { 345 for _, input := range txn.Inputs { 346 currentUtxo := utxoKey{ 347 TxIDHex: input.PrevTxIDHex, 348 Index: input.Index, 349 } 350 outputsSpentInResponseInputs[currentUtxo] = true 351 } 352 353 } 354 355 bitcoinUtxos := []*BitcoinUtxo{} 356 for _, txn := range apiData.Txns { 357 for outputIndex, output := range txn.Outputs { 358 if output.SpentBy != "" { 359 // This is how we determine if an output is spent or not. 360 continue 361 } 362 if output.ScriptHex != hex.EncodeToString(outputScriptForAddr) { 363 // Only outputs that are destined for the passed-in address are considered. 364 continue 365 } 366 if output.AmountSatoshis == 0 { 367 // Ignore outputs that don't have any BTC in them. This is also helpful 368 // to prevent weird bugs where the API response changed the name of a 369 // field or something. 370 continue 371 } 372 373 // If the output is spent in one of the inputs of the API response then 374 // consider it spent here. We do this to count confirmed transactions in 375 // the utxo set. 376 currentUtxo := utxoKey{ 377 TxIDHex: txn.TxIDHex, 378 Index: int64(outputIndex), 379 } 380 if _, exists := outputsSpentInResponseInputs[currentUtxo]; exists { 381 continue 382 } 383 384 // If we get here we know we are dealing with an unspent output destined 385 // for the address passed in. 386 387 bUtxo := &BitcoinUtxo{} 388 bUtxo.AmountSatoshis = output.AmountSatoshis 389 bUtxo.Index = int64(outputIndex) 390 txid := (chainhash.Hash)(*mustDecodeHexBlockHashBitcoin(txn.TxIDHex)) 391 bUtxo.TxID = &txid 392 393 bitcoinUtxos = append(bitcoinUtxos, bUtxo) 394 } 395 } 396 397 glog.V(2).Infof("BlockCypherExtractBitcoinUtxosFromResponse: Extracted %d BitcoinUtxos", 398 len(bitcoinUtxos)) 399 400 return bitcoinUtxos, nil 401 } 402 403 func GetBlockCypherAPIFullAddressResponse(addrString string, params *DeSoParams) ( 404 _apiData *BlockCypherAPIFullAddressResponse, _err error) { 405 406 URL := fmt.Sprintf("http://api.blockcypher.com/v1/btc/main/addrs/%s/full", addrString) 407 if IsBitcoinTestnet(params) { 408 URL = fmt.Sprintf("http://api.blockcypher.com/v1/btc/test3/addrs/%s/full", addrString) 409 } 410 glog.V(2).Infof("GetBlockCypherAPIFullAddressResponse: Querying URL: %s", URL) 411 412 // jsonValue, err := json.Marshal(postData) 413 req, _ := http.NewRequest("GET", URL, nil) 414 req.Header.Set("Content-Type", "application/json") 415 416 // To add a ?a=b query string, use the below. 417 q := req.URL.Query() 418 // TODO: Right now we'll only fetch a maximum of 50 transactions from the API. 419 // This means if the user has done more than 50 transactions with their address 420 // we'll start missing some of the older utxos. This is easy to fix, though, and 421 // just amounts to cycling through the API's pages. Note also that this does not 422 // prevent a user from buying DeSo in this case nor does it prevent her from being 423 // able to recover her Bitcoin. Both of these can be accomplished by loading the 424 // address in a standard Bitcoin wallet like Electrum. 425 q.Add("limit", "50") 426 req.URL.RawQuery = q.Encode() 427 glog.V(2).Infof("GetBlockCypherAPIFullAddressResponse: URL with params: %s", req.URL) 428 429 client := &http.Client{} 430 resp, err := client.Do(req) 431 if err != nil { 432 return nil, fmt.Errorf("GetBlockCypherAPIFullAddressResponse: Problem with HTTP request %s: %v", URL, err) 433 } 434 defer resp.Body.Close() 435 436 // Decode the response into the appropriate struct. 437 body, _ := ioutil.ReadAll(resp.Body) 438 responseData := &BlockCypherAPIFullAddressResponse{} 439 decoder := json.NewDecoder(bytes.NewReader(body)) 440 if err := decoder.Decode(responseData); err != nil { 441 return nil, fmt.Errorf("GetBlockCypherAPIFullAddressResponse: Problem decoding response JSON into "+ 442 "interface %v, response: %v, error: %v", responseData, resp, err) 443 } 444 //glog.V(2).Infof("BlockCypherUtxoSource: Received response: %v", responseData) 445 446 if responseData.Error != "" { 447 return nil, fmt.Errorf("GetBlockCypherAPIFullAddressResponse: Had an "+ 448 "error in the response: %s", responseData.Error) 449 } 450 451 return responseData, nil 452 } 453 454 func BlockCypherUtxoSource(addrString string, params *DeSoParams) ( 455 []*BitcoinUtxo, error) { 456 457 apiData, err := GetBlockCypherAPIFullAddressResponse(addrString, params) 458 if err != nil { 459 return nil, errors.Wrapf(err, "BlockCypherUtxoSource: Problem getting API data "+ 460 "for address %s: ", addrString) 461 } 462 463 return BlockCypherExtractBitcoinUtxosFromResponse(apiData, addrString, params) 464 } 465 466 // The frontend passes in the apiData. We do this so that our server doesn't 467 // get rate-limited by the free tier. 468 func FrontendBlockCypherUtxoSource( 469 apiData *BlockCypherAPIFullAddressResponse, addrString string, 470 params *DeSoParams) ( 471 []*BitcoinUtxo, error) { 472 473 return BlockCypherExtractBitcoinUtxosFromResponse(apiData, addrString, params) 474 } 475 476 func BlockchainInfoCheckBitcoinDoubleSpend(txnHash *chainhash.Hash, blockCypherAPIKey string, params *DeSoParams) ( 477 _isDoubleSpend bool, _err error) { 478 479 // Always pass on testnet for simplicity. 480 if IsBitcoinTestnet(params) { 481 return false, nil 482 } 483 URL := fmt.Sprintf("https://blockchain.info/rawtx/%s", txnHash.String()) 484 glog.V(2).Infof("BlockchainInfoCheckBitcoinDoubleSpend: Querying URL: %s", URL) 485 486 req, _ := http.NewRequest("GET", URL, nil) 487 req.Header.Set("Content-Type", "application/json") 488 489 client := &http.Client{} 490 resp, err := client.Do(req) 491 if err != nil { 492 return false, fmt.Errorf("BlockchainInfoCheckBitcoinDoubleSpend: "+ 493 "Problem with HTTP request %s: %v", URL, err) 494 } 495 defer resp.Body.Close() 496 497 if resp.StatusCode != 200 { 498 glog.V(2).Infof("BlockchainInfoCheckBitcoinDoubleSpend: Bitcoin txn with "+ 499 "hash %v was not found in Blockchain.info OR an error occurred: %v", txnHash, spew.Sdump(resp)) 500 return true, nil 501 } 502 503 // Decode the response into the appropriate struct. 504 body, _ := ioutil.ReadAll(resp.Body) 505 responseData := &BlockchainInfoAPIResponse{} 506 decoder := json.NewDecoder(bytes.NewReader(body)) 507 if err := decoder.Decode(responseData); err != nil { 508 return false, fmt.Errorf("BlockchainInfoCheckBitcoinDoubleSpend: "+ 509 "Problem decoding response JSON into "+ 510 "interface %v, response: %v, error: %v", responseData, resp, err) 511 } 512 513 if responseData.DoubleSpend { 514 glog.V(2).Infof("BlockchainInfoCheckBitcoinDoubleSpend: Bitcoin "+ 515 "txn with hash %v was a double spend", txnHash) 516 return true, nil 517 } 518 519 return false, nil 520 } 521 522 func GetBlockCypherTxnResponse(txnHash *chainhash.Hash, blockCypherAPIKey string, params *DeSoParams) (*BlockCypherAPITxnResponse, error) { 523 URL := fmt.Sprintf("http://api.blockcypher.com/v1/btc/main/txs/%s", txnHash.String()) 524 if IsBitcoinTestnet(params) { 525 URL = fmt.Sprintf("http://api.blockcypher.com/v1/btc/test3/txs/%s", txnHash.String()) 526 } 527 glog.V(2).Infof("CheckBitcoinDoubleSpend: Querying URL: %s", URL) 528 529 // jsonValue, err := json.Marshal(postData) 530 req, _ := http.NewRequest("GET", URL, nil) 531 req.Header.Set("Content-Type", "application/json") 532 533 // To add a ?a=b query string, use the below. 534 q := req.URL.Query() 535 // TODO: Right now we'll only fetch a maximum of 50 transactions from the API. 536 // This means if the user has done more than 50 transactions with their address 537 // we'll start missing some of the older utxos. This is easy to fix, though, and 538 // just amounts to cycling through the API's pages. Note also that this does not 539 // prevent a user from buying DeSo in this case nor does it prevent her from being 540 // able to recover her Bitcoin. Both of these can be accomplished by loading the 541 // address in a standard Bitcoin wallet like Electrum. 542 q.Add("token", blockCypherAPIKey) 543 req.URL.RawQuery = q.Encode() 544 glog.V(2).Infof("CheckBitcoinDoubleSpend: URL with params: %s", req.URL) 545 546 client := &http.Client{} 547 resp, err := client.Do(req) 548 if err != nil { 549 return nil, fmt.Errorf("CheckBitcoinDoubleSpend: Problem with HTTP request %s: %v", URL, err) 550 } 551 defer resp.Body.Close() 552 553 if resp.StatusCode == 404 { 554 return nil, nil 555 } else if resp.StatusCode != 200 { 556 body, _ := ioutil.ReadAll(resp.Body) 557 return nil, fmt.Errorf("CheckBitcoinDoubleSpend: Error code returned "+ 558 "from BlockCypher: %v %v", resp.StatusCode, string(body)) 559 } 560 561 // Decode the response into the appropriate struct. 562 body, _ := ioutil.ReadAll(resp.Body) 563 responseData := &BlockCypherAPITxnResponse{} 564 decoder := json.NewDecoder(bytes.NewReader(body)) 565 if err := decoder.Decode(responseData); err != nil { 566 return nil, fmt.Errorf("CheckBitcoinDoubleSpend: Problem decoding response JSON into "+ 567 "interface %v, response: %v, error: %v", responseData, resp, err) 568 } 569 570 return responseData, nil 571 } 572 573 func BlockCypherCheckBitcoinDoubleSpend(txnHash *chainhash.Hash, blockCypherAPIKey string, params *DeSoParams) ( 574 _isDoubleSpend bool, _err error) { 575 576 glog.Infof("CheckBitcoinDoubleSpend: Checking txn %v for double-spend", txnHash) 577 responseData, err := GetBlockCypherTxnResponse(txnHash, blockCypherAPIKey, params) 578 if err != nil { 579 return false, errors.Wrapf(err, "BlockCypherCheckBitcoinDoubleSpend: Error fetching txn: ") 580 } 581 582 // If we didn't find the txn in BlockCypher then we consider this a double-spend 583 if responseData == nil { 584 glog.V(2).Infof("CheckBitcoinDoubleSpend: Bitcoin txn with hash %v was not found in BlockCypher", txnHash) 585 return true, nil 586 } 587 588 if responseData.DoubleSpend { 589 glog.V(2).Infof("CheckBitcoinDoubleSpend: Bitcoin txn with hash %v was a double spend", txnHash) 590 return true, nil 591 } 592 593 // If the transaction is not mined, then we need to recursively check 594 // its inputs to determine whether they are double-spends. 595 if responseData.Confirmations == 0 { 596 // If there are too many inputs on this txn, then just mark it as a double-spend. 597 // This avoids us having to use our API quota unnecessarily. 598 if len(responseData.Inputs) > 25 { 599 glog.Warningf("CheckBitcoinDoubleSpend: Unmined bitcoin txn with hash %v had %v inputs, "+ 600 "which made us label it as a double-spend. If this is happening a lot, consider "+ 601 "increasing the limit on this if statement", txnHash, len(responseData.Inputs)) 602 return true, nil 603 } 604 // Recursively scan through the unmined inputs one at a time to see 605 // if any ancestors contain double-spends. If they do, then this txn is 606 // a double-spend by transitivity. 607 for _, txIn := range responseData.Inputs { 608 inputTxHash, err := chainhash.NewHashFromStr(txIn.PrevTxIDHex) 609 if err != nil { 610 return false, errors.Wrapf( 611 err, "BlockCypherCheckBitcoinDoubleSpend: Error parsing INPUT txn hash: %v", inputTxHash) 612 } 613 glog.V(1).Infof("CheckBitcoinDoubleSpend: Checking INPUT %v for double-spend", inputTxHash) 614 inputIsDoubleSpend, err := BlockCypherCheckBitcoinDoubleSpend(inputTxHash, blockCypherAPIKey, params) 615 if err != nil { 616 return false, errors.Wrapf( 617 err, "BlockCypherCheckBitcoinDoubleSpend: Error fetching INPUT txn: %v", inputTxHash) 618 } 619 if inputIsDoubleSpend { 620 return true, nil 621 } 622 } 623 } 624 // If we get here, it means that this txn wasn't a double-spend *and* 625 // none of its unmined inputs were double-spends either. 626 627 return false, nil 628 } 629 func BlockCypherPushTransaction(txnHex string, txnHash *chainhash.Hash, blockCypherAPIKey string, params *DeSoParams) ( 630 _added bool, _err error) { 631 632 URL := fmt.Sprintf("http://api.blockcypher.com/v1/btc/main/txs/push") 633 if IsBitcoinTestnet(params) { 634 URL = fmt.Sprintf("http://api.blockcypher.com/v1/btc/test3/txs/push") 635 } 636 glog.V(2).Infof("PushTransaction: Querying URL: %s", URL) 637 638 json_data, err := json.Marshal(map[string]string{ 639 "tx": txnHex, 640 }) 641 if err != nil { 642 return false, fmt.Errorf( 643 "PushTransaction: Error encoding request as JSON %s: %v", URL, err) 644 } 645 646 // jsonValue, err := json.Marshal(postData) 647 req, _ := http.NewRequest("POST", URL, bytes.NewBuffer(json_data)) 648 req.Header.Set("Content-Type", "application/json") 649 650 // To add a ?a=b query string, use the below. 651 q := req.URL.Query() 652 // TODO: Right now we'll only fetch a maximum of 50 transactions from the API. 653 // This means if the user has done more than 50 transactions with their address 654 // we'll start missing some of the older utxos. This is easy to fix, though, and 655 // just amounts to cycling through the API's pages. Note also that this does not 656 // prevent a user from buying DeSo in this case nor does it prevent her from being 657 // able to recover her Bitcoin. Both of these can be accomplished by loading the 658 // address in a standard Bitcoin wallet like Electrum. 659 q.Add("token", blockCypherAPIKey) 660 req.URL.RawQuery = q.Encode() 661 glog.V(2).Infof("PushTransaction: URL with params: %s", req.URL) 662 663 client := &http.Client{} 664 resp, err := client.Do(req) 665 if err != nil { 666 return false, fmt.Errorf("PushTransaction: Problem with HTTP request %s: %v", URL, err) 667 } 668 defer resp.Body.Close() 669 670 body, _ := ioutil.ReadAll(resp.Body) 671 if resp.StatusCode == 201 { 672 glog.V(1).Infof("PushTransaction: Successfully added BitcoinExchange "+ 673 "txn hash: %v, full txn: %v body: %v", txnHash, txnHex, string(body)) 674 return true, nil 675 } 676 677 // If we get here then we had a bad status code. 678 return false, fmt.Errorf("PushTransaction: Failed to submit transaction "+ 679 "to Bitcoin blockchain: %v, Body: %v, Txn Hash: %v", resp.StatusCode, string(body), txnHash) 680 } 681 682 func CheckBitcoinDoubleSpend(txnHash *chainhash.Hash, blockCypherAPIKey string, params *DeSoParams) error { 683 684 { 685 isDoubleSpend, err := BlockCypherCheckBitcoinDoubleSpend(txnHash, blockCypherAPIKey, params) 686 if err != nil { 687 return fmt.Errorf("PushAndWaitForTxn: Error occurred when checking for "+ 688 "double-spend on BlockCypher. Your transaction will go through once it "+ 689 "has been mined into a Bitcoin block. Txn hash: %v", txnHash) 690 } 691 if isDoubleSpend { 692 return fmt.Errorf("PushAndWaitForTxn: Error: double-spend detected "+ 693 "by BlockCypher. Your transaction will go through once it mines into the "+ 694 "next Bitcoin block, which should take about ten minutes. Txn hash: %v", txnHash) 695 } 696 } 697 698 // Also check the Blockchain.com API for a double-spend. This prevents an attack 699 // that exploits a weakness in BlockCypher's APIs. 700 // 701 // TODO: Document this attack later. 702 { 703 isDoubleSpend, err := BlockchainInfoCheckBitcoinDoubleSpend(txnHash, blockCypherAPIKey, params) 704 if err != nil { 705 return fmt.Errorf("PushAndWaitForTxn: Error occurred when checking for "+ 706 "double-spend on blockchain.info. Your transaction will go through once "+ 707 "it has been mined into a Bitcoin block. Txn hash: %v", txnHash) 708 } 709 if isDoubleSpend { 710 return fmt.Errorf("PushAndWaitForTxn: Error: double-spend detected "+ 711 "by blockchain.info. Your transaction will go through once it mines "+ 712 "into the next Bitcoin block. Txn hash: %v", txnHash) 713 } 714 } 715 716 // If we get here then there was no double-spend detected. 717 return nil 718 } 719 720 func BlockCypherPushAndWaitForTxn(txnHex string, txnHash *chainhash.Hash, 721 blockCypherAPIKey string, doubleSpendWaitSeconds float64, params *DeSoParams) (_isDoubleSpend bool, _err error) { 722 _, err := BlockCypherPushTransaction(txnHex, txnHash, blockCypherAPIKey, params) 723 if err != nil { 724 return false, fmt.Errorf("PushAndWaitForTxn: %v", err) 725 } 726 // Wait some amount of time before checking for a double-spend. 727 time.Sleep(time.Duration(doubleSpendWaitSeconds) * time.Second) 728 729 if err := CheckBitcoinDoubleSpend(txnHash, blockCypherAPIKey, params); err != nil { 730 return true, err 731 } 732 733 return false, nil 734 } 735 736 type BlockonomicsRBFResponse struct { 737 RBF int64 `json:"rbf"` 738 Status string `json:"status"` 739 } 740 741 func BlockonomicsCheckRBF(bitcoinTxnHash string) ( 742 _hasRBF bool, _err error) { 743 744 URL := fmt.Sprintf("https://www.blockonomics.co/api/tx_detail?txid=%s", bitcoinTxnHash) 745 glog.V(1).Infof("BlockonomicsCheckRBF: Querying URL: %s", URL) 746 747 req, _ := http.NewRequest("GET", URL, nil) 748 req.Header.Set("Content-Type", "application/json") 749 750 client := &http.Client{} 751 resp, err := client.Do(req) 752 if err != nil { 753 return false, fmt.Errorf("BlockonomicsCheckRBF: Problem with HTTP request %s: %v", URL, err) 754 } 755 defer resp.Body.Close() 756 757 // Decode the body. 758 body, _ := ioutil.ReadAll(resp.Body) 759 if resp.StatusCode != 200 { 760 retErr := fmt.Errorf("BlockonomicsCheckRBF: Error when checking "+ 761 "RBF tatus for txn %v: %v", bitcoinTxnHash, string(body)) 762 return false, retErr 763 } 764 765 responseData := &BlockonomicsRBFResponse{} 766 decoder := json.NewDecoder(bytes.NewReader(body)) 767 if err := decoder.Decode(responseData); err != nil { 768 return false, fmt.Errorf("BlockonomicsCheckRBF: Problem decoding response JSON into "+ 769 "interface %v, response: %v, body: %v, error: %v", responseData, resp, string(body), err) 770 } 771 //glog.V(2).Infof("UtxoSource: Received response: %v", responseData) 772 773 if strings.ToLower(responseData.Status) == "unconfirmed" && 774 (responseData.RBF == 1 || responseData.RBF == 2) { 775 776 glog.V(1).Infof("BlockonomicsCheckRBF: Bitcoin txn with hash %v has RBF set", bitcoinTxnHash) 777 return true, nil 778 } 779 780 return false, nil 781 }