Local development
To build Bitcoin dapps on ICP, developers can set up a local development environment with Bitcoin support and implement workflows like generating Bitcoin addresses, creating and signing transactions, submitting them to the local Bitcoin network, and reading data like balances or transaction details.
How are Bitcoin apps tested?
The IC SDK (dfx
) is a toolkit for building and deploying apps on ICP, supported on Linux and macOS Monterey or newer, with Windows users needing WSL 2 to use it.
Using dfx
, you can connect to regtest, a local Bitcoin network designed for development and testing. Regtest allows you to create blocks and control the network environment, making it ideal for simulating Bitcoin interactions without using real BTC.
Regtest allows for instance to
- Mine blocks instantly for testing
- Generate and spend BTC in a controlled environment
- Inspect transactions in the mempool
With dfx
you can deploy an ICP smart contract locally, then execute Bitcoin tasks like address generation, transaction creation, signing, and submission workflows.
Learn more about setting up your local developer environment with Bitcoin support.
Example: Generating an address
The Bitcoin network uses different types of addresses (e.g., P2PKH, P2SH), most of which can be generated from an ECDSA public key. For example, here is how you can generate a P2TR address from a Rust based smart contract:
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()
}
Interacting with smart contracts locally
Once a smart contract has been deployed locally, you can interact with it using the dfx
CLI tool. This allows for rapid application development and manual testing in the local environment.
dfx canister call <smart_contract_name> get_p2tr_key_path_only_address
In addition to the CLI, each deployed canister is automatically assigned a Candid UI, a web-based interface that lets you call exposed functions from your browser. This is especially useful for inspecting or testing application behavior without needing to write scripts or CLI commands. The Candid UI is automatically generated based on the canister's interface and can be accessed locally or on the IC mainnet.
With both the CLI and Candid UI available, developers have flexible options for interacting with smart contracts—making it easy to test, debug, and explore Bitcoin workflows during development.
Learn more about generating Bitcoin addresses and signing transactions on ICP.