Skip to content

Commit ce09897

Browse files
evanlinjinclaude
andcommitted
Add ancestor-aware coin selection for CPFP bump fee calculation
When spending unconfirmed UTXOs, miners evaluate the transaction as a package with its ancestors. This adds support for computing the package-level bump fee needed to bring ancestors up to the target feerate, with automatic deduplication of shared ancestors across candidates. - Add UnconfirmedAncestor struct (weight + fee_paid) - Add ancestors field to Candidate (indices into shared ancestor slice) - Add with_ancestors() builder on CoinSelector - Add selected_ancestor_bump_fee() with package-level computation - Subtract ancestor bump fee from excess and effective_value methods Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a2c9abc commit ce09897

8 files changed

Lines changed: 378 additions & 29 deletions

File tree

README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ let target = Target {
2929

3030
let candidates = vec![
3131
Candidate {
32-
// How many inputs does this candidate represents. Needed so we can
32+
// How many inputs does this candidate represents. Needed so we can
3333
// figure out the weight of the varint that encodes the number of inputs
3434
input_count: 1,
3535
// the value of the input
@@ -39,15 +39,18 @@ let candidates = vec![
3939
weight: TR_KEYSPEND_TXIN_WEIGHT,
4040
// wether it's a segwit input. Needed so we know whether to include the
4141
// segwit header in total weight calculations.
42-
is_segwit: true
42+
is_segwit: true,
43+
// indices into the shared ancestor slice (empty = confirmed or no ancestors)
44+
ancestors: vec![],
4345
},
4446
Candidate {
45-
// A candidate can represent multiple inputs in the case where you
47+
// A candidate can represent multiple inputs in the case where you
4648
// always want some inputs to be spent together.
4749
input_count: 2,
4850
weight: 2*TR_KEYSPEND_TXIN_WEIGHT,
4951
value: 3_000_000,
50-
is_segwit: true
52+
is_segwit: true,
53+
ancestors: vec![],
5154
}
5255
];
5356

@@ -106,19 +109,22 @@ let candidates = [
106109
input_count: 1,
107110
value: 400_000,
108111
weight: TR_KEYSPEND_TXIN_WEIGHT,
109-
is_segwit: true
112+
is_segwit: true,
113+
ancestors: vec![],
110114
},
111115
Candidate {
112116
input_count: 1,
113117
value: 200_000,
114118
weight: TR_KEYSPEND_TXIN_WEIGHT,
115-
is_segwit: true
119+
is_segwit: true,
120+
ancestors: vec![],
116121
},
117122
Candidate {
118123
input_count: 1,
119124
value: 11_000,
120125
weight: TR_KEYSPEND_TXIN_WEIGHT,
121-
is_segwit: true
126+
is_segwit: true,
127+
ancestors: vec![],
122128
}
123129
];
124130
let drain_weights = bdk_coin_select::DrainWeights::default();

src/coin_selector.rs

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,19 @@ use super::*;
22
#[allow(unused)] // some bug in <= 1.48.0 sees this as unused when it isn't
33
use crate::float::FloatExt;
44
use crate::{bnb::BnbMetric, float::Ordf32, ChangePolicy, FeeRate, Target};
5-
use alloc::{borrow::Cow, collections::BTreeSet};
5+
use alloc::{borrow::Cow, collections::BTreeSet, vec::Vec};
6+
7+
/// An unconfirmed ancestor transaction that may need a fee bump (CPFP).
8+
///
9+
/// When spending unconfirmed UTXOs, miners evaluate the transaction as a package with its
10+
/// unconfirmed ancestors. If ancestors paid below the target feerate, the child must overpay.
11+
#[derive(Debug, Clone, Copy)]
12+
pub struct UnconfirmedAncestor {
13+
/// The weight of the ancestor transaction in weight units.
14+
pub weight: u64,
15+
/// The fee already paid by the ancestor transaction in satoshis.
16+
pub fee_paid: u64,
17+
}
618

719
/// [`CoinSelector`] selects/deselects coins from a set of canididate coins.
820
///
@@ -14,6 +26,7 @@ use alloc::{borrow::Cow, collections::BTreeSet};
1426
#[derive(Debug, Clone)]
1527
pub struct CoinSelector<'a> {
1628
candidates: &'a [Candidate],
29+
ancestors: &'a [UnconfirmedAncestor],
1730
selected: Cow<'a, BTreeSet<usize>>,
1831
banned: Cow<'a, BTreeSet<usize>>,
1932
candidate_order: Cow<'a, [usize]>,
@@ -34,26 +47,35 @@ impl<'a> CoinSelector<'a> {
3447
pub fn new(candidates: &'a [Candidate]) -> Self {
3548
Self {
3649
candidates,
50+
ancestors: &[],
3751
selected: Cow::Owned(Default::default()),
3852
banned: Cow::Owned(Default::default()),
3953
candidate_order: Cow::Owned((0..candidates.len()).collect()),
4054
}
4155
}
4256

57+
/// Set the shared ancestor data for CPFP bump fee calculations.
58+
///
59+
/// Each [`Candidate`]'s `ancestors` field contains indices into this slice.
60+
pub fn with_ancestors(mut self, ancestors: &'a [UnconfirmedAncestor]) -> Self {
61+
self.ancestors = ancestors;
62+
self
63+
}
64+
4365
/// Iterate over all the candidates in their currently sorted order. Each item has the original
4466
/// index with the candidate.
4567
pub fn candidates(
4668
&self,
47-
) -> impl DoubleEndedIterator<Item = (usize, Candidate)> + ExactSizeIterator + '_ {
69+
) -> impl DoubleEndedIterator<Item = (usize, &Candidate)> + ExactSizeIterator + '_ {
4870
self.candidate_order
4971
.iter()
50-
.map(move |i| (*i, self.candidates[*i]))
72+
.map(move |i| (*i, &self.candidates[*i]))
5173
}
5274

5375
/// Get the candidate at `index`. `index` refers to its position in the original `candidates`
5476
/// slice passed into [`CoinSelector::new`].
55-
pub fn candidate(&self, index: usize) -> Candidate {
56-
self.candidates[index]
77+
pub fn candidate(&self, index: usize) -> &Candidate {
78+
&self.candidates[index]
5779
}
5880

5981
/// Deselect a candidate at `index`. `index` refers to its position in the original `candidates`
@@ -172,6 +194,36 @@ impl<'a> CoinSelector<'a> {
172194
+ target_ouputs.output_weight_with_drain(drain_weight)
173195
}
174196

197+
/// Compute the package-level ancestor bump fee for the current selection at the given feerate.
198+
///
199+
/// This collects unique ancestor indices across all selected candidates, sums their weights
200+
/// and fees, then computes `max(0, implied_fee(total_weight, feerate) - total_fees)`.
201+
///
202+
/// High-feerate ancestors subsidize low-feerate ones within the package (matching Bitcoin
203+
/// Core's package relay approach).
204+
pub fn selected_ancestor_bump_fee(&self, feerate: FeeRate) -> u64 {
205+
if self.ancestors.is_empty() {
206+
return 0;
207+
}
208+
let mut indices: Vec<usize> = self
209+
.selected
210+
.iter()
211+
.flat_map(|&i| self.candidates[i].ancestors.iter().copied())
212+
.collect();
213+
indices.sort_unstable();
214+
indices.dedup();
215+
216+
let mut total_weight = 0u64;
217+
let mut total_fee_paid = 0u64;
218+
for anc_index in indices {
219+
let anc = &self.ancestors[anc_index];
220+
total_weight += anc.weight;
221+
total_fee_paid += anc.fee_paid;
222+
}
223+
let implied = feerate.implied_fee(total_weight);
224+
implied.saturating_sub(total_fee_paid)
225+
}
226+
175227
/// How much the current selection overshoots the value needed to achieve `target`.
176228
///
177229
/// In order for the resulting transaction to be valid this must be 0 or above. If it's above 0
@@ -199,6 +251,7 @@ impl<'a> CoinSelector<'a> {
199251
- target.value() as i64
200252
- drain.value as i64
201253
- self.implied_fee_from_feerate(target, drain.weights) as i64
254+
- self.selected_ancestor_bump_fee(target.fee.rate) as i64
202255
}
203256

204257
/// Same as [rate_excess](Self::rate_excess) except `target.fee.rate` is applied to the
@@ -208,6 +261,7 @@ impl<'a> CoinSelector<'a> {
208261
- target.value() as i64
209262
- drain.value as i64
210263
- self.implied_fee_from_feerate_wu(target, drain.weights) as i64
264+
- self.selected_ancestor_bump_fee(target.fee.rate) as i64
211265
}
212266

213267
/// How much the current selection overshoots the value needed to satisfy `target.fee.absolute`
@@ -230,6 +284,7 @@ impl<'a> CoinSelector<'a> {
230284
- target.value() as i64
231285
- drain.value as i64
232286
- replacement_excess_needed as i64
287+
- self.selected_ancestor_bump_fee(target.fee.rate) as i64
233288
}
234289

235290
/// Same as [replacement_excess](Self::replacement_excess) except the replacement fee
@@ -244,6 +299,7 @@ impl<'a> CoinSelector<'a> {
244299
- target.value() as i64
245300
- drain.value as i64
246301
- replacement_excess_needed as i64
302+
- self.selected_ancestor_bump_fee(target.fee.rate) as i64
247303
}
248304

249305
/// The feerate the transaction would have if we were to use this selection of inputs to achieve
@@ -304,8 +360,11 @@ impl<'a> CoinSelector<'a> {
304360
}
305361

306362
/// The value of the current selected inputs minus the fee needed to pay for the selected inputs
363+
/// and any ancestor bump fee.
307364
pub fn effective_value(&self, feerate: FeeRate) -> i64 {
308-
self.selected_value() as i64 - (self.input_weight() as f32 * feerate.spwu()).ceil() as i64
365+
self.selected_value() as i64
366+
- (self.input_weight() as f32 * feerate.spwu()).ceil() as i64
367+
- self.selected_ancestor_bump_fee(feerate) as i64
309368
}
310369

311370
// /// Waste sum of all selected inputs.
@@ -324,11 +383,11 @@ impl<'a> CoinSelector<'a> {
324383
/// [`unselected`]: CoinSelector::unselected
325384
pub fn sort_candidates_by<F>(&mut self, mut cmp: F)
326385
where
327-
F: FnMut((usize, Candidate), (usize, Candidate)) -> core::cmp::Ordering,
386+
F: FnMut((usize, &Candidate), (usize, &Candidate)) -> core::cmp::Ordering,
328387
{
329388
let order = self.candidate_order.to_mut();
330389
let candidates = &self.candidates;
331-
order.sort_by(|a, b| cmp((*a, candidates[*a]), (*b, candidates[*b])))
390+
order.sort_by(|a, b| cmp((*a, &candidates[*a]), (*b, &candidates[*b])))
332391
}
333392

334393
/// Sorts the candidates by the key function.
@@ -342,10 +401,10 @@ impl<'a> CoinSelector<'a> {
342401
/// [`unselected`]: CoinSelector::unselected
343402
pub fn sort_candidates_by_key<F, K>(&mut self, mut key_fn: F)
344403
where
345-
F: FnMut((usize, Candidate)) -> K,
404+
F: FnMut((usize, &Candidate)) -> K,
346405
K: Ord,
347406
{
348-
self.sort_candidates_by(|a, b| key_fn(a).cmp(&key_fn(b)))
407+
self.sort_candidates_by(|a, b| key_fn(a).cmp(&key_fn(b)));
349408
}
350409

351410
/// Sorts the candidates by descending value per weight unit, tie-breaking with value.
@@ -391,20 +450,20 @@ impl<'a> CoinSelector<'a> {
391450
/// The selected candidates with their index.
392451
pub fn selected(
393452
&self,
394-
) -> impl ExactSizeIterator<Item = (usize, Candidate)> + DoubleEndedIterator + '_ {
453+
) -> impl ExactSizeIterator<Item = (usize, &Candidate)> + DoubleEndedIterator + '_ {
395454
self.selected
396455
.iter()
397-
.map(move |&index| (index, self.candidates[index]))
456+
.map(move |&index| (index, &self.candidates[index]))
398457
}
399458

400459
/// The unselected candidates with their index.
401460
///
402461
/// The candidates are returned in sorted order. See [`sort_candidates_by`].
403462
///
404463
/// [`sort_candidates_by`]: Self::sort_candidates_by
405-
pub fn unselected(&self) -> impl DoubleEndedIterator<Item = (usize, Candidate)> + '_ {
464+
pub fn unselected(&self) -> impl DoubleEndedIterator<Item = (usize, &Candidate)> + '_ {
406465
self.unselected_indices()
407-
.map(move |i| (i, self.candidates[i]))
466+
.map(move |i| (i, &self.candidates[i]))
408467
}
409468

410469
/// The indices of the selelcted candidates.
@@ -624,20 +683,23 @@ pub struct SelectIter<'a> {
624683
}
625684

626685
impl<'a> Iterator for SelectIter<'a> {
627-
type Item = (CoinSelector<'a>, usize, Candidate);
686+
type Item = (CoinSelector<'a>, usize, &'a Candidate);
628687

629688
fn next(&mut self) -> Option<Self::Item> {
630-
let (index, wv) = self.cs.unselected().next()?;
689+
let index = self.cs.unselected_indices().next()?;
690+
// Access the underlying slice directly to get the `'a` lifetime.
691+
let candidates: &'a [Candidate] = self.cs.candidates;
631692
self.cs.select(index);
632-
Some((self.cs.clone(), index, wv))
693+
Some((self.cs.clone(), index, &candidates[index]))
633694
}
634695
}
635696

636-
impl DoubleEndedIterator for SelectIter<'_> {
697+
impl<'a> DoubleEndedIterator for SelectIter<'a> {
637698
fn next_back(&mut self) -> Option<Self::Item> {
638-
let (index, wv) = self.cs.unselected().next_back()?;
699+
let index = self.cs.unselected_indices().next_back()?;
700+
let candidates: &'a [Candidate] = self.cs.candidates;
639701
self.cs.select(index);
640-
Some((self.cs.clone(), index, wv))
702+
Some((self.cs.clone(), index, &candidates[index]))
641703
}
642704
}
643705

@@ -682,7 +744,7 @@ impl std::error::Error for NoBnbSolution {}
682744
/// A `Candidate` represents an input candidate for [`CoinSelector`].
683745
///
684746
/// This can either be a single UTXO, or a group of UTXOs that should be spent together.
685-
#[derive(Debug, Clone, Copy)]
747+
#[derive(Debug, Clone)]
686748
pub struct Candidate {
687749
/// Total value of the UTXO(s) that this [`Candidate`] represents.
688750
pub value: u64,
@@ -694,6 +756,12 @@ pub struct Candidate {
694756
pub input_count: usize,
695757
/// Whether this [`Candidate`] contains at least one segwit spend.
696758
pub is_segwit: bool,
759+
/// Indices into the shared [`UnconfirmedAncestor`] slice (passed to
760+
/// [`CoinSelector::with_ancestors`]) that this candidate depends on.
761+
///
762+
/// When multiple candidates share ancestors, those ancestors are automatically deduplicated
763+
/// during bump fee computation.
764+
pub ancestors: Vec<usize>,
697765
}
698766

699767
impl Candidate {
@@ -707,13 +775,14 @@ impl Candidate {
707775
///
708776
/// `satisfaction_weight` is the weight of `scriptSigLen + scriptSig + scriptWitnessLen +
709777
/// scriptWitness`.
710-
pub fn new(value: u64, satisfaction_weight: u64, is_segwit: bool) -> Candidate {
778+
pub fn new(value: u64, satisfaction_weight: u64, is_segwit: bool) -> Self {
711779
let weight = TXIN_BASE_WEIGHT + satisfaction_weight;
712780
Candidate {
713781
value,
714782
weight,
715783
input_count: 1,
716784
is_segwit,
785+
ancestors: Vec::new(),
717786
}
718787
}
719788

0 commit comments

Comments
 (0)