Skip to main content

Generating a Bitcoin address

Advanced
Bitcoin

Overview

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.


/// 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))
};

View in the full example.

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, an untweaked key path address and a script spend address, from a canister's public key.

The Internet Computer currently supports exclusively one of both types of addresses, but not addresses that can use both a key path and a script path, meaning that an address supporting a key path cannot spend with a script and vice versa.

Generating an untweaked key path address

It is important to make sure that the address is generated from an untweaked key. Otherwise, the signature verification, and thus the Bitcoin transaction, will fail. Most libraries will automatically tweak the key when creating a taproot address by default, so make sure to use the correct function to generate the address as shown in the example below.


// Main.mo

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

// P2trRawKeySpend.mo

public func get_address(network : Network, key_name : Text, derivation_path : [[Nat8]]) : async BitcoinAddress {
// Fetch the public key of the given derivation path.
let sec1_public_key = await SchnorrApi.schnorr_public_key(key_name, Array.map(derivation_path, Blob.fromArray));
assert sec1_public_key.size() == 33;

let bip340_public_key_bytes = Array.subArray(Blob.toArray(sec1_public_key), 1, 32);

public_key_to_p2tr_key_spend_address(network, bip340_public_key_bytes);
};

// Converts a public key to a P2TR raw key spend address.
public func public_key_to_p2tr_key_spend_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);
};
};

View in the full example.

Generating a script path address


// Main.mo

public func get_p2tr_script_spend_address() : async BitcoinAddress {
await P2trScriptSpend.get_address(NETWORK, KEY_NAME, DERIVATION_PATH);
};

// P2trScriptSpend.mo

public func get_address(network : Network, key_name : Text, derivation_path : [[Nat8]]) : async BitcoinAddress {
// Fetch the public key of the given derivation path.
let sec1_public_key = await SchnorrApi.schnorr_public_key(key_name, Array.map(derivation_path, Blob.fromArray));
assert sec1_public_key.size() == 33;
let bip340_public_key_bytes = Array.subArray(Blob.toArray(sec1_public_key), 1, 32);
let { tweaked_address; is_even = _ } = public_key_to_p2tr_script_spend_address(network, bip340_public_key_bytes);
tweaked_address;
};

// Converts a public key to a P2TR script spend address.
public func public_key_to_p2tr_script_spend_address(network : Network, bip340_public_key_bytes : [Nat8]) : {
tweaked_address : BitcoinAddress;
is_even : Bool;
} {
let leaf_script = Utils.get_ok(leafScript(bip340_public_key_bytes));
let leaf_hash = leafHash(leaf_script);
let tweak = Utils.get_ok(tweakFromKeyAndHash(bip340_public_key_bytes, leaf_hash));
let { bip340_public_key = tweaked_public_key; is_even } = Utils.get_ok(tweakPublicKey(bip340_public_key_bytes, tweak));

// we can reuse `public_key_to_p2tr_key_spend_address` because this
// essentially encodes the input public key as a P2TR address without tweaking
{
tweaked_address = P2trRawKeySpend.public_key_to_p2tr_key_spend_address(network, tweaked_public_key);
is_even;
};
};

View in the full example.

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.