diff --git a/README.md b/README.md index ada89aa..3d21eec 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,8 @@ 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 @@ -47,7 +48,8 @@ let candidates = vec![ input_count: 2, weight: 2*TR_KEYSPEND_TXIN_WEIGHT, value: 3_000_000, - is_segwit: true + is_segwit: true, + ancestor_bump_fee: 0 } ]; @@ -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(); diff --git a/src/coin_selector.rs b/src/coin_selector.rs index 3f5a29c..bc081a8 100644 --- a/src/coin_selector.rs +++ b/src/coin_selector.rs @@ -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 @@ -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. @@ -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 @@ -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 @@ -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 @@ -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 { @@ -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 diff --git a/tests/ancestor_aware.rs b/tests/ancestor_aware.rs new file mode 100644 index 0000000..88c5619 --- /dev/null +++ b/tests/ancestor_aware.rs @@ -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); +} diff --git a/tests/bnb.rs b/tests/bnb.rs index d8c975e..9a3b75a 100644 --- a/tests/bnb.rs +++ b/tests/bnb.rs @@ -16,6 +16,7 @@ fn test_wv(mut rng: impl RngCore) -> impl Iterator { 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 diff --git a/tests/changeless.rs b/tests/changeless.rs index 774e2a6..be0059c 100644 --- a/tests/changeless.rs +++ b/tests/changeless.rs @@ -17,6 +17,7 @@ fn test_wv(mut rng: impl RngCore) -> impl Iterator { weight: rng.random_range(0..100), input_count: rng.random_range(1..2), is_segwit: false, + ancestor_bump_fee: 0, } }) } diff --git a/tests/common.rs b/tests/common.rs index 59dcb23..add05bc 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -253,6 +253,7 @@ pub fn gen_candidates(n: usize) -> Vec { weight, input_count, is_segwit, + ancestor_bump_fee: 0, } }) .take(n) diff --git a/tests/lowest_fee.rs b/tests/lowest_fee.rs index a14296f..badf804 100644 --- a/tests/lowest_fee.rs +++ b/tests/lowest_fee.rs @@ -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::>(); @@ -200,12 +201,14 @@ 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 { @@ -213,6 +216,7 @@ fn adding_another_input_to_remove_change() { weight: 100, input_count: 1, is_segwit: true, + ancestor_bump_fee: 0, }, ]; @@ -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, }, ]; diff --git a/tests/weight.rs b/tests/weight.rs index 6a8dbb5..3e6024c 100644 --- a/tests/weight.rs +++ b/tests/weight.rs @@ -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::>(); @@ -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::>(); @@ -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::>(); @@ -181,6 +184,7 @@ fn legacy_three_inputs_one_segwit() { .to_wu(), input_count: 1, is_segwit, + ancestor_bump_fee: 0, } }) .collect::>();