From 26b90b5b17d53d060405c809398b9e1b65e37183 Mon Sep 17 00:00:00 2001 From: Oleksandr Tkachenko Date: Fri, 5 Jul 2024 19:14:07 +0200 Subject: [PATCH 01/13] add taproot transactions and improvements --- rust/basic_bitcoin/Cargo.lock | 56 ++-- rust/basic_bitcoin/Makefile | 36 ++- rust/basic_bitcoin/README.md | 133 ++++++--- rust/basic_bitcoin/dfx.json | 28 +- rust/basic_bitcoin/rust-toolchain.toml | 2 +- .../src/basic_bitcoin/Cargo.toml | 5 +- .../src/basic_bitcoin/basic_bitcoin.did | 64 +++-- rust/basic_bitcoin/src/basic_bitcoin/build.sh | 20 -- .../src/bitcoin_wallet/common.rs | 116 ++++++++ .../basic_bitcoin/src/bitcoin_wallet/mod.rs | 15 + .../p2pkh.rs} | 219 +++++--------- .../src/bitcoin_wallet/p2tr_raw_key_spend.rs | 207 ++++++++++++++ .../src/bitcoin_wallet/p2tr_script_spend.rs | 269 ++++++++++++++++++ .../src/basic_bitcoin/src/lib.rs | 110 +++++-- .../src/basic_bitcoin/src/schnorr_api.rs | 96 +++++++ 15 files changed, 1082 insertions(+), 294 deletions(-) create mode 100644 rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/common.rs create mode 100644 rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/mod.rs rename rust/basic_bitcoin/src/basic_bitcoin/src/{bitcoin_wallet.rs => bitcoin_wallet/p2pkh.rs} (50%) create mode 100644 rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/p2tr_raw_key_spend.rs create mode 100644 rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/p2tr_script_spend.rs create mode 100644 rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs diff --git a/rust/basic_bitcoin/Cargo.lock b/rust/basic_bitcoin/Cargo.lock index 26ba5c50b..63c8dd3cc 100644 --- a/rust/basic_bitcoin/Cargo.lock +++ b/rust/basic_bitcoin/Cargo.lock @@ -25,21 +25,18 @@ name = "basic_bitcoin" version = "0.1.0" dependencies = [ "bitcoin", - "bs58", "candid", "hex", "ic-cdk", "ic-cdk-macros", - "ripemd", "serde", - "sha2", ] [[package]] name = "bech32" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" [[package]] name = "binread" @@ -66,20 +63,31 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.28.2" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d30fb43d287492017964a1fd7d3f82e8cc760818471c6ef2d44111e317d5c3" +checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462" dependencies = [ "bech32", + "bitcoin-private", "bitcoin_hashes", + "hex_lit", "secp256k1", ] +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + [[package]] name = "bitcoin_hashes" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", +] [[package]] name = "block-buffer" @@ -90,12 +98,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bs58" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" - [[package]] name = "byteorder" version = "1.4.3" @@ -244,6 +246,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "ic-cdk" version = "0.10.0" @@ -423,15 +431,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "ripemd" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" -dependencies = [ - "digest", -] - [[package]] name = "rustversion" version = "1.0.14" @@ -440,18 +439,19 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "secp256k1" -version = "0.22.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295642060261c80709ac034f52fca8e5a9fa2c7d341ded5cdb164b7c33768b2a" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" dependencies = [ + "bitcoin_hashes", "secp256k1-sys", ] [[package]] name = "secp256k1-sys" -version = "0.5.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" dependencies = [ "cc", ] diff --git a/rust/basic_bitcoin/Makefile b/rust/basic_bitcoin/Makefile index 727d3b935..61f98b2df 100644 --- a/rust/basic_bitcoin/Makefile +++ b/rust/basic_bitcoin/Makefile @@ -4,10 +4,44 @@ all: deploy .PHONY: deploy .SILENT: deploy deploy: - dfx deploy basic_bitcoin --argument '(variant { regtest })' + dfx deploy schnorr_canister && dfx deploy basic_bitcoin --argument '(variant { regtest }, "dfx_test_key" : text)' + +.PHONY: mock +.SILENT: mock +mock: deploy + SCHNORR_MOCK_CANISTER_ID=$(shell dfx canister id schnorr_canister); \ + THIS_CANISTER_ID=$(shell dfx canister id basic_bitcoin); \ + echo "Changing to using mock canister instead of management canister for signing"; \ + CMD="dfx canister call "$${THIS_CANISTER_ID}" for_test_only_set_schnorr_canister_id '("\"$${SCHNORR_MOCK_CANISTER_ID}\"")'"; \ + echo "$${CMD}"; \ + eval "$${CMD}" + +.PHONY: regtest_topup +.SILENT: regtest_topup +regtest_topup: + P2PKH_ADDR=$(shell dfx canister call basic_bitcoin get_p2pkh_address | tr -d '()') && \ + P2TR_SCRIPT_SPEND_ADDR=$(shell dfx canister call basic_bitcoin get_p2tr_script_spend_address | tr -d '()') && \ + P2TR_SCRIPT_KEY_SPEND_ADDR=$(shell dfx canister call basic_bitcoin get_p2tr_raw_key_spend_address | tr -d '()') && \ + TOPUP_CMD_P2PKH="bitcoin-cli -rpcport=8333 sendtoaddress $${P2PKH_ADDR} 1" && \ + TOPUP_CMD_P2TR_SCRIPT_SPEND="bitcoin-cli -rpcport=8333 sendtoaddress $${P2TR_SCRIPT_SPEND_ADDR} 1" && \ + TOPUP_CMD_P2TR_RAW_KEY_SPEND="bitcoin-cli -rpcport=8333 sendtoaddress $${P2TR_SCRIPT_KEY_SPEND_ADDR} 1" && \ + eval "$${TOPUP_CMD_P2PKH}" && \ + eval "$${TOPUP_CMD_P2PKH}" && \ + eval "$${TOPUP_CMD_P2PKH}" && \ + eval "$${TOPUP_CMD_P2TR_SCRIPT_SPEND}" && \ + eval "$${TOPUP_CMD_P2TR_SCRIPT_SPEND}" && \ + eval "$${TOPUP_CMD_P2TR_SCRIPT_SPEND}" && \ + eval "$${TOPUP_CMD_P2TR_RAW_KEY_SPEND}" && \ + eval "$${TOPUP_CMD_P2TR_RAW_KEY_SPEND}" && \ + eval "$${TOPUP_CMD_P2TR_RAW_KEY_SPEND}" && \ + bitcoin-cli -rpcport=8333 -generate 6 .PHONY: clean .SILENT: clean clean: rm -rf .dfx + rm -rf dist + rm -rf node_modules + rm -rf src/declarations + rm -f .env cargo clean diff --git a/rust/basic_bitcoin/README.md b/rust/basic_bitcoin/README.md index 93af88a82..1d882aa1d 100644 --- a/rust/basic_bitcoin/README.md +++ b/rust/basic_bitcoin/README.md @@ -11,8 +11,11 @@ This tutorial will walk you through how to deploy a sample [canister smart contr ## Architecture -This example internally leverages the [ECDSA API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-ecdsa_public_key) -and [Bitcoin API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin-api) of the Internet Computer. +This example internally leverages the [ECDSA +API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-ecdsa_public_key), +[Schnorr API TODO](TODO), and [Bitcoin +API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin-api) +of the Internet Computer. For a deeper understanding of the ICP < > BTC integration, see the [Bitcoin integration documentation](https://wiki.internetcomputer.org/wiki/Bitcoin_Integration). @@ -24,7 +27,9 @@ For a deeper understanding of the ICP < > BTC integration, see the [Bitcoin inte ### Clone the smart contract -This tutorial has the **same smart contract** written in different programming languages: in [Motoko](https://internetcomputer.org/docs/current/developer-docs/backend/motoko/index.md) and [Rust](https://internetcomputer.org/docs/current/developer-docs/backend/rust/index.md). +This tutorial has the same smart contract written in +[Motoko](https://internetcomputer.org/docs/current/developer-docs/backend/motoko/index.md) +using ECDSA and Bitcoin API but currently not Schnorr API. You can clone and deploy either one, as they both function in the same way. @@ -33,11 +38,9 @@ To clone and build the smart contract in **Rust**: ```bash git clone https://github.com/dfinity/examples cd examples/rust/basic_bitcoin -git submodule update --init --recursive +cargo build --release --target wasm32-unknown unknown ``` -**If you choose Rust and are using MacOS, you'll need to install Homebrew and run `brew install llvm` to be able to compile the example.** - ### Acquire cycles to deploy Deploying to the Internet Computer requires [cycles](https://internetcomputer.org/docs/current/developer-docs/setup/cycles) (the equivalent of "gas" in other blockchains). You can get free cycles from the [cycles faucet](https://internetcomputer.org/docs/current/developer-docs/setup/cycles/cycles-faucet.md). @@ -53,8 +56,12 @@ dfx deploy --network=ic basic_bitcoin --argument '(variant { testnet })' - `--network=ic` tells the command line to deploy the smart contract to the mainnet ICP blockchain - `--argument '(variant { testnet })'` passes the argument `testnet` to initialize the smart contract, telling it to connect to the Bitcoin testnet -**We're initializing the canister with `variant { testnet }` so that the canister connects to the [Bitcoin testnet](https://en.bitcoin.it/wiki/Testnet). To be specific, this connects to `Testnet3`, which is the current Bitcoin test network used by the Bitcoin community.** - +**We're initializing the canister with `variant { testnet }` so that the +canister connects to the [Bitcoin testnet](https://en.bitcoin.it/wiki/Testnet). +To be specific, this connects to `Testnet3`, which is the current Bitcoin test +network used by the Bitcoin community. This means that the addresses generated +in the smart contract can only be used to receive or send funds only on Bitcoin +testnet.** If successful, you should see an output that looks like this: @@ -70,30 +77,54 @@ Candid: Your canister is live and ready to use! You can interact with it using either the command line or using the Candid UI, which is the link you see in the output above. -In the output above, to see the Candid Web UI for your bitcoin canister, you would use the URL `https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=`. Here are the two methods you will see: - -* `public_key` -* `sign` +In the output above, to see the Candid Web UI for your bitcoin canister, you +would use the URL +`https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=`. Candid +Web UI will contain all methods implemented by the canister. ## Step 2: Generating a Bitcoin address -Bitcoin has different types of addresses (e.g. P2PKH, P2SH). Most of these -addresses can be generated from an ECDSA public key. The example code -showcases how your canister can generate a [P2PKH address](https://en.bitcoin.it/wiki/Transaction#Pay-to-PubkeyHash) using the [ecdsa_public_key](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-ecdsa_public_key) API. - -On the Candid UI of your canister, click the "Call" button under `get_p2pkh_address` to -generate a P2PKH Bitcoin address. +Bitcoin has different types of addresses (e.g. P2PKH, P2SH, P2TR). You may want +to check [this +article](https://bitcoinmagazine.com/technical/bitcoin-address-types-compared-p2pkh-p2sh-p2wpkh-and-more) +if you are interested in a high-level comparison of different address types. +These addresses can be generated from an ECDSA public key or a Schnorr +([BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)) +public key. The example code showcases how your canister can generate and spend +from three types of addresses: +1. A [P2PKH address](https://en.bitcoin.it/wiki/Transaction#Pay-to-PubkeyHash) + using the + [ecdsa_public_key](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-method-ecdsa_public_key) + API. +2. A [P2TR + address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) + where the funds can be spent using the raw (untweaked) internal key + (so-called P2TR key path spend, but untweaked). IMPORTANT: This type of + address MUST NOT be used with keys created by multiple parties (e.g., using + [MuSig2](https://ia.cr/2020/1261)). It is only secure in a single-party + scenario. The advantage of this approach compared to the one below is its + significantly smaller fee per transaction because checking the transaction + signature is analogous to P2PK but uses Schnorr instead of ECDSA. +3. A [P2TR + address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) + where the funds can be spent using the provided public key with the script + path, where the Merkelized Alternative Script Tree (MAST) consists of a + single script allowing to spend funds by exactly one key. + +Note that P2TR tweaked key spends are currently not available on the IC because +the threshold Schnorr signing interface does not allow applying BIP341 tweaks to +the private key but this may become available in the future - there is no +technical limitation on the IC side. For a technical comparison of different +ways of how single-signer P2TR addresses can be constructed and used, you may +want to take a look at [this post](https://bitcoin.stackexchange.com/a/111100) +by Pieter Wuille. Multi-signer P2TR addresses are out of scope of this example. + +On the Candid UI of your canister, click the "Call" button under +`get_${type}_address` to generate a `${type}` Bitcoin address, where `${type}` +is one of `[p2pkh, p2tr_raw_key_spend, p2tr_script_spend]`. Or, if you prefer the command line: - - dfx canister --network=ic call basic_bitcoin get_p2pkh_address - -* The Bitcoin address you see will be different from the one above because the - ECDSA public key your canister retrieves is unique. - -* We are generating a Bitcoin testnet address, which can only be -used for sending/receiving Bitcoin on the Bitcoin testnet. - + `dfx canister --network=ic call basic_bitcoin get_${type}_address` ## Step 3: Receiving bitcoin @@ -101,28 +132,44 @@ Now that the canister is deployed and you have a Bitcoin address, it's time to r some testnet bitcoin. You can use one of the Bitcoin faucets, such as [coinfaucet.eu](https://coinfaucet.eu), to receive some bitcoin. -Enter your address and click on "Send testnet bitcoins". This example will use the Bitcoin address `n31eU1K11m1r58aJMgTyxGonu7wSMoUYe7`, but you will use your address. The canister will be receiving 0.011 test BTC on the Bitcoin Testnet. +Enter your address and click on "Send testnet bitcoins". This example will use +the Bitcoin P2PHK address `mot21Ef7HNDpDJa4CBzt48WpEX7AxNyaqx`, but you will use +your address. The Bitcoin address you see will be different from the one above +because the ECDSA/Schnorr public key your canister retrieves is unique. Once the transaction has at least one confirmation, which can take a few minutes, you'll be able to see it in your canister's balance. +The addresses that have been used for the testing of this canister on Bitcoin +testnet are `mot21Ef7HNDpDJa4CBzt48WpEX7AxNyaqx` (P2PKH, +[transactions](https://blockstream.info/testnet/address/mot21Ef7HNDpDJa4CBzt48WpEX7AxNyaqx)), +`tb1pkkrwg6e9s5zf3jmftu224rc5ppax26g5yzdg03rhmqw84359xgpsv5mn2y` (P2TR raw key +spend, +[transactions](https://blockstream.info/testnet/address/tb1pkkrwg6e9s5zf3jmftu224rc5ppax26g5yzdg03rhmqw84359xgpsv5mn2y)), +and `tb1pnm743sjkw9tq3zf9uyetgqkrx7tauthmxnsl5dtyrwnyz9r7lu8qdtcnnc` (P2TR +script path spend, +[transactions](https://blockstream.info/testnet/address/tb1pnm743sjkw9tq3zf9uyetgqkrx7tauthmxnsl5dtyrwnyz9r7lu8qdtcnnc)). +It may be useful to click on "transactions" links if you are interested in how +they are structured. + ## Step 4: Checking your bitcoin balance You can check a Bitcoin address's balance by using the `get_balance` endpoint on your canister. In the Candid UI, paste in your canister's address, and click on "Call": -Alternatively, make the call using the command line. Be sure to replace `mheyfRsAQ1XrjtzjfU1cCH2B6G1KmNarNL` with your own generated P2PKH address: +Alternatively, make the call using the command line. Be sure to replace `mot21Ef7HNDpDJa4CBzt48WpEX7AxNyaqx` with your own generated address: ```bash -dfx canister --network=ic call basic_bitcoin get_balance '("mheyfRsAQ1XrjtzjfU1cCH2B6G1KmNarNL")' +dfx canister --network=ic call basic_bitcoin get_balance '("mot21Ef7HNDpDJa4CBzt48WpEX7AxNyaqx")' ``` Checking the balance of a Bitcoin address relies on the [bitcoin_get_balance](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_get_balance) API. ## Step 5: Sending bitcoin -You can send bitcoin using the `send` endpoint on your canister. +You can send bitcoin using the `send_from_${type}` endpoint on your canister, where +`${type}` is on of `[p2pkh, p2tr_raw_key_spend, p2tr_script_spend]`. In the Candid UI, add a destination address and an amount to send. In the example below, we're sending 4'321 Satoshi (0.00004321 BTC) back to the testnet faucet. @@ -130,20 +177,24 @@ below, we're sending 4'321 Satoshi (0.00004321 BTC) back to the testnet faucet. Via command line, the same call would look like this: ```bash -dfx canister --network=ic call basic_bitcoin send '(record { destination_address = "tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt"; amount_in_satoshi = 4321; })' +dfx canister --network=ic call basic_bitcoin send_from_p2pkh '(record { destination_address = "tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt"; amount_in_satoshi = 4321; })' ``` -The `send` endpoint can send bitcoin by: +The `send_from_${type}` endpoint can send bitcoin by: -1. Getting the percentiles of the most recent fees on the Bitcoin network using the [bitcoin_get_current_fee_percentiles API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_get_current_fee_percentiles). -2. Fetching your unspent transaction outputs (UTXOs), using the [bitcoin_get_utxos API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_get_utxos). +1. Getting the percentiles of the most recent fees on the Bitcoin network using the [bitcoin_get_current_fee_percentiles API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-method-bitcoin_get_current_fee_percentiles). +2. Fetching your unspent transaction outputs (UTXOs), using the [bitcoin_get_utxos API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-method-bitcoin_get_utxos). 3. Building a transaction, using some of the UTXOs from step 2 as input and the destination address and amount to send as output. The fee percentiles obtained from step 1 is used to set an appropriate fee. -4. Signing the inputs of the transaction using the [sign_with_ecdsa API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-sign_with_ecdsa). -5. Sending the signed transaction to the Bitcoin network using the [bitcoin_send_transaction API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin_send_transaction). - -The `send` endpoint returns the ID of the transaction it sent to the network. -You can track the status of this transaction using a block explorer. Once the +4. Signing the inputs of the transaction using the + [sign_with_ecdsa + API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-method-sign_with_ecdsa)/\ + [sign_with_schnorr](TODO). +5. Sending the signed transaction to the Bitcoin network using the [bitcoin_send_transaction API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-method-bitcoin_send_transaction). + +This canister's `send_from_${type}` endpoint returns the ID of the transaction +it sent to the network. You can track the status of this transaction using a +[block explorer](https://en.bitcoin.it/wiki/Block_chain_browser). Once the transaction has at least one confirmation, you should be able to see it reflected in your current balance. @@ -156,7 +207,7 @@ In this tutorial, you were able to: * Connect the canister to the Bitcoin testnet. * Send the canister some testnet BTC. * Check the testnet BTC balance of the canister. -* Use the canister to send testnet BTC to another BTC address. +* Use the canister to send testnet BTC to another testnet BTC address. This example is extensively documented in the following tutorials: diff --git a/rust/basic_bitcoin/dfx.json b/rust/basic_bitcoin/dfx.json index 051597777..1ca1dd4e5 100644 --- a/rust/basic_bitcoin/dfx.json +++ b/rust/basic_bitcoin/dfx.json @@ -2,7 +2,7 @@ "version": 1, "canisters": { "basic_bitcoin": { - "type": "custom", + "type": "rust", "package": "basic_bitcoin", "candid": "src/basic_bitcoin/basic_bitcoin.did", "wasm": "target/wasm32-unknown-unknown/release/basic_bitcoin.wasm", @@ -11,7 +11,26 @@ { "name": "candid:service" } - ] + ], + "specified_id": "om77v-qqaaa-aaaap-ahmrq-cai", + "remote": { + "id": { + "ic": "om77v-qqaaa-aaaap-ahmrq-cai", + "playground": "om77v-qqaaa-aaaap-ahmrq-cai" + } + } + }, + "schnorr_canister": { + "type": "custom", + "candid": "https://github.com/domwoe/schnorr_canister/releases/latest/download/schnorr_canister.did", + "wasm": "https://github.com/domwoe/schnorr_canister/releases/latest/download/schnorr_canister.wasm.gz", + "specified_id": "6fwhw-fyaaa-aaaap-qb7ua-cai", + "remote": { + "id": { + "ic": "6fwhw-fyaaa-aaaap-qb7ua-cai", + "playground": "6fwhw-fyaaa-aaaap-qb7ua-cai" + } + } } }, "defaults": { @@ -26,10 +45,5 @@ "packtool": "", "args": "" } - }, - "networks": { - "local": { - "bind": "127.0.0.1:4943" - } } } diff --git a/rust/basic_bitcoin/rust-toolchain.toml b/rust/basic_bitcoin/rust-toolchain.toml index b5f878501..2e3facb68 100644 --- a/rust/basic_bitcoin/rust-toolchain.toml +++ b/rust/basic_bitcoin/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.68.0" +channel = "1.79.0" targets = ["wasm32-unknown-unknown"] diff --git a/rust/basic_bitcoin/src/basic_bitcoin/Cargo.toml b/rust/basic_bitcoin/src/basic_bitcoin/Cargo.toml index b41674372..002142c67 100644 --- a/rust/basic_bitcoin/src/basic_bitcoin/Cargo.toml +++ b/rust/basic_bitcoin/src/basic_bitcoin/Cargo.toml @@ -10,11 +10,8 @@ crate-type = ["cdylib"] [dependencies] hex = "0.4.3" -bitcoin = "0.28.1" -bs58 = "0.4.0" +bitcoin = "0.30.2" candid = "0.9.10" ic-cdk = "0.10.0" ic-cdk-macros = "0.7.1" -ripemd = "0.1.1" serde = "1.0.132" -sha2 = "0.10.2" diff --git a/rust/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did b/rust/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did index 7002f2579..1dc5c37d2 100644 --- a/rust/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did +++ b/rust/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did @@ -9,40 +9,62 @@ type transaction_id = text; type block_hash = blob; type network = variant { - regtest; - testnet; - mainnet; + regtest; + testnet; + mainnet; }; type outpoint = record { txid : blob; - vout : nat32 + vout : nat32; }; type utxo = record { - outpoint: outpoint; - value: satoshi; - height: nat32; + outpoint : outpoint; + value : satoshi; + height : nat32; }; type get_utxos_response = record { - utxos: vec utxo; - tip_block_hash: block_hash; - tip_height: nat32; - next_page: opt blob; + utxos : vec utxo; + tip_block_hash : block_hash; + tip_height : nat32; + next_page : opt blob; }; -service : (network) -> { - "get_p2pkh_address": () -> (bitcoin_address); +service : (network, text) -> { + "for_test_only_set_schnorr_canister_id" : (text) -> (); - "get_balance": (address: bitcoin_address) -> (satoshi); + "get_p2pkh_address" : () -> (bitcoin_address); - "get_utxos": (bitcoin_address) -> (get_utxos_response); + "get_p2tr_script_spend_address" : () -> (bitcoin_address); - "get_current_fee_percentiles": () -> (vec millisatoshi_per_vbyte); + "get_p2tr_raw_key_spend_address" : () -> (bitcoin_address); - "send": (record { - destination_address: bitcoin_address; - amount_in_satoshi: satoshi; - }) -> (transaction_id); -} + "get_balance" : (address : bitcoin_address) -> (satoshi); + + "get_utxos" : (bitcoin_address) -> (get_utxos_response); + + "get_current_fee_percentiles" : () -> (vec millisatoshi_per_vbyte); + + "send_from_p2pkh" : ( + record { + destination_address : bitcoin_address; + amount_in_satoshi : satoshi; + } + ) -> (transaction_id); + + "send_from_p2tr_script_spend" : ( + record { + destination_address : bitcoin_address; + amount_in_satoshi : satoshi; + } + ) -> (transaction_id); + + "send_from_p2tr_raw_key_spend" : ( + record { + destination_address : bitcoin_address; + amount_in_satoshi : satoshi; + } + ) -> (transaction_id); +}; diff --git a/rust/basic_bitcoin/src/basic_bitcoin/build.sh b/rust/basic_bitcoin/src/basic_bitcoin/build.sh index 4ecc823a6..e69de29bb 100755 --- a/rust/basic_bitcoin/src/basic_bitcoin/build.sh +++ b/rust/basic_bitcoin/src/basic_bitcoin/build.sh @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -TARGET="wasm32-unknown-unknown" -SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" - -pushd $SCRIPT_DIR - -# NOTE: On macOS a specific version of llvm-ar and clang need to be set here. -# Otherwise the wasm compilation of rust-secp256k1 will fail. -if [ "$(uname)" == "Darwin" ]; then - LLVM_PATH=$(brew --prefix llvm) - # On macs we need to use the brew versions - AR="${LLVM_PATH}/bin/llvm-ar" CC="${LLVM_PATH}/bin/clang" cargo build --target $TARGET --release -else - cargo build --target $TARGET --release -fi - -popd - diff --git a/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/common.rs b/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/common.rs new file mode 100644 index 000000000..20d234242 --- /dev/null +++ b/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/common.rs @@ -0,0 +1,116 @@ +use crate::bitcoin_api; +use bitcoin::{ + absolute::LockTime, blockdata::witness::Witness, hashes::Hash, Address, Network, OutPoint, + ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, +}; +use ic_cdk::api::management_canister::bitcoin::{BitcoinNetwork, Utxo}; + +pub fn build_transaction_with_fee( + own_utxos: &[Utxo], + own_address: &Address, + dst_address: &Address, + amount: u64, + fee: u64, +) -> Result<(Transaction, Vec), String> { + // Assume that any amount below this threshold is dust. + const DUST_THRESHOLD: u64 = 1_000; + + // Select which UTXOs to spend. We naively spend the oldest available UTXOs, + // even if they were previously spent in a transaction. This isn't a + // problem as long as at most one transaction is created per block and + // we're using min_confirmations of 1. + let mut utxos_to_spend = vec![]; + let mut total_spent = 0; + for utxo in own_utxos.iter().rev() { + total_spent += utxo.value; + utxos_to_spend.push(utxo); + if total_spent >= amount + fee { + // We have enough inputs to cover the amount we want to spend. + break; + } + } + + if total_spent < amount + fee { + return Err(format!( + "Insufficient balance: {}, trying to transfer {} satoshi with fee {}", + total_spent, amount, fee + )); + } + + let inputs: Vec = utxos_to_spend + .iter() + .map(|utxo| TxIn { + previous_output: OutPoint { + txid: Txid::from_raw_hash(Hash::from_slice(&utxo.outpoint.txid).unwrap()), + vout: utxo.outpoint.vout, + }, + sequence: Sequence::max_value(), + witness: Witness::new(), + script_sig: ScriptBuf::new(), + }) + .collect(); + + let prevouts = utxos_to_spend + .into_iter() + .map(|utxo| TxOut { + value: utxo.value, + script_pubkey: own_address.script_pubkey(), + }) + .collect(); + + let mut outputs = vec![TxOut { + script_pubkey: dst_address.script_pubkey(), + value: amount, + }]; + + let remaining_amount = total_spent - amount - fee; + + if remaining_amount >= DUST_THRESHOLD { + outputs.push(TxOut { + script_pubkey: own_address.script_pubkey(), + value: remaining_amount, + }); + } + + Ok(( + Transaction { + input: inputs, + output: outputs, + lock_time: LockTime::ZERO, + version: 2, + }, + prevouts, + )) +} + +pub fn transform_network(network: BitcoinNetwork) -> Network { + match network { + BitcoinNetwork::Mainnet => Network::Bitcoin, + BitcoinNetwork::Testnet => Network::Testnet, + BitcoinNetwork::Regtest => Network::Regtest, + } +} + +pub async fn get_fee_per_byte(network: BitcoinNetwork) -> u64 { + // Get fee percentiles from previous transactions to estimate our own fee. + let fee_percentiles = bitcoin_api::get_current_fee_percentiles(network).await; + + if fee_percentiles.is_empty() { + // There are no fee percentiles. This case can only happen on a regtest + // network where there are no non-coinbase transactions. In this case, + // we use a default of 2000 millisatoshis/byte (i.e. 2 satoshi/byte) + 2000 + } else { + // Choose the 50th percentile for sending fees. + fee_percentiles[50] + } +} + +// A mock for rubber-stamping signatures. +pub async fn mock_signer( + _key_name: String, + _derivation_path: Vec>, + _message_hash: Vec, +) -> Vec { + vec![255; 64] +} diff --git a/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/mod.rs b/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/mod.rs new file mode 100644 index 000000000..99308e1ef --- /dev/null +++ b/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/mod.rs @@ -0,0 +1,15 @@ +//! A demo of a very bare-bones bitcoin "wallet". +//! +//! The wallet here showcases how bitcoin addresses can be be computed +//! and how bitcoin transactions can be signed. It is missing several +//! pieces that any production-grade wallet would have, including: +//! +//! * Support for address types that aren't P2PKH, P2TR script spend, or P2TR +//! key spend with *untweaked* key. +//! * Caching spent UTXOs so that they are not reused in future transactions. +//! * Option to set the fee. + +mod common; +pub mod p2pkh; +pub mod p2tr_raw_key_spend; +pub mod p2tr_script_spend; \ No newline at end of file diff --git a/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet.rs b/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/p2pkh.rs similarity index 50% rename from rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet.rs rename to rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/p2pkh.rs index 75773eafd..f1b1ecd36 100644 --- a/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet.rs +++ b/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/p2pkh.rs @@ -1,30 +1,24 @@ -//! A demo of a very bare-bones bitcoin "wallet". -//! -//! The wallet here showcases how bitcoin addresses can be be computed -//! and how bitcoin transactions can be signed. It is missing several -//! pieces that any production-grade wallet would have, including: -//! -//! * Support for address types that aren't P2PKH. -//! * Caching spent UTXOs so that they are not reused in future transactions. -//! * Option to set the fee. use crate::{bitcoin_api, ecdsa_api}; -use bitcoin::util::psbt::serialize::Serialize; use bitcoin::{ - blockdata::{script::Builder, witness::Witness}, + consensus::serialize, hashes::Hash, - Address, AddressType, EcdsaSighashType, OutPoint, Script, Transaction, TxIn, TxOut, Txid, + script::{Builder, PushBytesBuf}, + sighash::{EcdsaSighashType, SighashCache}, + Address, AddressType, PublicKey, Transaction, Txid, }; use ic_cdk::api::management_canister::bitcoin::{ BitcoinNetwork, MillisatoshiPerByte, Satoshi, Utxo, }; use ic_cdk::print; -use sha2::Digest; +use std::convert::TryFrom; use std::str::FromStr; -const SIG_HASH_TYPE: EcdsaSighashType = EcdsaSighashType::All; +use super::common::transform_network; + +const ECDSA_SIG_HASH_TYPE: EcdsaSighashType = EcdsaSighashType::All; /// Returns the P2PKH address of this canister at the given derivation path. -pub async fn get_p2pkh_address( +pub async fn get_address( network: BitcoinNetwork, key_name: String, derivation_path: Vec>, @@ -46,18 +40,7 @@ pub async fn send( dst_address: String, amount: Satoshi, ) -> Txid { - // Get fee percentiles from previous transactions to estimate our own fee. - let fee_percentiles = bitcoin_api::get_current_fee_percentiles(network).await; - - let fee_per_byte = if fee_percentiles.is_empty() { - // There are no fee percentiles. This case can only happen on a regtest - // network where there are no non-coinbase transactions. In this case, - // we use a default of 2000 millisatoshis/byte (i.e. 2 satoshi/byte) - 2000 - } else { - // Choose the 50th percentile for sending fees. - fee_percentiles[50] - }; + let fee_per_byte = super::common::get_fee_per_byte(network).await; // Fetch our public key, P2PKH address, and UTXOs. let own_public_key = @@ -72,11 +55,17 @@ pub async fn send( .await .utxos; - let own_address = Address::from_str(&own_address).unwrap(); - let dst_address = Address::from_str(&dst_address).unwrap(); + let own_address = Address::from_str(&own_address) + .unwrap() + .require_network(super::common::transform_network(network)) + .expect("should be valid address for the network"); + let dst_address = Address::from_str(&dst_address) + .unwrap() + .require_network(super::common::transform_network(network)) + .expect("should be valid address for the network"); // Build the transaction that sends `amount` to the destination address. - let transaction = build_transaction( + let transaction = build_p2pkh_spend_tx( &own_public_key, &own_address, &own_utxos, @@ -86,11 +75,11 @@ pub async fn send( ) .await; - let tx_bytes = transaction.serialize(); + let tx_bytes = serialize(&transaction); print(format!("Transaction to sign: {}", hex::encode(tx_bytes))); // Sign the transaction. - let signed_transaction = sign_transaction( + let signed_transaction = ecdsa_sign_transaction( &own_public_key, &own_address, transaction, @@ -100,7 +89,7 @@ pub async fn send( ) .await; - let signed_transaction_bytes = signed_transaction.serialize(); + let signed_transaction_bytes = serialize(&signed_transaction); print(format!( "Signed transaction: {}", hex::encode(&signed_transaction_bytes) @@ -115,13 +104,13 @@ pub async fn send( // Builds a transaction to send the given `amount` of satoshis to the // destination address. -async fn build_transaction( +async fn build_p2pkh_spend_tx( own_public_key: &[u8], own_address: &Address, own_utxos: &[Utxo], dst_address: &Address, amount: Satoshi, - fee_per_byte: MillisatoshiPerByte, + fee_per_vbyte: MillisatoshiPerByte, ) -> Transaction { // We have a chicken-and-egg problem where we need to know the length // of the transaction in order to compute its proper fee, but we need @@ -134,100 +123,38 @@ async fn build_transaction( print("Building transaction..."); let mut total_fee = 0; loop { - let transaction = - build_transaction_with_fee(own_utxos, own_address, dst_address, amount, total_fee) - .expect("Error building transaction."); + let (transaction, _prevouts) = super::common::build_transaction_with_fee( + own_utxos, + own_address, + dst_address, + amount, + total_fee, + ) + .expect("Error building transaction."); // Sign the transaction. In this case, we only care about the size // of the signed transaction, so we use a mock signer here for efficiency. - let signed_transaction = sign_transaction( + let signed_transaction = ecdsa_sign_transaction( own_public_key, own_address, transaction.clone(), String::from(""), // mock key name vec![], // mock derivation path - mock_signer, + super::common::mock_signer, ) .await; - let signed_tx_bytes_len = signed_transaction.serialize().len() as u64; + let tx_vsize = signed_transaction.vsize() as u64; - if (signed_tx_bytes_len * fee_per_byte) / 1000 == total_fee { + if (tx_vsize * fee_per_vbyte) / 1000 == total_fee { print(format!("Transaction built with fee {}.", total_fee)); return transaction; } else { - total_fee = (signed_tx_bytes_len * fee_per_byte) / 1000; + total_fee = (tx_vsize * fee_per_vbyte) / 1000; } } } -fn build_transaction_with_fee( - own_utxos: &[Utxo], - own_address: &Address, - dst_address: &Address, - amount: u64, - fee: u64, -) -> Result { - // Assume that any amount below this threshold is dust. - const DUST_THRESHOLD: u64 = 1_000; - - // Select which UTXOs to spend. We naively spend the oldest available UTXOs, - // even if they were previously spent in a transaction. This isn't a - // problem as long as at most one transaction is created per block and - // we're using min_confirmations of 1. - let mut utxos_to_spend = vec![]; - let mut total_spent = 0; - for utxo in own_utxos.iter().rev() { - total_spent += utxo.value; - utxos_to_spend.push(utxo); - if total_spent >= amount + fee { - // We have enough inputs to cover the amount we want to spend. - break; - } - } - - if total_spent < amount + fee { - return Err(format!( - "Insufficient balance: {}, trying to transfer {} satoshi with fee {}", - total_spent, amount, fee - )); - } - - let inputs: Vec = utxos_to_spend - .into_iter() - .map(|utxo| TxIn { - previous_output: OutPoint { - txid: Txid::from_hash(Hash::from_slice(&utxo.outpoint.txid).unwrap()), - vout: utxo.outpoint.vout, - }, - sequence: 0xffffffff, - witness: Witness::new(), - script_sig: Script::new(), - }) - .collect(); - - let mut outputs = vec![TxOut { - script_pubkey: dst_address.script_pubkey(), - value: amount, - }]; - - let remaining_amount = total_spent - amount - fee; - - if remaining_amount >= DUST_THRESHOLD { - outputs.push(TxOut { - script_pubkey: own_address.script_pubkey(), - value: remaining_amount, - }); - } - - Ok(Transaction { - input: inputs, - output: outputs, - lock_time: 0, - version: 1, - }) -} - // Sign a bitcoin transaction. // // IMPORTANT: This method is for demonstration purposes only and it only @@ -235,7 +162,7 @@ fn build_transaction_with_fee( // // 1. All the inputs are referencing outpoints that are owned by `own_address`. // 2. `own_address` is a P2PKH address. -async fn sign_transaction( +async fn ecdsa_sign_transaction( own_public_key: &[u8], own_address: &Address, mut transaction: Transaction, @@ -256,19 +183,32 @@ where let txclone = transaction.clone(); for (index, input) in transaction.input.iter_mut().enumerate() { - let sighash = - txclone.signature_hash(index, &own_address.script_pubkey(), SIG_HASH_TYPE.to_u32()); - - let signature = signer(key_name.clone(), derivation_path.clone(), sighash.to_vec()).await; + let sighash = SighashCache::new(&txclone) + .legacy_signature_hash( + index, + &own_address.script_pubkey(), + ECDSA_SIG_HASH_TYPE.to_u32(), + ) + .unwrap(); + + let signature = signer( + key_name.clone(), + derivation_path.clone(), + sighash.as_byte_array().to_vec(), + ) + .await; // Convert signature to DER. let der_signature = sec1_to_der(signature); - let mut sig_with_hashtype = der_signature; - sig_with_hashtype.push(SIG_HASH_TYPE.to_u32() as u8); + let mut sig_with_hashtype: Vec = der_signature; + sig_with_hashtype.push(ECDSA_SIG_HASH_TYPE.to_u32() as u8); + + let sig_with_hashtype_push_bytes = PushBytesBuf::try_from(sig_with_hashtype).unwrap(); + let own_public_key_push_bytes = PushBytesBuf::try_from(own_public_key.to_vec()).unwrap(); input.script_sig = Builder::new() - .push_slice(sig_with_hashtype.as_slice()) - .push_slice(own_public_key) + .push_slice(sig_with_hashtype_push_bytes) + .push_slice(own_public_key_push_bytes) .into_script(); input.witness.clear(); } @@ -276,44 +216,13 @@ where transaction } -fn sha256(data: &[u8]) -> Vec { - let mut hasher = sha2::Sha256::new(); - hasher.update(data); - hasher.finalize().to_vec() -} -fn ripemd160(data: &[u8]) -> Vec { - let mut hasher = ripemd::Ripemd160::new(); - hasher.update(data); - hasher.finalize().to_vec() -} - // Converts a public key to a P2PKH address. fn public_key_to_p2pkh_address(network: BitcoinNetwork, public_key: &[u8]) -> String { - // SHA-256 & RIPEMD-160 - let result = ripemd160(&sha256(public_key)); - - let prefix = match network { - BitcoinNetwork::Testnet | BitcoinNetwork::Regtest => 0x6f, - BitcoinNetwork::Mainnet => 0x00, - }; - let mut data_with_prefix = vec![prefix]; - data_with_prefix.extend(result); - - let checksum = &sha256(&sha256(&data_with_prefix.clone()))[..4]; - - let mut full_address = data_with_prefix; - full_address.extend(checksum); - - bs58::encode(full_address).into_string() -} - -// A mock for rubber-stamping ECDSA signatures. -async fn mock_signer( - _key_name: String, - _derivation_path: Vec>, - _message_hash: Vec, -) -> Vec { - vec![255; 64] + Address::p2pkh( + &PublicKey::from_slice(public_key).expect("failed to parse public key"), + transform_network(network), + ) + .to_string() } // Converts a SEC1 ECDSA signature to the DER format. diff --git a/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/p2tr_raw_key_spend.rs b/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/p2tr_raw_key_spend.rs new file mode 100644 index 000000000..aa52d5ba8 --- /dev/null +++ b/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/p2tr_raw_key_spend.rs @@ -0,0 +1,207 @@ +use crate::{bitcoin_api, schnorr_api}; +use bitcoin::{ + blockdata::witness::Witness, + consensus::serialize, + hashes::Hash, + key::TweakedPublicKey, + secp256k1::{schnorr::Signature, PublicKey}, + sighash::{SighashCache, TapSighashType}, + Address, AddressType, ScriptBuf, Sequence, Transaction, TxOut, Txid, +}; +use ic_cdk::api::management_canister::bitcoin::{ + BitcoinNetwork, MillisatoshiPerByte, Satoshi, Utxo, +}; +use ic_cdk::print; +use std::str::FromStr; + +/// Returns the P2TR address of this canister at the given derivation path. +pub async fn get_address( + network: BitcoinNetwork, + key_name: String, + derivation_path: Vec>, +) -> Address { + let public_key = schnorr_api::schnorr_public_key(key_name, derivation_path).await; + let x_only_pubkey = + bitcoin::key::XOnlyPublicKey::from(PublicKey::from_slice(&public_key).unwrap()); + let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(x_only_pubkey); + Address::p2tr_tweaked(tweaked_pubkey, super::common::transform_network(network)) +} + +/// Sends a P2TR script spend transaction to the network that transfers the +/// given amount to the given destination, where the source of the funds is the +/// canister itself at the given derivation path. +pub async fn send( + network: BitcoinNetwork, + derivation_path: Vec>, + key_name: String, + dst_address: String, + amount: Satoshi, +) -> Txid { + let fee_per_byte = super::common::get_fee_per_byte(network).await; + + // Fetch our public key, P2PKH address, and UTXOs. + let own_public_key = + schnorr_api::schnorr_public_key(key_name.clone(), derivation_path.clone()).await; + let x_only_pubkey = + bitcoin::key::XOnlyPublicKey::from(PublicKey::from_slice(&own_public_key).unwrap()); + let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(x_only_pubkey); + + let own_address = + Address::p2tr_tweaked(tweaked_pubkey, super::common::transform_network(network)); + + print("Fetching UTXOs..."); + // Note that pagination may have to be used to get all UTXOs for the given address. + // For the sake of simplicity, it is assumed here that the `utxo` field in the response + // contains all UTXOs. + let own_utxos = bitcoin_api::get_utxos(network, own_address.to_string()) + .await + .utxos; + + let dst_address = Address::from_str(&dst_address) + .unwrap() + .require_network(super::common::transform_network(network)) + .expect("should be valid address for the network"); + // Build the transaction that sends `amount` to the destination address. + let (transaction, prevouts) = + build_p2tr_key_path_spend_tx(&own_address, &own_utxos, &dst_address, amount, fee_per_byte) + .await; + + let tx_bytes = serialize(&transaction); + print(format!("Transaction to sign: {}", hex::encode(tx_bytes))); + + // Sign the transaction. + let signed_transaction = schnorr_sign_key_spend_transaction( + &own_address, + transaction, + prevouts.as_slice(), + key_name, + derivation_path, + schnorr_api::sign_with_schnorr, + ) + .await; + + let signed_transaction_bytes = serialize(&signed_transaction); + print(format!( + "Signed transaction: {}", + hex::encode(&signed_transaction_bytes) + )); + + print("Sending transaction..."); + bitcoin_api::send_transaction(network, signed_transaction_bytes).await; + print("Done"); + + signed_transaction.txid() +} + +// Builds a transaction to send the given `amount` of satoshis to the +// destination address. +async fn build_p2tr_key_path_spend_tx( + own_address: &Address, + own_utxos: &[Utxo], + dst_address: &Address, + amount: Satoshi, + fee_per_vbyte: MillisatoshiPerByte, +) -> (Transaction, Vec) { + // We have a chicken-and-egg problem where we need to know the length + // of the transaction in order to compute its proper fee, but we need + // to know the proper fee in order to figure out the inputs needed for + // the transaction. + // + // We solve this problem iteratively. We start with a fee of zero, build + // and sign a transaction, see what its size is, and then update the fee, + // rebuild the transaction, until the fee is set to the correct amount. + print("Building transaction..."); + let mut total_fee = 0; + loop { + let (transaction, prevouts) = super::common::build_transaction_with_fee( + own_utxos, + own_address, + dst_address, + amount, + total_fee, + ) + .expect("Error building transaction."); + + // Sign the transaction. In this case, we only care about the size + // of the signed transaction, so we use a mock signer here for efficiency. + let signed_transaction = schnorr_sign_key_spend_transaction( + own_address, + transaction.clone(), + &prevouts, + String::from(""), // mock key name + vec![], // mock derivation path + super::common::mock_signer, + ) + .await; + + let tx_vsize = signed_transaction.vsize() as u64; + + if (tx_vsize * fee_per_vbyte) / 1000 == total_fee { + print(format!("Transaction built with fee {}.", total_fee)); + return (transaction, prevouts); + } else { + total_fee = (tx_vsize * fee_per_vbyte) / 1000; + } + } +} + +// Sign a P2TR key spend transaction. +// +// IMPORTANT: This method is for demonstration purposes only and it only +// supports signing transactions if: +// +// 1. All the inputs are referencing outpoints that are owned by `own_address`. +// 2. `own_address` is a P2TR script path spend address. +async fn schnorr_sign_key_spend_transaction( + own_address: &Address, + mut transaction: Transaction, + prevouts: &[TxOut], + key_name: String, + derivation_path: Vec>, + signer: SignFun, +) -> Transaction +where + SignFun: Fn(String, Vec>, Vec) -> Fut, + Fut: std::future::Future>, +{ + assert_eq!(own_address.address_type(), Some(AddressType::P2tr),); + + for input in transaction.input.iter_mut() { + input.script_sig = ScriptBuf::default(); + input.witness = Witness::default(); + input.sequence = Sequence::ENABLE_RBF_NO_LOCKTIME; + } + + let num_inputs = transaction.input.len(); + + for i in 0..num_inputs { + let mut sighasher = SighashCache::new(&mut transaction); + + let signing_data = sighasher + .taproot_key_spend_signature_hash( + i, + &bitcoin::sighash::Prevouts::All(&prevouts), + TapSighashType::Default, + ) + .expect("Failed to ecnode signing data") + .as_byte_array() + .to_vec(); + + let raw_signature = signer( + key_name.clone(), + derivation_path.clone(), + signing_data.clone(), + ) + .await; + + // Update the witness stack. + let witness = sighasher.witness_mut(i).unwrap(); + let signature = bitcoin::taproot::Signature { + sig: Signature::from_slice(&raw_signature).expect("failed to parse signature"), + hash_ty: TapSighashType::Default, + }; + witness.push(&signature.to_vec()); + } + + transaction +} diff --git a/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/p2tr_script_spend.rs b/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/p2tr_script_spend.rs new file mode 100644 index 000000000..2c23147aa --- /dev/null +++ b/rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/p2tr_script_spend.rs @@ -0,0 +1,269 @@ +use crate::{bitcoin_api, schnorr_api}; +use bitcoin::{ + blockdata::witness::Witness, + consensus::serialize, + hashes::Hash, + key::XOnlyPublicKey, + secp256k1::{schnorr::Signature, PublicKey, Secp256k1}, + sighash::{SighashCache, TapSighashType}, + taproot::{ControlBlock, LeafVersion, TapLeafHash, TaprootBuilder, TaprootSpendInfo}, + Address, AddressType, ScriptBuf, Sequence, Transaction, TxOut, Txid, +}; +use ic_cdk::api::management_canister::bitcoin::{ + BitcoinNetwork, MillisatoshiPerByte, Satoshi, Utxo, +}; +use ic_cdk::print; +use std::str::FromStr; + +/// Returns the P2TR address of this canister at the given derivation path. +pub async fn get_address( + network: BitcoinNetwork, + key_name: String, + derivation_path: Vec>, +) -> Address { + let public_key = schnorr_api::schnorr_public_key(key_name, derivation_path).await; + public_key_to_p2tr_script_spend_address(network, public_key.as_slice()) +} + +// Converts a public key to a P2TR address. To compute the address, the public +// key is tweaked with the taproot value, which is computed from the public key +// and the Merkelized Abstract Syntax Tree (MAST, essentially a Merkle tree +// containing scripts, in our case just one). Addresses are computed differently +// for different Bitcoin networks. +pub fn public_key_to_p2tr_script_spend_address( + bitcoin_network: BitcoinNetwork, + public_key: &[u8], +) -> Address { + let network = super::common::transform_network(bitcoin_network); + let taproot_spend_info = p2tr_scipt_spend_info(public_key); + Address::p2tr_tweaked(taproot_spend_info.output_key(), network) +} + +fn p2tr_scipt_spend_info(public_key: &[u8]) -> TaprootSpendInfo { + let spend_script = p2tr_script(public_key); + let dummy_random_secp256k1 = Secp256k1::new(); + let schnorr_public_key = XOnlyPublicKey::from(PublicKey::from_slice(&public_key).unwrap()); + + TaprootBuilder::new() + .add_leaf(0, spend_script.clone()) + .expect("adding leaf should work") + .finalize(&dummy_random_secp256k1, schnorr_public_key) + .expect("finalizing taproot builder should work") +} + +/// Computes a simple P2TR script that allows the `public_key` and no other keys +/// to be used for spending. +fn p2tr_script(public_key: &[u8]) -> ScriptBuf { + let x_only_public_key = XOnlyPublicKey::from(PublicKey::from_slice(public_key).unwrap()); + bitcoin::blockdata::script::Builder::new() + .push_x_only_key(&x_only_public_key) + .push_opcode(bitcoin::blockdata::opcodes::all::OP_CHECKSIG) + .into_script() +} + +/// Sends a P2TR script spend transaction to the network that transfers the +/// given amount to the given destination, where the source of the funds is the +/// canister itself at the given derivation path. +pub async fn send( + network: BitcoinNetwork, + derivation_path: Vec>, + key_name: String, + dst_address: String, + amount: Satoshi, +) -> Txid { + let fee_per_byte = super::common::get_fee_per_byte(network).await; + + // Fetch our public key, P2PKH address, and UTXOs. + let own_public_key = + schnorr_api::schnorr_public_key(key_name.clone(), derivation_path.clone()).await; + let taproot_spend_info = p2tr_scipt_spend_info(own_public_key.as_slice()); + + let own_address = Address::p2tr_tweaked( + taproot_spend_info.output_key(), + super::common::transform_network(network), + ); + + print("Fetching UTXOs..."); + // Note that pagination may have to be used to get all UTXOs for the given address. + // For the sake of simplicity, it is assumed here that the `utxo` field in the response + // contains all UTXOs. + let own_utxos = bitcoin_api::get_utxos(network, own_address.to_string()) + .await + .utxos; + + let dst_address = Address::from_str(&dst_address) + .unwrap() + .require_network(super::common::transform_network(network)) + .expect("should be valid address for the network"); + + let script = p2tr_script(own_public_key.as_slice()); + let control_block = taproot_spend_info + .control_block(&(script.clone(), LeafVersion::TapScript)) + .expect("should compute control block"); + // Build the transaction that sends `amount` to the destination address. + let (transaction, prevouts) = build_p2tr_script_path_spend_tx( + &own_address, + &control_block, + &script, + &own_utxos, + &dst_address, + amount, + fee_per_byte, + ) + .await; + + let tx_bytes = serialize(&transaction); + print(format!("Transaction to sign: {}", hex::encode(tx_bytes))); + + // Sign the transaction. + let signed_transaction = schnorr_sign_script_spend_transaction( + &own_address, + transaction, + prevouts.as_slice(), + &control_block, + &script, + key_name, + derivation_path, + schnorr_api::sign_with_schnorr, + ) + .await; + + let signed_transaction_bytes = serialize(&signed_transaction); + print(format!( + "Signed transaction: {}", + hex::encode(&signed_transaction_bytes) + )); + + print("Sending transaction..."); + bitcoin_api::send_transaction(network, signed_transaction_bytes).await; + print("Done"); + + signed_transaction.txid() +} + +// Builds a transaction to send the given `amount` of satoshis to the +// destination address. +async fn build_p2tr_script_path_spend_tx( + own_address: &Address, + control_block: &ControlBlock, + script: &ScriptBuf, + own_utxos: &[Utxo], + dst_address: &Address, + amount: Satoshi, + fee_per_byte: MillisatoshiPerByte, +) -> (Transaction, Vec) { + // We have a chicken-and-egg problem where we need to know the length + // of the transaction in order to compute its proper fee, but we need + // to know the proper fee in order to figure out the inputs needed for + // the transaction. + // + // We solve this problem iteratively. We start with a fee of zero, build + // and sign a transaction, see what its size is, and then update the fee, + // rebuild the transaction, until the fee is set to the correct amount. + print("Building transaction..."); + let mut total_fee = 0; + loop { + let (transaction, prevouts) = super::common::build_transaction_with_fee( + own_utxos, + own_address, + dst_address, + amount, + total_fee, + ) + .expect("Error building transaction."); + + // Sign the transaction. In this case, we only care about the size + // of the signed transaction, so we use a mock signer here for efficiency. + let signed_transaction = schnorr_sign_script_spend_transaction( + own_address, + transaction.clone(), + &prevouts, + control_block, + script, + String::from(""), // mock key name + vec![], // mock derivation path + super::common::mock_signer, + ) + .await; + + let tx_vsize = signed_transaction.vsize() as u64; + + if (tx_vsize * fee_per_byte) / 1000 == total_fee { + print(format!("Transaction built with fee {}.", total_fee)); + return (transaction, prevouts); + } else { + total_fee = (tx_vsize * fee_per_byte) / 1000; + } + } +} + +// Sign a P2TR script spend transaction. +// +// IMPORTANT: This method is for demonstration purposes only and it only +// supports signing transactions if: +// +// 1. All the inputs are referencing outpoints that are owned by `own_address`. +// 2. `own_address` is a P2TR script path spend address. +async fn schnorr_sign_script_spend_transaction( + own_address: &Address, + mut transaction: Transaction, + prevouts: &[TxOut], + control_block: &ControlBlock, + script: &ScriptBuf, + key_name: String, + derivation_path: Vec>, + signer: SignFun, +) -> Transaction +where + SignFun: Fn(String, Vec>, Vec) -> Fut, + Fut: std::future::Future>, +{ + assert_eq!(own_address.address_type(), Some(AddressType::P2tr),); + + for input in transaction.input.iter_mut() { + input.script_sig = ScriptBuf::default(); + input.witness = Witness::default(); + input.sequence = Sequence::ENABLE_RBF_NO_LOCKTIME; + } + + let num_inputs = transaction.input.len(); + + for i in 0..num_inputs { + let mut sighasher = SighashCache::new(&mut transaction); + + let leaf_hash = TapLeafHash::from_script(&script, LeafVersion::TapScript); + + let signing_data = sighasher + .taproot_script_spend_signature_hash( + i, + &bitcoin::sighash::Prevouts::All(&prevouts), + // &bitcoin::sighash::Prevouts::All(&prevouts[i..i + 1]), + leaf_hash, + TapSighashType::Default, + ) + .expect("Failed to ecnode signing data") + .as_byte_array() + .to_vec(); + + let raw_signature = signer( + key_name.clone(), + derivation_path.clone(), + signing_data.clone(), + ) + .await; + + // Update the witness stack. + + let witness = sighasher.witness_mut(i).unwrap(); + witness.clear(); + let signature = bitcoin::taproot::Signature { + sig: Signature::from_slice(&raw_signature).expect("failed to parse signature"), + hash_ty: TapSighashType::Default, + }; + witness.push(signature.to_vec()); + witness.push(&script.to_bytes()); + witness.push(control_block.serialize()); + } + + transaction +} diff --git a/rust/basic_bitcoin/src/basic_bitcoin/src/lib.rs b/rust/basic_bitcoin/src/basic_bitcoin/src/lib.rs index bd6284fbe..d813588f1 100644 --- a/rust/basic_bitcoin/src/basic_bitcoin/src/lib.rs +++ b/rust/basic_bitcoin/src/basic_bitcoin/src/lib.rs @@ -1,13 +1,15 @@ mod bitcoin_api; mod bitcoin_wallet; mod ecdsa_api; +mod schnorr_api; mod types; use ic_cdk::api::management_canister::bitcoin::{ BitcoinNetwork, GetUtxosResponse, MillisatoshiPerByte, }; -use ic_cdk_macros::{init, post_upgrade, pre_upgrade, update}; +use ic_cdk_macros::{init, update}; use std::cell::{Cell, RefCell}; +use std::env; thread_local! { // The bitcoin network to connect to. @@ -17,15 +19,17 @@ thread_local! { // `Mainnet` is currently unsupported. static NETWORK: Cell = Cell::new(BitcoinNetwork::Testnet); - // The derivation path to use for ECDSA secp256k1. + // The derivation path to use for the threshold key. static DERIVATION_PATH: Vec> = vec![]; // The ECDSA key name. static KEY_NAME: RefCell = RefCell::new(String::from("")); + + pub static SCHNORR_CANISTER: RefCell = RefCell::new(String::from("")); } #[init] -pub fn init(network: BitcoinNetwork) { +pub fn init(network: BitcoinNetwork, schnorr_canister: String) { NETWORK.with(|n| n.set(network)); KEY_NAME.with(|key_name| { @@ -36,6 +40,24 @@ pub fn init(network: BitcoinNetwork) { BitcoinNetwork::Mainnet | BitcoinNetwork::Testnet => "test_key_1", })) }); + + SCHNORR_CANISTER.with(|schnorr_canister_id| { + let canister_id = env::var("CANISTER_ID_SCHNORR_CANISTER").unwrap_or(schnorr_canister); + ic_cdk::println!("CANISTER_ID_SCHNORR_CANISTER: {}", &canister_id); + schnorr_canister_id.replace(canister_id); + }); +} + +#[update] +pub fn for_test_only_set_schnorr_canister_id(new_schnorr_canister_id: String) { + SCHNORR_CANISTER.with(|schnorr_canister_id| { + ic_cdk::println!( + "Changing schnorr canister id from {} to {}", + schnorr_canister_id.borrow(), + new_schnorr_canister_id + ); + schnorr_canister_id.replace(new_schnorr_canister_id); + }); } /// Returns the balance of the given bitcoin address. @@ -66,17 +88,50 @@ pub async fn get_p2pkh_address() -> String { let derivation_path = DERIVATION_PATH.with(|d| d.clone()); let key_name = KEY_NAME.with(|kn| kn.borrow().to_string()); let network = NETWORK.with(|n| n.get()); - bitcoin_wallet::get_p2pkh_address(network, key_name, derivation_path).await + bitcoin_wallet::p2pkh::get_address(network, key_name, derivation_path).await } -/// Sends the given amount of bitcoin from this canister to the given address. +/// Sends the given amount of bitcoin from this canister's p2pkh address to the given address. /// Returns the transaction ID. #[update] -pub async fn send(request: types::SendRequest) -> String { +pub async fn send_from_p2pkh(request: types::SendRequest) -> String { let derivation_path = DERIVATION_PATH.with(|d| d.clone()); let network = NETWORK.with(|n| n.get()); let key_name = KEY_NAME.with(|kn| kn.borrow().to_string()); - let tx_id = bitcoin_wallet::send( + let tx_id = bitcoin_wallet::p2pkh::send( + network, + derivation_path, + key_name, + request.destination_address, + request.amount_in_satoshi, + ) + .await; + + tx_id.to_string() +} + +/// Returns the P2TR address of this canister at a specific derivation path. +#[update] +pub async fn get_p2tr_script_spend_address() -> String { + let mut derivation_path = DERIVATION_PATH.with(|d| d.clone()); + derivation_path.push(b"script_spend".to_vec()); + let key_name = KEY_NAME.with(|kn| kn.borrow().to_string()); + let network = NETWORK.with(|n| n.get()); + + bitcoin_wallet::p2tr_script_spend::get_address(network, key_name, derivation_path) + .await + .to_string() +} + +/// Sends the given amount of bitcoin from this canister's p2tr address to the given address. +/// Returns the transaction ID. +#[update] +pub async fn send_from_p2tr_script_spend(request: types::SendRequest) -> String { + let mut derivation_path = DERIVATION_PATH.with(|d| d.clone()); + derivation_path.push(b"script_spend".to_vec()); + let network = NETWORK.with(|n| n.get()); + let key_name = KEY_NAME.with(|kn| kn.borrow().to_string()); + let tx_id = bitcoin_wallet::p2tr_script_spend::send( network, derivation_path, key_name, @@ -88,17 +143,40 @@ pub async fn send(request: types::SendRequest) -> String { tx_id.to_string() } -#[pre_upgrade] -fn pre_upgrade() { +/// Returns the P2TR address of this canister at a specific derivation path. +#[update] +pub async fn get_p2tr_raw_key_spend_address() -> String { + let mut derivation_path = DERIVATION_PATH.with(|d| d.clone()); + derivation_path.push(b"key_spend".to_vec()); + let key_name = KEY_NAME.with(|kn| kn.borrow().to_string()); let network = NETWORK.with(|n| n.get()); - ic_cdk::storage::stable_save((network,)).expect("Saving network to stable store must succeed."); + + bitcoin_wallet::p2tr_raw_key_spend::get_address(network, key_name, derivation_path) + .await + .to_string() } -#[post_upgrade] -fn post_upgrade() { - let network = ic_cdk::storage::stable_restore::<(BitcoinNetwork,)>() - .expect("Failed to read network from stable memory.") - .0; +/// Sends the given amount of bitcoin from this canister's p2tr address to the +/// given address. Returns the transaction ID. +/// +/// IMPORTANT: This function uses an untweaked key as the spending key. +/// +/// WARNING: This function is not suited for multi-party scenarios where +/// multiple keys are used for spending. +#[update] +pub async fn send_from_p2tr_raw_key_spend(request: types::SendRequest) -> String { + let mut derivation_path = DERIVATION_PATH.with(|d| d.clone()); + derivation_path.push(b"key_spend".to_vec()); + let network = NETWORK.with(|n| n.get()); + let key_name = KEY_NAME.with(|kn| kn.borrow().to_string()); + let tx_id = bitcoin_wallet::p2tr_raw_key_spend::send( + network, + derivation_path, + key_name, + request.destination_address, + request.amount_in_satoshi, + ) + .await; - init(network); + tx_id.to_string() } diff --git a/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs b/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs new file mode 100644 index 000000000..67ecab76a --- /dev/null +++ b/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs @@ -0,0 +1,96 @@ +use candid::{CandidType, Deserialize, Principal}; +use serde::Serialize; + +use crate::SCHNORR_CANISTER; + +#[derive(CandidType, Deserialize, Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum SchnorrAlgorithm { + #[serde(rename = "bip340secp256k1")] + Bip340Secp256k1, + #[serde(rename = "ed25519")] + Ed25519, +} + +#[derive(CandidType, Deserialize, Serialize, Debug, Clone)] +struct SchnorrKeyId { + pub name: String, + pub algorithm: SchnorrAlgorithm, +} + +#[derive(CandidType, Deserialize, Serialize, Debug)] +struct SchnorrPublicKey { + pub canister_id: Option, + pub derivation_path: Vec>, + pub key_id: SchnorrKeyId, +} + +#[derive(CandidType, Deserialize, Debug)] +struct SchnorrPublicKeyReply { + pub public_key: Vec, + pub chain_code: Vec, +} + +#[derive(CandidType, Deserialize, Serialize, Debug)] +struct SignWithSchnorr { + pub message: Vec, + pub derivation_path: Vec>, + pub key_id: SchnorrKeyId, +} + +#[derive(CandidType, Deserialize, Debug)] +struct SignWithSchnorrReply { + pub signature: Vec, +} + +/// Returns the Schnorr public key of this canister at the given derivation path. +pub async fn schnorr_public_key(key_name: String, derivation_path: Vec>) -> Vec { + let canister_id = SCHNORR_CANISTER.with(|schnorr_canister| { + ic_cdk::println!( + "CANISTER_ID_SCHNORR_CANISTER: {:?}", + &schnorr_canister.borrow() + ); + + Principal::from_text(schnorr_canister.borrow().as_str()).unwrap() + }); + + let res: Result<(SchnorrPublicKeyReply,), _> = ic_cdk::call( + canister_id, + "schnorr_public_key", + (SchnorrPublicKey { + canister_id: None, + derivation_path, + key_id: SchnorrKeyId { + name: key_name, + algorithm: SchnorrAlgorithm::Bip340Secp256k1, + }, + },), + ) + .await; + + res.unwrap().0.public_key +} + +pub async fn sign_with_schnorr( + key_name: String, + derivation_path: Vec>, + message: Vec, +) -> Vec { + let canister_id = SCHNORR_CANISTER + .with(|schnorr_canister| Principal::from_text(schnorr_canister.borrow().as_str()).unwrap()); + + let res: Result<(SignWithSchnorrReply,), _> = ic_cdk::call( + canister_id, + "sign_with_schnorr", + (SignWithSchnorr { + message, + derivation_path, + key_id: SchnorrKeyId { + name: key_name, + algorithm: SchnorrAlgorithm::Bip340Secp256k1, + }, + },), + ) + .await; + + res.unwrap().0.signature +} From 063be9a658871b8908c30735f2814b06de2c3f3e Mon Sep 17 00:00:00 2001 From: Oleksandr Tkachenko Date: Fri, 5 Jul 2024 19:32:40 +0200 Subject: [PATCH 02/13] resolve readme todos --- rust/basic_bitcoin/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/basic_bitcoin/README.md b/rust/basic_bitcoin/README.md index 1d882aa1d..d97b22887 100644 --- a/rust/basic_bitcoin/README.md +++ b/rust/basic_bitcoin/README.md @@ -13,7 +13,7 @@ This tutorial will walk you through how to deploy a sample [canister smart contr This example internally leverages the [ECDSA API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-ecdsa_public_key), -[Schnorr API TODO](TODO), and [Bitcoin +[Schnorr API](https://org5p-7iaaa-aaaak-qckna-cai.icp0.io/docs#ic-sign_with_schnorr), and [Bitcoin API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-bitcoin-api) of the Internet Computer. @@ -189,7 +189,7 @@ The `send_from_${type}` endpoint can send bitcoin by: 4. Signing the inputs of the transaction using the [sign_with_ecdsa API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-method-sign_with_ecdsa)/\ - [sign_with_schnorr](TODO). + [sign_with_schnorr](https://org5p-7iaaa-aaaak-qckna-cai.icp0.io/docs#ic-sign_with_schnorr). 5. Sending the signed transaction to the Bitcoin network using the [bitcoin_send_transaction API](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-method-bitcoin_send_transaction). This canister's `send_from_${type}` endpoint returns the ID of the transaction From 3b69e4874cc3153e9e5d11c5a6294250b893d9a9 Mon Sep 17 00:00:00 2001 From: Oleksandr Tkachenko Date: Fri, 5 Jul 2024 21:13:30 +0200 Subject: [PATCH 03/13] revert removal of build.sh --- rust/basic_bitcoin/src/basic_bitcoin/build.sh | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/rust/basic_bitcoin/src/basic_bitcoin/build.sh b/rust/basic_bitcoin/src/basic_bitcoin/build.sh index e69de29bb..4ecc823a6 100755 --- a/rust/basic_bitcoin/src/basic_bitcoin/build.sh +++ b/rust/basic_bitcoin/src/basic_bitcoin/build.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +TARGET="wasm32-unknown-unknown" +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +pushd $SCRIPT_DIR + +# NOTE: On macOS a specific version of llvm-ar and clang need to be set here. +# Otherwise the wasm compilation of rust-secp256k1 will fail. +if [ "$(uname)" == "Darwin" ]; then + LLVM_PATH=$(brew --prefix llvm) + # On macs we need to use the brew versions + AR="${LLVM_PATH}/bin/llvm-ar" CC="${LLVM_PATH}/bin/clang" cargo build --target $TARGET --release +else + cargo build --target $TARGET --release +fi + +popd + From 2df74a404d4753e9029051654851f7a03a330353 Mon Sep 17 00:00:00 2001 From: Oleksandr Tkachenko Date: Fri, 5 Jul 2024 21:24:37 +0200 Subject: [PATCH 04/13] revert type custom -> rust; is this why CI is failing? --- rust/basic_bitcoin/dfx.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/basic_bitcoin/dfx.json b/rust/basic_bitcoin/dfx.json index 1ca1dd4e5..4ab18481a 100644 --- a/rust/basic_bitcoin/dfx.json +++ b/rust/basic_bitcoin/dfx.json @@ -2,7 +2,7 @@ "version": 1, "canisters": { "basic_bitcoin": { - "type": "rust", + "type": "custom", "package": "basic_bitcoin", "candid": "src/basic_bitcoin/basic_bitcoin.did", "wasm": "target/wasm32-unknown-unknown/release/basic_bitcoin.wasm", From fb9856929d4d45bb7b8ed812891bd89b1d72bd03 Mon Sep 17 00:00:00 2001 From: Oleksandr Tkachenko Date: Tue, 9 Jul 2024 12:54:51 +0200 Subject: [PATCH 05/13] rm schnorr API mock and minor cleanup --- rust/basic_bitcoin/Makefile | 12 +---- rust/basic_bitcoin/dfx.json | 12 ----- .../src/basic_bitcoin/basic_bitcoin.did | 4 +- .../src/basic_bitcoin/src/lib.rs | 36 +++++---------- .../src/basic_bitcoin/src/schnorr_api.rs | 18 +------- .../src/basic_bitcoin/src/types.rs | 45 ------------------- 6 files changed, 14 insertions(+), 113 deletions(-) delete mode 100644 rust/basic_bitcoin/src/basic_bitcoin/src/types.rs diff --git a/rust/basic_bitcoin/Makefile b/rust/basic_bitcoin/Makefile index 61f98b2df..c5d6d7a66 100644 --- a/rust/basic_bitcoin/Makefile +++ b/rust/basic_bitcoin/Makefile @@ -4,17 +4,7 @@ all: deploy .PHONY: deploy .SILENT: deploy deploy: - dfx deploy schnorr_canister && dfx deploy basic_bitcoin --argument '(variant { regtest }, "dfx_test_key" : text)' - -.PHONY: mock -.SILENT: mock -mock: deploy - SCHNORR_MOCK_CANISTER_ID=$(shell dfx canister id schnorr_canister); \ - THIS_CANISTER_ID=$(shell dfx canister id basic_bitcoin); \ - echo "Changing to using mock canister instead of management canister for signing"; \ - CMD="dfx canister call "$${THIS_CANISTER_ID}" for_test_only_set_schnorr_canister_id '("\"$${SCHNORR_MOCK_CANISTER_ID}\"")'"; \ - echo "$${CMD}"; \ - eval "$${CMD}" + dfx deploy basic_bitcoin --argument '(variant { regtest })' .PHONY: regtest_topup .SILENT: regtest_topup diff --git a/rust/basic_bitcoin/dfx.json b/rust/basic_bitcoin/dfx.json index 4ab18481a..40ccfeb2e 100644 --- a/rust/basic_bitcoin/dfx.json +++ b/rust/basic_bitcoin/dfx.json @@ -19,18 +19,6 @@ "playground": "om77v-qqaaa-aaaap-ahmrq-cai" } } - }, - "schnorr_canister": { - "type": "custom", - "candid": "https://github.com/domwoe/schnorr_canister/releases/latest/download/schnorr_canister.did", - "wasm": "https://github.com/domwoe/schnorr_canister/releases/latest/download/schnorr_canister.wasm.gz", - "specified_id": "6fwhw-fyaaa-aaaap-qb7ua-cai", - "remote": { - "id": { - "ic": "6fwhw-fyaaa-aaaap-qb7ua-cai", - "playground": "6fwhw-fyaaa-aaaap-qb7ua-cai" - } - } } }, "defaults": { diff --git a/rust/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did b/rust/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did index 1dc5c37d2..097a28e64 100644 --- a/rust/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did +++ b/rust/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did @@ -32,9 +32,7 @@ type get_utxos_response = record { next_page : opt blob; }; -service : (network, text) -> { - "for_test_only_set_schnorr_canister_id" : (text) -> (); - +service : (network) -> { "get_p2pkh_address" : () -> (bitcoin_address); "get_p2tr_script_spend_address" : () -> (bitcoin_address); diff --git a/rust/basic_bitcoin/src/basic_bitcoin/src/lib.rs b/rust/basic_bitcoin/src/basic_bitcoin/src/lib.rs index d813588f1..ec417f99e 100644 --- a/rust/basic_bitcoin/src/basic_bitcoin/src/lib.rs +++ b/rust/basic_bitcoin/src/basic_bitcoin/src/lib.rs @@ -2,14 +2,12 @@ mod bitcoin_api; mod bitcoin_wallet; mod ecdsa_api; mod schnorr_api; -mod types; use ic_cdk::api::management_canister::bitcoin::{ BitcoinNetwork, GetUtxosResponse, MillisatoshiPerByte, }; use ic_cdk_macros::{init, update}; use std::cell::{Cell, RefCell}; -use std::env; thread_local! { // The bitcoin network to connect to. @@ -24,12 +22,10 @@ thread_local! { // The ECDSA key name. static KEY_NAME: RefCell = RefCell::new(String::from("")); - - pub static SCHNORR_CANISTER: RefCell = RefCell::new(String::from("")); } #[init] -pub fn init(network: BitcoinNetwork, schnorr_canister: String) { +pub fn init(network: BitcoinNetwork) { NETWORK.with(|n| n.set(network)); KEY_NAME.with(|key_name| { @@ -40,24 +36,6 @@ pub fn init(network: BitcoinNetwork, schnorr_canister: String) { BitcoinNetwork::Mainnet | BitcoinNetwork::Testnet => "test_key_1", })) }); - - SCHNORR_CANISTER.with(|schnorr_canister_id| { - let canister_id = env::var("CANISTER_ID_SCHNORR_CANISTER").unwrap_or(schnorr_canister); - ic_cdk::println!("CANISTER_ID_SCHNORR_CANISTER: {}", &canister_id); - schnorr_canister_id.replace(canister_id); - }); -} - -#[update] -pub fn for_test_only_set_schnorr_canister_id(new_schnorr_canister_id: String) { - SCHNORR_CANISTER.with(|schnorr_canister_id| { - ic_cdk::println!( - "Changing schnorr canister id from {} to {}", - schnorr_canister_id.borrow(), - new_schnorr_canister_id - ); - schnorr_canister_id.replace(new_schnorr_canister_id); - }); } /// Returns the balance of the given bitcoin address. @@ -94,7 +72,7 @@ pub async fn get_p2pkh_address() -> String { /// Sends the given amount of bitcoin from this canister's p2pkh address to the given address. /// Returns the transaction ID. #[update] -pub async fn send_from_p2pkh(request: types::SendRequest) -> String { +pub async fn send_from_p2pkh(request: SendRequest) -> String { let derivation_path = DERIVATION_PATH.with(|d| d.clone()); let network = NETWORK.with(|n| n.get()); let key_name = KEY_NAME.with(|kn| kn.borrow().to_string()); @@ -126,7 +104,7 @@ pub async fn get_p2tr_script_spend_address() -> String { /// Sends the given amount of bitcoin from this canister's p2tr address to the given address. /// Returns the transaction ID. #[update] -pub async fn send_from_p2tr_script_spend(request: types::SendRequest) -> String { +pub async fn send_from_p2tr_script_spend(request: SendRequest) -> String { let mut derivation_path = DERIVATION_PATH.with(|d| d.clone()); derivation_path.push(b"script_spend".to_vec()); let network = NETWORK.with(|n| n.get()); @@ -164,7 +142,7 @@ pub async fn get_p2tr_raw_key_spend_address() -> String { /// WARNING: This function is not suited for multi-party scenarios where /// multiple keys are used for spending. #[update] -pub async fn send_from_p2tr_raw_key_spend(request: types::SendRequest) -> String { +pub async fn send_from_p2tr_raw_key_spend(request: SendRequest) -> String { let mut derivation_path = DERIVATION_PATH.with(|d| d.clone()); derivation_path.push(b"key_spend".to_vec()); let network = NETWORK.with(|n| n.get()); @@ -180,3 +158,9 @@ pub async fn send_from_p2tr_raw_key_spend(request: types::SendRequest) -> String tx_id.to_string() } + +#[derive(candid::CandidType, candid::Deserialize)] +pub struct SendRequest { + pub destination_address: String, + pub amount_in_satoshi: u64, +} diff --git a/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs b/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs index 67ecab76a..0923b33b6 100644 --- a/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs +++ b/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs @@ -1,8 +1,6 @@ use candid::{CandidType, Deserialize, Principal}; use serde::Serialize; -use crate::SCHNORR_CANISTER; - #[derive(CandidType, Deserialize, Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SchnorrAlgorithm { #[serde(rename = "bip340secp256k1")] @@ -44,17 +42,8 @@ struct SignWithSchnorrReply { /// Returns the Schnorr public key of this canister at the given derivation path. pub async fn schnorr_public_key(key_name: String, derivation_path: Vec>) -> Vec { - let canister_id = SCHNORR_CANISTER.with(|schnorr_canister| { - ic_cdk::println!( - "CANISTER_ID_SCHNORR_CANISTER: {:?}", - &schnorr_canister.borrow() - ); - - Principal::from_text(schnorr_canister.borrow().as_str()).unwrap() - }); - let res: Result<(SchnorrPublicKeyReply,), _> = ic_cdk::call( - canister_id, + Principal::management_canister(), "schnorr_public_key", (SchnorrPublicKey { canister_id: None, @@ -75,11 +64,8 @@ pub async fn sign_with_schnorr( derivation_path: Vec>, message: Vec, ) -> Vec { - let canister_id = SCHNORR_CANISTER - .with(|schnorr_canister| Principal::from_text(schnorr_canister.borrow().as_str()).unwrap()); - let res: Result<(SignWithSchnorrReply,), _> = ic_cdk::call( - canister_id, + Principal::management_canister(), "sign_with_schnorr", (SignWithSchnorr { message, diff --git a/rust/basic_bitcoin/src/basic_bitcoin/src/types.rs b/rust/basic_bitcoin/src/basic_bitcoin/src/types.rs deleted file mode 100644 index 2b348b92b..000000000 --- a/rust/basic_bitcoin/src/basic_bitcoin/src/types.rs +++ /dev/null @@ -1,45 +0,0 @@ -use candid::{CandidType, Deserialize, Principal}; -use serde::Serialize; - -#[derive(CandidType, Deserialize)] -pub struct SendRequest { - pub destination_address: String, - pub amount_in_satoshi: u64, -} - -#[derive(CandidType, Serialize, Deserialize, Debug)] -pub struct ECDSAPublicKeyReply { - pub public_key: Vec, - pub chain_code: Vec, -} - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] -pub struct EcdsaKeyId { - pub curve: EcdsaCurve, - pub name: String, -} - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] -pub enum EcdsaCurve { - #[serde(rename = "secp256k1")] - Secp256k1, -} - -#[derive(CandidType, Deserialize, Debug)] -pub struct SignWithECDSAReply { - pub signature: Vec, -} - -#[derive(CandidType, Serialize, Deserialize, Debug, Clone)] -pub struct ECDSAPublicKey { - pub canister_id: Option, - pub derivation_path: Vec>, - pub key_id: EcdsaKeyId, -} - -#[derive(CandidType, Serialize, Debug)] -pub struct SignWithECDSA { - pub message_hash: Vec, - pub derivation_path: Vec>, - pub key_id: EcdsaKeyId, -} From 52c239c19b227e9b3fee38c809ff0645e10165da Mon Sep 17 00:00:00 2001 From: Oleksandr Tkachenko Date: Tue, 9 Jul 2024 13:00:55 +0200 Subject: [PATCH 06/13] add dfx version to readme --- rust/basic_bitcoin/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/basic_bitcoin/README.md b/rust/basic_bitcoin/README.md index d97b22887..3602b1bbe 100644 --- a/rust/basic_bitcoin/README.md +++ b/rust/basic_bitcoin/README.md @@ -21,7 +21,9 @@ For a deeper understanding of the ICP < > BTC integration, see the [Bitcoin inte ## Prerequisites -* [x] Install the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/setup/install/index.mdx). +* [x] Install the [IC + SDK](https://internetcomputer.org/docs/current/developer-docs/setup/install/index.mdx). + For local testing, `dfx >= 0.22.0-beta.0` is required. ## Step 1: Building and deploying sample code From 2c7746c32d04bd051dda7aa5d68e4344cdb98eaa Mon Sep 17 00:00:00 2001 From: Oleksandr Tkachenko <108659113+altkdf@users.noreply.github.com> Date: Fri, 19 Jul 2024 10:35:45 +0200 Subject: [PATCH 07/13] Update rust/basic_bitcoin/README.md apply revision Co-authored-by: Andrea Cerulli <19587477+andreacerulli@users.noreply.github.com> --- rust/basic_bitcoin/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rust/basic_bitcoin/README.md b/rust/basic_bitcoin/README.md index 3602b1bbe..990a25f11 100644 --- a/rust/basic_bitcoin/README.md +++ b/rust/basic_bitcoin/README.md @@ -115,8 +115,7 @@ from three types of addresses: Note that P2TR tweaked key spends are currently not available on the IC because the threshold Schnorr signing interface does not allow applying BIP341 tweaks to -the private key but this may become available in the future - there is no -technical limitation on the IC side. For a technical comparison of different +For a technical comparison of different ways of how single-signer P2TR addresses can be constructed and used, you may want to take a look at [this post](https://bitcoin.stackexchange.com/a/111100) by Pieter Wuille. Multi-signer P2TR addresses are out of scope of this example. From 142a1ed5847e224826cb87303935bb00a0363e88 Mon Sep 17 00:00:00 2001 From: Oleksandr Tkachenko Date: Mon, 15 Jul 2024 20:02:24 +0200 Subject: [PATCH 08/13] call_with_payment128 for sign_with_schnorr --- rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs b/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs index 0923b33b6..312ce9706 100644 --- a/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs +++ b/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs @@ -1,6 +1,8 @@ use candid::{CandidType, Deserialize, Principal}; use serde::Serialize; +const SIGN_WITH_SCHNORR_FEE: u128 = 10_000_000_000; + #[derive(CandidType, Deserialize, Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SchnorrAlgorithm { #[serde(rename = "bip340secp256k1")] @@ -64,7 +66,7 @@ pub async fn sign_with_schnorr( derivation_path: Vec>, message: Vec, ) -> Vec { - let res: Result<(SignWithSchnorrReply,), _> = ic_cdk::call( + let res: Result<(SignWithSchnorrReply,), _> = ic_cdk::api::call::call_with_payment128( Principal::management_canister(), "sign_with_schnorr", (SignWithSchnorr { @@ -75,6 +77,7 @@ pub async fn sign_with_schnorr( algorithm: SchnorrAlgorithm::Bip340Secp256k1, }, },), + SIGN_WITH_SCHNORR_FEE, ) .await; From 4051a41ae9223fa013e8cfa661cc5393dca298c8 Mon Sep 17 00:00:00 2001 From: Oleksandr Tkachenko Date: Thu, 18 Jul 2024 23:25:35 +0200 Subject: [PATCH 09/13] multisignature comment --- rust/basic_bitcoin/README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/rust/basic_bitcoin/README.md b/rust/basic_bitcoin/README.md index 990a25f11..0378c9f86 100644 --- a/rust/basic_bitcoin/README.md +++ b/rust/basic_bitcoin/README.md @@ -101,12 +101,16 @@ from three types of addresses: 2. A [P2TR address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) where the funds can be spent using the raw (untweaked) internal key - (so-called P2TR key path spend, but untweaked). IMPORTANT: This type of - address MUST NOT be used with keys created by multiple parties (e.g., using - [MuSig2](https://ia.cr/2020/1261)). It is only secure in a single-party - scenario. The advantage of this approach compared to the one below is its - significantly smaller fee per transaction because checking the transaction - signature is analogous to P2PK but uses Schnorr instead of ECDSA. + (so-called P2TR key path spend, but untweaked). The advantage of this + approach compared to the one below is its significantly smaller fee per + transaction because checking the transaction signature is analogous to P2PK + but uses Schnorr instead of ECDSA. IMPORTANT: Note that + [BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23) + advises against using taproot addresses that can be spent with an untweaked + key. This precaution is to prevent attacks that can occur when creating + taproot multisigner addresses using specific multisignature schemes. However, + the Schnorr API of the internet computer does not support Schnorr + multisignatures. 3. A [P2TR address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) where the funds can be spent using the provided public key with the script From 3a1c89abc325ad416031f2e544360cc39d76c0a8 Mon Sep 17 00:00:00 2001 From: Oleksandr Tkachenko Date: Fri, 19 Jul 2024 10:31:32 +0200 Subject: [PATCH 10/13] key path spending with tweaked keys --- rust/basic_bitcoin/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/rust/basic_bitcoin/README.md b/rust/basic_bitcoin/README.md index 0378c9f86..5b2a5f50b 100644 --- a/rust/basic_bitcoin/README.md +++ b/rust/basic_bitcoin/README.md @@ -117,12 +117,14 @@ from three types of addresses: path, where the Merkelized Alternative Script Tree (MAST) consists of a single script allowing to spend funds by exactly one key. -Note that P2TR tweaked key spends are currently not available on the IC because -the threshold Schnorr signing interface does not allow applying BIP341 tweaks to -For a technical comparison of different -ways of how single-signer P2TR addresses can be constructed and used, you may -want to take a look at [this post](https://bitcoin.stackexchange.com/a/111100) -by Pieter Wuille. Multi-signer P2TR addresses are out of scope of this example. +Note that P2TR *key path* spending with a tweaked key is currently not available +on the IC because the threshold Schnorr signing interface does not allow +applying BIP341 tweaks to the private key. In contrast, the +tweaked public key is used to spend in the script path, which is availble on the +IC. For a technical comparison of different ways of how single-signer P2TR +addresses can be constructed and used, you may want to take a look at [this +post](https://bitcoin.stackexchange.com/a/111100) by Pieter Wuille. Multi-signer +P2TR addresses are out of scope of this example. On the Candid UI of your canister, click the "Call" button under `get_${type}_address` to generate a `${type}` Bitcoin address, where `${type}` From 1f9747b52f2e95f1ba6b11badc3f54438a57abbc Mon Sep 17 00:00:00 2001 From: Oleksandr Tkachenko Date: Fri, 19 Jul 2024 10:34:21 +0200 Subject: [PATCH 11/13] rm multisig comment --- rust/basic_bitcoin/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rust/basic_bitcoin/README.md b/rust/basic_bitcoin/README.md index 5b2a5f50b..e34befa0a 100644 --- a/rust/basic_bitcoin/README.md +++ b/rust/basic_bitcoin/README.md @@ -123,8 +123,7 @@ applying BIP341 tweaks to the private key. In contrast, the tweaked public key is used to spend in the script path, which is availble on the IC. For a technical comparison of different ways of how single-signer P2TR addresses can be constructed and used, you may want to take a look at [this -post](https://bitcoin.stackexchange.com/a/111100) by Pieter Wuille. Multi-signer -P2TR addresses are out of scope of this example. +post](https://bitcoin.stackexchange.com/a/111100) by Pieter Wuille. On the Candid UI of your canister, click the "Call" button under `get_${type}_address` to generate a `${type}` Bitcoin address, where `${type}` From 7e4237f184aeb1c7ff57aef2f8277bbcca8f0b4c Mon Sep 17 00:00:00 2001 From: Oleksandr Tkachenko Date: Fri, 19 Jul 2024 10:40:32 +0200 Subject: [PATCH 12/13] consistent fee with ECDSA --- rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs b/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs index 312ce9706..f95eca711 100644 --- a/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs +++ b/rust/basic_bitcoin/src/basic_bitcoin/src/schnorr_api.rs @@ -1,7 +1,7 @@ use candid::{CandidType, Deserialize, Principal}; use serde::Serialize; -const SIGN_WITH_SCHNORR_FEE: u128 = 10_000_000_000; +const SIGN_WITH_SCHNORR_FEE: u128 = 25_000_000_000; #[derive(CandidType, Deserialize, Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SchnorrAlgorithm { From cc914283dd1f0c24b8c8ad3f3f4db25a8eb2b078 Mon Sep 17 00:00:00 2001 From: Oleksandr Tkachenko Date: Fri, 19 Jul 2024 15:38:21 +0200 Subject: [PATCH 13/13] add link to mainnet --- rust/basic_bitcoin/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rust/basic_bitcoin/README.md b/rust/basic_bitcoin/README.md index e34befa0a..7b5073479 100644 --- a/rust/basic_bitcoin/README.md +++ b/rust/basic_bitcoin/README.md @@ -84,6 +84,12 @@ would use the URL `https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=`. Candid Web UI will contain all methods implemented by the canister. +The `basic_bitcoin` example is deployed on mainnet for illustration purposes and +is interacting with Bitcoin testnet. It has the URL +https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=vvha6-7qaaa-aaaap-ahodq-cai +and serves up the Candid web UI for this particular canister deployed on +mainnet. + ## Step 2: Generating a Bitcoin address Bitcoin has different types of addresses (e.g. P2PKH, P2SH, P2TR). You may want