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