Skip to content

Commit 1975835

Browse files
committed
Merge #1798: Refactor/use iterators to preselect utxos
2f83b45 test(wallet): check there are no duplicates across required and optional utxos (nymius) 39df2b9 refactor(wallet): remove coin_selection::filter_duplicates (nymius) 79bd7da refactor(wallet): use iterators and adaptors in preselect_utxos (nymius) Pull request description: ### Description There were multiple calls for de-duplication of selected UTxOs in `Wallet::create_tx`: ([1](https://github.com/bitcoindevkit/bdk/blob/abc305612160c0e2ce85c7bba4c2c162ff488adc/crates/wallet/src/wallet/mod.rs#L2016-L2020)) and ([2](https://github.com/bitcoindevkit/bdk/blob/abc305612160c0e2ce85c7bba4c2c162ff488adc/crates/wallet/src/wallet/mod.rs#L1452)). As the test [`test_filter_duplicates`](https://github.com/bitcoindevkit/bdk/blob/master/crates/wallet/src/wallet/coin_selection.rs#L1666-L1695) 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`](https://github.com/bitcoindevkit/bdk/blob/a5335a18431dfecea1f524af44c953b79a96867c/crates/chain/src/tx_graph.rs#L911-L912) 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 does the following: - Refactors `TxParams::utxos` type to be `HashSet<LocalOutput>` avoiding the duplication case 3 - Keeps avoiding case 4 without overhead 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) thanks to the above change. - Still covers case 2 because the [source of `required_utxos`, `Wallet::list_unspent`](https://github.com/bitcoindevkit/bdk/blob/a5335a18431dfecea1f524af44c953b79a96867c/crates/chain/src/tx_graph.rs#L911-L912) comes from a `HashMap` which should avoid duplication by definition. - 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, e.g.: filter locked utxos ```rust .filter(|utxo| !self.is_utxo_locked(utxo.outpoint, self.latest_checkpoint().height())) ``` - Adds more comments for each filtering step. - Differentiates `foreign_utxos`, which should include a provided satisfation weight to use them effectively, and `utxos`, manually selected UTxOs for which the wallet can compute their satisfaction weight without external resources. With these changes all four cases would be covered, and `coin_selection::filter_duplicates` is no longer needed. Fixes #1794. ### Notes to the reviewers I added three test to cover the interesting cases for duplication: - there are no duplicates in required utxos - there are no duplicates in optional utxos - there are no duplicates across optional and required utxos the three of them have been prefixed with `not_duplicated_utxos*` to allow its joint execution under the command: ```bash cargo test -- not_duplicated_utxos ``` because the guarantees for the three conditions above are spread in different parts of the code. ### Changelog notice No changes to public APIs. ### Checklists * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing * [x] This pull request does not break the existing API * [x] I've added tests to reproduce the issue which are now passing * [x] I'm linking the issue being fixed by this PR ACKs for top commit: evanlinjin: ACK 2f83b45 Tree-SHA512: 82317acce8a7e83e4ea1a500605142026ab10d6842d4f278b09563566f1eb4d295f77a9c9616eff067211f0faa9f1e94370fc0ec3af48e6e1baef53b46979db7
2 parents 362c3dc + 2f83b45 commit 1975835

File tree

3 files changed

+465
-314
lines changed

3 files changed

+465
-314
lines changed

crates/wallet/src/wallet/coin_selection.rs

Lines changed: 2 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,13 @@
101101
//! # Ok::<(), anyhow::Error>(())
102102
//! ```
103103
104-
use crate::chain::collections::HashSet;
105104
use crate::wallet::utils::IsDust;
106105
use crate::Utxo;
107106
use crate::WeightedUtxo;
108107
use bitcoin::{Amount, FeeRate, SignedAmount};
109108

110109
use alloc::vec::Vec;
111110
use bitcoin::consensus::encode::serialize;
112-
use bitcoin::OutPoint;
113111
use bitcoin::TxIn;
114112
use bitcoin::{Script, Weight};
115113

@@ -720,38 +718,19 @@ fn calculate_cs_result(
720718
}
721719
}
722720

723-
/// Remove duplicate UTXOs.
724-
///
725-
/// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept.
726-
pub(crate) fn filter_duplicates<I>(required: I, optional: I) -> (I, I)
727-
where
728-
I: IntoIterator<Item = WeightedUtxo> + FromIterator<WeightedUtxo>,
729-
{
730-
let mut visited = HashSet::<OutPoint>::new();
731-
let required = required
732-
.into_iter()
733-
.filter(|utxo| visited.insert(utxo.utxo.outpoint()))
734-
.collect::<I>();
735-
let optional = optional
736-
.into_iter()
737-
.filter(|utxo| visited.insert(utxo.utxo.outpoint()))
738-
.collect::<I>();
739-
(required, optional)
740-
}
741-
742721
#[cfg(test)]
743722
mod test {
744723
use assert_matches::assert_matches;
745724
use bitcoin::hashes::Hash;
746-
use chain::{BlockId, ChainPosition, ConfirmationBlockTime};
725+
use bitcoin::OutPoint;
726+
use chain::{ChainPosition, ConfirmationBlockTime};
747727
use core::str::FromStr;
748728
use rand::rngs::StdRng;
749729

750730
use bitcoin::{Amount, BlockHash, ScriptBuf, TxIn, TxOut};
751731

752732
use super::*;
753733
use crate::types::*;
754-
use crate::wallet::coin_selection::filter_duplicates;
755734

756735
use rand::prelude::SliceRandom;
757736
use rand::{thread_rng, Rng, RngCore, SeedableRng};
@@ -1618,102 +1597,6 @@ mod test {
16181597
assert_eq!(res.selected_amount(), Amount::from_sat(200_000));
16191598
}
16201599

1621-
#[test]
1622-
fn test_filter_duplicates() {
1623-
fn utxo(txid: &str, value: u64) -> WeightedUtxo {
1624-
WeightedUtxo {
1625-
satisfaction_weight: Weight::ZERO,
1626-
utxo: Utxo::Local(LocalOutput {
1627-
outpoint: OutPoint::new(bitcoin::hashes::Hash::hash(txid.as_bytes()), 0),
1628-
txout: TxOut {
1629-
value: Amount::from_sat(value),
1630-
script_pubkey: ScriptBuf::new(),
1631-
},
1632-
keychain: KeychainKind::External,
1633-
is_spent: false,
1634-
derivation_index: 0,
1635-
chain_position: ChainPosition::Confirmed {
1636-
anchor: ConfirmationBlockTime {
1637-
block_id: BlockId {
1638-
height: 12345,
1639-
hash: BlockHash::all_zeros(),
1640-
},
1641-
confirmation_time: 12345,
1642-
},
1643-
transitively: None,
1644-
},
1645-
}),
1646-
}
1647-
}
1648-
1649-
fn to_utxo_vec(utxos: &[(&str, u64)]) -> Vec<WeightedUtxo> {
1650-
let mut v = utxos
1651-
.iter()
1652-
.map(|&(txid, value)| utxo(txid, value))
1653-
.collect::<Vec<_>>();
1654-
v.sort_by_key(|u| u.utxo.outpoint());
1655-
v
1656-
}
1657-
1658-
struct TestCase<'a> {
1659-
name: &'a str,
1660-
required: &'a [(&'a str, u64)],
1661-
optional: &'a [(&'a str, u64)],
1662-
exp_required: &'a [(&'a str, u64)],
1663-
exp_optional: &'a [(&'a str, u64)],
1664-
}
1665-
1666-
let test_cases = [
1667-
TestCase {
1668-
name: "no_duplicates",
1669-
required: &[("A", 1000), ("B", 2100)],
1670-
optional: &[("C", 1000)],
1671-
exp_required: &[("A", 1000), ("B", 2100)],
1672-
exp_optional: &[("C", 1000)],
1673-
},
1674-
TestCase {
1675-
name: "duplicate_required_utxos",
1676-
required: &[("A", 3000), ("B", 1200), ("C", 1234), ("A", 3000)],
1677-
optional: &[("D", 2100)],
1678-
exp_required: &[("A", 3000), ("B", 1200), ("C", 1234)],
1679-
exp_optional: &[("D", 2100)],
1680-
},
1681-
TestCase {
1682-
name: "duplicate_optional_utxos",
1683-
required: &[("A", 3000), ("B", 1200)],
1684-
optional: &[("C", 5000), ("D", 1300), ("C", 5000)],
1685-
exp_required: &[("A", 3000), ("B", 1200)],
1686-
exp_optional: &[("C", 5000), ("D", 1300)],
1687-
},
1688-
TestCase {
1689-
name: "duplicate_across_required_and_optional_utxos",
1690-
required: &[("A", 3000), ("B", 1200), ("C", 2100)],
1691-
optional: &[("A", 3000), ("D", 1200), ("E", 5000)],
1692-
exp_required: &[("A", 3000), ("B", 1200), ("C", 2100)],
1693-
exp_optional: &[("D", 1200), ("E", 5000)],
1694-
},
1695-
];
1696-
1697-
for (i, t) in test_cases.into_iter().enumerate() {
1698-
let (required, optional) =
1699-
filter_duplicates(to_utxo_vec(t.required), to_utxo_vec(t.optional));
1700-
assert_eq!(
1701-
required,
1702-
to_utxo_vec(t.exp_required),
1703-
"[{}:{}] unexpected `required` result",
1704-
i,
1705-
t.name
1706-
);
1707-
assert_eq!(
1708-
optional,
1709-
to_utxo_vec(t.exp_optional),
1710-
"[{}:{}] unexpected `optional` result",
1711-
i,
1712-
t.name
1713-
);
1714-
}
1715-
}
1716-
17171600
#[test]
17181601
fn test_deterministic_coin_selection_picks_same_utxos() {
17191602
enum CoinSelectionAlgo {

0 commit comments

Comments
 (0)