diff --git a/Cargo.toml b/Cargo.toml index 8573a6a..f23acd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,4 +23,4 @@ std = [] [dev-dependencies] rand = "0.8" proptest = "1.4" -bitcoin = "0.30" +bitcoin = "0.32" diff --git a/src/bnb.rs b/src/bnb.rs index 5df7754..4f525c0 100644 --- a/src/bnb.rs +++ b/src/bnb.rs @@ -8,15 +8,15 @@ use alloc::collections::BinaryHeap; /// An [`Iterator`] that iterates over rounds of branch and bound to minimize the score of the /// provided [`BnbMetric`]. #[derive(Debug)] -pub(crate) struct BnbIter<'a, M: BnbMetric> { - queue: BinaryHeap>, +pub(crate) struct BnbIter<'a, C, M: BnbMetric> { + queue: BinaryHeap>, best: Option, /// The `BnBMetric` that will score each selection metric: M, } -impl<'a, M: BnbMetric> Iterator for BnbIter<'a, M> { - type Item = Option<(CoinSelector<'a>, Ordf32)>; +impl<'a, C, M: BnbMetric> Iterator for BnbIter<'a, C, M> { + type Item = Option<(CoinSelector<'a, C>, Ordf32)>; fn next(&mut self) -> Option { // { @@ -70,8 +70,8 @@ impl<'a, M: BnbMetric> Iterator for BnbIter<'a, M> { } } -impl<'a, M: BnbMetric> BnbIter<'a, M> { - pub(crate) fn new(mut selector: CoinSelector<'a>, metric: M) -> Self { +impl<'a, C, M: BnbMetric> BnbIter<'a, C, M> { + pub(crate) fn new(mut selector: CoinSelector<'a, C>, metric: M) -> Self { let mut iter = BnbIter { queue: BinaryHeap::default(), best: None, @@ -87,7 +87,7 @@ impl<'a, M: BnbMetric> BnbIter<'a, M> { iter } - fn consider_adding_to_queue(&mut self, cs: &CoinSelector<'a>, is_exclusion: bool) { + fn consider_adding_to_queue(&mut self, cs: &CoinSelector<'a, C>, is_exclusion: bool) { let bound = self.metric.bound(cs); if let Some(bound) = bound { let is_good_enough = match self.best { @@ -127,7 +127,7 @@ impl<'a, M: BnbMetric> BnbIter<'a, M> { }*/ } - fn insert_new_branches(&mut self, cs: &CoinSelector<'a>) { + fn insert_new_branches(&mut self, cs: &CoinSelector<'a, C>) { let (next_index, next) = match cs.unselected().next() { Some(c) => c, None => return, // exhausted @@ -161,13 +161,13 @@ impl<'a, M: BnbMetric> BnbIter<'a, M> { } #[derive(Debug, Clone)] -struct Branch<'a> { +struct Branch<'a, C> { lower_bound: Ordf32, - selector: CoinSelector<'a>, + selector: CoinSelector<'a, C>, is_exclusion: bool, } -impl<'a> Ord for Branch<'a> { +impl<'a, C> Ord for Branch<'a, C> { fn cmp(&self, other: &Self) -> core::cmp::Ordering { // NOTE: Reverse comparision `lower_bound` because we want a min-heap (by default BinaryHeap // is a max-heap). @@ -181,19 +181,19 @@ impl<'a> Ord for Branch<'a> { } } -impl<'a> PartialOrd for Branch<'a> { +impl<'a, C> PartialOrd for Branch<'a, C> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl<'a> PartialEq for Branch<'a> { +impl<'a, C> PartialEq for Branch<'a, C> { fn eq(&self, other: &Self) -> bool { self.lower_bound == other.lower_bound } } -impl<'a> Eq for Branch<'a> {} +impl<'a, C> Eq for Branch<'a, C> {} /// A branch and bound metric where we minimize the [`Ordf32`] score. /// @@ -202,7 +202,7 @@ pub trait BnbMetric { /// Get the score of a given selection. /// /// If this returns `None`, the selection is invalid. - fn score(&mut self, cs: &CoinSelector<'_>) -> Option; + fn score(&mut self, cs: &CoinSelector<'_, C>) -> Option; /// Get the lower bound score using a heuristic. /// @@ -211,7 +211,7 @@ pub trait BnbMetric { /// /// If this returns `None`, the current branch and all descendant branches will not have valid /// solutions. - fn bound(&mut self, cs: &CoinSelector<'_>) -> Option; + fn bound(&mut self, cs: &CoinSelector<'_, C>) -> Option; /// Returns whether the metric requies we order candidates by descending value per weight unit. fn requires_ordering_by_descending_value_pwu(&self) -> bool { diff --git a/src/coin_selector.rs b/src/coin_selector.rs index e23729a..08b799d 100644 --- a/src/coin_selector.rs +++ b/src/coin_selector.rs @@ -11,35 +11,49 @@ use alloc::{borrow::Cow, collections::BTreeSet, vec::Vec}; /// /// [`select`]: CoinSelector::select /// [`bnb_solutions`]: CoinSelector::bnb_solutions -#[derive(Debug, Clone)] -pub struct CoinSelector<'a> { - candidates: &'a [Candidate], +#[derive(Debug)] +pub struct CoinSelector<'a, C> { + inputs: &'a [C], + candidates: Cow<'a, [Candidate]>, selected: Cow<'a, BTreeSet>, banned: Cow<'a, BTreeSet>, candidate_order: Cow<'a, Vec>, } -impl<'a> CoinSelector<'a> { - /// Creates a new coin selector from some candidate inputs and a `base_weight`. - /// - /// The `base_weight` is the weight of the transaction without any inputs and without a change - /// output. - /// - /// The `CoinSelector` does not keep track of the final transaction's output count. The caller - /// is responsible for including the potential output-count varint weight change in the - /// corresponding [`DrainWeights`]. +impl<'a, C> Clone for CoinSelector<'a, C> { + fn clone(&self) -> Self { + Self { + inputs: self.inputs, + candidates: self.candidates.clone(), + selected: self.selected.clone(), + banned: self.banned.clone(), + candidate_order: self.candidate_order.clone(), + } + } +} + +impl<'a, C> CoinSelector<'a, C> { + /// Create a coin selector from raw `inputs` and a `to_candidate` closure. /// - /// Note that methods in `CoinSelector` will refer to inputs by the index in the `candidates` - /// slice you pass in. - pub fn new(candidates: &'a [Candidate]) -> Self { + /// `to_candidate` maps each raw input to a [`Candidate`] representation. + pub fn new(inputs: &'a [C], to_candidate: F) -> Self + where + F: Fn(&C) -> Candidate, + { Self { - candidates, + inputs, + candidates: inputs.iter().map(to_candidate).collect(), selected: Cow::Owned(Default::default()), banned: Cow::Owned(Default::default()), - candidate_order: Cow::Owned((0..candidates.len()).collect()), + candidate_order: Cow::Owned((0..inputs.len()).collect()), } } + /// Get a reference to the raw inputs. + pub fn raw_inputs(&self) -> &[C] { + self.inputs + } + /// Iterate over all the candidates in their currently sorted order. Each item has the original /// index with the candidate. pub fn candidates( @@ -62,11 +76,39 @@ impl<'a> CoinSelector<'a> { self.selected.to_mut().remove(&index) } - /// Convienince method to pick elements of a slice by the indexes that are currently selected. - /// Obviously the slice must represent the inputs ordered in the same way as when they were - /// passed to `Candidates::new`. - pub fn apply_selection(&self, candidates: &'a [T]) -> impl Iterator + '_ { - self.selected.iter().map(move |i| &candidates[*i]) + /// Apply the current coin selection. + /// + /// `apply_action` is a closure that is meant to construct an unsigned transaction based on the + /// current selection. `apply_action` is a [`FnMut`] so it can mutate a structure of the + /// caller's liking (most likely a transaction). The input is a [`FinishAction`], which conveys + /// adding inputs or outputs. + /// + /// # Errors + /// + /// The selection must satisfy `target` otherwise an [`InsufficientFunds`] error is returned. + pub fn finish( + self, + target: Target, + change_policy: ChangePolicy, + mut apply_action: F, + ) -> Result<(), InsufficientFunds> + where + F: FnMut(FinishAction<'a, C>), + { + let excess = self.excess(target, Drain::NONE); + if excess < 0 { + let missing = excess.unsigned_abs(); + return Err(InsufficientFunds { missing }); + } + let drain = self.drain(target, change_policy); + for i in self.selected.iter().copied() { + apply_action(FinishAction::Input(&self.inputs[i])); + } + apply_action(FinishAction::TargetOutput(target)); + if drain.is_some() { + apply_action(FinishAction::DrainOutput(drain)); + } + Ok(()) } /// Select the input at `index`. `index` refers to its position in the original `candidates` @@ -331,7 +373,7 @@ impl<'a> CoinSelector<'a> { let mut excess_waste = self.excess(target, drain).max(0) as f32; // we allow caller to discount this waste depending on how wasteful excess actually is // to them. - excess_waste *= excess_discount.max(0.0).min(1.0); + excess_waste *= excess_discount.clamp(0.0, 1.0); waste += excess_waste; } else { waste += @@ -489,7 +531,7 @@ impl<'a> CoinSelector<'a> { #[must_use] pub fn select_until( &mut self, - mut predicate: impl FnMut(&CoinSelector<'a>) -> bool, + mut predicate: impl FnMut(&CoinSelector<'a, C>) -> bool, ) -> Option<()> { loop { if predicate(&*self) { @@ -503,7 +545,7 @@ impl<'a> CoinSelector<'a> { } /// Return an iterator that can be used to select candidates. - pub fn select_iter(self) -> SelectIter<'a> { + pub fn select_iter(self) -> SelectIter<'a, C> { SelectIter { cs: self.clone() } } @@ -517,7 +559,7 @@ impl<'a> CoinSelector<'a> { pub fn bnb_solutions( &self, metric: M, - ) -> impl Iterator, Ordf32)>> { + ) -> impl Iterator, Ordf32)>> { crate::bnb::BnbIter::new(self.clone(), metric) } @@ -545,7 +587,7 @@ impl<'a> CoinSelector<'a> { } } -impl<'a> core::fmt::Display for CoinSelector<'a> { +impl<'a, C> core::fmt::Display for CoinSelector<'a, C> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "[")?; let mut candidates = self.candidates().peekable(); @@ -572,12 +614,12 @@ impl<'a> core::fmt::Display for CoinSelector<'a> { /// The `SelectIter` allows you to select candidates by calling [`Iterator::next`]. /// /// The [`Iterator::Item`] is a tuple of `(selector, last_selected_index, last_selected_candidate)`. -pub struct SelectIter<'a> { - cs: CoinSelector<'a>, +pub struct SelectIter<'a, C> { + cs: CoinSelector<'a, C>, } -impl<'a> Iterator for SelectIter<'a> { - type Item = (CoinSelector<'a>, usize, Candidate); +impl<'a, C> Iterator for SelectIter<'a, C> { + type Item = (CoinSelector<'a, C>, usize, Candidate); fn next(&mut self) -> Option { let (index, wv) = self.cs.unselected().next()?; @@ -586,7 +628,7 @@ impl<'a> Iterator for SelectIter<'a> { } } -impl<'a> DoubleEndedIterator for SelectIter<'a> { +impl<'a, C> DoubleEndedIterator for SelectIter<'a, C> { fn next_back(&mut self) -> Option { let (index, wv) = self.cs.unselected().next_back()?; self.cs.select(index); @@ -632,6 +674,18 @@ impl core::fmt::Display for NoBnbSolution { #[cfg(feature = "std")] impl std::error::Error for NoBnbSolution {} +/// Action to apply on a transaction. +/// +/// This is used in [`CoinSelector::finish`] to populate a transaction with the current selection. +pub enum FinishAction<'a, C> { + /// Input to add to the transaction. + Input(&'a C), + /// Recipient output to add to the transaction. + TargetOutput(Target), + /// Drain (change) output to add to the transction. + DrainOutput(Drain), +} + /// A `Candidate` represents an input candidate for [`CoinSelector`]. /// /// This can either be a single UTXO, or a group of UTXOs that should be spent together. diff --git a/src/metrics.rs b/src/metrics.rs index 4e7d90d..9362c38 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -14,7 +14,11 @@ pub use changeless::*; // // NOTE: this should stay private because it requires cs to be sorted such that all negative // effective value candidates are next to each other. -fn change_lower_bound(cs: &CoinSelector, target: Target, change_policy: ChangePolicy) -> Drain { +fn change_lower_bound( + cs: &CoinSelector, + target: Target, + change_policy: ChangePolicy, +) -> Drain { let has_change_now = cs.drain_value(target, change_policy).is_some(); if has_change_now { @@ -38,7 +42,7 @@ macro_rules! impl_for_tuple { where $($a: BnbMetric),* { #[allow(unused)] - fn score(&mut self, cs: &CoinSelector<'_>) -> Option { + fn score(&mut self, cs: &CoinSelector<'_, C>) -> Option { let mut acc = Option::::None; for (score, ratio) in [$((self.$b.0.score(cs)?, self.$b.1)),*] { let score: Ordf32 = score; @@ -51,7 +55,7 @@ macro_rules! impl_for_tuple { acc.map(Ordf32) } #[allow(unused)] - fn bound(&mut self, cs: &CoinSelector<'_>) -> Option { + fn bound(&mut self, cs: &CoinSelector<'_, C>) -> Option { let mut acc = Option::::None; for (score, ratio) in [$((self.$b.0.bound(cs)?, self.$b.1)),*] { let score: Ordf32 = score; @@ -72,7 +76,7 @@ macro_rules! impl_for_tuple { } impl_for_tuple!(); -impl_for_tuple!(A 0 B 1); -impl_for_tuple!(A 0 B 1 C 2); -impl_for_tuple!(A 0 B 1 C 2 D 3); -impl_for_tuple!(A 0 B 1 C 2 D 3 E 4); +impl_for_tuple!(TA 0 TB 1); +impl_for_tuple!(TA 0 TB 1 TC 2); +impl_for_tuple!(TA 0 TB 1 TC 2 TD 3); +impl_for_tuple!(TA 0 TB 1 TC 2 TD 3 TE 4); diff --git a/src/metrics/changeless.rs b/src/metrics/changeless.rs index eddaa10..755ae07 100644 --- a/src/metrics/changeless.rs +++ b/src/metrics/changeless.rs @@ -11,7 +11,7 @@ pub struct Changeless { } impl BnbMetric for Changeless { - fn score(&mut self, cs: &CoinSelector<'_>) -> Option { + fn score(&mut self, cs: &CoinSelector<'_, C>) -> Option { if cs.is_target_met(self.target) && cs.drain_value(self.target, self.change_policy).is_none() { @@ -21,7 +21,7 @@ impl BnbMetric for Changeless { } } - fn bound(&mut self, cs: &CoinSelector<'_>) -> Option { + fn bound(&mut self, cs: &CoinSelector<'_, C>) -> Option { if change_lower_bound(cs, self.target, self.change_policy).is_some() { None } else { diff --git a/src/metrics/lowest_fee.rs b/src/metrics/lowest_fee.rs index 9e3e569..601eafb 100644 --- a/src/metrics/lowest_fee.rs +++ b/src/metrics/lowest_fee.rs @@ -23,7 +23,7 @@ pub struct LowestFee { } impl BnbMetric for LowestFee { - fn score(&mut self, cs: &CoinSelector<'_>) -> Option { + fn score(&mut self, cs: &CoinSelector<'_, C>) -> Option { if !cs.is_target_met(self.target) { return None; } @@ -44,7 +44,7 @@ impl BnbMetric for LowestFee { Some(Ordf32(long_term_fee as f32)) } - fn bound(&mut self, cs: &CoinSelector<'_>) -> Option { + fn bound(&mut self, cs: &CoinSelector<'_, C>) -> Option { if cs.is_target_met(self.target) { let current_score = self.score(cs).unwrap(); diff --git a/src/target.rs b/src/target.rs index 6c4e65e..8089420 100644 --- a/src/target.rs +++ b/src/target.rs @@ -72,10 +72,10 @@ impl TargetOutputs { /// There are two orthogonal constraints: /// /// - `rate`: The feerate of the transaction must at least be this high. You set this to control how -/// quickly your transaction is confirmed. Typically a coin selection will try and hit this target -/// exactly but it might go over if the `replace` constraint takes precedence or if the -/// [`ChangePolicy`] determines that the excess value should just be given to miners (rather than -/// create a change output). +/// quickly your transaction is confirmed. Typically a coin selection will try and hit this target +/// exactly but it might go over if the `replace` constraint takes precedence or if the +/// [`ChangePolicy`] determines that the excess value should just be given to miners (rather than +/// create a change output). /// - `replace`: The selection must have a high enough fee to satisfy [RBF rule 4] /// /// [RBF rule 4]: https://github.com/bitcoin/bitcoin/blob/master/doc/policy/mempool-replacements.md#current-replace-by-fee-policy diff --git a/tests/bnb.rs b/tests/bnb.rs index cab483b..9ced44e 100644 --- a/tests/bnb.rs +++ b/tests/bnb.rs @@ -34,7 +34,7 @@ struct MinExcessThenWeight { const EXCESS_RATIO: f32 = 1_000_000_f32; impl BnbMetric for MinExcessThenWeight { - fn score(&mut self, cs: &CoinSelector<'_>) -> Option { + fn score(&mut self, cs: &CoinSelector<'_, C>) -> Option { let excess = cs.excess(self.target, Drain::NONE); if excess < 0 { None @@ -45,7 +45,7 @@ impl BnbMetric for MinExcessThenWeight { } } - fn bound(&mut self, cs: &CoinSelector<'_>) -> Option { + fn bound(&mut self, cs: &CoinSelector<'_, C>) -> Option { let mut cs = cs.clone(); cs.select_until_target_met(self.target).ok()?; Some(Ordf32(cs.input_weight() as f32)) @@ -67,7 +67,7 @@ fn bnb_finds_an_exact_solution_in_n_iter() { let solution: Vec = (0..solution_len).map(|_| wv.next().unwrap()).collect(); let solution_weight = { - let mut cs = CoinSelector::new(&solution); + let mut cs = CoinSelector::new(&solution, common::c_to_c); cs.select_all(); cs.input_weight() }; @@ -78,7 +78,7 @@ fn bnb_finds_an_exact_solution_in_n_iter() { candidates.extend(wv.take(num_additional_canidates)); candidates.sort_unstable_by_key(|wv| core::cmp::Reverse(wv.value)); - let cs = CoinSelector::new(&candidates); + let cs = CoinSelector::new(&candidates, common::c_to_c); let target = Target { outputs: TargetOutputs { @@ -113,7 +113,7 @@ fn bnb_finds_solution_if_possible_in_n_iter() { let wv = test_wv(&mut rng); let candidates = wv.take(num_inputs).collect::>(); - let cs = CoinSelector::new(&candidates); + let cs = CoinSelector::new(&candidates, common::c_to_c); let target = Target { outputs: TargetOutputs { @@ -145,7 +145,7 @@ proptest! { let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); let wv = test_wv(&mut rng); let candidates = wv.take(num_inputs).collect::>(); - let cs = CoinSelector::new(&candidates); + let cs = CoinSelector::new(&candidates, common::c_to_c); let target = Target { outputs: TargetOutputs { value_sum: target_value, weight_sum: 0, n_outputs: 1 }, diff --git a/tests/common.rs b/tests/common.rs index 642a42f..3eb5a11 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -25,6 +25,10 @@ pub fn maybe_replace( proptest::option::of(replace(fee_strategy)) } +pub fn c_to_c(c: &Candidate) -> Candidate { + *c +} + /// Used for constructing a proptest that compares an exhaustive search result with a bnb result /// with the given metric. /// @@ -45,7 +49,7 @@ where let target = params.target(); - let mut selection = CoinSelector::new(&candidates); + let mut selection = CoinSelector::new(&candidates, c_to_c); let mut exp_selection = selection.clone(); if metric.requires_ordering_by_descending_value_pwu() { @@ -142,7 +146,7 @@ where let target = params.target(); let init_cs = { - let mut cs = CoinSelector::new(&candidates); + let mut cs = CoinSelector::new(&candidates, c_to_c); if metric.requires_ordering_by_descending_value_pwu() { cs.sort_candidates_by_descending_value_pwu(); } @@ -259,7 +263,7 @@ pub fn gen_candidates(n: usize) -> Vec { .collect() } -pub fn print_candidates(params: &StrategyParams, cs: &CoinSelector<'_>) { +pub fn print_candidates(params: &StrategyParams, cs: &CoinSelector<'_, C>) { println!("\tcandidates:"); for (i, candidate) in cs.candidates() { println!( @@ -273,18 +277,18 @@ pub fn print_candidates(params: &StrategyParams, cs: &CoinSelector<'_>) { } } -pub struct ExhaustiveIter<'a> { - stack: Vec<(CoinSelector<'a>, bool)>, // for branches: (cs, this_index, include?) +pub struct ExhaustiveIter<'a, C> { + stack: Vec<(CoinSelector<'a, C>, bool)>, // for branches: (cs, this_index, include?) } -impl<'a> ExhaustiveIter<'a> { - fn new(cs: &CoinSelector<'a>) -> Option { +impl<'a, C> ExhaustiveIter<'a, C> { + fn new(cs: &CoinSelector<'a, C>) -> Option { let mut iter = Self { stack: Vec::new() }; iter.push_branches(cs); Some(iter) } - fn push_branches(&mut self, cs: &CoinSelector<'a>) { + fn push_branches(&mut self, cs: &CoinSelector<'a, C>) { let next_index = match cs.unselected_indices().next() { Some(next_index) => next_index, None => return, @@ -306,8 +310,8 @@ impl<'a> ExhaustiveIter<'a> { } } -impl<'a> Iterator for ExhaustiveIter<'a> { - type Item = (CoinSelector<'a>, bool); +impl<'a, C> Iterator for ExhaustiveIter<'a, C> { + type Item = (CoinSelector<'a, C>, bool); fn next(&mut self) -> Option { let (cs, inclusion) = self.stack.pop()?; @@ -316,7 +320,7 @@ impl<'a> Iterator for ExhaustiveIter<'a> { } } -pub fn exhaustive_search(cs: &mut CoinSelector, metric: &mut M) -> Option<(Ordf32, usize)> +pub fn exhaustive_search(cs: &mut CoinSelector, metric: &mut M) -> Option<(Ordf32, usize)> where M: BnbMetric, { @@ -324,7 +328,7 @@ where cs.sort_candidates_by_descending_value_pwu(); } - let mut best = Option::<(CoinSelector, Ordf32)>::None; + let mut best = Option::<(CoinSelector, Ordf32)>::None; let mut rounds = 0; let iter = ExhaustiveIter::new(cs)? @@ -353,8 +357,8 @@ where best.map(|(_, score)| (score, rounds)) } -pub fn bnb_search( - cs: &mut CoinSelector, +pub fn bnb_search( + cs: &mut CoinSelector, metric: M, max_rounds: usize, ) -> Result<(Ordf32, usize), NoBnbSolution> @@ -402,7 +406,7 @@ pub fn compare_against_benchmarks( let start = std::time::Instant::now(); let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); let target = params.target(); - let cs = CoinSelector::new(&candidates); + let cs = CoinSelector::new(&candidates, c_to_c); let solutions = cs.bnb_solutions(metric.clone()); let best = solutions @@ -465,13 +469,13 @@ pub fn compare_against_benchmarks( } #[allow(unused)] -fn randomly_satisfy_target<'a>( - cs: &CoinSelector<'a>, +fn randomly_satisfy_target<'a, C>( + cs: &CoinSelector<'a, C>, target: Target, change_policy: ChangePolicy, rng: &mut impl RngCore, mut metric: impl BnbMetric, -) -> CoinSelector<'a> { +) -> CoinSelector<'a, C> { let mut cs = cs.clone(); let mut last_score: Option = None; diff --git a/tests/lowest_fee.rs b/tests/lowest_fee.rs index 3a6bf69..6541bb6 100644 --- a/tests/lowest_fee.rs +++ b/tests/lowest_fee.rs @@ -151,8 +151,8 @@ fn combined_changeless_metric() { }; let candidates = common::gen_candidates(params.n_candidates); - let mut cs_a = CoinSelector::new(&candidates); - let mut cs_b = CoinSelector::new(&candidates); + let mut cs_a = CoinSelector::new(&candidates, common::c_to_c); + let mut cs_b = CoinSelector::new(&candidates, common::c_to_c); let change_policy = ChangePolicy::min_value(params.drain_weights(), params.drain_dust); @@ -216,7 +216,7 @@ fn adding_another_input_to_remove_change() { }, ]; - let mut cs = CoinSelector::new(&candidates); + let mut cs = CoinSelector::new(&candidates, common::c_to_c); let best_solution = { let mut cs = cs.clone(); diff --git a/tests/weight.rs b/tests/weight.rs index b7b3eb2..6f10a19 100644 --- a/tests/weight.rs +++ b/tests/weight.rs @@ -1,5 +1,6 @@ #![allow(clippy::zero_prefixed_literal)] +mod common; use bdk_coin_select::{Candidate, CoinSelector, Drain, DrainWeights, TargetOutputs}; use bitcoin::{consensus::Decodable, ScriptBuf, Transaction}; @@ -24,9 +25,8 @@ pub fn hex_decode(hex: &str) -> Vec { #[test] fn segwit_one_input_one_output() { // FROM https://mempool.space/tx/e627fbb7f775a57fd398bf9b150655d4ac3e1f8afed4255e74ee10d7a345a9cc - let mut tx_bytes = hex_decode("01000000000101b2ec00fd7d3f2c89eb27e3e280960356f69fc88a324a4bca187dd4b020aa36690000000000ffffffff01d0bb9321000000001976a9141dc94fe723f43299c6187094b1dc5a032d47b06888ac024730440220669b764de7e9dcedcba6d6d57c8c761be2acc4e1a66938ceecacaa6d494f582d02202641df89d1758eeeed84290079dd9ad36611c73cd9e381dd090b83f5e5b1422e012103f6544e4ffaff4f8649222003ada5d74bd6d960162bcd85af2b619646c8c45a5298290c00"); - let mut cursor = std::io::Cursor::new(&mut tx_bytes); - let tx = Transaction::consensus_decode(&mut cursor).unwrap(); + let tx_bytes = hex_decode("01000000000101b2ec00fd7d3f2c89eb27e3e280960356f69fc88a324a4bca187dd4b020aa36690000000000ffffffff01d0bb9321000000001976a9141dc94fe723f43299c6187094b1dc5a032d47b06888ac024730440220669b764de7e9dcedcba6d6d57c8c761be2acc4e1a66938ceecacaa6d494f582d02202641df89d1758eeeed84290079dd9ad36611c73cd9e381dd090b83f5e5b1422e012103f6544e4ffaff4f8649222003ada5d74bd6d960162bcd85af2b619646c8c45a5298290c00"); + let tx = Transaction::consensus_decode(&mut tx_bytes.as_slice()).unwrap(); let input_values = vec![563_336_755]; let candidates = tx @@ -35,19 +35,23 @@ fn segwit_one_input_one_output() { .zip(input_values) .map(|(txin, value)| Candidate { value, - weight: txin.segwit_weight() as u32, + weight: txin.segwit_weight().to_wu() as u32, input_count: 1, is_segwit: true, }) .collect::>(); let target_ouputs = TargetOutputs { - value_sum: tx.output.iter().map(|output| output.value).sum(), - weight_sum: tx.output.iter().map(|output| output.weight() as u32).sum(), + value_sum: tx.output.iter().map(|output| output.value.to_sat()).sum(), + weight_sum: tx + .output + .iter() + .map(|output| output.weight().to_wu() as u32) + .sum(), n_outputs: tx.output.len(), }; - let mut coin_selector = CoinSelector::new(&candidates); + let mut coin_selector = CoinSelector::new(&candidates, common::c_to_c); coin_selector.select_all(); assert_eq!( @@ -68,9 +72,9 @@ fn segwit_one_input_one_output() { #[test] fn segwit_two_inputs_one_output() { // FROM https://mempool.space/tx/37d2883bdf1b4c110b54cb624d36ab6a30140f8710ed38a52678260a7685e708 - let mut tx_bytes = hex_decode("020000000001021edcae5160b1ba2370a45ea9342b4c883a8941274539612bddf1c379ba7ecf180700000000ffffffff5c85e19bf4f0e293c0d5f9665cb05d2a55d8bba959edc5ef02075f6a1eb9fc120100000000ffffffff0168ce3000000000001976a9145ff742d992276a1f46e5113dde7382896ff86e2a88ac0247304402202e588db55227e0c24db7f07b65f221ebcae323fb595d13d2e1c360b773d809b0022008d2f57a618bd346cfd031549a3971f22464e3e3308cee340a976f1b47a96f0b012102effbcc87e6c59b810c2fa20b0bc3eb909a20b40b25b091cf005d416b85db8c8402483045022100bdc115b86e9c863279132b4808459cf9b266c8f6a9c14a3dfd956986b807e3320220265833b85197679687c5d5eed1b2637489b34249d44cf5d2d40bc7b514181a51012102077741a668889ce15d59365886375aea47a7691941d7a0d301697edbc773b45b00000000"); - let mut cursor = std::io::Cursor::new(&mut tx_bytes); - let tx = Transaction::consensus_decode(&mut cursor).unwrap(); + let tx_bytes = hex_decode("020000000001021edcae5160b1ba2370a45ea9342b4c883a8941274539612bddf1c379ba7ecf180700000000ffffffff5c85e19bf4f0e293c0d5f9665cb05d2a55d8bba959edc5ef02075f6a1eb9fc120100000000ffffffff0168ce3000000000001976a9145ff742d992276a1f46e5113dde7382896ff86e2a88ac0247304402202e588db55227e0c24db7f07b65f221ebcae323fb595d13d2e1c360b773d809b0022008d2f57a618bd346cfd031549a3971f22464e3e3308cee340a976f1b47a96f0b012102effbcc87e6c59b810c2fa20b0bc3eb909a20b40b25b091cf005d416b85db8c8402483045022100bdc115b86e9c863279132b4808459cf9b266c8f6a9c14a3dfd956986b807e3320220265833b85197679687c5d5eed1b2637489b34249d44cf5d2d40bc7b514181a51012102077741a668889ce15d59365886375aea47a7691941d7a0d301697edbc773b45b00000000"); + // let mut cursor = std::io::Cursor::new(&mut tx_bytes); + let tx = Transaction::consensus_decode(&mut tx_bytes.as_slice()).unwrap(); let input_values = vec![003_194_967, 000_014_068]; let candidates = tx @@ -79,17 +83,21 @@ fn segwit_two_inputs_one_output() { .zip(input_values) .map(|(txin, value)| Candidate { value, - weight: txin.segwit_weight() as u32, + weight: txin.segwit_weight().to_wu() as u32, input_count: 1, is_segwit: true, }) .collect::>(); - let mut coin_selector = CoinSelector::new(&candidates); + let mut coin_selector = CoinSelector::new(&candidates, common::c_to_c); let target_ouputs = TargetOutputs { - value_sum: tx.output.iter().map(|output| output.value).sum(), - weight_sum: tx.output.iter().map(|output| output.weight() as u32).sum(), + value_sum: tx.output.iter().map(|output| output.value.to_sat()).sum(), + weight_sum: tx + .output + .iter() + .map(|output| output.weight().to_wu() as u32) + .sum(), n_outputs: tx.output.len(), }; @@ -113,9 +121,8 @@ fn segwit_two_inputs_one_output() { #[test] fn legacy_three_inputs() { // FROM https://mempool.space/tx/5f231df4f73694b3cca9211e336451c20dab136e0a843c2e3166cdcb093e91f4 - let mut tx_bytes = hex_decode("0100000003fe785783e14669f638ba902c26e8e3d7036fb183237bc00f8a10542191c7171300000000fdfd00004730440220418996f20477d143d02ad47e74e5949641b6c2904159ab7c592d2cfc659f9bd802205b18f18ac86b714971f84a8b74a4cb14ad5c1a5b9d0d939bb32c6ae4032f4ea10148304502210091296ff8dd87b5ebfc3d47cb82cfe4750d52c544a2b88a85970354a4d0d4b1db022069632067ee6f30f06145f649bc76d5e5d5e6404dbe985e006fcde938f778c297014c695221030502b8ade694d57a6e86998180a64f4ce993372830dc796c3d561ad8b2a504de210272b68e1c037c4630eff7ea5858640cc0748e36f5de82fb38529ef1fd0a89670d2103ba0544a3a2aa9f2314022760b78b5c833aebf6f88468a089550f93834a2886ed53aeffffffff7e048a7c53a8af656e24442c65fe4c4299b1494f6c7579fe0fd9fa741ce83e3279000000fc004730440220018fa343acccd048ed8f8f179e1b6ae27435a41b5fb2c1d96a5a772777acc6dc022074783814f2100c6fc4d4c976f941212be50825814502ca0cbe3f929db789979e0147304402206373f01b73fb09876d0f5ee3087e0614cab3be249934bc2b7eb64ee67f53dc8302200b50f8a327020172b82aaba7480c77ecf07bb32322a05f4afbc543aa97d2fde8014c69522103039d906b2494e310f6c7774c98618be552720d04781e073dd3ff25d5906f22662103d82026baa529619b103ec6341d548a7eb6d924061a8469a7416155513a3071c12102e452bc4aa726d44646ba80db70465683b30efde282a19aa35c6029ae8925df5e53aeffffffffef80f0b1cc543de4f73d59c02a3c575ae5d0af17c1e11e6be7abe3325c777507ad000000fdfd00004730440220220fee11bf836621a11a8ea9100a4600c109c13895f11468d3e2062210c5481902201c5c8a462175538e87b8248e1ed3927c3a461c66d1b46215641c875e86eb22c4014830450221008d2de8c2f20a720129c372791e595b9602b1a9bce99618497aec5266148ffc1302203a493359d700ed96323f8805ed03e909959ff0f22eff359028db6861486b1555014c6952210374a4add33567f09967592c5bcdc3db421fdbba67bac4636328f96d941da31bd221039636c2ffac90afb7499b16e265078113dfb2d77b54270e37353217c9eaeaf3052103d0bcea6d10cdd2f16018ea71572631708e26f457f67cda36a7f816a87f7791d253aeffffffff04977261000000000016001470385d054721987f41521648d7b2f5c77f735d6bee92030000000000225120d0cda1b675a0b369964cbfa381721aae3549dd2c9c6f2cf71ff67d5bc277afd3f2aaf30000000000160014ed2d41ba08313dbb2630a7106b2fedafc14aa121d4f0c70000000000220020e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec00000000"); - let mut cursor = std::io::Cursor::new(&mut tx_bytes); - let tx = Transaction::consensus_decode(&mut cursor).unwrap(); + let tx_bytes = hex_decode("0100000003fe785783e14669f638ba902c26e8e3d7036fb183237bc00f8a10542191c7171300000000fdfd00004730440220418996f20477d143d02ad47e74e5949641b6c2904159ab7c592d2cfc659f9bd802205b18f18ac86b714971f84a8b74a4cb14ad5c1a5b9d0d939bb32c6ae4032f4ea10148304502210091296ff8dd87b5ebfc3d47cb82cfe4750d52c544a2b88a85970354a4d0d4b1db022069632067ee6f30f06145f649bc76d5e5d5e6404dbe985e006fcde938f778c297014c695221030502b8ade694d57a6e86998180a64f4ce993372830dc796c3d561ad8b2a504de210272b68e1c037c4630eff7ea5858640cc0748e36f5de82fb38529ef1fd0a89670d2103ba0544a3a2aa9f2314022760b78b5c833aebf6f88468a089550f93834a2886ed53aeffffffff7e048a7c53a8af656e24442c65fe4c4299b1494f6c7579fe0fd9fa741ce83e3279000000fc004730440220018fa343acccd048ed8f8f179e1b6ae27435a41b5fb2c1d96a5a772777acc6dc022074783814f2100c6fc4d4c976f941212be50825814502ca0cbe3f929db789979e0147304402206373f01b73fb09876d0f5ee3087e0614cab3be249934bc2b7eb64ee67f53dc8302200b50f8a327020172b82aaba7480c77ecf07bb32322a05f4afbc543aa97d2fde8014c69522103039d906b2494e310f6c7774c98618be552720d04781e073dd3ff25d5906f22662103d82026baa529619b103ec6341d548a7eb6d924061a8469a7416155513a3071c12102e452bc4aa726d44646ba80db70465683b30efde282a19aa35c6029ae8925df5e53aeffffffffef80f0b1cc543de4f73d59c02a3c575ae5d0af17c1e11e6be7abe3325c777507ad000000fdfd00004730440220220fee11bf836621a11a8ea9100a4600c109c13895f11468d3e2062210c5481902201c5c8a462175538e87b8248e1ed3927c3a461c66d1b46215641c875e86eb22c4014830450221008d2de8c2f20a720129c372791e595b9602b1a9bce99618497aec5266148ffc1302203a493359d700ed96323f8805ed03e909959ff0f22eff359028db6861486b1555014c6952210374a4add33567f09967592c5bcdc3db421fdbba67bac4636328f96d941da31bd221039636c2ffac90afb7499b16e265078113dfb2d77b54270e37353217c9eaeaf3052103d0bcea6d10cdd2f16018ea71572631708e26f457f67cda36a7f816a87f7791d253aeffffffff04977261000000000016001470385d054721987f41521648d7b2f5c77f735d6bee92030000000000225120d0cda1b675a0b369964cbfa381721aae3549dd2c9c6f2cf71ff67d5bc277afd3f2aaf30000000000160014ed2d41ba08313dbb2630a7106b2fedafc14aa121d4f0c70000000000220020e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec00000000"); + let tx = Transaction::consensus_decode(&mut tx_bytes.as_slice()).unwrap(); let orig_weight = tx.weight(); let input_values = vec![022_680_000, 006_558_175, 006_558_200]; let candidates = tx @@ -124,19 +131,23 @@ fn legacy_three_inputs() { .zip(input_values) .map(|(txin, value)| Candidate { value, - weight: txin.legacy_weight() as u32, + weight: txin.legacy_weight().to_wu() as u32, input_count: 1, is_segwit: false, }) .collect::>(); let target_ouputs = TargetOutputs { - value_sum: tx.output.iter().map(|output| output.value).sum(), - weight_sum: tx.output.iter().map(|output| output.weight() as u32).sum(), + value_sum: tx.output.iter().map(|output| output.value.to_sat()).sum(), + weight_sum: tx + .output + .iter() + .map(|output| output.weight().to_wu() as u32) + .sum(), n_outputs: tx.output.len(), }; - let mut coin_selector = CoinSelector::new(&candidates); + let mut coin_selector = CoinSelector::new(&candidates, common::c_to_c); coin_selector.select_all(); assert_eq!( @@ -158,9 +169,8 @@ fn legacy_three_inputs() { fn legacy_three_inputs_one_segwit() { // FROM https://mempool.space/tx/5f231df4f73694b3cca9211e336451c20dab136e0a843c2e3166cdcb093e91f4 // Except we change the middle input to segwit - let mut tx_bytes = hex_decode("0100000003fe785783e14669f638ba902c26e8e3d7036fb183237bc00f8a10542191c7171300000000fdfd00004730440220418996f20477d143d02ad47e74e5949641b6c2904159ab7c592d2cfc659f9bd802205b18f18ac86b714971f84a8b74a4cb14ad5c1a5b9d0d939bb32c6ae4032f4ea10148304502210091296ff8dd87b5ebfc3d47cb82cfe4750d52c544a2b88a85970354a4d0d4b1db022069632067ee6f30f06145f649bc76d5e5d5e6404dbe985e006fcde938f778c297014c695221030502b8ade694d57a6e86998180a64f4ce993372830dc796c3d561ad8b2a504de210272b68e1c037c4630eff7ea5858640cc0748e36f5de82fb38529ef1fd0a89670d2103ba0544a3a2aa9f2314022760b78b5c833aebf6f88468a089550f93834a2886ed53aeffffffff7e048a7c53a8af656e24442c65fe4c4299b1494f6c7579fe0fd9fa741ce83e3279000000fc004730440220018fa343acccd048ed8f8f179e1b6ae27435a41b5fb2c1d96a5a772777acc6dc022074783814f2100c6fc4d4c976f941212be50825814502ca0cbe3f929db789979e0147304402206373f01b73fb09876d0f5ee3087e0614cab3be249934bc2b7eb64ee67f53dc8302200b50f8a327020172b82aaba7480c77ecf07bb32322a05f4afbc543aa97d2fde8014c69522103039d906b2494e310f6c7774c98618be552720d04781e073dd3ff25d5906f22662103d82026baa529619b103ec6341d548a7eb6d924061a8469a7416155513a3071c12102e452bc4aa726d44646ba80db70465683b30efde282a19aa35c6029ae8925df5e53aeffffffffef80f0b1cc543de4f73d59c02a3c575ae5d0af17c1e11e6be7abe3325c777507ad000000fdfd00004730440220220fee11bf836621a11a8ea9100a4600c109c13895f11468d3e2062210c5481902201c5c8a462175538e87b8248e1ed3927c3a461c66d1b46215641c875e86eb22c4014830450221008d2de8c2f20a720129c372791e595b9602b1a9bce99618497aec5266148ffc1302203a493359d700ed96323f8805ed03e909959ff0f22eff359028db6861486b1555014c6952210374a4add33567f09967592c5bcdc3db421fdbba67bac4636328f96d941da31bd221039636c2ffac90afb7499b16e265078113dfb2d77b54270e37353217c9eaeaf3052103d0bcea6d10cdd2f16018ea71572631708e26f457f67cda36a7f816a87f7791d253aeffffffff04977261000000000016001470385d054721987f41521648d7b2f5c77f735d6bee92030000000000225120d0cda1b675a0b369964cbfa381721aae3549dd2c9c6f2cf71ff67d5bc277afd3f2aaf30000000000160014ed2d41ba08313dbb2630a7106b2fedafc14aa121d4f0c70000000000220020e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec00000000"); - let mut cursor = std::io::Cursor::new(&mut tx_bytes); - let mut tx = Transaction::consensus_decode(&mut cursor).unwrap(); + let tx_bytes = hex_decode("0100000003fe785783e14669f638ba902c26e8e3d7036fb183237bc00f8a10542191c7171300000000fdfd00004730440220418996f20477d143d02ad47e74e5949641b6c2904159ab7c592d2cfc659f9bd802205b18f18ac86b714971f84a8b74a4cb14ad5c1a5b9d0d939bb32c6ae4032f4ea10148304502210091296ff8dd87b5ebfc3d47cb82cfe4750d52c544a2b88a85970354a4d0d4b1db022069632067ee6f30f06145f649bc76d5e5d5e6404dbe985e006fcde938f778c297014c695221030502b8ade694d57a6e86998180a64f4ce993372830dc796c3d561ad8b2a504de210272b68e1c037c4630eff7ea5858640cc0748e36f5de82fb38529ef1fd0a89670d2103ba0544a3a2aa9f2314022760b78b5c833aebf6f88468a089550f93834a2886ed53aeffffffff7e048a7c53a8af656e24442c65fe4c4299b1494f6c7579fe0fd9fa741ce83e3279000000fc004730440220018fa343acccd048ed8f8f179e1b6ae27435a41b5fb2c1d96a5a772777acc6dc022074783814f2100c6fc4d4c976f941212be50825814502ca0cbe3f929db789979e0147304402206373f01b73fb09876d0f5ee3087e0614cab3be249934bc2b7eb64ee67f53dc8302200b50f8a327020172b82aaba7480c77ecf07bb32322a05f4afbc543aa97d2fde8014c69522103039d906b2494e310f6c7774c98618be552720d04781e073dd3ff25d5906f22662103d82026baa529619b103ec6341d548a7eb6d924061a8469a7416155513a3071c12102e452bc4aa726d44646ba80db70465683b30efde282a19aa35c6029ae8925df5e53aeffffffffef80f0b1cc543de4f73d59c02a3c575ae5d0af17c1e11e6be7abe3325c777507ad000000fdfd00004730440220220fee11bf836621a11a8ea9100a4600c109c13895f11468d3e2062210c5481902201c5c8a462175538e87b8248e1ed3927c3a461c66d1b46215641c875e86eb22c4014830450221008d2de8c2f20a720129c372791e595b9602b1a9bce99618497aec5266148ffc1302203a493359d700ed96323f8805ed03e909959ff0f22eff359028db6861486b1555014c6952210374a4add33567f09967592c5bcdc3db421fdbba67bac4636328f96d941da31bd221039636c2ffac90afb7499b16e265078113dfb2d77b54270e37353217c9eaeaf3052103d0bcea6d10cdd2f16018ea71572631708e26f457f67cda36a7f816a87f7791d253aeffffffff04977261000000000016001470385d054721987f41521648d7b2f5c77f735d6bee92030000000000225120d0cda1b675a0b369964cbfa381721aae3549dd2c9c6f2cf71ff67d5bc277afd3f2aaf30000000000160014ed2d41ba08313dbb2630a7106b2fedafc14aa121d4f0c70000000000220020e5c7c00d174631d2d1e365d6347b016fb87b6a0c08902d8e443989cb771fa7ec00000000"); + let mut tx = Transaction::consensus_decode(&mut tx_bytes.as_slice()).unwrap(); tx.input[1].script_sig = ScriptBuf::default(); tx.input[1].witness = vec![ // semi-realistic p2wpkh spend @@ -181,7 +191,8 @@ fn legacy_three_inputs_one_segwit() { txin.segwit_weight() } else { txin.legacy_weight() - } as u32, + } + .to_wu() as u32, input_count: 1, is_segwit, } @@ -189,12 +200,16 @@ fn legacy_three_inputs_one_segwit() { .collect::>(); let target_ouputs = TargetOutputs { - value_sum: tx.output.iter().map(|output| output.value).sum(), - weight_sum: tx.output.iter().map(|output| output.weight() as u32).sum(), + value_sum: tx.output.iter().map(|output| output.value.to_sat()).sum(), + weight_sum: tx + .output + .iter() + .map(|output| output.weight().to_wu() as u32) + .sum(), n_outputs: tx.output.len(), }; - let mut coin_selector = CoinSelector::new(&candidates); + let mut coin_selector = CoinSelector::new(&candidates, common::c_to_c); coin_selector.select_all(); assert_eq!(