diff --git a/src/lib.rs b/src/lib.rs index 6931ad4..2996e2d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,7 @@ pub struct WeightedUtxo { #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] pub fn select_coins( target: Amount, - cost_of_change: FeeRate, + cost_of_change: Amount, fee_rate: FeeRate, weighted_utxos: &mut [WeightedUtxo], ) -> Option> { @@ -149,7 +149,7 @@ pub fn select_coins( // doesn't have enough value left to meet the target, we conclude our search at [3, 2]. pub fn select_coins_bnb( target: Amount, - _fee_rate: FeeRate, + cost_of_change: Amount, mut weighted_utxos: Vec, ) -> Option> { // Total_Tries in Core: @@ -225,32 +225,27 @@ pub fn select_coins_bnb( // * not enough value to make it to the target. // Therefore, explore a new new subtree. - // - // - condition - - // - // o - // / \ - // 4 - // / \ - // 3 - // / \ - // 2 - // / - // 1 if available_value + value < target { backtrack_subtree = true; } - // * value meets or exceeds the target. - // Therefore backtrack one node to - // try another permutation. + // * value exceeds target, however it the solution is too wasteful. // - // - condition - + // This optimization provides an upper bound on the amount of waste that is acceptable. + // Since value is lost when we create a change output due to increasing the size of the + // transaction by an output, we accept solutions that may be larger than the target as + // if they are exactly equal to the target and consider the overage waste or a throw + // away amount. However we do not consider values greater than value + cost_of_change. // - // o - // / - // 4 - // / - // 3 + // This effectively creates a range of possible solution where; + // range = (target, target + cost_of_change] + // + // That is, the range includes solutions that exactly equal the target up to but not + // including values greater than target + cost_of_change. + else if value > target + cost_of_change { + backtrack = true; + } + // * value meets or exceeds the target. + // Record the solution and the waste then continue. // // Check if index_selection is better than the previous known best, and // update best_selection accordingly. @@ -266,25 +261,6 @@ pub fn select_coins_bnb( } // * 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. if backtrack { let last_index = index_selection.pop().unwrap(); value -= weighted_utxos[last_index].utxo.value; @@ -292,24 +268,6 @@ pub fn select_coins_bnb( assert_eq!(index, last_index); } // * 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 - // / \ - // 3 - // / \ - // 2 - // / - // 1 - // - // We try excluding 4 now - // o - // / \ - // 4 else if backtrack_subtree { // No new subtree left to explore. if index_selection.is_empty() { @@ -337,20 +295,6 @@ pub fn select_coins_bnb( value = Amount::ZERO; } // * Add next node to the inclusion branch. - // - // - Transformation - - // - // From - // o - // / - // 4 - // - // To - // o - // / - // 4 - // / - // 3 else { let utxo_value = weighted_utxos[index].utxo.value; @@ -376,7 +320,6 @@ mod tests { use bitcoin::Weight; use core::str::FromStr; - const FEE_RATE: FeeRate = FeeRate::from_sat_per_kwu(10); const SATISFACTION_SIZE: Weight = Weight::from_wu(204); fn create_weighted_utxos() -> Vec { @@ -427,7 +370,7 @@ mod tests { }, }]; - let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); let expected_index_list = vec![0]; assert_eq!(index_list, expected_index_list); } @@ -453,7 +396,7 @@ mod tests { }, ]; - let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); let expected_index_list = vec![0]; assert_eq!(index_list, expected_index_list); } @@ -479,7 +422,7 @@ mod tests { }, ]; - let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); let expected_index_list = vec![0, 1]; assert_eq!(index_list, expected_index_list); } @@ -488,7 +431,7 @@ mod tests { fn one() { let target = Amount::from_str("1 cBTC").unwrap(); let weighted_utxos = create_weighted_utxos(); - let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); let expected_index_list = vec![3]; assert_eq!(index_list, expected_index_list); } @@ -497,7 +440,7 @@ mod tests { fn two() { let target = Amount::from_str("2 cBTC").unwrap(); let weighted_utxos = create_weighted_utxos(); - let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); let expected_index_list = vec![2]; assert_eq!(index_list, expected_index_list); } @@ -506,7 +449,7 @@ mod tests { fn three() { let target = Amount::from_str("3 cBTC").unwrap(); let weighted_utxos = create_weighted_utxos(); - let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); let expected_index_list = vec![2, 3]; assert_eq!(index_list, expected_index_list); } @@ -515,7 +458,7 @@ mod tests { fn four() { let target = Amount::from_str("4 cBTC").unwrap(); let weighted_utxos = create_weighted_utxos(); - let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); let expected_index_list = vec![1, 3]; assert_eq!(index_list, expected_index_list); } @@ -524,7 +467,7 @@ mod tests { fn five() { let target = Amount::from_str("5 cBTC").unwrap(); let weighted_utxos = create_weighted_utxos(); - let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); let expected_index_list = vec![1, 2]; assert_eq!(index_list, expected_index_list); } @@ -533,7 +476,7 @@ mod tests { fn six() { let target = Amount::from_str("6 cBTC").unwrap(); let weighted_utxos = create_weighted_utxos(); - let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); let expected_index_list = vec![1, 2, 3]; assert_eq!(index_list, expected_index_list); } @@ -542,7 +485,7 @@ mod tests { fn seven() { let target = Amount::from_str("7 cBTC").unwrap(); let weighted_utxos = create_weighted_utxos(); - let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); let expected_index_list = vec![0, 2, 3]; assert_eq!(index_list, expected_index_list); } @@ -551,7 +494,7 @@ mod tests { fn eight() { let target = Amount::from_str("8 cBTC").unwrap(); let weighted_utxos = create_weighted_utxos(); - let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); let expected_index_list = vec![0, 1, 3]; assert_eq!(index_list, expected_index_list); } @@ -560,7 +503,7 @@ mod tests { fn nine() { let target = Amount::from_str("9 cBTC").unwrap(); let weighted_utxos = create_weighted_utxos(); - let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); let expected_index_list = vec![0, 1, 2]; assert_eq!(index_list, expected_index_list); } @@ -569,10 +512,34 @@ mod tests { fn ten() { let target = Amount::from_str("10 cBTC").unwrap(); let weighted_utxos = create_weighted_utxos(); - let index_list = select_coins_bnb(target, FEE_RATE, weighted_utxos).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); let expected_index_list = vec![0, 1, 2, 3]; assert_eq!(index_list, expected_index_list); } + + #[test] + fn cost_of_change() { + let target = Amount::from_str("1 cBTC").unwrap(); + + // Since cost of change here is one, we accept any solution + // between 1 and 2. Range = (1, 2] + let cost_of_change = target; + + let weighted_utxos = vec![WeightedUtxo { + satisfaction_weight: SATISFACTION_SIZE, + utxo: TxOut { + value: Amount::from_str("1.5 cBTC").unwrap(), + script_pubkey: ScriptBuf::new(), + }, + }]; + + let index_list = select_coins_bnb(target, cost_of_change, weighted_utxos.clone()).unwrap(); + let expected_index_list = vec![0]; + assert_eq!(index_list, expected_index_list); + + let index_list = select_coins_bnb(target, Amount::ZERO, weighted_utxos).unwrap(); + assert_eq!(index_list, vec![]); + } } #[cfg(bench)]