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
5 changed files with 491 additions and 130 deletions

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,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\")"