|
| 1 | +use crate::{CPFPParams, CanonicalUnspents, Input}; |
| 2 | +use alloc::collections::BTreeMap; |
| 3 | +use alloc::sync::Arc; |
| 4 | +use alloc::vec::Vec; |
| 5 | +use bdk_chain::tx_graph::CalculateFeeError; |
| 6 | +use bdk_chain::Anchor; |
| 7 | +use miniscript::bitcoin::{absolute::Height, Amount, FeeRate, OutPoint, Transaction, Txid, Weight}; |
| 8 | +use std::collections::HashSet; |
| 9 | + |
| 10 | +/// Set of CPFP |
| 11 | +#[derive(Debug, Clone)] |
| 12 | +pub struct CPFPSet { |
| 13 | + /// Parent transactions and their unconfirmed ancestors. |
| 14 | + pub txs: BTreeMap<Txid, Arc<Transaction>>, |
| 15 | + /// Total fee of parent transactions and their ancestors. |
| 16 | + pub total_fee: Amount, |
| 17 | + /// Total weight of parent transactions and their ancestors. |
| 18 | + pub total_weight: Weight, |
| 19 | +} |
| 20 | + |
| 21 | +/// CPFP errors. |
| 22 | +#[derive(Debug)] |
| 23 | +pub enum CPFPError { |
| 24 | + /// A specified parent transaction ID does not exist in the transaction graph. |
| 25 | + MissingParent(Txid), |
| 26 | + /// A previous transaction (prevout) referenced by a parent transaction is missing. |
| 27 | + MissingPrevTx(Txid), |
| 28 | + /// An output referenced by an outpoint in a parent transaction is missing. |
| 29 | + MissingPrevTxOut(OutPoint), |
| 30 | + /// No parent transactions were provided for the CPFP operation. |
| 31 | + NoParents, |
| 32 | + /// A parent transaction has no unspent outputs available to spend in the CPFP transaction. |
| 33 | + NoUnspentOutput(Txid), |
| 34 | + /// The number of unconfirmed ancestors exceeds the Bitcoin protocol limit (25). |
| 35 | + ExcessUnconfirmedAncestor, |
| 36 | + /// An error occurred while calculating the fee for a transaction. |
| 37 | + CalculateFee(CalculateFeeError), |
| 38 | +} |
| 39 | + |
| 40 | +impl core::fmt::Display for CPFPError { |
| 41 | + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
| 42 | + match self { |
| 43 | + Self::MissingParent(txid) => write!(f, "parent transaction {} not found", txid), |
| 44 | + Self::MissingPrevTx(txid) => write!(f, "previous transaction {} not found", txid), |
| 45 | + Self::MissingPrevTxOut(outpoint) => write!(f, "previous output {} not found", outpoint), |
| 46 | + Self::NoParents => write!(f, "no parent transactions provided"), |
| 47 | + Self::ExcessUnconfirmedAncestor => write!(f, "too many unconfirmed ancestor"), |
| 48 | + Self::NoUnspentOutput(txid) => { |
| 49 | + write!(f, "no unspent output found for parent transaction {}", txid) |
| 50 | + } |
| 51 | + Self::CalculateFee(err) => write!(f, "failed to calculate fee: {}", err), |
| 52 | + } |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +impl From<CalculateFeeError> for CPFPError { |
| 57 | + fn from(err: CalculateFeeError) -> Self { |
| 58 | + CPFPError::CalculateFee(err) |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +#[cfg(feature = "std")] |
| 63 | +impl std::error::Error for CPFPError {} |
| 64 | + |
| 65 | +impl CPFPSet { |
| 66 | + /// Create a new CPFPSet from parent transactions and their ancestors. |
| 67 | + pub fn new( |
| 68 | + parent_txids: impl IntoIterator<Item = Txid>, |
| 69 | + graph: &bdk_chain::TxGraph, |
| 70 | + tip_height: Height, |
| 71 | + ) -> Result<Self, CPFPError> { |
| 72 | + let mut parent_fee = Amount::ZERO; |
| 73 | + let mut parent_weight = Weight::ZERO; |
| 74 | + |
| 75 | + let mut txs: BTreeMap<Txid, Arc<Transaction>> = BTreeMap::new(); |
| 76 | + |
| 77 | + let parent_txids: Vec<Txid> = parent_txids.into_iter().collect(); |
| 78 | + |
| 79 | + for txid in parent_txids { |
| 80 | + let mut stack = vec![txid]; |
| 81 | + while let Some(current_txid) = stack.pop() { |
| 82 | + if let Some(tx_node) = graph.get_tx_node(current_txid) { |
| 83 | + // Check if transaction is unconfirmed |
| 84 | + let is_unconfirmed = tx_node.anchors.is_empty() |
| 85 | + || tx_node.anchors.iter().all(|anchor| { |
| 86 | + anchor.anchor_block().height > tip_height.to_consensus_u32() |
| 87 | + }); |
| 88 | + |
| 89 | + if is_unconfirmed { |
| 90 | + // Calculate fees and weights for all unconfirmed ancestors |
| 91 | + let tx = tx_node.tx; |
| 92 | + parent_fee += graph.calculate_fee(&tx)?; |
| 93 | + parent_weight += tx.weight(); |
| 94 | + txs.insert(txid, tx.clone()); |
| 95 | + |
| 96 | + for input in &tx.input { |
| 97 | + stack.push(input.previous_output.txid); |
| 98 | + } |
| 99 | + } |
| 100 | + } else { |
| 101 | + return Err(CPFPError::MissingParent(txid)); |
| 102 | + } |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + const MAX_ANCESTORS: usize = 25; |
| 107 | + if txs.len() > MAX_ANCESTORS { |
| 108 | + return Err(CPFPError::NoParents); |
| 109 | + } |
| 110 | + |
| 111 | + Ok(Self { |
| 112 | + txs, |
| 113 | + total_fee: parent_fee, |
| 114 | + total_weight: parent_weight, |
| 115 | + }) |
| 116 | + } |
| 117 | + |
| 118 | + /// Select the largest unspent output for each parent transaction. |
| 119 | + pub fn must_select_largest_input_of_each_parent( |
| 120 | + &self, |
| 121 | + canon_utxos: &CanonicalUnspents, |
| 122 | + ) -> Result<HashSet<OutPoint>, CPFPError> { |
| 123 | + let mut must_select = HashSet::new(); |
| 124 | + |
| 125 | + for (txid, tx) in &self.txs { |
| 126 | + let outpoint = tx |
| 127 | + .output |
| 128 | + .iter() |
| 129 | + .enumerate() |
| 130 | + .map(|(vout, _)| OutPoint { |
| 131 | + txid: *txid, |
| 132 | + vout: vout as u32, |
| 133 | + }) |
| 134 | + .filter(|op| canon_utxos.is_unspent(*op)) |
| 135 | + .max_by_key(|op| { |
| 136 | + canon_utxos |
| 137 | + .get_tx(&op.txid) |
| 138 | + .and_then(|tx| tx.output.get(op.vout as usize)) |
| 139 | + .map(|txout| txout.value) |
| 140 | + .unwrap_or(Amount::ZERO) |
| 141 | + }) |
| 142 | + .ok_or_else(|| CPFPError::NoUnspentOutput(*txid))?; |
| 143 | + |
| 144 | + must_select.insert(outpoint); |
| 145 | + } |
| 146 | + |
| 147 | + Ok(must_select) |
| 148 | + } |
| 149 | + |
| 150 | + /// Filter input for candidates |
| 151 | + pub fn candidate_filter<'a>( |
| 152 | + &'a self, |
| 153 | + canon_utxos: &'a CanonicalUnspents, |
| 154 | + tip_height: Height, |
| 155 | + ) -> impl Fn(&Input) -> bool + 'a { |
| 156 | + let parent_outpoints = self |
| 157 | + .txs |
| 158 | + .values() |
| 159 | + .flat_map(|tx| tx.input.iter().map(|txin| txin.previous_output)) |
| 160 | + .collect::<HashSet<OutPoint>>(); |
| 161 | + |
| 162 | + move |input: &Input| { |
| 163 | + if parent_outpoints.contains(&input.prev_outpoint()) { |
| 164 | + return true; |
| 165 | + } |
| 166 | + |
| 167 | + input.confirmations(tip_height) > 0 |
| 168 | + || canon_utxos |
| 169 | + .get_spend(&input.prev_outpoint()) |
| 170 | + .map_or(false, |txid| self.txs.contains_key(txid)) |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + /// Generate CPFP parameters for coin selection. |
| 175 | + pub fn selector_cpfp_params(&self, target_feerate: FeeRate) -> CPFPParams { |
| 176 | + CPFPParams::new( |
| 177 | + self.txs.keys().cloned(), |
| 178 | + target_feerate, |
| 179 | + self.total_fee, |
| 180 | + self.total_weight, |
| 181 | + ) |
| 182 | + } |
| 183 | +} |
0 commit comments