Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 38 additions & 62 deletions src/wallet/coin_selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -84,7 +76,7 @@
//! }
//!
//! Ok(CoinSelectionResult {
//! txin: all_utxos_selected,
//! selected: all_utxos_selected,
//! selected_amount,
//! fee_amount: fee_amount + additional_fees,
//! })
Expand All @@ -105,8 +97,6 @@
//! # Ok::<(), bdk::Error>(())
//! ```

use bitcoin::{Script, TxIn};

use crate::database::Database;
use crate::error::Error;
use crate::types::{FeeRate, UTXO};
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
}
Expand All @@ -539,7 +514,6 @@ impl BranchAndBoundCoinSelection {
mod test {
use std::str::FromStr;

use bitcoin::consensus::encode::serialize;
use bitcoin::{OutPoint, Script, TxOut};

use super::*;
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
);
}
}
74 changes: 46 additions & 28 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ where
)?;

let coin_selection::CoinSelectionResult {
txin,
selected,
selected_amount,
mut fee_amount,
} = builder.coin_selection.coin_select(
Expand All @@ -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 {
Expand Down Expand Up @@ -485,7 +484,7 @@ where
builder.ordering.sort_tx(&mut tx);

let txid = tx.txid();
let psbt = self.complete_transaction(tx, prev_script_pubkeys, builder)?;
let psbt = self.complete_transaction(tx, selected, builder)?;

let transaction_details = TransactionDetails {
transaction: None,
Expand Down Expand Up @@ -701,7 +700,7 @@ where
};

let coin_selection::CoinSelectionResult {
txin,
selected,
selected_amount,
fee_amount,
} = builder.coin_selection.coin_select(
Expand All @@ -713,18 +712,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
Comment thread
LLFourn marked this conversation as resolved.
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;

Expand Down Expand Up @@ -773,7 +770,7 @@ where
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, selected, builder)?;

Ok((psbt, details))
}
Expand Down Expand Up @@ -1126,19 +1123,23 @@ where
>(
&self,
tx: Transaction,
prev_script_pubkeys: HashMap<OutPoint, Script>,
selected: Vec<UTXO>,
builder: TxBuilder<D, Cs, Ctx>,
) -> Result<PSBT, Error> {
let mut psbt = PSBT::from_unsigned_tx(tx)?;
let lookup_output = selected
.into_iter()
.map(|utxo| (utxo.outpoint, utxo))
.collect::<HashMap<_, _>>();

// add metadata for the inputs
for (psbt_input, input) in psbt
.inputs
.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,
};

Expand All @@ -1153,7 +1154,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,
Expand Down Expand Up @@ -2116,6 +2117,23 @@ mod test {
assert!(psbt.inputs[0].witness_utxo.is_some());
}

#[test]
fn test_create_tx_shwpkh_has_witness_utxo() {
let (wallet, _, _) =
get_funded_wallet("sh(wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))");
let addr = wallet.get_new_address().unwrap();
let (psbt, _) = wallet
.create_tx(
TxBuilder::new()
.set_single_recipient(addr.script_pubkey())
.drain_wallet(),
)
.unwrap();

assert!(psbt.inputs[0].non_witness_utxo.is_none());
assert!(psbt.inputs[0].witness_utxo.is_some());
}

#[test]
fn test_create_tx_both_non_witness_utxo_and_witness_utxo() {
let (wallet, _, _) =
Expand Down