-
Notifications
You must be signed in to change notification settings - Fork 17
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
Changes from 9 commits
cc835d2
34664ac
6547d59
fab6521
2a6f20f
f3491ab
8d9e970
c80152c
9077ad5
a35692d
7f2bbb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,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 | ||
/// | ||
// 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove this comment, confusing to the people who don't have context There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is correct, so remove the comment |
||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// Perform Coinselection via Knapsack solver. | ||
|
@@ -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, | ||
|
@@ -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() { | ||
|
@@ -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(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clippy doc error
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done