From be91997d84fafee8a3f91016e8b659c91fc806cf Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Sat, 31 Oct 2020 16:27:33 +0100 Subject: [PATCH] [wallet] Add tests for BranchAndBoundCoinSelection::coin_select --- src/wallet/coin_selection.rs | 185 +++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index 86a83f10..8f3b9241 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -112,6 +112,10 @@ use crate::error::Error; use crate::types::{FeeRate, UTXO}; use rand::seq::SliceRandom; +#[cfg(not(test))] +use rand::thread_rng; +#[cfg(test)] +use rand::{rngs::StdRng, SeedableRng}; /// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not /// overridden @@ -532,12 +536,17 @@ impl BranchAndBoundCoinSelection { mod test { use std::str::FromStr; + use bitcoin::consensus::encode::serialize; use bitcoin::{OutPoint, Script, TxOut}; use super::*; use crate::database::MemoryDatabase; use crate::types::*; + use rand::rngs::StdRng; + use rand::seq::SliceRandom; + use rand::{Rng, SeedableRng}; + const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2; fn get_test_utxos() -> Vec<(UTXO, usize)> { @@ -573,6 +582,53 @@ mod test { ] } + fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<(UTXO, usize)> { + let mut res = Vec::new(); + for _ in 0..utxos_number { + res.push(( + UTXO { + outpoint: OutPoint::from_str( + "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", + ) + .unwrap(), + txout: TxOut { + value: rng.gen_range(0, 200000000), + script_pubkey: Script::new(), + }, + is_internal: false, + }, + P2WPKH_WITNESS_SIZE, + )); + } + res + } + + fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<(UTXO, usize)> { + let utxo = ( + UTXO { + outpoint: OutPoint::from_str( + "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", + ) + .unwrap(), + txout: TxOut { + value: utxos_value, + script_pubkey: Script::new(), + }, + is_internal: false, + }, + P2WPKH_WITNESS_SIZE, + ); + vec![utxo; utxos_number] + } + + fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<(UTXO, usize)>) -> u64 { + let utxos_picked_len = rng.gen_range(2, utxos.len() / 2); + utxos.shuffle(&mut rng); + utxos[..utxos_picked_len] + .iter() + .fold(0, |acc, x| acc + x.0.txout.value) + } + #[test] fn test_largest_first_coin_selection_success() { let utxos = get_test_utxos(); @@ -671,4 +727,133 @@ mod test { ) .unwrap(); } + + #[test] + fn test_bnb_coin_selection_success() { + // In this case bnb won't find a suitable match and single random draw will + // select three outputs + let utxos = generate_same_value_utxos(100_000, 20); + + let database = MemoryDatabase::default(); + + let result = BranchAndBoundCoinSelection::default() + .coin_select( + &database, + vec![], + utxos, + FeeRate::from_sat_per_vb(1.0), + 250_000, + 50.0, + ) + .unwrap(); + + assert_eq!(result.txin.len(), 3); + assert_eq!(result.selected_amount, 300_000); + assert_eq!(result.fee_amount, 254.0); + } + + #[test] + fn test_bnb_coin_selection_required_are_enough() { + let utxos = get_test_utxos(); + let database = MemoryDatabase::default(); + + let result = BranchAndBoundCoinSelection::default() + .coin_select( + &database, + utxos.clone(), + utxos, + FeeRate::from_sat_per_vb(1.0), + 20_000, + 50.0, + ) + .unwrap(); + + assert_eq!(result.txin.len(), 2); + assert_eq!(result.selected_amount, 300_000); + assert_eq!(result.fee_amount, 186.0); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_bnb_coin_selection_insufficient_funds() { + let utxos = get_test_utxos(); + let database = MemoryDatabase::default(); + + BranchAndBoundCoinSelection::default() + .coin_select( + &database, + vec![], + utxos, + FeeRate::from_sat_per_vb(1.0), + 500_000, + 50.0, + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_bnb_coin_selection_insufficient_funds_high_fees() { + let utxos = get_test_utxos(); + let database = MemoryDatabase::default(); + + BranchAndBoundCoinSelection::default() + .coin_select( + &database, + vec![], + utxos, + FeeRate::from_sat_per_vb(1000.0), + 250_000, + 50.0, + ) + .unwrap(); + } + + #[test] + fn test_bnb_coin_selection_check_fee_rate() { + let utxos = get_test_utxos(); + let database = MemoryDatabase::default(); + + let result = BranchAndBoundCoinSelection::new(0) + .coin_select( + &database, + vec![], + utxos.clone(), + FeeRate::from_sat_per_vb(1.0), + 99932, // first utxo's effective value + 0.0, + ) + .unwrap(); + + assert_eq!(result.txin.len(), 1); + assert_eq!(result.selected_amount, 100_000); + let result_size = + serialize(result.txin.first().unwrap()).len() as f32 + P2WPKH_WITNESS_SIZE as f32 / 4.0; + let epsilon = 0.5; + assert!((1.0 - (result.fee_amount / result_size)).abs() < epsilon); + } + + #[test] + fn test_bnb_coin_selection_exact_match() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + let database = MemoryDatabase::default(); + + for _i in 0..200 { + let mut optional_utxos = generate_random_utxos(&mut rng, 16); + let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos); + let result = BranchAndBoundCoinSelection::new(0) + .coin_select( + &database, + vec![], + optional_utxos, + FeeRate::from_sat_per_vb(0.0), + target_amount, + 0.0, + ) + .unwrap(); + assert_eq!(result.selected_amount, target_amount); + } + } + }