diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f14b2cb..98cf3dba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,7 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 type to mark for a missing client. - 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 final transaction is created by calling `finish` on the builder. @@ -65,6 +65,13 @@ final transaction is created by calling `finish` on the builder. - Added `Wallet::get_utxo` - 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 #### Changed - Remove `cli.rs` module, `cli-utils` feature and `repl.rs` example; moved to new [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli) repository diff --git a/src/blockchain/compact_filters/mod.rs b/src/blockchain/compact_filters/mod.rs index 649c53df..38c4d180 100644 --- a/src/blockchain/compact_filters/mod.rs +++ b/src/blockchain/compact_filters/mod.rs @@ -83,7 +83,7 @@ mod sync; use super::{Blockchain, Capability, ConfigurableBlockchain, Progress}; use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils}; use crate::error::Error; -use crate::types::{KeychainKind, TransactionDetails, UTXO}; +use crate::types::{KeychainKind, LocalUtxo, TransactionDetails}; use crate::FeeRate; use peer::*; @@ -194,7 +194,7 @@ impl CompactFiltersBlockchain { database.get_path_from_script_pubkey(&output.script_pubkey)? { debug!("{} output #{} is mine, adding utxo", tx.txid(), i); - updates.set_utxo(&UTXO { + updates.set_utxo(&LocalUtxo { outpoint: OutPoint::new(tx.txid(), i as u32), txout: output.clone(), keychain, diff --git a/src/blockchain/utils.rs b/src/blockchain/utils.rs index 2e32ed1b..5fc27434 100644 --- a/src/blockchain/utils.rs +++ b/src/blockchain/utils.rs @@ -34,7 +34,7 @@ use bitcoin::{BlockHeader, OutPoint, Script, Transaction, Txid}; use super::*; use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils}; use crate::error::Error; -use crate::types::{KeychainKind, TransactionDetails, UTXO}; +use crate::types::{KeychainKind, LocalUtxo, TransactionDetails}; use crate::wallet::time::Instant; use crate::wallet::utils::ChunksIterator; @@ -353,7 +353,7 @@ fn save_transaction_details_and_utxos( // this output is ours, we have a path to derive it if let Some((keychain, _child)) = db.get_path_from_script_pubkey(&output.script_pubkey)? { debug!("{} output #{} is mine, adding utxo", txid, i); - updates.set_utxo(&UTXO { + updates.set_utxo(&LocalUtxo { outpoint: OutPoint::new(tx.txid(), i as u32), txout: output.clone(), keychain, diff --git a/src/database/any.rs b/src/database/any.rs index 021ab7d6..a332b8d5 100644 --- a/src/database/any.rs +++ b/src/database/any.rs @@ -133,7 +133,7 @@ impl BatchOperations for AnyDatabase { child ) } - fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error> { + fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> { impl_inner_method!(AnyDatabase, self, set_utxo, utxo) } fn set_raw_tx(&mut self, transaction: &Transaction) -> Result<(), Error> { @@ -165,7 +165,7 @@ impl BatchOperations for AnyDatabase { ) -> Result, Error> { impl_inner_method!(AnyDatabase, self, del_path_from_script_pubkey, script) } - fn del_utxo(&mut self, outpoint: &OutPoint) -> Result, Error> { + fn del_utxo(&mut self, outpoint: &OutPoint) -> Result, Error> { impl_inner_method!(AnyDatabase, self, del_utxo, outpoint) } fn del_raw_tx(&mut self, txid: &Txid) -> Result, Error> { @@ -201,7 +201,7 @@ impl Database for AnyDatabase { fn iter_script_pubkeys(&self, keychain: Option) -> Result, Error> { impl_inner_method!(AnyDatabase, self, iter_script_pubkeys, keychain) } - fn iter_utxos(&self) -> Result, Error> { + fn iter_utxos(&self) -> Result, Error> { impl_inner_method!(AnyDatabase, self, iter_utxos) } fn iter_raw_txs(&self) -> Result, Error> { @@ -230,7 +230,7 @@ impl Database for AnyDatabase { ) -> Result, Error> { impl_inner_method!(AnyDatabase, self, get_path_from_script_pubkey, script) } - fn get_utxo(&self, outpoint: &OutPoint) -> Result, Error> { + fn get_utxo(&self, outpoint: &OutPoint) -> Result, Error> { impl_inner_method!(AnyDatabase, self, get_utxo, outpoint) } fn get_raw_tx(&self, txid: &Txid) -> Result, Error> { @@ -257,7 +257,7 @@ impl BatchOperations for AnyBatch { ) -> Result<(), Error> { impl_inner_method!(AnyBatch, self, set_script_pubkey, script, keychain, child) } - fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error> { + fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> { impl_inner_method!(AnyBatch, self, set_utxo, utxo) } fn set_raw_tx(&mut self, transaction: &Transaction) -> Result<(), Error> { @@ -283,7 +283,7 @@ impl BatchOperations for AnyBatch { ) -> Result, Error> { impl_inner_method!(AnyBatch, self, del_path_from_script_pubkey, script) } - fn del_utxo(&mut self, outpoint: &OutPoint) -> Result, Error> { + fn del_utxo(&mut self, outpoint: &OutPoint) -> Result, Error> { impl_inner_method!(AnyBatch, self, del_utxo, outpoint) } fn del_raw_tx(&mut self, txid: &Txid) -> Result, Error> { diff --git a/src/database/keyvalue.rs b/src/database/keyvalue.rs index b8234978..7b9526a9 100644 --- a/src/database/keyvalue.rs +++ b/src/database/keyvalue.rs @@ -51,7 +51,7 @@ macro_rules! impl_batch_operations { Ok(()) } - fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error> { + fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> { let key = MapKey::UTXO(Some(&utxo.outpoint)).as_map_key(); let value = json!({ "t": utxo.txout, @@ -120,7 +120,7 @@ macro_rules! impl_batch_operations { } } - fn del_utxo(&mut self, outpoint: &OutPoint) -> Result, Error> { + fn del_utxo(&mut self, outpoint: &OutPoint) -> Result, Error> { let key = MapKey::UTXO(Some(outpoint)).as_map_key(); let res = self.remove(key); let res = $process_delete!(res); @@ -132,7 +132,7 @@ macro_rules! impl_batch_operations { let txout = serde_json::from_value(val["t"].take())?; let keychain = serde_json::from_value(val["i"].take())?; - Ok(Some(UTXO { outpoint: outpoint.clone(), txout, keychain })) + Ok(Some(LocalUtxo { outpoint: outpoint.clone(), txout, keychain })) } } } @@ -234,7 +234,7 @@ impl Database for Tree { .collect() } - fn iter_utxos(&self) -> Result, Error> { + fn iter_utxos(&self) -> Result, Error> { let key = MapKey::UTXO(None).as_map_key(); self.scan_prefix(key) .map(|x| -> Result<_, Error> { @@ -245,7 +245,7 @@ impl Database for Tree { let txout = serde_json::from_value(val["t"].take())?; let keychain = serde_json::from_value(val["i"].take())?; - Ok(UTXO { + Ok(LocalUtxo { outpoint, txout, keychain, @@ -305,7 +305,7 @@ impl Database for Tree { .transpose() } - fn get_utxo(&self, outpoint: &OutPoint) -> Result, Error> { + fn get_utxo(&self, outpoint: &OutPoint) -> Result, Error> { let key = MapKey::UTXO(Some(outpoint)).as_map_key(); self.get(key)? .map(|b| -> Result<_, Error> { @@ -313,7 +313,7 @@ impl Database for Tree { let txout = serde_json::from_value(val["t"].take())?; let keychain = serde_json::from_value(val["i"].take())?; - Ok(UTXO { + Ok(LocalUtxo { outpoint: *outpoint, txout, keychain, diff --git a/src/database/memory.rs b/src/database/memory.rs index 84ecf30a..7256f6aa 100644 --- a/src/database/memory.rs +++ b/src/database/memory.rs @@ -157,7 +157,7 @@ impl BatchOperations for MemoryDatabase { Ok(()) } - fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error> { + fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> { let key = MapKey::UTXO(Some(&utxo.outpoint)).as_map_key(); self.map .insert(key, Box::new((utxo.txout.clone(), utxo.keychain))); @@ -223,7 +223,7 @@ impl BatchOperations for MemoryDatabase { } } } - fn del_utxo(&mut self, outpoint: &OutPoint) -> Result, Error> { + fn del_utxo(&mut self, outpoint: &OutPoint) -> Result, Error> { let key = MapKey::UTXO(Some(outpoint)).as_map_key(); let res = self.map.remove(&key); self.deleted_keys.push(key); @@ -232,7 +232,7 @@ impl BatchOperations for MemoryDatabase { None => Ok(None), Some(b) => { let (txout, keychain) = b.downcast_ref().cloned().unwrap(); - Ok(Some(UTXO { + Ok(Some(LocalUtxo { outpoint: *outpoint, txout, keychain, @@ -316,14 +316,14 @@ impl Database for MemoryDatabase { .collect() } - fn iter_utxos(&self) -> Result, Error> { + fn iter_utxos(&self) -> Result, Error> { let key = MapKey::UTXO(None).as_map_key(); self.map .range::, _>((Included(&key), Excluded(&after(&key)))) .map(|(k, v)| { let outpoint = deserialize(&k[1..]).unwrap(); let (txout, keychain) = v.downcast_ref().cloned().unwrap(); - Ok(UTXO { + Ok(LocalUtxo { outpoint, txout, keychain, @@ -382,11 +382,11 @@ impl Database for MemoryDatabase { })) } - fn get_utxo(&self, outpoint: &OutPoint) -> Result, Error> { + fn get_utxo(&self, outpoint: &OutPoint) -> Result, Error> { let key = MapKey::UTXO(Some(outpoint)).as_map_key(); Ok(self.map.get(&key).map(|b| { let (txout, keychain) = b.downcast_ref().cloned().unwrap(); - UTXO { + LocalUtxo { outpoint: *outpoint, txout, keychain, @@ -502,7 +502,7 @@ macro_rules! populate_test_db { db.set_tx(&tx_details).unwrap(); for (vout, out) in tx.output.iter().enumerate() { - db.set_utxo(&UTXO { + db.set_utxo(&LocalUtxo { txout: out.clone(), outpoint: OutPoint { txid, diff --git a/src/database/mod.rs b/src/database/mod.rs index 33ec7ccf..e8face1e 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -64,8 +64,8 @@ pub trait BatchOperations { keychain: KeychainKind, child: u32, ) -> Result<(), Error>; - /// Store a [`UTXO`] - fn set_utxo(&mut self, utxo: &UTXO) -> Result<(), Error>; + /// Store a [`LocalUtxo`] + fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error>; /// Store a raw transaction fn set_raw_tx(&mut self, transaction: &Transaction) -> Result<(), Error>; /// Store the metadata of a transaction @@ -85,8 +85,8 @@ pub trait BatchOperations { &mut self, script: &Script, ) -> Result, Error>; - /// Delete a [`UTXO`] given its [`OutPoint`] - fn del_utxo(&mut self, outpoint: &OutPoint) -> Result, Error>; + /// Delete a [`LocalUtxo`] given its [`OutPoint`] + fn del_utxo(&mut self, outpoint: &OutPoint) -> Result, Error>; /// Delete a raw transaction given its [`Txid`] fn del_raw_tx(&mut self, txid: &Txid) -> Result, Error>; /// Delete the metadata of a transaction and optionally the raw transaction itself @@ -116,8 +116,8 @@ pub trait Database: BatchOperations { /// Return the list of script_pubkeys fn iter_script_pubkeys(&self, keychain: Option) -> Result, Error>; - /// Return the list of [`UTXO`]s - fn iter_utxos(&self) -> Result, Error>; + /// Return the list of [`LocalUtxo`]s + fn iter_utxos(&self) -> Result, Error>; /// Return the list of raw transactions fn iter_raw_txs(&self) -> Result, Error>; /// Return the list of transactions metadata @@ -134,8 +134,8 @@ pub trait Database: BatchOperations { &self, script: &Script, ) -> Result, Error>; - /// Fetch a [`UTXO`] given its [`OutPoint`] - fn get_utxo(&self, outpoint: &OutPoint) -> Result, Error>; + /// Fetch a [`LocalUtxo`] given its [`OutPoint`] + fn get_utxo(&self, outpoint: &OutPoint) -> Result, Error>; /// Fetch a raw transaction given its [`Txid`] fn get_raw_tx(&self, txid: &Txid) -> Result, Error>; /// Fetch the transaction metadata and optionally also the raw transaction @@ -298,7 +298,7 @@ pub mod test { value: 133742, script_pubkey: script, }; - let utxo = UTXO { + let utxo = LocalUtxo { txout, outpoint, keychain: KeychainKind::External, diff --git a/src/types.rs b/src/types.rs index 4a60e2d9..9873150a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -25,7 +25,7 @@ use std::convert::AsRef; use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut}; -use bitcoin::hash_types::Txid; +use bitcoin::{hash_types::Txid, util::psbt}; use serde::{Deserialize, Serialize}; @@ -90,9 +90,11 @@ 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)] -pub struct UTXO { +pub struct LocalUtxo { /// Reference to a transaction output pub outpoint: OutPoint, /// Transaction output @@ -101,6 +103,64 @@ pub struct UTXO { pub keychain: KeychainKind, } +/// A [`Utxo`] with its `satisfaction_weight`. +#[derive(Debug, Clone, PartialEq)] +pub struct WeightedUtxo { + /// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to + /// properly maintain the feerate when adding this input to a transaction during coin selection. + /// + /// [weight units]: https://en.bitcoin.it/wiki/Weight_units + 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, + }, +} + +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 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] pub struct TransactionDetails { diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index c57f251d..f0b0bb7a 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -50,8 +50,8 @@ //! fn coin_select( //! &self, //! database: &D, -//! required_utxos: Vec<(UTXO, usize)>, -//! optional_utxos: Vec<(UTXO, usize)>, +//! required_utxos: Vec, +//! optional_utxos: Vec, //! fee_rate: FeeRate, //! amount_needed: u64, //! fee_amount: f32, @@ -60,11 +60,10 @@ //! let mut additional_weight = 0; //! let all_utxos_selected = required_utxos //! .into_iter().chain(optional_utxos) -//! .scan((&mut selected_amount, &mut additional_weight), |(selected_amount, additional_weight), (utxo, weight)| { -//! **selected_amount += utxo.txout.value; -//! **additional_weight += TXIN_BASE_WEIGHT + weight; -//! -//! Some(utxo) +//! .scan((&mut selected_amount, &mut additional_weight), |(selected_amount, additional_weight), weighted_utxo| { +//! **selected_amount += weighted_utxo.utxo.txout().value; +//! **additional_weight += TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight; +//! Some(weighted_utxo.utxo) //! }) //! .collect::>(); //! let additional_fees = additional_weight as f32 * fee_rate.as_sat_vb() / 4.0; @@ -75,7 +74,6 @@ //! //! Ok(CoinSelectionResult { //! selected: all_utxos_selected, -//! selected_amount, //! fee_amount: fee_amount + additional_fees, //! }) //! } @@ -97,9 +95,9 @@ //! # Ok::<(), bdk::Error>(()) //! ``` -use crate::database::Database; -use crate::error::Error; -use crate::types::{FeeRate, UTXO}; +use crate::types::FeeRate; +use crate::{database::Database, WeightedUtxo}; +use crate::{error::Error, Utxo}; use rand::seq::SliceRandom; #[cfg(not(test))] @@ -122,13 +120,29 @@ pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4 + 1) * 4; #[derive(Debug)] pub struct CoinSelectionResult { /// List of outputs selected for use as inputs - pub selected: Vec, - /// Sum of the selected inputs' value - pub selected_amount: u64, + pub selected: Vec, /// Total fee amount in satoshi 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 /// /// This trait can be implemented to make the [`Wallet`](super::Wallet) use a customized coin @@ -151,8 +165,8 @@ pub trait CoinSelectionAlgorithm: std::fmt::Debug { fn coin_select( &self, database: &D, - required_utxos: Vec<(UTXO, usize)>, - optional_utxos: Vec<(UTXO, usize)>, + required_utxos: Vec, + optional_utxos: Vec, fee_rate: FeeRate, amount_needed: u64, fee_amount: f32, @@ -163,15 +177,15 @@ pub trait CoinSelectionAlgorithm: std::fmt::Debug { /// /// This coin selection algorithm sorts the available UTXOs by value and then picks them starting /// from the largest ones until the required amount is reached. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone, Copy)] pub struct LargestFirstCoinSelection; impl CoinSelectionAlgorithm for LargestFirstCoinSelection { fn coin_select( &self, _database: &D, - required_utxos: Vec<(UTXO, usize)>, - mut optional_utxos: Vec<(UTXO, usize)>, + required_utxos: Vec, + mut optional_utxos: Vec, fee_rate: FeeRate, amount_needed: u64, mut fee_amount: f32, @@ -188,7 +202,7 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { // We put the "required UTXOs" first and make sure the optional UTXOs are sorted, // initially smallest to largest, before being reversed with `.rev()`. 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 .into_iter() .map(|utxo| (true, utxo)) @@ -201,18 +215,19 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { let selected = utxos .scan( (&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) { - **fee_amount += calc_fee_bytes(TXIN_BASE_WEIGHT + weight); - **selected_amount += utxo.txout.value; + **fee_amount += + calc_fee_bytes(TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight); + **selected_amount += weighted_utxo.utxo.txout().value; log::debug!( "Selected {}, updated fee_amount = `{}`", - utxo.outpoint, + weighted_utxo.utxo.outpoint(), fee_amount ); - Some(utxo) + Some(weighted_utxo.utxo) } else { None } @@ -231,7 +246,6 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { Ok(CoinSelectionResult { selected, fee_amount, - selected_amount, }) } } @@ -239,9 +253,7 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { #[derive(Debug, Clone)] // Adds fee information to an UTXO. struct OutputGroup { - utxo: UTXO, - // weight needed to satisfy the UTXO, as described in `Descriptor::max_satisfaction_weight` - satisfaction_weight: usize, + weighted_utxo: WeightedUtxo, // Amount of fees for spending a certain utxo, calculated using a certain FeeRate fee: f32, // 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 { - fn new(utxo: UTXO, satisfaction_weight: usize, fee_rate: FeeRate) -> Self { - let fee = (TXIN_BASE_WEIGHT + satisfaction_weight) as f32 / 4.0 * fee_rate.as_sat_vb(); - let effective_value = utxo.txout.value as i64 - fee.ceil() as i64; + fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self { + let fee = (TXIN_BASE_WEIGHT + weighted_utxo.satisfaction_weight) as f32 / 4.0 + * fee_rate.as_sat_vb(); + let effective_value = weighted_utxo.utxo.txout().value as i64 - fee.ceil() as i64; OutputGroup { - utxo, - satisfaction_weight, + weighted_utxo, effective_value, fee, } @@ -291,8 +303,8 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { fn coin_select( &self, _database: &D, - required_utxos: Vec<(UTXO, usize)>, - optional_utxos: Vec<(UTXO, usize)>, + required_utxos: Vec, + optional_utxos: Vec, fee_rate: FeeRate, amount_needed: u64, fee_amount: f32, @@ -300,7 +312,7 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { // Mapping every (UTXO, usize) to an output group let required_utxos: Vec = required_utxos .into_iter() - .map(|u| OutputGroup::new(u.0, u.1, fee_rate)) + .map(|u| OutputGroup::new(u, fee_rate)) .collect(); // Mapping every (UTXO, usize) to an output group. @@ -308,7 +320,7 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { // adding them is more than their value let optional_utxos: Vec = optional_utxos .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) .collect(); @@ -507,14 +519,12 @@ impl BranchAndBoundCoinSelection { fee_amount += selected_utxos.iter().map(|u| u.fee).sum::(); let selected = selected_utxos .into_iter() - .map(|u| u.utxo) + .map(|u| u.weighted_utxo.utxo) .collect::>(); - let selected_amount = selected.iter().map(|u| u.txout.value).sum(); CoinSelectionResult { selected, fee_amount, - selected_amount, } } } @@ -535,10 +545,11 @@ mod test { const P2WPKH_WITNESS_SIZE: usize = 73 + 33 + 2; - fn get_test_utxos() -> Vec<(UTXO, usize)> { + fn get_test_utxos() -> Vec { vec![ - ( - UTXO { + WeightedUtxo { + satisfaction_weight: P2WPKH_WITNESS_SIZE, + utxo: Utxo::Local(LocalUtxo { outpoint: OutPoint::from_str( "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", ) @@ -548,11 +559,11 @@ mod test { script_pubkey: Script::new(), }, keychain: KeychainKind::External, - }, - P2WPKH_WITNESS_SIZE, - ), - ( - UTXO { + }), + }, + WeightedUtxo { + satisfaction_weight: P2WPKH_WITNESS_SIZE, + utxo: Utxo::Local(LocalUtxo { outpoint: OutPoint::from_str( "65d92ddff6b6dc72c89624a6491997714b90f6004f928d875bc0fd53f264fa85:0", ) @@ -562,17 +573,17 @@ mod test { script_pubkey: Script::new(), }, keychain: KeychainKind::Internal, - }, - P2WPKH_WITNESS_SIZE, - ), + }), + }, ] } - fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<(UTXO, usize)> { + fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec { let mut res = Vec::new(); for _ in 0..utxos_number { - res.push(( - UTXO { + res.push(WeightedUtxo { + satisfaction_weight: P2WPKH_WITNESS_SIZE, + utxo: Utxo::Local(LocalUtxo { outpoint: OutPoint::from_str( "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", ) @@ -582,16 +593,16 @@ mod test { script_pubkey: Script::new(), }, keychain: KeychainKind::External, - }, - P2WPKH_WITNESS_SIZE, - )); + }), + }); } res } - fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<(UTXO, usize)> { - let utxo = ( - UTXO { + fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec { + let utxo = WeightedUtxo { + satisfaction_weight: P2WPKH_WITNESS_SIZE, + utxo: Utxo::Local(LocalUtxo { outpoint: OutPoint::from_str( "ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:0", ) @@ -601,18 +612,18 @@ mod test { script_pubkey: Script::new(), }, keychain: KeychainKind::External, - }, - P2WPKH_WITNESS_SIZE, - ); + }), + }; vec![utxo; utxos_number] } - fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec<(UTXO, usize)>) -> u64 { + fn sum_random_utxos(mut rng: &mut StdRng, utxos: &mut Vec) -> u64 { let utxos_picked_len = rng.gen_range(2, utxos.len() / 2); utxos.shuffle(&mut rng); utxos[..utxos_picked_len] .iter() - .fold(0, |acc, x| acc + x.0.txout.value) + .map(|u| u.utxo.txout().value) + .sum() } #[test] @@ -632,7 +643,7 @@ mod test { .unwrap(); 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); } @@ -653,7 +664,7 @@ mod test { .unwrap(); 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); } @@ -674,7 +685,7 @@ mod test { .unwrap(); 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); } @@ -734,7 +745,7 @@ mod test { .unwrap(); 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); } @@ -755,7 +766,7 @@ mod test { .unwrap(); 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); } @@ -812,7 +823,7 @@ mod test { .unwrap(); 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 epsilon = 0.5; assert!((1.0 - (result.fee_amount / input_size)).abs() < epsilon); @@ -837,7 +848,7 @@ mod test { 0.0, ) .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 utxos: Vec = get_test_utxos() .into_iter() - .map(|u| OutputGroup::new(u.0, u.1, fee_rate)) + .map(|u| OutputGroup::new(u, fee_rate)) .collect(); let curr_available_value = utxos @@ -875,7 +886,7 @@ mod test { let fee_rate = FeeRate::from_sat_per_vb(10.0); let utxos: Vec = generate_same_value_utxos(100_000, 100_000) .into_iter() - .map(|u| OutputGroup::new(u.0, u.1, fee_rate)) + .map(|u| OutputGroup::new(u, fee_rate)) .collect(); let curr_available_value = utxos @@ -908,7 +919,7 @@ mod test { let utxos: Vec<_> = generate_same_value_utxos(50_000, 10) .into_iter() - .map(|u| OutputGroup::new(u.0, u.1, fee_rate)) + .map(|u| OutputGroup::new(u, fee_rate)) .collect(); let curr_value = 0; @@ -933,7 +944,7 @@ mod test { ) .unwrap(); 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 @@ -946,7 +957,7 @@ mod test { for _ in 0..200 { let optional_utxos: Vec<_> = generate_random_utxos(&mut rng, 40) .into_iter() - .map(|u| OutputGroup::new(u.0, u.1, fee_rate)) + .map(|u| OutputGroup::new(u, fee_rate)) .collect(); let curr_value = 0; @@ -969,7 +980,7 @@ mod test { 0.0, ) .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 utxos: Vec = utxos .into_iter() - .map(|u| OutputGroup::new(u.0, u.1, fee_rate)) + .map(|u| OutputGroup::new(u, fee_rate)) .collect(); let result = BranchAndBoundCoinSelection::default().single_random_draw( @@ -994,7 +1005,7 @@ mod test { 50.0, ); - assert!(result.selected_amount > target_amount); + assert!(result.selected_amount() > target_amount); assert_eq!( result.fee_amount, 50.0 + result.selected.len() as f32 * 68.0 diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 8bb254c4..58a706d1 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -199,13 +199,13 @@ where /// /// Note that this methods only operate on the internal database, which first needs to be /// [`Wallet::sync`] manually. - pub fn list_unspent(&self) -> Result, Error> { + pub fn list_unspent(&self) -> Result, Error> { self.database.borrow().iter_utxos() } /// Returns the `UTXO` owned by this wallet corresponding to `outpoint` if it exists in the /// wallet's database. - pub fn get_utxo(&self, outpoint: OutPoint) -> Result, Error> { + pub fn get_utxo(&self, outpoint: OutPoint) -> Result, Error> { self.database.borrow().get_utxo(&outpoint) } @@ -513,11 +513,7 @@ where params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee )?; - let coin_selection::CoinSelectionResult { - selected, - selected_amount, - mut fee_amount, - } = coin_selection.coin_select( + let coin_selection = coin_selection.coin_select( self.database.borrow().deref(), required_utxos, optional_utxos, @@ -525,10 +521,13 @@ where outgoing, fee_amount, )?; - tx.input = selected + let mut fee_amount = coin_selection.fee_amount; + + tx.input = coin_selection + .selected .iter() .map(|u| bitcoin::TxIn { - previous_output: u.outpoint, + previous_output: u.outpoint(), script_sig: Script::default(), sequence: n_sequence, witness: vec![], @@ -550,9 +549,8 @@ where Some(change_output) } }; - 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 { None if change_val.is_dust() => { @@ -588,14 +586,15 @@ where params.ordering.sort_tx(&mut tx); 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 { transaction: None, txid, timestamp: time::get_timestamp(), received, - sent: selected_amount, + sent, fees: fee_amount, height: None, }; @@ -699,13 +698,16 @@ where } }; - let utxo = UTXO { + let utxo = LocalUtxo { outpoint: txin.previous_output, txout, keychain, }; - Ok((utxo, weight)) + Ok(WeightedUtxo { + satisfaction_weight: weight, + utxo: Utxo::Local(utxo), + }) }) .collect::, _>>()?; @@ -1016,7 +1018,7 @@ where Ok(()) } - fn get_available_utxos(&self) -> Result, Error> { + fn get_available_utxos(&self) -> Result, Error> { Ok(self .list_unspent()? .into_iter() @@ -1039,18 +1041,18 @@ where &self, change_policy: tx_builder::ChangeSpendPolicy, unspendable: &HashSet, - manually_selected: Vec<(UTXO, usize)>, + manually_selected: Vec, must_use_all_available: bool, manual_only: bool, must_only_use_confirmed_tx: bool, - ) -> Result<(Vec<(UTXO, usize)>, Vec<(UTXO, usize)>), Error> { + ) -> Result<(Vec, Vec), Error> { // must_spend <- manually selected utxos // may_spend <- all other available utxos let mut may_spend = self.get_available_utxos()?; may_spend.retain(|may_spend| { manually_selected .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() }); let mut must_spend = manually_selected; @@ -1088,6 +1090,14 @@ where 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 { must_spend.append(&mut may_spend); } @@ -1098,7 +1108,7 @@ where fn complete_transaction( &self, tx: Transaction, - selected: Vec, + selected: Vec, params: TxParams, ) -> Result { use bitcoin::util::psbt::serialize::Serialize; @@ -1131,9 +1141,9 @@ where } } - let lookup_output = selected + let mut lookup_output = selected .into_iter() - .map(|utxo| (utxo.outpoint, utxo)) + .map(|utxo| (utxo.outpoint(), utxo)) .collect::>(); // add metadata for the inputs @@ -1142,7 +1152,7 @@ where .iter_mut() .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, None => continue, }; @@ -1153,32 +1163,50 @@ where 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, - // and the derivation index - let (keychain, child) = match self - .database - .borrow() - .get_path_from_script_pubkey(&utxo.txout.script_pubkey)? - { - Some(x) => x, - None => continue, - }; + match utxo { + Utxo::Local(utxo) => { + // Try to find the prev_script in our db to figure out if this is internal or external, + // and the derivation index + let (keychain, child) = match self + .database + .borrow() + .get_path_from_script_pubkey(&utxo.txout.script_pubkey)? + { + Some(x) => x, + None => continue, + }; - let (desc, _) = self._get_descriptor_for_keychain(keychain); - let derived_descriptor = desc.as_derived(child, &self.secp); - psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp)?; + let desc = self.get_descriptor_for_keychain(keychain); + let derived_descriptor = desc.as_derived(child, &self.secp); + psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp)?; - psbt_input.redeem_script = derived_descriptor.psbt_redeem_script(); - psbt_input.witness_script = derived_descriptor.psbt_witness_script(); + psbt_input.redeem_script = derived_descriptor.psbt_redeem_script(); + psbt_input.witness_script = derived_descriptor.psbt_witness_script(); - let prev_output = input.previous_output; - if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? { - if desc.is_witness() { - psbt_input.witness_utxo = - Some(prev_tx.output[prev_output.vout as usize].clone()); + let prev_output = input.previous_output; + if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? { + if desc.is_witness() { + psbt_input.witness_utxo = + 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 { - psbt_input.non_witness_utxo = Some(prev_tx); + Utxo::Foreign { + 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 { use std::str::FromStr; - use bitcoin::Network; + use bitcoin::{util::psbt, Network}; use crate::database::memory::MemoryDatabase; use crate::database::Database; @@ -2237,6 +2265,187 @@ mod test { 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 mut builder = wallet1.build_tx(); + assert!( + builder + .add_foreign_utxo( + utxo2.outpoint, + psbt::Input { + non_witness_utxo: Some(tx1), + ..Default::default() + }, + satisfaction_weight + ) + .is_err(), + "should fail when outpoint doesn't match psbt_input" + ); + assert!( + builder + .add_foreign_utxo( + utxo2.outpoint, + psbt::Input { + non_witness_utxo: Some(tx2), + ..Default::default() + }, + satisfaction_weight + ) + .is_ok(), + "shoulld be ok when outpoint 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 fail with only 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] #[should_panic( expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")" diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 86dca563..04f11139 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -54,15 +54,15 @@ use std::collections::HashSet; use std::default::Default; 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 miniscript::descriptor::DescriptorTrait; use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; -use crate::{database::BatchDatabase, Error, Wallet}; +use crate::{database::BatchDatabase, Error, Utxo, Wallet}; use crate::{ - types::{FeeRate, KeychainKind, UTXO}, + types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo}, TransactionDetails, }; /// Context in which the [`TxBuilder`] is valid @@ -129,7 +129,7 @@ impl TxBuilderContext for BumpFee {} /// [`build_fee_bump`]: Wallet::build_fee_bump /// [`finish`]: Self::finish /// [`coin_selection`]: Self::coin_selection -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct TxBuilder<'a, B, D, Cs, Ctx> { pub(crate) wallet: &'a Wallet, // params and coin_selection are Options not becasue they are optionally set (they are always @@ -150,7 +150,7 @@ pub(crate) struct TxParams { pub(crate) fee_policy: Option, pub(crate) internal_policy_path: Option>>, pub(crate) external_policy_path: Option>>, - pub(crate) utxos: Vec<(UTXO, usize)>, + pub(crate) utxos: Vec, pub(crate) unspendable: HashSet, pub(crate) manually_selected_only: bool, pub(crate) sighash: Option, @@ -183,6 +183,17 @@ impl std::default::Default for FeePolicy { } } +impl<'a, Cs: Clone, Ctx, B, D> Clone for TxBuilder<'a, B, D, Cs, Ctx> { + fn clone(&self) -> Self { + TxBuilder { + wallet: self.wallet, + params: self.params.clone(), + coin_selection: self.coin_selection.clone(), + phantom: PhantomData, + } + } +} + // methods supported by both contexts, for any CoinSelectionAlgorithm impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, B, D, Cs, Ctx> @@ -286,7 +297,10 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderConte for utxo in utxos { let descriptor = self.wallet.get_descriptor_for_keychain(utxo.keychain); 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) @@ -300,6 +314,84 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderConte 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 `witness_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`]. /// /// The wallet will **not** add additional utxos to the transaction even if they are needed to @@ -618,7 +710,7 @@ impl Default for ChangeSpendPolicy { } impl ChangeSpendPolicy { - pub(crate) fn is_satisfied_by(&self, utxo: &UTXO) -> bool { + pub(crate) fn is_satisfied_by(&self, utxo: &LocalUtxo) -> bool { match self { ChangeSpendPolicy::ChangeAllowed => true, ChangeSpendPolicy::OnlyChange => utxo.keychain == KeychainKind::Internal, @@ -709,9 +801,9 @@ mod test { assert_eq!(tx.output[2].script_pubkey, From::from(vec![0xAA, 0xEE])); } - fn get_test_utxos() -> Vec { + fn get_test_utxos() -> Vec { vec![ - UTXO { + LocalUtxo { outpoint: OutPoint { txid: Default::default(), vout: 0, @@ -719,7 +811,7 @@ mod test { txout: Default::default(), keychain: KeychainKind::External, }, - UTXO { + LocalUtxo { outpoint: OutPoint { txid: Default::default(), vout: 1,