Skip to content

Commit da45b63

Browse files
authored
add BIP341 to Motoko basic_bitcoin (#1080)
1 parent 8ccaf6c commit da45b63

File tree

12 files changed

+677
-643
lines changed

12 files changed

+677
-643
lines changed

motoko/basic_bitcoin/README.md

Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,9 @@ For a deeper understanding of the ICP < > BTC integration, see the [Bitcoin inte
1515
## Prerequisites
1616

1717
- [x] Install the [IC
18-
SDK](https://internetcomputer.org/docs/current/developer-docs/getting-started/install). For local testing, `dfx >= 0.22.0` is required.
18+
SDK](https://internetcomputer.org/docs/current/developer-docs/getting-started/install).
1919
- [x] Clone the example dapp project: `git clone https://github.com/dfinity/examples`
2020

21-
> [!WARNING]
22-
> This example is designed to be deployed on the mainnet. It will return errors when deployed locally; these errors are expected.
23-
2421
Begin by opening a terminal window.
2522

2623
## Step 1: Setup the project environment
@@ -89,58 +86,40 @@ to check [this
8986
article](https://bitcoinmagazine.com/technical/bitcoin-address-types-compared-p2pkh-p2sh-p2wpkh-and-more)
9087
if you are interested in a high-level comparison of different address types.
9188
These addresses can be generated from an ECDSA public key or a Schnorr
92-
([BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki))
93-
public key. The example code showcases how your canister can generate and spend
94-
from three types of addresses:
89+
([BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki),
90+
[BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)) public
91+
key. The example code showcases how your canister can generate and spend from
92+
three types of addresses:
9593
1. A [P2PKH address](https://en.bitcoin.it/wiki/Transaction#Pay-to-PubkeyHash)
9694
using the
9795
[ecdsa_public_key](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-method-ecdsa_public_key)
9896
API.
9997
2. A [P2TR
10098
address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)
101-
where the funds can be spent using the raw (untweaked) internal key
102-
(so-called P2TR key path spend, but untweaked). The advantage of this
103-
approach compared to P2TR script spends is its significantly smaller fee per
104-
transaction because checking the transaction signature is analogous to P2PK
105-
but uses Schnorr instead of ECDSA. The limitation of untweaked P2TR addresses
106-
is that they cannot be used with scripts. IMPORTANT: Note that
107-
[BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23)
108-
advises against using taproot addresses that can be spent with an untweaked
109-
key. This precaution is to prevent attacks that can occur when creating
110-
taproot multisigner addresses using specific multisignature schemes. However,
111-
the Schnorr API of the internet computer does not support Schnorr
112-
multisignatures.
99+
where the funds can be spent using the internal key only ([P2TR key path
100+
spend with unspendable script
101+
tree](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23)).
113102
3. A [P2TR
114103
address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)
115-
where the funds can be spent using the provided public key with the script
116-
path, where the Merkelized Alternative Script Tree (MAST) consists of a
117-
single script allowing to spend funds by exactly one key.
118-
119-
Note that P2TR *key path* spending with a tweaked key is currently not available
120-
on the IC because the threshold Schnorr signing interface does not allow
121-
applying BIP341 tweaks to the private key. In contrast, the
122-
tweaked public key is used to spend in the script path, which is availble on the
123-
IC. For a technical comparison of different ways of how single-signer P2TR
124-
addresses can be constructed and used, you may want to take a look at [this
125-
post](https://bitcoin.stackexchange.com/a/111100) by Pieter Wuille.
104+
where the funds can be spent using either 1) the internal key or 2) the
105+
provided public key with the script path, where the Merkelized Alternative
106+
Script Tree (MAST) consists of a single script allowing to spend funds by
107+
exactly one key.
126108

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

131114
Or, if you prefer the command line:
132115

133116
```bash
134117
dfx canister --network=ic call basic_bitcoin get_${type}_address
135118
```
136119

137-
* The Bitcoin address you see will be different from the one above because the
138-
ECDSA public key your canister retrieves is unique.
139-
140120
* We are generating a Bitcoin testnet address, which can only be
141121
used for sending/receiving Bitcoin on the Bitcoin testnet.
142122

143-
144123
## Step 3: Receiving bitcoin
145124

146125
Now that the canister is deployed and you have a Bitcoin address, it's time to receive
@@ -150,7 +129,6 @@ to receive some bitcoin.
150129
Enter your address and click on "Send testnet bitcoins". In the example below we will use Bitcoin address `n31eU1K11m1r58aJMgTyxGonu7wSMoUYe7`, but you will use your address. The Bitcoin address you see will be different from the one above
151130
because the ECDSA/Schnorr public key your canister retrieves is unique.
152131

153-
154132
Once the transaction has at least one confirmation, which can take a few minutes,
155133
you'll be able to see it in your canister's balance.
156134

@@ -171,15 +149,16 @@ Checking the balance of a Bitcoin address relies on the [bitcoin_get_balance](ht
171149
## Step 5: Sending bitcoin
172150

173151
You can send bitcoin using the `send_from_${type}` endpoint on your canister, where
174-
`${type}` is on of `[p2pkh, p2tr_raw_key_spend, p2tr_script_spend]`.
152+
`${type}` is one of
153+
`[p2pkh_address, p2tr_key_only_address, p2tr_address_key_path, p2tr_address_script_path]`.
175154

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

179158
Via the command line, the same call would look like this:
180159

181160
```bash
182-
dfx canister --network=ic call basic_bitcoin send_from_p2pkh '(record { destination_address = "tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt"; amount_in_satoshi = 4321; })'
161+
dfx canister --network=ic call basic_bitcoin send_from_p2pkh_address '(record { destination_address = "tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt"; amount_in_satoshi = 4321; })'
183162
```
184163

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

motoko/basic_bitcoin/dfx.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
"version": 1,
33
"canisters": {
44
"basic_bitcoin": {
5-
"main": "src/basic_bitcoin/src/Main.mo"
5+
"main": "src/basic_bitcoin/src/Main.mo",
6+
"type": "motoko"
7+
},
8+
"chainkey_testing_canister": {
9+
"type": "custom",
10+
"candid": "https://github.com/dfinity/chainkey-testing-canister/releases/download/v0.1.0/chainkey_testing_canister.did",
11+
"wasm": "https://github.com/dfinity/chainkey-testing-canister/releases/download/v0.1.0/chainkey_testing_canister.wasm.gz"
612
}
713
},
814
"defaults": {

motoko/basic_bitcoin/src/basic_bitcoin/src/EcdsaApi.mo

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,38 @@ module {
88
type SignWithECDSA = Types.SignWithECDSA;
99
type SignWithECDSAReply = Types.SignWithECDSAReply;
1010
type Cycles = Types.Cycles;
11-
12-
/// Actor definition to handle interactions with the ECDSA canister.
13-
type EcdsaCanisterActor = actor {
14-
ecdsa_public_key : ECDSAPublicKey -> async ECDSAPublicKeyReply;
15-
sign_with_ecdsa : SignWithECDSA -> async SignWithECDSAReply;
16-
};
11+
type EcdsaCanisterActor = Types.EcdsaCanisterActor;
1712

1813
// The fee for the `sign_with_ecdsa` endpoint using the test key.
1914
let SIGN_WITH_ECDSA_COST_CYCLES : Cycles = 10_000_000_000;
2015

21-
let ecdsa_canister_actor : EcdsaCanisterActor = actor("aaaaa-aa");
22-
2316
/// Returns the ECDSA public key of this canister at the given derivation path.
24-
public func ecdsa_public_key(key_name : Text, derivation_path : [Blob]) : async Blob {
17+
public func ecdsa_public_key(ecdsa_canister_actor: EcdsaCanisterActor, key_name : Text, derivation_path : [Blob]) : async Blob {
2518
// Retrieve the public key of this canister at derivation path
2619
// from the ECDSA API.
2720
let res = await ecdsa_canister_actor.ecdsa_public_key({
28-
canister_id = null;
29-
derivation_path;
30-
key_id = {
31-
curve = #secp256k1;
32-
name = key_name;
33-
};
21+
canister_id = null;
22+
derivation_path;
23+
key_id = {
24+
curve = #secp256k1;
25+
name = key_name;
26+
};
3427
});
35-
36-
res.public_key
28+
29+
res.public_key;
3730
};
3831

39-
public func sign_with_ecdsa(key_name : Text, derivation_path : [Blob], message_hash : Blob) : async Blob {
32+
public func sign_with_ecdsa(ecdsa_canister_actor: EcdsaCanisterActor, key_name : Text, derivation_path : [Blob], message_hash : Blob) : async Blob {
4033
ExperimentalCycles.add<system>(SIGN_WITH_ECDSA_COST_CYCLES);
4134
let res = await ecdsa_canister_actor.sign_with_ecdsa({
42-
message_hash;
43-
derivation_path;
44-
key_id = {
45-
curve = #secp256k1;
46-
name = key_name;
47-
};
35+
message_hash;
36+
derivation_path;
37+
key_id = {
38+
curve = #secp256k1;
39+
name = key_name;
40+
};
4841
});
49-
50-
res.signature
42+
43+
res.signature;
5144
};
52-
}
45+
};
Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,54 @@
1+
import Principal "mo:base/Principal";
12
import Text "mo:base/Text";
3+
import Array "mo:base/Array";
4+
import Blob "mo:base/Blob";
25

36
import BitcoinApi "BitcoinApi";
47
import P2pkh "P2pkh";
5-
import P2trRawKeySpend "P2trRawKeySpend";
6-
import P2trScriptSpend "P2trScriptSpend";
8+
import P2trKeyOnly "P2trKeyOnly";
9+
import P2tr "P2tr";
710
import Types "Types";
811
import Utils "Utils";
912

10-
actor class BasicBitcoin(_network : Types.Network) {
13+
actor class BasicBitcoin(network : Types.Network) {
1114
type GetUtxosResponse = Types.GetUtxosResponse;
1215
type MillisatoshiPerVByte = Types.MillisatoshiPerVByte;
1316
type SendRequest = Types.SendRequest;
1417
type Network = Types.Network;
1518
type BitcoinAddress = Types.BitcoinAddress;
1619
type Satoshi = Types.Satoshi;
1720
type TransactionId = Text;
21+
type EcdsaCanisterActor = Types.EcdsaCanisterActor;
22+
type SchnorrCanisterActor = Types.SchnorrCanisterActor;
23+
type P2trDerivationPaths = Types.P2trDerivationPaths;
1824

19-
// The Bitcoin network to connect to.
20-
//
21-
// When developing locally this should be `regtest`.
22-
// When deploying to the IC this should be `testnet`.
23-
// `mainnet` is currently unsupported.
24-
stable let NETWORK : Network = _network;
25+
/// The Bitcoin network to connect to.
26+
///
27+
/// When developing locally this should be `regtest`.
28+
/// When deploying to the IC this should be `testnet`.
29+
/// `mainnet` is currently unsupported.
30+
stable let NETWORK : Network = network;
2531

26-
// The derivation path to use for ECDSA secp256k1.
32+
/// The derivation path to use for ECDSA secp256k1 or Schnorr BIP340/BIP341 key
33+
/// derivation.
2734
let DERIVATION_PATH : [[Nat8]] = [];
2835

2936
// The ECDSA key name.
3037
let KEY_NAME : Text = switch NETWORK {
3138
// For local development, we use a special test key with dfx.
32-
case (#regtest) "dfx_test_key";
39+
case (#regtest) "insecure_test_key_1";
3340
// On the IC we're using a test ECDSA key.
3441
case _ "test_key_1";
3542
};
3643

44+
var ecdsa_canister_actor : EcdsaCanisterActor = actor ("aaaaa-aa");
45+
var schnorr_canister_actor : SchnorrCanisterActor = actor ("aaaaa-aa");
46+
47+
public func for_test_only_change_management_canister_id(p : Principal) {
48+
ecdsa_canister_actor := actor (Principal.toText(p));
49+
schnorr_canister_actor := actor (Principal.toText(p));
50+
};
51+
3752
/// Returns the balance of the given Bitcoin address.
3853
public func get_balance(address : BitcoinAddress) : async Satoshi {
3954
await BitcoinApi.get_balance(NETWORK, address);
@@ -52,28 +67,51 @@ actor class BasicBitcoin(_network : Types.Network) {
5267

5368
/// Returns the P2PKH address of this canister at a specific derivation path.
5469
public func get_p2pkh_address() : async BitcoinAddress {
55-
await P2pkh.get_address(NETWORK, KEY_NAME, DERIVATION_PATH);
70+
await P2pkh.get_address(ecdsa_canister_actor, NETWORK, KEY_NAME, p2pkhDerivationPath());
5671
};
5772

5873
/// Sends the given amount of bitcoin from this canister to the given address.
5974
/// Returns the transaction ID.
6075
public func send_from_p2pkh_address(request : SendRequest) : async TransactionId {
61-
Utils.bytesToText(await P2pkh.send(NETWORK, DERIVATION_PATH, KEY_NAME, request.destination_address, request.amount_in_satoshi));
76+
Utils.bytesToText(await P2pkh.send(ecdsa_canister_actor, NETWORK, p2pkhDerivationPath(), KEY_NAME, request.destination_address, request.amount_in_satoshi));
77+
};
78+
79+
public func get_p2tr_key_only_address() : async BitcoinAddress {
80+
await P2trKeyOnly.get_address_key_only(schnorr_canister_actor, NETWORK, KEY_NAME, p2trKeyOnlyDerivationPath());
81+
};
82+
83+
public func send_from_p2tr_key_only_address(request : SendRequest) : async TransactionId {
84+
Utils.bytesToText(await P2trKeyOnly.send(schnorr_canister_actor, NETWORK, p2trKeyOnlyDerivationPath(), KEY_NAME, request.destination_address, request.amount_in_satoshi));
85+
};
86+
87+
public func get_p2tr_address() : async BitcoinAddress {
88+
await P2tr.get_address(schnorr_canister_actor, NETWORK, KEY_NAME, p2trDerivationPaths());
89+
};
90+
91+
public func send_from_p2tr_address_key_path(request : SendRequest) : async TransactionId {
92+
Utils.bytesToText(await P2tr.send_key_path(schnorr_canister_actor, NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi));
93+
};
94+
95+
public func send_from_p2tr_address_script_path(request : SendRequest) : async TransactionId {
96+
Utils.bytesToText(await P2tr.send_script_path(schnorr_canister_actor, NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi));
6297
};
6398

64-
public func get_p2tr_raw_key_spend_address() : async BitcoinAddress {
65-
await P2trRawKeySpend.get_address(NETWORK, KEY_NAME, DERIVATION_PATH);
99+
func p2pkhDerivationPath() : [[Nat8]] {
100+
derivationPathWithSuffix("p2pkh");
66101
};
67102

68-
public func send_from_p2tr_raw_key_spend_address(request : SendRequest) : async TransactionId {
69-
Utils.bytesToText(await P2trRawKeySpend.send(NETWORK, DERIVATION_PATH, KEY_NAME, request.destination_address, request.amount_in_satoshi));
103+
func p2trKeyOnlyDerivationPath() : [[Nat8]] {
104+
derivationPathWithSuffix("p2tr_key_only");
70105
};
71106

72-
public func get_p2tr_script_spend_address() : async BitcoinAddress {
73-
await P2trScriptSpend.get_address(NETWORK, KEY_NAME, DERIVATION_PATH);
107+
func p2trDerivationPaths() : P2trDerivationPaths {
108+
{
109+
key_path_derivation_path = derivationPathWithSuffix("p2tr_internal_key");
110+
script_path_derivation_path = derivationPathWithSuffix("p2tr_script_key");
111+
};
74112
};
75113

76-
public func send_from_p2tr_script_spend_address(request : SendRequest) : async TransactionId {
77-
Utils.bytesToText(await P2trScriptSpend.send(NETWORK, DERIVATION_PATH, KEY_NAME, request.destination_address, request.amount_in_satoshi));
114+
func derivationPathWithSuffix(suffix : Blob) : [[Nat8]] {
115+
Array.flatten([DERIVATION_PATH, [Blob.toArray(suffix)]]);
78116
};
79117
};

0 commit comments

Comments
 (0)