diff --git a/src/lib.rs b/src/lib.rs index 1039d8f..538634e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ //! A blockchain-agnostic Rust Coinselection library -use rand::{seq::SliceRandom, thread_rng, Rng}; +use rand::{rngs::ThreadRng, seq::SliceRandom, thread_rng, Rng}; use std::cmp::Reverse; use std::collections::HashSet; use std::hash::{Hash, Hasher}; @@ -98,25 +98,176 @@ pub struct SelectionOutput { /// The waste amount, for the above inputs. pub waste: WasteMetric, } +/// Struct for three arguments : target_for_match, match_range and target_feerate +/// +/// Wrapped in a struct or else input for fn bnb takes too many arguments - 9/7 +/// Leading to usage of stack instead of registers - https://users.rust-lang.org/t/avoiding-too-many-arguments-passing-to-a-function/103581 +/// Fit in : 1 XMM register, 1 GPR +#[derive(Debug)] +pub struct MatchParameters { + target_for_match: u64, + match_range: u64, + target_feerate: f32, +} /// Perform Coinselection via Branch And Bound algorithm. pub fn select_coin_bnb( inputs: &[OutputGroup], options: CoinSelectionOpt, ) -> Result { - unimplemented!() + let mut selected_inputs: Vec = vec![]; + + /// Variable is mutable for decrement of bnb_tries for every iteration of fn bnb + let mut bnb_tries: u32 = 1_000_000; + + let rng = &mut thread_rng(); + + let match_parameters = MatchParameters { + target_for_match: options.target_value + + calculate_fee(options.base_weight, options.target_feerate) + + options.cost_per_output, + match_range: options.cost_per_input + options.cost_per_output, + target_feerate: options.target_feerate, + }; + + let mut sorted_inputs: Vec<(usize, OutputGroup)> = inputs + .iter() + .enumerate() + .map(|(index, input)| (index, *input)) + .collect(); + sorted_inputs.sort_by_key(|(_, input)| std::cmp::Reverse(input.value)); + + let bnb_selected_coin = bnb( + &sorted_inputs, + &mut selected_inputs, + 0, + 0, + &mut bnb_tries, + rng, + &match_parameters, + ); + match bnb_selected_coin { + Some(selected_coin) => { + let accumulated_value: u64 = selected_coin + .iter() + .fold(0, |acc, &i| acc + inputs[i].value); + let accumulated_weight: u32 = selected_coin + .iter() + .fold(0, |acc, &i| acc + inputs[i].weight); + let estimated_fee = 0; + let waste = calculate_waste( + inputs, + &selected_inputs, + &options, + accumulated_value, + accumulated_weight, + estimated_fee, + ); + let selection_output = SelectionOutput { + selected_inputs: selected_coin, + waste: WasteMetric(waste), + }; + Ok(selection_output) + } + None => Err(SelectionError::NoSolutionFound), + } } -/// Return empty vec if no solutions are found. +/// Return empty vec if no solutions are found +/// +// changing the selected_inputs : &[usize] -> &mut Vec fn bnb( inputs_in_desc_value: &[(usize, OutputGroup)], - selected_inputs: &[usize], - effective_value: u64, + selected_inputs: &mut Vec, + acc_eff_value: u64, depth: usize, - bnp_tries: u32, - options: &CoinSelectionOpt, -) -> Vec { - unimplemented!() + bnb_tries: &mut u32, + rng: &mut ThreadRng, + match_parameters: &MatchParameters, +) -> Option> { + if acc_eff_value > match_parameters.target_for_match + match_parameters.match_range { + return None; + } + if acc_eff_value >= match_parameters.target_for_match { + return Some(selected_inputs.to_vec()); + } + + // Decrement of bnb_tries for every iteration + *bnb_tries -= 1; + // Capping the number of iterations on the computation + if *bnb_tries == 0 || depth >= inputs_in_desc_value.len() { + return None; + } + if rng.gen_bool(0.5) { + // exploring the inclusion branch + // first include then omit + let new_effective_value = acc_eff_value + + effective_value( + &inputs_in_desc_value[depth].1, + match_parameters.target_feerate, + ); + selected_inputs.push(inputs_in_desc_value[depth].0); + let with_this = bnb( + inputs_in_desc_value, + selected_inputs, + new_effective_value, + depth + 1, + bnb_tries, + rng, + match_parameters, + ); + match with_this { + Some(_) => with_this, + None => { + selected_inputs.pop(); // popping out the selected utxo if it does not fit + bnb( + inputs_in_desc_value, + selected_inputs, + acc_eff_value, + depth + 1, + bnb_tries, + rng, + match_parameters, + ) + } + } + } else { + match bnb( + inputs_in_desc_value, + selected_inputs, + acc_eff_value, + depth + 1, + bnb_tries, + rng, + match_parameters, + ) { + Some(without_this) => Some(without_this), + None => { + let new_effective_value = acc_eff_value + + effective_value( + &inputs_in_desc_value[depth].1, + match_parameters.target_feerate, + ); + selected_inputs.push(inputs_in_desc_value[depth].0); + let with_this = bnb( + inputs_in_desc_value, + selected_inputs, + new_effective_value, + depth + 1, + bnb_tries, + rng, + match_parameters, + ); + match with_this { + Some(_) => with_this, + None => { + selected_inputs.pop(); // poping out the selected utxo if it does not fit + None + } + } + } + } + } } /// Perform Coinselection via Knapsack solver. @@ -446,6 +597,7 @@ pub fn select_coin( options: CoinSelectionOpt, ) -> Result { let algorithms: Vec = vec![ + select_coin_bnb, select_coin_fifo, select_coin_lowestlarger, select_coin_srd, @@ -767,9 +919,110 @@ mod test { }) } } - #[test] - fn test_bnb() { - // Perform BNB selection of set of test values. + + fn bnb_setup_options(target_value: u64) -> CoinSelectionOpt { + CoinSelectionOpt { + target_value, + target_feerate: 0.5, // Simplified feerate + long_term_feerate: None, + min_absolute_fee: 0, + base_weight: 10, + drain_weight: 50, + drain_cost: 10, + cost_per_input: 20, + cost_per_output: 10, + min_drain_value: 500, + excess_strategy: ExcessStrategy::ToDrain, + } + } + + fn test_bnb_solution() { + // Define the test values + let values = [ + OutputGroup { + value: 55000, + weight: 500, + input_count: 1, + is_segwit: false, + creation_sequence: None, + }, + OutputGroup { + value: 400, + weight: 200, + input_count: 1, + is_segwit: false, + creation_sequence: None, + }, + OutputGroup { + value: 40000, + weight: 300, + input_count: 1, + is_segwit: false, + creation_sequence: None, + }, + OutputGroup { + value: 25000, + weight: 100, + input_count: 1, + is_segwit: false, + creation_sequence: None, + }, + OutputGroup { + value: 35000, + weight: 150, + input_count: 1, + is_segwit: false, + creation_sequence: None, + }, + OutputGroup { + value: 600, + weight: 250, + input_count: 1, + is_segwit: false, + creation_sequence: None, + }, + OutputGroup { + value: 30000, + weight: 120, + input_count: 1, + is_segwit: false, + creation_sequence: None, + }, + OutputGroup { + value: 5000, + weight: 50, + input_count: 1, + is_segwit: false, + creation_sequence: None, + }, + ]; + + // Adjust the target value to ensure it tests for multiple valid solutions + let opt = bnb_setup_options(5730); + let ans = select_coin_bnb(&values, opt); + if let Ok(selection_output) = ans { + let expected_solution = vec![7, 5, 1]; + 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 + ); } fn test_successful_selection() { @@ -1209,6 +1462,12 @@ mod test { test_core_knapsack_vectors(); } + #[test] + fn test_bnb() { + test_bnb_solution(); + test_bnb_no_solution(); + } + #[test] fn test_fifo() { test_successful_selection();