Skip to content

Commit 09bab5c

Browse files
committed
lsps2: Fail over lease negotiation
Try the next eligible provider when a selected LSP rejects or times out during the buy request. Skip remaining fee-menu entries from a failed provider while preserving bounded whole-round retries. Co-Authored-By: HAL 9000
1 parent 9c69063 commit 09bab5c

1 file changed

Lines changed: 147 additions & 61 deletions

File tree

  • src/liquidity/client/lsps2

src/liquidity/client/lsps2/mod.rs

Lines changed: 147 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
66
// accordance with one or both of these licenses.
77

8-
use std::collections::{BTreeMap, HashMap};
8+
use std::collections::{BTreeMap, HashMap, HashSet};
99
use std::future::Future;
10+
use std::hash::Hash;
1011
use std::ops::Deref;
1112
use std::sync::{Arc, Mutex, RwLock};
1213
use std::time::Duration;
@@ -69,6 +70,33 @@ pub(crate) struct JitInvoiceResponse {
6970
pub(crate) allow_mpp: bool,
7071
}
7172

73+
async fn try_lease_candidates<T, K, R, E, KF, AF, Fut>(
74+
candidates: Vec<T>, candidate_key: KF, mut attempt: AF,
75+
) -> Result<R, E>
76+
where
77+
K: Copy + Eq + Hash,
78+
KF: Fn(&T) -> K,
79+
AF: FnMut(T) -> Fut,
80+
Fut: Future<Output = Result<R, E>>,
81+
{
82+
let mut failed_candidates = HashSet::new();
83+
let mut last_error = None;
84+
for candidate in candidates {
85+
let key = candidate_key(&candidate);
86+
if failed_candidates.contains(&key) {
87+
continue;
88+
}
89+
match attempt(candidate).await {
90+
Ok(result) => return Ok(result),
91+
Err(error) => {
92+
failed_candidates.insert(key);
93+
last_error = Some(error);
94+
},
95+
}
96+
}
97+
Err(last_error.expect("lease candidates are non-empty"))
98+
}
99+
72100
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73101
pub(crate) enum JitInvoiceRequest {
74102
Fixed { amount_msat: u64, absolute_expiry: Option<u64> },
@@ -331,13 +359,7 @@ where
331359
) -> Result<(PaymentLease, u64, LspConfig), Error> {
332360
let mut attempt = 1;
333361
loop {
334-
let result = self
335-
.negotiate_fixed_lease_once(
336-
amount_msat,
337-
max_total_lsp_fee_limit_msat,
338-
connection_manager,
339-
)
340-
.await;
362+
let result = self.negotiate_fixed_lease_once(amount_msat, connection_manager).await;
341363
match result {
342364
Err(error) if should_retry_lease_negotiation(error, attempt) => {
343365
log_warn!(
@@ -354,11 +376,10 @@ where
354376
}
355377

356378
async fn negotiate_fixed_lease_once(
357-
self: &Arc<Self>, amount_msat: u64, max_total_lsp_fee_limit_msat: Option<u64>,
358-
connection_manager: &Arc<ConnectionManager<L>>,
379+
self: &Arc<Self>, amount_msat: u64, connection_manager: &Arc<ConnectionManager<L>>,
359380
) -> Result<(PaymentLease, u64, LspConfig), Error> {
360381
let all_offers = self.gather_lsps2_offers(connection_manager).await?;
361-
let (cheapest_lsp, min_total_fee_msat, min_opening_params) = all_offers
382+
let mut candidates = all_offers
362383
.into_iter()
363384
.flat_map(|(lsp, resp)| {
364385
resp.opening_fee_params_menu
@@ -382,11 +403,12 @@ where
382403
.map(|fee| (lsp, fee, params))
383404
}
384405
})
385-
.min_by_key(|(_, fee, _)| *fee)
386-
.ok_or_else(|| {
387-
log_error!(self.logger, "Failed to handle response from liquidity service",);
388-
Error::LiquidityRequestFailed
389-
})?;
406+
.collect::<Vec<_>>();
407+
candidates.sort_unstable_by_key(|(_, fee, _)| *fee);
408+
let min_total_fee_msat = candidates.first().map(|(_, fee, _)| *fee).ok_or_else(|| {
409+
log_error!(self.logger, "Failed to handle response from liquidity service",);
410+
Error::LiquidityRequestFailed
411+
})?;
390412

391413
if let Some(max_total_lsp_fee_limit_msat) = self.config.lsps2_max_total_lsp_fee_limit_msat {
392414
if min_total_fee_msat > max_total_lsp_fee_limit_msat {
@@ -396,23 +418,44 @@ where
396418
);
397419
return Err(Error::LiquidityFeeTooHigh);
398420
}
421+
candidates.retain(|(_, fee, _)| *fee <= max_total_lsp_fee_limit_msat);
399422
}
400423

401-
log_debug!(
402-
self.logger,
403-
"Choosing cheapest liquidity offer from LSP {}, will pay {}msat in total LSP fees",
404-
cheapest_lsp.node_id,
405-
min_total_fee_msat
406-
);
407-
408-
let negotiated_lease = self
409-
.lsps2_send_buy_request(
410-
Some(amount_msat),
411-
min_opening_params,
412-
Some(&cheapest_lsp.node_id),
413-
)
414-
.await?;
415-
Ok((negotiated_lease, min_total_fee_msat, cheapest_lsp))
424+
try_lease_candidates(
425+
candidates,
426+
|(lsp, _, _)| lsp.node_id,
427+
|(lsp, total_fee_msat, opening_params)| {
428+
let client = Arc::clone(self);
429+
async move {
430+
log_debug!(
431+
client.logger,
432+
"Choosing liquidity offer from LSP {}, will pay {}msat in total LSP fees",
433+
lsp.node_id,
434+
total_fee_msat
435+
);
436+
match client
437+
.lsps2_send_buy_request(
438+
Some(amount_msat),
439+
opening_params,
440+
Some(&lsp.node_id),
441+
)
442+
.await
443+
{
444+
Ok(lease) => Ok((lease, total_fee_msat, lsp)),
445+
Err(error) => {
446+
log_warn!(
447+
client.logger,
448+
"Failed negotiating LSPS2 payment lease with LSP {}, trying the next candidate: {}",
449+
lsp.node_id,
450+
error
451+
);
452+
Err(error)
453+
},
454+
}
455+
}
456+
},
457+
)
458+
.await
416459
}
417460

418461
async fn acquire_variable_lease(
@@ -445,12 +488,7 @@ where
445488
) -> Result<(PaymentLease, u64, LspConfig), Error> {
446489
let mut attempt = 1;
447490
loop {
448-
let result = self
449-
.negotiate_variable_lease_once(
450-
max_proportional_lsp_fee_limit_ppm_msat,
451-
connection_manager,
452-
)
453-
.await;
491+
let result = self.negotiate_variable_lease_once(connection_manager).await;
454492
match result {
455493
Err(error) if should_retry_lease_negotiation(error, attempt) => {
456494
log_warn!(
@@ -467,12 +505,11 @@ where
467505
}
468506

469507
async fn negotiate_variable_lease_once(
470-
self: &Arc<Self>, max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
471-
connection_manager: &Arc<ConnectionManager<L>>,
508+
self: &Arc<Self>, connection_manager: &Arc<ConnectionManager<L>>,
472509
) -> Result<(PaymentLease, u64, LspConfig), Error> {
473510
let all_offers = self.gather_lsps2_offers(connection_manager).await?;
474511
let mut rejected_for_fee = false;
475-
let (cheapest_lsp, min_prop_fee_ppm_msat, min_opening_params) = all_offers
512+
let mut candidates = all_offers
476513
.into_iter()
477514
.flat_map(|(lsp, resp)| {
478515
resp.opening_fee_params_menu.into_iter().map(move |params| (lsp.clone(), params))
@@ -489,29 +526,52 @@ where
489526
rejected_for_fee |= !allowed;
490527
allowed
491528
})
492-
.min_by_key(|(_, ppm, _)| *ppm)
493-
.ok_or_else(|| {
494-
if rejected_for_fee {
495-
log_error!(
496-
self.logger,
497-
"Failed to request inbound JIT channel as all LSP offers exceed our configured fee limit"
498-
);
499-
return Error::LiquidityFeeTooHigh;
500-
}
529+
.collect::<Vec<_>>();
530+
candidates.sort_unstable_by_key(|(_, ppm, _)| *ppm);
531+
if candidates.is_empty() {
532+
return Err(if rejected_for_fee {
533+
log_error!(
534+
self.logger,
535+
"Failed to request inbound JIT channel as all LSP offers exceed our configured fee limit"
536+
);
537+
Error::LiquidityFeeTooHigh
538+
} else {
501539
log_error!(self.logger, "Failed to handle response from liquidity service",);
502540
Error::LiquidityRequestFailed
503-
})?;
504-
log_debug!(
505-
self.logger,
506-
"Choosing cheapest liquidity offer from LSP {}, will pay {}ppm msat in proportional LSP fees",
507-
cheapest_lsp.node_id,
508-
min_prop_fee_ppm_msat
509-
);
510-
511-
let negotiated_lease = self
512-
.lsps2_send_buy_request(None, min_opening_params, Some(&cheapest_lsp.node_id))
513-
.await?;
514-
Ok((negotiated_lease, min_prop_fee_ppm_msat, cheapest_lsp))
541+
});
542+
}
543+
544+
try_lease_candidates(
545+
candidates,
546+
|(lsp, _, _)| lsp.node_id,
547+
|(lsp, proportional_fee_ppm_msat, opening_params)| {
548+
let client = Arc::clone(self);
549+
async move {
550+
log_debug!(
551+
client.logger,
552+
"Choosing liquidity offer from LSP {}, will pay {}ppm msat in proportional LSP fees",
553+
lsp.node_id,
554+
proportional_fee_ppm_msat
555+
);
556+
match client
557+
.lsps2_send_buy_request(None, opening_params, Some(&lsp.node_id))
558+
.await
559+
{
560+
Ok(lease) => Ok((lease, proportional_fee_ppm_msat, lsp)),
561+
Err(error) => {
562+
log_warn!(
563+
client.logger,
564+
"Failed negotiating LSPS2 payment lease with LSP {}, trying the next candidate: {}",
565+
lsp.node_id,
566+
error
567+
);
568+
Err(error)
569+
},
570+
}
571+
}
572+
},
573+
)
574+
.await
515575
}
516576

517577
fn schedule_fixed_lease_refill(
@@ -1050,6 +1110,32 @@ mod tests {
10501110
assert!(!should_retry_lease_negotiation(Error::LiquidityFeeTooHigh, 1));
10511111
assert!(!should_retry_lease_negotiation(Error::LiquiditySourceUnavailable, 1));
10521112
}
1113+
1114+
#[tokio::test]
1115+
async fn lease_negotiation_fails_over_between_lsps() {
1116+
let candidates = vec![(1, 10), (1, 20), (2, 30)];
1117+
let attempted_lsps = Arc::new(Mutex::new(Vec::new()));
1118+
let attempted_lsps_ref = Arc::clone(&attempted_lsps);
1119+
let result = try_lease_candidates(
1120+
candidates,
1121+
|candidate| candidate.0,
1122+
move |candidate| {
1123+
let attempted_lsps = Arc::clone(&attempted_lsps_ref);
1124+
async move {
1125+
attempted_lsps.lock().unwrap().push(candidate.0);
1126+
if candidate.0 == 1 {
1127+
Err(())
1128+
} else {
1129+
Ok(candidate.1)
1130+
}
1131+
}
1132+
},
1133+
)
1134+
.await;
1135+
1136+
assert_eq!(result, Ok(30));
1137+
assert_eq!(*attempted_lsps.lock().unwrap(), vec![1, 2]);
1138+
}
10531139
}
10541140

10551141
pub(crate) mod router;

0 commit comments

Comments
 (0)