Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# Unreleased

- **Breaking:** `BnbMetric`'s `score`, `bound`, and `drain` take the `target: Target` as a parameter, and `CoinSelector::run_bnb`/`bnb_solutions` gain a leading `target` argument. Consequently `LowestFee` and `Changeless` no longer store a `target` field. This removes the target that `Changeless<M>` previously had to keep in sync with its inner metric, and aligns the metric API with the rest of `CoinSelector`, where `target` is always passed in.
- **Breaking:** Replace `Candidate`'s `input_count` and `is_segwit` fields with `segwit_count` and `legacy_count`, fixing `CoinSelector::input_weight` undercounting candidates that group multiple inputs: in a segwit transaction every legacy input still serializes an empty witness (1 WU), which was previously paid once per candidate instead of once per legacy input, so a group of N legacy inputs came out N-1 WU short. Splitting the count by script type also means a single candidate may now mix legacy and segwit inputs and still be priced exactly. Replaces `Candidate::new` with `Candidate::new_segwit` and `Candidate::new_legacy`.
- **Breaking:** Add `SelectionProblem`, which owns the target, candidates, and optional unconfirmed-ancestor data for a selection run. `CoinSelector::new` now takes `&SelectionProblem`, and the selector borrows it for its lifetime.
- Charge selections for the fee needed to bring the union of their unconfirmed ancestors up to the target feerate. Build ancestor-aware problems from `Input`/`InputGroup` and `AncestorToBump` with `SelectionProblem::new`; use `SelectionProblem::new_no_ancestors` for prebuilt candidates that need no CPFP bump.
- Add `SelectionView`, a cached view obtained with `CoinSelector::compute_view`. `BnbMetric::{score, bound, drain}` now consume `&SelectionView`; branch and bound maintains its aggregates incrementally while the underlying selector remains unchanged.
- Add a per-branch cursor to avoid repeatedly scanning already-decided candidates during branch-and-bound search.
- **Breaking:** `BnbMetric` metrics now decide the change output themselves. The trait gains a `drain(&mut self, cs) -> Drain` method; call it on a branch-and-bound solution (or the `LowestFee` metric directly) to get the change output the metric optimized against, instead of computing a separate `ChangePolicy`.
- **Breaking:** `CoinSelector::run_bnb` now returns `(Ordf32, Drain)` instead of just `Ordf32`, handing back the change output the metric decided on for the winning selection.
- **Breaking:** `LowestFee` no longer takes a `change_policy`. It now takes `dust_relay_feerate: FeeRate` and `drain_weights: DrainWeights`, and adds change only when doing so lowers the long-term fee and the change would not be dust.
- **Breaking:** `LowestFee` no longer takes a `change_policy`. It now takes `dust_relay_feerate: FeeRate` and `drain_weights: DrainWeights`, and adds change only when doing so lowers the long-term fee, the value is at least the dust threshold, and the transaction with change fits `Target::max_weight`.
- Add `DrainWeights::dust_threshold(dust_relay_feerate)`, the minimum value a change output with these weights must have to not be dust.
- Add `CoinSelector::select_srd`, a Single Random Draw selector (port of Bitcoin Core's `SelectCoinsSRD`) that adds candidates in random order until the change reaches `change_lower`, producing a healthy-sized (privacy-friendly) change output instead of minimizing fees. Adds the `CHANGE_LOWER` constant for Core's value.
- **Breaking:** `Changeless` is now `Changeless<M>`, wrapping an inner metric it constrains to changeless solutions (e.g. `Changeless<LowestFee>`), replacing the previous tuple-composition approach.
- **Breaking:** Removed the `BnbMetric` tuple implementations (`impl BnbMetric for ((A, f32), ...)`). Weighted composition of independent metrics is no longer supported; the only composition still provided is the changeless constraint, now expressed as `Changeless<M>`. If you relied on tuples to blend multiple objectives, there is no drop-in replacement.
- Add `LowestFeeChangeless`, a dedicated lowest-fee changeless metric. It applies its changeless-specific bound to pools of at most 24 candidates and retains `LowestFee`'s search bound and ordering for larger pools to avoid starving useful branches under finite round limits.
- **Breaking:** Remove `Changeless` and the `BnbMetric` tuple implementations (`impl BnbMetric for ((A, f32), ...)`). Generic metric composition is no longer supported. Use `LowestFeeChangeless` when a changeless transaction is required; otherwise use `LowestFee`.
- **Breaking:** `CoinSelector::selected_indices` and `CoinSelector::banned` now return `&Bitset` instead of `&BTreeSet<usize>`. `Bitset` exposes `contains`/`len`/`is_empty`/`iter` (#46)
- Replace the internal `Cow<BTreeSet>`/`Cow<[usize]>` selection state with a `Bitset` and an `Arc`-shared candidate order, making the per-branch clones in branch-and-bound substantially cheaper (#46)
- Fix compilation error when building with `--no-default-features` (#36)
Expand All @@ -27,4 +31,3 @@
- No more `base_weight` in `CoinSelector`. Weight of the outputs is tracked in `target`.
- You now account for the number of outputs in both drain and target and their weight.
- Removed waste metric because it was pretty broken and took a lot to maintain

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ criterion = "0.5"
[[bench]]
name = "coin_selector"
harness = false

# Enable debug symbols so profilers (perf, samply, flamegraph) can resolve
# function names. No runtime cost.
[profile.bench]
debug = true
111 changes: 80 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
> ⚠ This work is only ready to use by those who expect (potentially catastrophic) bugs and will have
> the time to investigate them and contribute back to this crate.

## Synopis
## Synopsis

```rust
use std::str::FromStr;
use bdk_coin_select::{ CoinSelector, Candidate, TR_KEYSPEND_TXIN_WEIGHT, Drain, FeeRate, Target, ChangePolicy, TargetOutputs, TargetFee, DrainWeights};
use bdk_coin_select::{ CoinSelector, Candidate, SelectionProblem, TR_KEYSPEND_TXIN_WEIGHT, Drain, FeeRate, Target, ChangePolicy, TargetOutputs, TargetFee, DrainWeights};
use bitcoin::{ Amount, Address, Network, Transaction, TxIn, TxOut };

let recipient_addr: Address = "tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46"
Expand All @@ -33,34 +33,34 @@ let candidates = vec![
Candidate {
// How many inputs does this candidate represents. Needed so we can
// figure out the weight of the varint that encodes the number of inputs
input_count: 1,
// and whether segwit transaction fields need to be counted in.
segwit_count: 1,
legacy_count: 0,
// the value of the input
value: 1_000_000,
// the total weight of the input(s) including their witness/scriptSig
// you may need to use miniscript to figure out the correct value here.
weight: TR_KEYSPEND_TXIN_WEIGHT,
// wether it's a segwit input. Needed so we know whether to include the
// segwit header in total weight calculations.
is_segwit: true
},
Candidate {
// A candidate can represent multiple inputs in the case where you
// always want some inputs to be spent together.
input_count: 2,
segwit_count: 2,
legacy_count: 0,
weight: 2*TR_KEYSPEND_TXIN_WEIGHT,
value: 3_000_000,
is_segwit: true
}
];

// You can now select coins!
let mut coin_selector = CoinSelector::new(&candidates);
let problem = SelectionProblem::new_no_ancestors(target, candidates);
let mut coin_selector = CoinSelector::new(&problem);
coin_selector.select(0);

assert!(!coin_selector.is_funded(target), "we didn't select enough");
println!("we didn't select enough yet we're missing: {}", coin_selector.missing(target));
assert!(!coin_selector.is_funded(), "we didn't select enough");
println!("we didn't select enough yet we're missing: {}", coin_selector.missing());
coin_selector.select(1);
assert!(coin_selector.is_funded(target), "we should have enough now");
assert!(coin_selector.is_funded(), "we should have enough now");

// Now we need to know if we need a change output to drain the excess if we overshot too much
//
Expand All @@ -69,7 +69,7 @@ assert!(coin_selector.is_funded(target), "we should have enough now");
let drain_weights = DrainWeights::TR_KEYSPEND;
// Our policy is to only add a change output if the value is over 1_000 sats
let change_policy = ChangePolicy::min_value(drain_weights, 1_000);
let change = coin_selector.drain(target, change_policy);
let change = coin_selector.drain(change_policy);
if change.is_some() {
println!("We need to add our change output to the transaction with {} value", change.value);
} else {
Expand All @@ -89,7 +89,7 @@ metric by implementing the [`BnbMetric`] yourself but we don't recommend this.

```rust
use std::str::FromStr;
use bdk_coin_select::{ BnbMetric, Candidate, CoinSelector, FeeRate, Target, TargetFee, TargetOutputs, TR_KEYSPEND_TXIN_WEIGHT};
use bdk_coin_select::{ BnbMetric, Candidate, CoinSelector, FeeRate, SelectionProblem, Target, TargetFee, TargetOutputs, TR_KEYSPEND_TXIN_WEIGHT};
use bdk_coin_select::metrics::LowestFee;
use bitcoin::{ Address, Amount, Network, Transaction, TxIn, TxOut };

Expand All @@ -105,43 +105,49 @@ let outputs = vec![TxOut {

let candidates = [
Candidate {
input_count: 1,
segwit_count: 1,
legacy_count: 0,
value: 400_000,
weight: TR_KEYSPEND_TXIN_WEIGHT,
is_segwit: true
},
Candidate {
input_count: 1,
segwit_count: 1,
legacy_count: 0,
value: 200_000,
weight: TR_KEYSPEND_TXIN_WEIGHT,
is_segwit: true
},
Candidate {
input_count: 1,
segwit_count: 1,
legacy_count: 0,
value: 11_000,
weight: TR_KEYSPEND_TXIN_WEIGHT,
is_segwit: true
}
];
let drain_weights = bdk_coin_select::DrainWeights::default();
// You could determine this by looking at the user's transaction history and taking an average of the feerate.
let drain_weights = bdk_coin_select::DrainWeights::TR_KEYSPEND;
// A wallet-policy or fee-estimator assumption for the future spend of change.
let long_term_feerate = FeeRate::from_sat_per_vb(10.0);

let mut coin_selector = CoinSelector::new(&candidates);

let target = Target {
fee: TargetFee::from_feerate(FeeRate::from_sat_per_vb(15.0)),
outputs: TargetOutputs::fund_outputs(outputs.iter().map(|output| (output.weight().to_wu(), output.value.to_sat()))),
max_weight: None,
};

let problem = SelectionProblem::new_no_ancestors(target, candidates);
let mut coin_selector = CoinSelector::new(&problem);

// For repeated read-only calculations, compute a cached view of the current selection.
let empty_view = coin_selector.compute_view();
assert_eq!(empty_view.selected_value(), 0);

// The feerate used to work out whether a change output would be dust (and so shouldn't be added).
// The standard dust relay feerate is 3 sat/vb.
let dust_relay_feerate = FeeRate::from_sat_per_vb(3.0);

// The LowestFee metric tries to make selections that minimize your total fees paid over time. It
// decides for itself whether to add a change output: change is added whenever doing so reduces the
// long-term fee (factoring in the cost to spend the output later on) and the change wouldn't be dust.
// long-term fee (factoring in the cost to spend the output later on), the value is at least the
// dust threshold, and the transaction with change fits its weight cap.
let mut metric = LowestFee {
long_term_feerate, // used to calculate the cost of spending the change output in the future
dust_relay_feerate,
Expand All @@ -150,13 +156,13 @@ let mut metric = LowestFee {

// We run the branch and bound algorithm with a max round limit of 100,000.
// On success it returns the score along with the change output the metric decided on.
let change = match coin_selector.run_bnb(target, metric, 100_000) {
let change = match coin_selector.run_bnb(metric, 100_000) {
Err(err) => {
println!("failed to find a solution: {}", err);
// fall back to naive selection
coin_selector.select_until_target_met(target).expect("a selection was impossible!");
coin_selector.select_until_target_met().expect("a selection was impossible!");
// the metric still decides the change output for whatever we end up selecting
metric.drain(&coin_selector, target)
metric.drain(&coin_selector.compute_view())
}
Ok((score, change)) => {
println!("we found a solution with score {}", score);
Expand All @@ -170,12 +176,55 @@ let selection = coin_selector
.collect::<Vec<_>>();

println!("we selected {} inputs", selection.len());
println!("We are including a change output of {} value (0 means not change)", change.value);
println!("We are including a change output of {} value (0 means no change)", change.value);


```

# Minimum Supported Rust Version (MSRV)
## Unconfirmed ancestors

Use `SelectionProblem::new` when spending unconfirmed UTXOs. Supply every unconfirmed transaction
that created an input and all of its transitive unconfirmed ancestors; missing transaction ids are
treated as confirmed and can make the required CPFP fee too low. Parent lists contain direct parents
only. Ancestors shared by several selected inputs are charged once over their union.

```rust
use bdk_coin_select::{
AncestorToBump, FeeRate, Input, SelectionProblem, Target, TargetFee, TargetOutputs,
};

let target = Target {
fee: TargetFee::from_feerate(FeeRate::from_sat_per_vb(5.0)),
outputs: TargetOutputs::fund_outputs([(136, 50_000)]),
max_weight: None,
};
let inputs = [Input {
value: 100_000,
weight: 272,
is_segwit: true,
residing_txid: "child",
}];
let ancestors = [
AncestorToBump {
txid: "parent",
weight: 400,
fee: 100,
parents: vec![],
},
AncestorToBump {
txid: "child",
weight: 600,
fee: 200,
parents: vec!["parent"],
},
];
let problem = SelectionProblem::new(target, inputs, ancestors);
let mut coin_selector = problem.selector();
```

Adding an input may drag in more fee debt than value, so funding is not necessarily monotone for
ancestor-aware problems. `run_bnb` accounts for this and de-duplicates shared ancestors.

This library is compiles on rust v1.54 and above
## Minimum Supported Rust Version (MSRV)

This library compiles on Rust 1.54 and above.
Loading
Loading