diff --git a/src/branch_and_bound.rs b/src/branch_and_bound.rs index 5fa164a8..07516d76 100644 --- a/src/branch_and_bound.rs +++ b/src/branch_and_bound.rs @@ -6,6 +6,7 @@ use crate::WeightedUtxo; use bitcoin::Amount; +use bitcoin::SignedAmount; use bitcoin::FeeRate; /// Select coins bnb performs a depth first branch and bound search on binary tree. @@ -108,6 +109,7 @@ pub fn select_coins_bnb( target: Amount, cost_of_change: Amount, fee_rate: FeeRate, + long_term_fee_rate: FeeRate, weighted_utxos: &mut [WeightedUtxo], ) -> Option> { // Total_Tries in Core: @@ -120,7 +122,8 @@ pub fn select_coins_bnb( let mut backtrack_subtree; let mut value = Amount::ZERO; - let mut waste = Amount::MAX_MONEY; + let mut current_waste: SignedAmount = SignedAmount::ZERO; + let mut best_waste = SignedAmount::MAX_MONEY; let mut index_selection: Vec = vec![]; let mut best_selection: Option> = None; @@ -128,7 +131,7 @@ pub fn select_coins_bnb( // Create a tuple of (effective_value, weighted_utxo) and filter out negative values // and errors (effective_values that are None). The effective_value will be used // during the coin-selection process, and the weighted_utxo is index into on return. - let mut effective_values: Vec<(Amount, &WeightedUtxo)> = weighted_utxos + let mut w_utxos: Vec<(Amount, &WeightedUtxo)> = weighted_utxos .iter() .map(|wu| (wu.effective_value(fee_rate), wu)) .filter(|(eff_val, _)| !eff_val.is_none()) @@ -137,11 +140,11 @@ pub fn select_coins_bnb( .map(|(eff_val, wu)| (eff_val.to_unsigned().unwrap(), wu)) .collect(); - effective_values.sort_by_key(|u| u.0); - effective_values.reverse(); + w_utxos.sort_by_key(|u| u.0); + w_utxos.reverse(); // TODO check for overflow - let mut available_value = effective_values.iter().fold(Amount::ZERO, |mut s, &(v, _)| { + let mut available_value = w_utxos.iter().fold(Amount::ZERO, |mut s, &(v, _)| { s += v; s }); @@ -151,6 +154,10 @@ pub fn select_coins_bnb( } while iteration < ITERATION_LIMIT { + println!("---"); + println!("value: {}", value); + println!("waste: {}", current_waste); + println!("best waste: {}", best_waste); // There are two conditions for backtracking: // // 1_ Not enough value to make it to target. @@ -215,7 +222,7 @@ pub fn select_coins_bnb( // // 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 { + else if value > target + cost_of_change || current_waste > best_waste && fee_rate > long_term_fee_rate { backtrack = true; } // * value meets or exceeds the target. @@ -226,18 +233,25 @@ pub fn select_coins_bnb( else if value >= target { backtrack = true; - let current_waste = value - target; + let v = value.to_signed().ok()?; + let t = target.to_signed().ok()?; + let waste: SignedAmount = v.checked_sub(t)?; + current_waste += waste; - if current_waste <= waste { + if current_waste <= best_waste { best_selection = Some(index_selection.clone()); - waste = current_waste; + best_waste = current_waste; } + + current_waste -= waste; } // * Backtrack if backtrack { let last_index = index_selection.pop().unwrap(); - let (eff_value, _) = effective_values[last_index]; + let (eff_value, w_utxo) = w_utxos[last_index]; + + current_waste -= w_utxo.waste(fee_rate, long_term_fee_rate)?; value -= eff_value; index -= 1; assert_eq!(index, last_index); @@ -246,7 +260,7 @@ pub fn select_coins_bnb( else if backtrack_subtree { // No new subtree left to explore. if index_selection.is_empty() { - return index_to_utxo_list(best_selection, effective_values); + return index_to_utxo_list(best_selection, w_utxos); } // Anchor the new subtree at the next available index. @@ -256,14 +270,25 @@ pub fn select_coins_bnb( // The available value of the next iteration. This should never overflow // since the value is always less than the last available_value calculation. available_value = - effective_values[index + 1..].iter().fold(Amount::ZERO, |mut s, &(v, _)| { + w_utxos[index + 1..].iter().fold(Amount::ZERO, |mut s, &(v, _)| { s += v; s }); + // Unwind the waste. + //let excess_waste = w_utxos[index + 1..].iter().fold(SignedAmount::ZERO, |mut s, &(_, w_utxo)| { + //s += w_utxo.waste(fee_rate, long_term_fee_rate).unwrap(); + //s + //}); + + //println!("current_waste: {}", current_waste); + //println!("excess_waste: {}", excess_waste); + //current_waste -= excess_waste; + current_waste = SignedAmount::ZERO; + // If the new subtree does not have enough value, we are done searching. if available_value < target { - return index_to_utxo_list(best_selection, effective_values); + return index_to_utxo_list(best_selection, w_utxos); } // Start a new selection and add the root of the new subtree to the index selection. @@ -272,18 +297,27 @@ pub fn select_coins_bnb( } // * Add next node to the inclusion branch. else { - let (eff_value, _) = effective_values[index]; + let (eff_value, w_utxo) = w_utxos[index]; + current_waste += w_utxo.waste(fee_rate, long_term_fee_rate)?; index_selection.push(index); value += eff_value; + available_value -= eff_value; + + //println!("- adding to inclusion branch -"); + //println!("eff_value: {}", eff_value); + //println!("utxo waste: {}", w_utxo.waste(fee_rate, long_term_fee_rate)?); + //println!("current_waste: {}", current_waste); + //println!("available_value: {}", available_value); + //println!("---"); } index += 1; iteration += 1; } - index_to_utxo_list(best_selection, effective_values) + index_to_utxo_list(best_selection, w_utxos) } fn index_to_utxo_list( @@ -304,17 +338,19 @@ mod tests { use core::str::FromStr; fn create_weighted_utxos() -> Vec { + let fee = Amount::from_str("5 sats").unwrap(); let amts = [ - Amount::from_str("1 cBTC").unwrap(), - Amount::from_str("2 cBTC").unwrap(), - Amount::from_str("3 cBTC").unwrap(), - Amount::from_str("4 cBTC").unwrap(), + Amount::from_str("1 cBTC").unwrap() + fee, + Amount::from_str("2 cBTC").unwrap() + fee, + Amount::from_str("3 cBTC").unwrap() + fee, + Amount::from_str("4 cBTC").unwrap() + fee, ]; amts.iter() .map(|amt| WeightedUtxo { satisfaction_weight: Weight::ZERO, - utxo: TxOut { value: *amt, script_pubkey: ScriptBuf::new() }, + utxo: TxOut { value: *amt, + script_pubkey: ScriptBuf::new() }, }) .collect() } @@ -325,7 +361,7 @@ mod tests { let mut weighted_utxos = create_weighted_utxos(); let list = - select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); + select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); assert_eq!(list.len(), 1); assert_eq!(list[0].utxo.value, Amount::from_str("1 cBTC").unwrap()); } @@ -336,7 +372,7 @@ mod tests { let mut weighted_utxos = create_weighted_utxos(); let list = - select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); + select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); assert_eq!(list.len(), 1); assert_eq!(list[0].utxo.value, Amount::from_str("2 cBTC").unwrap()); } @@ -347,7 +383,7 @@ mod tests { let mut weighted_utxos = create_weighted_utxos(); let list = - select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); + select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); assert_eq!(list.len(), 2); assert_eq!(list[0].utxo.value, Amount::from_str("2 cBTC").unwrap()); assert_eq!(list[1].utxo.value, Amount::from_str("1 cBTC").unwrap()); @@ -359,7 +395,7 @@ mod tests { let mut weighted_utxos = create_weighted_utxos(); let list = - select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); + select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); assert_eq!(list.len(), 2); assert_eq!(list[0].utxo.value, Amount::from_str("3 cBTC").unwrap()); assert_eq!(list[1].utxo.value, Amount::from_str("1 cBTC").unwrap()); @@ -371,7 +407,7 @@ mod tests { let mut weighted_utxos = create_weighted_utxos(); let list = - select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); + select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); assert_eq!(list.len(), 2); assert_eq!(list[0].utxo.value, Amount::from_str("3 cBTC").unwrap()); assert_eq!(list[1].utxo.value, Amount::from_str("2 cBTC").unwrap()); @@ -383,7 +419,7 @@ mod tests { let mut weighted_utxos = create_weighted_utxos(); let list = - select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); + select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); assert_eq!(list.len(), 3); assert_eq!(list[0].utxo.value, Amount::from_str("3 cBTC").unwrap()); assert_eq!(list[1].utxo.value, Amount::from_str("2 cBTC").unwrap()); @@ -396,7 +432,7 @@ mod tests { let mut weighted_utxos = create_weighted_utxos(); let list = - select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); + select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); assert_eq!(list.len(), 3); assert_eq!(list[0].utxo.value, Amount::from_str("4 cBTC").unwrap()); assert_eq!(list[1].utxo.value, Amount::from_str("2 cBTC").unwrap()); @@ -409,7 +445,7 @@ mod tests { let mut weighted_utxos = create_weighted_utxos(); let list = - select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); + select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); assert_eq!(list.len(), 3); assert_eq!(list[0].utxo.value, Amount::from_str("4 cBTC").unwrap()); assert_eq!(list[1].utxo.value, Amount::from_str("3 cBTC").unwrap()); @@ -422,7 +458,7 @@ mod tests { let mut weighted_utxos = create_weighted_utxos(); let list = - select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); + select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); assert_eq!(list.len(), 3); assert_eq!(list[0].utxo.value, Amount::from_str("4 cBTC").unwrap()); assert_eq!(list[1].utxo.value, Amount::from_str("3 cBTC").unwrap()); @@ -435,7 +471,7 @@ mod tests { let mut weighted_utxos = create_weighted_utxos(); let list = - select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); + select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); assert_eq!(list.len(), 4); assert_eq!(list[0].utxo.value, Amount::from_str("4 cBTC").unwrap()); assert_eq!(list[1].utxo.value, Amount::from_str("3 cBTC").unwrap()); @@ -461,12 +497,12 @@ mod tests { let mut wu = weighted_utxos.clone(); - let list = select_coins_bnb(target, cost_of_change, FeeRate::ZERO, &mut wu).unwrap(); + let list = select_coins_bnb(target, cost_of_change, FeeRate::ZERO, FeeRate::ZERO, &mut wu).unwrap(); assert_eq!(list.len(), 1); assert_eq!(list[0].utxo.value, Amount::from_str("1.5 cBTC").unwrap()); - let index_list = select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, &mut wu); + let index_list = select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, FeeRate::ZERO, &mut wu); assert_eq!(index_list, None); } @@ -487,7 +523,7 @@ mod tests { }]; let mut wu = weighted_utxos.clone(); - let index_list = select_coins_bnb(target, Amount::ZERO, fee_rate, &mut wu).unwrap(); + let index_list = select_coins_bnb(target, Amount::ZERO, fee_rate, fee_rate, &mut wu).unwrap(); assert!(index_list.is_empty()); } @@ -523,7 +559,7 @@ mod tests { ]; let mut wu = weighted_utxos.clone(); - let list = select_coins_bnb(target, cost_of_change, fee_rate, &mut wu).unwrap(); + let list = select_coins_bnb(target, cost_of_change, fee_rate, fee_rate, &mut wu).unwrap(); assert_eq!(list.len(), 1); assert_eq!(list[0].utxo.value, Amount::from_str("1.5 cBTC").unwrap()); } @@ -533,10 +569,36 @@ mod tests { let target = Amount::from_str("11 cBTC").unwrap(); let mut weighted_utxos = create_weighted_utxos(); let list = - select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); + select_coins_bnb(target, Amount::ZERO, FeeRate::ZERO, FeeRate::ZERO, &mut weighted_utxos).unwrap(); assert!(list.is_empty()); } + #[test] + fn consume_more_inputs_when_cheap() { + let target = Amount::from_str("6 cBTC").unwrap(); + let fee = Amount::from_str("5 sats").unwrap(); + let mut weighted_utxos = create_weighted_utxos(); + + let fee_rate = FeeRate::from_sat_per_kwu(20); + let mut lt_fee_rate = fee_rate; + + // the possible combinations are 2,4 or 1,2,3 + let list = + select_coins_bnb(target, Amount::ZERO, fee_rate, lt_fee_rate, &mut weighted_utxos).unwrap(); + assert_eq!(list.len(), 3); + assert_eq!(list[0].utxo.value, Amount::from_str("3 cBTC").unwrap() + fee); + assert_eq!(list[1].utxo.value, Amount::from_str("2 cBTC").unwrap() + fee); + assert_eq!(list[2].utxo.value, Amount::from_str("1 cBTC").unwrap() + fee); + + lt_fee_rate = FeeRate::from_sat_per_kwu(10); + + let list = + select_coins_bnb(target, Amount::ZERO, fee_rate, lt_fee_rate, &mut weighted_utxos).unwrap(); + assert_eq!(list.len(), 2); + assert_eq!(list[0].utxo.value, Amount::from_str("4 cBTC").unwrap() + fee); + assert_eq!(list[1].utxo.value, Amount::from_str("2 cBTC").unwrap() + fee); + } + // TODO - rework this test. //#[test] //fn utxo_pool_sum_overflow() { diff --git a/src/lib.rs b/src/lib.rs index e3dac71b..46a326b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,8 @@ pub trait Utxo: Clone { // https://github.com/bitcoin/bitcoin/blob/f722a9bd132222d9d5cd503b5af25c905b205cdb/src/wallet/coinselection.h#L20 const CHANGE_LOWER: Amount = Amount::from_sat(50_000); +const TX_IN_BASE_WEIGHT: Weight = Weight::from_wu(204); + /// This struct contains the weight of all params needed to satisfy the UTXO. /// /// The idea of using a WeightUtxo type was inspired by the BDK implementation: @@ -60,13 +62,16 @@ impl WeightedUtxo { } fn calculate_fee(&self, fee_rate: FeeRate) -> Option { - // TODO TxIn::BASE_WEIGHT - let weight = self.satisfaction_weight.checked_add(Weight::ZERO)?; + let weight = self.satisfaction_weight.checked_add(TX_IN_BASE_WEIGHT)?; + //println!("fee: {}",fee_rate.checked_mul_by_weight(weight).unwrap()); fee_rate.checked_mul_by_weight(weight) } - fn is_wasteful(&self, fee_rate: FeeRate, long_term_fee_rate: FeeRate) -> bool { - self.calculate_fee(fee_rate) > self.calculate_fee(long_term_fee_rate) + fn waste(&self, fee_rate: FeeRate, long_term_fee_rate: FeeRate) -> Option { + let fee: SignedAmount = self.calculate_fee(fee_rate)?.to_signed().ok()?; + let lt_fee: SignedAmount = self.calculate_fee(long_term_fee_rate)?.to_signed().ok()?; + //println!("waste calc - fee_rate: {} lt_fee_rate: {} fee {} let_fee {}", fee_rate, long_term_fee_rate, fee, lt_fee); + fee.checked_sub(lt_fee) } } @@ -81,9 +86,10 @@ pub fn select_coins( target: Amount, cost_of_change: Amount, fee_rate: FeeRate, + long_term_fee_rate: FeeRate, weighted_utxos: &mut [WeightedUtxo], ) -> Option> { - if let Some(coins) = select_coins_bnb(target, cost_of_change, fee_rate, weighted_utxos) { + if let Some(coins) = select_coins_bnb(target, cost_of_change, fee_rate, long_term_fee_rate, weighted_utxos) { Some(coins.into_iter().cloned().collect()) } else { select_coins_srd(target, fee_rate, weighted_utxos, &mut thread_rng())