From 20ad2a7e342ae7bbc4f692777699f702bb028a5f Mon Sep 17 00:00:00 2001 From: Cesar Alvarez Vallero <46329881+csralvall@users.noreply.github.com> Date: Sun, 11 Sep 2022 17:35:11 +0200 Subject: [PATCH] Add `process_and_select_coins` function There were some common tasks identified in the construction of a coin selection result that the different algorithms performed separately. Those tasks were blended inside the algorithms or separated into functions, but the underlying actions were the same: - Do some preprocessing to the inputs. - Select utxos based on the algorithm criterium. - Decide change. - Build the coin selection result. This commit intends to create a single function that consumes different algorithms and perform all the mentioned tasks as a pipeline taking utxos as input and producing coin selection results at the end. The new function reduces the work needed to implement other coin selection algorithms, modularizes the code not related directly to the selection process and reduces the number of lines to review and understand its logic. --- src/types.rs | 14 +- src/wallet/coin_selection.rs | 1215 ++++++++++++++++------------------ src/wallet/mod.rs | 50 +- 3 files changed, 623 insertions(+), 656 deletions(-) diff --git a/src/types.rs b/src/types.rs index bae86477f..c97921dc5 100644 --- a/src/types.rs +++ b/src/types.rs @@ -13,7 +13,7 @@ use std::convert::AsRef; use std::ops::Sub; use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut}; -use bitcoin::{hash_types::Txid, util::psbt}; +use bitcoin::{hash_types::Txid, util::psbt, Script}; use serde::{Deserialize, Serialize}; @@ -177,6 +177,18 @@ pub struct WeightedUtxo { pub utxo: Utxo, } +/// A wallet owned script_pubkey with the weight of the `witness` + `scriptSig` in [weight units] +/// to spend from it. +/// Useful to optimize reducing the waste metric in coin selection algorithms. +/// [weight units]: +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WeightedScriptPubkey { + /// The weight of the witness data and `scriptSig` expressed in [weight units]. + pub satisfaction_weight: usize, + /// The script_pubkey + pub script_pubkey: Script, +} + #[derive(Debug, Clone, PartialEq)] /// An unspent transaction output (UTXO). pub enum Utxo { diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index 702ba1855..e5b60881e 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -29,7 +29,6 @@ //! # use bdk::wallet::{self, coin_selection::*}; //! # use bdk::database::Database; //! # use bdk::*; -//! # use bdk::wallet::coin_selection::decide_change; //! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4; //! #[derive(Debug)] //! struct AlwaysSpendEverything; @@ -38,44 +37,12 @@ //! fn coin_select( //! &self, //! database: &D, -//! required_utxos: Vec, -//! optional_utxos: Vec, +//! optional_utxos: Vec, //! fee_rate: FeeRate, //! target_amount: u64, -//! drain_script: &Script, -//! ) -> Result { -//! let mut selected_amount = 0; -//! let mut additional_weight = 0; -//! let all_utxos_selected = required_utxos -//! .into_iter() -//! .chain(optional_utxos) -//! .scan( -//! (&mut selected_amount, &mut additional_weight), -//! |(selected_amount, additional_weight), weighted_utxo| { -//! **selected_amount += weighted_utxo.utxo.txout().value; -//! **additional_weight += TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight; -//! Some(weighted_utxo.utxo) -//! }, -//! ) -//! .collect::>(); -//! let additional_fees = fee_rate.fee_wu(additional_weight); -//! let amount_needed_with_fees = additional_fees + target_amount; -//! if selected_amount < amount_needed_with_fees { -//! return Err(bdk::Error::InsufficientFunds { -//! needed: amount_needed_with_fees, -//! available: selected_amount, -//! }); -//! } -//! -//! let remaining_amount = selected_amount - amount_needed_with_fees; -//! -//! let excess = decide_change(remaining_amount, fee_rate, drain_script); -//! -//! Ok(CoinSelectionResult { -//! selected: all_utxos_selected, -//! fee_amount: additional_fees, -//! excess, -//! }) +//! available_value: i64, +//! ) -> Result, bdk::Error> { +//! Ok(optional_utxos) //! } //! } //! @@ -94,9 +61,9 @@ //! # Ok::<(), bdk::Error>(()) //! ``` -use crate::types::FeeRate; +use crate::database::Database; +use crate::types::{FeeRate, WeightedScriptPubkey, WeightedUtxo}; use crate::wallet::utils::IsDust; -use crate::{database::Database, WeightedUtxo}; use crate::{error::Error, Utxo}; use bitcoin::consensus::encode::serialize; @@ -171,6 +138,171 @@ impl CoinSelectionResult { } } +/// Perform the coin selection +/// +/// - `algorithm`: the algorithm to use to select the UTXOs for the transaction +/// - `database`: database reference to use by those algorithms that require extra information +/// about UTXOs +/// - `required_utxos`: the utxos that must be spent regardless of `target_amount` with their +/// weight cost +/// - `optional_utxos`: the remaining available utxos to satisfy `target_amount` with their +/// weight cost +/// - `fee_rate`: fee rate to use +/// - `target_amount`: the outgoing amount in satoshis and the fees already +/// accumulated from added outputs and transaction’s header. +/// - `weighted_drain_script`: the drain script used to create the change output associated with +/// the max satisfaction weight needed to spend it +pub fn process_and_select_coins>( + algorithm: Cs, + database: &D, + required_utxos: Vec, + optional_utxos: Vec, + fee_rate: FeeRate, + target_amount: u64, + weighted_drain_script: &WeightedScriptPubkey, +) -> Result { + // #################################################################### + // ######################### PREPROCESSING ############################ + // #################################################################### + + // Mapping every (UTXO, usize) to an output group + let mut required_output_groups: Vec = required_utxos + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .collect(); + + // Mapping every (UTXO, usize) to an output group. + let optional_output_groups: Vec = optional_utxos + .into_iter() + .map(|u| OutputGroup::new(u, fee_rate)) + .filter(|u| u.effective_value.is_positive()) + .collect(); + + let (required_effective_value, required_fees) = required_output_groups + .iter() + .fold((0, 0), |(eff_value, fees), x| { + (eff_value + x.effective_value, fees + x.fee) + }); + + let (optional_effective_value, optional_fees) = optional_output_groups + .iter() + .fold((0, 0), |(eff_value, fees), x| { + (eff_value + x.effective_value, fees + x.fee) + }); + + // `required_effective_value` and `optional_effective_value` are both the sum of *effective_values* of + // the UTXOs. For the optional UTXOs (optional_effective_value) we filter out UTXOs with + // negative effective value, so it will always be positive. + // + // Since we are required to spend the required UTXOs (required_effective_value) we have to consider + // all their effective values, even when negative, which means that required_effective_value could + // be negative as well. + // + // If the sum of required_effective_value and optional_effective_value is negative or lower than our target, + // we can immediately exit with an error, as it's guaranteed we will never find a solution + // if we actually run the coin selection algorithm + let total_effective_value = optional_effective_value + required_effective_value; + let total_value: Result = total_effective_value.try_into(); + match total_value { + Ok(v) if v >= target_amount => {} + _ => { + // Assume we spend all the UTXOs we can (all the required + all the optional with + // positive effective value), sum their value and their fee cost. + let utxo_fees = optional_fees + required_fees; + let utxo_value = total_effective_value + utxo_fees as i64; + + // Add to the target the fee cost of the UTXOs + return Err(Error::InsufficientFunds { + needed: target_amount + utxo_fees, + available: utxo_value as u64, + }); + } + } + + let target_amount_i64 = target_amount + .try_into() + .expect("Bitcoin amount to fit into i64"); + + // #################################################################### + // ######################### COIN SELECTION ########################### + // #################################################################### + + let (output_groups, effective_value, fee_amount) = + if required_effective_value > target_amount_i64 { + ( + required_output_groups, + required_effective_value, + required_fees, + ) + } else if total_effective_value == target_amount_i64 { + let mut selected_output_groups = optional_output_groups; + selected_output_groups.append(&mut required_output_groups); + ( + selected_output_groups, + total_effective_value, + optional_fees + required_fees, + ) + } else { + // from now on, target_amount can only be positive + let target_amount = (target_amount_i64 - required_effective_value) as u64; + let mut selected_output_groups = algorithm.coin_select( + database, + optional_output_groups, + fee_rate, + target_amount, + optional_effective_value, + )?; + let selected_effective_value: i64 = selected_output_groups + .iter() + .map(|og| og.effective_value) + .sum(); + let selected_fees: u64 = selected_output_groups.iter().map(|og| og.fee).sum(); + selected_output_groups.append(&mut required_output_groups); + ( + selected_output_groups, + selected_effective_value + required_effective_value, + selected_fees + required_fees, + ) + }; + + // #################################################################### + // ############################ GET EXCESS ############################ + // #################################################################### + + // if coin_select finish, it means it found a valid coin selection. The effective value of that + // coin selection should be greater than target_amount (an u64) so it's safe to apply the + // conversion from i64 to u64 + let effective_value = effective_value as u64; + + // remaining_amount = selected_amount - (target_amount + selection_fees) + let remaining_amount = effective_value.saturating_sub(target_amount); + + let excess = decide_change( + remaining_amount, + fee_rate, + &weighted_drain_script.script_pubkey, + ); + + // #################################################################### + // ############################ GET UTXOS ############################# + // #################################################################### + + let selected = output_groups + .into_iter() + .map(|x| x.weighted_utxo.utxo) + .collect::>(); + + // #################################################################### + // ##################### BUILD COIN SELECTION RESULT ################## + // #################################################################### + + Ok(CoinSelectionResult { + selected, + fee_amount, + excess, + }) +} + /// Trait for generalized coin selection algorithms /// /// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin @@ -182,24 +314,20 @@ pub trait CoinSelectionAlgorithm: std::fmt::Debug { /// /// - `database`: a reference to the wallet's database that can be used to lookup additional /// details for a specific UTXO - /// - `required_utxos`: the utxos that must be spent regardless of `target_amount` with their - /// weight cost - /// - `optional_utxos`: the remaining available utxos to satisfy `target_amount` with their + /// - `optional_utxos`: the utxos available for selection to satisfy `target_amount` with their /// weight cost /// - `fee_rate`: fee rate to use /// - `target_amount`: the outgoing amount in satoshis and the fees already /// accumulated from added outputs and transaction’s header. - /// - `drain_script`: the script to use in case of change - #[allow(clippy::too_many_arguments)] + /// - `available_value`: the total effective value of all the optional utxos fn coin_select( &self, database: &D, - required_utxos: Vec, - optional_utxos: Vec, + optional_utxos: Vec, fee_rate: FeeRate, target_amount: u64, - drain_script: &Script, - ) -> Result; + available_value: i64, + ) -> Result, Error>; } /// Simple and dumb coin selection @@ -213,29 +341,21 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { fn coin_select( &self, _database: &D, - required_utxos: Vec, - mut optional_utxos: Vec, - fee_rate: FeeRate, + mut optional_utxos: Vec, + _fee_rate: FeeRate, target_amount: u64, - drain_script: &Script, - ) -> Result { - log::debug!( - "target_amount = `{}`, fee_rate = `{:?}`", - target_amount, - fee_rate - ); + _available_value: i64, + ) -> Result, Error> { + log::debug!("target_amount = `{}`", target_amount,); - // We put the "required UTXOs" first and make sure the optional UTXOs are sorted, - // initially smallest to largest, before being reversed with `.rev()`. + // We make sure the optional UTXOs are sorted, initially smallest to largest, before being + // reversed with `.rev()`. let utxos = { - optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value); - required_utxos - .into_iter() - .map(|utxo| (true, utxo)) - .chain(optional_utxos.into_iter().rev().map(|utxo| (false, utxo))) + optional_utxos.sort_unstable_by_key(|og| og.effective_value); + optional_utxos.into_iter().rev() }; - select_sorted_utxos(utxos, fee_rate, target_amount, drain_script) + Ok(select_sorted_utxos(utxos, target_amount)) } } @@ -250,16 +370,15 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection { fn coin_select( &self, database: &D, - required_utxos: Vec, - mut optional_utxos: Vec, - fee_rate: FeeRate, + mut optional_utxos: Vec, + _fee_rate: FeeRate, target_amount: u64, - drain_script: &Script, - ) -> Result { + _available_value: i64, + ) -> Result, Error> { // query db and create a blockheight lookup table let blockheights = optional_utxos .iter() - .map(|wu| wu.utxo.outpoint().txid) + .map(|og| og.weighted_utxo.utxo.outpoint().txid) // fold is used so we can skip db query for txid that already exist in hashmap acc .fold(Ok(HashMap::new()), |bh_result_acc, txid| { bh_result_acc.and_then(|mut bh_acc| { @@ -277,24 +396,20 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection { }) })?; - // We put the "required UTXOs" first and make sure the optional UTXOs are sorted from - // oldest to newest according to blocktime + // We make sure the optional UTXOs are sorted from oldest to newest according to blocktime // For utxo that doesn't exist in DB, they will have lowest priority to be selected let utxos = { - optional_utxos.sort_unstable_by_key(|wu| { - match blockheights.get(&wu.utxo.outpoint().txid) { + optional_utxos.sort_unstable_by_key(|og| { + match blockheights.get(&og.weighted_utxo.utxo.outpoint().txid) { Some(Some(blockheight)) => blockheight, _ => &u32::MAX, } }); - required_utxos - .into_iter() - .map(|utxo| (true, utxo)) - .chain(optional_utxos.into_iter().map(|utxo| (false, utxo))) + optional_utxos.into_iter() }; - select_sorted_utxos(utxos, fee_rate, target_amount, drain_script) + Ok(select_sorted_utxos(utxos, target_amount)) } } @@ -325,63 +440,36 @@ pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Sc } fn select_sorted_utxos( - utxos: impl Iterator, - fee_rate: FeeRate, + utxos: impl Iterator, target_amount: u64, - drain_script: &Script, -) -> Result { - let mut selected_amount = 0; - let mut fee_amount = 0; - let selected = utxos - .scan( - (&mut selected_amount, &mut fee_amount), - |(selected_amount, fee_amount), (must_use, weighted_utxo)| { - if must_use || **selected_amount < target_amount + **fee_amount { - **fee_amount += - fee_rate.fee_wu(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight); - **selected_amount += weighted_utxo.utxo.txout().value; - - log::debug!( - "Selected {}, updated fee_amount = `{}`", - weighted_utxo.utxo.outpoint(), - fee_amount - ); - - Some(weighted_utxo.utxo) - } else { - None - } - }, - ) - .collect::>(); - - let amount_needed_with_fees = target_amount + fee_amount; - if selected_amount < amount_needed_with_fees { - return Err(Error::InsufficientFunds { - needed: amount_needed_with_fees, - available: selected_amount, - }); - } - - let remaining_amount = selected_amount - amount_needed_with_fees; +) -> Vec { + let mut selected_amount = 0_u64; + utxos + .scan(&mut selected_amount, |selected_amount, output_group| { + if **selected_amount < target_amount { + // all outputs have a positive effective value, so the conversion it's safe + **selected_amount += output_group.effective_value as u64; - let excess = decide_change(remaining_amount, fee_rate, drain_script); + log::debug!("Selected {}", output_group.weighted_utxo.utxo.outpoint(),); - Ok(CoinSelectionResult { - selected, - fee_amount, - excess, - }) + Some(output_group) + } else { + None + } + }) + .collect::>() } #[derive(Debug, Clone)] -// Adds fee information to an UTXO. -struct OutputGroup { - weighted_utxo: WeightedUtxo, - // Amount of fees for spending a certain utxo, calculated using a certain FeeRate - fee: u64, - // The effective value of the UTXO, i.e., the utxo value minus the fee for spending it - effective_value: i64, +/// A [`WeightedUtxo`] with its associated fee and the real value after discounting that fee +/// from the carried value +pub struct OutputGroup { + /// The weighted utxo + pub weighted_utxo: WeightedUtxo, + /// Amount of fees for spending a certain utxo, calculated using a certain FeeRate + pub fee: u64, + /// The effective value of the UTXO, i.e., the utxo value minus the fee for spending it + pub effective_value: i64, } impl OutputGroup { @@ -426,129 +514,40 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { fn coin_select( &self, _database: &D, - required_utxos: Vec, - optional_utxos: Vec, + optional_utxos: Vec, fee_rate: FeeRate, target_amount: u64, - drain_script: &Script, - ) -> Result { - // Mapping every (UTXO, usize) to an output group - let required_utxos: Vec = required_utxos - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) - .collect(); - - // Mapping every (UTXO, usize) to an output group, filtering UTXOs with a negative - // effective value - let optional_utxos: Vec = optional_utxos - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) - .filter(|u| u.effective_value.is_positive()) - .collect(); - - let curr_value = required_utxos - .iter() - .fold(0, |acc, x| acc + x.effective_value); - - let curr_available_value = optional_utxos - .iter() - .fold(0, |acc, x| acc + x.effective_value); - - let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_per_vb(); - - // `curr_value` and `curr_available_value` are both the sum of *effective_values* of - // the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with - // negative effective value, so it will always be positive. - // - // Since we are required to spend the required UTXOs (curr_value) we have to consider - // all their effective values, even when negative, which means that curr_value could - // be negative as well. - // - // If the sum of curr_value and curr_available_value is negative or lower than our target, - // we can immediately exit with an error, as it's guaranteed we will never find a solution - // if we actually run the BnB. - let total_value: Result = (curr_available_value + curr_value).try_into(); - match total_value { - Ok(v) if v >= target_amount => {} - _ => { - // Assume we spend all the UTXOs we can (all the required + all the optional with - // positive effective value), sum their value and their fee cost. - let (utxo_fees, utxo_value) = required_utxos - .iter() - .chain(optional_utxos.iter()) - .fold((0, 0), |(mut fees, mut value), utxo| { - fees += utxo.fee; - value += utxo.weighted_utxo.utxo.txout().value; - - (fees, value) - }); - - // Add to the target the fee cost of the UTXOs - return Err(Error::InsufficientFunds { - needed: target_amount + utxo_fees, - available: utxo_value, - }); - } - } - + available_value: i64, + ) -> Result, Error> { + // convert target amount on i64 to use in comparisons and assignments let target_amount = target_amount .try_into() .expect("Bitcoin amount to fit into i64"); - if curr_value > target_amount { - // remaining_amount can't be negative as that would mean the - // selection wasn't successful - // target_amount = amount_needed + (fee_amount - vin_fees) - let remaining_amount = (curr_value - target_amount) as u64; - - let excess = decide_change(remaining_amount, fee_rate, drain_script); - - return Ok(BranchAndBoundCoinSelection::calculate_cs_result( - vec![], - required_utxos, - excess, - )); - } + let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_per_vb(); Ok(self .bnb( - required_utxos.clone(), optional_utxos.clone(), - curr_value, - curr_available_value, target_amount, cost_of_change, - drain_script, - fee_rate, + available_value, ) - .unwrap_or_else(|_| { - self.single_random_draw( - required_utxos, - optional_utxos, - curr_value, - target_amount, - drain_script, - fee_rate, - ) - })) + .unwrap_or_else(|_| self.single_random_draw(optional_utxos, target_amount))) } } impl BranchAndBoundCoinSelection { - // TODO: make this more Rust-onic :) - // (And perhaps refactor with less arguments?) - #[allow(clippy::too_many_arguments)] fn bnb( &self, - required_utxos: Vec, mut optional_utxos: Vec, - mut curr_value: i64, - mut curr_available_value: i64, target_amount: i64, cost_of_change: f32, - drain_script: &Script, - fee_rate: FeeRate, - ) -> Result { + mut available_value: i64, + ) -> Result, Error> { + // the value of the current selection + let mut selected_value = 0; + // 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 @@ -567,27 +566,28 @@ impl BranchAndBoundCoinSelection { 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, + // Cannot possibly reach target with the amount remaining in the available_value, // or the selected value is out of range. // Go back and try other branch - if curr_value + curr_available_value < target_amount - || curr_value > target_amount + cost_of_change as i64 + if selected_value + available_value < target_amount + || selected_value > target_amount + cost_of_change as i64 { backtrack = true; - } else if curr_value >= target_amount { + } else if selected_value >= target_amount { // 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() { + if best_selection_value.is_none() || selected_value < best_selection_value.unwrap() + { best_selection = current_selection.clone(); - best_selection_value = Some(curr_value); + best_selection_value = Some(selected_value); } // If we found a perfect match, break here - if curr_value == target_amount { + if selected_value == target_amount { break; } } @@ -597,7 +597,7 @@ impl BranchAndBoundCoinSelection { // 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; + available_value += optional_utxos[current_selection.len()].effective_value; } if current_selection.last_mut().is_none() { @@ -615,17 +615,17 @@ impl BranchAndBoundCoinSelection { } let utxo = &optional_utxos[current_selection.len() - 1]; - curr_value -= utxo.effective_value; + selected_value -= utxo.effective_value; } 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; + // Remove this utxo from the available_value utxo amount + available_value -= utxo.effective_value; // Inclusion branch first (Largest First Exploration) current_selection.push(true); - curr_value += utxo.effective_value; + selected_value += utxo.effective_value; } } @@ -641,32 +641,14 @@ impl BranchAndBoundCoinSelection { .filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None }) .collect::>(); - let selected_amount = best_selection_value.unwrap(); - - // remaining_amount can't be negative as that would mean the - // selection wasn't successful - // target_amount = amount_needed + (fee_amount - vin_fees) - let remaining_amount = (selected_amount - target_amount) as u64; - - let excess = decide_change(remaining_amount, fee_rate, drain_script); - - Ok(BranchAndBoundCoinSelection::calculate_cs_result( - selected_utxos, - required_utxos, - excess, - )) + Ok(selected_utxos) } - #[allow(clippy::too_many_arguments)] fn single_random_draw( &self, - required_utxos: Vec, mut optional_utxos: Vec, - curr_value: i64, target_amount: i64, - drain_script: &Script, - fee_rate: FeeRate, - ) -> CoinSelectionResult { + ) -> Vec { #[cfg(not(test))] optional_utxos.shuffle(&mut thread_rng()); #[cfg(test)] @@ -676,46 +658,17 @@ impl BranchAndBoundCoinSelection { optional_utxos.shuffle(&mut rng); } - let selected_utxos = optional_utxos.into_iter().fold( - (curr_value, vec![]), - |(mut amount, mut utxos), utxo| { - if amount >= target_amount { - (amount, utxos) + optional_utxos + .into_iter() + .scan(0, |acc_value, utxo| { + if *acc_value >= target_amount { + None } else { - amount += utxo.effective_value; - utxos.push(utxo); - (amount, utxos) + *acc_value += utxo.effective_value; + Some(utxo) } - }, - ); - - // remaining_amount can't be negative as that would mean the - // selection wasn't successful - // target_amount = amount_needed + (fee_amount - vin_fees) - let remaining_amount = (selected_utxos.0 - target_amount) as u64; - - let excess = decide_change(remaining_amount, fee_rate, drain_script); - - BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess) - } - - fn calculate_cs_result( - mut selected_utxos: Vec, - mut required_utxos: Vec, - excess: Excess, - ) -> CoinSelectionResult { - selected_utxos.append(&mut required_utxos); - let fee_amount = selected_utxos.iter().map(|u| u.fee).sum::(); - let selected = selected_utxos - .into_iter() - .map(|u| u.weighted_utxo.utxo) - .collect::>(); - - CoinSelectionResult { - selected, - fee_amount, - excess, - } + }) + .collect::>() } } @@ -723,7 +676,7 @@ impl BranchAndBoundCoinSelection { mod test { use std::str::FromStr; - use bitcoin::{OutPoint, Script, TxOut}; + use bitcoin::{Address, OutPoint, Script, TxOut}; use super::*; use crate::database::{BatchOperations, MemoryDatabase}; @@ -769,6 +722,16 @@ mod test { ] } + fn get_test_weighted_drain_script() -> WeightedScriptPubkey { + let script_p2wpkh = Address::from_str("bc1qxlh2mnc0yqwas76gqq665qkggee5m98t8yskd8") + .unwrap() + .script_pubkey(); + WeightedScriptPubkey { + satisfaction_weight: P2WPKH_SATISFACTION_SIZE, + script_pubkey: script_p2wpkh, + } + } + fn setup_database_and_get_oldest_first_test_utxos( database: &mut D, ) -> Vec { @@ -876,44 +839,22 @@ mod test { #[test] fn test_largest_first_coin_selection_success() { + let fee_rate = FeeRate::from_sat_per_vb(1.0); let utxos = get_test_utxos(); let database = MemoryDatabase::default(); - let drain_script = Script::default(); let target_amount = 250_000 + FEE_AMOUNT; + let weighted_drain_script = get_test_weighted_drain_script(); - let result = LargestFirstCoinSelection::default() - .coin_select( - &database, - utxos, - vec![], - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); - - assert_eq!(result.selected.len(), 3); - assert_eq!(result.selected_amount(), 300_010); - assert_eq!(result.fee_amount, 204) - } - - #[test] - fn test_largest_first_coin_selection_use_all() { - let utxos = get_test_utxos(); - let database = MemoryDatabase::default(); - let drain_script = Script::default(); - let target_amount = 20_000 + FEE_AMOUNT; - - let result = LargestFirstCoinSelection::default() - .coin_select( - &database, - utxos, - vec![], - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + let result = process_and_select_coins( + LargestFirstCoinSelection::default(), + &database, + utxos, + vec![], + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300_010); @@ -922,21 +863,22 @@ mod test { #[test] fn test_largest_first_coin_selection_use_only_necessary() { + let fee_rate = FeeRate::from_sat_per_vb(1.0); let utxos = get_test_utxos(); let database = MemoryDatabase::default(); - let drain_script = Script::default(); let target_amount = 20_000 + FEE_AMOUNT; + let weighted_drain_script = get_test_weighted_drain_script(); - let result = LargestFirstCoinSelection::default() - .coin_select( - &database, - vec![], - utxos, - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + let result = process_and_select_coins( + LargestFirstCoinSelection::default(), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); assert_eq!(result.selected.len(), 1); assert_eq!(result.selected_amount(), 200_000); @@ -946,64 +888,67 @@ mod test { #[test] #[should_panic(expected = "InsufficientFunds")] fn test_largest_first_coin_selection_insufficient_funds() { + let fee_rate = FeeRate::from_sat_per_vb(1.0); let utxos = get_test_utxos(); let database = MemoryDatabase::default(); - let drain_script = Script::default(); let target_amount = 500_000 + FEE_AMOUNT; + let weighted_drain_script = get_test_weighted_drain_script(); - LargestFirstCoinSelection::default() - .coin_select( - &database, - vec![], - utxos, - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + process_and_select_coins( + LargestFirstCoinSelection::default(), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); } #[test] #[should_panic(expected = "InsufficientFunds")] fn test_largest_first_coin_selection_insufficient_funds_high_fees() { + let fee_rate = FeeRate::from_sat_per_vb(1000.0); let utxos = get_test_utxos(); let database = MemoryDatabase::default(); - let drain_script = Script::default(); let target_amount = 250_000 + FEE_AMOUNT; + let weighted_drain_script = get_test_weighted_drain_script(); - LargestFirstCoinSelection::default() - .coin_select( - &database, - vec![], - utxos, - FeeRate::from_sat_per_vb(1000.0), - target_amount, - &drain_script, - ) - .unwrap(); + process_and_select_coins( + LargestFirstCoinSelection::default(), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); } #[test] fn test_oldest_first_coin_selection_success() { + let fee_rate = FeeRate::from_sat_per_vb(1.0); let mut database = MemoryDatabase::default(); let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); - let drain_script = Script::default(); let target_amount = 180_000 + FEE_AMOUNT; + let weighted_drain_script = get_test_weighted_drain_script(); - let result = OldestFirstCoinSelection::default() - .coin_select( - &database, - vec![], - utxos, - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + let result = process_and_select_coins( + OldestFirstCoinSelection::default(), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); assert_eq!(result.selected.len(), 2); assert_eq!(result.selected_amount(), 200_000); - assert_eq!(result.fee_amount, 136) + assert_eq!(result.fee_amount, 136); } #[test] @@ -1012,7 +957,6 @@ mod test { let utxo1 = utxo(120_000, 1); let utxo2 = utxo(80_000, 2); let utxo3 = utxo(300_000, 3); - let drain_script = Script::default(); let mut database = MemoryDatabase::default(); @@ -1047,41 +991,45 @@ mod test { database.set_tx(&utxo1_tx_details).unwrap(); database.set_tx(&utxo2_tx_details).unwrap(); + let fee_rate = FeeRate::from_sat_per_vb(1.0); + let utxos = vec![utxo3, utxo1, utxo2]; let target_amount = 180_000 + FEE_AMOUNT; + let weighted_drain_script = get_test_weighted_drain_script(); - let result = OldestFirstCoinSelection::default() - .coin_select( - &database, - vec![], - vec![utxo3, utxo1, utxo2], - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + let result = process_and_select_coins( + OldestFirstCoinSelection::default(), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); assert_eq!(result.selected.len(), 2); assert_eq!(result.selected_amount(), 200_000); - assert_eq!(result.fee_amount, 136) + assert_eq!(result.fee_amount, 136); } #[test] fn test_oldest_first_coin_selection_use_all() { let mut database = MemoryDatabase::default(); let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); - let drain_script = Script::default(); + let fee_rate = FeeRate::from_sat_per_vb(1.0); let target_amount = 20_000 + FEE_AMOUNT; + let weighted_drain_script = get_test_weighted_drain_script(); - let result = OldestFirstCoinSelection::default() - .coin_select( - &database, - utxos, - vec![], - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + let result = process_and_select_coins( + OldestFirstCoinSelection::default(), + &database, + utxos, + vec![], + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 500_000); @@ -1092,19 +1040,20 @@ mod test { fn test_oldest_first_coin_selection_use_only_necessary() { let mut database = MemoryDatabase::default(); let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); - let drain_script = Script::default(); + let fee_rate = FeeRate::from_sat_per_vb(1.0); let target_amount = 20_000 + FEE_AMOUNT; + let weighted_drain_script = get_test_weighted_drain_script(); - let result = OldestFirstCoinSelection::default() - .coin_select( - &database, - vec![], - utxos, - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + let result = process_and_select_coins( + OldestFirstCoinSelection::default(), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); assert_eq!(result.selected.len(), 1); assert_eq!(result.selected_amount(), 120_000); @@ -1116,19 +1065,20 @@ mod test { fn test_oldest_first_coin_selection_insufficient_funds() { let mut database = MemoryDatabase::default(); let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); - let drain_script = Script::default(); + let fee_rate = FeeRate::from_sat_per_vb(1.0); let target_amount = 600_000 + FEE_AMOUNT; + let weighted_drain_script = get_test_weighted_drain_script(); - OldestFirstCoinSelection::default() - .coin_select( - &database, - vec![], - utxos, - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + process_and_select_coins( + OldestFirstCoinSelection::default(), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); } #[test] @@ -1136,20 +1086,20 @@ mod test { fn test_oldest_first_coin_selection_insufficient_funds_high_fees() { let mut database = MemoryDatabase::default(); let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database); - let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::() - 50; - let drain_script = Script::default(); + let fee_rate = FeeRate::from_sat_per_vb(1000.0); + let weighted_drain_script = get_test_weighted_drain_script(); - OldestFirstCoinSelection::default() - .coin_select( - &database, - vec![], - utxos, - FeeRate::from_sat_per_vb(1000.0), - target_amount, - &drain_script, - ) - .unwrap(); + process_and_select_coins( + OldestFirstCoinSelection::default(), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); } #[test] @@ -1157,22 +1107,21 @@ mod test { // 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 drain_script = Script::default(); - let target_amount = 250_000 + FEE_AMOUNT; + let fee_rate = FeeRate::from_sat_per_vb(1.0); + let weighted_drain_script = get_test_weighted_drain_script(); - let result = BranchAndBoundCoinSelection::default() - .coin_select( - &database, - vec![], - utxos, - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + let result = process_and_select_coins( + BranchAndBoundCoinSelection::default(), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300_000); @@ -1183,19 +1132,20 @@ mod test { fn test_bnb_coin_selection_required_are_enough() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); - let drain_script = Script::default(); let target_amount = 20_000 + FEE_AMOUNT; + let fee_rate = FeeRate::from_sat_per_vb(1.0); + let weighted_drain_script = get_test_weighted_drain_script(); - let result = BranchAndBoundCoinSelection::default() - .coin_select( - &database, - utxos.clone(), - utxos, - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + let result = process_and_select_coins( + BranchAndBoundCoinSelection::default(), + &database, + utxos.clone(), + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); assert_eq!(result.selected.len(), 3); assert_eq!(result.selected_amount(), 300_010); @@ -1206,22 +1156,23 @@ mod test { fn test_bnb_coin_selection_optional_are_enough() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); - let drain_script = Script::default(); let target_amount = 299756 + FEE_AMOUNT; + let fee_rate = FeeRate::from_sat_per_vb(1.0); + let weighted_drain_script = get_test_weighted_drain_script(); - let result = BranchAndBoundCoinSelection::default() - .coin_select( - &database, - vec![], - utxos, - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + let result = process_and_select_coins( + BranchAndBoundCoinSelection::default(), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); assert_eq!(result.selected.len(), 2); - assert_eq!(result.selected_amount(), 300000); + assert_eq!(result.selected_amount(), 300_000); assert_eq!(result.fee_amount, 136); } @@ -1239,20 +1190,21 @@ mod test { assert_eq!(amount, 100_000); let amount: u64 = optional.iter().map(|u| u.utxo.txout().value).sum(); assert!(amount > 150_000); - let drain_script = Script::default(); let target_amount = 150_000 + FEE_AMOUNT; + let fee_rate = FeeRate::from_sat_per_vb(1.0); + let weighted_drain_script = get_test_weighted_drain_script(); - let result = BranchAndBoundCoinSelection::default() - .coin_select( - &database, - required, - optional, - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + let result = process_and_select_coins( + BranchAndBoundCoinSelection::default(), + &database, + required, + optional, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); assert_eq!(result.selected.len(), 2); assert_eq!(result.selected_amount(), 300_000); @@ -1264,19 +1216,20 @@ mod test { fn test_bnb_coin_selection_insufficient_funds() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); - let drain_script = Script::default(); let target_amount = 500_000 + FEE_AMOUNT; + let fee_rate = FeeRate::from_sat_per_vb(1.0); + let weighted_drain_script = get_test_weighted_drain_script(); - BranchAndBoundCoinSelection::default() - .coin_select( - &database, - vec![], - utxos, - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + process_and_select_coins( + BranchAndBoundCoinSelection::default(), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); } #[test] @@ -1284,38 +1237,40 @@ mod test { fn test_bnb_coin_selection_insufficient_funds_high_fees() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); - let drain_script = Script::default(); let target_amount = 250_000 + FEE_AMOUNT; + let fee_rate = FeeRate::from_sat_per_vb(1000.0); + let weighted_drain_script = get_test_weighted_drain_script(); - BranchAndBoundCoinSelection::default() - .coin_select( - &database, - vec![], - utxos, - FeeRate::from_sat_per_vb(1000.0), - target_amount, - &drain_script, - ) - .unwrap(); + process_and_select_coins( + BranchAndBoundCoinSelection::default(), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); } #[test] fn test_bnb_coin_selection_check_fee_rate() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); - let drain_script = Script::default(); let target_amount = 99932; // first utxo's effective value + let fee_rate = FeeRate::from_sat_per_vb(1.0); + let weighted_drain_script = get_test_weighted_drain_script(); - let result = BranchAndBoundCoinSelection::new(0) - .coin_select( - &database, - vec![], - utxos, - FeeRate::from_sat_per_vb(1.0), - target_amount, - &drain_script, - ) - .unwrap(); + let result = process_and_select_coins( + BranchAndBoundCoinSelection::new(0), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); assert_eq!(result.selected.len(), 1); assert_eq!(result.selected_amount(), 100_000); @@ -1329,21 +1284,24 @@ mod test { let seed = [0; 32]; let mut rng: StdRng = SeedableRng::from_seed(seed); let database = MemoryDatabase::default(); + let weighted_drain_script = get_test_weighted_drain_script(); for _i in 0..200 { let mut optional_utxos = generate_random_utxos(&mut rng, 16); + let fee_rate = FeeRate::from_sat_per_vb(0.0); let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos); - let drain_script = Script::default(); - let result = BranchAndBoundCoinSelection::new(0) - .coin_select( - &database, - vec![], - optional_utxos, - FeeRate::from_sat_per_vb(0.0), - target_amount, - &drain_script, - ) - .unwrap(); + + let result = process_and_select_coins( + BranchAndBoundCoinSelection::new(0), + &database, + vec![], + optional_utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap(); + assert_eq!(result.selected_amount(), target_amount); } } @@ -1352,28 +1310,22 @@ mod 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() + let optional_utxos: Vec = get_test_utxos() .into_iter() .map(|u| OutputGroup::new(u, fee_rate)) .collect(); - - let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); + let available_value = optional_utxos.iter().map(|x| x.effective_value).sum(); let size_of_change = 31; let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb(); - - let drain_script = Script::default(); let target_amount = 20_000 + FEE_AMOUNT; + BranchAndBoundCoinSelection::new(size_of_change) .bnb( - vec![], - utxos, - 0, - curr_available_value, + optional_utxos, target_amount as i64, cost_of_change, - &drain_script, - fee_rate, + available_value, ) .unwrap(); } @@ -1382,29 +1334,22 @@ mod 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) + let optional_utxos: Vec = generate_same_value_utxos(100_000, 100_000) .into_iter() .map(|u| OutputGroup::new(u, fee_rate)) .collect(); - - let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); + let available_value = optional_utxos.iter().map(|x| x.effective_value).sum(); let size_of_change = 31; let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb(); let target_amount = 20_000 + FEE_AMOUNT; - let drain_script = Script::default(); - BranchAndBoundCoinSelection::new(size_of_change) .bnb( - vec![], - utxos, - 0, - curr_available_value, + optional_utxos, target_amount as i64, cost_of_change, - &drain_script, - fee_rate, + available_value, ) .unwrap(); } @@ -1412,44 +1357,37 @@ mod test { // The match won't be exact but still in the range #[test] fn test_bnb_function_almost_exact_match_with_fees() { + let database = MemoryDatabase::default(); 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_per_vb(); - let utxos: Vec<_> = generate_same_value_utxos(50_000, 10) - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) - .collect(); - - let curr_value = 0; - - let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value); + let utxos: Vec<_> = generate_same_value_utxos(50_000, 10); // 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 i64 + 5; + let weighted_drain_script = get_test_weighted_drain_script(); - let drain_script = Script::default(); + let result = process_and_select_coins( + BranchAndBoundCoinSelection::new(size_of_change), + &database, + vec![], + utxos, + fee_rate, + target_amount as u64, + &weighted_drain_script, + ) + .unwrap(); - let result = BranchAndBoundCoinSelection::new(size_of_change) - .bnb( - vec![], - utxos, - curr_value, - curr_available_value, - target_amount, - cost_of_change, - &drain_script, - fee_rate, - ) - .unwrap(); - assert_eq!(result.selected_amount(), 100_000); assert_eq!(result.fee_amount, 136); + 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 database = MemoryDatabase::default(); let seed = [0; 32]; let mut rng: StdRng = SeedableRng::from_seed(seed); let fee_rate = FeeRate::from_sat_per_vb(0.0); @@ -1459,31 +1397,28 @@ mod test { .into_iter() .map(|u| OutputGroup::new(u, fee_rate)) .collect(); - - let curr_value = 0; - - let curr_available_value = optional_utxos - .iter() - .fold(0, |acc, x| acc + x.effective_value); + let available_value = optional_utxos.iter().map(|x| x.effective_value).sum(); let target_amount = optional_utxos[3].effective_value + optional_utxos[23].effective_value; - let drain_script = Script::default(); - - let result = BranchAndBoundCoinSelection::new(0) - .bnb( - vec![], + let selected_utxos = BranchAndBoundCoinSelection::new(0) + .coin_select( + &database, optional_utxos, - curr_value, - curr_available_value, - target_amount, - 0.0, - &drain_script, fee_rate, + // fee rate is zero so effective value can't be negative + target_amount as u64, + available_value, ) .unwrap(); - assert_eq!(result.selected_amount(), target_amount as u64); + + let selected_amount: u64 = selected_utxos + .iter() + .map(|og| og.weighted_utxo.utxo.txout().value) + .sum(); + + assert_eq!(selected_amount, target_amount as u64); } } @@ -1493,44 +1428,44 @@ mod test { 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) + FEE_AMOUNT; - let fee_rate = FeeRate::from_sat_per_vb(1.0); - let utxos: Vec = utxos + let optional_utxos: Vec<_> = utxos .into_iter() .map(|u| OutputGroup::new(u, fee_rate)) .collect(); - let drain_script = Script::default(); + let selected_utxos = BranchAndBoundCoinSelection::default() + .single_random_draw(optional_utxos, target_amount as i64); - let result = BranchAndBoundCoinSelection::default().single_random_draw( - vec![], - utxos, - 0, - target_amount as i64, - &drain_script, - fee_rate, - ); + let selected_amount: u64 = selected_utxos + .iter() + .map(|og| og.weighted_utxo.utxo.txout().value) + .sum(); - assert!(result.selected_amount() > target_amount); - assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64); + let fee_amount: u64 = selected_utxos.iter().map(|og| og.fee).sum(); + + assert!(selected_amount > target_amount); + assert_eq!(fee_amount, (selected_utxos.len() * 68) as u64); } #[test] fn test_bnb_exclude_negative_effective_value() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); - let drain_script = Script::default(); + let fee_rate = FeeRate::from_sat_per_vb(10.0); + let weighted_drain_script = get_test_weighted_drain_script(); + let target_amount = 500_000; - let err = BranchAndBoundCoinSelection::default() - .coin_select( - &database, - vec![], - utxos, - FeeRate::from_sat_per_vb(10.0), - 500_000, - &drain_script, - ) - .unwrap_err(); + let err = process_and_select_coins( + BranchAndBoundCoinSelection::default(), + &database, + vec![], + utxos, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap_err(); assert!(matches!( err, @@ -1545,22 +1480,24 @@ mod test { fn test_bnb_include_negative_effective_value_when_required() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); - let drain_script = Script::default(); + let fee_rate = FeeRate::from_sat_per_vb(10.0); + let target_amount = 500_000; + let weighted_drain_script = get_test_weighted_drain_script(); let (required, optional) = utxos .into_iter() .partition(|u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value < 1000)); - let err = BranchAndBoundCoinSelection::default() - .coin_select( - &database, - required, - optional, - FeeRate::from_sat_per_vb(10.0), - 500_000, - &drain_script, - ) - .unwrap_err(); + let err = process_and_select_coins( + BranchAndBoundCoinSelection::default(), + &database, + required, + optional, + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap_err(); assert!(matches!( err, @@ -1575,18 +1512,20 @@ mod test { fn test_bnb_sum_of_effective_value_negative() { let utxos = get_test_utxos(); let database = MemoryDatabase::default(); - let drain_script = Script::default(); + let fee_rate = FeeRate::from_sat_per_vb(10.0); + let target_amount = 500_000; + let weighted_drain_script = get_test_weighted_drain_script(); - let err = BranchAndBoundCoinSelection::default() - .coin_select( - &database, - utxos, - vec![], - FeeRate::from_sat_per_vb(10_000.0), - 500_000, - &drain_script, - ) - .unwrap_err(); + let err = process_and_select_coins( + BranchAndBoundCoinSelection::default(), + &database, + utxos, + vec![], + fee_rate, + target_amount, + &weighted_drain_script, + ) + .unwrap_err(); assert!(matches!( err, diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 04c5a6508..ae25ef41c 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -56,7 +56,7 @@ pub use utils::IsDust; #[allow(deprecated)] use address_validator::AddressValidator; -use coin_selection::DefaultCoinSelectionAlgorithm; +use coin_selection::{process_and_select_coins, DefaultCoinSelectionAlgorithm, Excess}; use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner}; use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams}; use utils::{check_nlocktime, check_nsequence_rbf, After, Older, SecpCtx}; @@ -76,7 +76,6 @@ use crate::psbt::PsbtUtils; use crate::signer::SignerError; use crate::testutils; use crate::types::*; -use crate::wallet::coin_selection::Excess::{Change, NoChange}; const CACHE_ADDR_BATCH_SIZE: u32 = 100; const COINBASE_MATURITY: u32 = 100; @@ -843,25 +842,41 @@ where current_height, )?; - // get drain script - let drain_script = match params.drain_to { - Some(ref drain_recipient) => drain_recipient.clone(), - None => self - .get_internal_address(AddressIndex::New)? - .address - .script_pubkey(), + // prepare the drain script + let weighted_drain_script = { + let script_pubkey = match params.drain_to { + Some(ref drain_recipient) => drain_recipient.clone(), + None => self + .get_internal_address(AddressIndex::New)? + .address + .script_pubkey(), + }; + + let satisfaction_weight = if params.drain_to.is_none() { + self.get_descriptor_for_keychain(KeychainKind::Internal) + .max_satisfaction_weight() + .unwrap() + } else { + 0 + }; + + WeightedScriptPubkey { + satisfaction_weight, + script_pubkey, + } }; - let coin_selection = coin_selection.coin_select( + let coin_selection = process_and_select_coins( + coin_selection, self.database.borrow().deref(), required_utxos, optional_utxos, fee_rate, outgoing + fee_amount, - &drain_script, + &weighted_drain_script, )?; + fee_amount += coin_selection.fee_amount; - let excess = &coin_selection.excess; tx.input = coin_selection .selected @@ -883,11 +898,11 @@ where // Otherwise, we don't know who we should send the funds to, and how much // we should send! if params.drain_to.is_some() && (params.drain_wallet || !params.utxos.is_empty()) { - if let NoChange { + if let Excess::NoChange { dust_threshold, remaining_amount, change_fee, - } = excess + } = &coin_selection.excess { return Err(Error::InsufficientFunds { needed: *dust_threshold, @@ -899,11 +914,12 @@ where } } - match excess { - NoChange { + match &coin_selection.excess { + Excess::NoChange { remaining_amount, .. } => fee_amount += remaining_amount, - Change { amount, fee } => { + Excess::Change { amount, fee } => { + let drain_script = weighted_drain_script.script_pubkey; if self.is_mine(&drain_script)? { received += amount; }