Taproot Spending Paths
A taproot wallet has one keypath (the primary spending condition) and zero or more script-path leaves (one per non-primary condition). Each script-path leaf has a 32-byte leaf hash. To spend through a specific path, you set selected_leaf_hash on FundWalletTransaction.
This guide walks the full lifecycle of a primary + recovery taproot wallet.
Why taproot for multi-condition wallets
Section titled “Why taproot for multi-condition wallets”Wallets with multiple time-locked conditions (primary path + 1y recovery + 2y recovery, etc.) are best expressed as taproot wallets. The reasons:
- Cheap keypath spend. When you spend via the primary, the witness reveals only the keypath signature — no scripts, minimum on-chain cost.
- Per-leaf privacy. Only the script you actually use is revealed at spend time. The other recovery branches stay private.
- Per-leaf timelocks. Each leaf carries its own CSV timelock, enforced by Bitcoin Core script semantics.
walletrs uses the Liana compiler (vendored at contrib/liana/) to turn primary + recovery descriptors into a taproot tree. The internal key is the primary’s policy; each recovery becomes a separate leaf.
Composing a taproot wallet
Section titled “Composing a taproot wallet”Build a CreateGenericWalletRequest with one primary condition and one or more time-locked recovery conditions:
{ "user_id": "alice", "wallet_id": "alice-vault", "network": "regtest", "preferred_script_type": "SCRIPT_TYPE_TAPROOT", "spending_conditions": [ { "id": "primary", "is_primary": true, "timelock": 0, "threshold": 1, "policy": "SINGLE", "managed_key_ids": ["alice-hot"] }, { "id": "recovery-1y", "is_primary": false, "timelock": 52560, "threshold": 2, "policy": "MULTI", "managed_key_ids": ["family-1", "family-2", "family-3"] } ]}That gives Alice a wallet she can spend immediately with alice-hot, or — after one year of CSV — that any 2 of her 3 family-member keys can recover.
Reading the leaf info
Section titled “Reading the leaf info”The CreateGenericWallet response carries taproot_leaf_info:
{ "wallet_id": "alice-vault", "external_descriptor": "tr(...)", "merkle_root": "...", "internal_key": "...", "taproot_leaf_info": [ { "leaf_hash": "keypath", "spending_condition_id": "primary", "policy_type": "single", "threshold": 1, "description": "primary keypath spend" }, { "leaf_hash": "abcdef0123...", "spending_condition_id": "recovery-1y", "timelock": 52560, "policy_type": "multi", "threshold": 2, "description": "1-year recovery, 2-of-3" } ]}You can also fetch the leaf info later with GetWalletSpendingPaths:
curl -sS -X POST http://127.0.0.1:8080/wallet/get_spending_paths \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{"wallet_id":"alice-vault"}'Spending via a specific path
Section titled “Spending via a specific path”FundWalletTransaction takes a selected_leaf_hash field:
""(empty) — defaults to the keypath. Same as"keypath"."keypath"— explicit keypath spend (cheapest, no script witness)."<hex leaf hash>"— script-path spend through the named leaf.
Keypath spend (everyday)
Section titled “Keypath spend (everyday)”curl -sS -X POST http://127.0.0.1:8080/wallet/fund_transaction \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{ "wallet_id": "alice-vault", "destination_address": "bcrt1q...", "destination_value": 50000, "fee_per_kb": 1000, "selected_leaf_hash": "keypath" }'The signer maps "keypath" to BDK policy-path index vec![0] and produces the cheapest possible witness.
Recovery path spend
Section titled “Recovery path spend”After 52,560 blocks, the family keys can spend through the recovery leaf:
curl -sS -X POST http://127.0.0.1:8080/wallet/fund_transaction \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{ "wallet_id": "alice-vault", "destination_address": "bcrt1q...", "destination_value": 50000, "fee_per_kb": 1000, "selected_leaf_hash": "abcdef0123..." }'The signer maps the leaf hash to BDK policy-path index vec![idx + 1] (where idx is the leaf’s position in taproot_leaf_info) and produces a script-path witness.
Signer behaviour
Section titled “Signer behaviour”For a taproot script-path spend, walletrs needs partial signatures from a threshold of the leaf’s signers. With customer-managed keys, that means each external signer produces a PSBT with their partial signature, and you push them in via add_verify_transaction_signature until the threshold is met. With system-managed keys (less common for recovery branches but supported), sign_wallet_transaction does it in-process per device.
The leaf-hash → policy-path mapping is implemented in crates/server/src/wallet/signer/. If the leaf hash you pass doesn’t match any leaf in the descriptor, you get:
INVALID_ARGUMENT: Leaf hash 'X' does not match any spending path in the descriptorCommon errors
Section titled “Common errors”| Error | Cause |
|---|---|
INTERNAL: Failed to extract taproot leaf hash for recovery condition X | The descriptor’s signer fingerprints didn’t surface in tap_key_origins — usually a malformed input key. Re-check xpub + fingerprint + derivation_path on the CreateCustomerManagedKey call. |
INVALID_ARGUMENT: Leaf hash 'X' does not match any spending path | The leaf hash you sent isn’t part of this wallet’s taproot tree. Check the TaprootLeafInfo[] from CreateGenericWallet or GetWalletSpendingPaths. |
Spend rejected by mempool with non-BIP68-final | The recovery leaf’s CSV timelock hasn’t elapsed yet. For regtest, mine more blocks. |