]> Untitled Git - bdk/commitdiff
Added `add_foreign_utxo`
authorLLFourn <lloyd.fourn@gmail.com>
Mon, 8 Feb 2021 04:40:56 +0000 (15:40 +1100)
committerLLFourn <lloyd.fourn@gmail.com>
Fri, 26 Feb 2021 02:33:52 +0000 (13:33 +1100)
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.

CHANGELOG.md
src/types.rs
src/wallet/coin_selection.rs
src/wallet/mod.rs
src/wallet/tx_builder.rs

index b2528d8cc7791678b48330ccc88a9c725215875d..cf9683b3e5ce6dbd72eea6160fcf4fa50906c160 100644 (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.
 - 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.
@@ -61,6 +61,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
index 5e20a7de19505184b8b36183ace0361f807917e8..347325a8e0cb921e718caf16e63ae14e0210cd5d 100644 (file)
@@ -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,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)]
 pub struct LocalUtxo {
     /// Reference to a transaction output
@@ -101,6 +103,62 @@ pub struct LocalUtxo {
     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
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
 pub struct TransactionDetails {
index 493a2bdf926255b18fe65957dbb533685c734347..f0b0bb7a0c58f0c64f575deabbb69bd1e6b106c5 100644 (file)
@@ -50,8 +50,8 @@
 //!     fn coin_select(
 //!         &self,
 //!         database: &D,
-//!         required_utxos: Vec<(LocalUtxo, usize)>,
-//!         optional_utxos: Vec<(LocalUtxo, usize)>,
+//!         required_utxos: Vec<WeightedUtxo>,
+//!         optional_utxos: Vec<WeightedUtxo>,
 //!         fee_rate: FeeRate,
 //!         amount_needed: u64,
 //!         fee_amount: f32,
 //!         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::<Vec<_>>();
 //!         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, LocalUtxo};
+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<LocalUtxo>,
-    /// Sum of the selected inputs' value
-    pub selected_amount: u64,
+    pub selected: Vec<Utxo>,
     /// 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<D: Database>: std::fmt::Debug {
     fn coin_select(
         &self,
         database: &D,
-        required_utxos: Vec<(LocalUtxo, usize)>,
-        optional_utxos: Vec<(LocalUtxo, usize)>,
+        required_utxos: Vec<WeightedUtxo>,
+        optional_utxos: Vec<WeightedUtxo>,
         fee_rate: FeeRate,
         amount_needed: u64,
         fee_amount: f32,
@@ -170,8 +184,8 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
     fn coin_select(
         &self,
         _database: &D,
-        required_utxos: Vec<(LocalUtxo, usize)>,
-        mut optional_utxos: Vec<(LocalUtxo, usize)>,
+        required_utxos: Vec<WeightedUtxo>,
+        mut optional_utxos: Vec<WeightedUtxo>,
         fee_rate: FeeRate,
         amount_needed: u64,
         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,
         // 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<D: Database> CoinSelectionAlgorithm<D> 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<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
         Ok(CoinSelectionResult {
             selected,
             fee_amount,
-            selected_amount,
         })
     }
 }
@@ -239,9 +253,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for LargestFirstCoinSelection {
 #[derive(Debug, Clone)]
 // Adds fee information to an UTXO.
 struct OutputGroup {
-    utxo: LocalUtxo,
-    // 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: LocalUtxo, 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<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
     fn coin_select(
         &self,
         _database: &D,
-        required_utxos: Vec<(LocalUtxo, usize)>,
-        optional_utxos: Vec<(LocalUtxo, usize)>,
+        required_utxos: Vec<WeightedUtxo>,
+        optional_utxos: Vec<WeightedUtxo>,
         fee_rate: FeeRate,
         amount_needed: u64,
         fee_amount: f32,
@@ -300,7 +312,7 @@ impl<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
         // Mapping every (UTXO, usize) to an output group
         let required_utxos: Vec<OutputGroup> = 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<D: Database> CoinSelectionAlgorithm<D> for BranchAndBoundCoinSelection {
         // adding them is more than their value
         let optional_utxos: Vec<OutputGroup> = 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::<f32>();
         let selected = selected_utxos
             .into_iter()
-            .map(|u| u.utxo)
+            .map(|u| u.weighted_utxo.utxo)
             .collect::<Vec<_>>();
-        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<(LocalUtxo, usize)> {
+    fn get_test_utxos() -> Vec<WeightedUtxo> {
         vec![
-            (
-                LocalUtxo {
+            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,
-            ),
-            (
-                LocalUtxo {
+                }),
+            },
+            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<(LocalUtxo, usize)> {
+    fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec<WeightedUtxo> {
         let mut res = Vec::new();
         for _ in 0..utxos_number {
-            res.push((
-                LocalUtxo {
+            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<(LocalUtxo, usize)> {
-        let utxo = (
-            LocalUtxo {
+    fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
+        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<(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);
         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<OutputGroup> = 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<OutputGroup> = 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<OutputGroup> = 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
index 88b7876902c309b893f364f5d31e8e11a4e552a4..d2b1e0c890c58e05f76516806669b208e07e59c3 100644 (file)
@@ -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,
         };
@@ -705,7 +704,10 @@ where
                     keychain,
                 };
 
-                Ok((utxo, weight))
+                Ok(WeightedUtxo {
+                    satisfaction_weight: weight,
+                    utxo: Utxo::Local(utxo),
+                })
             })
             .collect::<Result<Vec<_>, _>>()?;
 
@@ -1039,18 +1041,18 @@ where
         &self,
         change_policy: tx_builder::ChangeSpendPolicy,
         unspendable: &HashSet<OutPoint>,
-        manually_selected: Vec<(LocalUtxo, usize)>,
+        manually_selected: Vec<WeightedUtxo>,
         must_use_all_available: bool,
         manual_only: 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
         //    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<LocalUtxo>,
+        selected: Vec<Utxo>,
         params: TxParams,
     ) -> Result<PSBT, Error> {
         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::<HashMap<_, _>>();
 
         // 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,182 @@ 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 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]
     #[should_panic(
         expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")"
index f1fdf82a50f5d1c614f10a959e74c72e680c945f..bd9181eed50e4a838d85cdc09cca9a991b929ac1 100644 (file)
@@ -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, LocalUtxo},
+    types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo},
     TransactionDetails,
 };
 /// Context in which the [`TxBuilder`] is valid
@@ -150,7 +150,7 @@ pub(crate) struct TxParams {
     pub(crate) fee_policy: Option<FeePolicy>,
     pub(crate) internal_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) manually_selected_only: bool,
     pub(crate) sighash: Option<SigHashType>,
@@ -297,7 +297,10 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, 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)
@@ -311,6 +314,84 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, 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 `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`].
     ///
     /// The wallet will **not** add additional utxos to the transaction even if they are needed to