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.
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.
- 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
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};
}
}
-/// 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
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 {
//! 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;
//!
//! Ok(CoinSelectionResult {
//! selected: all_utxos_selected,
-//! selected_amount,
//! fee_amount: fee_amount + additional_fees,
//! })
//! }
//! # 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))]
#[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
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,
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,
// 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))
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
}
Ok(CoinSelectionResult {
selected,
fee_amount,
- selected_amount,
})
}
}
#[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
}
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,
}
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,
// 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.
// 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();
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,
}
}
}
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",
)
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",
)
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",
)
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",
)
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]
.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);
}
.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);
}
.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);
}
.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);
}
.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);
}
.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);
0.0,
)
.unwrap();
- assert_eq!(result.selected_amount, target_amount);
+ assert_eq!(result.selected_amount(), target_amount);
}
}
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
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
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;
)
.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
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;
0.0,
)
.unwrap();
- assert_eq!(result.selected_amount, target_amount);
+ assert_eq!(result.selected_amount(), target_amount);
}
}
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(
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
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,
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![],
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() => {
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,
};
keychain,
};
- Ok((utxo, weight))
+ Ok(WeightedUtxo {
+ satisfaction_weight: weight,
+ utxo: Utxo::Local(utxo),
+ })
})
.collect::<Result<Vec<_>, _>>()?;
&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;
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);
}
fn complete_transaction(
&self,
tx: Transaction,
- selected: Vec<LocalUtxo>,
+ selected: Vec<Utxo>,
params: TxParams,
) -> Result<PSBT, Error> {
use bitcoin::util::psbt::serialize::Serialize;
}
}
- 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
.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,
};
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;
}
}
}
mod test {
use std::str::FromStr;
- use bitcoin::Network;
+ use bitcoin::{util::psbt, Network};
use crate::database::memory::MemoryDatabase;
use crate::database::Database;
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\")"
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
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>,
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)
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