Generating a Bitcoin address
For a canister to receive Bitcoin payments, it must generate a Bitcoin address. In contrast to most other blockchains, Bitcoin doesn't use accounts. Instead, it uses a UTXO model. A UTXO is an unspent transaction output, and a Bitcoin transaction spends one or more UTXOs and creates new UTXOs. Each UTXO is associated with a Bitcoin address, which is derived from a public key or a script that defines the conditions under which the UTXO can be spent. A Bitcoin address is often used as a single use invoice instead of a persistent address to increase privacy.
Bitcoin address types
Bitcoin uses multiple address types:
Legacy addresses
These addresses start with a 1
and are called P2PKH
(Pay to Public Key Hash)
addresses. They encode the hash of an ECDSA public key.
There is also another type of legacy address that starts with a 3
called P2SH
(Pay to Script Hash) that encodes the hash of a
script. The script can define complex
conditions such as multisig or timelocks.
SegWit addresses
SegWit addresses are newer addresses following the
Bech32
format that start with bc1
. They are cheaper to spend than legacy addresses
and solve problems regarding transaction malleability, which is important for advanced use cases
like Partially Signed Bitcoin Transactions (PSBT) or the Lightning Network.
SegWit addresses can be of three types:
P2WPKH
(Pay to witness public key hash): A SegWit address that encodes the hash of an ECDSA public key.P2WSH
(Pay to witness script hash): A SegWit address that encodes the hash of a script.P2TR
(Pay to taproot): A SegWit address that can be unlocked by a Schnorr signature or a script.
Generating a Bitcoin address
As mentioned above, a Bitcoin address is derived from a public key or a script. To generate a Bitcoin address that can only be spent by a specific canister or a specific caller of a canister, you need to derive the address from a canister's public key.
Generating addresses with threshold ECDSA
An ECDSA public key can be retrieved using the
ecdsa_public_key
API. The basic Bitcoin
example
demonstrates how to generate a P2PKH
address from a canister's public key.
- Motoko
- Rust
/// Returns the P2PKH address of this canister at the given derivation path.
public func get_p2pkh_address(network : Network, key_name : Text, derivation_path : [[Nat8]]) : async BitcoinAddress {
// Fetch the public key of the given derivation path.
let public_key = await EcdsaApi.ecdsa_public_key(key_name, Array.map(derivation_path, Blob.fromArray));
// Compute the address.
public_key_to_p2pkh_address(network, Blob.toArray(public_key))
};
// Converts a public key to a P2PKH address.
func public_key_to_p2pkh_address(network : Network, public_key_bytes : [Nat8]) : BitcoinAddress {
let public_key = public_key_bytes_to_public_key(public_key_bytes);
// Compute the P2PKH address from our public key.
P2pkh.deriveAddress(Types.network_to_network_camel_case(network), Publickey.toSec1(public_key, true))
};
/// Returns the P2PKH address of this canister at the given derivation path.
pub async fn get_p2pkh_address(
network: BitcoinNetwork,
key_name: String,
derivation_path: Vec<Vec<u8>>,
) -> String {
// Fetch the public key of the given derivation path.
let public_key = ecdsa_api::ecdsa_public_key(key_name, derivation_path).await;
// Compute the address.
public_key_to_p2pkh_address(network, &public_key)
}
// Converts a public key to a P2PKH address.
fn public_key_to_p2pkh_address(network: BitcoinNetwork, public_key: &[u8]) -> String {
Address::p2pkh(
&PublicKey::from_slice(public_key).expect("failed to parse public key"),
transform_network(network),
)
.to_string()
}
Generating addresses with threshold Schnorr
A Schnorr public key can be retrieved using the
schnorr_public_key
API. The basic Bitcoin
example
also demonstrates how to generate two different types of P2TR
addresses,
a key-only address and an address allowing spending using a key or script, from a
canister's public key.
Generating a key-only P2TR address
- Motoko
- Rust
// Main.mo
public func get_p2tr_key_only_address() : async BitcoinAddress {
await P2trKeyOnly.get_address_key_only(schnorr_canister_actor, NETWORK, KEY_NAME, p2trKeyOnlyDerivationPath());
};
// P2trKeyOnly.mo
import { tweakFromKeyAndHash; tweakPublicKey } "mo:bitcoin/bitcoin/P2tr";
/// Returns the P2TR key-only address of this canister at a specific
/// derivation path. The Merkle tree root is computed as
/// `taggedHash(bip340_public_key_bytes, "TapTweak")` and is unspendable.
public func get_address_key_only(schnorr_canister_actor : SchnorrCanisterActor, network : Network, key_name : Text, derivation_path : [[Nat8]]) : async BitcoinAddress {
let bip340_public_key_bytes = await P2tr.fetch_bip340_public_key(schnorr_canister_actor, key_name, derivation_path);
let merkleRoot = P2tr.unspendableMerkleRoot(bip340_public_key_bytes);
let tweak = Utils.get_ok(tweakFromKeyAndHash(bip340_public_key_bytes, merkleRoot));
let tweaked_public_key = Utils.get_ok(tweakPublicKey(bip340_public_key_bytes, tweak)).bip340_public_key;
P2tr.tweaked_public_key_to_p2tr_address(network, tweaked_public_key);
};
// P2tr.mo
public func unspendableMerkleRoot(untweaked_bip340_public_key : [Nat8]) : [Nat8] {
Hash.taggedHash(untweaked_bip340_public_key, "TapTweak");
};
public func fetch_bip340_public_key(schnorr_canister_actor : SchnorrCanisterActor, key_name : Text, derivation_path : [[Nat8]]) : async [Nat8] {
let sec1_public_key = Blob.toArray(await SchnorrApi.schnorr_public_key(schnorr_canister_actor, key_name, Array.map(derivation_path, Blob.fromArray)));
Array.subArray(sec1_public_key, 1, 32);
};
// Converts a tweaked public key to a P2TR address.
public func tweaked_public_key_to_p2tr_address(network : Network, bip340_public_key_bytes : [Nat8]) : BitcoinAddress {
// human-readable part of the address
let hrp = switch (network) {
case (#mainnet) "bc";
case (#testnet) "tb";
case (#regtest) "bcrt";
};
let version : Nat8 = 1;
assert bip340_public_key_bytes.size() == 32;
switch (Segwit.encode(hrp, { version; program = bip340_public_key_bytes })) {
case (#ok address) address;
case (#err msg) Debug.trap("Error encoding segwit address: " # msg);
};
};
/// Returns the P2TR key-only address of this canister at the given derivation
/// path.
///
/// Quoting the `bitcoin` crate's rustdoc:
///
/// *Note*: As per BIP341
///
/// When the Merkle root is [`None`], the output key commits to an unspendable script path
/// instead of having no script path. This is achieved by computing the output key point as
/// `Q = P + int(hashTapTweak(bytes(P)))G`. See also [`TaprootSpendInfo::tap_tweak`].
pub async fn get_address(
network: BitcoinNetwork,
key_name: String,
derivation_path: Vec<Vec<u8>>,
) -> 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 secp256k1_engine = Secp256k1::new();
Address::p2tr(
&secp256k1_engine,
x_only_pubkey,
None,
super::common::transform_network(network),
)
}
Generating a P2TR address
- Motoko
- Rust
// Main.mo
public func get_p2tr_address() : async BitcoinAddress {
await P2tr.get_address(schnorr_canister_actor, NETWORK, KEY_NAME, p2trDerivationPaths());
};
// P2tr.mo
import {
leafHash;
leafScript;
tweakFromKeyAndHash;
tweakPublicKey;
} "mo:bitcoin/bitcoin/P2tr";
/// Returns the P2TR address that allows for key as well as script spends.
public func get_address(schnorr_canister_actor : SchnorrCanisterActor, network : Network, key_name : Text, derivation_paths : P2trDerivationPaths) : async BitcoinAddress {
let internal_bip340_public_key = await fetch_bip340_public_key(schnorr_canister_actor, key_name, derivation_paths.key_path_derivation_path);
let script_bip340_public_key = await fetch_bip340_public_key(schnorr_canister_actor, key_name, derivation_paths.script_path_derivation_path);
let { tweaked_address; is_even = _ } = internal_key_and_script_key_to_p2tr_address(internal_bip340_public_key, script_bip340_public_key, network);
tweaked_address;
};
public func fetch_bip340_public_key(schnorr_canister_actor : SchnorrCanisterActor, key_name : Text, derivation_path : [[Nat8]]) : async [Nat8] {
let sec1_public_key = Blob.toArray(await SchnorrApi.schnorr_public_key(schnorr_canister_actor, key_name, Array.map(derivation_path, Blob.fromArray)));
Array.subArray(sec1_public_key, 1, 32);
};
/// Converts an internal public key and a script public key to a P2TR spend
/// address. The script public key is used to derive the leaf script, which
/// can be spent only using the script public key.
public func internal_key_and_script_key_to_p2tr_address(internal_bip340_public_key : [Nat8], script_bip340_public_key : [Nat8], network : Network) : {
tweaked_address : BitcoinAddress;
is_even : Bool;
} {
let leaf_script = Utils.get_ok(leafScript(script_bip340_public_key));
let leaf_hash = leafHash(leaf_script);
let tweak = Utils.get_ok(tweakFromKeyAndHash(internal_bip340_public_key, leaf_hash));
let { bip340_public_key = tweaked_public_key; is_even } = Utils.get_ok(tweakPublicKey(internal_bip340_public_key, tweak));
{
tweaked_address = tweaked_public_key_to_p2tr_address(network, tweaked_public_key);
is_even;
};
};
// Converts a tweaked public key to a P2TR address.
public func tweaked_public_key_to_p2tr_address(network : Network, bip340_public_key_bytes : [Nat8]) : BitcoinAddress {
// human-readable part of the address
let hrp = switch (network) {
case (#mainnet) "bc";
case (#testnet) "tb";
case (#regtest) "bcrt";
};
let version : Nat8 = 1;
assert bip340_public_key_bytes.size() == 32;
switch (Segwit.encode(hrp, { version; program = bip340_public_key_bytes })) {
case (#ok address) address;
case (#err msg) Debug.trap("Error encoding segwit address: " # msg);
};
};
/// Returns the P2TR address of this canister at the given derivation path. This
/// address uses two public keys:
/// 1) an internal key,
/// 2) a key that can be used to spend from a script.
///
/// The keys are derived by appending additional information to the provided
/// `derivation_path`.
pub async fn get_address(
network: BitcoinNetwork,
key_name: String,
derivation_path: Vec<Vec<u8>>,
) -> Address {
let (internal_key, script_path_key) = get_public_keys(key_name, derivation_path).await;
public_keys_to_p2tr_script_spend_address(
network,
internal_key.as_slice(),
script_path_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_keys_to_p2tr_script_spend_address(
bitcoin_network: BitcoinNetwork,
internal_key: &[u8],
script_key: &[u8],
) -> Address {
let network = super::common::transform_network(bitcoin_network);
let taproot_spend_info = p2tr_script_spend_info(internal_key, script_key);
Address::p2tr_tweaked(taproot_spend_info.output_key(), network)
}
fn p2tr_script_spend_info(internal_key_bytes: &[u8], script_key_bytes: &[u8]) -> TaprootSpendInfo {
// Script used in script path spending.
let spend_script = p2tr_script(script_key_bytes);
let secp256k1_engine = Secp256k1::new();
// Key used in the key path spending.
let internal_key = XOnlyPublicKey::from(PublicKey::from_slice(&internal_key_bytes).unwrap());
// Taproot with an internal key and a single script.
TaprootBuilder::new()
.add_leaf(0, spend_script.clone())
.expect("adding leaf should work")
.finalize(&secp256k1_engine, internal_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()
}
Learn more
See more examples of addresses generated using rust-bitcoin
.
Learn more about Bitcoin addresses using ECDSA.
Learn more about Bitcoin addresses using Schnorr:
Learn more about the ecdsa_public_key
API.
Learn more about the schnorr_public_key
API.
Next steps
Learn how to create a Bitcoin transaction to spend the BTC received by the address.