From 19d193ed7825170a0b41da6aa17121f36db736dd Mon Sep 17 00:00:00 2001 From: LLFourn Date: Thu, 23 Mar 2023 11:41:32 +1100 Subject: [PATCH 1/8] Move over from old PR --- nursery/coin_select/Cargo.toml | 12 +- nursery/coin_select/src/bnb.rs | 700 +++------------- nursery/coin_select/src/change_policy.rs | 50 ++ nursery/coin_select/src/coin_selector.rs | 750 +++++++----------- nursery/coin_select/src/feerate.rs | 89 +++ nursery/coin_select/src/lib.rs | 51 +- nursery/coin_select/src/metrics.rs | 93 +++ nursery/coin_select/src/metrics/waste.rs | 215 +++++ nursery/coin_select/src/ord_float.rs | 62 ++ nursery/coin_select/tests/bnb.rs | 187 +++++ .../tests/waste.proptest-regressions | 10 + nursery/coin_select/tests/waste.rs | 440 ++++++++++ 12 files changed, 1564 insertions(+), 1095 deletions(-) create mode 100644 nursery/coin_select/src/change_policy.rs create mode 100644 nursery/coin_select/src/feerate.rs create mode 100644 nursery/coin_select/src/metrics.rs create mode 100644 nursery/coin_select/src/metrics/waste.rs create mode 100644 nursery/coin_select/src/ord_float.rs create mode 100644 nursery/coin_select/tests/bnb.rs create mode 100644 nursery/coin_select/tests/waste.proptest-regressions create mode 100644 nursery/coin_select/tests/waste.rs diff --git a/nursery/coin_select/Cargo.toml b/nursery/coin_select/Cargo.toml index 0830ad93e..ce83dae66 100644 --- a/nursery/coin_select/Cargo.toml +++ b/nursery/coin_select/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "bdk_coin_select" -version = "0.0.1" -authors = [ "LLFourn " ] +version = "0.1.0" +edition = "2021" [dependencies] -bdk_chain = { path = "../../crates/chain" } +# No dependencies! Don't add any please! -[features] -default = ["std"] -std = [] +[dev-dependencies] +rand = "0.8" +proptest = "1" diff --git a/nursery/coin_select/src/bnb.rs b/nursery/coin_select/src/bnb.rs index 6938185b9..abf7bda22 100644 --- a/nursery/coin_select/src/bnb.rs +++ b/nursery/coin_select/src/bnb.rs @@ -1,645 +1,141 @@ -use super::*; - -/// Strategy in which we should branch. -pub enum BranchStrategy { - /// We continue exploring subtrees of this node, starting with the inclusion branch. - Continue, - /// We continue exploring ONLY the omission branch of this node, skipping the inclusion branch. - SkipInclusion, - /// We skip both the inclusion and omission branches of this node. - SkipBoth, -} - -impl BranchStrategy { - pub fn will_continue(&self) -> bool { - matches!(self, Self::Continue | Self::SkipInclusion) - } +use super::CoinSelector; +use alloc::collections::BinaryHeap; + +#[derive(Debug)] +pub(crate) struct BnbIter<'a, M: BnBMetric> { + queue: BinaryHeap>, + best: Option, + /// The `BnBMetric` that will score each selection + metric: M, } -/// Closure to decide the branching strategy, alongside a score (if the current selection is a -/// candidate solution). -pub type DecideStrategy<'c, S> = dyn Fn(&Bnb<'c, S>) -> (BranchStrategy, Option); +impl<'a, M: BnBMetric> Iterator for BnbIter<'a, M> { + type Item = Option<(CoinSelector<'a>, M::Score)>; -/// [`Bnb`] represents the current state of the BnB algorithm. -pub struct Bnb<'c, S> { - pub pool: Vec<(usize, &'c WeightedValue)>, - pub pool_pos: usize, - pub best_score: S, - - pub selection: CoinSelector<'c>, - pub rem_abs: u64, - pub rem_eff: i64, -} + fn next(&mut self) -> Option { + // { + // println!("=========================== {:?}", self.best); + // for thing in self.queue.iter() { + // println!("{} {:?}", &thing.selector, thing.lower_bound); + // } + // let _ = std::io::stdin().read_line(&mut String::new()); + // } + + let branch = self.queue.pop()?; + if let Some(best) = &self.best { + // If the next thing in queue is worse than our best we're done + if *best < branch.lower_bound { + return None; + } + } -impl<'c, S: Ord> Bnb<'c, S> { - /// Creates a new [`Bnb`]. - pub fn new(selector: CoinSelector<'c>, pool: Vec<(usize, &'c WeightedValue)>, max: S) -> Self { - let (rem_abs, rem_eff) = pool.iter().fold((0, 0), |(abs, eff), (_, c)| { - ( - abs + c.value, - eff + c.effective_value(selector.opts.target_feerate), - ) - }); + let selector = branch.selector; - Self { - pool, - pool_pos: 0, - best_score: max, - selection: selector, - rem_abs, - rem_eff, - } - } + self.insert_new_branches(&selector); - /// Turns our [`Bnb`] state into an iterator. - /// - /// `strategy` should assess our current selection/node and determine the branching strategy and - /// whether this selection is a candidate solution (if so, return the selection score). - pub fn into_iter<'f>(self, strategy: &'f DecideStrategy<'c, S>) -> BnbIter<'c, 'f, S> { - BnbIter { - state: self, - done: false, - strategy, + if branch.is_exclusion { + return Some(None); } - } - /// Attempt to backtrack to the previously selected node's omission branch, return false - /// otherwise (no more solutions). - pub fn backtrack(&mut self) -> bool { - (0..self.pool_pos).rev().any(|pos| { - let (index, candidate) = self.pool[pos]; + let score = match self.metric.score(&selector) { + Some(score) => score, + None => return Some(None), + }; - if self.selection.is_selected(index) { - // deselect the last `pos`, so the next round will check the omission branch - self.pool_pos = pos; - self.selection.deselect(index); - true - } else { - self.rem_abs += candidate.value; - self.rem_eff += candidate.effective_value(self.selection.opts.target_feerate); - false + match &self.best { + Some(best_score) if score >= *best_score => Some(None), + _ => { + self.best = Some(score.clone()); + return Some(Some((selector, score))); } - }) - } - - /// Continue down this branch and skip the inclusion branch if specified. - pub fn forward(&mut self, skip: bool) { - let (index, candidate) = self.pool[self.pool_pos]; - self.rem_abs -= candidate.value; - self.rem_eff -= candidate.effective_value(self.selection.opts.target_feerate); - - if !skip { - self.selection.select(index); - } - } - - /// Compare the advertised score with the current best. The new best will be the smaller value. Return true - /// if best is replaced. - pub fn advertise_new_score(&mut self, score: S) -> bool { - if score <= self.best_score { - self.best_score = score; - return true; } - false } } -pub struct BnbIter<'c, 'f, S> { - state: Bnb<'c, S>, - done: bool, - - /// Check our current selection (node) and returns the branching strategy alongside a score - /// (if the current selection is a candidate solution). - strategy: &'f DecideStrategy<'c, S>, -} - -impl<'c, 'f, S: Ord + Copy + Display> Iterator for BnbIter<'c, 'f, S> { - type Item = Option>; +impl<'a, M: BnBMetric> BnbIter<'a, M> { + pub fn new(mut selector: CoinSelector<'a>, metric: M) -> Self { + let mut iter = BnbIter { + queue: BinaryHeap::default(), + best: None, + metric, + }; - fn next(&mut self) -> Option { - if self.done { - return None; + if iter.metric.requires_ordering_by_descending_value_pwu() { + selector.sort_candidates_by_descending_value_pwu(); } - let (strategy, score) = (self.strategy)(&self.state); + iter.consider_adding_to_queue(&selector, false); - let mut found_best = Option::::None; + iter + } - if let Some(score) = score { - if self.state.advertise_new_score(score) { - found_best = Some(self.state.selection.clone()); + fn consider_adding_to_queue(&mut self, cs: &CoinSelector<'a>, is_exclusion: bool) { + let bound = self.metric.bound(cs); + if let Some(bound) = bound { + if self.best.is_none() || self.best.as_ref().unwrap() > &bound { + self.queue.push(Branch { + lower_bound: bound, + selector: cs.clone(), + is_exclusion, + }); } } + } - debug_assert!( - !strategy.will_continue() || self.state.pool_pos < self.state.pool.len(), - "Faulty strategy implementation! Strategy suggested that we continue traversing, however, we have already reached the end of the candidates pool! pool_len={}, pool_pos={}", - self.state.pool.len(), self.state.pool_pos, - ); - - match strategy { - BranchStrategy::Continue => { - self.state.forward(false); - } - BranchStrategy::SkipInclusion => { - self.state.forward(true); - } - BranchStrategy::SkipBoth => { - if !self.state.backtrack() { - self.done = true; - } - } - }; + fn insert_new_branches(&mut self, cs: &CoinSelector<'a>) { + if cs.is_exhausted() { + return; + } - // increment selection pool position for next round - self.state.pool_pos += 1; + let next_unselected = cs.unselected_indexes().next().unwrap(); + let mut inclusion_cs = cs.clone(); + inclusion_cs.select(next_unselected); + let mut exclusion_cs = cs.clone(); + exclusion_cs.ban(next_unselected); - if found_best.is_some() || !self.done { - Some(found_best) - } else { - // we have traversed all branches - None + for (child_cs, is_exclusion) in [(&inclusion_cs, false), (&exclusion_cs, true)] { + self.consider_adding_to_queue(child_cs, is_exclusion) } } } -/// Determines how we should limit rounds of branch and bound. -pub enum BnbLimit { - Rounds(usize), - #[cfg(feature = "std")] - Duration(core::time::Duration), +#[derive(Debug, Clone)] +struct Branch<'a, O> { + lower_bound: O, + selector: CoinSelector<'a>, + is_exclusion: bool, } -impl From for BnbLimit { - fn from(v: usize) -> Self { - Self::Rounds(v) +impl<'a, O: Ord> Ord for Branch<'a, O> { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + // NOTE: Reverse comparision `other.cmp(self)` because we want a min-heap (by default BinaryHeap is a max-heap). + // NOTE: We tiebreak equal scores based on whether it's exlusion or not (preferring inclusion). + // We do this because we want to try and get to evaluating complete selection returning + // actual scores as soon as possible. + (&other.lower_bound, other.is_exclusion).cmp(&(&self.lower_bound, self.is_exclusion)) } } -#[cfg(feature = "std")] -impl From for BnbLimit { - fn from(v: core::time::Duration) -> Self { - Self::Duration(v) +impl<'a, O: Ord> PartialOrd for Branch<'a, O> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } } -/// This is a variation of the Branch and Bound Coin Selection algorithm designed by Murch (as seen -/// in Bitcoin Core). -/// -/// The differences are as follows: -/// * In addition to working with effective values, we also work with absolute values. -/// This way, we can use bounds of the absolute values to enforce `min_absolute_fee` (which is used by -/// RBF), and `max_extra_target` (which can be used to increase the possible solution set, given -/// that the sender is okay with sending extra to the receiver). -/// -/// Murch's Master Thesis: -/// Bitcoin Core Implementation: -/// -/// TODO: Another optimization we could do is figure out candidates with the smallest waste, and -/// if we find a result with waste equal to this, we can just break. -pub fn coin_select_bnb(limit: L, selector: CoinSelector) -> Option -where - L: Into, -{ - let opts = selector.opts; - - // prepare the pool of candidates to select from: - // * filter out candidates with negative/zero effective values - // * sort candidates by descending effective value - let pool = { - let mut pool = selector - .unselected() - .filter(|(_, c)| c.effective_value(opts.target_feerate) > 0) - .collect::>(); - pool.sort_unstable_by(|(_, a), (_, b)| { - let a = a.effective_value(opts.target_feerate); - let b = b.effective_value(opts.target_feerate); - b.cmp(&a) - }); - pool - }; - - let feerate_decreases = opts.target_feerate > opts.long_term_feerate(); - - let target_abs = opts.target_value.unwrap_or(0) + opts.min_absolute_fee; - let target_eff = selector.effective_target(); - - let upper_bound_abs = target_abs + (opts.drain_weight as f32 * opts.target_feerate) as u64; - let upper_bound_eff = target_eff + opts.drain_waste(); - - let strategy = move |bnb: &Bnb| -> (BranchStrategy, Option) { - let selected_abs = bnb.selection.selected_absolute_value(); - let selected_eff = bnb.selection.selected_effective_value(); - - // backtrack if the remaining value is not enough to reach the target - if selected_abs + bnb.rem_abs < target_abs || selected_eff + bnb.rem_eff < target_eff { - return (BranchStrategy::SkipBoth, None); - } - - // backtrack if the selected value has already surpassed upper bounds - if selected_abs > upper_bound_abs && selected_eff > upper_bound_eff { - return (BranchStrategy::SkipBoth, None); - } - - let selected_waste = bnb.selection.selected_waste(); - - // when feerate decreases, waste without excess is guaranteed to increase with each - // selection. So if we have already surpassed the best score, we can backtrack. - if feerate_decreases && selected_waste > bnb.best_score { - return (BranchStrategy::SkipBoth, None); - } - - // solution? - if selected_abs >= target_abs && selected_eff >= target_eff { - let waste = selected_waste + bnb.selection.current_excess(); - return (BranchStrategy::SkipBoth, Some(waste)); - } - - // early bailout optimization: - // If the candidate at the previous position is NOT selected and has the same weight and - // value as the current candidate, we can skip selecting the current candidate. - if bnb.pool_pos > 0 && !bnb.selection.is_empty() { - let (_, candidate) = bnb.pool[bnb.pool_pos]; - let (prev_index, prev_candidate) = bnb.pool[bnb.pool_pos - 1]; - - if !bnb.selection.is_selected(prev_index) - && candidate.value == prev_candidate.value - && candidate.weight == prev_candidate.weight - { - return (BranchStrategy::SkipInclusion, None); - } - } - - // check out the inclusion branch first - (BranchStrategy::Continue, None) - }; - - // determine the sum of absolute and effective values for the current selection - let (selected_abs, selected_eff) = selector.selected().fold((0, 0), |(abs, eff), (_, c)| { - ( - abs + c.value, - eff + c.effective_value(selector.opts.target_feerate), - ) - }); - - let bnb = Bnb::new(selector, pool, i64::MAX); - - // not enough to select anyway - if selected_abs + bnb.rem_abs < target_abs || selected_eff + bnb.rem_eff < target_eff { - return None; +impl<'a, O: PartialEq> PartialEq for Branch<'a, O> { + fn eq(&self, other: &Self) -> bool { + self.lower_bound == other.lower_bound } - - match limit.into() { - BnbLimit::Rounds(rounds) => { - bnb.into_iter(&strategy) - .take(rounds) - .reduce(|b, c| if c.is_some() { c } else { b }) - } - #[cfg(feature = "std")] - BnbLimit::Duration(duration) => { - let start = std::time::SystemTime::now(); - bnb.into_iter(&strategy) - .take_while(|_| start.elapsed().expect("failed to get system time") <= duration) - .reduce(|b, c| if c.is_some() { c } else { b }) - } - }? } -#[cfg(all(test, feature = "miniscript"))] -mod test { - use bitcoin::secp256k1::Secp256k1; - - use crate::coin_select::{evaluate_cs::evaluate, ExcessStrategyKind}; +impl<'a, O: PartialEq> Eq for Branch<'a, O> {} - use super::{ - coin_select_bnb, - evaluate_cs::{Evaluation, EvaluationError}, - tester::Tester, - CoinSelector, CoinSelectorOpt, Vec, WeightedValue, - }; +pub trait BnBMetric { + type Score: Ord + Clone + core::fmt::Debug; - fn tester() -> Tester { - const DESC_STR: &str = "tr(xprv9uBuvtdjghkz8D1qzsSXS9Vs64mqrUnXqzNccj2xcvnCHPpXKYE1U2Gbh9CDHk8UPyF2VuXpVkDA7fk5ZP4Hd9KnhUmTscKmhee9Dp5sBMK)"; - Tester::new(&Secp256k1::default(), DESC_STR) - } - - fn evaluate_bnb( - initial_selector: CoinSelector, - max_tries: usize, - ) -> Result { - evaluate(initial_selector, |cs| { - coin_select_bnb(max_tries, cs.clone()).map_or(false, |new_cs| { - *cs = new_cs; - true - }) - }) - } - - #[test] - fn not_enough_coins() { - let t = tester(); - let candidates: Vec = vec![ - t.gen_candidate(0, 100_000).into(), - t.gen_candidate(1, 100_000).into(), - ]; - let opts = t.gen_opts(200_000); - let selector = CoinSelector::new(&candidates, &opts); - assert!(!coin_select_bnb(10_000, selector).is_some()); - } - - #[test] - fn exactly_enough_coins_preselected() { - let t = tester(); - let candidates: Vec = vec![ - t.gen_candidate(0, 100_000).into(), // to preselect - t.gen_candidate(1, 100_000).into(), // to preselect - t.gen_candidate(2, 100_000).into(), - ]; - let opts = CoinSelectorOpt { - target_feerate: 0.0, - ..t.gen_opts(200_000) - }; - let selector = { - let mut selector = CoinSelector::new(&candidates, &opts); - selector.select(0); // preselect - selector.select(1); // preselect - selector - }; - - let evaluation = evaluate_bnb(selector, 10_000).expect("eval failed"); - println!("{}", evaluation); - assert_eq!(evaluation.solution.selected, (0..=1).collect()); - assert_eq!(evaluation.solution.excess_strategies.len(), 1); - assert_eq!( - evaluation.feerate_offset(ExcessStrategyKind::ToFee).floor(), - 0.0 - ); - } - - /// `cost_of_change` acts as the upper-bound in Bnb; we check whether these boundaries are - /// enforced in code - #[test] - fn cost_of_change() { - let t = tester(); - let candidates: Vec = vec![ - t.gen_candidate(0, 200_000).into(), - t.gen_candidate(1, 200_000).into(), - t.gen_candidate(2, 200_000).into(), - ]; - - // lowest and highest possible `recipient_value` opts for derived `drain_waste`, assuming - // that we want 2 candidates selected - let (lowest_opts, highest_opts) = { - let opts = t.gen_opts(0); - - let fee_from_inputs = - (candidates[0].weight as f32 * opts.target_feerate).ceil() as u64 * 2; - let fee_from_template = - ((opts.base_weight + 2) as f32 * opts.target_feerate).ceil() as u64; - - let lowest_opts = CoinSelectorOpt { - target_value: Some( - 400_000 - fee_from_inputs - fee_from_template - opts.drain_waste() as u64, - ), - ..opts - }; - - let highest_opts = CoinSelectorOpt { - target_value: Some(400_000 - fee_from_inputs - fee_from_template), - ..opts - }; - - (lowest_opts, highest_opts) - }; - - // test lowest possible target we can select - let lowest_eval = evaluate_bnb(CoinSelector::new(&candidates, &lowest_opts), 10_000); - assert!(lowest_eval.is_ok()); - let lowest_eval = lowest_eval.unwrap(); - println!("LB {}", lowest_eval); - assert_eq!(lowest_eval.solution.selected.len(), 2); - assert_eq!(lowest_eval.solution.excess_strategies.len(), 1); - assert_eq!( - lowest_eval - .feerate_offset(ExcessStrategyKind::ToFee) - .floor(), - 0.0 - ); - - // test the highest possible target we can select - let highest_eval = evaluate_bnb(CoinSelector::new(&candidates, &highest_opts), 10_000); - assert!(highest_eval.is_ok()); - let highest_eval = highest_eval.unwrap(); - println!("UB {}", highest_eval); - assert_eq!(highest_eval.solution.selected.len(), 2); - assert_eq!(highest_eval.solution.excess_strategies.len(), 1); - assert_eq!( - highest_eval - .feerate_offset(ExcessStrategyKind::ToFee) - .floor(), - 0.0 - ); - - // test lower out of bounds - let loob_opts = CoinSelectorOpt { - target_value: lowest_opts.target_value.map(|v| v - 1), - ..lowest_opts - }; - let loob_eval = evaluate_bnb(CoinSelector::new(&candidates, &loob_opts), 10_000); - assert!(loob_eval.is_err()); - println!("Lower OOB: {}", loob_eval.unwrap_err()); - - // test upper out of bounds - let uoob_opts = CoinSelectorOpt { - target_value: highest_opts.target_value.map(|v| v + 1), - ..highest_opts - }; - let uoob_eval = evaluate_bnb(CoinSelector::new(&candidates, &uoob_opts), 10_000); - assert!(uoob_eval.is_err()); - println!("Upper OOB: {}", uoob_eval.unwrap_err()); - } - - #[test] - fn try_select() { - let t = tester(); - let candidates: Vec = vec![ - t.gen_candidate(0, 300_000).into(), - t.gen_candidate(1, 300_000).into(), - t.gen_candidate(2, 300_000).into(), - t.gen_candidate(3, 200_000).into(), - t.gen_candidate(4, 200_000).into(), - ]; - let make_opts = |v: u64| -> CoinSelectorOpt { - CoinSelectorOpt { - target_feerate: 0.0, - ..t.gen_opts(v) - } - }; - - let test_cases = vec![ - (make_opts(100_000), false, 0), - (make_opts(200_000), true, 1), - (make_opts(300_000), true, 1), - (make_opts(500_000), true, 2), - (make_opts(1_000_000), true, 4), - (make_opts(1_200_000), false, 0), - (make_opts(1_300_000), true, 5), - (make_opts(1_400_000), false, 0), - ]; - - for (opts, expect_solution, expect_selected) in test_cases { - let res = evaluate_bnb(CoinSelector::new(&candidates, &opts), 10_000); - assert_eq!(res.is_ok(), expect_solution); - - match res { - Ok(eval) => { - println!("{}", eval); - assert_eq!(eval.feerate_offset(ExcessStrategyKind::ToFee), 0.0); - assert_eq!(eval.solution.selected.len(), expect_selected as _); - } - Err(err) => println!("expected failure: {}", err), - } - } - } - - #[test] - fn early_bailout_optimization() { - let t = tester(); - - // target: 300_000 - // candidates: 2x of 125_000, 1000x of 100_000, 1x of 50_000 - // expected solution: 2x 125_000, 1x 50_000 - // set bnb max tries: 1100, should succeed - let candidates = { - let mut candidates: Vec = vec![ - t.gen_candidate(0, 125_000).into(), - t.gen_candidate(1, 125_000).into(), - t.gen_candidate(2, 50_000).into(), - ]; - (3..3 + 1000_u32) - .for_each(|index| candidates.push(t.gen_candidate(index, 100_000).into())); - candidates - }; - let opts = CoinSelectorOpt { - target_feerate: 0.0, - ..t.gen_opts(300_000) - }; - - let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), 1100); - assert!(result.is_ok()); - - let eval = result.unwrap(); - println!("{}", eval); - assert_eq!(eval.solution.selected, (0..=2).collect()); - } - - #[test] - fn should_exhaust_iteration() { - static MAX_TRIES: usize = 1000; - let t = tester(); - let candidates = (0..MAX_TRIES + 1) - .map(|index| t.gen_candidate(index as _, 10_000).into()) - .collect::>(); - let opts = t.gen_opts(10_001 * MAX_TRIES as u64); - let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), MAX_TRIES); - assert!(result.is_err()); - println!("error as expected: {}", result.unwrap_err()); - } - - /// Solution should have fee >= min_absolute_fee (or no solution at all) - #[test] - fn min_absolute_fee() { - let t = tester(); - let candidates = { - let mut candidates = Vec::new(); - t.gen_weighted_values(&mut candidates, 5, 10_000); - t.gen_weighted_values(&mut candidates, 5, 20_000); - t.gen_weighted_values(&mut candidates, 5, 30_000); - t.gen_weighted_values(&mut candidates, 10, 10_300); - t.gen_weighted_values(&mut candidates, 10, 10_500); - t.gen_weighted_values(&mut candidates, 10, 10_700); - t.gen_weighted_values(&mut candidates, 10, 10_900); - t.gen_weighted_values(&mut candidates, 10, 11_000); - t.gen_weighted_values(&mut candidates, 10, 12_000); - t.gen_weighted_values(&mut candidates, 10, 13_000); - candidates - }; - let mut opts = CoinSelectorOpt { - min_absolute_fee: 1, - ..t.gen_opts(100_000) - }; - - (1..=120_u64).for_each(|fee_factor| { - opts.min_absolute_fee = fee_factor * 31; - - let result = evaluate_bnb(CoinSelector::new(&candidates, &opts), 21_000); - match result { - Ok(result) => { - println!("Solution {}", result); - let fee = result.solution.excess_strategies[&ExcessStrategyKind::ToFee].fee; - assert!(fee >= opts.min_absolute_fee); - assert_eq!(result.solution.excess_strategies.len(), 1); - } - Err(err) => { - println!("No Solution: {}", err); - } - } - }); - } - - /// For a decreasing feerate (long-term feerate is lower than effective feerate), we should - /// select less. For increasing feerate (long-term feerate is higher than effective feerate), we - /// should select more. - #[test] - fn feerate_difference() { - let t = tester(); - let candidates = { - let mut candidates = Vec::new(); - t.gen_weighted_values(&mut candidates, 10, 2_000); - t.gen_weighted_values(&mut candidates, 10, 5_000); - t.gen_weighted_values(&mut candidates, 10, 20_000); - candidates - }; - - let decreasing_feerate_opts = CoinSelectorOpt { - target_feerate: 1.25, - long_term_feerate: Some(0.25), - ..t.gen_opts(100_000) - }; - - let increasing_feerate_opts = CoinSelectorOpt { - target_feerate: 0.25, - long_term_feerate: Some(1.25), - ..t.gen_opts(100_000) - }; - - let decreasing_res = evaluate_bnb( - CoinSelector::new(&candidates, &decreasing_feerate_opts), - 21_000, - ) - .expect("no result"); - let decreasing_len = decreasing_res.solution.selected.len(); - - let increasing_res = evaluate_bnb( - CoinSelector::new(&candidates, &increasing_feerate_opts), - 21_000, - ) - .expect("no result"); - let increasing_len = increasing_res.solution.selected.len(); - - println!("decreasing_len: {}", decreasing_len); - println!("increasing_len: {}", increasing_len); - assert!(decreasing_len < increasing_len); + fn score<'a>(&mut self, cs: &CoinSelector<'a>) -> Option; + fn bound<'a>(&mut self, cs: &CoinSelector<'a>) -> Option; + fn requires_ordering_by_descending_value_pwu(&self) -> bool { + false } - - /// TODO: UNIMPLEMENTED TESTS: - /// * Excess strategies: - /// * We should always have `ExcessStrategy::ToFee`. - /// * We should only have `ExcessStrategy::ToRecipient` when `max_extra_target > 0`. - /// * We should only have `ExcessStrategy::ToDrain` when `drain_value >= min_drain_value`. - /// * Fuzz - /// * Solution feerate should never be lower than target feerate - /// * Solution fee should never be lower than `min_absolute_fee`. - /// * Preselected should always remain selected - fn _todo() {} } diff --git a/nursery/coin_select/src/change_policy.rs b/nursery/coin_select/src/change_policy.rs new file mode 100644 index 000000000..588918465 --- /dev/null +++ b/nursery/coin_select/src/change_policy.rs @@ -0,0 +1,50 @@ +use crate::{CoinSelector, Drain, FeeRate, Target}; + +/// Add a change output if the value it would receive is greater than or equal to `min_value`. +/// +/// Note that the value field of the `drain` is ignored. +pub fn min_value(mut drain: Drain, min_value: u64) -> impl Fn(&CoinSelector, Target) -> Drain { + debug_assert!(drain.is_some()); + let min_value: i64 = min_value + .try_into() + .expect("min_value is ridiculously large"); + drain.value = 0; + move |cs, target| { + let excess = cs.excess(target, drain); + if excess >= min_value { + let mut drain = drain; + drain.value = excess.try_into().expect( + "cannot be negative since we checked it against min_value which is positive", + ); + drain + } else { + Drain::none() + } + } +} + +/// Add a change output if it would reduce the overall waste of the transaction. +/// +/// Note that the value field of the `drain` is ignored. +/// The `value` will be set to whatever needs to be to reach the given target. +pub fn min_waste( + mut drain: Drain, + long_term_feerate: FeeRate, +) -> impl Fn(&CoinSelector, Target) -> Drain { + debug_assert!(drain.is_some()); + drain.value = 0; + + move |cs, target| { + let excess = cs.excess(target, Drain::none()); + if excess > drain.waste(target.feerate, long_term_feerate).ceil() as i64 { + let mut drain = drain; + drain.value = cs + .excess(target, drain) + .try_into() + .expect("the excess must be positive because drain free excess was > waste"); + drain + } else { + Drain::none() + } + } +} diff --git a/nursery/coin_select/src/coin_selector.rs b/nursery/coin_select/src/coin_selector.rs index 7b136c211..c064831e2 100644 --- a/nursery/coin_select/src/coin_selector.rs +++ b/nursery/coin_select/src/coin_selector.rs @@ -1,4 +1,6 @@ use super::*; +use crate::{bnb::BnBMetric, ord_float::Ordf32, FeeRate}; +use alloc::{borrow::Cow, collections::BTreeSet, vec::Vec}; /// A [`WeightedValue`] represents an input candidate for [`CoinSelector`]. This can either be a /// single UTXO, or a group of UTXOs that should be spent together. @@ -10,7 +12,7 @@ pub struct WeightedValue { /// `txin` fields: `prevout`, `nSequence`, `scriptSigLen`, `scriptSig`, `scriptWitnessLen`, /// `scriptWitness` should all be included. pub weight: u32, - /// The total number of inputs; so we can calculate extra `varint` weight due to `vin` length changes. + /// Total number of inputs; so we can calculate extra `varint` weight due to `vin` len changes. pub input_count: usize, /// Whether this [`WeightedValue`] contains at least one segwit spend. pub is_segwit: bool, @@ -32,142 +34,145 @@ impl WeightedValue { } /// Effective value of this input candidate: `actual_value - input_weight * feerate (sats/wu)`. - pub fn effective_value(&self, effective_feerate: f32) -> i64 { - // We prefer undershooting the candidate's effective value (so we over-estimate the fee of a - // candidate). If we overshoot the candidate's effective value, it may be possible to find a - // solution which does not meet the target feerate. - self.value as i64 - (self.weight as f32 * effective_feerate).ceil() as i64 + pub fn effective_value(&self, feerate: FeeRate) -> Ordf32 { + Ordf32(self.value as f32 - (self.weight as f32 * feerate.spwu())) } -} -#[derive(Debug, Clone, Copy)] -pub struct CoinSelectorOpt { - /// The value we need to select. - /// If the value is `None`, then the selection will be complete if it can pay for the drain - /// output and satisfy the other constraints (e.g., minimum fees). - pub target_value: Option, - /// Additional leeway for the target value. - pub max_extra_target: u64, // TODO: Maybe out of scope here? - - /// The feerate we should try and achieve in sats per weight unit. - pub target_feerate: f32, - /// The feerate - pub long_term_feerate: Option, // TODO: Maybe out of scope? (waste) - /// The minimum absolute fee. I.e., needed for RBF. - pub min_absolute_fee: u64, - - /// The weight of the template transaction, including fixed fields and outputs. - pub base_weight: u32, - /// Additional weight if we include the drain (change) output. - pub drain_weight: u32, - /// Weight of spending the drain (change) output in the future. - pub spend_drain_weight: u32, // TODO: Maybe out of scope? (waste) - - /// Minimum value allowed for a drain (change) output. - pub min_drain_value: u64, + pub fn value_pwu(&self) -> Ordf32 { + Ordf32(self.value as f32 / self.weight as f32) + } } -impl CoinSelectorOpt { - fn from_weights(base_weight: u32, drain_weight: u32, spend_drain_weight: u32) -> Self { - // 0.25 sats/wu == 1 sat/vb - let target_feerate = 0.25_f32; - - // set `min_drain_value` to dust limit - let min_drain_value = - 3 * ((drain_weight + spend_drain_weight) as f32 * target_feerate) as u64; +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] +pub struct Drain { + pub weight: u32, + pub value: u64, + pub spend_weight: u32, +} - Self { - target_value: None, - max_extra_target: 0, - target_feerate, - long_term_feerate: None, - min_absolute_fee: 0, - base_weight, - drain_weight, - spend_drain_weight, - min_drain_value, - } +impl Drain { + pub fn none() -> Self { + Self::default() } - pub fn fund_outputs( - txouts: &[TxOut], - drain_output: &TxOut, - drain_satisfaction_weight: u32, - ) -> Self { - let mut tx = Transaction { - input: vec![], - version: 1, - lock_time: LockTime::ZERO.into(), - output: txouts.to_vec(), - }; - let base_weight = tx.weight(); - // this awkward calculation is necessary since TxOut doesn't have \.weight() - let drain_weight = { - tx.output.push(drain_output.clone()); - tx.weight() - base_weight - }; - Self { - target_value: if txouts.is_empty() { - None - } else { - Some(txouts.iter().map(|txout| txout.value).sum()) - }, - ..Self::from_weights( - base_weight as u32, - drain_weight as u32, - TXIN_BASE_WEIGHT + drain_satisfaction_weight, - ) - } + pub fn is_none(&self) -> bool { + self == &Drain::none() } - pub fn long_term_feerate(&self) -> f32 { - self.long_term_feerate.unwrap_or(self.target_feerate) + pub fn is_some(&self) -> bool { + !self.is_none() } - pub fn drain_waste(&self) -> i64 { - (self.drain_weight as f32 * self.target_feerate - + self.spend_drain_weight as f32 * self.long_term_feerate()) as i64 + pub fn waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 { + self.weight as f32 * feerate.spwu() + self.spend_weight as f32 * long_term_feerate.spwu() } } -/// [`CoinSelector`] selects and deselects from a set of candidates. +/// [`CoinSelector`] is responsible for selecting and deselecting from a set of canididates. #[derive(Debug, Clone)] pub struct CoinSelector<'a> { - pub opts: &'a CoinSelectorOpt, - pub candidates: &'a Vec, - selected: BTreeSet, + base_weight: u32, + candidates: &'a [WeightedValue], + selected: Cow<'a, BTreeSet>, + banned: Cow<'a, BTreeSet>, + candidate_order: Cow<'a, Vec>, } -impl<'a> CoinSelector<'a> { - pub fn candidate(&self, index: usize) -> &WeightedValue { - &self.candidates[index] +#[derive(Debug, Clone, Copy)] +pub struct Target { + pub feerate: FeeRate, + pub min_fee: u64, + pub value: u64, +} + +impl Default for Target { + fn default() -> Self { + Self { + feerate: FeeRate::default_min_relay_fee(), + min_fee: 0, // TODO figure out what the actual network rule is for this + value: 0, + } } +} - pub fn new(candidates: &'a Vec, opts: &'a CoinSelectorOpt) -> Self { +impl<'a> CoinSelector<'a> { + // TODO: constructor should be number of outputs and output weight instead so we can keep track + pub fn new(candidates: &'a [WeightedValue], base_weight: u32) -> Self { Self { + base_weight, candidates, - selected: Default::default(), - opts, + selected: Cow::Owned(Default::default()), + banned: Cow::Owned(Default::default()), + candidate_order: Cow::Owned((0..candidates.len()).collect()), } } + pub fn candidates( + &self, + ) -> impl DoubleEndedIterator + ExactSizeIterator + '_ { + self.candidate_order + .iter() + .map(|i| (*i, self.candidates[*i])) + } + + pub fn candidate(&self, index: usize) -> WeightedValue { + self.candidates[index] + } + + pub fn deselect(&mut self, index: usize) -> bool { + self.selected.to_mut().remove(&index) + } + + pub fn apply_selection(&self, candidates: &'a [T]) -> impl Iterator + '_ { + self.selected.iter().map(|i| &candidates[*i]) + } + pub fn select(&mut self, index: usize) -> bool { assert!(index < self.candidates.len()); - self.selected.insert(index) + self.selected.to_mut().insert(index) } - pub fn deselect(&mut self, index: usize) -> bool { - self.selected.remove(&index) + pub fn select_next(&mut self) -> bool { + let next = self.unselected_indexes().next(); + if let Some(next) = next { + self.select(next); + true + } else { + false + } + } + + pub fn ban(&mut self, index: usize) { + self.banned.to_mut().insert(index); + } + + pub fn banned(&self) -> &BTreeSet { + &self.banned } pub fn is_selected(&self, index: usize) -> bool { self.selected.contains(&index) } + pub fn is_selection_possible(&self, target: Target, drain: Drain) -> bool { + let mut test = self.clone(); + test.select_all_effective(target.feerate); + test.is_target_met(target, drain) + } + + pub fn is_selection_plausible_with_change_policy( + &self, + target: Target, + change_policy: &impl Fn(&CoinSelector<'a>, Target) -> Drain, + ) -> bool { + let mut test = self.clone(); + test.select_all_effective(target.feerate); + test.is_target_met(target, change_policy(&test, target)) + } + pub fn is_empty(&self) -> bool { self.selected.is_empty() } - /// Weight sum of all selected inputs. pub fn selected_weight(&self) -> u32 { self.selected @@ -176,30 +181,7 @@ impl<'a> CoinSelector<'a> { .sum() } - /// Effective value sum of all selected inputs. - pub fn selected_effective_value(&self) -> i64 { - self.selected - .iter() - .map(|&index| self.candidates[index].effective_value(self.opts.target_feerate)) - .sum() - } - - /// Absolute value sum of all selected inputs. - pub fn selected_absolute_value(&self) -> u64 { - self.selected - .iter() - .map(|&index| self.candidates[index].value) - .sum() - } - - /// Waste sum of all selected inputs. - pub fn selected_waste(&self) -> i64 { - (self.selected_weight() as f32 * (self.opts.target_feerate - self.opts.long_term_feerate())) - as i64 - } - - /// Current weight of template tx + selected inputs. - pub fn current_weight(&self) -> u32 { + pub fn input_weight(&self) -> u32 { let witness_header_extra_weight = self .selected() .find(|(_, wv)| wv.is_segwit) @@ -209,407 +191,243 @@ impl<'a> CoinSelector<'a> { let input_count = self.selected().map(|(_, wv)| wv.input_count).sum::(); (varint_size(input_count) - 1) * 4 }; - self.opts.base_weight - + self.selected_weight() - + witness_header_extra_weight - + vin_count_varint_extra_weight - } - /// Current excess. - pub fn current_excess(&self) -> i64 { - self.selected_effective_value() - self.effective_target() + self.selected_weight() + witness_header_extra_weight + vin_count_varint_extra_weight } - /// This is the effective target value. - pub fn effective_target(&self) -> i64 { - let (has_segwit, max_input_count) = self - .candidates + /// Absolute value sum of all selected inputs. + pub fn selected_value(&self) -> u64 { + self.selected .iter() - .fold((false, 0_usize), |(is_segwit, input_count), c| { - (is_segwit || c.is_segwit, input_count + c.input_count) - }); + .map(|&index| self.candidates[index].value) + .sum() + } - let effective_base_weight = self.opts.base_weight - + if has_segwit { 2_u32 } else { 0_u32 } - + (varint_size(max_input_count) - 1) * 4; + /// Current weight of template tx + selected inputs. + pub fn weight(&self, drain_weight: u32) -> u32 { + // TODO take into account whether drain tips over varint for number of outputs + // + // TODO: take into account the witness stack length for each input + self.base_weight + self.input_weight() + drain_weight + } - self.opts.target_value.unwrap_or(0) as i64 - + (effective_base_weight as f32 * self.opts.target_feerate).ceil() as i64 + /// How much the current selection overshoots the value needed to acheive `target`. + /// + /// In order for the resulting transaction to be valid this must be 0. + pub fn excess(&self, target: Target, drain: Drain) -> i64 { + self.selected_value() as i64 + - target.value as i64 + - drain.value as i64 + - self.implied_fee(target.feerate, target.min_fee, drain.weight) as i64 } - pub fn selected_count(&self) -> usize { - self.selected.len() + pub fn rate_excess(&self, target: Target, drain: Drain) -> i64 { + self.selected_value() as i64 + - target.value as i64 + - drain.value as i64 + - self.implied_fee_from_feerate(target.feerate, drain.weight) as i64 } - pub fn selected(&self) -> impl Iterator + '_ { - self.selected - .iter() - .map(move |&index| (index, &self.candidates[index])) + pub fn absolute_excess(&self, target: Target, drain: Drain) -> i64 { + self.selected_value() as i64 + - target.value as i64 + - drain.value as i64 + - target.min_fee as i64 } - pub fn unselected(&self) -> impl Iterator + '_ { - self.candidates - .iter() - .enumerate() - .filter(move |(index, _)| !self.selected.contains(index)) + /// The feerate the transaction would have if we were to use this selection of inputs to acheive + /// the + pub fn implied_feerate(&self, target_value: u64, drain: Drain) -> FeeRate { + let numerator = self.selected_value() as i64 - target_value as i64 - drain.value as i64; + let denom = self.weight(drain.weight); + FeeRate::from_sat_per_wu(numerator as f32 / denom as f32) } - pub fn selected_indexes(&self) -> impl Iterator + '_ { - self.selected.iter().cloned() + pub fn implied_fee(&self, feerate: FeeRate, min_fee: u64, drain_weight: u32) -> u64 { + (self.implied_fee_from_feerate(feerate, drain_weight)).max(min_fee) } - pub fn unselected_indexes(&self) -> impl Iterator + '_ { - (0..self.candidates.len()).filter(move |index| !self.selected.contains(index)) + pub fn implied_fee_from_feerate(&self, feerate: FeeRate, drain_weight: u32) -> u64 { + (self.weight(drain_weight) as f32 * feerate.spwu()).ceil() as u64 } - pub fn all_selected(&self) -> bool { - self.selected.len() == self.candidates.len() + /// The value of the current selected inputs minus the fee needed to pay for the selected inputs + pub fn effective_value(&self, feerate: FeeRate) -> i64 { + self.selected_value() as i64 - (self.input_weight() as f32 * feerate.spwu()).ceil() as i64 } - pub fn select_all(&mut self) { - self.selected = (0..self.candidates.len()).collect(); + // /// Waste sum of all selected inputs. + fn selected_waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 { + self.selected_weight() as f32 * (feerate.spwu() - long_term_feerate.spwu()) } - pub fn select_until_finished(&mut self) -> Result { - let mut selection = self.finish(); + pub fn sort_candidates_by(&mut self, mut cmp: F) + where + F: FnMut((usize, WeightedValue), (usize, WeightedValue)) -> core::cmp::Ordering, + { + let order = self.candidate_order.to_mut(); + order.sort_by(|a, b| cmp((*a, self.candidates[*a]), (*b, self.candidates[*b]))) + } - if selection.is_ok() { - return selection; - } + pub fn sort_candidates_by_key(&mut self, mut key_fn: F) + where + F: FnMut((usize, WeightedValue)) -> K, + K: Ord, + { + self.sort_candidates_by(|a, b| key_fn(a).cmp(&key_fn(b))) + } - let unselected = self.unselected_indexes().collect::>(); + pub fn sort_candidates_by_descending_value_pwu(&mut self) { + self.sort_candidates_by_key(|(_, wv)| core::cmp::Reverse(wv.value_pwu())); + } - for index in unselected { - self.select(index); - selection = self.finish(); + pub fn waste( + &self, + target: Target, + long_term_feerate: FeeRate, + drain: Drain, + excess_discount: f32, + ) -> f32 { + debug_assert!(excess_discount >= 0.0 && excess_discount <= 1.0); + let mut waste = self.selected_waste(target.feerate, long_term_feerate); - if selection.is_ok() { - break; - } + if drain.is_none() { + // We don't allow negative excess waste since negative excess just means you haven't + // satisified target yet in which case you probably shouldn't be calling this function. + 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); + waste += excess_waste; + } else { + waste += drain.weight as f32 * target.feerate.spwu() + + drain.spend_weight as f32 * long_term_feerate.spwu(); } - selection - } - - pub fn finish(&self) -> Result { - let weight_without_drain = self.current_weight(); - let weight_with_drain = weight_without_drain + self.opts.drain_weight; - - let fee_without_drain = - (weight_without_drain as f32 * self.opts.target_feerate).ceil() as u64; - let fee_with_drain = (weight_with_drain as f32 * self.opts.target_feerate).ceil() as u64; - - let inputs_minus_outputs = { - let target_value = self.opts.target_value.unwrap_or(0); - let selected = self.selected_absolute_value(); - - // find the largest unsatisfied constraint (if any), and return the error of that constraint - // "selected" should always be greater than or equal to these selected values - [ - ( - SelectionConstraint::TargetValue, - target_value.saturating_sub(selected), - ), - ( - SelectionConstraint::TargetFee, - (target_value + fee_without_drain).saturating_sub(selected), - ), - ( - SelectionConstraint::MinAbsoluteFee, - (target_value + self.opts.min_absolute_fee).saturating_sub(selected), - ), - ( - SelectionConstraint::MinDrainValue, - // when we have no target value (hence no recipient txouts), we need to ensure - // the selected amount can satisfy requirements for a drain output (so we at least have one txout) - if self.opts.target_value.is_none() { - (fee_with_drain + self.opts.min_drain_value).saturating_sub(selected) - } else { - 0 - }, - ), - ] - .iter() - .filter(|&(_, v)| v > &0) - .max_by_key(|&(_, v)| v) - .map_or(Ok(()), |(constraint, missing)| { - Err(SelectionError { - selected, - missing: *missing, - constraint: *constraint, - }) - })?; - - selected - target_value - }; + waste + } - let fee_without_drain = fee_without_drain.max(self.opts.min_absolute_fee); - let fee_with_drain = fee_with_drain.max(self.opts.min_absolute_fee); - - let excess_without_drain = inputs_minus_outputs - fee_without_drain; - let input_waste = self.selected_waste(); - - // begin preparing excess strategies for final selection - let mut excess_strategies = HashMap::new(); - - // only allow `ToFee` and `ToRecipient` excess strategies when we have a `target_value`, - // otherwise, we will result in a result with no txouts, or attempt to add value to an output - // that does not exist. - if self.opts.target_value.is_some() { - // no drain, excess to fee - excess_strategies.insert( - ExcessStrategyKind::ToFee, - ExcessStrategy { - recipient_value: self.opts.target_value, - drain_value: None, - fee: fee_without_drain + excess_without_drain, - weight: weight_without_drain, - waste: input_waste + excess_without_drain as i64, - }, - ); - - // no drain, send the excess to the recipient - // if `excess == 0`, this result will be the same as the previous, so don't consider it - // if `max_extra_target == 0`, there is no leeway for this strategy - if excess_without_drain > 0 && self.opts.max_extra_target > 0 { - let extra_recipient_value = - core::cmp::min(self.opts.max_extra_target, excess_without_drain); - let extra_fee = excess_without_drain - extra_recipient_value; - excess_strategies.insert( - ExcessStrategyKind::ToRecipient, - ExcessStrategy { - recipient_value: self.opts.target_value.map(|v| v + extra_recipient_value), - drain_value: None, - fee: fee_without_drain + extra_fee, - weight: weight_without_drain, - waste: input_waste + extra_fee as i64, - }, - ); - } - } + pub fn selected(&self) -> impl ExactSizeIterator + '_ { + self.selected + .iter() + .map(|&index| (index, self.candidates[index])) + } - // with drain - if fee_with_drain >= self.opts.min_absolute_fee - && inputs_minus_outputs >= fee_with_drain + self.opts.min_drain_value - { - excess_strategies.insert( - ExcessStrategyKind::ToDrain, - ExcessStrategy { - recipient_value: self.opts.target_value, - drain_value: Some(inputs_minus_outputs.saturating_sub(fee_with_drain)), - fee: fee_with_drain, - weight: weight_with_drain, - waste: input_waste + self.opts.drain_waste(), - }, - ); - } + pub fn unselected(&self) -> impl DoubleEndedIterator + '_ { + self.unselected_indexes().map(|i| (i, self.candidates[i])) + } - debug_assert!( - !excess_strategies.is_empty(), - "should have at least one excess strategy." - ); + pub fn selected_indexes(&self) -> &BTreeSet { + &self.selected + } - Ok(Selection { - selected: self.selected.clone(), - excess: excess_without_drain, - excess_strategies, - }) + pub fn unselected_indexes(&self) -> impl DoubleEndedIterator + '_ { + self.candidate_order + .iter() + .filter(|index| !(self.selected.contains(index) || self.banned.contains(index))) + .map(|index| *index) } -} -#[derive(Clone, Debug)] -pub struct SelectionError { - selected: u64, - missing: u64, - constraint: SelectionConstraint, -} + pub fn is_exhausted(&self) -> bool { + self.unselected_indexes().next().is_none() + } -impl core::fmt::Display for SelectionError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let SelectionError { - selected, - missing, - constraint, - } = self; - write!( - f, - "insufficient coins selected; selected={}, missing={}, unsatisfied_constraint={:?}", - selected, missing, constraint - ) + pub fn is_target_met(&self, target: Target, drain: Drain) -> bool { + self.excess(target, drain) >= 0 } -} -#[cfg(feature = "std")] -impl std::error::Error for SelectionError {} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum SelectionConstraint { - /// The target is not met - TargetValue, - /// The target fee (given the feerate) is not met - TargetFee, - /// Min absolute fee is not met - MinAbsoluteFee, - /// Min drain value is not met - MinDrainValue, -} + pub fn select_all(&mut self) { + loop { + if !self.select_next() { + break; + } + } + } -impl core::fmt::Display for SelectionConstraint { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - SelectionConstraint::TargetValue => core::write!(f, "target_value"), - SelectionConstraint::TargetFee => core::write!(f, "target_fee"), - SelectionConstraint::MinAbsoluteFee => core::write!(f, "min_absolute_fee"), - SelectionConstraint::MinDrainValue => core::write!(f, "min_drain_value"), + pub fn select_all_effective(&mut self, feerate: FeeRate) { + // TODO: do this without allocating + for i in self.unselected_indexes().collect::>() { + if self.candidates[i].effective_value(feerate) > Ordf32(0.0) { + self.select(i); + } } } -} -#[derive(Clone, Debug)] -pub struct Selection { - pub selected: BTreeSet, - pub excess: u64, - pub excess_strategies: HashMap, -} + #[must_use] + pub fn select_until_target_met(&mut self, target: Target, drain: Drain) -> Option<()> { + self.select_until(|cs| cs.is_target_met(target, drain)) + } -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)] -pub enum ExcessStrategyKind { - ToFee, - ToRecipient, - ToDrain, -} + #[must_use] + pub fn select_until( + &mut self, + mut predicate: impl FnMut(&CoinSelector<'a>) -> bool, + ) -> Option<()> { + loop { + if predicate(&*self) { + break Some(()); + } -#[derive(Clone, Copy, Debug)] -pub struct ExcessStrategy { - pub recipient_value: Option, - pub drain_value: Option, - pub fee: u64, - pub weight: u32, - pub waste: i64, -} + if !self.select_next() { + break None; + } + } + } -impl Selection { - pub fn apply_selection<'a, T>( - &'a self, - candidates: &'a [T], - ) -> impl Iterator + 'a { - self.selected.iter().map(move |i| &candidates[*i]) + pub fn select_iter(self) -> SelectIter<'a> { + SelectIter { cs: self.clone() } } - /// Returns the [`ExcessStrategy`] that results in the least waste. - pub fn best_strategy(&self) -> (&ExcessStrategyKind, &ExcessStrategy) { - self.excess_strategies - .iter() - .min_by_key(|&(_, a)| a.waste) - .expect("selection has no excess strategy") + pub fn branch_and_bound( + &self, + metric: M, + ) -> impl Iterator, M::Score)>> { + crate::bnb::BnbIter::new(self.clone(), metric) } } -impl core::fmt::Display for ExcessStrategyKind { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - ExcessStrategyKind::ToFee => core::write!(f, "to_fee"), - ExcessStrategyKind::ToRecipient => core::write!(f, "to_recipient"), - ExcessStrategyKind::ToDrain => core::write!(f, "to_drain"), - } - } +pub struct SelectIter<'a> { + cs: CoinSelector<'a>, } -impl ExcessStrategy { - /// Returns feerate in sats/wu. - pub fn feerate(&self) -> f32 { - self.fee as f32 / self.weight as f32 +impl<'a> Iterator for SelectIter<'a> { + type Item = (CoinSelector<'a>, usize, WeightedValue); + + fn next(&mut self) -> Option { + let (index, wv) = self.cs.unselected().next()?; + self.cs.select(index); + Some((self.cs.clone(), index, wv)) } } -#[cfg(test)] -mod test { - use crate::{ExcessStrategyKind, SelectionConstraint}; - - use super::{CoinSelector, CoinSelectorOpt, WeightedValue}; - - /// Ensure `target_value` is respected. Can't have any disrespect. - #[test] - fn target_value_respected() { - let target_value = 1000_u64; - - let candidates = (500..1500_u64) - .map(|value| WeightedValue { - value, - weight: 100, - input_count: 1, - is_segwit: false, - }) - .collect::>(); - - let opts = CoinSelectorOpt { - target_value: Some(target_value), - max_extra_target: 0, - target_feerate: 0.00, - long_term_feerate: None, - min_absolute_fee: 0, - base_weight: 10, - drain_weight: 10, - spend_drain_weight: 10, - min_drain_value: 10, - }; - - for (index, v) in candidates.iter().enumerate() { - let mut selector = CoinSelector::new(&candidates, &opts); - assert!(selector.select(index)); +impl<'a> DoubleEndedIterator for SelectIter<'a> { + fn next_back(&mut self) -> Option { + let (index, wv) = self.cs.unselected().next_back()?; + self.cs.select(index); + Some((self.cs.clone(), index, wv)) + } +} - let res = selector.finish(); - if v.value < opts.target_value.unwrap_or(0) { - let err = res.expect_err("should have failed"); - assert_eq!(err.selected, v.value); - assert_eq!(err.missing, target_value - v.value); - assert_eq!(err.constraint, SelectionConstraint::MinAbsoluteFee); +impl<'a> core::fmt::Display for CoinSelector<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[")?; + let mut candidates = self.candidates().peekable(); + + while let Some((i, _)) = candidates.next() { + write!(f, "{}", i)?; + if self.is_selected(i) { + write!(f, "✔")?; + } else if self.banned().contains(&i) { + write!(f, "✘")? } else { - let sel = res.expect("should have succeeded"); - assert_eq!(sel.excess, v.value - opts.target_value.unwrap_or(0)); + write!(f, "☐")?; } - } - } - - #[test] - fn drain_all() { - let candidates = (0..100) - .map(|_| WeightedValue { - value: 666, - weight: 166, - input_count: 1, - is_segwit: false, - }) - .collect::>(); - - let opts = CoinSelectorOpt { - target_value: None, - max_extra_target: 0, - target_feerate: 0.25, - long_term_feerate: None, - min_absolute_fee: 0, - base_weight: 10, - drain_weight: 100, - spend_drain_weight: 66, - min_drain_value: 1000, - }; - let selection = CoinSelector::new(&candidates, &opts) - .select_until_finished() - .expect("should succeed"); - - assert!(selection.selected.len() > 1); - assert_eq!(selection.excess_strategies.len(), 1); + if candidates.peek().is_some() { + write!(f, ", ")?; + } + } - let (kind, strategy) = selection.best_strategy(); - assert_eq!(*kind, ExcessStrategyKind::ToDrain); - assert!(strategy.recipient_value.is_none()); - assert!(strategy.drain_value.is_some()); + write!(f, "]") } - - /// TODO: Tests to add: - /// * `finish` should ensure at least `target_value` is selected. - /// * actual feerate should be equal or higher than `target_feerate`. - /// * actual drain value should be equal to or higher than `min_drain_value` (or else no drain). - fn _todo() {} } diff --git a/nursery/coin_select/src/feerate.rs b/nursery/coin_select/src/feerate.rs new file mode 100644 index 000000000..5f57f818c --- /dev/null +++ b/nursery/coin_select/src/feerate.rs @@ -0,0 +1,89 @@ +use crate::ord_float::Ordf32; +use core::ops::{Add, Sub}; + +/// Fee rate +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +// Internally stored as satoshi/weight unit +pub struct FeeRate(Ordf32); + +impl FeeRate { + /// Create a new instance checking the value provided + /// + /// ## Panics + /// + /// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative. + fn new_checked(value: f32) -> Self { + assert!(value.is_normal() || value == 0.0); + assert!(value.is_sign_positive()); + + Self(Ordf32(value)) + } + + /// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes + /// + /// ## Panics + /// + /// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative. + pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self { + Self::new_checked(btc_per_kvb * 1e5 / 4.0) + } + + /// A feerate of zero + pub fn zero() -> Self { + Self(Ordf32(0.0)) + } + + /// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte + /// + /// ## Panics + /// + /// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative. + pub fn from_sat_per_vb(sat_per_vb: f32) -> Self { + Self::new_checked(sat_per_vb / 4.0) + } + + /// Create a new [`FeeRate`] with the default min relay fee value + pub const fn default_min_relay_fee() -> Self { + Self(Ordf32(0.25)) + } + + /// Calculate fee rate from `fee` and weight units (`wu`). + pub fn from_wu(fee: u64, wu: usize) -> Self { + Self::from_sat_per_wu(fee as f32 / wu as f32) + } + + pub fn from_sat_per_wu(sats_per_wu: f32) -> Self { + Self::new_checked(sats_per_wu) + } + + /// Calculate fee rate from `fee` and `vbytes`. + pub fn from_vb(fee: u64, vbytes: usize) -> Self { + let rate = fee as f32 / vbytes as f32; + Self::from_sat_per_vb(rate) + } + + /// Return the value as satoshi/vbyte + pub fn as_sat_vb(&self) -> f32 { + self.0 .0 * 4.0 + } + + pub fn spwu(&self) -> f32 { + self.0 .0 + } +} + +impl Add for FeeRate { + type Output = Self; + + fn add(self, rhs: FeeRate) -> Self::Output { + Self(Ordf32(self.0 .0 + rhs.0 .0)) + } +} + +impl Sub for FeeRate { + type Output = Self; + + fn sub(self, rhs: FeeRate) -> Self::Output { + Self(Ordf32(self.0 .0 - rhs.0 .0)) + } +} diff --git a/nursery/coin_select/src/lib.rs b/nursery/coin_select/src/lib.rs index ff4d45399..992cfd1b5 100644 --- a/nursery/coin_select/src/lib.rs +++ b/nursery/coin_select/src/lib.rs @@ -1,33 +1,42 @@ -#![no_std] - -#[cfg(feature = "std")] -extern crate std; - +#[no_std] +#[allow(unused)] #[macro_use] extern crate alloc; -extern crate bdk_chain; - -use alloc::vec::Vec; -use bdk_chain::{ - bitcoin, - collections::{BTreeSet, HashMap}, -}; -use bitcoin::{LockTime, Transaction, TxOut}; -use core::fmt::{Debug, Display}; mod coin_selector; +pub mod ord_float; pub use coin_selector::*; -mod bnb; -pub use bnb::*; +pub mod bnb; +pub mod metrics; + +mod feerate; +pub use feerate::*; +pub mod change_policy; /// Txin "base" fields include `outpoint` (32+4) and `nSequence` (4). This does not include -/// `scriptSigLen` or `scriptSig`. +/// `scriptSigLen`, `scriptSig` or witness stack length pub const TXIN_BASE_WEIGHT: u32 = (32 + 4 + 4) * 4; +/// The weight of a TXOUT without the `scriptPubkey` (and script pubkey length field). +/// Just the weight of the value field. +pub const TXOUT_BASE_WEIGHT: u32 = 4 * core::mem::size_of::() as u32; // just the value + /// Helper to calculate varint size. `v` is the value the varint represents. -// Shamelessly copied from -// https://github.com/rust-bitcoin/rust-miniscript/blob/d5615acda1a7fdc4041a11c1736af139b8c7ebe8/src/util.rs#L8 -pub(crate) fn varint_size(v: usize) -> u32 { - bitcoin::VarInt(v as u64).len() as u32 +fn varint_size(v: usize) -> u32 { + if v <= 0xfc { + return 1; + } + if v <= 0xffff { + return 3; + } + if v <= 0xffff_ffff { + return 5; + } + return 9; +} + +#[allow(unused)] +fn txout_weight_from_spk_len(spk_len: usize) -> u32 { + (TXOUT_BASE_WEIGHT + varint_size(spk_len) + (spk_len as u32)) * 4 } diff --git a/nursery/coin_select/src/metrics.rs b/nursery/coin_select/src/metrics.rs new file mode 100644 index 000000000..1ddb0f982 --- /dev/null +++ b/nursery/coin_select/src/metrics.rs @@ -0,0 +1,93 @@ +use crate::{bnb::BnBMetric, ord_float::Ordf32, CoinSelector, Drain, Target}; +mod waste; +pub use waste::*; + +pub struct Changeless<'c, C> { + target: Target, + change_policy: &'c C, +} + +impl<'c, C> BnBMetric for Changeless<'c, C> +where + for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain, +{ + type Score = bool; + + fn score<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { + let drain = (self.change_policy)(cs, self.target); + if cs.is_target_met(self.target, drain) { + let has_drain = !drain.is_none(); + Some(has_drain) + } else { + None + } + } + + fn bound<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { + Some(change_lower_bound(cs, self.target, &self.change_policy).is_some()) + } + + fn requires_ordering_by_descending_value_pwu(&self) -> bool { + true + } +} + +// Returns a drain if the current selection and every possible future selection would have a change +// output (otherwise Drain::none()) by using the heurisitic that if it has change with the current +// selection and it has one when we select every negative effective value candidate then it will +// always have a drain. We are essentially assuming that the change_policy is monotone with respect +// to the excess of the selection. +// +// 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<'a>( + cs: &CoinSelector<'a>, + target: Target, + change_policy: &impl Fn(&CoinSelector<'a>, Target) -> Drain, +) -> Drain { + let has_change_now = change_policy(cs, target).is_some(); + + if has_change_now { + let mut least_excess = cs.clone(); + cs.unselected() + .rev() + .take_while(|(_, wv)| wv.effective_value(target.feerate) < Ordf32(0.0)) + .for_each(|(index, _)| { + least_excess.select(index); + }); + + change_policy(&least_excess, target) + } else { + Drain::none() + } +} + +macro_rules! impl_for_tuple { + ($($a:ident $b:tt)*) => { + impl<$($a),*> BnBMetric for ($($a),*) + where $($a: BnBMetric),* + { + type Score=($(<$a>::Score),*); + + #[allow(unused)] + fn score<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { + Some(($(self.$b.score(cs)?),*)) + } + #[allow(unused)] + fn bound<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { + Some(($(self.$b.bound(cs)?),*)) + } + #[allow(unused)] + fn requires_ordering_by_descending_value_pwu(&self) -> bool { + [$(self.$b.requires_ordering_by_descending_value_pwu()),*].iter().all(|x| *x) + + } + } + }; +} + +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); diff --git a/nursery/coin_select/src/metrics/waste.rs b/nursery/coin_select/src/metrics/waste.rs new file mode 100644 index 000000000..5b29a744a --- /dev/null +++ b/nursery/coin_select/src/metrics/waste.rs @@ -0,0 +1,215 @@ +use super::change_lower_bound; +use crate::{ + bnb::BnBMetric, ord_float::Ordf32, CoinSelector, Drain, FeeRate, Target, WeightedValue, +}; + +pub struct Waste<'c, C> { + pub target: Target, + pub long_term_feerate: FeeRate, + pub change_policy: &'c C, +} + +impl<'c, C> BnBMetric for Waste<'c, C> +where + for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain, +{ + type Score = Ordf32; + + fn score<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { + let drain = (self.change_policy)(cs, self.target); + if !cs.is_target_met(self.target, drain) { + return None; + } + let score = cs.waste(self.target, self.long_term_feerate, drain, 1.0); + Some(Ordf32(score)) + } + + fn bound<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { + let rate_diff = self.target.feerate.spwu() - self.long_term_feerate.spwu(); + // whether from this coin selection it's possible to avoid change + let change_lower_bound = change_lower_bound(&cs, self.target, &self.change_policy); + const IGNORE_EXCESS: f32 = 0.0; + const INCLUDE_EXCESS: f32 = 1.0; + + if rate_diff >= 0.0 { + // Our lower bound algorithms differ depending on whether we have already met the target or not. + if cs.is_target_met(self.target, change_lower_bound) { + let current_change = (self.change_policy)(&cs, self.target); + + // first lower bound candidate is just the selection itself + let mut lower_bound = cs.waste( + self.target, + self.long_term_feerate, + current_change, + INCLUDE_EXCESS, + ); + + // But don't stop there we might be able to select negative value inputs which might + // lower excess and reduce waste either by: + // - removing the need for a change output + // - reducing the excess if the current selection is changeless (only possible when rate_diff is small). + let should_explore_changeless = change_lower_bound.is_none(); + + if should_explore_changeless { + let selection_with_as_much_negative_ev_as_possible = cs + .clone() + .select_iter() + .rev() + .take_while(|(cs, _, wv)| { + wv.effective_value(self.target.feerate).0 < 0.0 + && cs.is_target_met(self.target, Drain::none()) + }) + .last(); + + if let Some((cs, _, _)) = selection_with_as_much_negative_ev_as_possible { + let can_do_better_by_slurping = + cs.unselected().rev().next().and_then(|(_, wv)| { + if wv.effective_value(self.target.feerate).0 < 0.0 { + Some(wv) + } else { + None + } + }); + let lower_bound_without_change = match can_do_better_by_slurping { + Some(finishing_input) => { + // NOTE we are slurping negative value here to try and reduce excess in + // the hopes of getting rid of the change output + let value_to_slurp = -cs.rate_excess(self.target, Drain::none()); + let weight_to_extinguish_excess = + slurp_wv(finishing_input, value_to_slurp, self.target.feerate); + let waste_to_extinguish_excess = + weight_to_extinguish_excess * rate_diff; + let waste_after_excess_reduction = cs.waste( + self.target, + self.long_term_feerate, + Drain::none(), + IGNORE_EXCESS, + ) + waste_to_extinguish_excess; + waste_after_excess_reduction + } + None => cs.waste( + self.target, + self.long_term_feerate, + Drain::none(), + INCLUDE_EXCESS, + ), + }; + + lower_bound = lower_bound.min(lower_bound_without_change); + } + } + + Some(Ordf32(lower_bound)) + } else { + // If feerate >= long_term_feerate, You *might* think that the waste lower bound + // here is just the fewest number of inputs we need to meet the target but **no**. + // Consider if there is 1 sat remaining to reach target. Should you add all the + // weight of the next input for the waste calculation? *No* this leaads to a + // pesimistic lower bound even if we ignore the excess because it adds too much + // weight. + // + // Step 1: select everything up until the input that hits the target. + let (mut cs, slurp_index, to_slurp) = cs + .clone() + .select_iter() + .find(|(cs, _, _)| cs.is_target_met(self.target, change_lower_bound))?; + + cs.deselect(slurp_index); + + // Step 2: We pretend that the final input exactly cancels out the remaining excess + // by taking whatever value we want from it but at the value per weight of the real + // input. + let ideal_next_weight = { + // satisfying absolute and feerate requires different calculations sowe do them + // both indepdently and find which requires the most weight of the next input. + let remaining_rate = cs.rate_excess(self.target, change_lower_bound); + let remaining_abs = cs.absolute_excess(self.target, change_lower_bound); + + let weight_to_satisfy_abs = + remaining_abs.min(0) as f32 / to_slurp.value_pwu().0; + let weight_to_satisfy_rate = + slurp_wv(to_slurp, remaining_rate.min(0), self.target.feerate); + let weight_to_satisfy = weight_to_satisfy_abs.max(weight_to_satisfy_rate); + debug_assert!(weight_to_satisfy <= to_slurp.weight as f32); + weight_to_satisfy + }; + let weight_lower_bound = cs.selected_weight() as f32 + ideal_next_weight; + let mut waste = weight_lower_bound * rate_diff; + waste += change_lower_bound.waste(self.target.feerate, self.long_term_feerate); + + Some(Ordf32(waste)) + } + } else { + // When long_term_feerate > current feerate each input by itself has negative waste. + // This doesn't mean that waste monotonically decreases as you add inputs because + // somewhere along the line adding an input might cause the change policy to add a + // change ouput which could increase waste. + // + // So we have to try two things and we which one is best to find the lower bound: + // 1. try selecting everything regardless of change + let mut lower_bound = { + let mut cs = cs.clone(); + // ... but first check that by selecting all effective we can actually reach target + cs.select_all_effective(self.target.feerate); + if !cs.is_target_met(self.target, Drain::none()) { + return None; + } + let change_at_value_optimum = (self.change_policy)(&cs, self.target); + cs.select_all(); + // NOTE: we use the change from our "all effective" selection for min waste since + // selecting all might not have change but in that case we'll catch it below. + cs.waste( + self.target, + self.long_term_feerate, + change_at_value_optimum, + IGNORE_EXCESS, + ) + }; + + let look_for_changeless_solution = change_lower_bound.is_none(); + + if look_for_changeless_solution { + // 2. select the highest weight solution with no change + let highest_weight_selection_without_change = cs + .clone() + .select_iter() + .rev() + .take_while(|(cs, _, wv)| { + wv.effective_value(self.target.feerate).0 < 0.0 + || (self.change_policy)(&cs, self.target).is_none() + }) + .last(); + + if let Some((cs, _, _)) = highest_weight_selection_without_change { + let no_change_waste = cs.waste( + self.target, + self.long_term_feerate, + Drain::none(), + IGNORE_EXCESS, + ); + + lower_bound = lower_bound.min(no_change_waste) + } + } + + Some(Ordf32(lower_bound)) + } + } + + fn requires_ordering_by_descending_value_pwu(&self) -> bool { + true + } +} + +/// Used to pretend that a candidate had precisely `value_to_slurp` + fee needed to include it. It +/// tells you how much weight such a perfect candidate would have if it had the same value per +/// weight unit as `candidate`. This is useful for estimating a lower weight bound for a perfect +/// match. +fn slurp_wv(candidate: WeightedValue, value_to_slurp: i64, feerate: FeeRate) -> f32 { + // the value per weight unit this candidate offers at feerate + let value_per_wu = (candidate.value as f32 / candidate.weight as f32) - feerate.spwu(); + // return how much weight we need + let weight_needed = value_to_slurp as f32 / value_per_wu; + debug_assert!(weight_needed <= candidate.weight as f32); + weight_needed.min(0.0) +} diff --git a/nursery/coin_select/src/ord_float.rs b/nursery/coin_select/src/ord_float.rs new file mode 100644 index 000000000..2f29e4fdf --- /dev/null +++ b/nursery/coin_select/src/ord_float.rs @@ -0,0 +1,62 @@ +//! Newtypes around `f32` and `f64` that implement `Ord`. +//! +//! Backported from rust std lib [`total_cmp`] in version 1.62.0. Hopefully some day rust has this +//! in core: https://github.com/rust-lang/rfcs/issues/1249 +//! +//! [`total_cmp`]: https://doc.rust-lang.org/core/primitive.f32.html#method.total_cmp + +/// Wrapper for `f32` that implements `Ord`. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Ordf32(pub f32); +/// Wrapper for `f64` that implements `Ord`. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Ordf64(pub f64); + +impl Ord for Ordf32 { + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let mut left = self.0.to_bits() as i32; + let mut right = other.0.to_bits() as i32; + left ^= (((left >> 31) as u32) >> 1) as i32; + right ^= (((right >> 31) as u32) >> 1) as i32; + left.cmp(&right) + } +} + +impl Ord for Ordf64 { + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let mut left = self.0.to_bits() as i64; + let mut right = other.0.to_bits() as i64; + left ^= (((left >> 63) as u64) >> 1) as i64; + right ^= (((right >> 63) as u64) >> 1) as i64; + left.cmp(&right) + } +} + +impl Eq for Ordf64 {} +impl Eq for Ordf32 {} + +impl PartialOrd for Ordf32 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialOrd for Ordf64 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl core::fmt::Display for Ordf32 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl core::fmt::Display for Ordf64 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} diff --git a/nursery/coin_select/tests/bnb.rs b/nursery/coin_select/tests/bnb.rs new file mode 100644 index 000000000..ba8b66893 --- /dev/null +++ b/nursery/coin_select/tests/bnb.rs @@ -0,0 +1,187 @@ +use bdk_coin_select::{bnb::BnBMetric, CoinSelector, Drain, FeeRate, Target, WeightedValue}; +#[macro_use] +extern crate alloc; + +use alloc::vec::Vec; +use proptest::{ + prelude::*, + test_runner::{RngAlgorithm, TestRng}, +}; +use rand::{Rng, RngCore}; + +fn test_wv(mut rng: impl RngCore) -> impl Iterator { + core::iter::repeat_with(move || { + let value = rng.gen_range(0..1_000); + WeightedValue { + value, + weight: 100, + input_count: rng.gen_range(1..2), + is_segwit: rng.gen_bool(0.5), + } + }) +} + +struct MinExcessThenWeight { + target: Target, +} + +impl BnBMetric for MinExcessThenWeight { + type Score = (i64, u32); + + fn score<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { + if cs.excess(self.target, Drain::none()) < 0 { + None + } else { + Some((cs.excess(self.target, Drain::none()), cs.selected_weight())) + } + } + + fn bound<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { + let lower_bound_excess = cs.excess(self.target, Drain::none()).max(0); + let lower_bound_weight = { + let mut cs = cs.clone(); + cs.select_until_target_met(self.target, Drain::none())?; + cs.selected_weight() + }; + Some((lower_bound_excess, lower_bound_weight)) + } +} + +#[test] +/// Detect regressions/improvements by making sure it always finds the solution in the same +/// number of iterations. +fn bnb_finds_an_exact_solution_in_n_iter() { + let solution_len = 8; + let num_additional_canidates = 50; + + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + let mut wv = test_wv(&mut rng); + + let solution: Vec = (0..solution_len).map(|_| wv.next().unwrap()).collect(); + let solution_weight = solution.iter().map(|sol| sol.weight).sum(); + let target = solution.iter().map(|c| c.value).sum(); + + let mut candidates = solution.clone(); + candidates.extend(wv.take(num_additional_canidates)); + candidates.sort_unstable_by_key(|wv| core::cmp::Reverse(wv.value)); + + let cs = CoinSelector::new(&candidates, 0); + + let target = Target { + value: target, + // we're trying to find an exact selection value so set fees to 0 + feerate: FeeRate::zero(), + min_fee: 0, + }; + + let solutions = cs.branch_and_bound(MinExcessThenWeight { target }); + + let (i, (best, _score)) = solutions + .enumerate() + .take(807) + .filter_map(|(i, sol)| Some((i, sol?))) + .last() + .expect("it found a solution"); + + assert_eq!(i, 806); + + assert!(best.selected_weight() <= solution_weight); + assert_eq!(best.selected_value(), target.value); +} + +#[test] +fn bnb_finds_solution_if_possible_in_n_iter() { + let num_inputs = 18; + let target = 8_314; + 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, 0); + + let target = Target { + value: target, + feerate: FeeRate::default_min_relay_fee(), + min_fee: 0, + }; + + let solutions = cs.branch_and_bound(MinExcessThenWeight { target }); + + let (i, (sol, _score)) = solutions + .enumerate() + .filter_map(|(i, sol)| Some((i, sol?))) + .last() + .expect("found a solution"); + + assert_eq!(i, 176); + let excess = sol.excess(target, Drain::none()); + assert_eq!(excess, 8); +} + +proptest! { + + #[test] + fn bnb_always_finds_solution_if_possible(num_inputs in 1usize..50, target in 0u64..10_000) { + 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, 0); + + let target = Target { + value: target, + feerate: FeeRate::zero(), + min_fee: 0, + }; + + let solutions = cs.branch_and_bound(MinExcessThenWeight { target }); + + match solutions.enumerate().filter_map(|(i, sol)| Some((i, sol?))).last() { + Some((_i, (sol, _score))) => assert!(sol.selected_value() >= target.value), + _ => prop_assert!(!cs.is_selection_possible(target, Drain::none())), + } + } + + #[test] + fn bnb_always_finds_exact_solution_eventually( + solution_len in 1usize..10, + num_additional_canidates in 0usize..100, + num_preselected in 0usize..10 + ) { + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + let mut wv = test_wv(&mut rng); + + let solution: Vec = (0..solution_len).map(|_| wv.next().unwrap()).collect(); + let target = solution.iter().map(|c| c.value).sum(); + let solution_weight = solution.iter().map(|sol| sol.weight).sum(); + + let mut candidates = solution.clone(); + candidates.extend(wv.take(num_additional_canidates)); + + let mut cs = CoinSelector::new(&candidates, 0); + for i in 0..num_preselected.min(solution_len) { + cs.select(i); + } + + // sort in descending value + cs.sort_candidates_by_key(|(_, wv)| core::cmp::Reverse(wv.value)); + + let target = Target { + value: target, + // we're trying to find an exact selection value so set fees to 0 + feerate: FeeRate::zero(), + min_fee: 0 + }; + + let solutions = cs.branch_and_bound(MinExcessThenWeight { target }); + + let (_i, (best, _score)) = solutions + .enumerate() + .filter_map(|(i, sol)| Some((i, sol?))) + .last() + .expect("it found a solution"); + + + + prop_assert!(best.selected_weight() <= solution_weight); + prop_assert_eq!(best.selected_value(), target.value); + } +} diff --git a/nursery/coin_select/tests/waste.proptest-regressions b/nursery/coin_select/tests/waste.proptest-regressions new file mode 100644 index 000000000..38fa1c15d --- /dev/null +++ b/nursery/coin_select/tests/waste.proptest-regressions @@ -0,0 +1,10 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc b526e3a05e5dffce95e0cf357f68d6819b5b92a1c4abd79fd8fe0e2582521352 # shrinks to num_inputs = 45, target = 16494, feerate = 3.0291684, min_fee = 0, base_weight = 155, long_term_feerate_diff = -0.70271873, change_weight = 58, change_spend_weight = 82 +cc f3c37a516004e7eda9183816d72bede9084ce678830d6582f2d63306f618adee # shrinks to num_inputs = 40, target = 6598, feerate = 8.487553, min_fee = 221, base_weight = 126, long_term_feerate_diff = 3.3214626, change_weight = 18, change_spend_weight = 18 +cc a6d03a6d93eb8d5a082d69a3d1677695377823acafe3dba954ac86519accf152 # shrinks to num_inputs = 49, target = 2917, feerate = 9.786607, min_fee = 0, base_weight = 4, long_term_feerate_diff = -0.75053596, change_weight = 77, change_spend_weight = 81 +cc a1eccddab6d7da9677575154a27a1e49b391041ed9e32b9bf937efd72ef0ab03 # shrinks to num_inputs = 12, target = 3988, feerate = 4.3125916, min_fee = 453, base_weight = 0, long_term_feerate_diff = -0.018570423, change_weight = 15, change_spend_weight = 32 diff --git a/nursery/coin_select/tests/waste.rs b/nursery/coin_select/tests/waste.rs new file mode 100644 index 000000000..441a684e4 --- /dev/null +++ b/nursery/coin_select/tests/waste.rs @@ -0,0 +1,440 @@ +#[cfg(test)] +use bdk_coin_select::{ + change_policy, metrics::Waste, ord_float::Ordf32, CoinSelector, Drain, FeeRate, Target, + WeightedValue, +}; +use proptest::{ + prelude::*, + test_runner::{RngAlgorithm, TestRng}, +}; +use rand::prelude::IteratorRandom; + +#[test] +fn waste_all_selected_except_one_is_optimal_and_awkward() { + let num_inputs = 40; + let target = 15578; + let feerate = 8.190512; + let min_fee = 0; + let base_weight = 453; + let long_term_feerate_diff = -3.630499; + let change_weight = 1; + let change_spend_weight = 41; + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + let long_term_feerate = + FeeRate::from_sat_per_vb((0.0f32).max(feerate - long_term_feerate_diff)); + let feerate = FeeRate::from_sat_per_vb(feerate); + let drain = Drain { + weight: change_weight, + spend_weight: change_spend_weight, + value: 0, + }; + + let change_policy = change_policy::min_waste(drain, long_term_feerate); + let wv = test_wv(&mut rng); + let candidates = wv.take(num_inputs).collect::>(); + + let cs = CoinSelector::new(&candidates, base_weight); + let target = Target { + value: target, + feerate, + min_fee, + }; + + let solutions = cs.branch_and_bound(Waste { + target, + long_term_feerate, + change_policy: &change_policy, + }); + + let (_i, (best, score)) = solutions + .enumerate() + .filter_map(|(i, sol)| Some((i, sol?))) + .last() + .expect("it should have found solution"); + + let mut all_selected = cs.clone(); + all_selected.select_all(); + let target_waste = all_selected.waste( + target, + long_term_feerate, + change_policy(&all_selected, target), + 1.0, + ); + assert!(score.0 < target_waste); + assert_eq!(best.selected().len(), 39); +} + +#[test] +fn waste_naive_effective_value_shouldnt_be_better() { + let num_inputs = 23; + let target = 1475; + let feerate = 1.0; + let min_fee = 989; + let base_weight = 0; + let long_term_feerate_diff = 3.8413858; + let change_weight = 1; + let change_spend_weight = 1; + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + let long_term_feerate = + FeeRate::from_sat_per_vb((0.0f32).max(feerate - long_term_feerate_diff)); + let feerate = FeeRate::from_sat_per_vb(feerate); + let drain = Drain { + weight: change_weight, + spend_weight: change_spend_weight, + value: 0, + }; + + let change_policy = change_policy::min_waste(drain, long_term_feerate); + let wv = test_wv(&mut rng); + let candidates = wv.take(num_inputs).collect::>(); + + let cs = CoinSelector::new(&candidates, base_weight); + + let target = Target { + value: target, + feerate, + min_fee, + }; + + let solutions = cs.branch_and_bound(Waste { + target, + long_term_feerate, + change_policy: &change_policy, + }); + + let (_i, (_best, score)) = solutions + .enumerate() + .filter_map(|(i, sol)| Some((i, sol?))) + .last() + .expect("should find solution"); + + let mut naive_select = cs.clone(); + naive_select.sort_candidates_by_key(|(_, wv)| core::cmp::Reverse(wv.value_pwu())); + // we filter out failing onces below + let _ = naive_select.select_until_target_met(target, drain); + + let bench_waste = naive_select.waste( + target, + long_term_feerate, + change_policy(&naive_select, target), + 1.0, + ); + + assert!(score < Ordf32(bench_waste)); +} + +#[test] +fn waste_doesnt_take_too_long_to_finish() { + let start = std::time::Instant::now(); + let num_inputs = 22; + let target = 0; + let feerate = 4.9522414; + let min_fee = 0; + let base_weight = 2; + let long_term_feerate_diff = -0.17994404; + let change_weight = 1; + let change_spend_weight = 34; + + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + let long_term_feerate = + FeeRate::from_sat_per_vb((0.0f32).max(feerate - long_term_feerate_diff)); + let feerate = FeeRate::from_sat_per_vb(feerate); + let drain = Drain { + weight: change_weight, + spend_weight: change_spend_weight, + value: 0, + }; + + let change_policy = change_policy::min_waste(drain, long_term_feerate); + let wv = test_wv(&mut rng); + let candidates = wv.take(num_inputs).collect::>(); + + let cs = CoinSelector::new(&candidates, base_weight); + + let target = Target { + value: target, + feerate, + min_fee, + }; + + let solutions = cs.branch_and_bound(Waste { + target, + long_term_feerate, + change_policy: &change_policy, + }); + + solutions + .enumerate() + .inspect(|_| { + if start.elapsed().as_millis() > 1_000 { + panic!("took too long to finish") + } + }) + .filter_map(|(i, sol)| Some((i, sol?))) + .last() + .expect("should find solution"); +} + +/// When long term feerate is lower than current adding new inputs should in general make things +/// worse except in the case that we can get rid of the change output with negative effective +/// value inputs. In this case the right answer to select everything. +#[test] +fn waste_lower_long_term_feerate_but_still_need_to_select_all() { + let num_inputs = 16; + let target = 5586; + let feerate = 9.397041; + let min_fee = 0; + let base_weight = 91; + let long_term_feerate_diff = 0.22074795; + let change_weight = 1; + let change_spend_weight = 27; + + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + let long_term_feerate = FeeRate::from_sat_per_vb(0.0f32.max(feerate - long_term_feerate_diff)); + let feerate = FeeRate::from_sat_per_vb(feerate); + let drain = Drain { + weight: change_weight, + spend_weight: change_spend_weight, + value: 0, + }; + + let change_policy = change_policy::min_waste(drain, long_term_feerate); + let wv = test_wv(&mut rng); + let candidates = wv.take(num_inputs).collect::>(); + + let cs = CoinSelector::new(&candidates, base_weight); + + let target = Target { + value: target, + feerate, + min_fee, + }; + + let solutions = cs.branch_and_bound(Waste { + target, + long_term_feerate, + change_policy: &change_policy, + }); + let bench = { + let mut all_selected = cs.clone(); + all_selected.select_all(); + all_selected + }; + + let (_i, (_sol, waste)) = solutions + .enumerate() + .filter_map(|(i, sol)| Some((i, sol?))) + .last() + .expect("should find solution"); + + let bench_waste = bench.waste( + target, + long_term_feerate, + change_policy(&bench, target), + 1.0, + ); + + assert!(waste <= Ordf32(bench_waste)); +} + +#[test] +fn waste_low_but_non_negative_rate_diff_means_adding_more_inputs_might_reduce_excess() { + let num_inputs = 22; + let target = 7620; + let feerate = 8.173157; + let min_fee = 0; + let base_weight = 35; + let long_term_feerate_diff = 0.0; + let change_weight = 1; + let change_spend_weight = 47; + + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + let long_term_feerate = FeeRate::from_sat_per_vb(0.0f32.max(feerate - long_term_feerate_diff)); + let feerate = FeeRate::from_sat_per_vb(feerate); + let drain = Drain { + weight: change_weight, + spend_weight: change_spend_weight, + value: 0, + }; + + let change_policy = change_policy::min_waste(drain, long_term_feerate); + let wv = test_wv(&mut rng); + let candidates = wv.take(num_inputs).collect::>(); + + let cs = CoinSelector::new(&candidates, base_weight); + + let target = Target { + value: target, + feerate, + min_fee, + }; + + let solutions = cs.branch_and_bound(Waste { + target, + long_term_feerate, + change_policy: &change_policy, + }); + let bench = { + let mut all_selected = cs.clone(); + all_selected.select_all(); + all_selected + }; + + let (_i, (_sol, waste)) = solutions + .enumerate() + .filter_map(|(i, sol)| Some((i, sol?))) + .last() + .expect("should find solution"); + + let bench_waste = bench.waste( + target, + long_term_feerate, + change_policy(&bench, target), + 1.0, + ); + + assert!(waste <= Ordf32(bench_waste)); +} + +proptest! { + #![proptest_config(ProptestConfig { + timeout: 6_000, + cases: 1_000, + ..Default::default() + })] + #[test] + #[cfg(not(debug_assertions))] // too slow if compiling for debug + fn waste_prop_waste( + num_inputs in 0usize..50, + target in 0u64..25_000, + feerate in 1.0f32..10.0, + min_fee in 0u64..1_000, + base_weight in 0u32..500, + long_term_feerate_diff in -5.0f32..5.0, + change_weight in 1u32..100, + change_spend_weight in 1u32..100, + ) { + println!("======================================="); + let start = std::time::Instant::now(); + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + let long_term_feerate = FeeRate::from_sat_per_vb(0.0f32.max(feerate - long_term_feerate_diff)); + let feerate = FeeRate::from_sat_per_vb(feerate); + let drain = Drain { + weight: change_weight, + spend_weight: change_spend_weight, + value: 0 + }; + + let change_policy = crate::change_policy::min_waste(drain, long_term_feerate); + let wv = test_wv(&mut rng); + let candidates = wv.take(num_inputs).collect::>(); + + let cs = CoinSelector::new(&candidates, base_weight); + + let target = Target { + value: target, + feerate, + min_fee + }; + + let solutions = cs.branch_and_bound(Waste { + target, + long_term_feerate, + change_policy: &change_policy + }); + + + let best = solutions + .enumerate() + .filter_map(|(i, sol)| Some((i, sol?))) + .last(); + + match best { + Some((_i, (sol, _score))) => { + + let mut cmp_benchmarks = vec![ + { + let mut naive_select = cs.clone(); + naive_select.sort_candidates_by_key(|(_, wv)| core::cmp::Reverse(wv.effective_value(target.feerate))); + // we filter out failing onces below + let _ = naive_select.select_until_target_met(target, drain); + naive_select + }, + { + let mut all_selected = cs.clone(); + all_selected.select_all(); + all_selected + }, + { + let mut all_effective_selected = cs.clone(); + all_effective_selected.select_all_effective(target.feerate); + all_effective_selected + } + ]; + + // add some random selections -- technically it's possible that one of these is better but it's very unlikely if our algorithm is working correctly. + cmp_benchmarks.extend((0..10).map(|_|randomly_satisfy_target_with_low_waste(&cs, target, long_term_feerate, &change_policy, &mut rng))); + + let cmp_benchmarks = cmp_benchmarks.into_iter().filter(|cs| cs.is_target_met(target, change_policy(&cs, target))); + let sol_waste = sol.waste(target, long_term_feerate, change_policy(&sol, target), 1.0); + + for (_bench_id, mut bench) in cmp_benchmarks.enumerate() { + let bench_waste = bench.waste(target, long_term_feerate, change_policy(&bench, target), 1.0); + if sol_waste > bench_waste { + dbg!(_bench_id); + println!("bnb solution: {}", sol); + bench.sort_candidates_by_descending_value_pwu(); + println!("found better: {}", bench); + } + prop_assert!(sol_waste <= bench_waste); + } + }, + None => { + dbg!(feerate - long_term_feerate); + prop_assert!(!cs.is_selection_plausible_with_change_policy(target, &change_policy)); + } + } + + dbg!(start.elapsed()); + } +} + +fn test_wv(mut rng: impl RngCore) -> impl Iterator { + core::iter::repeat_with(move || { + let value = rng.gen_range(0..1_000); + WeightedValue { + value, + weight: rng.gen_range(0..100), + input_count: rng.gen_range(1..2), + is_segwit: rng.gen_bool(0.5), + } + }) +} + +// this is probably a useful thing to have on CoinSelector but I don't want to design it yet +#[allow(unused)] +fn randomly_satisfy_target_with_low_waste<'a>( + cs: &CoinSelector<'a>, + target: Target, + long_term_feerate: FeeRate, + change_policy: &impl Fn(&CoinSelector, Target) -> Drain, + rng: &mut impl RngCore, +) -> CoinSelector<'a> { + let mut cs = cs.clone(); + + let mut last_waste: Option = None; + while let Some(next) = cs.unselected_indexes().choose(rng) { + cs.select(next); + let change = change_policy(&cs, target); + if cs.is_target_met(target, change) { + let curr_waste = cs.waste(target, long_term_feerate, change, 1.0); + if let Some(last_waste) = last_waste { + if curr_waste > last_waste { + break; + } + } + last_waste = Some(curr_waste); + } + } + cs +} From ee196e824f2d8ba00ed47361e33148101db12d52 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Thu, 23 Mar 2023 12:26:55 +1100 Subject: [PATCH 2/8] Make bdk_coin_select work on 1.48.0 and add some tooling to enforce this --- Cargo.1.48.0.toml | 4 ++++ build-msrv-crates.sh | 16 ++++++++++++++++ nursery/coin_select/Cargo.toml | 2 +- nursery/coin_select/src/bnb.rs | 4 ++-- nursery/coin_select/src/change_policy.rs | 1 + nursery/coin_select/src/coin_selector.rs | 14 ++++++++------ 6 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 Cargo.1.48.0.toml create mode 100755 build-msrv-crates.sh diff --git a/Cargo.1.48.0.toml b/Cargo.1.48.0.toml new file mode 100644 index 000000000..922862700 --- /dev/null +++ b/Cargo.1.48.0.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "nursery/coin_select" +] diff --git a/build-msrv-crates.sh b/build-msrv-crates.sh new file mode 100755 index 000000000..4402dd1ee --- /dev/null +++ b/build-msrv-crates.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +trap ' + signal=$?; + cleanup + exit $signal; +' INT + +cleanup() { + mv Cargo.tmp.toml Cargo.toml 2>/dev/null +} + +cp Cargo.toml Cargo.tmp.toml +cp Cargo.1.48.0.toml Cargo.toml +cat Cargo.toml +cargo build --release +cleanup diff --git a/nursery/coin_select/Cargo.toml b/nursery/coin_select/Cargo.toml index ce83dae66..5221d31f2 100644 --- a/nursery/coin_select/Cargo.toml +++ b/nursery/coin_select/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bdk_coin_select" version = "0.1.0" -edition = "2021" +edition = "2018" [dependencies] # No dependencies! Don't add any please! diff --git a/nursery/coin_select/src/bnb.rs b/nursery/coin_select/src/bnb.rs index abf7bda22..b9a384898 100644 --- a/nursery/coin_select/src/bnb.rs +++ b/nursery/coin_select/src/bnb.rs @@ -93,8 +93,8 @@ impl<'a, M: BnBMetric> BnbIter<'a, M> { let mut exclusion_cs = cs.clone(); exclusion_cs.ban(next_unselected); - for (child_cs, is_exclusion) in [(&inclusion_cs, false), (&exclusion_cs, true)] { - self.consider_adding_to_queue(child_cs, is_exclusion) + for (child_cs, is_exclusion) in &[(&inclusion_cs, false), (&exclusion_cs, true)] { + self.consider_adding_to_queue(child_cs, *is_exclusion) } } } diff --git a/nursery/coin_select/src/change_policy.rs b/nursery/coin_select/src/change_policy.rs index 588918465..65c2baeab 100644 --- a/nursery/coin_select/src/change_policy.rs +++ b/nursery/coin_select/src/change_policy.rs @@ -1,4 +1,5 @@ use crate::{CoinSelector, Drain, FeeRate, Target}; +use core::convert::TryInto; /// Add a change output if the value it would receive is greater than or equal to `min_value`. /// diff --git a/nursery/coin_select/src/coin_selector.rs b/nursery/coin_select/src/coin_selector.rs index c064831e2..6db86a6a7 100644 --- a/nursery/coin_select/src/coin_selector.rs +++ b/nursery/coin_select/src/coin_selector.rs @@ -112,7 +112,7 @@ impl<'a> CoinSelector<'a> { ) -> impl DoubleEndedIterator + ExactSizeIterator + '_ { self.candidate_order .iter() - .map(|i| (*i, self.candidates[*i])) + .map(move |i| (*i, self.candidates[*i])) } pub fn candidate(&self, index: usize) -> WeightedValue { @@ -124,7 +124,7 @@ impl<'a> CoinSelector<'a> { } pub fn apply_selection(&self, candidates: &'a [T]) -> impl Iterator + '_ { - self.selected.iter().map(|i| &candidates[*i]) + self.selected.iter().map(move |i| &candidates[*i]) } pub fn select(&mut self, index: usize) -> bool { @@ -266,7 +266,8 @@ impl<'a> CoinSelector<'a> { F: FnMut((usize, WeightedValue), (usize, WeightedValue)) -> core::cmp::Ordering, { let order = self.candidate_order.to_mut(); - order.sort_by(|a, b| cmp((*a, self.candidates[*a]), (*b, self.candidates[*b]))) + let candidates = &self.candidates; + order.sort_by(|a, b| cmp((*a, candidates[*a]), (*b, candidates[*b]))) } pub fn sort_candidates_by_key(&mut self, mut key_fn: F) @@ -310,11 +311,12 @@ impl<'a> CoinSelector<'a> { pub fn selected(&self) -> impl ExactSizeIterator + '_ { self.selected .iter() - .map(|&index| (index, self.candidates[index])) + .map(move |&index| (index, self.candidates[index])) } pub fn unselected(&self) -> impl DoubleEndedIterator + '_ { - self.unselected_indexes().map(|i| (i, self.candidates[i])) + self.unselected_indexes() + .map(move |i| (i, self.candidates[i])) } pub fn selected_indexes(&self) -> &BTreeSet { @@ -324,7 +326,7 @@ impl<'a> CoinSelector<'a> { pub fn unselected_indexes(&self) -> impl DoubleEndedIterator + '_ { self.candidate_order .iter() - .filter(|index| !(self.selected.contains(index) || self.banned.contains(index))) + .filter(move |index| !(self.selected.contains(index) || self.banned.contains(index))) .map(|index| *index) } From 948b09c3a2196c045559e42bc1b3891497260841 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Thu, 23 Mar 2023 17:18:09 +1100 Subject: [PATCH 3/8] Introduce new coin selection implementation --- nursery/coin_select/Cargo.toml | 5 + nursery/coin_select/src/change_policy.rs | 2 + nursery/coin_select/src/coin_selector.rs | 79 +++++++++++- nursery/coin_select/src/feerate.rs | 2 +- nursery/coin_select/src/float.rs | 95 ++++++++++++++ nursery/coin_select/src/lib.rs | 9 +- nursery/coin_select/src/metrics.rs | 36 +----- nursery/coin_select/src/metrics/changeless.rs | 32 +++++ nursery/coin_select/src/metrics/waste.rs | 29 ++++- nursery/coin_select/src/ord_float.rs | 62 --------- nursery/coin_select/tests/changeless.rs | 120 ++++++++++++++++++ nursery/coin_select/tests/waste.rs | 3 +- 12 files changed, 369 insertions(+), 105 deletions(-) create mode 100644 nursery/coin_select/src/float.rs create mode 100644 nursery/coin_select/src/metrics/changeless.rs delete mode 100644 nursery/coin_select/src/ord_float.rs create mode 100644 nursery/coin_select/tests/changeless.rs diff --git a/nursery/coin_select/Cargo.toml b/nursery/coin_select/Cargo.toml index 5221d31f2..691fd619c 100644 --- a/nursery/coin_select/Cargo.toml +++ b/nursery/coin_select/Cargo.toml @@ -2,6 +2,7 @@ name = "bdk_coin_select" version = "0.1.0" edition = "2018" +license = "MIT OR Apache-2.0" [dependencies] # No dependencies! Don't add any please! @@ -9,3 +10,7 @@ edition = "2018" [dev-dependencies] rand = "0.8" proptest = "1" + +[features] +default = ["std"] +std = [] diff --git a/nursery/coin_select/src/change_policy.rs b/nursery/coin_select/src/change_policy.rs index 65c2baeab..8b78ba702 100644 --- a/nursery/coin_select/src/change_policy.rs +++ b/nursery/coin_select/src/change_policy.rs @@ -1,3 +1,5 @@ +#[allow(unused)] // some bug in <= 1.48.0 sees this as unused when it isn't +use crate::float::FloatExt; use crate::{CoinSelector, Drain, FeeRate, Target}; use core::convert::TryInto; diff --git a/nursery/coin_select/src/coin_selector.rs b/nursery/coin_select/src/coin_selector.rs index 6db86a6a7..9deb4ecce 100644 --- a/nursery/coin_select/src/coin_selector.rs +++ b/nursery/coin_select/src/coin_selector.rs @@ -1,5 +1,7 @@ use super::*; -use crate::{bnb::BnBMetric, ord_float::Ordf32, FeeRate}; +#[allow(unused)] // some bug in <= 1.48.0 sees this as unused when it isn't +use crate::float::FloatExt; +use crate::{bnb::BnBMetric, float::Ordf32, FeeRate}; use alloc::{borrow::Cow, collections::BTreeSet, vec::Vec}; /// A [`WeightedValue`] represents an input candidate for [`CoinSelector`]. This can either be a @@ -38,37 +40,58 @@ impl WeightedValue { Ordf32(self.value as f32 - (self.weight as f32 * feerate.spwu())) } + /// Value per weight unit pub fn value_pwu(&self) -> Ordf32 { Ordf32(self.value as f32 / self.weight as f32) } } +/// A drain (A.K.A. change) output. +/// Technically it could represent multiple outputs. +/// +/// These are usually created by a [`change_policy`]. +/// +/// [`change_policy`]: crate::change_policy #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] pub struct Drain { + /// The weight of adding this drain pub weight: u32, + /// The value that should be assigned to the drain pub value: u64, + /// The weight of spending this drain pub spend_weight: u32, } impl Drain { + /// A drian representing no drain at all. pub fn none() -> Self { Self::default() } + /// is the "none" drain pub fn is_none(&self) -> bool { self == &Drain::none() } + /// Is not the "none" drain pub fn is_some(&self) -> bool { !self.is_none() } + /// The waste of adding this drain to a transaction according to the [waste metric]. + /// + /// [waste metric]; https://bitcoin.stackexchange.com/questions/113622/what-does-waste-metric-mean-in-the-context-of-coin-selection pub fn waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 { self.weight as f32 * feerate.spwu() + self.spend_weight as f32 * long_term_feerate.spwu() } } /// [`CoinSelector`] is responsible for selecting and deselecting from a set of canididates. +/// +/// You can do this manually by calling methods like [`select`] or automatically with methods like [`branch_and_bound`]. +/// +/// [`select`]: CoinSelector::select +/// [`branch_and_bound`]: CoinSelector::branch_and_bound #[derive(Debug, Clone)] pub struct CoinSelector<'a> { base_weight: u32, @@ -78,10 +101,14 @@ pub struct CoinSelector<'a> { candidate_order: Cow<'a, Vec>, } +/// A target value to select for along with feerate constraints. #[derive(Debug, Clone, Copy)] pub struct Target { + /// The minimum feerate that the selection must have pub feerate: FeeRate, + /// The minimum fee the selection must have pub min_fee: u64, + /// The minmum value that should be left for the output pub value: u64, } @@ -96,7 +123,15 @@ impl Default for Target { } 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. + /// + /// Note that methods in `CoinSelector` will refer to inputs by the index in the `candidates` + /// slice you pass in. // TODO: constructor should be number of outputs and output weight instead so we can keep track + // of varint number of outputs pub fn new(candidates: &'a [WeightedValue], base_weight: u32) -> Self { Self { base_weight, @@ -107,6 +142,8 @@ impl<'a> CoinSelector<'a> { } } + /// Iterate over all the candidates in their currently sorted order. Each item has the original + /// index with the candidate. pub fn candidates( &self, ) -> impl DoubleEndedIterator + ExactSizeIterator + '_ { @@ -115,23 +152,33 @@ impl<'a> CoinSelector<'a> { .map(move |i| (*i, self.candidates[*i])) } + /// Get the candidate at `index`. `index` refers to its position in the original `candidates` slice passed + /// into [`CoinSelector::new`]. pub fn candidate(&self, index: usize) -> WeightedValue { self.candidates[index] } + /// Deselect a candidate at `index`. `index` refers to its position in the original `candidates` slice passed + /// into [`CoinSelector::new`]. pub fn deselect(&mut self, index: usize) -> bool { 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]) } + /// Select the input at `index`. `index` refers to its position in the original `candidates` slice passed + /// into [`CoinSelector::new`]. pub fn select(&mut self, index: usize) -> bool { assert!(index < self.candidates.len()); self.selected.to_mut().insert(index) } + /// Select the next unselected candidate in the sorted order fo the candidates. pub fn select_next(&mut self) -> bool { let next = self.unselected_indexes().next(); if let Some(next) = next { @@ -142,24 +189,51 @@ impl<'a> CoinSelector<'a> { } } + /// Ban an input from being selected. Banning the input means it won't show up in [`unselected`] + /// or [`unselected_indexes`]. Note it can still be manually selected. + /// + /// `index` refers to its position in the original `candidates` slice passed into [`CoinSelector::new`]. + /// + /// [`unselected`]: Self::unselected + /// [`unselected_indexes`]: Self::unselected_indexes pub fn ban(&mut self, index: usize) { self.banned.to_mut().insert(index); } + /// Gets the list of inputs that have been banned by [`ban`]. + /// + /// [`ban`]: Self::ban pub fn banned(&self) -> &BTreeSet { &self.banned } + /// Is the input at `index` selected. `index` refers to its position in the original + /// `candidates` slice passed into [`CoinSelector::new`]. pub fn is_selected(&self, index: usize) -> bool { 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. + /// + /// This simply selects all effective inputs at the target's feerate and checks whether we have + /// enough value. + /// + /// [`ban`]: Self::ban pub fn is_selection_possible(&self, target: Target, drain: Drain) -> bool { let mut test = self.clone(); test.select_all_effective(target.feerate); test.is_target_met(target, drain) } + /// Is meeting the target *plausible* with this `change_policy`. + /// Note this will respect [`ban`]ned candidates. + /// + /// This is very similar to [`is_selection_possible`] except that you pass in a change policy. + /// This method will give the right answer as long as `change_policy` is monotone but otherwise + /// can it can give false negatives. + /// + /// [`is_selection_possible`]: Self::is_selection_possible pub fn is_selection_plausible_with_change_policy( &self, target: Target, @@ -170,6 +244,7 @@ impl<'a> CoinSelector<'a> { test.is_target_met(target, change_policy(&test, target)) } + /// Returns true if no candidates have been selected. pub fn is_empty(&self) -> bool { self.selected.is_empty() } @@ -411,7 +486,7 @@ impl<'a> DoubleEndedIterator for SelectIter<'a> { } impl<'a> core::fmt::Display for CoinSelector<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "[")?; let mut candidates = self.candidates().peekable(); diff --git a/nursery/coin_select/src/feerate.rs b/nursery/coin_select/src/feerate.rs index 5f57f818c..8919e2e4e 100644 --- a/nursery/coin_select/src/feerate.rs +++ b/nursery/coin_select/src/feerate.rs @@ -1,4 +1,4 @@ -use crate::ord_float::Ordf32; +use crate::float::Ordf32; use core::ops::{Add, Sub}; /// Fee rate diff --git a/nursery/coin_select/src/float.rs b/nursery/coin_select/src/float.rs new file mode 100644 index 000000000..da9d07bc5 --- /dev/null +++ b/nursery/coin_select/src/float.rs @@ -0,0 +1,95 @@ +//! Newtypes around `f32` and `f64` that implement `Ord`. +//! +//! Backported from rust std lib [`total_cmp`] in version 1.62.0. Hopefully some day rust has this +//! in core: https://github.com/rust-lang/rfcs/issues/1249 +//! +//! [`total_cmp`]: https://doc.rust-lang.org/core/primitive.f32.html#method.total_cmp + +/// Wrapper for `f32` that implements `Ord`. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Ordf32(pub f32); +/// Wrapper for `f64` that implements `Ord`. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Ordf64(pub f64); + +impl Ord for Ordf32 { + #[inline] + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + let mut left = self.0.to_bits() as i32; + let mut right = other.0.to_bits() as i32; + left ^= (((left >> 31) as u32) >> 1) as i32; + right ^= (((right >> 31) as u32) >> 1) as i32; + left.cmp(&right) + } +} + +impl Ord for Ordf64 { + #[inline] + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + let mut left = self.0.to_bits() as i64; + let mut right = other.0.to_bits() as i64; + left ^= (((left >> 63) as u64) >> 1) as i64; + right ^= (((right >> 63) as u64) >> 1) as i64; + left.cmp(&right) + } +} + +impl Eq for Ordf64 {} +impl Eq for Ordf32 {} + +impl PartialOrd for Ordf32 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialOrd for Ordf64 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl core::fmt::Display for Ordf32 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} + +impl core::fmt::Display for Ordf64 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} + +/// Extension trait for adding basic float ops to f32 that don't exist in core for reasons. +pub trait FloatExt { + fn ceil(self) -> Self; +} + +impl FloatExt for f32 { + fn ceil(self) -> Self { + // From https://doc.rust-lang.org/reference/expressions/operator-expr.html#type-cast-expressions + // > Casting from a float to an integer will round the float towards zero + // > Casting from an integer to float will produce the closest possible float + let floored_towards_zero = (self as i32) as f32; + if self < 0.0 || floored_towards_zero == self { + floored_towards_zero + } else { + floored_towards_zero + 1.0 + } + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn ceil32() { + assert_eq!((-1.1).ceil(), -1.0); + assert_eq!((-0.1).ceil(), 0.0); + assert_eq!((0.0).ceil(), 0.0); + assert_eq!((1.0).ceil(), 1.0); + assert_eq!((1.1).ceil(), 2.0); + assert_eq!((2.9).ceil(), 3.0); + } +} diff --git a/nursery/coin_select/src/lib.rs b/nursery/coin_select/src/lib.rs index 992cfd1b5..e985b51be 100644 --- a/nursery/coin_select/src/lib.rs +++ b/nursery/coin_select/src/lib.rs @@ -1,10 +1,15 @@ -#[no_std] +#![no_std] #[allow(unused)] #[macro_use] extern crate alloc; +#[cfg(feature = "std")] +#[allow(unused)] +#[macro_use] +extern crate std; + mod coin_selector; -pub mod ord_float; +pub mod float; pub use coin_selector::*; pub mod bnb; diff --git a/nursery/coin_select/src/metrics.rs b/nursery/coin_select/src/metrics.rs index 1ddb0f982..0d13375b5 100644 --- a/nursery/coin_select/src/metrics.rs +++ b/nursery/coin_select/src/metrics.rs @@ -1,41 +1,13 @@ -use crate::{bnb::BnBMetric, ord_float::Ordf32, CoinSelector, Drain, Target}; +use crate::{bnb::BnBMetric, float::Ordf32, CoinSelector, Drain, Target}; mod waste; pub use waste::*; - -pub struct Changeless<'c, C> { - target: Target, - change_policy: &'c C, -} - -impl<'c, C> BnBMetric for Changeless<'c, C> -where - for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain, -{ - type Score = bool; - - fn score<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { - let drain = (self.change_policy)(cs, self.target); - if cs.is_target_met(self.target, drain) { - let has_drain = !drain.is_none(); - Some(has_drain) - } else { - None - } - } - - fn bound<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { - Some(change_lower_bound(cs, self.target, &self.change_policy).is_some()) - } - - fn requires_ordering_by_descending_value_pwu(&self) -> bool { - true - } -} +mod changeless; +pub use changeless::*; // Returns a drain if the current selection and every possible future selection would have a change // output (otherwise Drain::none()) by using the heurisitic that if it has change with the current // selection and it has one when we select every negative effective value candidate then it will -// always have a drain. We are essentially assuming that the change_policy is monotone with respect +// always have change. We are essentially assuming that the change_policy is monotone with respect // to the excess of the selection. // // NOTE: this should stay private because it requires cs to be sorted such that all negative diff --git a/nursery/coin_select/src/metrics/changeless.rs b/nursery/coin_select/src/metrics/changeless.rs new file mode 100644 index 000000000..afe0c70ca --- /dev/null +++ b/nursery/coin_select/src/metrics/changeless.rs @@ -0,0 +1,32 @@ +use super::change_lower_bound; +use crate::{bnb::BnBMetric, float::Ordf32, CoinSelector, Drain, Target}; + +pub struct Changeless<'c, C> { + pub target: Target, + pub change_policy: &'c C, +} + +impl<'c, C> BnBMetric for Changeless<'c, C> +where + for<'a, 'b> C: Fn(&'b CoinSelector<'a>, Target) -> Drain, +{ + type Score = bool; + + fn score<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { + let drain = (self.change_policy)(cs, self.target); + if cs.is_target_met(self.target, drain) { + let has_drain = !drain.is_none(); + Some(has_drain) + } else { + None + } + } + + fn bound<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { + Some(change_lower_bound(cs, self.target, &self.change_policy).is_some()) + } + + fn requires_ordering_by_descending_value_pwu(&self) -> bool { + true + } +} diff --git a/nursery/coin_select/src/metrics/waste.rs b/nursery/coin_select/src/metrics/waste.rs index 5b29a744a..0c85998dc 100644 --- a/nursery/coin_select/src/metrics/waste.rs +++ b/nursery/coin_select/src/metrics/waste.rs @@ -1,8 +1,21 @@ use super::change_lower_bound; -use crate::{ - bnb::BnBMetric, ord_float::Ordf32, CoinSelector, Drain, FeeRate, Target, WeightedValue, -}; - +use crate::{bnb::BnBMetric, float::Ordf32, CoinSelector, Drain, FeeRate, Target, WeightedValue}; + +/// The "waste" metric used by bitcoin core. +/// +/// See this [great +/// explanation](https://bitcoin.stackexchange.com/questions/113622/what-does-waste-metric-mean-in-the-context-of-coin-selection) for an understanding of the waste metric. +/// +/// ## WARNING: Waste metric considered wasteful +/// +/// Note that bitcoin core at the time of writing use the waste metric to +/// +/// 1. minimise the waste while searching for changeless solutions. +/// 2. It tiebreaks multiple valid selections from different algorithms (which do not try and minimise waste) with waste. +/// +/// This is **very** different from minimising waste in general which is what this metric will do when used in [`CoinSelector::branch_and_bound`]. +/// The waste metric tends to over consolidate funds. If the `long_term_feerate` is even slightly +/// higher than the current feerate (specified in `target`) it will select all your coins! pub struct Waste<'c, C> { pub target: Target, pub long_term_feerate: FeeRate, @@ -25,6 +38,14 @@ where } fn bound<'a>(&mut self, cs: &CoinSelector<'a>) -> Option { + // Welcome my bretheren. This dungeon was authored by Lloyd Fournier A.K.A "LLFourn" with + // the assistance of chat GPT and the developers of the IOTA cryptocurrency. There are + // comments trying to make sense of the logic here but it's really just me pretending I know + // what's going on. I have tried to simplify the logic here many times but always end up + // making it fail proptests. + // + // Don't be afraid. This function is a "heuristic" lower bound. It doesn't need to be super + // duper correct. In testing it seems to come up with pretty good results pretty fast. let rate_diff = self.target.feerate.spwu() - self.long_term_feerate.spwu(); // whether from this coin selection it's possible to avoid change let change_lower_bound = change_lower_bound(&cs, self.target, &self.change_policy); diff --git a/nursery/coin_select/src/ord_float.rs b/nursery/coin_select/src/ord_float.rs deleted file mode 100644 index 2f29e4fdf..000000000 --- a/nursery/coin_select/src/ord_float.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! Newtypes around `f32` and `f64` that implement `Ord`. -//! -//! Backported from rust std lib [`total_cmp`] in version 1.62.0. Hopefully some day rust has this -//! in core: https://github.com/rust-lang/rfcs/issues/1249 -//! -//! [`total_cmp`]: https://doc.rust-lang.org/core/primitive.f32.html#method.total_cmp - -/// Wrapper for `f32` that implements `Ord`. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Ordf32(pub f32); -/// Wrapper for `f64` that implements `Ord`. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Ordf64(pub f64); - -impl Ord for Ordf32 { - #[inline] - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - let mut left = self.0.to_bits() as i32; - let mut right = other.0.to_bits() as i32; - left ^= (((left >> 31) as u32) >> 1) as i32; - right ^= (((right >> 31) as u32) >> 1) as i32; - left.cmp(&right) - } -} - -impl Ord for Ordf64 { - #[inline] - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - let mut left = self.0.to_bits() as i64; - let mut right = other.0.to_bits() as i64; - left ^= (((left >> 63) as u64) >> 1) as i64; - right ^= (((right >> 63) as u64) >> 1) as i64; - left.cmp(&right) - } -} - -impl Eq for Ordf64 {} -impl Eq for Ordf32 {} - -impl PartialOrd for Ordf32 { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialOrd for Ordf64 { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl core::fmt::Display for Ordf32 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl core::fmt::Display for Ordf64 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} diff --git a/nursery/coin_select/tests/changeless.rs b/nursery/coin_select/tests/changeless.rs new file mode 100644 index 000000000..4b2c9b171 --- /dev/null +++ b/nursery/coin_select/tests/changeless.rs @@ -0,0 +1,120 @@ +use bdk_coin_select::{ + change_policy, float::Ordf32, metrics, CoinSelector, Drain, FeeRate, Target, WeightedValue, +}; +use proptest::{ + prelude::*, + test_runner::{RngAlgorithm, TestRng}, +}; +use rand::prelude::IteratorRandom; + +fn test_wv(mut rng: impl RngCore) -> impl Iterator { + core::iter::repeat_with(move || { + let value = rng.gen_range(0..1_000); + WeightedValue { + value, + weight: rng.gen_range(0..100), + input_count: rng.gen_range(1..2), + is_segwit: rng.gen_bool(0.5), + } + }) +} + +proptest! { + #![proptest_config(ProptestConfig { + timeout: 1_000, + cases: 1_000, + ..Default::default() + })] + #[test] + #[cfg(not(debug_assertions))] // too slow if compiling for debug + fn changeless_prop( + num_inputs in 0usize..15, + target in 0u64..15_000, + feerate in 1.0f32..10.0, + min_fee in 0u64..1_000, + base_weight in 0u32..500, + long_term_feerate_diff in -5.0f32..5.0, + change_weight in 1u32..100, + change_spend_weight in 1u32..100, + ) { + println!("======================================="); + let start = std::time::Instant::now(); + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + let long_term_feerate = FeeRate::from_sat_per_vb(0.0f32.max(feerate - long_term_feerate_diff)); + let feerate = FeeRate::from_sat_per_vb(feerate); + let drain = Drain { + weight: change_weight, + spend_weight: change_spend_weight, + value: 0 + }; + + let change_policy = crate::change_policy::min_waste(drain, long_term_feerate); + let wv = test_wv(&mut rng); + let candidates = wv.take(num_inputs).collect::>(); + + let cs = CoinSelector::new(&candidates, base_weight); + + let target = Target { + value: target, + feerate, + min_fee + }; + + let solutions = cs.branch_and_bound(metrics::Changeless { + target, + change_policy: &change_policy + }); + + + let best = solutions + .enumerate() + .filter_map(|(i, sol)| Some((i, sol?))) + .last(); + + + match best { + Some((_i, (sol, _score))) => { + let mut cmp_benchmarks = vec![ + { + let mut naive_select = cs.clone(); + naive_select.sort_candidates_by_key(|(_, wv)| core::cmp::Reverse(wv.effective_value(target.feerate))); + // we filter out failing onces below + let _ = naive_select.select_until_target_met(target, drain); + naive_select + }, + ]; + + cmp_benchmarks.extend((0..10).map(|_|random_minimal_selection(&cs, target, long_term_feerate, &change_policy, &mut rng))); + + let cmp_benchmarks = cmp_benchmarks.into_iter().filter(|cs| cs.is_target_met(target, change_policy(&cs, target))); + for (_bench_id, bench) in cmp_benchmarks.enumerate() { + prop_assert!(change_policy(&bench, target).is_some() >= change_policy(&sol, target).is_some()); + } + } + None => { + prop_assert!(!cs.is_selection_plausible_with_change_policy(target, &change_policy)); + } + } + dbg!(start.elapsed()); + } +} + +// this is probably a useful thing to have on CoinSelector but I don't want to design it yet +#[allow(unused)] +fn random_minimal_selection<'a>( + cs: &CoinSelector<'a>, + target: Target, + long_term_feerate: FeeRate, + change_policy: &impl Fn(&CoinSelector, Target) -> Drain, + rng: &mut impl RngCore, +) -> CoinSelector<'a> { + let mut cs = cs.clone(); + let mut last_waste: Option = None; + while let Some(next) = cs.unselected_indexes().choose(rng) { + cs.select(next); + if cs.is_target_met(target, change_policy(&cs, target)) { + break; + } + } + cs +} diff --git a/nursery/coin_select/tests/waste.rs b/nursery/coin_select/tests/waste.rs index 441a684e4..3fb01c9fa 100644 --- a/nursery/coin_select/tests/waste.rs +++ b/nursery/coin_select/tests/waste.rs @@ -1,6 +1,5 @@ -#[cfg(test)] use bdk_coin_select::{ - change_policy, metrics::Waste, ord_float::Ordf32, CoinSelector, Drain, FeeRate, Target, + change_policy, float::Ordf32, metrics::Waste, CoinSelector, Drain, FeeRate, Target, WeightedValue, }; use proptest::{ From f48f558b8f84ac70b4cee5f1a4376a9961003ce5 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Tue, 23 May 2023 12:04:02 +0800 Subject: [PATCH 4/8] tmp --- .../keychain_tracker_example_cli/src/lib.rs | 4 +- nursery/coin_select/Cargo.toml | 1 + nursery/coin_select/README.md | 58 ++++++ nursery/coin_select/src/coin_selector.rs | 182 +++++++++--------- nursery/coin_select/src/lib.rs | 12 +- nursery/coin_select/src/metrics/changeless.rs | 2 +- nursery/coin_select/src/metrics/waste.rs | 4 +- nursery/coin_select/tests/bnb.rs | 10 +- nursery/coin_select/tests/changeless.rs | 6 +- nursery/coin_select/tests/waste.rs | 6 +- 10 files changed, 175 insertions(+), 110 deletions(-) create mode 100644 nursery/coin_select/README.md diff --git a/example-crates/keychain_tracker_example_cli/src/lib.rs b/example-crates/keychain_tracker_example_cli/src/lib.rs index df42df1ac..3a5dcc02f 100644 --- a/example-crates/keychain_tracker_example_cli/src/lib.rs +++ b/example-crates/keychain_tracker_example_cli/src/lib.rs @@ -15,7 +15,7 @@ use bdk_chain::{ sparse_chain::{self, ChainPosition}, DescriptorExt, FullTxOut, }; -use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue}; +use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, Candidate}; use bdk_file_store::KeychainStore; use clap::{Parser, Subcommand}; use std::{ @@ -352,7 +352,7 @@ pub fn create_tx( let wv_candidates = candidates .iter() .map(|(plan, utxo)| { - WeightedValue::new( + Candidate::new( utxo.txout.value, plan.expected_weight() as _, plan.witness_version().is_some(), diff --git a/nursery/coin_select/Cargo.toml b/nursery/coin_select/Cargo.toml index 691fd619c..c31727dd1 100644 --- a/nursery/coin_select/Cargo.toml +++ b/nursery/coin_select/Cargo.toml @@ -10,6 +10,7 @@ license = "MIT OR Apache-2.0" [dev-dependencies] rand = "0.8" proptest = "1" +bitcoin = "0.30" [features] default = ["std"] diff --git a/nursery/coin_select/README.md b/nursery/coin_select/README.md new file mode 100644 index 000000000..dab5a030e --- /dev/null +++ b/nursery/coin_select/README.md @@ -0,0 +1,58 @@ +# BDK Coin Selection + +`bdk_coin_select` is a tool to help you select inputs for making Bitcoin (ticker: BTC) transactions. It's got zero dependencies so you can pasta it into your project without concern. + + +## Synopsis + +```rust +use bdk_coin_select::{CoinSelector, Candidate, TXIN_BASE_WEIGHT}; +use bitcoin::{ Transaction, TxIn }; + +// You should use miniscript to figure out the satisfaction weight for your coins! +const TR_SATISFACTION_WEIGHT: u32 = 66; +const TR_INPUT_WEIGHT: u32 = TXIN_BASE_WEIGHT + TR_SATISFACTION_WEIGHT; + + +let candidates = vec![ + Candidate { + // How many inputs does this candidate represent. Needed so we can figure out the weight + // of the varint that encodes the number of inputs. + input_count: 1, + // the value of the input + value: 1_000_000, + // the total weight of the input(s). This doesn't include + weight: TR_INPUT_WEIGHT, + // wether it's a segwit input. Needed so we know whether to include the segwit header + // in total weight calculations. + is_segwit: true + }, + Candidate { + // A candidate can represent multiple inputs in the case where you always want some inputs + // to be spent together. + input_count: 2, + weight: 2*tr_input_weight, + value: 3_000_000, + is_segwit: true + }, + Candidate { + input_count: 1, + weight: TR_INPUT_WEIGHT, + value: 5_000_000, + is_segwit: true, + } +]; + +let base_weight = Transaction { + input: vec![], + output: vec![], + lock_time: bitcoin::absolute::LockTime::from_height(0).unwrap(), + version: 1, +}.weight().to_wu() as u32; + +panic!("{}", base_weight); + +let coin_selector = CoinSelector::new(&candidates,base_weight); + +``` + diff --git a/nursery/coin_select/src/coin_selector.rs b/nursery/coin_select/src/coin_selector.rs index 9deb4ecce..8b86f6dfe 100644 --- a/nursery/coin_select/src/coin_selector.rs +++ b/nursery/coin_select/src/coin_selector.rs @@ -4,88 +4,6 @@ use crate::float::FloatExt; use crate::{bnb::BnBMetric, float::Ordf32, FeeRate}; use alloc::{borrow::Cow, collections::BTreeSet, vec::Vec}; -/// A [`WeightedValue`] represents an input candidate for [`CoinSelector`]. This can either be a -/// single UTXO, or a group of UTXOs that should be spent together. -#[derive(Debug, Clone, Copy)] -pub struct WeightedValue { - /// Total value of the UTXO(s) that this [`WeightedValue`] represents. - pub value: u64, - /// Total weight of including this/these UTXO(s). - /// `txin` fields: `prevout`, `nSequence`, `scriptSigLen`, `scriptSig`, `scriptWitnessLen`, - /// `scriptWitness` should all be included. - pub weight: u32, - /// Total number of inputs; so we can calculate extra `varint` weight due to `vin` len changes. - pub input_count: usize, - /// Whether this [`WeightedValue`] contains at least one segwit spend. - pub is_segwit: bool, -} - -impl WeightedValue { - /// Create a new [`WeightedValue`] that represents a single input. - /// - /// `satisfaction_weight` is the weight of `scriptSigLen + scriptSig + scriptWitnessLen + - /// scriptWitness`. - pub fn new(value: u64, satisfaction_weight: u32, is_segwit: bool) -> WeightedValue { - let weight = TXIN_BASE_WEIGHT + satisfaction_weight; - WeightedValue { - value, - weight, - input_count: 1, - is_segwit, - } - } - - /// Effective value of this input candidate: `actual_value - input_weight * feerate (sats/wu)`. - pub fn effective_value(&self, feerate: FeeRate) -> Ordf32 { - Ordf32(self.value as f32 - (self.weight as f32 * feerate.spwu())) - } - - /// Value per weight unit - pub fn value_pwu(&self) -> Ordf32 { - Ordf32(self.value as f32 / self.weight as f32) - } -} - -/// A drain (A.K.A. change) output. -/// Technically it could represent multiple outputs. -/// -/// These are usually created by a [`change_policy`]. -/// -/// [`change_policy`]: crate::change_policy -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] -pub struct Drain { - /// The weight of adding this drain - pub weight: u32, - /// The value that should be assigned to the drain - pub value: u64, - /// The weight of spending this drain - pub spend_weight: u32, -} - -impl Drain { - /// A drian representing no drain at all. - pub fn none() -> Self { - Self::default() - } - - /// is the "none" drain - pub fn is_none(&self) -> bool { - self == &Drain::none() - } - - /// Is not the "none" drain - pub fn is_some(&self) -> bool { - !self.is_none() - } - - /// The waste of adding this drain to a transaction according to the [waste metric]. - /// - /// [waste metric]; https://bitcoin.stackexchange.com/questions/113622/what-does-waste-metric-mean-in-the-context-of-coin-selection - pub fn waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 { - self.weight as f32 * feerate.spwu() + self.spend_weight as f32 * long_term_feerate.spwu() - } -} - /// [`CoinSelector`] is responsible for selecting and deselecting from a set of canididates. /// /// You can do this manually by calling methods like [`select`] or automatically with methods like [`branch_and_bound`]. @@ -95,7 +13,7 @@ impl Drain { #[derive(Debug, Clone)] pub struct CoinSelector<'a> { base_weight: u32, - candidates: &'a [WeightedValue], + candidates: &'a [Candidate], selected: Cow<'a, BTreeSet>, banned: Cow<'a, BTreeSet>, candidate_order: Cow<'a, Vec>, @@ -132,7 +50,7 @@ impl<'a> CoinSelector<'a> { /// slice you pass in. // TODO: constructor should be number of outputs and output weight instead so we can keep track // of varint number of outputs - pub fn new(candidates: &'a [WeightedValue], base_weight: u32) -> Self { + pub fn new(candidates: &'a [Candidate], base_weight: u32) -> Self { Self { base_weight, candidates, @@ -146,7 +64,7 @@ impl<'a> CoinSelector<'a> { /// index with the candidate. pub fn candidates( &self, - ) -> impl DoubleEndedIterator + ExactSizeIterator + '_ { + ) -> impl DoubleEndedIterator + ExactSizeIterator + '_ { self.candidate_order .iter() .map(move |i| (*i, self.candidates[*i])) @@ -154,7 +72,7 @@ impl<'a> CoinSelector<'a> { /// Get the candidate at `index`. `index` refers to its position in the original `candidates` slice passed /// into [`CoinSelector::new`]. - pub fn candidate(&self, index: usize) -> WeightedValue { + pub fn candidate(&self, index: usize) -> Candidate { self.candidates[index] } @@ -338,7 +256,7 @@ impl<'a> CoinSelector<'a> { pub fn sort_candidates_by(&mut self, mut cmp: F) where - F: FnMut((usize, WeightedValue), (usize, WeightedValue)) -> core::cmp::Ordering, + F: FnMut((usize, Candidate), (usize, Candidate)) -> core::cmp::Ordering, { let order = self.candidate_order.to_mut(); let candidates = &self.candidates; @@ -347,7 +265,7 @@ impl<'a> CoinSelector<'a> { pub fn sort_candidates_by_key(&mut self, mut key_fn: F) where - F: FnMut((usize, WeightedValue)) -> K, + F: FnMut((usize, Candidate)) -> K, K: Ord, { self.sort_candidates_by(|a, b| key_fn(a).cmp(&key_fn(b))) @@ -383,13 +301,13 @@ impl<'a> CoinSelector<'a> { waste } - pub fn selected(&self) -> impl ExactSizeIterator + '_ { + pub fn selected(&self) -> impl ExactSizeIterator + '_ { self.selected .iter() .map(move |&index| (index, self.candidates[index])) } - pub fn unselected(&self) -> impl DoubleEndedIterator + '_ { + pub fn unselected(&self) -> impl DoubleEndedIterator + '_ { self.unselected_indexes() .map(move |i| (i, self.candidates[i])) } @@ -468,7 +386,7 @@ pub struct SelectIter<'a> { } impl<'a> Iterator for SelectIter<'a> { - type Item = (CoinSelector<'a>, usize, WeightedValue); + type Item = (CoinSelector<'a>, usize, Candidate); fn next(&mut self) -> Option { let (index, wv) = self.cs.unselected().next()?; @@ -508,3 +426,85 @@ impl<'a> core::fmt::Display for CoinSelector<'a> { write!(f, "]") } } + +/// 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. +#[derive(Debug, Clone, Copy)] +pub struct Candidate { + /// Total value of the UTXO(s) that this [`Candidate`] represents. + pub value: u64, + /// Total weight of including this/these UTXO(s). + /// `txin` fields: `prevout`, `nSequence`, `scriptSigLen`, `scriptSig`, `scriptWitnessLen`, + /// `scriptWitness` should all be included. + pub weight: u32, + /// Total number of inputs; so we can calculate extra `varint` weight due to `vin` len changes. + pub input_count: usize, + /// Whether this [`Candidate`] contains at least one segwit spend. + pub is_segwit: bool, +} + +impl Candidate { + /// Create a new [`Candidate`] that represents a single input. + /// + /// `satisfaction_weight` is the weight of `scriptSigLen + scriptSig + scriptWitnessLen + + /// scriptWitness`. + pub fn new(value: u64, satisfaction_weight: u32, is_segwit: bool) -> Candidate { + let weight = TXIN_BASE_WEIGHT + satisfaction_weight; + Candidate { + value, + weight, + input_count: 1, + is_segwit, + } + } + + /// Effective value of this input candidate: `actual_value - input_weight * feerate (sats/wu)`. + pub fn effective_value(&self, feerate: FeeRate) -> Ordf32 { + Ordf32(self.value as f32 - (self.weight as f32 * feerate.spwu())) + } + + /// Value per weight unit + pub fn value_pwu(&self) -> Ordf32 { + Ordf32(self.value as f32 / self.weight as f32) + } +} + +/// A drain (A.K.A. change) output. +/// Technically it could represent multiple outputs. +/// +/// These are usually created by a [`change_policy`]. +/// +/// [`change_policy`]: crate::change_policy +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] +pub struct Drain { + /// The weight of adding this drain + pub weight: u32, + /// The value that should be assigned to the drain + pub value: u64, + /// The weight of spending this drain + pub spend_weight: u32, +} + +impl Drain { + /// A drian representing no drain at all. + pub fn none() -> Self { + Self::default() + } + + /// is the "none" drain + pub fn is_none(&self) -> bool { + self == &Drain::none() + } + + /// Is not the "none" drain + pub fn is_some(&self) -> bool { + !self.is_none() + } + + /// The waste of adding this drain to a transaction according to the [waste metric]. + /// + /// [waste metric]; https://bitcoin.stackexchange.com/questions/113622/what-does-waste-metric-mean-in-the-context-of-coin-selection + pub fn waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 { + self.weight as f32 * feerate.spwu() + self.spend_weight as f32 * long_term_feerate.spwu() + } +} diff --git a/nursery/coin_select/src/lib.rs b/nursery/coin_select/src/lib.rs index e985b51be..8d799c6e8 100644 --- a/nursery/coin_select/src/lib.rs +++ b/nursery/coin_select/src/lib.rs @@ -1,4 +1,9 @@ #![no_std] +#![warn(missing_docs)] +#![doc = include_str!("../README.md")] +#![deny(unsafe_code)] + + #[allow(unused)] #[macro_use] extern crate alloc; @@ -8,6 +13,7 @@ extern crate alloc; #[macro_use] extern crate std; + mod coin_selector; pub mod float; pub use coin_selector::*; @@ -19,9 +25,9 @@ mod feerate; pub use feerate::*; pub mod change_policy; -/// Txin "base" fields include `outpoint` (32+4) and `nSequence` (4). This does not include -/// `scriptSigLen`, `scriptSig` or witness stack length -pub const TXIN_BASE_WEIGHT: u32 = (32 + 4 + 4) * 4; +/// Txin "base" fields include `outpoint` (32+4) and `nSequence` (4) and 1 byte for the scriptSig +/// length. +pub const TXIN_BASE_WEIGHT: u32 = (32 + 4 + 4 + 1) * 4; /// The weight of a TXOUT without the `scriptPubkey` (and script pubkey length field). /// Just the weight of the value field. diff --git a/nursery/coin_select/src/metrics/changeless.rs b/nursery/coin_select/src/metrics/changeless.rs index afe0c70ca..7a539d6a9 100644 --- a/nursery/coin_select/src/metrics/changeless.rs +++ b/nursery/coin_select/src/metrics/changeless.rs @@ -1,5 +1,5 @@ use super::change_lower_bound; -use crate::{bnb::BnBMetric, float::Ordf32, CoinSelector, Drain, Target}; +use crate::{bnb::BnBMetric, CoinSelector, Drain, Target}; pub struct Changeless<'c, C> { pub target: Target, diff --git a/nursery/coin_select/src/metrics/waste.rs b/nursery/coin_select/src/metrics/waste.rs index 0c85998dc..01c3829c7 100644 --- a/nursery/coin_select/src/metrics/waste.rs +++ b/nursery/coin_select/src/metrics/waste.rs @@ -1,5 +1,5 @@ use super::change_lower_bound; -use crate::{bnb::BnBMetric, float::Ordf32, CoinSelector, Drain, FeeRate, Target, WeightedValue}; +use crate::{bnb::BnBMetric, float::Ordf32, CoinSelector, Drain, FeeRate, Target, Candidate}; /// The "waste" metric used by bitcoin core. /// @@ -226,7 +226,7 @@ where /// tells you how much weight such a perfect candidate would have if it had the same value per /// weight unit as `candidate`. This is useful for estimating a lower weight bound for a perfect /// match. -fn slurp_wv(candidate: WeightedValue, value_to_slurp: i64, feerate: FeeRate) -> f32 { +fn slurp_wv(candidate: Candidate, value_to_slurp: i64, feerate: FeeRate) -> f32 { // the value per weight unit this candidate offers at feerate let value_per_wu = (candidate.value as f32 / candidate.weight as f32) - feerate.spwu(); // return how much weight we need diff --git a/nursery/coin_select/tests/bnb.rs b/nursery/coin_select/tests/bnb.rs index ba8b66893..edcf28398 100644 --- a/nursery/coin_select/tests/bnb.rs +++ b/nursery/coin_select/tests/bnb.rs @@ -1,4 +1,4 @@ -use bdk_coin_select::{bnb::BnBMetric, CoinSelector, Drain, FeeRate, Target, WeightedValue}; +use bdk_coin_select::{bnb::BnBMetric, CoinSelector, Drain, FeeRate, Target, Candidate}; #[macro_use] extern crate alloc; @@ -9,10 +9,10 @@ use proptest::{ }; use rand::{Rng, RngCore}; -fn test_wv(mut rng: impl RngCore) -> impl Iterator { +fn test_wv(mut rng: impl RngCore) -> impl Iterator { core::iter::repeat_with(move || { let value = rng.gen_range(0..1_000); - WeightedValue { + Candidate { value, weight: 100, input_count: rng.gen_range(1..2), @@ -57,7 +57,7 @@ fn bnb_finds_an_exact_solution_in_n_iter() { let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); let mut wv = test_wv(&mut rng); - let solution: Vec = (0..solution_len).map(|_| wv.next().unwrap()).collect(); + let solution: Vec = (0..solution_len).map(|_| wv.next().unwrap()).collect(); let solution_weight = solution.iter().map(|sol| sol.weight).sum(); let target = solution.iter().map(|c| c.value).sum(); @@ -149,7 +149,7 @@ proptest! { let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); let mut wv = test_wv(&mut rng); - let solution: Vec = (0..solution_len).map(|_| wv.next().unwrap()).collect(); + let solution: Vec = (0..solution_len).map(|_| wv.next().unwrap()).collect(); let target = solution.iter().map(|c| c.value).sum(); let solution_weight = solution.iter().map(|sol| sol.weight).sum(); diff --git a/nursery/coin_select/tests/changeless.rs b/nursery/coin_select/tests/changeless.rs index 4b2c9b171..2dbebaef6 100644 --- a/nursery/coin_select/tests/changeless.rs +++ b/nursery/coin_select/tests/changeless.rs @@ -1,5 +1,5 @@ use bdk_coin_select::{ - change_policy, float::Ordf32, metrics, CoinSelector, Drain, FeeRate, Target, WeightedValue, + change_policy, float::Ordf32, metrics, CoinSelector, Drain, FeeRate, Target, Candidate, }; use proptest::{ prelude::*, @@ -7,10 +7,10 @@ use proptest::{ }; use rand::prelude::IteratorRandom; -fn test_wv(mut rng: impl RngCore) -> impl Iterator { +fn test_wv(mut rng: impl RngCore) -> impl Iterator { core::iter::repeat_with(move || { let value = rng.gen_range(0..1_000); - WeightedValue { + Candidate { value, weight: rng.gen_range(0..100), input_count: rng.gen_range(1..2), diff --git a/nursery/coin_select/tests/waste.rs b/nursery/coin_select/tests/waste.rs index 3fb01c9fa..5269bea12 100644 --- a/nursery/coin_select/tests/waste.rs +++ b/nursery/coin_select/tests/waste.rs @@ -1,6 +1,6 @@ use bdk_coin_select::{ change_policy, float::Ordf32, metrics::Waste, CoinSelector, Drain, FeeRate, Target, - WeightedValue, + Candidate, }; use proptest::{ prelude::*, @@ -398,10 +398,10 @@ proptest! { } } -fn test_wv(mut rng: impl RngCore) -> impl Iterator { +fn test_wv(mut rng: impl RngCore) -> impl Iterator { core::iter::repeat_with(move || { let value = rng.gen_range(0..1_000); - WeightedValue { + Candidate { value, weight: rng.gen_range(0..100), input_count: rng.gen_range(1..2), From 9129b43f1ee75185f3cc3a0f27edcd00df59a61d Mon Sep 17 00:00:00 2001 From: LLFourn Date: Thu, 25 May 2023 10:03:33 +0800 Subject: [PATCH 5/8] tmp --- nursery/coin_select/README.md | 7 +- nursery/coin_select/src/bnb.rs | 3 +- nursery/coin_select/src/coin_selector.rs | 157 ++++++++++++++++++----- nursery/coin_select/src/float.rs | 1 + nursery/coin_select/src/lib.rs | 12 +- nursery/coin_select/src/metrics.rs | 1 + nursery/coin_select/src/metrics/waste.rs | 4 +- nursery/coin_select/tests/bnb.rs | 4 +- nursery/coin_select/tests/changeless.rs | 2 +- nursery/coin_select/tests/waste.rs | 2 +- 10 files changed, 146 insertions(+), 47 deletions(-) diff --git a/nursery/coin_select/README.md b/nursery/coin_select/README.md index dab5a030e..e688eff3e 100644 --- a/nursery/coin_select/README.md +++ b/nursery/coin_select/README.md @@ -10,8 +10,8 @@ use bdk_coin_select::{CoinSelector, Candidate, TXIN_BASE_WEIGHT}; use bitcoin::{ Transaction, TxIn }; // You should use miniscript to figure out the satisfaction weight for your coins! -const TR_SATISFACTION_WEIGHT: u32 = 66; -const TR_INPUT_WEIGHT: u32 = TXIN_BASE_WEIGHT + TR_SATISFACTION_WEIGHT; +const tr_satisfaction_weight: u32 = 66; +const tr_input_weight: u32 = txin_base_weight + tr_satisfaction_weight; let candidates = vec![ @@ -52,7 +52,8 @@ let base_weight = Transaction { panic!("{}", base_weight); -let coin_selector = CoinSelector::new(&candidates,base_weight); +let mut coin_selector = CoinSelector::new(&candidates,base_weight); + ``` diff --git a/nursery/coin_select/src/bnb.rs b/nursery/coin_select/src/bnb.rs index b9a384898..859f7040f 100644 --- a/nursery/coin_select/src/bnb.rs +++ b/nursery/coin_select/src/bnb.rs @@ -87,7 +87,7 @@ impl<'a, M: BnBMetric> BnbIter<'a, M> { return; } - let next_unselected = cs.unselected_indexes().next().unwrap(); + let next_unselected = cs.unselected_indices().next().unwrap(); let mut inclusion_cs = cs.clone(); inclusion_cs.select(next_unselected); let mut exclusion_cs = cs.clone(); @@ -130,6 +130,7 @@ impl<'a, O: PartialEq> PartialEq for Branch<'a, O> { impl<'a, O: PartialEq> Eq for Branch<'a, O> {} +/// A branch and bound metric pub trait BnBMetric { type Score: Ord + Clone + core::fmt::Debug; diff --git a/nursery/coin_select/src/coin_selector.rs b/nursery/coin_select/src/coin_selector.rs index 8b86f6dfe..94ee630df 100644 --- a/nursery/coin_select/src/coin_selector.rs +++ b/nursery/coin_select/src/coin_selector.rs @@ -98,7 +98,7 @@ impl<'a> CoinSelector<'a> { /// Select the next unselected candidate in the sorted order fo the candidates. pub fn select_next(&mut self) -> bool { - let next = self.unselected_indexes().next(); + let next = self.unselected_indices().next(); if let Some(next) = next { self.select(next); true @@ -108,12 +108,12 @@ impl<'a> CoinSelector<'a> { } /// Ban an input from being selected. Banning the input means it won't show up in [`unselected`] - /// or [`unselected_indexes`]. Note it can still be manually selected. + /// or [`unselected_indices`]. Note it can still be manually selected. /// /// `index` refers to its position in the original `candidates` slice passed into [`CoinSelector::new`]. /// /// [`unselected`]: Self::unselected - /// [`unselected_indexes`]: Self::unselected_indexes + /// [`unselected_indices`]: Self::unselected_indices pub fn ban(&mut self, index: usize) { self.banned.to_mut().insert(index); } @@ -174,7 +174,9 @@ impl<'a> CoinSelector<'a> { .sum() } - pub fn input_weight(&self) -> u32 { + /// The weight of the inputs including the witness header and the varint for the number of + /// inputs. + fn input_weight(&self) -> u32 { let witness_header_extra_weight = self .selected() .find(|(_, wv)| wv.is_segwit) @@ -214,6 +216,8 @@ impl<'a> CoinSelector<'a> { - self.implied_fee(target.feerate, target.min_fee, drain.weight) as i64 } + /// How much the current selection overshoots the value need to satisfy `target.feerate` and + /// `target.value` (while ignoring `target.min_fee`). pub fn rate_excess(&self, target: Target, drain: Drain) -> i64 { self.selected_value() as i64 - target.value as i64 @@ -221,6 +225,8 @@ impl<'a> CoinSelector<'a> { - self.implied_fee_from_feerate(target.feerate, drain.weight) as i64 } + /// How much the current selection overshoots the vlaue needed to satisfy `target.min_fee` and + /// `target.value` (while ignoring `target.feerate`). pub fn absolute_excess(&self, target: Target, drain: Drain) -> i64 { self.selected_value() as i64 - target.value as i64 @@ -236,11 +242,12 @@ impl<'a> CoinSelector<'a> { FeeRate::from_sat_per_wu(numerator as f32 / denom as f32) } - pub fn implied_fee(&self, feerate: FeeRate, min_fee: u64, drain_weight: u32) -> u64 { + /// The fee the current selection should pay to reach `feerate` and provide `min_fee` + fn implied_fee(&self, feerate: FeeRate, min_fee: u64, drain_weight: u32) -> u64 { (self.implied_fee_from_feerate(feerate, drain_weight)).max(min_fee) } - pub fn implied_fee_from_feerate(&self, feerate: FeeRate, drain_weight: u32) -> u64 { + fn implied_fee_from_feerate(&self, feerate: FeeRate, drain_weight: u32) -> u64 { (self.weight(drain_weight) as f32 * feerate.spwu()).ceil() as u64 } @@ -254,6 +261,15 @@ impl<'a> CoinSelector<'a> { self.selected_weight() as f32 * (feerate.spwu() - long_term_feerate.spwu()) } + /// Sorts the candidates by the comparision function. + /// + /// The comparision function takes the candidates's index and the [`Candidate`]. + /// + /// Note this function does not change the index of the candidates after sorting, just the order + /// in which they will be returned when interating over them in [`candidates`] and [`unselected`]. + /// + /// [`candidates`]: CoinSelector::candidates + /// [`unselected`]: CoinSelector::unselected pub fn sort_candidates_by(&mut self, mut cmp: F) where F: FnMut((usize, Candidate), (usize, Candidate)) -> core::cmp::Ordering, @@ -263,6 +279,17 @@ impl<'a> CoinSelector<'a> { order.sort_by(|a, b| cmp((*a, candidates[*a]), (*b, candidates[*b]))) } + + + /// Sorts the candidates by the key function. + /// + /// The key function takes the candidates's index and the [`Candidate`]. + /// + /// Note this function does not change the index of the candidates after sorting, just the order + /// in which they will be returned when interating over them in [`candidates`] and [`unselected`]. + /// + /// [`candidates`]: CoinSelector::candidates + /// [`unselected`]: CoinSelector::unselected pub fn sort_candidates_by_key(&mut self, mut key_fn: F) where F: FnMut((usize, Candidate)) -> K, @@ -271,10 +298,16 @@ impl<'a> CoinSelector<'a> { self.sort_candidates_by(|a, b| key_fn(a).cmp(&key_fn(b))) } + /// Sorts the candidates by descending value per weight unit pub fn sort_candidates_by_descending_value_pwu(&mut self) { self.sort_candidates_by_key(|(_, wv)| core::cmp::Reverse(wv.value_pwu())); } + /// The waste created by the current selection as measured by the [waste metric]. + /// + /// You can pass in an `excess_discount` which must be between `0.0..1.0`. Passing in `1.0` gives you no discount + /// + /// [waste metric]; https://bitcoin.stackexchange.com/questions/113622/what-does-waste-metric-mean-in-the-context-of-coin-selection pub fn waste( &self, target: Target, @@ -301,36 +334,51 @@ impl<'a> CoinSelector<'a> { waste } + /// The selected candidates with their index. pub fn selected(&self) -> impl ExactSizeIterator + '_ { self.selected .iter() .map(move |&index| (index, self.candidates[index])) } + /// The unselected candidates with their index. + /// + /// The candidates are returned in sorted order. See [`sort_candidates_by`]. + /// + /// [`sort_candidates_by`]: Self::sort_candidates_by pub fn unselected(&self) -> impl DoubleEndedIterator + '_ { - self.unselected_indexes() + self.unselected_indices() .map(move |i| (i, self.candidates[i])) } - pub fn selected_indexes(&self) -> &BTreeSet { + /// The indices of the selelcted candidates. + pub fn selected_indices(&self) -> &BTreeSet { &self.selected } - pub fn unselected_indexes(&self) -> impl DoubleEndedIterator + '_ { + /// The indices of the unselected candidates. + /// + /// This excludes candidates that have been selected or [`banned`]. + /// + /// [`banned`]: Self::ban + pub fn unselected_indices(&self) -> impl DoubleEndedIterator + '_ { self.candidate_order .iter() .filter(move |index| !(self.selected.contains(index) || self.banned.contains(index))) .map(|index| *index) } + /// Whether there are any unselected candidates left. pub fn is_exhausted(&self) -> bool { - self.unselected_indexes().next().is_none() + self.unselected_indices().next().is_none() } + /// Whether the constraints of `Target` have been met if we include the `drain` ouput. pub fn is_target_met(&self, target: Target, drain: Drain) -> bool { self.excess(target, drain) >= 0 } + /// Select all unselected candidates pub fn select_all(&mut self) { loop { if !self.select_next() { @@ -339,20 +387,28 @@ impl<'a> CoinSelector<'a> { } } + /// Select all candidates with an *effective value* greater than 0 at the provided `feerate`. + /// + /// A candidate if effective if it provides more value than it takes to pay for at `feerate`. pub fn select_all_effective(&mut self, feerate: FeeRate) { // TODO: do this without allocating - for i in self.unselected_indexes().collect::>() { + for i in self.unselected_indices().collect::>() { if self.candidates[i].effective_value(feerate) > Ordf32(0.0) { self.select(i); } } } + /// Select candidates until `target` has been met assuming the `drain` output is attached. + /// + /// Returns an `Some(_)` if it was able to meet the target. #[must_use] - pub fn select_until_target_met(&mut self, target: Target, drain: Drain) -> Option<()> { - self.select_until(|cs| cs.is_target_met(target, drain)) + pub fn select_until_target_met(&mut self, target: Target, drain: Drain) -> Result<(), InsufficientFunds> { + self.select_until(|cs| cs.is_target_met(target, drain)).ok_or_else(|| InsufficientFunds { missing: self.excess(target, drain).abs() as u64 }) } + + /// Select candidates until some predicate has been satisfied. #[must_use] pub fn select_until( &mut self, @@ -369,10 +425,12 @@ impl<'a> CoinSelector<'a> { } } + /// Return an iterator that can be used to select candidates. pub fn select_iter(self) -> SelectIter<'a> { SelectIter { cs: self.clone() } } + /// Runs a branch and bound algorithm to optimize for the provided metric pub fn branch_and_bound( &self, metric: M, @@ -381,28 +439,6 @@ impl<'a> CoinSelector<'a> { } } -pub struct SelectIter<'a> { - cs: CoinSelector<'a>, -} - -impl<'a> Iterator for SelectIter<'a> { - type Item = (CoinSelector<'a>, usize, Candidate); - - fn next(&mut self) -> Option { - let (index, wv) = self.cs.unselected().next()?; - self.cs.select(index); - Some((self.cs.clone(), index, wv)) - } -} - -impl<'a> DoubleEndedIterator for SelectIter<'a> { - fn next_back(&mut self) -> Option { - let (index, wv) = self.cs.unselected().next_back()?; - self.cs.select(index); - Some((self.cs.clone(), index, wv)) - } -} - impl<'a> core::fmt::Display for CoinSelector<'a> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "[")?; @@ -444,6 +480,11 @@ pub struct Candidate { } impl Candidate { + + pub fn new_tr_keyspend(value: u64) -> Self { + let weight = TXIN_BASE_WEIGHT + TR_KEYSPEND_SATISFACTION_WEIGHT; + Self::new(value, weight, true) + } /// Create a new [`Candidate`] that represents a single input. /// /// `satisfaction_weight` is the weight of `scriptSigLen + scriptSig + scriptWitnessLen + @@ -501,6 +542,14 @@ impl Drain { !self.is_none() } + pub fn new_tr_keyspend() -> Self { + Self { + weight: TXOUT_BASE_WEIGHT + TR_SPK_WEIGHT, + value: 0, + spend_weight: TXIN_BASE_WEIGHT + TR_KEYSPEND_SATISFACTION_WEIGHT, + } + } + /// The waste of adding this drain to a transaction according to the [waste metric]. /// /// [waste metric]; https://bitcoin.stackexchange.com/questions/113622/what-does-waste-metric-mean-in-the-context-of-coin-selection @@ -508,3 +557,41 @@ impl Drain { self.weight as f32 * feerate.spwu() + self.spend_weight as f32 * long_term_feerate.spwu() } } + +/// The `SelectIter` allows you to select candidates by calling `.next`. +pub struct SelectIter<'a> { + cs: CoinSelector<'a>, +} + +impl<'a> Iterator for SelectIter<'a> { + type Item = (CoinSelector<'a>, usize, Candidate); + + fn next(&mut self) -> Option { + let (index, wv) = self.cs.unselected().next()?; + self.cs.select(index); + Some((self.cs.clone(), index, wv)) + } +} + +impl<'a> DoubleEndedIterator for SelectIter<'a> { + fn next_back(&mut self) -> Option { + let (index, wv) = self.cs.unselected().next_back()?; + self.cs.select(index); + Some((self.cs.clone(), index, wv)) + } +} + + +#[derive(Clone, Debug, Copy, PartialEq, Eq)] +pub struct InsufficientFunds { + missing: u64 +} + +impl core::fmt::Display for InsufficientFunds { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "Insufficient funds. Missing {} sats.", self.missing) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for InsufficientFunds {} diff --git a/nursery/coin_select/src/float.rs b/nursery/coin_select/src/float.rs index da9d07bc5..680352adc 100644 --- a/nursery/coin_select/src/float.rs +++ b/nursery/coin_select/src/float.rs @@ -63,6 +63,7 @@ impl core::fmt::Display for Ordf64 { /// Extension trait for adding basic float ops to f32 that don't exist in core for reasons. pub trait FloatExt { + /// Adds the ceil method to `f32` fn ceil(self) -> Self; } diff --git a/nursery/coin_select/src/lib.rs b/nursery/coin_select/src/lib.rs index 8d799c6e8..be03a1c33 100644 --- a/nursery/coin_select/src/lib.rs +++ b/nursery/coin_select/src/lib.rs @@ -1,5 +1,5 @@ #![no_std] -#![warn(missing_docs)] +// #![warn(missing_docs)] #![doc = include_str!("../README.md")] #![deny(unsafe_code)] @@ -18,7 +18,9 @@ mod coin_selector; pub mod float; pub use coin_selector::*; -pub mod bnb; +mod bnb; +pub use bnb::*; + pub mod metrics; mod feerate; @@ -33,6 +35,12 @@ pub const TXIN_BASE_WEIGHT: u32 = (32 + 4 + 4 + 1) * 4; /// Just the weight of the value field. pub const TXOUT_BASE_WEIGHT: u32 = 4 * core::mem::size_of::() as u32; // just the value +pub const TR_KEYSPEND_SATISFACTION_WEIGHT: u32 = 66; + +/// The weight of a taproot script pubkey +pub const TR_SPK_WEIGHT: u32 = (1 + 1 + 32) * 4; // version + push + key + + /// Helper to calculate varint size. `v` is the value the varint represents. fn varint_size(v: usize) -> u32 { if v <= 0xfc { diff --git a/nursery/coin_select/src/metrics.rs b/nursery/coin_select/src/metrics.rs index 0d13375b5..eb5175c20 100644 --- a/nursery/coin_select/src/metrics.rs +++ b/nursery/coin_select/src/metrics.rs @@ -1,3 +1,4 @@ +//! Branch and bound metrics that can be passed to [`CoinSelector::branch_and_bound`]. use crate::{bnb::BnBMetric, float::Ordf32, CoinSelector, Drain, Target}; mod waste; pub use waste::*; diff --git a/nursery/coin_select/src/metrics/waste.rs b/nursery/coin_select/src/metrics/waste.rs index 01c3829c7..3db706b4c 100644 --- a/nursery/coin_select/src/metrics/waste.rs +++ b/nursery/coin_select/src/metrics/waste.rs @@ -141,8 +141,8 @@ where // by taking whatever value we want from it but at the value per weight of the real // input. let ideal_next_weight = { - // satisfying absolute and feerate requires different calculations sowe do them - // both indepdently and find which requires the most weight of the next input. + // satisfying absolute and feerate constraints requires different calculations so we do them + // both independently and find which requires the most weight of the next input. let remaining_rate = cs.rate_excess(self.target, change_lower_bound); let remaining_abs = cs.absolute_excess(self.target, change_lower_bound); diff --git a/nursery/coin_select/tests/bnb.rs b/nursery/coin_select/tests/bnb.rs index edcf28398..f391d0cbc 100644 --- a/nursery/coin_select/tests/bnb.rs +++ b/nursery/coin_select/tests/bnb.rs @@ -1,4 +1,4 @@ -use bdk_coin_select::{bnb::BnBMetric, CoinSelector, Drain, FeeRate, Target, Candidate}; +use bdk_coin_select::{BnBMetric, CoinSelector, Drain, FeeRate, Target, Candidate}; #[macro_use] extern crate alloc; @@ -40,7 +40,7 @@ impl BnBMetric for MinExcessThenWeight { let lower_bound_excess = cs.excess(self.target, Drain::none()).max(0); let lower_bound_weight = { let mut cs = cs.clone(); - cs.select_until_target_met(self.target, Drain::none())?; + cs.select_until_target_met(self.target, Drain::none()).ok()?; cs.selected_weight() }; Some((lower_bound_excess, lower_bound_weight)) diff --git a/nursery/coin_select/tests/changeless.rs b/nursery/coin_select/tests/changeless.rs index 2dbebaef6..cac82c354 100644 --- a/nursery/coin_select/tests/changeless.rs +++ b/nursery/coin_select/tests/changeless.rs @@ -110,7 +110,7 @@ fn random_minimal_selection<'a>( ) -> CoinSelector<'a> { let mut cs = cs.clone(); let mut last_waste: Option = None; - while let Some(next) = cs.unselected_indexes().choose(rng) { + while let Some(next) = cs.unselected_indices().choose(rng) { cs.select(next); if cs.is_target_met(target, change_policy(&cs, target)) { break; diff --git a/nursery/coin_select/tests/waste.rs b/nursery/coin_select/tests/waste.rs index 5269bea12..aa1a0953b 100644 --- a/nursery/coin_select/tests/waste.rs +++ b/nursery/coin_select/tests/waste.rs @@ -422,7 +422,7 @@ fn randomly_satisfy_target_with_low_waste<'a>( let mut cs = cs.clone(); let mut last_waste: Option = None; - while let Some(next) = cs.unselected_indexes().choose(rng) { + while let Some(next) = cs.unselected_indices().choose(rng) { cs.select(next); let change = change_policy(&cs, target); if cs.is_target_met(target, change) { From a86fed1f78b12a606ed773168595f919e5ddff67 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Wed, 21 Jun 2023 12:24:55 +0800 Subject: [PATCH 6/8] cargo fmt --- nursery/coin_select/src/coin_selector.rs | 18 ++++++++++-------- nursery/coin_select/src/lib.rs | 3 --- nursery/coin_select/src/metrics/waste.rs | 2 +- nursery/coin_select/tests/bnb.rs | 5 +++-- nursery/coin_select/tests/changeless.rs | 5 ++--- nursery/coin_select/tests/waste.rs | 3 +-- 6 files changed, 17 insertions(+), 19 deletions(-) diff --git a/nursery/coin_select/src/coin_selector.rs b/nursery/coin_select/src/coin_selector.rs index 94ee630df..f20a1673d 100644 --- a/nursery/coin_select/src/coin_selector.rs +++ b/nursery/coin_select/src/coin_selector.rs @@ -279,8 +279,6 @@ impl<'a> CoinSelector<'a> { order.sort_by(|a, b| cmp((*a, candidates[*a]), (*b, candidates[*b]))) } - - /// Sorts the candidates by the key function. /// /// The key function takes the candidates's index and the [`Candidate`]. @@ -403,11 +401,17 @@ impl<'a> CoinSelector<'a> { /// /// Returns an `Some(_)` if it was able to meet the target. #[must_use] - pub fn select_until_target_met(&mut self, target: Target, drain: Drain) -> Result<(), InsufficientFunds> { - self.select_until(|cs| cs.is_target_met(target, drain)).ok_or_else(|| InsufficientFunds { missing: self.excess(target, drain).abs() as u64 }) + pub fn select_until_target_met( + &mut self, + target: Target, + drain: Drain, + ) -> Result<(), InsufficientFunds> { + self.select_until(|cs| cs.is_target_met(target, drain)) + .ok_or_else(|| InsufficientFunds { + missing: self.excess(target, drain).abs() as u64, + }) } - /// Select candidates until some predicate has been satisfied. #[must_use] pub fn select_until( @@ -480,7 +484,6 @@ pub struct Candidate { } impl Candidate { - pub fn new_tr_keyspend(value: u64) -> Self { let weight = TXIN_BASE_WEIGHT + TR_KEYSPEND_SATISFACTION_WEIGHT; Self::new(value, weight, true) @@ -581,10 +584,9 @@ impl<'a> DoubleEndedIterator for SelectIter<'a> { } } - #[derive(Clone, Debug, Copy, PartialEq, Eq)] pub struct InsufficientFunds { - missing: u64 + missing: u64, } impl core::fmt::Display for InsufficientFunds { diff --git a/nursery/coin_select/src/lib.rs b/nursery/coin_select/src/lib.rs index be03a1c33..d2633dec9 100644 --- a/nursery/coin_select/src/lib.rs +++ b/nursery/coin_select/src/lib.rs @@ -3,7 +3,6 @@ #![doc = include_str!("../README.md")] #![deny(unsafe_code)] - #[allow(unused)] #[macro_use] extern crate alloc; @@ -13,7 +12,6 @@ extern crate alloc; #[macro_use] extern crate std; - mod coin_selector; pub mod float; pub use coin_selector::*; @@ -40,7 +38,6 @@ pub const TR_KEYSPEND_SATISFACTION_WEIGHT: u32 = 66; /// The weight of a taproot script pubkey pub const TR_SPK_WEIGHT: u32 = (1 + 1 + 32) * 4; // version + push + key - /// Helper to calculate varint size. `v` is the value the varint represents. fn varint_size(v: usize) -> u32 { if v <= 0xfc { diff --git a/nursery/coin_select/src/metrics/waste.rs b/nursery/coin_select/src/metrics/waste.rs index 3db706b4c..f9f297e6a 100644 --- a/nursery/coin_select/src/metrics/waste.rs +++ b/nursery/coin_select/src/metrics/waste.rs @@ -1,5 +1,5 @@ use super::change_lower_bound; -use crate::{bnb::BnBMetric, float::Ordf32, CoinSelector, Drain, FeeRate, Target, Candidate}; +use crate::{bnb::BnBMetric, float::Ordf32, Candidate, CoinSelector, Drain, FeeRate, Target}; /// The "waste" metric used by bitcoin core. /// diff --git a/nursery/coin_select/tests/bnb.rs b/nursery/coin_select/tests/bnb.rs index f391d0cbc..4d9124c71 100644 --- a/nursery/coin_select/tests/bnb.rs +++ b/nursery/coin_select/tests/bnb.rs @@ -1,4 +1,4 @@ -use bdk_coin_select::{BnBMetric, CoinSelector, Drain, FeeRate, Target, Candidate}; +use bdk_coin_select::{BnBMetric, Candidate, CoinSelector, Drain, FeeRate, Target}; #[macro_use] extern crate alloc; @@ -40,7 +40,8 @@ impl BnBMetric for MinExcessThenWeight { let lower_bound_excess = cs.excess(self.target, Drain::none()).max(0); let lower_bound_weight = { let mut cs = cs.clone(); - cs.select_until_target_met(self.target, Drain::none()).ok()?; + cs.select_until_target_met(self.target, Drain::none()) + .ok()?; cs.selected_weight() }; Some((lower_bound_excess, lower_bound_weight)) diff --git a/nursery/coin_select/tests/changeless.rs b/nursery/coin_select/tests/changeless.rs index cac82c354..02d664b70 100644 --- a/nursery/coin_select/tests/changeless.rs +++ b/nursery/coin_select/tests/changeless.rs @@ -1,6 +1,5 @@ -use bdk_coin_select::{ - change_policy, float::Ordf32, metrics, CoinSelector, Drain, FeeRate, Target, Candidate, -}; +#![allow(unused)] +use bdk_coin_select::{float::Ordf32, metrics, Candidate, CoinSelector, Drain, FeeRate, Target}; use proptest::{ prelude::*, test_runner::{RngAlgorithm, TestRng}, diff --git a/nursery/coin_select/tests/waste.rs b/nursery/coin_select/tests/waste.rs index aa1a0953b..a007c3dbc 100644 --- a/nursery/coin_select/tests/waste.rs +++ b/nursery/coin_select/tests/waste.rs @@ -1,6 +1,5 @@ use bdk_coin_select::{ - change_policy, float::Ordf32, metrics::Waste, CoinSelector, Drain, FeeRate, Target, - Candidate, + change_policy, float::Ordf32, metrics::Waste, Candidate, CoinSelector, Drain, FeeRate, Target, }; use proptest::{ prelude::*, From 02f72c8e30849512f5f2726962397c72b274e476 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Wed, 21 Jun 2023 12:35:40 +0800 Subject: [PATCH 7/8] Include the SPK length field weight in TXOUT_BASE_weight it's inconsistent to not include it and I confused myself. --- nursery/coin_select/src/lib.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/nursery/coin_select/src/lib.rs b/nursery/coin_select/src/lib.rs index d2633dec9..f849e8560 100644 --- a/nursery/coin_select/src/lib.rs +++ b/nursery/coin_select/src/lib.rs @@ -29,13 +29,18 @@ pub mod change_policy; /// length. pub const TXIN_BASE_WEIGHT: u32 = (32 + 4 + 4 + 1) * 4; -/// The weight of a TXOUT without the `scriptPubkey` (and script pubkey length field). -/// Just the weight of the value field. -pub const TXOUT_BASE_WEIGHT: u32 = 4 * core::mem::size_of::() as u32; // just the value - +/// The weight of a TXOUT with a zero length `scriptPubkey` +pub const TXOUT_BASE_WEIGHT: u32 = + // The value + 4 * core::mem::size_of::() as u32 + // The spk length + + (4 * 1); + +/// The additional weight over [`TXIN_BASE_WEIGHT`] incurred by satisfying an input with a keyspend +/// and the default sighash. pub const TR_KEYSPEND_SATISFACTION_WEIGHT: u32 = 66; -/// The weight of a taproot script pubkey +/// The additional weight of an output with segwit `v1` (taproot) script pubkey over a blank output (i.e. with weight [`TXOUT_BASE_WEIGHT`]). pub const TR_SPK_WEIGHT: u32 = (1 + 1 + 32) * 4; // version + push + key /// Helper to calculate varint size. `v` is the value the varint represents. From 4e0c907b9d297b7df9e95495dc6ca26d4ecd490e Mon Sep 17 00:00:00 2001 From: LLFourn Date: Tue, 4 Jul 2023 13:50:43 +0800 Subject: [PATCH 8/8] Fix weight calculations for mixed legacy and segwit see: https://github.com/bitcoindevkit/bdk/pull/924#discussion_r1243605989 Was a PITA since branch and bound is hard to do with this interference between segiwt and legacy weights. It would find solutions that looked good until you add the final input which was segwit and then the solution would be suboptimal and fail the test. --- .../keychain_tracker_example_cli/src/lib.rs | 2 +- nursery/coin_select/src/coin_selector.rs | 41 ++--- nursery/coin_select/src/metrics/waste.rs | 2 +- nursery/coin_select/tests/bnb.rs | 39 ++++- nursery/coin_select/tests/changeless.rs | 2 +- nursery/coin_select/tests/waste.rs | 7 +- nursery/coin_select/tests/weight.rs | 163 ++++++++++++++++++ 7 files changed, 223 insertions(+), 33 deletions(-) create mode 100644 nursery/coin_select/tests/weight.rs diff --git a/example-crates/keychain_tracker_example_cli/src/lib.rs b/example-crates/keychain_tracker_example_cli/src/lib.rs index 3a5dcc02f..1d2bdcde7 100644 --- a/example-crates/keychain_tracker_example_cli/src/lib.rs +++ b/example-crates/keychain_tracker_example_cli/src/lib.rs @@ -15,7 +15,7 @@ use bdk_chain::{ sparse_chain::{self, ChainPosition}, DescriptorExt, FullTxOut, }; -use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, Candidate}; +use bdk_coin_select::{coin_select_bnb, Candidate, CoinSelector, CoinSelectorOpt}; use bdk_file_store::KeychainStore; use clap::{Parser, Subcommand}; use std::{ diff --git a/nursery/coin_select/src/coin_selector.rs b/nursery/coin_select/src/coin_selector.rs index f20a1673d..e93dedfa3 100644 --- a/nursery/coin_select/src/coin_selector.rs +++ b/nursery/coin_select/src/coin_selector.rs @@ -166,28 +166,31 @@ impl<'a> CoinSelector<'a> { pub fn is_empty(&self) -> bool { self.selected.is_empty() } - /// Weight sum of all selected inputs. - pub fn selected_weight(&self) -> u32 { - self.selected - .iter() - .map(|&index| self.candidates[index].weight) - .sum() - } /// The weight of the inputs including the witness header and the varint for the number of /// inputs. - fn input_weight(&self) -> u32 { - let witness_header_extra_weight = self - .selected() - .find(|(_, wv)| wv.is_segwit) - .map(|_| 2) - .unwrap_or(0); + pub fn input_weight(&self) -> u32 { + let is_segwit_tx = self.selected().any(|(_, wv)| wv.is_segwit); + let witness_header_extra_weight = is_segwit_tx as u32 * 2; let vin_count_varint_extra_weight = { let input_count = self.selected().map(|(_, wv)| wv.input_count).sum::(); (varint_size(input_count) - 1) * 4 }; - self.selected_weight() + witness_header_extra_weight + vin_count_varint_extra_weight + let selected_weight: u32 = self + .selected() + .map(|(_, candidate)| { + let mut weight = candidate.weight; + if is_segwit_tx && !candidate.is_segwit { + // non-segwit candidates do not have the witness length field included in their + // weight field so we need to add 1 here if it's in a segwit tx. + weight += 1; + } + weight + }) + .sum(); + + selected_weight + witness_header_extra_weight + vin_count_varint_extra_weight } /// Absolute value sum of all selected inputs. @@ -201,8 +204,6 @@ impl<'a> CoinSelector<'a> { /// Current weight of template tx + selected inputs. pub fn weight(&self, drain_weight: u32) -> u32 { // TODO take into account whether drain tips over varint for number of outputs - // - // TODO: take into account the witness stack length for each input self.base_weight + self.input_weight() + drain_weight } @@ -235,7 +236,7 @@ impl<'a> CoinSelector<'a> { } /// The feerate the transaction would have if we were to use this selection of inputs to acheive - /// the + /// the ``target_value` pub fn implied_feerate(&self, target_value: u64, drain: Drain) -> FeeRate { let numerator = self.selected_value() as i64 - target_value as i64 - drain.value as i64; let denom = self.weight(drain.weight); @@ -257,8 +258,8 @@ impl<'a> CoinSelector<'a> { } // /// Waste sum of all selected inputs. - fn selected_waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 { - self.selected_weight() as f32 * (feerate.spwu() - long_term_feerate.spwu()) + fn input_waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 { + self.input_weight() as f32 * (feerate.spwu() - long_term_feerate.spwu()) } /// Sorts the candidates by the comparision function. @@ -314,7 +315,7 @@ impl<'a> CoinSelector<'a> { excess_discount: f32, ) -> f32 { debug_assert!(excess_discount >= 0.0 && excess_discount <= 1.0); - let mut waste = self.selected_waste(target.feerate, long_term_feerate); + let mut waste = self.input_waste(target.feerate, long_term_feerate); if drain.is_none() { // We don't allow negative excess waste since negative excess just means you haven't diff --git a/nursery/coin_select/src/metrics/waste.rs b/nursery/coin_select/src/metrics/waste.rs index f9f297e6a..c6fdb8b97 100644 --- a/nursery/coin_select/src/metrics/waste.rs +++ b/nursery/coin_select/src/metrics/waste.rs @@ -154,7 +154,7 @@ where debug_assert!(weight_to_satisfy <= to_slurp.weight as f32); weight_to_satisfy }; - let weight_lower_bound = cs.selected_weight() as f32 + ideal_next_weight; + let weight_lower_bound = cs.input_weight() as f32 + ideal_next_weight; let mut waste = weight_lower_bound * rate_diff; waste += change_lower_bound.waste(self.target.feerate, self.long_term_feerate); diff --git a/nursery/coin_select/tests/bnb.rs b/nursery/coin_select/tests/bnb.rs index 4d9124c71..90f54e8c5 100644 --- a/nursery/coin_select/tests/bnb.rs +++ b/nursery/coin_select/tests/bnb.rs @@ -12,12 +12,17 @@ use rand::{Rng, RngCore}; fn test_wv(mut rng: impl RngCore) -> impl Iterator { core::iter::repeat_with(move || { let value = rng.gen_range(0..1_000); - Candidate { + let mut candidate = Candidate { value, weight: 100, input_count: rng.gen_range(1..2), is_segwit: rng.gen_bool(0.5), - } + }; + // HACK: set is_segwit = true for all these tests because you can't actually lower bound + // things easily with how segwit inputs interfere with their weights. We can't modify the + // above since that would change what we pull from rng. + candidate.is_segwit = true; + candidate }) } @@ -32,7 +37,7 @@ impl BnBMetric for MinExcessThenWeight { if cs.excess(self.target, Drain::none()) < 0 { None } else { - Some((cs.excess(self.target, Drain::none()), cs.selected_weight())) + Some((cs.excess(self.target, Drain::none()), cs.input_weight())) } } @@ -42,7 +47,7 @@ impl BnBMetric for MinExcessThenWeight { let mut cs = cs.clone(); cs.select_until_target_met(self.target, Drain::none()) .ok()?; - cs.selected_weight() + cs.input_weight() }; Some((lower_bound_excess, lower_bound_weight)) } @@ -56,10 +61,18 @@ fn bnb_finds_an_exact_solution_in_n_iter() { let num_additional_canidates = 50; let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); - let mut wv = test_wv(&mut rng); + let mut wv = test_wv(&mut rng).map(|mut candidate| { + candidate.is_segwit = true; + candidate + }); let solution: Vec = (0..solution_len).map(|_| wv.next().unwrap()).collect(); - let solution_weight = solution.iter().map(|sol| sol.weight).sum(); + let solution_weight = { + let mut cs = CoinSelector::new(&solution, 0); + cs.select_all(); + cs.input_weight() + }; + let target = solution.iter().map(|c| c.value).sum(); let mut candidates = solution.clone(); @@ -86,7 +99,7 @@ fn bnb_finds_an_exact_solution_in_n_iter() { assert_eq!(i, 806); - assert!(best.selected_weight() <= solution_weight); + assert!(best.input_weight() <= solution_weight); assert_eq!(best.selected_value(), target.value); } @@ -97,6 +110,7 @@ fn bnb_finds_solution_if_possible_in_n_iter() { 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, 0); let target = Target { @@ -151,13 +165,20 @@ proptest! { let mut wv = test_wv(&mut rng); let solution: Vec = (0..solution_len).map(|_| wv.next().unwrap()).collect(); + let solution_weight = { + let mut cs = CoinSelector::new(&solution, 0); + cs.select_all(); + cs.input_weight() + }; + let target = solution.iter().map(|c| c.value).sum(); - let solution_weight = solution.iter().map(|sol| sol.weight).sum(); let mut candidates = solution.clone(); candidates.extend(wv.take(num_additional_canidates)); let mut cs = CoinSelector::new(&candidates, 0); + + for i in 0..num_preselected.min(solution_len) { cs.select(i); } @@ -182,7 +203,7 @@ proptest! { - prop_assert!(best.selected_weight() <= solution_weight); + prop_assert!(best.input_weight() <= solution_weight); prop_assert_eq!(best.selected_value(), target.value); } } diff --git a/nursery/coin_select/tests/changeless.rs b/nursery/coin_select/tests/changeless.rs index 02d664b70..4f3479e4d 100644 --- a/nursery/coin_select/tests/changeless.rs +++ b/nursery/coin_select/tests/changeless.rs @@ -47,7 +47,7 @@ proptest! { value: 0 }; - let change_policy = crate::change_policy::min_waste(drain, long_term_feerate); + let change_policy = bdk_coin_select::change_policy::min_waste(drain, long_term_feerate); let wv = test_wv(&mut rng); let candidates = wv.take(num_inputs).collect::>(); diff --git a/nursery/coin_select/tests/waste.rs b/nursery/coin_select/tests/waste.rs index a007c3dbc..dc0fad499 100644 --- a/nursery/coin_select/tests/waste.rs +++ b/nursery/coin_select/tests/waste.rs @@ -257,7 +257,12 @@ fn waste_low_but_non_negative_rate_diff_means_adding_more_inputs_might_reduce_ex let change_policy = change_policy::min_waste(drain, long_term_feerate); let wv = test_wv(&mut rng); - let candidates = wv.take(num_inputs).collect::>(); + let mut candidates = wv.take(num_inputs).collect::>(); + // HACK: for this test had to set segwit true to keep it working once we + // started properly accounting for legacy weight variations + candidates + .iter_mut() + .for_each(|candidate| candidate.is_segwit = true); let cs = CoinSelector::new(&candidates, base_weight); diff --git a/nursery/coin_select/tests/weight.rs b/nursery/coin_select/tests/weight.rs new file mode 100644 index 000000000..3fa9c3ff3 --- /dev/null +++ b/nursery/coin_select/tests/weight.rs @@ -0,0 +1,163 @@ +use bdk_coin_select::{Candidate, CoinSelector, Drain}; +use bitcoin::{consensus::Decodable, ScriptBuf, Transaction}; + +fn hex_val(c: u8) -> u8 { + match c { + b'A'..=b'F' => c - b'A' + 10, + b'a'..=b'f' => c - b'a' + 10, + b'0'..=b'9' => c - b'0', + _ => panic!("invalid"), + } +} + +// Appears that Transaction has no from_str so I had to roll my own hex decoder +pub fn hex_decode(hex: &str) -> Vec { + let mut bytes = Vec::with_capacity(hex.len() * 2); + for hex_byte in hex.as_bytes().chunks(2) { + bytes.push(hex_val(hex_byte[0]) << 4 | hex_val(hex_byte[1])) + } + bytes +} + +#[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 mut tx = Transaction::consensus_decode(&mut cursor).unwrap(); + let input_values = vec![563_336_755]; + let inputs = core::mem::take(&mut tx.input); + + let candidates = inputs + .iter() + .zip(input_values) + .map(|(txin, value)| Candidate { + value, + weight: txin.segwit_weight() as u32, + input_count: 1, + is_segwit: true, + }) + .collect::>(); + + let mut coin_selector = CoinSelector::new(&candidates, tx.weight().to_wu() as u32); + coin_selector.select_all(); + + assert_eq!(coin_selector.weight(0), 449); + assert_eq!( + (coin_selector + .implied_feerate(tx.output[0].value, Drain::none()) + .as_sat_vb() + * 10.0) + .round(), + 60.2 * 10.0 + ); +} + +#[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 mut tx = Transaction::consensus_decode(&mut cursor).unwrap(); + let input_values = vec![003_194_967, 000_014_068]; + let inputs = core::mem::take(&mut tx.input); + + let candidates = inputs + .iter() + .zip(input_values) + .map(|(txin, value)| Candidate { + value, + weight: txin.segwit_weight() as u32, + input_count: 1, + is_segwit: true, + }) + .collect::>(); + + let mut coin_selector = CoinSelector::new(&candidates, tx.weight().to_wu() as u32); + coin_selector.select_all(); + + assert_eq!(coin_selector.weight(0), 721); + assert_eq!( + (coin_selector + .implied_feerate(tx.output[0].value, Drain::none()) + .as_sat_vb() + * 10.0) + .round(), + 58.1 * 10.0 + ); +} + +#[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 mut tx = Transaction::consensus_decode(&mut cursor).unwrap(); + let orig_weight = tx.weight(); + let input_values = vec![022_680_000, 006_558_175, 006_558_200]; + let inputs = core::mem::take(&mut tx.input); + let candidates = inputs + .iter() + .zip(input_values) + .map(|(txin, value)| Candidate { + value, + weight: txin.legacy_weight() as u32, + input_count: 1, + is_segwit: false, + }) + .collect::>(); + + let mut coin_selector = CoinSelector::new(&candidates, tx.weight().to_wu() as u32); + coin_selector.select_all(); + + assert_eq!(coin_selector.weight(0), orig_weight.to_wu() as u32); + assert_eq!( + (coin_selector + .implied_feerate(tx.output.iter().map(|o| o.value).sum(), Drain::none()) + .as_sat_vb() + * 10.0) + .round(), + 99.2 * 10.0 + ); +} + +#[test] +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(); + tx.input[1].script_sig = ScriptBuf::default(); + tx.input[1].witness = vec![ + // semi-realistic p2wpkh spend + hex_decode("3045022100bdc115b86e9c863279132b4808459cf9b266c8f6a9c14a3dfd956986b807e3320220265833b85197679687c5d5eed1b2637489b34249d44cf5d2d40bc7b514181a5101"), + hex_decode("02077741a668889ce15d59365886375aea47a7691941d7a0d301697edbc773b45b"), + ].into(); + let orig_weight = tx.weight(); + let input_values = vec![022_680_000, 006_558_175, 006_558_200]; + let inputs = core::mem::take(&mut tx.input); + let candidates = inputs + .iter() + .zip(input_values) + .enumerate() + .map(|(i, (txin, value))| { + let is_segwit = i == 1; + Candidate { + value, + weight: if is_segwit { + txin.segwit_weight() + } else { + txin.legacy_weight() + } as u32, + input_count: 1, + is_segwit, + } + }) + .collect::>(); + + let mut coin_selector = CoinSelector::new(&candidates, tx.weight().to_wu() as u32); + coin_selector.select_all(); + + assert_eq!(coin_selector.weight(0), orig_weight.to_wu() as u32); +}