Crate bdk_coin_select
source ·Expand description
§BDK Coin Selection
bdk_coin_select
is a zero-dependency tool to help you select inputs for making Bitcoin (ticker: BTC) transactions.
⚠ 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
use std::str::FromStr;
use bdk_coin_select::{ CoinSelector, Candidate, TR_KEYSPEND_TXIN_WEIGHT, Drain, FeeRate, Target, ChangePolicy, TargetOutputs, TargetFee, DrainWeights};
use bitcoin::{ Address, Network, Transaction, TxIn, TxOut };
let recipient_addr =
Address::from_str("tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46").unwrap();
let outputs = vec![TxOut {
value: 3_500_000,
script_pubkey: recipient_addr.payload.script_pubkey(),
}];
let target = Target {
outputs: TargetOutputs::fund_outputs(outputs.iter().map(|output| (output.weight() as u32, output.value))),
fee: TargetFee::from_feerate(FeeRate::from_sat_per_vb(42.0))
};
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,
// 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,
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);
coin_selector.select(0);
assert!(!coin_selector.is_target_met(target), "we didn't select enough");
println!("we didn't select enough yet we're missing: {}", coin_selector.missing(target));
coin_selector.select(1);
assert!(coin_selector.is_target_met(target), "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
//
// We don't need to know exactly which change output we're going to use yet but we assume it's a taproot output
// that we'll use a keyspend to spend from.
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);
if change.is_some() {
println!("We need to add our change output to the transaction with {} value", change.value);
} else {
println!("Yay we don't need to add a change output");
}
§Automatic selection with Branch and Bound
You can use methods such as CoinSelector::select
to manually select coins, or methods such as
CoinSelector::select_until_target_met
for a rudimentary automatic selection. Probably you want
to use CoinSelector::run_bnb
to do this in a smart way.
Built-in metrics are provided in the metrics
submodule. Currently, only the
LowestFee
metric is considered stable. Note you can try and write your own
metric by implementing the BnbMetric
yourself but we don’t recommend this.
use std::str::FromStr;
use bdk_coin_select::{ Candidate, CoinSelector, FeeRate, Target, TargetFee, TargetOutputs, ChangePolicy, TR_KEYSPEND_TXIN_WEIGHT, TR_DUST_RELAY_MIN_VALUE};
use bdk_coin_select::metrics::LowestFee;
use bitcoin::{ Address, Network, Transaction, TxIn, TxOut };
let recipient_addr =
Address::from_str("tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46").unwrap();
let outputs = vec![TxOut {
value: 210_000,
script_pubkey: recipient_addr.payload.script_pubkey(),
}];
let candidates = [
Candidate {
input_count: 1,
value: 400_000,
weight: TR_KEYSPEND_TXIN_WEIGHT,
is_segwit: true
},
Candidate {
input_count: 1,
value: 200_000,
weight: TR_KEYSPEND_TXIN_WEIGHT,
is_segwit: true
},
Candidate {
input_count: 1,
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 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() as u32, output.value))),
};
// The change output must be at least this size to be relayed.
// To choose it you need to know the kind of script pubkey on your change txout.
// Here we assume it's a taproot output
let dust_limit = TR_DUST_RELAY_MIN_VALUE;
// We use a change policy that introduces a change output if doing so reduces
// 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).
let change_policy = ChangePolicy::min_value_and_waste(
drain_weights,
dust_limit,
target.fee.rate,
long_term_feerate,
);
// The LowestFee metric tries make selections that minimize your total fees paid over time.
let metric = LowestFee {
target,
long_term_feerate, // used to calculate the cost of spending th change output if the future
change_policy
};
// We run the branch and bound algorithm with a max round limit of 100,000.
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!");
}
Ok(score) => {
println!("we found a solution with score {}", score);
let selection = coin_selector
.apply_selection(&candidates)
.collect::<Vec<_>>();
let change = coin_selector.drain(target, change_policy);
println!("we selected {} inputs", selection.len());
println!("We are including a change output of {} value (0 means not change)", change.value);
}
};
§Minimum Supported Rust Version (MSRV)
This library is compiles on rust v1.54 and above
Modules§
- Newtypes around
f32
andf64
that implementOrd
. - Branch and bound metrics that can be passed to
CoinSelector::bnb_solutions
orCoinSelector::run_bnb
.
Structs§
- A
Candidate
represents an input candidate forCoinSelector
. - Describes when a change output (although it could represent several) should be added that drains the excess in the coin selection. It includes the
drain_weights
to account for the cost of adding this outupt(s). CoinSelector
selects/deselects coins from a set of canididate coins.- A drain (A.K.A. change) output. Technically it could represent multiple outputs.
- Represents the weight costs of a drain (a.k.a. change) output.
- Fee rate
- Error type that occurs when the target amount cannot be met.
- Error type for when a solution cannot be found by branch-and-bound.
- The weight transaction(s) that this new transaction is replacing including the feerate.
- The
SelectIter
allows you to select candidates by callingIterator::next
. - A target value to select for along with feerate constraints.
- The fee constraints of a coin selection.
- Information about the outputs we’re trying to fund. Note the fields are total values since we don’t care about the weights or the values of individual outputs for the purposes of coin selection.
Constants§
- The minimum value a taproot output can have to be relayed with Bitcoin core’s default dust relay fee
- The weight of a taproot keyspend witness
- The weight of a taproot TxIn with witness
- The weight of a segwit
v1
(taproot) script pubkey in an output. This does not include the weight of theTxOut
itself or the script pubkey length field. - Txin “base” fields include
outpoint
(32+4) andnSequence
(4) and 1 byte for the scriptSig length. - The weight of a TXOUT with a zero length
scriptPubKey
- The weight of the
nVersion
andnLockTime
transaction fields
Traits§
- A branch and bound metric where we minimize the
Ordf32
score.