Skip to content
Open
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ let outputs = vec![TxOut {

let target = Target {
outputs: TargetOutputs::fund_outputs(outputs.iter().map(|output| (output.weight().to_wu(), output.value.to_sat()))),
fee: TargetFee::from_feerate(FeeRate::from_sat_per_vb(42.0))
fee: TargetFee::from_feerate(FeeRate::from_sat_per_vb(42.0)),
max_weight: None
};

let candidates = vec![
Expand Down Expand Up @@ -130,6 +131,7 @@ let mut coin_selector = CoinSelector::new(&candidates);
let target = Target {
fee: TargetFee::from_feerate(FeeRate::from_sat_per_vb(15.0)),
outputs: TargetOutputs::fund_outputs(outputs.iter().map(|output| (output.weight().to_wu(), output.value.to_sat()))),
max_weight: None
};

// The change output must be at least this size to be relayed.
Expand Down
88 changes: 66 additions & 22 deletions src/coin_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ impl<'a> CoinSelector<'a> {
self.selected_value() as i64 - (self.input_weight() as f32 * feerate.spwu()).ceil() as i64
}

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

/// Whether the tx implied by the current selection and `drain` is within `target.max_weight`.
fn is_within_max_weight(&self, target: Target, drain: Drain) -> bool {
match target.max_weight {
Some(max_weight) => self.weight(target.outputs, drain.weights) <= max_weight,
None => true,
}
}

/// Whether the constraints of `Target` have been met if we include a specific `drain` ouput.
///
/// Note if [`is_target_met`] is true and the `drain` is produced from the [`drain`] method then
/// this method will also always be true.
///
/// [`is_target_met`]: Self::is_target_met
/// [`drain`]: Self::drain
/// Note: even if [`Self::is_target_met`] is true, this can be false when adding the `drain` pushes
/// the transaction weight over [`Target::max_weight`].
pub fn is_target_met_with_drain(&self, target: Target, drain: Drain) -> bool {
self.excess(target, drain) >= 0
self.excess(target, drain) >= 0 && self.is_within_max_weight(target, drain)
}

/// Whether the constraints of `Target` have been met.
Expand Down Expand Up @@ -467,16 +472,14 @@ impl<'a> CoinSelector<'a> {
},
);
if excess > change_policy.min_value as i64 {
debug_assert_eq!(
self.is_target_met(target),
self.is_target_met_with_drain(
target,
Drain {
weights: change_policy.drain_weights,
value: excess as u64
}
),
"if the target is met without a drain it must be met after adding the drain"
let drain = Drain {
weights: change_policy.drain_weights,
value: excess as u64,
};
debug_assert!(
self.is_target_met_with_drain(target, drain)
|| !self.is_within_max_weight(target, drain),
"if the target is met without a drain it must be met after adding the drain, unless max weight is exceeded"
);
Some(excess as u64)
} else {
Expand Down Expand Up @@ -520,13 +523,22 @@ impl<'a> CoinSelector<'a> {
}
}

/// Select candidates until `target` has been met assuming the `drain` output is attached.
/// Select candidates until `target` has been met.
///
/// Returns an error if the target was unable to be met.
pub fn select_until_target_met(&mut self, target: Target) -> Result<(), InsufficientFunds> {
/// # Errors
///
/// - [`SelectError::InsufficientFunds`] if the candidates can't cover the target value.
/// - [`SelectError::MaxWeightExceeded`] if the value is met but the selection exceeds [`Target::max_weight`].
pub fn select_until_target_met(&mut self, target: Target) -> Result<(), SelectError> {
self.select_until(|cs| cs.is_target_met(target))
.ok_or_else(|| InsufficientFunds {
missing: self.excess(target, Drain::NONE).unsigned_abs(),
.ok_or_else(|| {
if !self.is_within_max_weight(target, Drain::NONE) {
SelectError::MaxWeightExceeded
} else {
SelectError::InsufficientFunds(InsufficientFunds {
missing: self.excess(target, Drain::NONE).unsigned_abs(),
})
}
})
}

Expand Down Expand Up @@ -641,6 +653,38 @@ impl DoubleEndedIterator for SelectIter<'_> {
}
}

/// Error returned when a selection cannot satisfy the [`Target`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SelectError {
/// The selected inputs don't cover the target value (plus fees).
InsufficientFunds(InsufficientFunds),
/// The target value is met, but the resulting transaction exceeds [`Target::max_weight`].
MaxWeightExceeded,
}

impl core::fmt::Display for SelectError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InsufficientFunds(e) => write!(f, "{}", e),
Self::MaxWeightExceeded => {
write!(
f,
"selection exceeds the maximum allowed transaction weight"
)
}
}
}
}

#[cfg(feature = "std")]
impl std::error::Error for SelectError {}

impl From<InsufficientFunds> for SelectError {
fn from(e: InsufficientFunds) -> Self {
Self::InsufficientFunds(e)
}
}

/// Error type that occurs when the target amount cannot be met.
#[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub struct InsufficientFunds {
Expand Down
3 changes: 3 additions & 0 deletions src/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ pub struct Target {
pub fee: TargetFee,
/// The aggregate properties of outputs you're trying to fund
pub outputs: TargetOutputs,
/// Maximum allowed weight of the resulting transaction (WU).
/// `None` = unconstrained.
pub max_weight: Option<u64>,
}

impl Target {
Expand Down
8 changes: 4 additions & 4 deletions tests/bnb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ fn bnb_finds_an_exact_solution_in_n_iter() {
},
// we're trying to find an exact selection value so set fees to 0
fee: TargetFee::ZERO,
max_weight: None,
};

let solutions = cs.bnb_solutions(MinExcessThenWeight { target });
Expand Down Expand Up @@ -152,6 +153,7 @@ fn bnb_finds_solution_if_possible_in_n_iter() {
n_outputs: 1,
},
fee: TargetFee::default(),
max_weight: None,
};

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

let target = Target {
outputs: TargetOutputs { value_sum: target_value, weight_sum: 0, n_outputs: 1 },
fee: TargetFee::ZERO,
};
let target = Target {outputs:TargetOutputs{value_sum:target_value,weight_sum:0,n_outputs:1},fee:TargetFee::ZERO, max_weight: None };

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

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

let solutions = cs.bnb_solutions(MinExcessThenWeight { target });
Expand Down
3 changes: 2 additions & 1 deletion tests/changeless.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ proptest! {
rate: feerate,
replace,
..TargetFee::ZERO
}
},
max_weight: None
};

let solutions = cs.bnb_solutions(metrics::Changeless {
Expand Down
1 change: 1 addition & 0 deletions tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ impl StrategyParams {
weight_sum: self.target_weight as u64,
n_outputs: self.n_target_outputs,
},
max_weight: None,
}
}

Expand Down
2 changes: 2 additions & 0 deletions tests/lowest_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ fn adding_another_input_to_remove_change() {
weight_sum: 200 - TX_FIXED_FIELD_WEIGHT - 1,
n_outputs: 1,
},
max_weight: None,
};

let candidates = vec![
Expand Down Expand Up @@ -284,6 +285,7 @@ fn zero_fee_tx() {
weight_sum: 200 - TX_FIXED_FIELD_WEIGHT - 1,
n_outputs: 1,
},
max_weight: None,
};

let candidates = vec![
Expand Down
Loading