Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check foreign utxos are not local before inclusion #1823

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 48 additions & 19 deletions crates/wallet/src/wallet/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,9 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
///
/// 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`.
/// 1. The provided outpoint is associated to a [`LocalOutput`].
/// 2. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
/// 3. The data in `non_witness_utxo` does not match what is in `outpoint`.
///
/// Note unless you set [`only_witness_utxo`] any non-taproot `psbt_input` you pass to this
/// method must have `non_witness_utxo` set otherwise you will get an error when [`finish`]
Expand Down Expand Up @@ -374,24 +375,31 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
satisfaction_weight: Weight,
sequence: Sequence,
) -> Result<&mut Self, AddForeignUtxoError> {
if psbt_input.witness_utxo.is_none() {
match psbt_input.non_witness_utxo.as_ref() {
Some(tx) => {
if tx.compute_txid() != outpoint.txid {
return Err(AddForeignUtxoError::InvalidTxid {
input_txid: tx.compute_txid(),
foreign_utxo: outpoint,
});
}
if tx.output.len() <= outpoint.vout as usize {
return Err(AddForeignUtxoError::InvalidOutpoint(outpoint));
}
}
None => {
return Err(AddForeignUtxoError::MissingUtxo);
}
let script_pubkey = if let Some(ref txout) = psbt_input.witness_utxo {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit, but I'm inclined to write this as a match statement with the primary goal of locating the txout.

let txout = match (&psbt_input.witness_utxo, &psbt_input.non_witness_utxo) { ... };

txout.script_pubkey.clone()
} else if let Some(tx) = psbt_input.non_witness_utxo.as_ref() {
if tx.compute_txid() != outpoint.txid {
return Err(AddForeignUtxoError::InvalidTxid {
input_txid: tx.compute_txid(),
foreign_utxo: outpoint,
});
}
}

if let Ok(txout) = tx.tx_out(outpoint.vout as usize) {
txout.script_pubkey.clone()
} else {
return Err(AddForeignUtxoError::InvalidOutpoint(outpoint));
}
} else {
return Err(AddForeignUtxoError::MissingUtxo);
};

// Avoid the inclusion of local utxos as foreign utxos
if self.wallet.is_mine(script_pubkey) {
// TODO(2.0.0): return Err(AddForeignUtxoError::NotForeignUtxo);
// No-op to avoid breaking changes until next major release
return Ok(self);
};

self.params.utxos.push(WeightedUtxo {
satisfaction_weight,
Expand Down Expand Up @@ -711,6 +719,8 @@ pub enum AddForeignUtxoError {
InvalidOutpoint(OutPoint),
/// Foreign utxo missing witness_utxo or non_witness_utxo
MissingUtxo,
// Avoid breaking changes until next major release
// TODO(2.0.0): NotForeignUtxo,
}

impl fmt::Display for AddForeignUtxoError {
Expand All @@ -730,6 +740,8 @@ impl fmt::Display for AddForeignUtxoError {
outpoint.txid, outpoint.vout,
),
Self::MissingUtxo => write!(f, "Foreign utxo missing witness_utxo or non_witness_utxo"),
// Avoid breaking changes until next major release
// TODO(2.0.0): Self::NotForeignUtxo => write!(f, "UTxO is owned by wallet"),
}
}
}
Expand Down Expand Up @@ -1098,4 +1110,21 @@ mod test {
builder.fee_rate(FeeRate::from_sat_per_kwu(feerate + 250));
let _ = builder.finish().unwrap();
}

#[test]
fn test_add_foreign_utxo_fails_when_utxo_is_owned_by_wallet() {
use crate::test_utils::get_funded_wallet_wpkh;
let (mut wallet, _) = get_funded_wallet_wpkh();
let outpoint = wallet.list_unspent().next().expect("must exist").outpoint;
let foreign_utxo_satisfaction = wallet
.public_descriptor(KeychainKind::External)
.max_weight_to_satisfy()
.unwrap();

let mut builder = wallet.build_tx();
let _ =
builder.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction);
// TODO(2.0.0): assert!(matches!(result, Err(AddForeignUtxoError::NotForeignUtxo)));
assert!(builder.params.utxos.is_empty());
Comment on lines +1126 to +1128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you still need to provide the witness utxo if you want this to reach the is_mine check.

}
}
23 changes: 20 additions & 3 deletions crates/wallet/tests/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1646,15 +1646,32 @@ fn test_calculate_fee_with_missing_foreign_utxo() {
#[test]
fn test_add_foreign_utxo_invalid_psbt_input() {
let (mut wallet, _) = get_funded_wallet_wpkh();
let outpoint = wallet.list_unspent().next().expect("must exist").outpoint;
let address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
.expect("address")
.require_network(Network::Regtest)
.unwrap();
let tx0 = Transaction {
output: vec![TxOut {
value: Amount::from_sat(76_000),
script_pubkey: address.script_pubkey(),
}],
..new_tx(0)
};
let external_outpoint = OutPoint {
txid: tx0.compute_txid(),
vout: 0,
};
let foreign_utxo_satisfaction = wallet
.public_descriptor(KeychainKind::External)
.max_weight_to_satisfy()
.unwrap();

let mut builder = wallet.build_tx();
let result =
builder.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction);
let result = builder.add_foreign_utxo(
external_outpoint,
psbt::Input::default(),
foreign_utxo_satisfaction,
);
assert!(matches!(result, Err(AddForeignUtxoError::MissingUtxo)));
}

Expand Down
Loading