Skip to content

Commit 7d30a99

Browse files
committed
fix(tx_builder)!: check foreign utxos are not local before inclusion
The new check is added to ensure all added foreign utxos are not owned by the wallet.
1 parent 362c3dc commit 7d30a99

File tree

2 files changed

+142
-28
lines changed

2 files changed

+142
-28
lines changed

crates/wallet/src/wallet/tx_builder.rs

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,9 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
342342
///
343343
/// This method returns errors in the following circumstances:
344344
///
345-
/// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
346-
/// 2. The data in `non_witness_utxo` does not match what is in `outpoint`.
345+
/// 1. The provided outpoint is associated to a [`LocalOutput`].
346+
/// 2. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`.
347+
/// 3. The data in `non_witness_utxo` does not match what is in `outpoint`.
347348
///
348349
/// Note unless you set [`only_witness_utxo`] any non-taproot `psbt_input` you pass to this
349350
/// method must have `non_witness_utxo` set otherwise you will get an error when [`finish`]
@@ -374,24 +375,27 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
374375
satisfaction_weight: Weight,
375376
sequence: Sequence,
376377
) -> Result<&mut Self, AddForeignUtxoError> {
377-
if psbt_input.witness_utxo.is_none() {
378-
match psbt_input.non_witness_utxo.as_ref() {
379-
Some(tx) => {
380-
if tx.compute_txid() != outpoint.txid {
381-
return Err(AddForeignUtxoError::InvalidTxid {
382-
input_txid: tx.compute_txid(),
383-
foreign_utxo: outpoint,
384-
});
385-
}
386-
if tx.output.len() <= outpoint.vout as usize {
387-
return Err(AddForeignUtxoError::InvalidOutpoint(outpoint));
388-
}
389-
}
390-
None => {
391-
return Err(AddForeignUtxoError::MissingUtxo);
378+
let txout = match (&psbt_input.witness_utxo, &psbt_input.non_witness_utxo) {
379+
(Some(ref txout), _) => Ok(txout.clone()),
380+
(_, Some(tx)) => {
381+
if tx.compute_txid() != outpoint.txid {
382+
Err(AddForeignUtxoError::InvalidTxid {
383+
input_txid: tx.compute_txid(),
384+
foreign_utxo: outpoint,
385+
})
386+
} else {
387+
tx.tx_out(outpoint.vout as usize)
388+
.map_err(|_| AddForeignUtxoError::InvalidOutpoint(outpoint))
389+
.cloned()
392390
}
393391
}
394-
}
392+
(_, _) => Err(AddForeignUtxoError::MissingUtxo),
393+
}?;
394+
395+
// Avoid the inclusion of local utxos as foreign utxos
396+
if self.wallet.is_mine(txout.script_pubkey) {
397+
return Err(AddForeignUtxoError::NotForeignUtxo);
398+
};
395399

396400
self.params.utxos.push(WeightedUtxo {
397401
satisfaction_weight,
@@ -711,6 +715,8 @@ pub enum AddForeignUtxoError {
711715
InvalidOutpoint(OutPoint),
712716
/// Foreign utxo missing witness_utxo or non_witness_utxo
713717
MissingUtxo,
718+
/// UTxO is owned by wallet
719+
NotForeignUtxo,
714720
}
715721

716722
impl fmt::Display for AddForeignUtxoError {
@@ -730,6 +736,7 @@ impl fmt::Display for AddForeignUtxoError {
730736
outpoint.txid, outpoint.vout,
731737
),
732738
Self::MissingUtxo => write!(f, "Foreign utxo missing witness_utxo or non_witness_utxo"),
739+
Self::NotForeignUtxo => write!(f, "UTxO is owned by wallet"),
733740
}
734741
}
735742
}

crates/wallet/tests/wallet.rs

Lines changed: 117 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1644,18 +1644,121 @@ fn test_calculate_fee_with_missing_foreign_utxo() {
16441644
}
16451645

16461646
#[test]
1647-
fn test_add_foreign_utxo_invalid_psbt_input() {
1647+
fn test_add_foreign_utxo_fails_without_psbt_input_prev_tx_data() {
16481648
let (mut wallet, _) = get_funded_wallet_wpkh();
1649-
let outpoint = wallet.list_unspent().next().expect("must exist").outpoint;
1649+
let address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
1650+
.expect("address")
1651+
.require_network(Network::Regtest)
1652+
.unwrap();
1653+
let tx0 = Transaction {
1654+
output: vec![TxOut {
1655+
value: Amount::from_sat(76_000),
1656+
script_pubkey: address.script_pubkey(),
1657+
}],
1658+
..new_tx(0)
1659+
};
1660+
let external_outpoint = OutPoint {
1661+
txid: tx0.compute_txid(),
1662+
vout: 0,
1663+
};
16501664
let foreign_utxo_satisfaction = wallet
16511665
.public_descriptor(KeychainKind::External)
16521666
.max_weight_to_satisfy()
16531667
.unwrap();
16541668

16551669
let mut builder = wallet.build_tx();
1656-
let result =
1657-
builder.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction);
1658-
assert!(matches!(result, Err(AddForeignUtxoError::MissingUtxo)));
1670+
let result = builder.add_foreign_utxo(
1671+
external_outpoint,
1672+
psbt::Input::default(),
1673+
foreign_utxo_satisfaction,
1674+
);
1675+
assert!(
1676+
matches!(result, Err(AddForeignUtxoError::MissingUtxo)),
1677+
"should fail when there is no data about the transaction creating the input"
1678+
);
1679+
}
1680+
1681+
#[test]
1682+
fn test_add_foreign_utxo_fails_when_utxo_is_owned_by_wallet_with_prev_txout() {
1683+
let (mut wallet1, _) =
1684+
get_funded_wallet_single("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
1685+
1686+
let utxo = wallet1.list_unspent().next().expect("must take!");
1687+
let foreign_utxo_satisfaction = wallet1
1688+
.public_descriptor(KeychainKind::External)
1689+
.max_weight_to_satisfy()
1690+
.unwrap();
1691+
1692+
let psbt_input = psbt::Input {
1693+
// Add txout
1694+
witness_utxo: Some(utxo.txout.clone()),
1695+
..Default::default()
1696+
};
1697+
1698+
let mut builder = wallet1.build_tx();
1699+
let result = builder.add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction);
1700+
assert!(
1701+
matches!(result, Err(AddForeignUtxoError::NotForeignUtxo)),
1702+
"should fail when the outpoint provided is not foreign to the wallet"
1703+
);
1704+
}
1705+
1706+
#[test]
1707+
fn test_add_foreign_utxo_fails_when_utxo_is_owned_by_wallet_with_full_prev_tx() {
1708+
let (mut wallet1, txid1) =
1709+
get_funded_wallet_single("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
1710+
1711+
let utxo = wallet1.list_unspent().next().expect("must take!");
1712+
let full_tx = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone();
1713+
let foreign_utxo_satisfaction = wallet1
1714+
.public_descriptor(KeychainKind::External)
1715+
.max_weight_to_satisfy()
1716+
.unwrap();
1717+
1718+
let psbt_input = psbt::Input {
1719+
// Add full transaction
1720+
non_witness_utxo: Some(full_tx.as_ref().clone()),
1721+
..Default::default()
1722+
};
1723+
1724+
let mut builder = wallet1.build_tx();
1725+
let result = builder.add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction);
1726+
assert!(
1727+
matches!(result, Err(AddForeignUtxoError::NotForeignUtxo)),
1728+
"should fail when the outpoint provided is not foreign to the wallet"
1729+
);
1730+
}
1731+
1732+
#[test]
1733+
fn test_add_foreign_utxo_fails_with_invalid_outpoint_vout() {
1734+
let (mut wallet1, txid1) =
1735+
get_funded_wallet_single("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
1736+
1737+
let utxo = wallet1.list_unspent().next().expect("must take!");
1738+
let full_tx = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone();
1739+
let foreign_utxo_satisfaction = wallet1
1740+
.public_descriptor(KeychainKind::External)
1741+
.max_weight_to_satisfy()
1742+
.unwrap();
1743+
1744+
let psbt_input = psbt::Input {
1745+
non_witness_utxo: Some(full_tx.as_ref().clone()),
1746+
..Default::default()
1747+
};
1748+
1749+
let mut builder = wallet1.build_tx();
1750+
let result = builder.add_foreign_utxo(
1751+
OutPoint {
1752+
vout: full_tx.output.len() as u32 + 1,
1753+
txid: utxo.outpoint.txid,
1754+
},
1755+
psbt_input,
1756+
foreign_utxo_satisfaction,
1757+
);
1758+
assert!(
1759+
matches!(result, Err(AddForeignUtxoError::InvalidOutpoint { .. })),
1760+
"should fail when the outpoint provided index is not one of the possible transaction vouts"
1761+
);
16591762
}
16601763

16611764
#[test]
@@ -1674,19 +1777,23 @@ fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() {
16741777
.unwrap();
16751778

16761779
let mut builder = wallet1.build_tx();
1780+
1781+
// Check we are activating the code path that should rise the error
16771782
assert!(
1678-
builder
1679-
.add_foreign_utxo(
1783+
matches!(
1784+
builder.add_foreign_utxo(
16801785
utxo2.outpoint,
16811786
psbt::Input {
16821787
non_witness_utxo: Some(tx1.as_ref().clone()),
16831788
..Default::default()
16841789
},
16851790
satisfaction_weight
1686-
)
1687-
.is_err(),
1688-
"should fail when outpoint doesn't match psbt_input"
1791+
),
1792+
Err(AddForeignUtxoError::InvalidTxid { .. })
1793+
),
1794+
"should fail when outpoint txid doesn't match psbt_input txout txid"
16891795
);
1796+
16901797
assert!(
16911798
builder
16921799
.add_foreign_utxo(

0 commit comments

Comments
 (0)