use crate::{
types::{CoinSelectionOpt, OutputGroup, SelectionError, SelectionOutput, WasteMetric},
utils::{calculate_fee, calculate_waste},
};
#[derive(Debug)]
struct BnbContext {
target_for_match: u64,
match_range: u64,
options: CoinSelectionOpt,
tries: u32,
best_solution: Option<(Vec<usize>, f32)>,
}
pub fn select_coin_bnb(
inputs: &[OutputGroup],
options: &CoinSelectionOpt,
) -> Result<SelectionOutput, SelectionError> {
let cost_per_input = calculate_fee(options.avg_input_weight, options.target_feerate)?;
let cost_per_output = calculate_fee(options.avg_output_weight, options.target_feerate)?;
let base_fee = calculate_fee(options.base_weight, options.target_feerate)?;
let mut sorted_inputs: Vec<(usize, &OutputGroup)> = inputs.iter().enumerate().collect();
sorted_inputs.sort_by_key(|(_, input)| input.value);
let mut ctx = BnbContext {
target_for_match: options.target_value
+ options.min_change_value
+ base_fee.max(options.min_absolute_fee),
match_range: cost_per_input + cost_per_output,
options: options.clone(),
tries: 1_000_000,
best_solution: None,
};
let mut selected_inputs = vec![];
bnb(&sorted_inputs, &mut selected_inputs, 0, 0, 0, &mut ctx);
match ctx.best_solution {
Some((selected, waste)) => Ok(SelectionOutput {
selected_inputs: selected,
waste: WasteMetric(waste),
}),
None => Err(SelectionError::NoSolutionFound),
}
}
fn bnb(
sorted: &[(usize, &OutputGroup)],
selected: &mut Vec<usize>,
acc_value: u64,
acc_weight: u64,
depth: usize,
ctx: &mut BnbContext,
) {
if ctx.tries == 0 || depth >= sorted.len() {
return;
}
ctx.tries -= 1;
let fee = calculate_fee(acc_weight, ctx.options.target_feerate)
.unwrap_or(ctx.options.min_absolute_fee);
let effective_value = acc_value.saturating_sub(fee);
if effective_value > ctx.target_for_match + ctx.match_range {
return;
}
if effective_value >= ctx.target_for_match {
let waste = calculate_waste(&ctx.options, acc_value, acc_weight, fee);
if ctx.best_solution.is_none() || waste < ctx.best_solution.as_ref().unwrap().1 {
ctx.best_solution = Some((selected.clone(), waste));
}
return;
}
let (index, input) = sorted[depth];
let input_effective_value = input.value.saturating_sub(
calculate_fee(input.weight, ctx.options.target_feerate)
.unwrap_or(ctx.options.min_absolute_fee),
);
selected.push(index);
bnb(
sorted,
selected,
acc_value + input_effective_value,
acc_weight + input.weight,
depth + 1,
ctx,
);
selected.pop();
bnb(sorted, selected, acc_value, acc_weight, depth + 1, ctx);
}
#[cfg(test)]
mod test {
use crate::{
algorithms::bnb::select_coin_bnb,
types::{CoinSelectionOpt, ExcessStrategy, OutputGroup, SelectionError},
};
fn setup_basic_output_groups() -> Vec<OutputGroup> {
vec![
OutputGroup {
value: 1000,
weight: 100,
input_count: 1,
creation_sequence: None,
},
OutputGroup {
value: 2000,
weight: 200,
input_count: 1,
creation_sequence: None,
},
OutputGroup {
value: 3000,
weight: 300,
input_count: 1,
creation_sequence: None,
},
]
}
fn bnb_setup_options(target_value: u64) -> CoinSelectionOpt {
CoinSelectionOpt {
target_value,
target_feerate: 0.5, long_term_feerate: None,
min_absolute_fee: 500,
base_weight: 10,
change_weight: 50,
change_cost: 10,
avg_input_weight: 40,
avg_output_weight: 20,
min_change_value: 500,
excess_strategy: ExcessStrategy::ToChange,
}
}
fn test_bnb_solution() {
let mut values = [
OutputGroup {
value: 55000,
weight: 500,
input_count: 1,
creation_sequence: None,
},
OutputGroup {
value: 400,
weight: 200,
input_count: 1,
creation_sequence: None,
},
OutputGroup {
value: 40000,
weight: 300,
input_count: 1,
creation_sequence: None,
},
OutputGroup {
value: 25000,
weight: 100,
input_count: 1,
creation_sequence: None,
},
OutputGroup {
value: 35000,
weight: 150,
input_count: 1,
creation_sequence: None,
},
OutputGroup {
value: 600,
weight: 250,
input_count: 1,
creation_sequence: None,
},
OutputGroup {
value: 30000,
weight: 120,
input_count: 1,
creation_sequence: None,
},
OutputGroup {
value: 94730,
weight: 50,
input_count: 1,
creation_sequence: None,
},
OutputGroup {
value: 29810,
weight: 500,
input_count: 1,
creation_sequence: None,
},
OutputGroup {
value: 78376,
weight: 200,
input_count: 1,
creation_sequence: None,
},
OutputGroup {
value: 17218,
weight: 300,
input_count: 1,
creation_sequence: None,
},
OutputGroup {
value: 13728,
weight: 100,
input_count: 1,
creation_sequence: None,
},
];
let opt = bnb_setup_options(195782);
let ans = select_coin_bnb(&values, &opt);
values.sort_by_key(|v| v.value);
if let Ok(selection_output) = ans {
let expected_solution = vec![1, 5, 11, 6, 4, 2, 9];
assert_eq!(
selection_output.selected_inputs, expected_solution,
"Expected solution {:?}, but got {:?}",
expected_solution, selection_output.selected_inputs
);
} else {
panic!("Failed to find a solution");
}
}
fn test_bnb_no_solution() {
let inputs = setup_basic_output_groups();
let total_input_value: u64 = inputs.iter().map(|input| input.value).sum();
let impossible_target = total_input_value + 1000;
let options = bnb_setup_options(impossible_target);
let result = select_coin_bnb(&inputs, &options);
assert!(
matches!(result, Err(SelectionError::NoSolutionFound)),
"Expected NoSolutionFound error, got {:?}",
result
);
}
#[test]
fn test_bnb() {
test_bnb_solution();
test_bnb_no_solution();
}
}