Skip to content

Commit 79bd7da

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 362c3dc commit 79bd7da

File tree

2 files changed

+393
-195
lines changed

2 files changed

+393
-195
lines changed

crates/wallet/src/wallet/mod.rs

Lines changed: 119 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use bdk_chain::{
3636
use bitcoin::{
3737
absolute,
3838
consensus::encode::serialize,
39-
constants::{genesis_block, COINBASE_MATURITY},
39+
constants::genesis_block,
4040
psbt,
4141
secp256k1::Secp256k1,
4242
sighash::{EcdsaSighashType, TapSighashType},
@@ -1417,8 +1417,19 @@ impl Wallet {
14171417

14181418
fee_amount += fee_rate * tx.weight();
14191419

1420-
let (required_utxos, optional_utxos) =
1421-
self.preselect_utxos(&params, Some(current_height.to_consensus_u32()));
1420+
let (required_utxos, optional_utxos) = {
1421+
// NOTE: manual selection overrides unspendable
1422+
let mut required: Vec<WeightedUtxo> = params.utxos.values().cloned().collect();
1423+
let optional = self.filter_utxos(&params, current_height.to_consensus_u32());
1424+
1425+
// if drain_wallet is true, all UTxOs are required
1426+
if params.drain_wallet {
1427+
required.extend(optional);
1428+
(required, vec![])
1429+
} else {
1430+
(required, optional)
1431+
}
1432+
};
14221433

14231434
// get drain script
14241435
let mut drain_index = Option::<(KeychainKind, u32)>::None;
@@ -1447,9 +1458,6 @@ impl Wallet {
14471458
}
14481459
};
14491460

1450-
let (required_utxos, optional_utxos) =
1451-
coin_selection::filter_duplicates(required_utxos, optional_utxos);
1452-
14531461
let coin_selection = coin_selection
14541462
.coin_select(
14551463
required_utxos,
@@ -1618,60 +1626,71 @@ impl Wallet {
16181626
.map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
16191627

16201628
// 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()
1629+
let utxos = tx
1630+
.input
1631+
.drain(..)
16241632
.map(|txin| -> Result<_, BuildFeeBumpError> {
1625-
let prev_tx = graph
1633+
graph
1634+
// Get previous transaction
16261635
.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,
1636+
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))
1637+
// Get chain position
1638+
.and_then(|prev_tx| {
1639+
chain_positions
1640+
.get(&txin.previous_output.txid)
1641+
.cloned()
1642+
.ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))
1643+
.map(|chain_position| (prev_tx, chain_position))
1644+
})
1645+
.map(|(prev_tx, chain_position)| {
1646+
let txout = prev_tx.output[txin.previous_output.vout as usize].clone();
1647+
match txout_index.index_of_spk(txout.script_pubkey.clone()) {
1648+
Some(&(keychain, derivation_index)) => (
1649+
txin.previous_output,
1650+
WeightedUtxo {
1651+
satisfaction_weight: self
1652+
.public_descriptor(keychain)
1653+
.max_weight_to_satisfy()
1654+
.unwrap(),
1655+
utxo: Utxo::Local(LocalOutput {
1656+
outpoint: txin.previous_output,
1657+
txout: txout.clone(),
1658+
keychain,
1659+
is_spent: true,
1660+
derivation_index,
1661+
chain_position,
1662+
}),
1663+
},
1664+
),
1665+
None => {
1666+
let satisfaction_weight = Weight::from_wu_usize(
1667+
serialize(&txin.script_sig).len() * 4
1668+
+ serialize(&txin.witness).len(),
1669+
);
1670+
1671+
(
1672+
txin.previous_output,
1673+
WeightedUtxo {
1674+
utxo: Utxo::Foreign {
1675+
outpoint: txin.previous_output,
1676+
sequence: txin.sequence,
1677+
psbt_input: Box::new(psbt::Input {
1678+
witness_utxo: txout
1679+
.script_pubkey
1680+
.witness_version()
1681+
.map(|_| txout.clone()),
1682+
non_witness_utxo: Some(prev_tx.as_ref().clone()),
1683+
..Default::default()
1684+
}),
1685+
},
1686+
satisfaction_weight,
1687+
},
1688+
)
1689+
}
16681690
}
1669-
}
1670-
};
1671-
1672-
Ok(weighted_utxo)
1691+
})
16731692
})
1674-
.collect::<Result<Vec<_>, _>>()?;
1693+
.collect::<Result<HashMap<OutPoint, WeightedUtxo>, BuildFeeBumpError>>()?;
16751694

16761695
if tx.output.len() > 1 {
16771696
let mut change_index = None;
@@ -1698,7 +1717,7 @@ impl Wallet {
16981717
.into_iter()
16991718
.map(|txout| (txout.script_pubkey, txout.value))
17001719
.collect(),
1701-
utxos: original_utxos,
1720+
utxos,
17021721
bumping_fee: Some(tx_builder::PreviousFee {
17031722
absolute: fee,
17041723
rate: fee_rate,
@@ -1976,117 +1995,52 @@ impl Wallet {
19761995
descriptor.at_derivation_index(child).ok()
19771996
}
19781997

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-
19921998
/// Given the options returns the list of utxos that must be used to form the
19931999
/// transaction and any further that may be used if needed.
1994-
fn preselect_utxos(
1995-
&self,
1996-
params: &TxParams,
1997-
current_height: Option<u32>,
1998-
) -> (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-
let spend_height = current_height + 1;
2056-
let coin_age_at_spend_height =
2057-
spend_height.saturating_sub(anchor.block_id.height);
2058-
spendable &= coin_age_at_spend_height >= COINBASE_MATURITY;
2059-
}
2060-
ChainPosition::Unconfirmed { .. } => spendable = false,
2061-
}
2062-
}
2063-
}
2064-
spendable
2065-
})
2066-
.collect::<Vec<_>>();
2067-
2068-
let mut i = 0;
2069-
may_spend.retain(|u| {
2070-
let retain = (self.keychains().count() == 1 || change_policy.is_satisfied_by(&u.0))
2071-
&& !unspendable.contains(&u.0.outpoint)
2072-
&& satisfies_confirmed[i];
2073-
i += 1;
2074-
retain
2075-
});
2076-
2077-
let mut may_spend = may_spend
2078-
.into_iter()
2079-
.map(|(local_utxo, satisfaction_weight)| WeightedUtxo {
2080-
satisfaction_weight,
2081-
utxo: Utxo::Local(local_utxo),
2082-
})
2083-
.collect();
2084-
2085-
if must_use_all_available {
2086-
must_spend.append(&mut may_spend);
2000+
fn filter_utxos(&self, params: &TxParams, current_height: u32) -> Vec<WeightedUtxo> {
2001+
if params.manually_selected_only {
2002+
vec![]
2003+
// only process optional UTxOs if manually_selected_only is false
2004+
} else {
2005+
self.indexed_graph
2006+
.graph()
2007+
// get all unspent UTxOs from wallet
2008+
// NOTE: the UTxOs returned by the following method already belong to wallet as the
2009+
// call chain uses get_tx_node infallibly
2010+
.filter_chain_unspents(
2011+
&self.chain,
2012+
self.chain.tip().block_id(),
2013+
self.indexed_graph.index.outpoints().iter().cloned(),
2014+
)
2015+
// only create LocalOutput if UTxO is mature
2016+
.filter_map(move |((k, i), full_txo)| {
2017+
full_txo
2018+
.is_mature(current_height)
2019+
.then(|| new_local_utxo(k, i, full_txo))
2020+
})
2021+
// only process UTxOs not selected manually, they will be considered later in the chain
2022+
// NOTE: this avoid UTxOs in both required and optional list
2023+
.filter(|may_spend| !params.utxos.contains_key(&may_spend.outpoint))
2024+
// only add to optional UTxOs those which satisfy the change policy if we reuse change
2025+
.filter(|local_output| {
2026+
self.keychains().count() == 1
2027+
|| params.change_policy.is_satisfied_by(local_output)
2028+
})
2029+
// only add to optional UTxOs those marked as spendable
2030+
.filter(|local_output| !params.unspendable.contains(&local_output.outpoint))
2031+
// if bumping fees only add to optional UTxOs those confirmed
2032+
.filter(|local_output| {
2033+
params.bumping_fee.is_none() || local_output.chain_position.is_confirmed()
2034+
})
2035+
.map(|utxo| WeightedUtxo {
2036+
satisfaction_weight: self
2037+
.public_descriptor(utxo.keychain)
2038+
.max_weight_to_satisfy()
2039+
.unwrap(),
2040+
utxo: Utxo::Local(utxo),
2041+
})
2042+
.collect()
20872043
}
2088-
2089-
(must_spend, may_spend)
20902044
}
20912045

20922046
fn complete_transaction(

0 commit comments

Comments
 (0)