diff --git a/src/lib.rs b/src/lib.rs index c3b54deb..df3da32a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ pub fn select_coins( fee_rate: FeeRate, weighted_utxos: &mut [WeightedUtxo], ) -> Option> { - match select_coins_bnb(target, cost_of_change, weighted_utxos) { + match select_coins_bnb(target, cost_of_change, weighted_utxos.to_vec()) { Some(_res) => Some(Vec::new()), None => select_coins_srd(target, fee_rate, weighted_utxos, &mut thread_rng()), } @@ -150,21 +150,23 @@ pub fn select_coins( pub fn select_coins_bnb( target: Amount, _fee_rate: FeeRate, - weighted_utxos: &mut [WeightedUtxo], + mut weighted_utxos: Vec, ) -> Option> { // Total_Tries in Core: // https://github.com/bitcoin/bitcoin/blob/1d9da8da309d1dbf9aef15eb8dc43b4a2dc3d309/src/wallet/coinselection.cpp#L74 - const ITERATION_LIMIT: i32 = 100_000; + const ITERATION_LIMIT: i32 = 10; let mut iteration = 0; let mut index = 0; + let mut backtrack; + let mut new_tree; let mut value = Amount::ZERO; - let waste = Amount::MAX_MONEY; + let mut waste = Amount::MAX_MONEY; - let mut selection: Vec = vec![]; + let mut selection_index: Vec = vec![]; let mut best_selection: Vec = vec![]; - let available_value: Amount = weighted_utxos.iter().map(|u| u.utxo.value).sum(); + let mut available_value: Amount = weighted_utxos.iter().map(|u| u.utxo.value).sum(); if available_value < target { return None; @@ -173,54 +175,178 @@ pub fn select_coins_bnb( weighted_utxos.sort_by(|a, b| b.utxo.value.cmp(&a.utxo.value)); while iteration < ITERATION_LIMIT { - //println!("iteration: {} index: {} value: {} selection: {:?} best_selection {:?}", iteration, index, value, selection, best_selection); - // backtrack + println!("{}", iteration); + // There are two conditions for backtracking: // - // There are three conditions for backtracking: + // 1_ Not enough value to make it to target. + // This condition happens before reaching a leaf node. + // Looking for a leaf node condition should not make a difference. + // This backtrack removes more than one node and instead starts + // the exploration of a new subtree. // - // * 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: + // From: + // o + // / \ + // 4 + // / \ + // 3 + // / \ + // 2 + // / + // 1 // - // For example: + // To: + // o + // / \ + // 4 + // / \ + // 3 + // + // * This condition sets both new_tree and backtrack to true + // + // 2_ value meets or exceeded target. + // In this condition, we only backtrack one node + // + // From: // o // / // 4 // / // 3 // - // Transform to: + // 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. + // * This condition sets backtrack to true + + // Set inital loop state + backtrack = false; + new_tree = false; + + // * not enouch value to make it to the target. + // Therefore, explore a new new subtree. + // + // - condition - + // + // o + // / \ + // 4 + // / \ + // 3 + // / \ + // 2 + // / + // 1 + // + // index = 3 + // target = 5 + // selection_index = [4, 1] + // value = 5 + // available_value = 0 + if available_value + value < target { + println!("condition to start a new tree match "); + new_tree = true; + } + + // * value meets or exceeds the target. + // Therefore backtrack one node to + // try another permutation. + // + // - condition - + // + // o + // / + // 4 + // / + // 3 + // + // Check if selection_index is better than the previous known best, and + // update best_selection accordingly. + // + // + // Before: + // index = 1 + // target = 5 + // available_value = [2, 1] = 3 + // selection_index = [4, 3] + // value = 7 + // best_selection = [] + // + // After: + // index = 1 + // target = 5 + // available_value = [2, 1] = 3 + // selection_index = [4, 3] + // value = 7 + // best_selection_index = [4, 3] if value >= target { + println!("condition to backtrack once met"); + backtrack = true; + let current_waste = value - target; if current_waste <= waste { - best_selection = selection.clone(); + best_selection = selection_index.clone(); + waste = current_waste; } + } - selection.pop(); - - if selection.is_empty() { + // * Backtrack + // + // - Transformation - + // + // From + // o + // / + // 4 + // / + // 3 + // + // To + // o + // / + // 4 + // / \ + // 3 + // + // since [4, 3] meet or exceed our target, we now backtrack + // and add 3 to the exclusion branch. + // + // Before: + // index = 1 + // target = 5 + // selection_index = [0, 1] + // value = 7 + // best_selection = [0, 1] + // available_value = [2, 1] = 3 + // + // After: + // index = 1 + // target = 5 + // selection_index = [0] + // value = 4 + // availale_value = [2, 1] = 3 + if backtrack { + // we are at a leaf + if index == weighted_utxos.len() { + println!("leaf node return"); return Some(best_selection); } - let utxo = &weighted_utxos[index]; - value -= utxo.utxo.value; + let last_index = selection_index.pop().unwrap(); + value -= weighted_utxos[last_index].utxo.value; index -= 1; + assert_eq!(index, last_index); } - // value is a leaf node, therefore, we remove the root - // and try the exclusion branch of that root node. + // * Backtrack to new tree + // Not enough value in the tree to continue checking, + // so start checking a new subtree by removing the root + // from the previous tree. + // // o // / \ // 4 @@ -235,33 +361,89 @@ pub fn select_coins_bnb( // 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 { - //} + // pool: [4, 3, 2, 1] + // index: [0, 1, 2, 3] + // + // Before: + // index = 3 + // target = 5 + // value = 5 + // selection_index = [0, 3] + // available_value = [] = 0 + // + // After: + // index = 1 + // target = 5 + // value = 0 + // selection_index = [] + // available_value = [3,2,1] = 6 + else if new_tree { + println!("* new_tree"); + + // we can't search a new tree if the size is one, therefore our search is done + if index == weighted_utxos.len() - 1 { + return Some(best_selection); + } + + // remove the current index value from the available value + // since we will be starting a new subtree of size available_value + //available_value -= weighted_utxos[index].utxo.value; + + // remove the head of the list and add 1 to start our search from + // the next subtree. + index = selection_index.remove(0) + 1; - // proceed with depth first search. + available_value = + Amount::from_sat(weighted_utxos[..index].iter().fold(0u64, |mut s, u| { + s += u.utxo.value.to_sat(); + s + })); + + selection_index = vec![]; + } + // * Add next node to the inclusion branch. + // + // target = 5 + // index = 1 + // + // Before: + // selection_index = [4] + // value = 4 + // available_value = [3,2,1] = 6 + // + // After: + // selection_index = [4, 3] + // value = 7 + // available_value = [2, 1] = 3 + // + // - Transformation - + // + // From + // o + // / + // 4 + // + // To + // o + // / + // 4 + // / + // 3 else { - let utxo = &weighted_utxos[index]; - selection.push(index); - value += utxo.utxo.value; + println!("try inclusion branch"); + let utxo_value = weighted_utxos[index].utxo.value; + + selection_index.push(index); + value += utxo_value; + available_value -= utxo_value; } index += 1; iteration += 1; } - println!("all solutions checked, returning best_selection: {:?}", best_selection); - Some(best_selection) + None } #[cfg(test)] @@ -277,47 +459,47 @@ mod tests { const FEE_RATE: FeeRate = FeeRate::from_sat_per_kwu(10); const SATISFACTION_SIZE: Weight = Weight::from_wu(204); - fn create_weighted_utxos() -> Vec { - let utxo_one = WeightedUtxo { - satisfaction_weight: SATISFACTION_SIZE, - utxo: TxOut { - value: Amount::from_str("1 cBTC").unwrap(), - script_pubkey: ScriptBuf::new(), - }, - }; - - let utxo_two = WeightedUtxo { - satisfaction_weight: SATISFACTION_SIZE, - utxo: TxOut { - value: Amount::from_str("2 cBTC").unwrap(), - script_pubkey: ScriptBuf::new(), - }, - }; - - 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] - } + //fn create_weighted_utxos() -> Vec { + //let utxo_one = WeightedUtxo { + //satisfaction_weight: SATISFACTION_SIZE, + //utxo: TxOut { + //value: Amount::from_str("1 cBTC").unwrap(), + //script_pubkey: ScriptBuf::new(), + //}, + //}; + + //let utxo_two = WeightedUtxo { + //satisfaction_weight: SATISFACTION_SIZE, + //utxo: TxOut { + //value: Amount::from_str("2 cBTC").unwrap(), + //script_pubkey: ScriptBuf::new(), + //}, + //}; + + //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 { + let weighted_utxos = vec![WeightedUtxo { satisfaction_weight: SATISFACTION_SIZE, utxo: TxOut { value: Amount::from_str("1 cBTC").unwrap(), @@ -325,16 +507,16 @@ mod tests { }, }]; - let index_list = select_coins_bnb(target, FEE_RATE, &mut weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); let expected_index_list = vec![0]; assert_eq!(index_list, expected_index_list); } #[test] - fn one_solution_two_coins() { + fn one_coin_solution_of_two_coins() { let target = Amount::from_str("4 cBTC").unwrap(); - let mut weighted_utxos = vec![ + let weighted_utxos = vec![ WeightedUtxo { satisfaction_weight: SATISFACTION_SIZE, utxo: TxOut { @@ -351,11 +533,37 @@ mod tests { }, ]; - let index_list = select_coins_bnb(target, FEE_RATE, &mut weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); let expected_index_list = vec![0]; assert_eq!(index_list, expected_index_list); } + #[test] + fn two_coin_solution_of_two_coins() { + let target = Amount::from_str("7 cBTC").unwrap(); + + let 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 index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let expected_index_list = vec![0, 1]; + assert_eq!(index_list, expected_index_list); + } + //#[test] //fn find_solution_2_btc() { //let utxo_match = find_solution(TWO_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); @@ -425,15 +633,15 @@ mod tests { //assert_eq!(expected_bool_vec, utxo_match); //} - #[test] - fn select_coins_bnb_not_enough_value() { - let target: Amount = Amount::from_str("11 cBTC").unwrap(); - let mut weighted_utxos: Vec = create_weighted_utxos(); + //#[test] + //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); + //let result = select_coins_bnb(target, FEE_RATE, &mut weighted_utxos); - assert!(result.is_none()); - } + //assert!(result.is_none()); + //} //#[test] //fn find_solution_with_large_cost_of_change() {