[wallet] Make coin_select take may/must use utxo lists

so that in the future you can add a UTXO that you *must* spend and let
the coin selection fill in the rest.

This partially addresses #121
This commit is contained in:
LLFourn 2020-10-14 14:03:12 +11:00 committed by Alekos Filini
parent 64b4cfe308
commit 17f7294c8e
No known key found for this signature in database
GPG Key ID: 5E8AFC3034FDFA4F
2 changed files with 77 additions and 50 deletions

View File

@ -51,24 +51,25 @@
//! impl CoinSelectionAlgorithm for AlwaysSpendEverything { //! impl CoinSelectionAlgorithm for AlwaysSpendEverything {
//! fn coin_select( //! fn coin_select(
//! &self, //! &self,
//! utxos: Vec<UTXO>, //! must_use_utxos: Vec<UTXO>,
//! _use_all_utxos: bool, //! may_use_utxos: Vec<UTXO>,
//! fee_rate: FeeRate, //! fee_rate: FeeRate,
//! amount_needed: u64, //! amount_needed: u64,
//! input_witness_weight: usize, //! input_witness_weight: usize,
//! fee_amount: f32, //! fee_amount: f32,
//! ) -> Result<CoinSelectionResult, bdk::Error> { //! ) -> Result<CoinSelectionResult, bdk::Error> {
//! let selected_amount = utxos.iter().fold(0, |acc, utxo| acc + utxo.txout.value); //! let mut selected_amount = 0;
//! let all_utxos_selected = utxos //! let all_utxos_selected = must_use_utxos
//! .into_iter() //! .into_iter().chain(may_use_utxos)
//! .map(|utxo| { //! .scan(&mut selected_amount, |selected_amount, utxo| {
//! ( //! **selected_amount += utxo.txout.value;
//! Some((
//! TxIn { //! TxIn {
//! previous_output: utxo.outpoint, //! previous_output: utxo.outpoint,
//! ..Default::default() //! ..Default::default()
//! }, //! },
//! utxo.txout.script_pubkey, //! utxo.txout.script_pubkey,
//! ) //! ))
//! }) //! })
//! .collect::<Vec<_>>(); //! .collect::<Vec<_>>();
//! let additional_weight = all_utxos_selected.iter().fold(0, |acc, (txin, _)| { //! let additional_weight = all_utxos_selected.iter().fold(0, |acc, (txin, _)| {
@ -132,16 +133,16 @@ pub struct CoinSelectionResult {
pub trait CoinSelectionAlgorithm: std::fmt::Debug { pub trait CoinSelectionAlgorithm: std::fmt::Debug {
/// Perform the coin selection /// Perform the coin selection
/// ///
/// - `utxos`: the list of spendable UTXOs /// - `must_use_utxos`: the utxos that must be spent regardless of `amount_needed`
/// - `use_all_utxos`: if true all utxos should be spent unconditionally /// - `may_be_spent`: the utxos that may be spent to satisfy `amount_needed`
/// - `fee_rate`: fee rate to use /// - `fee_rate`: fee rate to use
/// - `amount_needed`: the amount in satoshi to select /// - `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 /// - `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
fn coin_select( fn coin_select(
&self, &self,
utxos: Vec<UTXO>, must_use_utxos: Vec<UTXO>,
use_all_utxos: bool, may_use_utxos: Vec<UTXO>,
fee_rate: FeeRate, fee_rate: FeeRate,
amount_needed: u64, amount_needed: u64,
input_witness_weight: usize, input_witness_weight: usize,
@ -159,14 +160,13 @@ pub struct DumbCoinSelection;
impl CoinSelectionAlgorithm for DumbCoinSelection { impl CoinSelectionAlgorithm for DumbCoinSelection {
fn coin_select( fn coin_select(
&self, &self,
mut utxos: Vec<UTXO>, must_use_utxos: Vec<UTXO>,
use_all_utxos: bool, mut may_use_utxos: Vec<UTXO>,
fee_rate: FeeRate, fee_rate: FeeRate,
outgoing_amount: u64, outgoing_amount: u64,
input_witness_weight: usize, input_witness_weight: usize,
mut fee_amount: f32, mut fee_amount: f32,
) -> Result<CoinSelectionResult, Error> { ) -> Result<CoinSelectionResult, Error> {
let mut txin = Vec::new();
let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0; let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0;
log::debug!( log::debug!(
@ -176,35 +176,51 @@ impl CoinSelectionAlgorithm for DumbCoinSelection {
fee_rate fee_rate
); );
// sort so that we pick them starting from the larger. // We put the "must_use" UTXOs first and make sure the "may_use" are sorted largest to smallest
utxos.sort_by(|a, b| a.txout.value.partial_cmp(&b.txout.value).unwrap()); let utxos = {
may_use_utxos.sort_by(|a, b| b.txout.value.partial_cmp(&a.txout.value).unwrap());
let mut selected_amount: u64 = 0; must_use_utxos
while use_all_utxos || selected_amount < outgoing_amount + (fee_amount.ceil() as u64) { .into_iter()
let utxo = match utxos.pop() { .map(|utxo| (true, utxo))
Some(utxo) => utxo, .chain(may_use_utxos.into_iter().map(|utxo| (false, utxo)))
None if selected_amount < outgoing_amount + (fee_amount.ceil() as u64) => {
return Err(Error::InsufficientFunds)
}
None if use_all_utxos => break,
None => return Err(Error::InsufficientFunds),
}; };
// Keep including inputs until we've got enough.
// Store the total input value in selected_amount and the total fee being paid in fee_amount
let mut selected_amount = 0;
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)
{
let new_in = TxIn { let new_in = TxIn {
previous_output: utxo.outpoint, previous_output: utxo.outpoint,
script_sig: Script::default(), script_sig: Script::default(),
sequence: 0, // Let the caller choose the right nSequence sequence: 0, // Let the caller choose the right nSequence
witness: vec![], 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 + input_witness_weight);
**selected_amount += utxo.txout.value;
log::debug!( log::debug!(
"Selected {}, updated fee_amount = `{}`", "Selected {}, updated fee_amount = `{}`",
new_in.previous_output, new_in.previous_output,
fee_amount fee_amount
); );
txin.push((new_in, utxo.txout.script_pubkey)); Some((new_in, utxo.txout.script_pubkey))
selected_amount += utxo.txout.value; } else {
None
}
},
)
.collect::<Vec<_>>();
if selected_amount < outgoing_amount + (fee_amount.ceil() as u64) {
return Err(Error::InsufficientFunds);
} }
Ok(CoinSelectionResult { Ok(CoinSelectionResult {
@ -259,8 +275,8 @@ mod test {
let result = DumbCoinSelection let result = DumbCoinSelection
.coin_select( .coin_select(
vec![],
utxos, utxos,
false,
FeeRate::from_sat_per_vb(1.0), FeeRate::from_sat_per_vb(1.0),
250_000, 250_000,
P2WPKH_WITNESS_SIZE, P2WPKH_WITNESS_SIZE,
@ -280,7 +296,7 @@ mod test {
let result = DumbCoinSelection let result = DumbCoinSelection
.coin_select( .coin_select(
utxos, utxos,
true, vec![],
FeeRate::from_sat_per_vb(1.0), FeeRate::from_sat_per_vb(1.0),
20_000, 20_000,
P2WPKH_WITNESS_SIZE, P2WPKH_WITNESS_SIZE,
@ -299,8 +315,8 @@ mod test {
let result = DumbCoinSelection let result = DumbCoinSelection
.coin_select( .coin_select(
vec![],
utxos, utxos,
false,
FeeRate::from_sat_per_vb(1.0), FeeRate::from_sat_per_vb(1.0),
20_000, 20_000,
P2WPKH_WITNESS_SIZE, P2WPKH_WITNESS_SIZE,
@ -320,8 +336,8 @@ mod test {
DumbCoinSelection DumbCoinSelection
.coin_select( .coin_select(
vec![],
utxos, utxos,
false,
FeeRate::from_sat_per_vb(1.0), FeeRate::from_sat_per_vb(1.0),
500_000, 500_000,
P2WPKH_WITNESS_SIZE, P2WPKH_WITNESS_SIZE,
@ -337,8 +353,8 @@ mod test {
DumbCoinSelection DumbCoinSelection
.coin_select( .coin_select(
vec![],
utxos, utxos,
false,
FeeRate::from_sat_per_vb(1000.0), FeeRate::from_sat_per_vb(1000.0),
250_000, 250_000,
P2WPKH_WITNESS_SIZE, P2WPKH_WITNESS_SIZE,

View File

@ -359,13 +359,18 @@ where
&builder.unspendable, &builder.unspendable,
builder.send_all, builder.send_all,
)?; )?;
let (must_use_utxos, may_use_utxos) = match use_all_utxos {
true => (available_utxos, vec![]),
false => (vec![], available_utxos),
};
let coin_selection::CoinSelectionResult { let coin_selection::CoinSelectionResult {
txin, txin,
selected_amount, selected_amount,
mut fee_amount, mut fee_amount,
} = builder.coin_selection.coin_select( } = builder.coin_selection.coin_select(
available_utxos, must_use_utxos,
use_all_utxos, may_use_utxos,
fee_rate, fee_rate,
outgoing, outgoing,
input_witness_weight, input_witness_weight,
@ -597,13 +602,19 @@ where
self.database.borrow().deref(), self.database.borrow().deref(),
available_utxos.into_iter(), available_utxos.into_iter(),
)?; )?;
let (must_use_utxos, may_use_utxos) = match use_all_utxos {
true => (available_utxos, vec![]),
false => (vec![], available_utxos),
};
let coin_selection::CoinSelectionResult { let coin_selection::CoinSelectionResult {
txin, txin,
selected_amount, selected_amount,
fee_amount, fee_amount,
} = builder.coin_selection.coin_select( } = builder.coin_selection.coin_select(
available_utxos, must_use_utxos,
use_all_utxos, may_use_utxos,
new_feerate, new_feerate,
fee_difference.saturating_sub(removed_change_output.value), fee_difference.saturating_sub(removed_change_output.value),
input_witness_weight, input_witness_weight,