Skip to content

Commit 4572958

Browse files
committed
feat!: enforce max_weight in Target
1 parent d2ba08d commit 4572958

8 files changed

Lines changed: 75 additions & 56 deletions

File tree

src/coin_selector.rs

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ impl<'a> CoinSelector<'a> {
121121
pub fn is_selection_possible(&self, target: Target) -> bool {
122122
let mut test = self.clone();
123123
test.select_all_effective(target.fee.rate);
124-
test.is_target_met(target)
124+
test.excess(target, Drain::NONE) >= 0
125125
}
126126

127127
/// Returns true if no candidates have been selected.
@@ -308,7 +308,7 @@ impl<'a> CoinSelector<'a> {
308308
self.selected_value() as i64 - (self.input_weight() as f32 * feerate.spwu()).ceil() as i64
309309
}
310310

311-
// /// Waste sum of all selected inputs.
311+
/// Waste sum of all selected inputs.
312312
fn input_waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 {
313313
self.input_weight() as f32 * (feerate.spwu() - long_term_feerate.spwu())
314314
}
@@ -429,15 +429,20 @@ impl<'a> CoinSelector<'a> {
429429
self.unselected_indices().next().is_none()
430430
}
431431

432+
/// Whether the tx implied by the current selection and `drain` is within `target.max_weight`.
433+
fn is_within_max_weight(&self, target: Target, drain: Drain) -> bool {
434+
match target.max_weight {
435+
Some(max_weight) => self.weight(target.outputs, drain.weights) <= max_weight,
436+
None => true,
437+
}
438+
}
439+
432440
/// Whether the constraints of `Target` have been met if we include a specific `drain` ouput.
433441
///
434-
/// Note if [`is_target_met`] is true and the `drain` is produced from the [`drain`] method then
435-
/// this method will also always be true.
436-
///
437-
/// [`is_target_met`]: Self::is_target_met
438-
/// [`drain`]: Self::drain
442+
/// Note: even if [`Self::is_target_met`] is true, this can be false when adding the `drain` pushes
443+
/// the transaction weight over [`Target::max_weight`].
439444
pub fn is_target_met_with_drain(&self, target: Target, drain: Drain) -> bool {
440-
self.excess(target, drain) >= 0
445+
self.excess(target, drain) >= 0 && self.is_within_max_weight(target, drain)
441446
}
442447

443448
/// Whether the constraints of `Target` have been met.
@@ -520,13 +525,26 @@ impl<'a> CoinSelector<'a> {
520525
}
521526
}
522527

523-
/// Select candidates until `target` has been met assuming the `drain` output is attached.
528+
/// Select candidates until `target` has been met.
529+
///
530+
/// # Errors
524531
///
525-
/// Returns an error if the target was unable to be met.
526-
pub fn select_until_target_met(&mut self, target: Target) -> Result<(), InsufficientFunds> {
532+
/// - [`SelectError::InsufficientFunds`] if the candidates can't cover the target value.
533+
/// - [`SelectError::MaxWeightExceeded`] if the value is met but the selection exceeds
534+
/// [`Target::max_weight`].
535+
pub fn select_until_target_met(&mut self, target: Target) -> Result<(), SelectError> {
527536
self.select_until(|cs| cs.is_target_met(target))
528-
.ok_or_else(|| InsufficientFunds {
529-
missing: self.excess(target, Drain::NONE).unsigned_abs(),
537+
.ok_or_else(|| {
538+
let excess = self.excess(target, Drain::NONE);
539+
// excess < 0 means unfundable (InsufficientFunds),
540+
// else value is met so the weight cap failed (MaxWeightExceeded).
541+
if excess >= 0 {
542+
SelectError::MaxWeightExceeded
543+
} else {
544+
SelectError::InsufficientFunds(InsufficientFunds {
545+
missing: excess.unsigned_abs(),
546+
})
547+
}
530548
})
531549
}
532550

@@ -641,6 +659,38 @@ impl DoubleEndedIterator for SelectIter<'_> {
641659
}
642660
}
643661

662+
/// Error returned when a selection cannot satisfy the [`Target`].
663+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
664+
pub enum SelectError {
665+
/// The selected inputs don't cover the target value (plus fees).
666+
InsufficientFunds(InsufficientFunds),
667+
/// The target value is met, but the resulting transaction exceeds [`Target::max_weight`].
668+
MaxWeightExceeded,
669+
}
670+
671+
impl core::fmt::Display for SelectError {
672+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
673+
match self {
674+
Self::InsufficientFunds(e) => write!(f, "{}", e),
675+
Self::MaxWeightExceeded => {
676+
write!(
677+
f,
678+
"selection exceeds the maximum allowed transaction weight"
679+
)
680+
}
681+
}
682+
}
683+
}
684+
685+
#[cfg(feature = "std")]
686+
impl std::error::Error for SelectError {}
687+
688+
impl From<InsufficientFunds> for SelectError {
689+
fn from(e: InsufficientFunds) -> Self {
690+
Self::InsufficientFunds(e)
691+
}
692+
}
693+
644694
/// Error type that occurs when the target amount cannot be met.
645695
#[derive(Clone, Debug, Copy, PartialEq, Eq)]
646696
pub struct InsufficientFunds {

src/metrics.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ mod lowest_fee;
55
pub use lowest_fee::*;
66
mod changeless;
77
pub use changeless::*;
8-
mod max_weight;
9-
pub use max_weight::*;
108

119
// Returns a drain if the current selection and every possible future selection would have a change
1210
// output (otherwise Drain::none()) by using the heurisitic that if it has change with the current

src/metrics/max_weight.rs

Lines changed: 0 additions & 36 deletions
This file was deleted.

src/target.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ pub struct Target {
77
pub fee: TargetFee,
88
/// The aggregate properties of outputs you're trying to fund
99
pub outputs: TargetOutputs,
10+
/// Maximum allowed weight of the resulting transaction (WU).
11+
/// `None` = unconstrained.
12+
pub max_weight: Option<u64>,
1013
}
1114

1215
impl Target {

tests/bnb.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ fn bnb_finds_an_exact_solution_in_n_iter() {
118118
},
119119
// we're trying to find an exact selection value so set fees to 0
120120
fee: TargetFee::ZERO,
121+
max_weight: None,
121122
};
122123

123124
let solutions = cs.bnb_solutions(MinExcessThenWeight { target });
@@ -152,6 +153,7 @@ fn bnb_finds_solution_if_possible_in_n_iter() {
152153
n_outputs: 1,
153154
},
154155
fee: TargetFee::default(),
156+
max_weight: None,
155157
};
156158

157159
let solutions = cs.bnb_solutions(MinExcessThenWeight { target });
@@ -195,10 +197,7 @@ proptest! {
195197
let candidates = wv.take(num_inputs).collect::<Vec<_>>();
196198
let cs = CoinSelector::new(&candidates);
197199

198-
let target = Target {
199-
outputs: TargetOutputs { value_sum: target_value, weight_sum: 0, n_outputs: 1 },
200-
fee: TargetFee::ZERO,
201-
};
200+
let target = Target {outputs:TargetOutputs{value_sum:target_value,weight_sum:0,n_outputs:1},fee:TargetFee::ZERO, max_weight: None };
202201

203202
let solutions = cs.bnb_solutions(MinExcessThenWeight { target });
204203

@@ -244,6 +243,7 @@ proptest! {
244243
outputs: TargetOutputs { value_sum: target_value, weight_sum: 0, n_outputs: 1 },
245244
// we're trying to find an exact selection value so set fees to 0
246245
fee: TargetFee::ZERO,
246+
max_weight: None
247247
};
248248

249249
let solutions = cs.bnb_solutions(MinExcessThenWeight { target });

tests/changeless.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ proptest! {
6767
rate: feerate,
6868
replace,
6969
..TargetFee::ZERO
70-
}
70+
},
71+
max_weight: None
7172
};
7273

7374
let solutions = cs.bnb_solutions(metrics::Changeless {

tests/common.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ impl StrategyParams {
221221
weight_sum: self.target_weight as u64,
222222
n_outputs: self.n_target_outputs,
223223
},
224+
max_weight: None,
224225
}
225226
}
226227

tests/lowest_fee.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ fn adding_another_input_to_remove_change() {
192192
weight_sum: 200 - TX_FIXED_FIELD_WEIGHT - 1,
193193
n_outputs: 1,
194194
},
195+
max_weight: None,
195196
};
196197

197198
let candidates = vec![
@@ -284,6 +285,7 @@ fn zero_fee_tx() {
284285
weight_sum: 200 - TX_FIXED_FIELD_WEIGHT - 1,
285286
n_outputs: 1,
286287
},
288+
max_weight: None,
287289
};
288290

289291
let candidates = vec![

0 commit comments

Comments
 (0)