Skip to content

Commit eb079d5

Browse files
LLFournevanlinjin
authored andcommitted
Fix weight calculations for mixed legacy and segwit
see: #924 (comment) Was a PITA since branch and bound is hard to do with this interference between segiwt and legacy weights. It would find solutions that looked good until you add the final input which was segwit and then the solution would be suboptimal and fail the test.
1 parent 30c6469 commit eb079d5

File tree

10 files changed

+260
-68
lines changed

10 files changed

+260
-68
lines changed

crates/bdk/src/wallet/coin_selection.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -836,7 +836,7 @@ mod test {
836836
let drain_script = ScriptBuf::default();
837837
let target_amount = 250_000 + FEE_AMOUNT;
838838

839-
let result = LargestFirstCoinSelection::default()
839+
let result = LargestFirstCoinSelection
840840
.coin_select(
841841
utxos,
842842
vec![],
@@ -857,7 +857,7 @@ mod test {
857857
let drain_script = ScriptBuf::default();
858858
let target_amount = 20_000 + FEE_AMOUNT;
859859

860-
let result = LargestFirstCoinSelection::default()
860+
let result = LargestFirstCoinSelection
861861
.coin_select(
862862
utxos,
863863
vec![],
@@ -878,7 +878,7 @@ mod test {
878878
let drain_script = ScriptBuf::default();
879879
let target_amount = 20_000 + FEE_AMOUNT;
880880

881-
let result = LargestFirstCoinSelection::default()
881+
let result = LargestFirstCoinSelection
882882
.coin_select(
883883
vec![],
884884
utxos,
@@ -900,7 +900,7 @@ mod test {
900900
let drain_script = ScriptBuf::default();
901901
let target_amount = 500_000 + FEE_AMOUNT;
902902

903-
LargestFirstCoinSelection::default()
903+
LargestFirstCoinSelection
904904
.coin_select(
905905
vec![],
906906
utxos,
@@ -918,7 +918,7 @@ mod test {
918918
let drain_script = ScriptBuf::default();
919919
let target_amount = 250_000 + FEE_AMOUNT;
920920

921-
LargestFirstCoinSelection::default()
921+
LargestFirstCoinSelection
922922
.coin_select(
923923
vec![],
924924
utxos,
@@ -935,7 +935,7 @@ mod test {
935935
let drain_script = ScriptBuf::default();
936936
let target_amount = 180_000 + FEE_AMOUNT;
937937

938-
let result = OldestFirstCoinSelection::default()
938+
let result = OldestFirstCoinSelection
939939
.coin_select(
940940
vec![],
941941
utxos,
@@ -956,7 +956,7 @@ mod test {
956956
let drain_script = ScriptBuf::default();
957957
let target_amount = 20_000 + FEE_AMOUNT;
958958

959-
let result = OldestFirstCoinSelection::default()
959+
let result = OldestFirstCoinSelection
960960
.coin_select(
961961
utxos,
962962
vec![],
@@ -977,7 +977,7 @@ mod test {
977977
let drain_script = ScriptBuf::default();
978978
let target_amount = 20_000 + FEE_AMOUNT;
979979

980-
let result = OldestFirstCoinSelection::default()
980+
let result = OldestFirstCoinSelection
981981
.coin_select(
982982
vec![],
983983
utxos,
@@ -999,7 +999,7 @@ mod test {
999999
let drain_script = ScriptBuf::default();
10001000
let target_amount = 600_000 + FEE_AMOUNT;
10011001

1002-
OldestFirstCoinSelection::default()
1002+
OldestFirstCoinSelection
10031003
.coin_select(
10041004
vec![],
10051005
utxos,
@@ -1018,7 +1018,7 @@ mod test {
10181018
let target_amount: u64 = utxos.iter().map(|wu| wu.utxo.txout().value).sum::<u64>() - 50;
10191019
let drain_script = ScriptBuf::default();
10201020

1021-
OldestFirstCoinSelection::default()
1021+
OldestFirstCoinSelection
10221022
.coin_select(
10231023
vec![],
10241024
utxos,

nursery/coin_select/README.md

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use bdk_coin_select::{CoinSelector, Candidate, TXIN_BASE_WEIGHT};
1010
use bitcoin::{ Transaction, TxIn };
1111

1212
// You should use miniscript to figure out the satisfaction weight for your coins!
13-
const tr_satisfaction_weight: u32 = 66;
14-
const tr_input_weight: u32 = txin_base_weight + tr_satisfaction_weight;
13+
const TR_SATISFACTION_WEIGHT: u32 = 66;
14+
const TR_INPUT_WEIGHT: u32 = TXIN_BASE_WEIGHT + TR_SATISFACTION_WEIGHT;
1515

1616

1717
let candidates = vec![
@@ -21,17 +21,17 @@ let candidates = vec![
2121
input_count: 1,
2222
// the value of the input
2323
value: 1_000_000,
24-
// the total weight of the input(s). This doesn't include
24+
// the total weight of the input(s). This doesn't include
2525
weight: TR_INPUT_WEIGHT,
2626
// wether it's a segwit input. Needed so we know whether to include the segwit header
2727
// in total weight calculations.
2828
is_segwit: true
2929
},
3030
Candidate {
31-
// A candidate can represent multiple inputs in the case where you always want some inputs
31+
// A candidate can represent multiple inputs in the case where you always want some inputs
3232
// to be spent together.
3333
input_count: 2,
34-
weight: 2*tr_input_weight,
34+
weight: 2*TR_INPUT_WEIGHT,
3535
value: 3_000_000,
3636
is_segwit: true
3737
},
@@ -50,10 +50,7 @@ let base_weight = Transaction {
5050
version: 1,
5151
}.weight().to_wu() as u32;
5252

53-
panic!("{}", base_weight);
53+
println!("base weight: {}", base_weight);
5454

5555
let mut coin_selector = CoinSelector::new(&candidates,base_weight);
56-
57-
5856
```
59-

nursery/coin_select/src/bnb.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ impl<'a, M: BnBMetric> Iterator for BnbIter<'a, M> {
4242
None => return Some(None),
4343
};
4444

45-
match &self.best {
46-
Some(best_score) if score >= *best_score => Some(None),
47-
_ => {
48-
self.best = Some(score.clone());
49-
Some(Some((selector, score)))
45+
if let Some(best_score) = &self.best {
46+
if score >= *best_score {
47+
return Some(None);
5048
}
5149
}
50+
self.best = Some(score.clone());
51+
Some(Some((selector, score)))
5252
}
5353
}
5454

nursery/coin_select/src/coin_selector.rs

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -167,28 +167,31 @@ impl<'a> CoinSelector<'a> {
167167
pub fn is_empty(&self) -> bool {
168168
self.selected.is_empty()
169169
}
170-
/// Weight sum of all selected inputs.
171-
pub fn selected_weight(&self) -> u32 {
172-
self.selected
173-
.iter()
174-
.map(|&index| self.candidates[index].weight)
175-
.sum()
176-
}
177170

178171
/// The weight of the inputs including the witness header and the varint for the number of
179172
/// inputs.
180-
fn input_weight(&self) -> u32 {
181-
let witness_header_extra_weight = self
182-
.selected()
183-
.find(|(_, wv)| wv.is_segwit)
184-
.map(|_| 2)
185-
.unwrap_or(0);
173+
pub fn input_weight(&self) -> u32 {
174+
let is_segwit_tx = self.selected().any(|(_, wv)| wv.is_segwit);
175+
let witness_header_extra_weight = is_segwit_tx as u32 * 2;
186176
let vin_count_varint_extra_weight = {
187177
let input_count = self.selected().map(|(_, wv)| wv.input_count).sum::<usize>();
188178
(varint_size(input_count) - 1) * 4
189179
};
190180

191-
self.selected_weight() + witness_header_extra_weight + vin_count_varint_extra_weight
181+
let selected_weight: u32 = self
182+
.selected()
183+
.map(|(_, candidate)| {
184+
let mut weight = candidate.weight;
185+
if is_segwit_tx && !candidate.is_segwit {
186+
// non-segwit candidates do not have the witness length field included in their
187+
// weight field so we need to add 1 here if it's in a segwit tx.
188+
weight += 1;
189+
}
190+
weight
191+
})
192+
.sum();
193+
194+
selected_weight + witness_header_extra_weight + vin_count_varint_extra_weight
192195
}
193196

194197
/// Absolute value sum of all selected inputs.
@@ -202,8 +205,6 @@ impl<'a> CoinSelector<'a> {
202205
/// Current weight of template tx + selected inputs.
203206
pub fn weight(&self, drain_weight: u32) -> u32 {
204207
// TODO take into account whether drain tips over varint for number of outputs
205-
//
206-
// TODO: take into account the witness stack length for each input
207208
self.base_weight + self.input_weight() + drain_weight
208209
}
209210

@@ -235,8 +236,8 @@ impl<'a> CoinSelector<'a> {
235236
- target.min_fee as i64
236237
}
237238

238-
/// The feerate the transaction would have if we were to use this selection of inputs to achieve
239-
/// the ???
239+
/// The feerate the transaction would have if we were to use this selection of inputs to acheive
240+
/// the `target_value`
240241
pub fn implied_feerate(&self, target_value: u64, drain: Drain) -> FeeRate {
241242
let numerator = self.selected_value() as i64 - target_value as i64 - drain.value as i64;
242243
let denom = self.weight(drain.weight);
@@ -258,8 +259,8 @@ impl<'a> CoinSelector<'a> {
258259
}
259260

260261
// /// Waste sum of all selected inputs.
261-
fn selected_waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 {
262-
self.selected_weight() as f32 * (feerate.spwu() - long_term_feerate.spwu())
262+
fn input_waste(&self, feerate: FeeRate, long_term_feerate: FeeRate) -> f32 {
263+
self.input_weight() as f32 * (feerate.spwu() - long_term_feerate.spwu())
263264
}
264265

265266
/// Sorts the candidates by the comparision function.
@@ -315,7 +316,7 @@ impl<'a> CoinSelector<'a> {
315316
excess_discount: f32,
316317
) -> f32 {
317318
debug_assert!((0.0..=1.0).contains(&excess_discount));
318-
let mut waste = self.selected_waste(target.feerate, long_term_feerate);
319+
let mut waste = self.input_waste(target.feerate, long_term_feerate);
319320

320321
if drain.is_none() {
321322
// We don't allow negative excess waste since negative excess just means you haven't

nursery/coin_select/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ pub mod change_policy;
2828
/// length.
2929
pub const TXIN_BASE_WEIGHT: u32 = (32 + 4 + 4 + 1) * 4;
3030

31-
/// The weight of a TXOUT with a zero length `scriptPubkey`
31+
/// The weight of a TXOUT with a zero length `scriptPubKey`
32+
#[allow(clippy::identity_op)]
3233
pub const TXOUT_BASE_WEIGHT: u32 =
3334
// The value
3435
4 * core::mem::size_of::<u64>() as u32

nursery/coin_select/src/metrics/waste.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@ use crate::{bnb::BnBMetric, float::Ordf32, Candidate, CoinSelector, Drain, FeeRa
33

44
/// The "waste" metric used by bitcoin core.
55
///
6-
/// See this [great
7-
/// explanation](https://bitcoin.stackexchange.com/questions/113622/what-does-waste-metric-mean-in-the-context-of-coin-selection) for an understanding of the waste metric.
6+
/// See this [great explanation](https://bitcoin.stackexchange.com/questions/113622/what-does-waste-metric-mean-in-the-context-of-coin-selection)
7+
/// for an understanding of the waste metric.
88
///
99
/// ## WARNING: Waste metric considered wasteful
1010
///
1111
/// Note that bitcoin core at the time of writing use the waste metric to
1212
///
1313
/// 1. minimise the waste while searching for changeless solutions.
14-
/// 2. It tiebreaks multiple valid selections from different algorithms (which do not try and minimise waste) with waste.
14+
/// 2. It tiebreaks multiple valid selections from different algorithms (which do not try and
15+
/// minimise waste) with waste.
1516
///
16-
/// This is **very** different from minimising waste in general which is what this metric will do when used in [`CoinSelector::branch_and_bound`].
17-
/// The waste metric tends to over consolidate funds. If the `long_term_feerate` is even slightly
18-
/// higher than the current feerate (specified in `target`) it will select all your coins!
17+
/// This is **very** different from minimising waste in general which is what this metric will do
18+
/// when used in [`CoinSelector::branch_and_bound`]. The waste metric tends to over consolidate
19+
/// funds. If the `long_term_feerate` is even slightly higher than the current feerate (specified
20+
/// in `target`) it will select all your coins!
1921
pub struct Waste<'c, C> {
2022
pub target: Target,
2123
pub long_term_feerate: FeeRate,
@@ -154,7 +156,7 @@ where
154156
debug_assert!(weight_to_satisfy <= to_slurp.weight as f32);
155157
weight_to_satisfy
156158
};
157-
let weight_lower_bound = cs.selected_weight() as f32 + ideal_next_weight;
159+
let weight_lower_bound = cs.input_weight() as f32 + ideal_next_weight;
158160
let mut waste = weight_lower_bound * rate_diff;
159161
waste += change_lower_bound.waste(self.target.feerate, self.long_term_feerate);
160162

0 commit comments

Comments
 (0)