use bitcoin_units::{Amount, Weight};
use crate::OverflowError::{Addition, Subtraction};
use crate::SelectionError::{
InsufficentFunds, IterationLimitReached, MaxWeightExceeded, Overflow, SolutionNotFound,
};
use crate::{Return, WeightedUtxo};
pub const ITERATION_LIMIT: u32 = 100_000;
pub fn branch_and_bound<'a, T: IntoIterator<Item = &'a WeightedUtxo> + std::marker::Copy>(
target: Amount,
cost_of_change: Amount,
max_weight: Weight,
weighted_utxos: T,
) -> Return<'a> {
let mut iteration = 0;
let mut index = 0;
let mut max_tx_weight_exceeded = false;
let mut backtrack;
let mut value = 0;
let mut weight = Weight::ZERO;
let mut current_waste: i64 = 0;
let mut best_waste: i64 = Amount::MAX_MONEY.to_sat() as i64;
let mut index_selection: Vec<usize> = vec![];
let mut best_selection: Vec<usize> = vec![];
let upper_bound = target.checked_add(cost_of_change).ok_or(Overflow(Addition))?.to_sat();
let target = target.to_sat();
let mut available_value: u64 = weighted_utxos
.into_iter()
.map(|u| u.effective_value())
.try_fold(Amount::ZERO, Amount::checked_add)
.ok_or(Overflow(Addition))?
.to_sat();
let _ = weighted_utxos
.into_iter()
.map(|u| u.weight())
.try_fold(Weight::ZERO, Weight::checked_add)
.ok_or(Overflow(Addition))?;
let mut weighted_utxos: Vec<_> = weighted_utxos.into_iter().collect();
weighted_utxos.sort_by(|a, b| {
b.effective_value().cmp(&a.effective_value()).then(a.waste().cmp(&b.waste()))
});
if available_value < target {
return Err(InsufficentFunds);
}
while iteration < ITERATION_LIMIT {
backtrack = false;
if available_value + value < target
|| value > upper_bound
|| current_waste > best_waste && weighted_utxos[0].is_fee_expensive()
{
backtrack = true;
} else if weight > max_weight {
max_tx_weight_exceeded = true;
backtrack = true;
}
else if value >= target {
backtrack = true;
let waste: i64 =
(value as i64).checked_sub(target as i64).ok_or(Overflow(Subtraction))?;
current_waste = current_waste.checked_add(waste).ok_or(Overflow(Addition))?;
if current_waste <= best_waste {
best_selection = index_selection.clone();
best_waste = current_waste;
}
current_waste = current_waste.checked_sub(waste).ok_or(Overflow(Subtraction))?;
}
if backtrack {
if index_selection.is_empty() {
return index_to_utxo_list(
iteration,
best_selection,
max_tx_weight_exceeded,
weighted_utxos,
);
}
loop {
index -= 1;
if index <= *index_selection.last().unwrap() {
break;
}
let eff_value = weighted_utxos[index].effective_value_raw();
available_value += eff_value;
}
assert_eq!(index, *index_selection.last().unwrap());
let eff_value = weighted_utxos[index].effective_value_raw();
let utxo_waste = weighted_utxos[index].waste_raw();
let utxo_weight = weighted_utxos[index].weight();
current_waste = current_waste.checked_sub(utxo_waste).ok_or(Overflow(Subtraction))?;
value = value.checked_sub(eff_value).ok_or(Overflow(Addition))?;
weight -= utxo_weight;
index_selection.pop().unwrap();
}
else {
let eff_value = weighted_utxos[index].effective_value_raw();
let utxo_weight = weighted_utxos[index].weight();
let utxo_waste = weighted_utxos[index].waste_raw();
available_value -= eff_value;
if index_selection.is_empty()
|| index - 1 == *index_selection.last().unwrap()
|| weighted_utxos[index].effective_value_raw() != weighted_utxos[index - 1].effective_value_raw()
{
index_selection.push(index);
current_waste = current_waste.checked_add(utxo_waste).ok_or(Overflow(Addition))?;
value += eff_value;
weight += utxo_weight;
}
}
index += 1;
iteration += 1;
}
index_to_utxo_list(iteration, best_selection, max_tx_weight_exceeded, weighted_utxos)
}
fn index_to_utxo_list<'a>(
iterations: u32,
index_list: Vec<usize>,
max_tx_weight_exceeded: bool,
wu: Vec<&'a WeightedUtxo>,
) -> Return<'a> {
let mut result: Vec<_> = Vec::new();
for i in index_list {
let wu = wu[i];
result.push(wu);
}
if result.is_empty() {
if iterations == ITERATION_LIMIT {
Err(IterationLimitReached)
} else if max_tx_weight_exceeded {
Err(MaxWeightExceeded)
} else {
Err(SolutionNotFound)
}
} else {
Ok((iterations, result))
}
}
#[cfg(test)]
mod tests {
use core::str::FromStr;
use std::iter::{once, zip};
use arbitrary::{Arbitrary, Result, Unstructured};
use arbtest::arbtest;
use bitcoin_units::{Amount, FeeRate, Weight};
use super::*;
use crate::tests::{assert_ref_eq, parse_fee_rate, Selection};
use crate::SelectionError::ProgramError;
use crate::WeightedUtxo;
#[derive(Debug)]
pub struct TestBnB<'a> {
target: &'a str,
cost_of_change: &'a str,
fee_rate: &'a str,
lt_fee_rate: &'a str,
max_weight: &'a str,
weighted_utxos: &'a [&'a str],
expected_utxos: &'a [&'a str],
expected_error: Option<crate::SelectionError>,
expected_iterations: u32,
}
impl TestBnB<'_> {
fn assert(&self) {
let target = Amount::from_str(self.target).unwrap();
let cost_of_change = Amount::from_str(self.cost_of_change).unwrap();
let fee_rate = parse_fee_rate(self.fee_rate);
let lt_fee_rate = parse_fee_rate(self.lt_fee_rate);
let max_weight: Vec<_> = self.max_weight.split(" ").collect();
let max_weight = Weight::from_str(max_weight[0]).unwrap();
let candidate_selection = Selection::new(self.weighted_utxos, fee_rate, lt_fee_rate);
let result =
branch_and_bound(target, cost_of_change, max_weight, &candidate_selection.utxos);
match result {
Ok((iterations, inputs)) => {
assert_eq!(iterations, self.expected_iterations);
let expected_selection =
Selection::new(self.expected_utxos, fee_rate, lt_fee_rate);
assert_ref_eq(inputs, expected_selection.utxos);
}
Err(e) => {
let expected_error = self.expected_error.clone().unwrap();
assert!(self.expected_utxos.is_empty());
assert_eq!(e, expected_error);
}
}
}
}
pub struct AssertBnB {
target: Amount,
cost_of_change: Amount,
max_weight: Weight,
candidate_selection: Selection,
expected_inputs: Vec<WeightedUtxo>,
}
impl AssertBnB {
fn exec(self) {
let target = self.target;
let cost_of_change = self.cost_of_change;
let max_weight = self.max_weight;
let candidate_selection = &self.candidate_selection;
let candidate_utxos = &candidate_selection.utxos;
let expected_inputs = self.expected_inputs;
let upper_bound = target.checked_add(cost_of_change);
let result = branch_and_bound(target, cost_of_change, max_weight, candidate_utxos);
match result {
Ok((i, utxos)) => {
assert!(i > 0 || target == Amount::ZERO);
crate::tests::assert_target_selection(&utxos, target, upper_bound);
}
Err(InsufficentFunds) => {
let available_value = candidate_selection.available_value().unwrap();
assert!(available_value < target);
}
Err(IterationLimitReached) => {}
Err(Overflow(_)) => {
let available_value = candidate_selection.available_value();
let weight_total = candidate_selection.weight_total();
assert!(
available_value.is_none()
|| weight_total.is_none()
|| upper_bound.is_none()
);
}
Err(ProgramError) => panic!("un-expected result"),
Err(SolutionNotFound) => {
assert!(expected_inputs.is_empty() || target == Amount::ZERO)
}
Err(MaxWeightExceeded) => {
let weight_total = candidate_selection.weight_total().unwrap();
assert!(weight_total > max_weight);
}
}
}
}
fn assert_coin_select(target_str: &str, expected_iterations: u32, expected_utxos: &[&str]) {
TestBnB {
target: target_str,
cost_of_change: "0",
fee_rate: "0",
lt_fee_rate: "0",
max_weight: "40000 wu",
weighted_utxos: &["1 cBTC/68 vB", "2 cBTC/68 vB", "3 cBTC/68 vB", "4 cBTC/68 vB"],
expected_utxos,
expected_error: None,
expected_iterations,
}
.assert();
}
impl<'a> Arbitrary<'a> for AssertBnB {
fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
let cost_of_change = Amount::arbitrary(u)?;
let fee_rate = FeeRate::arbitrary(u)?;
let long_term_fee_rate = FeeRate::arbitrary(u)?;
let max_weight = Weight::arbitrary(u)?;
let init: Vec<(Amount, Weight, bool)> = Vec::arbitrary(u)?;
let expected_inputs: Vec<WeightedUtxo> = init
.iter()
.filter(|(_, _, include)| *include)
.filter_map(|(amt, weight, _)| {
WeightedUtxo::new(*amt, *weight, fee_rate, long_term_fee_rate)
})
.collect();
let utxos: Vec<WeightedUtxo> = init
.iter()
.filter_map(|(amt, weight, _)| {
WeightedUtxo::new(*amt, *weight, fee_rate, long_term_fee_rate)
})
.collect();
let candidate_selection = Selection { utxos, fee_rate, long_term_fee_rate };
let target_set: Vec<_> = expected_inputs.iter().map(|u| u.effective_value()).collect();
let target: Amount = target_set
.clone()
.into_iter()
.try_fold(Amount::ZERO, Amount::checked_add)
.unwrap_or(Amount::ZERO);
Ok(AssertBnB {
target,
cost_of_change,
max_weight,
candidate_selection,
expected_inputs,
})
}
}
#[test]
fn select_coins_bnb_one() { assert_coin_select("1 cBTC", 8, &["1 cBTC/68 vB"]); }
#[test]
fn select_coins_bnb_two() { assert_coin_select("2 cBTC", 6, &["2 cBTC/68 vB"]); }
#[test]
fn select_coins_bnb_three() {
assert_coin_select("3 cBTC", 8, &["2 cBTC/68 vB", "1 cBTC/68 vB"]);
}
#[test]
fn select_coins_bnb_four() {
assert_coin_select("4 cBTC", 8, &["3 cBTC/68 vB", "1 cBTC/68 vB"]);
}
#[test]
fn select_coins_bnb_five() {
assert_coin_select("5 cBTC", 12, &["3 cBTC/68 vB", "2 cBTC/68 vB"]);
}
#[test]
fn select_coins_bnb_six() {
assert_coin_select("6 cBTC", 12, &["3 cBTC/68 vB", "2 cBTC/68 vB", "1 cBTC/68 vB"]);
}
#[test]
fn select_coins_bnb_seven() {
assert_coin_select("7 cBTC", 8, &["4 cBTC/68 vB", "2 cBTC/68 vB", "1 cBTC/68 vB"]);
}
#[test]
fn select_coins_bnb_eight() {
assert_coin_select("8 cBTC", 8, &["4 cBTC/68 vB", "3 cBTC/68 vB", "1 cBTC/68 vB"]);
}
#[test]
fn select_coins_bnb_nine() {
assert_coin_select("9 cBTC", 6, &["4 cBTC/68 vB", "3 cBTC/68 vB", "2 cBTC/68 vB"]);
}
#[test]
fn select_coins_bnb_ten() {
assert_coin_select(
"10 cBTC",
8,
&["4 cBTC/68 vB", "3 cBTC/68 vB", "2 cBTC/68 vB", "1 cBTC/68 vB"],
);
}
#[test]
#[should_panic]
fn select_coins_bnb_eleven_invalid_target_should_panic() {
assert_coin_select("11 cBTC", 8, &["1 cBTC/68 vB"]);
}
#[test]
#[should_panic]
fn select_coins_bnb_params_invalid_target_should_panic() {
TestBnB {
target: "11 cBTC",
cost_of_change: "1 cBTC",
fee_rate: "0",
lt_fee_rate: "0",
max_weight: "40000 wu",
weighted_utxos: &["1.5 cBTC/68 vB"],
expected_utxos: &["1.5 cBTC/68 vB"],
expected_error: None,
expected_iterations: 2,
}
.assert();
}
#[test]
fn select_coins_bnb_zero() {
TestBnB {
target: "0",
cost_of_change: "0",
fee_rate: "0",
lt_fee_rate: "0",
max_weight: "40000 wu",
weighted_utxos: &["1 cBTC/68 vB"],
expected_utxos: &[],
expected_error: Some(SolutionNotFound),
expected_iterations: 0,
}
.assert();
}
#[test]
fn select_coins_bnb_cost_of_change() {
let mut t = TestBnB {
target: "1 cBTC",
cost_of_change: "1 cBTC",
fee_rate: "0",
lt_fee_rate: "0",
max_weight: "40000 wu",
weighted_utxos: &["1.5 cBTC/68 vB"],
expected_utxos: &["1.5 cBTC/68 vB"],
expected_error: None,
expected_iterations: 2,
};
t.assert();
t.cost_of_change = "0";
t.expected_utxos = &[];
t.expected_error = Some(SolutionNotFound);
t.expected_iterations = 0;
t.assert();
}
#[test]
fn select_coins_bnb_effective_value() {
TestBnB {
target: "1 cBTC",
cost_of_change: "0",
fee_rate: "10 sat/kwu",
lt_fee_rate: "10 sat/kwu",
max_weight: "40000 wu",
weighted_utxos: &["1 cBTC/68 vB"],
expected_utxos: &[],
expected_error: Some(InsufficentFunds),
expected_iterations: 0,
}
.assert();
}
#[test]
fn select_coins_bnb_target_greater_than_value() {
TestBnB {
target: "11 cBTC",
cost_of_change: "0",
fee_rate: "0",
lt_fee_rate: "0",
max_weight: "40000 wu",
weighted_utxos: &["1 cBTC/68 vB", "2 cBTC/68 vB", "3 cBTC/68 vB", "4 cBTC/68 vB"],
expected_utxos: &[],
expected_error: Some(InsufficentFunds),
expected_iterations: 0,
}
.assert();
}
#[test]
fn select_coins_bnb_consume_more_inputs_when_cheap() {
TestBnB {
target: "6 sats",
cost_of_change: "0",
fee_rate: "10 sat/kwu",
lt_fee_rate: "20 sat/kwu",
max_weight: "40000 wu",
weighted_utxos: &[
"e(1 sats)/68 vB",
"e(2 sats)/68 vB",
"e(3 sats)/68 vB",
"e(4 sats)/68 vB",
],
expected_utxos: &["e(3 sats)/68 vB", "e(2 sats)/68 vB", "e(1 sats)/68 vB"],
expected_error: None,
expected_iterations: 12,
}
.assert();
}
#[test]
fn select_coins_bnb_consume_less_inputs_when_expensive() {
TestBnB {
target: "6 sats",
cost_of_change: "0",
fee_rate: "20 sat/kwu",
lt_fee_rate: "10 sat/kwu",
max_weight: "40000 wu",
weighted_utxos: &[
"e(1 sats)/68 vB",
"e(2 sats)/68 vB",
"e(3 sats)/68 vB",
"e(4 sats)/68 vB",
],
expected_utxos: &["e(4 sats)/68 vB", "e(2 sats)/68 vB"],
expected_error: None,
expected_iterations: 12,
}
.assert();
}
#[test]
fn select_coins_bnb_consume_less_inputs_with_excess_when_expensive() {
TestBnB {
target: "6 sats",
cost_of_change: "1 sats",
fee_rate: "20 sat/kwu",
lt_fee_rate: "10 sat/kwu",
max_weight: "40000 wu",
weighted_utxos: &[
"e(1 sats)/68 vB",
"e(2 sats)/68 vB",
"e(3 sats)/68 vB",
"e(4 sats)/68 vB",
],
expected_utxos: &["e(4 sats)/68 vB", "e(2 sats)/68 vB"],
expected_error: None,
expected_iterations: 12,
}
.assert();
}
#[test]
fn select_coins_bnb_utxo_pool_sum_overflow() {
TestBnB {
target: "1 cBTC",
cost_of_change: "0",
fee_rate: "0",
lt_fee_rate: "0",
max_weight: "40000 wu",
weighted_utxos: &["2100000000000000 sats/68 vB", "1 sats/68 vB"], expected_utxos: &[],
expected_error: Some(Overflow(Addition)),
expected_iterations: 0,
}
.assert();
}
#[test]
fn select_coins_bnb_upper_bound_overflow() {
TestBnB {
target: "1 sats",
cost_of_change: "2100000000000000 sats", fee_rate: "0",
lt_fee_rate: "0",
max_weight: "40000 wu",
weighted_utxos: &["e(1 sats)/68 vB"],
expected_utxos: &[],
expected_error: Some(Overflow(Addition)),
expected_iterations: 0,
}
.assert();
}
#[test]
fn select_coins_bnb_effective_value_tie_high_fee_rate() {
TestBnB {
target: "100 sats",
cost_of_change: "10 sats",
fee_rate: "20 sat/kwu",
lt_fee_rate: "10 sat/kwu",
max_weight: "40000 wu",
weighted_utxos: &["e(50 sats)/230 wu", "e(50 sats)/272 wu", "e(50 sats)/230 wu"],
expected_utxos: &["e(50 sats)/230 wu", "e(50 sats)/230 wu"],
expected_error: None,
expected_iterations: 9,
}
.assert();
}
#[test]
fn select_coins_bnb_effective_value_tie_low_fee_rate() {
TestBnB {
target: "100 sats",
cost_of_change: "10 sats",
fee_rate: "10 sat/kwu",
lt_fee_rate: "20 sat/kwu",
max_weight: "40000 wu",
weighted_utxos: &["e(50 sats)/272 wu", "e(50 sats)/230 wu", "e(50 sats)/272 wu"],
expected_utxos: &["e(50 sats)/272 wu", "e(50 sats)/272 wu"],
expected_error: None,
expected_iterations: 9,
}
.assert();
}
#[test]
fn select_coins_bnb_set_size_five() {
TestBnB {
target: "6 cBTC",
cost_of_change: "0",
fee_rate: "0",
lt_fee_rate: "0",
max_weight: "40000 wu",
weighted_utxos: &[
"3 cBTC/68 vB",
"2.9 cBTC/68 vB",
"2 cBTC/68 vB",
"1.0 cBTC/68 vB",
"1 cBTC/68 vB",
],
expected_utxos: &["3 cBTC/68 vB", "2 cBTC/68 vB", "1 cBTC/68 vB"],
expected_error: None,
expected_iterations: 22,
}
.assert();
}
#[test]
fn select_coins_bnb_set_size_seven() {
TestBnB {
target: "18 cBTC",
cost_of_change: "50 sats",
fee_rate: "0",
lt_fee_rate: "0",
max_weight: "40000 wu",
weighted_utxos: &[
"10 cBTC/68 vB",
"7000005 sats/68 vB",
"6000005 sats/68 vB",
"6 cBTC/68 vB",
"3 cBTC/68 vB",
"2 cBTC/68 vB",
"1000005 cBTC/68 vB",
],
expected_utxos: &["10 cBTC/68 vB", "6 cBTC/68 vB", "2 cBTC/68 vB"],
expected_error: None,
expected_iterations: 44,
}
.assert();
}
#[test]
fn select_coins_bnb_early_bail_optimization() {
let mut utxos =
vec!["7 cBTC/68 vB", "7 cBTC/68 vB", "7 cBTC/68 vB", "7 cBTC/68 vB", "2 cBTC/68 vB"];
for _i in 0..50_000 {
utxos.push("5 cBTC/68 vB");
}
TestBnB {
target: "30 cBTC",
cost_of_change: "5000 sats",
fee_rate: "0",
lt_fee_rate: "0",
max_weight: "40000 wu",
weighted_utxos: &utxos,
expected_utxos: &[
"7 cBTC/68 vB",
"7 cBTC/68 vB",
"7 cBTC/68 vB",
"7 cBTC/68 vB",
"2 cBTC/68 vB",
],
expected_error: None,
expected_iterations: 100_000,
}
.assert();
}
#[test]
fn select_coins_bnb_max_weight_yields_no_solution() {
TestBnB {
target: "16 cBTC",
cost_of_change: "0",
fee_rate: "0",
lt_fee_rate: "0",
max_weight: "40000",
weighted_utxos: &[
"10 cBTC/68 vB",
"9 cBTC/68 vB",
"8 cBTC/68 vB",
"5 cBTC/10000 vB",
"3 cBTC/68 vB",
"1 cBTC/68 vB",
],
expected_utxos: &[],
expected_error: Some(MaxWeightExceeded),
expected_iterations: 26,
}
.assert();
}
#[test]
fn select_coins_bnb_max_weight_without_error() {
TestBnB {
target: "1 cBTC",
cost_of_change: "1000 sats",
fee_rate: "10 sat/kwu",
lt_fee_rate: "20 sat/kwu",
max_weight: "40000",
weighted_utxos: &["e(2 cBTC)/30000 wu", "e(1 cBTC)/20000 wu"],
expected_utxos: &["e(1 cBTC)/20000 wu"],
expected_error: None,
expected_iterations: 4,
}
.assert();
}
#[test]
fn select_coins_bnb_utxo_pool_weight_overflow() {
TestBnB {
target: "1 cBTC",
cost_of_change: "0",
fee_rate: "0",
lt_fee_rate: "0",
max_weight: "40000 wu",
weighted_utxos: &["1 sats/18446744073709551615 wu", "1 sats/1 wu"], expected_utxos: &[],
expected_error: Some(Overflow(Addition)),
expected_iterations: 0,
}
.assert();
}
#[test]
fn select_coins_bnb_exhaust() {
let base: usize = 2;
let alpha = (0..17).enumerate().map(|(i, _)| base.pow(17 + i as u32));
let target = Amount::from_sat_u32(alpha.clone().sum::<usize>() as u32);
let fee_rate = FeeRate::ZERO;
let lt_fee_rate = FeeRate::ZERO;
let max_weight = Weight::from_wu(40_000);
let beta = (0..17).enumerate().map(|(i, _)| {
let a = base.pow(17 + i as u32);
let b = base.pow(16 - i as u32);
a + b
});
let amts: Vec<_> = zip(alpha, beta)
.flat_map(|tup| once(tup.0).chain(once(tup.1)))
.map(|a| Amount::from_sat_u32(a as u32))
.collect();
let pool: Vec<_> = amts
.into_iter()
.filter_map(|a| WeightedUtxo::new(a, Weight::ZERO, fee_rate, lt_fee_rate))
.collect();
let result = branch_and_bound(target, Amount::ONE_SAT, max_weight, &pool);
match result {
Err(IterationLimitReached) => {}
_ => panic!(),
}
}
#[test]
fn select_coins_bnb_exhaust_v2() {
let base: u32 = 2;
let mut target = 0;
let max_weight = Weight::from_wu(40_000);
let vals = (0..15).enumerate().flat_map(|(i, _)| {
let a = base.pow(15 + i as u32);
target += a;
vec![a, a + 2]
});
let fee_rate = FeeRate::ZERO;
let lt_fee_rate = FeeRate::ZERO;
let amts: Vec<_> = vals.map(Amount::from_sat_u32).collect();
let pool: Vec<_> = amts
.into_iter()
.filter_map(|a| WeightedUtxo::new(a, Weight::ZERO, fee_rate, lt_fee_rate))
.collect();
let result =
branch_and_bound(Amount::from_sat_u32(target), Amount::ONE_SAT, max_weight, &pool);
match result {
Err(IterationLimitReached) => {}
_ => panic!(),
}
}
#[test]
fn select_coins_bnb_exhaust_with_result() {
let base: u32 = 2;
let mut target = 0;
let max_weight = Weight::from_wu(40_000);
let amts = (0..15).enumerate().flat_map(|(i, _)| {
let a = base.pow(15 + i as u32);
target += a;
vec![a, a + 2]
});
let fee_rate = FeeRate::ZERO;
let lt_fee_rate = FeeRate::ZERO;
let mut amts: Vec<_> = amts.map(Amount::from_sat_u32).collect();
amts.push(Amount::from_sat_u32(target));
let pool: Vec<_> = amts
.into_iter()
.filter_map(|a| WeightedUtxo::new(a, Weight::ZERO, fee_rate, lt_fee_rate))
.collect();
let (iterations, utxos) =
branch_and_bound(Amount::from_sat_u32(target), Amount::ONE_SAT, max_weight, &pool)
.unwrap();
assert_eq!(utxos.len(), 1);
assert_eq!(utxos[0].value(), Amount::from_sat_u32(target));
assert_eq!(100000, iterations);
}
#[test]
fn select_coins_bnb_solution_proptest() {
arbtest(|u| {
let assert_bnb = AssertBnB::arbitrary(u)?;
assert_bnb.exec();
Ok(())
});
}
#[test]
fn select_coins_bnb_thrifty_proptest() {
arbtest(|u| {
let candidate_selection = Selection::arbitrary(u)?;
let target = Amount::arbitrary(u)?;
let cost_of_change = Amount::arbitrary(u)?;
let fee_rate_a = candidate_selection.fee_rate;
let fee_rate_b = candidate_selection.long_term_fee_rate;
let max_weight = Weight::MAX;
let candidate_utxos = candidate_selection.utxos;
let result_a = branch_and_bound(target, cost_of_change, max_weight, &candidate_utxos);
let utxo_selection_attributes =
candidate_utxos.clone().into_iter().map(|u| (u.value(), u.weight()));
let utxos_b: Vec<WeightedUtxo> = utxo_selection_attributes
.filter_map(|(amt, weight)| WeightedUtxo::new(amt, weight, fee_rate_b, fee_rate_a))
.collect();
let result_b = branch_and_bound(target, cost_of_change, max_weight, &utxos_b);
if let Ok((_, utxos_a)) = result_a {
if let Ok((_, utxos_b)) = result_b {
let weight_sum_a = utxos_a
.iter()
.map(|u| u.weight())
.try_fold(Weight::ZERO, Weight::checked_add);
let weight_sum_b = utxos_b
.iter()
.map(|u| u.weight())
.try_fold(Weight::ZERO, Weight::checked_add);
if let Some(weight_a) = weight_sum_a {
if let Some(weight_b) = weight_sum_b {
if fee_rate_a < fee_rate_b {
assert!(weight_a >= weight_b);
}
if fee_rate_b < fee_rate_a {
assert!(weight_b >= weight_a);
}
if fee_rate_a == fee_rate_b {
assert!(weight_a == weight_b);
}
}
}
}
}
Ok(())
});
}
}