Skip to content

Commit e8cb15c

Browse files
committed
Merge #21: No more base weight
c650306 fix: `CoinSelector::implied_fee` (志宇) 4d0ae4c docs: fix typos (志宇) 2e26aa5 fix: rm `Drain::none` method (志宇) 8eb582b [lowest-fee] ignore failure when selection is not possible (LLFourn) 064996c [lowest-fee] Fix off by one in test (LLFourn) 956685a [changeless] Revert number of candidates (LLFourn) c6f0682 Remove typo in lowest_fee comment (LLFourn) 76e67a3 Move ChangePolicy to drain.rs (LLFourn) 7e82c3f Remove base_weight, put weight in Target (LLFourn) Pull request description: Fixes #1 On top of #19 - 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 the varint for the number of outputs. This wasn't what the issue had in mind but it was easier to take the `base_weight` out of `CoinSelector` and put it in `Target` rather than put `Target` in `CoinSelector`. Getting rid of `base_weight` is a more crucial change than expected because rust bitcoin changed what `Transaction::weight` returns for empty output transactions recently so using it to determine `base_weight` will get different answers between versions (this breaks our weight tests but this PR will fix it I think if we uprade dev deps). We only need to know the total weight of the outputs and how many there are now to get the right answers for weight. ACKs for top commit: evanlinjin: ACK c650306 Tree-SHA512: bd2d6bba15a172b56451d13a9560b6221a6f005417857e1cf7f0cd7022cebd0c2e4d6ff78dac9b99690771297351f90eb3fea26f85aa34a2841c91cfe0d73f9c
2 parents 798d7b6 + c650306 commit e8cb15c

19 files changed

+516
-1301
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: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1 @@
1-
//! This module contains a collection of change policies.
2-
//!
3-
//! A change policy determines whether a given coin selection (presented by [`CoinSelector`]) should
4-
//! construct a transaction with a change output. A change policy is represented as a function of
5-
//! type `Fn(&CoinSelector, Target) -> Drain`.
61

7-
#[allow(unused)] // some bug in <= 1.48.0 sees this as unused when it isn't
8-
use crate::float::FloatExt;
9-
use crate::{DrainWeights, FeeRate};
10-
11-
/// Describes when a change output (although it could represent several) should be added that drains
12-
/// the excess in the coin selection. It includes the `drain_weights` to account for the cost of
13-
/// adding this outupt(s).
14-
#[derive(Clone, Copy, Debug, PartialEq)]
15-
pub struct ChangePolicy {
16-
/// The minimum amount of excesss there needs to be add a change output.
17-
pub min_value: u64,
18-
/// The weights of the drain that would be added according to the policy.
19-
pub drain_weights: DrainWeights,
20-
}
21-
22-
impl ChangePolicy {
23-
/// Construct a change policy that creates change when the change value is greater than
24-
/// `min_value`.
25-
pub fn min_value(drain_weights: DrainWeights, min_value: u64) -> Self {
26-
Self {
27-
drain_weights,
28-
min_value,
29-
}
30-
}
31-
32-
/// Construct a change policy that creates change when it would reduce the transaction waste
33-
/// given that `min_value` is respected.
34-
pub fn min_value_and_waste(
35-
drain_weights: DrainWeights,
36-
min_value: u64,
37-
target_feerate: FeeRate,
38-
long_term_feerate: FeeRate,
39-
) -> Self {
40-
// The output waste of a changeless solution is the excess.
41-
let waste_with_change = drain_weights
42-
.waste(target_feerate, long_term_feerate)
43-
.ceil() as u64;
44-
45-
Self {
46-
drain_weights,
47-
min_value: waste_with_change.max(min_value),
48-
}
49-
}
50-
}

0 commit comments

Comments
 (0)