Skip to content

Commit 8ccaf6c

Browse files
authored
add BIP341 to the basic_bitcoin rust example (#1074)
1 parent 0c27b54 commit 8ccaf6c

File tree

15 files changed

+521
-339
lines changed

15 files changed

+521
-339
lines changed

rust/basic_bitcoin/Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/basic_bitcoin/Makefile

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,37 @@ all: deploy
44
.PHONY: deploy
55
.SILENT: deploy
66
deploy:
7+
dfx deploy chainkey_testing_canister
78
dfx deploy basic_bitcoin --argument '(variant { regtest })'
89

10+
.PHONY: mock
11+
.SILENT: mock
12+
mock: deploy
13+
SCHNORR_MOCK_CANISTER_ID=$(shell dfx canister id chainkey_testing_canister); \
14+
BASIC_BITCOIN_CANISTER_ID=$(shell dfx canister id basic_bitcoin); \
15+
echo "Changing to using mock canister instead of management canister"; \
16+
CMD="dfx canister call "$${BASIC_BITCOIN_CANISTER_ID}" for_test_only_change_management_canister_id '("\"$${SCHNORR_MOCK_CANISTER_ID}\"")'"; \
17+
eval "$${CMD}"
18+
919
.PHONY: regtest_topup
1020
.SILENT: regtest_topup
1121
regtest_topup:
1222
P2PKH_ADDR=$(shell dfx canister call basic_bitcoin get_p2pkh_address | tr -d '()') && \
13-
P2TR_SCRIPT_SPEND_ADDR=$(shell dfx canister call basic_bitcoin get_p2tr_script_spend_address | tr -d '()') && \
14-
P2TR_SCRIPT_KEY_SPEND_ADDR=$(shell dfx canister call basic_bitcoin get_p2tr_raw_key_spend_address | tr -d '()') && \
15-
TOPUP_CMD_P2PKH="bitcoin-cli -rpcport=8333 sendtoaddress $${P2PKH_ADDR} 1" && \
16-
TOPUP_CMD_P2TR_SCRIPT_SPEND="bitcoin-cli -rpcport=8333 sendtoaddress $${P2TR_SCRIPT_SPEND_ADDR} 1" && \
17-
TOPUP_CMD_P2TR_RAW_KEY_SPEND="bitcoin-cli -rpcport=8333 sendtoaddress $${P2TR_SCRIPT_KEY_SPEND_ADDR} 1" && \
18-
eval "$${TOPUP_CMD_P2PKH}" && \
19-
eval "$${TOPUP_CMD_P2PKH}" && \
20-
eval "$${TOPUP_CMD_P2PKH}" && \
21-
eval "$${TOPUP_CMD_P2TR_SCRIPT_SPEND}" && \
22-
eval "$${TOPUP_CMD_P2TR_SCRIPT_SPEND}" && \
23-
eval "$${TOPUP_CMD_P2TR_SCRIPT_SPEND}" && \
24-
eval "$${TOPUP_CMD_P2TR_RAW_KEY_SPEND}" && \
25-
eval "$${TOPUP_CMD_P2TR_RAW_KEY_SPEND}" && \
26-
eval "$${TOPUP_CMD_P2TR_RAW_KEY_SPEND}" && \
27-
bitcoin-cli -rpcport=8333 -generate 6
23+
P2TR_ADDR=$(shell dfx canister call basic_bitcoin get_p2tr_address | tr -d '()') && \
24+
P2TR_KEY_ONLY_ADDR=$(shell dfx canister call basic_bitcoin get_p2tr_key_only_address | tr -d '()') && \
25+
TOPUP_CMD_P2PKH_ADDR="bitcoin-cli -regtest -rpcport=8333 sendtoaddress $${P2PKH_ADDR} 1" && \
26+
TOPUP_CMD_P2TR_ADDR="bitcoin-cli -regtest -rpcport=8333 sendtoaddress $${P2TR_ADDR} 1" && \
27+
TOPUP_CMD_P2TR_KEY_ONLY_ADDR="bitcoin-cli -regtest -rpcport=8333 sendtoaddress $${P2TR_KEY_ONLY_ADDR} 1" && \
28+
eval "$${TOPUP_CMD_P2PKH_ADDR}" && \
29+
eval "$${TOPUP_CMD_P2PKH_ADDR}" && \
30+
eval "$${TOPUP_CMD_P2PKH_ADDR}" && \
31+
eval "$${TOPUP_CMD_P2TR_ADDR}" && \
32+
eval "$${TOPUP_CMD_P2TR_ADDR}" && \
33+
eval "$${TOPUP_CMD_P2TR_ADDR}" && \
34+
eval "$${TOPUP_CMD_P2TR_KEY_ONLY_ADDR}" && \
35+
eval "$${TOPUP_CMD_P2TR_KEY_ONLY_ADDR}" && \
36+
eval "$${TOPUP_CMD_P2TR_KEY_ONLY_ADDR}" && \
37+
bitcoin-cli -regtest -rpcport=8333 -generate 6
2838

2939
.PHONY: clean
3040
.SILENT: clean

rust/basic_bitcoin/README.md

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ For a deeper understanding of the ICP < > BTC integration, see the [Bitcoin inte
1818

1919
* [x] Install the [IC
2020
SDK](https://internetcomputer.org/docs/current/developer-docs/getting-started/install).
21-
For local testing, `dfx >= 0.22.0-beta.0` is required.
2221
* [x] On macOS, `llvm` with the `wasm32-unknown-unknown` target (which is not included in the XCode installation by default) is required.
2322
To install, run `brew install llvm`.
2423

@@ -92,43 +91,30 @@ to check [this
9291
article](https://bitcoinmagazine.com/technical/bitcoin-address-types-compared-p2pkh-p2sh-p2wpkh-and-more)
9392
if you are interested in a high-level comparison of different address types.
9493
These addresses can be generated from an ECDSA public key or a Schnorr
95-
([BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki))
96-
public key. The example code showcases how your canister can generate and spend
97-
from three types of addresses:
94+
([BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki),
95+
[BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)) public
96+
key. The example code showcases how your canister can generate and spend from
97+
three types of addresses:
9898
1. A [P2PKH address](https://en.bitcoin.it/wiki/Transaction#Pay-to-PubkeyHash)
9999
using the
100100
[ecdsa_public_key](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-method-ecdsa_public_key)
101101
API.
102102
2. A [P2TR
103103
address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)
104-
where the funds can be spent using the raw (untweaked) internal key
105-
(so-called P2TR key path spend, but untweaked). The advantage of this
106-
approach compared to the one below is its significantly smaller fee per
107-
transaction because checking the transaction signature is analogous to P2PK
108-
but uses Schnorr instead of ECDSA. IMPORTANT: Note that
109-
[BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23)
110-
advises against using taproot addresses that can be spent with an untweaked
111-
key. This precaution is to prevent attacks that can occur when creating
112-
taproot multisigner addresses using specific multisignature schemes. However,
113-
the Schnorr API of the internet computer does not support Schnorr
114-
multisignatures.
104+
where the funds can be spent using the internal key only ([P2TR key path
105+
spend with unspendable script
106+
tree](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23)).
115107
3. A [P2TR
116108
address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)
117-
where the funds can be spent using the provided public key with the script
118-
path, where the Merkelized Alternative Script Tree (MAST) consists of a
119-
single script allowing to spend funds by exactly one key.
120-
121-
Note that P2TR *key path* spending with a tweaked key is currently not available
122-
on the IC because the threshold Schnorr signing interface does not allow
123-
applying BIP341 tweaks to the private key. In contrast, the
124-
tweaked public key is used to spend in the script path, which is available on the
125-
IC. For a technical comparison of different ways of how single-signer P2TR
126-
addresses can be constructed and used, you may want to take a look at [this
127-
post](https://bitcoin.stackexchange.com/a/111100) by Pieter Wuille.
109+
where the funds can be spent using either 1) the internal key or 2) the
110+
provided public key with the script path, where the Merkelized Alternative
111+
Script Tree (MAST) consists of a single script allowing to spend funds by
112+
exactly one key.
128113

129114
On the Candid UI of your canister, click the "Call" button under
130115
`get_${type}_address` to generate a `${type}` Bitcoin address, where `${type}`
131-
is one of `[p2pkh, p2tr_raw_key_spend, p2tr_script_spend]`.
116+
is one of `[p2pkh, p2tr_key_only, p2tr]` (corresponding to the three types of
117+
addresses described above, in the same order).
132118

133119
Or, if you prefer the command line:
134120
`dfx canister --network=ic call basic_bitcoin get_${type}_address`
@@ -176,15 +162,16 @@ Checking the balance of a Bitcoin address relies on the [bitcoin_get_balance](ht
176162
## Step 5: Sending bitcoin
177163

178164
You can send bitcoin using the `send_from_${type}` endpoint on your canister, where
179-
`${type}` is on of `[p2pkh, p2tr_raw_key_spend, p2tr_script_spend]`.
165+
`${type}` is one of
166+
`[p2pkh_address, p2tr_key_only_address, p2tr_address_key_path, p2tr_address_script_path]`.
180167

181168
In the Candid UI, add a destination address and an amount to send. In the example
182169
below, we're sending 4'321 Satoshi (0.00004321 BTC) back to the testnet faucet.
183170

184171
Via command line, the same call would look like this:
185172

186173
```bash
187-
dfx canister --network=ic call basic_bitcoin send_from_p2pkh '(record { destination_address = "tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt"; amount_in_satoshi = 4321; })'
174+
dfx canister --network=ic call basic_bitcoin send_from_p2pkh_address '(record { destination_address = "tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt"; amount_in_satoshi = 4321; })'
188175
```
189176

190177
The `send_from_${type}` endpoint can send bitcoin by:

rust/basic_bitcoin/dfx.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
"name": "candid:service"
1313
}
1414
]
15+
},
16+
"chainkey_testing_canister": {
17+
"type": "custom",
18+
"candid": "https://github.com/dfinity/chainkey-testing-canister/releases/download/v0.1.0/chainkey_testing_canister.did",
19+
"wasm": "https://github.com/dfinity/chainkey-testing-canister/releases/download/v0.1.0/chainkey_testing_canister.wasm.gz"
1520
}
1621
},
1722
"defaults": {

rust/basic_bitcoin/src/basic_bitcoin/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ candid = "0.9.10"
1515
ic-cdk = "0.10.0"
1616
ic-cdk-macros = "0.7.1"
1717
serde = "1.0.132"
18+
serde_bytes = "0.11.15"

rust/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ type get_block_headers_response = record {
4141
};
4242

4343
service : (network) -> {
44+
"for_test_only_change_management_canister_id" : (text) -> (variant { Ok: null; Err: text });
45+
4446
"get_p2pkh_address" : () -> (bitcoin_address);
4547

46-
"get_p2tr_script_spend_address" : () -> (bitcoin_address);
48+
"get_p2tr_address" : () -> (bitcoin_address);
4749

48-
"get_p2tr_raw_key_spend_address" : () -> (bitcoin_address);
50+
"get_p2tr_key_only_address" : () -> (bitcoin_address);
4951

5052
"get_balance" : (address : bitcoin_address) -> (satoshi);
5153

@@ -55,21 +57,28 @@ service : (network) -> {
5557

5658
"get_current_fee_percentiles" : () -> (vec millisatoshi_per_vbyte);
5759

58-
"send_from_p2pkh" : (
60+
"send_from_p2pkh_address" : (
61+
record {
62+
destination_address : bitcoin_address;
63+
amount_in_satoshi : satoshi;
64+
}
65+
) -> (transaction_id);
66+
67+
"send_from_p2tr_address_key_path" : (
5968
record {
6069
destination_address : bitcoin_address;
6170
amount_in_satoshi : satoshi;
6271
}
6372
) -> (transaction_id);
6473

65-
"send_from_p2tr_script_spend" : (
74+
"send_from_p2tr_address_script_path" : (
6675
record {
6776
destination_address : bitcoin_address;
6877
amount_in_satoshi : satoshi;
6978
}
7079
) -> (transaction_id);
7180

72-
"send_from_p2tr_raw_key_spend" : (
81+
"send_from_p2tr_key_only_address" : (
7382
record {
7483
destination_address : bitcoin_address;
7584
amount_in_satoshi : satoshi;

rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/common.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ pub async fn get_fee_per_byte(network: BitcoinNetwork) -> u64 {
110110
pub async fn mock_signer(
111111
_key_name: String,
112112
_derivation_path: Vec<Vec<u8>>,
113+
_merkle_root_hash: Option<Vec<u8>>,
113114
_message_hash: Vec<u8>,
114115
) -> Vec<u8> {
115116
vec![255; 64]

rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@
1111
1212
mod common;
1313
pub mod p2pkh;
14-
pub mod p2tr_raw_key_spend;
15-
pub mod p2tr_script_spend;
14+
pub mod p2tr;
15+
pub mod p2tr_key_only;

rust/basic_bitcoin/src/basic_bitcoin/src/bitcoin_wallet/p2pkh.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ async fn build_p2pkh_spend_tx(
140140
transaction.clone(),
141141
String::from(""), // mock key name
142142
vec![], // mock derivation path
143-
super::common::mock_signer,
143+
mock_signer,
144144
)
145145
.await;
146146

@@ -258,3 +258,11 @@ fn sec1_to_der(sec1_signature: Vec<u8>) -> Vec<u8> {
258258
.flatten()
259259
.collect()
260260
}
261+
262+
async fn mock_signer(
263+
_key_name: String,
264+
_derivation_path: Vec<Vec<u8>>,
265+
_signing_data: Vec<u8>,
266+
) -> Vec<u8> {
267+
vec![0; 64]
268+
}

0 commit comments

Comments
 (0)