~/Adding taproot descriptor compilation to bdk-cli$
How I added taproot support to bdk-cli's compile command.
What compile does
bdk-cli's compile takes a miniscript policy and turns it into a Bitcoin descriptor:
% bdk-cli compile "and(pk(A),pk(B))" -t sh
{
"descriptor": "sh(and_v(v:pk(A),pk(B)))#0s43jst8"
}
It supported sh, wsh, and sh-wsh — but not taproot. Issue #204 asked for it.
Why taproot is different
A taproot descriptor is tr(INTERNAL_KEY, TREE). The internal key enables key-path spending, the tree holds script-path conditions.
When compiling from a policy, all keys live in the tree. There's no "real" internal key — so we need an unspendable one to disable the key path entirely.
NUMS key from BIP-341
BIP-341 defines a Nothing Up My Sleeve (NUMS) point — a point on secp256k1 for which nobody knows the discrete logarithm:
const NUMS_UNSPENDABLE_KEY_HEX: &str =
"50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";Implementation
Compile the policy into taproot miniscript, wrap it in a taptree leaf, use the NUMS key as internal key:
"tr" => {
let taproot_policy: Miniscript<String, Tap> = policy
.compile()
.map_err(|e| Error::Generic(e.to_string()))?;
let xonly_public_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX)
.map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?;
let tree = TapTree::Leaf(Arc::new(taproot_policy));
Descriptor::new_tr(xonly_public_key.to_string(), Some(tree))
}
Result for or(pk(A),pk(B)):
tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{pk(A),pk(B)})Why not compile_tr?
You might notice that rust-miniscript already has policy.compile_tr() which does all of this — takes a policy and an optional unspendable key, and returns a taproot descriptor directly. Why not use it? That's a good question, and I'll cover it in a separate post.
What's next
A fixed NUMS key works but leaks privacy — all compiled descriptors share the same internal key, which is a fingerprint. The follow-up is randomizing it per compilation.
Links
- BIP-341: Taproot
- Issue #204 — original request
- PR #208 — implementation
- PR #225 — follow-up: randomized NUMS keys