Skip to content

16.4: Using BDK with Miniscript

BDK, the Bitcoin Dev Kit, is a powerful set of libraries based on rust-bitcoin and rust-minscript that can be used for coding Bitcoin functionality in Rust. It includes bdk-cli, a command-line tool built on the BDK libraries that mirrors and expands much of the functionality of bitcoin-cli. bdk-cli could support a "Learning" course of its own; this chapter just shows how to install it, how to use it for some very basic functionality, and most importantly how to use it as a Policy compiler.

Install BDK

bdk-cli is a Rust package, so it can be installed with cargo. You'll want to add either the electrum or therpcfeature (to talk to abitcoindserver) and thecompiler` feature (to get access to BDK's Policy compiler.

cargo install bdk-cli --features electrum,compiler

Work with Bitcoind

Choose Your Server

bdk-cli can be built to work with Esplora, Electrum, Kyoto, or RPC. Usually, we'd choose RPC, so that we can use our own private server ... except bdk-cli doesn't sync properly with a pruned node.

So we're instead going to use electrum, as ssl://mempool.space:60602 provides access to a public Signet server.

Config a Wallet

All commands in bdk-cli are hierarchical, so for example you have a wallet command and as a config command that allows you to create a wallet from a descriptor (much like importdescriptors for bitcoin-cli).

In this case, we're going to demonstrate basic bdk-cli functionality (and also offer a real-world example of how descriptors are great for moving funds between wallets) by copying a descriptor from bitcoin-cli to bdk-cli.

Here's our Segwit descriptors from bitcoin-cli, with their private keys:

bitcoin-cli listdescriptors true
{
   ...
    {
      "desc": "wpkh(tprv8ZgxMBicQKsPfCYFA9DANn81pjg3GPfAzLEdtywSxJAL3pGz41CgVZ8ChvdULoetthwZmd7nYvghRBaY2wDvg1WoStSumpGcdqRW3ZdpY1b/84h/1h/0h/0/*)#9fxgzcuj",
      "timestamp": 1784828880,
      "active": true,
      "internal": false,
      "range": [
        0,
        1000
      ],
      "next": 1,
      "next_index": 1
    },
    {
      "desc": "wpkh(tprv8ZgxMBicQKsPfCYFA9DANn81pjg3GPfAzLEdtywSxJAL3pGz41CgVZ8ChvdULoetthwZmd7nYvghRBaY2wDvg1WoStSumpGcdqRW3ZdpY1b/84h/1h/0h/1/*)#5arfldv2",
      "timestamp": 1784828881,
      "active": true,
      "internal": true,
      "range": [
        0,
        999
      ],
      "next": 0,
      "next_index": 0
    }
  ]
}

The following command will then import those descriptors into bdk-cli:

bdk-cli \
    --network signet \
    wallet --wallet bitcoincore config \
    --ext-descriptor "wpkh(tprv8ZgxMBicQKsPfCYFA9DANn81pjg3GPfAzLEdtywSxJAL3pGz41CgVZ8ChvdULoetthwZmd7nYvghRBaY2wDvg1WoStSumpGcdqRW3ZdpY1b/84h/1h/0h/0/*)" \
    --int-descriptor "wpkh(tprv8ZgxMBicQKsPfCYFA9DANn81pjg3GPfAzLEdtywSxJAL3pGz41CgVZ8ChvdULoetthwZmd7nYvghRBaY2wDvg1WoStSumpGcdqRW3ZdpY1b/84h/1h/0h/1/*)" \
    --url "ssl://mempool.space:60602" \
    --client-type electrum \
    --database-type sqlite 

(There are shorter versions of all these flags, and some of them default to command-line variables, but here we're showing them in their full glory to demonstrate how they work.)

After some warnings you should see:

| {
|   "message": "Wallet 'bitcoincore' initialized successfully in \"/home/standup/.bdk-bitcoin/config.toml\""
| }

Commands like wallets and descriptor will now show you what's available in your bdk-cli wallet.

bdk-cli descriptor

| {
|   "public_descriptors": {
|     "external": "wpkh([3fa9af92/84'/1'/0']tpubDD1TyyMFnYyV7wq9627FgdxHJ8gwHNXQyuJDunMnR1UQqiqw3rV7hnW1TfPbFbupm6K6SdxPhr8kyMvXbP2gAs22J6LQknwpGGH5JeYyC2S/0/*)#890undlk",
|     "internal": "wpkh([3fa9af92/84'/1'/0']tpubDD1TyyMFnYyV7wq9627FgdxHJ8gwHNXQyuJDunMnR1UQqiqw3rV7hnW1TfPbFbupm6K6SdxPhr8kyMvXbP2gAs22J6LQknwpGGH5JeYyC2S/1/*)#k32awc0w"
|   },
|   "private_descriptors": {
|     "external": "wpkh(tprv8ZgxMBicQKsPet8NfGxLAnqfd4WNwKLgtnaJrYaEvLHvbvMaEq7dR2nv1EFS8V2GCqVATmor9CueNkoHo26NtyEPAXbaazKWjf92UxczoMQ/84'/1'/0'/0/*)#cruklgqk",
|     "internal": "wpkh(tprv8ZgxMBicQKsPet8NfGxLAnqfd4WNwKLgtnaJrYaEvLHvbvMaEq7dR2nv1EFS8V2GCqVATmor9CueNkoHo26NtyEPAXbaazKWjf92UxczoMQ/84'/1'/0'/1/*)#fhehzasw"
|   },
|   "mnemonic": "orphan quote seek ankle raccoon major ivory lunch axis piece ostrich stem",
|   "fingerprint": "3fa9af92"
| }

bdk-cli wallets

| {
|   "bitcoincore": {
|     "wallet": "bitcoincore",
|     "network": "signet",
|     "ext_descriptor": "wpkh(tprv8ZgxMBicQKsPfCYFA9DANn81pjg3GPfAzLEdtywSxJAL3pGz41CgVZ8ChvdULoetthwZmd7nYvghRBaY2wDvg1WoStSumpGcdqRW3ZdpY1b/84h/1h/0h/0/*)",
|     "int_descriptor": "wpkh(tprv8ZgxMBicQKsPfCYFA9DANn81pjg3GPfAzLEdtywSxJAL3pGz41CgVZ8ChvdULoetthwZmd7nYvghRBaY2wDvg1WoStSumpGcdqRW3ZdpY1b/84h/1h/0h/1/*)",
|     "database_type": "sqlite",
|     "client_type": "electrum",
|     "server_url": "ssl://mempool.space:60602",
|     "batch_size": 10,
|     "proxy": null,
|     "proxy_auth": null,
|     "proxy_retries": 5,
|     "proxy_timeout": null
|   }
| }

You'll note wallet has saved information like your server and your network so that you don't have to keep typing it in!

At this point, you'd usually want to check addresses. Here's the first two addresses with bdk-cli:

bdk-cli wallet --wallet bitcoincore new_address

| {
|   "address": "tb1q7aecrwahqkewhhupt8ru5pumtnla65lhc9yuj2",
|   "index": 0
| }

bdk-cli wallet --wallet bitcoincore new_address

| {
|   "address": "tb1qg6pqcgltff4edp79hhzxc928l28lj7d2vqtxl4",
|   "index": 1
| }

Happily, they match with bitcoin-cli, where this descriptor originated:

bitcoin-cli getnewaddress

| tb1q7aecrwahqkewhhupt8ru5pumtnla65lhc9yuj2

bitcoin-cli getnewaddress

| tb1qg6pqcgltff4edp79hhzxc928l28lj7d2vqtxl4

You don't actually need a network for bdk-cli to work. From here, you could generate addresses, create transactions, and work with PSBTs.

But if you want to know about transactions, balances, and UTXOs, then you need to sync bdk-cli with a server, either an electrum or Bitcoin server. This will take a long time, as it'll need to pull down the blockchain (as normal).

bdk-cli wallet --wallet bitcoincore sync

| Chain tip advanced from height 0 to 314500
| {
|   "message": "Wallet synced successfully."
| }

Afterward you can start working with commands like wallet balance and wallet transactions that work with the current state of the blockchain, but that goes beyond the scope of this quick intro. For now, we just want to see how BDK works with miniscript!

Compile Policy with BDK

For our current purposes, really exciting part of BDK is its Policy comiler, because it's the only command-line Miniscript Policy that we know about.

You access it with the compile command (which was one of --features that you enabled when you installed bdk-cli). Not only will it compile from Policy to Miniscript, but it'll also embed your Miniscript as a Minidescriptor.

Here's an example of our executive Policy in its abstract form:

bdk-cli compile "or(pk(president),thresh(2,pk(vp1),pk(vp2),pk(vp3)))"

| {
|   "descriptor": "wsh(t:or_c(pk(president),v:multi(2,vp1,vp2,vp3)))#jvnnrg8k"
| }

It's interesting to note that bdk-cli chose a different compilation that sipa.be, which instead picked or_d(pk(president),multi(2,vp1,vp2,vp3)), but as we said Policy is not fully specified: it's the encoding from Miniscript to Bitcoin Script that is meant to be full analyzable and consistent.

Beyond that, we see that bdk-cli indeed added on both the wsh() and the checksum. This is a descriptor that is fully ready to import into bitcoin-cli, bdk-cli, or the other wallet of your choice.

Or it would be if you'd use the keys, but that's easy to do too:

SEEDPP=$(seedtool)
KEYPP=$(keytool --seed $SEEDPP --account-derivation-path m/84h/1h/0h/0h --network testnet account-key-base58)
SEEDVP1P=$(seedtool)
KEYVP1P=$(keytool --seed $SEEDVP1P --account-derivation-path m/84h/1h/0h/0h --network testnet account-key-base58)
SEEDVP2P=$(seedtool)
KEYVP2P=$(keytool --seed $SEEDVP2P --account-derivation-path m/84h/1h/0h/0h --network testnet account-key-base58)
SEEDVP3P=$(seedtool)
KEYVP3P=$(keytool --seed $SEEDVP3P --account-derivation-path m/84h/1h/0h/0h --network testnet account-key-base58)
EXEC_PRIVATE=$(echo "or(pk($KEYPP/*),thresh(2,pk($KEYVP1P/*),pk($KEYVP2P/*),pk($KEYVP3P/*)))")
EXEC_PRIVATE_DESC=$(bdk-cli compile $EXEC_PRIVATE | jq -r '.descriptor')

| wsh(t:or_c(pk(tprv8hmuNrAoEFnCr2MaRNjX94GjAxDiKxG5FVhPjJE6jqbEBCY5o3FEvvC3hRqjEHbAR1CTSUhcHCHTmDsCqo27YpyAMVokseWzzjSBan2zYrN/*),v:multi(2,tprv8inKZ567XARkdHGUzvyH741df7ZsVTTs2cg6yCgMJPxxhN7GKgpb2LYbg4h9fGGnnu5BXZMW2EeD89g5kuAMZXK9GCizPySjv7jefHaYsNp/*,tprv8iPGcCodCT5SrQoq1uXNQUV1iBjpgEXH6eQgV5EavAsUTw5wVa8eaCMgNpmQECX6C6jTSjpJs3tQZyZXBEhJ9FxmK4t5jVwLdheaKPzn6yA/*,tprv8hZzKjjEA4e9tCW2XVsUud17Ftbn71NBjVCW275K3gvKVwxbNDrPvoEuDFiScjtAEr4TLGiEn2L3tJixuLxdEWbNppLPNnbuhECK22BpHGr/*)))#2lzcceyf

Import a BDK Miniscript

This descriptor can't currently be imported into bitcoin-cli (because it includes private keys), but it'll work fine in bdk-cli, which has put more focus into Miniscript:

bdk-cli \
    --network signet \
    wallet --wallet execs config \
    --ext-descriptor "$EXEC_PRIVATE_DESC" \
    --url "ssl://mempool.space:60602" \
    --client-type electrum \
    --database-type sqlite

You'll immediately be able to see it in your list of wallets:

bdk-cli wallets

| {
|   "bitcoincore": {
|     "wallet": "bitcoincore",
|     "network": "signet",
|     "ext_descriptor": "wpkh(tprv8ZgxMBicQKsPfCYFA9DANn81pjg3GPfAzLEdtywSxJAL3pGz41CgVZ8ChvdULoetthwZmd7nYvghRBaY2wDvg1WoStSumpGcdqRW3ZdpY1b/84h/1h/0h/0/*)",
|     "int_descriptor": "wpkh(tprv8ZgxMBicQKsPfCYFA9DANn81pjg3GPfAzLEdtywSxJAL3pGz41CgVZ8ChvdULoetthwZmd7nYvghRBaY2wDvg1WoStSumpGcdqRW3ZdpY1b/84h/1h/0h/1/*)",
|     "database_type": "sqlite",
|     "client_type": "electrum",
|     "server_url": "ssl://mempool.space:60602",
|     "batch_size": 10,
|     "proxy": null,
|     "proxy_auth": null,
|     "proxy_retries": 5,
|     "proxy_timeout": null
|   },
|   "execs": {
|     "wallet": "execs",
|     "network": "signet",
|     "ext_descriptor": "wsh(t:or_c(pk(tprv8hmuNrAoEFnCr2MaRNjX94GjAxDiKxG5FVhPjJE6jqbEBCY5o3FEvvC3hRqjEHbAR1CTSUhcHCHTmDsCqo27YpyAMVokseWzzjSBan2zYrN/*),v:multi(2,tprv8inKZ567XARkdHGUzvyH741df7ZsVTTs2cg6yCgMJPxxhN7GKgpb2LYbg4h9fGGnnu5BXZMW2EeD89g5kuAMZXK9GCizPySjv7jefHaYsNp/*,tprv8iPGcCodCT5SrQoq1uXNQUV1iBjpgEXH6eQgV5EavAsUTw5wVa8eaCMgNpmQECX6C6jTSjpJs3tQZyZXBEhJ9FxmK4t5jVwLdheaKPzn6yA/*,tprv8hZzKjjEA4e9tCW2XVsUud17Ftbn71NBjVCW275K3gvKVwxbNDrPvoEuDFiScjtAEr4TLGiEn2L3tJixuLxdEWbNppLPNnbuhECK22BpHGr/*)))#2lzcceyf",
|     "int_descriptor": null,
|     "database_type": "sqlite",
|     "client_type": "electrum",
|     "server_url": "ssl://mempool.space:60602",
|     "batch_size": 10,
|     "proxy": null,
|     "proxy_auth": null,
|     "proxy_retries": 5,
|     "proxy_timeout": null
|   }
| }

And you can generate addresses from it:

bdk-cli wallet --wallet execs new_address

| {
|   "address": "tb1qdsap579txagtk2mj66l4ccj73xn4hkfpf9k4ymlc26q30tx8p3wsr8ckzl",
|   "index": 0
| }

Summary: Using BDK with Miniscript

BDK is a powerful set of Rust libraries for Bitcoin that has its own CLI, bdk-cli. It can be used as an alternative to bitcoin-cli, but for our current work on Miniscript it has two exciting features: it will compile Policy to Miniscript from the command line, and it will import Miniscript containing private keys.

What's Next?

The next section will be all new, on Taproot!