@@ -285,6 +285,20 @@ pub async fn select_coins_to_spend(
285285 // See also "let upper_target = target.saturating_mul(2);" in "fn random_improve()".
286286 const TOTAL_AMOUNT_ADJUSTMENT_FACTOR : u64 = 2 ;
287287
288+ // After selecting large coins that cover at least twice the required amount,
289+ // we include a limited number of small (dust) coins. The maximum number of dust coins
290+ // is determined by the multiplier defined below. Specifically, the number of dust coins
291+ // will never exceed FACTOR times the number of large coins selected.
292+ //
293+ // This limit prevents excessive dust coins from being included in cases where
294+ // the query lacks a specified maximum limit (defaulting to 255).
295+ //
296+ // Example:
297+ // - If 3 large coins are selected (and FACTOR is 5), up to 15 dust coins may be included (0..=15).
298+ // - Still, if the selected dust can cover the amount of some big coins, the
299+ // latter will be removed from the set
300+ const DUST_TO_BIG_COINS_FACTOR : u16 = 5 ;
301+
288302 if total == 0 || max == 0 {
289303 return Err ( CoinsQueryError :: IncorrectQueryParameters {
290304 provided_total : total,
@@ -327,7 +341,8 @@ pub async fn select_coins_to_spend(
327341 }
328342 } ) ?;
329343
330- let max_dust_count = max_dust_count ( max, number_of_big_coins) ;
344+ let max_dust_count =
345+ max_dust_count ( max, number_of_big_coins, DUST_TO_BIG_COINS_FACTOR ) ;
331346 let ( dust_coins_total, selected_dust_coins) = dust_coins (
332347 dust_coins_stream,
333348 last_selected_big_coin,
@@ -408,9 +423,14 @@ fn is_excluded(key: &CoinsToSpendIndexKey, excluded_ids: &ExcludedCoinIds) -> bo
408423 }
409424}
410425
411- fn max_dust_count ( max : u16 , big_coins_len : u16 ) -> u16 {
426+ fn max_dust_count ( max : u16 , big_coins_len : u16 , dust_to_big_coins_factor : u16 ) -> u16 {
412427 let mut rng = rand:: thread_rng ( ) ;
413- rng. gen_range ( 0 ..=max. saturating_sub ( big_coins_len) )
428+
429+ let max_from_factor = big_coins_len. saturating_mul ( dust_to_big_coins_factor) ;
430+ let max_adjusted = max. saturating_sub ( big_coins_len) ;
431+ let upper_bound = max_from_factor. min ( max_adjusted) ;
432+
433+ rng. gen_range ( 0 ..=upper_bound)
414434}
415435
416436fn skip_big_coins_up_to_amount (
@@ -442,6 +462,7 @@ mod tests {
442462 use crate :: {
443463 coins_query:: {
444464 largest_first,
465+ max_dust_count,
445466 random_improve,
446467 CoinsQueryError ,
447468 SpendQuery ,
@@ -491,6 +512,10 @@ mod tests {
491512 } ;
492513 use futures:: TryStreamExt ;
493514 use itertools:: Itertools ;
515+ use proptest:: {
516+ prelude:: * ,
517+ proptest,
518+ } ;
494519 use rand:: {
495520 rngs:: StdRng ,
496521 Rng ,
@@ -1537,6 +1562,27 @@ mod tests {
15371562 )
15381563 }
15391564
1565+ proptest ! {
1566+ #[ test]
1567+ fn max_dust_count_respects_limits(
1568+ max in 1u16 ..255 ,
1569+ number_of_big_coins in 1u16 ..255 ,
1570+ factor in 1u16 ..10 ,
1571+ ) {
1572+ // We're at the stage of the algorithm where we have already selected the big coins and
1573+ // we're trying to select the dust coins.
1574+ // So we're sure that the following assumptions hold:
1575+ // 1. number_of_big_coins <= max - big coin selection algo is capped at 'max'.
1576+ // 2. there must be at least one big coin selected, otherwise we'll break
1577+ // with the `InsufficientCoinsForTheMax` error earlier.
1578+ prop_assume!( number_of_big_coins <= max && number_of_big_coins >= 1 ) ;
1579+
1580+ let max_dust_count = max_dust_count( max, number_of_big_coins, factor) ;
1581+ prop_assert!( number_of_big_coins + max_dust_count <= max) ;
1582+ prop_assert!( max_dust_count <= number_of_big_coins. saturating_mul( factor) ) ;
1583+ }
1584+ }
1585+
15401586 #[ test_case:: test_case(
15411587 TestCase {
15421588 db_amount: vec![ u64 :: MAX , u64 :: MAX ] ,
0 commit comments