Skip to content

Commit cfa0928

Browse files
committed
wip
1 parent 78cbc36 commit cfa0928

File tree

6 files changed

+155
-122
lines changed

6 files changed

+155
-122
lines changed

Cargo.toml

+9-9
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ readme = "README.md"
1414
rust-version = "1.63.0"
1515

1616
[dependencies]
17-
bitcoin = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
17+
bitcoin = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }
1818
rand = { version = "0.8.5", default-features = false, optional = true }
1919

2020
[dev-dependencies]
21-
bitcoin = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e", features = ["arbitrary"] }
21+
bitcoin = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f", features = ["arbitrary"] }
2222
criterion = "0.3"
2323
bitcoin-coin-selection = {path = ".", features = ["rand"]}
2424
rand = "0.8.5"
@@ -31,10 +31,10 @@ name = "coin_selection"
3131
harness = false
3232

3333
[patch.crates-io]
34-
bitcoin_hashes = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
35-
base58ck = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
36-
bitcoin-internals = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
37-
bitcoin-io = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
38-
bitcoin-primitives = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
39-
bitcoin-addresses = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
40-
bitcoin-units = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
34+
bitcoin_hashes = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }
35+
base58ck = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }
36+
bitcoin-internals = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }
37+
bitcoin-io = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }
38+
bitcoin-primitives = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }
39+
bitcoin-addresses = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }
40+
bitcoin-units = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }

benches/coin_selection.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ impl WeightedUtxo for Utxo {
1515

1616
pub fn criterion_benchmark(c: &mut Criterion) {
1717
// https://github.com/bitcoin/bitcoin/blob/f3bc1a72825fe2b51f4bc20e004cef464f05b965/src/wallet/coinselection.h#L18
18-
let cost_of_change = Amount::from_sat(50_000);
18+
let cost_of_change = Amount::from_sat(50_000).unwrap();
1919

2020
let one = Utxo {
21-
output: TxOut { value: Amount::from_sat(1_000), script_pubkey: ScriptBuf::new() },
21+
output: TxOut { value: Amount::from_sat(1_000).unwrap(), script_pubkey: ScriptBuf::new() },
2222
weight: Weight::ZERO,
2323
};
2424

2525
let two = Utxo {
26-
output: TxOut { value: Amount::from_sat(3), script_pubkey: ScriptBuf::new() },
26+
output: TxOut { value: Amount::from_sat(3).unwrap(), script_pubkey: ScriptBuf::new() },
2727
weight: Weight::ZERO,
2828
};
2929

30-
let target = Amount::from_sat(1_003);
30+
let target = Amount::from_sat(1_003).unwrap();
3131
let mut utxo_pool = vec![one; 1000];
3232
utxo_pool.push(two);
3333

@@ -44,8 +44,8 @@ pub fn criterion_benchmark(c: &mut Criterion) {
4444
assert_eq!(iteration_count, 100000);
4545

4646
assert_eq!(2, inputs.len());
47-
assert_eq!(Amount::from_sat(1_000), inputs[0].value());
48-
assert_eq!(Amount::from_sat(3), inputs[1].value());
47+
assert_eq!(Amount::from_sat(1_000).unwrap(), inputs[0].value());
48+
assert_eq!(Amount::from_sat(3).unwrap(), inputs[1].value());
4949
})
5050
});
5151
}

fuzz/Cargo.toml

+8-8
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ cargo-fuzz = true
1010
[dependencies]
1111
libfuzzer-sys = "0.4"
1212
rand = "0.8.5"
13-
bitcoin = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e", features = ["arbitrary"] }
13+
bitcoin = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f", features = ["arbitrary"] }
1414
arbitrary = { version = "1", features = ["derive"] }
1515

1616
[dependencies.bitcoin-coin-selection]
@@ -39,10 +39,10 @@ doc = false
3939
bench = false
4040

4141
[patch.crates-io]
42-
bitcoin_hashes = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
43-
base58ck = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
44-
bitcoin-internals = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
45-
bitcoin-io = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
46-
bitcoin-primitives = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
47-
bitcoin-addresses = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
48-
bitcoin-units = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "7df5e7c1bcb4aaf3247f0b76591db9744f03425e" }
42+
bitcoin_hashes = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }
43+
base58ck = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }
44+
bitcoin-internals = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }
45+
bitcoin-io = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }
46+
bitcoin-primitives = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }
47+
bitcoin-addresses = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }
48+
bitcoin-units = { git = "https://github.com/rust-bitcoin/rust-bitcoin.git", rev = "52f9c13358c97c358543f3302b325f37ac49392f" }

src/branch_and_bound.rs

+44-50
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! This module introduces the Branch and Bound Coin-Selection Algorithm.
66
77
use bitcoin::amount::CheckedSum;
8-
use bitcoin::{Amount, FeeRate, SignedAmount};
8+
use bitcoin::{Amount, FeeRate};
99

1010
use crate::{Return, WeightedUtxo};
1111

@@ -161,18 +161,18 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
161161
let mut index = 0;
162162
let mut backtrack;
163163

164-
let mut value = Amount::ZERO;
164+
let mut value = 0;
165165

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;
168168

169169
let mut index_selection: Vec<usize> = vec![];
170170
let mut best_selection: Vec<usize> = vec![];
171171

172172
let upper_bound = target.checked_add(cost_of_change)?;
173173

174174
// 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
176176
.iter()
177177
// calculate effective_value and waste for each w_utxo.
178178
.map(|wu| (wu.effective_value(fee_rate), wu.waste(fee_rate, long_term_fee_rate), wu))
@@ -183,18 +183,24 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
183183
// filter out all effective_values that are negative.
184184
.filter(|(eff_val, _, _)| eff_val.is_positive())
185185
// 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));
188187

189-
// descending sort by effective_value using satisfaction weight as tie breaker.
190-
w_utxos.sort_by(|a, b| b.0.cmp(&a.0).then(b.2.weight().cmp(&a.2.weight())));
191-
192-
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()?;
193189

194190
if available_value < target || target == Amount::ZERO {
195191
return None;
196192
}
197193

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)> =
198+
w_utxos.map(|(e, w, u)| (e.to_sat(), w.to_sat(), u)).collect();
199+
let target = target.to_sat();
200+
201+
// descending sort by effective_value using satisfaction weight as tie breaker.
202+
w_utxos.sort_by(|a, b| b.0.cmp(&a.0).then(b.2.weight().cmp(&a.2.weight())));
203+
198204
while iteration < ITERATION_LIMIT {
199205
backtrack = false;
200206

@@ -203,7 +209,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
203209
// unchecked_add is used here for performance. Before entering the search loop, all
204210
// utxos are summed and checked for overflow. Since there was no overflow then, any
205211
// subset of addition will not overflow.
206-
if available_value.unchecked_add(value) < target
212+
if available_value + value < target
207213
// Provides an upper bound on the excess value that is permissible.
208214
// Since value is lost when we create a change output due to increasing the size of the
209215
// transaction by an output (the change output), we accept solutions that may be
@@ -215,7 +221,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
215221
//
216222
// That is, the range includes solutions that exactly equal the target up to but not
217223
// including values greater than target + cost_of_change.
218-
|| value > upper_bound
224+
|| value > upper_bound.to_sat()
219225
// if current_waste > best_waste, then backtrack. However, only backtrack if
220226
// it's high fee_rate environment. During low fee environments, a utxo may
221227
// have negative waste, therefore adding more utxos in such an environment
@@ -229,9 +235,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
229235
else if value >= target {
230236
backtrack = true;
231237

232-
let v = value.to_signed().ok()?;
233-
let t = target.to_signed().ok()?;
234-
let waste: SignedAmount = v.checked_sub(t)?;
238+
let waste: i64 = (value as i64).checked_sub(target as i64)?;
235239
current_waste = current_waste.checked_add(waste)?;
236240

237241
// Check if index_selection is better than the previous known best, and
@@ -263,6 +267,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
263267
assert_eq!(index, *index_selection.last().unwrap());
264268
let (eff_value, utxo_waste, _) = w_utxos[index];
265269
current_waste = current_waste.checked_sub(utxo_waste)?;
270+
266271
value = value.checked_sub(eff_value)?;
267272
index_selection.pop().unwrap();
268273
}
@@ -273,7 +278,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
273278
// unchecked sub is used her for performance.
274279
// The bounds for available_value are at most the sum of utxos
275280
// and at least zero.
276-
available_value = available_value.unchecked_sub(eff_value);
281+
available_value = available_value - eff_value;
277282

278283
// Check if we can omit the currently selected depending on if the last
279284
// was omitted. Therefore, check if index_selection has a previous one.
@@ -288,7 +293,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
288293

289294
// unchecked add is used here for performance. Since the sum of all utxo values
290295
// did not overflow, then any positive subset of the sum will not overflow.
291-
value = value.unchecked_add(eff_value);
296+
value = value + eff_value;
292297
}
293298
}
294299

@@ -303,7 +308,7 @@ pub fn select_coins_bnb<Utxo: WeightedUtxo>(
303308
fn index_to_utxo_list<Utxo: WeightedUtxo>(
304309
iterations: u32,
305310
index_list: Vec<usize>,
306-
wu: Vec<(Amount, SignedAmount, &Utxo)>,
311+
wu: Vec<(u64, i64, &Utxo)>,
307312
) -> Return<Utxo> {
308313
let mut result: Vec<_> = Vec::new();
309314
let list = index_list;
@@ -327,7 +332,7 @@ mod tests {
327332

328333
use arbitrary::{Arbitrary, Unstructured};
329334
use arbtest::arbtest;
330-
use bitcoin::{Amount, Weight};
335+
use bitcoin::{Amount, SignedAmount, Weight};
331336

332337
use super::*;
333338
use crate::tests::{assert_proptest_bnb, assert_ref_eq, parse_fee_rate, Utxo, UtxoPool};
@@ -387,7 +392,7 @@ mod tests {
387392
// see: https://github.com/rust-fuzz/arbitrary/pull/192
388393
fn arb_amount_in_range(u: &mut Unstructured, r: std::ops::RangeInclusive<u64>) -> Amount {
389394
let u = u.int_in_range::<u64>(r).unwrap();
390-
Amount::from_sat(u)
395+
Amount::from_sat(u).unwrap()
391396
}
392397

393398
// Use in place of arbitrary_in_range()
@@ -627,7 +632,7 @@ mod tests {
627632
cost_of_change: "0",
628633
fee_rate: "0",
629634
lt_fee_rate: "0",
630-
weighted_utxos: &["18446744073709551615 sats/68 vB", "1 sats/68 vB"], // [u64::MAX, 1 sat]
635+
weighted_utxos: &["2100000000000000 sats/68 vB", "1 sats/68 vB"], // [Amount::MAX, 1 sat]
631636
expected_utxos: None,
632637
expected_iterations: 0,
633638
}
@@ -638,7 +643,7 @@ mod tests {
638643
fn select_coins_bnb_upper_bound_overflow() {
639644
TestBnB {
640645
target: "1 sats",
641-
cost_of_change: "18446744073709551615 sats", // u64::MAX
646+
cost_of_change: "2100000000000000 sats", // Amount::MAX
642647
fee_rate: "0",
643648
lt_fee_rate: "0",
644649
weighted_utxos: &["1 sats/68 vB"],
@@ -648,20 +653,6 @@ mod tests {
648653
.assert();
649654
}
650655

651-
#[test]
652-
fn select_coins_bnb_utxo_greater_than_max_money() {
653-
TestBnB {
654-
target: "1 sats",
655-
cost_of_change: "18141417255681066410 sats",
656-
fee_rate: "1 sat/kwu",
657-
lt_fee_rate: "0",
658-
weighted_utxos: &["8740670712339394302 sats/68 vB"],
659-
expected_utxos: None,
660-
expected_iterations: 0,
661-
}
662-
.assert();
663-
}
664-
665656
#[test]
666657
fn select_coins_bnb_effective_value_tie_high_fee_rate() {
667658
// If the fee_rate is expensive prefer lower weight UTXOS
@@ -786,7 +777,7 @@ mod tests {
786777
// Takes 327,661 iterations to find a solution.
787778
let base: usize = 2;
788779
let alpha = (0..17).enumerate().map(|(i, _)| base.pow(17 + i as u32));
789-
let target = Amount::from_sat(alpha.clone().sum::<usize>() as u64);
780+
let target = Amount::from_sat(alpha.clone().sum::<usize>() as u64).unwrap();
790781

791782
let beta = (0..17).enumerate().map(|(i, _)| {
792783
let a = base.pow(17 + i as u32);
@@ -798,7 +789,7 @@ mod tests {
798789
// flatten requires iterable types.
799790
// use once() to make tuple iterable.
800791
.flat_map(|tup| once(tup.0).chain(once(tup.1)))
801-
.map(|a| Amount::from_sat(a as u64))
792+
.map(|a| Amount::from_sat(a as u64).unwrap())
802793
.collect();
803794

804795
let pool: Vec<_> = amts.into_iter().map(|a| Utxo::new(a, Weight::ZERO)).collect();
@@ -819,11 +810,11 @@ mod tests {
819810
vec![a, a + 2]
820811
});
821812

822-
let amts: Vec<_> = vals.map(Amount::from_sat).collect();
813+
let amts: Vec<_> = vals.map(|v| Amount::from_sat(v).unwrap()).collect();
823814
let pool: Vec<_> = amts.into_iter().map(|a| Utxo::new(a, Weight::ZERO)).collect();
824815

825816
let list = select_coins_bnb(
826-
Amount::from_sat(target),
817+
Amount::from_sat(target).unwrap(),
827818
Amount::ONE_SAT,
828819
FeeRate::ZERO,
829820
FeeRate::ZERO,
@@ -845,14 +836,14 @@ mod tests {
845836
vec![a, a + 2]
846837
});
847838

848-
let mut amts: Vec<_> = amts.map(Amount::from_sat).collect();
839+
let mut amts: Vec<_> = amts.map(|v| Amount::from_sat(v).unwrap()).collect();
849840

850841
// Add a value that will match the target before iteration exhaustion occurs.
851-
amts.push(Amount::from_sat(target));
842+
amts.push(Amount::from_sat(target).unwrap());
852843
let pool: Vec<_> = amts.into_iter().map(|a| Utxo::new(a, Weight::ZERO)).collect();
853844

854845
let (iterations, utxos) = select_coins_bnb(
855-
Amount::from_sat(target),
846+
Amount::from_sat(target).unwrap(),
856847
Amount::ONE_SAT,
857848
FeeRate::ZERO,
858849
FeeRate::ZERO,
@@ -861,7 +852,7 @@ mod tests {
861852
.unwrap();
862853

863854
assert_eq!(utxos.len(), 1);
864-
assert_eq!(utxos[0].value(), Amount::from_sat(target));
855+
assert_eq!(utxos[0].value(), Amount::from_sat(target).unwrap());
865856
assert_eq!(100000, iterations);
866857
}
867858

@@ -897,7 +888,7 @@ mod tests {
897888
if let Some(f) = max_fee_rate {
898889
let fee_rate = arb_fee_rate_in_range(u, 1..=f.to_sat_per_kwu());
899890

900-
//TODO update eff value interface
891+
// TODO update eff value interface
901892
let target_effective_value =
902893
effective_value(fee_rate, utxo.weight(), utxo.value()).unwrap();
903894

@@ -909,7 +900,8 @@ mod tests {
909900
.clone()
910901
.into_iter()
911902
.map(|u| effective_value(fee_rate, u.weight(), u.value()).unwrap())
912-
.sum();
903+
.checked_sum()
904+
.unwrap();
913905
let amount_sum = sum.to_unsigned().unwrap();
914906
assert_eq!(amount_sum, target);
915907

@@ -985,7 +977,8 @@ mod tests {
985977
.to_unsigned()
986978
.unwrap()
987979
})
988-
.sum();
980+
.checked_sum()
981+
.unwrap();
989982
assert_eq!(effective_value_sum, target);
990983

991984
// TODO checked_add not available in Weight
@@ -1028,9 +1021,10 @@ mod tests {
10281021

10291022
let result = select_coins_bnb(target, cost_of_change, fee_rate, lt_fee_rate, &utxos);
10301023

1031-
assert_proptest_bnb(target, cost_of_change, fee_rate, pool, result);
1024+
assert_proptest_bnb(target, cost_of_change, fee_rate, lt_fee_rate, pool, result);
10321025

10331026
Ok(())
1034-
});
1027+
})
1028+
.seed(0xcde68a8900000060);
10351029
}
10361030
}

0 commit comments

Comments
 (0)