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};
7use bdk_wallet::chain::{BlockId, CanonicalizationParams, ChainPosition, ConfirmationBlockTime};
8use bdk_wallet::coin_selection::InsufficientFunds;
9use bdk_wallet::error::CreateTxError;
10use bitcoin::consensus::encode::serialize_hex;
11use bitcoin::{Amount, BlockHash, FeeRate, OutPoint, Transaction, TxOut, Txid, Weight, Witness};
12use bitcoin::psbt::{ExtractTxError, Input};
13use log::{debug, trace};
14
15use crate::TransactionExt;
16use crate::cpfp::MakeCpfpFees;
17use crate::fee::FEE_ANCHOR_SPEND_WEIGHT;
18
19/// One canonical wallet tx, with its trust verdict already decided.
20#[derive(Debug, Clone)]
21pub struct LocalTransaction {
22	/// Refcounted handle into BDK's in-memory tx graph; cloning is cheap.
23	pub tx: Arc<Transaction>,
24	pub chain_position: ChainPosition<ConfirmationBlockTime>,
25	pub is_trusted: bool,
26}
27
28/// Borrowed view of one of our unspent outputs, returned by
29/// [`TrustedCanonicalization::list_unspent`]. Carries the trust verdict
30/// already decided for the creating tx so callers don't re-look-it-up.
31pub struct TrustedUtxo<'a> {
32	pub outpoint: OutPoint,
33	pub txout: &'a TxOut,
34	pub chain_position: &'a ChainPosition<ConfirmationBlockTime>,
35	pub is_trusted: bool,
36}
37
38/// Single-pass canonical view of the wallet's tx graph with trust
39/// verdicts pre-computed.
40///
41/// Built via one [`TxGraph::list_ordered_canonical_txs`] call which
42/// yields txs in topological (parents-before-children) order. We mark
43/// each tx trusted/untrusted in that order, so by the time we look at a
44/// tx every ancestor is already decided — no recursion, no per-tx
45/// `Wallet::get_tx`, no ancestor-walk budget heuristic.
46///
47/// In the same pass we also collect this wallet's UTXOs (ours-outpoints
48/// from the keychain index, minus anything consumed by another canonical
49/// tx). [`TrustedCanonicalization::list_unspent`] returns them without
50/// triggering a second canonicalization the way [`Wallet::list_unspent`]
51/// would.
52///
53/// [`TxGraph::list_ordered_canonical_txs`]: bdk_wallet::chain::TxGraph::list_ordered_canonical_txs
54pub struct TrustedCanonicalization {
55	txs: HashMap<Txid, LocalTransaction>,
56	unspent: Vec<OutPoint>,
57}
58
59impl TrustedCanonicalization {
60	/// Take one canonicalization snapshot of `w` and decide trust for
61	/// every canonical tx using `min_confs` as the confirmation
62	/// threshold.
63	pub fn from_wallet(w: &Wallet, min_confs: u32) -> Self {
64		let tip = w.latest_checkpoint().height();
65		let chain = w.local_chain();
66		let chain_tip = w.latest_checkpoint().block_id();
67
68		let mut txs: HashMap<Txid, LocalTransaction> = HashMap::new();
69		let mut spent: HashSet<OutPoint> = HashSet::new();
70
71		for ctx in w.tx_graph().list_ordered_canonical_txs(
72			chain, chain_tip, CanonicalizationParams::default(),
73		) {
74			let txid = ctx.tx_node.txid;
75			let tx = ctx.tx_node.tx.clone();
76			let chain_position = ctx.chain_position.clone();
77
78			for input in tx.input.iter() {
79				spent.insert(input.previous_output);
80			}
81
82			let nb_confs = match chain_position.confirmation_height_upper_bound() {
83				Some(h) => tip.saturating_sub(h) + 1,
84				None => 0,
85			};
86			let is_trusted = nb_confs >= min_confs || tx.input.iter().all(|input| {
87				let prev = input.previous_output;
88				let Some(prev_entry) = txs.get(&prev.txid) else { return false };
89				let Some(prev_out) = prev_entry.tx.output.get(prev.vout as usize) else { return false };
90				// Trust rule: this input must spend an output of ours,
91				// AND the prev tx itself must already be trusted.
92				// Topological order guarantees the prev entry is
93				// fully decided.
94				w.is_mine(prev_out.script_pubkey.clone()) && prev_entry.is_trusted
95			});
96
97			txs.insert(txid, LocalTransaction { tx, chain_position, is_trusted });
98		}
99
100		// Unspent = ours-outpoints (from the keychain index) ∩ canonical
101		// txs ∖ spent. Mirrors `Wallet::list_unspent`'s use of
102		// `spk_index().outpoints()` but reuses the canonical view we
103		// just built instead of running a second canonicalization.
104		let unspent = w.spk_index().outpoints().iter()
105			.map(|(_, op)| *op)
106			.filter(|op| !spent.contains(op))
107			.filter(|op| txs.contains_key(&op.txid))
108			.collect();
109
110		Self { txs, unspent }
111	}
112
113	/// Trust verdict for `txid`. Unknown txids (not in the wallet's
114	/// canonical view) are treated as untrusted.
115	pub fn is_trusted(&self, txid: Txid) -> bool {
116		self.txs.get(&txid).map(|e| e.is_trusted).unwrap_or(false)
117	}
118
119	/// Iterate this wallet's unspent outputs in canonical view, each
120	/// carrying its trust verdict.
121	pub fn list_unspent(&self) -> impl Iterator<Item = TrustedUtxo<'_>> + '_ {
122		self.unspent.iter().map(move |op| {
123			let lt = &self.txs[&op.txid];
124			TrustedUtxo {
125				outpoint: *op,
126				txout: &lt.tx.output[op.vout as usize],
127				chain_position: &lt.chain_position,
128				is_trusted: lt.is_trusted,
129			}
130		})
131	}
132}
133
134/// Balance categorized by our recursive trust model.
135#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
136pub struct TrustedBalance {
137	/// Funds in UTXOs we trust (confirmed or all-ours unconfirmed chains).
138	pub trusted: Amount,
139	/// Funds in UTXOs we don't trust.
140	pub untrusted: Amount,
141}
142
143impl TrustedBalance {
144	pub fn total(&self) -> Amount {
145		self.trusted + self.untrusted
146	}
147}
148
149/// The [bdk_wallet::KeychainKind] that is always used, because we only use a single keychain.
150pub const KEYCHAIN: bdk_wallet::KeychainKind = bdk_wallet::KeychainKind::External;
151
152
153/// An extension trait for [TxBuilder].
154pub trait TxBuilderExt<'a, A>: BorrowMut<TxBuilder<'a, A>> {
155	/// Add an input to the tx that spends a fee anchor.
156	fn add_fee_anchor_spend(&mut self, anchor: OutPoint, output: &TxOut)
157	where
158		A: bdk_wallet::coin_selection::CoinSelectionAlgorithm,
159	{
160		let psbt_in = Input {
161			witness_utxo: Some(output.clone()),
162			final_script_witness: Some(Witness::new()),
163			..Default::default()
164		};
165		self.borrow_mut().add_foreign_utxo(anchor, psbt_in, FEE_ANCHOR_SPEND_WEIGHT)
166			.expect("adding foreign utxo");
167	}
168}
169impl<'a, A> TxBuilderExt<'a, A> for TxBuilder<'a, A> {}
170
171#[derive(Debug, thiserror::Error)]
172pub enum CpfpInternalError {
173	#[error("{0}")]
174	General(String),
175	#[error("Unable to construct transaction: {0}")]
176	Create(CreateTxError),
177	#[error("Unable to extract the final transaction after signing the PSBT: {0}")]
178	Extract(ExtractTxError),
179	#[error("Failed to determine the weight/fee when creating a P2A CPFP")]
180	Fee(),
181	#[error("Unable to finalize CPFP transaction: {0}")]
182	FinalizeError(String),
183	#[error("You need more confirmations on your on-chain funds: {0}")]
184	InsufficientConfirmedFunds(InsufficientFunds),
185	#[error("Transaction has no fee anchor: {0}")]
186	NoFeeAnchor(Txid),
187	#[allow(deprecated)]
188	#[error("Unable to sign transaction: {0}")]
189	Signer(bdk_wallet::signer::SignerError),
190}
191
192/// An extension trait for [Wallet].
193pub trait WalletExt: BorrowMut<Wallet> {
194	/// Peek into the next address.
195	fn peek_next_address(&self) -> AddressInfo {
196		self.borrow().peek_address(KEYCHAIN, self.borrow().next_derivation_index(KEYCHAIN))
197	}
198
199	/// Returns an iterator for each unconfirmed transaction in the wallet.
200	fn unconfirmed_txids(&self) -> impl Iterator<Item = Txid> {
201		self.borrow().transactions().filter_map(|tx| {
202			if tx.chain_position.is_unconfirmed() {
203				Some(tx.tx_node.txid)
204			} else {
205				None
206			}
207		})
208	}
209
210	/// Returns an iterator for each unconfirmed transaction in the wallet, useful for syncing
211	/// with bitcoin core.
212	fn unconfirmed_txs(&self) -> impl Iterator<Item = Arc<Transaction>> {
213		self.borrow().transactions().filter_map(|tx| {
214			if tx.chain_position.is_unconfirmed() {
215				Some(tx.tx_node.tx.clone())
216			} else {
217				None
218			}
219		})
220	}
221
222	/// Compute the wallet balance using our recursive trust model.
223	fn trusted_balance(&self, min_confs: u32) -> TrustedBalance {
224		let canon = TrustedCanonicalization::from_wallet(self.borrow(), min_confs);
225		let mut trusted = Amount::ZERO;
226		let mut untrusted = Amount::ZERO;
227		for utxo in canon.list_unspent() {
228			if utxo.is_trusted {
229				trusted += utxo.txout.value;
230			} else {
231				untrusted += utxo.txout.value;
232			}
233		}
234		TrustedBalance { trusted, untrusted }
235	}
236
237	/// Return all UTXOs that are untrusted.
238	fn untrusted_utxos(&self, min_confs: u32) -> Vec<OutPoint> {
239		TrustedCanonicalization::from_wallet(self.borrow(), min_confs)
240			.list_unspent()
241			.filter(|u| !u.is_trusted)
242			.map(|u| u.outpoint)
243			.collect()
244	}
245
246	/// Check if a transaction is fully owned by the wallet (all inputs spend
247	/// wallet-owned outputs).
248	fn is_fully_owned_tx(&self, txid: Txid) -> bool {
249		let wallet = self.borrow();
250		let graph = wallet.tx_graph();
251		match graph.get_tx(txid) {
252			Some(tx) => {
253				tx.input.iter().all(|input| {
254					let prev = input.previous_output;
255					graph.get_tx(prev.txid)
256						.and_then(|prev_tx| prev_tx.output.get(prev.vout as usize).cloned())
257						.map(|out| wallet.is_mine(out.script_pubkey))
258						.unwrap_or(false)
259					})
260			}, None => false
261		}
262
263	}
264
265	/// Insert a checkpoint into the wallet.
266	///
267	/// It's advised to use this only when recovering a wallet with a birthday.
268	fn set_checkpoint(&mut self, height: u32, hash: BlockHash) {
269		let checkpoint = BlockId { height, hash };
270		let wallet = self.borrow_mut();
271		wallet.apply_update(bdk_wallet::Update {
272			chain: Some(wallet.latest_checkpoint().insert(checkpoint)),
273			..Default::default()
274		}).expect("should work, might fail if tip is genesis");
275	}
276
277	/// Mark the keys used in the outputs of this tx as unused
278	///
279	/// Used to replaced removed `cancel_tx` function as per suggestion:
280	/// https://github.com/bitcoindevkit/bdk_wallet/pull/393
281	fn mark_output_keys_unused(&mut self, tx: &Transaction) {
282		let wallet = self.borrow_mut();
283		for txout in &tx.output {
284			if let Some((keychain, index)) = wallet.spk_index().index_of_spk(txout.script_pubkey.clone()) {
285				// NOTE: unmark_used will **not** make something unused if it has actually been used
286				// by a tx in the tracker. It only removes the superficial marking.
287				wallet.unmark_used(*keychain, *index);
288			}
289		}
290	}
291
292	fn make_signed_p2a_cpfp(
293		&mut self,
294		tx: &Transaction,
295		fees: MakeCpfpFees,
296	) -> Result<Transaction, CpfpInternalError> {
297		let wallet = self.borrow_mut();
298		let (fee_anchor_point, fee_anchor_txout) = tx.fee_anchor()
299			.ok_or_else(|| CpfpInternalError::NoFeeAnchor(tx.compute_txid()))?;
300
301		// Since BDK doesn't support adding extra weight for fees, we have to loop to achieve the
302		// effective fee rate and potential minimum fee we need.
303		let parent_weight = tx.weight();
304		let extra_fee_needed = parent_weight * fees.effective();
305
306		// Since BDK doesn't allow tx without recipients, we add a drain output.
307		let change_addr = wallet.next_unused_address(KEYCHAIN);
308		let dust_limit = change_addr.address.script_pubkey().minimal_non_dust();
309
310		// We will loop, constructing the transaction and signing it until we exceed the effective
311		// fee rate and meet any minimum fee requirements
312		let mut final_child_weight = Weight::ZERO;
313		let mut fee_needed = extra_fee_needed;
314		for i in 0..100 {
315			// We need to account for a particularly annoying BDK bug when using foreign UTXOs when
316			// BDK tries to use the P2A value to pay the fees. If the P2A has a value of 420 sats
317			// and the absolute fee is 200 sats, this will produce a 220 sat change output which
318			// results in a coin selection error. Ideally, BDK would pull in an extra UTXO to ensure
319			// the change output is more than the dust limit; however, this seems to be an edge case
320			// with experimental foreign UTXOs.
321			if fee_needed < fee_anchor_txout.value {
322				if fee_anchor_txout.value - fee_needed < dust_limit {
323					fee_needed = fee_anchor_txout.value + Amount::ONE_SAT;
324				}
325			}
326
327			let mut b = wallet.build_tx();
328			b.only_witness_utxo();
329			b.exclude_unconfirmed();
330			b.version(3); // for 1p1c package relay, all inputs must be confirmed
331			b.add_fee_anchor_spend(fee_anchor_point, fee_anchor_txout);
332			b.drain_to(change_addr.address.script_pubkey());
333			b.fee_absolute(fee_needed);
334
335			// Attempt to create and sign the transaction
336			let mut psbt = b.finish().map_err(|e| match e {
337				CreateTxError::CoinSelection(e) => CpfpInternalError::InsufficientConfirmedFunds(e),
338				_ => CpfpInternalError::Create(e),
339			})?;
340			#[allow(deprecated)]
341			let opts = bdk_wallet::SignOptions {
342				trust_witness_utxo: true,
343				..Default::default()
344			};
345			let finalized = wallet.sign(&mut psbt, opts)
346				.map_err(|e| CpfpInternalError::Signer(e))?;
347			if !finalized {
348				return Err(CpfpInternalError::FinalizeError("finalization failed".into()));
349			}
350			let tx = psbt.extract_tx()
351				.map_err(|e| CpfpInternalError::Extract(e))?;
352			assert!(tx.input.iter().any(|i| i.previous_output == fee_anchor_point),
353				"Missing anchor spend, tx is {}", serialize_hex(&tx),
354			);
355
356			// We can finally check the fees and weight
357			let tx_weight = tx.weight();
358			let total_weight = tx_weight + parent_weight;
359			if tx_weight != final_child_weight {
360				// Since the weight changed, we can drop the transaction and recalculate the
361				// required fee amount.
362				wallet.mark_output_keys_unused(&tx);
363				final_child_weight = tx_weight;
364				fee_needed = match fees {
365					MakeCpfpFees::Effective(fr) => total_weight * fr,
366					MakeCpfpFees::Rbf { min_effective_fee_rate, current_package_fee } => {
367						// RBF requires that you spend at least the total fee of every
368						// unconfirmed ancestor and the transaction you want to replace,
369						// then you must add mintxrelayfee * package_vbytes on top.
370						let min_tx_relay_fee = FeeRate::from_sat_per_vb(1).unwrap();
371						let min_package_fee = current_package_fee +
372							parent_weight * min_tx_relay_fee +
373							tx_weight * min_tx_relay_fee;
374
375						// This is the fee we want to pay based on the given minimum effective fee
376						// rate. It's possible that the desired fee is lower than the minimum
377						// package fee if the currently broadcast child transaction is bigger than
378						// the transaction we just produced.
379						let desired_fee = total_weight * min_effective_fee_rate;
380						if desired_fee < min_package_fee {
381							debug!("Using a minimum fee of {} instead of the desired fee of {} for RBF",
382								min_package_fee, desired_fee,
383							);
384							min_package_fee
385						} else {
386							trace!("Attempting to use the desired fee of {} for CPFP RBF",
387								desired_fee,
388							);
389							desired_fee
390						}
391					}
392				}
393			} else {
394				debug!("Created P2A CPFP with weight {} and fee {} in {} iterations",
395					total_weight, fee_needed, i,
396				);
397				return Ok(tx);
398			}
399		}
400		Err(CpfpInternalError::General("Reached max iterations".into()))
401	}
402}
403
404impl WalletExt for Wallet {}