breez_sdk_liquid/payjoin/utxo_select/
mod.rs1mod 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
15const 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
99fn 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 || value > upper_bound
147 || current_change > best_change
148 {
149 step_back = true;
150 } else if value >= target_value {
151 step_back = true;
154
155 let change = value - target_value;
156 current_change += change;
157
158 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 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 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; 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}