BDK Wallet Descriptors
If you like Bitcoin and coding in Rust then you will likely want to try out BDK.
BDK introduces you to “Wallet Descriptors“
I’ve used them in previous articles. Here I want to create them, rather than copy and paste from the BDK documentation.
This article is effectively me trying out the example code and trying to explain it as a mere mortal, and “seeing what happens”.
https://bitcoindevkit.org/getting-started/
Let’s run the example code :
[dependencies]
bdk = { version = "0.28.1", default-feature = false, features = ["all-keys"] }
use bdk::bitcoin::Network;
use bdk::database::MemoryDatabase;
use bdk::keys::{DerivableKey, GeneratableKey, GeneratedKey, ExtendedKey, bip39::{Mnemonic, WordCount, Language}};
use bdk::template::Bip84;
use bdk::{miniscript, Wallet, KeychainKind};
fn main() {
let network = Network::Testnet; // Or this can be Network::Bitcoin, Network::Signet or Network::Regtest
// Generate fresh mnemonic
let mnemonic: GeneratedKey<_, miniscript::Segwitv0> = Mnemonic::generate((WordCount::Words12, Language::English)).unwrap();
// Convert mnemonic to string
let mnemonic_words = mnemonic.to_string();
// Parse a mnemonic
let mnemonic = Mnemonic::parse(&mnemonic_words).unwrap();
// Generate the extended key
let xkey: ExtendedKey = mnemonic.into_extended_key().unwrap();
// Get xprv from the extended key
let xprv = xkey.into_xprv(network).unwrap();
// Create a BDK wallet structure using BIP 84 descriptor ("m/84h/1h/0h/0" and "m/84h/1h/0h/1")
let wallet = Wallet::new(
Bip84(xprv, KeychainKind::External),
Some(Bip84(xprv, KeychainKind::Internal)),
network,
MemoryDatabase::default(),
)
.unwrap();
println!("mnemonic: {}\n\nrecv desc (pub key): {:#?}\n\nchng desc (pub key): {:#?}",
mnemonic_words,
wallet.get_descriptor_for_keychain(KeychainKind::External).to_string(),
wallet.get_descriptor_for_keychain(KeychainKind::Internal).to_string());
}
mnemonic: badge prefer various cousin raccoon embrace shoot else nerve poverty gas pact
recv desc (pub key): "wpkh([ad23c517/84'/1'/0']tpubDCPzURjLu93Jg5iy92GbUL8jHPSNHWTwRkJUrBpGfcyfsYDWxcaJ4jeJDm3s34tT1MLHtLDCMh5NYpvCzNv3JmdaDoVenRVyCQyEATA52qv/0/*)#9e3q3l2v"
chng desc (pub key): "wpkh([ad23c517/84'/1'/0']tpubDCPzURjLu93Jg5iy92GbUL8jHPSNHWTwRkJUrBpGfcyfsYDWxcaJ4jeJDm3s34tT1MLHtLDCMh5NYpvCzNv3JmdaDoVenRVyCQyEATA52qv/1/*)#5d5pv265"
“A simplified explanation of how seed phrases work is that the wallet software has a list of words taken from a dictionary, with each word assigned to a number. The seed phrase can be converted to a number which is used as the seed integer to a deterministic wallet that generates all the key pairs used in the wallet.”
https://en.bitcoin.it/wiki/Seed_phrase
These are the lines of code that convert the 12 words into the xprv
The xprv is private. Keep it private! A private key can create a public key.
An xpub is public and safe to disclose. A public key cannot be reversed to find the private key.
// Generate the extended key
let xkey: ExtendedKey = mnemonic.into_extended_key().unwrap();
// Get xprv from the extended key
let xprv = xkey.into_xprv(network).unwrap();
If you want to view the word list, it is published here:
https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt
If you want to view xprv for educational purposes only :
// Get xprv from the extended key
let xprv = xkey.into_xprv(network).unwrap();
println!("xprv {:?}\n", xprv);
BDK CLI
You can also generate your seed phrase and xprv from bdk-cli :
Rather than just watch their tutorial, let’s not trust, but verify!
https://bitcoindevkit.org/tutorials/bdk_cli_basics/#step-1-seed-generate
❯ bdk-cli key generate | tee key.json
{
"fingerprint": "8162daf7",
"mnemonic": "slam combine index monster online wish guilt very tissue manage surprise prison salt cloth choose syrup voice surge major rapid ride clip this ankle",
"xprv": "tprv8ZgxMBicQKsPdgQTZd1aqc377DfUR6mABk6PRnKPwEYWhY2p9bAEzJGZdVW1359mdQW43192cocpHxY2CyWvkZtuvNKJL1tz5ay3ekPjNjF"
}
!! Note, all of this is purely for educational purposes !! Keep your private keys private…else : rekt!
Anyway, back to the Rust code…
❯ cargo run
warning: unused manifest key: dependencies.bdk.default-feature
Compiling bdk_mnemonics v0.1.0 (/home/rag/Documents/rust/bdk_mnemonics)
Finished dev [unoptimized + debuginfo] target(s) in 2.48s
Running `/home/rag/Documents/rust/bdk_mnemonics/target/debug/bdk_mnemonics`
xprv ExtendedPrivKey { network: Testnet, depth: 0, parent_fingerprint: 00000000, child_number: Normal { index: 0 }, private_key: SecretKey(#9055c68e4a16ae3d), chain_code: 28eedcf7d9d68f7288a34c0746038bc27a6f4790c6eb5069c9207060af9ba208 }
Note the “chain code“?
In Bitcoin, the term “chain code” is associated with hierarchical deterministic (HD) wallets. An extended private key (xprv) is a component of an HD wallet that includes both a private key and a chain code.
HD wallets use a tree-like structure, known as a hierarchical tree, to derive multiple child key pairs from a single master key pair. The chain code is a 256-bit value that, when combined with the master private key, is used to generate child keys deterministically.
The chain code serves two main purposes:
- Deterministic Key Derivation: It ensures that child keys can be deterministically generated from the master key. This means that if you have the master key and chain code, you can derive all the child keys in a predictable and secure manner.
- Security: It enhances the security of the key derivation process. Even if an attacker gains access to a child key and its corresponding public key, they cannot derive any other keys in the hierarchy without the chain code, which is kept private.
In summary, the chain code is a crucial component in the hierarchical deterministic key derivation process, providing a secure and deterministic way to generate a hierarchy of private and public keys in Bitcoin HD wallets. See BIP-32
Create a wallet and receive test sats with BDK-CLI
Here is the full procedure as per https://bitcoindevkit.org/tutorials/bdk_cli_basics/#step-1-seed-generate
❯ bdk-cli key generate | tee key.json
{
"fingerprint": "06b194a3",
"mnemonic": "attend radio install sign giraffe easy latin recall useful hybrid brisk relief type stand explain garage city merit sand wear napkin found corn blue",
"xprv": "tprv8ZgxMBicQKsPeVuHNFPCAfgepJ4TZvYyk7iWkQiLRUVcSt1frfZsYw74PrZUC8Ds2NWervZXjAKnHM4jVrdgub2J6CZkLojnDEhVKSJVNjo"
}
❯ cd ~
❯ bdk-cli key generate | tee key.json
{
"fingerprint": "2f0ccc74",
"mnemonic": "base expose leader erupt mountain ginger erupt liar plug sword crouch board amount amused method asthma note gather grain urban able adjust palm escape",
"xprv": "tprv8ZgxMBicQKsPfEAfYG7ASWbARA57P1DrSLevF7ZiHehZq6wZitoRsHRg8pHeqXq13SLWiRXTcF557ocmYEYjXjxCf2mPKXaofhZh1Q2nLZa"
}
❯ export XPRV_00=$(cat key.json | jq -r .xprv)
❯ env | rg XPRV
XPRV_00=tprv8ZgxMBicQKsPfEAfYG7ASWbARA57P1DrSLevF7ZiHehZq6wZitoRsHRg8pHeqXq13SLWiRXTcF557ocmYEYjXjxCf2mPKXaofhZh1Q2nLZa
❯ export my_descriptor="wpkh($XPRV_00/84h/1h/0h/0/*)"
❯ env | rg my_descriptor
my_descriptor=wpkh(tprv8ZgxMBicQKsPfEAfYG7ASWbARA57P1DrSLevF7ZiHehZq6wZitoRsHRg8pHeqXq13SLWiRXTcF557ocmYEYjXjxCf2mPKXaofhZh1Q2nLZa/84h/1h/0h/0/*)
❯ bdk-cli wallet --wallet wallet_name --descriptor $my_descriptor get_new_address | jq
{
"address": "tb1q52fxg6aeu9u7hhtqav9exuerflsuz83cw4fyln"
}
❯ bdk-cli wallet --wallet wallet_name --descriptor $my_descriptor sync
{}
❯ bdk-cli wallet --wallet wallet_name --descriptor $my_descriptor get_balance
{
"satoshi": {
"confirmed": 0,
"immature": 0,
"trusted_pending": 0,
"untrusted_pending": 1000
}
}
10+ mins later….
❯ bdk-cli wallet --wallet wallet_name --descriptor $my_descriptor sync
{}
❯ bdk-cli wallet --wallet wallet_name --descriptor $my_descriptor get_balance
{
"satoshi": {
"confirmed": 1000,
"immature": 0,
"trusted_pending": 0,
"untrusted_pending": 0
}
}
Create Transaction (PSBT)
Now we have funds in our wallet, let’s make a transaction :
bdk-cli wallet --wallet wallet_name --descriptor $my_descriptor create_tx --to tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6:50000
❯ bdk-cli wallet --wallet wallet_name --descriptor $my_descriptor create_tx --to tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6:50000
[2023-11-21T21:00:41Z ERROR bdk_cli] Insufficient funds: 1000 sat available of 50110 sat needed
❯ bdk-cli wallet --wallet wallet_name --descriptor $my_descriptor create_tx --to tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6:800
{
"details": {
"confirmation_time": null,
"fee": 200,
"received": 0,
"sent": 1000,
"transaction": null,
"txid": "828f3852b1430526dd857c6526b7ed1da78314d11ed946d1874a54c7d731d33b"
},
"psbt": "cHNidP8BAFIBAAAAAR8VfeU9cgsJDTtDO9FIPw1ibpLCMEMqnoz2UCCKFYX5AAAAAAD+////ASADAAAAAAAAFgAUcrEfm4kwMqMWFRtFfVFhTVvkwhKwwCYAAAEA3gIAAAAAAQGuOGurXdo6P4oOiUJ3MgN4CPTVs/9tbizuNshsBBsygQAAAAAA/f///wLoAwAAAAAAABYAFKKSZGu54Xnr3WDrC5NzI0/hwR44pgoLAAAAAAAWABQERU48YXWriBCkW8GLTtI7pIM0OAJHMEQCIFrtBZv58RrFyuxnVrR9sokr2+BbX8NqGWODuEkbEi4aAiB0pQa1Wae0jn6bkryzekfDLhh49lbbdYDe+zg8DDXPjQEhA3+zpDH9Dgd2XZemXhEczJpmj4bG+fzjKZlKYY13v4Lzp8AmAAEBH+gDAAAAAAAAFgAUopJka7nheevdYOsLk3MjT+HBHjgiBgMbZUeyNEUuzDzYjbjA3R4AGcFniiEAmEYlQOTMixmM+xgvDMx0VAAAgAEAAIAAAACAAAAAAAAAAAAAAA=="
}
The amount was set to 50000, hence insufficient funds so i changed it to an amount greater than “dust” (546) and less than 1000 (to allow for fees) and then it created a PSBT…..Nice!
Export psbt as env variable and then sign it
❯ export PSBT="cHNidP8BAFIBAAAAAR8VfeU9cgsJDTtDO9FIPw1ibpLCMEMqnoz2UCCKFYX5AAAAAAD+////ASADAAAAAAAAFgAUcrEfm4kwMqMWFRtFfVFhTVvkwhKwwCYAAAEA3gIAAAAAAQGuOGurXdo6P4oOiUJ3MgN4CPTVs/9tbizuNshsBBsygQAAAAAA/f///wLoAwAAAAAAABYAFKKSZGu54Xnr3WDrC5NzI0/hwR44pgoLAAAAAAAWABQERU48YXWriBCkW8GLTtI7pIM0OAJHMEQCIFrtBZv58RrFyuxnVrR9sokr2+BbX8NqGWODuEkbEi4aAiB0pQa1Wae0jn6bkryzekfDLhh49lbbdYDe+zg8DDXPjQEhA3+zpDH9Dgd2XZemXhEczJpmj4bG+fzjKZlKYY13v4Lzp8AmAAEBH+gDAAAAAAAAFgAUopJka7nheevdYOsLk3MjT+HBHjgiBgMbZUeyNEUuzDzYjbjA3R4AGcFniiEAmEYlQOTMixmM+xgvDMx0VAAAgAEAAIAAAACAAAAAAAAAAAAAAA=="
❯ bdk-cli wallet --wallet wallet_name --descriptor $my_descriptor sign --psbt $PSBT
{
"is_finalized": true,
"psbt": "cHNidP8BAFIBAAAAAR8VfeU9cgsJDTtDO9FIPw1ibpLCMEMqnoz2UCCKFYX5AAAAAAD+////ASADAAAAAAAAFgAUcrEfm4kwMqMWFRtFfVFhTVvkwhKwwCYAAAEA3gIAAAAAAQGuOGurXdo6P4oOiUJ3MgN4CPTVs/9tbizuNshsBBsygQAAAAAA/f///wLoAwAAAAAAABYAFKKSZGu54Xnr3WDrC5NzI0/hwR44pgoLAAAAAAAWABQERU48YXWriBCkW8GLTtI7pIM0OAJHMEQCIFrtBZv58RrFyuxnVrR9sokr2+BbX8NqGWODuEkbEi4aAiB0pQa1Wae0jn6bkryzekfDLhh49lbbdYDe+zg8DDXPjQEhA3+zpDH9Dgd2XZemXhEczJpmj4bG+fzjKZlKYY13v4Lzp8AmAAEBH+gDAAAAAAAAFgAUopJka7nheevdYOsLk3MjT+HBHjgiBgMbZUeyNEUuzDzYjbjA3R4AGcFniiEAmEYlQOTMixmM+xgvDMx0VAAAgAEAAIAAAACAAAAAAAAAAAABBwABCGsCRzBEAiBMtvwbRpJNjrry60cvvgQmfbaS07MAh14xaCZBxIOeQAIgFh4LvF+HFnxSdJqykRUOco7EjRujOxEIRunWWPHlOFwBIQMbZUeyNEUuzDzYjbjA3R4AGcFniiEAmEYlQOTMixmM+wAA"
}
Export signed psbt and broadcast it
❯ export SIGNED_PSBT="cHNidP8BAFIBAAAAAR8VfeU9cgsJDTtDO9FIPw1ibpLCMEMqnoz2UCCKFYX5AAAAAAD+////ASADAAAAAAAAFgAUcrEfm4kwMqMWFRtFfVFhTVvkwhKwwCYAAAEA3gIAAAAAAQGuOGurXdo6P4oOiUJ3MgN4CPTVs/9tbizuNshsBBsygQAAAAAA/f///wLoAwAAAAAAABYAFKKSZGu54Xnr3WDrC5NzI0/hwR44pgoLAAAAAAAWABQERU48YXWriBCkW8GLTtI7pIM0OAJHMEQCIFrtBZv58RrFyuxnVrR9sokr2+BbX8NqGWODuEkbEi4aAiB0pQa1Wae0jn6bkryzekfDLhh49lbbdYDe+zg8DDXPjQEhA3+zpDH9Dgd2XZemXhEczJpmj4bG+fzjKZlKYY13v4Lzp8AmAAEBH+gDAAAAAAAAFgAUopJka7nheevdYOsLk3MjT+HBHjgiBgMbZUeyNEUuzDzYjbjA3R4AGcFniiEAmEYlQOTMixmM+xgvDMx0VAAAgAEAAIAAAACAAAAAAAAAAAABBwABCGsCRzBEAiBMtvwbRpJNjrry60cvvgQmfbaS07MAh14xaCZBxIOeQAIgFh4LvF+HFnxSdJqykRUOco7EjRujOxEIRunWWPHlOFwBIQMbZUeyNEUuzDzYjbjA3R4AGcFniiEAmEYlQOTMixmM+wAA"
❯ bdk-cli wallet --wallet wallet_name --descriptor $my_descriptor broadcast --psbt $SIGNED_PSBT
{
"txid": "828f3852b1430526dd857c6526b7ed1da78314d11ed946d1874a54c7d731d33b"
}
txid = success!
check in mempool.space