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; }