From 48fc15715388a238071d85df145228848b22b3de Mon Sep 17 00:00:00 2001 From: yancy Date: Fri, 17 Feb 2023 21:34:23 +0100 Subject: [PATCH] Add constant CHANGE_LOWER --- src/lib.rs | 4 ++++ src/single_random_draw.rs | 35 +++++++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 44df0c6..165662e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,10 @@ pub use crate::single_random_draw::select_coins_srd; #[cfg(any(test, feature = "rand"))] use crate::errors::LibError; +// https://github.com/bitcoin/bitcoin/blob/f722a9bd132222d9d5cd503b5af25c905b205cdb/src/wallet/coinselection.h#L20 +#[cfg(any(test, feature = "rand"))] +const CHANGE_LOWER: u64 = 50_000; + /// Trait that a UTXO struct must implement to be used as part of the coin selection /// algorithm. pub trait Utxo: Clone { diff --git a/src/single_random_draw.rs b/src/single_random_draw.rs index f8d0353..461d5a5 100644 --- a/src/single_random_draw.rs +++ b/src/single_random_draw.rs @@ -4,6 +4,9 @@ #[cfg(any(test, feature = "rand"))] use crate::Utxo; +#[cfg(any(test, feature = "rand"))] +use crate::CHANGE_LOWER; + #[cfg(any(test, feature = "rand"))] use rand::seq::SliceRandom; @@ -30,7 +33,12 @@ pub fn select_coins_srd( let mut i = 0; - while sum < target && i < utxo_pool.len() { + // The required size of a change output must be at least + // larger than the target by CHANGE_LOWER amount to avoid + // needlessly small change outputs. + let lower_bound = target + CHANGE_LOWER; + + while sum < lower_bound && i < utxo_pool.len() { let utxo = &utxo_pool[i]; let fee = get_fee_vbytes(fee_rate, utxo.get_weight())?; @@ -41,7 +49,7 @@ pub fn select_coins_srd( i += 1; } - if sum >= target { + if sum >= lower_bound { Ok(Some(res)) } else { Ok(None) @@ -52,6 +60,7 @@ pub fn select_coins_srd( mod tests { use crate::single_random_draw::select_coins_srd; use crate::Utxo; + use crate::CHANGE_LOWER; use rand::rngs::mock::StepRng; // https://github.com/bitcoin/bitcoin/blob/b264410e012a61b103e1a03c43df4e17b9b75452/src/test/util/setup_common.h#L80 @@ -60,8 +69,8 @@ mod tests { const FEE_RATE: u64 = 10; const UTXO_POOL: [MinimalUtxo; 2] = [ - MinimalUtxo { value: CENT, weight: WEIGHT }, - MinimalUtxo { value: 2 * CENT, weight: WEIGHT }, + MinimalUtxo { value: CENT + CHANGE_LOWER, weight: WEIGHT }, + MinimalUtxo { value: 2 * CENT + CHANGE_LOWER, weight: WEIGHT }, ]; #[derive(Clone, Debug, Eq, PartialEq)] @@ -112,7 +121,7 @@ mod tests { #[test] fn select_coins_srd_with_solution() { - let expected_result = vec![MinimalUtxo { value: 2 * CENT, weight: WEIGHT }]; + let expected_result = vec![MinimalUtxo { value: 2 * CENT + CHANGE_LOWER, weight: WEIGHT }]; let utxo_match = select_coins_srd( target_minus_fee(2 * CENT, 1), &mut UTXO_POOL.clone(), @@ -127,8 +136,8 @@ mod tests { #[test] fn select_coins_all_solution() { let expected_result = vec![ - MinimalUtxo { value: CENT * 2, weight: WEIGHT }, - MinimalUtxo { value: CENT, weight: WEIGHT }, + MinimalUtxo { value: CENT * 2 + CHANGE_LOWER, weight: WEIGHT }, + MinimalUtxo { value: CENT + CHANGE_LOWER, weight: WEIGHT }, ]; let utxo_match = select_coins_srd( @@ -164,7 +173,7 @@ mod tests { #[test] fn select_coins_srd_with_error_can_succeed() { - let value = CENT; + let value = CENT + CHANGE_LOWER; let expected_result = vec![MinimalUtxo { value, weight: WEIGHT }]; let mut utxo_pool = vec![ @@ -177,4 +186,14 @@ mod tests { assert_eq!(utxo_match.expect("did not properly select coins"), expected_result); } + + #[test] + fn select_coins_srd_change_output_too_small() { + let mut utxo_pool = [MinimalUtxo { value: CENT, weight: WEIGHT }]; + + let utxo_match = + select_coins_srd(target_minus_fee(CENT, 1), &mut utxo_pool, FEE_RATE, &mut get_rng()) + .expect("unexpected error"); + assert!(utxo_match.is_none()); + } }