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 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.
Bitcoin SegWit addresses
SegWit addresses are newer addresses following the Bech32
format that starts 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 addresses with threshold ECDSA
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.
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.
To test canisters locally that use the following code snippets, you will need to enable local Bitcoin development. To do this, you can either start the local development environment with dfx start --enable-bitcoin
or you can include the following configuration in the project's dfx.json
file:
"defaults": {
"bitcoin": {
"enabled": true,
"nodes": [
"127.0.0.1:18444"
],
"log_level": "info"
},
:::
- Motoko
- Rust
public func get_utxos(address : BitcoinAddress) : async GetUtxosResponse {
await BitcoinApi.get_utxos(NETWORK, address);
};
/// Returns the 100 fee percentiles measured in millisatoshi/vbyte.
/// Percentiles are computed from the last 10,000 transactions (if available).
public func get_current_fee_percentiles() : async [MillisatoshiPerVByte] {
await BitcoinApi.get_current_fee_percentiles(NETWORK);
};
/// Returns the P2PKH address of this canister at a specific derivation path.
public func get_p2pkh_address() : async BitcoinAddress {
await P2pkh.get_address(ecdsa_canister_actor, NETWORK, KEY_NAME, p2pkhDerivationPath());
};
/// Sends the given amount of bitcoin from this canister to the given address.
/// Returns the transaction ID.
public func send_from_p2pkh_address(request : SendRequest) : async TransactionId {
Utils.bytesToText(await P2pkh.send(ecdsa_canister_actor, NETWORK, p2pkhDerivationPath(), KEY_NAME, request.destination_address, request.amount_in_satoshi));
};
public func get_p2tr_key_only_address() : async BitcoinAddress {
await P2trKeyOnly.get_address_key_only(schnorr_canister_actor, NETWORK, KEY_NAME, p2trKeyOnlyDerivationPath());
};
use crate::{common::DerivationPath, ecdsa::get_ecdsa_public_key, BTC_CONTEXT};
use bitcoin::{Address, PublicKey};
use ic_cdk::update;
/// Returns a legacy P2PKH (Pay-to-PubKey-Hash) address for this smart contract.
///
/// This address uses an ECDSA public key and encodes it in the legacy Base58 format.
/// It is supported by all bitcoin wallets and full nodes.
#[update]
pub async fn get_p2pkh_address() -> String {
let ctx = BTC_CONTEXT.with(|ctx| ctx.get());
// Unique derivation paths are used for every address type generated, to ensure
// each address has its own unique key pair.
let derivation_path = DerivationPath::p2pkh(0, 0);
// Get the ECDSA public key of this smart contract at the given derivation path
let public_key = get_ecdsa_public_key(&ctx, derivation_path.to_vec_u8_path()).await;
// Convert the public key to the format used by the Bitcoin library
let public_key = PublicKey::from_slice(&public_key).unwrap();
// Generate a legacy P2PKH address from the public key.
// The address encoding (Base58) depends on the network type.
Address::p2pkh(public_key, ctx.bitcoin_network).to_string()
}
Generating a P2WPKH address
- Rust
use crate::{common::DerivationPath, ecdsa::get_ecdsa_public_key, BTC_CONTEXT};
use bitcoin::{Address, CompressedPublicKey};
use ic_cdk::update;
/// Returns a native SegWit (P2WPKH) address for this smart contract.
///
/// This address uses a compressed ECDSA public key and is encoded in Bech32 (BIP-173).
/// It is widely supported and offers lower fees due to reduced witness data size.
#[update]
pub async fn get_p2wpkh_address() -> String {
let ctx = BTC_CONTEXT.with(|ctx| ctx.get());
// Unique derivation paths are used for every address type generated, to ensure
// each address has its own unique key pair.
let derivation_path = DerivationPath::p2wpkh(0, 0);
// Get the ECDSA public key of this smart contract at the given derivation path
let public_key = get_ecdsa_public_key(&ctx, derivation_path.to_vec_u8_path()).await;
// Create a CompressedPublicKey from the raw public key bytes
let public_key = CompressedPublicKey::from_slice(&public_key).unwrap();
// Generate a P2WPKH Bech32 address.
// The network (mainnet, testnet, regtest) determines the HRP (e.g., "bc1" or "tb1").
Address::p2wpkh(&public_key, ctx.bitcoin_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
public func send_from_p2tr_key_only_address(request : SendRequest) : async TransactionId {
Utils.bytesToText(await P2trKeyOnly.send(schnorr_canister_actor, NETWORK, p2trKeyOnlyDerivationPath(), KEY_NAME, request.destination_address, request.amount_in_satoshi));
};
public func get_p2tr_address() : async BitcoinAddress {
await P2tr.get_address(schnorr_canister_actor, NETWORK, KEY_NAME, p2trDerivationPaths());
};
public func send_from_p2tr_address_key_path(request : SendRequest) : async TransactionId {
Utils.bytesToText(await P2tr.send_key_path(schnorr_canister_actor, NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi));
};
public func send_from_p2tr_address_script_path(request : SendRequest) : async TransactionId {
Utils.bytesToText(await P2tr.send_script_path(schnorr_canister_actor, NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi));
};
func p2pkhDerivationPath() : [[Nat8]] {
derivationPathWithSuffix("p2pkh");
};
func p2trKeyOnlyDerivationPath() : [[Nat8]] {
derivationPathWithSuffix("p2tr_key_only");
};
func p2trDerivationPaths() : P2trDerivationPaths {
{
key_path_derivation_path = derivationPathWithSuffix("p2tr_internal_key");
script_path_derivation_path = derivationPathWithSuffix("p2tr_script_key");
};
};
func derivationPathWithSuffix(suffix : Blob) : [[Nat8]] {
Array.flatten([DERIVATION_PATH, [Blob.toArray(suffix)]]);
use bitcoin::{key::Secp256k1, Address, PublicKey, XOnlyPublicKey};
use ic_cdk::update;
use crate::{common::DerivationPath, schnorr::get_schnorr_public_key, BTC_CONTEXT};
/// Returns a Taproot (P2TR) address of this smart contract that supports **key path spending only**.
///
/// This address does not commit to a script path (it commits to an unspendable path per BIP-341).
/// It allows spending using a single Schnorr signature corresponding to the internal key.
#[update]
pub async fn get_p2tr_key_path_only_address() -> String {
let ctx = BTC_CONTEXT.with(|ctx| ctx.get());
// Derivation path strategy:
// We assign fixed address indexes for key roles within Taproot:
// - Index 0: key-path-only Taproot (no script tree committed)
// - Index 1: internal key for a Taproot output that includes a script tree
// - Index 2: script leaf key committed to in the Merkle tree
let internal_key_path = DerivationPath::p2tr(0, 0);
// Derive the public key used as the internal key (untweaked key path base).
// This key is used for key path spending only, without any committed script tree.
let internal_key = get_schnorr_public_key(&ctx, internal_key_path.to_vec_u8_path()).await;
// Convert the internal key to an x-only public key, as required by Taproot (BIP-341).
let internal_key = XOnlyPublicKey::from(PublicKey::from_slice(&internal_key).unwrap());
// Create a Taproot address using the internal key only.
// We pass `None` as the Merkle root, which per BIP-341 means the address commits
// to an unspendable script path, enabling only key path spending.
let secp256k1_engine = Secp256k1::new();
Address::p2tr(&secp256k1_engine, internal_key, None, ctx.bitcoin_network).to_string()
}
Generating a P2TR address
- Motoko
- Rust
public func send_from_p2tr_address_key_path(request : SendRequest) : async TransactionId {
Utils.bytesToText(await P2tr.send_key_path(schnorr_canister_actor, NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi));
};
public func send_from_p2tr_address_script_path(request : SendRequest) : async TransactionId {
Utils.bytesToText(await P2tr.send_script_path(schnorr_canister_actor, NETWORK, p2trDerivationPaths(), KEY_NAME, request.destination_address, request.amount_in_satoshi));
};
func p2pkhDerivationPath() : [[Nat8]] {
derivationPathWithSuffix("p2pkh");
};
func p2trKeyOnlyDerivationPath() : [[Nat8]] {
derivationPathWithSuffix("p2tr_key_only");
};
func p2trDerivationPaths() : P2trDerivationPaths {
{
key_path_derivation_path = derivationPathWithSuffix("p2tr_internal_key");
script_path_derivation_path = derivationPathWithSuffix("p2tr_script_key");
};
};
func derivationPathWithSuffix(suffix : Blob) : [[Nat8]] {
Array.flatten([DERIVATION_PATH, [Blob.toArray(suffix)]]);
};
};
use crate::{common::DerivationPath, p2tr, schnorr::get_schnorr_public_key, BTC_CONTEXT};
use bitcoin::Address;
use ic_cdk::update;
/// Returns a Taproot (P2TR) address with a spendable script path.
///
/// This address supports:
/// - Key path spending via a tweaked internal key (standard Taproot path)
/// - Script path spending via a single script leaf: `<script_leaf_key> CHECKSIG`
///
/// The two public keys are derived from distinct derivation paths:
/// - Internal key: p2tr(0, 1) — used for tweaking and key path spending
/// - Script leaf key: p2tr(0, 2) — used in the script tree (as `<key> OP_CHECKSIG`)
#[update]
pub async fn get_p2tr_script_path_enabled_address() -> String {
let ctx = BTC_CONTEXT.with(|ctx| ctx.get());
// Derivation path strategy:
// We assign fixed address indexes for key roles within Taproot:
// - Index 0: key-path-only Taproot (no script tree committed)
// - Index 1: internal key for a Taproot output that includes a script tree
// - Index 2: script leaf key committed to in the Merkle tree
let internal_key_path = DerivationPath::p2tr(0, 1);
let script_leaf_key_path = DerivationPath::p2tr(0, 2);
// Derive the Schnorr public keys used in this Taproot output:
// - `internal_key` is used as the untweaked base key (for key path spending)
// - `script_key` is used inside a Taproot leaf script (for script path spending)
let internal_key = get_schnorr_public_key(&ctx, internal_key_path.to_vec_u8_path()).await;
let script_key = get_schnorr_public_key(&ctx, script_leaf_key_path.to_vec_u8_path()).await;
// Construct the Taproot leaf script: <script_key> OP_CHECKSIG
// This is a simple script that allows spending via the script_key alone.
let taproot_spend_info = p2tr::create_taproot_spend_info(&internal_key, &script_key);
// Construct and return the final Taproot address.
// The address encodes the tweaked output key and is network-aware (mainnet, testnet, etc.).
Address::p2tr_tweaked(taproot_spend_info.output_key(), ctx.bitcoin_network).to_string()
}
Learn more
Learn more about Bitcoin addresses using ECDSA.
Learn more about Bitcoin addresses using Schnorr: