Added add_foreign_utxo

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

Main changes:

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

View File

@ -50,7 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
type to mark for a missing client.
- 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

View 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 {

View 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,
@ -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::<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

View 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,6 +1163,8 @@ where
psbt_input.sighash_type = Some(sighash_type);
}
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
@ -1164,7 +1176,7 @@ where
None => continue,
};
let (desc, _) = self._get_descriptor_for_keychain(keychain);
let desc = self.get_descriptor_for_keychain(keychain);
let derived_descriptor = desc.as_derived(child, &self.secp);
psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp)?;
@ -1182,6 +1194,22 @@ where
}
}
}
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;
}
}
}
// probably redundant but it doesn't hurt...
self.add_input_hd_keypaths(&mut psbt)?;
@ -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\")"

View 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