Skip to content

Commit c5564f4

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

File tree

5 files changed

+362
-10
lines changed

5 files changed

+362
-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: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
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 txout = TxOut {
125+
script_pubkey: emergency_wallet
126+
.next_unused_address(KeychainKind::External)
127+
.script_pubkey(),
128+
value: Amount::from_sat(750),
129+
};
130+
131+
// Add the TxIn and TxOut to the transaction we're working on
132+
psbt.unsigned_tx.input.push(txin);
133+
psbt.unsigned_tx.output.push(txout);
134+
135+
// 5. If there are multiple spend paths, use the `plan` module to format the psbt properly
136+
137+
// Our vault happens to have two spend paths, and the miniscript satisfier will freak out
138+
// if we don't tell it which path we're formatting this transaction for. It's like a
139+
// compile-time check vs a runtime check.
140+
//
141+
// In order to tell it whether we are trying for the unvault + timelock spend path,
142+
// or the emergency spend path, we can use the `plan` module from `rust-miniscript`.
143+
//
144+
// The plan module says: "given x assets, can I satisfy the
145+
// miniscript descriptor y?". It can also automatically update the psbt
146+
// with the information. When the psbt is finalized, miniscript will check
147+
// whether the formatted transaction can satisfy the descriptor or not.
148+
149+
// Let's try using the plan module on the emergency spend path.
150+
151+
// First we define our emergency key as a possible asset we can use in the plan
152+
// to attempt to satisfy the descriptor.
153+
println!("Adding a spending plan to the emergency spend psbt");
154+
let emergency_key_asset = DescriptorPublicKey::from_str(emergency_pk).unwrap();
155+
156+
// Then we add the emergency key to our list of plan assets. If we had more than one
157+
// asset (e.g. multiple keys, timelocks, etc) in the descriptor branch we are trying
158+
// to spend on, we would define and add multiple assets.
159+
let assets = Assets::new().add(emergency_key_asset);
160+
161+
// Automatically generate a plan for spending the descriptor
162+
let emergency_plan = definite_descriptor
163+
.clone()
164+
.plan(&assets)
165+
.expect("couldn't create emergency plan");
166+
167+
// Create an input where we can put the plan data
168+
// Add the witness_utxo from the deposit transaction to the input
169+
let mut input = psbt::Input {
170+
witness_utxo: Some(witness_utxo.clone()),
171+
..Default::default()
172+
};
173+
174+
// Update the input with the generated plan
175+
println!("Update the emergency spend psbt with spend plan");
176+
emergency_plan.update_psbt_input(&mut input);
177+
178+
// Push the input to the PSBT
179+
psbt.inputs.push(input);
180+
181+
// Add a default output to the PSBT
182+
psbt.outputs.push(psbt::Output::default());
183+
184+
// 6. Sign the spending psbt
185+
186+
// At this point, we have a PSBT that is ready to be signed.
187+
// It contains public data in its inputs, and data which needs to be signed
188+
// in its `unsigned_tx.{input, output}s`
189+
190+
// Sign the psbt
191+
println!("Signing emergency spend psbt");
192+
let secp = Secp256k1::new();
193+
let emergency_key = Xpriv::from_str(emergency_tprv).expect("couldn't create emergency key");
194+
psbt.sign(&emergency_key, &secp)
195+
.expect("failed to sign emergency spend psbt");
196+
197+
// 7. Finalize the psbt. At this point, miniscript will check whether the transaction
198+
// satisfies the descriptor, and will notify you if it doesn't.
199+
200+
psbt.finalize_mut(&secp)
201+
.expect("problem finalizing emergency psbt");
202+
println!("Finalized emergency spend psbt");
203+
204+
// 8. If desired, extract the transaction from the psbt and broadcast it. We won't do this
205+
// here as it saves messing around with faucets, wallets, etc.
206+
let _my_emergency_spend_tx = psbt.extract_tx().expect("failed to extract emergency tx");
207+
208+
println!("===================================================");
209+
210+
// Let's now try the same thing with the unvault transaction. We just need to make a new
211+
// plan, sign a new spending psbt, and finalize it.
212+
213+
// Build a spend transaction the unvault key path
214+
println!("Setting up a psbt for the unvault spend path");
215+
let timelock = absolute::LockTime::from_height(after).expect("couldn't format locktime");
216+
let unvault_spend_transaction = blank_transaction_with(timelock);
217+
let mut psbt = Psbt::from_unsigned_tx(unvault_spend_transaction)
218+
.expect("couldn't create psbt from unvault_spend_transaction");
219+
220+
// Format an input containing the previous output
221+
let txin = TxIn {
222+
previous_output,
223+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, // disables relative timelock
224+
..Default::default()
225+
};
226+
227+
// Format an output which spends some of the funds in the vault.
228+
let txout = TxOut {
229+
script_pubkey: unvault_wallet
230+
.next_unused_address(KeychainKind::External)
231+
.script_pubkey(),
232+
value: Amount::from_sat(750),
233+
};
234+
235+
// Add the TxIn and TxOut to the transaction we're working on
236+
psbt.unsigned_tx.input.push(txin);
237+
psbt.unsigned_tx.output.push(txout);
238+
239+
// Let's try using the Plan module, this time with two assets: the unvault_key
240+
// and our `after` timelock.
241+
println!("Adding a spending plan to the unvault spend psbt");
242+
let unvault_key_asset = DescriptorPublicKey::from_str(unvault_pk).unwrap();
243+
let timelock = absolute::LockTime::from_height(after).expect("couldn't format locktime");
244+
let unvault_assets = Assets::new().add(unvault_key_asset).after(timelock);
245+
246+
// Automatically generate a plan for spending the descriptor, using the assets in our plan
247+
let unvault_plan = definite_descriptor
248+
.clone()
249+
.plan(&unvault_assets)
250+
.expect("couldn't create plan");
251+
252+
// Create an input where we can put the plan data
253+
// Add the witness_utxo from the deposit transaction to the input
254+
let mut input = psbt::Input {
255+
witness_utxo: Some(witness_utxo.clone()),
256+
..Default::default()
257+
};
258+
259+
// Update the input with the generated plan
260+
println!("Update the unvault spend psbt with spend plan");
261+
unvault_plan.update_psbt_input(&mut input);
262+
263+
// Push the input to the PSBT
264+
psbt.inputs.push(input);
265+
266+
// Add a default output to the PSBT
267+
psbt.outputs.push(psbt::Output::default());
268+
269+
// Sign it
270+
println!("Signing unvault spend psbt");
271+
let secp = Secp256k1::new();
272+
let unvault_key = Xpriv::from_str(unvault_tprv).unwrap();
273+
psbt.sign(&unvault_key, &secp)
274+
.expect("failed to sign unvault psbt");
275+
276+
// Finalize the psbt. Miniscript satisfier checks are run at this point,
277+
// and if your transaction doesn't satisfy the descriptor, this will error.
278+
psbt.finalize_mut(&secp)
279+
.expect("problem finalizing unvault psbt");
280+
println!("Finalized unvault spend psbt");
281+
282+
// Once again, we could broadcast the transaction if we wanted to
283+
// spend using the unvault path. Spend attempts will fail until
284+
// after the absolute block height defined in the timelock.
285+
let _my_unvault_tx = psbt.extract_tx().expect("failed to extract unvault tx");
286+
287+
println!("Congratulations, you've just used a miniscript descriptor with a BDK wallet!");
288+
println!("Read the code comments for a more detailed look at what happened.")
289+
}
290+
291+
fn blank_transaction() -> bitcoin::Transaction {
292+
blank_transaction_with(absolute::LockTime::ZERO)
293+
}
294+
295+
fn blank_transaction_with(lock_time: absolute::LockTime) -> bitcoin::Transaction {
296+
bitcoin::Transaction {
297+
version: transaction::Version::TWO,
298+
lock_time,
299+
input: vec![],
300+
output: vec![],
301+
}
302+
}
303+
304+
fn deposit_transaction(wallet: &mut Wallet, tx: Transaction) -> OutPoint {
305+
use bdk_chain::{ConfirmationBlockTime, TxGraph};
306+
use bdk_wallet::Update;
307+
308+
let txid = tx.compute_txid();
309+
let vout = 0;
310+
let mut graph = TxGraph::<ConfirmationBlockTime>::new([tx]);
311+
let _ = graph.insert_seen_at(txid, 42);
312+
wallet
313+
.apply_update(Update {
314+
graph,
315+
..Default::default()
316+
})
317+
.unwrap();
318+
319+
OutPoint { txid, vout }
320+
}

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)