Skip to content

Commit eea5afe

Browse files
authored
Merge pull request #988 from dfinity/alex/add-p2tr-script-to-motoko-basic-bitcoin
add p2tr script to motoko basic bitcoin
2 parents 8c12efe + 718dae1 commit eea5afe

File tree

4 files changed

+303
-10
lines changed

4 files changed

+303
-10
lines changed

motoko/basic_bitcoin/src/basic_bitcoin/basic_bitcoin.did

+7
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,11 @@ service : (network) -> {
5252
destination_address: bitcoin_address;
5353
amount_in_satoshi: satoshi;
5454
}) -> (transaction_id);
55+
56+
"get_p2tr_script_spend_address": () -> (bitcoin_address);
57+
58+
"send_from_p2tr_script_spend_address": (record {
59+
destination_address: bitcoin_address;
60+
amount_in_satoshi: satoshi;
61+
}) -> (transaction_id);
5562
}

motoko/basic_bitcoin/src/basic_bitcoin/src/Main.mo

+12-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Text "mo:base/Text";
33
import BitcoinApi "BitcoinApi";
44
import P2pkh "P2pkh";
55
import P2trRawKeySpend "P2trRawKeySpend";
6+
import P2trScriptSpend "P2trScriptSpend";
67
import Types "Types";
78
import Utils "Utils";
89

@@ -13,6 +14,7 @@ actor class BasicBitcoin(_network : Types.Network) {
1314
type Network = Types.Network;
1415
type BitcoinAddress = Types.BitcoinAddress;
1516
type Satoshi = Types.Satoshi;
17+
type TransactionId = Text;
1618

1719
// The Bitcoin network to connect to.
1820
//
@@ -55,15 +57,23 @@ actor class BasicBitcoin(_network : Types.Network) {
5557

5658
/// Sends the given amount of bitcoin from this canister to the given address.
5759
/// Returns the transaction ID.
58-
public func send_from_p2pkh_address(request : SendRequest) : async Text {
60+
public func send_from_p2pkh_address(request : SendRequest) : async TransactionId {
5961
Utils.bytesToText(await P2pkh.send(NETWORK, DERIVATION_PATH, KEY_NAME, request.destination_address, request.amount_in_satoshi));
6062
};
6163

6264
public func get_p2tr_raw_key_spend_address() : async BitcoinAddress {
6365
await P2trRawKeySpend.get_address(NETWORK, KEY_NAME, DERIVATION_PATH);
6466
};
6567

66-
public func send_from_p2tr_raw_key_spend_address(request : SendRequest) : async Text {
68+
public func send_from_p2tr_raw_key_spend_address(request : SendRequest) : async TransactionId {
6769
Utils.bytesToText(await P2trRawKeySpend.send(NETWORK, DERIVATION_PATH, KEY_NAME, request.destination_address, request.amount_in_satoshi));
6870
};
71+
72+
public func get_p2tr_script_spend_address() : async BitcoinAddress {
73+
await P2trScriptSpend.get_address(NETWORK, KEY_NAME, DERIVATION_PATH);
74+
};
75+
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));
78+
};
6979
};

motoko/basic_bitcoin/src/basic_bitcoin/src/P2trRawKeySpend.mo

+7-8
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@ module {
4444
let sec1_public_key = await SchnorrApi.schnorr_public_key(key_name, Array.map(derivation_path, Blob.fromArray));
4545
assert sec1_public_key.size() == 33;
4646

47-
public_key_to_p2tr_key_spend_address(network, Blob.toArray(sec1_public_key));
47+
let bip340_public_key_bytes = Array.subArray(Blob.toArray(sec1_public_key), 1, 32);
48+
49+
public_key_to_p2tr_key_spend_address(network, bip340_public_key_bytes);
4850
};
4951

5052
// Converts a public key to a P2TR raw key spend address.
51-
func public_key_to_p2tr_key_spend_address(network : Network, public_key_bytes : [Nat8]) : BitcoinAddress {
53+
public func public_key_to_p2tr_key_spend_address(network : Network, bip340_public_key_bytes : [Nat8]) : BitcoinAddress {
5254
// human-readable part of the address
5355
let hrp = switch (network) {
5456
case (#mainnet) "bc";
@@ -57,11 +59,9 @@ module {
5759
};
5860

5961
let version : Nat8 = 1;
60-
let bip340PublicKeyBytes = Array.subArray(public_key_bytes, 1, 32);
61-
assert bip340PublicKeyBytes.size() == 32;
62-
let program = bip340PublicKeyBytes;
62+
assert bip340_public_key_bytes.size() == 32;
6363

64-
switch (Segwit.encode(hrp, { version; program })) {
64+
switch (Segwit.encode(hrp, { version; program = bip340_public_key_bytes })) {
6565
case (#ok address) address;
6666
case (#err msg) Debug.trap("Error encoding segwit address: " # msg);
6767
};
@@ -184,8 +184,7 @@ module {
184184
};
185185

186186
// Fetch our public key, P2TR raw key spend address, and UTXOs.
187-
let own_sec1_public_key = Blob.toArray(await SchnorrApi.schnorr_public_key(key_name, Array.map(derivation_path, Blob.fromArray)));
188-
let own_address = public_key_to_p2tr_key_spend_address(network, own_sec1_public_key);
187+
let own_address = await get_address(network, key_name, derivation_path);
189188

190189
Debug.print("Fetching UTXOs...");
191190
// Note that pagination may have to be used to get all UTXOs for the given address.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
//! A demo of a very bare-bones Bitcoin "wallet".
2+
//!
3+
//! The wallet here showcases how Bitcoin addresses can be computed
4+
//! and how Bitcoin transactions can be signed. It is missing several
5+
//! pieces that any production-grade wallet would have, including:
6+
//!
7+
//! * Support for address types that aren't P2TR script key spend.
8+
//! * Caching spent UTXOs so that they are not reused in future transactions.
9+
//! * Option to set the fee.
10+
11+
import Debug "mo:base/Debug";
12+
import Array "mo:base/Array";
13+
import Nat8 "mo:base/Nat8";
14+
import Nat32 "mo:base/Nat32";
15+
import Nat64 "mo:base/Nat64";
16+
import Iter "mo:base/Iter";
17+
import Blob "mo:base/Blob";
18+
import Nat "mo:base/Nat";
19+
import Option "mo:base/Option";
20+
21+
import Address "mo:bitcoin/bitcoin/Address";
22+
import Bitcoin "mo:bitcoin/bitcoin/Bitcoin";
23+
import { leafHash; leafScript; tweakFromKeyAndHash; tweakPublicKey } "mo:bitcoin/bitcoin/P2tr";
24+
import Transaction "mo:bitcoin/bitcoin/Transaction";
25+
import TxInput "mo:bitcoin/bitcoin/TxInput";
26+
import Script "mo:bitcoin/bitcoin/Script";
27+
28+
import BitcoinApi "BitcoinApi";
29+
import P2trRawKeySpend "P2trRawKeySpend";
30+
import SchnorrApi "SchnorrApi";
31+
import Types "Types";
32+
import Utils "Utils";
33+
34+
module {
35+
type Network = Types.Network;
36+
type BitcoinAddress = Types.BitcoinAddress;
37+
type Satoshi = Types.Satoshi;
38+
type Utxo = Types.Utxo;
39+
type MillisatoshiPerVByte = Types.MillisatoshiPerVByte;
40+
type Transaction = Transaction.Transaction;
41+
type Script = Script.Script;
42+
43+
public func get_address(network : Network, key_name : Text, derivation_path : [[Nat8]]) : async BitcoinAddress {
44+
// Fetch the public key of the given derivation path.
45+
let sec1_public_key = await SchnorrApi.schnorr_public_key(key_name, Array.map(derivation_path, Blob.fromArray));
46+
assert sec1_public_key.size() == 33;
47+
let bip340_public_key_bytes = Array.subArray(Blob.toArray(sec1_public_key), 1, 32);
48+
let { tweaked_address; is_even = _ } = public_key_to_p2tr_script_spend_address(network, bip340_public_key_bytes);
49+
tweaked_address;
50+
};
51+
52+
// Converts a public key to a P2TR script spend address.
53+
public func public_key_to_p2tr_script_spend_address(network : Network, bip340_public_key_bytes : [Nat8]) : {
54+
tweaked_address : BitcoinAddress;
55+
is_even : Bool;
56+
} {
57+
let leaf_script = Utils.get_ok(leafScript(bip340_public_key_bytes));
58+
let leaf_hash = leafHash(leaf_script);
59+
let tweak = Utils.get_ok(tweakFromKeyAndHash(bip340_public_key_bytes, leaf_hash));
60+
let { bip340_public_key = tweaked_public_key; is_even } = Utils.get_ok(tweakPublicKey(bip340_public_key_bytes, tweak));
61+
62+
// we can reuse `public_key_to_p2tr_key_spend_address` because this
63+
// essentially encodes the input public key as a P2TR address without tweaking
64+
{
65+
tweaked_address = P2trRawKeySpend.public_key_to_p2tr_key_spend_address(network, tweaked_public_key);
66+
is_even;
67+
};
68+
};
69+
70+
// Builds a transaction to send the given `amount` of satoshis to the
71+
// destination address.
72+
func build_transaction(
73+
own_address : BitcoinAddress,
74+
leaf_script : Script.Script,
75+
internal_public_key : [Nat8],
76+
tweaked_key_is_even : Bool,
77+
own_utxos : [Utxo],
78+
dst_address : BitcoinAddress,
79+
amount : Satoshi,
80+
fee_per_vbyte : MillisatoshiPerVByte,
81+
) : async [Nat8] {
82+
let dst_address_typed = Utils.get_ok_expect(Address.addressFromText(dst_address), "failed to decode destination address");
83+
84+
// We have a chicken-and-egg problem where we need to know the length
85+
// of the transaction in order to compute its proper fee, but we need
86+
// to know the proper fee in order to figure out the inputs needed for
87+
// the transaction.
88+
//
89+
// We solve this problem iteratively. We start with a fee of zero, build
90+
// and sign a transaction, see what its size is, and then update the fee,
91+
// rebuild the transaction, until the fee is set to the correct amount.
92+
let fee_per_vbyte_nat = Nat64.toNat(fee_per_vbyte);
93+
var total_fee : Nat = 0;
94+
95+
loop {
96+
let transaction = Utils.get_ok_expect(Bitcoin.buildTransaction(2, own_utxos, [(dst_address_typed, amount)], #p2tr_key own_address, Nat64.fromNat(total_fee)), "Error building transaction.");
97+
let tx_in_outpoints = Array.map<TxInput.TxInput, Types.OutPoint>(transaction.txInputs, func(txin) { txin.prevOutput });
98+
99+
let amounts = Array.mapFilter<Utxo, Satoshi>(
100+
own_utxos,
101+
func(utxo) {
102+
if (Option.isSome(Array.find<Types.OutPoint>(tx_in_outpoints, func(tx_in_outpoint) { tx_in_outpoint == utxo.outpoint }))) {
103+
?utxo.value;
104+
} else {
105+
null;
106+
};
107+
},
108+
);
109+
110+
// Sign the transaction. In this case, we only care about the size
111+
// of the signed transaction, so we use a mock signer here for efficiency.
112+
let signed_transaction_bytes = await sign_transaction(
113+
own_address,
114+
leaf_script,
115+
internal_public_key,
116+
tweaked_key_is_even,
117+
transaction,
118+
amounts,
119+
"", // mock key name
120+
[], // mock derivation path
121+
Utils.mock_signer,
122+
);
123+
124+
let signed_tx_bytes_len : Nat = signed_transaction_bytes.size();
125+
126+
if ((signed_tx_bytes_len * fee_per_vbyte_nat) / 1000 == total_fee) {
127+
Debug.print("Transaction built with fee " # debug_show (total_fee));
128+
return transaction.toBytes();
129+
} else {
130+
total_fee := (signed_tx_bytes_len * fee_per_vbyte_nat) / 1000;
131+
};
132+
};
133+
};
134+
135+
// Sign a bitcoin transaction.
136+
//
137+
// IMPORTANT: This method is for demonstration purposes only and it only
138+
// supports signing transactions if:
139+
//
140+
// 1. All the inputs are referencing outpoints that are owned by `own_address`.
141+
// 2. `own_address` is a P2TR script spend address.
142+
func sign_transaction(
143+
own_address : BitcoinAddress,
144+
leaf_script : Script.Script,
145+
internal_public_key : [Nat8],
146+
tweaked_key_is_even : Bool,
147+
transaction : Transaction,
148+
amounts : [Nat64],
149+
key_name : Text,
150+
derivation_path : [Blob],
151+
signer : Types.SignFunction,
152+
) : async [Nat8] {
153+
let leaf_hash = leafHash(leaf_script);
154+
155+
assert internal_public_key.size() == 32;
156+
157+
let script_bytes_sized = Script.toBytes(leaf_script);
158+
// remove the size prefix
159+
let script_bytes = Array.subArray(script_bytes_sized, 1, script_bytes_sized.size() - 1);
160+
161+
let _control_block = control_block(tweaked_key_is_even, internal_public_key);
162+
// Obtain the scriptPubKey of the source address which is also the
163+
// scriptPubKey of the Tx output being spent.
164+
switch (Address.scriptPubKey(#p2tr_key own_address)) {
165+
case (#ok scriptPubKey) {
166+
assert scriptPubKey.size() == 2;
167+
168+
// Obtain a witness for each Tx input.
169+
for (i in Iter.range(0, transaction.txInputs.size() - 1)) {
170+
let sighash = transaction.createTaprootScriptSpendSignatureHash(
171+
amounts,
172+
scriptPubKey,
173+
Nat32.fromIntWrap(i),
174+
leaf_hash,
175+
);
176+
177+
Debug.print("Signing sighash: " # debug_show (sighash));
178+
179+
let signature = Blob.toArray(await signer(key_name, derivation_path, Blob.fromArray(sighash)));
180+
transaction.witnesses[i] := [signature, script_bytes, _control_block];
181+
};
182+
};
183+
// Verify that our own address is P2TR key spend address.
184+
case (#err msg) Debug.trap("This example supports signing p2tr key spend addresses only: " # msg);
185+
};
186+
187+
transaction.toBytes();
188+
};
189+
190+
/// Sends a transaction to the network that transfers the given amount to the
191+
/// given destination, where the source of the funds is the canister itself
192+
/// at the given derivation path.
193+
public func send(network : Network, derivation_path : [[Nat8]], key_name : Text, dst_address : BitcoinAddress, amount : Satoshi) : async [Nat8] {
194+
// Get fee percentiles from previous transactions to estimate our own fee.
195+
let fee_percentiles = await BitcoinApi.get_current_fee_percentiles(network);
196+
197+
let fee_per_vbyte : MillisatoshiPerVByte = if (fee_percentiles.size() == 0) {
198+
// There are no fee percentiles. This case can only happen on a regtest
199+
// network where there are no non-coinbase transactions. In this case,
200+
// we use a default of 1000 millisatoshis/vbyte (i.e. 2 satoshi/byte)
201+
2000;
202+
} else {
203+
// Choose the 50th percentile for sending fees.
204+
fee_percentiles[50];
205+
};
206+
207+
// Fetch our public key, P2TR script spend address, and UTXOs.
208+
let own_sec1_public_key = Blob.toArray(await SchnorrApi.schnorr_public_key(key_name, Array.map(derivation_path, Blob.fromArray)));
209+
let own_bip340_public_key = Array.subArray(own_sec1_public_key, 1, 32);
210+
let { tweaked_address = own_tweaked_address; is_even } = public_key_to_p2tr_script_spend_address(network, own_bip340_public_key);
211+
212+
let own_leaf_script = Utils.get_ok(leafScript(own_bip340_public_key));
213+
214+
let _control_block = control_block(is_even, own_bip340_public_key);
215+
216+
Debug.print("Fetching UTXOs...");
217+
// Note that pagination may have to be used to get all UTXOs for the given address.
218+
// For the sake of simplicity, it is assumed here that the `utxo` field in the response
219+
// contains all UTXOs.
220+
let own_utxos = (await BitcoinApi.get_utxos(network, own_tweaked_address)).utxos;
221+
222+
// Build the transaction that sends `amount` to the destination address.
223+
let tx_bytes = await build_transaction(own_tweaked_address, own_leaf_script, own_bip340_public_key, is_even, own_utxos, dst_address, amount, fee_per_vbyte);
224+
let transaction = Utils.get_ok(Transaction.fromBytes(Iter.fromArray(tx_bytes)));
225+
226+
let tx_in_outpoints = Array.map<TxInput.TxInput, Types.OutPoint>(transaction.txInputs, func(txin) { txin.prevOutput });
227+
228+
let amounts = Array.mapFilter<Utxo, Satoshi>(
229+
own_utxos,
230+
func(utxo) {
231+
if (Option.isSome(Array.find<Types.OutPoint>(tx_in_outpoints, func(tx_in_outpoint) { tx_in_outpoint == utxo.outpoint }))) {
232+
?utxo.value;
233+
} else {
234+
null;
235+
};
236+
},
237+
);
238+
239+
// Sign the transaction.
240+
let signed_transaction_bytes = await sign_transaction(
241+
own_tweaked_address,
242+
own_leaf_script,
243+
own_bip340_public_key,
244+
is_even,
245+
transaction,
246+
amounts,
247+
key_name,
248+
Array.map(derivation_path, Blob.fromArray),
249+
SchnorrApi.sign_with_schnorr,
250+
);
251+
Debug.print("Sending transaction : " # debug_show (signed_transaction_bytes));
252+
let signed_transaction = Utils.get_ok(Transaction.fromBytes(Iter.fromArray(signed_transaction_bytes)));
253+
254+
Debug.print("Sending transaction...");
255+
await BitcoinApi.send_transaction(network, signed_transaction_bytes);
256+
257+
signed_transaction.txid();
258+
};
259+
260+
func control_block(tweaked_public_key_is_even : Bool, internal_public_key : [Nat8]) : [Nat8] {
261+
let leaf_version : Nat8 = 0xc0;
262+
let parity : Nat8 = if (tweaked_public_key_is_even) 0 else 1;
263+
let first_byte = [leaf_version | parity];
264+
265+
if (internal_public_key.size() != 32) {
266+
Debug.trap("Internal public key must be 32 bytes long to be used in control block.");
267+
};
268+
269+
let result = Array.flatten([first_byte, internal_public_key]);
270+
271+
if (result.size() != 33) {
272+
Debug.trap("Control block must be 33 bytes long.");
273+
};
274+
275+
result;
276+
};
277+
};

0 commit comments

Comments
 (0)