Skip to content

Commit

Permalink
wip: add waste minimization
Browse files Browse the repository at this point in the history
  • Loading branch information
yancyribbens committed Jan 26, 2024
1 parent dc0e5df commit 8d21ad1
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 40 deletions.
132 changes: 97 additions & 35 deletions src/branch_and_bound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<Vec<&WeightedUtxo>> {
// Total_Tries in Core:
Expand All @@ -120,15 +122,16 @@ 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<usize> = vec![];
let mut best_selection: Option<Vec<usize>> = None;

// 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())
Expand All @@ -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
});
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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);
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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(
Expand All @@ -304,17 +338,19 @@ mod tests {
use core::str::FromStr;

fn create_weighted_utxos() -> Vec<WeightedUtxo> {
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()
}
Expand All @@ -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());
}
Expand All @@ -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());
}
Expand All @@ -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());
Expand All @@ -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());
Expand All @@ -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());
Expand All @@ -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());
Expand All @@ -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());
Expand All @@ -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());
Expand All @@ -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());
Expand All @@ -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());
Expand All @@ -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);
}

Expand All @@ -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());
}

Expand Down Expand Up @@ -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());
}
Expand All @@ -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() {
Expand Down
16 changes: 11 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -60,13 +62,16 @@ impl WeightedUtxo {
}

fn calculate_fee(&self, fee_rate: FeeRate) -> Option<Amount> {
// 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<SignedAmount> {
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)
}
}

Expand All @@ -81,9 +86,10 @@ pub fn select_coins<T: Utxo>(
target: Amount,
cost_of_change: Amount,
fee_rate: FeeRate,
long_term_fee_rate: FeeRate,
weighted_utxos: &mut [WeightedUtxo],
) -> Option<Vec<WeightedUtxo>> {
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())
Expand Down

0 comments on commit 8d21ad1

Please sign in to comment.