5
5
//! This module introduces the Branch and Bound Coin-Selection Algorithm.
6
6
7
7
use bitcoin:: amount:: CheckedSum ;
8
- use bitcoin:: { Amount , FeeRate , SignedAmount } ;
8
+ use bitcoin:: { Amount , FeeRate } ;
9
9
10
10
use crate :: { Return , WeightedUtxo } ;
11
11
@@ -161,18 +161,18 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
161
161
let mut index = 0 ;
162
162
let mut backtrack;
163
163
164
- let mut value = Amount :: ZERO ;
164
+ let mut value = 0 ;
165
165
166
- let mut current_waste: SignedAmount = SignedAmount :: ZERO ;
167
- let mut best_waste = SignedAmount :: MAX_MONEY ;
166
+ let mut current_waste: i64 = 0 ;
167
+ let mut best_waste: i64 = Amount :: MAX_MONEY . to_sat ( ) as i64 ;
168
168
169
169
let mut index_selection: Vec < usize > = vec ! [ ] ;
170
170
let mut best_selection: Vec < usize > = vec ! [ ] ;
171
171
172
172
let upper_bound = target. checked_add ( cost_of_change) ?;
173
173
174
174
// Creates a tuple of (effective_value, waste, weighted_utxo)
175
- let mut w_utxos: Vec < ( Amount , SignedAmount , & Utxo ) > = weighted_utxos
175
+ let w_utxos = weighted_utxos
176
176
. iter ( )
177
177
// calculate effective_value and waste for each w_utxo.
178
178
. map ( |wu| ( wu. effective_value ( fee_rate) , wu. waste ( fee_rate, long_term_fee_rate) , wu) )
@@ -183,20 +183,25 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
183
183
// filter out all effective_values that are negative.
184
184
. filter ( |( eff_val, _, _) | eff_val. is_positive ( ) )
185
185
// all utxo effective_values are now positive (see previous step) - cast to unsigned.
186
- . map ( |( eff_val, waste, wu) | ( eff_val. to_unsigned ( ) . unwrap ( ) , waste, wu) )
187
- . collect ( ) ;
186
+ . map ( |( eff_val, waste, wu) | ( eff_val. to_unsigned ( ) . unwrap ( ) , waste, wu) ) ;
188
187
189
- // descending sort by effective_value using satisfaction weight as tie breaker.
190
- w_utxos. sort_by ( |a, b| {
191
- b. 0 . cmp ( & a. 0 ) . then ( b. 2 . satisfaction_weight ( ) . cmp ( & a. 2 . satisfaction_weight ( ) ) )
192
- } ) ;
193
-
194
- let mut available_value = w_utxos. clone ( ) . into_iter ( ) . map ( |( ev, _, _) | ev) . checked_sum ( ) ?;
188
+ let available_value = w_utxos. clone ( ) . map ( |( ev, _, _) | ev) . checked_sum ( ) ?;
195
189
196
190
if available_value < target || target == Amount :: ZERO {
197
191
return None ;
198
192
}
199
193
194
+ let mut available_value = available_value. to_sat ( ) ;
195
+
196
+ // cast from Amount/SignedAmount to u64/i64 for more performant operations.
197
+ let mut w_utxos: Vec < ( u64 , i64 , & Utxo ) > = w_utxos. map ( |( e, w, u) | ( e. to_sat ( ) , w. to_sat ( ) , u) ) . collect ( ) ;
198
+ let target = target. to_sat ( ) ;
199
+
200
+ // descending sort by effective_value using satisfaction weight as tie breaker.
201
+ w_utxos. sort_by ( |a, b| {
202
+ b. 0 . cmp ( & a. 0 ) . then ( b. 2 . satisfaction_weight ( ) . cmp ( & a. 2 . satisfaction_weight ( ) ) )
203
+ } ) ;
204
+
200
205
while iteration < ITERATION_LIMIT {
201
206
backtrack = false ;
202
207
@@ -205,7 +210,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
205
210
// unchecked_add is used here for performance. Before entering the search loop, all
206
211
// utxos are summed and checked for overflow. Since there was no overflow then, any
207
212
// subset of addition will not overflow.
208
- if available_value. unchecked_add ( value) < target
213
+ if available_value + value < target
209
214
// Provides an upper bound on the excess value that is permissible.
210
215
// Since value is lost when we create a change output due to increasing the size of the
211
216
// transaction by an output (the change output), we accept solutions that may be
@@ -217,7 +222,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
217
222
//
218
223
// That is, the range includes solutions that exactly equal the target up to but not
219
224
// including values greater than target + cost_of_change.
220
- || value > upper_bound
225
+ || value > upper_bound. to_sat ( )
221
226
// if current_waste > best_waste, then backtrack. However, only backtrack if
222
227
// it's high fee_rate environment. During low fee environments, a utxo may
223
228
// have negative waste, therefore adding more utxos in such an environment
@@ -231,9 +236,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
231
236
else if value >= target {
232
237
backtrack = true ;
233
238
234
- let v = value. to_signed ( ) . ok ( ) ?;
235
- let t = target. to_signed ( ) . ok ( ) ?;
236
- let waste: SignedAmount = v. checked_sub ( t) ?;
239
+ let waste: i64 = ( value as i64 ) . checked_sub ( target as i64 ) ?;
237
240
current_waste = current_waste. checked_add ( waste) ?;
238
241
239
242
// Check if index_selection is better than the previous known best, and
@@ -265,6 +268,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
265
268
assert_eq ! ( index, * index_selection. last( ) . unwrap( ) ) ;
266
269
let ( eff_value, utxo_waste, _) = w_utxos[ index] ;
267
270
current_waste = current_waste. checked_sub ( utxo_waste) ?;
271
+
268
272
value = value. checked_sub ( eff_value) ?;
269
273
index_selection. pop ( ) . unwrap ( ) ;
270
274
}
@@ -275,7 +279,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
275
279
// unchecked sub is used her for performance.
276
280
// The bounds for available_value are at most the sum of utxos
277
281
// and at least zero.
278
- available_value = available_value. unchecked_sub ( eff_value) ;
282
+ available_value = available_value - eff_value;
279
283
280
284
// Check if we can omit the currently selected depending on if the last
281
285
// was omitted. Therefore, check if index_selection has a previous one.
@@ -290,7 +294,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
290
294
291
295
// unchecked add is used here for performance. Since the sum of all utxo values
292
296
// did not overflow, then any positive subset of the sum will not overflow.
293
- value = value. unchecked_add ( eff_value) ;
297
+ value = value + eff_value;
294
298
}
295
299
}
296
300
@@ -305,7 +309,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
305
309
fn index_to_utxo_list < Utxo : WeightedUtxo > (
306
310
iterations : u32 ,
307
311
index_list : Vec < usize > ,
308
- wu : Vec < ( Amount , SignedAmount , & Utxo ) > ,
312
+ wu : Vec < ( u64 , i64 , & Utxo ) > ,
309
313
) -> Return < Utxo > {
310
314
let mut result: Vec < _ > = Vec :: new ( ) ;
311
315
let list = index_list;
@@ -330,7 +334,7 @@ mod tests {
330
334
use arbitrary:: { Arbitrary , Unstructured } ;
331
335
use arbtest:: arbtest;
332
336
use bitcoin:: transaction:: effective_value;
333
- use bitcoin:: { Amount , Weight } ;
337
+ use bitcoin:: { Amount , Weight , SignedAmount } ;
334
338
335
339
use super :: * ;
336
340
use crate :: tests:: { assert_proptest_bnb, assert_ref_eq, parse_fee_rate, Utxo , UtxoPool } ;
@@ -392,7 +396,7 @@ mod tests {
392
396
// see: https://github.com/rust-fuzz/arbitrary/pull/192
393
397
fn arb_amount_in_range ( u : & mut Unstructured , r : std:: ops:: RangeInclusive < u64 > ) -> Amount {
394
398
let u = u. int_in_range :: < u64 > ( r) . unwrap ( ) ;
395
- Amount :: from_sat ( u)
399
+ Amount :: from_sat ( u) . unwrap ( )
396
400
}
397
401
398
402
// Use in place of arbitrary_in_range()
@@ -634,7 +638,7 @@ mod tests {
634
638
cost_of_change : "0" ,
635
639
fee_rate : "0" ,
636
640
lt_fee_rate : "0" ,
637
- weighted_utxos : & [ "18446744073709551615 sats/68 vB" , "1 sats/68 vB" ] , // [u64 ::MAX, 1 sat]
641
+ weighted_utxos : & [ "2100000000000000 sats/68 vB" , "1 sats/68 vB" ] , // [Amount ::MAX, 1 sat]
638
642
expected_utxos : None ,
639
643
expected_iterations : 0 ,
640
644
}
@@ -645,7 +649,7 @@ mod tests {
645
649
fn select_coins_bnb_upper_bound_overflow ( ) {
646
650
TestBnB {
647
651
target : "1 sats" ,
648
- cost_of_change : "18446744073709551615 sats" , // u64 ::MAX
652
+ cost_of_change : "2100000000000000 sats" , // Amount ::MAX
649
653
fee_rate : "0" ,
650
654
lt_fee_rate : "0" ,
651
655
weighted_utxos : & [ "1 sats/68 vB" ] ,
@@ -655,20 +659,6 @@ mod tests {
655
659
. assert ( ) ;
656
660
}
657
661
658
- #[ test]
659
- fn select_coins_bnb_utxo_greater_than_max_money ( ) {
660
- TestBnB {
661
- target : "1 sats" ,
662
- cost_of_change : "18141417255681066410 sats" ,
663
- fee_rate : "1 sat/kwu" ,
664
- lt_fee_rate : "0" ,
665
- weighted_utxos : & [ "8740670712339394302 sats/68 vB" ] ,
666
- expected_utxos : None ,
667
- expected_iterations : 0 ,
668
- }
669
- . assert ( ) ;
670
- }
671
-
672
662
#[ test]
673
663
fn select_coins_bnb_effective_value_tie_high_fee_rate ( ) {
674
664
// If the fee_rate is expensive prefer lower weight UTXOS
@@ -793,7 +783,7 @@ mod tests {
793
783
// Takes 327,661 iterations to find a solution.
794
784
let base: usize = 2 ;
795
785
let alpha = ( 0 ..17 ) . enumerate ( ) . map ( |( i, _) | base. pow ( 17 + i as u32 ) ) ;
796
- let target = Amount :: from_sat ( alpha. clone ( ) . sum :: < usize > ( ) as u64 ) ;
786
+ let target = Amount :: from_sat ( alpha. clone ( ) . sum :: < usize > ( ) as u64 ) . unwrap ( ) ;
797
787
798
788
let beta = ( 0 ..17 ) . enumerate ( ) . map ( |( i, _) | {
799
789
let a = base. pow ( 17 + i as u32 ) ;
@@ -805,7 +795,7 @@ mod tests {
805
795
// flatten requires iterable types.
806
796
// use once() to make tuple iterable.
807
797
. flat_map ( |tup| once ( tup. 0 ) . chain ( once ( tup. 1 ) ) )
808
- . map ( |a| Amount :: from_sat ( a as u64 ) )
798
+ . map ( |a| Amount :: from_sat ( a as u64 ) . unwrap ( ) )
809
799
. collect ( ) ;
810
800
811
801
let pool: Vec < _ > = amts. into_iter ( ) . map ( |a| Utxo :: new ( a, Weight :: ZERO ) ) . collect ( ) ;
@@ -826,11 +816,11 @@ mod tests {
826
816
vec ! [ a, a + 2 ]
827
817
} ) ;
828
818
829
- let amts: Vec < _ > = vals. map ( Amount :: from_sat) . collect ( ) ;
819
+ let amts: Vec < _ > = vals. map ( |v| Amount :: from_sat ( v ) . unwrap ( ) ) . collect ( ) ;
830
820
let pool: Vec < _ > = amts. into_iter ( ) . map ( |a| Utxo :: new ( a, Weight :: ZERO ) ) . collect ( ) ;
831
821
832
822
let list = select_coins_bnb (
833
- Amount :: from_sat ( target) ,
823
+ Amount :: from_sat ( target) . unwrap ( ) ,
834
824
Amount :: ONE_SAT ,
835
825
FeeRate :: ZERO ,
836
826
FeeRate :: ZERO ,
@@ -852,14 +842,14 @@ mod tests {
852
842
vec ! [ a, a + 2 ]
853
843
} ) ;
854
844
855
- let mut amts: Vec < _ > = amts. map ( Amount :: from_sat) . collect ( ) ;
845
+ let mut amts: Vec < _ > = amts. map ( |v| Amount :: from_sat ( v ) . unwrap ( ) ) . collect ( ) ;
856
846
857
847
// Add a value that will match the target before iteration exhaustion occurs.
858
- amts. push ( Amount :: from_sat ( target) ) ;
848
+ amts. push ( Amount :: from_sat ( target) . unwrap ( ) ) ;
859
849
let pool: Vec < _ > = amts. into_iter ( ) . map ( |a| Utxo :: new ( a, Weight :: ZERO ) ) . collect ( ) ;
860
850
861
851
let ( iterations, utxos) = select_coins_bnb (
862
- Amount :: from_sat ( target) ,
852
+ Amount :: from_sat ( target) . unwrap ( ) ,
863
853
Amount :: ONE_SAT ,
864
854
FeeRate :: ZERO ,
865
855
FeeRate :: ZERO ,
@@ -868,7 +858,7 @@ mod tests {
868
858
. unwrap ( ) ;
869
859
870
860
assert_eq ! ( utxos. len( ) , 1 ) ;
871
- assert_eq ! ( utxos[ 0 ] . value( ) , Amount :: from_sat( target) ) ;
861
+ assert_eq ! ( utxos[ 0 ] . value( ) , Amount :: from_sat( target) . unwrap ( ) ) ;
872
862
assert_eq ! ( 100000 , iterations) ;
873
863
}
874
864
@@ -904,7 +894,7 @@ mod tests {
904
894
if let Some ( f) = max_fee_rate {
905
895
let fee_rate = arb_fee_rate_in_range ( u, 1 ..=f. to_sat_per_kwu ( ) ) ;
906
896
907
- //TODO update eff value interface
897
+ // TODO update eff value interface
908
898
let target_effective_value =
909
899
effective_value ( fee_rate, utxo. satisfaction_weight ( ) , utxo. value ( ) ) . unwrap ( ) ;
910
900
@@ -918,7 +908,8 @@ mod tests {
918
908
effective_value ( fee_rate, u. satisfaction_weight ( ) , u. value ( ) )
919
909
. unwrap ( )
920
910
} )
921
- . sum ( ) ;
911
+ . checked_sum ( )
912
+ . unwrap ( ) ;
922
913
let amount_sum = sum. to_unsigned ( ) . unwrap ( ) ;
923
914
assert_eq ! ( amount_sum, target) ;
924
915
@@ -998,7 +989,8 @@ mod tests {
998
989
. to_unsigned ( )
999
990
. unwrap ( )
1000
991
} )
1001
- . sum ( ) ;
992
+ . checked_sum ( )
993
+ . unwrap ( ) ;
1002
994
assert_eq ! ( effective_value_sum, target) ;
1003
995
1004
996
// TODO checked_add not available in Weight
@@ -1039,9 +1031,9 @@ mod tests {
1039
1031
1040
1032
let result = select_coins_bnb ( target, cost_of_change, fee_rate, lt_fee_rate, & utxos) ;
1041
1033
1042
- assert_proptest_bnb ( target, cost_of_change, fee_rate, pool, result) ;
1034
+ assert_proptest_bnb ( target, cost_of_change, fee_rate, lt_fee_rate , pool, result) ;
1043
1035
1044
1036
Ok ( ( ) )
1045
- } ) ;
1037
+ } ) . seed ( 0xcde68a8900000060 ) ;
1046
1038
}
1047
1039
}
0 commit comments