bitcoin_ext/
bdk.rs

1
2use std::borrow::BorrowMut;
3use std::sync::Arc;
4
5use bdk_wallet::{AddressInfo, TxBuilder, Wallet};
6use bdk_wallet::chain::BlockId;
7use bdk_wallet::coin_selection::InsufficientFunds;
8use bdk_wallet::error::CreateTxError;
9use bitcoin::consensus::encode::serialize_hex;
10use bitcoin::{Amount, BlockHash, FeeRate, OutPoint, Transaction, TxOut, Txid, Weight, Witness};
11use bitcoin::psbt::{ExtractTxError, Input};
12use log::{debug, trace};
13
14use crate::{BlockHeight, TransactionExt};
15use crate::cpfp::MakeCpfpFees;
16use crate::fee::FEE_ANCHOR_SPEND_WEIGHT;
17
18/// Balance categorized by our recursive trust model.
19#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20pub struct TrustedBalance {
21	/// Funds in UTXOs we trust (confirmed or all-ours unconfirmed chains).
22	pub trusted: Amount,
23	/// Funds in UTXOs we don't trust.
24	pub untrusted: Amount,
25}
26
27impl TrustedBalance {
28	pub fn total(&self) -> Amount {
29		self.trusted + self.untrusted
30	}
31}
32
33/// The [bdk_wallet::KeychainKind] that is always used, because we only use a single keychain.
34pub const KEYCHAIN: bdk_wallet::KeychainKind = bdk_wallet::KeychainKind::External;
35
36
37/// An extension trait for [TxBuilder].
38pub trait TxBuilderExt<'a, A>: BorrowMut<TxBuilder<'a, A>> {
39	/// Add an input to the tx that spends a fee anchor.
40	fn add_fee_anchor_spend(&mut self, anchor: OutPoint, output: &TxOut)
41	where
42		A: bdk_wallet::coin_selection::CoinSelectionAlgorithm,
43	{
44		let psbt_in = Input {
45			witness_utxo: Some(output.clone()),
46			final_script_witness: Some(Witness::new()),
47			..Default::default()
48		};
49		self.borrow_mut().add_foreign_utxo(anchor, psbt_in, FEE_ANCHOR_SPEND_WEIGHT)
50			.expect("adding foreign utxo");
51	}
52}
53impl<'a, A> TxBuilderExt<'a, A> for TxBuilder<'a, A> {}
54
55#[derive(Debug, thiserror::Error)]
56pub enum CpfpInternalError {
57	#[error("{0}")]
58	General(String),
59	#[error("Unable to construct transaction: {0}")]
60	Create(CreateTxError),
61	#[error("Unable to extract the final transaction after signing the PSBT: {0}")]
62	Extract(ExtractTxError),
63	#[error("Failed to determine the weight/fee when creating a P2A CPFP")]
64	Fee(),
65	#[error("Unable to finalize CPFP transaction: {0}")]
66	FinalizeError(String),
67	#[error("You need more confirmations on your on-chain funds: {0}")]
68	InsufficientConfirmedFunds(InsufficientFunds),
69	#[error("Transaction has no fee anchor: {0}")]
70	NoFeeAnchor(Txid),
71	#[allow(deprecated)]
72	#[error("Unable to sign transaction: {0}")]
73	Signer(bdk_wallet::signer::SignerError),
74}
75
76/// An extension trait for [Wallet].
77pub trait WalletExt: BorrowMut<Wallet> {
78	/// Peek into the next address.
79	fn peek_next_address(&self) -> AddressInfo {
80		self.borrow().peek_address(KEYCHAIN, self.borrow().next_derivation_index(KEYCHAIN))
81	}
82
83	/// Returns an iterator for each unconfirmed transaction in the wallet.
84	fn unconfirmed_txids(&self) -> impl Iterator<Item = Txid> {
85		self.borrow().transactions().filter_map(|tx| {
86			if tx.chain_position.is_unconfirmed() {
87				Some(tx.tx_node.txid)
88			} else {
89				None
90			}
91		})
92	}
93
94	/// Returns an iterator for each unconfirmed transaction in the wallet, useful for syncing
95	/// with bitcoin core.
96	fn unconfirmed_txs(&self) -> impl Iterator<Item = Arc<Transaction>> {
97		self.borrow().transactions().filter_map(|tx| {
98			if tx.chain_position.is_unconfirmed() {
99				Some(tx.tx_node.tx.clone())
100			} else {
101				None
102			}
103		})
104	}
105
106	/// Check whether a UTXO can be trusted for spending.
107	///
108	/// Delegates to [WalletExt::is_trusted_tx] on the creating transaction.
109	fn is_trusted_utxo(&self, outpoint: OutPoint, min_confs: u32) -> bool {
110		self.is_trusted_tx(outpoint.txid, min_confs)
111	}
112
113	/// Check whether a transaction can be trusted to confirm on chain.
114	///
115	/// A transaction is trusted if:
116	/// - `min_confs` is 0 (unconditionally trusted), or
117	/// - it has at least `min_confs` confirmations, or
118	/// - all its inputs are ours and their creating transactions are also
119	///   trusted (checked recursively).
120	///
121	/// To keep the check cheap, at most 100 ancestor transactions are visited.
122	/// If the budget is exhausted the transaction is considered untrusted.
123	fn is_trusted_tx(&self, txid: Txid, min_confs: u32) -> bool {
124		let w = self.borrow();
125		let tip = w.latest_checkpoint().height();
126		let mut budget = 100u32;
127		is_trusted_tx_inner(w, txid, min_confs, tip, &mut budget)
128	}
129
130	/// Compute the wallet balance using our recursive trust model.
131	fn trusted_balance(&self, min_confs: u32) -> TrustedBalance {
132		let mut trusted = Amount::ZERO;
133		let mut untrusted = Amount::ZERO;
134		for utxo in self.borrow().list_unspent() {
135			if self.is_trusted_utxo(utxo.outpoint, min_confs) {
136				trusted += utxo.txout.value;
137			} else {
138				untrusted += utxo.txout.value;
139			}
140		}
141		TrustedBalance { trusted, untrusted }
142	}
143
144	/// Return all UTXOs that are untrusted.
145	///
146	/// Delegates to [WalletExt::is_trusted_utxo] for each UTXO.
147	fn untrusted_utxos(&self, min_confs: u32) -> Vec<OutPoint> {
148		self.borrow().list_unspent()
149			.filter(|utxo| !self.is_trusted_utxo(utxo.outpoint, min_confs))
150			.map(|utxo| utxo.outpoint)
151			.collect()
152	}
153
154	/// Insert a checkpoint into the wallet.
155	///
156	/// It's advised to use this only when recovering a wallet with a birthday.
157	fn set_checkpoint(&mut self, height: u32, hash: BlockHash) {
158		let checkpoint = BlockId { height, hash };
159		let wallet = self.borrow_mut();
160		wallet.apply_update(bdk_wallet::Update {
161			chain: Some(wallet.latest_checkpoint().insert(checkpoint)),
162			..Default::default()
163		}).expect("should work, might fail if tip is genesis");
164	}
165
166	fn make_signed_p2a_cpfp(
167		&mut self,
168		tx: &Transaction,
169		fees: MakeCpfpFees,
170	) -> Result<Transaction, CpfpInternalError> {
171		let wallet = self.borrow_mut();
172		let (outpoint, txout) = tx.fee_anchor()
173			.ok_or_else(|| CpfpInternalError::NoFeeAnchor(tx.compute_txid()))?;
174
175		// Since BDK doesn't support adding extra weight for fees, we have to loop to achieve the
176		// effective fee rate and potential minimum fee we need.
177		let p2a_weight = tx.weight();
178		let extra_fee_needed = p2a_weight * fees.effective();
179
180		// Since BDK doesn't allow tx without recipients, we add a drain output.
181		let change_addr = wallet.reveal_next_address(KEYCHAIN);
182		let dust_limit = change_addr.address.script_pubkey().minimal_non_dust();
183
184		// We will loop, constructing the transaction and signing it until we exceed the effective
185		// fee rate and meet any minimum fee requirements
186		let mut spend_weight = Weight::ZERO;
187		let mut fee_needed = extra_fee_needed;
188		for i in 0..100 {
189			// We need to account for a particularly annoying BDK bug when using foreign UTXOs when
190			// BDK tries to use the P2A value to pay the fees. If the P2A has a value of 420 sats
191			// and the absolute fee is 200 sats, this will produce a 220 sat change output which
192			// results in a coin selection error. Ideally, BDK would pull in an extra UTXO to ensure
193			// the change output is more than the dust limit; however, this seems to be an edge case
194			// with experimental foreign UTXOs.
195			if fee_needed < txout.value {
196				if txout.value - fee_needed < dust_limit {
197					fee_needed = txout.value + Amount::ONE_SAT;
198				}
199			}
200
201			let mut b = wallet.build_tx();
202			b.only_witness_utxo();
203			b.exclude_unconfirmed();
204			b.version(3); // for 1p1c package relay, all inputs must be confirmed
205			b.add_fee_anchor_spend(outpoint, txout);
206			b.drain_to(change_addr.address.script_pubkey());
207			b.fee_absolute(fee_needed);
208
209			// Attempt to create and sign the transaction
210			let mut psbt = b.finish().map_err(|e| match e {
211				CreateTxError::CoinSelection(e) => CpfpInternalError::InsufficientConfirmedFunds(e),
212				_ => CpfpInternalError::Create(e),
213			})?;
214			#[allow(deprecated)]
215			let opts = bdk_wallet::SignOptions {
216				trust_witness_utxo: true,
217				..Default::default()
218			};
219			let finalized = wallet.sign(&mut psbt, opts)
220				.map_err(|e| CpfpInternalError::Signer(e))?;
221			if !finalized {
222				return Err(CpfpInternalError::FinalizeError("finalization failed".into()));
223			}
224			let tx = psbt.extract_tx()
225				.map_err(|e| CpfpInternalError::Extract(e))?;
226			let anchor_weight = FEE_ANCHOR_SPEND_WEIGHT.to_wu();
227			assert!(tx.input.iter().any(|i| i.witness.size() as u64 == anchor_weight),
228				"Missing anchor spend, tx is {}", serialize_hex(&tx),
229			);
230
231			// We can finally check the fees and weight
232			let tx_weight = tx.weight();
233			let total_weight = tx_weight + p2a_weight;
234			if tx_weight != spend_weight {
235				// Since the weight changed, we can drop the transaction and recalculate the
236				// required fee amount.
237				wallet.cancel_tx(&tx);
238				spend_weight = tx_weight;
239				fee_needed = match fees {
240					MakeCpfpFees::Effective(fr) => total_weight * fr,
241					MakeCpfpFees::Rbf { min_effective_fee_rate, current_package_fee } => {
242						// RBF requires that you spend at least the total fee of every
243						// unconfirmed ancestor and the transaction you want to replace,
244						// then you must add mintxrelayfee * package_vbytes on top.
245						let min_tx_relay_fee = FeeRate::from_sat_per_vb(1).unwrap();
246						let min_package_fee = current_package_fee +
247							p2a_weight * min_tx_relay_fee +
248							tx_weight * min_tx_relay_fee;
249
250						// This is the fee we want to pay based on the given minimum effective fee
251						// rate. It's possible that the desired fee is lower than the minimum
252						// package fee if the currently broadcast child transaction is bigger than
253						// the transaction we just produced.
254						let desired_fee = total_weight * min_effective_fee_rate;
255						if desired_fee < min_package_fee {
256							debug!("Using a minimum fee of {} instead of the desired fee of {} for RBF",
257								min_package_fee, desired_fee,
258							);
259							min_package_fee
260						} else {
261							trace!("Attempting to use the desired fee of {} for CPFP RBF",
262								desired_fee,
263							);
264							desired_fee
265						}
266					}
267				}
268			} else {
269				debug!("Created P2A CPFP with weight {} and fee {} in {} iterations",
270					total_weight, fee_needed, i,
271				);
272				return Ok(tx);
273			}
274		}
275		Err(CpfpInternalError::General("Reached max iterations".into()))
276	}
277}
278
279impl WalletExt for Wallet {}
280
281fn is_trusted_tx_inner(
282	w: &Wallet, txid: Txid, min_confs: u32, tip: BlockHeight, budget: &mut u32,
283) -> bool {
284	if *budget == 0 {
285		return false;
286	}
287	*budget -= 1;
288
289	let Some(tx) = w.get_tx(txid) else {
290		return false;
291	};
292
293	// Trust transactions with enough confirmations (unconfirmed = 0 confs).
294	let nb_confs = match tx.chain_position.confirmation_height_upper_bound() {
295		Some(h) => tip.saturating_sub(h) + 1,
296		None => 0,
297	};
298	if nb_confs >= min_confs {
299		return true;
300	}
301
302	// Recursively check that all inputs are ours and come from trusted transactions.
303	tx.tx_node.tx.input.iter().all(|input| {
304		let prev = input.previous_output;
305		let Some(prev_tx) = w.get_tx(prev.txid) else {
306			return false;
307		};
308		let Some(txout) = prev_tx.tx_node.tx.output.get(prev.vout as usize) else {
309			return false;
310		};
311		w.is_mine(txout.script_pubkey.clone())
312			&& is_trusted_tx_inner(w, prev.txid, min_confs, tip, budget)
313	})
314}