1use crate::types::FeeRate;
103use crate::wallet::utils::IsDust;
104use crate::{database::Database, WeightedUtxo};
105use crate::{error::Error, Utxo};
106
107use bitcoin::consensus::encode::serialize;
108use bitcoin::{Script, Weight};
109
110#[cfg(test)]
111use assert_matches::assert_matches;
112use rand::seq::SliceRandom;
113#[cfg(not(test))]
114use rand::thread_rng;
115use std::collections::HashMap;
116use std::convert::TryInto;
117
118#[cfg(not(test))]
121pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
122
123#[cfg(test)]
126pub type DefaultCoinSelectionAlgorithm = LargestFirstCoinSelection;
127
128pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
131
132#[derive(Debug)]
133pub enum Excess {
135 NoChange {
137 dust_threshold: u64,
139 remaining_amount: u64,
141 change_fee: u64,
143 },
144 Change {
146 amount: u64,
148 fee: u64,
150 },
151}
152
153#[derive(Debug)]
155pub struct CoinSelectionResult {
156 pub selected: Vec<Utxo>,
158 pub fee_amount: u64,
160 pub excess: Excess,
162}
163
164impl CoinSelectionResult {
165 pub fn selected_amount(&self) -> u64 {
167 self.selected.iter().map(|u| u.txout().value).sum()
168 }
169
170 pub fn local_selected_amount(&self) -> u64 {
172 self.selected
173 .iter()
174 .filter_map(|u| match u {
175 Utxo::Local(_) => Some(u.txout().value),
176 _ => None,
177 })
178 .sum()
179 }
180}
181
182pub trait CoinSelectionAlgorithm<D: Database>: std::fmt::Debug {
189 #[allow(clippy::too_many_arguments)]
202 fn coin_select(
203 &self,
204 database: &D,
205 required_utxos: Vec<WeightedUtxo>,
206 optional_utxos: Vec<WeightedUtxo>,
207 fee_rate: FeeRate,
208 target_amount: u64,
209 drain_script: &Script,
210 ) -> Result<CoinSelectionResult, Error>;
211}
212
213#[derive(Debug, Default, Clone, Copy)]
218pub struct LargestFirstCoinSelection;
219
220impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
221 fn coin_select(
222 &self,
223 _database: &D,
224 required_utxos: Vec<WeightedUtxo>,
225 mut optional_utxos: Vec<WeightedUtxo>,
226 fee_rate: FeeRate,
227 target_amount: u64,
228 drain_script: &Script,
229 ) -> Result<CoinSelectionResult, Error> {
230 log::debug!(
231 "target_amount = `{}`, fee_rate = `{:?}`",
232 target_amount,
233 fee_rate
234 );
235
236 let utxos = {
239 optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value);
240 required_utxos
241 .into_iter()
242 .map(|utxo| (true, utxo))
243 .chain(optional_utxos.into_iter().rev().map(|utxo| (false, utxo)))
244 };
245
246 select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
247 }
248}
249
250#[derive(Debug, Default, Clone, Copy)]
255pub struct OldestFirstCoinSelection;
256
257impl<D: Database> CoinSelectionAlgorithm<D> for OldestFirstCoinSelection {
258 fn coin_select(
259 &self,
260 database: &D,
261 required_utxos: Vec<WeightedUtxo>,
262 mut optional_utxos: Vec<WeightedUtxo>,
263 fee_rate: FeeRate,
264 target_amount: u64,
265 drain_script: &Script,
266 ) -> Result<CoinSelectionResult, Error> {
267 let blockheights = optional_utxos
269 .iter()
270 .map(|wu| wu.utxo.outpoint().txid)
271 .try_fold(HashMap::new(), |mut bh_acc, txid| {
273 if bh_acc.contains_key(&txid) {
274 Ok(bh_acc)
275 } else {
276 database.get_tx(&txid, false).map(|details| {
277 bh_acc.insert(
278 txid,
279 details.and_then(|d| d.confirmation_time.map(|ct| ct.height)),
280 );
281 bh_acc
282 })
283 }
284 })?;
285
286 let utxos = {
290 optional_utxos.sort_unstable_by_key(|wu| {
291 match blockheights.get(&wu.utxo.outpoint().txid) {
292 Some(Some(blockheight)) => blockheight,
293 _ => &u32::MAX,
294 }
295 });
296
297 required_utxos
298 .into_iter()
299 .map(|utxo| (true, utxo))
300 .chain(optional_utxos.into_iter().map(|utxo| (false, utxo)))
301 };
302
303 select_sorted_utxos(utxos, fee_rate, target_amount, drain_script)
304 }
305}
306
307pub fn decide_change(remaining_amount: u64, fee_rate: FeeRate, drain_script: &Script) -> Excess {
313 let drain_output_len = serialize(drain_script).len() + 8usize;
315 let change_fee = fee_rate.fee_vb(drain_output_len);
316 let drain_val = remaining_amount.saturating_sub(change_fee);
317
318 if drain_val.is_dust(drain_script) {
319 let dust_threshold = drain_script.dust_value().to_sat();
320 Excess::NoChange {
321 dust_threshold,
322 change_fee,
323 remaining_amount,
324 }
325 } else {
326 Excess::Change {
327 amount: drain_val,
328 fee: change_fee,
329 }
330 }
331}
332
333fn select_sorted_utxos(
334 utxos: impl Iterator<Item = (bool, WeightedUtxo)>,
335 fee_rate: FeeRate,
336 target_amount: u64,
337 drain_script: &Script,
338) -> Result<CoinSelectionResult, Error> {
339 let mut selected_amount = 0;
340 let mut fee_amount = 0;
341 let selected = utxos
342 .scan(
343 (&mut selected_amount, &mut fee_amount),
344 |(selected_amount, fee_amount), (must_use, weighted_utxo)| {
345 if must_use || **selected_amount < target_amount + **fee_amount {
346 **fee_amount += fee_rate.fee_wu(Weight::from_wu(
347 (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
348 ));
349 **selected_amount += weighted_utxo.utxo.txout().value;
350
351 log::debug!(
352 "Selected {}, updated fee_amount = `{}`",
353 weighted_utxo.utxo.outpoint(),
354 fee_amount
355 );
356
357 Some(weighted_utxo.utxo)
358 } else {
359 None
360 }
361 },
362 )
363 .collect::<Vec<_>>();
364
365 let amount_needed_with_fees = target_amount + fee_amount;
366 if selected_amount < amount_needed_with_fees {
367 return Err(Error::InsufficientFunds {
368 needed: amount_needed_with_fees,
369 available: selected_amount,
370 });
371 }
372
373 let remaining_amount = selected_amount - amount_needed_with_fees;
374
375 let excess = decide_change(remaining_amount, fee_rate, drain_script);
376
377 Ok(CoinSelectionResult {
378 selected,
379 fee_amount,
380 excess,
381 })
382}
383
384#[derive(Debug, Clone)]
385struct OutputGroup {
387 weighted_utxo: WeightedUtxo,
388 fee: u64,
390 effective_value: i64,
392}
393
394impl OutputGroup {
395 fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
396 let fee = fee_rate.fee_wu(Weight::from_wu(
397 (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as u64,
398 ));
399 let effective_value = weighted_utxo.utxo.txout().value as i64 - fee as i64;
400 OutputGroup {
401 weighted_utxo,
402 fee,
403 effective_value,
404 }
405 }
406}
407
408#[derive(Debug)]
412pub struct BranchAndBoundCoinSelection {
413 size_of_change: u64,
414}
415
416impl Default for BranchAndBoundCoinSelection {
417 fn default() -> Self {
418 Self {
419 size_of_change: 8 + 1 + 22,
421 }
422 }
423}
424
425impl BranchAndBoundCoinSelection {
426 pub fn new(size_of_change: u64) -> Self {
428 Self { size_of_change }
429 }
430}
431
432const BNB_TOTAL_TRIES: usize = 100_000;
433
434impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
435 fn coin_select(
436 &self,
437 _database: &D,
438 required_utxos: Vec<WeightedUtxo>,
439 optional_utxos: Vec<WeightedUtxo>,
440 fee_rate: FeeRate,
441 target_amount: u64,
442 drain_script: &Script,
443 ) -> Result<CoinSelectionResult, Error> {
444 let required_utxos: Vec<OutputGroup> = required_utxos
446 .into_iter()
447 .map(|u| OutputGroup::new(u, fee_rate))
448 .collect();
449
450 let optional_utxos: Vec<OutputGroup> = optional_utxos
453 .into_iter()
454 .map(|u| OutputGroup::new(u, fee_rate))
455 .filter(|u| u.effective_value.is_positive())
456 .collect();
457
458 let curr_value = required_utxos
459 .iter()
460 .fold(0, |acc, x| acc + x.effective_value);
461
462 let curr_available_value = optional_utxos
463 .iter()
464 .fold(0, |acc, x| acc + x.effective_value);
465
466 let cost_of_change = self.size_of_change as f32 * fee_rate.as_sat_per_vb();
467
468 let total_value: Result<u64, _> = (curr_available_value + curr_value).try_into();
480 match total_value {
481 Ok(v) if v >= target_amount => {}
482 _ => {
483 let (utxo_fees, utxo_value) = required_utxos
486 .iter()
487 .chain(optional_utxos.iter())
488 .fold((0, 0), |(mut fees, mut value), utxo| {
489 fees += utxo.fee;
490 value += utxo.weighted_utxo.utxo.txout().value;
491
492 (fees, value)
493 });
494
495 return Err(Error::InsufficientFunds {
497 needed: target_amount + utxo_fees,
498 available: utxo_value,
499 });
500 }
501 }
502
503 let target_amount = target_amount
504 .try_into()
505 .expect("Bitcoin amount to fit into i64");
506
507 if curr_value > target_amount {
508 let remaining_amount = (curr_value - target_amount) as u64;
512
513 let excess = decide_change(remaining_amount, fee_rate, drain_script);
514
515 return Ok(BranchAndBoundCoinSelection::calculate_cs_result(
516 vec![],
517 required_utxos,
518 excess,
519 ));
520 }
521
522 Ok(self
523 .bnb(
524 required_utxos.clone(),
525 optional_utxos.clone(),
526 curr_value,
527 curr_available_value,
528 target_amount,
529 cost_of_change,
530 drain_script,
531 fee_rate,
532 )
533 .unwrap_or_else(|_| {
534 self.single_random_draw(
535 required_utxos,
536 optional_utxos,
537 curr_value,
538 target_amount,
539 drain_script,
540 fee_rate,
541 )
542 }))
543 }
544}
545
546impl BranchAndBoundCoinSelection {
547 #[allow(clippy::too_many_arguments)]
550 fn bnb(
551 &self,
552 required_utxos: Vec<OutputGroup>,
553 mut optional_utxos: Vec<OutputGroup>,
554 mut curr_value: i64,
555 mut curr_available_value: i64,
556 target_amount: i64,
557 cost_of_change: f32,
558 drain_script: &Script,
559 fee_rate: FeeRate,
560 ) -> Result<CoinSelectionResult, Error> {
561 let mut current_selection: Vec<bool> = Vec::with_capacity(optional_utxos.len());
566
567 optional_utxos.sort_unstable_by_key(|a| a.effective_value);
569 optional_utxos.reverse();
570
571 let mut best_selection = Vec::new();
573 let mut best_selection_value = None;
574
575 for _ in 0..BNB_TOTAL_TRIES {
577 let mut backtrack = false;
579 if curr_value + curr_available_value < target_amount
583 || curr_value > target_amount + cost_of_change as i64
584 {
585 backtrack = true;
586 } else if curr_value >= target_amount {
587 backtrack = true;
590
591 if best_selection_value.is_none() || curr_value < best_selection_value.unwrap() {
594 best_selection = current_selection.clone();
595 best_selection_value = Some(curr_value);
596 }
597
598 if curr_value == target_amount {
600 break;
601 }
602 }
603
604 if backtrack {
606 while let Some(false) = current_selection.last() {
608 current_selection.pop();
609 curr_available_value += optional_utxos[current_selection.len()].effective_value;
610 }
611
612 if current_selection.last_mut().is_none() {
613 if best_selection.is_empty() {
616 return Err(Error::BnBNoExactMatch);
617 }
618 break;
619 }
620
621 if let Some(c) = current_selection.last_mut() {
622 *c = false;
624 }
625
626 let utxo = &optional_utxos[current_selection.len() - 1];
627 curr_value -= utxo.effective_value;
628 } else {
629 let utxo = &optional_utxos[current_selection.len()];
631
632 curr_available_value -= utxo.effective_value;
634
635 current_selection.push(true);
637 curr_value += utxo.effective_value;
638 }
639 }
640
641 if best_selection.is_empty() {
643 return Err(Error::BnBTotalTriesExceeded);
644 }
645
646 let selected_utxos = optional_utxos
648 .into_iter()
649 .zip(best_selection)
650 .filter_map(|(optional, is_in_best)| if is_in_best { Some(optional) } else { None })
651 .collect::<Vec<OutputGroup>>();
652
653 let selected_amount = best_selection_value.unwrap();
654
655 let remaining_amount = (selected_amount - target_amount) as u64;
659
660 let excess = decide_change(remaining_amount, fee_rate, drain_script);
661
662 Ok(BranchAndBoundCoinSelection::calculate_cs_result(
663 selected_utxos,
664 required_utxos,
665 excess,
666 ))
667 }
668
669 #[allow(clippy::too_many_arguments)]
670 fn single_random_draw(
671 &self,
672 required_utxos: Vec<OutputGroup>,
673 mut optional_utxos: Vec<OutputGroup>,
674 curr_value: i64,
675 target_amount: i64,
676 drain_script: &Script,
677 fee_rate: FeeRate,
678 ) -> CoinSelectionResult {
679 #[cfg(not(test))]
680 optional_utxos.shuffle(&mut thread_rng());
681 #[cfg(test)]
682 {
683 use rand::{rngs::StdRng, SeedableRng};
684 let seed = [0; 32];
685 let mut rng: StdRng = SeedableRng::from_seed(seed);
686 optional_utxos.shuffle(&mut rng);
687 }
688
689 let selected_utxos = optional_utxos.into_iter().fold(
690 (curr_value, vec![]),
691 |(mut amount, mut utxos), utxo| {
692 if amount >= target_amount {
693 (amount, utxos)
694 } else {
695 amount += utxo.effective_value;
696 utxos.push(utxo);
697 (amount, utxos)
698 }
699 },
700 );
701
702 let remaining_amount = (selected_utxos.0 - target_amount) as u64;
706
707 let excess = decide_change(remaining_amount, fee_rate, drain_script);
708
709 BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess)
710 }
711
712 fn calculate_cs_result(
713 mut selected_utxos: Vec<OutputGroup>,
714 mut required_utxos: Vec<OutputGroup>,
715 excess: Excess,
716 ) -> CoinSelectionResult {
717 selected_utxos.append(&mut required_utxos);
718 let fee_amount = selected_utxos.iter().map(|u| u.fee).sum::<u64>();
719 let selected = selected_utxos
720 .into_iter()
721 .map(|u| u.weighted_utxo.utxo)
722 .collect::<Vec<_>>();
723
724 CoinSelectionResult {
725 selected,
726 fee_amount,
727 excess,
728 }
729 }
730}
731
732#[cfg(test)]
733mod test {
734 use std::str::FromStr;
735
736 use bitcoin::{OutPoint, ScriptBuf, TxOut};
737
738 use super::*;
739 use crate::database::{BatchOperations, MemoryDatabase};
740 use crate::types::*;
741 use crate::wallet::Vbytes;
742
743 use rand::rngs::StdRng;
744 use rand::seq::SliceRandom;
745 use rand::{Rng, SeedableRng};
746
747 const P2WPKH_SATISFACTION_SIZE: usize = 1 + 1 + 72 + 1 + 33 + 4;
750
751 const FEE_AMOUNT: u64 = 50;
752
753 fn utxo(value: u64, index: u32) -> WeightedUtxo {
754 assert!(index < 10);
755 let outpoint = OutPoint::from_str(&format!(
756 "000000000000000000000000000000000000000000000000000000000000000{}:0",
757 index
758 ))
759 .unwrap();
760 WeightedUtxo {
761 satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
762 utxo: Utxo::Local(LocalUtxo {
763 outpoint,
764 txout: TxOut {
765 value,
766 script_pubkey: ScriptBuf::new(),
767 },
768 keychain: KeychainKind::External,
769 is_spent: false,
770 }),
771 }
772 }
773
774 fn get_test_utxos() -> Vec<WeightedUtxo> {
775 vec![utxo(100_000, 0), utxo(FEE_AMOUNT - 40, 1), utxo(200_000, 2)]
776 }
777
778 fn setup_database_and_get_oldest_first_test_utxos<D: Database>(
779 database: &mut D,
780 ) -> Vec<WeightedUtxo> {
781 let utxo1 = utxo(120_000, 1);
783 let utxo2 = utxo(80_000, 2);
784 let utxo3 = utxo(300_000, 3);
785
786 let utxo1_tx_details = TransactionDetails {
791 transaction: None,
792 txid: utxo1.utxo.outpoint().txid,
793 received: 1,
794 sent: 0,
795 fee: None,
796 confirmation_time: Some(BlockTime {
797 height: 1,
798 timestamp: 1231006505,
799 }),
800 };
801
802 let utxo2_tx_details = TransactionDetails {
803 transaction: None,
804 txid: utxo2.utxo.outpoint().txid,
805 received: 1,
806 sent: 0,
807 fee: None,
808 confirmation_time: Some(BlockTime {
809 height: 2,
810 timestamp: 1231006505,
811 }),
812 };
813
814 let utxo3_tx_details = TransactionDetails {
815 transaction: None,
816 txid: utxo3.utxo.outpoint().txid,
817 received: 1,
818 sent: 0,
819 fee: None,
820 confirmation_time: Some(BlockTime {
821 height: 3,
822 timestamp: 1231006505,
823 }),
824 };
825
826 database.set_tx(&utxo1_tx_details).unwrap();
827 database.set_tx(&utxo2_tx_details).unwrap();
828 database.set_tx(&utxo3_tx_details).unwrap();
829
830 vec![utxo1, utxo2, utxo3]
831 }
832
833 fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
834 let mut res = Vec::new();
835 for _ in 0..utxos_number {
836 res.push(WeightedUtxo {
837 satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
838 utxo: Utxo::Local(LocalUtxo {
839 outpoint: OutPoint::from_str(
840 "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
841 )
842 .unwrap(),
843 txout: TxOut {
844 value: rng.gen_range(0..200000000),
845 script_pubkey: ScriptBuf::new(),
846 },
847 keychain: KeychainKind::External,
848 is_spent: false,
849 }),
850 });
851 }
852 res
853 }
854
855 fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
856 let utxo = WeightedUtxo {
857 satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
858 utxo: Utxo::Local(LocalUtxo {
859 outpoint: OutPoint::from_str(
860 "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
861 )
862 .unwrap(),
863 txout: TxOut {
864 value: utxos_value,
865 script_pubkey: ScriptBuf::new(),
866 },
867 keychain: KeychainKind::External,
868 is_spent: false,
869 }),
870 };
871 vec![utxo; utxos_number]
872 }
873
874 fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut [WeightedUtxo]) -> u64 {
875 let utxos_picked_len = rng.gen_range(2..utxos.len() / 2);
876 utxos.shuffle(&mut rng);
877 utxos[..utxos_picked_len]
878 .iter()
879 .map(|u| u.utxo.txout().value)
880 .sum()
881 }
882
883 #[test]
884 fn test_largest_first_coin_selection_success() {
885 let utxos = get_test_utxos();
886 let database = MemoryDatabase::default();
887 let drain_script = ScriptBuf::default();
888 let target_amount = 250_000 + FEE_AMOUNT;
889
890 let result = LargestFirstCoinSelection
891 .coin_select(
892 &database,
893 utxos,
894 vec![],
895 FeeRate::from_sat_per_vb(1.0),
896 target_amount,
897 &drain_script,
898 )
899 .unwrap();
900
901 assert_eq!(result.selected.len(), 3);
902 assert_eq!(result.selected_amount(), 300_010);
903 assert_eq!(result.fee_amount, 204)
904 }
905
906 #[test]
907 fn test_largest_first_coin_selection_use_all() {
908 let utxos = get_test_utxos();
909 let database = MemoryDatabase::default();
910 let drain_script = ScriptBuf::default();
911 let target_amount = 20_000 + FEE_AMOUNT;
912
913 let result = LargestFirstCoinSelection
914 .coin_select(
915 &database,
916 utxos,
917 vec![],
918 FeeRate::from_sat_per_vb(1.0),
919 target_amount,
920 &drain_script,
921 )
922 .unwrap();
923
924 assert_eq!(result.selected.len(), 3);
925 assert_eq!(result.selected_amount(), 300_010);
926 assert_eq!(result.fee_amount, 204);
927 }
928
929 #[test]
930 fn test_largest_first_coin_selection_use_only_necessary() {
931 let utxos = get_test_utxos();
932 let database = MemoryDatabase::default();
933 let drain_script = ScriptBuf::default();
934 let target_amount = 20_000 + FEE_AMOUNT;
935
936 let result = LargestFirstCoinSelection
937 .coin_select(
938 &database,
939 vec![],
940 utxos,
941 FeeRate::from_sat_per_vb(1.0),
942 target_amount,
943 &drain_script,
944 )
945 .unwrap();
946
947 assert_eq!(result.selected.len(), 1);
948 assert_eq!(result.selected_amount(), 200_000);
949 assert_eq!(result.fee_amount, 68);
950 }
951
952 #[test]
953 #[should_panic(expected = "InsufficientFunds")]
954 fn test_largest_first_coin_selection_insufficient_funds() {
955 let utxos = get_test_utxos();
956 let database = MemoryDatabase::default();
957 let drain_script = ScriptBuf::default();
958 let target_amount = 500_000 + FEE_AMOUNT;
959
960 LargestFirstCoinSelection
961 .coin_select(
962 &database,
963 vec![],
964 utxos,
965 FeeRate::from_sat_per_vb(1.0),
966 target_amount,
967 &drain_script,
968 )
969 .unwrap();
970 }
971
972 #[test]
973 #[should_panic(expected = "InsufficientFunds")]
974 fn test_largest_first_coin_selection_insufficient_funds_high_fees() {
975 let utxos = get_test_utxos();
976 let database = MemoryDatabase::default();
977 let drain_script = ScriptBuf::default();
978 let target_amount = 250_000 + FEE_AMOUNT;
979
980 LargestFirstCoinSelection
981 .coin_select(
982 &database,
983 vec![],
984 utxos,
985 FeeRate::from_sat_per_vb(1000.0),
986 target_amount,
987 &drain_script,
988 )
989 .unwrap();
990 }
991
992 #[test]
993 fn test_oldest_first_coin_selection_success() {
994 let mut database = MemoryDatabase::default();
995 let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
996 let drain_script = ScriptBuf::default();
997 let target_amount = 180_000 + FEE_AMOUNT;
998
999 let result = OldestFirstCoinSelection
1000 .coin_select(
1001 &database,
1002 vec![],
1003 utxos,
1004 FeeRate::from_sat_per_vb(1.0),
1005 target_amount,
1006 &drain_script,
1007 )
1008 .unwrap();
1009
1010 assert_eq!(result.selected.len(), 2);
1011 assert_eq!(result.selected_amount(), 200_000);
1012 assert_eq!(result.fee_amount, 136)
1013 }
1014
1015 #[test]
1016 fn test_oldest_first_coin_selection_utxo_not_in_db_will_be_selected_last() {
1017 let utxo1 = utxo(120_000, 1);
1019 let utxo2 = utxo(80_000, 2);
1020 let utxo3 = utxo(300_000, 3);
1021 let drain_script = ScriptBuf::default();
1022
1023 let mut database = MemoryDatabase::default();
1024
1025 let utxo1_tx_details = TransactionDetails {
1030 transaction: None,
1031 txid: utxo1.utxo.outpoint().txid,
1032 received: 1,
1033 sent: 0,
1034 fee: None,
1035 confirmation_time: Some(BlockTime {
1036 height: 1,
1037 timestamp: 1231006505,
1038 }),
1039 };
1040
1041 let utxo2_tx_details = TransactionDetails {
1042 transaction: None,
1043 txid: utxo2.utxo.outpoint().txid,
1044 received: 1,
1045 sent: 0,
1046 fee: None,
1047 confirmation_time: Some(BlockTime {
1048 height: 2,
1049 timestamp: 1231006505,
1050 }),
1051 };
1052
1053 database.set_tx(&utxo1_tx_details).unwrap();
1054 database.set_tx(&utxo2_tx_details).unwrap();
1055
1056 let target_amount = 180_000 + FEE_AMOUNT;
1057
1058 let result = OldestFirstCoinSelection
1059 .coin_select(
1060 &database,
1061 vec![],
1062 vec![utxo3, utxo1, utxo2],
1063 FeeRate::from_sat_per_vb(1.0),
1064 target_amount,
1065 &drain_script,
1066 )
1067 .unwrap();
1068
1069 assert_eq!(result.selected.len(), 2);
1070 assert_eq!(result.selected_amount(), 200_000);
1071 assert_eq!(result.fee_amount, 136)
1072 }
1073
1074 #[test]
1075 fn test_oldest_first_coin_selection_use_all() {
1076 let mut database = MemoryDatabase::default();
1077 let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
1078 let drain_script = ScriptBuf::default();
1079 let target_amount = 20_000 + FEE_AMOUNT;
1080
1081 let result = OldestFirstCoinSelection
1082 .coin_select(
1083 &database,
1084 utxos,
1085 vec![],
1086 FeeRate::from_sat_per_vb(1.0),
1087 target_amount,
1088 &drain_script,
1089 )
1090 .unwrap();
1091
1092 assert_eq!(result.selected.len(), 3);
1093 assert_eq!(result.selected_amount(), 500_000);
1094 assert_eq!(result.fee_amount, 204);
1095 }
1096
1097 #[test]
1098 fn test_oldest_first_coin_selection_use_only_necessary() {
1099 let mut database = MemoryDatabase::default();
1100 let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
1101 let drain_script = ScriptBuf::default();
1102 let target_amount = 20_000 + FEE_AMOUNT;
1103
1104 let result = OldestFirstCoinSelection
1105 .coin_select(
1106 &database,
1107 vec![],
1108 utxos,
1109 FeeRate::from_sat_per_vb(1.0),
1110 target_amount,
1111 &drain_script,
1112 )
1113 .unwrap();
1114
1115 assert_eq!(result.selected.len(), 1);
1116 assert_eq!(result.selected_amount(), 120_000);
1117 assert_eq!(result.fee_amount, 68);
1118 }
1119
1120 #[test]
1121 #[should_panic(expected = "InsufficientFunds")]
1122 fn test_oldest_first_coin_selection_insufficient_funds() {
1123 let mut database = MemoryDatabase::default();
1124 let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
1125 let drain_script = ScriptBuf::default();
1126 let target_amount = 600_000 + FEE_AMOUNT;
1127
1128 OldestFirstCoinSelection
1129 .coin_select(
1130 &database,
1131 vec![],
1132 utxos,
1133 FeeRate::from_sat_per_vb(1.0),
1134 target_amount,
1135 &drain_script,
1136 )
1137 .unwrap();
1138 }
1139
1140 #[test]
1141 #[should_panic(expected = "InsufficientFunds")]
1142 fn test_oldest_first_coin_selection_insufficient_funds_high_fees() {
1143 let mut database = MemoryDatabase::default();
1144 let utxos = setup_database_and_get_oldest_first_test_utxos(&mut database);
1145
1146 let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::<u64>() - 50;
1147 let drain_script = ScriptBuf::default();
1148
1149 OldestFirstCoinSelection
1150 .coin_select(
1151 &database,
1152 vec![],
1153 utxos,
1154 FeeRate::from_sat_per_vb(1000.0),
1155 target_amount,
1156 &drain_script,
1157 )
1158 .unwrap();
1159 }
1160
1161 #[test]
1162 fn test_bnb_coin_selection_success() {
1163 let utxos = generate_same_value_utxos(100_000, 20);
1166
1167 let database = MemoryDatabase::default();
1168 let drain_script = ScriptBuf::default();
1169
1170 let target_amount = 250_000 + FEE_AMOUNT;
1171
1172 let result = BranchAndBoundCoinSelection::default()
1173 .coin_select(
1174 &database,
1175 vec![],
1176 utxos,
1177 FeeRate::from_sat_per_vb(1.0),
1178 target_amount,
1179 &drain_script,
1180 )
1181 .unwrap();
1182
1183 assert_eq!(result.selected.len(), 3);
1184 assert_eq!(result.selected_amount(), 300_000);
1185 assert_eq!(result.fee_amount, 204);
1186 }
1187
1188 #[test]
1189 fn test_bnb_coin_selection_required_are_enough() {
1190 let utxos = get_test_utxos();
1191 let database = MemoryDatabase::default();
1192 let drain_script = ScriptBuf::default();
1193 let target_amount = 20_000 + FEE_AMOUNT;
1194
1195 let result = BranchAndBoundCoinSelection::default()
1196 .coin_select(
1197 &database,
1198 utxos.clone(),
1199 utxos,
1200 FeeRate::from_sat_per_vb(1.0),
1201 target_amount,
1202 &drain_script,
1203 )
1204 .unwrap();
1205
1206 assert_eq!(result.selected.len(), 3);
1207 assert_eq!(result.selected_amount(), 300_010);
1208 assert_eq!(result.fee_amount, 204);
1209 }
1210
1211 #[test]
1212 fn test_bnb_coin_selection_optional_are_enough() {
1213 let utxos = get_test_utxos();
1214 let database = MemoryDatabase::default();
1215 let drain_script = ScriptBuf::default();
1216 let target_amount = 299756 + FEE_AMOUNT;
1217
1218 let result = BranchAndBoundCoinSelection::default()
1219 .coin_select(
1220 &database,
1221 vec![],
1222 utxos,
1223 FeeRate::from_sat_per_vb(1.0),
1224 target_amount,
1225 &drain_script,
1226 )
1227 .unwrap();
1228
1229 assert_eq!(result.selected.len(), 2);
1230 assert_eq!(result.selected_amount(), 300000);
1231 assert_eq!(result.fee_amount, 136);
1232 }
1233
1234 #[test]
1235 #[ignore]
1236 fn test_bnb_coin_selection_required_not_enough() {
1237 let utxos = get_test_utxos();
1238 let database = MemoryDatabase::default();
1239
1240 let required = vec![utxos[0].clone()];
1241 let mut optional = utxos[1..].to_vec();
1242 optional.push(utxo(500_000, 3));
1243
1244 let amount: u64 = required.iter().map(|u| u.utxo.txout().value).sum();
1246 assert_eq!(amount, 100_000);
1247 let amount: u64 = optional.iter().map(|u| u.utxo.txout().value).sum();
1248 assert!(amount > 150_000);
1249 let drain_script = ScriptBuf::default();
1250
1251 let target_amount = 150_000 + FEE_AMOUNT;
1252
1253 let result = BranchAndBoundCoinSelection::default()
1254 .coin_select(
1255 &database,
1256 required,
1257 optional,
1258 FeeRate::from_sat_per_vb(1.0),
1259 target_amount,
1260 &drain_script,
1261 )
1262 .unwrap();
1263
1264 assert_eq!(result.selected.len(), 2);
1265 assert_eq!(result.selected_amount(), 300_000);
1266 assert_eq!(result.fee_amount, 136);
1267 }
1268
1269 #[test]
1270 #[should_panic(expected = "InsufficientFunds")]
1271 fn test_bnb_coin_selection_insufficient_funds() {
1272 let utxos = get_test_utxos();
1273 let database = MemoryDatabase::default();
1274 let drain_script = ScriptBuf::default();
1275 let target_amount = 500_000 + FEE_AMOUNT;
1276
1277 BranchAndBoundCoinSelection::default()
1278 .coin_select(
1279 &database,
1280 vec![],
1281 utxos,
1282 FeeRate::from_sat_per_vb(1.0),
1283 target_amount,
1284 &drain_script,
1285 )
1286 .unwrap();
1287 }
1288
1289 #[test]
1290 #[should_panic(expected = "InsufficientFunds")]
1291 fn test_bnb_coin_selection_insufficient_funds_high_fees() {
1292 let utxos = get_test_utxos();
1293 let database = MemoryDatabase::default();
1294 let drain_script = ScriptBuf::default();
1295 let target_amount = 250_000 + FEE_AMOUNT;
1296
1297 BranchAndBoundCoinSelection::default()
1298 .coin_select(
1299 &database,
1300 vec![],
1301 utxos,
1302 FeeRate::from_sat_per_vb(1000.0),
1303 target_amount,
1304 &drain_script,
1305 )
1306 .unwrap();
1307 }
1308
1309 #[test]
1310 fn test_bnb_coin_selection_check_fee_rate() {
1311 let utxos = get_test_utxos();
1312 let database = MemoryDatabase::default();
1313 let drain_script = ScriptBuf::default();
1314 let target_amount = 99932; let result = BranchAndBoundCoinSelection::new(0)
1317 .coin_select(
1318 &database,
1319 vec![],
1320 utxos,
1321 FeeRate::from_sat_per_vb(1.0),
1322 target_amount,
1323 &drain_script,
1324 )
1325 .unwrap();
1326
1327 assert_eq!(result.selected.len(), 1);
1328 assert_eq!(result.selected_amount(), 100_000);
1329 let input_size = (TXIN_BASE_WEIGHT + P2WPKH_SATISFACTION_SIZE).vbytes();
1330 assert!((1.0 - (result.fee_amount as f32 / input_size as f32)).abs() < f32::EPSILON);
1332 }
1333
1334 #[test]
1335 fn test_bnb_coin_selection_exact_match() {
1336 let seed = [0; 32];
1337 let mut rng: StdRng = SeedableRng::from_seed(seed);
1338 let database = MemoryDatabase::default();
1339
1340 for _i in 0..200 {
1341 let mut optional_utxos = generate_random_utxos(&mut rng, 16);
1342 let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos);
1343 let drain_script = ScriptBuf::default();
1344 let result = BranchAndBoundCoinSelection::new(0)
1345 .coin_select(
1346 &database,
1347 vec![],
1348 optional_utxos,
1349 FeeRate::from_sat_per_vb(0.0),
1350 target_amount,
1351 &drain_script,
1352 )
1353 .unwrap();
1354 assert_eq!(result.selected_amount(), target_amount);
1355 }
1356 }
1357
1358 #[test]
1359 #[should_panic(expected = "BnBNoExactMatch")]
1360 fn test_bnb_function_no_exact_match() {
1361 let fee_rate = FeeRate::from_sat_per_vb(10.0);
1362 let utxos: Vec<OutputGroup> = get_test_utxos()
1363 .into_iter()
1364 .map(|u| OutputGroup::new(u, fee_rate))
1365 .collect();
1366
1367 let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
1368
1369 let size_of_change = 31;
1370 let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
1371
1372 let drain_script = ScriptBuf::default();
1373 let target_amount = 20_000 + FEE_AMOUNT;
1374 BranchAndBoundCoinSelection::new(size_of_change)
1375 .bnb(
1376 vec![],
1377 utxos,
1378 0,
1379 curr_available_value,
1380 target_amount as i64,
1381 cost_of_change,
1382 &drain_script,
1383 fee_rate,
1384 )
1385 .unwrap();
1386 }
1387
1388 #[test]
1389 #[should_panic(expected = "BnBTotalTriesExceeded")]
1390 fn test_bnb_function_tries_exceeded() {
1391 let fee_rate = FeeRate::from_sat_per_vb(10.0);
1392 let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
1393 .into_iter()
1394 .map(|u| OutputGroup::new(u, fee_rate))
1395 .collect();
1396
1397 let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
1398
1399 let size_of_change = 31;
1400 let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
1401 let target_amount = 20_000 + FEE_AMOUNT;
1402
1403 let drain_script = ScriptBuf::default();
1404
1405 BranchAndBoundCoinSelection::new(size_of_change)
1406 .bnb(
1407 vec![],
1408 utxos,
1409 0,
1410 curr_available_value,
1411 target_amount as i64,
1412 cost_of_change,
1413 &drain_script,
1414 fee_rate,
1415 )
1416 .unwrap();
1417 }
1418
1419 #[test]
1421 fn test_bnb_function_almost_exact_match_with_fees() {
1422 let fee_rate = FeeRate::from_sat_per_vb(1.0);
1423 let size_of_change = 31;
1424 let cost_of_change = size_of_change as f32 * fee_rate.as_sat_per_vb();
1425
1426 let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
1427 .into_iter()
1428 .map(|u| OutputGroup::new(u, fee_rate))
1429 .collect();
1430
1431 let curr_value = 0;
1432
1433 let curr_available_value = utxos.iter().fold(0, |acc, x| acc + x.effective_value);
1434
1435 let target_amount = 2 * 50_000 - 2 * 67 - cost_of_change.ceil() as i64 + 5;
1438
1439 let drain_script = ScriptBuf::default();
1440
1441 let result = BranchAndBoundCoinSelection::new(size_of_change)
1442 .bnb(
1443 vec![],
1444 utxos,
1445 curr_value,
1446 curr_available_value,
1447 target_amount,
1448 cost_of_change,
1449 &drain_script,
1450 fee_rate,
1451 )
1452 .unwrap();
1453 assert_eq!(result.selected_amount(), 100_000);
1454 assert_eq!(result.fee_amount, 136);
1455 }
1456
1457 #[test]
1459 fn test_bnb_function_exact_match_more_utxos() {
1460 let seed = [0; 32];
1461 let mut rng: StdRng = SeedableRng::from_seed(seed);
1462 let fee_rate = FeeRate::from_sat_per_vb(0.0);
1463
1464 for _ in 0..200 {
1465 let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
1466 .into_iter()
1467 .map(|u| OutputGroup::new(u, fee_rate))
1468 .collect();
1469
1470 let curr_value = 0;
1471
1472 let curr_available_value = optional_utxos
1473 .iter()
1474 .fold(0, |acc, x| acc + x.effective_value);
1475
1476 let target_amount =
1477 optional_utxos[3].effective_value + optional_utxos[23].effective_value;
1478
1479 let drain_script = ScriptBuf::default();
1480
1481 let result = BranchAndBoundCoinSelection::new(0)
1482 .bnb(
1483 vec![],
1484 optional_utxos,
1485 curr_value,
1486 curr_available_value,
1487 target_amount,
1488 0.0,
1489 &drain_script,
1490 fee_rate,
1491 )
1492 .unwrap();
1493 assert_eq!(result.selected_amount(), target_amount as u64);
1494 }
1495 }
1496
1497 #[test]
1498 fn test_single_random_draw_function_success() {
1499 let seed = [0; 32];
1500 let mut rng: StdRng = SeedableRng::from_seed(seed);
1501 let mut utxos = generate_random_utxos(&mut rng, 300);
1502 let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
1503
1504 let fee_rate = FeeRate::from_sat_per_vb(1.0);
1505 let utxos: Vec<OutputGroup> = utxos
1506 .into_iter()
1507 .map(|u| OutputGroup::new(u, fee_rate))
1508 .collect();
1509
1510 let drain_script = ScriptBuf::default();
1511
1512 let result = BranchAndBoundCoinSelection::default().single_random_draw(
1513 vec![],
1514 utxos,
1515 0,
1516 target_amount as i64,
1517 &drain_script,
1518 fee_rate,
1519 );
1520
1521 assert!(result.selected_amount() > target_amount);
1522 assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64);
1523 }
1524
1525 #[test]
1526 fn test_bnb_exclude_negative_effective_value() {
1527 let utxos = get_test_utxos();
1528 let database = MemoryDatabase::default();
1529 let drain_script = ScriptBuf::default();
1530
1531 let selection = BranchAndBoundCoinSelection::default().coin_select(
1532 &database,
1533 vec![],
1534 utxos,
1535 FeeRate::from_sat_per_vb(10.0),
1536 500_000,
1537 &drain_script,
1538 );
1539
1540 assert_matches!(
1541 selection,
1542 Err(Error::InsufficientFunds {
1543 available: 300_000,
1544 ..
1545 })
1546 );
1547 }
1548
1549 #[test]
1550 fn test_bnb_include_negative_effective_value_when_required() {
1551 let utxos = get_test_utxos();
1552 let database = MemoryDatabase::default();
1553 let drain_script = ScriptBuf::default();
1554
1555 let (required, optional) = utxos
1556 .into_iter()
1557 .partition(|u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value < 1000));
1558
1559 let selection = BranchAndBoundCoinSelection::default().coin_select(
1560 &database,
1561 required,
1562 optional,
1563 FeeRate::from_sat_per_vb(10.0),
1564 500_000,
1565 &drain_script,
1566 );
1567
1568 assert_matches!(
1569 selection,
1570 Err(Error::InsufficientFunds {
1571 available: 300_010,
1572 ..
1573 })
1574 );
1575 }
1576
1577 #[test]
1578 fn test_bnb_sum_of_effective_value_negative() {
1579 let utxos = get_test_utxos();
1580 let database = MemoryDatabase::default();
1581 let drain_script = ScriptBuf::default();
1582
1583 let selection = BranchAndBoundCoinSelection::default().coin_select(
1584 &database,
1585 utxos,
1586 vec![],
1587 FeeRate::from_sat_per_vb(10_000.0),
1588 500_000,
1589 &drain_script,
1590 );
1591
1592 assert_matches!(
1593 selection,
1594 Err(Error::InsufficientFunds {
1595 available: 300_010,
1596 ..
1597 })
1598 );
1599 }
1600}