From 75b6d0fda878ce200e9269b84cb6b58536c26607 Mon Sep 17 00:00:00 2001 From: yancy Date: Wed, 6 Dec 2023 21:27:11 +0100 Subject: [PATCH] wip --- src/lib.rs | 448 +++++++++++++++++++++++++++++------------------------ 1 file changed, 244 insertions(+), 204 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ee14c93..e426e19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,8 +16,6 @@ #[cfg(bench)] extern crate test; -use std::cmp::Reverse; - mod single_random_draw; use bitcoin::Amount; @@ -59,12 +57,11 @@ pub struct WeightedUtxo { #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] pub fn select_coins( target: Amount, - cost_of_change: u64, + cost_of_change: FeeRate, fee_rate: FeeRate, weighted_utxos: &mut [WeightedUtxo], - utxo_pool: &mut [T], ) -> Option> { - match select_coins_bnb(target.to_sat(), cost_of_change, utxo_pool) { + match select_coins_bnb(target, cost_of_change, weighted_utxos) { Some(_res) => Some(Vec::new()), None => select_coins_srd(target, fee_rate, weighted_utxos, &mut thread_rng()), } @@ -73,219 +70,262 @@ pub fn select_coins( /// Select coins using BnB algorithm similar to what is done in bitcoin /// core see: /// Returns None if BnB doesn't find a solution. -pub fn select_coins_bnb( - target: u64, - cost_of_change: u64, - utxo_pool: &mut [T], -) -> Option> { - let solution = find_solution(target, cost_of_change, utxo_pool)?; - Some( - solution - .into_iter() - .zip(utxo_pool.iter()) - .filter_map(|(include, utxo)| if include { Some(utxo.clone()) } else { None }) - .collect::>(), - ) -} - -fn find_solution( - target: u64, - cost_of_change: u64, - utxo_pool: &mut [T], -) -> Option> { - let utxo_sum = utxo_pool.iter().fold(0u64, |mut s, u| { - s += u.get_value(); - s - }); - - let utxo_pool_length = utxo_pool.len(); - utxo_pool.sort_by_key(|u| Reverse(u.get_value())); - - let mut curr_selection: Vec = vec![false; utxo_pool_length]; - let mut best_selection = None; - let mut remainder = utxo_sum; - - let lower_bound = target; - let upper_bound = cost_of_change + lower_bound; - - if utxo_sum < lower_bound { - return None; - } - - for m in 0..utxo_pool_length { - let mut curr_sum = 0; - let mut slice_remainder = remainder; - - for n in m..utxo_pool_length { - if slice_remainder + curr_sum < lower_bound { - break; - } - - let utxo_value = utxo_pool[n].get_value(); - curr_sum += utxo_value; - curr_selection[n] = true; - - if curr_sum >= lower_bound { - if curr_sum <= upper_bound { - best_selection = Some(curr_selection.clone()); - } - - curr_selection[n] = false; - curr_sum -= utxo_value; - } - - slice_remainder -= utxo_value; +// select_coins_bnb performs a depth first search on a binary tree. This can be thought of as +// exploring a binary tree where the left branch is the inclusion of a node and the right branch is +// the exclusion. For example, if the utxo set consist of a list of utxos: [4,3,2,1], and the +// target is 5, the selection process works as follows: +// +// Start at 4 and try including 4 in the total the first loop. We therefore have a tree with only +// one root node that is less than the total, so the next iteration occurs. The second iteration +// examines a tree where 4 is the root and the left branch is 3. +// o +// / +// 4 +// / +// 3 +// +// At this point, the total is determined to be 7 which exceeds the target of 5. We therefore +// remove 3 from the left branch and it becomes the left branch since 3 is now excluded +// (backtrack). +// o +// / +// 4 +// / \ +// 3 +// +// We next try including 2 on the left branch of 3 (add 2 to the inclusion branch). +// o +// / +// 4 +// / \ +// 3 +// / +// 2 +// +// The sum is now 6, since the sum of the right branch totals 6. Once again, we find the total +// exceeds 5, so we explore the exclusion branch of 2. +// o +// / +// 4 +// / \ +// 3 +// / \ +// 2 +// +// Finally, we add 1 to the inclusion branch. This ends our depth first search by matching two +// conditions, it is both the leaf node (end of the list) and matches our search criteria of +// matching 5. Both 4 and 1 are on the left inclusion branch. We therefore record our solution +// and backtrack to next try the exclusion branch of our rood node 4. +// o +// / \ +// 4 +// / \ +// 3 +// / \ +// 2 +// / +// 1 +// +// We try excluding 4 now +// o +// / \ +// 4 +// / \ +// 3 +// +// 3 is less than our target, so we next add 2 to our inclusion branch +// o +// / \ +// 4 +// / \ +// 3 +// / +// 2 +// +// We now stop our search again noticing that 3 and 2 equals our target as 5, and since this +// solution was found last, then [3, 2] overwrites the previously found solution [4,1]. We next +// backtrack and exclude our root node of this sub tree 3. Since our new sub tree starting at 2 +// 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, + weighted_utxos: &mut [WeightedUtxo], +) -> Option> { + let mut value = Amount::ZERO; + let mut selection: Vec = vec![]; + + for (_i, utxo) in weighted_utxos.iter().enumerate() { + // backtrack + // + // There are three conditions for backtracking: + // + // * value exceeded target + // * at a leaf node (nothing left to explorer) + // * not enough value to make it to target + if value >= target { + } + // proceed with depth first search. + else { + selection.push(utxo.clone()); + value += utxo.utxo.value; } - - remainder -= utxo_pool[m].get_value(); - curr_selection[m] = false; } - best_selection + Some(selection) } #[cfg(test)] mod tests { - use crate::*; - - const ONE_BTC: u64 = 100000000; - const TWO_BTC: u64 = 2 * 100000000; - const THREE_BTC: u64 = 3 * 100000000; - const FOUR_BTC: u64 = 4 * 100000000; - - const UTXO_POOL: [MinimalUtxo; 4] = [ - MinimalUtxo { value: ONE_BTC }, - MinimalUtxo { value: TWO_BTC }, - MinimalUtxo { value: THREE_BTC }, - MinimalUtxo { value: FOUR_BTC }, - ]; - - const COST_OF_CHANGE: u64 = 50000000; - - #[derive(Clone, Debug, Eq, PartialEq)] - struct MinimalUtxo { - value: u64, - } - - impl Utxo for MinimalUtxo { - fn get_value(&self) -> u64 { - self.value - } + use super::*; + use crate::single_random_draw::select_coins_srd; + use crate::WeightedUtxo; + use crate::CHANGE_LOWER; + use bitcoin::Amount; + use bitcoin::ScriptBuf; + use bitcoin::TxOut; + use bitcoin::Weight; + use core::str::FromStr; + use rand::rngs::mock::StepRng; + + 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(), + }, + }; + + vec![utxo_one, utxo_two] } #[test] fn find_solution_1_btc() { - let utxo_match = find_solution(ONE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, false, false, true]; - assert_eq!(expected_bool_vec, utxo_match); - } + let target: Amount = Amount::from_str("1.5 cBTC").unwrap(); + let mut weighted_utxos: Vec = create_weighted_utxos(); - #[test] - fn find_solution_2_btc() { - let utxo_match = find_solution(TWO_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, false, true, false]; - assert_eq!(expected_bool_vec, utxo_match); + let utxo_match = select_coins_bnb(target, FeeRate::ZERO, &mut weighted_utxos); + //let expected_bool_vec = vec![false, false, false, true]; + //assert_eq!(expected_bool_vec, utxo_match); } - #[test] - fn find_solution_3_btc() { - let utxo_match = find_solution(THREE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, false, true, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_4_btc() { - let utxo_match = find_solution(FOUR_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, true, false, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_5_btc() { - let five_btc = FOUR_BTC + ONE_BTC; - let utxo_match = find_solution(five_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, true, true, false]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_6_btc() { - let six_btc = FOUR_BTC + TWO_BTC; - let utxo_match = find_solution(six_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, true, true, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_7_btc() { - let seven_btc = FOUR_BTC + THREE_BTC; - let utxo_match = find_solution(seven_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![true, false, true, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_8_btc() { - let seven_btc = FOUR_BTC + THREE_BTC + ONE_BTC; - let utxo_match = find_solution(seven_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![true, true, false, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_9_btc() { - let seven_btc = FOUR_BTC + THREE_BTC + TWO_BTC; - let utxo_match = find_solution(seven_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![true, true, true, false]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_10_btc() { - let ten_btc = ONE_BTC + TWO_BTC + THREE_BTC + FOUR_BTC; - let utxo_match = find_solution(ten_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![true, true, true, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_11_btc_not_possible() { - let ten_btc = ONE_BTC + TWO_BTC + THREE_BTC + FOUR_BTC; - let utxo_match = find_solution(ten_btc + ONE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()); - assert_eq!(None, utxo_match); - } - - #[test] - fn find_solution_with_large_cost_of_change() { - let utxo_match = - find_solution(ONE_BTC * 9 / 10, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - let expected_bool_vec = vec![false, false, false, true]; - assert_eq!(expected_bool_vec, utxo_match); - } - - #[test] - fn find_solution_with_no_cost_of_change() { - let utxo_match = find_solution(ONE_BTC * 9 / 10, 0, &mut UTXO_POOL.clone()); - assert_eq!(None, utxo_match); - } - - #[test] - fn find_solution_with_not_input_fee() { - let utxo_match = find_solution(ONE_BTC + 1, COST_OF_CHANGE, &mut UTXO_POOL.clone()); - assert_eq!(None, utxo_match); - } - - #[test] - fn select_coins_bnb_with_match() { - select_coins_bnb(ONE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); - } - - #[test] - fn select_coins_bnb_with_no_match() { - let utxo_match = select_coins_bnb(1, COST_OF_CHANGE, &mut UTXO_POOL.clone()); - assert_eq!(None, utxo_match); - } + //#[test] + //fn find_solution_2_btc() { + //let utxo_match = find_solution(TWO_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + //let expected_bool_vec = vec![false, false, true, false]; + //assert_eq!(expected_bool_vec, utxo_match); + //} + + //#[test] + //fn find_solution_3_btc() { + //let utxo_match = find_solution(THREE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + //let expected_bool_vec = vec![false, false, true, true]; + //assert_eq!(expected_bool_vec, utxo_match); + //} + + //#[test] + //fn find_solution_4_btc() { + //let utxo_match = find_solution(FOUR_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + //let expected_bool_vec = vec![false, true, false, true]; + //assert_eq!(expected_bool_vec, utxo_match); + //} + + //#[test] + //fn find_solution_5_btc() { + //let five_btc = FOUR_BTC + ONE_BTC; + //let utxo_match = find_solution(five_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + //let expected_bool_vec = vec![false, true, true, false]; + //assert_eq!(expected_bool_vec, utxo_match); + //} + + //#[test] + //fn find_solution_6_btc() { + //let six_btc = FOUR_BTC + TWO_BTC; + //let utxo_match = find_solution(six_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + //let expected_bool_vec = vec![false, true, true, true]; + //assert_eq!(expected_bool_vec, utxo_match); + //} + + //#[test] + //fn find_solution_7_btc() { + //let seven_btc = FOUR_BTC + THREE_BTC; + //let utxo_match = find_solution(seven_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + //let expected_bool_vec = vec![true, false, true, true]; + //assert_eq!(expected_bool_vec, utxo_match); + //} + + //#[test] + //fn find_solution_8_btc() { + //let seven_btc = FOUR_BTC + THREE_BTC + ONE_BTC; + //let utxo_match = find_solution(seven_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + //let expected_bool_vec = vec![true, true, false, true]; + //assert_eq!(expected_bool_vec, utxo_match); + //} + + //#[test] + //fn find_solution_9_btc() { + //let seven_btc = FOUR_BTC + THREE_BTC + TWO_BTC; + //let utxo_match = find_solution(seven_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + //let expected_bool_vec = vec![true, true, true, false]; + //assert_eq!(expected_bool_vec, utxo_match); + //} + + //#[test] + //fn find_solution_10_btc() { + //let ten_btc = ONE_BTC + TWO_BTC + THREE_BTC + FOUR_BTC; + //let utxo_match = find_solution(ten_btc, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + //let expected_bool_vec = vec![true, true, true, true]; + //assert_eq!(expected_bool_vec, utxo_match); + //} + + //#[test] + //fn find_solution_11_btc_not_possible() { + //let ten_btc = ONE_BTC + TWO_BTC + THREE_BTC + FOUR_BTC; + //let utxo_match = find_solution(ten_btc + ONE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()); + //assert_eq!(None, utxo_match); + //} + + //#[test] + //fn find_solution_with_large_cost_of_change() { + //let utxo_match = + //find_solution(ONE_BTC * 9 / 10, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + //let expected_bool_vec = vec![false, false, false, true]; + //assert_eq!(expected_bool_vec, utxo_match); + //} + + //#[test] + //fn find_solution_with_no_cost_of_change() { + //let utxo_match = find_solution(ONE_BTC * 9 / 10, 0, &mut UTXO_POOL.clone()); + //assert_eq!(None, utxo_match); + //} + + //#[test] + //fn find_solution_with_not_input_fee() { + //let utxo_match = find_solution(ONE_BTC + 1, COST_OF_CHANGE, &mut UTXO_POOL.clone()); + //assert_eq!(None, utxo_match); + //} + + //#[test] + //fn select_coins_bnb_with_match() { + //select_coins_bnb(ONE_BTC, COST_OF_CHANGE, &mut UTXO_POOL.clone()).unwrap(); + //} + + //#[test] + //fn select_coins_bnb_with_no_match() { + //let utxo_match = select_coins_bnb(1, COST_OF_CHANGE, &mut UTXO_POOL.clone()); + //assert_eq!(None, utxo_match); + //} } #[cfg(bench)]