From a86706d1a6d72e13657fabc8ea1c5f541ef91277 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Sun, 8 Nov 2020 15:46:27 +0100 Subject: [PATCH 1/6] [wallet] Use TXIN_DEFAULT_WEIGHT constant in coin selection Replace all the occurences of `serialize(&txin)` with TXIN_DEFAULT_WEIGHT. --- src/wallet/coin_selection.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index 4f927f6c1d..a77e21ad3b 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -42,7 +42,6 @@ //! ```no_run //! # use std::str::FromStr; //! # use bitcoin::*; -//! # use bitcoin::consensus::serialize; //! # use bdk::wallet::coin_selection::*; //! # use bdk::database::Database; //! # use bdk::*; @@ -70,7 +69,7 @@ //! }; //! //! **selected_amount += utxo.txout.value; -//! **additional_weight += serialize(&txin).len() * 4 + weight; +//! **additional_weight += TXIN_BASE_WEIGHT + weight; //! //! Some(( //! txin, @@ -106,7 +105,6 @@ //! # Ok::<(), bdk::Error>(()) //! ``` -use bitcoin::consensus::encode::serialize; use bitcoin::{Script, TxIn}; use crate::database::Database; @@ -209,7 +207,7 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { witness: vec![], }; - **fee_amount += calc_fee_bytes(serialize(&new_in).len() * 4 + weight); + **fee_amount += calc_fee_bytes(TXIN_BASE_WEIGHT + weight); **selected_amount += utxo.txout.value; log::debug!( @@ -238,6 +236,10 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { } } +// Base weight of a Txin, not counting the weight needed for satisfaying it. +// prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes) + script_len (1 bytes) +pub const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4; + #[cfg(test)] mod test { use std::str::FromStr; From 99060c5627227f04ab95185f57eb66e446cfeba6 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Sat, 31 Oct 2020 16:24:59 +0100 Subject: [PATCH 2/6] [wallet] Add Branch and Bound coin selection --- src/error.rs | 2 + src/wallet/coin_selection.rs | 288 +++++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) diff --git a/src/error.rs b/src/error.rs index d87d719b15..f4ecd65de1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -40,6 +40,8 @@ pub enum Error { NoUtxosSelected, OutputBelowDustLimit(usize), InsufficientFunds, + BnBTotalTriesExceeded, + BnBNoExactMatch, InvalidAddressNetwork(Address), UnknownUTXO, DifferentTransactions, diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index a77e21ad3b..86a83f105c 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -111,6 +111,8 @@ use crate::database::Database; use crate::error::Error; use crate::types::{FeeRate, UTXO}; +use rand::seq::SliceRandom; + /// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not /// overridden pub type DefaultCoinSelectionAlgorithm = LargestFirstCoinSelection; @@ -240,6 +242,292 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { // prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes) + script_len (1 bytes) pub const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4; +#[derive(Debug, Clone)] +// Adds fee information to an UTXO. +struct OutputGroup { + utxo: UTXO, + // weight needed to satisfy the UTXO, as described in `Descriptor::max_satisfaction_weight` + satisfaction_weight: usize, + // Amount of fees for spending a certain utxo, calculated using a certain FeeRate + fee: f32, + // The effective value of the UTXO, i.e., the utxo value minus the fee for spending it + effective_value: i64, +} + +impl OutputGroup { + fn new(utxo: UTXO, satisfaction_weight: usize, fee_rate: FeeRate) -> Self { + let fee = (TXIN_BASE_WEIGHT + satisfaction_weight) as f32 / 4.0 * fee_rate.as_sat_vb(); + let effective_value = utxo.txout.value as i64 - fee.ceil() as i64; + OutputGroup { + utxo, + satisfaction_weight, + effective_value, + fee, + } + } +} + +/// Branch and bound coin selection. Code adapted from Bitcoin Core's implementation and from Mark +/// Erhardt Master's Thesis (http://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf) +#[derive(Debug)] +pub struct BranchAndBoundCoinSelection { + size_of_change: u64, +} + +impl Default for BranchAndBoundCoinSelection { + fn default() -> Self { + Self { + // P2WPKH cost of change -> value (8 bytes) + script len (1 bytes) + script (22 bytes) + size_of_change: 8 + 1 + 22, + } + } +} + +impl BranchAndBoundCoinSelection { + pub fn new(size_of_change: u64) -> Self { + Self { size_of_change } + } +} + +const BNB_TOTAL_TRIES: usize = 100_000; + +impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { + fn coin_select( + &self, + _database: &D, + required_utxos: Vec<(UTXO, usize)>, + optional_utxos: Vec<(UTXO, usize)>, + fee_rate: FeeRate, + amount_needed: u64, + fee_amount: f32, + ) -> Result { + // Mapping every (UTXO, usize) to an output group + let required_utxos: Vec = required_utxos + .into_iter() + .map(|u| OutputGroup::new(u.0, u.1, fee_rate)) + .collect(); + + // Mapping every (UTXO, usize) to an output group. + // Filtering UTXOs with an effective_value < 0, as the fee paid for + // adding them is more than their value + let optional_utxos: Vec = optional_utxos + .into_iter() + .map(|u| OutputGroup::new(u.0, u.1, fee_rate)) + .filter(|u| u.effective_value > 0) + .collect(); + + let curr_value = required_utxos + .iter() + .fold(0, |acc, x| acc + x.effective_value as u64); + + let curr_available_value = optional_utxos + .iter() + .fold(0, |acc, x| acc + x.effective_value as u64); + + let actual_target = fee_amount.ceil() as u64 + amount_needed; + let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_vb(); + + if curr_available_value + curr_value < actual_target { + return Err(Error::InsufficientFunds); + } + + Ok(self + .bnb( + required_utxos.clone(), + optional_utxos.clone(), + curr_value, + curr_available_value, + actual_target, + fee_amount, + cost_of_change, + ) + .unwrap_or_else(|_| { + self.single_random_draw( + required_utxos, + optional_utxos, + curr_value, + actual_target, + fee_amount, + ) + })) + } +} + +impl BranchAndBoundCoinSelection { + // TODO: make this more Rust-onic :) + // (And perhpaps refactor with less arguments?) + #[allow(clippy::too_many_arguments)] + fn bnb( + &self, + required_utxos: Vec, + mut optional_utxos: Vec, + mut curr_value: u64, + mut curr_available_value: u64, + actual_target: u64, + fee_amount: f32, + cost_of_change: f32, + ) -> Result { + // current_selection[i] will contain true if we are using optional_utxos[i], + // false otherwise. Note that current_selection.len() could be less than + // optional_utxos.len(), it just means that we still haven't decided if we should keep + // certain optional_utxos or not. + let mut current_selection: Vec = Vec::with_capacity(optional_utxos.len()); + + // Sort the utxo_pool + optional_utxos.sort_unstable_by_key(|a| a.effective_value); + optional_utxos.reverse(); + + // Contains the best selection we found + let mut best_selection = Vec::new(); + let mut best_selection_value = None; + + // Depth First search loop for choosing the UTXOs + for _ in 0..BNB_TOTAL_TRIES { + // Conditions for starting a backtrack + let mut backtrack = false; + // Cannot possibly reach target with the amount remaining in the curr_available_value, + // or the selected value is out of range. + // Go back and try other branch + if curr_value + curr_available_value < actual_target + || curr_value > actual_target + cost_of_change as u64 + { + backtrack = true; + } else if curr_value >= actual_target { + // Selected value is within range, there's no point in going forward. Start + // backtracking + backtrack = true; + + // If we found a solution better than the previous one, or if there wasn't previous + // solution, update the best solution + if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() { + best_selection = current_selection.clone(); + best_selection_value = Some(curr_value); + } + + // If we found a perfect match, break here + if curr_value == actual_target { + break; + } + } + + // Backtracking, moving backwards + if backtrack { + // Walk backwards to find the last included UTXO that still needs to have its omission branch traversed. + while let Some(false) = current_selection.last() { + current_selection.pop(); + curr_available_value += + optional_utxos[current_selection.len()].effective_value as u64; + } + + if current_selection.last_mut().is_none() { + // We have walked back to the first utxo and no branch is untraversed. All solutions searched + // If best selection is empty, then there's no exact match + if best_selection.is_empty() { + return Err(Error::BnBNoExactMatch); + } + break; + } + + if let Some(c) = current_selection.last_mut() { + // Output was included on previous iterations, try excluding now. + *c = false; + } + + let utxo = &optional_utxos[current_selection.len() - 1]; + curr_value -= utxo.effective_value as u64; + } else { + // Moving forwards, continuing down this branch + let utxo = &optional_utxos[current_selection.len()]; + + // Remove this utxo from the curr_available_value utxo amount + curr_available_value -= utxo.effective_value as u64; + + // Inclusion branch first (Largest First Exploration) + current_selection.push(true); + curr_value += utxo.effective_value as u64; + } + } + + // Check for solution + if best_selection.is_empty() { + return Err(Error::BnBTotalTriesExceeded); + } + + // Set output set + let selected_utxos = optional_utxos + .into_iter() + .zip(best_selection) + .filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None }) + .collect(); + + Ok(BranchAndBoundCoinSelection::calculate_cs_result( + selected_utxos, + required_utxos, + fee_amount, + )) + } + + fn single_random_draw( + &self, + required_utxos: Vec, + mut optional_utxos: Vec, + curr_value: u64, + actual_target: u64, + fee_amount: f32, + ) -> CoinSelectionResult { + #[cfg(not(test))] + optional_utxos.shuffle(&mut thread_rng()); + #[cfg(test)] + { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + optional_utxos.shuffle(&mut rng); + } + + let selected_utxos = optional_utxos + .into_iter() + .scan(curr_value, |curr_value, utxo| { + if *curr_value >= actual_target { + None + } else { + *curr_value += utxo.effective_value as u64; + Some(utxo) + } + }) + .collect::>(); + + BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos, required_utxos, fee_amount) + } + + fn calculate_cs_result( + selected_utxos: Vec, + required_utxos: Vec, + fee_amount: f32, + ) -> CoinSelectionResult { + let (txin, fee_amount, selected_amount) = + selected_utxos.into_iter().chain(required_utxos).fold( + (vec![], fee_amount, 0), + |(mut txin, mut fee_amount, mut selected_amount), output_group| { + selected_amount += output_group.utxo.txout.value; + fee_amount += output_group.fee; + txin.push(( + TxIn { + previous_output: output_group.utxo.outpoint, + ..Default::default() + }, + output_group.utxo.txout.script_pubkey, + )); + (txin, fee_amount, selected_amount) + }, + ); + CoinSelectionResult { + txin, + fee_amount, + selected_amount, + } + } +} + #[cfg(test)] mod test { use std::str::FromStr; From be91997d84fafee8a3f91016e8b659c91fc806cf Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Sat, 31 Oct 2020 16:27:33 +0100 Subject: [PATCH 3/6] [wallet] Add tests for BranchAndBoundCoinSelection::coin_select --- src/wallet/coin_selection.rs | 185 +++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index 86a83f105c..8f3b924131 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -112,6 +112,10 @@ use crate::error::Error; use crate::types::{FeeRate, UTXO}; use rand::seq::SliceRandom; +#[cfg(not(test))] +use rand::thread_rng; +#[cfg(test)] +use rand::{rngs::StdRng, SeedableRng}; /// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not /// overridden @@ -532,12 +536,17 @@ impl BranchAndBoundCoinSelection { mod test { use std::str::FromStr; + use bitcoin::consensus::encode::serialize; use bitcoin::{OutPoint, Script, TxOut}; use super::*; use crate::database::MemoryDatabase; use crate::types::*; + use rand::rngs::StdRng; + use rand::seq::SliceRandom; + use rand::{Rng, SeedableRng}; + const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2; fn get_test_utxos() -> Vec<(UTXO, usize)> { @@ -573,6 +582,53 @@ mod test { ] } + fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<(UTXO, usize)> { + let mut res = Vec::new(); + for _ in 0..utxos_number { + res.push(( + UTXO { + outpoint: OutPoint::from_str( + "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", + ) + .unwrap(), + txout: TxOut { + value: rng.gen_range(0, 200000000), + script_pubkey: Script::new(), + }, + is_internal: false, + }, + P2WPKH_WITNESS_SIZE, + )); + } + res + } + + fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<(UTXO, usize)> { + let utxo = ( + UTXO { + outpoint: OutPoint::from_str( + "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", + ) + .unwrap(), + txout: TxOut { + value: utxos_value, + script_pubkey: Script::new(), + }, + is_internal: false, + }, + P2WPKH_WITNESS_SIZE, + ); + vec![utxo; utxos_number] + } + + fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<(UTXO, usize)>) -> u64 { + let utxos_picked_len = rng.gen_range(2, utxos.len() / 2); + utxos.shuffle(&mut rng); + utxos[..utxos_picked_len] + .iter() + .fold(0, |acc, x| acc + x.0.txout.value) + } + #[test] fn test_largest_first_coin_selection_success() { let utxos = get_test_utxos(); @@ -671,4 +727,133 @@ mod test { ) .unwrap(); } + + #[test] + fn test_bnb_coin_selection_success() { + // In this case bnb won't find a suitable match and single random draw will + // select three outputs + let utxos = generate_same_value_utxos(100_000, 20); + + let database = MemoryDatabase::default(); + + let result = BranchAndBoundCoinSelection::default() + .coin_select( + &database, + vec![], + utxos, + FeeRate::from_sat_per_vb(1.0), + 250_000, + 50.0, + ) + .unwrap(); + + assert_eq!(result.txin.len(), 3); + assert_eq!(result.selected_amount, 300_000); + assert_eq!(result.fee_amount, 254.0); + } + + #[test] + fn test_bnb_coin_selection_required_are_enough() { + let utxos = get_test_utxos(); + let database = MemoryDatabase::default(); + + let result = BranchAndBoundCoinSelection::default() + .coin_select( + &database, + utxos.clone(), + utxos, + FeeRate::from_sat_per_vb(1.0), + 20_000, + 50.0, + ) + .unwrap(); + + assert_eq!(result.txin.len(), 2); + assert_eq!(result.selected_amount, 300_000); + assert_eq!(result.fee_amount, 186.0); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_bnb_coin_selection_insufficient_funds() { + let utxos = get_test_utxos(); + let database = MemoryDatabase::default(); + + BranchAndBoundCoinSelection::default() + .coin_select( + &database, + vec![], + utxos, + FeeRate::from_sat_per_vb(1.0), + 500_000, + 50.0, + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_bnb_coin_selection_insufficient_funds_high_fees() { + let utxos = get_test_utxos(); + let database = MemoryDatabase::default(); + + BranchAndBoundCoinSelection::default() + .coin_select( + &database, + vec![], + utxos, + FeeRate::from_sat_per_vb(1000.0), + 250_000, + 50.0, + ) + .unwrap(); + } + + #[test] + fn test_bnb_coin_selection_check_fee_rate() { + let utxos = get_test_utxos(); + let database = MemoryDatabase::default(); + + let result = BranchAndBoundCoinSelection::new(0) + .coin_select( + &database, + vec![], + utxos.clone(), + FeeRate::from_sat_per_vb(1.0), + 99932, // first utxo's effective value + 0.0, + ) + .unwrap(); + + assert_eq!(result.txin.len(), 1); + assert_eq!(result.selected_amount, 100_000); + let result_size = + serialize(result.txin.first().unwrap()).len() as f32 + P2WPKH_WITNESS_SIZE as f32 / 4.0; + let epsilon = 0.5; + assert!((1.0 - (result.fee_amount / result_size)).abs() < epsilon); + } + + #[test] + fn test_bnb_coin_selection_exact_match() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + let database = MemoryDatabase::default(); + + for _i in 0..200 { + let mut optional_utxos = generate_random_utxos(&mut rng, 16); + let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos); + let result = BranchAndBoundCoinSelection::new(0) + .coin_select( + &database, + vec![], + optional_utxos, + FeeRate::from_sat_per_vb(0.0), + target_amount, + 0.0, + ) + .unwrap(); + assert_eq!(result.selected_amount, target_amount); + } + } + } From 23824321ba4e936e3f96471b589577fbe74e38c8 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Sat, 31 Oct 2020 16:28:12 +0100 Subject: [PATCH 4/6] [wallet] Add tests for BranchAndBoundCoinSelection::bnb --- src/wallet/coin_selection.rs | 131 +++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index 8f3b924131..45bbe300db 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -856,4 +856,135 @@ mod test { } } + #[test] + #[should_panic(expected = "BnBNoExactMatch")] + fn test_bnb_function_no_exact_match() { + let fee_rate = FeeRate::from_sat_per_vb(10.0); + let utxos: Vec = get_test_utxos() + .into_iter() + .map(|u| OutputGroup::new(u.0, u.1, fee_rate)) + .collect(); + + let curr_available_value = utxos + .iter() + .fold(0, |acc, x| acc + x.effective_value as u64); + + let size_of_change = 31; + let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb(); + BranchAndBoundCoinSelection::new(size_of_change) + .bnb( + vec![], + utxos, + 0, + curr_available_value, + 20_000, + 50.0, + cost_of_change, + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "BnBTotalTriesExceeded")] + fn test_bnb_function_tries_exceeded() { + let fee_rate = FeeRate::from_sat_per_vb(10.0); + let utxos: Vec = generate_same_value_utxos(100_000, 100_000) + .into_iter() + .map(|u| OutputGroup::new(u.0, u.1, fee_rate)) + .collect(); + + let curr_available_value = utxos + .iter() + .fold(0, |acc, x| acc + x.effective_value as u64); + + let size_of_change = 31; + let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb(); + + BranchAndBoundCoinSelection::new(size_of_change) + .bnb( + vec![], + utxos, + 0, + curr_available_value, + 20_000, + 50.0, + cost_of_change, + ) + .unwrap(); + } + + // The match won't be exact but still in the range + #[test] + fn test_bnb_function_almost_exact_match_with_fees() { + let fee_rate = FeeRate::from_sat_per_vb(1.0); + let size_of_change = 31; + let cost_of_change = size_of_change as f32 * fee_rate.as_sat_vb(); + let fee_amount = 50.0; + + let utxos: Vec<_> = generate_same_value_utxos(50_000, 10) + .into_iter() + .map(|u| OutputGroup::new(u.0, u.1, fee_rate)) + .collect(); + + let curr_value = 0; + + let curr_available_value = utxos + .iter() + .fold(0, |acc, x| acc + x.effective_value as u64); + + // 2*(value of 1 utxo) - 2*(1 utxo fees with 1.0sat/vbyte fee rate) - + // cost_of_change + 5. + let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as u64 + 5; + + let result = BranchAndBoundCoinSelection::new(size_of_change) + .bnb( + vec![], + utxos, + curr_value, + curr_available_value, + target_amount, + fee_amount, + cost_of_change, + ) + .unwrap(); + assert_eq!(result.fee_amount, 186.0); + assert_eq!(result.selected_amount, 100_000); + } + + // TODO: bnb() function should be optimized, and this test should be done with more utxos + #[test] + fn test_bnb_function_exact_match_more_utxos() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + let fee_rate = FeeRate::from_sat_per_vb(0.0); + + for _ in 0..200 { + let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40) + .into_iter() + .map(|u| OutputGroup::new(u.0, u.1, fee_rate)) + .collect(); + + let curr_value = 0; + + let curr_available_value = optional_utxos + .iter() + .fold(0, |acc, x| acc + x.effective_value as u64); + + let target_amount = optional_utxos[3].effective_value as u64 + + optional_utxos[23].effective_value as u64; + + let result = BranchAndBoundCoinSelection::new(0) + .bnb( + vec![], + optional_utxos, + curr_value, + curr_available_value, + target_amount, + 0.0, + 0.0, + ) + .unwrap(); + assert_eq!(result.selected_amount, target_amount); + } + } } From c43f201e35effca16c2f0d243cb119405c644f60 Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Sat, 31 Oct 2020 16:28:21 +0100 Subject: [PATCH 5/6] [wallet] Add tests for BranchAndBoundCoinSelection::single_random_draw --- src/wallet/coin_selection.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index 45bbe300db..54fe6875a6 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -987,4 +987,29 @@ mod test { assert_eq!(result.selected_amount, target_amount); } } + + #[test] + fn test_single_random_draw_function_success() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + let mut utxos = generate_random_utxos(&mut rng, 300); + let target_amount = sum_random_utxos(&mut rng, &mut utxos); + + let fee_rate = FeeRate::from_sat_per_vb(1.0); + let utxos: Vec = utxos + .into_iter() + .map(|u| OutputGroup::new(u.0, u.1, fee_rate)) + .collect(); + + let result = BranchAndBoundCoinSelection::default().single_random_draw( + vec![], + utxos, + 0, + target_amount, + 50.0, + ); + + assert!(result.selected_amount > target_amount); + assert_eq!(result.fee_amount, 50.0 + result.txin.len() as f32 * 68.0); + } } From 9f31ad1bc8acdb90385e4b8f29423242da3766ef Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Sat, 31 Oct 2020 16:28:30 +0100 Subject: [PATCH 6/6] [wallet] Replace `must_use` with `required` in coin selection --- src/wallet/coin_selection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index 54fe6875a6..c52624826c 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -204,8 +204,8 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { let txin = utxos .scan( (&mut selected_amount, &mut fee_amount), - |(selected_amount, fee_amount), (must_use, (utxo, weight))| { - if must_use || **selected_amount < amount_needed + (fee_amount.ceil() as u64) { + |(selected_amount, fee_amount), (required, (utxo, weight))| { + if required || **selected_amount < amount_needed + (fee_amount.ceil() as u64) { let new_in = TxIn { previous_output: utxo.outpoint, script_sig: Script::default(),