breez_sdk_liquid/payjoin/utxo_select/
mod.rs

1mod asset;
2mod tests;
3
4use std::collections::BTreeMap;
5
6use anyhow::{anyhow, ensure, Result};
7use asset::{asset_select, AssetSelectRequest, AssetSelectResult};
8use lwk_wollet::elements::AssetId;
9
10use crate::payjoin::{
11    model::InOut,
12    network_fee::{self, TxFee},
13};
14
15// TOTAL_TRIES in Core:
16// https://github.com/bitcoin/bitcoin/blob/1d9da8da309d1dbf9aef15eb8dc43b4a2dc3d309/src/wallet/coinselection.cpp#L74
17const UTXO_SELECTION_ITERATION_LIMIT: u32 = 100_000;
18
19#[derive(Debug, Clone)]
20pub(crate) struct UtxoSelectRequest {
21    pub policy_asset: AssetId,
22    pub fee_asset: AssetId,
23    pub price: f64,
24    pub fixed_fee: u64,
25    pub wallet_utxos: Vec<InOut>,
26    pub server_utxos: Vec<InOut>,
27    pub user_outputs: Vec<InOut>,
28}
29
30#[derive(Debug)]
31pub(crate) struct UtxoSelectResult {
32    pub user_inputs: Vec<InOut>,
33    pub client_inputs: Vec<InOut>,
34    pub server_inputs: Vec<InOut>,
35
36    pub user_outputs: Vec<InOut>,
37    pub change_outputs: Vec<InOut>,
38    pub server_fee: InOut,
39    pub server_change: Option<InOut>,
40    pub fee_change: Option<InOut>,
41    pub network_fee: InOut,
42
43    pub cost: u64,
44}
45
46pub(crate) fn utxo_select(req: UtxoSelectRequest) -> Result<UtxoSelectResult> {
47    let utxo_select_res = utxo_select_inner(req.clone());
48
49    if let Ok(res) = &utxo_select_res {
50        validate_selection(&req, res)?;
51    }
52    utxo_select_res
53}
54
55pub(crate) fn utxo_select_fixed(
56    target_value: u64,
57    target_utxo_count: usize,
58    utxos: &[u64],
59) -> Option<Vec<u64>> {
60    let selected_utxos = utxos
61        .iter()
62        .copied()
63        .take(target_utxo_count)
64        .collect::<Vec<_>>();
65    let selected_value = selected_utxos.iter().sum::<u64>();
66    if selected_value < target_value {
67        None
68    } else {
69        Some(selected_utxos)
70    }
71}
72
73pub(crate) fn utxo_select_basic(target_value: u64, utxos: &[u64]) -> Option<Vec<u64>> {
74    let mut selected_utxos = Vec::new();
75    let mut selected_value = 0;
76
77    if target_value > 0 {
78        for utxo in utxos {
79            selected_utxos.push(*utxo);
80            selected_value += utxo;
81            if selected_value >= target_value {
82                break;
83            }
84        }
85    }
86
87    if selected_value < target_value {
88        None
89    } else {
90        Some(selected_utxos)
91    }
92}
93
94pub(crate) fn utxo_select_best(target_value: u64, utxos: &[u64]) -> Option<Vec<u64>> {
95    utxo_select_in_range(target_value, 0, 0, utxos)
96        .or_else(|| utxo_select_basic(target_value, utxos))
97}
98
99/// Try to select utxos so that their sum is in the range [target_value..target_value + upper_bound_delta].
100/// Set `upper_bound_delta` to 0 if you want to find utxos without change.
101/// All the values must be "sane" so their sum does not overflow.
102fn utxo_select_in_range(
103    target_value: u64,
104    upper_bound_delta: u64,
105    target_utxo_count: usize,
106    utxos: &[u64],
107) -> Option<Vec<u64>> {
108    let mut utxos = utxos.to_vec();
109    utxos.sort();
110    utxos.reverse();
111
112    let mut iteration = 0;
113    let mut index = 0;
114    let mut value = 0;
115
116    let mut current_change = 0;
117    let mut best_change = u64::MAX;
118
119    let mut index_selection: Vec<usize> = vec![];
120    let mut best_selection: Option<Vec<usize>> = None;
121
122    let upper_bound = target_value + upper_bound_delta;
123    let mut available_value = utxos.iter().sum::<u64>();
124
125    if available_value < target_value {
126        return None;
127    }
128
129    while iteration < UTXO_SELECTION_ITERATION_LIMIT {
130        let mut step_back = false;
131
132        if available_value + value < target_value
133            // If any of the conditions are met, step back.
134            //
135            // Provides an upper bound on the change value that is allowed.
136            // Since value is lost when we create a change output due to increasing the size of the
137            // transaction by an output (the change output), we accept solutions that may be
138            // larger than the target.  The change is added to the solutions change score.
139            // However, values greater than value + upper_bound_delta are not considered.
140            //
141            // This creates a range of possible solutions where;
142            // range = (target, target + upper_bound_delta]
143            //
144            // That is, the range includes solutions that exactly equal the target up to but not
145            // including values greater than target + upper_bound_delta.
146            || value > upper_bound
147            || current_change > best_change
148        {
149            step_back = true;
150        } else if value >= target_value {
151            // Value meets or exceeds the target.
152            // Record the solution and the change then continue.
153            step_back = true;
154
155            let change = value - target_value;
156            current_change += change;
157
158            // Check if index_selection is better than the previous known best, and
159            // update best_selection accordingly.
160            if current_change <= best_change
161                && ((target_utxo_count == 0
162                    && best_selection.clone().is_none_or(|best_selection| {
163                        index_selection.len() <= best_selection.len()
164                    }))
165                    || index_selection.len() == target_utxo_count)
166            {
167                best_selection = Some(index_selection.clone());
168                best_change = current_change;
169            }
170
171            current_change = current_change.saturating_sub(change);
172        }
173
174        if target_utxo_count != 0 && index_selection.len() >= target_utxo_count {
175            step_back = true;
176        }
177
178        if step_back {
179            // Step back
180            if index_selection.is_empty() {
181                break;
182            }
183
184            loop {
185                index -= 1;
186
187                if index_selection.last().is_none_or(|last| index <= *last) {
188                    break;
189                }
190
191                let utxo_value = utxos[index];
192                available_value += utxo_value;
193            }
194
195            if index_selection.last().is_some_and(|last| index == *last) {
196                let utxo_value = utxos[index];
197                value = value.saturating_sub(utxo_value);
198                index_selection.pop();
199            }
200        } else {
201            // Add the utxo to the current selection
202            let utxo_value = utxos[index];
203
204            index_selection.push(index);
205
206            value += utxo_value;
207            available_value = available_value.saturating_sub(utxo_value);
208        }
209
210        index += 1;
211        iteration += 1;
212    }
213
214    best_selection.map(|best_selection| best_selection.iter().map(|index| utxos[*index]).collect())
215}
216
217fn utxo_select_inner(
218    UtxoSelectRequest {
219        policy_asset,
220        fee_asset,
221        price,
222        fixed_fee,
223        wallet_utxos,
224        server_utxos,
225        user_outputs,
226    }: UtxoSelectRequest,
227) -> Result<UtxoSelectResult> {
228    ensure!(fee_asset != policy_asset);
229    ensure!(price > 0.0);
230    ensure!(fixed_fee > 0);
231    ensure!(wallet_utxos.iter().all(|utxo| utxo.value > 0));
232
233    ensure!(server_utxos
234        .iter()
235        .all(|utxo| utxo.asset_id == policy_asset && utxo.value > 0));
236
237    let fee_utoxs = wallet_utxos
238        .iter()
239        .filter(|utxo| utxo.asset_id == fee_asset)
240        .map(|utxo| utxo.value)
241        .collect::<Vec<_>>();
242    let mut server_utxos = server_utxos
243        .iter()
244        .map(|utxo| utxo.value)
245        .collect::<Vec<_>>();
246    server_utxos.sort();
247    server_utxos.reverse();
248
249    let AssetSelectResult {
250        asset_inputs,
251        user_outputs,
252        change_outputs,
253        user_output_amounts,
254    } = asset_select(AssetSelectRequest {
255        fee_asset,
256        wallet_utxos,
257        user_outputs,
258    })?;
259
260    let mut best_selection: Option<UtxoSelectResult> = None;
261
262    for with_fee_change in [false, true] {
263        for with_server_change in [false, true] {
264            for server_input_count in 1..=server_utxos.len() {
265                for fee_input_count in 1..=fee_utoxs.len() {
266                    if fee_input_count != fee_utoxs.len() {
267                        continue;
268                    }
269                    let user_input_count = asset_inputs.len() + fee_input_count;
270
271                    let output_count = user_outputs.len()
272                        + change_outputs.len()
273                        + usize::from(with_fee_change)
274                        + usize::from(with_server_change)
275                        + 1; // Server fee output
276
277                    let min_network_fee = TxFee {
278                        server_inputs: server_input_count,
279                        user_inputs: user_input_count,
280                        outputs: output_count,
281                    }
282                    .fee();
283
284                    let server_inputs = if with_server_change {
285                        utxo_select_fixed(min_network_fee + 1, server_input_count, &server_utxos)
286                    } else {
287                        let upper_bound_delta = network_fee::weight_to_fee(
288                            network_fee::WEIGHT_VOUT_NESTED,
289                            network_fee::MIN_FEE_RATE,
290                        );
291                        utxo_select_in_range(
292                            min_network_fee,
293                            upper_bound_delta,
294                            server_input_count,
295                            &server_utxos,
296                        )
297                    };
298
299                    let server_inputs = match server_inputs {
300                        Some(server_inputs) => server_inputs,
301                        None => continue,
302                    };
303
304                    let server_input = server_inputs.iter().sum::<u64>();
305                    let server_change = if with_server_change {
306                        server_input - min_network_fee
307                    } else {
308                        0
309                    };
310                    let network_fee = server_input - server_change;
311                    let min_asset_fee = (network_fee as f64 * price) as u64 + fixed_fee;
312
313                    let user_asset_output = user_output_amounts
314                        .get(&fee_asset)
315                        .copied()
316                        .unwrap_or_default();
317
318                    let fee_asset_target = user_asset_output + min_asset_fee;
319                    let fee_asset_inputs = if with_fee_change {
320                        utxo_select_fixed(fee_asset_target + 1, fee_input_count, &fee_utoxs)
321                    } else {
322                        let upper_bound_delta = (network_fee::weight_to_fee(
323                            network_fee::WEIGHT_VOUT_NESTED,
324                            network_fee::MIN_FEE_RATE,
325                        ) as f64
326                            * price) as u64;
327                        utxo_select_in_range(
328                            fee_asset_target,
329                            upper_bound_delta,
330                            fee_input_count,
331                            &fee_utoxs,
332                        )
333                    };
334
335                    let fee_asset_inputs = match fee_asset_inputs {
336                        Some(fee_asset_inputs) => fee_asset_inputs,
337                        None => continue,
338                    };
339
340                    let fee_input = fee_asset_inputs.iter().sum::<u64>();
341                    let fee_change = if with_fee_change {
342                        fee_input - fee_asset_target
343                    } else {
344                        0
345                    };
346                    let server_fee = fee_input - fee_change - user_asset_output;
347                    let new_cost = server_fee;
348
349                    if best_selection
350                        .as_ref()
351                        .map(|best| best.cost > new_cost)
352                        .unwrap_or(true)
353                    {
354                        let user_outputs = user_outputs.clone();
355
356                        best_selection = Some(UtxoSelectResult {
357                            user_inputs: asset_inputs.clone(),
358                            client_inputs: fee_asset_inputs
359                                .iter()
360                                .map(|value| InOut {
361                                    asset_id: fee_asset,
362                                    value: *value,
363                                })
364                                .collect(),
365                            server_inputs: server_inputs
366                                .iter()
367                                .map(|value| InOut {
368                                    asset_id: policy_asset,
369                                    value: *value,
370                                })
371                                .collect(),
372                            user_outputs,
373                            change_outputs: change_outputs.clone(),
374                            server_fee: InOut {
375                                asset_id: fee_asset,
376                                value: server_fee,
377                            },
378                            server_change: with_server_change.then_some(InOut {
379                                asset_id: policy_asset,
380                                value: server_change,
381                            }),
382                            fee_change: with_fee_change.then_some(InOut {
383                                asset_id: fee_asset,
384                                value: fee_change,
385                            }),
386                            network_fee: InOut {
387                                asset_id: policy_asset,
388                                value: network_fee,
389                            },
390                            cost: new_cost,
391                        })
392                    };
393                }
394            }
395        }
396    }
397
398    best_selection.ok_or_else(|| anyhow!("Utxo selection failed"))
399}
400
401fn validate_selection(
402    req: &UtxoSelectRequest,
403    UtxoSelectResult {
404        user_inputs,
405        client_inputs,
406        server_inputs,
407        user_outputs,
408        change_outputs,
409        server_fee,
410        server_change,
411        fee_change,
412        network_fee,
413        cost: _,
414    }: &UtxoSelectResult,
415) -> Result<()> {
416    let mut inputs = BTreeMap::<AssetId, u64>::new();
417    let mut outputs = BTreeMap::<AssetId, u64>::new();
418
419    for input in user_inputs
420        .iter()
421        .chain(client_inputs.iter())
422        .chain(server_inputs.iter())
423    {
424        *inputs.entry(input.asset_id).or_default() += input.value;
425    }
426
427    for output in user_outputs
428        .iter()
429        .chain(change_outputs.iter())
430        .chain(std::iter::once(server_fee))
431        .chain(server_change.iter())
432        .chain(fee_change.iter())
433        .chain(std::iter::once(network_fee))
434    {
435        *outputs.entry(output.asset_id).or_default() += output.value;
436    }
437
438    ensure!(inputs == outputs, "Check failed: {inputs:?} != {outputs:?}");
439
440    let client_input_count = user_inputs.len() + client_inputs.len();
441    let server_input_count = server_inputs.len();
442    let output_count = user_outputs.len()
443        + change_outputs.len()
444        + 1
445        + usize::from(server_change.is_some())
446        + usize::from(fee_change.is_some());
447
448    let min_network_fee = TxFee {
449        server_inputs: server_input_count,
450        user_inputs: client_input_count,
451        outputs: output_count,
452    }
453    .fee();
454
455    let actual_network_fee = network_fee.value;
456    ensure!(actual_network_fee >= min_network_fee);
457    ensure!(actual_network_fee <= 2 * min_network_fee);
458
459    let min_server_fee = (actual_network_fee as f64 * req.price) as u64 + req.fixed_fee;
460    let actual_server_fee = server_fee.value;
461    ensure!(actual_server_fee >= min_server_fee);
462    ensure!(actual_server_fee <= 2 * min_server_fee);
463
464    Ok(())
465}