Developing locally
Overview
In this guide, you'll explore how you can develop and test a Bitcoin dapp locally. Testing locally allows you to iterate and improve your dapp more quickly.
dfx
includes support for both the ECDSA API and
the Bitcoin API
so that you can locally test out your dapp before deploying it to the Internet Computer.
The Bitcoin Testnet API is currently disabled and the ckTestBTC minter canister has been stopped. This is temporary and they will be re-enabled in the future.
Setting up a local Bitcoin network
To develop Bitcoin dapps locally, you'll need to set up a local Bitcoin network on your machine. Having your own local Bitcoin network allows you to mine blocks quickly and at-will, which facilitates testing various cases without having to rely on the (slow) Bitcoin testnet or the (even slower) Bitcoin mainnet.
Step 1: Download Bitcoin core v25. It is recommended to use the
.tar.gz
version for Mac users.Step 2: Unpack the
.tar.gz
file:tar -xfz bitcoin-25.0-x86_64-apple-darwin.tar.gz
Step 3: Create a directory named
data
inside the unpacked folder:cd bitcoin-25.0 && mkdir data
Step 4: Create a file called
bitcoin.conf
at the root of the unpacked folder and add the following contents:
# Enable regtest mode. This is required to setup a private bitcoin network.
regtest=1
# Dummy credentials that are required by `bitcoin-cli`.
rpcuser=ic-btc-integration
rpcpassword=QPQiNaph19FqUsCrBRN0FII7lyM26B51fAMeBQzCb-E=
rpcauth=ic-btc-integration:cdf2741387f3a12438f69092f0fdad8e$62081498c98bee09a0dce2b30671123fa561932992ce377585e8e08bb0c11dfa
Step 5: Run
bitcoind
to start the bitcoin client using the following command:
./bin/bitcoind -conf=$(pwd)/bitcoin.conf -datadir=$(pwd)/data --port=18444
If everything went well, you should see the following output. bitcoind
is now ready to accept requests.
2022-07-11T12:49:51Z Bitcoin Core version v22.0.0 (release build)
...
2022-07-11T12:49:51Z Config file arg: regtest="1"
2022-07-11T12:49:51Z Config file arg: rpcauth=****
2022-07-11T12:49:51Z Config file arg: rpcpassword=****
2022-07-11T12:49:51Z Config file arg: rpcuser=****
...
2022-07-11T12:49:51Z Bound to 127.0.0.1:18445
2022-07-11T12:49:51Z Bound to [::]:18444
2022-07-11T12:49:51Z Bound to 0.0.0.0:18444
...
2022-07-11T12:49:52Z opencon thread start
2022-07-11T12:49:52Z addcon thread start
2022-07-11T12:49:52Z 0 addresses found from DNS seeds
2022-07-11T12:49:52Z dnsseed thread exit
The command above assumes that port 18444
on your machine is available. If it isn't, change the port specified in --port
accordingly.
Deploying your Bitcoin dapp
Now that you have a local Bitcoin network, you're ready to start developing and locally testing your Bitcoin dapps.
For the next steps,you'll be leveraging the example project in the examples repo to showcase some of the key concepts. Let's clone it and explore what it's like to deploy a Bitcoin dapp locally.
Step 1: Clone the
examples
repo.
git clone https://github.com/dfinity/examples
Step 2: Go to the
basic_bitcoin
example in the language of your choice:
# For motoko
cd examples/motoko/basic_bitcoin
# For rust
cd examples/rust/basic_bitcoin
Step 3: Initialize the git submodules.
git submodule update --init --recursive
Step 4: If you're on a Mac, install Homebrew and then run the following to install additional packages:
brew install llvm binaryen cmake
Deploying in regtest
Mode
Your local Bitcoin node operates in what's called "regression testing mode", or regtest mode.
You can now deploy your canister and configure it to connect to your local regtest
network.
Step 1: Run
dfx start --clean
:
If when running dfx start
you see errors like
Failed to connect to 127.0.0.1:18444 ::: Connecting to the stream timed out.
that means that dfx
isn't able to connect to your Bitcoin node. Make sure your Bitcoin
node is up and running, and that you're setting the correct port (default is 18444
).
The port used can be changed in dfx.json
.
Step 2: Deploy the example canister:
dfx deploy basic_bitcoin --argument '(variant { regtest })'
If successful, you should see an output that looks like this:
Deploying: basic_bitcoin
Building canisters...
...
Deployed canisters.
URLs:
Candid:
basic_bitcoin: http://127.0.0.1:4943/?canisterId=...
Your canister is live and ready to use! You can interact with it using either the command line, or using the Candid UI, which is the link you see in the output above.
Generating a Bitcoin address
Bitcoin has different types of addresses (e.g. P2PKH, P2SH). Most of these addresses can be generated from an ECDSA public key. The example code showcases how your canister can generate a P2PKH address using the ecdsa_public_key API.
On the Candid UI of your canister, click the "Call" button under get_p2pkh_address
to
generate a P2PKH Bitcoin address:
Or, if you prefer the command line:
dfx canister call basic_bitcoin get_p2pkh_address
The Bitcoin address you see will be different from the one above, because the ECDSA public key your canister retrieves is unique.
Receiving Bitcoin
Mining blocks
In order to receive BTC on your local Bitcoin network, you need to mine blocks. For every block that you mine, you get some BTC as a reward for mining it.
The one key difference between working with a local Bitcoin network and Bitcoin testnet or mainnet, is how you receive Bitcoin.
In the same directory as bitcoind
, you can issue the following command to mine blocks.
./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf generatetoaddress <number-of-blocks> <address>
For example, let's mine a block and have the block reward be given to your canister:
./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf generatetoaddress 1 <your-canister-btc-address>
where <your-canister-btc-address>
is the address you obtained from calling the get_p2pkh_address
endpoint on your canister.
If successful, you'll see an output that looks similar, but not identical, to this:
[
"5eaf0bb0947bc5c3348749b7e194e000f6d93902235e7422b6472a1edfa5a821"
]
This is the hash of the block you just mined. In the logs of dfx
, within a few seconds
you should see something like this:
INFO .../blockchainmanager Added headers: Height = 1, Active chain's tip = 5eaf0bb0947bc5c3348749b7e194e000f6d93902235e7422b6472a1edfa5a821
DEBG .../ic_btc_canister/heartbeat New Bitcoin tip height: 1
These logs indicate that your local dfx
project has ingested the block you just mined.
Syncing the first bitcoin block can take up to 30 seconds. Subsequent blocks sync nearly instantly.
Now, check your BTC balance:
Or, via the command line:
dfx canister call basic_bitcoin get_balance '("<your-canister-btc-address>")'
If everything worked well, you should see a balance of 5_000_000_000 Satoshi, which is 50 BTC. This is the reward you received for mining one block.
The BTC you mine is valid only in your local bitcoin network and cannot be spent or used elsewhere.
Coinbase maturity
In the previous step, you mined one block, giving your canister a reward of 50 BTC in the process.
One caveat of these block rewards is that they are subject to the Coinbase maturity rule, which states that, in order for you to spend them, you will first need to mine 100 additional blocks.
Let's mine 100 additional blocks by running the following command:
./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf generatetoaddress 100 mtbZzVBwLnDmhH4pE9QynWAgh6H3aC1E6M
The above command will mine 100 blocks, giving the block rewards to a random address. By doing so, the 50 BTC in rewards that your canister already has will now be spendable.
If successful, you'll see an output that looks similar, but not identical, to this:
[
"0e70a5e8a56f1799e13fd7a52b445023c2d857a5d4b508971390dc9ae010dedc",
...
"3e0410961a3b144d16dcdc95d73c262e0ec09e9812ce33835b2a55606d2be84b"
]
In the logs of dfx
, you should see something like this within a few seconds:
INFO .../blockchainmanager Added headers: Height = 101, Active chain's tip = 3e0410961a3b144d16dcdc95d73c262e0ec09e9812ce33835b2a55606d2be84b
DEBG .../ic_btc_canister/heartbeat New Bitcoin tip height: 101
These logs indicate that your local dfx
project has ingested the 100 new blocks you just mined.
The BTC your canister owns can now be spent.
Sending Bitcoin
You can send Bitcoin using the send
endpoint on your canister.
In the Candid UI, add a destination address and an amount to send. In the example
below, you're sending 1 BTC to the address n2dcQfuwFw7M2UYzLfM6P7DwewsQaygb8S
.
Via command line, the same call would look like this:
dfx canister call basic_bitcoin send '(record { destination_address = "n2dcQfuwFw7M2UYzLfM6P7DwewsQaygb8S"; amount_in_satoshi = 100000000; })'
The command above creates a transaction and sends it out to your local Bitcoin node.
For more details on how the send
endpoint works, see the deploying your first Bitcoin dapp tutorial.
There still remains one additional step, which is to mine a block so that the transaction
you just sent becomes part of the blockchain. Run the following from the bitcoind
directory:
./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf generatetoaddress 1 mtbZzVBwLnDmhH4pE9QynWAgh6H3aC1E6M
The above command is similar to what you did in the section on Coinbase maturity.
And, similarly, you should see the logs in dfx
updating to indicate the ingestion of this new
block.
If everything worked, you should now see that address n2dcQfuwFw7M2UYzLfM6P7DwewsQaygb8S
has a balance of 1 BTC.
Troubleshooting
Sending transactions
If you're trying to send a transaction and the transaction isn't being mined,
try sending the same transaction via bitcoin-cli
, as it can reveal helpful errors:
./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf sendrawtransaction <tx-in-hex>
For example:
./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf sendrawtransaction 0200000001b0ca9600da1057765dab692467579cb309aba2524d0cd45376874d6e39e1cd50000000006a47304402200024c768daeb38591438cb3cc7e40212442e8a3662f7465bd466d00ceede8014022069ca804f7ca0ba757c9531c5c37c41943a1a1b53b96697b27e928fb35637c5b10121024f9671d4f1a434cfa6c5ca863670b34f2836c259ef2c8c33f4195f2997dc3ee3ffffffff0200e1f505000000001976a914ce0966271055a5b17abb0e9021a7eafdfe557d3088ac1f101024010000001976a9143fc55ebf65a72a3658f0e14c75c99f6eae65b9b388ac00000000
error code: -25
error message:
bad-txns-inputs-missingorspent
Resetting the state
It's often useful to delete all the Bitcoin state you have locally and to start from scratch. To do so:
Step 1: Run the following commands in the directory of your
dfx
project to delete the local state ofdfx
.
dfx stop
rm -rf .dfx
Running rm -rf .dfx
will permanently delete all the canisters you have
installed locally.
Step 2: In the folder where you're running
bitcoind
, stop thebitcoind
process if it is running, and then run the following to delete the chain you created.
rm -r data
mkdir data