• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bitcoindevkit / bdk / 5834188079

pending completion
5834188079

Pull #1071

github

web-flow
Merge 68b42331c into 0ba6bbe11
Pull Request #1071: Update rust bitcoin (BDK 0.28)

563 of 563 new or added lines in 28 files covered. (100.0%)

14625 of 18342 relevant lines covered (79.74%)

9267.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.43
/src/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

102
use crate::types::FeeRate;
103
use crate::wallet::utils::IsDust;
104
use crate::{database::Database, WeightedUtxo};
105
use crate::{error::Error, Utxo};
106

107
use bitcoin::consensus::encode::serialize;
108
use bitcoin::{Script, Weight};
109

110
#[cfg(test)]
111
use assert_matches::assert_matches;
112
use rand::seq::SliceRandom;
113
#[cfg(not(test))]
114
use rand::thread_rng;
115
use std::collections::HashMap;
116
use std::convert::TryInto;
117

118
/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
119
/// overridden
120
#[cfg(not(test))]
121
pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
122
#[cfg(test)]
123
pub type DefaultCoinSelectionAlgorithm = LargestFirstCoinSelection; // make the tests more predictable
124

125
// Base weight of a Txin, not counting the weight needed for satisfying it.
126
// prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes)
127
pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
128

129
#[derive(Debug)]
×
130
/// Remaining amount after performing coin selection
131
pub enum Excess {
132
    /// It's not possible to create spendable output from excess using the current drain output
133
    NoChange {
134
        /// Threshold to consider amount as dust for this particular change script_pubkey
135
        dust_threshold: u64,
136
        /// Exceeding amount of current selection over outgoing value and fee costs
137
        remaining_amount: u64,
138
        /// The calculated fee for the drain TxOut with the selected script_pubkey
139
        change_fee: u64,
140
    },
141
    /// It's possible to create spendable output from excess using the current drain output
142
    Change {
143
        /// Effective amount available to create change after deducting the change output fee
144
        amount: u64,
145
        /// The deducted change output fee
146
        fee: u64,
147
    },
148
}
149

150
/// Result of a successful coin selection
151
#[derive(Debug)]
×
152
pub struct CoinSelectionResult {
153
    /// List of outputs selected for use as inputs
154
    pub selected: Vec<Utxo>,
155
    /// Total fee amount for the selected utxos in satoshis
156
    pub fee_amount: u64,
157
    /// Remaining amount after deducing fees and outgoing outputs
158
    pub excess: Excess,
159
}
160

161
impl CoinSelectionResult {
162
    /// The total value of the inputs selected.
163
    pub fn selected_amount(&self) -> u64 {
826✔
164
        self.selected.iter().map(|u| u.txout().value).sum()
3,103✔
165
    }
826✔
166

167
    /// The total value of the inputs selected from the local wallet.
168
    pub fn local_selected_amount(&self) -> u64 {
439✔
169
        self.selected
439✔
170
            .iter()
439✔
171
            .filter_map(|u| match u {
651✔
172
                Utxo::Local(_) => Some(u.txout().value),
467✔
173
                _ => None,
10✔
174
            })
651✔
175
            .sum()
439✔
176
    }
439✔
177
}
178

179
/// Trait for generalized coin selection algorithms
180
///
181
/// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin
182
/// selection algorithm when it creates transactions.
183
///
184
/// For an example see [this module](crate::wallet::coin_selection)'s documentation.
185
pub trait CoinSelectionAlgorithm<D: Database>: std::fmt::Debug {
186
    /// Perform the coin selection
187
    ///
188
    /// - `database`: a reference to the wallet's database that can be used to lookup additional
189
    ///               details for a specific UTXO
190
    /// - `required_utxos`: the utxos that must be spent regardless of `target_amount` with their
191
    ///                     weight cost
192
    /// - `optional_utxos`: the remaining available utxos to satisfy `target_amount` with their
193
    ///                     weight cost
194
    /// - `fee_rate`: fee rate to use
195
    /// - `target_amount`: the outgoing amount in satoshis and the fees already
196
    ///                    accumulated from added outputs and transaction’s header.
197
    /// - `drain_script`: the script to use in case of change
198
    #[allow(clippy::too_many_arguments)]
199
    fn coin_select(
200
        &self,
201
        database: &D,
202
        required_utxos: Vec<WeightedUtxo>,
203
        optional_utxos: Vec<WeightedUtxo>,
204
        fee_rate: FeeRate,
205
        target_amount: u64,
206
        drain_script: &Script,
207
    ) -> Result<CoinSelectionResult, Error>;
208
}
209

210
/// Simple and dumb coin selection
211
///
212
/// This coin selection algorithm sorts the available UTXOs by value and then picks them starting
213
/// from the largest ones until the required amount is reached.
214
#[derive(Debug, Default, Clone, Copy)]
394✔
215
pub struct LargestFirstCoinSelection;
216

217
impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
218
    fn coin_select(
362✔
219
        &self,
362✔
220
        _database: &D,
362✔
221
        required_utxos: Vec<WeightedUtxo>,
362✔
222
        mut optional_utxos: Vec<WeightedUtxo>,
362✔
223
        fee_rate: FeeRate,
362✔
224
        target_amount: u64,
362✔
225
        drain_script: &Script,
362✔
226
    ) -> Result<CoinSelectionResult, Error> {
362✔
227
        log::debug!(
362✔
228
            "target_amount = `{}`, fee_rate = `{:?}`",
×
229
            target_amount,
230
            fee_rate
231
        );
232

233
        // We put the "required UTXOs" first and make sure the optional UTXOs are sorted,
234
        // initially smallest to largest, before being reversed with `.rev()`.
235
        let utxos = {
362✔
236
            optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value);
362✔
237
            required_utxos
362✔
238
                .into_iter()
362✔
239
                .map(|utxo| (true, utxo))
362✔
240
                .chain(optional_utxos.into_iter().rev().map(|utxo| (false, utxo)))
362✔
241
        };
362✔
242

362✔
243
        select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
362✔
244
    }
362✔
245
}
246

247
/// OldestFirstCoinSelection always picks the utxo with the smallest blockheight to add to the selected coins next
248
///
249
/// This coin selection algorithm sorts the available UTXOs by blockheight and then picks them starting
250
/// from the oldest ones until the required amount is reached.
251
#[derive(Debug, Default, Clone, Copy)]
12✔
252
pub struct OldestFirstCoinSelection;
253

254
impl<D: Database> CoinSelectionAlgorithm<D> for OldestFirstCoinSelection {
255
    fn coin_select(
12✔
256
        &self,
12✔
257
        database: &D,
12✔
258
        required_utxos: Vec<WeightedUtxo>,
12✔
259
        mut optional_utxos: Vec<WeightedUtxo>,
12✔
260
        fee_rate: FeeRate,
12✔
261
        target_amount: u64,
12✔
262
        drain_script: &Script,
12✔
263
    ) -> Result<CoinSelectionResult, Error> {
12✔
264
        // query db and create a blockheight lookup table
265
        let blockheights = optional_utxos
12✔
266
            .iter()
12✔
267
            .map(|wu| wu.utxo.outpoint().txid)
30✔
268
            // fold is used so we can skip db query for txid that already exist in hashmap acc
12✔
269
            .fold(Ok(HashMap::new()), |bh_result_acc, txid| {
30✔
270
                bh_result_acc.and_then(|mut bh_acc| {
30✔
271
                    if bh_acc.contains_key(&txid) {
30✔
272
                        Ok(bh_acc)
×
273
                    } else {
274
                        database.get_tx(&txid, false).map(|details| {
30✔
275
                            bh_acc.insert(
30✔
276
                                txid,
30✔
277
                                details.and_then(|d| d.confirmation_time.map(|ct| ct.height)),
30✔
278
                            );
30✔
279
                            bh_acc
30✔
280
                        })
30✔
281
                    }
282
                })
30✔
283
            })?;
30✔
284

285
        // We put the "required UTXOs" first and make sure the optional UTXOs are sorted from
286
        // oldest to newest according to blocktime
287
        // For utxo that doesn't exist in DB, they will have lowest priority to be selected
288
        let utxos = {
12✔
289
            optional_utxos.sort_unstable_by_key(|wu| {
12✔
290
                match blockheights.get(&wu.utxo.outpoint().txid) {
44✔
291
                    Some(Some(blockheight)) => blockheight,
40✔
292
                    _ => &u32::MAX,
4✔
293
                }
294
            });
44✔
295

12✔
296
            required_utxos
12✔
297
                .into_iter()
12✔
298
                .map(|utxo| (true, utxo))
12✔
299
                .chain(optional_utxos.into_iter().map(|utxo| (false, utxo)))
28✔
300
        };
12✔
301

12✔
302
        select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
12✔
303
    }
12✔
304
}
305

306
/// Decide if change can be created
307
///
308
/// - `remaining_amount`: the amount in which the selected coins exceed the target amount
309
/// - `fee_rate`: required fee rate for the current selection
310
/// - `drain_script`: script to consider change creation
311
pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess {
1,271✔
312
    // drain_output_len = size(len(script_pubkey)) + len(script_pubkey) + size(output_value)
1,271✔
313
    let drain_output_len = serialize(drain_script).len() + 8usize;
1,271✔
314
    let change_fee = fee_rate.fee_vb(drain_output_len);
1,271✔
315
    let drain_val = remaining_amount.saturating_sub(change_fee);
1,271✔
316

1,271✔
317
    if drain_val.is_dust(drain_script) {
1,271✔
318
        let dust_threshold = drain_script.dust_value().to_sat();
822✔
319
        Excess::NoChange {
822✔
320
            dust_threshold,
822✔
321
            change_fee,
822✔
322
            remaining_amount,
822✔
323
        }
822✔
324
    } else {
325
        Excess::Change {
449✔
326
            amount: drain_val,
449✔
327
            fee: change_fee,
449✔
328
        }
449✔
329
    }
330
}
1,271✔
331

332
fn select_sorted_utxos(
374✔
333
    utxos: impl Iterator<Item = (bool, WeightedUtxo)>,
374✔
334
    fee_rate: FeeRate,
374✔
335
    target_amount: u64,
374✔
336
    drain_script: &Script,
374✔
337
) -> Result<CoinSelectionResult, Error> {
374✔
338
    let mut selected_amount = 0;
374✔
339
    let mut fee_amount = 0;
374✔
340
    let selected = utxos
374✔
341
        .scan(
374✔
342
            (&mut selected_amount, &mut fee_amount),
374✔
343
            |(selected_amount, fee_amount), (must_use, weighted_utxo)| {
374✔
344
                if must_use || **selected_amount < target_amount + **fee_amount {
476✔
345
                    **fee_amount += fee_rate.fee_wu(Weight::from_wu(
440✔
346
                        (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
440✔
347
                    ));
440✔
348
                    **selected_amount += weighted_utxo.utxo.txout().value;
440✔
349

440✔
350
                    log::debug!(
440✔
351
                        "Selected {}, updated fee_amount = `{}`",
×
352
                        weighted_utxo.utxo.outpoint(),
×
353
                        fee_amount
354
                    );
355

356
                    Some(weighted_utxo.utxo)
440✔
357
                } else {
358
                    None
36✔
359
                }
360
            },
476✔
361
        )
374✔
362
        .collect::<Vec<_>>();
374✔
363

374✔
364
    let amount_needed_with_fees = target_amount + fee_amount;
374✔
365
    if selected_amount < amount_needed_with_fees {
374✔
366
        return Err(Error::InsufficientFunds {
20✔
367
            needed: amount_needed_with_fees,
20✔
368
            available: selected_amount,
20✔
369
        });
20✔
370
    }
354✔
371

354✔
372
    let remaining_amount = selected_amount - amount_needed_with_fees;
354✔
373

354✔
374
    let excess = decide_change(remaining_amount, fee_rate, drain_script);
354✔
375

354✔
376
    Ok(CoinSelectionResult {
354✔
377
        selected,
354✔
378
        fee_amount,
354✔
379
        excess,
354✔
380
    })
354✔
381
}
374✔
382

383
#[derive(Debug, Clone)]
6,448✔
384
// Adds fee information to an UTXO.
385
struct OutputGroup {
386
    weighted_utxo: WeightedUtxo,
387
    // Amount of fees for spending a certain utxo, calculated using a certain FeeRate
388
    fee: u64,
389
    // The effective value of the UTXO, i.e., the utxo value minus the fee for spending it
390
    effective_value: i64,
391
}
392

393
impl OutputGroup {
394
    fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
223,210✔
395
        let fee = fee_rate.fee_wu(Weight::from_wu(
223,210✔
396
            (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
223,210✔
397
        ));
223,210✔
398
        let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64;
223,210✔
399
        OutputGroup {
223,210✔
400
            weighted_utxo,
223,210✔
401
            fee,
223,210✔
402
            effective_value,
223,210✔
403
        }
223,210✔
404
    }
223,210✔
405
}
406

407
/// Branch and bound coin selection
408
///
409
/// Code adapted from Bitcoin Core's implementation and from Mark Erhardt Master's Thesis: <http://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf>
410
#[derive(Debug)]
×
411
pub struct BranchAndBoundCoinSelection {
412
    size_of_change: u64,
413
}
414

415
impl Default for BranchAndBoundCoinSelection {
416
    fn default() -> Self {
138✔
417
        Self {
138✔
418
            // P2WPKH cost of change -> value (8 bytes) + script len (1 bytes) + script (22 bytes)
138✔
419
            size_of_change: 8 + 1 + 22,
138✔
420
        }
138✔
421
    }
138✔
422
}
423

424
impl BranchAndBoundCoinSelection {
425
    /// Create new instance with target size for change output
426
    pub fn new(size_of_change: u64) -> Self {
808✔
427
        Self { size_of_change }
808✔
428
    }
808✔
429
}
430

431
const BNB_TOTAL_TRIES: usize = 100_000;
432

433
impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
434
    fn coin_select(
418✔
435
        &self,
418✔
436
        _database: &D,
418✔
437
        required_utxos: Vec<WeightedUtxo>,
418✔
438
        optional_utxos: Vec<WeightedUtxo>,
418✔
439
        fee_rate: FeeRate,
418✔
440
        target_amount: u64,
418✔
441
        drain_script: &Script,
418✔
442
    ) -> Result<CoinSelectionResult, Error> {
418✔
443
        // Mapping every (UTXO, usize) to an output group
418✔
444
        let required_utxos: Vec<OutputGroup> = required_utxos
418✔
445
            .into_iter()
418✔
446
            .map(|u| OutputGroup::new(u, fee_rate))
418✔
447
            .collect();
418✔
448

418✔
449
        // Mapping every (UTXO, usize) to an output group, filtering UTXOs with a negative
418✔
450
        // effective value
418✔
451
        let optional_utxos: Vec<OutputGroup> = optional_utxos
418✔
452
            .into_iter()
418✔
453
            .map(|u| OutputGroup::new(u, fee_rate))
6,480✔
454
            .filter(|u| u.effective_value.is_positive())
6,480✔
455
            .collect();
418✔
456

418✔
457
        let curr_value = required_utxos
418✔
458
            .iter()
418✔
459
            .fold(0, |acc, x| acc + x.effective_value);
418✔
460

418✔
461
        let curr_available_value = optional_utxos
418✔
462
            .iter()
418✔
463
            .fold(0, |acc, x| acc + x.effective_value);
6,468✔
464

418✔
465
        let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_per_vb();
418✔
466

418✔
467
        // `curr_value` and `curr_available_value` are both the sum of *effective_values* of
418✔
468
        // the UTXOs. For the optional UTXOs (curr_available_value) we filter out UTXOs with
418✔
469
        // negative effective value, so it will always be positive.
418✔
470
        //
418✔
471
        // Since we are required to spend the required UTXOs (curr_value) we have to consider
418✔
472
        // all their effective values, even when negative, which means that curr_value could
418✔
473
        // be negative as well.
418✔
474
        //
418✔
475
        // If the sum of curr_value and curr_available_value is negative or lower than our target,
418✔
476
        // we can immediately exit with an error, as it's guaranteed we will never find a solution
418✔
477
        // if we actually run the BnB.
418✔
478
        let total_value: Result<u64, _> = (curr_available_value + curr_value).try_into();
418✔
479
        match total_value {
416✔
480
            Ok(v) if v >= target_amount => {}
416✔
481
            _ => {
482
                // Assume we spend all the UTXOs we can (all the required + all the optional with
483
                // positive effective value), sum their value and their fee cost.
484
                let (utxo_fees, utxo_value) = required_utxos
10✔
485
                    .iter()
10✔
486
                    .chain(optional_utxos.iter())
10✔
487
                    .fold((0, 0), |(mut fees, mut value), utxo| {
24✔
488
                        fees += utxo.fee;
24✔
489
                        value += utxo.weighted_utxo.utxo.txout().value;
24✔
490

24✔
491
                        (fees, value)
24✔
492
                    });
24✔
493

10✔
494
                // Add to the target the fee cost of the UTXOs
10✔
495
                return Err(Error::InsufficientFunds {
10✔
496
                    needed: target_amount + utxo_fees,
10✔
497
                    available: utxo_value,
10✔
498
                });
10✔
499
            }
500
        }
501

502
        let target_amount = target_amount
408✔
503
            .try_into()
408✔
504
            .expect("Bitcoin amount to fit into i64");
408✔
505

408✔
506
        if curr_value > target_amount {
408✔
507
            // remaining_amount can't be negative as that would mean the
508
            // selection wasn't successful
509
            // target_amount = amount_needed + (fee_amount - vin_fees)
510
            let remaining_amount = (curr_value - target_amount) as u64;
2✔
511

2✔
512
            let excess = decide_change(remaining_amount, fee_rate, drain_script);
2✔
513

2✔
514
            return Ok(BranchAndBoundCoinSelection::calculate_cs_result(
2✔
515
                vec![],
2✔
516
                required_utxos,
2✔
517
                excess,
2✔
518
            ));
2✔
519
        }
406✔
520

406✔
521
        Ok(self
406✔
522
            .bnb(
406✔
523
                required_utxos.clone(),
406✔
524
                optional_utxos.clone(),
406✔
525
                curr_value,
406✔
526
                curr_available_value,
406✔
527
                target_amount,
406✔
528
                cost_of_change,
406✔
529
                drain_script,
406✔
530
                fee_rate,
406✔
531
            )
406✔
532
            .unwrap_or_else(|_| {
406✔
533
                self.single_random_draw(
4✔
534
                    required_utxos,
4✔
535
                    optional_utxos,
4✔
536
                    curr_value,
4✔
537
                    target_amount,
4✔
538
                    drain_script,
4✔
539
                    fee_rate,
4✔
540
                )
4✔
541
            }))
406✔
542
    }
418✔
543
}
544

545
impl BranchAndBoundCoinSelection {
546
    // TODO: make this more Rust-onic :)
547
    // (And perhaps refactor with less arguments?)
548
    #[allow(clippy::too_many_arguments)]
549
    fn bnb(
887✔
550
        &self,
887✔
551
        required_utxos: Vec<OutputGroup>,
887✔
552
        mut optional_utxos: Vec<OutputGroup>,
887✔
553
        mut curr_value: i64,
887✔
554
        mut curr_available_value: i64,
887✔
555
        target_amount: i64,
887✔
556
        cost_of_change: f32,
887✔
557
        drain_script: &Script,
887✔
558
        fee_rate: FeeRate,
887✔
559
    ) -> Result<CoinSelectionResult, Error> {
887✔
560
        // current_selection[i] will contain true if we are using optional_utxos[i],
887✔
561
        // false otherwise. Note that current_selection.len() could be less than
887✔
562
        // optional_utxos.len(), it just means that we still haven't decided if we should keep
887✔
563
        // certain optional_utxos or not.
887✔
564
        let mut current_selection: Vec<bool> = Vec::with_capacity(optional_utxos.len());
887✔
565

887✔
566
        // Sort the utxo_pool
887✔
567
        optional_utxos.sort_unstable_by_key(|a| a.effective_value);
642,389✔
568
        optional_utxos.reverse();
887✔
569

887✔
570
        // Contains the best selection we found
887✔
571
        let mut best_selection = Vec::new();
887✔
572
        let mut best_selection_value = None;
887✔
573

574
        // Depth First search loop for choosing the UTXOs
575
        for _ in 0..BNB_TOTAL_TRIES {
1,885,103✔
576
            // Conditions for starting a backtrack
577
            let mut backtrack = false;
1,885,101✔
578
            // Cannot possibly reach target with the amount remaining in the curr_available_value,
1,885,101✔
579
            // or the selected value is out of range.
1,885,101✔
580
            // Go back and try other branch
1,885,101✔
581
            if curr_value + curr_available_value < target_amount
1,885,101✔
582
                || curr_value > target_amount + cost_of_change as i64
1,611,362✔
583
            {
940,802✔
584
                backtrack = true;
940,802✔
585
            } else if curr_value >= target_amount {
944,374✔
586
                // Selected value is within range, there's no point in going forward. Start
587
                // backtracking
588
                backtrack = true;
892✔
589

892✔
590
                // If we found a solution better than the previous one, or if there wasn't previous
892✔
591
                // solution, update the best solution
892✔
592
                if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() {
892✔
593
                    best_selection = current_selection.clone();
804✔
594
                    best_selection_value = Some(curr_value);
804✔
595
                }
804✔
596

597
                // If we found a perfect match, break here
598
                if curr_value == target_amount {
892✔
599
                    break;
802✔
600
                }
90✔
601
            }
943,407✔
602

603
            // Backtracking, moving backwards
604
            if backtrack {
1,884,299✔
605
                // Walk backwards to find the last included UTXO that still needs to have its omission branch traversed.
606
                while let Some(false) = current_selection.last() {
1,768,065✔
607
                    current_selection.pop();
827,173✔
608
                    curr_available_value += optional_utxos[current_selection.len()].effective_value;
827,173✔
609
                }
827,173✔
610

611
                if current_selection.last_mut().is_none() {
940,892✔
612
                    // We have walked back to the first utxo and no branch is untraversed. All solutions searched
613
                    // If best selection is empty, then there's no exact match
614
                    if best_selection.is_empty() {
83✔
615
                        return Err(Error::BnBNoExactMatch);
81✔
616
                    }
2✔
617
                    break;
2✔
618
                }
940,809✔
619

620
                if let Some(c) = current_selection.last_mut() {
940,809✔
621
                    // Output was included on previous iterations, try excluding now.
940,809✔
622
                    *c = false;
940,809✔
623
                }
940,809✔
624

625
                let utxo = &optional_utxos[current_selection.len() - 1];
940,809✔
626
                curr_value -= utxo.effective_value;
940,809✔
627
            } else {
943,407✔
628
                // Moving forwards, continuing down this branch
943,407✔
629
                let utxo = &optional_utxos[current_selection.len()];
943,407✔
630

943,407✔
631
                // Remove this utxo from the curr_available_value utxo amount
943,407✔
632
                curr_available_value -= utxo.effective_value;
943,407✔
633

943,407✔
634
                // Inclusion branch first (Largest First Exploration)
943,407✔
635
                current_selection.push(true);
943,407✔
636
                curr_value += utxo.effective_value;
943,407✔
637
            }
943,407✔
638
        }
639

640
        // Check for solution
641
        if best_selection.is_empty() {
806✔
642
            return Err(Error::BnBTotalTriesExceeded);
2✔
643
        }
804✔
644

804✔
645
        // Set output set
804✔
646
        let selected_utxos = optional_utxos
804✔
647
            .into_iter()
804✔
648
            .zip(best_selection)
804✔
649
            .filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None })
16,640✔
650
            .collect::<Vec<OutputGroup>>();
804✔
651

804✔
652
        let selected_amount = best_selection_value.unwrap();
804✔
653

804✔
654
        // remaining_amount can't be negative as that would mean the
804✔
655
        // selection wasn't successful
804✔
656
        // target_amount = amount_needed + (fee_amount - vin_fees)
804✔
657
        let remaining_amount = (selected_amount - target_amount) as u64;
804✔
658

804✔
659
        let excess = decide_change(remaining_amount, fee_rate, drain_script);
804✔
660

804✔
661
        Ok(BranchAndBoundCoinSelection::calculate_cs_result(
804✔
662
            selected_utxos,
804✔
663
            required_utxos,
804✔
664
            excess,
804✔
665
        ))
804✔
666
    }
887✔
667

668
    #[allow(clippy::too_many_arguments)]
669
    fn single_random_draw(
81✔
670
        &self,
81✔
671
        required_utxos: Vec<OutputGroup>,
81✔
672
        mut optional_utxos: Vec<OutputGroup>,
81✔
673
        curr_value: i64,
81✔
674
        target_amount: i64,
81✔
675
        drain_script: &Script,
81✔
676
        fee_rate: FeeRate,
81✔
677
    ) -> CoinSelectionResult {
81✔
678
        #[cfg(not(test))]
81✔
679
        optional_utxos.shuffle(&mut thread_rng());
81✔
680
        #[cfg(test)]
81✔
681
        {
81✔
682
            use rand::{rngs::StdRng, SeedableRng};
81✔
683
            let seed = [0; 32];
81✔
684
            let mut rng: StdRng = SeedableRng::from_seed(seed);
81✔
685
            optional_utxos.shuffle(&mut rng);
81✔
686
        }
81✔
687

81✔
688
        let selected_utxos = optional_utxos.into_iter().fold(
81✔
689
            (curr_value, vec![]),
81✔
690
            |(mut amount, mut utxos), utxo| {
727✔
691
                if amount >= target_amount {
719✔
692
                    (amount, utxos)
592✔
693
                } else {
694
                    amount += utxo.effective_value;
127✔
695
                    utxos.push(utxo);
127✔
696
                    (amount, utxos)
127✔
697
                }
698
            },
727✔
699
        );
81✔
700

81✔
701
        // remaining_amount can't be negative as that would mean the
81✔
702
        // selection wasn't successful
81✔
703
        // target_amount = amount_needed + (fee_amount - vin_fees)
81✔
704
        let remaining_amount = (selected_utxos.0 - target_amount) as u64;
81✔
705

81✔
706
        let excess = decide_change(remaining_amount, fee_rate, drain_script);
81✔
707

81✔
708
        BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess)
81✔
709
    }
81✔
710

711
    fn calculate_cs_result(
902✔
712
        mut selected_utxos: Vec<OutputGroup>,
902✔
713
        mut required_utxos: Vec<OutputGroup>,
902✔
714
        excess: Excess,
902✔
715
    ) -> CoinSelectionResult {
902✔
716
        selected_utxos.append(&mut required_utxos);
902✔
717
        let fee_amount = selected_utxos.iter().map(|u| u.fee).sum::<u64>();
3,162✔
718
        let selected = selected_utxos
902✔
719
            .into_iter()
902✔
720
            .map(|u| u.weighted_utxo.utxo)
3,162✔
721
            .collect::<Vec<_>>();
902✔
722

902✔
723
        CoinSelectionResult {
902✔
724
            selected,
902✔
725
            fee_amount,
902✔
726
            excess,
902✔
727
        }
902✔
728
    }
902✔
729
}
730

731
#[cfg(test)]
732
mod test {
733
    use std::str::FromStr;
734

735
    use bitcoin::{OutPoint, ScriptBuf, TxOut};
736

737
    use super::*;
738
    use crate::database::{BatchOperations, MemoryDatabase};
739
    use crate::types::*;
740
    use crate::wallet::Vbytes;
741

742
    use rand::rngs::StdRng;
743
    use rand::seq::SliceRandom;
744
    use rand::{Rng, SeedableRng};
745

746
    // n. of items on witness (1WU) + signature len (1WU) + signature and sighash (72WU)
747
    // + pubkey len (1WU) + pubkey (33WU) + script sig len (1 byte, 4WU)
748
    const P2WPKH_SATISFACTION_SIZE: usize = 1 + 1 + 72 + 1 + 33 + 4;
749

750
    const FEE_AMOUNT: u64 = 50;
751

752
    fn utxo(value: u64, index: u32) -> WeightedUtxo {
120✔
753
        assert!(index < 10);
120✔
754
        let outpoint = OutPoint::from_str(&format!(
120✔
755
            "000000000000000000000000000000000000000000000000000000000000000{}:0",
120✔
756
            index
120✔
757
        ))
120✔
758
        .unwrap();
120✔
759
        WeightedUtxo {
120✔
760
            satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
120✔
761
            utxo: Utxo::Local(LocalUtxo {
120✔
762
                outpoint,
120✔
763
                txout: TxOut {
120✔
764
                    value,
120✔
765
                    script_pubkey: ScriptBuf::new(),
120✔
766
                },
120✔
767
                keychain: KeychainKind::External,
120✔
768
                is_spent: false,
120✔
769
            }),
120✔
770
        }
120✔
771
    }
120✔
772

773
    fn get_test_utxos() -> Vec<WeightedUtxo> {
28✔
774
        vec![
28✔
775
            utxo(100_000, 0),
28✔
776
            utxo(FEE_AMOUNT as u64 - 40, 1),
28✔
777
            utxo(200_000, 2),
28✔
778
        ]
28✔
779
    }
28✔
780

781
    fn setup_database_and_get_oldest_first_test_utxos<D: Database>(
10✔
782
        database: &mut D,
10✔
783
    ) -> Vec<WeightedUtxo> {
10✔
784
        // ensure utxos are from different tx
10✔
785
        let utxo1 = utxo(120_000, 1);
10✔
786
        let utxo2 = utxo(80_000, 2);
10✔
787
        let utxo3 = utxo(300_000, 3);
10✔
788

10✔
789
        // add tx to DB so utxos are sorted by blocktime asc
10✔
790
        // utxos will be selected by the following order
10✔
791
        // utxo1(blockheight 1) -> utxo2(blockheight 2), utxo3 (blockheight 3)
10✔
792
        // timestamp are all set as the same to ensure that only block height is used in sorting
10✔
793
        let utxo1_tx_details = TransactionDetails {
10✔
794
            transaction: None,
10✔
795
            txid: utxo1.utxo.outpoint().txid,
10✔
796
            received: 1,
10✔
797
            sent: 0,
10✔
798
            fee: None,
10✔
799
            confirmation_time: Some(BlockTime {
10✔
800
                height: 1,
10✔
801
                timestamp: 1231006505,
10✔
802
            }),
10✔
803
        };
10✔
804

10✔
805
        let utxo2_tx_details = TransactionDetails {
10✔
806
            transaction: None,
10✔
807
            txid: utxo2.utxo.outpoint().txid,
10✔
808
            received: 1,
10✔
809
            sent: 0,
10✔
810
            fee: None,
10✔
811
            confirmation_time: Some(BlockTime {
10✔
812
                height: 2,
10✔
813
                timestamp: 1231006505,
10✔
814
            }),
10✔
815
        };
10✔
816

10✔
817
        let utxo3_tx_details = TransactionDetails {
10✔
818
            transaction: None,
10✔
819
            txid: utxo3.utxo.outpoint().txid,
10✔
820
            received: 1,
10✔
821
            sent: 0,
10✔
822
            fee: None,
10✔
823
            confirmation_time: Some(BlockTime {
10✔
824
                height: 3,
10✔
825
                timestamp: 1231006505,
10✔
826
            }),
10✔
827
        };
10✔
828

10✔
829
        database.set_tx(&utxo1_tx_details).unwrap();
10✔
830
        database.set_tx(&utxo2_tx_details).unwrap();
10✔
831
        database.set_tx(&utxo3_tx_details).unwrap();
10✔
832

10✔
833
        vec![utxo1, utxo2, utxo3]
10✔
834
    }
10✔
835

836
    fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
802✔
837
        let mut res = Vec::new();
802✔
838
        for _ in 0..utxos_number {
23,000✔
839
            res.push(WeightedUtxo {
23,000✔
840
                satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
23,000✔
841
                utxo: Utxo::Local(LocalUtxo {
23,000✔
842
                    outpoint: OutPoint::from_str(
23,000✔
843
                        "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
23,000✔
844
                    )
23,000✔
845
                    .unwrap(),
23,000✔
846
                    txout: TxOut {
23,000✔
847
                        value: rng.gen_range(0..200000000),
23,000✔
848
                        script_pubkey: ScriptBuf::new(),
23,000✔
849
                    },
23,000✔
850
                    keychain: KeychainKind::External,
23,000✔
851
                    is_spent: false,
23,000✔
852
                }),
23,000✔
853
            });
23,000✔
854
        }
23,000✔
855
        res
802✔
856
    }
802✔
857

858
    fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
6✔
859
        let utxo = WeightedUtxo {
6✔
860
            satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
6✔
861
            utxo: Utxo::Local(LocalUtxo {
6✔
862
                outpoint: OutPoint::from_str(
6✔
863
                    "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
6✔
864
                )
6✔
865
                .unwrap(),
6✔
866
                txout: TxOut {
6✔
867
                    value: utxos_value,
6✔
868
                    script_pubkey: ScriptBuf::new(),
6✔
869
                },
6✔
870
                keychain: KeychainKind::External,
6✔
871
                is_spent: false,
6✔
872
            }),
6✔
873
        };
6✔
874
        vec![utxo; utxos_number]
6✔
875
    }
6✔
876

877
    fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<WeightedUtxo>) -> u64 {
402✔
878
        let utxos_picked_len = rng.gen_range(2..utxos.len() / 2);
402✔
879
        utxos.shuffle(&mut rng);
402✔
880
        utxos[..utxos_picked_len]
402✔
881
            .iter()
402✔
882
            .map(|u| u.utxo.txout().value)
2,039✔
883
            .sum()
402✔
884
    }
402✔
885

886
    #[test]
2✔
887
    fn test_largest_first_coin_selection_success() {
2✔
888
        let utxos = get_test_utxos();
2✔
889
        let database = MemoryDatabase::default();
2✔
890
        let drain_script = ScriptBuf::default();
2✔
891
        let target_amount = 250_000 + FEE_AMOUNT;
2✔
892

2✔
893
        let result = LargestFirstCoinSelection::default()
2✔
894
            .coin_select(
2✔
895
                &database,
2✔
896
                utxos,
2✔
897
                vec![],
2✔
898
                FeeRate::from_sat_per_vb(1.0),
2✔
899
                target_amount,
2✔
900
                &drain_script,
2✔
901
            )
2✔
902
            .unwrap();
2✔
903

2✔
904
        assert_eq!(result.selected.len(), 3);
2✔
905
        assert_eq!(result.selected_amount(), 300_010);
2✔
906
        assert_eq!(result.fee_amount, 204)
2✔
907
    }
2✔
908

909
    #[test]
2✔
910
    fn test_largest_first_coin_selection_use_all() {
2✔
911
        let utxos = get_test_utxos();
2✔
912
        let database = MemoryDatabase::default();
2✔
913
        let drain_script = ScriptBuf::default();
2✔
914
        let target_amount = 20_000 + FEE_AMOUNT;
2✔
915

2✔
916
        let result = LargestFirstCoinSelection::default()
2✔
917
            .coin_select(
2✔
918
                &database,
2✔
919
                utxos,
2✔
920
                vec![],
2✔
921
                FeeRate::from_sat_per_vb(1.0),
2✔
922
                target_amount,
2✔
923
                &drain_script,
2✔
924
            )
2✔
925
            .unwrap();
2✔
926

2✔
927
        assert_eq!(result.selected.len(), 3);
2✔
928
        assert_eq!(result.selected_amount(), 300_010);
2✔
929
        assert_eq!(result.fee_amount, 204);
2✔
930
    }
2✔
931

932
    #[test]
2✔
933
    fn test_largest_first_coin_selection_use_only_necessary() {
2✔
934
        let utxos = get_test_utxos();
2✔
935
        let database = MemoryDatabase::default();
2✔
936
        let drain_script = ScriptBuf::default();
2✔
937
        let target_amount = 20_000 + FEE_AMOUNT;
2✔
938

2✔
939
        let result = LargestFirstCoinSelection::default()
2✔
940
            .coin_select(
2✔
941
                &database,
2✔
942
                vec![],
2✔
943
                utxos,
2✔
944
                FeeRate::from_sat_per_vb(1.0),
2✔
945
                target_amount,
2✔
946
                &drain_script,
2✔
947
            )
2✔
948
            .unwrap();
2✔
949

2✔
950
        assert_eq!(result.selected.len(), 1);
2✔
951
        assert_eq!(result.selected_amount(), 200_000);
2✔
952
        assert_eq!(result.fee_amount, 68);
2✔
953
    }
2✔
954

955
    #[test]
2✔
956
    #[should_panic(expected = "InsufficientFunds")]
957
    fn test_largest_first_coin_selection_insufficient_funds() {
2✔
958
        let utxos = get_test_utxos();
2✔
959
        let database = MemoryDatabase::default();
2✔
960
        let drain_script = ScriptBuf::default();
2✔
961
        let target_amount = 500_000 + FEE_AMOUNT;
2✔
962

2✔
963
        LargestFirstCoinSelection::default()
2✔
964
            .coin_select(
2✔
965
                &database,
2✔
966
                vec![],
2✔
967
                utxos,
2✔
968
                FeeRate::from_sat_per_vb(1.0),
2✔
969
                target_amount,
2✔
970
                &drain_script,
2✔
971
            )
2✔
972
            .unwrap();
2✔
973
    }
2✔
974

975
    #[test]
2✔
976
    #[should_panic(expected = "InsufficientFunds")]
977
    fn test_largest_first_coin_selection_insufficient_funds_high_fees() {
2✔
978
        let utxos = get_test_utxos();
2✔
979
        let database = MemoryDatabase::default();
2✔
980
        let drain_script = ScriptBuf::default();
2✔
981
        let target_amount = 250_000 + FEE_AMOUNT;
2✔
982

2✔
983
        LargestFirstCoinSelection::default()
2✔
984
            .coin_select(
2✔
985
                &database,
2✔
986
                vec![],
2✔
987
                utxos,
2✔
988
                FeeRate::from_sat_per_vb(1000.0),
2✔
989
                target_amount,
2✔
990
                &drain_script,
2✔
991
            )
2✔
992
            .unwrap();
2✔
993
    }
2✔
994

995
    #[test]
2✔
996
    fn test_oldest_first_coin_selection_success() {
2✔
997
        let mut database = MemoryDatabase::default();
2✔
998
        let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
2✔
999
        let drain_script = ScriptBuf::default();
2✔
1000
        let target_amount = 180_000 + FEE_AMOUNT;
2✔
1001

2✔
1002
        let result = OldestFirstCoinSelection::default()
2✔
1003
            .coin_select(
2✔
1004
                &database,
2✔
1005
                vec![],
2✔
1006
                utxos,
2✔
1007
                FeeRate::from_sat_per_vb(1.0),
2✔
1008
                target_amount,
2✔
1009
                &drain_script,
2✔
1010
            )
2✔
1011
            .unwrap();
2✔
1012

2✔
1013
        assert_eq!(result.selected.len(), 2);
2✔
1014
        assert_eq!(result.selected_amount(), 200_000);
2✔
1015
        assert_eq!(result.fee_amount, 136)
2✔
1016
    }
2✔
1017

1018
    #[test]
2✔
1019
    fn test_oldest_first_coin_selection_utxo_not_in_db_will_be_selected_last() {
2✔
1020
        // ensure utxos are from different tx
2✔
1021
        let utxo1 = utxo(120_000, 1);
2✔
1022
        let utxo2 = utxo(80_000, 2);
2✔
1023
        let utxo3 = utxo(300_000, 3);
2✔
1024
        let drain_script = ScriptBuf::default();
2✔
1025

2✔
1026
        let mut database = MemoryDatabase::default();
2✔
1027

2✔
1028
        // add tx to DB so utxos are sorted by blocktime asc
2✔
1029
        // utxos will be selected by the following order
2✔
1030
        // utxo1(blockheight 1) -> utxo2(blockheight 2), utxo3 (not exist in DB)
2✔
1031
        // timestamp are all set as the same to ensure that only block height is used in sorting
2✔
1032
        let utxo1_tx_details = TransactionDetails {
2✔
1033
            transaction: None,
2✔
1034
            txid: utxo1.utxo.outpoint().txid,
2✔
1035
            received: 1,
2✔
1036
            sent: 0,
2✔
1037
            fee: None,
2✔
1038
            confirmation_time: Some(BlockTime {
2✔
1039
                height: 1,
2✔
1040
                timestamp: 1231006505,
2✔
1041
            }),
2✔
1042
        };
2✔
1043

2✔
1044
        let utxo2_tx_details = TransactionDetails {
2✔
1045
            transaction: None,
2✔
1046
            txid: utxo2.utxo.outpoint().txid,
2✔
1047
            received: 1,
2✔
1048
            sent: 0,
2✔
1049
            fee: None,
2✔
1050
            confirmation_time: Some(BlockTime {
2✔
1051
                height: 2,
2✔
1052
                timestamp: 1231006505,
2✔
1053
            }),
2✔
1054
        };
2✔
1055

2✔
1056
        database.set_tx(&utxo1_tx_details).unwrap();
2✔
1057
        database.set_tx(&utxo2_tx_details).unwrap();
2✔
1058

2✔
1059
        let target_amount = 180_000 + FEE_AMOUNT;
2✔
1060

2✔
1061
        let result = OldestFirstCoinSelection::default()
2✔
1062
            .coin_select(
2✔
1063
                &database,
2✔
1064
                vec![],
2✔
1065
                vec![utxo3, utxo1, utxo2],
2✔
1066
                FeeRate::from_sat_per_vb(1.0),
2✔
1067
                target_amount,
2✔
1068
                &drain_script,
2✔
1069
            )
2✔
1070
            .unwrap();
2✔
1071

2✔
1072
        assert_eq!(result.selected.len(), 2);
2✔
1073
        assert_eq!(result.selected_amount(), 200_000);
2✔
1074
        assert_eq!(result.fee_amount, 136)
2✔
1075
    }
2✔
1076

1077
    #[test]
2✔
1078
    fn test_oldest_first_coin_selection_use_all() {
2✔
1079
        let mut database = MemoryDatabase::default();
2✔
1080
        let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
2✔
1081
        let drain_script = ScriptBuf::default();
2✔
1082
        let target_amount = 20_000 + FEE_AMOUNT;
2✔
1083

2✔
1084
        let result = OldestFirstCoinSelection::default()
2✔
1085
            .coin_select(
2✔
1086
                &database,
2✔
1087
                utxos,
2✔
1088
                vec![],
2✔
1089
                FeeRate::from_sat_per_vb(1.0),
2✔
1090
                target_amount,
2✔
1091
                &drain_script,
2✔
1092
            )
2✔
1093
            .unwrap();
2✔
1094

2✔
1095
        assert_eq!(result.selected.len(), 3);
2✔
1096
        assert_eq!(result.selected_amount(), 500_000);
2✔
1097
        assert_eq!(result.fee_amount, 204);
2✔
1098
    }
2✔
1099

1100
    #[test]
2✔
1101
    fn test_oldest_first_coin_selection_use_only_necessary() {
2✔
1102
        let mut database = MemoryDatabase::default();
2✔
1103
        let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
2✔
1104
        let drain_script = ScriptBuf::default();
2✔
1105
        let target_amount = 20_000 + FEE_AMOUNT;
2✔
1106

2✔
1107
        let result = OldestFirstCoinSelection::default()
2✔
1108
            .coin_select(
2✔
1109
                &database,
2✔
1110
                vec![],
2✔
1111
                utxos,
2✔
1112
                FeeRate::from_sat_per_vb(1.0),
2✔
1113
                target_amount,
2✔
1114
                &drain_script,
2✔
1115
            )
2✔
1116
            .unwrap();
2✔
1117

2✔
1118
        assert_eq!(result.selected.len(), 1);
2✔
1119
        assert_eq!(result.selected_amount(), 120_000);
2✔
1120
        assert_eq!(result.fee_amount, 68);
2✔
1121
    }
2✔
1122

1123
    #[test]
2✔
1124
    #[should_panic(expected = "InsufficientFunds")]
1125
    fn test_oldest_first_coin_selection_insufficient_funds() {
2✔
1126
        let mut database = MemoryDatabase::default();
2✔
1127
        let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
2✔
1128
        let drain_script = ScriptBuf::default();
2✔
1129
        let target_amount = 600_000 + FEE_AMOUNT;
2✔
1130

2✔
1131
        OldestFirstCoinSelection::default()
2✔
1132
            .coin_select(
2✔
1133
                &database,
2✔
1134
                vec![],
2✔
1135
                utxos,
2✔
1136
                FeeRate::from_sat_per_vb(1.0),
2✔
1137
                target_amount,
2✔
1138
                &drain_script,
2✔
1139
            )
2✔
1140
            .unwrap();
2✔
1141
    }
2✔
1142

1143
    #[test]
2✔
1144
    #[should_panic(expected = "InsufficientFunds")]
1145
    fn test_oldest_first_coin_selection_insufficient_funds_high_fees() {
2✔
1146
        let mut database = MemoryDatabase::default();
2✔
1147
        let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
2✔
1148

2✔
1149
        let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::<u64>() - 50;
7✔
1150
        let drain_script = ScriptBuf::default();
2✔
1151

2✔
1152
        OldestFirstCoinSelection::default()
2✔
1153
            .coin_select(
2✔
1154
                &database,
2✔
1155
                vec![],
2✔
1156
                utxos,
2✔
1157
                FeeRate::from_sat_per_vb(1000.0),
2✔
1158
                target_amount,
2✔
1159
                &drain_script,
2✔
1160
            )
2✔
1161
            .unwrap();
2✔
1162
    }
2✔
1163

1164
    #[test]
2✔
1165
    fn test_bnb_coin_selection_success() {
2✔
1166
        // In this case bnb won't find a suitable match and single random draw will
2✔
1167
        // select three outputs
2✔
1168
        let utxos = generate_same_value_utxos(100_000, 20);
2✔
1169

2✔
1170
        let database = MemoryDatabase::default();
2✔
1171
        let drain_script = ScriptBuf::default();
2✔
1172

2✔
1173
        let target_amount = 250_000 + FEE_AMOUNT;
2✔
1174

2✔
1175
        let result = BranchAndBoundCoinSelection::default()
2✔
1176
            .coin_select(
2✔
1177
                &database,
2✔
1178
                vec![],
2✔
1179
                utxos,
2✔
1180
                FeeRate::from_sat_per_vb(1.0),
2✔
1181
                target_amount,
2✔
1182
                &drain_script,
2✔
1183
            )
2✔
1184
            .unwrap();
2✔
1185

2✔
1186
        assert_eq!(result.selected.len(), 3);
2✔
1187
        assert_eq!(result.selected_amount(), 300_000);
2✔
1188
        assert_eq!(result.fee_amount, 204);
2✔
1189
    }
2✔
1190

1191
    #[test]
2✔
1192
    fn test_bnb_coin_selection_required_are_enough() {
2✔
1193
        let utxos = get_test_utxos();
2✔
1194
        let database = MemoryDatabase::default();
2✔
1195
        let drain_script = ScriptBuf::default();
2✔
1196
        let target_amount = 20_000 + FEE_AMOUNT;
2✔
1197

2✔
1198
        let result = BranchAndBoundCoinSelection::default()
2✔
1199
            .coin_select(
2✔
1200
                &database,
2✔
1201
                utxos.clone(),
2✔
1202
                utxos,
2✔
1203
                FeeRate::from_sat_per_vb(1.0),
2✔
1204
                target_amount,
2✔
1205
                &drain_script,
2✔
1206
            )
2✔
1207
            .unwrap();
2✔
1208

2✔
1209
        assert_eq!(result.selected.len(), 3);
2✔
1210
        assert_eq!(result.selected_amount(), 300_010);
2✔
1211
        assert_eq!(result.fee_amount, 204);
2✔
1212
    }
2✔
1213

1214
    #[test]
2✔
1215
    fn test_bnb_coin_selection_optional_are_enough() {
2✔
1216
        let utxos = get_test_utxos();
2✔
1217
        let database = MemoryDatabase::default();
2✔
1218
        let drain_script = ScriptBuf::default();
2✔
1219
        let target_amount = 299756 + FEE_AMOUNT;
2✔
1220

2✔
1221
        let result = BranchAndBoundCoinSelection::default()
2✔
1222
            .coin_select(
2✔
1223
                &database,
2✔
1224
                vec![],
2✔
1225
                utxos,
2✔
1226
                FeeRate::from_sat_per_vb(1.0),
2✔
1227
                target_amount,
2✔
1228
                &drain_script,
2✔
1229
            )
2✔
1230
            .unwrap();
2✔
1231

2✔
1232
        assert_eq!(result.selected.len(), 2);
2✔
1233
        assert_eq!(result.selected_amount(), 300000);
2✔
1234
        assert_eq!(result.fee_amount, 136);
2✔
1235
    }
2✔
1236

1237
    #[test]
×
1238
    #[ignore]
1239
    fn test_bnb_coin_selection_required_not_enough() {
×
1240
        let utxos = get_test_utxos();
×
1241
        let database = MemoryDatabase::default();
×
1242

×
1243
        let required = vec![utxos[0].clone()];
×
1244
        let mut optional = utxos[1..].to_vec();
×
1245
        optional.push(utxo(500_000, 3));
×
1246

×
1247
        // Defensive assertions, for sanity and in case someone changes the test utxos vector.
×
1248
        let amount: u64 = required.iter().map(|u| u.utxo.txout().value).sum();
×
1249
        assert_eq!(amount, 100_000);
×
1250
        let amount: u64 = optional.iter().map(|u| u.utxo.txout().value).sum();
×
1251
        assert!(amount > 150_000);
×
1252
        let drain_script = ScriptBuf::default();
×
1253

×
1254
        let target_amount = 150_000 + FEE_AMOUNT;
×
1255

×
1256
        let result = BranchAndBoundCoinSelection::default()
×
1257
            .coin_select(
×
1258
                &database,
×
1259
                required,
×
1260
                optional,
×
1261
                FeeRate::from_sat_per_vb(1.0),
×
1262
                target_amount,
×
1263
                &drain_script,
×
1264
            )
×
1265
            .unwrap();
×
1266

×
1267
        assert_eq!(result.selected.len(), 2);
×
1268
        assert_eq!(result.selected_amount(), 300_000);
×
1269
        assert_eq!(result.fee_amount, 136);
×
1270
    }
×
1271

1272
    #[test]
2✔
1273
    #[should_panic(expected = "InsufficientFunds")]
1274
    fn test_bnb_coin_selection_insufficient_funds() {
2✔
1275
        let utxos = get_test_utxos();
2✔
1276
        let database = MemoryDatabase::default();
2✔
1277
        let drain_script = ScriptBuf::default();
2✔
1278
        let target_amount = 500_000 + FEE_AMOUNT;
2✔
1279

2✔
1280
        BranchAndBoundCoinSelection::default()
2✔
1281
            .coin_select(
2✔
1282
                &database,
2✔
1283
                vec![],
2✔
1284
                utxos,
2✔
1285
                FeeRate::from_sat_per_vb(1.0),
2✔
1286
                target_amount,
2✔
1287
                &drain_script,
2✔
1288
            )
2✔
1289
            .unwrap();
2✔
1290
    }
2✔
1291

1292
    #[test]
2✔
1293
    #[should_panic(expected = "InsufficientFunds")]
1294
    fn test_bnb_coin_selection_insufficient_funds_high_fees() {
2✔
1295
        let utxos = get_test_utxos();
2✔
1296
        let database = MemoryDatabase::default();
2✔
1297
        let drain_script = ScriptBuf::default();
2✔
1298
        let target_amount = 250_000 + FEE_AMOUNT;
2✔
1299

2✔
1300
        BranchAndBoundCoinSelection::default()
2✔
1301
            .coin_select(
2✔
1302
                &database,
2✔
1303
                vec![],
2✔
1304
                utxos,
2✔
1305
                FeeRate::from_sat_per_vb(1000.0),
2✔
1306
                target_amount,
2✔
1307
                &drain_script,
2✔
1308
            )
2✔
1309
            .unwrap();
2✔
1310
    }
2✔
1311

1312
    #[test]
2✔
1313
    fn test_bnb_coin_selection_check_fee_rate() {
2✔
1314
        let utxos = get_test_utxos();
2✔
1315
        let database = MemoryDatabase::default();
2✔
1316
        let drain_script = ScriptBuf::default();
2✔
1317
        let target_amount = 99932; // first utxo's effective value
2✔
1318

2✔
1319
        let result = BranchAndBoundCoinSelection::new(0)
2✔
1320
            .coin_select(
2✔
1321
                &database,
2✔
1322
                vec![],
2✔
1323
                utxos,
2✔
1324
                FeeRate::from_sat_per_vb(1.0),
2✔
1325
                target_amount,
2✔
1326
                &drain_script,
2✔
1327
            )
2✔
1328
            .unwrap();
2✔
1329

2✔
1330
        assert_eq!(result.selected.len(), 1);
2✔
1331
        assert_eq!(result.selected_amount(), 100_000);
2✔
1332
        let input_size = (TXIN_BASE_WEIGHT + P2WPKH_SATISFACTION_SIZE).vbytes();
2✔
1333
        // the final fee rate should be exactly the same as the fee rate given
2✔
1334
        assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < f32::EPSILON);
2✔
1335
    }
2✔
1336

1337
    #[test]
2✔
1338
    fn test_bnb_coin_selection_exact_match() {
2✔
1339
        let seed = [0; 32];
2✔
1340
        let mut rng: StdRng = SeedableRng::from_seed(seed);
2✔
1341
        let database = MemoryDatabase::default();
2✔
1342

1343
        for _i in 0..200 {
402✔
1344
            let mut optional_utxos = generate_random_utxos(&mut rng, 16);
400✔
1345
            let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos);
400✔
1346
            let drain_script = ScriptBuf::default();
400✔
1347
            let result = BranchAndBoundCoinSelection::new(0)
400✔
1348
                .coin_select(
400✔
1349
                    &database,
400✔
1350
                    vec![],
400✔
1351
                    optional_utxos,
400✔
1352
                    FeeRate::from_sat_per_vb(0.0),
400✔
1353
                    target_amount,
400✔
1354
                    &drain_script,
400✔
1355
                )
400✔
1356
                .unwrap();
400✔
1357
            assert_eq!(result.selected_amount(), target_amount);
400✔
1358
        }
1359
    }
2✔
1360

1361
    #[test]
2✔
1362
    #[should_panic(expected = "BnBNoExactMatch")]
1363
    fn test_bnb_function_no_exact_match() {
2✔
1364
        let fee_rate = FeeRate::from_sat_per_vb(10.0);
2✔
1365
        let utxos: Vec<OutputGroup> = get_test_utxos()
2✔
1366
            .into_iter()
2✔
1367
            .map(|u| OutputGroup::new(u, fee_rate))
7✔
1368
            .collect();
2✔
1369

2✔
1370
        let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
7✔
1371

2✔
1372
        let size_of_change = 31;
2✔
1373
        let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
2✔
1374

2✔
1375
        let drain_script = ScriptBuf::default();
2✔
1376
        let target_amount = 20_000 + FEE_AMOUNT;
2✔
1377
        BranchAndBoundCoinSelection::new(size_of_change)
2✔
1378
            .bnb(
2✔
1379
                vec![],
2✔
1380
                utxos,
2✔
1381
                0,
2✔
1382
                curr_available_value,
2✔
1383
                target_amount as i64,
2✔
1384
                cost_of_change,
2✔
1385
                &drain_script,
2✔
1386
                fee_rate,
2✔
1387
            )
2✔
1388
            .unwrap();
2✔
1389
    }
2✔
1390

1391
    #[test]
2✔
1392
    #[should_panic(expected = "BnBTotalTriesExceeded")]
1393
    fn test_bnb_function_tries_exceeded() {
2✔
1394
        let fee_rate = FeeRate::from_sat_per_vb(10.0);
2✔
1395
        let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
2✔
1396
            .into_iter()
2✔
1397
            .map(|u| OutputGroup::new(u, fee_rate))
200,001✔
1398
            .collect();
2✔
1399

2✔
1400
        let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
200,001✔
1401

2✔
1402
        let size_of_change = 31;
2✔
1403
        let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
2✔
1404
        let target_amount = 20_000 + FEE_AMOUNT;
2✔
1405

2✔
1406
        let drain_script = ScriptBuf::default();
2✔
1407

2✔
1408
        BranchAndBoundCoinSelection::new(size_of_change)
2✔
1409
            .bnb(
2✔
1410
                vec![],
2✔
1411
                utxos,
2✔
1412
                0,
2✔
1413
                curr_available_value,
2✔
1414
                target_amount as i64,
2✔
1415
                cost_of_change,
2✔
1416
                &drain_script,
2✔
1417
                fee_rate,
2✔
1418
            )
2✔
1419
            .unwrap();
2✔
1420
    }
2✔
1421

1422
    // The match won't be exact but still in the range
1423
    #[test]
2✔
1424
    fn test_bnb_function_almost_exact_match_with_fees() {
2✔
1425
        let fee_rate = FeeRate::from_sat_per_vb(1.0);
2✔
1426
        let size_of_change = 31;
2✔
1427
        let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
2✔
1428

2✔
1429
        let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
2✔
1430
            .into_iter()
2✔
1431
            .map(|u| OutputGroup::new(u, fee_rate))
21✔
1432
            .collect();
2✔
1433

2✔
1434
        let curr_value = 0;
2✔
1435

2✔
1436
        let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
21✔
1437

2✔
1438
        // 2*(value of 1 utxo)  - 2*(1 utxo fees with 1.0sat/vbyte fee rate) -
2✔
1439
        // cost_of_change + 5.
2✔
1440
        let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as i64 + 5;
2✔
1441

2✔
1442
        let drain_script = ScriptBuf::default();
2✔
1443

2✔
1444
        let result = BranchAndBoundCoinSelection::new(size_of_change)
2✔
1445
            .bnb(
2✔
1446
                vec![],
2✔
1447
                utxos,
2✔
1448
                curr_value,
2✔
1449
                curr_available_value,
2✔
1450
                target_amount,
2✔
1451
                cost_of_change,
2✔
1452
                &drain_script,
2✔
1453
                fee_rate,
2✔
1454
            )
2✔
1455
            .unwrap();
2✔
1456
        assert_eq!(result.selected_amount(), 100_000);
2✔
1457
        assert_eq!(result.fee_amount, 136);
2✔
1458
    }
2✔
1459

1460
    // TODO: bnb() function should be optimized, and this test should be done with more utxos
1461
    #[test]
2✔
1462
    fn test_bnb_function_exact_match_more_utxos() {
2✔
1463
        let seed = [0; 32];
2✔
1464
        let mut rng: StdRng = SeedableRng::from_seed(seed);
2✔
1465
        let fee_rate = FeeRate::from_sat_per_vb(0.0);
2✔
1466

1467
        for _ in 0..200 {
402✔
1468
            let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
400✔
1469
                .into_iter()
400✔
1470
                .map(|u| OutputGroup::new(u, fee_rate))
16,200✔
1471
                .collect();
400✔
1472

400✔
1473
            let curr_value = 0;
400✔
1474

400✔
1475
            let curr_available_value = optional_utxos
400✔
1476
                .iter()
400✔
1477
                .fold(0, |acc, x| acc + x.effective_value);
16,200✔
1478

400✔
1479
            let target_amount =
400✔
1480
                optional_utxos[3].effective_value + optional_utxos[23].effective_value;
400✔
1481

400✔
1482
            let drain_script = ScriptBuf::default();
400✔
1483

400✔
1484
            let result = BranchAndBoundCoinSelection::new(0)
400✔
1485
                .bnb(
400✔
1486
                    vec![],
400✔
1487
                    optional_utxos,
400✔
1488
                    curr_value,
400✔
1489
                    curr_available_value,
400✔
1490
                    target_amount,
400✔
1491
                    0.0,
400✔
1492
                    &drain_script,
400✔
1493
                    fee_rate,
400✔
1494
                )
400✔
1495
                .unwrap();
400✔
1496
            assert_eq!(result.selected_amount(), target_amount as u64);
400✔
1497
        }
1498
    }
2✔
1499

1500
    #[test]
2✔
1501
    fn test_single_random_draw_function_success() {
2✔
1502
        let seed = [0; 32];
2✔
1503
        let mut rng: StdRng = SeedableRng::from_seed(seed);
2✔
1504
        let mut utxos = generate_random_utxos(&mut rng, 300);
2✔
1505
        let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
2✔
1506

2✔
1507
        let fee_rate = FeeRate::from_sat_per_vb(1.0);
2✔
1508
        let utxos: Vec<OutputGroup> = utxos
2✔
1509
            .into_iter()
2✔
1510
            .map(|u| OutputGroup::new(u, fee_rate))
601✔
1511
            .collect();
2✔
1512

2✔
1513
        let drain_script = ScriptBuf::default();
2✔
1514

2✔
1515
        let result = BranchAndBoundCoinSelection::default().single_random_draw(
2✔
1516
            vec![],
2✔
1517
            utxos,
2✔
1518
            0,
2✔
1519
            target_amount as i64,
2✔
1520
            &drain_script,
2✔
1521
            fee_rate,
2✔
1522
        );
2✔
1523

2✔
1524
        assert!(result.selected_amount() > target_amount);
2✔
1525
        assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64);
2✔
1526
    }
2✔
1527

1528
    #[test]
2✔
1529
    fn test_bnb_exclude_negative_effective_value() {
2✔
1530
        let utxos = get_test_utxos();
2✔
1531
        let database = MemoryDatabase::default();
2✔
1532
        let drain_script = ScriptBuf::default();
2✔
1533

2✔
1534
        let selection = BranchAndBoundCoinSelection::default().coin_select(
2✔
1535
            &database,
2✔
1536
            vec![],
2✔
1537
            utxos,
2✔
1538
            FeeRate::from_sat_per_vb(10.0),
2✔
1539
            500_000,
2✔
1540
            &drain_script,
2✔
1541
        );
2✔
1542

1543
        assert_matches!(
×
1544
            selection,
2✔
1545
            Err(Error::InsufficientFunds {
1546
                available: 300_000,
1547
                ..
1548
            })
1549
        );
1550
    }
2✔
1551

1552
    #[test]
2✔
1553
    fn test_bnb_include_negative_effective_value_when_required() {
2✔
1554
        let utxos = get_test_utxos();
2✔
1555
        let database = MemoryDatabase::default();
2✔
1556
        let drain_script = ScriptBuf::default();
2✔
1557

2✔
1558
        let (required, optional) = utxos
2✔
1559
            .into_iter()
2✔
1560
            .partition(|u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value < 1000));
7✔
1561

2✔
1562
        let selection = BranchAndBoundCoinSelection::default().coin_select(
2✔
1563
            &database,
2✔
1564
            required,
2✔
1565
            optional,
2✔
1566
            FeeRate::from_sat_per_vb(10.0),
2✔
1567
            500_000,
2✔
1568
            &drain_script,
2✔
1569
        );
2✔
1570

1571
        assert_matches!(
×
1572
            selection,
2✔
1573
            Err(Error::InsufficientFunds {
1574
                available: 300_010,
1575
                ..
1576
            })
1577
        );
1578
    }
2✔
1579

1580
    #[test]
2✔
1581
    fn test_bnb_sum_of_effective_value_negative() {
2✔
1582
        let utxos = get_test_utxos();
2✔
1583
        let database = MemoryDatabase::default();
2✔
1584
        let drain_script = ScriptBuf::default();
2✔
1585

2✔
1586
        let selection = BranchAndBoundCoinSelection::default().coin_select(
2✔
1587
            &database,
2✔
1588
            utxos,
2✔
1589
            vec![],
2✔
1590
            FeeRate::from_sat_per_vb(10_000.0),
2✔
1591
            500_000,
2✔
1592
            &drain_script,
2✔
1593
        );
2✔
1594

1595
        assert_matches!(
×
1596
            selection,
2✔
1597
            Err(Error::InsufficientFunds {
1598
                available: 300_010,
1599
                ..
1600
            })
1601
        );
1602
    }
2✔
1603
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc