Skip to content

Commit 1f68cb6

Browse files
committed
docs(wallet): add example usage of descriptor and plan
1 parent 9695296 commit 1f68cb6

File tree

5 files changed

+383
-10
lines changed

5 files changed

+383
-10
lines changed

.github/workflows/cont_integration.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,32 @@ jobs:
130130
with:
131131
token: ${{ secrets.GITHUB_TOKEN }}
132132
args: --all-features --all-targets -- -D warnings
133+
134+
build-examples:
135+
name: Build Examples
136+
runs-on: ubuntu-latest
137+
strategy:
138+
matrix:
139+
example-dir:
140+
- example_cli
141+
- example_bitcoind_rpc_polling
142+
- example_electrum
143+
- example_esplora
144+
- wallet_electrum
145+
- wallet_esplora_async
146+
- wallet_esplora_blocking
147+
- wallet_rpc
148+
steps:
149+
- name: checkout
150+
uses: actions/checkout@v2
151+
- name: Install Rust toolchain
152+
uses: actions-rs/toolchain@v1
153+
with:
154+
toolchain: stable
155+
override: true
156+
profile: minimal
157+
- name: Rust Cache
158+
uses: Swatinem/rust-cache@v2.2.1
159+
- name: Build
160+
working-directory: example-crates/${{ matrix.example-dir }}
161+
run: cargo build

crates/wallet/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ rand = "^0.8"
4747
all-features = true
4848
rustdoc-args = ["--cfg", "docsrs"]
4949

50+
[[example]]
51+
name = "descriptor_with_plan"
52+
path = "examples/descriptor_with_plan.rs"
53+
required-features = []
54+
5055
[[example]]
5156
name = "mnemonic_to_descriptors"
5257
path = "examples/mnemonic_to_descriptors.rs"
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
#![allow(unused)]
2+
use std::str::FromStr;
3+
4+
use bdk_wallet::bitcoin::bip32::Xpriv;
5+
use bdk_wallet::bitcoin::hashes::Hash;
6+
use bdk_wallet::bitcoin::key::Secp256k1;
7+
8+
use bdk_wallet::bitcoin::{
9+
self, psbt, Address, Network, OutPoint, Psbt, Script, Sequence, Transaction, TxIn, TxOut, Txid,
10+
};
11+
12+
use bdk_wallet::keys::DescriptorPublicKey;
13+
use bdk_wallet::miniscript::plan::Assets;
14+
use bdk_wallet::miniscript::policy::Concrete;
15+
use bdk_wallet::miniscript::psbt::PsbtExt;
16+
use bdk_wallet::miniscript::{DefiniteDescriptorKey, Descriptor};
17+
use bdk_wallet::{KeychainKind, Wallet};
18+
use bitcoin::{absolute, transaction, Amount};
19+
20+
// Using a descriptor and spending plans with BDK wallets.
21+
//
22+
// Consider the basic flow of using a descriptor. The steps are:
23+
// 1. Set up the Descriptor
24+
// 2. Deposit sats into the descriptor
25+
// 3. Get the previous output and witness from the deposit transaction
26+
// 4. Set up a psbt to spend the deposited funds
27+
// 5. If there are multiple spend paths, use the `plan` module to format the psbt properly
28+
// 6. Sign the spending psbt
29+
// 7. Finalize the psbt. At this point, miniscript will check whether the transaction
30+
// satisfies the descriptor, and will notify you if it doesn't.
31+
// 8. If desired, extract the transaction from the psbt and broadcast it.
32+
fn main() {
33+
// In order to try out descriptors, let's define a Bitcoin vault with two spend paths.
34+
//
35+
// The vault works like this:
36+
//
37+
// A. If you have the `unvault_key`, you can spend the funds, but only *after* a specified block height
38+
// B. If you have the `emergency_key`, presumably kept in deep cold storage, you can spend at any time.
39+
40+
// Let's set up some wallets so we have keys to work with.
41+
42+
// Regular unvault spend path keys + blockheight. You can use wallet descriptors like this,
43+
// or you could potentially use a mnemonic and derive from that. See the `mnemonic_to_descriptors.rs`
44+
// example if you want to do that.
45+
let unvault_tprv = "tprv8ZgxMBicQKsPdKyH699thnjrFcmJMrUUoaNZvHYxxqvhySPhAYZpmxtR39u5QAYnhtYSfMBuBBH6pGuSgmoK3NpfNDU3RAbrVpcbpLmz5ot";
46+
let unvault_pk = "02e7c62fd3a65abdc7ff233fba5637f89c9eaba7fe6baaf15ca99d81e0f5145bf8";
47+
let after = 1311208;
48+
49+
// Emergency path keys
50+
let emergency_tprv = "tprv8ZgxMBicQKsPekKEvzvCnK7qe5r6ausugHDyrPeX9TLQ4oADSYLWtA4m3XsEMmUZEbVaeJtuZimakomLkecLTMwerVJKpAZFtXoo7DYb84B";
51+
let emergency_pk = "033b4ac89f5d83de29af72d8b99963c4dbd416fa7c8a8aee6b4761f8f85e588f80";
52+
53+
// Make a wallet for the unvault user
54+
let unvault_desc = format!("wpkh({unvault_tprv}/84'/1'/0'/0/*)");
55+
let unvault_change_desc = format!("wpkh({unvault_tprv}/84'/1'/0'/1/*)");
56+
let mut unvault_wallet = Wallet::create(unvault_desc, unvault_change_desc)
57+
.network(Network::Testnet)
58+
.create_wallet_no_persist()
59+
.expect("couldn't create unvault_wallet");
60+
61+
// Make a wallet for the emergency user
62+
let emergency_desc = format!("wpkh({emergency_tprv}/84'/1'/0'/0/*)");
63+
let emergency_change_desc = format!("wpkh({emergency_tprv}/84'/1'/0'/1/*)");
64+
let mut emergency_wallet = Wallet::create(emergency_desc, emergency_change_desc)
65+
.network(Network::Testnet)
66+
.create_wallet_no_persist()
67+
.expect("couldn't create emergency_wallet");
68+
69+
// 1. Set up the Descriptor
70+
71+
// The following string defines a miniscript vault policy with two possible spend paths (`or`):
72+
// * spend at any time with the `emergency_pk`
73+
// * spend `after` the timelock with the `unvault_pk`
74+
let policy_str = format!("or(pk({emergency_pk}),and(pk({unvault_pk}),after({after})))");
75+
76+
// Refer to `examples/compiler.rs` for compiling a miniscript descriptor from a policy string.
77+
let desc_str = "wsh(or_d(pk(033b4ac89f5d83de29af72d8b99963c4dbd416fa7c8a8aee6b4761f8f85e588f80),and_v(v:pk(02e7c62fd3a65abdc7ff233fba5637f89c9eaba7fe6baaf15ca99d81e0f5145bf8),after(1311208))))#9xvht4sc";
78+
println!("The vault descriptor is: {}\n", desc_str);
79+
80+
// Alternately, we can make a wallet for our vault and get its address:
81+
let mut vault = Wallet::create_single(desc_str)
82+
.network(Network::Testnet)
83+
.create_wallet_no_persist()
84+
.unwrap();
85+
let vault_address = vault.peek_address(KeychainKind::External, 0).address;
86+
println!("The vault address is {:?}", vault_address);
87+
let vault_descriptor = vault.public_descriptor(KeychainKind::External).clone();
88+
let definite_descriptor = vault_descriptor.at_derivation_index(0).unwrap();
89+
90+
// We don't need to broadcast the funding transaction in this tutorial -
91+
// having it locally is good enough to get the information we need, and it saves
92+
// messing around with faucets etc.
93+
94+
// Fund the vault by inserting a transaction:
95+
let witness_utxo = TxOut {
96+
value: Amount::from_sat(76_000),
97+
script_pubkey: vault_address.script_pubkey(),
98+
};
99+
let tx = Transaction {
100+
output: vec![witness_utxo.clone()],
101+
..blank_transaction()
102+
};
103+
104+
let previous_output = deposit_transaction(&mut vault, tx);
105+
println!("Vault balance: {}", vault.balance().total());
106+
107+
// 3. Get the previous output and txout from the deposit transaction. In a real application
108+
// you would get this from the blockchain if you didn't make the deposit_tx.
109+
println!("The deposit transaction's outpoint was {}", previous_output);
110+
111+
// 4. Set up a psbt to spend the deposited funds
112+
println!("Setting up a psbt for the emergency spend path");
113+
let emergency_spend = blank_transaction();
114+
let mut psbt =
115+
Psbt::from_unsigned_tx(emergency_spend).expect("couldn't create psbt from emergency_spend");
116+
117+
// Format an input containing the previous output
118+
let txin = TxIn {
119+
previous_output,
120+
..Default::default()
121+
};
122+
123+
// Format an output which spends some of the funds in the vault
124+
let txout1 = TxOut {
125+
script_pubkey: emergency_wallet
126+
.next_unused_address(KeychainKind::External)
127+
.script_pubkey(),
128+
value: Amount::from_sat(750),
129+
};
130+
131+
// Leave some sats aside for fees
132+
let fees = Amount::from_sat(500);
133+
134+
// Calculate the change amount (total balance minus the amount sent in txout1)
135+
let change_amount = emergency_wallet
136+
.balance()
137+
.confirmed
138+
.checked_sub(txout1.value)
139+
.expect("failed to generate change amount")
140+
.checked_sub(fees)
141+
.expect("couldn't subtract fee amount");
142+
143+
// Change output
144+
let txout2 = TxOut {
145+
script_pubkey: emergency_wallet
146+
.next_unused_address(KeychainKind::Internal)
147+
.script_pubkey(),
148+
value: change_amount,
149+
};
150+
151+
// Add the TxIn and TxOut to the transaction we're working on
152+
psbt.unsigned_tx.input.push(txin);
153+
psbt.unsigned_tx.output.push(txout1);
154+
psbt.unsigned_tx.output.push(txout2);
155+
156+
// 5. If there are multiple spend paths, use the `plan` module to format the psbt properly
157+
158+
// Our vault happens to have two spend paths, and the miniscript satisfier will freak out
159+
// if we don't tell it which path we're formatting this transaction for. It's like a
160+
// compile-time check vs a runtime check.
161+
//
162+
// In order to tell it whether we are trying for the unvault + timelock spend path,
163+
// or the emergency spend path, we can use the `plan` module from `rust-miniscript`.
164+
//
165+
// The plan module says: "given x assets, can I satisfy the
166+
// miniscript descriptor y?". It can also automatically update the psbt
167+
// with the information. When the psbt is finalized, miniscript will check
168+
// whether the formatted transaction can satisfy the descriptor or not.
169+
170+
// Let's try using the plan module on the emergency spend path.
171+
172+
// First we define our emergency key as a possible asset we can use in the plan
173+
// to attempt to satisfy the descriptor.
174+
println!("Adding a spending plan to the emergency spend psbt");
175+
let emergency_key_asset = DescriptorPublicKey::from_str(emergency_pk).unwrap();
176+
177+
// Then we add the emergency key to our list of plan assets. If we had more than one
178+
// asset (e.g. multiple keys, timelocks, etc) in the descriptor branch we are trying
179+
// to spend on, we would define and add multiple assets.
180+
let assets = Assets::new().add(emergency_key_asset);
181+
182+
// Automatically generate a plan for spending the descriptor
183+
let emergency_plan = definite_descriptor
184+
.clone()
185+
.plan(&assets)
186+
.expect("couldn't create emergency plan");
187+
188+
// Create an input where we can put the plan data
189+
// Add the witness_utxo from the deposit transaction to the input
190+
let mut input = psbt::Input {
191+
witness_utxo: Some(witness_utxo.clone()),
192+
..Default::default()
193+
};
194+
195+
// Update the input with the generated plan
196+
println!("Update the emergency spend psbt with spend plan");
197+
emergency_plan.update_psbt_input(&mut input);
198+
199+
// Push the input to the PSBT
200+
psbt.inputs.push(input);
201+
202+
// Add a default output to the PSBT
203+
psbt.outputs.push(psbt::Output::default());
204+
205+
// 6. Sign the spending psbt
206+
207+
// At this point, we have a PSBT that is ready to be signed.
208+
// It contains public data in its inputs, and data which needs to be signed
209+
// in its `unsigned_tx.{input, output}s`
210+
211+
// Sign the psbt
212+
println!("Signing emergency spend psbt");
213+
let secp = Secp256k1::new();
214+
let emergency_key = Xpriv::from_str(emergency_tprv).expect("couldn't create emergency key");
215+
psbt.sign(&emergency_key, &secp)
216+
.expect("failed to sign emergency spend psbt");
217+
218+
// 7. Finalize the psbt. At this point, miniscript will check whether the transaction
219+
// satisfies the descriptor, and will notify you if it doesn't.
220+
221+
psbt.finalize_mut(&secp)
222+
.expect("problem finalizing emergency psbt");
223+
println!("Finalized emergency spend psbt");
224+
225+
// 8. If desired, extract the transaction from the psbt and broadcast it. We won't do this
226+
// here as it saves messing around with faucets, wallets, etc.
227+
let _my_emergency_spend_tx = psbt.extract_tx().expect("failed to extract emergency tx");
228+
229+
println!("===================================================");
230+
231+
// Let's now try the same thing with the unvault transaction. We just need to make a new
232+
// plan, sign a new spending psbt, and finalize it.
233+
234+
// Build a spend transaction the unvault key path
235+
println!("Setting up a psbt for the unvault spend path");
236+
let timelock = absolute::LockTime::from_height(after).expect("couldn't format locktime");
237+
let unvault_spend_transaction = blank_transaction_with(timelock);
238+
let mut psbt = Psbt::from_unsigned_tx(unvault_spend_transaction)
239+
.expect("couldn't create psbt from unvault_spend_transaction");
240+
241+
// Format an input containing the previous output
242+
let txin = TxIn {
243+
previous_output,
244+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, // disables relative timelock
245+
..Default::default()
246+
};
247+
248+
// Format an output which spends some of the funds in the vault.
249+
let txout = TxOut {
250+
script_pubkey: unvault_wallet
251+
.next_unused_address(KeychainKind::External)
252+
.script_pubkey(),
253+
value: Amount::from_sat(750),
254+
};
255+
256+
// Add the TxIn and TxOut to the transaction we're working on
257+
psbt.unsigned_tx.input.push(txin);
258+
psbt.unsigned_tx.output.push(txout);
259+
260+
// Let's try using the Plan module, this time with two assets: the unvault_key
261+
// and our `after` timelock.
262+
println!("Adding a spending plan to the unvault spend psbt");
263+
let unvault_key_asset = DescriptorPublicKey::from_str(unvault_pk).unwrap();
264+
let timelock = absolute::LockTime::from_height(after).expect("couldn't format locktime");
265+
let unvault_assets = Assets::new().add(unvault_key_asset).after(timelock);
266+
267+
// Automatically generate a plan for spending the descriptor, using the assets in our plan
268+
let unvault_plan = definite_descriptor
269+
.clone()
270+
.plan(&unvault_assets)
271+
.expect("couldn't create plan");
272+
273+
// Create an input where we can put the plan data
274+
// Add the witness_utxo from the deposit transaction to the input
275+
let mut input = psbt::Input {
276+
witness_utxo: Some(witness_utxo.clone()),
277+
..Default::default()
278+
};
279+
280+
// Update the input with the generated plan
281+
println!("Update the unvault spend psbt with spend plan");
282+
unvault_plan.update_psbt_input(&mut input);
283+
284+
// Push the input to the PSBT
285+
psbt.inputs.push(input);
286+
287+
// Add a default output to the PSBT
288+
psbt.outputs.push(psbt::Output::default());
289+
290+
// Sign it
291+
println!("Signing unvault spend psbt");
292+
let secp = Secp256k1::new();
293+
let unvault_key = Xpriv::from_str(unvault_tprv).unwrap();
294+
psbt.sign(&unvault_key, &secp)
295+
.expect("failed to sign unvault psbt");
296+
297+
// Finalize the psbt. Miniscript satisfier checks are run at this point,
298+
// and if your transaction doesn't satisfy the descriptor, this will error.
299+
psbt.finalize_mut(&secp)
300+
.expect("problem finalizing unvault psbt");
301+
println!("Finalized unvault spend psbt");
302+
303+
// Once again, we could broadcast the transaction if we wanted to
304+
// spend using the unvault path. Spend attempts will fail until
305+
// after the absolute block height defined in the timelock.
306+
let _my_unvault_tx = psbt.extract_tx().expect("failed to extract unvault tx");
307+
308+
println!("Congratulations, you've just used a miniscript descriptor with a BDK wallet!");
309+
println!("Read the code comments for a more detailed look at what happened.")
310+
}
311+
312+
fn blank_transaction() -> bitcoin::Transaction {
313+
blank_transaction_with(absolute::LockTime::ZERO)
314+
}
315+
316+
fn blank_transaction_with(lock_time: absolute::LockTime) -> bitcoin::Transaction {
317+
bitcoin::Transaction {
318+
version: transaction::Version::TWO,
319+
lock_time,
320+
input: vec![],
321+
output: vec![],
322+
}
323+
}
324+
325+
fn deposit_transaction(wallet: &mut Wallet, tx: Transaction) -> OutPoint {
326+
use bdk_chain::{ConfirmationBlockTime, TxGraph};
327+
use bdk_wallet::Update;
328+
329+
let txid = tx.compute_txid();
330+
let vout = 0;
331+
let mut graph = TxGraph::<ConfirmationBlockTime>::new([tx]);
332+
let _ = graph.insert_seen_at(txid, 42);
333+
wallet
334+
.apply_update(Update {
335+
graph,
336+
..Default::default()
337+
})
338+
.unwrap();
339+
340+
OutPoint { txid, vout }
341+
}

example-crates/example_cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ edition = "2021"
99
bdk_chain = { path = "../../crates/chain", features = ["serde", "miniscript"]}
1010
bdk_coin_select = "0.3.0"
1111
bdk_file_store = { path = "../../crates/file_store" }
12+
bitcoin = { version = "0.32.0", features = ["base64"], default-features = false }
1213

1314
anyhow = "1"
1415
clap = { version = "3.2.23", features = ["derive", "env"] }
16+
rand = "0.8"
1517
serde = { version = "1", features = ["derive"] }
1618
serde_json = "1.0"

0 commit comments

Comments
 (0)