diff --git a/Cargo.1.48.0.toml b/Cargo.1.48.0.toml new file mode 100644 index 0000000000..922862700c --- /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 0000000000..4402dd1ee4 --- /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/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index 513f74b8c2..47044d82f6 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -1,9 +1,9 @@ pub use anyhow; use anyhow::Context; -use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue}; +use bdk_coin_select::{Candidate, CoinSelector}; use bdk_file_store::Store; use serde::{de::DeserializeOwned, Serialize}; -use std::{cmp::Reverse, collections::HashMap, path::PathBuf, sync::Mutex, time::Duration}; +use std::{cmp::Reverse, collections::HashMap, path::PathBuf, sync::Mutex}; use bdk_chain::{ bitcoin::{ @@ -16,7 +16,7 @@ use bdk_chain::{ descriptor::{DescriptorSecretKey, KeyMap}, Descriptor, DescriptorPublicKey, }, - Anchor, Append, ChainOracle, DescriptorExt, FullTxOut, Persist, PersistBackend, + Anchor, Append, ChainOracle, FullTxOut, Persist, PersistBackend, }; pub use bdk_file_store; pub use clap; @@ -412,39 +412,18 @@ where }; // TODO use planning module - let mut candidates = planned_utxos(graph, chain, &assets)?; - - // apply coin selection algorithm - match cs_algorithm { - CoinSelectionAlgo::LargestFirst => { - candidates.sort_by_key(|(_, utxo)| Reverse(utxo.txout.value)) - } - CoinSelectionAlgo::SmallestFirst => candidates.sort_by_key(|(_, utxo)| utxo.txout.value), - CoinSelectionAlgo::OldestFirst => { - candidates.sort_by_key(|(_, utxo)| utxo.chain_position.clone()) - } - CoinSelectionAlgo::NewestFirst => { - candidates.sort_by_key(|(_, utxo)| Reverse(utxo.chain_position.clone())) - } - CoinSelectionAlgo::BranchAndBound => {} - } - + let raw_candidates = planned_utxos(graph, chain, &assets)?; // turn the txos we chose into weight and value - let wv_candidates = candidates + let candidates = raw_candidates .iter() .map(|(plan, utxo)| { - WeightedValue::new( + Candidate::new( utxo.txout.value, plan.expected_weight() as _, plan.witness_version().is_some(), ) }) - .collect(); - - let mut outputs = vec![TxOut { - value, - script_pubkey: address.script_pubkey(), - }]; + .collect::>(); let internal_keychain = if graph.index.keychains().get(&Keychain::Internal).is_some() { Keychain::Internal @@ -457,7 +436,7 @@ where additions.append(change_additions); // Clone to drop the immutable reference. - let change_script = change_script.into(); + let change_script = change_script.to_owned(); let change_plan = bdk_tmp_plan::plan_satisfaction( &graph @@ -471,68 +450,113 @@ where ) .expect("failed to obtain change plan"); - let mut change_output = TxOut { - value: 0, - script_pubkey: change_script, + let mut transaction = Transaction { + version: 0x02, + // because the temporary planning module does not support timelocks, we can use the chain + // tip as the `lock_time` for anti-fee-sniping purposes + lock_time: chain + .get_chain_tip()? + .and_then(|block_id| absolute::LockTime::from_height(block_id.height).ok()) + .unwrap_or(absolute::LockTime::ZERO), + input: vec![], + output: vec![TxOut { + value, + script_pubkey: address.script_pubkey(), + }], }; - let cs_opts = CoinSelectorOpt { - target_feerate: 0.5, - min_drain_value: graph - .index - .keychains() - .get(&internal_keychain) - .expect("must exist") - .dust_value(), - ..CoinSelectorOpt::fund_outputs( - &outputs, - &change_output, - change_plan.expected_weight() as u32, - ) + let target = bdk_coin_select::Target { + feerate: bdk_coin_select::FeeRate::from_sat_per_vb(1.0), + min_fee: 0, + value: transaction.output.iter().map(|txo| txo.value).sum(), }; - // TODO: How can we make it easy to shuffle in order of inputs and outputs here? - // apply coin selection by saying we need to fund these outputs - let mut coin_selector = CoinSelector::new(&wv_candidates, &cs_opts); + let drain = bdk_coin_select::Drain { + weight: { + // we calculate the weight difference of including the drain output in the base tx + // this method will detect varint size changes of txout count + let tx_weight = transaction.weight(); + let tx_weight_with_drain = { + let mut tx = transaction.clone(); + tx.output.push(TxOut { + script_pubkey: change_script.clone(), + ..Default::default() + }); + tx.weight() + }; + (tx_weight_with_drain - tx_weight).to_wu() as u32 - 1 + }, + value: 0, + spend_weight: change_plan.expected_weight() as u32, + }; + let long_term_feerate = bdk_coin_select::FeeRate::from_sat_per_wu(0.25); + let drain_policy = bdk_coin_select::change_policy::min_waste(drain, long_term_feerate); - // just select coins in the order provided until we have enough - // only use the first result (least waste) - let selection = match cs_algorithm { + let mut selector = CoinSelector::new(&candidates, transaction.weight().to_wu() as u32); + match cs_algorithm { CoinSelectionAlgo::BranchAndBound => { - coin_select_bnb(Duration::from_secs(10), coin_selector.clone()) - .map_or_else(|| coin_selector.select_until_finished(), |cs| cs.finish())? + let metric = bdk_coin_select::metrics::Waste { + target, + long_term_feerate, + change_policy: &drain_policy, + }; + let (final_selection, _score) = selector + .branch_and_bound(metric) + .take(50_000) + // we only process viable solutions + .flatten() + .reduce(|(best_sol, best_score), (curr_sol, curr_score)| { + // we are reducing waste + if curr_score < best_score { + (curr_sol, curr_score) + } else { + (best_sol, best_score) + } + }) + .ok_or(anyhow::format_err!("no bnb solution found"))?; + selector = final_selection; + } + cs_algorithm => { + match cs_algorithm { + CoinSelectionAlgo::LargestFirst => { + selector.sort_candidates_by_key(|(_, c)| Reverse(c.value)) + } + CoinSelectionAlgo::SmallestFirst => { + selector.sort_candidates_by_key(|(_, c)| c.value) + } + CoinSelectionAlgo::OldestFirst => selector + .sort_candidates_by_key(|(i, _)| raw_candidates[i].1.chain_position.clone()), + CoinSelectionAlgo::NewestFirst => selector.sort_candidates_by_key(|(i, _)| { + Reverse(raw_candidates[i].1.chain_position.clone()) + }), + CoinSelectionAlgo::BranchAndBound => unreachable!("bnb variant is matched already"), + } + selector.select_until_target_met(target, drain)? } - _ => coin_selector.select_until_finished()?, }; - let (_, selection_meta) = selection.best_strategy(); // get the selected utxos - let selected_txos = selection.apply_selection(&candidates).collect::>(); + let selected_txos = selector + .apply_selection(&raw_candidates) + .collect::>(); - if let Some(drain_value) = selection_meta.drain_value { - change_output.value = drain_value; - // if the selection tells us to use change and the change value is sufficient, we add it as an output - outputs.push(change_output) + let drain = drain_policy(&selector, target); + if drain.is_some() { + transaction.output.push(TxOut { + value: drain.value, + script_pubkey: change_script, + }); } - let mut transaction = Transaction { - version: 0x02, - // because the temporary planning module does not support timelocks, we can use the chain - // tip as the `lock_time` for anti-fee-sniping purposes - lock_time: chain - .get_chain_tip()? - .and_then(|block_id| absolute::LockTime::from_height(block_id.height).ok()) - .unwrap_or(absolute::LockTime::ZERO), - input: selected_txos - .iter() - .map(|(_, utxo)| TxIn { - previous_output: utxo.outpoint, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - }) - .collect(), - output: outputs, - }; + // fill transaction inputs + transaction.input = selected_txos + .iter() + .map(|(_, utxo)| TxIn { + previous_output: utxo.outpoint, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + }) + .collect(); let prevouts = selected_txos .iter() @@ -593,7 +617,7 @@ where } } - let change_info = if selection_meta.drain_value.is_some() { + let change_info = if drain.is_some() { Some((additions, (internal_keychain, change_index))) } else { None diff --git a/nursery/coin_select/Cargo.toml b/nursery/coin_select/Cargo.toml index 0830ad93e6..c31727dd18 100644 --- a/nursery/coin_select/Cargo.toml +++ b/nursery/coin_select/Cargo.toml @@ -1,10 +1,16 @@ [package] name = "bdk_coin_select" -version = "0.0.1" -authors = [ "LLFourn " ] +version = "0.1.0" +edition = "2018" +license = "MIT OR Apache-2.0" [dependencies] -bdk_chain = { path = "../../crates/chain" } +# No dependencies! Don't add any please! + +[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 0000000000..e688eff3e1 --- /dev/null +++ b/nursery/coin_select/README.md @@ -0,0 +1,59 @@ +# 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 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 6938185b9b..0f35c1d0be 100644 --- a/nursery/coin_select/src/bnb.rs +++ b/nursery/coin_select/src/bnb.rs @@ -1,645 +1,142 @@ -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()); + 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_indices().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, - }; +/// A branch and bound metric +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(&mut self, cs: &CoinSelector<'_>) -> Option; + fn bound(&mut self, cs: &CoinSelector<'_>) -> 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 0000000000..b86199f1dd --- /dev/null +++ b/nursery/coin_select/src/change_policy.rs @@ -0,0 +1,53 @@ +#[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; + +/// Add a change output if the change value would be 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 281992a963..8a8a593784 100644 --- a/nursery/coin_select/src/coin_selector.rs +++ b/nursery/coin_select/src/coin_selector.rs @@ -1,175 +1,172 @@ use super::*; +#[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}; + +/// [`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, + candidates: &'a [Candidate], + selected: Cow<'a, BTreeSet>, + banned: Cow<'a, BTreeSet>, + candidate_order: Cow<'a, 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. +/// A target value to select for along with feerate constraints. #[derive(Debug, Clone, Copy)] -pub struct WeightedValue { - /// Total value of the UTXO(s) that this [`WeightedValue`] represents. +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, - /// Total weight of including this/these UTXO(s). - /// `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. - 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, +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, } } - - /// 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 - } -} - -#[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, } -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; - +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 [Candidate], base_weight: u32) -> Self { 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, + candidates, + selected: Cow::Owned(Default::default()), + banned: Cow::Owned(Default::default()), + candidate_order: Cow::Owned((0..candidates.len()).collect()), } } - pub fn fund_outputs( - txouts: &[TxOut], - drain_output: &TxOut, - drain_satisfaction_weight: u32, - ) -> Self { - let mut tx = Transaction { - input: vec![], - version: 1, - lock_time: absolute::LockTime::ZERO, - output: txouts.to_vec(), - }; - let base_weight = tx.weight(); - // Calculating drain_weight like this instead of using .weight() - // allows us to take into account the output len varint increase that - // might happen when adding a new output - 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.to_wu() as u32, - drain_weight.to_wu() as u32, - TXIN_BASE_WEIGHT + drain_satisfaction_weight, - ) - } + /// 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 + '_ { + self.candidate_order + .iter() + .map(move |i| (*i, self.candidates[*i])) } - pub fn long_term_feerate(&self) -> f32 { - self.long_term_feerate.unwrap_or(self.target_feerate) + /// 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) -> Candidate { + self.candidates[index] } - 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 + /// 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) } -} -/// [`CoinSelector`] selects and deselects from a set of candidates. -#[derive(Debug, Clone)] -pub struct CoinSelector<'a> { - pub opts: &'a CoinSelectorOpt, - pub candidates: &'a Vec, - selected: BTreeSet, -} + /// 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]) + } -impl<'a> CoinSelector<'a> { - pub fn candidate(&self, index: usize) -> &WeightedValue { - &self.candidates[index] + /// 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) } - pub fn new(candidates: &'a Vec, opts: &'a CoinSelectorOpt) -> Self { - Self { - candidates, - selected: Default::default(), - opts, + /// Select the next unselected candidate in the sorted order fo the candidates. + pub fn select_next(&mut self) -> bool { + let next = self.unselected_indices().next(); + if let Some(next) = next { + self.select(next); + true + } else { + false } } - pub fn select(&mut self, index: usize) -> bool { - assert!(index < self.candidates.len()); - self.selected.insert(index) + /// Ban an input from being selected. Banning the input means it won't show up in [`unselected`] + /// 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_indices`]: Self::unselected_indices + pub fn ban(&mut self, index: usize) { + self.banned.to_mut().insert(index); } - pub fn deselect(&mut self, index: usize) -> bool { - self.selected.remove(&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. + /// + /// [`ban`]: Self::ban + /// [`is_selection_possible`]: Self::is_selection_possible + 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)) + } + + /// Returns true if no candidates have been selected. pub fn is_empty(&self) -> bool { self.selected.is_empty() } - /// Weight sum of all selected inputs. pub fn selected_weight(&self) -> u32 { self.selected @@ -178,440 +175,425 @@ 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() + /// 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); + 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 } /// Absolute value sum of all selected inputs. - pub fn selected_absolute_value(&self) -> u64 { + pub fn selected_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 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 } - /// Current weight of template tx + selected inputs. - pub fn current_weight(&self) -> u32 { - let witness_header_extra_weight = self - .selected() - .find(|(_, wv)| wv.is_segwit) - .map(|_| 2) - .unwrap_or(0); - let vin_count_varint_extra_weight = { - 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 + /// 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 } - /// Current excess. - pub fn current_excess(&self) -> i64 { - self.selected_effective_value() - self.effective_target() + /// 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 + - drain.value as i64 + - self.implied_fee_from_feerate(target.feerate, drain.weight) as i64 } - /// This is the effective target value. - pub fn effective_target(&self) -> i64 { - let (has_segwit, max_input_count) = self - .candidates - .iter() - .fold((false, 0_usize), |(is_segwit, input_count), c| { - (is_segwit || c.is_segwit, input_count + c.input_count) - }); + /// How much the current selection overshoots the value 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 + - drain.value as i64 + - target.min_fee as i64 + } - let effective_base_weight = self.opts.base_weight - + if has_segwit { 2_u32 } else { 0_u32 } - + (varint_size(max_input_count) - 1) * 4; + /// The feerate the transaction would have if we were to use this selection of inputs to achieve + /// 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) + } - self.opts.target_value.unwrap_or(0) as i64 - + (effective_base_weight as f32 * self.opts.target_feerate).ceil() as i64 + /// 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 selected_count(&self) -> usize { - self.selected.len() + 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 selected(&self) -> impl Iterator + '_ { - self.selected - .iter() - .map(move |&index| (index, &self.candidates[index])) + /// 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 unselected(&self) -> impl Iterator + '_ { - self.candidates - .iter() - .enumerate() - .filter(move |(index, _)| !self.selected.contains(index)) + // /// 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()) + } + + /// 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, + { + let order = self.candidate_order.to_mut(); + let candidates = &self.candidates; + 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, + K: Ord, + { + self.sort_candidates_by(|a, b| key_fn(a).cmp(&key_fn(b))) } - pub fn selected_indexes(&self) -> impl Iterator + '_ { - self.selected.iter().cloned() + /// 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())); } - pub fn unselected_indexes(&self) -> impl Iterator + '_ { - (0..self.candidates.len()).filter(move |index| !self.selected.contains(index)) + /// 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, + long_term_feerate: FeeRate, + drain: Drain, + excess_discount: f32, + ) -> f32 { + debug_assert!((0.0..=1.0).contains(&excess_discount)); + let mut waste = self.selected_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 + // 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(); + } + + waste } - pub fn all_selected(&self) -> bool { - self.selected.len() == self.candidates.len() + /// The selected candidates with their index. + pub fn selected(&self) -> impl ExactSizeIterator + '_ { + self.selected + .iter() + .map(move |&index| (index, self.candidates[index])) } - pub fn select_all(&mut self) { - self.selected = (0..self.candidates.len()).collect(); + /// 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_indices() + .map(move |i| (i, self.candidates[i])) } - pub fn select_until_finished(&mut self) -> Result { - let mut selection = self.finish(); + /// The indices of the selelcted candidates. + pub fn selected_indices(&self) -> &BTreeSet { + &self.selected + } - if selection.is_ok() { - return selection; - } + /// 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))) + .copied() + } - let unselected = self.unselected_indexes().collect::>(); + /// Whether there are any unselected candidates left. + pub fn is_exhausted(&self) -> bool { + self.unselected_indices().next().is_none() + } - for index in unselected { - self.select(index); - selection = self.finish(); + /// 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 + } - if selection.is_ok() { + /// Select all unselected candidates + pub fn select_all(&mut self) { + loop { + if !self.select_next() { break; } } + } - 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 - }; - - 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, - }, - ); + /// 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_indices().collect::>() { + if self.candidates[i].effective_value(feerate) > Ordf32(0.0) { + self.select(i); } } + } - // 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(), - }, - ); - } + /// Select candidates until `target` has been met assuming the `drain` output is attached. + /// + /// Returns an `Some(_)` if it was able to meet the target. + 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).unsigned_abs(), + }) + } - debug_assert!( - !excess_strategies.is_empty(), - "should have at least one excess strategy." - ); + /// Select candidates until some predicate has been satisfied. + #[must_use] + pub fn select_until( + &mut self, + mut predicate: impl FnMut(&CoinSelector<'a>) -> bool, + ) -> Option<()> { + loop { + if predicate(&*self) { + break Some(()); + } - Ok(Selection { - selected: self.selected.clone(), - excess: excess_without_drain, - excess_strategies, - }) + if !self.select_next() { + break None; + } + } } -} -#[derive(Clone, Debug)] -pub struct SelectionError { - selected: u64, - missing: u64, - constraint: SelectionConstraint, + /// 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, + ) -> impl Iterator, M::Score)>> { + crate::bnb::BnbIter::new(self.clone(), metric) + } } -impl core::fmt::Display for SelectionError { +impl<'a> core::fmt::Display for CoinSelector<'a> { 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 - ) + 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 { + write!(f, "☐")?; + } + + if candidates.peek().is_some() { + write!(f, ", ")?; + } + } + + write!(f, "]") } } -#[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, +/// 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 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"), +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 + + /// 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, } } -} -#[derive(Clone, Debug)] -pub struct Selection { - pub selected: BTreeSet, - pub excess: u64, - pub excess_strategies: HashMap, -} + /// 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())) + } -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)] -pub enum ExcessStrategyKind { - ToFee, - ToRecipient, - ToDrain, + /// Value per weight unit + pub fn value_pwu(&self) -> Ordf32 { + Ordf32(self.value as f32 / self.weight as f32) + } } -#[derive(Clone, Copy, Debug)] -pub struct ExcessStrategy { - pub recipient_value: Option, - pub drain_value: Option, - pub fee: u64, +/// 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, - pub waste: i64, + /// The value that should be assigned to the drain + pub value: u64, + /// The weight of spending this drain + pub spend_weight: u32, } -impl Selection { - pub fn apply_selection<'a, T>( - &'a self, - candidates: &'a [T], - ) -> impl Iterator + 'a { - self.selected.iter().map(move |i| &candidates[*i]) +impl Drain { + /// A drian representing no drain at all. + pub fn none() -> Self { + Self::default() } - /// 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") + /// is the "none" drain + pub fn is_none(&self) -> bool { + self == &Drain::none() } -} -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"), + /// Is not the "none" drain + pub fn is_some(&self) -> bool { + !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, } } -} -impl ExcessStrategy { - /// Returns feerate in sats/wu. - pub fn feerate(&self) -> f32 { - self.fee as f32 / self.weight as f32 + /// 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() } } -#[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, - }; +/// The `SelectIter` allows you to select candidates by calling `.next`. +pub struct SelectIter<'a> { + cs: CoinSelector<'a>, +} - for (index, v) in candidates.iter().enumerate() { - let mut selector = CoinSelector::new(&candidates, &opts); - assert!(selector.select(index)); +impl<'a> Iterator for SelectIter<'a> { + type Item = (CoinSelector<'a>, usize, Candidate); - 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); - } else { - let sel = res.expect("should have succeeded"); - assert_eq!(sel.excess, v.value - opts.target_value.unwrap_or(0)); - } - } + fn next(&mut self) -> Option { + let (index, wv) = self.cs.unselected().next()?; + self.cs.select(index); + Some((self.cs.clone(), index, wv)) } +} - #[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"); +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)) + } +} - assert!(selection.selected.len() > 1); - assert_eq!(selection.excess_strategies.len(), 1); +#[derive(Clone, Debug, Copy, PartialEq, Eq)] +pub struct InsufficientFunds { + missing: u64, +} - let (kind, strategy) = selection.best_strategy(); - assert_eq!(*kind, ExcessStrategyKind::ToDrain); - assert!(strategy.recipient_value.is_none()); - assert!(strategy.drain_value.is_some()); +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) } - - /// 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() {} } + +#[cfg(feature = "std")] +impl std::error::Error for InsufficientFunds {} diff --git a/nursery/coin_select/src/feerate.rs b/nursery/coin_select/src/feerate.rs new file mode 100644 index 0000000000..8919e2e4ec --- /dev/null +++ b/nursery/coin_select/src/feerate.rs @@ -0,0 +1,89 @@ +use crate::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/float.rs b/nursery/coin_select/src/float.rs new file mode 100644 index 0000000000..61b756fee0 --- /dev/null +++ b/nursery/coin_select/src/float.rs @@ -0,0 +1,96 @@ +//! 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: +//! +//! [`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 { + /// Adds the ceil method to `f32` + 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 dc38c676db..b38b3a985d 100644 --- a/nursery/coin_select/src/lib.rs +++ b/nursery/coin_select/src/lib.rs @@ -1,33 +1,57 @@ #![no_std] +// #![warn(missing_docs)] +#![doc = include_str!("../README.md")] +#![deny(unsafe_code)] -#[cfg(feature = "std")] -extern crate std; - +#[allow(unused_imports)] #[macro_use] extern crate alloc; -extern crate bdk_chain; -use alloc::vec::Vec; -use bdk_chain::{ - bitcoin, - collections::{BTreeSet, HashMap}, -}; -use bitcoin::{absolute, Transaction, TxOut}; -use core::fmt::{Debug, Display}; +#[cfg(feature = "std")] +#[macro_use] +extern crate std; mod coin_selector; +pub mod float; pub use coin_selector::*; mod bnb; pub use bnb::*; -/// Txin "base" fields include `outpoint` (32+4) and `nSequence` (4). This does not include -/// `scriptSigLen` or `scriptSig`. -pub const TXIN_BASE_WEIGHT: u32 = (32 + 4 + 4) * 4; +pub mod metrics; + +mod feerate; +pub use feerate::*; +pub mod change_policy; + +/// 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. +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. -// 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; + } + 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 0000000000..f53780ea84 --- /dev/null +++ b/nursery/coin_select/src/metrics.rs @@ -0,0 +1,66 @@ +//! 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::*; +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 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 +// 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(&mut self, cs: &CoinSelector<'_>) -> Option { + Some(($(self.$b.score(cs)?),*)) + } + #[allow(unused)] + fn bound(&mut self, cs: &CoinSelector<'_>) -> 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/changeless.rs b/nursery/coin_select/src/metrics/changeless.rs new file mode 100644 index 0000000000..5ea1010864 --- /dev/null +++ b/nursery/coin_select/src/metrics/changeless.rs @@ -0,0 +1,32 @@ +use super::change_lower_bound; +use crate::{bnb::BnBMetric, 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(&mut self, cs: &CoinSelector<'_>) -> 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(&mut self, cs: &CoinSelector<'_>) -> 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 new file mode 100644 index 0000000000..38366b6402 --- /dev/null +++ b/nursery/coin_select/src/metrics/waste.rs @@ -0,0 +1,236 @@ +use super::change_lower_bound; +use crate::{bnb::BnBMetric, float::Ordf32, Candidate, CoinSelector, Drain, FeeRate, Target}; + +/// 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, + 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(&mut self, cs: &CoinSelector<'_>) -> 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(&mut self, cs: &CoinSelector<'_>) -> 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); + 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().next_back().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; + // return: waste after excess reduction + cs.waste( + self.target, + self.long_term_feerate, + Drain::none(), + IGNORE_EXCESS, + ) + waste_to_extinguish_excess + } + 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 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); + + 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: 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 + 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/tests/bnb.rs b/nursery/coin_select/tests/bnb.rs new file mode 100644 index 0000000000..4d9124c716 --- /dev/null +++ b/nursery/coin_select/tests/bnb.rs @@ -0,0 +1,188 @@ +use bdk_coin_select::{BnBMetric, Candidate, CoinSelector, Drain, FeeRate, Target}; +#[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); + Candidate { + 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()) + .ok()?; + 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/changeless.rs b/nursery/coin_select/tests/changeless.rs new file mode 100644 index 0000000000..02d664b701 --- /dev/null +++ b/nursery/coin_select/tests/changeless.rs @@ -0,0 +1,119 @@ +#![allow(unused)] +use bdk_coin_select::{float::Ordf32, metrics, Candidate, CoinSelector, Drain, FeeRate, Target}; +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); + Candidate { + 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_indices().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.proptest-regressions b/nursery/coin_select/tests/waste.proptest-regressions new file mode 100644 index 0000000000..38fa1c15df --- /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 0000000000..a007c3dbc8 --- /dev/null +++ b/nursery/coin_select/tests/waste.rs @@ -0,0 +1,438 @@ +use bdk_coin_select::{ + change_policy, float::Ordf32, metrics::Waste, Candidate, CoinSelector, Drain, FeeRate, Target, +}; +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); + Candidate { + 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_indices().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 +}