Direct Integration with Bitcoin

I may have lost 0.01277662 BTC, worth about 300 u.

This is entirely my own fault and has nothing to do with the security of the ECDSA and Bitcoin interface.

Here is the details:

A few days ago, I deployed canister to the mainnet and tested the BTC on the testnet, everything works fine!

So why not test the mainnet BTC? Although I saw the announcement saying not to use valuable BTC, I thought this is mainly used to explain that the key of ecdsa may be changed when it is officially released in the future, but it does not matter, because I will transfer the BTC out immediately.

So I ran:

dfx canister --network ic install basic_bitcoin --argument='(variant {Mainnet})' -m=reinstall

dfx canister --network ic call basic_bitcoin get_p2pkh_address
("1T6xcKM23GjYNNeZXrQrctSEFAKcfmGq2")

Everything looked good, and I could get the address of the BTC mainnet correctly. It seemed everything is normal, so I withdrew a small amount of BTC from the exchange to this address. I knew the risks, so I limited the possible losses to what I can afford. In the boring waiting, (compared to IC, BTC is really slow), I tried to randomly obtain an address with a balance from the BTC explorer, to test whether the balance can be obtained correctly:

dfx canister --network ic call basic_bitcoin get_balance '("<btc address>")'
Error: Failed update call.
Caused by: Failed update call.
  The Replica returned an error: code 5, message: "Canister zso2k-nqaaa-aaaak-aao3q-cai trapped explicitly: Panicked at 'called `Result::unwrap()` on an `Err` value: (CanisterReject, "Received request for mainnet but the subnet supports testnet")', src/bitcoin_api.rs:31:17"

I realized that something was wrong, so I quickly went to check the withdrawal interface, the 2min cancellation period has passed, the transaction has been sent to the node’s memory pool, and I want to consume the utxo by sending another transaction with a higher fee price again to cancel the previous transaction, but the withdrawal is through the exchange, and it may time out waiting for customer service.


src

Since the interface of ecdsa is not banned (getting address requires getting public key from ecdsa), then theoretically I should be able to withdraw from this address.

The interface get_current_fee_percentiles to get fee_price (fee per byte) is not available, I deleted it, and then checked some recent transactions from the btc explorer, most of them are below 100, so setting it to 110.

Then, deleting the get_utxos, input the UTXOs as a parameter. The height and value in Utxo are easy to find from the BTC explorer, and the txid in OutPoint generated from tx_hash:

    // the hash is from: https://www.blockchain.com/btc/tx/e1d3f2afe6ef975a1c2675cac813d4fe1c981e6beeb3edec1ad0d872996edb21
    let hash = bitcoin_hashes::sha256d::Hash::from_str(
        "e1d3f2afe6ef975a1c2675cac813d4fe1c981e6beeb3edec1ad0d872996edb21",
    )
    .unwrap();

    let txid = bitcoin::hash_types::Txid::from_hash(hash);
    let txid_vec = txid.to_vec();

vout may be the index of utxo under this address, because there is only one transaction, so I set it to 0;

send_transaction doesn’t work either, so I return the signed transaction bytes.

Finally achieved this effect:

dfx canister --network ic call basic_bitcoin tx '(record {destination_address= "3QnYGgtC1hNrSRBHdUnDn7EBZ3R8fwezVh"; amount_in_satoshi=1247662; }, record {height=747887;value=1277662; outpoint=record {txid=vec{33;219;110;153;114;216;208;26;236;237;179;238;107;30;152;28;254;212;19;200;202;117;38;28;90;151;239;230;175;242;211;225;};vout=0;}})'
(
  "020000000121db6e9972d8d01aecedb3ee6b1e981cfed413c8ca75261c5a97efe6aff2d3e1000000006b483045022100e2644f76115c826a73488967e11390a444f0d673686b1f4e00b1b16a9ebb1e670220131e74ffb8cbf116446c84c99c805b0c1b3824d453a6a85d3ffcd60a78161d6c012103dcdb13eda6c212e807626e6e72ef8aca901a4c0f31f1bf7036803e3756240b96ffffffff02ae0913000000000017a914fd564e358f1f3f7f88990244ed1b5293946964558718750000000000001976a91404efca051ff7d7d8ba81f7dcc5c783c7381566b188ac00000000",
  blob "\02\00\00\00\01!\dbn\99r\d8\d0\1a\ec\ed\b3\eek\1e\98\1c\fe\d4\13\c8\cau&\1cZ\97\ef\e6\af\f2\d3\e1\00\00\00\00kH0E\02!\00\e2dOv\11\5c\82jsH\89g\e1\13\90\a4D\f0\d6shk\1fN\00\b1\b1j\9e\bb\1eg\02 \13\1et\ff\b8\cb\f1\16Dl\84\c9\9c\80[\0c\1b8$\d4S\a6\a8]?\fc\d6\0ax\16\1dl\01!\03\dc\db\13\ed\a6\c2\12\e8\07bnnr\ef\8a\ca\90\1aL\0f1\f1\bfp6\80>7V$\0b\96\ff\ff\ff\ff\02\ae\09\13\00\00\00\00\00\17\a9\14\fdVN5\8f\1f?\7f\88\99\02D\ed\1bR\93\94idU\87\18u\00\00\00\00\00\00\19v\a9\14\04\ef\ca\05\1f\f7\d7\d8\ba\81\f7\dc\c5\c7\83\c78\15f\b1\88\ac\00\00\00\00",
)

curl -H "Authorization: Bearer <API TOKEN>"  -d '{"tx": "020000000121db6e9972d8d01aecedb3ee6b1e981cfed413c8ca75261c5a97efe6aff2d3e1000000006b483045022100f79de358f564aed17cb8fd7cf95d79aee65218698bcd1cf037e136605029f93702203dba9fed10b25da52d6db6bd643631af832442e87051eea8f19a0f6e8d72b2f9012103dcdb13eda6c212e807626e6e72ef8aca901a4c0f31f1bf7036803e3756240b96ffffffff02ae0913000000000017a914fd564e358f1f3f7f88990244ed1b5293946964558718750000000000001976a91404efca051ff7d7d8ba81f7dcc5c783c7381566b188ac00000000"}' -X POST https://ubiquity.api.blockdaemon.com/v2/bitcoin/mainnet/tx/send
{"type":"bad-request","code":16397,"title":"Bad Request","status":400,"detail":"failed to send transaction. Error: bad-txns-inputs-missingorspent. Code: -25"}%  

# If I changed to vout to 1, and got another error, it seems vout = 0 is correct:
{"type":"bad-request","code":16397,"title":"Bad Request","status":400,"detail":"failed to send transaction. Error: mandatory-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element). Code: -26"}%  

I have added permission control to interfaces that needs to be signed with an ECDSA key.

the code of tx interface
pub async fn tx(
    network: Network,
    derivation_path: Vec<Vec<u8>>,
    key_name: String,
    dst_address: String,
    amount: Satoshi,
    utxos: Vec<Utxo>,
) -> Vec<u8> {
    // Get fee percentiles from previous transactions to estimate our own fee.

    let fee_per_byte = 110;

    // Fetch our public key, P2PKH address, and UTXOs.
    let own_public_key =
        ecdsa_api::ecdsa_public_key(key_name.clone(), derivation_path.clone()).await;
    let own_address = public_key_to_p2pkh_address(network, &own_public_key);

    print("Fetching UTXOs...");
    let own_utxos = utxos;

    let own_address = Address::from_str(&own_address).unwrap();
    let dst_address = Address::from_str(&dst_address).unwrap();

    // Build the transaction that sends `amount` to the destination address.
    let transaction = build_transaction(
        &own_public_key,
        &own_address,
        &own_utxos,
        &dst_address,
        amount,
        fee_per_byte,
    )
    .await;

    // Sign the transaction.
    let signed_transaction = sign_transaction(
        &own_public_key,
        &own_address,
        transaction,
        key_name,
        derivation_path,
        ecdsa_api::sign_with_ecdsa,
    )
    .await;

    let signed_transaction_bytes = signed_transaction.serialize();

    signed_transaction_bytes
}

Finally, still failed.

  1. Is there any problem with the above steps?
  2. It has been determined that the ECDSA key will be modified before the application subnet supports the BTC mainnet interface, so waiting for the development of the BTC interface will not solve the problem, is there any other solution?
3 Likes