bdk/wallet/
coin_selection.rs

1// Bitcoin Dev Kit
2// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
3//
4// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
5//
6// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
7// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
8// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
9// You may not use this file except in accordance with one or both of these
10// licenses.
11
12//! Coin selection
13//!
14//! This module provides the trait [`CoinSelectionAlgorithm`] that can be implemented to
15//! define custom coin selection algorithms.
16//!
17//! You can specify a custom coin selection algorithm through the [`coin_selection`] method on
18//! [`TxBuilder`]. [`DefaultCoinSelectionAlgorithm`] aliases the coin selection algorithm that will
19//! be used if it is not explicitly set.
20//!
21//! [`TxBuilder`]: super::tx_builder::TxBuilder
22//! [`coin_selection`]: super::tx_builder::TxBuilder::coin_selection
23//!
24//! ## Example
25//!
26//! ```
27//! # use std::str::FromStr;
28//! # use bitcoin::*;
29//! # use bdk::wallet::{self, coin_selection::*};
30//! # use bdk::database::Database;
31//! # use bdk::*;
32//! # use bdk::wallet::coin_selection::decide_change;
33//! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
34//! #[derive(Debug)]
35//! struct AlwaysSpendEverything;
36//!
37//! impl<D: Database> CoinSelectionAlgorithm<D> for AlwaysSpendEverything {
38//!     fn coin_select(
39//!         &self,
40//!         database: &D,
41//!         required_utxos: Vec<WeightedUtxo>,
42//!         optional_utxos: Vec<WeightedUtxo>,
43//!         fee_rate: bdk::FeeRate,
44//!         target_amount: u64,
45//!         drain_script: &Script,
46//!     ) -> Result<CoinSelectionResult, bdk::Error> {
47//!         let mut selected_amount = 0;
48//!         let mut additional_weight = Weight::ZERO;
49//!         let all_utxos_selected = required_utxos
50//!             .into_iter()
51//!             .chain(optional_utxos)
52//!             .scan(
53//!                 (&mut selected_amount, &mut additional_weight),
54//!                 |(selected_amount, additional_weight), weighted_utxo| {
55//!                     **selected_amount += weighted_utxo.utxo.txout().value;
56//!                     **additional_weight += Weight::from_wu(
57//!                         (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
58//!                     );
59//!                     Some(weighted_utxo.utxo)
60//!                 },
61//!             )
62//!             .collect::<Vec<_>>();
63//!         let additional_fees = fee_rate.fee_wu(additional_weight);
64//!         let amount_needed_with_fees = additional_fees + target_amount;
65//!         if selected_amount < amount_needed_with_fees {
66//!             return Err(bdk::Error::InsufficientFunds {
67//!                 needed: amount_needed_with_fees,
68//!                 available: selected_amount,
69//!             });
70//!         }
71//!
72//!         let remaining_amount = selected_amount - amount_needed_with_fees;
73//!
74//!         let excess = decide_change(remaining_amount, fee_rate, drain_script);
75//!
76//!         Ok(CoinSelectionResult {
77//!             selected: all_utxos_selected,
78//!             fee_amount: additional_fees,
79//!             excess,
80//!         })
81//!     }
82//! }
83//!
84//! # let wallet = doctest_wallet!();
85//! // create wallet, sync, ...
86//!
87//! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
88//!     .unwrap()
89//!     .require_network(Network::Testnet)
90//!     .unwrap();
91//! let (psbt, details) = {
92//!     let mut builder = wallet.build_tx().coin_selection(AlwaysSpendEverything);
93//!     builder.add_recipient(to_address.script_pubkey(), 50_000);
94//!     builder.finish()?
95//! };
96//!
97//! // inspect, sign, broadcast, ...
98//!
99//! # Ok::<(), bdk::Error>(())
100//! ```
101
102use crate::types::FeeRate;
103use crate::wallet::utils::IsDust;
104use crate::{database::Database, WeightedUtxo};
105use crate::{error::Error, Utxo};
106
107use bitcoin::consensus::encode::serialize;
108use bitcoin::{Script, Weight};
109
110#[cfg(test)]
111use assert_matches::assert_matches;
112use rand::seq::SliceRandom;
113#[cfg(not(test))]
114use rand::thread_rng;
115use std::collections::HashMap;
116use std::convert::TryInto;
117
118/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
119/// overridden
120#[cfg(not(test))]
121pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
122
123/// Default deterministic coin selection algorithm for testing used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
124/// overridden
125#[cfg(test)]
126pub type DefaultCoinSelectionAlgorithm = LargestFirstCoinSelection;
127
128// Base weight of a Txin, not counting the weight needed for satisfying it.
129// prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes)
130pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
131
132#[derive(Debug)]
133/// Remaining amount after performing coin selection
134pub enum Excess {
135    /// It's not possible to create spendable output from excess using the current drain output
136    NoChange {
137        /// Threshold to consider amount as dust for this particular change script_pubkey
138        dust_threshold: u64,
139        /// Exceeding amount of current selection over outgoing value and fee costs
140        remaining_amount: u64,
141        /// The calculated fee for the drain TxOut with the selected script_pubkey
142        change_fee: u64,
143    },
144    /// It's possible to create spendable output from excess using the current drain output
145    Change {
146        /// Effective amount available to create change after deducting the change output fee
147        amount: u64,
148        /// The deducted change output fee
149        fee: u64,
150    },
151}
152
153/// Result of a successful coin selection
154#[derive(Debug)]
155pub struct CoinSelectionResult {
156    /// List of outputs selected for use as inputs
157    pub selected: Vec<Utxo>,
158    /// Total fee amount for the selected utxos in satoshis
159    pub fee_amount: u64,
160    /// Remaining amount after deducing fees and outgoing outputs
161    pub excess: Excess,
162}
163
164impl CoinSelectionResult {
165    /// The total value of the inputs selected.
166    pub fn selected_amount(&self) -> u64 {
167        self.selected.iter().map(|u| u.txout().value).sum()
168    }
169
170    /// The total value of the inputs selected from the local wallet.
171    pub fn local_selected_amount(&self) -> u64 {
172        self.selected
173            .iter()
174            .filter_map(|u| match u {
175                Utxo::Local(_) => Some(u.txout().value),
176                _ => None,
177            })
178            .sum()
179    }
180}
181
182/// Trait for generalized coin selection algorithms
183///
184/// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin
185/// selection algorithm when it creates transactions.
186///
187/// For an example see [this module](crate::wallet::coin_selection)'s documentation.
188pub trait CoinSelectionAlgorithm<D: Database>: std::fmt::Debug {
189    /// Perform the coin selection
190    ///
191    /// - `database`: a reference to the wallet's database that can be used to lookup additional
192    ///               details for a specific UTXO
193    /// - `required_utxos`: the utxos that must be spent regardless of `target_amount` with their
194    ///                     weight cost
195    /// - `optional_utxos`: the remaining available utxos to satisfy `target_amount` with their
196    ///                     weight cost
197    /// - `fee_rate`: fee rate to use
198    /// - `target_amount`: the outgoing amount in satoshis and the fees already
199    ///                    accumulated from added outputs and transaction’s header.
200    /// - `drain_script`: the script to use in case of change
201    #[allow(clippy::too_many_arguments)]
202    fn coin_select(
203        &self,
204        database: &D,
205        required_utxos: Vec<WeightedUtxo>,
206        optional_utxos: Vec<WeightedUtxo>,
207        fee_rate: FeeRate,
208        target_amount: u64,
209        drain_script: &Script,
210    ) -> Result<CoinSelectionResult, Error>;
211}
212
213/// Simple and dumb coin selection
214///
215/// This coin selection algorithm sorts the available UTXOs by value and then picks them starting
216/// from the largest ones until the required amount is reached.
217#[derive(Debug, Default, Clone, Copy)]
218pub struct LargestFirstCoinSelection;
219
220impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
221    fn coin_select(
222        &self,
223        _database: &D,
224        required_utxos: Vec<WeightedUtxo>,
225        mut optional_utxos: Vec<WeightedUtxo>,
226        fee_rate: FeeRate,
227        target_amount: u64,
228        drain_script: &Script,
229    ) -> Result<CoinSelectionResult, Error> {
230        log::debug!(
231            "target_amount = `{}`, fee_rate = `{:?}`",
232            target_amount,
233            fee_rate
234        );
235
236        // We put the "required UTXOs" first and make sure the optional UTXOs are sorted,
237        // initially smallest to largest, before being reversed with `.rev()`.
238        let utxos = {
239            optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value);
240            required_utxos
241                .into_iter()
242                .map(|utxo| (true, utxo))
243                .chain(optional_utxos.into_iter().rev().map(|utxo| (false, utxo)))
244        };
245
246        select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
247    }
248}
249
250/// OldestFirstCoinSelection always picks the utxo with the smallest blockheight to add to the selected coins next
251///
252/// This coin selection algorithm sorts the available UTXOs by blockheight and then picks them starting
253/// from the oldest ones until the required amount is reached.
254#[derive(Debug, Default, Clone, Copy)]
255pub struct OldestFirstCoinSelection;
256
257impl<D: Database> CoinSelectionAlgorithm<D> for OldestFirstCoinSelection {
258    fn coin_select(
259        &self,
260        database: &D,
261        required_utxos: Vec<WeightedUtxo>,
262        mut optional_utxos: Vec<WeightedUtxo>,
263        fee_rate: FeeRate,
264        target_amount: u64,
265        drain_script: &Script,
266    ) -> Result<CoinSelectionResult, Error> {
267        // query db and create a blockheight lookup table
268        let blockheights = optional_utxos
269            .iter()
270            .map(|wu| wu.utxo.outpoint().txid)
271            // fold is used so we can skip db query for txid that already exist in hashmap acc
272            .try_fold(HashMap::new(), |mut bh_acc, txid| {
273                if bh_acc.contains_key(&txid) {
274                    Ok(bh_acc)
275                } else {
276                    database.get_tx(&txid, false).map(|details| {
277                        bh_acc.insert(
278                            txid,
279                            details.and_then(|d| d.confirmation_time.map(|ct| ct.height)),
280                        );
281                        bh_acc
282                    })
283                }
284            })?;
285
286        // We put the "required UTXOs" first and make sure the optional UTXOs are sorted from
287        // oldest to newest according to blocktime
288        // For utxo that doesn't exist in DB, they will have lowest priority to be selected
289        let utxos = {
290            optional_utxos.sort_unstable_by_key(|wu| {
291                match blockheights.get(&wu.utxo.outpoint().txid) {
292                    Some(Some(blockheight)) => blockheight,
293                    _ => &u32::MAX,
294                }
295            });
296
297            required_utxos
298                .into_iter()
299                .map(|utxo| (true, utxo))
300                .chain(optional_utxos.into_iter().map(|utxo| (false, utxo)))
301        };
302
303        select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
304    }
305}
306
307/// Decide if change can be created
308///
309/// - `remaining_amount`: the amount in which the selected coins exceed the target amount
310/// - `fee_rate`: required fee rate for the current selection
311/// - `drain_script`: script to consider change creation
312pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess {
313    // drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value)
314    let drain_output_len = serialize(drain_script).len() + 8usize;
315    let change_fee = fee_rate.fee_vb(drain_output_len);
316    let drain_val = remaining_amount.saturating_sub(change_fee);
317
318    if drain_val.is_dust(drain_script) {
319        let dust_threshold = drain_script.dust_value().to_sat();
320        Excess::NoChange {
321            dust_threshold,
322            change_fee,
323            remaining_amount,
324        }
325    } else {
326        Excess::Change {
327            amount: drain_val,
328            fee: change_fee,
329        }
330    }
331}
332
333fn select_sorted_utxos(
334    utxos: impl Iterator<Item = (bool, WeightedUtxo)>,
335    fee_rate: FeeRate,
336    target_amount: u64,
337    drain_script: &Script,
338) -> Result<CoinSelectionResult, Error> {
339    let mut selected_amount = 0;
340    let mut fee_amount = 0;
341    let selected = utxos
342        .scan(
343            (&mut selected_amount, &mut fee_amount),
344            |(selected_amount, fee_amount), (must_use, weighted_utxo)| {
345                if must_use || **selected_amount < target_amount + **fee_amount {
346                    **fee_amount += fee_rate.fee_wu(Weight::from_wu(
347                        (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
348                    ));
349                    **selected_amount += weighted_utxo.utxo.txout().value;
350
351                    log::debug!(
352                        "Selected {}, updated fee_amount = `{}`",
353                        weighted_utxo.utxo.outpoint(),
354                        fee_amount
355                    );
356
357                    Some(weighted_utxo.utxo)
358                } else {
359                    None
360                }
361            },
362        )
363        .collect::<Vec<_>>();
364
365    let amount_needed_with_fees = target_amount + fee_amount;
366    if selected_amount < amount_needed_with_fees {
367        return Err(Error::InsufficientFunds {
368            needed: amount_needed_with_fees,
369            available: selected_amount,
370        });
371    }
372
373    let remaining_amount = selected_amount - amount_needed_with_fees;
374
375    let excess = decide_change(remaining_amount, fee_rate, drain_script);
376
377    Ok(CoinSelectionResult {
378        selected,
379        fee_amount,
380        excess,
381    })
382}
383
384#[derive(Debug, Clone)]
385// Adds fee information to an UTXO.
386struct OutputGroup {
387    weighted_utxo: WeightedUtxo,
388    // Amount of fees for spending a certain utxo, calculated using a certain FeeRate
389    fee: u64,
390    // The effective value of the UTXO, i.e., the utxo value minus the fee for spending it
391    effective_value: i64,
392}
393
394impl OutputGroup {
395    fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
396        let fee = fee_rate.fee_wu(Weight::from_wu(
397            (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
398        ));
399        let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64;
400        OutputGroup {
401            weighted_utxo,
402            fee,
403            effective_value,
404        }
405    }
406}
407
408/// Branch and bound coin selection
409///
410/// Code adapted from Bitcoin Core's implementation and from Mark Erhardt Master's Thesis: <http://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf>
411#[derive(Debug)]
412pub struct BranchAndBoundCoinSelection {
413    size_of_change: u64,
414}
415
416impl Default for BranchAndBoundCoinSelection {
417    fn default() -> Self {
418        Self {
419            // P2WPKH cost of change -> value (8 bytes) + script len (1 bytes) + script (22 bytes)
420            size_of_change: 8 + 1 + 22,
421        }
422    }
423}
424
425impl BranchAndBoundCoinSelection {
426    /// Create new instance with target size for change output
427    pub fn new(size_of_change: u64) -> Self {
428        Self { size_of_change }
429    }
430}
431
432const BNB_TOTAL_TRIES: usize = 100_000;
433
434impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
435    fn coin_select(
436        &self,
437        _database: &D,
438        required_utxos: Vec<WeightedUtxo>,
439        optional_utxos: Vec<WeightedUtxo>,
440        fee_rate: FeeRate,
441        target_amount: u64,
442        drain_script: &Script,
443    ) -> Result<CoinSelectionResult, Error> {
444        // Mapping every (UTXO, usize) to an output group
445        let required_utxos: Vec<OutputGroup> = required_utxos
446            .into_iter()
447            .map(|u| OutputGroup::new(u, fee_rate))
448            .collect();
449
450        // Mapping every (UTXO, usize) to an output group, filtering UTXOs with a negative
451        // effective value
452        let optional_utxos: Vec<OutputGroup> = optional_utxos
453            .into_iter()
454            .map(|u| OutputGroup::new(u, fee_rate))
455            .filter(|u| u.effective_value.is_positive())
456            .collect();
457
458        let curr_value = required_utxos
459            .iter()
460            .fold(0, |acc, x| acc + x.effective_value);
461
462        let curr_available_value = optional_utxos
463            .iter()
464            .fold(0, |acc, x| acc + x.effective_value);
465
466        let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_per_vb();
467
468        // `curr_value` and `curr_available_value` are both the sum of *effective_values* of
469        // the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with
470        // negative effective value, so it will always be positive.
471        //
472        // Since we are required to spend the required UTXOs (curr_value) we have to consider
473        // all their effective values, even when negative, which means that curr_value could
474        // be negative as well.
475        //
476        // If the sum of curr_value and curr_available_value is negative or lower than our target,
477        // we can immediately exit with an error, as it's guaranteed we will never find a solution
478        // if we actually run the BnB.
479        let total_value: Result<u64, _> = (curr_available_value + curr_value).try_into();
480        match total_value {
481            Ok(v) if v >= target_amount => {}
482            _ => {
483                // Assume we spend all the UTXOs we can (all the required + all the optional with
484                // positive effective value), sum their value and their fee cost.
485                let (utxo_fees, utxo_value) = required_utxos
486                    .iter()
487                    .chain(optional_utxos.iter())
488                    .fold((0, 0), |(mut fees, mut value), utxo| {
489                        fees += utxo.fee;
490                        value += utxo.weighted_utxo.utxo.txout().value;
491
492                        (fees, value)
493                    });
494
495                // Add to the target the fee cost of the UTXOs
496                return Err(Error::InsufficientFunds {
497                    needed: target_amount + utxo_fees,
498                    available: utxo_value,
499                });
500            }
501        }
502
503        let target_amount = target_amount
504            .try_into()
505            .expect("Bitcoin amount to fit into i64");
506
507        if curr_value > target_amount {
508            // remaining_amount can't be negative as that would mean the
509            // selection wasn't successful
510            // target_amount = amount_needed + (fee_amount - vin_fees)
511            let remaining_amount = (curr_value - target_amount) as u64;
512
513            let excess = decide_change(remaining_amount, fee_rate, drain_script);
514
515            return Ok(BranchAndBoundCoinSelection::calculate_cs_result(
516                vec![],
517                required_utxos,
518                excess,
519            ));
520        }
521
522        Ok(self
523            .bnb(
524                required_utxos.clone(),
525                optional_utxos.clone(),
526                curr_value,
527                curr_available_value,
528                target_amount,
529                cost_of_change,
530                drain_script,
531                fee_rate,
532            )
533            .unwrap_or_else(|_| {
534                self.single_random_draw(
535                    required_utxos,
536                    optional_utxos,
537                    curr_value,
538                    target_amount,
539                    drain_script,
540                    fee_rate,
541                )
542            }))
543    }
544}
545
546impl BranchAndBoundCoinSelection {
547    // TODO: make this more Rust-onic :)
548    // (And perhaps refactor with less arguments?)
549    #[allow(clippy::too_many_arguments)]
550    fn bnb(
551        &self,
552        required_utxos: Vec<OutputGroup>,
553        mut optional_utxos: Vec<OutputGroup>,
554        mut curr_value: i64,
555        mut curr_available_value: i64,
556        target_amount: i64,
557        cost_of_change: f32,
558        drain_script: &Script,
559        fee_rate: FeeRate,
560    ) -> Result<CoinSelectionResult, Error> {
561        // current_selection[i] will contain true if we are using optional_utxos[i],
562        // false otherwise. Note that current_selection.len() could be less than
563        // optional_utxos.len(), it just means that we still haven't decided if we should keep
564        // certain optional_utxos or not.
565        let mut current_selection: Vec<bool> = Vec::with_capacity(optional_utxos.len());
566
567        // Sort the utxo_pool
568        optional_utxos.sort_unstable_by_key(|a| a.effective_value);
569        optional_utxos.reverse();
570
571        // Contains the best selection we found
572        let mut best_selection = Vec::new();
573        let mut best_selection_value = None;
574
575        // Depth First search loop for choosing the UTXOs
576        for _ in 0..BNB_TOTAL_TRIES {
577            // Conditions for starting a backtrack
578            let mut backtrack = false;
579            // Cannot possibly reach target with the amount remaining in the curr_available_value,
580            // or the selected value is out of range.
581            // Go back and try other branch
582            if curr_value + curr_available_value < target_amount
583                || curr_value > target_amount + cost_of_change as i64
584            {
585                backtrack = true;
586            } else if curr_value >= target_amount {
587                // Selected value is within range, there's no point in going forward. Start
588                // backtracking
589                backtrack = true;
590
591                // If we found a solution better than the previous one, or if there wasn't previous
592                // solution, update the best solution
593                if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() {
594                    best_selection = current_selection.clone();
595                    best_selection_value = Some(curr_value);
596                }
597
598                // If we found a perfect match, break here
599                if curr_value == target_amount {
600                    break;
601                }
602            }
603
604            // Backtracking, moving backwards
605            if backtrack {
606                // Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
607                while let Some(false) = current_selection.last() {
608                    current_selection.pop();
609                    curr_available_value += optional_utxos[current_selection.len()].effective_value;
610                }
611
612                if current_selection.last_mut().is_none() {
613                    // We have walked back to the first utxo and no branch is untraversed. All solutions searched
614                    // If best selection is empty, then there's no exact match
615                    if best_selection.is_empty() {
616                        return Err(Error::BnBNoExactMatch);
617                    }
618                    break;
619                }
620
621                if let Some(c) = current_selection.last_mut() {
622                    // Output was included on previous iterations, try excluding now.
623                    *c = false;
624                }
625
626                let utxo = &optional_utxos[current_selection.len() - 1];
627                curr_value -= utxo.effective_value;
628            } else {
629                // Moving forwards, continuing down this branch
630                let utxo = &optional_utxos[current_selection.len()];
631
632                // Remove this utxo from the curr_available_value utxo amount
633                curr_available_value -= utxo.effective_value;
634
635                // Inclusion branch first (Largest First Exploration)
636                current_selection.push(true);
637                curr_value += utxo.effective_value;
638            }
639        }
640
641        // Check for solution
642        if best_selection.is_empty() {
643            return Err(Error::BnBTotalTriesExceeded);
644        }
645
646        // Set output set
647        let selected_utxos = optional_utxos
648            .into_iter()
649            .zip(best_selection)
650            .filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None })
651            .collect::<Vec<OutputGroup>>();
652
653        let selected_amount = best_selection_value.unwrap();
654
655        // remaining_amount can't be negative as that would mean the
656        // selection wasn't successful
657        // target_amount = amount_needed + (fee_amount - vin_fees)
658        let remaining_amount = (selected_amount - target_amount) as u64;
659
660        let excess = decide_change(remaining_amount, fee_rate, drain_script);
661
662        Ok(BranchAndBoundCoinSelection::calculate_cs_result(
663            selected_utxos,
664            required_utxos,
665            excess,
666        ))
667    }
668
669    #[allow(clippy::too_many_arguments)]
670    fn single_random_draw(
671        &self,
672        required_utxos: Vec<OutputGroup>,
673        mut optional_utxos: Vec<OutputGroup>,
674        curr_value: i64,
675        target_amount: i64,
676        drain_script: &Script,
677        fee_rate: FeeRate,
678    ) -> CoinSelectionResult {
679        #[cfg(not(test))]
680        optional_utxos.shuffle(&mut thread_rng());
681        #[cfg(test)]
682        {
683            use rand::{rngs::StdRng, SeedableRng};
684            let seed = [0; 32];
685            let mut rng: StdRng = SeedableRng::from_seed(seed);
686            optional_utxos.shuffle(&mut rng);
687        }
688
689        let selected_utxos = optional_utxos.into_iter().fold(
690            (curr_value, vec![]),
691            |(mut amount, mut utxos), utxo| {
692                if amount >= target_amount {
693                    (amount, utxos)
694                } else {
695                    amount += utxo.effective_value;
696                    utxos.push(utxo);
697                    (amount, utxos)
698                }
699            },
700        );
701
702        // remaining_amount can't be negative as that would mean the
703        // selection wasn't successful
704        // target_amount = amount_needed + (fee_amount - vin_fees)
705        let remaining_amount = (selected_utxos.0 - target_amount) as u64;
706
707        let excess = decide_change(remaining_amount, fee_rate, drain_script);
708
709        BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess)
710    }
711
712    fn calculate_cs_result(
713        mut selected_utxos: Vec<OutputGroup>,
714        mut required_utxos: Vec<OutputGroup>,
715        excess: Excess,
716    ) -> CoinSelectionResult {
717        selected_utxos.append(&mut required_utxos);
718        let fee_amount = selected_utxos.iter().map(|u| u.fee).sum::<u64>();
719        let selected = selected_utxos
720            .into_iter()
721            .map(|u| u.weighted_utxo.utxo)
722            .collect::<Vec<_>>();
723
724        CoinSelectionResult {
725            selected,
726            fee_amount,
727            excess,
728        }
729    }
730}
731
732#[cfg(test)]
733mod test {
734    use std::str::FromStr;
735
736    use bitcoin::{OutPoint, ScriptBuf, TxOut};
737
738    use super::*;
739    use crate::database::{BatchOperations, MemoryDatabase};
740    use crate::types::*;
741    use crate::wallet::Vbytes;
742
743    use rand::rngs::StdRng;
744    use rand::seq::SliceRandom;
745    use rand::{Rng, SeedableRng};
746
747    // n. of items on witness (1WU) + signature len (1WU) + signature and sighash (72WU)
748    // + pubkey len (1WU) + pubkey (33WU) + script sig len (1 byte, 4WU)
749    const P2WPKH_SATISFACTION_SIZE: usize = 1 + 1 + 72 + 1 + 33 + 4;
750
751    const FEE_AMOUNT: u64 = 50;
752
753    fn utxo(value: u64, index: u32) -> WeightedUtxo {
754        assert!(index < 10);
755        let outpoint = OutPoint::from_str(&format!(
756            "000000000000000000000000000000000000000000000000000000000000000{}:0",
757            index
758        ))
759        .unwrap();
760        WeightedUtxo {
761            satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
762            utxo: Utxo::Local(LocalUtxo {
763                outpoint,
764                txout: TxOut {
765                    value,
766                    script_pubkey: ScriptBuf::new(),
767                },
768                keychain: KeychainKind::External,
769                is_spent: false,
770            }),
771        }
772    }
773
774    fn get_test_utxos() -> Vec<WeightedUtxo> {
775        vec![utxo(100_000, 0), utxo(FEE_AMOUNT - 40, 1), utxo(200_000, 2)]
776    }
777
778    fn setup_database_and_get_oldest_first_test_utxos<D: Database>(
779        database: &mut D,
780    ) -> Vec<WeightedUtxo> {
781        // ensure utxos are from different tx
782        let utxo1 = utxo(120_000, 1);
783        let utxo2 = utxo(80_000, 2);
784        let utxo3 = utxo(300_000, 3);
785
786        // add tx to DB so utxos are sorted by blocktime asc
787        // utxos will be selected by the following order
788        // utxo1(blockheight 1) -> utxo2(blockheight 2), utxo3 (blockheight 3)
789        // timestamp are all set as the same to ensure that only block height is used in sorting
790        let utxo1_tx_details = TransactionDetails {
791            transaction: None,
792            txid: utxo1.utxo.outpoint().txid,
793            received: 1,
794            sent: 0,
795            fee: None,
796            confirmation_time: Some(BlockTime {
797                height: 1,
798                timestamp: 1231006505,
799            }),
800        };
801
802        let utxo2_tx_details = TransactionDetails {
803            transaction: None,
804            txid: utxo2.utxo.outpoint().txid,
805            received: 1,
806            sent: 0,
807            fee: None,
808            confirmation_time: Some(BlockTime {
809                height: 2,
810                timestamp: 1231006505,
811            }),
812        };
813
814        let utxo3_tx_details = TransactionDetails {
815            transaction: None,
816            txid: utxo3.utxo.outpoint().txid,
817            received: 1,
818            sent: 0,
819            fee: None,
820            confirmation_time: Some(BlockTime {
821                height: 3,
822                timestamp: 1231006505,
823            }),
824        };
825
826        database.set_tx(&utxo1_tx_details).unwrap();
827        database.set_tx(&utxo2_tx_details).unwrap();
828        database.set_tx(&utxo3_tx_details).unwrap();
829
830        vec![utxo1, utxo2, utxo3]
831    }
832
833    fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
834        let mut res = Vec::new();
835        for _ in 0..utxos_number {
836            res.push(WeightedUtxo {
837                satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
838                utxo: Utxo::Local(LocalUtxo {
839                    outpoint: OutPoint::from_str(
840                        "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
841                    )
842                    .unwrap(),
843                    txout: TxOut {
844                        value: rng.gen_range(0..200000000),
845                        script_pubkey: ScriptBuf::new(),
846                    },
847                    keychain: KeychainKind::External,
848                    is_spent: false,
849                }),
850            });
851        }
852        res
853    }
854
855    fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
856        let utxo = WeightedUtxo {
857            satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
858            utxo: Utxo::Local(LocalUtxo {
859                outpoint: OutPoint::from_str(
860                    "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
861                )
862                .unwrap(),
863                txout: TxOut {
864                    value: utxos_value,
865                    script_pubkey: ScriptBuf::new(),
866                },
867                keychain: KeychainKind::External,
868                is_spent: false,
869            }),
870        };
871        vec![utxo; utxos_number]
872    }
873
874    fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut [WeightedUtxo]) -> u64 {
875        let utxos_picked_len = rng.gen_range(2..utxos.len() / 2);
876        utxos.shuffle(&mut rng);
877        utxos[..utxos_picked_len]
878            .iter()
879            .map(|u| u.utxo.txout().value)
880            .sum()
881    }
882
883    #[test]
884    fn test_largest_first_coin_selection_success() {
885        let utxos = get_test_utxos();
886        let database = MemoryDatabase::default();
887        let drain_script = ScriptBuf::default();
888        let target_amount = 250_000 + FEE_AMOUNT;
889
890        let result = LargestFirstCoinSelection
891            .coin_select(
892                &database,
893                utxos,
894                vec![],
895                FeeRate::from_sat_per_vb(1.0),
896                target_amount,
897                &drain_script,
898            )
899            .unwrap();
900
901        assert_eq!(result.selected.len(), 3);
902        assert_eq!(result.selected_amount(), 300_010);
903        assert_eq!(result.fee_amount, 204)
904    }
905
906    #[test]
907    fn test_largest_first_coin_selection_use_all() {
908        let utxos = get_test_utxos();
909        let database = MemoryDatabase::default();
910        let drain_script = ScriptBuf::default();
911        let target_amount = 20_000 + FEE_AMOUNT;
912
913        let result = LargestFirstCoinSelection
914            .coin_select(
915                &database,
916                utxos,
917                vec![],
918                FeeRate::from_sat_per_vb(1.0),
919                target_amount,
920                &drain_script,
921            )
922            .unwrap();
923
924        assert_eq!(result.selected.len(), 3);
925        assert_eq!(result.selected_amount(), 300_010);
926        assert_eq!(result.fee_amount, 204);
927    }
928
929    #[test]
930    fn test_largest_first_coin_selection_use_only_necessary() {
931        let utxos = get_test_utxos();
932        let database = MemoryDatabase::default();
933        let drain_script = ScriptBuf::default();
934        let target_amount = 20_000 + FEE_AMOUNT;
935
936        let result = LargestFirstCoinSelection
937            .coin_select(
938                &database,
939                vec![],
940                utxos,
941                FeeRate::from_sat_per_vb(1.0),
942                target_amount,
943                &drain_script,
944            )
945            .unwrap();
946
947        assert_eq!(result.selected.len(), 1);
948        assert_eq!(result.selected_amount(), 200_000);
949        assert_eq!(result.fee_amount, 68);
950    }
951
952    #[test]
953    #[should_panic(expected = "InsufficientFunds")]
954    fn test_largest_first_coin_selection_insufficient_funds() {
955        let utxos = get_test_utxos();
956        let database = MemoryDatabase::default();
957        let drain_script = ScriptBuf::default();
958        let target_amount = 500_000 + FEE_AMOUNT;
959
960        LargestFirstCoinSelection
961            .coin_select(
962                &database,
963                vec![],
964                utxos,
965                FeeRate::from_sat_per_vb(1.0),
966                target_amount,
967                &drain_script,
968            )
969            .unwrap();
970    }
971
972    #[test]
973    #[should_panic(expected = "InsufficientFunds")]
974    fn test_largest_first_coin_selection_insufficient_funds_high_fees() {
975        let utxos = get_test_utxos();
976        let database = MemoryDatabase::default();
977        let drain_script = ScriptBuf::default();
978        let target_amount = 250_000 + FEE_AMOUNT;
979
980        LargestFirstCoinSelection
981            .coin_select(
982                &database,
983                vec![],
984                utxos,
985                FeeRate::from_sat_per_vb(1000.0),
986                target_amount,
987                &drain_script,
988            )
989            .unwrap();
990    }
991
992    #[test]
993    fn test_oldest_first_coin_selection_success() {
994        let mut database = MemoryDatabase::default();
995        let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
996        let drain_script = ScriptBuf::default();
997        let target_amount = 180_000 + FEE_AMOUNT;
998
999        let result = OldestFirstCoinSelection
1000            .coin_select(
1001                &database,
1002                vec![],
1003                utxos,
1004                FeeRate::from_sat_per_vb(1.0),
1005                target_amount,
1006                &drain_script,
1007            )
1008            .unwrap();
1009
1010        assert_eq!(result.selected.len(), 2);
1011        assert_eq!(result.selected_amount(), 200_000);
1012        assert_eq!(result.fee_amount, 136)
1013    }
1014
1015    #[test]
1016    fn test_oldest_first_coin_selection_utxo_not_in_db_will_be_selected_last() {
1017        // ensure utxos are from different tx
1018        let utxo1 = utxo(120_000, 1);
1019        let utxo2 = utxo(80_000, 2);
1020        let utxo3 = utxo(300_000, 3);
1021        let drain_script = ScriptBuf::default();
1022
1023        let mut database = MemoryDatabase::default();
1024
1025        // add tx to DB so utxos are sorted by blocktime asc
1026        // utxos will be selected by the following order
1027        // utxo1(blockheight 1) -> utxo2(blockheight 2), utxo3 (not exist in DB)
1028        // timestamp are all set as the same to ensure that only block height is used in sorting
1029        let utxo1_tx_details = TransactionDetails {
1030            transaction: None,
1031            txid: utxo1.utxo.outpoint().txid,
1032            received: 1,
1033            sent: 0,
1034            fee: None,
1035            confirmation_time: Some(BlockTime {
1036                height: 1,
1037                timestamp: 1231006505,
1038            }),
1039        };
1040
1041        let utxo2_tx_details = TransactionDetails {
1042            transaction: None,
1043            txid: utxo2.utxo.outpoint().txid,
1044            received: 1,
1045            sent: 0,
1046            fee: None,
1047            confirmation_time: Some(BlockTime {
1048                height: 2,
1049                timestamp: 1231006505,
1050            }),
1051        };
1052
1053        database.set_tx(&utxo1_tx_details).unwrap();
1054        database.set_tx(&utxo2_tx_details).unwrap();
1055
1056        let target_amount = 180_000 + FEE_AMOUNT;
1057
1058        let result = OldestFirstCoinSelection
1059            .coin_select(
1060                &database,
1061                vec![],
1062                vec![utxo3, utxo1, utxo2],
1063                FeeRate::from_sat_per_vb(1.0),
1064                target_amount,
1065                &drain_script,
1066            )
1067            .unwrap();
1068
1069        assert_eq!(result.selected.len(), 2);
1070        assert_eq!(result.selected_amount(), 200_000);
1071        assert_eq!(result.fee_amount, 136)
1072    }
1073
1074    #[test]
1075    fn test_oldest_first_coin_selection_use_all() {
1076        let mut database = MemoryDatabase::default();
1077        let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
1078        let drain_script = ScriptBuf::default();
1079        let target_amount = 20_000 + FEE_AMOUNT;
1080
1081        let result = OldestFirstCoinSelection
1082            .coin_select(
1083                &database,
1084                utxos,
1085                vec![],
1086                FeeRate::from_sat_per_vb(1.0),
1087                target_amount,
1088                &drain_script,
1089            )
1090            .unwrap();
1091
1092        assert_eq!(result.selected.len(), 3);
1093        assert_eq!(result.selected_amount(), 500_000);
1094        assert_eq!(result.fee_amount, 204);
1095    }
1096
1097    #[test]
1098    fn test_oldest_first_coin_selection_use_only_necessary() {
1099        let mut database = MemoryDatabase::default();
1100        let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
1101        let drain_script = ScriptBuf::default();
1102        let target_amount = 20_000 + FEE_AMOUNT;
1103
1104        let result = OldestFirstCoinSelection
1105            .coin_select(
1106                &database,
1107                vec![],
1108                utxos,
1109                FeeRate::from_sat_per_vb(1.0),
1110                target_amount,
1111                &drain_script,
1112            )
1113            .unwrap();
1114
1115        assert_eq!(result.selected.len(), 1);
1116        assert_eq!(result.selected_amount(), 120_000);
1117        assert_eq!(result.fee_amount, 68);
1118    }
1119
1120    #[test]
1121    #[should_panic(expected = "InsufficientFunds")]
1122    fn test_oldest_first_coin_selection_insufficient_funds() {
1123        let mut database = MemoryDatabase::default();
1124        let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
1125        let drain_script = ScriptBuf::default();
1126        let target_amount = 600_000 + FEE_AMOUNT;
1127
1128        OldestFirstCoinSelection
1129            .coin_select(
1130                &database,
1131                vec![],
1132                utxos,
1133                FeeRate::from_sat_per_vb(1.0),
1134                target_amount,
1135                &drain_script,
1136            )
1137            .unwrap();
1138    }
1139
1140    #[test]
1141    #[should_panic(expected = "InsufficientFunds")]
1142    fn test_oldest_first_coin_selection_insufficient_funds_high_fees() {
1143        let mut database = MemoryDatabase::default();
1144        let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
1145
1146        let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::<u64>() - 50;
1147        let drain_script = ScriptBuf::default();
1148
1149        OldestFirstCoinSelection
1150            .coin_select(
1151                &database,
1152                vec![],
1153                utxos,
1154                FeeRate::from_sat_per_vb(1000.0),
1155                target_amount,
1156                &drain_script,
1157            )
1158            .unwrap();
1159    }
1160
1161    #[test]
1162    fn test_bnb_coin_selection_success() {
1163        // In this case bnb won't find a suitable match and single random draw will
1164        // select three outputs
1165        let utxos = generate_same_value_utxos(100_000, 20);
1166
1167        let database = MemoryDatabase::default();
1168        let drain_script = ScriptBuf::default();
1169
1170        let target_amount = 250_000 + FEE_AMOUNT;
1171
1172        let result = BranchAndBoundCoinSelection::default()
1173            .coin_select(
1174                &database,
1175                vec![],
1176                utxos,
1177                FeeRate::from_sat_per_vb(1.0),
1178                target_amount,
1179                &drain_script,
1180            )
1181            .unwrap();
1182
1183        assert_eq!(result.selected.len(), 3);
1184        assert_eq!(result.selected_amount(), 300_000);
1185        assert_eq!(result.fee_amount, 204);
1186    }
1187
1188    #[test]
1189    fn test_bnb_coin_selection_required_are_enough() {
1190        let utxos = get_test_utxos();
1191        let database = MemoryDatabase::default();
1192        let drain_script = ScriptBuf::default();
1193        let target_amount = 20_000 + FEE_AMOUNT;
1194
1195        let result = BranchAndBoundCoinSelection::default()
1196            .coin_select(
1197                &database,
1198                utxos.clone(),
1199                utxos,
1200                FeeRate::from_sat_per_vb(1.0),
1201                target_amount,
1202                &drain_script,
1203            )
1204            .unwrap();
1205
1206        assert_eq!(result.selected.len(), 3);
1207        assert_eq!(result.selected_amount(), 300_010);
1208        assert_eq!(result.fee_amount, 204);
1209    }
1210
1211    #[test]
1212    fn test_bnb_coin_selection_optional_are_enough() {
1213        let utxos = get_test_utxos();
1214        let database = MemoryDatabase::default();
1215        let drain_script = ScriptBuf::default();
1216        let target_amount = 299756 + FEE_AMOUNT;
1217
1218        let result = BranchAndBoundCoinSelection::default()
1219            .coin_select(
1220                &database,
1221                vec![],
1222                utxos,
1223                FeeRate::from_sat_per_vb(1.0),
1224                target_amount,
1225                &drain_script,
1226            )
1227            .unwrap();
1228
1229        assert_eq!(result.selected.len(), 2);
1230        assert_eq!(result.selected_amount(), 300000);
1231        assert_eq!(result.fee_amount, 136);
1232    }
1233
1234    #[test]
1235    #[ignore]
1236    fn test_bnb_coin_selection_required_not_enough() {
1237        let utxos = get_test_utxos();
1238        let database = MemoryDatabase::default();
1239
1240        let required = vec![utxos[0].clone()];
1241        let mut optional = utxos[1..].to_vec();
1242        optional.push(utxo(500_000, 3));
1243
1244        // Defensive assertions, for sanity and in case someone changes the test utxos vector.
1245        let amount: u64 = required.iter().map(|u| u.utxo.txout().value).sum();
1246        assert_eq!(amount, 100_000);
1247        let amount: u64 = optional.iter().map(|u| u.utxo.txout().value).sum();
1248        assert!(amount > 150_000);
1249        let drain_script = ScriptBuf::default();
1250
1251        let target_amount = 150_000 + FEE_AMOUNT;
1252
1253        let result = BranchAndBoundCoinSelection::default()
1254            .coin_select(
1255                &database,
1256                required,
1257                optional,
1258                FeeRate::from_sat_per_vb(1.0),
1259                target_amount,
1260                &drain_script,
1261            )
1262            .unwrap();
1263
1264        assert_eq!(result.selected.len(), 2);
1265        assert_eq!(result.selected_amount(), 300_000);
1266        assert_eq!(result.fee_amount, 136);
1267    }
1268
1269    #[test]
1270    #[should_panic(expected = "InsufficientFunds")]
1271    fn test_bnb_coin_selection_insufficient_funds() {
1272        let utxos = get_test_utxos();
1273        let database = MemoryDatabase::default();
1274        let drain_script = ScriptBuf::default();
1275        let target_amount = 500_000 + FEE_AMOUNT;
1276
1277        BranchAndBoundCoinSelection::default()
1278            .coin_select(
1279                &database,
1280                vec![],
1281                utxos,
1282                FeeRate::from_sat_per_vb(1.0),
1283                target_amount,
1284                &drain_script,
1285            )
1286            .unwrap();
1287    }
1288
1289    #[test]
1290    #[should_panic(expected = "InsufficientFunds")]
1291    fn test_bnb_coin_selection_insufficient_funds_high_fees() {
1292        let utxos = get_test_utxos();
1293        let database = MemoryDatabase::default();
1294        let drain_script = ScriptBuf::default();
1295        let target_amount = 250_000 + FEE_AMOUNT;
1296
1297        BranchAndBoundCoinSelection::default()
1298            .coin_select(
1299                &database,
1300                vec![],
1301                utxos,
1302                FeeRate::from_sat_per_vb(1000.0),
1303                target_amount,
1304                &drain_script,
1305            )
1306            .unwrap();
1307    }
1308
1309    #[test]
1310    fn test_bnb_coin_selection_check_fee_rate() {
1311        let utxos = get_test_utxos();
1312        let database = MemoryDatabase::default();
1313        let drain_script = ScriptBuf::default();
1314        let target_amount = 99932; // first utxo's effective value
1315
1316        let result = BranchAndBoundCoinSelection::new(0)
1317            .coin_select(
1318                &database,
1319                vec![],
1320                utxos,
1321                FeeRate::from_sat_per_vb(1.0),
1322                target_amount,
1323                &drain_script,
1324            )
1325            .unwrap();
1326
1327        assert_eq!(result.selected.len(), 1);
1328        assert_eq!(result.selected_amount(), 100_000);
1329        let input_size = (TXIN_BASE_WEIGHT + P2WPKH_SATISFACTION_SIZE).vbytes();
1330        // the final fee rate should be exactly the same as the fee rate given
1331        assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < f32::EPSILON);
1332    }
1333
1334    #[test]
1335    fn test_bnb_coin_selection_exact_match() {
1336        let seed = [0; 32];
1337        let mut rng: StdRng = SeedableRng::from_seed(seed);
1338        let database = MemoryDatabase::default();
1339
1340        for _i in 0..200 {
1341            let mut optional_utxos = generate_random_utxos(&mut rng, 16);
1342            let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos);
1343            let drain_script = ScriptBuf::default();
1344            let result = BranchAndBoundCoinSelection::new(0)
1345                .coin_select(
1346                    &database,
1347                    vec![],
1348                    optional_utxos,
1349                    FeeRate::from_sat_per_vb(0.0),
1350                    target_amount,
1351                    &drain_script,
1352                )
1353                .unwrap();
1354            assert_eq!(result.selected_amount(), target_amount);
1355        }
1356    }
1357
1358    #[test]
1359    #[should_panic(expected = "BnBNoExactMatch")]
1360    fn test_bnb_function_no_exact_match() {
1361        let fee_rate = FeeRate::from_sat_per_vb(10.0);
1362        let utxos: Vec<OutputGroup> = get_test_utxos()
1363            .into_iter()
1364            .map(|u| OutputGroup::new(u, fee_rate))
1365            .collect();
1366
1367        let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
1368
1369        let size_of_change = 31;
1370        let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
1371
1372        let drain_script = ScriptBuf::default();
1373        let target_amount = 20_000 + FEE_AMOUNT;
1374        BranchAndBoundCoinSelection::new(size_of_change)
1375            .bnb(
1376                vec![],
1377                utxos,
1378                0,
1379                curr_available_value,
1380                target_amount as i64,
1381                cost_of_change,
1382                &drain_script,
1383                fee_rate,
1384            )
1385            .unwrap();
1386    }
1387
1388    #[test]
1389    #[should_panic(expected = "BnBTotalTriesExceeded")]
1390    fn test_bnb_function_tries_exceeded() {
1391        let fee_rate = FeeRate::from_sat_per_vb(10.0);
1392        let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
1393            .into_iter()
1394            .map(|u| OutputGroup::new(u, fee_rate))
1395            .collect();
1396
1397        let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
1398
1399        let size_of_change = 31;
1400        let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
1401        let target_amount = 20_000 + FEE_AMOUNT;
1402
1403        let drain_script = ScriptBuf::default();
1404
1405        BranchAndBoundCoinSelection::new(size_of_change)
1406            .bnb(
1407                vec![],
1408                utxos,
1409                0,
1410                curr_available_value,
1411                target_amount as i64,
1412                cost_of_change,
1413                &drain_script,
1414                fee_rate,
1415            )
1416            .unwrap();
1417    }
1418
1419    // The match won't be exact but still in the range
1420    #[test]
1421    fn test_bnb_function_almost_exact_match_with_fees() {
1422        let fee_rate = FeeRate::from_sat_per_vb(1.0);
1423        let size_of_change = 31;
1424        let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
1425
1426        let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
1427            .into_iter()
1428            .map(|u| OutputGroup::new(u, fee_rate))
1429            .collect();
1430
1431        let curr_value = 0;
1432
1433        let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
1434
1435        // 2*(value of 1 utxo)  - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
1436        // cost_of_change + 5.
1437        let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as i64 + 5;
1438
1439        let drain_script = ScriptBuf::default();
1440
1441        let result = BranchAndBoundCoinSelection::new(size_of_change)
1442            .bnb(
1443                vec![],
1444                utxos,
1445                curr_value,
1446                curr_available_value,
1447                target_amount,
1448                cost_of_change,
1449                &drain_script,
1450                fee_rate,
1451            )
1452            .unwrap();
1453        assert_eq!(result.selected_amount(), 100_000);
1454        assert_eq!(result.fee_amount, 136);
1455    }
1456
1457    // TODO: bnb() function should be optimized, and this test should be done with more utxos
1458    #[test]
1459    fn test_bnb_function_exact_match_more_utxos() {
1460        let seed = [0; 32];
1461        let mut rng: StdRng = SeedableRng::from_seed(seed);
1462        let fee_rate = FeeRate::from_sat_per_vb(0.0);
1463
1464        for _ in 0..200 {
1465            let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
1466                .into_iter()
1467                .map(|u| OutputGroup::new(u, fee_rate))
1468                .collect();
1469
1470            let curr_value = 0;
1471
1472            let curr_available_value = optional_utxos
1473                .iter()
1474                .fold(0, |acc, x| acc + x.effective_value);
1475
1476            let target_amount =
1477                optional_utxos[3].effective_value + optional_utxos[23].effective_value;
1478
1479            let drain_script = ScriptBuf::default();
1480
1481            let result = BranchAndBoundCoinSelection::new(0)
1482                .bnb(
1483                    vec![],
1484                    optional_utxos,
1485                    curr_value,
1486                    curr_available_value,
1487                    target_amount,
1488                    0.0,
1489                    &drain_script,
1490                    fee_rate,
1491                )
1492                .unwrap();
1493            assert_eq!(result.selected_amount(), target_amount as u64);
1494        }
1495    }
1496
1497    #[test]
1498    fn test_single_random_draw_function_success() {
1499        let seed = [0; 32];
1500        let mut rng: StdRng = SeedableRng::from_seed(seed);
1501        let mut utxos = generate_random_utxos(&mut rng, 300);
1502        let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
1503
1504        let fee_rate = FeeRate::from_sat_per_vb(1.0);
1505        let utxos: Vec<OutputGroup> = utxos
1506            .into_iter()
1507            .map(|u| OutputGroup::new(u, fee_rate))
1508            .collect();
1509
1510        let drain_script = ScriptBuf::default();
1511
1512        let result = BranchAndBoundCoinSelection::default().single_random_draw(
1513            vec![],
1514            utxos,
1515            0,
1516            target_amount as i64,
1517            &drain_script,
1518            fee_rate,
1519        );
1520
1521        assert!(result.selected_amount() > target_amount);
1522        assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64);
1523    }
1524
1525    #[test]
1526    fn test_bnb_exclude_negative_effective_value() {
1527        let utxos = get_test_utxos();
1528        let database = MemoryDatabase::default();
1529        let drain_script = ScriptBuf::default();
1530
1531        let selection = BranchAndBoundCoinSelection::default().coin_select(
1532            &database,
1533            vec![],
1534            utxos,
1535            FeeRate::from_sat_per_vb(10.0),
1536            500_000,
1537            &drain_script,
1538        );
1539
1540        assert_matches!(
1541            selection,
1542            Err(Error::InsufficientFunds {
1543                available: 300_000,
1544                ..
1545            })
1546        );
1547    }
1548
1549    #[test]
1550    fn test_bnb_include_negative_effective_value_when_required() {
1551        let utxos = get_test_utxos();
1552        let database = MemoryDatabase::default();
1553        let drain_script = ScriptBuf::default();
1554
1555        let (required, optional) = utxos
1556            .into_iter()
1557            .partition(|u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value < 1000));
1558
1559        let selection = BranchAndBoundCoinSelection::default().coin_select(
1560            &database,
1561            required,
1562            optional,
1563            FeeRate::from_sat_per_vb(10.0),
1564            500_000,
1565            &drain_script,
1566        );
1567
1568        assert_matches!(
1569            selection,
1570            Err(Error::InsufficientFunds {
1571                available: 300_010,
1572                ..
1573            })
1574        );
1575    }
1576
1577    #[test]
1578    fn test_bnb_sum_of_effective_value_negative() {
1579        let utxos = get_test_utxos();
1580        let database = MemoryDatabase::default();
1581        let drain_script = ScriptBuf::default();
1582
1583        let selection = BranchAndBoundCoinSelection::default().coin_select(
1584            &database,
1585            utxos,
1586            vec![],
1587            FeeRate::from_sat_per_vb(10_000.0),
1588            500_000,
1589            &drain_script,
1590        );
1591
1592        assert_matches!(
1593            selection,
1594            Err(Error::InsufficientFunds {
1595                available: 300_010,
1596                ..
1597            })
1598        );
1599    }
1600}