Skip to content

Commit 1c5c665

Browse files
committed
feat!: improve api to apply selection to transaction
1 parent 434c6e8 commit 1c5c665

11 files changed

+192
-119
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@ std = []
2323
[dev-dependencies]
2424
rand = "0.8"
2525
proptest = "1.4"
26-
bitcoin = "0.30"
26+
bitcoin = "0.32"

src/bnb.rs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ use alloc::collections::BinaryHeap;
88
/// An [`Iterator`] that iterates over rounds of branch and bound to minimize the score of the
99
/// provided [`BnbMetric`].
1010
#[derive(Debug)]
11-
pub(crate) struct BnbIter<'a, M: BnbMetric> {
12-
queue: BinaryHeap<Branch<'a>>,
11+
pub(crate) struct BnbIter<'a, C, M: BnbMetric> {
12+
queue: BinaryHeap<Branch<'a, C>>,
1313
best: Option<Ordf32>,
1414
/// The `BnBMetric` that will score each selection
1515
metric: M,
1616
}
1717

18-
impl<'a, M: BnbMetric> Iterator for BnbIter<'a, M> {
19-
type Item = Option<(CoinSelector<'a>, Ordf32)>;
18+
impl<'a, C, M: BnbMetric> Iterator for BnbIter<'a, C, M> {
19+
type Item = Option<(CoinSelector<'a, C>, Ordf32)>;
2020

2121
fn next(&mut self) -> Option<Self::Item> {
2222
// {
@@ -70,8 +70,8 @@ impl<'a, M: BnbMetric> Iterator for BnbIter<'a, M> {
7070
}
7171
}
7272

73-
impl<'a, M: BnbMetric> BnbIter<'a, M> {
74-
pub(crate) fn new(mut selector: CoinSelector<'a>, metric: M) -> Self {
73+
impl<'a, C, M: BnbMetric> BnbIter<'a, C, M> {
74+
pub(crate) fn new(mut selector: CoinSelector<'a, C>, metric: M) -> Self {
7575
let mut iter = BnbIter {
7676
queue: BinaryHeap::default(),
7777
best: None,
@@ -87,7 +87,7 @@ impl<'a, M: BnbMetric> BnbIter<'a, M> {
8787
iter
8888
}
8989

90-
fn consider_adding_to_queue(&mut self, cs: &CoinSelector<'a>, is_exclusion: bool) {
90+
fn consider_adding_to_queue(&mut self, cs: &CoinSelector<'a, C>, is_exclusion: bool) {
9191
let bound = self.metric.bound(cs);
9292
if let Some(bound) = bound {
9393
let is_good_enough = match self.best {
@@ -127,7 +127,7 @@ impl<'a, M: BnbMetric> BnbIter<'a, M> {
127127
}*/
128128
}
129129

130-
fn insert_new_branches(&mut self, cs: &CoinSelector<'a>) {
130+
fn insert_new_branches(&mut self, cs: &CoinSelector<'a, C>) {
131131
let (next_index, next) = match cs.unselected().next() {
132132
Some(c) => c,
133133
None => return, // exhausted
@@ -161,13 +161,13 @@ impl<'a, M: BnbMetric> BnbIter<'a, M> {
161161
}
162162

163163
#[derive(Debug, Clone)]
164-
struct Branch<'a> {
164+
struct Branch<'a, C> {
165165
lower_bound: Ordf32,
166-
selector: CoinSelector<'a>,
166+
selector: CoinSelector<'a, C>,
167167
is_exclusion: bool,
168168
}
169169

170-
impl<'a> Ord for Branch<'a> {
170+
impl<'a, C> Ord for Branch<'a, C> {
171171
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
172172
// NOTE: Reverse comparision `lower_bound` because we want a min-heap (by default BinaryHeap
173173
// is a max-heap).
@@ -181,19 +181,19 @@ impl<'a> Ord for Branch<'a> {
181181
}
182182
}
183183

184-
impl<'a> PartialOrd for Branch<'a> {
184+
impl<'a, C> PartialOrd for Branch<'a, C> {
185185
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
186186
Some(self.cmp(other))
187187
}
188188
}
189189

190-
impl<'a> PartialEq for Branch<'a> {
190+
impl<'a, C> PartialEq for Branch<'a, C> {
191191
fn eq(&self, other: &Self) -> bool {
192192
self.lower_bound == other.lower_bound
193193
}
194194
}
195195

196-
impl<'a> Eq for Branch<'a> {}
196+
impl<'a, C> Eq for Branch<'a, C> {}
197197

198198
/// A branch and bound metric where we minimize the [`Ordf32`] score.
199199
///
@@ -202,7 +202,7 @@ pub trait BnbMetric {
202202
/// Get the score of a given selection.
203203
///
204204
/// If this returns `None`, the selection is invalid.
205-
fn score(&mut self, cs: &CoinSelector<'_>) -> Option<Ordf32>;
205+
fn score<C>(&mut self, cs: &CoinSelector<'_, C>) -> Option<Ordf32>;
206206

207207
/// Get the lower bound score using a heuristic.
208208
///
@@ -211,7 +211,7 @@ pub trait BnbMetric {
211211
///
212212
/// If this returns `None`, the current branch and all descendant branches will not have valid
213213
/// solutions.
214-
fn bound(&mut self, cs: &CoinSelector<'_>) -> Option<Ordf32>;
214+
fn bound<C>(&mut self, cs: &CoinSelector<'_, C>) -> Option<Ordf32>;
215215

216216
/// Returns whether the metric requies we order candidates by descending value per weight unit.
217217
fn requires_ordering_by_descending_value_pwu(&self) -> bool {

src/coin_selector.rs

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,49 @@ use alloc::{borrow::Cow, collections::BTreeSet, vec::Vec};
1111
///
1212
/// [`select`]: CoinSelector::select
1313
/// [`bnb_solutions`]: CoinSelector::bnb_solutions
14-
#[derive(Debug, Clone)]
15-
pub struct CoinSelector<'a> {
16-
candidates: &'a [Candidate],
14+
#[derive(Debug)]
15+
pub struct CoinSelector<'a, C> {
16+
inputs: &'a [C],
17+
candidates: Cow<'a, [Candidate]>,
1718
selected: Cow<'a, BTreeSet<usize>>,
1819
banned: Cow<'a, BTreeSet<usize>>,
1920
candidate_order: Cow<'a, Vec<usize>>,
2021
}
2122

22-
impl<'a> CoinSelector<'a> {
23-
/// Creates a new coin selector from some candidate inputs and a `base_weight`.
24-
///
25-
/// The `base_weight` is the weight of the transaction without any inputs and without a change
26-
/// output.
27-
///
28-
/// The `CoinSelector` does not keep track of the final transaction's output count. The caller
29-
/// is responsible for including the potential output-count varint weight change in the
30-
/// corresponding [`DrainWeights`].
23+
impl<'a, C> Clone for CoinSelector<'a, C> {
24+
fn clone(&self) -> Self {
25+
Self {
26+
inputs: self.inputs,
27+
candidates: self.candidates.clone(),
28+
selected: self.selected.clone(),
29+
banned: self.banned.clone(),
30+
candidate_order: self.candidate_order.clone(),
31+
}
32+
}
33+
}
34+
35+
impl<'a, C> CoinSelector<'a, C> {
36+
/// Create a coin selector from raw `inputs` and a `to_candidate` closure.
3137
///
32-
/// Note that methods in `CoinSelector` will refer to inputs by the index in the `candidates`
33-
/// slice you pass in.
34-
pub fn new(candidates: &'a [Candidate]) -> Self {
38+
/// `to_candidate` maps each raw input to a [`Candidate`] representation.
39+
pub fn new<F>(inputs: &'a [C], to_candidate: F) -> Self
40+
where
41+
F: Fn(&C) -> Candidate,
42+
{
3543
Self {
36-
candidates,
44+
inputs,
45+
candidates: inputs.iter().map(to_candidate).collect(),
3746
selected: Cow::Owned(Default::default()),
3847
banned: Cow::Owned(Default::default()),
39-
candidate_order: Cow::Owned((0..candidates.len()).collect()),
48+
candidate_order: Cow::Owned((0..inputs.len()).collect()),
4049
}
4150
}
4251

52+
/// Get a reference to the raw inputs.
53+
pub fn raw_inputs(&self) -> &[C] {
54+
self.inputs
55+
}
56+
4357
/// Iterate over all the candidates in their currently sorted order. Each item has the original
4458
/// index with the candidate.
4559
pub fn candidates(
@@ -62,11 +76,39 @@ impl<'a> CoinSelector<'a> {
6276
self.selected.to_mut().remove(&index)
6377
}
6478

65-
/// Convienince method to pick elements of a slice by the indexes that are currently selected.
66-
/// Obviously the slice must represent the inputs ordered in the same way as when they were
67-
/// passed to `Candidates::new`.
68-
pub fn apply_selection<T>(&self, candidates: &'a [T]) -> impl Iterator<Item = &'a T> + '_ {
69-
self.selected.iter().map(move |i| &candidates[*i])
79+
/// Apply the current coin selection.
80+
///
81+
/// `apply_action` is a closure that is meant to construct an unsigned transaction based on the
82+
/// current selection. `apply_action` is a [`FnMut`] so it can mutate a structure of the
83+
/// caller's liking (most likely a transaction). The input is a [`FinishAction`], which conveys
84+
/// adding inputs or outputs.
85+
///
86+
/// # Errors
87+
///
88+
/// The selection must satisfy `target` otherwise an [`InsufficientFunds`] error is returned.
89+
pub fn finish<F>(
90+
self,
91+
target: Target,
92+
change_policy: ChangePolicy,
93+
mut apply_action: F,
94+
) -> Result<(), InsufficientFunds>
95+
where
96+
F: FnMut(FinishAction<'a, C>),
97+
{
98+
let excess = self.excess(target, Drain::NONE);
99+
if excess < 0 {
100+
let missing = excess.unsigned_abs();
101+
return Err(InsufficientFunds { missing });
102+
}
103+
let drain = self.drain(target, change_policy);
104+
for i in self.selected.iter().copied() {
105+
apply_action(FinishAction::Input(&self.inputs[i]));
106+
}
107+
apply_action(FinishAction::TargetOutput(target));
108+
if drain.is_some() {
109+
apply_action(FinishAction::DrainOutput(drain));
110+
}
111+
Ok(())
70112
}
71113

72114
/// Select the input at `index`. `index` refers to its position in the original `candidates`
@@ -331,7 +373,7 @@ impl<'a> CoinSelector<'a> {
331373
let mut excess_waste = self.excess(target, drain).max(0) as f32;
332374
// we allow caller to discount this waste depending on how wasteful excess actually is
333375
// to them.
334-
excess_waste *= excess_discount.max(0.0).min(1.0);
376+
excess_waste *= excess_discount.clamp(0.0, 1.0);
335377
waste += excess_waste;
336378
} else {
337379
waste +=
@@ -489,7 +531,7 @@ impl<'a> CoinSelector<'a> {
489531
#[must_use]
490532
pub fn select_until(
491533
&mut self,
492-
mut predicate: impl FnMut(&CoinSelector<'a>) -> bool,
534+
mut predicate: impl FnMut(&CoinSelector<'a, C>) -> bool,
493535
) -> Option<()> {
494536
loop {
495537
if predicate(&*self) {
@@ -503,7 +545,7 @@ impl<'a> CoinSelector<'a> {
503545
}
504546

505547
/// Return an iterator that can be used to select candidates.
506-
pub fn select_iter(self) -> SelectIter<'a> {
548+
pub fn select_iter(self) -> SelectIter<'a, C> {
507549
SelectIter { cs: self.clone() }
508550
}
509551

@@ -517,7 +559,7 @@ impl<'a> CoinSelector<'a> {
517559
pub fn bnb_solutions<M: BnbMetric>(
518560
&self,
519561
metric: M,
520-
) -> impl Iterator<Item = Option<(CoinSelector<'a>, Ordf32)>> {
562+
) -> impl Iterator<Item = Option<(CoinSelector<'a, C>, Ordf32)>> {
521563
crate::bnb::BnbIter::new(self.clone(), metric)
522564
}
523565

@@ -545,7 +587,7 @@ impl<'a> CoinSelector<'a> {
545587
}
546588
}
547589

548-
impl<'a> core::fmt::Display for CoinSelector<'a> {
590+
impl<'a, C> core::fmt::Display for CoinSelector<'a, C> {
549591
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
550592
write!(f, "[")?;
551593
let mut candidates = self.candidates().peekable();
@@ -572,12 +614,12 @@ impl<'a> core::fmt::Display for CoinSelector<'a> {
572614
/// The `SelectIter` allows you to select candidates by calling [`Iterator::next`].
573615
///
574616
/// The [`Iterator::Item`] is a tuple of `(selector, last_selected_index, last_selected_candidate)`.
575-
pub struct SelectIter<'a> {
576-
cs: CoinSelector<'a>,
617+
pub struct SelectIter<'a, C> {
618+
cs: CoinSelector<'a, C>,
577619
}
578620

579-
impl<'a> Iterator for SelectIter<'a> {
580-
type Item = (CoinSelector<'a>, usize, Candidate);
621+
impl<'a, C> Iterator for SelectIter<'a, C> {
622+
type Item = (CoinSelector<'a, C>, usize, Candidate);
581623

582624
fn next(&mut self) -> Option<Self::Item> {
583625
let (index, wv) = self.cs.unselected().next()?;
@@ -586,7 +628,7 @@ impl<'a> Iterator for SelectIter<'a> {
586628
}
587629
}
588630

589-
impl<'a> DoubleEndedIterator for SelectIter<'a> {
631+
impl<'a, C> DoubleEndedIterator for SelectIter<'a, C> {
590632
fn next_back(&mut self) -> Option<Self::Item> {
591633
let (index, wv) = self.cs.unselected().next_back()?;
592634
self.cs.select(index);
@@ -632,6 +674,18 @@ impl core::fmt::Display for NoBnbSolution {
632674
#[cfg(feature = "std")]
633675
impl std::error::Error for NoBnbSolution {}
634676

677+
/// Action to apply on a transaction.
678+
///
679+
/// This is used in [`CoinSelector::finish`] to populate a transaction with the current selection.
680+
pub enum FinishAction<'a, C> {
681+
/// Input to add to the transaction.
682+
Input(&'a C),
683+
/// Recipient output to add to the transaction.
684+
TargetOutput(Target),
685+
/// Drain (change) output to add to the transction.
686+
DrainOutput(Drain),
687+
}
688+
635689
/// A `Candidate` represents an input candidate for [`CoinSelector`].
636690
///
637691
/// This can either be a single UTXO, or a group of UTXOs that should be spent together.

src/metrics.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub use changeless::*;
1414
//
1515
// NOTE: this should stay private because it requires cs to be sorted such that all negative
1616
// effective value candidates are next to each other.
17-
fn change_lower_bound(cs: &CoinSelector, target: Target, change_policy: ChangePolicy) -> Drain {
17+
fn change_lower_bound<C>(cs: &CoinSelector<C>, target: Target, change_policy: ChangePolicy) -> Drain {
1818
let has_change_now = cs.drain_value(target, change_policy).is_some();
1919

2020
if has_change_now {
@@ -38,7 +38,7 @@ macro_rules! impl_for_tuple {
3838
where $($a: BnbMetric),*
3939
{
4040
#[allow(unused)]
41-
fn score(&mut self, cs: &CoinSelector<'_>) -> Option<crate::float::Ordf32> {
41+
fn score<C>(&mut self, cs: &CoinSelector<'_, C>) -> Option<crate::float::Ordf32> {
4242
let mut acc = Option::<f32>::None;
4343
for (score, ratio) in [$((self.$b.0.score(cs)?, self.$b.1)),*] {
4444
let score: Ordf32 = score;
@@ -51,7 +51,7 @@ macro_rules! impl_for_tuple {
5151
acc.map(Ordf32)
5252
}
5353
#[allow(unused)]
54-
fn bound(&mut self, cs: &CoinSelector<'_>) -> Option<crate::float::Ordf32> {
54+
fn bound<C>(&mut self, cs: &CoinSelector<'_, C>) -> Option<crate::float::Ordf32> {
5555
let mut acc = Option::<f32>::None;
5656
for (score, ratio) in [$((self.$b.0.bound(cs)?, self.$b.1)),*] {
5757
let score: Ordf32 = score;
@@ -72,7 +72,7 @@ macro_rules! impl_for_tuple {
7272
}
7373

7474
impl_for_tuple!();
75-
impl_for_tuple!(A 0 B 1);
76-
impl_for_tuple!(A 0 B 1 C 2);
77-
impl_for_tuple!(A 0 B 1 C 2 D 3);
78-
impl_for_tuple!(A 0 B 1 C 2 D 3 E 4);
75+
impl_for_tuple!(TA 0 TB 1);
76+
impl_for_tuple!(TA 0 TB 1 TC 2);
77+
impl_for_tuple!(TA 0 TB 1 TC 2 TD 3);
78+
impl_for_tuple!(TA 0 TB 1 TC 2 TD 3 TE 4);

src/metrics/changeless.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub struct Changeless {
1111
}
1212

1313
impl BnbMetric for Changeless {
14-
fn score(&mut self, cs: &CoinSelector<'_>) -> Option<Ordf32> {
14+
fn score<C>(&mut self, cs: &CoinSelector<'_, C>) -> Option<Ordf32> {
1515
if cs.is_target_met(self.target)
1616
&& cs.drain_value(self.target, self.change_policy).is_none()
1717
{
@@ -21,7 +21,7 @@ impl BnbMetric for Changeless {
2121
}
2222
}
2323

24-
fn bound(&mut self, cs: &CoinSelector<'_>) -> Option<Ordf32> {
24+
fn bound<C>(&mut self, cs: &CoinSelector<'_, C>) -> Option<Ordf32> {
2525
if change_lower_bound(cs, self.target, self.change_policy).is_some() {
2626
None
2727
} else {

src/metrics/lowest_fee.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub struct LowestFee {
2323
}
2424

2525
impl BnbMetric for LowestFee {
26-
fn score(&mut self, cs: &CoinSelector<'_>) -> Option<Ordf32> {
26+
fn score<C>(&mut self, cs: &CoinSelector<'_, C>) -> Option<Ordf32> {
2727
if !cs.is_target_met(self.target) {
2828
return None;
2929
}
@@ -44,7 +44,7 @@ impl BnbMetric for LowestFee {
4444
Some(Ordf32(long_term_fee as f32))
4545
}
4646

47-
fn bound(&mut self, cs: &CoinSelector<'_>) -> Option<Ordf32> {
47+
fn bound<C>(&mut self, cs: &CoinSelector<'_, C>) -> Option<Ordf32> {
4848
if cs.is_target_met(self.target) {
4949
let current_score = self.score(cs).unwrap();
5050

0 commit comments

Comments
 (0)