Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,17 @@ let candidates = vec![
weight: TR_KEYSPEND_TXIN_WEIGHT,
// wether it's a segwit input. Needed so we know whether to include the
// segwit header in total weight calculations.
is_segwit: true
is_segwit: true,
ancestor_bump_fee: 0
},
Candidate {
// A candidate can represent multiple inputs in the case where you
// always want some inputs to be spent together.
input_count: 2,
weight: 2*TR_KEYSPEND_TXIN_WEIGHT,
value: 3_000_000,
is_segwit: true
is_segwit: true,
ancestor_bump_fee: 0
}
];

Expand Down Expand Up @@ -106,19 +108,22 @@ let candidates = [
input_count: 1,
value: 400_000,
weight: TR_KEYSPEND_TXIN_WEIGHT,
is_segwit: true
is_segwit: true,
ancestor_bump_fee: 0
},
Candidate {
input_count: 1,
value: 200_000,
weight: TR_KEYSPEND_TXIN_WEIGHT,
is_segwit: true
is_segwit: true,
ancestor_bump_fee: 0
},
Candidate {
input_count: 1,
value: 11_000,
weight: TR_KEYSPEND_TXIN_WEIGHT,
is_segwit: true
is_segwit: true,
ancestor_bump_fee: 0
}
];
let drain_weights = bdk_coin_select::DrainWeights::default();
Expand Down
22 changes: 20 additions & 2 deletions src/coin_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ impl<'a> CoinSelector<'a> {
- target.value() as i64
- drain.value as i64
- self.implied_fee_from_feerate(target, drain.weights) as i64
- self.selected_ancestor_bump_fee() as i64
}

/// Same as [rate_excess](Self::rate_excess) except `target.fee.rate` is applied to the
Expand All @@ -207,6 +208,7 @@ impl<'a> CoinSelector<'a> {
- target.value() as i64
- drain.value as i64
- self.implied_fee_from_feerate_wu(target, drain.weights) as i64
- self.selected_ancestor_bump_fee() as i64
}

/// How much the current selection overshoots the value needed to satisfy RBF's rule 4.
Expand All @@ -220,6 +222,7 @@ impl<'a> CoinSelector<'a> {
- target.value() as i64
- drain.value as i64
- replacement_excess_needed as i64
- self.selected_ancestor_bump_fee() as i64
}

/// Same as [replacement_excess](Self::replacement_excess) except the replacement fee
Expand All @@ -234,6 +237,7 @@ impl<'a> CoinSelector<'a> {
- target.value() as i64
- drain.value as i64
- replacement_excess_needed as i64
- self.selected_ancestor_bump_fee() as i64
}

/// The feerate the transaction would have if we were to use this selection of inputs to achieve
Expand Down Expand Up @@ -576,6 +580,14 @@ impl<'a> CoinSelector<'a> {
*self = selector;
Ok(score)
}

/// Total ancestor bump fee across all selected candidates.
pub fn selected_ancestor_bump_fee(&self) -> u64 {
self.selected
.iter()
.map(|&index| self.candidates[index].ancestor_bump_fee)
.sum()
}
}

// Allow this for now due to MSRV
Expand Down Expand Up @@ -682,6 +694,11 @@ pub struct Candidate {
pub input_count: usize,
/// Whether this [`Candidate`] contains at least one segwit spend.
pub is_segwit: bool,
/// Additional fee (in satoshis) the child must pay to bring this
/// candidate's unconfirmed ancestors up to the target feerate.
///
/// Defaults to `0` for confirmed UTXOs.
pub ancestor_bump_fee: u64,
}

impl Candidate {
Expand All @@ -702,12 +719,13 @@ impl Candidate {
weight,
input_count: 1,
is_segwit,
ancestor_bump_fee: 0,
}
}

/// Effective value of this input candidate: `actual_value - input_weight * feerate (sats/wu)`.
/// Effective value after accounting for input weight and ancestor bump fee.
pub fn effective_value(&self, feerate: FeeRate) -> f32 {
self.value as f32 - (self.weight as f32 * feerate.spwu())
self.value as f32 - (self.weight as f32 * feerate.spwu()) - self.ancestor_bump_fee as f32
}

/// Value per weight unit
Expand Down
62 changes: 62 additions & 0 deletions tests/ancestor_aware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use bdk_coin_select::*;

#[test]
fn zero_ancestor_bump_fee_is_backward_compatible() {
let candidates = [
Candidate::new_tr_keyspend(500_000),
Candidate::new_tr_keyspend(200_000),
];

let target = Target {
fee: TargetFee::from_feerate(FeeRate::from_sat_per_vb(5.0)),
outputs: TargetOutputs::fund_outputs([(200, 100_000)]),
};

let mut cs = CoinSelector::new(&candidates);
cs.select(0);

assert_eq!(cs.selected_ancestor_bump_fee(), 0);
assert!(cs.is_target_met(target));
}

#[test]
fn ancestor_bump_fee_reduces_effective_value() {
let without = Candidate::new_tr_keyspend(500_000);
let with = Candidate {
ancestor_bump_fee: 10_000,
..Candidate::new_tr_keyspend(500_000)
};

let feerate = FeeRate::from_sat_per_vb(5.0);
assert_eq!(
without.effective_value(feerate) - with.effective_value(feerate),
10_000.0
);
}

#[test]
fn ancestor_bump_fee_reduces_excess() {
let candidates = [Candidate {
ancestor_bump_fee: 20_000,
..Candidate::new_tr_keyspend(500_000)
}];

let target = Target {
fee: TargetFee::from_feerate(FeeRate::from_sat_per_vb(1.0)),
outputs: TargetOutputs::fund_outputs([(200, 100_000)]),
};

let mut cs = CoinSelector::new(&candidates);
cs.select(0);

let excess_with = cs.rate_excess(target, Drain::NONE);

// Compare against same candidate without ancestor cost.
let candidates_no_anc = [Candidate::new_tr_keyspend(500_000)];
let mut cs_no_anc = CoinSelector::new(&candidates_no_anc);
cs_no_anc.select(0);

let excess_without = cs_no_anc.rate_excess(target, Drain::NONE);

assert_eq!(excess_without - excess_with, 20_000);
}
1 change: 1 addition & 0 deletions tests/bnb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ fn test_wv(mut rng: impl RngCore) -> impl Iterator<Item = Candidate> {
weight: 100,
input_count: rng.random_range(1..2),
is_segwit: rng.random_bool(0.5),
ancestor_bump_fee: 0,
};
// HACK: set is_segwit = true for all these tests because you can't actually lower bound
// things easily with how segwit inputs interfere with their weights. We can't modify the
Expand Down
1 change: 1 addition & 0 deletions tests/changeless.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ fn test_wv(mut rng: impl RngCore) -> impl Iterator<Item = Candidate> {
weight: rng.random_range(0..100),
input_count: rng.random_range(1..2),
is_segwit: false,
ancestor_bump_fee: 0,
}
})
}
Expand Down
1 change: 1 addition & 0 deletions tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ pub fn gen_candidates(n: usize) -> Vec<Candidate> {
weight,
input_count,
is_segwit,
ancestor_bump_fee: 0,
}
})
.take(n)
Expand Down
6 changes: 6 additions & 0 deletions tests/lowest_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ proptest! {
weight: (32 + 4 + 4 + 1) * 4 + 64 + 32,
input_count: 1,
is_segwit: true,
ancestor_bump_fee: 0
})
.take(params.n_candidates)
.collect::<Vec<_>>();
Expand Down Expand Up @@ -200,19 +201,22 @@ fn adding_another_input_to_remove_change() {
weight: 100,
input_count: 1,
is_segwit: true,
ancestor_bump_fee: 0,
},
Candidate {
value: 50_000,
weight: 100,
input_count: 1,
is_segwit: true,
ancestor_bump_fee: 0,
},
// NOTE: this input has negative effective value
Candidate {
value: 10,
weight: 100,
input_count: 1,
is_segwit: true,
ancestor_bump_fee: 0,
},
];

Expand Down Expand Up @@ -291,12 +295,14 @@ fn zero_fee_tx() {
weight: 100,
input_count: 1,
is_segwit: true,
ancestor_bump_fee: 0,
},
Candidate {
value: 50_000,
weight: 100,
input_count: 1,
is_segwit: true,
ancestor_bump_fee: 0,
},
];

Expand Down
4 changes: 4 additions & 0 deletions tests/weight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fn segwit_one_input_one_output() {
weight: txin.segwit_weight().to_wu(),
input_count: 1,
is_segwit: true,
ancestor_bump_fee: 0,
})
.collect::<Vec<_>>();

Expand Down Expand Up @@ -80,6 +81,7 @@ fn segwit_two_inputs_one_output() {
weight: txin.segwit_weight().to_wu(),
input_count: 1,
is_segwit: true,
ancestor_bump_fee: 0,
})
.collect::<Vec<_>>();

Expand Down Expand Up @@ -124,6 +126,7 @@ fn legacy_three_inputs() {
weight: txin.legacy_weight().to_wu(),
input_count: 1,
is_segwit: false,
ancestor_bump_fee: 0,
})
.collect::<Vec<_>>();

Expand Down Expand Up @@ -181,6 +184,7 @@ fn legacy_three_inputs_one_segwit() {
.to_wu(),
input_count: 1,
is_segwit,
ancestor_bump_fee: 0,
}
})
.collect::<Vec<_>>();
Expand Down
Loading