Giter VIP home page Giter VIP logo

coin-select's Introduction

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

coin-select's People

Contributors

darosior avatar evanlinjin avatar jp1ac4 avatar llfourn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

coin-select's Issues

ChangePolicy should be decided by metrics in branch and bound

Right now we pass in a change policy when creating the metric type:

#[derive(Clone, Copy)]
pub struct LowestFee {
    /// The target parameters for the resultant selection.
    pub target: Target,
    /// The estimated feerate needed to spend our change output later.
    pub long_term_feerate: FeeRate,
    /// Policy to determine the change output (if any) of a given selection.
    pub change_policy: ChangePolicy,
}

But what if we pass in a really wacky change_policy that doesn't try and lower your fees? This means a whole lot of complicated logic is needed within the bound function of the metric to try and still get the correct bound with a ChangePolicy that is working against the metric!

This would allow us to remove this entire if/else statement:

if let Some(drain_value) = drain_value {
// it's possible that adding another input might reduce your long term fee if it
// gets rid of an expensive change output. Our strategy is to take the lowest sat
// per value candidate we have and use it as a benchmark. We imagine it has the
// perfect value (but the same sats per weight unit) to get rid of the change output
// by adding negative effective value (i.e. perfectly reducing excess to the point
// where change wouldn't be added according to the policy).
//
// TODO: This metric could be tighter by being more complicated but this seems to be
// good enough for now.
let amount_above_change_threshold = drain_value - self.change_policy.min_value;
if let Some((_, low_sats_per_wu_candidate)) = cs.unselected().next_back() {
let ev = low_sats_per_wu_candidate.effective_value(self.target.fee.rate);
// we can only reduce excess if ev is negative
if ev < -0.0 {
let value_per_negative_effective_value =
low_sats_per_wu_candidate.value as f32 / ev.abs();
// this is how much abosolute value we have to add to cancel out the excess
let extra_value_needed_to_get_rid_of_change = amount_above_change_threshold
as f32
* value_per_negative_effective_value;
// NOTE: the drain_value goes to fees if we get rid of it so it's part of
// the cost of removing the change output
let cost_of_getting_rid_of_change =
extra_value_needed_to_get_rid_of_change + drain_value as f32;
let cost_of_change = self
.change_policy
.drain_weights
.waste(self.target.fee.rate, self.long_term_feerate);
let best_score_without_change = Ordf32(
current_score.0 + cost_of_getting_rid_of_change - cost_of_change,
);
if best_score_without_change < current_score {
return Some(best_score_without_change);
}
}
}
} else {
// Ok but maybe adding change could improve the metric?
let cost_of_adding_change = self
.change_policy
.drain_weights
.waste(self.target.fee.rate, self.long_term_feerate);
let cost_of_no_change = cs.excess(self.target, Drain::none());
let best_score_with_change =
Ordf32(current_score.0 - cost_of_no_change as f32 + cost_of_adding_change);
if best_score_with_change < current_score {
return Some(best_score_with_change);
}
}

Can anyone think of a reason you'd want a change policy that is not inline with the metric you are trying to optimize for?

Simplify `DrainWeights`/`Target` API

Original discussion: bitcoindevkit/bdk#1072 (comment)

@jp1ac4 @darosior I'm working on some breaking changes to the API.

After a discussion with @LLFourn, we've decided to include DrainWeights in Target and keep Target in the CoinSelector. This simplifies the API and gives us an easy pathway to take into account the output count varint weight changes when including the drain output.

How this simplifies the API:

  • The target & drain weights are included in the CoinSelector, so they don't need to be passed in as method inputs.
  • Change policy can be simplified to just: Fn(&CoinSelector) -> bool.

How we can take into account the output varint weight changes when including drain:

pub struct Target {
    /*other fields*/

    pub base_weight: u32,
    pub drain_weights: DrainWeights,
}

impl Target {
    pub fn new(
        feerate: FeeRate,
        recipient_outputs: impl IntoIterator<Item = (u32 /*weight*/, u64 /*value*/)>
        drain_outputs: impl IntoIterator<Item = DrainWeights>,
    ) -> Self {
        todo!()
    }
}

This calculates the base_weight and the drain_weight (which includes output varint changes) that are both included in Target.

Make `Changeless` metric more useful when combined.

@LLFourn mentioned in a private discussion:

... in theory the combined will be looser than each individual one in the sense that it will take more iterations to find the optimal solution for the combined one I think. (A,B) has to find an optimal solution for A but this will not necessarily be optimal for (A,B) so it has to keep looking.

I responded with:

Would you agree that combining another metric with Changeless is only useful if it tightens the bounds? Otherwise, we might as will do Iterator::filter on the BnbIter.

Relevant Context

Remove debug `println!`s

There is still a couple debugging statement in non-test code. For instance:

coin-select/src/bnb.rs

Lines 101 to 110 in f5c08c2

println!(
"\t\t(PUSH) branch={} inclusion={} lb={:?} score={:?}",
branch.selector,
!branch.is_exclusion,
branch.lower_bound,
self.metric.score(&branch.selector),
);
self.queue.push(branch);
} else {
println!(

Option to calculate fee based on rounded-up vbytes

When targeting a fee rate in sat/vb, could there be the option to calculate the final fee based on the rounded-up vbytes, i.e.
vb = (wu / 4).ceil().

Currently, when using the rounded-up vbytes to calculate the fee rate after using coin selection, we can end up with a transaction paying a slightly lower value than our target.

I think it might be enough to modify the method CoinSelector::implied_fee_from_feerate so that it can optionally round up the vbytes. It should be sufficient to do the rounding up at the end once the selection is complete, and not for intermediate calculations, e.g. when determining the cost of adding a single input, as we don't want to round up once for each input.

Perhaps this option could be set as part of TargetFee.

If you think this would be fine to add, I'd be happy to work on it. Thanks.

CPFP: Allow telling coin selector that a candidate has unconfirmed parents

I guess this is just a new field on Candidate which lets you say the combined weight and fee of all parents. I think the approach to use in practice in bdk is to roll all inputs that are in an unconfirmed tree into a single candidate. This will give you the correct answer but has the disadvantage of forcing you to spend them altogether. Needs some design work before trying to implement it. Looking at the actual CPFP related logic in core might provide some inspiration.

Rename `Drain` to `Change`

When I originally named change outputs "drain" outputs I thought I was being clever because sometimes you use these kinds of outputs to drain value to things that are not technically change addresses. However the conversation over here led me to think about what you would do if you wanted to drain a bunch of funds to an output e.g. sweep a wallet or something and my conclusion was that you would actually not use the Drain system but rather:

  1. Set an output whose value is above dust
  2. Select all the things somehow
  3. Set the value of the output to the excess

The main point being that "drain" system we have is really an optional output system to take excess otherwise known as change output or it is at least close enough it's unhelpful to name it something else.

`CoinSelector::run_bnb` should fallback to `select_until_target_met`?

@LLFourn commented:

When i was suggesting this method I was thinking that it would fall back to select_until_target_met if it can't find anything. This would mean you don't need this error type -- rather you can just have InsufficientFunds.

Do you think it's worth having run_bnb_with_fallback() that does that?

Also another sneaky request -- if #[cfg(feature = "std")] can you add a method that lets you pass in a std::time::Duration and checks the time passed every 1_000 rounds and stops after the time has been exceeded.

-- https://github.com/bitcoindevkit/bdk/pull/1072/files/d620dc61e809a8c280d623d5275abe8413b4b00a#r1393667785

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.