Skip to content

Commit 5482f81

Browse files
committed
Remove base_weight, put weight in Target
- CoinSelector no longer tracks anything but input weight - Previously the value of the target outputs was in `Target` but the weights were accounted for in CoinSelector. Now they're in all in target. - This allows us to actually figure out how many outputs there are and therefore the actual weight of the transaction accounting for varints.
1 parent 0f7cc31 commit 5482f81

18 files changed

+442
-1224
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@
44
- Remove `min_fee` in favour of `replace` which allows you to replace a transaction
55
- Remove `Drain` argument from `CoinSelector::select_until_target_met` because adding a drain won't
66
change when the target is met.
7+
- No more `base_weight` in `CoinSelector`. Weight of the outputs is tracked in `target`.
8+
- You now account for the number of outputs in both drain and target and their weight.
9+
- Removed waste metric because it was pretty broken and took a lot to maintain
710

README.md

Lines changed: 62 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,40 @@
11
# BDK Coin Selection
22

3-
`bdk_coin_select` is a tool to help you select inputs for making Bitcoin (ticker: BTC) transactions.
4-
It's got zero dependencies so you can paste it into your project without concern.
3+
`bdk_coin_select` is a zero-dependency tool to help you select inputs for making Bitcoin (ticker: BTC) transactions.
54

65
> ⚠ This work is only ready to use by those who expect (potentially catastrophic) bugs and will have
76
> the time to investigate them and contribute back to this crate.
87
9-
## Constructing the `CoinSelector`
10-
11-
The main structure is [`CoinSelector`](crate::CoinSelector). To construct it, we specify a list of
12-
candidate UTXOs and a transaction `base_weight`. The `base_weight` includes the recipient outputs
13-
and mandatory inputs (if any).
8+
## Synopis
149

1510
```rust
1611
use std::str::FromStr;
17-
use bdk_coin_select::{ CoinSelector, Candidate, TR_KEYSPEND_TXIN_WEIGHT};
12+
use bdk_coin_select::{ CoinSelector, Candidate, TR_KEYSPEND_TXIN_WEIGHT, Drain, FeeRate, Target, ChangePolicy, TargetOutputs, TargetFee, DrainWeights};
1813
use bitcoin::{ Address, Network, Transaction, TxIn, TxOut };
1914

20-
// The address where we want to send our coins.
2115
let recipient_addr =
2216
Address::from_str("tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46").unwrap();
2317

18+
let outputs = vec![TxOut {
19+
value: 3_500_000,
20+
script_pubkey: recipient_addr.payload.script_pubkey(),
21+
}];
22+
23+
let target = Target {
24+
outputs: TargetOutputs::fund_outputs(outputs.iter().map(|output| (output.weight() as u32, output.value))),
25+
fee: TargetFee::from_feerate(FeeRate::from_sat_per_vb(42.0))
26+
};
27+
2428
let candidates = vec![
2529
Candidate {
2630
// How many inputs does this candidate represents. Needed so we can
2731
// figure out the weight of the varint that encodes the number of inputs
2832
input_count: 1,
2933
// the value of the input
3034
value: 1_000_000,
31-
// the total weight of the input(s).
35+
// the total weight of the input(s) including their witness/scriptSig
3236
// you may need to use miniscript to figure out the correct value here.
33-
weight: TR_KEYSPEND_TXIN_WEIGHT,
37+
weight: TR_KEYSPEND_TXIN_WEIGHT,
3438
// wether it's a segwit input. Needed so we know whether to include the
3539
// segwit header in total weight calculations.
3640
is_segwit: true
@@ -45,91 +49,54 @@ let candidates = vec![
4549
}
4650
];
4751

48-
let base_tx = Transaction {
49-
input: vec![],
50-
// include your recipient outputs here
51-
output: vec![TxOut {
52-
value: 900_000,
53-
script_pubkey: recipient_addr.payload.script_pubkey(),
54-
}],
55-
lock_time: bitcoin::absolute::LockTime::from_height(0).unwrap(),
56-
version: 0x02,
57-
};
58-
let base_weight = base_tx.weight().to_wu() as u32;
59-
println!("base weight: {}", base_weight);
60-
6152
// You can now select coins!
62-
let mut coin_selector = CoinSelector::new(&candidates, base_weight);
53+
let mut coin_selector = CoinSelector::new(&candidates);
6354
coin_selector.select(0);
55+
56+
assert!(!coin_selector.is_target_met(target), "we didn't select enough");
57+
println!("we didn't select enough yet we're missing: {}", coin_selector.missing(target));
58+
coin_selector.select(1);
59+
assert!(coin_selector.is_target_met(target), "we should have enough now");
60+
61+
// Now we need to know if we need a change output to drain the excess if we overshot too much
62+
//
63+
// We don't need to know exactly which change output we're going to use yet but we assume it's a taproot output
64+
// that we'll use a keyspend to spend from.
65+
let drain_weights = DrainWeights::TR_KEYSPEND;
66+
// Our policy is to only add a change output if the value is over 1_000 sats
67+
let change_policy = ChangePolicy::min_value(drain_weights, 1_000);
68+
let change = coin_selector.drain(target, change_policy);
69+
if change.is_some() {
70+
println!("We need to add our change output to the transaction with {} value", change.value);
71+
} else {
72+
println!("Yay we don't need to add a change output");
73+
}
6474
```
6575

66-
## Change Policy
76+
## Automatic selection with Branch and Bound
6777

68-
A change policy determines whether the drain output(s) should be in the final solution. The
69-
determination is simple: if the excess value is above a threshold then the drain should be added. To
70-
construct a change policy you always provide `DrainWeights` which tell the coin selector the weight
71-
cost of adding the drain. `DrainWeights` includes two weights. One is the weight of the drain
72-
output(s). The other is the weight of spending the drain output later on (the input weight).
78+
You can use methods such as [`CoinSelector::select`] to manually select coins, or methods such as
79+
[`CoinSelector::select_until_target_met`] for a rudimentary automatic selection. Probably you want
80+
to use [`CoinSelector::run_bnb`] to do this in a smart way.
7381

82+
Built-in metrics are provided in the [`metrics`] submodule. Currently, only the
83+
[`LowestFee`](metrics::LowestFee) metric is considered stable. Note you *can* try and write your own
84+
metric by implementing the [`BnbMetric`] yourself but we don't recommend this.
7485

7586
```rust
7687
use std::str::FromStr;
77-
use bdk_coin_select::{CoinSelector, Candidate, DrainWeights, TXIN_BASE_WEIGHT, ChangePolicy, TR_KEYSPEND_TXIN_WEIGHT};
78-
use bitcoin::{Address, Network, Transaction, TxIn, TxOut};
79-
let base_tx = Transaction {
80-
input: vec![],
81-
output: vec![/* include your recipient outputs here */],
82-
lock_time: bitcoin::absolute::LockTime::from_height(0).unwrap(),
83-
version: 0x02,
84-
};
85-
let base_weight = base_tx.weight().to_wu() as u32;
86-
87-
// The change output that may or may not be included in the final transaction.
88-
let drain_addr =
89-
Address::from_str("tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46")
90-
.expect("address must be valid")
91-
.require_network(Network::Testnet)
92-
.expect("network must match");
93-
94-
// The drain output(s) may or may not be included in the final tx. We calculate
95-
// the drain weight to include the output length varint weight changes from
96-
// including the drain output(s).
97-
let drain_output_weight = {
98-
let mut tx_with_drain = base_tx.clone();
99-
tx_with_drain.output.push(TxOut {
100-
script_pubkey: drain_addr.script_pubkey(),
101-
..Default::default()
102-
});
103-
tx_with_drain.weight().to_wu() as u32 - base_weight
104-
};
105-
println!("drain output weight: {}", drain_output_weight);
106-
107-
let drain_weights = DrainWeights {
108-
output_weight: drain_output_weight,
109-
spend_weight: TR_KEYSPEND_TXIN_WEIGHT,
110-
};
111-
112-
// This constructs a change policy that creates change when the change value is
113-
// greater than or equal to the dust limit.
114-
let change_policy = ChangePolicy::min_value(
115-
drain_weights,
116-
drain_addr.script_pubkey().dust_value().to_sat(),
117-
);
118-
```
119-
120-
## Branch and Bound
88+
use bdk_coin_select::{ Candidate, CoinSelector, FeeRate, Target, TargetFee, TargetOutputs, ChangePolicy, TR_KEYSPEND_TXIN_WEIGHT, TR_DUST_RELAY_MIN_VALUE};
89+
use bdk_coin_select::metrics::LowestFee;
90+
use bitcoin::{ Address, Network, Transaction, TxIn, TxOut };
12191

122-
You can use methods such as [`CoinSelector::select`] to manually select coins, or methods such as
123-
[`CoinSelector::select_until_target_met`] for a rudimentary automatic selection. However, if you
124-
wish to automatically select coins to optimize for a given metric, [`CoinSelector::run_bnb`] can be
125-
used.
92+
let recipient_addr =
93+
Address::from_str("tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46").unwrap();
12694

127-
Built-in metrics are provided in the [`metrics`] submodule. Currently, only the
128-
[`LowestFee`](metrics::LowestFee) metric is considered stable.
95+
let outputs = vec![TxOut {
96+
value: 210_000,
97+
script_pubkey: recipient_addr.payload.script_pubkey(),
98+
}];
12999

130-
```rust
131-
use bdk_coin_select::{ Candidate, CoinSelector, FeeRate, Target, TargetFee, ChangePolicy, TR_KEYSPEND_TXIN_WEIGHT };
132-
use bdk_coin_select::metrics::LowestFee;
133100
let candidates = [
134101
Candidate {
135102
input_count: 1,
@@ -150,34 +117,35 @@ let candidates = [
150117
is_segwit: true
151118
}
152119
];
153-
let base_weight = 0;
154120
let drain_weights = bdk_coin_select::DrainWeights::default();
155-
let dust_limit = 0;
121+
// You could determine this by looking at the user's transaction history and taking an average of the feerate.
156122
let long_term_feerate = FeeRate::from_sat_per_vb(10.0);
157123

158-
let mut coin_selector = CoinSelector::new(&candidates, base_weight);
124+
let mut coin_selector = CoinSelector::new(&candidates);
159125

160126
let target = Target {
161127
fee: TargetFee::from_feerate(FeeRate::from_sat_per_vb(15.0)),
162-
value: 210_000,
128+
outputs: TargetOutputs::fund_outputs(outputs.iter().map(|output| (output.weight() as u32, output.value))),
163129
};
164130

131+
// The change output must be at least this size to be relayed.
132+
// To choose it you need to know the kind of script pubkey on your change txout.
133+
// Here we assume it's a taproot output
134+
let dust_limit = TR_DUST_RELAY_MIN_VALUE;
135+
165136
// We use a change policy that introduces a change output if doing so reduces
166-
// the "waste" and that the change output's value is at least that of the
167-
// `dust_limit`.
137+
// the "waste" (i.e. adding change doesn't increase the fees we'd pay if we factor in the cost to spend the output later on).
168138
let change_policy = ChangePolicy::min_value_and_waste(
169139
drain_weights,
170140
dust_limit,
171141
target.fee.rate,
172142
long_term_feerate,
173143
);
174144

175-
// This metric minimizes transaction fees paid over time. The
176-
// `long_term_feerate` is used to calculate the additional fee from spending
177-
// the change output in the future.
145+
// The LowestFee metric tries make selections that minimize your total fees paid over time.
178146
let metric = LowestFee {
179147
target,
180-
long_term_feerate,
148+
long_term_feerate, // used to calculate the cost of spending th change output if the future
181149
change_policy
182150
};
183151

@@ -203,79 +171,6 @@ match coin_selector.run_bnb(metric, 100_000) {
203171

204172
```
205173

206-
## Finalizing a Selection
207-
208-
- [`is_target_met`] checks whether the current state of [`CoinSelector`] meets the [`Target`].
209-
- [`apply_selection`] applies the selection to the original list of candidate `TxOut`s.
210-
211-
[`is_target_met`]: crate::CoinSelector::is_target_met
212-
[`apply_selection`]: crate::CoinSelector::apply_selection
213-
[`CoinSelector`]: crate::CoinSelector
214-
[`Target`]: crate::Target
215-
216-
```rust
217-
use bdk_coin_select::{CoinSelector, Candidate, DrainWeights, Target, ChangePolicy, TR_KEYSPEND_TXIN_WEIGHT, Drain};
218-
use bitcoin::{Amount, TxOut, Address};
219-
let base_weight = 0_u32;
220-
let drain_weights = DrainWeights::TR_KEYSPEND;
221-
use core::str::FromStr;
222-
223-
// A random target, as an example.
224-
let target = Target {
225-
value: 21_000,
226-
..Default::default()
227-
};
228-
// Am arbitary drain policy, for the example.
229-
let change_policy = ChangePolicy::min_value(drain_weights, 1337);
230-
231-
// This is a list of candidate txouts for coin selection. If a txout is picked,
232-
// our transaction's input will spend it.
233-
let candidate_txouts = vec![
234-
TxOut {
235-
value: 100_000,
236-
script_pubkey: Address::from_str("bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr").unwrap().payload.script_pubkey(),
237-
},
238-
TxOut {
239-
value: 150_000,
240-
script_pubkey: Address::from_str("bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh").unwrap().payload.script_pubkey(),
241-
},
242-
TxOut {
243-
value: 200_000,
244-
script_pubkey: Address::from_str("bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8").unwrap().payload.script_pubkey()
245-
}
246-
];
247-
// We transform the candidate txouts into something `CoinSelector` can
248-
// understand.
249-
let candidates = candidate_txouts
250-
.iter()
251-
.map(|txout| Candidate {
252-
input_count: 1,
253-
value: txout.value,
254-
weight: TR_KEYSPEND_TXIN_WEIGHT, // you need to figure out the weight of the txin somehow
255-
is_segwit: txout.script_pubkey.is_witness_program(),
256-
})
257-
.collect::<Vec<_>>();
258-
259-
let mut selector = CoinSelector::new(&candidates, base_weight);
260-
selector
261-
.select_until_target_met(target)
262-
.expect("we've got enough coins");
263-
264-
// Get a list of coins that are selected.
265-
let selected_coins = selector
266-
.apply_selection(&candidate_txouts)
267-
.collect::<Vec<_>>();
268-
assert_eq!(selected_coins.len(), 1);
269-
270-
// Determine whether we should add a change output.
271-
let drain = selector.drain(target, change_policy);
272-
273-
if drain.is_some() {
274-
// add our change output to the transaction
275-
let change_value = drain.value;
276-
}
277-
```
278-
279174
# Minimum Supported Rust Version (MSRV)
280175

281176
This library is compiles on rust v1.54 and above

src/change_policy.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ impl ChangePolicy {
3939
) -> Self {
4040
// The output waste of a changeless solution is the excess.
4141
let waste_with_change = drain_weights
42-
.waste(target_feerate, long_term_feerate)
42+
.waste(
43+
target_feerate,
44+
long_term_feerate,
45+
0, /* ignore varint cost for now */
46+
)
4347
.ceil() as u64;
4448

4549
Self {

0 commit comments

Comments
 (0)