Skip to main content

Generating a Bitcoin address

Advanced
Bitcoin

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.

  };

/// Returns the UTXOs of the given Bitcoin address.
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));
};

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

  public func get_p2tr_key_only_address() : async BitcoinAddress {
await P2trKeyOnly.get_address_key_only(schnorr_canister_actor, NETWORK, KEY_NAME, p2trKeyOnlyDerivationPath());
};

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

Generating a P2TR address

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

Learn more

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.