diff --git a/src/coin_selector.rs b/src/coin_selector.rs index 3f5a29c..d6cdd7b 100644 --- a/src/coin_selector.rs +++ b/src/coin_selector.rs @@ -178,6 +178,7 @@ impl<'a> CoinSelector<'a> { /// this means the transaction will overpay for what it needs to reach `target`. pub fn excess(&self, target: Target, drain: Drain) -> i64 { self.rate_excess(target, drain) + .min(self.absolute_excess(target, drain)) .min(self.replacement_excess(target, drain)) } @@ -192,7 +193,7 @@ impl<'a> CoinSelector<'a> { } /// How much the current selection overshoots the value need to satisfy `target.fee.rate` and - /// `target.value` (while ignoring `target.min_fee`). + /// `target.value` (while ignoring `target.fee.absolute`). pub fn rate_excess(&self, target: Target, drain: Drain) -> i64 { self.selected_value() as i64 - target.value() as i64 @@ -209,6 +210,15 @@ impl<'a> CoinSelector<'a> { - self.implied_fee_from_feerate_wu(target, drain.weights) as i64 } + /// How much the current selection overshoots the value needed to satisfy `target.fee.absolute` + /// and `target.value` (while ignoring `target.fee.rate`). + pub fn absolute_excess(&self, target: Target, drain: Drain) -> i64 { + self.selected_value() as i64 + - target.value() as i64 + - drain.value as i64 + - target.fee.absolute as i64 + } + /// How much the current selection overshoots the value needed to satisfy RBF's rule 4. pub fn replacement_excess(&self, target: Target, drain: Drain) -> i64 { let mut replacement_excess_needed = 0; @@ -257,7 +267,9 @@ impl<'a> CoinSelector<'a> { /// /// `drain_weight` can be 0 to indicate no draining output. pub fn implied_fee(&self, target: Target, drain_weights: DrainWeights) -> u64 { - let mut implied_fee = self.implied_fee_from_feerate(target, drain_weights); + let mut implied_fee = self + .implied_fee_from_feerate(target, drain_weights) + .max(target.fee.absolute); if let Some(replace) = target.fee.replace { implied_fee = Ord::max( diff --git a/src/metrics/lowest_fee.rs b/src/metrics/lowest_fee.rs index 2d7278c..730edaa 100644 --- a/src/metrics/lowest_fee.rs +++ b/src/metrics/lowest_fee.rs @@ -172,6 +172,20 @@ impl BnbMetric for LowestFee { } } } + // Handle absolute fee constraint. Unlike feerate and replacement, the + // absolute fee is a fixed amount (not weight-proportional), so we just + // need enough raw value to cover the gap. + let absolute_excess = cs.absolute_excess(self.target, Drain::NONE) as f32; + if absolute_excess < 0.0 { + let remaining = absolute_excess.abs(); + if to_resize.value > 0 { + let absolute_scale = remaining / to_resize.value as f32; + scale = scale.max(Ordf32(absolute_scale)); + } else { + return None; // we can never satisfy the constraint + } + } + // `scale` could be 0 even if `is_target_met` is `false` due to the latter being based on // rounded-up vbytes. let ideal_fee = scale.0 * to_resize.value as f32 + cs.selected_value() as f32 diff --git a/src/target.rs b/src/target.rs index 36a8803..1bd3ae0 100644 --- a/src/target.rs +++ b/src/target.rs @@ -81,8 +81,10 @@ impl TargetOutputs { /// [RBF rule 4]: https://github.com/bitcoin/bitcoin/blob/master/doc/policy/mempool-replacements.md#current-replace-by-fee-policy /// [`ChangePolicy`]: crate::ChangePolicy pub struct TargetFee { - /// The feerate the transaction must have + /// The minimum feerate the transaction must have. pub rate: FeeRate, + /// The minimum absolute fee the transaction must have. + pub absolute: u64, /// The fee must enough enough to replace this pub replace: Option, } @@ -92,15 +94,16 @@ impl Default for TargetFee { fn default() -> Self { Self { rate: FeeRate::DEFAULT_MIN_RELAY, - replace: None, + ..Self::ZERO } } } impl TargetFee { - /// A target fee of 0 sats per vbyte (and no replacement) + /// A target fee of 0 sats per vbyte, 0 absolute fee (and no replacement) pub const ZERO: Self = TargetFee { rate: FeeRate::ZERO, + absolute: 0, replace: None, }; @@ -108,7 +111,7 @@ impl TargetFee { pub fn from_feerate(feerate: FeeRate) -> Self { Self { rate: feerate, - replace: None, + ..Self::ZERO } } } diff --git a/tests/changeless.rs b/tests/changeless.rs index 774e2a6..42224e9 100644 --- a/tests/changeless.rs +++ b/tests/changeless.rs @@ -66,6 +66,7 @@ proptest! { fee: TargetFee { rate: feerate, replace, + ..TargetFee::ZERO } }; diff --git a/tests/common.rs b/tests/common.rs index 59dcb23..8b8cb9d 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -214,6 +214,7 @@ impl StrategyParams { fee: TargetFee { rate: FeeRate::from_sat_per_vb(self.feerate), replace: self.replace, + ..TargetFee::ZERO }, outputs: TargetOutputs { value_sum: self.target_value, diff --git a/tests/lowest_fee.rs b/tests/lowest_fee.rs index a14296f..27e2948 100644 --- a/tests/lowest_fee.rs +++ b/tests/lowest_fee.rs @@ -277,6 +277,7 @@ fn zero_fee_tx() { fee: TargetFee { rate: target_feerate, replace: None, + ..TargetFee::ZERO }, outputs: TargetOutputs { value_sum: 99_870,