Skip to content

Commit ee587da

Browse files
evanlinjinclaude
andcommitted
feat: Integrate package context into fee and weight calculations
When a Package is set via with_package(), all fee and weight calculations now automatically include parent transaction context: - weight() returns child_weight + parent_weight - fee() returns child_fee + parent_fee - implied_feerate() returns package feerate New methods for when child-only values are needed: - weight_without_package() - fee_without_package() - package() accessor RBF calculations (replacement_excess) use child weight only since Bitcoin's RBF rule 4 applies to the replacing transaction, not the package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a66cf82 commit ee587da

4 files changed

Lines changed: 140 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
# Unreleased
2+
3+
- Add package-aware coin selection for CPFP (Child Pays for Parent) scenarios
4+
- New `Package` struct to specify parent transaction fee and weight
5+
- `CoinSelector::with_package()` to configure package context and auto-select linking inputs
6+
- `weight()` and `fee()` now include parent context when package is set
7+
- New `weight_without_package()` and `fee_without_package()` for child-only values
8+
- `implied_feerate()` returns package feerate when package is set
9+
- RBF calculations correctly use child weight only
10+
111
# 0.4.0
212

313
- Use `u64` for weights instead of u32

src/coin_selector.rs

Lines changed: 106 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub struct CoinSelector<'a> {
1717
selected: Cow<'a, BTreeSet<usize>>,
1818
banned: Cow<'a, BTreeSet<usize>>,
1919
candidate_order: Cow<'a, [usize]>,
20+
package: Option<Package>,
2021
}
2122

2223
impl<'a> CoinSelector<'a> {
@@ -37,9 +38,55 @@ impl<'a> CoinSelector<'a> {
3738
selected: Cow::Owned(Default::default()),
3839
banned: Cow::Owned(Default::default()),
3940
candidate_order: Cow::Owned((0..candidates.len()).collect()),
41+
package: None,
4042
}
4143
}
4244

45+
/// Configures package-aware coin selection for CPFP (Child Pays for Parent) scenarios.
46+
///
47+
/// When you need to bump the feerate of unconfirmed parent transactions, this method lets you
48+
/// specify the parent context and which candidates link to those parents.
49+
///
50+
/// `package` describes the aggregate fee and weight of the parent transactions.
51+
/// `link_indices` are the indices of candidates that spend outputs from the parent
52+
/// transactions. These will be automatically selected since they must be included to create
53+
/// the child relationship.
54+
///
55+
/// # Example
56+
///
57+
/// ```
58+
/// # use bdk_coin_select::*;
59+
/// // Parent tx has 200 sats fee and 400 weight units
60+
/// let package = Package { parent_fee: 200, parent_weight: 400 };
61+
///
62+
/// // Candidate at index 0 spends from the parent
63+
/// # let candidates = vec![Candidate::new_tr_keyspend(10_000)];
64+
/// let selector = CoinSelector::new(&candidates).with_package(package, [0]);
65+
///
66+
/// // Index 0 is now selected
67+
/// assert!(selector.is_selected(0));
68+
/// ```
69+
pub fn with_package(
70+
mut self,
71+
package: Package,
72+
link_indices: impl IntoIterator<Item = usize>,
73+
) -> Self {
74+
self.package = Some(package);
75+
for index in link_indices {
76+
self.select(index);
77+
}
78+
self
79+
}
80+
81+
/// Returns the package context if set.
82+
///
83+
/// See [`with_package`] for more information on package-aware coin selection.
84+
///
85+
/// [`with_package`]: Self::with_package
86+
pub fn package(&self) -> Option<Package> {
87+
self.package
88+
}
89+
4390
/// Iterate over all the candidates in their currently sorted order. Each item has the original
4491
/// index with the candidate.
4592
pub fn candidates(
@@ -164,9 +211,29 @@ impl<'a> CoinSelector<'a> {
164211

165212
/// Current weight of transaction implied by the selection.
166213
///
214+
/// When a [`Package`] is set, this includes the parent transaction weight. Use
215+
/// [`weight_without_package`] if you need the child transaction weight only.
216+
///
167217
/// If you don't have any drain outputs (only target outputs) just set drain_weights to
168218
/// [`DrainWeights::NONE`].
219+
///
220+
/// [`weight_without_package`]: Self::weight_without_package
169221
pub fn weight(&self, target_ouputs: TargetOutputs, drain_weight: DrainWeights) -> u64 {
222+
let child_weight = self.weight_without_package(target_ouputs, drain_weight);
223+
match self.package {
224+
Some(pkg) => child_weight + pkg.parent_weight,
225+
None => child_weight,
226+
}
227+
}
228+
229+
/// Weight of the child transaction only, excluding any package parent weight.
230+
///
231+
/// This is useful for RBF calculations where constraints apply to the child transaction only.
232+
pub fn weight_without_package(
233+
&self,
234+
target_ouputs: TargetOutputs,
235+
drain_weight: DrainWeights,
236+
) -> u64 {
170237
TX_FIXED_FIELD_WEIGHT
171238
+ self.input_weight()
172239
+ target_ouputs.output_weight_with_drain(drain_weight)
@@ -210,11 +277,18 @@ impl<'a> CoinSelector<'a> {
210277
}
211278

212279
/// How much the current selection overshoots the value needed to satisfy RBF's rule 4.
280+
///
281+
/// Note: RBF constraints apply to the child transaction only, so this method uses
282+
/// [`weight_without_package`] even when a package is set.
283+
///
284+
/// [`weight_without_package`]: Self::weight_without_package
213285
pub fn replacement_excess(&self, target: Target, drain: Drain) -> i64 {
214286
let mut replacement_excess_needed = 0;
215287
if let Some(replace) = target.fee.replace {
216-
replacement_excess_needed =
217-
replace.min_fee_to_do_replacement(self.weight(target.outputs, drain.weights))
288+
// RBF rule 4 applies to the child transaction only
289+
replacement_excess_needed = replace.min_fee_to_do_replacement(
290+
self.weight_without_package(target.outputs, drain.weights),
291+
)
218292
}
219293
self.selected_value() as i64
220294
- target.value() as i64
@@ -227,8 +301,10 @@ impl<'a> CoinSelector<'a> {
227301
pub fn replacement_excess_wu(&self, target: Target, drain: Drain) -> i64 {
228302
let mut replacement_excess_needed = 0;
229303
if let Some(replace) = target.fee.replace {
230-
replacement_excess_needed =
231-
replace.min_fee_to_do_replacement_wu(self.weight(target.outputs, drain.weights))
304+
// RBF rule 4 applies to the child transaction only
305+
replacement_excess_needed = replace.min_fee_to_do_replacement_wu(
306+
self.weight_without_package(target.outputs, drain.weights),
307+
)
232308
}
233309
self.selected_value() as i64
234310
- target.value() as i64
@@ -239,15 +315,19 @@ impl<'a> CoinSelector<'a> {
239315
/// The feerate the transaction would have if we were to use this selection of inputs to achieve
240316
/// the `target`'s value and weight. It is essentially telling you what target feerate you currently have.
241317
///
318+
/// When a [`Package`] is set, this returns the package feerate:
319+
/// `(parent_fee + child_fee) / (parent_weight + child_weight)`.
320+
///
242321
/// Returns `None` if the feerate would be negative or infinity.
243322
pub fn implied_feerate(&self, target_outputs: TargetOutputs, drain: Drain) -> Option<FeeRate> {
244-
let numerator =
245-
self.selected_value() as i64 - target_outputs.value_sum as i64 - drain.value as i64;
246-
let denom = self.weight(target_outputs, drain.weights);
247-
if numerator < 0 || denom == 0 {
323+
let total_fee = self.fee(target_outputs.value_sum, drain.value);
324+
let total_weight = self.weight(target_outputs, drain.weights);
325+
if total_fee < 0 || total_weight == 0 {
248326
return None;
249327
}
250-
Some(FeeRate::from_sat_per_wu(numerator as f32 / denom as f32))
328+
Some(FeeRate::from_sat_per_wu(
329+
total_fee as f32 / total_weight as f32,
330+
))
251331
}
252332

253333
/// The fee the current selection and `drain_weight` should pay to satisfy `target_fee`.
@@ -286,8 +366,25 @@ impl<'a> CoinSelector<'a> {
286366
/// The actual fee the selection would pay if it was used in a transaction that had
287367
/// `target_value` value for outputs and change output of `drain_value`.
288368
///
369+
/// When a [`Package`] is set, this includes the parent transaction fee. Use
370+
/// [`fee_without_package`] if you need the child transaction fee only.
371+
///
289372
/// This can be negative when the selection is invalid (outputs are greater than inputs).
373+
///
374+
/// [`fee_without_package`]: Self::fee_without_package
290375
pub fn fee(&self, target_value: u64, drain_value: u64) -> i64 {
376+
let child_fee = self.fee_without_package(target_value, drain_value);
377+
match self.package {
378+
Some(pkg) => child_fee + pkg.parent_fee as i64,
379+
None => child_fee,
380+
}
381+
}
382+
383+
/// Fee of the child transaction only, excluding any package parent fee.
384+
///
385+
/// This is useful when you need to know what the child transaction actually pays,
386+
/// separate from the package context.
387+
pub fn fee_without_package(&self, target_value: u64, drain_value: u64) -> i64 {
291388
self.selected_value() as i64 - target_value as i64 - drain_value as i64
292389
}
293390

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ mod target;
2626
pub use target::*;
2727
mod drain;
2828
pub use drain::*;
29+
mod package;
30+
pub use package::*;
2931

3032
/// Txin "base" fields include `outpoint` (32+4) and `nSequence` (4) and 1 byte for the scriptSig
3133
/// length.

src/package.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// Context for package-aware coin selection (CPFP scenarios).
2+
///
3+
/// When a transaction has unconfirmed parents, miners evaluate the *package feerate* rather than
4+
/// the child's feerate alone. This struct captures the aggregate fee and weight of all parent
5+
/// transactions so that coin selection can target a feerate that makes the entire package
6+
/// attractive to miners.
7+
///
8+
/// The package feerate is calculated as:
9+
/// ```text
10+
/// package_feerate = (parent_fee + child_fee) / (parent_weight + child_weight)
11+
/// ```
12+
///
13+
/// Use [`CoinSelector::with_package`] to create a package-aware coin selector.
14+
///
15+
/// [`CoinSelector::with_package`]: crate::CoinSelector::with_package
16+
#[derive(Debug, Clone, Copy, PartialEq)]
17+
pub struct Package {
18+
/// Total fees already paid by all parent transactions (in satoshis).
19+
pub parent_fee: u64,
20+
/// Total weight of all parent transactions (in weight units).
21+
pub parent_weight: u64,
22+
}

0 commit comments

Comments
 (0)