Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation for BNB Issue#1 #29

Merged
merged 11 commits into from
Sep 21, 2024
284 changes: 272 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -98,25 +98,177 @@ 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<SelectionOutput, SelectionError> {
unimplemented!()
let mut selected_inputs: Vec<usize> = 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clippy doc error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

///
// changing the selected_inputs : &[usize] -> &mut Vec<usize>
fn bnb(
inputs_in_desc_value: &[(usize, OutputGroup)],
selected_inputs: &[usize],
effective_value: u64,
selected_inputs: &mut Vec<usize>,
acc_eff_value: u64,
depth: usize,
bnp_tries: u32,
options: &CoinSelectionOpt,
) -> Vec<usize> {
unimplemented!()
bnb_tries: &mut u32,
rng: &mut ThreadRng,
match_parameters: &MatchParameters,
) -> Option<Vec<usize>> {
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 {
/// Avoided creation of intermediate variable, for marginally better performance.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment, confusing to the people who don't have context

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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 // this may or may not be correct
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct, so remove the comment

}
}
}
}
}
}

/// Perform Coinselection via Knapsack solver.
Expand Down Expand Up @@ -446,6 +598,7 @@ pub fn select_coin(
options: CoinSelectionOpt,
) -> Result<SelectionOutput, SelectionError> {
let algorithms: Vec<CoinSelectionFn> = vec![
select_coin_bnb,
select_coin_fifo,
select_coin_lowestlarger,
select_coin_srd,
Expand Down Expand Up @@ -767,9 +920,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() {
Expand Down Expand Up @@ -1209,6 +1463,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();
Expand Down
Loading