[wallet] Make coin_select return UTXOs instead of TxIns

- We want to keep the metadata in the UTXO around for things later
- It is easier to turn a UTXO into a TxIn outside
This commit is contained in:
LLFourn 2020-10-30 14:09:59 +11:00
parent 13c1170304
commit 004f81b0a8
No known key found for this signature in database
GPG Key ID: A27093B54DA11F65
2 changed files with 71 additions and 90 deletions

View File

@ -63,18 +63,10 @@
//! let all_utxos_selected = required_utxos
//! .into_iter().chain(optional_utxos)
//! .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 += TXIN_BASE_WEIGHT + weight;
//!
//! Some((
//! txin,
//! utxo.txout.script_pubkey,
//! ))
//! Some(utxo)
//! })
//! .collect::<Vec<_>>();
//! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0;
@ -84,7 +76,7 @@
//! }
//!
//! Ok(CoinSelectionResult {
//! txin: all_utxos_selected,
//! selected: all_utxos_selected,
//! selected_amount,
//! fee_amount: fee_amount + additional_fees,
//! })
@ -105,8 +97,6 @@
//! # Ok::<(), bdk::Error>(())
//! ```
use bitcoin::{Script, TxIn};
use crate::database::Database;
use crate::error::Error;
use crate::types::{FeeRate, UTXO};
@ -124,11 +114,15 @@ pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
#[cfg(test)]
pub type DefaultCoinSelectionAlgorithm = LargestFirstCoinSelection; // make the tests more predictable
// Base weight of a Txin, not counting the weight needed for satisfaying it.
// prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes) + script_len (1 bytes)
pub const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4;
/// Result of a successful coin selection
#[derive(Debug)]
pub struct CoinSelectionResult {
/// List of inputs to use, with the respective previous script_pubkey
pub txin: Vec<(TxIn, Script)>,
/// List of outputs selected for use as inputs
pub selected: Vec<UTXO>,
/// Sum of the selected inputs' value
pub selected_amount: u64,
/// Total fee amount in satoshi
@ -204,28 +198,21 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
// 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
let selected = utxos
.scan(
(&mut selected_amount, &mut fee_amount),
|(selected_amount, fee_amount), (required, (utxo, weight))| {
if required || **selected_amount < amount_needed + (fee_amount.ceil() as u64) {
let new_in = TxIn {
previous_output: utxo.outpoint,
script_sig: Script::default(),
sequence: 0, // Let the caller choose the right nSequence
witness: vec![],
};
|(selected_amount, fee_amount), (must_use, (utxo, weight))| {
if must_use || **selected_amount < amount_needed + (fee_amount.ceil() as u64) {
**fee_amount += calc_fee_bytes(TXIN_BASE_WEIGHT + weight);
**selected_amount += utxo.txout.value;
log::debug!(
"Selected {}, updated fee_amount = `{}`",
new_in.previous_output,
utxo.outpoint,
fee_amount
);
Some((new_in, utxo.txout.script_pubkey))
Some(utxo)
} else {
None
}
@ -238,17 +225,13 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
}
Ok(CoinSelectionResult {
txin,
selected,
fee_amount,
selected_amount,
})
}
}
// Base weight of a Txin, not counting the weight needed for satisfaying it.
// prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes) + script_len (1 bytes)
pub const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4;
#[derive(Debug, Clone)]
// Adds fee information to an UTXO.
struct OutputGroup {
@ -507,28 +490,20 @@ impl BranchAndBoundCoinSelection {
}
fn calculate_cs_result(
selected_utxos: Vec<OutputGroup>,
required_utxos: Vec<OutputGroup>,
fee_amount: f32,
mut selected_utxos: Vec<OutputGroup>,
mut required_utxos: Vec<OutputGroup>,
mut fee_amount: f32,
) -> CoinSelectionResult {
let (txin, fee_amount, selected_amount) =
selected_utxos.into_iter().chain(required_utxos).fold(
(vec![], fee_amount, 0),
|(mut txin, mut fee_amount, mut selected_amount), output_group| {
selected_amount += output_group.utxo.txout.value;
fee_amount += output_group.fee;
txin.push((
TxIn {
previous_output: output_group.utxo.outpoint,
..Default::default()
},
output_group.utxo.txout.script_pubkey,
));
(txin, fee_amount, selected_amount)
},
);
selected_utxos.append(&mut required_utxos);
fee_amount += selected_utxos.iter().map(|u| u.fee).sum::<f32>();
let selected = selected_utxos
.into_iter()
.map(|u| u.utxo)
.collect::<Vec<_>>();
let selected_amount = selected.iter().map(|u| u.txout.value).sum();
CoinSelectionResult {
txin,
selected,
fee_amount,
selected_amount,
}
@ -539,7 +514,6 @@ impl BranchAndBoundCoinSelection {
mod test {
use std::str::FromStr;
use bitcoin::consensus::encode::serialize;
use bitcoin::{OutPoint, Script, TxOut};
use super::*;
@ -648,7 +622,7 @@ mod test {
)
.unwrap();
assert_eq!(result.txin.len(), 2);
assert_eq!(result.selected.len(), 2);
assert_eq!(result.selected_amount, 300_000);
assert_eq!(result.fee_amount, 186.0);
}
@ -669,7 +643,7 @@ mod test {
)
.unwrap();
assert_eq!(result.txin.len(), 2);
assert_eq!(result.selected.len(), 2);
assert_eq!(result.selected_amount, 300_000);
assert_eq!(result.fee_amount, 186.0);
}
@ -690,7 +664,7 @@ mod test {
)
.unwrap();
assert_eq!(result.txin.len(), 1);
assert_eq!(result.selected.len(), 1);
assert_eq!(result.selected_amount, 200_000);
assert_eq!(result.fee_amount, 118.0);
}
@ -750,7 +724,7 @@ mod test {
)
.unwrap();
assert_eq!(result.txin.len(), 3);
assert_eq!(result.selected.len(), 3);
assert_eq!(result.selected_amount, 300_000);
assert_eq!(result.fee_amount, 254.0);
}
@ -771,7 +745,7 @@ mod test {
)
.unwrap();
assert_eq!(result.txin.len(), 2);
assert_eq!(result.selected.len(), 2);
assert_eq!(result.selected_amount, 300_000);
assert_eq!(result.fee_amount, 186.0);
}
@ -828,12 +802,11 @@ mod test {
)
.unwrap();
assert_eq!(result.txin.len(), 1);
assert_eq!(result.selected.len(), 1);
assert_eq!(result.selected_amount, 100_000);
let result_size =
serialize(result.txin.first().unwrap()).len() as f32 + P2WPKH_WITNESS_SIZE as f32 / 4.0;
let input_size = (TXIN_BASE_WEIGHT as f32) / 4.0 + P2WPKH_WITNESS_SIZE as f32 / 4.0;
let epsilon = 0.5;
assert!((1.0 - (result.fee_amount / result_size)).abs() < epsilon);
assert!((1.0 - (result.fee_amount / input_size)).abs() < epsilon);
}
#[test]
@ -1013,6 +986,9 @@ mod test {
);
assert!(result.selected_amount > target_amount);
assert_eq!(result.fee_amount, 50.0 + result.txin.len() as f32 * 68.0);
assert_eq!(
result.fee_amount,
50.0 + result.selected.len() as f32 * 68.0
);
}
}

View File

@ -414,7 +414,7 @@ where
)?;
let coin_selection::CoinSelectionResult {
txin,
selected,
selected_amount,
mut fee_amount,
} = builder.coin_selection.coin_select(
@ -425,16 +425,15 @@ where
outgoing,
fee_amount,
)?;
let (mut txin, prev_script_pubkeys): (Vec<_>, Vec<_>) = txin.into_iter().unzip();
// map that allows us to lookup the prev_script_pubkey for a given previous_output
let prev_script_pubkeys = txin
tx.input = selected
.iter()
.zip(prev_script_pubkeys.into_iter())
.map(|(txin, script)| (txin.previous_output, script))
.collect::<HashMap<_, _>>();
txin.iter_mut().for_each(|i| i.sequence = n_sequence);
tx.input = txin;
.map(|u| bitcoin::TxIn {
previous_output: u.outpoint,
script_sig: Script::default(),
sequence: n_sequence,
witness: vec![],
})
.collect();
// prepare the change output
let change_output = match builder.single_recipient {
@ -485,7 +484,11 @@ where
builder.ordering.sort_tx(&mut tx);
let txid = tx.txid();
let psbt = self.complete_transaction(tx, prev_script_pubkeys, builder)?;
let lookup_output = selected
.into_iter()
.map(|utxo| (utxo.outpoint, utxo))
.collect();
let psbt = self.complete_transaction(tx, lookup_output, builder)?;
let transaction_details = TransactionDetails {
transaction: None,
@ -701,7 +704,7 @@ where
};
let coin_selection::CoinSelectionResult {
txin,
selected,
selected_amount,
fee_amount,
} = builder.coin_selection.coin_select(
@ -713,18 +716,16 @@ where
initial_fee,
)?;
let (mut txin, prev_script_pubkeys): (Vec<_>, Vec<_>) = txin.into_iter().unzip();
// map that allows us to lookup the prev_script_pubkey for a given previous_output
let prev_script_pubkeys = txin
tx.input = selected
.iter()
.zip(prev_script_pubkeys.into_iter())
.map(|(txin, script)| (txin.previous_output, script))
.collect::<HashMap<_, _>>();
// TODO: use builder.n_sequence??
// use the same n_sequence
txin.iter_mut().for_each(|i| i.sequence = original_sequence);
tx.input = txin;
.map(|u| bitcoin::TxIn {
previous_output: u.outpoint,
script_sig: Script::default(),
// TODO: use builder.n_sequence??
sequence: original_sequence,
witness: vec![],
})
.collect();
details.sent = selected_amount;
@ -770,10 +771,14 @@ where
// TODO: check that we are not replacing more than 100 txs from mempool
details.txid = tx.txid();
let lookup_output = selected
.into_iter()
.map(|utxo| (utxo.outpoint, utxo))
.collect();
details.fees = fee_amount;
details.timestamp = time::get_timestamp();
let psbt = self.complete_transaction(tx, prev_script_pubkeys, builder)?;
let psbt = self.complete_transaction(tx, lookup_output, builder)?;
Ok((psbt, details))
}
@ -1126,7 +1131,7 @@ where
>(
&self,
tx: Transaction,
prev_script_pubkeys: HashMap<OutPoint, Script>,
lookup_output: HashMap<OutPoint, UTXO>,
builder: TxBuilder<D, Cs, Ctx>,
) -> Result<PSBT, Error> {
let mut psbt = PSBT::from_unsigned_tx(tx)?;
@ -1137,8 +1142,8 @@ where
.iter_mut()
.zip(psbt.global.unsigned_tx.input.iter())
{
let prev_script = match prev_script_pubkeys.get(&input.previous_output) {
Some(prev_script) => prev_script,
let utxo = match lookup_output.get(&input.previous_output) {
Some(utxo) => utxo,
None => continue,
};
@ -1153,7 +1158,7 @@ where
let (script_type, child) = match self
.database
.borrow()
.get_path_from_script_pubkey(&prev_script)?
.get_path_from_script_pubkey(&utxo.txout.script_pubkey)?
{
Some(x) => x,
None => continue,