Skip to content

Commit d168359

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 a077ec9 commit d168359

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
@@ -198,6 +250,7 @@ impl<'a> CoinSelector<'a> {
198250
- target.value() as i64
199251
- drain.value as i64
200252
- self.implied_fee_from_feerate(target, drain.weights) as i64
253+
- self.selected_ancestor_bump_fee(target.fee.rate) as i64
201254
}
202255

203256
/// Same as [rate_excess](Self::rate_excess) except `target.fee.rate` is applied to the
@@ -207,6 +260,7 @@ impl<'a> CoinSelector<'a> {
207260
- target.value() as i64
208261
- drain.value as i64
209262
- self.implied_fee_from_feerate_wu(target, drain.weights) as i64
263+
- self.selected_ancestor_bump_fee(target.fee.rate) as i64
210264
}
211265

212266
/// How much the current selection overshoots the value needed to satisfy RBF's rule 4.
@@ -220,6 +274,7 @@ impl<'a> CoinSelector<'a> {
220274
- target.value() as i64
221275
- drain.value as i64
222276
- replacement_excess_needed as i64
277+
- self.selected_ancestor_bump_fee(target.fee.rate) as i64
223278
}
224279

225280
/// Same as [replacement_excess](Self::replacement_excess) except the replacement fee
@@ -234,6 +289,7 @@ impl<'a> CoinSelector<'a> {
234289
- target.value() as i64
235290
- drain.value as i64
236291
- replacement_excess_needed as i64
292+
- self.selected_ancestor_bump_fee(target.fee.rate) as i64
237293
}
238294

239295
/// The feerate the transaction would have if we were to use this selection of inputs to achieve
@@ -292,8 +348,11 @@ impl<'a> CoinSelector<'a> {
292348
}
293349

294350
/// The value of the current selected inputs minus the fee needed to pay for the selected inputs
351+
/// and any ancestor bump fee.
295352
pub fn effective_value(&self, feerate: FeeRate) -> i64 {
296-
self.selected_value() as i64 - (self.input_weight() as f32 * feerate.spwu()).ceil() as i64
353+
self.selected_value() as i64
354+
- (self.input_weight() as f32 * feerate.spwu()).ceil() as i64
355+
- self.selected_ancestor_bump_fee(feerate) as i64
297356
}
298357

299358
// /// Waste sum of all selected inputs.
@@ -312,11 +371,11 @@ impl<'a> CoinSelector<'a> {
312371
/// [`unselected`]: CoinSelector::unselected
313372
pub fn sort_candidates_by<F>(&mut self, mut cmp: F)
314373
where
315-
F: FnMut((usize, Candidate), (usize, Candidate)) -> core::cmp::Ordering,
374+
F: FnMut((usize, &Candidate), (usize, &Candidate)) -> core::cmp::Ordering,
316375
{
317376
let order = self.candidate_order.to_mut();
318377
let candidates = &self.candidates;
319-
order.sort_by(|a, b| cmp((*a, candidates[*a]), (*b, candidates[*b])))
378+
order.sort_by(|a, b| cmp((*a, &candidates[*a]), (*b, &candidates[*b])))
320379
}
321380

322381
/// Sorts the candidates by the key function.
@@ -330,10 +389,10 @@ impl<'a> CoinSelector<'a> {
330389
/// [`unselected`]: CoinSelector::unselected
331390
pub fn sort_candidates_by_key<F, K>(&mut self, mut key_fn: F)
332391
where
333-
F: FnMut((usize, Candidate)) -> K,
392+
F: FnMut((usize, &Candidate)) -> K,
334393
K: Ord,
335394
{
336-
self.sort_candidates_by(|a, b| key_fn(a).cmp(&key_fn(b)))
395+
self.sort_candidates_by(|a, b| key_fn(a).cmp(&key_fn(b)));
337396
}
338397

339398
/// Sorts the candidates by descending value per weight unit, tie-breaking with value.
@@ -379,20 +438,20 @@ impl<'a> CoinSelector<'a> {
379438
/// The selected candidates with their index.
380439
pub fn selected(
381440
&self,
382-
) -> impl ExactSizeIterator<Item = (usize, Candidate)> + DoubleEndedIterator + '_ {
441+
) -> impl ExactSizeIterator<Item = (usize, &Candidate)> + DoubleEndedIterator + '_ {
383442
self.selected
384443
.iter()
385-
.map(move |&index| (index, self.candidates[index]))
444+
.map(move |&index| (index, &self.candidates[index]))
386445
}
387446

388447
/// The unselected candidates with their index.
389448
///
390449
/// The candidates are returned in sorted order. See [`sort_candidates_by`].
391450
///
392451
/// [`sort_candidates_by`]: Self::sort_candidates_by
393-
pub fn unselected(&self) -> impl DoubleEndedIterator<Item = (usize, Candidate)> + '_ {
452+
pub fn unselected(&self) -> impl DoubleEndedIterator<Item = (usize, &Candidate)> + '_ {
394453
self.unselected_indices()
395-
.map(move |i| (i, self.candidates[i]))
454+
.map(move |i| (i, &self.candidates[i]))
396455
}
397456

398457
/// The indices of the selelcted candidates.
@@ -612,20 +671,23 @@ pub struct SelectIter<'a> {
612671
}
613672

614673
impl<'a> Iterator for SelectIter<'a> {
615-
type Item = (CoinSelector<'a>, usize, Candidate);
674+
type Item = (CoinSelector<'a>, usize, &'a Candidate);
616675

617676
fn next(&mut self) -> Option<Self::Item> {
618-
let (index, wv) = self.cs.unselected().next()?;
677+
let index = self.cs.unselected_indices().next()?;
678+
// Access the underlying slice directly to get the `'a` lifetime.
679+
let candidates: &'a [Candidate] = self.cs.candidates;
619680
self.cs.select(index);
620-
Some((self.cs.clone(), index, wv))
681+
Some((self.cs.clone(), index, &candidates[index]))
621682
}
622683
}
623684

624-
impl DoubleEndedIterator for SelectIter<'_> {
685+
impl<'a> DoubleEndedIterator for SelectIter<'a> {
625686
fn next_back(&mut self) -> Option<Self::Item> {
626-
let (index, wv) = self.cs.unselected().next_back()?;
687+
let index = self.cs.unselected_indices().next_back()?;
688+
let candidates: &'a [Candidate] = self.cs.candidates;
627689
self.cs.select(index);
628-
Some((self.cs.clone(), index, wv))
690+
Some((self.cs.clone(), index, &candidates[index]))
629691
}
630692
}
631693

@@ -670,7 +732,7 @@ impl std::error::Error for NoBnbSolution {}
670732
/// A `Candidate` represents an input candidate for [`CoinSelector`].
671733
///
672734
/// This can either be a single UTXO, or a group of UTXOs that should be spent together.
673-
#[derive(Debug, Clone, Copy)]
735+
#[derive(Debug, Clone)]
674736
pub struct Candidate {
675737
/// Total value of the UTXO(s) that this [`Candidate`] represents.
676738
pub value: u64,
@@ -682,6 +744,12 @@ pub struct Candidate {
682744
pub input_count: usize,
683745
/// Whether this [`Candidate`] contains at least one segwit spend.
684746
pub is_segwit: bool,
747+
/// Indices into the shared [`UnconfirmedAncestor`] slice (passed to
748+
/// [`CoinSelector::with_ancestors`]) that this candidate depends on.
749+
///
750+
/// When multiple candidates share ancestors, those ancestors are automatically deduplicated
751+
/// during bump fee computation.
752+
pub ancestors: Vec<usize>,
685753
}
686754

687755
impl Candidate {
@@ -695,13 +763,14 @@ impl Candidate {
695763
///
696764
/// `satisfaction_weight` is the weight of `scriptSigLen + scriptSig + scriptWitnessLen +
697765
/// scriptWitness`.
698-
pub fn new(value: u64, satisfaction_weight: u64, is_segwit: bool) -> Candidate {
766+
pub fn new(value: u64, satisfaction_weight: u64, is_segwit: bool) -> Self {
699767
let weight = TXIN_BASE_WEIGHT + satisfaction_weight;
700768
Candidate {
701769
value,
702770
weight,
703771
input_count: 1,
704772
is_segwit,
773+
ancestors: Vec::new(),
705774
}
706775
}
707776

0 commit comments

Comments
 (0)