From 743fd3ee9bb636df034f53367b858a705558426c Mon Sep 17 00:00:00 2001 From: yancy Date: Thu, 7 Dec 2023 22:16:53 +0100 Subject: [PATCH] wip: Add loop iteration limit --- src/lib.rs | 171 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 149 insertions(+), 22 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0e29ce4..c3b54de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,7 @@ const CHANGE_LOWER: Amount = Amount::from_sat(50_000); /// The idea of using a WeightUtxo type was inspired by the BDK implementation: /// #[derive(Clone, Debug, PartialEq)] +// note, change this to private? No good reason to be public. pub struct WeightedUtxo { /// The satisfaction_weight is the size of the required params to satisfy the UTXO. pub satisfaction_weight: Weight, @@ -150,9 +151,19 @@ pub fn select_coins_bnb( target: Amount, _fee_rate: FeeRate, weighted_utxos: &mut [WeightedUtxo], -) -> Option> { +) -> Option> { + // Total_Tries in Core: + // https://github.com/bitcoin/bitcoin/blob/1d9da8da309d1dbf9aef15eb8dc43b4a2dc3d309/src/wallet/coinselection.cpp#L74 + const ITERATION_LIMIT: i32 = 100_000; + + let mut iteration = 0; + let mut index = 0; + let mut value = Amount::ZERO; - let mut selection: Vec = vec![]; + let waste = Amount::MAX_MONEY; + + let mut selection: Vec = vec![]; + let mut best_selection: Vec = vec![]; let available_value: Amount = weighted_utxos.iter().map(|u| u.utxo.value).sum(); if available_value < target { @@ -161,29 +172,96 @@ pub fn select_coins_bnb( weighted_utxos.sort_by(|a, b| b.utxo.value.cmp(&a.utxo.value)); - for (_i, utxo) in weighted_utxos.iter().enumerate() { + while iteration < ITERATION_LIMIT { + //println!("iteration: {} index: {} value: {} selection: {:?} best_selection {:?}", iteration, index, value, selection, best_selection); // backtrack // // There are three conditions for backtracking: // - // * value meets or exceeded target - // * leaf node (nothing left to explorer) - // * not enough value to make it to target - if value >= target - || available_value + value < target - || utxo == weighted_utxos.last().unwrap() - { - // backtrack + // * value meets or exceeded target. + // * leaf node (nothing left to explorer). + // * not enough value to make it to target. + + // value meets or exceeds the target: + // + // For example: + // o + // / + // 4 + // / + // 3 + // + // Transform to: + // o + // / + // 4 + // / \ + // 3 + // + // The search index is decremented by 1 and the value + // is subtracted by the current search index. Therefore + // the omission branch at the search index is tested + // next. + if value >= target { + let current_waste = value - target; + + if current_waste <= waste { + best_selection = selection.clone(); + } + + selection.pop(); + + if selection.is_empty() { + return Some(best_selection); + } + + let utxo = &weighted_utxos[index]; + value -= utxo.utxo.value; + index -= 1; + } + // value is a leaf node, therefore, we remove the root + // and try the exclusion branch of that root node. + // o + // / \ + // 4 + // / \ + // 3 + // / \ + // 2 + // / + // 1 + // + // We try excluding 4 now + // o + // / \ + // 4 + // / \ + // 3 + // + // A new subtree is checked by resetting the search index + // to the root that was just removed. Therefore, the next + // iteration, the search index is incremented by 1 and we + // start with a root node that is the next in list. + else if index == weighted_utxos.len() { + index = selection[0]; + selection = vec![]; } + //if available_value + value < target { + //} // proceed with depth first search. else { - selection.push(utxo.clone()); + let utxo = &weighted_utxos[index]; + selection.push(index); value += utxo.utxo.value; } + + index += 1; + iteration += 1; } - Some(selection) + println!("all solutions checked, returning best_selection: {:?}", best_selection); + Some(best_selection) } #[cfg(test)] @@ -216,17 +294,66 @@ mod tests { }, }; - vec![utxo_one, utxo_two] + let utxo_three = WeightedUtxo { + satisfaction_weight: SATISFACTION_SIZE, + utxo: TxOut { + value: Amount::from_str("3 cBTC").unwrap(), + script_pubkey: ScriptBuf::new(), + }, + }; + + let utxo_four = WeightedUtxo { + satisfaction_weight: SATISFACTION_SIZE, + utxo: TxOut { + value: Amount::from_str("4 cBTC").unwrap(), + script_pubkey: ScriptBuf::new(), + }, + }; + + vec![utxo_one, utxo_two, utxo_three, utxo_four] + } + + #[test] + fn one_solution_one_coin() { + let target = Amount::from_str("1 cBTC").unwrap(); + + let mut weighted_utxos = vec![WeightedUtxo { + satisfaction_weight: SATISFACTION_SIZE, + utxo: TxOut { + value: Amount::from_str("1 cBTC").unwrap(), + script_pubkey: ScriptBuf::new(), + }, + }]; + + let index_list = select_coins_bnb(target, FEE_RATE, &mut weighted_utxos).unwrap(); + let expected_index_list = vec![0]; + assert_eq!(index_list, expected_index_list); } #[test] - fn find_solution_one_cbtc() { - let _target = Amount::from_str("1.5 cBTC").unwrap(); - let _weighted_utxos: Vec = create_weighted_utxos(); + fn one_solution_two_coins() { + let target = Amount::from_str("4 cBTC").unwrap(); + + let mut weighted_utxos = vec![ + WeightedUtxo { + satisfaction_weight: SATISFACTION_SIZE, + utxo: TxOut { + value: Amount::from_str("4 cBTC").unwrap(), + script_pubkey: ScriptBuf::new(), + }, + }, + WeightedUtxo { + satisfaction_weight: SATISFACTION_SIZE, + utxo: TxOut { + value: Amount::from_str("3 cBTC").unwrap(), + script_pubkey: ScriptBuf::new(), + }, + }, + ]; - //let utxo_match = select_coins_bnb(target, FEE_RATE, &mut weighted_utxos); - //let expected_bool_vec = vec![false, false, false, true]; - //assert_eq!(expected_bool_vec, utxo_match); + let index_list = select_coins_bnb(target, FEE_RATE, &mut weighted_utxos).unwrap(); + let expected_index_list = vec![0]; + assert_eq!(index_list, expected_index_list); } //#[test] @@ -299,8 +426,8 @@ mod tests { //} #[test] - fn select_coins_bnb_no_solution() { - let target: Amount = Amount::from_str("4 cBTC").unwrap(); + fn select_coins_bnb_not_enough_value() { + let target: Amount = Amount::from_str("11 cBTC").unwrap(); let mut weighted_utxos: Vec = create_weighted_utxos(); let result = select_coins_bnb(target, FEE_RATE, &mut weighted_utxos);