diff --git a/src/coin_selector.rs b/src/coin_selector.rs index 604abd8..096e2c3 100644 --- a/src/coin_selector.rs +++ b/src/coin_selector.rs @@ -8,6 +8,21 @@ use alloc::{sync::Arc, vec::Vec}; /// `change_lower` argument of [`CoinSelector::select_srd`]. pub const CHANGE_LOWER: u64 = 50_000; +/// An unconfirmed ancestor transaction that may need a fee bump (CPFP). +/// +/// When spending unconfirmed UTXOs, miners evaluate the transaction as a package with its +/// unconfirmed ancestors. If ancestors paid below the target feerate, the child must overpay. +#[derive(Debug, Clone)] +pub struct UnconfirmedAncestor { + /// The weight of the ancestor transaction in weight units. + pub weight: u64, + /// The fee already paid by the ancestor transaction in satoshis. + pub fee_paid: u64, + /// Candidate indices whose selection includes this ancestor in the CPFP package. + /// Indices use the candidate slice passed to [`CoinSelector::new`]. + pub dependent_candidates: Vec, +} + /// [`CoinSelector`] selects/deselects coins from a set of canididate coins. /// /// You can manually select coins using methods like [`select`], or automatically with methods such @@ -18,6 +33,12 @@ pub const CHANGE_LOWER: u64 = 50_000; #[derive(Debug, Clone)] pub struct CoinSelector<'a> { candidates: &'a [Candidate], + /// CPFP lookup table (via [`CoinSelector::with_ancestors`]). Each ancestor tracks the + /// candidates that depend on it. + ancestors: &'a [UnconfirmedAncestor], + /// The union of every ancestor's `dependent_candidates`, deduplicated. These are banned from + /// automatic selection; see [`CoinSelector::with_ancestors`]. + ancestor_dependents: Bitset, selected: Bitset, banned: Bitset, candidate_order: Arc>, @@ -38,12 +59,81 @@ impl<'a> CoinSelector<'a> { pub fn new(candidates: &'a [Candidate]) -> Self { Self { candidates, + ancestors: &[], + ancestor_dependents: Bitset::with_capacity(candidates.len()), selected: Bitset::with_capacity(candidates.len()), banned: Bitset::with_capacity(candidates.len()), candidate_order: Arc::new((0..candidates.len()).collect::>()), } } + /// Set the shared ancestor data for CPFP bump fee calculations. + /// + /// Each [`UnconfirmedAncestor`] contains the indices of candidates that depend on it. Every + /// such candidate is [`ban`]ned, so the automatic selection algorithms will never pick one on + /// their own. You can still [`select`] them manually — that is the intended CPFP flow: you + /// decide which unconfirmed UTXOs to spend, and this priced the resulting package for you. + /// + /// # Why they are banned + /// + /// The ancestor bump fee is a property of the *package*, not of any one candidate: ancestors + /// shared between candidates are counted once, and an overpaying ancestor subsidizes an + /// underpaying one. So the cost of adding a candidate depends on what else is selected. + /// + /// Every automatic algorithm here ranks and accumulates using per-candidate figures + /// ([`Candidate::effective_value`], [`Candidate::value_pwu`]), which cannot see ancestors. If + /// such candidates were selectable, adding one could *lower* the excess, and the algorithms + /// break in ways that produce no error: [`select_until_target_met`] and [`select_srd`] can + /// report insufficient funds when a funding selection exists, and branch and bound's bounds + /// stop being lower bounds, silently pruning the optimal solution. + /// + /// Banning keeps the ancestor set — and hence the bump — identical across every selection an + /// algorithm can reach, which is what those algorithms need to stay correct. + /// + /// # Panics + /// + /// If any `dependent_candidates` index is out of bounds for the candidate slice passed to + /// [`CoinSelector::new`]. + /// + /// [`ban`]: Self::ban + /// [`select`]: Self::select + /// [`select_until_target_met`]: Self::select_until_target_met + /// [`select_srd`]: Self::select_srd + pub fn with_ancestors(mut self, ancestors: &'a [UnconfirmedAncestor]) -> Self { + for ancestor in ancestors { + for &candidate_index in &ancestor.dependent_candidates { + assert!( + candidate_index < self.candidates.len(), + "ancestor dependent candidate index {} out of bounds for {} candidates", + candidate_index, + self.candidates.len() + ); + self.ancestor_dependents.insert(candidate_index); + self.ban(candidate_index); + } + } + self.ancestors = ancestors; + self + } + + /// The candidates that have unconfirmed ancestors, by index into the original `candidates` + /// slice passed to [`CoinSelector::new`]. + /// + /// These are exactly the candidates [`with_ancestors`] banned from automatic selection. To + /// spend one, [`select`] it manually — the [ancestor bump fee] is then priced into every + /// excess calculation. + /// + /// Prefer this over filtering [`banned`], which also contains any candidates you banned + /// yourself. + /// + /// [`with_ancestors`]: Self::with_ancestors + /// [`select`]: Self::select + /// [`banned`]: Self::banned + /// [ancestor bump fee]: Self::selected_ancestor_bump_fee + pub fn candidates_with_ancestors(&self) -> impl Iterator + '_ { + self.ancestor_dependents.iter() + } + /// Iterate over all the candidates in their currently sorted order. Each item has the original /// index with the candidate. pub fn candidates( @@ -182,6 +272,40 @@ impl<'a> CoinSelector<'a> { + target_ouputs.output_weight_with_drain(drain_weight) } + /// Compute the package-level ancestor bump fee for the current selection at the given feerate. + /// + /// This includes ancestors with at least one selected dependent candidate, sums their weights + /// and fees once, then computes `max(0, implied_fee(total_weight, feerate) - total_fees)`. + /// + /// High-feerate ancestors subsidize low-feerate ones within the package (matching Bitcoin + /// Core's package relay approach). + pub fn selected_ancestor_bump_fee(&self, feerate: FeeRate) -> u64 { + if self.ancestors.is_empty() { + return 0; + } + debug_assert!( + self.ancestor_dependents + .iter() + .all(|i| self.banned.contains(i)), + "candidates with unconfirmed ancestors must stay banned, so that the ancestor bump \ + fee is constant across every selection an algorithm can reach" + ); + let mut total_weight = 0u64; + let mut total_fee_paid = 0u64; + for ancestor in self.ancestors { + if ancestor + .dependent_candidates + .iter() + .any(|&candidate_index| self.selected.contains(candidate_index)) + { + total_weight += ancestor.weight; + total_fee_paid += ancestor.fee_paid; + } + } + let implied = feerate.implied_fee(total_weight); + implied.saturating_sub(total_fee_paid) + } + /// How much the current selection overshoots the value needed to achieve `target`. /// /// In order for the resulting transaction to be valid this must be 0 or above. If it's above 0 @@ -208,7 +332,7 @@ impl<'a> CoinSelector<'a> { self.selected_value() as i64 - target.value() as i64 - drain.value as i64 - - self.implied_fee_from_feerate(target, drain.weights) as i64 + - self.implied_package_fee_from_feerate(target, drain.weights) as i64 } /// Same as [rate_excess](Self::rate_excess) except `target.fee.rate` is applied to the @@ -217,7 +341,7 @@ impl<'a> CoinSelector<'a> { self.selected_value() as i64 - target.value() as i64 - drain.value as i64 - - self.implied_fee_from_feerate_wu(target, drain.weights) as i64 + - self.implied_package_fee_from_feerate_wu(target, drain.weights) as i64 } /// How much the current selection overshoots the value needed to satisfy `target.fee.absolute` @@ -231,29 +355,19 @@ impl<'a> CoinSelector<'a> { /// How much the current selection overshoots the value needed to satisfy RBF's rule 4. pub fn replacement_excess(&self, target: Target, drain: Drain) -> i64 { - let mut replacement_excess_needed = 0; - if let Some(replace) = target.fee.replace { - replacement_excess_needed = - replace.min_fee_to_do_replacement(self.weight(target.outputs, drain.weights)) - } self.selected_value() as i64 - target.value() as i64 - drain.value as i64 - - replacement_excess_needed as i64 + - self.implied_package_fee_from_replacement(target, drain.weights) as i64 } /// Same as [replacement_excess](Self::replacement_excess) except the replacement fee /// is calculated using weight units directly without any conversion to vbytes. pub fn replacement_excess_wu(&self, target: Target, drain: Drain) -> i64 { - let mut replacement_excess_needed = 0; - if let Some(replace) = target.fee.replace { - replacement_excess_needed = - replace.min_fee_to_do_replacement_wu(self.weight(target.outputs, drain.weights)) - } self.selected_value() as i64 - target.value() as i64 - drain.value as i64 - - replacement_excess_needed as i64 + - self.implied_package_fee_from_replacement_wu(target, drain.weights) as i64 } /// The feerate the transaction would have if we were to use this selection of inputs to achieve @@ -272,37 +386,84 @@ impl<'a> CoinSelector<'a> { /// The fee the current selection and `drain_weight` should pay to satisfy `target_fee`. /// - /// This compares the fee calculated from the target feerate with the fee calculated from the - /// [`Replace`] constraints and returns the larger of the two. + /// This is the largest of the fees implied by `target.fee.rate`, `target.fee.absolute` and the + /// [`Replace`] constraints. The feerate and replacement fees include any [ancestor bump fee]; + /// `target.fee.absolute` is a minimum fee floor rather than an additive charge, so it does not. + /// + /// This is the exact counterpart of [`excess`](Self::excess): + /// `excess == selected_value - target.value() - drain.value - implied_fee`. /// /// `drain_weight` can be 0 to indicate no draining output. + /// + /// [ancestor bump fee]: Self::selected_ancestor_bump_fee pub fn implied_fee(&self, target: Target, drain_weights: DrainWeights) -> u64 { - let mut implied_fee = self - .implied_fee_from_feerate(target, drain_weights) - .max(target.fee.absolute); - - if let Some(replace) = target.fee.replace { - implied_fee = Ord::max( - implied_fee, - replace.min_fee_to_do_replacement(self.weight(target.outputs, drain_weights)), - ); - } - - implied_fee + self.implied_package_fee_from_feerate(target, drain_weights) + .max(target.fee.absolute) + .max(self.implied_package_fee_from_replacement(target, drain_weights)) } - fn implied_fee_from_feerate(&self, target: Target, drain_weights: DrainWeights) -> u64 { + /// The fee implied by `target.fee.rate` for the whole CPFP package — this transaction plus any + /// unconfirmed ancestors — i.e. the fee for the transaction's own weight plus the [ancestor + /// bump fee]. + /// + /// The bump is folded in here rather than at each call site because every caller needs it. + /// + /// [ancestor bump fee]: Self::selected_ancestor_bump_fee + fn implied_package_fee_from_feerate(&self, target: Target, drain_weights: DrainWeights) -> u64 { target .fee .rate .implied_fee(self.weight(target.outputs, drain_weights)) + + self.selected_ancestor_bump_fee(target.fee.rate) } - fn implied_fee_from_feerate_wu(&self, target: Target, drain_weights: DrainWeights) -> u64 { + /// Same as [`implied_package_fee_from_feerate`](Self::implied_package_fee_from_feerate) except `target.fee.rate` + /// is applied to weight units directly without any conversion to vbytes. + fn implied_package_fee_from_feerate_wu( + &self, + target: Target, + drain_weights: DrainWeights, + ) -> u64 { target .fee .rate .implied_fee_wu(self.weight(target.outputs, drain_weights)) + + self.selected_ancestor_bump_fee(target.fee.rate) + } + + /// The fee needed for the whole CPFP package to satisfy RBF's rule 4, i.e. the replacement fee + /// plus the [ancestor bump fee]. No replacement (`target.fee.replace` is `None`) still leaves + /// the bump to pay. + /// + /// [ancestor bump fee]: Self::selected_ancestor_bump_fee + fn implied_package_fee_from_replacement( + &self, + target: Target, + drain_weights: DrainWeights, + ) -> u64 { + let replacement_fee = match target.fee.replace { + Some(replace) => { + replace.min_fee_to_do_replacement(self.weight(target.outputs, drain_weights)) + } + None => 0, + }; + replacement_fee + self.selected_ancestor_bump_fee(target.fee.rate) + } + + /// Same as [`implied_package_fee_from_replacement`](Self::implied_package_fee_from_replacement) except the + /// replacement fee is calculated using weight units directly without any conversion to vbytes. + fn implied_package_fee_from_replacement_wu( + &self, + target: Target, + drain_weights: DrainWeights, + ) -> u64 { + let replacement_fee = match target.fee.replace { + Some(replace) => { + replace.min_fee_to_do_replacement_wu(self.weight(target.outputs, drain_weights)) + } + None => 0, + }; + replacement_fee + self.selected_ancestor_bump_fee(target.fee.rate) } /// The actual fee the selection would pay if it was used in a transaction that had @@ -314,8 +475,11 @@ impl<'a> CoinSelector<'a> { } /// The value of the current selected inputs minus the fee needed to pay for the selected inputs + /// and any ancestor bump fee. pub fn effective_value(&self, feerate: FeeRate) -> i64 { - self.selected_value() as i64 - (self.input_weight() as f32 * feerate.spwu()).ceil() as i64 + self.selected_value() as i64 + - (self.input_weight() as f32 * feerate.spwu()).ceil() as i64 + - self.selected_ancestor_bump_fee(feerate) as i64 } // /// Waste sum of all selected inputs. diff --git a/src/metrics/changeless.rs b/src/metrics/changeless.rs index a9c9e32..9ad299c 100644 --- a/src/metrics/changeless.rs +++ b/src/metrics/changeless.rs @@ -25,6 +25,10 @@ impl Changeless { /// NOTE: this relies on candidates being sorted so that all negative effective value candidates /// are next to each other, which [`requires_ordering_by_descending_value_pwu`] guarantees. /// + /// NOTE: it also needs `Candidate::effective_value` to tell the whole story about how much a + /// candidate lowers the excess, which holds only because `CoinSelector::with_ancestors` bans + /// candidates with unconfirmed ancestors. See its docs. + /// /// [`requires_ordering_by_descending_value_pwu`]: BnbMetric::requires_ordering_by_descending_value_pwu fn change_unavoidable(&mut self, cs: &CoinSelector<'_>, target: Target) -> bool { if self.0.drain(cs, target).is_none() { diff --git a/src/metrics/lowest_fee.rs b/src/metrics/lowest_fee.rs index 5499777..e20c723 100644 --- a/src/metrics/lowest_fee.rs +++ b/src/metrics/lowest_fee.rs @@ -146,6 +146,10 @@ impl BnbMetric for LowestFee { // `drain_value`, where `change_value` is `excess_with_drain_weight` and `spend_fee` is // `drain_spend_cost`). With `v >= 0` the difference is strictly positive: B always // costs more. + // + // NOTE: this needs the ancestor bump fee to cancel between A and B, which holds only + // because `CoinSelector::with_ancestors` bans candidates with unconfirmed ancestors. + // See its docs. if self.drain_value(cs, target).is_none() { // But a descendant might *add* a change output that improves the metric. This // happens when the current selection is changeless only because the change would be @@ -180,6 +184,10 @@ impl BnbMetric for LowestFee { Some(current_score) } else { // Step 1: select everything up until the input that hits the target. + // + // NOTE: this prices a greedy *prefix* that descendants need not select, so it is a + // lower bound only while the ancestor bump fee is the same for both. See + // `CoinSelector::with_ancestors`. let (mut cs, resize_index, to_resize) = cs .clone() .select_iter() diff --git a/tests/ancestor_aware.rs b/tests/ancestor_aware.rs new file mode 100644 index 0000000..4fadcea --- /dev/null +++ b/tests/ancestor_aware.rs @@ -0,0 +1,527 @@ +use bdk_coin_select::{ + Candidate, CoinSelector, Drain, DrainWeights, FeeRate, Replace, Target, TargetFee, + TargetOutputs, UnconfirmedAncestor, TR_KEYSPEND_TXIN_WEIGHT, +}; + +fn simple_target(feerate: f32) -> Target { + Target { + outputs: TargetOutputs { + value_sum: 100_000, + weight_sum: 200, + n_outputs: 1, + }, + fee: TargetFee::from_feerate(FeeRate::from_sat_per_vb(feerate)), + max_weight: None, + } +} + +#[test] +fn zero_ancestors_backward_compatible() { + let candidates = [Candidate { + input_count: 1, + value: 200_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }]; + + let mut cs = CoinSelector::new(&candidates); + cs.select(0); + + assert_eq!( + cs.selected_ancestor_bump_fee(FeeRate::from_sat_per_vb(10.0)), + 0 + ); + + let target = simple_target(10.0); + let excess_no_ancestors = cs.excess(target, Drain::NONE); + assert!( + excess_no_ancestors > 0, + "should meet target without ancestors" + ); +} + +#[test] +fn single_ancestor_reduces_excess() { + // Ancestor: 400 wu, paid 10 sats (very low feerate) + let ancestors = [UnconfirmedAncestor { + weight: 400, + fee_paid: 10, + dependent_candidates: vec![0], + }]; + + let candidates = [Candidate { + input_count: 1, + value: 200_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }]; + + let feerate = FeeRate::from_sat_per_vb(10.0); + let target = simple_target(10.0); + + // Without ancestors + let mut cs_no_anc = CoinSelector::new(&candidates); + cs_no_anc.select(0); + let excess_no_anc = cs_no_anc.excess(target, Drain::NONE); + + // With ancestors + let mut cs_with_anc = CoinSelector::new(&candidates).with_ancestors(&ancestors); + cs_with_anc.select(0); + + let bump_fee = cs_with_anc.selected_ancestor_bump_fee(feerate); + assert!(bump_fee > 0, "ancestor should need bumping"); + + let excess_with_anc = cs_with_anc.excess(target, Drain::NONE); + assert!( + excess_with_anc < excess_no_anc, + "ancestor bump fee should reduce excess: {} < {}", + excess_with_anc, + excess_no_anc + ); + assert_eq!( + excess_no_anc - excess_with_anc, + bump_fee as i64, + "excess difference should equal bump fee" + ); +} + +#[test] +fn shared_ancestors_are_deduplicated() { + // Both candidates share the same ancestor + let ancestors = [UnconfirmedAncestor { + weight: 400, + fee_paid: 10, + dependent_candidates: vec![0, 1], + }]; + + let candidates = [ + Candidate { + input_count: 1, + value: 100_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }, + Candidate { + input_count: 1, + value: 100_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }, + ]; + + let feerate = FeeRate::from_sat_per_vb(10.0); + + // Select only candidate 0 + let mut cs_one = CoinSelector::new(&candidates).with_ancestors(&ancestors); + cs_one.select(0); + let bump_one = cs_one.selected_ancestor_bump_fee(feerate); + + // Select both candidates + let mut cs_both = CoinSelector::new(&candidates).with_ancestors(&ancestors); + cs_both.select(0); + cs_both.select(1); + let bump_both = cs_both.selected_ancestor_bump_fee(feerate); + + // The bump fee should be the SAME because the ancestor is shared (deduplicated) + assert_eq!( + bump_one, bump_both, + "shared ancestor should only be counted once: one={} both={}", + bump_one, bump_both + ); +} + +#[test] +fn high_feerate_ancestor_subsidizes_low_feerate() { + // Two ancestors: one overpaid, one underpaid + // At package level, the overpayment subsidizes the underpayment + let ancestors = [ + UnconfirmedAncestor { + weight: 400, + fee_paid: 10, // very low fee + dependent_candidates: vec![0], + }, + UnconfirmedAncestor { + weight: 400, + fee_paid: 10_000, // very high fee + dependent_candidates: vec![0], + }, + ]; + + let candidates = [Candidate { + input_count: 1, + value: 200_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }]; + + let feerate = FeeRate::from_sat_per_vb(10.0); + + let mut cs = CoinSelector::new(&candidates).with_ancestors(&ancestors); + cs.select(0); + + let bump = cs.selected_ancestor_bump_fee(feerate); + + // Package: total_weight = 800, total_fee_paid = 10_010 + // implied_fee at 10 sat/vb = ceil(800/4) * 10 = 2000 sats + // bump = max(0, 2000 - 10_010) = 0 + assert_eq!( + bump, 0, + "high-feerate ancestor should subsidize low-feerate ancestor in the package" + ); +} + +#[test] +fn ancestor_package_above_target_contributes_zero_bump() { + let ancestors = [UnconfirmedAncestor { + weight: 400, + fee_paid: 10_000, // way above any reasonable feerate + dependent_candidates: vec![0], + }]; + + let candidates = [Candidate { + input_count: 1, + value: 200_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }]; + + let feerate = FeeRate::from_sat_per_vb(10.0); + + let mut cs = CoinSelector::new(&candidates).with_ancestors(&ancestors); + cs.select(0); + + assert_eq!( + cs.selected_ancestor_bump_fee(feerate), + 0, + "ancestor already above target feerate should contribute zero bump" + ); +} + +#[test] +fn different_feerates_produce_different_bump_fees() { + let ancestors = [UnconfirmedAncestor { + weight: 400, + fee_paid: 100, // 1 sat/vb + dependent_candidates: vec![0], + }]; + + let candidates = [Candidate { + input_count: 1, + value: 200_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }]; + + let mut cs = CoinSelector::new(&candidates).with_ancestors(&ancestors); + cs.select(0); + + let bump_low = cs.selected_ancestor_bump_fee(FeeRate::from_sat_per_vb(5.0)); + let bump_high = cs.selected_ancestor_bump_fee(FeeRate::from_sat_per_vb(20.0)); + + assert!( + bump_high > bump_low, + "higher feerate should produce larger bump fee: high={} low={}", + bump_high, + bump_low + ); +} + +#[test] +fn effective_value_includes_ancestor_bump() { + let ancestors = [UnconfirmedAncestor { + weight: 400, + fee_paid: 10, + dependent_candidates: vec![0], + }]; + + let candidates = [Candidate { + input_count: 1, + value: 200_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }]; + + let feerate = FeeRate::from_sat_per_vb(10.0); + + let mut cs_no_anc = CoinSelector::new(&candidates); + cs_no_anc.select(0); + let ev_no_anc = cs_no_anc.effective_value(feerate); + + let mut cs_with_anc = CoinSelector::new(&candidates).with_ancestors(&ancestors); + cs_with_anc.select(0); + let ev_with_anc = cs_with_anc.effective_value(feerate); + + let bump = cs_with_anc.selected_ancestor_bump_fee(feerate); + assert!(bump > 0); + assert_eq!( + ev_no_anc - ev_with_anc, + bump as i64, + "effective value difference should equal bump fee" + ); +} + +/// `implied_fee` is the exact counterpart of `excess`, so it must carry the ancestor bump. A +/// wallet sizing its change output from `implied_fee` would otherwise underpay the package by +/// exactly the bump amount. +#[test] +fn implied_fee_includes_ancestor_bump() { + let candidates = [Candidate { + input_count: 1, + value: 200_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }]; + // 400 wu = 100 vb, so the ancestor owes 100 * 10 = 1000 sats but paid 10 => 990 sat bump. + let ancestors = [UnconfirmedAncestor { + weight: 400, + fee_paid: 10, + dependent_candidates: vec![0], + }]; + let target = simple_target(10.0); + + let mut without = CoinSelector::new(&candidates); + without.select(0); + let mut with = CoinSelector::new(&candidates).with_ancestors(&ancestors); + with.select(0); + + assert_eq!( + with.selected_ancestor_bump_fee(FeeRate::from_sat_per_vb(10.0)), + 990 + ); + assert_eq!( + with.implied_fee(target, DrainWeights::NONE) + - without.implied_fee(target, DrainWeights::NONE), + 990, + "implied_fee must carry the ancestor bump" + ); +} + +/// `excess == selected_value - target.value() - drain.value - implied_fee` must hold for every +/// combination of fee constraints. This pins the whole `*_excess` family against `implied_fee`, so +/// a bump added to one but not the other cannot go unnoticed. +#[test] +fn excess_and_implied_fee_agree() { + let candidates = [ + Candidate { + input_count: 1, + value: 200_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }, + Candidate { + input_count: 1, + value: 50_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }, + ]; + + let ancestor_sets: [Vec; 3] = [ + // no ancestors at all: the identity must hold on the pre-existing code paths too + vec![], + // one underpaying ancestor + vec![UnconfirmedAncestor { + weight: 400, + fee_paid: 10, + dependent_candidates: vec![0], + }], + // an underpaying ancestor plus an overpaying one that subsidizes it + vec![ + UnconfirmedAncestor { + weight: 400, + fee_paid: 10, + dependent_candidates: vec![0], + }, + UnconfirmedAncestor { + weight: 1_000, + fee_paid: 100_000, + dependent_candidates: vec![1], + }, + ], + ]; + + for ancestors in &ancestor_sets { + for absolute in [0_u64, 5_000, 500_000] { + for replace in [None, Some(Replace::new(1_000))] { + for feerate in [1.0_f32, 10.0, 50.0] { + for drain in [ + Drain::NONE, + Drain { + weights: DrainWeights::TR_KEYSPEND, + value: 20_000, + }, + ] { + let target = Target { + outputs: TargetOutputs { + value_sum: 100_000, + weight_sum: 200, + n_outputs: 1, + }, + fee: TargetFee { + rate: FeeRate::from_sat_per_vb(feerate), + replace, + absolute, + }, + max_weight: None, + }; + + for selection in [vec![], vec![0], vec![1], vec![0, 1]] { + let mut cs = CoinSelector::new(&candidates).with_ancestors(ancestors); + for i in &selection { + cs.select(*i); + } + + assert_eq!( + cs.excess(target, drain), + cs.selected_value() as i64 + - target.value() as i64 + - drain.value as i64 + - cs.implied_fee(target, drain.weights) as i64, + "identity broken: n_ancestors={} absolute={} replace={} \ + feerate={} drain={} selection={:?}", + ancestors.len(), + absolute, + replace.is_some(), + feerate, + drain.value, + selection, + ); + } + } + } + } + } + } +} + +/// Candidates with unconfirmed ancestors are banned from the automatic selection algorithms, +/// because the bump fee is a package-level property that per-candidate ranking cannot see. +#[test] +fn candidates_with_ancestors_are_banned_from_automatic_selection() { + let candidates = [ + // depends on an expensive unconfirmed ancestor + Candidate { + input_count: 1, + value: 200_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }, + // ordinary confirmed candidate + Candidate { + input_count: 1, + value: 200_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }, + ]; + let ancestors = [UnconfirmedAncestor { + weight: 400, + fee_paid: 10, + dependent_candidates: vec![0], + }]; + + let cs = CoinSelector::new(&candidates).with_ancestors(&ancestors); + + assert!(cs.banned().contains(0), "ancestor candidate must be banned"); + assert!(!cs.banned().contains(1), "candidate 1 has no ancestors"); + assert_eq!( + cs.unselected_indices().collect::>(), + vec![1], + "only the ancestor-free candidate is reachable" + ); + + // A greedy selection must fund itself from candidate 1 alone. + let mut greedy = cs.clone(); + greedy + .select_until_target_met(simple_target(10.0)) + .expect("candidate 1 alone covers the target"); + assert!(!greedy.is_selected(0)); + assert!(greedy.is_selected(1)); + assert_eq!( + greedy.selected_ancestor_bump_fee(FeeRate::from_sat_per_vb(10.0)), + 0, + "no ancestor is reachable, so the bump stays zero" + ); +} + +/// Banning is only about *automatic* selection — the caller can still spend an unconfirmed UTXO +/// explicitly, which is the actual CPFP flow, and the bump is priced when they do. +#[test] +fn banned_ancestor_candidates_remain_manually_selectable() { + let candidates = [Candidate { + input_count: 1, + value: 200_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }]; + let ancestors = [UnconfirmedAncestor { + weight: 400, + fee_paid: 10, + dependent_candidates: vec![0], + }]; + + let mut cs = CoinSelector::new(&candidates).with_ancestors(&ancestors); + assert!(cs.banned().contains(0)); + + assert!(cs.select(0), "ban must not block manual selection"); + assert!(cs.is_selected(0)); + assert_eq!( + cs.selected_ancestor_bump_fee(FeeRate::from_sat_per_vb(10.0)), + 990, + "the package is still priced once the candidate is selected" + ); +} + +/// `candidates_with_ancestors` reports each affected candidate once even when several ancestors +/// name it, and excludes candidates the caller banned for their own reasons. +#[test] +fn candidates_with_ancestors_is_deduplicated_and_distinct_from_banned() { + let candidates = [ + Candidate { + input_count: 1, + value: 100_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }, + Candidate { + input_count: 1, + value: 100_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }, + Candidate { + input_count: 1, + value: 100_000, + weight: TR_KEYSPEND_TXIN_WEIGHT, + is_segwit: true, + }, + ]; + // candidate 0 is named by both ancestors; candidate 1 by one of them + let ancestors = [ + UnconfirmedAncestor { + weight: 400, + fee_paid: 10, + dependent_candidates: vec![0, 1], + }, + UnconfirmedAncestor { + weight: 400, + fee_paid: 20, + dependent_candidates: vec![0], + }, + ]; + + let mut cs = CoinSelector::new(&candidates).with_ancestors(&ancestors); + // a ban of the caller's own, unrelated to ancestors + cs.ban(2); + + assert_eq!( + cs.candidates_with_ancestors().collect::>(), + vec![0, 1], + "each affected candidate reported once, and candidate 2 is not one of them" + ); + assert_eq!( + cs.banned().iter().collect::>(), + vec![0, 1, 2], + "banned() also carries the caller's own ban" + ); +}