[wallet] Improve CoinSelectionAlgorithm

Implement the improvements described in issue #121.

Closes #121, closes #131.
This commit is contained in:
Alekos Filini
2020-10-14 15:21:22 +02:00
parent 17f7294c8e
commit a5713a8348
5 changed files with 185 additions and 120 deletions

View File

@@ -44,37 +44,40 @@
//! # use bitcoin::*;
//! # use bitcoin::consensus::serialize;
//! # use bdk::wallet::coin_selection::*;
//! # use bdk::database::Database;
//! # use bdk::*;
//! #[derive(Debug)]
//! struct AlwaysSpendEverything;
//!
//! impl CoinSelectionAlgorithm for AlwaysSpendEverything {
//! impl<D: Database> CoinSelectionAlgorithm<D> for AlwaysSpendEverything {
//! fn coin_select(
//! &self,
//! must_use_utxos: Vec<UTXO>,
//! may_use_utxos: Vec<UTXO>,
//! database: &D,
//! must_use_utxos: Vec<(UTXO, usize)>,
//! may_use_utxos: Vec<(UTXO, usize)>,
//! fee_rate: FeeRate,
//! amount_needed: u64,
//! input_witness_weight: usize,
//! fee_amount: f32,
//! ) -> Result<CoinSelectionResult, bdk::Error> {
//! let mut selected_amount = 0;
//! let mut additional_weight = 0;
//! let all_utxos_selected = must_use_utxos
//! .into_iter().chain(may_use_utxos)
//! .scan(&mut selected_amount, |selected_amount, utxo| {
//! .scan((&mut selected_amount, &mut additional_weight), |(selected_amount, additional_weight), (utxo, weight)| {
//! let txin = TxIn {
//! previous_output: utxo.outpoint,
//! ..Default::default()
//! };
//!
//! **selected_amount += utxo.txout.value;
//! **additional_weight += serialize(&txin).len() * 4 + weight;
//!
//! Some((
//! TxIn {
//! previous_output: utxo.outpoint,
//! ..Default::default()
//! },
//! txin,
//! utxo.txout.script_pubkey,
//! ))
//! })
//! .collect::<Vec<_>>();
//! let additional_weight = all_utxos_selected.iter().fold(0, |acc, (txin, _)| {
//! acc + serialize(txin).len() * 4 + input_witness_weight
//! });
//! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0;
//!
//! if (fee_amount + additional_fees).ceil() as u64 + amount_needed > selected_amount {
@@ -106,6 +109,7 @@
use bitcoin::consensus::encode::serialize;
use bitcoin::{Script, TxIn};
use crate::database::Database;
use crate::error::Error;
use crate::types::{FeeRate, UTXO};
@@ -130,22 +134,26 @@ pub struct CoinSelectionResult {
/// selection algorithm when it creates transactions.
///
/// For an example see [this module](crate::wallet::coin_selection)'s documentation.
pub trait CoinSelectionAlgorithm: std::fmt::Debug {
pub trait CoinSelectionAlgorithm<D: Database>: std::fmt::Debug {
/// Perform the coin selection
///
/// - `must_use_utxos`: the utxos that must be spent regardless of `amount_needed`
/// - `may_be_spent`: the utxos that may be spent to satisfy `amount_needed`
/// - `database`: a reference to the wallet's database that can be used to lookup additional
/// details for a specific UTXO
/// - `must_use_utxos`: the utxos that must be spent regardless of `amount_needed` with their
/// weight cost
/// - `may_be_spent`: the utxos that may be spent to satisfy `amount_needed` with their weight
/// cost
/// - `fee_rate`: fee rate to use
/// - `amount_needed`: the amount in satoshi to select
/// - `input_witness_weight`: the weight of an input's witness to keep into account for the fees
/// - `fee_amount`: the amount of fees in satoshi already accumulated from adding outputs
/// - `fee_amount`: the amount of fees in satoshi already accumulated from adding outputs and
/// the transaction's header
fn coin_select(
&self,
must_use_utxos: Vec<UTXO>,
may_use_utxos: Vec<UTXO>,
database: &D,
must_use_utxos: Vec<(UTXO, usize)>,
may_use_utxos: Vec<(UTXO, usize)>,
fee_rate: FeeRate,
amount_needed: u64,
input_witness_weight: usize,
fee_amount: f32,
) -> Result<CoinSelectionResult, Error>;
}
@@ -157,32 +165,33 @@ pub trait CoinSelectionAlgorithm: std::fmt::Debug {
#[derive(Debug, Default)]
pub struct DumbCoinSelection;
impl CoinSelectionAlgorithm for DumbCoinSelection {
impl<D: Database> CoinSelectionAlgorithm<D> for DumbCoinSelection {
fn coin_select(
&self,
must_use_utxos: Vec<UTXO>,
mut may_use_utxos: Vec<UTXO>,
_database: &D,
must_use_utxos: Vec<(UTXO, usize)>,
mut may_use_utxos: Vec<(UTXO, usize)>,
fee_rate: FeeRate,
outgoing_amount: u64,
input_witness_weight: usize,
amount_needed: u64,
mut fee_amount: f32,
) -> Result<CoinSelectionResult, Error> {
let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0;
log::debug!(
"outgoing_amount = `{}`, fee_amount = `{}`, fee_rate = `{:?}`",
outgoing_amount,
"amount_needed = `{}`, fee_amount = `{}`, fee_rate = `{:?}`",
amount_needed,
fee_amount,
fee_rate
);
// We put the "must_use" UTXOs first and make sure the "may_use" are sorted largest to smallest
// We put the "must_use" UTXOs first and make sure the "may_use" are sorted, initially
// smallest to largest, before being reversed with `.rev()`.
let utxos = {
may_use_utxos.sort_by(|a, b| b.txout.value.partial_cmp(&a.txout.value).unwrap());
may_use_utxos.sort_unstable_by_key(|(utxo, _)| utxo.txout.value);
must_use_utxos
.into_iter()
.map(|utxo| (true, utxo))
.chain(may_use_utxos.into_iter().map(|utxo| (false, utxo)))
.chain(may_use_utxos.into_iter().rev().map(|utxo| (false, utxo)))
};
// Keep including inputs until we've got enough.
@@ -191,9 +200,8 @@ impl CoinSelectionAlgorithm for DumbCoinSelection {
let txin = utxos
.scan(
(&mut selected_amount, &mut fee_amount),
|(selected_amount, fee_amount), (must_use, utxo)| {
if must_use || **selected_amount < outgoing_amount + (fee_amount.ceil() as u64)
{
|(selected_amount, fee_amount), (must_use, (utxo, weight))| {
if must_use || **selected_amount < amount_needed + (fee_amount.ceil() as u64) {
let new_in = TxIn {
previous_output: utxo.outpoint,
script_sig: Script::default(),
@@ -201,8 +209,7 @@ impl CoinSelectionAlgorithm for DumbCoinSelection {
witness: vec![],
};
**fee_amount +=
calc_fee_bytes(serialize(&new_in).len() * 4 + input_witness_weight);
**fee_amount += calc_fee_bytes(serialize(&new_in).len() * 4 + weight);
**selected_amount += utxo.txout.value;
log::debug!(
@@ -219,7 +226,7 @@ impl CoinSelectionAlgorithm for DumbCoinSelection {
)
.collect::<Vec<_>>();
if selected_amount < outgoing_amount + (fee_amount.ceil() as u64) {
if selected_amount < amount_needed + (fee_amount.ceil() as u64) {
return Err(Error::InsufficientFunds);
}
@@ -238,48 +245,56 @@ mod test {
use bitcoin::{OutPoint, Script, TxOut};
use super::*;
use crate::database::MemoryDatabase;
use crate::types::*;
const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2;
fn get_test_utxos() -> Vec<UTXO> {
fn get_test_utxos() -> Vec<(UTXO, usize)> {
vec![
UTXO {
outpoint: OutPoint::from_str(
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
)
.unwrap(),
txout: TxOut {
value: 100_000,
script_pubkey: Script::new(),
(
UTXO {
outpoint: OutPoint::from_str(
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
)
.unwrap(),
txout: TxOut {
value: 100_000,
script_pubkey: Script::new(),
},
is_internal: false,
},
is_internal: false,
},
UTXO {
outpoint: OutPoint::from_str(
"65d92ddff6b6dc72c89624a6491997714b90f6004f928d875bc0fd53f264fa85:0",
)
.unwrap(),
txout: TxOut {
value: 200_000,
script_pubkey: Script::new(),
P2WPKH_WITNESS_SIZE,
),
(
UTXO {
outpoint: OutPoint::from_str(
"65d92ddff6b6dc72c89624a6491997714b90f6004f928d875bc0fd53f264fa85:0",
)
.unwrap(),
txout: TxOut {
value: 200_000,
script_pubkey: Script::new(),
},
is_internal: true,
},
is_internal: true,
},
P2WPKH_WITNESS_SIZE,
),
]
}
#[test]
fn test_dumb_coin_selection_success() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let result = DumbCoinSelection
let result = DumbCoinSelection::default()
.coin_select(
vec![],
&database,
utxos,
vec![],
FeeRate::from_sat_per_vb(1.0),
250_000,
P2WPKH_WITNESS_SIZE,
50.0,
)
.unwrap();
@@ -292,14 +307,15 @@ mod test {
#[test]
fn test_dumb_coin_selection_use_all() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let result = DumbCoinSelection
let result = DumbCoinSelection::default()
.coin_select(
&database,
utxos,
vec![],
FeeRate::from_sat_per_vb(1.0),
20_000,
P2WPKH_WITNESS_SIZE,
50.0,
)
.unwrap();
@@ -312,14 +328,15 @@ mod test {
#[test]
fn test_dumb_coin_selection_use_only_necessary() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
let result = DumbCoinSelection
let result = DumbCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
20_000,
P2WPKH_WITNESS_SIZE,
50.0,
)
.unwrap();
@@ -333,14 +350,15 @@ mod test {
#[should_panic(expected = "InsufficientFunds")]
fn test_dumb_coin_selection_insufficient_funds() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
DumbCoinSelection
DumbCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1.0),
500_000,
P2WPKH_WITNESS_SIZE,
50.0,
)
.unwrap();
@@ -350,14 +368,15 @@ mod test {
#[should_panic(expected = "InsufficientFunds")]
fn test_dumb_coin_selection_insufficient_funds_high_fees() {
let utxos = get_test_utxos();
let database = MemoryDatabase::default();
DumbCoinSelection
DumbCoinSelection::default()
.coin_select(
&database,
vec![],
utxos,
FeeRate::from_sat_per_vb(1000.0),
250_000,
P2WPKH_WITNESS_SIZE,
50.0,
)
.unwrap();