Skip to content

Commit d08c270

Browse files
committed
fix(wallet): ensure there are not duplicated foreign utxos
1 parent a8d77f8 commit d08c270

File tree

2 files changed

+87
-32
lines changed

2 files changed

+87
-32
lines changed

crates/wallet/src/wallet/mod.rs

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1432,7 +1432,7 @@ impl Wallet {
14321432
utxo: Utxo::Local(utxo),
14331433
})
14341434
// include foreign UTxOs as required
1435-
.chain(params.foreign_utxos.clone())
1435+
.chain(params.foreign_utxos.clone().into_values())
14361436
.collect();
14371437
let optional = self.filter_utxos(&params, current_height.to_consensus_u32());
14381438

@@ -1640,9 +1640,12 @@ impl Wallet {
16401640
.map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
16411641

16421642
// remove the inputs from the tx and process them
1643-
let (local, foreign): (HashSet<LocalOutput>, Vec<WeightedUtxo>) =
1643+
let (local, foreign): (HashSet<LocalOutput>, HashMap<OutPoint, WeightedUtxo>) =
16441644
tx.input.drain(..).try_fold(
1645-
(HashSet::<LocalOutput>::new(), Vec::<WeightedUtxo>::new()),
1645+
(
1646+
HashSet::<LocalOutput>::new(),
1647+
HashMap::<OutPoint, WeightedUtxo>::new(),
1648+
),
16461649
|(mut local, mut foreign), txin| -> Result<_, BuildFeeBumpError> {
16471650
let prev_tx = graph
16481651
.get_tx(txin.previous_output.txid)
@@ -1670,27 +1673,24 @@ impl Wallet {
16701673
+ serialize(&txin.witness).len(),
16711674
);
16721675

1673-
let psbt_input_data = if txout.script_pubkey.witness_version().is_some()
1674-
{
1675-
Box::new(psbt::Input {
1676-
witness_utxo: Some(txout.clone()),
1677-
non_witness_utxo: Some(prev_tx.as_ref().clone()),
1678-
..Default::default()
1679-
})
1680-
} else {
1681-
Box::new(psbt::Input {
1682-
non_witness_utxo: Some(prev_tx.as_ref().clone()),
1683-
..Default::default()
1684-
})
1685-
};
1686-
foreign.push(WeightedUtxo {
1687-
utxo: Utxo::Foreign {
1688-
outpoint: txin.previous_output,
1689-
sequence: txin.sequence,
1690-
psbt_input: psbt_input_data,
1676+
foreign.insert(
1677+
txin.previous_output,
1678+
WeightedUtxo {
1679+
utxo: Utxo::Foreign {
1680+
outpoint: txin.previous_output,
1681+
sequence: txin.sequence,
1682+
psbt_input: Box::new(psbt::Input {
1683+
witness_utxo: txout
1684+
.script_pubkey
1685+
.witness_version()
1686+
.map(|_| txout.clone()),
1687+
non_witness_utxo: Some(prev_tx.as_ref().clone()),
1688+
..Default::default()
1689+
}),
1690+
},
1691+
satisfaction_weight,
16911692
},
1692-
satisfaction_weight,
1693-
});
1693+
);
16941694
true
16951695
}
16961696
};

crates/wallet/src/wallet/tx_builder.rs

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ use rand_core::RngCore;
5252
use super::coin_selection::CoinSelectionAlgorithm;
5353
use super::utils::shuffle_slice;
5454
use super::{CreateTxError, Wallet};
55-
use crate::collections::{BTreeMap, HashSet};
55+
use crate::collections::{BTreeMap, HashMap, HashSet};
5656
use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
5757

5858
/// A transaction builder
@@ -126,7 +126,7 @@ pub(crate) struct TxParams {
126126
pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>,
127127
pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
128128
pub(crate) utxos: HashSet<LocalOutput>,
129-
pub(crate) foreign_utxos: Vec<WeightedUtxo>,
129+
pub(crate) foreign_utxos: HashMap<OutPoint, WeightedUtxo>,
130130
pub(crate) unspendable: HashSet<OutPoint>,
131131
pub(crate) manually_selected_only: bool,
132132
pub(crate) sighash: Option<psbt::PsbtSighashType>,
@@ -382,14 +382,17 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
382382
}
383383
}
384384

385-
self.params.foreign_utxos.push(WeightedUtxo {
386-
satisfaction_weight,
387-
utxo: Utxo::Foreign {
388-
outpoint,
389-
sequence,
390-
psbt_input: Box::new(psbt_input),
385+
self.params.foreign_utxos.insert(
386+
outpoint,
387+
WeightedUtxo {
388+
satisfaction_weight,
389+
utxo: Utxo::Foreign {
390+
outpoint,
391+
sequence,
392+
psbt_input: Box::new(psbt_input),
393+
},
391394
},
392-
});
395+
);
393396

394397
Ok(self)
395398
}
@@ -1100,4 +1103,56 @@ mod test {
11001103
params.utxos.into_iter().collect::<Vec<_>>()
11011104
);
11021105
}
1106+
1107+
#[test]
1108+
fn not_duplicated_foreign_utxos_with_same_outpoint_but_different_weight() {
1109+
use crate::test_utils::{get_funded_wallet_single, get_funded_wallet_wpkh, get_test_wpkh};
1110+
1111+
// Use two different wallets to avoid adding local utxos
1112+
let (wallet1, txid1) = get_funded_wallet_wpkh();
1113+
let (mut wallet2, txid2) = get_funded_wallet_single(get_test_wpkh());
1114+
1115+
// if the transactions were produced by the same wallet the following assert should fail
1116+
assert_ne!(txid1, txid2);
1117+
1118+
let utxo1 = wallet1.list_unspent().next().unwrap();
1119+
let tx1 = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone();
1120+
1121+
let satisfaction_weight = wallet1
1122+
.public_descriptor(KeychainKind::External)
1123+
.max_weight_to_satisfy()
1124+
.unwrap();
1125+
1126+
let mut builder = wallet2.build_tx();
1127+
1128+
// add foreign utxo with satisfaction weight x
1129+
assert!(builder
1130+
.add_foreign_utxo(
1131+
utxo1.outpoint,
1132+
psbt::Input {
1133+
non_witness_utxo: Some(tx1.as_ref().clone()),
1134+
..Default::default()
1135+
},
1136+
satisfaction_weight,
1137+
)
1138+
.is_ok());
1139+
1140+
let modified_satisfaction_weight = satisfaction_weight - Weight::from_wu(6);
1141+
1142+
assert_ne!(satisfaction_weight, modified_satisfaction_weight);
1143+
1144+
// add foreign utxo with same outpoint but satisfaction weight x - 6wu
1145+
assert!(builder
1146+
.add_foreign_utxo(
1147+
utxo1.outpoint,
1148+
psbt::Input {
1149+
non_witness_utxo: Some(tx1.as_ref().clone()),
1150+
..Default::default()
1151+
},
1152+
modified_satisfaction_weight,
1153+
)
1154+
.is_ok());
1155+
1156+
assert_eq!(builder.params.foreign_utxos.len(), 1);
1157+
}
11031158
}

0 commit comments

Comments
 (0)