Skip to main content

bitcoin_ext/
bdk.rs

1
2use std::borrow::BorrowMut;
3use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5
6use bdk_wallet::{AddressInfo, TxBuilder, Wallet, WeightedUtxo};
7use bdk_wallet::chain::{BlockId, CanonicalizationParams, ChainPosition, ConfirmationBlockTime};
8use bdk_wallet::coin_selection::{
9	decide_change, CoinSelectionAlgorithm, CoinSelectionResult, DefaultCoinSelectionAlgorithm,
10	InsufficientFunds,
11};
12use bdk_wallet::error::CreateTxError;
13use bitcoin::consensus::encode::{serialize, serialize_hex};
14use bitcoin::{
15	Amount, BlockHash, FeeRate, OutPoint, Script, Transaction, TxOut, Txid, Weight, Witness,
16};
17use bitcoin::psbt::{ExtractTxError, Input};
18use log::{debug, trace};
19use rand_core::RngCore;
20
21use crate::TransactionExt;
22use crate::cpfp::MakeCpfpFees;
23use crate::fee::FEE_ANCHOR_SPEND_WEIGHT;
24
25/// One canonical wallet tx, with its trust verdict already decided.
26#[derive(Debug, Clone)]
27pub struct LocalTransaction {
28	/// Refcounted handle into BDK's in-memory tx graph; cloning is cheap.
29	pub tx: Arc<Transaction>,
30	pub chain_position: ChainPosition<ConfirmationBlockTime>,
31	pub is_trusted: bool,
32}
33
34/// Borrowed view of one of our unspent outputs, returned by
35/// [`TrustedCanonicalization::list_unspent`]. Carries the trust verdict
36/// already decided for the creating tx so callers don't re-look-it-up.
37pub struct TrustedUtxo<'a> {
38	pub outpoint: OutPoint,
39	pub txout: &'a TxOut,
40	pub chain_position: &'a ChainPosition<ConfirmationBlockTime>,
41	pub is_trusted: bool,
42}
43
44/// Single-pass canonical view of the wallet's tx graph with trust
45/// verdicts pre-computed.
46///
47/// Built via one [`TxGraph::list_ordered_canonical_txs`] call which
48/// yields txs in topological (parents-before-children) order. We mark
49/// each tx trusted/untrusted in that order, so by the time we look at a
50/// tx every ancestor is already decided — no recursion, no per-tx
51/// `Wallet::get_tx`, no ancestor-walk budget heuristic.
52///
53/// In the same pass we also collect this wallet's UTXOs (ours-outpoints
54/// from the keychain index, minus anything consumed by another canonical
55/// tx). [`TrustedCanonicalization::list_unspent`] returns them without
56/// triggering a second canonicalization the way [`Wallet::list_unspent`]
57/// would.
58///
59/// [`TxGraph::list_ordered_canonical_txs`]: bdk_wallet::chain::TxGraph::list_ordered_canonical_txs
60pub struct TrustedCanonicalization {
61	txs: HashMap<Txid, LocalTransaction>,
62	unspent: Vec<OutPoint>,
63}
64
65impl TrustedCanonicalization {
66	/// Take one canonicalization snapshot of `w` and decide trust for
67	/// every canonical tx using `min_confs` as the confirmation
68	/// threshold.
69	pub fn from_wallet(w: &Wallet, min_confs: u32) -> Self {
70		let tip = w.latest_checkpoint().height();
71		let chain = w.local_chain();
72		let chain_tip = w.latest_checkpoint().block_id();
73
74		let mut txs: HashMap<Txid, LocalTransaction> = HashMap::new();
75		let mut spent: HashSet<OutPoint> = HashSet::new();
76
77		for ctx in w.tx_graph().list_ordered_canonical_txs(
78			chain, chain_tip, CanonicalizationParams::default(),
79		) {
80			let txid = ctx.tx_node.txid;
81			let tx = ctx.tx_node.tx.clone();
82			let chain_position = ctx.chain_position.clone();
83
84			for input in tx.input.iter() {
85				spent.insert(input.previous_output);
86			}
87
88			let nb_confs = match chain_position.confirmation_height_upper_bound() {
89				Some(h) => tip.saturating_sub(h) + 1,
90				None => 0,
91			};
92			let is_trusted = nb_confs >= min_confs || tx.input.iter().all(|input| {
93				let prev = input.previous_output;
94				let Some(prev_entry) = txs.get(&prev.txid) else { return false };
95				let Some(prev_out) = prev_entry.tx.output.get(prev.vout as usize) else { return false };
96				// Trust rule: this input must spend an output of ours,
97				// AND the prev tx itself must already be trusted.
98				// Topological order guarantees the prev entry is
99				// fully decided.
100				w.is_mine(prev_out.script_pubkey.clone()) && prev_entry.is_trusted
101			});
102
103			txs.insert(txid, LocalTransaction { tx, chain_position, is_trusted });
104		}
105
106		// Unspent = ours-outpoints (from the keychain index) ∩ canonical
107		// txs ∖ spent. Mirrors `Wallet::list_unspent`'s use of
108		// `spk_index().outpoints()` but reuses the canonical view we
109		// just built instead of running a second canonicalization.
110		let unspent = w.spk_index().outpoints().iter()
111			.map(|(_, op)| *op)
112			.filter(|op| !spent.contains(op))
113			.filter(|op| txs.contains_key(&op.txid))
114			.collect();
115
116		Self { txs, unspent }
117	}
118
119	/// Trust verdict for `txid`. Unknown txids (not in the wallet's
120	/// canonical view) are treated as untrusted.
121	pub fn is_trusted(&self, txid: Txid) -> bool {
122		self.txs.get(&txid).map(|e| e.is_trusted).unwrap_or(false)
123	}
124
125	/// Iterate this wallet's unspent outputs in canonical view, each
126	/// carrying its trust verdict.
127	pub fn list_unspent(&self) -> impl Iterator<Item = TrustedUtxo<'_>> + '_ {
128		self.unspent.iter().map(move |op| {
129			let lt = &self.txs[&op.txid];
130			TrustedUtxo {
131				outpoint: *op,
132				txout: &lt.tx.output[op.vout as usize],
133				chain_position: &lt.chain_position,
134				is_trusted: lt.is_trusted,
135			}
136		})
137	}
138}
139
140/// Balance categorized by our recursive trust model.
141#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
142pub struct TrustedBalance {
143	/// Funds in UTXOs we trust (confirmed or all-ours unconfirmed chains).
144	pub trusted: Amount,
145	/// Funds in UTXOs we don't trust.
146	pub untrusted: Amount,
147}
148
149impl TrustedBalance {
150	pub fn total(&self) -> Amount {
151		self.trusted + self.untrusted
152	}
153}
154
155/// The [bdk_wallet::KeychainKind] that is always used, because we only use a single keychain.
156pub const KEYCHAIN: bdk_wallet::KeychainKind = bdk_wallet::KeychainKind::External;
157
158
159/// Coin selection for transactions whose only output is the drain (change) output, like a CPFP
160/// child. Guarantees that this output ends up above the dust limit.
161///
162/// BDK's default algorithm stops selecting coins as soon as the target amount is covered. If the
163/// selected coins overshoot the target by less than the dust limit, the leftover is too small to
164/// be a valid output. Normally BDK would drop it and let it go to fees, but when the drain is the
165/// transaction's only output, dropping it leaves no outputs at all, so `TxBuilder::finish` fails
166/// with [InsufficientFunds] — even if the wallet has plenty of other coins available.
167///
168/// This wrapper asks the default algorithm for slightly more: the dust limit, plus the fee the
169/// drain output itself adds. That makes it keep pulling in coins until the leftover is a valid
170/// output. The result is then adjusted so that the extra ends up in the drain output rather than
171/// being burned as fee.
172#[derive(Debug, Clone, Copy, Default)]
173pub struct NonDustDrainCoinSelection;
174
175impl CoinSelectionAlgorithm for NonDustDrainCoinSelection {
176	fn coin_select<R: RngCore>(
177		&self,
178		required_utxos: Vec<WeightedUtxo>,
179		optional_utxos: Vec<WeightedUtxo>,
180		fee_rate: FeeRate,
181		target_amount: Amount,
182		drain_script: &Script,
183		rand: &mut R,
184	) -> Result<CoinSelectionResult, InsufficientFunds> {
185		// Fee cost of the drain output itself, computed exactly like bdk_wallet's `decide_change`
186		// does: the leftover only becomes change after paying for the extra output, so selection
187		// must cover that fee too. Zero when the caller uses an absolute fee, since BDK then
188		// passes FeeRate::ZERO here.
189		let drain_output_len = serialize(drain_script).len() + 8;
190		let drain_output_fee = fee_rate
191			* Weight::from_vb(drain_output_len as u64).expect("script length fits in Weight");
192		let raise = drain_script.minimal_non_dust() + drain_output_fee;
193
194		let mut result = DefaultCoinSelectionAlgorithm::default().coin_select(
195			required_utxos, optional_utxos, fee_rate, target_amount + raise, drain_script, rand,
196		)?;
197
198		// The inner algorithm measured its leftover against the raised target, so it considers
199		// the raise part of the fee. Recompute the leftover against the real target to hand the
200		// raise back to the drain output. It is at least `raise`, so `decide_change` always
201		// yields a non-dust `Excess::Change`.
202		let remaining = result.selected_amount()
203			.checked_sub(target_amount + result.fee_amount)
204			.expect("selection covers the raised target");
205		result.excess = decide_change(remaining, fee_rate, drain_script);
206		Ok(result)
207	}
208}
209
210/// An extension trait for [TxBuilder].
211pub trait TxBuilderExt<'a, A>: BorrowMut<TxBuilder<'a, A>> {
212	/// Add an input to the tx that spends a fee anchor.
213	fn add_fee_anchor_spend(&mut self, anchor: OutPoint, output: &TxOut)
214	where
215		A: bdk_wallet::coin_selection::CoinSelectionAlgorithm,
216	{
217		let psbt_in = Input {
218			witness_utxo: Some(output.clone()),
219			final_script_witness: Some(Witness::new()),
220			..Default::default()
221		};
222		self.borrow_mut().add_foreign_utxo(anchor, psbt_in, FEE_ANCHOR_SPEND_WEIGHT)
223			.expect("adding foreign utxo");
224	}
225}
226impl<'a, A> TxBuilderExt<'a, A> for TxBuilder<'a, A> {}
227
228#[derive(Debug, thiserror::Error)]
229pub enum CpfpInternalError {
230	#[error("{0}")]
231	General(String),
232	#[error("Unable to construct transaction: {0}")]
233	Create(CreateTxError),
234	#[error("Unable to extract the final transaction after signing the PSBT: {0}")]
235	Extract(ExtractTxError),
236	#[error("Failed to determine the weight/fee when creating a P2A CPFP")]
237	Fee(),
238	#[error("Unable to finalize CPFP transaction: {0}")]
239	FinalizeError(String),
240	#[error("You need more confirmations on your on-chain funds: {0}")]
241	InsufficientConfirmedFunds(InsufficientFunds),
242	#[error("Transaction has no fee anchor: {0}")]
243	NoFeeAnchor(Txid),
244	#[allow(deprecated)]
245	#[error("Unable to sign transaction: {0}")]
246	Signer(bdk_wallet::signer::SignerError),
247}
248
249/// An extension trait for [Wallet].
250pub trait WalletExt: BorrowMut<Wallet> {
251	/// Peek into the next address.
252	fn peek_next_address(&self) -> AddressInfo {
253		self.borrow().peek_address(KEYCHAIN, self.borrow().next_derivation_index(KEYCHAIN))
254	}
255
256	/// Returns an iterator for each unconfirmed transaction in the wallet.
257	fn unconfirmed_txids(&self) -> impl Iterator<Item = Txid> {
258		self.borrow().transactions().filter_map(|tx| {
259			if tx.chain_position.is_unconfirmed() {
260				Some(tx.tx_node.txid)
261			} else {
262				None
263			}
264		})
265	}
266
267	/// Returns an iterator for each unconfirmed transaction in the wallet, useful for syncing
268	/// with bitcoin core.
269	fn unconfirmed_txs(&self) -> impl Iterator<Item = Arc<Transaction>> {
270		self.borrow().transactions().filter_map(|tx| {
271			if tx.chain_position.is_unconfirmed() {
272				Some(tx.tx_node.tx.clone())
273			} else {
274				None
275			}
276		})
277	}
278
279	/// Compute the wallet balance using our recursive trust model.
280	fn trusted_balance(&self, min_confs: u32) -> TrustedBalance {
281		let canon = TrustedCanonicalization::from_wallet(self.borrow(), min_confs);
282		let mut trusted = Amount::ZERO;
283		let mut untrusted = Amount::ZERO;
284		for utxo in canon.list_unspent() {
285			if utxo.is_trusted {
286				trusted += utxo.txout.value;
287			} else {
288				untrusted += utxo.txout.value;
289			}
290		}
291		TrustedBalance { trusted, untrusted }
292	}
293
294	/// Return all UTXOs that are untrusted.
295	fn untrusted_utxos(&self, min_confs: u32) -> Vec<OutPoint> {
296		TrustedCanonicalization::from_wallet(self.borrow(), min_confs)
297			.list_unspent()
298			.filter(|u| !u.is_trusted)
299			.map(|u| u.outpoint)
300			.collect()
301	}
302
303	/// Check if a transaction is fully owned by the wallet (all inputs spend
304	/// wallet-owned outputs).
305	fn is_fully_owned_tx(&self, txid: Txid) -> bool {
306		let wallet = self.borrow();
307		let graph = wallet.tx_graph();
308		match graph.get_tx(txid) {
309			Some(tx) => {
310				tx.input.iter().all(|input| {
311					let prev = input.previous_output;
312					graph.get_tx(prev.txid)
313						.and_then(|prev_tx| prev_tx.output.get(prev.vout as usize).cloned())
314						.map(|out| wallet.is_mine(out.script_pubkey))
315						.unwrap_or(false)
316					})
317			}, None => false
318		}
319
320	}
321
322	/// Insert a checkpoint into the wallet.
323	///
324	/// It's advised to use this only when recovering a wallet with a birthday.
325	fn set_checkpoint(&mut self, height: u32, hash: BlockHash) {
326		let checkpoint = BlockId { height, hash };
327		let wallet = self.borrow_mut();
328		wallet.apply_update(bdk_wallet::Update {
329			chain: Some(wallet.latest_checkpoint().insert(checkpoint)),
330			..Default::default()
331		}).expect("should work, might fail if tip is genesis");
332	}
333
334	/// Mark the keys used in the outputs of this tx as unused
335	///
336	/// Used to replaced removed `cancel_tx` function as per suggestion:
337	/// <https://github.com/bitcoindevkit/bdk_wallet/pull/393>
338	fn mark_output_keys_unused(&mut self, tx: &Transaction) {
339		let wallet = self.borrow_mut();
340		for txout in &tx.output {
341			if let Some((keychain, index)) = wallet.spk_index().index_of_spk(txout.script_pubkey.clone()) {
342				// NOTE: unmark_used will **not** make something unused if it has actually been used
343				// by a tx in the tracker. It only removes the superficial marking.
344				wallet.unmark_used(*keychain, *index);
345			}
346		}
347	}
348
349	fn make_signed_p2a_cpfp(
350		&mut self,
351		tx: &Transaction,
352		fees: MakeCpfpFees,
353	) -> Result<Transaction, CpfpInternalError> {
354		let wallet = self.borrow_mut();
355		let (fee_anchor_point, fee_anchor_txout) = tx.fee_anchor()
356			.ok_or_else(|| CpfpInternalError::NoFeeAnchor(tx.compute_txid()))?;
357
358		// Since BDK doesn't support adding extra weight for fees, we have to loop to achieve the
359		// effective fee rate and potential minimum fee we need.
360		let parent_weight = tx.weight();
361		let extra_fee_needed = parent_weight * fees.effective();
362
363		// Since BDK doesn't allow tx without recipients, we add a drain output.
364		let change_addr = wallet.next_unused_address(KEYCHAIN);
365
366		// We will loop, constructing the transaction and signing it until we exceed the effective
367		// fee rate and meet any minimum fee requirements
368		let mut final_child_weight = Weight::ZERO;
369		let mut fee_needed = extra_fee_needed;
370		for i in 0..100 {
371			// The change is this transaction's only output, so use a coin selection that
372			// guarantees it stays above the dust limit.
373			let mut b = wallet.build_tx().coin_selection(NonDustDrainCoinSelection);
374			b.only_witness_utxo();
375			b.exclude_unconfirmed();
376			b.version(3); // for 1p1c package relay, all inputs must be confirmed
377			b.add_fee_anchor_spend(fee_anchor_point, fee_anchor_txout);
378			b.drain_to(change_addr.address.script_pubkey());
379			b.fee_absolute(fee_needed);
380
381			// Attempt to create and sign the transaction
382			let mut psbt = b.finish().map_err(|e| match e {
383				CreateTxError::CoinSelection(e) => CpfpInternalError::InsufficientConfirmedFunds(e),
384				_ => CpfpInternalError::Create(e),
385			})?;
386			#[allow(deprecated)]
387			let opts = bdk_wallet::SignOptions {
388				trust_witness_utxo: true,
389				..Default::default()
390			};
391			let finalized = wallet.sign(&mut psbt, opts)
392				.map_err(|e| CpfpInternalError::Signer(e))?;
393			if !finalized {
394				return Err(CpfpInternalError::FinalizeError("finalization failed".into()));
395			}
396			let tx = psbt.extract_tx()
397				.map_err(|e| CpfpInternalError::Extract(e))?;
398			assert!(tx.input.iter().any(|i| i.previous_output == fee_anchor_point),
399				"Missing anchor spend, tx is {}", serialize_hex(&tx),
400			);
401
402			// We can finally check the fees and weight
403			let tx_weight = tx.weight();
404			let total_weight = tx_weight + parent_weight;
405			if tx_weight != final_child_weight {
406				// Since the weight changed, we can drop the transaction and recalculate the
407				// required fee amount.
408				wallet.mark_output_keys_unused(&tx);
409				final_child_weight = tx_weight;
410				fee_needed = match fees {
411					MakeCpfpFees::Effective(fr) => total_weight * fr,
412					MakeCpfpFees::Rbf { min_effective_fee_rate, current_package_fee } => {
413						// RBF requires that you spend at least the total fee of every
414						// unconfirmed ancestor and the transaction you want to replace,
415						// then you must add mintxrelayfee * package_vbytes on top.
416						let min_tx_relay_fee = FeeRate::from_sat_per_vb(1).unwrap();
417						let min_package_fee = current_package_fee +
418							parent_weight * min_tx_relay_fee +
419							tx_weight * min_tx_relay_fee;
420
421						// This is the fee we want to pay based on the given minimum effective fee
422						// rate. It's possible that the desired fee is lower than the minimum
423						// package fee if the currently broadcast child transaction is bigger than
424						// the transaction we just produced.
425						let desired_fee = total_weight * min_effective_fee_rate;
426						if desired_fee < min_package_fee {
427							debug!("Using a minimum fee of {} instead of the desired fee of {} for RBF",
428								min_package_fee, desired_fee,
429							);
430							min_package_fee
431						} else {
432							trace!("Attempting to use the desired fee of {} for CPFP RBF",
433								desired_fee,
434							);
435							desired_fee
436						}
437					}
438				}
439			} else {
440				debug!("Created P2A CPFP with weight {} and fee {} in {} iterations",
441					total_weight, fee_needed, i,
442				);
443				return Ok(tx);
444			}
445		}
446		Err(CpfpInternalError::General("Reached max iterations".into()))
447	}
448}
449
450#[cfg(test)]
451mod test {
452	use super::*;
453
454	use bdk_wallet::KeychainKind;
455	use bdk_wallet::chain::BlockId;
456	use bdk_wallet::test_utils::{get_test_wpkh, insert_checkpoint, receive_output_in_latest_block};
457	use bitcoin::Network;
458	use bitcoin::hashes::Hash;
459
460	/// A wallet with two confirmed UTXOs of 1000 and 1001 sats.
461	fn two_utxo_wallet() -> (Wallet, OutPoint) {
462		let mut wallet = Wallet::create_single(get_test_wpkh())
463			.network(Network::Regtest)
464			.create_wallet_no_persist()
465			.unwrap();
466		insert_checkpoint(&mut wallet, BlockId { height: 1_000, hash: BlockHash::all_zeros() });
467		let op1 = receive_output_in_latest_block(&mut wallet, Amount::from_sat(1_000));
468		receive_output_in_latest_block(&mut wallet, Amount::from_sat(1_001));
469		(wallet, op1)
470	}
471
472	/// Build the drain-only tx shape of a CPFP child: one mandatory input, an absolute fee it
473	/// covers on its own, and the drain as sole output. The 1000-sat input minus the 900-sat fee
474	/// leaves 100 sats: below the change script's dust limit, so default coin selection fails
475	/// (`InsufficientFunds`) instead of pulling in the second UTXO.
476	#[test]
477	fn non_dust_drain_selection_rescues_sub_dust_change() {
478		let (mut wallet, op1) = two_utxo_wallet();
479		let change_spk = wallet.reveal_next_address(KeychainKind::External)
480			.address.script_pubkey();
481		let fee = Amount::from_sat(900);
482		assert!(Amount::from_sat(100) < change_spk.minimal_non_dust(), "premise");
483
484		let mut b = wallet.build_tx().coin_selection(NonDustDrainCoinSelection);
485		b.add_utxo(op1).unwrap();
486		b.only_witness_utxo();
487		b.drain_to(change_spk.clone());
488		b.fee_absolute(fee);
489		let psbt = b.finish().expect("both UTXOs cover fee + dust");
490
491		let tx = &psbt.unsigned_tx;
492		assert_eq!(tx.input.len(), 2, "must pull in the second UTXO");
493		assert_eq!(tx.output.len(), 1);
494		let change = tx.output[0].value;
495		assert!(change >= change_spk.minimal_non_dust(), "change {} is dust", change);
496		// The raised selection target must flow into the change, not the fee.
497		assert_eq!(change, Amount::from_sat(2_001) - fee);
498		assert_eq!(psbt.fee().unwrap(), fee);
499	}
500
501	/// When even the whole wallet can't leave a non-dust drain, selection must fail with
502	/// [InsufficientFunds] instead of producing a dust (non-standard) output.
503	#[test]
504	fn non_dust_drain_selection_fails_when_change_can_only_be_dust() {
505		let (mut wallet, op1) = two_utxo_wallet();
506		let change_spk = wallet.reveal_next_address(KeychainKind::External)
507			.address.script_pubkey();
508		// Both UTXOs together hold 2001 sats; this fee leaves 101 sats, below the dust limit.
509		let fee = Amount::from_sat(1_900);
510		let dust = change_spk.minimal_non_dust();
511		assert!(Amount::from_sat(101) < dust, "premise");
512
513		let mut b = wallet.build_tx().coin_selection(NonDustDrainCoinSelection);
514		b.add_utxo(op1).unwrap();
515		b.only_witness_utxo();
516		b.drain_to(change_spk);
517		b.fee_absolute(fee);
518
519		match b.finish() {
520			Err(CreateTxError::CoinSelection(e)) => {
521				assert_eq!(e.needed, fee + dust, "needed must cover fee plus a non-dust drain");
522				assert_eq!(e.available, Amount::from_sat(2_001), "available must be the whole wallet");
523			},
524			other => panic!("expected InsufficientFunds, got {:?}", other),
525		}
526	}
527
528	/// When a single UTXO leaves non-dust change, no extra input should be pulled in.
529	#[test]
530	fn non_dust_drain_selection_no_extra_input_when_change_is_fine() {
531		let (mut wallet, op1) = two_utxo_wallet();
532		let change_spk = wallet.reveal_next_address(KeychainKind::External)
533			.address.script_pubkey();
534		let fee = Amount::from_sat(500);
535
536		let mut b = wallet.build_tx().coin_selection(NonDustDrainCoinSelection);
537		b.add_utxo(op1).unwrap();
538		b.only_witness_utxo();
539		b.drain_to(change_spk);
540		b.fee_absolute(fee);
541		let psbt = b.finish().unwrap();
542
543		let tx = &psbt.unsigned_tx;
544		assert_eq!(tx.input.len(), 1, "1000-sat input alone leaves non-dust change");
545		assert_eq!(tx.output[0].value, Amount::from_sat(500));
546		assert_eq!(psbt.fee().unwrap(), fee);
547	}
548}
549
550impl WalletExt for Wallet {}