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#[derive(Debug, Clone)]
27pub struct LocalTransaction {
28 pub tx: Arc<Transaction>,
30 pub chain_position: ChainPosition<ConfirmationBlockTime>,
31 pub is_trusted: bool,
32}
33
34pub 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
44pub struct TrustedCanonicalization {
61 txs: HashMap<Txid, LocalTransaction>,
62 unspent: Vec<OutPoint>,
63}
64
65impl TrustedCanonicalization {
66 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 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 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 pub fn is_trusted(&self, txid: Txid) -> bool {
122 self.txs.get(&txid).map(|e| e.is_trusted).unwrap_or(false)
123 }
124
125 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: <.tx.output[op.vout as usize],
133 chain_position: <.chain_position,
134 is_trusted: lt.is_trusted,
135 }
136 })
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
142pub struct TrustedBalance {
143 pub trusted: Amount,
145 pub untrusted: Amount,
147}
148
149impl TrustedBalance {
150 pub fn total(&self) -> Amount {
151 self.trusted + self.untrusted
152 }
153}
154
155pub const KEYCHAIN: bdk_wallet::KeychainKind = bdk_wallet::KeychainKind::External;
157
158
159#[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 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 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
210pub trait TxBuilderExt<'a, A>: BorrowMut<TxBuilder<'a, A>> {
212 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
249pub trait WalletExt: BorrowMut<Wallet> {
251 fn peek_next_address(&self) -> AddressInfo {
253 self.borrow().peek_address(KEYCHAIN, self.borrow().next_derivation_index(KEYCHAIN))
254 }
255
256 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 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 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 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 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 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 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 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 let parent_weight = tx.weight();
361 let extra_fee_needed = parent_weight * fees.effective();
362
363 let change_addr = wallet.next_unused_address(KEYCHAIN);
365
366 let mut final_child_weight = Weight::ZERO;
369 let mut fee_needed = extra_fee_needed;
370 for i in 0..100 {
371 let mut b = wallet.build_tx().coin_selection(NonDustDrainCoinSelection);
374 b.only_witness_utxo();
375 b.exclude_unconfirmed();
376 b.version(3); 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 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 let tx_weight = tx.weight();
404 let total_weight = tx_weight + parent_weight;
405 if tx_weight != final_child_weight {
406 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 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 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 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 #[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 assert_eq!(change, Amount::from_sat(2_001) - fee);
498 assert_eq!(psbt.fee().unwrap(), fee);
499 }
500
501 #[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 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 #[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 {}