Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "bdk_coin_select"
version = "0.5.0"
edition = "2018"
#rust-version = "1.54"
rust-version = "1.54"
homepage = "https://bitcoindevkit.org"
repository = "https://github.com/bitcoindevkit/coin-select"
documentation = "https://docs.rs/bdk_coin_select"
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ 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)),
// An optional cap on the resulting transaction weight (e.g. for TRUC). `None` = unconstrained.
max_weight: None,
};

let candidates = vec![
Expand Down Expand Up @@ -55,10 +57,10 @@ let candidates = vec![
let mut coin_selector = CoinSelector::new(&candidates);
coin_selector.select(0);

assert!(!coin_selector.is_target_met(target), "we didn't select enough");
assert!(!coin_selector.is_funded(target), "we didn't select enough");
println!("we didn't select enough yet we're missing: {}", coin_selector.missing(target));
coin_selector.select(1);
assert!(coin_selector.is_target_met(target), "we should have enough now");
assert!(coin_selector.is_funded(target), "we should have enough now");

// Now we need to know if we need a change output to drain the excess if we overshot too much
//
Expand Down Expand Up @@ -130,6 +132,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 feerate used to work out whether a change output would be dust (and so shouldn't be added).
Expand Down
6 changes: 6 additions & 0 deletions benches/coin_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
//!
//! Run with `cargo bench`. Filter with `cargo bench -- <pattern>`.

// Benchmarks are dev-only and are never built under the MSRV (the `build-msrv` CI job excludes
// dev-dependencies), so lints about newer std APIs — e.g. `black_box`, stable since 1.66 — don't
// apply here.
#![allow(clippy::incompatible_msrv)]

use bdk_coin_select::{
metrics::LowestFee, Candidate, CoinSelector, DrainWeights, FeeRate, Target, TargetFee,
TargetOutputs, TR_SPK_WEIGHT, TXIN_BASE_WEIGHT, TXOUT_BASE_WEIGHT,
Expand Down Expand Up @@ -42,6 +47,7 @@ fn make_bnb_inputs(candidates: &[Candidate]) -> (Target, FeeRate) {
let target = Target {
fee: TargetFee::from_feerate(target_fr),
outputs: TargetOutputs::fund_outputs([(TXOUT_BASE_WEIGHT + TR_SPK_WEIGHT, total / 2)]),
max_weight: None,
};
(target, long_term_fr)
}
Expand Down
133 changes: 106 additions & 27 deletions src/coin_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,23 @@ impl<'a> CoinSelector<'a> {
self.selected.contains(index)
}

/// Is meeting this `target` possible with the current selection with this `drain` (i.e. change output).
/// Note this will respect [`ban`]ned candidates.
/// Whether the candidates can cover this `target`'s **value** (net of input fees) — i.e. whether
/// enough value is reachable for [`is_funded`] to hold. Respects [`ban`]ned candidates.
///
/// This simply selects all effective inputs at the target's feerate and checks whether we have
/// enough value.
/// Selecting *all* effective inputs maximizes the value available, so if that can't meet the
/// target value, nothing can. Monotone, hence exact.
///
/// NOTE: this does **not** account for [`Target::max_weight`] — a `true` result can still be
/// infeasible under the weight cap. Use [`select_until_target_met`] or branch and bound (both of
/// which enforce the cap) to actually build a selection.
///
/// [`ban`]: Self::ban
pub fn is_selection_possible(&self, target: Target) -> bool {
/// [`is_funded`]: Self::is_funded
/// [`select_until_target_met`]: Self::select_until_target_met
pub fn is_fundable(&self, target: Target) -> bool {
let mut test = self.clone();
test.select_all_effective(target.fee.rate);
test.is_target_met(target)
test.is_funded(target)
}

/// Returns true if no candidates have been selected.
Expand Down Expand Up @@ -407,6 +413,17 @@ impl<'a> CoinSelector<'a> {
.map(move |i| (i, self.candidates[i]))
}

/// The weight of the lightest unselected (addable) candidate, or `None` when nothing is left to
/// add.
///
/// This is a lower bound on the extra input weight any descendant selection must take on to add
/// more value, which weight-aware branch-and-bound bounds use to reason about `max_weight`.
pub fn min_input_weight(&self) -> Option<u64> {
self.unselected()
.map(|(_, candidate)| candidate.weight)
.min()
}

/// The indices of the selelcted candidates.
pub fn selected_indices(&self) -> &Bitset {
&self.selected
Expand All @@ -429,20 +446,39 @@ impl<'a> CoinSelector<'a> {
self.unselected_indices().next().is_none()
}

/// Whether the constraints of `Target` have been met if we include a specific `drain` ouput.
/// Whether the tx implied by the current selection plus a drain of `drain_weights` is within
/// [`Target::max_weight`]. Pass [`DrainWeights::NONE`] for a changeless tx.
///
/// Note if [`is_target_met`] is true and the `drain` is produced from the [`drain`] method then
/// this method will also always be true.
/// Always `true` when `max_weight` is `None`. Note this is the *anti-monotone* half of
/// feasibility (adding inputs adds weight), so it is kept separate from the monotone
/// value-only [`is_funded`](Self::is_funded).
pub fn is_within_max_weight(&self, target: Target, drain_weights: DrainWeights) -> bool {
match target.max_weight {
Some(max_weight) => self.weight(target.outputs, drain_weights) <= max_weight,
None => true,
}
}

/// Whether the selection covers the target value (i.e. [`excess`](Self::excess) is
/// non-negative), ignoring [`Target::max_weight`].
///
/// [`is_target_met`]: Self::is_target_met
/// [`drain`]: Self::drain
pub fn is_target_met_with_drain(&self, target: Target, drain: Drain) -> bool {
/// This is **monotone**: selecting more never un-meets it. It deliberately does *not* include
/// the weight cap — see [`is_within_max_weight`](Self::is_within_max_weight).
pub fn is_funded_with_drain(&self, target: Target, drain: Drain) -> bool {
self.excess(target, drain) >= 0
}

/// Whether the constraints of `Target` have been met.
pub fn is_target_met(&self, target: Target) -> bool {
self.is_target_met_with_drain(target, Drain::NONE)
/// Whether the selection covers the target **value** (net of input fees), i.e. [`excess`] is
/// non-negative. **Monotone** (selecting more never un-meets it), and it deliberately does
/// *not* check [`Target::max_weight`] — that is the separate, anti-monotone
/// [`is_within_max_weight`]. See [`is_funded_with_drain`] for the version that
/// accounts for a specific `drain`.
///
/// [`excess`]: Self::excess
/// [`is_within_max_weight`]: Self::is_within_max_weight
/// [`is_funded_with_drain`]: Self::is_funded_with_drain
pub fn is_funded(&self, target: Target) -> bool {
self.is_funded_with_drain(target, Drain::NONE)
}

/// Select all unselected candidates
Expand All @@ -468,8 +504,8 @@ 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(
self.is_funded(target),
self.is_funded_with_drain(
target,
Drain {
weights: change_policy.drain_weights,
Expand All @@ -488,12 +524,12 @@ impl<'a> CoinSelector<'a> {
/// `change_policy`. If it should not, then it will return [`Drain::NONE`]. The value of the
/// `Drain` will be the same as [`drain_value`].
///
/// If [`is_target_met`] returns true for this selection then [`is_target_met_with_drain`] will
/// If [`is_funded`] returns true for this selection then [`is_funded_with_drain`] will
/// also be true if you pass in the drain returned from this method.
///
/// [`drain_value`]: Self::drain_value
/// [`is_target_met_with_drain`]: Self::is_target_met_with_drain
/// [`is_target_met`]: Self::is_target_met
/// [`is_funded_with_drain`]: Self::is_funded_with_drain
/// [`is_funded`]: Self::is_funded
#[must_use]
pub fn drain(&self, target: Target, change_policy: ChangePolicy) -> Drain {
match self.drain_value(target, change_policy) {
Expand Down Expand Up @@ -521,14 +557,25 @@ 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> {
self.select_until(|cs| cs.is_target_met(target))
.ok_or_else(|| InsufficientFunds {
missing: self.excess(target, Drain::NONE).unsigned_abs(),
})
/// # Errors
///
/// - [`SelectError::InsufficientFunds`] if the candidates can't cover the target value.
/// - [`SelectError::MaxWeightExceeded`] if the value is met but the resulting selection exceeds
/// [`Target::max_weight`]. Note this only reflects *this* in-order greedy selection; a
/// different selection might still fit the cap (use branch and bound to search for one).
pub fn select_until_target_met(&mut self, target: Target) -> Result<(), SelectError> {
self.select_until(|cs| cs.is_funded(target))
.ok_or_else(|| {
SelectError::InsufficientFunds(InsufficientFunds {
missing: self.excess(target, Drain::NONE).unsigned_abs(),
})
})?;
if !self.is_within_max_weight(target, DrainWeights::NONE) {
return Err(SelectError::MaxWeightExceeded);
}
Ok(())
}

/// Select candidates until some predicate has been satisfied.
Expand Down Expand Up @@ -663,6 +710,38 @@ impl core::fmt::Display for InsufficientFunds {
#[cfg(feature = "std")]
impl std::error::Error for InsufficientFunds {}

/// Error returned by [`CoinSelector::select_until_target_met`].
#[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub enum SelectError {
/// The candidates can't cover the target value.
InsufficientFunds(InsufficientFunds),
/// The value target is met, but the resulting selection exceeds [`Target::max_weight`].
MaxWeightExceeded,
}

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

impl core::fmt::Display for SelectError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match self {
SelectError::InsufficientFunds(e) => write!(f, "{}", e),
SelectError::MaxWeightExceeded => {
write!(
f,
"Selection meets the target value but exceeds `max_weight`."
)
}
}
}
}

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

/// Error type for when a solution cannot be found by branch-and-bound.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NoBnbSolution {
Expand Down
Loading
Loading