1use crate::{
2 types::{CoinSelectionOpt, OutputGroup, SelectionError, SelectionOutput, WasteMetric},
3 utils::{calculate_fee, calculate_waste},
4};
5
6#[derive(Debug)]
8struct BnbContext {
9 target_for_match: u64,
10 match_range: u64,
11 options: CoinSelectionOpt,
12 tries: u32,
13 best_solution: Option<(Vec<usize>, f32)>,
14 }
17
18pub fn select_coin_bnb(
20 inputs: &[OutputGroup],
21 options: &CoinSelectionOpt,
22) -> Result<SelectionOutput, SelectionError> {
23 let cost_per_input = calculate_fee(options.avg_input_weight, options.target_feerate)?;
24 let cost_per_output = calculate_fee(options.avg_output_weight, options.target_feerate)?;
25 let base_fee = calculate_fee(options.base_weight, options.target_feerate)?;
26
27 let mut sorted_inputs: Vec<(usize, &OutputGroup)> = inputs.iter().enumerate().collect();
28 sorted_inputs.sort_by_key(|(_, input)| input.value);
29
30 let mut ctx = BnbContext {
31 target_for_match: options.target_value
32 + options.min_change_value
33 + base_fee.max(options.min_absolute_fee),
34 match_range: cost_per_input + cost_per_output,
35 options: options.clone(),
36 tries: 1_000_000,
37 best_solution: None,
38 };
39
40 let mut selected_inputs = vec![];
41
42 bnb(&sorted_inputs, &mut selected_inputs, 0, 0, 0, &mut ctx);
43
44 match ctx.best_solution {
45 Some((selected, waste)) => Ok(SelectionOutput {
46 selected_inputs: selected,
47 waste: WasteMetric(waste),
48 }),
49 None => Err(SelectionError::NoSolutionFound),
50 }
51}
52
53fn bnb(
54 sorted: &[(usize, &OutputGroup)],
55 selected: &mut Vec<usize>,
56 acc_value: u64,
57 acc_weight: u64,
58 depth: usize,
59 ctx: &mut BnbContext,
60) {
61 if ctx.tries == 0 || depth >= sorted.len() {
62 return;
63 }
64 ctx.tries -= 1;
65
66 let fee = calculate_fee(acc_weight, ctx.options.target_feerate)
68 .unwrap_or(ctx.options.min_absolute_fee);
69 let effective_value = acc_value.saturating_sub(fee);
73
74 if effective_value > ctx.target_for_match + ctx.match_range {
76 return;
77 }
78
79 if effective_value >= ctx.target_for_match {
81 let waste = calculate_waste(&ctx.options, acc_value, acc_weight, fee);
82 if ctx.best_solution.is_none() || waste < ctx.best_solution.as_ref().unwrap().1 {
83 ctx.best_solution = Some((selected.clone(), waste));
84 }
85 return;
86 }
87
88 let (index, input) = sorted[depth];
89 let input_effective_value = input.value.saturating_sub(
90 calculate_fee(input.weight, ctx.options.target_feerate)
91 .unwrap_or(ctx.options.min_absolute_fee),
92 );
93
94 selected.push(index);
96 bnb(
97 sorted,
98 selected,
99 acc_value + input_effective_value,
100 acc_weight + input.weight,
101 depth + 1,
102 ctx,
103 );
104 selected.pop();
105
106 bnb(sorted, selected, acc_value, acc_weight, depth + 1, ctx);
108}
109
110#[cfg(test)]
111mod test {
112 use crate::{
113 algorithms::bnb::select_coin_bnb,
114 types::{CoinSelectionOpt, ExcessStrategy, OutputGroup, SelectionError},
115 };
116
117 fn setup_basic_output_groups() -> Vec<OutputGroup> {
118 vec![
119 OutputGroup {
120 value: 1000,
121 weight: 100,
122 input_count: 1,
123 creation_sequence: None,
124 },
125 OutputGroup {
126 value: 2000,
127 weight: 200,
128 input_count: 1,
129 creation_sequence: None,
130 },
131 OutputGroup {
132 value: 3000,
133 weight: 300,
134 input_count: 1,
135 creation_sequence: None,
136 },
137 ]
138 }
139
140 fn bnb_setup_options(target_value: u64) -> CoinSelectionOpt {
141 CoinSelectionOpt {
142 target_value,
143 target_feerate: 0.5, long_term_feerate: None,
145 min_absolute_fee: 500,
146 base_weight: 10,
147 change_weight: 50,
148 change_cost: 10,
149 avg_input_weight: 40,
150 avg_output_weight: 20,
151 min_change_value: 500,
152 excess_strategy: ExcessStrategy::ToChange,
153 }
154 }
155
156 fn test_bnb_solution() {
157 let mut values = [
159 OutputGroup {
160 value: 55000,
161 weight: 500,
162 input_count: 1,
163 creation_sequence: None,
164 },
165 OutputGroup {
166 value: 400,
167 weight: 200,
168 input_count: 1,
169 creation_sequence: None,
170 },
171 OutputGroup {
172 value: 40000,
173 weight: 300,
174 input_count: 1,
175 creation_sequence: None,
176 },
177 OutputGroup {
178 value: 25000,
179 weight: 100,
180 input_count: 1,
181 creation_sequence: None,
182 },
183 OutputGroup {
184 value: 35000,
185 weight: 150,
186 input_count: 1,
187 creation_sequence: None,
188 },
189 OutputGroup {
190 value: 600,
191 weight: 250,
192 input_count: 1,
193 creation_sequence: None,
194 },
195 OutputGroup {
196 value: 30000,
197 weight: 120,
198 input_count: 1,
199 creation_sequence: None,
200 },
201 OutputGroup {
202 value: 94730,
203 weight: 50,
204 input_count: 1,
205 creation_sequence: None,
206 },
207 OutputGroup {
208 value: 29810,
209 weight: 500,
210 input_count: 1,
211 creation_sequence: None,
212 },
213 OutputGroup {
214 value: 78376,
215 weight: 200,
216 input_count: 1,
217 creation_sequence: None,
218 },
219 OutputGroup {
220 value: 17218,
221 weight: 300,
222 input_count: 1,
223 creation_sequence: None,
224 },
225 OutputGroup {
226 value: 13728,
227 weight: 100,
228 input_count: 1,
229 creation_sequence: None,
230 },
231 ];
232
233 let opt = bnb_setup_options(195782);
235 let ans = select_coin_bnb(&values, &opt);
236 values.sort_by_key(|v| v.value);
237 if let Ok(selection_output) = ans {
238 let expected_solution = vec![1, 5, 11, 6, 4, 2, 9];
239 assert_eq!(
240 selection_output.selected_inputs, expected_solution,
241 "Expected solution {:?}, but got {:?}",
242 expected_solution, selection_output.selected_inputs
243 );
244 } else {
245 panic!("Failed to find a solution");
246 }
247 }
248
249 fn test_bnb_no_solution() {
250 let inputs = setup_basic_output_groups();
251 let total_input_value: u64 = inputs.iter().map(|input| input.value).sum();
252 let impossible_target = total_input_value + 1000;
253 let options = bnb_setup_options(impossible_target);
254 let result = select_coin_bnb(&inputs, &options);
255 assert!(
256 matches!(result, Err(SelectionError::NoSolutionFound)),
257 "Expected NoSolutionFound error, got {:?}",
258 result
259 );
260 }
261
262 #[test]
263 fn test_bnb() {
264 test_bnb_solution();
265 test_bnb_no_solution();
266 }
267}