Skip to content

Commit 389de5d

Browse files
committed
refactor(wallet): use iterators and adaptors in preselect_utxos
There were multiple calls for de-duplication of selected UTxOs. As the test `test_filter_duplicates` shows, there are four possible cases for duplication of UTxOs while feeding the coin selection algorithms. 1. no duplication: out of concern 2. duplication in the required utxos only: covered by the source of `required_utxos`, `Wallet::list_unspent`, which roots back the provided `UTxOs` to a `HashMap` which should avoid any duplication by definition 3. duplication in the optional utxos only: is the only one possible as optional `UTxOs` are stored in a `Vec` and no checks are performed about the duplicity of their members. 4. duplication across the required and optional utxos: is already covered by `Wallet::preselect_utxos`, which avoid the processing of required UTxOs while listing the unspent available UTxOs in the wallet. This refactor changes: - `TxParams::utxos` type to be `HashSet<LocalOutput>` avoiding the duplication case 3, and allowing a query closer to O(1) on avg. to cover duplication case 4 (before was O(n) where n is the size of required utxos). - Moves the computation of the `WeightedUtxos` to the last part of UTxO filtering, allowing the unification of the computation for local outputs. - Removes some extra allocations done for helpers structures or intermediate results while filtering UTxOs. - Allows for future integration of UTxO filtering methods for other utilities. - Adds more comments for each filtering step. With these changes all four cases would be covered, and `coin_selection::filter_duplicates` would be no longer needed.
1 parent a5335a1 commit 389de5d

File tree

2 files changed

+126
-173
lines changed

2 files changed

+126
-173
lines changed

crates/wallet/src/wallet/mod.rs

Lines changed: 117 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1618,60 +1618,54 @@ impl Wallet {
16181618
.map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
16191619

16201620
// remove the inputs from the tx and process them
1621-
let original_txin = tx.input.drain(..).collect::<Vec<_>>();
1622-
let original_utxos = original_txin
1623-
.iter()
1624-
.map(|txin| -> Result<_, BuildFeeBumpError> {
1625-
let prev_tx = graph
1626-
.get_tx(txin.previous_output.txid)
1627-
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
1628-
let txout = &prev_tx.output[txin.previous_output.vout as usize];
1629-
1630-
let chain_position = chain_positions
1631-
.get(&txin.previous_output.txid)
1632-
.cloned()
1633-
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
1634-
1635-
let weighted_utxo = match txout_index.index_of_spk(txout.script_pubkey.clone()) {
1636-
Some(&(keychain, derivation_index)) => {
1637-
let satisfaction_weight = self
1638-
.public_descriptor(keychain)
1639-
.max_weight_to_satisfy()
1640-
.unwrap();
1641-
WeightedUtxo {
1642-
utxo: Utxo::Local(LocalOutput {
1643-
outpoint: txin.previous_output,
1644-
txout: txout.clone(),
1645-
keychain,
1646-
is_spent: true,
1647-
derivation_index,
1648-
chain_position,
1649-
}),
1650-
satisfaction_weight,
1651-
}
1652-
}
1653-
None => {
1654-
let satisfaction_weight = Weight::from_wu_usize(
1655-
serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len(),
1656-
);
1657-
WeightedUtxo {
1658-
utxo: Utxo::Foreign {
1659-
outpoint: txin.previous_output,
1660-
sequence: txin.sequence,
1661-
psbt_input: Box::new(psbt::Input {
1662-
witness_utxo: Some(txout.clone()),
1663-
non_witness_utxo: Some(prev_tx.as_ref().clone()),
1664-
..Default::default()
1665-
}),
1666-
},
1667-
satisfaction_weight,
1621+
let (local, foreign): (HashSet<LocalOutput>, Vec<WeightedUtxo>) =
1622+
tx.input.drain(..).try_fold(
1623+
(HashSet::<LocalOutput>::new(), Vec::<WeightedUtxo>::new()),
1624+
|(mut local, mut foreign), txin| -> Result<_, BuildFeeBumpError> {
1625+
let prev_tx = graph
1626+
.get_tx(txin.previous_output.txid)
1627+
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
1628+
let txout = &prev_tx.output[txin.previous_output.vout as usize];
1629+
1630+
let chain_position =
1631+
chain_positions
1632+
.get(&txin.previous_output.txid)
1633+
.cloned()
1634+
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
1635+
1636+
match txout_index.index_of_spk(txout.script_pubkey.clone()) {
1637+
Some(&(keychain, derivation_index)) => local.insert(LocalOutput {
1638+
outpoint: txin.previous_output,
1639+
txout: txout.clone(),
1640+
keychain,
1641+
is_spent: true,
1642+
derivation_index,
1643+
chain_position,
1644+
}),
1645+
None => {
1646+
let satisfaction_weight = Weight::from_wu_usize(
1647+
serialize(&txin.script_sig).len() * 4
1648+
+ serialize(&txin.witness).len(),
1649+
);
1650+
foreign.push(WeightedUtxo {
1651+
utxo: Utxo::Foreign {
1652+
outpoint: txin.previous_output,
1653+
sequence: txin.sequence,
1654+
psbt_input: Box::new(psbt::Input {
1655+
witness_utxo: Some(txout.clone()),
1656+
non_witness_utxo: Some(prev_tx.as_ref().clone()),
1657+
..Default::default()
1658+
}),
1659+
},
1660+
satisfaction_weight,
1661+
});
1662+
true
16681663
}
1669-
}
1670-
};
1664+
};
16711665

1672-
Ok(weighted_utxo)
1673-
})
1674-
.collect::<Result<Vec<_>, _>>()?;
1666+
Ok((local, foreign))
1667+
},
1668+
)?;
16751669

16761670
if tx.output.len() > 1 {
16771671
let mut change_index = None;
@@ -1698,7 +1692,8 @@ impl Wallet {
16981692
.into_iter()
16991693
.map(|txout| (txout.script_pubkey, txout.value))
17001694
.collect(),
1701-
utxos: original_utxos,
1695+
utxos: local,
1696+
foreign_utxos: foreign,
17021697
bumping_fee: Some(tx_builder::PreviousFee {
17031698
absolute: fee,
17041699
rate: fee_rate,
@@ -1976,116 +1971,85 @@ impl Wallet {
19761971
descriptor.at_derivation_index(child).ok()
19771972
}
19781973

1979-
fn get_available_utxos(&self) -> Vec<(LocalOutput, Weight)> {
1980-
self.list_unspent()
1981-
.map(|utxo| {
1982-
let keychain = utxo.keychain;
1983-
(utxo, {
1984-
self.public_descriptor(keychain)
1985-
.max_weight_to_satisfy()
1986-
.unwrap()
1987-
})
1988-
})
1989-
.collect()
1990-
}
1991-
19921974
/// Given the options returns the list of utxos that must be used to form the
19931975
/// transaction and any further that may be used if needed.
19941976
fn preselect_utxos(
19951977
&self,
19961978
params: &TxParams,
19971979
current_height: Option<u32>,
19981980
) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) {
1999-
let TxParams {
2000-
change_policy,
2001-
unspendable,
2002-
utxos,
2003-
drain_wallet,
2004-
manually_selected_only,
2005-
bumping_fee,
2006-
..
2007-
} = params;
2008-
2009-
let manually_selected = utxos.clone();
2010-
// we mandate confirmed transactions if we're bumping the fee
2011-
let must_only_use_confirmed_tx = bumping_fee.is_some();
2012-
let must_use_all_available = *drain_wallet;
2013-
2014-
// must_spend <- manually selected utxos
2015-
// may_spend <- all other available utxos
2016-
let mut may_spend = self.get_available_utxos();
2017-
2018-
may_spend.retain(|may_spend| {
2019-
!manually_selected
2020-
.iter()
2021-
.any(|manually_selected| manually_selected.utxo.outpoint() == may_spend.0.outpoint)
2022-
});
2023-
let mut must_spend = manually_selected;
2024-
2025-
// NOTE: we are intentionally ignoring `unspendable` here. i.e manual
2026-
// selection overrides unspendable.
2027-
if *manually_selected_only {
2028-
return (must_spend, vec![]);
2029-
}
2030-
2031-
let satisfies_confirmed = may_spend
2032-
.iter()
2033-
.map(|u| -> bool {
2034-
let txid = u.0.outpoint.txid;
2035-
let tx = match self.indexed_graph.graph().get_tx(txid) {
2036-
Some(tx) => tx,
2037-
None => return false,
2038-
};
2039-
2040-
// Whether the UTXO is mature and, if needed, confirmed
2041-
let mut spendable = true;
2042-
let chain_position = u.0.chain_position;
2043-
if must_only_use_confirmed_tx && !chain_position.is_confirmed() {
2044-
return false;
2045-
}
2046-
if tx.is_coinbase() {
2047-
debug_assert!(
2048-
chain_position.is_confirmed(),
2049-
"coinbase must always be confirmed"
2050-
);
2051-
if let Some(current_height) = current_height {
2052-
match chain_position {
2053-
ChainPosition::Confirmed { anchor, .. } => {
2054-
// https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375
2055-
spendable &= (current_height
2056-
.saturating_sub(anchor.block_id.height))
2057-
>= COINBASE_MATURITY;
2058-
}
2059-
ChainPosition::Unconfirmed { .. } => spendable = false,
2060-
}
2061-
}
1981+
self
1982+
// get all unspent UTxOs from wallet
1983+
.list_unspent()
1984+
// only process UTxOs not selected manually, they will be considered later in the chain
1985+
// NOTE: this avoid UTxOs in both required and optional list
1986+
.filter(|may_spend| !params.utxos.contains(may_spend))
1987+
// only process optional UTxOs if manually_selected_only is false
1988+
.take_while(|_| !params.manually_selected_only)
1989+
// only add to optional UTxOs those which satisfy the change policy if we reuse change
1990+
.filter(|local_output| {
1991+
self.keychains().count() == 1 || params.change_policy.is_satisfied_by(local_output)
1992+
})
1993+
// only add to optional UTxOs those marked as spendable
1994+
.filter(|local_output| !params.unspendable.contains(&local_output.outpoint))
1995+
// if bumping fees only add to optional UTxOs those confirmed
1996+
.filter(|local_output| {
1997+
params.bumping_fee.is_none() || local_output.chain_position.is_confirmed()
1998+
})
1999+
// only use UTxOs we posess and are mature if the origin tx is coinbase
2000+
.filter(|local_output| {
2001+
self.indexed_graph
2002+
.graph()
2003+
.get_tx(local_output.outpoint.txid)
2004+
.is_some_and(|tx| {
2005+
!tx.is_coinbase()
2006+
|| (local_output.chain_position.is_confirmed()
2007+
&& current_height.is_some_and(|current_height| {
2008+
local_output
2009+
.chain_position
2010+
.confirmation_height_upper_bound()
2011+
.is_some_and(|local_output_height| {
2012+
current_height.saturating_sub(local_output_height)
2013+
>= COINBASE_MATURITY
2014+
})
2015+
}))
2016+
})
2017+
})
2018+
// combine optional UTxOs with manually selected UTxOs
2019+
// NOTE: if manually_selected_only is true, then the previous iterator in the chain is
2020+
// empty and only manually selected UTxOs will be considered
2021+
// NOTE: manual selection overrides unspendable
2022+
.chain(params.utxos.clone())
2023+
// transform every UTxO into a WeightedUtxo
2024+
.map(|utxo| {
2025+
let keychain = utxo.keychain;
2026+
WeightedUtxo {
2027+
satisfaction_weight: self
2028+
.public_descriptor(keychain)
2029+
.max_weight_to_satisfy()
2030+
.unwrap(),
2031+
utxo: Utxo::Local(utxo),
20622032
}
2063-
spendable
20642033
})
2065-
.collect::<Vec<_>>();
2066-
2067-
let mut i = 0;
2068-
may_spend.retain(|u| {
2069-
let retain = (self.keychains().count() == 1 || change_policy.is_satisfied_by(&u.0))
2070-
&& !unspendable.contains(&u.0.outpoint)
2071-
&& satisfies_confirmed[i];
2072-
i += 1;
2073-
retain
2074-
});
2075-
2076-
let mut may_spend = may_spend
2077-
.into_iter()
2078-
.map(|(local_utxo, satisfaction_weight)| WeightedUtxo {
2079-
satisfaction_weight,
2080-
utxo: Utxo::Local(local_utxo),
2034+
// include foreign UTxOs
2035+
.chain(params.foreign_utxos.clone())
2036+
// TODO: here preselect_utxos could be splitted further into two functions to allow the
2037+
// addition of extra filters for inputs based on properties of the WeightedUtxos
2038+
//
2039+
// split UTxOs in required and optional
2040+
// if drain_wallet is true, all UTxOs are required, if false, only manually selected
2041+
// and foreign are required
2042+
// TODO: collection could be kept separate from selection
2043+
.partition::<Vec<WeightedUtxo>, _>(|x| {
2044+
params.drain_wallet
2045+
// keep UTxO in required UTxOs if
2046+
|| match x.utxo {
2047+
// it is in manually selected group
2048+
Utxo::Local(ref lo) => params.utxos.contains(lo),
2049+
// it is foreign
2050+
Utxo::Foreign {..} => true,
2051+
}
20812052
})
2082-
.collect();
2083-
2084-
if must_use_all_available {
2085-
must_spend.append(&mut may_spend);
2086-
}
2087-
2088-
(must_spend, may_spend)
20892053
}
20902054

20912055
fn complete_transaction(

crates/wallet/src/wallet/tx_builder.rs

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ pub(crate) struct TxParams {
125125
pub(crate) fee_policy: Option<FeePolicy>,
126126
pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>,
127127
pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
128-
pub(crate) utxos: Vec<WeightedUtxo>,
128+
pub(crate) utxos: HashSet<LocalOutput>,
129+
pub(crate) foreign_utxos: Vec<WeightedUtxo>,
129130
pub(crate) unspendable: HashSet<OutPoint>,
130131
pub(crate) manually_selected_only: bool,
131132
pub(crate) sighash: Option<psbt::PsbtSighashType>,
@@ -274,26 +275,14 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
274275
/// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
275276
/// the "utxos" and the "unspendable" list, it will be spent.
276277
pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, AddUtxoError> {
277-
{
278-
let wallet = &mut self.wallet;
279-
let utxos = outpoints
280-
.iter()
278+
let _ = outpoints.iter().try_for_each(|outpoint| {
279+
self.wallet
280+
.get_utxo(*outpoint)
281281
.map(|outpoint| {
282-
wallet
283-
.get_utxo(*outpoint)
284-
.ok_or(AddUtxoError::UnknownUtxo(*outpoint))
282+
self.params.utxos.insert(outpoint);
285283
})
286-
.collect::<Result<Vec<_>, _>>()?;
287-
288-
for utxo in utxos {
289-
let descriptor = wallet.public_descriptor(utxo.keychain);
290-
let satisfaction_weight = descriptor.max_weight_to_satisfy().unwrap();
291-
self.params.utxos.push(WeightedUtxo {
292-
satisfaction_weight,
293-
utxo: Utxo::Local(utxo),
294-
});
295-
}
296-
}
284+
.ok_or(AddUtxoError::UnknownUtxo(*outpoint))
285+
});
297286

298287
Ok(self)
299288
}
@@ -393,7 +382,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
393382
}
394383
}
395384

396-
self.params.utxos.push(WeightedUtxo {
385+
self.params.foreign_utxos.push(WeightedUtxo {
397386
satisfaction_weight,
398387
utxo: Utxo::Foreign {
399388
outpoint,

0 commit comments

Comments
 (0)