Added add_foreign_utxo

To allow adding UTXOs external to the current wallet.
The caller must provide the psbt::Input so we can create a coherent PSBT
at the end and so this is compatible with existing PSBT workflows.

Main changes:

- There are now two types of UTXOs, local and foreign reflected in a
`Utxo` enum.
- `WeightedUtxo` now captures floating `(Utxo, usize)` tuples
- `CoinSelectionResult` now has methods on it for distinguishing between
local amount included vs total.
This commit is contained in:
LLFourn 2021-02-08 15:40:56 +11:00
parent 9a918f285d
commit 1fbfeabd77
No known key found for this signature in database
GPG Key ID: A27093B54DA11F65
5 changed files with 491 additions and 130 deletions

View File

@ -50,7 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
type to mark for a missing client. type to mark for a missing client.
- Upgrade `tokio` to `1.0`. - Upgrade `tokio` to `1.0`.
#### Transaction Creation Overhaul ### Transaction Creation Overhaul
The `TxBuilder` is now created from the `build_tx` or `build_fee_bump` functions on wallet and the The `TxBuilder` is now created from the `build_tx` or `build_fee_bump` functions on wallet and the
final transaction is created by calling `finish` on the builder. final transaction is created by calling `finish` on the builder.
@ -61,6 +61,13 @@ final transaction is created by calling `finish` on the builder.
- Added `Wallet::get_utxo` - Added `Wallet::get_utxo`
- Added `Wallet::get_descriptor_for_keychain` - Added `Wallet::get_descriptor_for_keychain`
### `add_foreign_utxo`
- Renamed `UTXO` to `LocalUtxo`
- Added `WeightedUtxo` to replace floating `(UTXO, usize)`.
- Added `Utxo` enum to incorporate both local utxos and foreign utxos
- Added `TxBuilder::add_foreign_utxo` which allows adding a utxo external to the wallet.
### CLI ### CLI
#### Changed #### Changed
- Remove `cli.rs` module, `cli-utils` feature and `repl.rs` example; moved to new [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli) repository - Remove `cli.rs` module, `cli-utils` feature and `repl.rs` example; moved to new [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli) repository

View File

@ -25,7 +25,7 @@
use std::convert::AsRef; use std::convert::AsRef;
use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut}; use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut};
use bitcoin::hash_types::Txid; use bitcoin::{hash_types::Txid, util::psbt};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -90,7 +90,9 @@ impl std::default::Default for FeeRate {
} }
} }
/// A wallet unspent output /// An unspent output owned by a [`Wallet`].
///
/// [`Wallet`]: crate::Wallet
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct LocalUtxo { pub struct LocalUtxo {
/// Reference to a transaction output /// Reference to a transaction output
@ -101,6 +103,62 @@ pub struct LocalUtxo {
pub keychain: KeychainKind, pub keychain: KeychainKind,
} }
/// A [`Utxo`] with its `satisfaction_weight`.
#[derive(Debug, Clone, PartialEq)]
pub struct WeightedUtxo {
/// The weight of the witness data or `scriptSig`.
/// This is used to properly maintain the feerate when doing coin selection.
pub satisfaction_weight: usize,
/// The UTXO
pub utxo: Utxo,
}
#[derive(Debug, Clone, PartialEq)]
/// An unspent transaction output (UTXO).
pub enum Utxo {
/// A UTXO owned by the local wallet.
Local(LocalUtxo),
/// A UTXO owned by another wallet.
Foreign {
/// The location of the output.
outpoint: OutPoint,
/// The information about the input we require to add it to a PSBT.
// Box it to stop the type being too big.
psbt_input: Box<psbt::Input>,
},
}
impl Utxo {
/// Get the location of the UTXO
pub fn outpoint(&self) -> OutPoint {
match &self {
Utxo::Local(local) => local.outpoint,
Utxo::Foreign { outpoint, .. } => *outpoint,
}
}
/// Get the `TxOut` of the UTXO
pub fn txout(&self) -> &TxOut {
match &self {
Utxo::Local(local) => &local.txout,
Utxo::Foreign {
outpoint,
psbt_input,
} => {
if let Some(prev_tx) = &psbt_input.non_witness_utxo {
return &prev_tx.output[outpoint.vout as usize];
}
if let Some(txout) = &psbt_input.witness_utxo {
return &txout;
}
unreachable!("Foreign UTXOs will always have one of these set")
}
}
}
}
/// A wallet transaction /// A wallet transaction
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
pub struct TransactionDetails { pub struct TransactionDetails {

View File

@ -50,8 +50,8 @@
//! fn coin_select( //! fn coin_select(
//! &self, //! &self,
//! database: &D, //! database: &D,
//! required_utxos: Vec<(LocalUtxo, usize)>, //! required_utxos: Vec<WeightedUtxo>,
//! optional_utxos: Vec<(LocalUtxo, usize)>, //! optional_utxos: Vec<WeightedUtxo>,
//! fee_rate: FeeRate, //! fee_rate: FeeRate,
//! amount_needed: u64, //! amount_needed: u64,
//! fee_amount: f32, //! fee_amount: f32,
@ -60,11 +60,10 @@
//! let mut additional_weight = 0; //! let mut additional_weight = 0;
//! let all_utxos_selected = required_utxos //! let all_utxos_selected = required_utxos
//! .into_iter().chain(optional_utxos) //! .into_iter().chain(optional_utxos)
//! .scan((&mut selected_amount, &mut additional_weight), |(selected_amount, additional_weight), (utxo, weight)| { //! .scan((&mut selected_amount, &mut additional_weight), |(selected_amount, additional_weight), weighted_utxo| {
//! **selected_amount += utxo.txout.value; //! **selected_amount += weighted_utxo.utxo.txout().value;
//! **additional_weight += TXIN_BASE_WEIGHT + weight; //! **additional_weight += TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight;
//! //! Some(weighted_utxo.utxo)
//! Some(utxo)
//! }) //! })
//! .collect::<Vec<_>>(); //! .collect::<Vec<_>>();
//! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0; //! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0;
@ -75,7 +74,6 @@
//! //!
//! Ok(CoinSelectionResult { //! Ok(CoinSelectionResult {
//! selected: all_utxos_selected, //! selected: all_utxos_selected,
//! selected_amount,
//! fee_amount: fee_amount + additional_fees, //! fee_amount: fee_amount + additional_fees,
//! }) //! })
//! } //! }
@ -97,9 +95,9 @@
//! # Ok::<(), bdk::Error>(()) //! # Ok::<(), bdk::Error>(())
//! ``` //! ```
use crate::database::Database; use crate::types::FeeRate;
use crate::error::Error; use crate::{database::Database, WeightedUtxo};
use crate::types::{FeeRate, LocalUtxo}; use crate::{error::Error, Utxo};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
#[cfg(not(test))] #[cfg(not(test))]
@ -122,13 +120,29 @@ pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4;
#[derive(Debug)] #[derive(Debug)]
pub struct CoinSelectionResult { pub struct CoinSelectionResult {
/// List of outputs selected for use as inputs /// List of outputs selected for use as inputs
pub selected: Vec<LocalUtxo>, pub selected: Vec<Utxo>,
/// Sum of the selected inputs' value
pub selected_amount: u64,
/// Total fee amount in satoshi /// Total fee amount in satoshi
pub fee_amount: f32, pub fee_amount: f32,
} }
impl CoinSelectionResult {
/// The total value of the inputs selected.
pub fn selected_amount(&self) -> u64 {
self.selected.iter().map(|u| u.txout().value).sum()
}
/// The total value of the inputs selected from the local wallet.
pub fn local_selected_amount(&self) -> u64 {
self.selected
.iter()
.filter_map(|u| match u {
Utxo::Local(_) => Some(u.txout().value),
_ => None,
})
.sum()
}
}
/// Trait for generalized coin selection algorithms /// Trait for generalized coin selection algorithms
/// ///
/// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin /// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin
@ -151,8 +165,8 @@ pub trait CoinSelectionAlgorithm<D: Database>: std::fmt::Debug {
fn coin_select( fn coin_select(
&self, &self,
database: &D, database: &D,
required_utxos: Vec<(LocalUtxo, usize)>, required_utxos: Vec<WeightedUtxo>,
optional_utxos: Vec<(LocalUtxo, usize)>, optional_utxos: Vec<WeightedUtxo>,
fee_rate: FeeRate, fee_rate: FeeRate,
amount_needed: u64, amount_needed: u64,
fee_amount: f32, fee_amount: f32,
@ -170,8 +184,8 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
fn coin_select( fn coin_select(
&self, &self,
_database: &D, _database: &D,
required_utxos: Vec<(LocalUtxo, usize)>, required_utxos: Vec<WeightedUtxo>,
mut optional_utxos: Vec<(LocalUtxo, usize)>, mut optional_utxos: Vec<WeightedUtxo>,
fee_rate: FeeRate, fee_rate: FeeRate,
amount_needed: u64, amount_needed: u64,
mut fee_amount: f32, mut fee_amount: f32,
@ -188,7 +202,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
// We put the "required UTXOs" first and make sure the optional UTXOs are sorted, // We put the "required UTXOs" first and make sure the optional UTXOs are sorted,
// initially smallest to largest, before being reversed with `.rev()`. // initially smallest to largest, before being reversed with `.rev()`.
let utxos = { let utxos = {
optional_utxos.sort_unstable_by_key(|(utxo, _)| utxo.txout.value); optional_utxos.sort_unstable_by_key(|wu| wu.utxo.txout().value);
required_utxos required_utxos
.into_iter() .into_iter()
.map(|utxo| (true, utxo)) .map(|utxo| (true, utxo))
@ -201,18 +215,19 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
let selected = utxos let selected = utxos
.scan( .scan(
(&mut selected_amount, &mut fee_amount), (&mut selected_amount, &mut fee_amount),
|(selected_amount, fee_amount), (must_use, (utxo, weight))| { |(selected_amount, fee_amount), (must_use, weighted_utxo)| {
if must_use || **selected_amount < amount_needed + (fee_amount.ceil() as u64) { if must_use || **selected_amount < amount_needed + (fee_amount.ceil() as u64) {
**fee_amount += calc_fee_bytes(TXIN_BASE_WEIGHT + weight); **fee_amount +=
**selected_amount += utxo.txout.value; calc_fee_bytes(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight);
**selected_amount += weighted_utxo.utxo.txout().value;
log::debug!( log::debug!(
"Selected {}, updated fee_amount = `{}`", "Selected {}, updated fee_amount = `{}`",
utxo.outpoint, weighted_utxo.utxo.outpoint(),
fee_amount fee_amount
); );
Some(utxo) Some(weighted_utxo.utxo)
} else { } else {
None None
} }
@ -231,7 +246,6 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
Ok(CoinSelectionResult { Ok(CoinSelectionResult {
selected, selected,
fee_amount, fee_amount,
selected_amount,
}) })
} }
} }
@ -239,9 +253,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
// Adds fee information to an UTXO. // Adds fee information to an UTXO.
struct OutputGroup { struct OutputGroup {
utxo: LocalUtxo, weighted_utxo: WeightedUtxo,
// weight needed to satisfy the UTXO, as described in `Descriptor::max_satisfaction_weight`
satisfaction_weight: usize,
// Amount of fees for spending a certain utxo, calculated using a certain FeeRate // Amount of fees for spending a certain utxo, calculated using a certain FeeRate
fee: f32, fee: f32,
// The effective value of the UTXO, i.e., the utxo value minus the fee for spending it // The effective value of the UTXO, i.e., the utxo value minus the fee for spending it
@ -249,12 +261,12 @@ struct OutputGroup {
} }
impl OutputGroup { impl OutputGroup {
fn new(utxo: LocalUtxo, satisfaction_weight: usize, fee_rate: FeeRate) -> Self { fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
let fee = (TXIN_BASE_WEIGHT + satisfaction_weight) as f32 / 4.0 * fee_rate.as_sat_vb(); let fee = (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as f32 / 4.0
let effective_value = utxo.txout.value as i64 - fee.ceil() as i64; * fee_rate.as_sat_vb();
let effective_value = weighted_utxo.utxo.txout().value as i64 - fee.ceil() as i64;
OutputGroup { OutputGroup {
utxo, weighted_utxo,
satisfaction_weight,
effective_value, effective_value,
fee, fee,
} }
@ -291,8 +303,8 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
fn coin_select( fn coin_select(
&self, &self,
_database: &D, _database: &D,
required_utxos: Vec<(LocalUtxo, usize)>, required_utxos: Vec<WeightedUtxo>,
optional_utxos: Vec<(LocalUtxo, usize)>, optional_utxos: Vec<WeightedUtxo>,
fee_rate: FeeRate, fee_rate: FeeRate,
amount_needed: u64, amount_needed: u64,
fee_amount: f32, fee_amount: f32,
@ -300,7 +312,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
// Mapping every (UTXO, usize) to an output group // Mapping every (UTXO, usize) to an output group
let required_utxos: Vec<OutputGroup> = required_utxos let required_utxos: Vec<OutputGroup> = required_utxos
.into_iter() .into_iter()
.map(|u| OutputGroup::new(u.0, u.1, fee_rate)) .map(|u| OutputGroup::new(u, fee_rate))
.collect(); .collect();
// Mapping every (UTXO, usize) to an output group. // Mapping every (UTXO, usize) to an output group.
@ -308,7 +320,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
// adding them is more than their value // adding them is more than their value
let optional_utxos: Vec<OutputGroup> = optional_utxos let optional_utxos: Vec<OutputGroup> = optional_utxos
.into_iter() .into_iter()
.map(|u| OutputGroup::new(u.0, u.1, fee_rate)) .map(|u| OutputGroup::new(u, fee_rate))
.filter(|u| u.effective_value > 0) .filter(|u| u.effective_value > 0)
.collect(); .collect();
@ -507,14 +519,12 @@ impl BranchAndBoundCoinSelection {
fee_amount += selected_utxos.iter().map(|u| u.fee).sum::<f32>(); fee_amount += selected_utxos.iter().map(|u| u.fee).sum::<f32>();
let selected = selected_utxos let selected = selected_utxos
.into_iter() .into_iter()
.map(|u| u.utxo) .map(|u| u.weighted_utxo.utxo)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let selected_amount = selected.iter().map(|u| u.txout.value).sum();
CoinSelectionResult { CoinSelectionResult {
selected, selected,
fee_amount, fee_amount,
selected_amount,
} }
} }
} }
@ -535,10 +545,11 @@ mod test {
const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2; const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2;
fn get_test_utxos() -> Vec<(LocalUtxo, usize)> { fn get_test_utxos() -> Vec<WeightedUtxo> {
vec![ vec![
( WeightedUtxo {
LocalUtxo { satisfaction_weight: P2WPKH_WITNESS_SIZE,
utxo: Utxo::Local(LocalUtxo {
outpoint: OutPoint::from_str( outpoint: OutPoint::from_str(
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
) )
@ -548,11 +559,11 @@ mod test {
script_pubkey: Script::new(), script_pubkey: Script::new(),
}, },
keychain: KeychainKind::External, keychain: KeychainKind::External,
}, }),
P2WPKH_WITNESS_SIZE, },
), WeightedUtxo {
( satisfaction_weight: P2WPKH_WITNESS_SIZE,
LocalUtxo { utxo: Utxo::Local(LocalUtxo {
outpoint: OutPoint::from_str( outpoint: OutPoint::from_str(
"65d92ddff6b6dc72c89624a6491997714b90f6004f928d875bc0fd53f264fa85:0", "65d92ddff6b6dc72c89624a6491997714b90f6004f928d875bc0fd53f264fa85:0",
) )
@ -562,17 +573,17 @@ mod test {
script_pubkey: Script::new(), script_pubkey: Script::new(),
}, },
keychain: KeychainKind::Internal, keychain: KeychainKind::Internal,
}, }),
P2WPKH_WITNESS_SIZE, },
),
] ]
} }
fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<(LocalUtxo, usize)> { fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
let mut res = Vec::new(); let mut res = Vec::new();
for _ in 0..utxos_number { for _ in 0..utxos_number {
res.push(( res.push(WeightedUtxo {
LocalUtxo { satisfaction_weight: P2WPKH_WITNESS_SIZE,
utxo: Utxo::Local(LocalUtxo {
outpoint: OutPoint::from_str( outpoint: OutPoint::from_str(
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
) )
@ -582,16 +593,16 @@ mod test {
script_pubkey: Script::new(), script_pubkey: Script::new(),
}, },
keychain: KeychainKind::External, keychain: KeychainKind::External,
}, }),
P2WPKH_WITNESS_SIZE, });
));
} }
res res
} }
fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<(LocalUtxo, usize)> { fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
let utxo = ( let utxo = WeightedUtxo {
LocalUtxo { satisfaction_weight: P2WPKH_WITNESS_SIZE,
utxo: Utxo::Local(LocalUtxo {
outpoint: OutPoint::from_str( outpoint: OutPoint::from_str(
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0",
) )
@ -601,18 +612,18 @@ mod test {
script_pubkey: Script::new(), script_pubkey: Script::new(),
}, },
keychain: KeychainKind::External, keychain: KeychainKind::External,
}, }),
P2WPKH_WITNESS_SIZE, };
);
vec![utxo; utxos_number] vec![utxo; utxos_number]
} }
fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<(LocalUtxo, usize)>) -> u64 { fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<WeightedUtxo>) -> u64 {
let utxos_picked_len = rng.gen_range(2, utxos.len() / 2); let utxos_picked_len = rng.gen_range(2, utxos.len() / 2);
utxos.shuffle(&mut rng); utxos.shuffle(&mut rng);
utxos[..utxos_picked_len] utxos[..utxos_picked_len]
.iter() .iter()
.fold(0, |acc, x| acc + x.0.txout.value) .map(|u| u.utxo.txout().value)
.sum()
} }
#[test] #[test]
@ -632,7 +643,7 @@ mod test {
.unwrap(); .unwrap();
assert_eq!(result.selected.len(), 2); assert_eq!(result.selected.len(), 2);
assert_eq!(result.selected_amount, 300_000); assert_eq!(result.selected_amount(), 300_000);
assert_eq!(result.fee_amount, 186.0); assert_eq!(result.fee_amount, 186.0);
} }
@ -653,7 +664,7 @@ mod test {
.unwrap(); .unwrap();
assert_eq!(result.selected.len(), 2); assert_eq!(result.selected.len(), 2);
assert_eq!(result.selected_amount, 300_000); assert_eq!(result.selected_amount(), 300_000);
assert_eq!(result.fee_amount, 186.0); assert_eq!(result.fee_amount, 186.0);
} }
@ -674,7 +685,7 @@ mod test {
.unwrap(); .unwrap();
assert_eq!(result.selected.len(), 1); assert_eq!(result.selected.len(), 1);
assert_eq!(result.selected_amount, 200_000); assert_eq!(result.selected_amount(), 200_000);
assert_eq!(result.fee_amount, 118.0); assert_eq!(result.fee_amount, 118.0);
} }
@ -734,7 +745,7 @@ mod test {
.unwrap(); .unwrap();
assert_eq!(result.selected.len(), 3); assert_eq!(result.selected.len(), 3);
assert_eq!(result.selected_amount, 300_000); assert_eq!(result.selected_amount(), 300_000);
assert_eq!(result.fee_amount, 254.0); assert_eq!(result.fee_amount, 254.0);
} }
@ -755,7 +766,7 @@ mod test {
.unwrap(); .unwrap();
assert_eq!(result.selected.len(), 2); assert_eq!(result.selected.len(), 2);
assert_eq!(result.selected_amount, 300_000); assert_eq!(result.selected_amount(), 300_000);
assert_eq!(result.fee_amount, 186.0); assert_eq!(result.fee_amount, 186.0);
} }
@ -812,7 +823,7 @@ mod test {
.unwrap(); .unwrap();
assert_eq!(result.selected.len(), 1); assert_eq!(result.selected.len(), 1);
assert_eq!(result.selected_amount, 100_000); assert_eq!(result.selected_amount(), 100_000);
let input_size = (TXIN_BASE_WEIGHT as f32) / 4.0 + 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; let epsilon = 0.5;
assert!((1.0 - (result.fee_amount / input_size)).abs() < epsilon); assert!((1.0 - (result.fee_amount / input_size)).abs() < epsilon);
@ -837,7 +848,7 @@ mod test {
0.0, 0.0,
) )
.unwrap(); .unwrap();
assert_eq!(result.selected_amount, target_amount); assert_eq!(result.selected_amount(), target_amount);
} }
} }
@ -847,7 +858,7 @@ mod test {
let fee_rate = FeeRate::from_sat_per_vb(10.0); let fee_rate = FeeRate::from_sat_per_vb(10.0);
let utxos: Vec<OutputGroup> = get_test_utxos() let utxos: Vec<OutputGroup> = get_test_utxos()
.into_iter() .into_iter()
.map(|u| OutputGroup::new(u.0, u.1, fee_rate)) .map(|u| OutputGroup::new(u, fee_rate))
.collect(); .collect();
let curr_available_value = utxos let curr_available_value = utxos
@ -875,7 +886,7 @@ mod test {
let fee_rate = FeeRate::from_sat_per_vb(10.0); let fee_rate = FeeRate::from_sat_per_vb(10.0);
let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000) let utxos: Vec<OutputGroup> = generate_same_value_utxos(100_000, 100_000)
.into_iter() .into_iter()
.map(|u| OutputGroup::new(u.0, u.1, fee_rate)) .map(|u| OutputGroup::new(u, fee_rate))
.collect(); .collect();
let curr_available_value = utxos let curr_available_value = utxos
@ -908,7 +919,7 @@ mod test {
let utxos: Vec<_> = generate_same_value_utxos(50_000, 10) let utxos: Vec<_> = generate_same_value_utxos(50_000, 10)
.into_iter() .into_iter()
.map(|u| OutputGroup::new(u.0, u.1, fee_rate)) .map(|u| OutputGroup::new(u, fee_rate))
.collect(); .collect();
let curr_value = 0; let curr_value = 0;
@ -933,7 +944,7 @@ mod test {
) )
.unwrap(); .unwrap();
assert_eq!(result.fee_amount, 186.0); assert_eq!(result.fee_amount, 186.0);
assert_eq!(result.selected_amount, 100_000); assert_eq!(result.selected_amount(), 100_000);
} }
// TODO: bnb() function should be optimized, and this test should be done with more utxos // TODO: bnb() function should be optimized, and this test should be done with more utxos
@ -946,7 +957,7 @@ mod test {
for _ in 0..200 { for _ in 0..200 {
let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40) let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40)
.into_iter() .into_iter()
.map(|u| OutputGroup::new(u.0, u.1, fee_rate)) .map(|u| OutputGroup::new(u, fee_rate))
.collect(); .collect();
let curr_value = 0; let curr_value = 0;
@ -969,7 +980,7 @@ mod test {
0.0, 0.0,
) )
.unwrap(); .unwrap();
assert_eq!(result.selected_amount, target_amount); assert_eq!(result.selected_amount(), target_amount);
} }
} }
@ -983,7 +994,7 @@ mod test {
let fee_rate = FeeRate::from_sat_per_vb(1.0); let fee_rate = FeeRate::from_sat_per_vb(1.0);
let utxos: Vec<OutputGroup> = utxos let utxos: Vec<OutputGroup> = utxos
.into_iter() .into_iter()
.map(|u| OutputGroup::new(u.0, u.1, fee_rate)) .map(|u| OutputGroup::new(u, fee_rate))
.collect(); .collect();
let result = BranchAndBoundCoinSelection::default().single_random_draw( let result = BranchAndBoundCoinSelection::default().single_random_draw(
@ -994,7 +1005,7 @@ mod test {
50.0, 50.0,
); );
assert!(result.selected_amount > target_amount); assert!(result.selected_amount() > target_amount);
assert_eq!( assert_eq!(
result.fee_amount, result.fee_amount,
50.0 + result.selected.len() as f32 * 68.0 50.0 + result.selected.len() as f32 * 68.0

View File

@ -513,11 +513,7 @@ where
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
)?; )?;
let coin_selection::CoinSelectionResult { let coin_selection = coin_selection.coin_select(
selected,
selected_amount,
mut fee_amount,
} = coin_selection.coin_select(
self.database.borrow().deref(), self.database.borrow().deref(),
required_utxos, required_utxos,
optional_utxos, optional_utxos,
@ -525,10 +521,13 @@ where
outgoing, outgoing,
fee_amount, fee_amount,
)?; )?;
tx.input = selected let mut fee_amount = coin_selection.fee_amount;
tx.input = coin_selection
.selected
.iter() .iter()
.map(|u| bitcoin::TxIn { .map(|u| bitcoin::TxIn {
previous_output: u.outpoint, previous_output: u.outpoint(),
script_sig: Script::default(), script_sig: Script::default(),
sequence: n_sequence, sequence: n_sequence,
witness: vec![], witness: vec![],
@ -550,9 +549,8 @@ where
Some(change_output) Some(change_output)
} }
}; };
let mut fee_amount = fee_amount.ceil() as u64; let mut fee_amount = fee_amount.ceil() as u64;
let change_val = (selected_amount - outgoing).saturating_sub(fee_amount); let change_val = (coin_selection.selected_amount() - outgoing).saturating_sub(fee_amount);
match change_output { match change_output {
None if change_val.is_dust() => { None if change_val.is_dust() => {
@ -588,14 +586,15 @@ where
params.ordering.sort_tx(&mut tx); params.ordering.sort_tx(&mut tx);
let txid = tx.txid(); let txid = tx.txid();
let psbt = self.complete_transaction(tx, selected, params)?; let sent = coin_selection.local_selected_amount();
let psbt = self.complete_transaction(tx, coin_selection.selected, params)?;
let transaction_details = TransactionDetails { let transaction_details = TransactionDetails {
transaction: None, transaction: None,
txid, txid,
timestamp: time::get_timestamp(), timestamp: time::get_timestamp(),
received, received,
sent: selected_amount, sent,
fees: fee_amount, fees: fee_amount,
height: None, height: None,
}; };
@ -705,7 +704,10 @@ where
keychain, keychain,
}; };
Ok((utxo, weight)) Ok(WeightedUtxo {
satisfaction_weight: weight,
utxo: Utxo::Local(utxo),
})
}) })
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
@ -1039,18 +1041,18 @@ where
&self, &self,
change_policy: tx_builder::ChangeSpendPolicy, change_policy: tx_builder::ChangeSpendPolicy,
unspendable: &HashSet<OutPoint>, unspendable: &HashSet<OutPoint>,
manually_selected: Vec<(LocalUtxo, usize)>, manually_selected: Vec<WeightedUtxo>,
must_use_all_available: bool, must_use_all_available: bool,
manual_only: bool, manual_only: bool,
must_only_use_confirmed_tx: bool, must_only_use_confirmed_tx: bool,
) -> Result<(Vec<(LocalUtxo, usize)>, Vec<(LocalUtxo, usize)>), Error> { ) -> Result<(Vec<WeightedUtxo>, Vec<WeightedUtxo>), Error> {
// must_spend <- manually selected utxos // must_spend <- manually selected utxos
// may_spend <- all other available utxos // may_spend <- all other available utxos
let mut may_spend = self.get_available_utxos()?; let mut may_spend = self.get_available_utxos()?;
may_spend.retain(|may_spend| { may_spend.retain(|may_spend| {
manually_selected manually_selected
.iter() .iter()
.find(|manually_selected| manually_selected.0.outpoint == may_spend.0.outpoint) .find(|manually_selected| manually_selected.utxo.outpoint() == may_spend.0.outpoint)
.is_none() .is_none()
}); });
let mut must_spend = manually_selected; let mut must_spend = manually_selected;
@ -1088,6 +1090,14 @@ where
retain retain
}); });
let mut may_spend = may_spend
.into_iter()
.map(|(local_utxo, satisfaction_weight)| WeightedUtxo {
satisfaction_weight,
utxo: Utxo::Local(local_utxo),
})
.collect();
if must_use_all_available { if must_use_all_available {
must_spend.append(&mut may_spend); must_spend.append(&mut may_spend);
} }
@ -1098,7 +1108,7 @@ where
fn complete_transaction( fn complete_transaction(
&self, &self,
tx: Transaction, tx: Transaction,
selected: Vec<LocalUtxo>, selected: Vec<Utxo>,
params: TxParams, params: TxParams,
) -> Result<PSBT, Error> { ) -> Result<PSBT, Error> {
use bitcoin::util::psbt::serialize::Serialize; use bitcoin::util::psbt::serialize::Serialize;
@ -1131,9 +1141,9 @@ where
} }
} }
let lookup_output = selected let mut lookup_output = selected
.into_iter() .into_iter()
.map(|utxo| (utxo.outpoint, utxo)) .map(|utxo| (utxo.outpoint(), utxo))
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
// add metadata for the inputs // add metadata for the inputs
@ -1142,7 +1152,7 @@ where
.iter_mut() .iter_mut()
.zip(psbt.global.unsigned_tx.input.iter()) .zip(psbt.global.unsigned_tx.input.iter())
{ {
let utxo = match lookup_output.get(&input.previous_output) { let utxo = match lookup_output.remove(&input.previous_output) {
Some(utxo) => utxo, Some(utxo) => utxo,
None => continue, None => continue,
}; };
@ -1153,32 +1163,50 @@ where
psbt_input.sighash_type = Some(sighash_type); psbt_input.sighash_type = Some(sighash_type);
} }
// Try to find the prev_script in our db to figure out if this is internal or external, match utxo {
// and the derivation index Utxo::Local(utxo) => {
let (keychain, child) = match self // Try to find the prev_script in our db to figure out if this is internal or external,
.database // and the derivation index
.borrow() let (keychain, child) = match self
.get_path_from_script_pubkey(&utxo.txout.script_pubkey)? .database
{ .borrow()
Some(x) => x, .get_path_from_script_pubkey(&utxo.txout.script_pubkey)?
None => continue, {
}; Some(x) => x,
None => continue,
};
let (desc, _) = self._get_descriptor_for_keychain(keychain); let desc = self.get_descriptor_for_keychain(keychain);
let derived_descriptor = desc.as_derived(child, &self.secp); let derived_descriptor = desc.as_derived(child, &self.secp);
psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp)?; psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp)?;
psbt_input.redeem_script = derived_descriptor.psbt_redeem_script(); psbt_input.redeem_script = derived_descriptor.psbt_redeem_script();
psbt_input.witness_script = derived_descriptor.psbt_witness_script(); psbt_input.witness_script = derived_descriptor.psbt_witness_script();
let prev_output = input.previous_output; let prev_output = input.previous_output;
if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? { if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? {
if desc.is_witness() { if desc.is_witness() {
psbt_input.witness_utxo = psbt_input.witness_utxo =
Some(prev_tx.output[prev_output.vout as usize].clone()); Some(prev_tx.output[prev_output.vout as usize].clone());
}
if !desc.is_witness() || params.force_non_witness_utxo {
psbt_input.non_witness_utxo = Some(prev_tx);
}
}
} }
if !desc.is_witness() || params.force_non_witness_utxo { Utxo::Foreign {
psbt_input.non_witness_utxo = Some(prev_tx); psbt_input: foreign_psbt_input,
outpoint,
} => {
if params.force_non_witness_utxo
&& foreign_psbt_input.non_witness_utxo.is_none()
{
return Err(Error::Generic(format!(
"Missing non_witness_utxo on foreign utxo {}",
outpoint
)));
}
*psbt_input = *foreign_psbt_input;
} }
} }
} }
@ -1348,7 +1376,7 @@ where
mod test { mod test {
use std::str::FromStr; use std::str::FromStr;
use bitcoin::Network; use bitcoin::{util::psbt, Network};
use crate::database::memory::MemoryDatabase; use crate::database::memory::MemoryDatabase;
use crate::database::Database; use crate::database::Database;
@ -2237,6 +2265,182 @@ mod test {
assert_eq!(psbt.global.unknown.get(&psbt_key), Some(&value_bytes)); assert_eq!(psbt.global.unknown.get(&psbt_key), Some(&value_bytes));
} }
#[test]
fn test_add_foreign_utxo() {
let (wallet1, _, _) = get_funded_wallet(get_test_wpkh());
let (wallet2, _, _) =
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let utxo = wallet2.list_unspent().unwrap().remove(0);
let foreign_utxo_satisfaction = wallet2
.get_descriptor_for_keychain(KeychainKind::External)
.max_satisfaction_weight()
.unwrap();
let psbt_input = psbt::Input {
witness_utxo: Some(utxo.txout.clone()),
..Default::default()
};
let mut builder = wallet1.build_tx();
builder
.add_recipient(addr.script_pubkey(), 60_000)
.add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction)
.unwrap();
let (psbt, details) = builder.finish().unwrap();
assert_eq!(
details.sent - details.received,
10_000 + details.fees,
"we should have only net spent ~10_000"
);
assert!(
psbt.global
.unsigned_tx
.input
.iter()
.find(|input| input.previous_output == utxo.outpoint)
.is_some(),
"foreign_utxo should be in there"
);
let (psbt, finished) = wallet1.sign(psbt, None).unwrap();
assert!(
!finished,
"only one of the inputs should have been signed so far"
);
let (_, finished) = wallet2.sign(psbt, None).unwrap();
assert!(finished, "all the inputs should have been signed now");
}
#[test]
#[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")]
fn test_add_foreign_utxo_invalid_psbt_input() {
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let mut builder = wallet.build_tx();
let outpoint = wallet.list_unspent().unwrap()[0].outpoint;
let foreign_utxo_satisfaction = wallet
.get_descriptor_for_keychain(KeychainKind::External)
.max_satisfaction_weight()
.unwrap();
builder
.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction)
.unwrap();
}
#[test]
fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() {
let (wallet1, _, txid1) = get_funded_wallet(get_test_wpkh());
let (wallet2, _, txid2) =
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
let utxo2 = wallet2.list_unspent().unwrap().remove(0);
let tx1 = wallet1
.database
.borrow()
.get_tx(&txid1, true)
.unwrap()
.unwrap()
.transaction
.unwrap();
let tx2 = wallet2
.database
.borrow()
.get_tx(&txid2, true)
.unwrap()
.unwrap()
.transaction
.unwrap();
let satisfaction_weight = wallet2
.get_descriptor_for_keychain(KeychainKind::External)
.max_satisfaction_weight()
.unwrap();
let psbt_input1 = psbt::Input {
non_witness_utxo: Some(tx1),
..Default::default()
};
let psbt_input2 = psbt::Input {
non_witness_utxo: Some(tx2),
..Default::default()
};
let mut builder = wallet1.build_tx();
assert!(
builder
.add_foreign_utxo(utxo2.outpoint, psbt_input1, satisfaction_weight)
.is_err(),
"should fail when outpoint doesn't match psbt_input"
);
assert!(
builder
.add_foreign_utxo(utxo2.outpoint, psbt_input2, satisfaction_weight)
.is_ok(),
"shoulld be ok when outpoing does match psbt_input"
);
}
#[test]
fn test_add_foreign_utxo_force_non_witness_utxo() {
let (wallet1, _, _) = get_funded_wallet(get_test_wpkh());
let (wallet2, _, txid2) =
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let utxo2 = wallet2.list_unspent().unwrap().remove(0);
let satisfaction_weight = wallet2
.get_descriptor_for_keychain(KeychainKind::External)
.max_satisfaction_weight()
.unwrap();
let mut builder = wallet1.build_tx();
builder
.add_recipient(addr.script_pubkey(), 60_000)
.force_non_witness_utxo();
{
let mut builder = builder.clone();
let psbt_input = psbt::Input {
witness_utxo: Some(utxo2.txout.clone()),
..Default::default()
};
builder
.add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
.unwrap();
assert!(
builder.finish().is_err(),
"psbt_input with witness_utxo should succeed with witness_utxo"
);
}
{
let mut builder = builder.clone();
let tx2 = wallet2
.database
.borrow()
.get_tx(&txid2, true)
.unwrap()
.unwrap()
.transaction
.unwrap();
let psbt_input = psbt::Input {
non_witness_utxo: Some(tx2),
..Default::default()
};
builder
.add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight)
.unwrap();
assert!(
builder.finish().is_ok(),
"psbt_input with non_witness_utxo should succeed with force_non_witness_utxo"
);
}
}
#[test] #[test]
#[should_panic( #[should_panic(
expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")" expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")"

View File

@ -54,15 +54,15 @@ use std::collections::HashSet;
use std::default::Default; use std::default::Default;
use std::marker::PhantomData; use std::marker::PhantomData;
use bitcoin::util::psbt::PartiallySignedTransaction as PSBT; use bitcoin::util::psbt::{self, PartiallySignedTransaction as PSBT};
use bitcoin::{OutPoint, Script, SigHashType, Transaction}; use bitcoin::{OutPoint, Script, SigHashType, Transaction};
use miniscript::descriptor::DescriptorTrait; use miniscript::descriptor::DescriptorTrait;
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
use crate::{database::BatchDatabase, Error, Wallet}; use crate::{database::BatchDatabase, Error, Utxo, Wallet};
use crate::{ use crate::{
types::{FeeRate, KeychainKind, LocalUtxo}, types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo},
TransactionDetails, TransactionDetails,
}; };
/// Context in which the [`TxBuilder`] is valid /// Context in which the [`TxBuilder`] is valid
@ -150,7 +150,7 @@ pub(crate) struct TxParams {
pub(crate) fee_policy: Option<FeePolicy>, pub(crate) fee_policy: Option<FeePolicy>,
pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>, pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>,
pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>, pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
pub(crate) utxos: Vec<(LocalUtxo, usize)>, pub(crate) utxos: Vec<WeightedUtxo>,
pub(crate) unspendable: HashSet<OutPoint>, pub(crate) unspendable: HashSet<OutPoint>,
pub(crate) manually_selected_only: bool, pub(crate) manually_selected_only: bool,
pub(crate) sighash: Option<SigHashType>, pub(crate) sighash: Option<SigHashType>,
@ -297,7 +297,10 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
for utxo in utxos { for utxo in utxos {
let descriptor = self.wallet.get_descriptor_for_keychain(utxo.keychain); let descriptor = self.wallet.get_descriptor_for_keychain(utxo.keychain);
let satisfaction_weight = descriptor.max_satisfaction_weight().unwrap(); let satisfaction_weight = descriptor.max_satisfaction_weight().unwrap();
self.params.utxos.push((utxo, satisfaction_weight)); self.params.utxos.push(WeightedUtxo {
satisfaction_weight,
utxo: Utxo::Local(utxo),
});
} }
Ok(self) Ok(self)
@ -311,6 +314,84 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
self.add_utxos(&[outpoint]) self.add_utxos(&[outpoint])
} }
/// Add a foreign UTXO i.e. A UTXO not owned by this wallet.
///
/// At a minimum to add a foreign UTXO we need:
///
/// 1. `outpoint`: To add it to the raw transaction.
/// 2. `psbt_input`: To know the value.
/// 3. `satisfaction_weight`: To know how much weight/vbytes the input will add to the transaction for fee calculation.
///
/// There are several security concerns about adding foregin UTXOs that application
/// developers should consider. First, how do you know the value of the input is correct? If a
/// `non_witness_utxo` is provided in the `psbt_input` then this method implicitly verifies the
/// value by checking it against the transaction. If only a `wintess_utxo` is provided then this
/// method doesn't verify the value but just takes it as a given -- it is up to you to check
/// that whoever sent you the `input_psbt` was not lying!
///
/// Secondly, you must somehow provide `satisfaction_weight` of the input. Depending on your
/// application it may be important that this be known precisely. If not, a malicious
/// counterparty may fool you into putting in a value that is too low, giving the transaction a
/// lower than expected feerate. They could also fool you into putting a value that is too high
/// causing you to pay a fee that is too high. The party who is broadcasting the transaction can
/// of course check the real input weight matches the expected weight prior to broadcasting.
///
/// To guarantee the `satisfaction_weight` is correct, you can require the party providing the
/// `psbt_input` provide a miniscript descriptor for the input so you can check it against the
/// `script_pubkey` and then ask it for the [`max_satisfaction_weight`].
///
/// This is an **EXPERIMENTAL** feature, API and other major changes are expected.
///
/// # Errors
///
/// This method returns errors in the following circumstances:
///
/// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
/// 2. The data in `non_witness_utxo` does not match what is in `outpoint`.
///
/// Note if you set [`force_non_witness_utxo`] any `psbt_input` you pass to this method must
/// have `non_witness_utxo` set otherwise you will get an error when [`finish`] is called.
///
/// [`force_non_witness_utxo`]: Self::force_non_witness_utxo
/// [`finish`]: Self::finish
/// [`max_satisfaction_weight`]: miniscript::Descriptor::max_satisfaction_weight
pub fn add_foreign_utxo(
&mut self,
outpoint: OutPoint,
psbt_input: psbt::Input,
satisfaction_weight: usize,
) -> Result<&mut Self, Error> {
if psbt_input.witness_utxo.is_none() {
match psbt_input.non_witness_utxo.as_ref() {
Some(tx) => {
if tx.txid() != outpoint.txid {
return Err(Error::Generic(
"Foreign utxo outpoint does not match PSBT input".into(),
));
}
if tx.output.len() <= outpoint.vout as usize {
return Err(Error::InvalidOutpoint(outpoint));
}
}
None => {
return Err(Error::Generic(
"Foreign utxo missing witness_utxo or non_witness_utxo".into(),
))
}
}
}
self.params.utxos.push(WeightedUtxo {
satisfaction_weight,
utxo: Utxo::Foreign {
outpoint,
psbt_input: Box::new(psbt_input),
},
});
Ok(self)
}
/// Only spend utxos added by [`add_utxo`]. /// Only spend utxos added by [`add_utxo`].
/// ///
/// The wallet will **not** add additional utxos to the transaction even if they are needed to /// The wallet will **not** add additional utxos to the transaction even if they are needed to