Skip to content

Commit f147ebc

Browse files
authored
Merge pull request #14 from LLFourn/simplify-change-policy
Simplify change policy
2 parents 29b187f + dac0641 commit f147ebc

15 files changed

+310
-268
lines changed

.github/workflows/test.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
on:
2+
push:
3+
branches:
4+
- master
5+
pull_request:
6+
7+
8+
# Make sure CI fails on all warnings, including Clippy lints
9+
env:
10+
RUSTFLAGS: "-Dwarnings"
11+
RUSTDOCFLAGS: "-Dwarnings"
12+
13+
jobs:
14+
fmt:
15+
name: Rustfmt
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: dtolnay/rust-toolchain@stable
20+
with:
21+
components: rustfmt
22+
- run: cargo fmt --all -- --check
23+
24+
clippy_check:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v4
28+
- uses: dtolnay/rust-toolchain@stable
29+
with:
30+
components: clippy
31+
- uses: Swatinem/rust-cache@v2
32+
- run: cargo clippy --all-targets --all-features --tests
33+
34+
build-msrv:
35+
runs-on: ubuntu-latest
36+
steps:
37+
- uses: actions/checkout@v4
38+
- uses: dtolnay/[email protected]
39+
# don't want dev-dependencies for MSRV check
40+
- run: sed -i 's/\[dev-dependencies]/[ignore-this-warning-fren]/g' Cargo.toml
41+
- run: cargo build --release
42+
43+
test:
44+
runs-on: ubuntu-latest
45+
steps:
46+
- uses: actions/checkout@v4
47+
- uses: dtolnay/rust-toolchain@stable
48+
- run: cargo test --release
49+
50+
doc-build:
51+
name: doc-build
52+
runs-on: ubuntu-latest
53+
steps:
54+
- uses: actions/checkout@v4
55+
- uses: dtolnay/rust-toolchain@stable
56+
- run: cargo doc --no-deps

Cargo.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ readme = "README.md"
1515
[dependencies]
1616
# No dependencies! Please do not add any please!
1717

18-
[dev-dependencies]
19-
rand = "0.7"
20-
proptest = "0.10"
21-
bitcoin = "0.30"
2218

2319
[features]
2420
default = ["std"]
2521
std = []
22+
23+
[dev-dependencies]
24+
rand = "0.8"
25+
proptest = "1.4"
26+
bitcoin = "0.30"

README.md

Lines changed: 59 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
`bdk_coin_select` is a tool to help you select inputs for making Bitcoin (ticker: BTC) transactions.
44
It's got zero dependencies so you can paste it into your project without concern.
55

6+
> ⚠ This work is only ready to use by those who expect (potentially catastrophic) bugs and will have
7+
> the time to investigate them and contribute back to this crate.
8+
69
## Constructing the `CoinSelector`
710

811
The main structure is [`CoinSelector`](crate::CoinSelector). To construct it, we specify a list of
@@ -11,18 +14,12 @@ and mandatory inputs (if any).
1114

1215
```rust
1316
use std::str::FromStr;
14-
use bdk_coin_select::{ CoinSelector, Candidate, TR_KEYSPEND_SATISFACTION_WEIGHT, TXIN_BASE_WEIGHT };
17+
use bdk_coin_select::{ CoinSelector, Candidate, TR_KEYSPEND_TXIN_WEIGHT};
1518
use bitcoin::{ Address, Network, Transaction, TxIn, TxOut };
1619

17-
// You should use miniscript to figure out the satisfaction weight for your coins!
18-
const TR_INPUT_WEIGHT: u32 = TXIN_BASE_WEIGHT + TR_KEYSPEND_SATISFACTION_WEIGHT;
19-
2020
// The address where we want to send our coins.
2121
let recipient_addr =
22-
Address::from_str("tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46")
23-
.expect("address must be valid")
24-
.require_network(Network::Testnet)
25-
.expect("network must match");
22+
Address::from_str("tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46").unwrap();
2623

2724
let candidates = vec![
2825
Candidate {
@@ -32,7 +29,8 @@ let candidates = vec![
3229
// the value of the input
3330
value: 1_000_000,
3431
// the total weight of the input(s).
35-
weight: TR_INPUT_WEIGHT,
32+
// you may need to use miniscript to figure out the correct value here.
33+
weight: TR_KEYSPEND_TXIN_WEIGHT,
3634
// wether it's a segwit input. Needed so we know whether to include the
3735
// segwit header in total weight calculations.
3836
is_segwit: true
@@ -41,7 +39,7 @@ let candidates = vec![
4139
// A candidate can represent multiple inputs in the case where you
4240
// always want some inputs to be spent together.
4341
input_count: 2,
44-
weight: 2*TR_INPUT_WEIGHT,
42+
weight: 2*TR_KEYSPEND_TXIN_WEIGHT,
4543
value: 3_000_000,
4644
is_segwit: true
4745
}
@@ -52,7 +50,7 @@ let base_tx = Transaction {
5250
// include your recipient outputs here
5351
output: vec![TxOut {
5452
value: 900_000,
55-
script_pubkey: recipient_addr.script_pubkey(),
53+
script_pubkey: recipient_addr.payload.script_pubkey(),
5654
}],
5755
lock_time: bitcoin::absolute::LockTime::from_height(0).unwrap(),
5856
version: 0x02,
@@ -67,30 +65,25 @@ coin_selector.select(0);
6765

6866
## Change Policy
6967

70-
A change policy determines whether the drain output(s) should be in the final solution. A change
71-
policy is represented by a closure of signature `Fn(&CoinSelector, Target) -> Drain`. We provide 3
72-
built-in change policies; `min_value`, `min_waste` and `min_value_and_waste` (refer to the
73-
[module-level docs](crate::change_policy) for more).
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).
7473

75-
Typically, to construct a change policy, the [`DrainWeights`] need to be provided. `DrainWeights`
76-
includes two weights. One is the weight of the drain output(s). The other is the weight of spending
77-
the drain output later on (the input weight).
7874

7975
```rust
80-
# use std::str::FromStr;
81-
# use bdk_coin_select::{ CoinSelector, Candidate, DrainWeights, TXIN_BASE_WEIGHT };
82-
# use bitcoin::{ Address, Network, Transaction, TxIn, TxOut };
83-
use bdk_coin_select::change_policy::min_value;
84-
# const TR_SATISFACTION_WEIGHT: u32 = 66;
85-
# const TR_INPUT_WEIGHT: u32 = TXIN_BASE_WEIGHT + TR_SATISFACTION_WEIGHT;
86-
# let base_tx = Transaction {
87-
# input: vec![],
88-
# // include your recipient outputs here
89-
# output: vec![],
90-
# lock_time: bitcoin::absolute::LockTime::from_height(0).unwrap(),
91-
# version: 1,
92-
# };
93-
# let base_weight = base_tx.weight().to_wu() as u32;
76+
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+
const TR_SATISFACTION_WEIGHT: u32 = 66;
80+
let base_tx = Transaction {
81+
input: vec![],
82+
output: vec![/* include your recipient outputs here */],
83+
lock_time: bitcoin::absolute::LockTime::from_height(0).unwrap(),
84+
version: 0x02,
85+
};
86+
let base_weight = base_tx.weight().to_wu() as u32;
9487

9588
// The change output that may or may not be included in the final transaction.
9689
let drain_addr =
@@ -114,12 +107,12 @@ println!("drain output weight: {}", drain_output_weight);
114107

115108
let drain_weights = DrainWeights {
116109
output_weight: drain_output_weight,
117-
spend_weight: TR_INPUT_WEIGHT,
110+
spend_weight: TR_KEYSPEND_TXIN_WEIGHT,
118111
};
119112

120113
// This constructs a change policy that creates change when the change value is
121114
// greater than or equal to the dust limit.
122-
let change_policy = min_value(
115+
let change_policy = ChangePolicy::min_value(
123116
drain_weights,
124117
drain_addr.script_pubkey().dust_value().to_sat(),
125118
);
@@ -136,14 +129,13 @@ Built-in metrics are provided in the [`metrics`] submodule. Currently, only the
136129
[`LowestFee`](metrics::LowestFee) metric is considered stable.
137130

138131
```rust
139-
use bdk_coin_select::{ Candidate, CoinSelector, FeeRate, Target };
132+
use bdk_coin_select::{ Candidate, CoinSelector, FeeRate, Target, ChangePolicy };
140133
use bdk_coin_select::metrics::LowestFee;
141-
use bdk_coin_select::change_policy::min_value_and_waste;
142-
# let candidates = [];
143-
# let base_weight = 0;
144-
# let drain_weights = bdk_coin_select::DrainWeights::default();
145-
# let dust_limit = 0;
146-
# let long_term_feerate = FeeRate::default_min_relay_fee();
134+
let candidates = [];
135+
let base_weight = 0;
136+
let drain_weights = bdk_coin_select::DrainWeights::default();
137+
let dust_limit = 0;
138+
let long_term_feerate = FeeRate::default_min_relay_fee();
147139

148140
let mut coin_selector = CoinSelector::new(&candidates, base_weight);
149141

@@ -156,9 +148,10 @@ let target = Target {
156148
// We use a change policy that introduces a change output if doing so reduces
157149
// the "waste" and that the change output's value is at least that of the
158150
// `dust_limit`.
159-
let change_policy = min_value_and_waste(
151+
let change_policy = ChangePolicy::min_value_and_waste(
160152
drain_weights,
161153
dust_limit,
154+
target.feerate,
162155
long_term_feerate,
163156
);
164157

@@ -168,7 +161,7 @@ let change_policy = min_value_and_waste(
168161
let metric = LowestFee {
169162
target,
170163
long_term_feerate,
171-
change_policy: &change_policy,
164+
change_policy
172165
};
173166

174167
// We run the branch and bound algorithm with a max round limit of 100,000.
@@ -180,10 +173,10 @@ match coin_selector.run_bnb(metric, 100_000) {
180173
let selection = coin_selector
181174
.apply_selection(&candidates)
182175
.collect::<Vec<_>>();
183-
let change = change_policy(&coin_selector, target);
176+
let change = coin_selector.drain(target, change_policy);
184177

185178
println!("we selected {} inputs", selection.len());
186-
println!("are we including the change output? {}", change.is_some());
179+
println!("We are including a change output of {} value (0 means not change)", change.value);
187180
}
188181
};
189182
```
@@ -199,26 +192,34 @@ match coin_selector.run_bnb(metric, 100_000) {
199192
[`Target`]: crate::Target
200193

201194
```rust
202-
use bdk_coin_select::{ CoinSelector, Candidate, DrainWeights, Target };
203-
use bdk_coin_select::change_policy::min_value;
204-
use bitcoin::{ Amount, ScriptBuf, TxOut };
205-
# let base_weight = 0_u32;
206-
# let drain_weights = DrainWeights::new_tr_keyspend();
195+
use bdk_coin_select::{CoinSelector, Candidate, DrainWeights, Target, ChangePolicy, TR_KEYSPEND_TXIN_WEIGHT, Drain};
196+
use bitcoin::{Amount, TxOut, Address};
197+
let base_weight = 0_u32;
198+
let drain_weights = DrainWeights::new_tr_keyspend();
199+
use core::str::FromStr;
207200

208201
// A random target, as an example.
209202
let target = Target {
210203
value: 21_000,
211204
..Default::default()
212205
};
213-
// A random drain policy, as an example.
214-
let drain_policy = min_value(drain_weights, 0);
206+
// Am arbitary drain policy, for the example.
207+
let change_policy = ChangePolicy::min_value(drain_weights, 1337);
215208

216209
// This is a list of candidate txouts for coin selection. If a txout is picked,
217210
// our transaction's input will spend it.
218211
let candidate_txouts = vec![
219212
TxOut {
220213
value: 100_000,
221-
script_pubkey: ScriptBuf::new(),
214+
script_pubkey: Address::from_str("bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr").unwrap().payload.script_pubkey(),
215+
},
216+
TxOut {
217+
value: 150_000,
218+
script_pubkey: Address::from_str("bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh").unwrap().payload.script_pubkey(),
219+
},
220+
TxOut {
221+
value: 200_000,
222+
script_pubkey: Address::from_str("bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8").unwrap().payload.script_pubkey()
222223
}
223224
];
224225
// We transform the candidate txouts into something `CoinSelector` can
@@ -228,19 +229,19 @@ let candidates = candidate_txouts
228229
.map(|txout| Candidate {
229230
input_count: 1,
230231
value: txout.value,
231-
weight: txout.weight() as u32,
232+
weight: TR_KEYSPEND_TXIN_WEIGHT, // you need to figure out the weight of the txin somehow
232233
is_segwit: txout.script_pubkey.is_witness_program(),
233234
})
234235
.collect::<Vec<_>>();
235236

236237
let mut selector = CoinSelector::new(&candidates, base_weight);
237238
let _result = selector
238-
.select_until_target_met(target, drain_policy(&selector, target));
239+
.select_until_target_met(target, Drain::none());
239240

240241
// Determine what the drain output will be, based on our selection.
241-
let drain = drain_policy(&selector, target);
242+
let drain = selector.drain(target, change_policy);
242243

243-
// Check that selection is finished!
244+
// In theory the target must always still be met at this point
244245
assert!(selector.is_target_met(target, drain));
245246

246247
// Get a list of coins that are selected.
@@ -252,7 +253,7 @@ assert_eq!(selected_coins.len(), 1);
252253

253254
# Minimum Supported Rust Version (MSRV)
254255

255-
This library should compile with Rust 1.54.0.
256+
This library is tested to compile on 1.54
256257

257258
To build with the MSRV, you will need to pin the following dependencies:
258259

0 commit comments

Comments
 (0)