Skip to content

Commit af17f49

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 f62b001 commit af17f49

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;
@@ -68,6 +69,33 @@ pub(crate) struct JitInvoiceResponse {
6869
pub(crate) allow_mpp: bool,
6970
}
7071

72+
async fn try_lease_candidates<T, K, R, E, KF, AF, Fut>(
73+
candidates: Vec<T>, candidate_key: KF, mut attempt: AF,
74+
) -> Result<R, E>
75+
where
76+
K: Copy + Eq + Hash,
77+
KF: Fn(&T) -> K,
78+
AF: FnMut(T) -> Fut,
79+
Fut: Future<Output = Result<R, E>>,
80+
{
81+
let mut failed_candidates = HashSet::new();
82+
let mut last_error = None;
83+
for candidate in candidates {
84+
let key = candidate_key(&candidate);
85+
if failed_candidates.contains(&key) {
86+
continue;
87+
}
88+
match attempt(candidate).await {
89+
Ok(result) => return Ok(result),
90+
Err(error) => {
91+
failed_candidates.insert(key);
92+
last_error = Some(error);
93+
},
94+
}
95+
}
96+
Err(last_error.expect("lease candidates are non-empty"))
97+
}
98+
7199
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72100
pub(crate) enum JitInvoiceRequest {
73101
Fixed { amount_msat: u64, absolute_expiry: Option<u64> },
@@ -320,13 +348,7 @@ where
320348
) -> Result<(PaymentLease, u64, LspConfig), Error> {
321349
let mut attempt = 1;
322350
loop {
323-
let result = self
324-
.negotiate_fixed_lease_once(
325-
amount_msat,
326-
max_total_lsp_fee_limit_msat,
327-
connection_manager,
328-
)
329-
.await;
351+
let result = self.negotiate_fixed_lease_once(amount_msat, connection_manager).await;
330352
match result {
331353
Err(error) if should_retry_lease_negotiation(error, attempt) => {
332354
log_warn!(
@@ -343,11 +365,10 @@ where
343365
}
344366

345367
async fn negotiate_fixed_lease_once(
346-
self: &Arc<Self>, amount_msat: u64, max_total_lsp_fee_limit_msat: Option<u64>,
347-
connection_manager: &Arc<ConnectionManager<L>>,
368+
self: &Arc<Self>, amount_msat: u64, connection_manager: &Arc<ConnectionManager<L>>,
348369
) -> Result<(PaymentLease, u64, LspConfig), Error> {
349370
let all_offers = self.gather_lsps2_offers(connection_manager).await?;
350-
let (cheapest_lsp, min_total_fee_msat, min_opening_params) = all_offers
371+
let mut candidates = all_offers
351372
.into_iter()
352373
.flat_map(|(lsp, resp)| {
353374
resp.opening_fee_params_menu
@@ -371,11 +392,12 @@ where
371392
.map(|fee| (lsp, fee, params))
372393
}
373394
})
374-
.min_by_key(|(_, fee, _)| *fee)
375-
.ok_or_else(|| {
376-
log_error!(self.logger, "Failed to handle response from liquidity service",);
377-
Error::LiquidityRequestFailed
378-
})?;
395+
.collect::<Vec<_>>();
396+
candidates.sort_unstable_by_key(|(_, fee, _)| *fee);
397+
let min_total_fee_msat = candidates.first().map(|(_, fee, _)| *fee).ok_or_else(|| {
398+
log_error!(self.logger, "Failed to handle response from liquidity service",);
399+
Error::LiquidityRequestFailed
400+
})?;
379401

380402
if let Some(max_total_lsp_fee_limit_msat) = self.config.lsps2_max_total_lsp_fee_limit_msat {
381403
if min_total_fee_msat > max_total_lsp_fee_limit_msat {
@@ -385,23 +407,44 @@ where
385407
);
386408
return Err(Error::LiquidityFeeTooHigh);
387409
}
410+
candidates.retain(|(_, fee, _)| *fee <= max_total_lsp_fee_limit_msat);
388411
}
389412

390-
log_debug!(
391-
self.logger,
392-
"Choosing cheapest liquidity offer from LSP {}, will pay {}msat in total LSP fees",
393-
cheapest_lsp.node_id,
394-
min_total_fee_msat
395-
);
396-
397-
let negotiated_lease = self
398-
.lsps2_send_buy_request(
399-
Some(amount_msat),
400-
min_opening_params,
401-
Some(&cheapest_lsp.node_id),
402-
)
403-
.await?;
404-
Ok((negotiated_lease, min_total_fee_msat, cheapest_lsp))
413+
try_lease_candidates(
414+
candidates,
415+
|(lsp, _, _)| lsp.node_id,
416+
|(lsp, total_fee_msat, opening_params)| {
417+
let client = Arc::clone(self);
418+
async move {
419+
log_debug!(
420+
client.logger,
421+
"Choosing liquidity offer from LSP {}, will pay {}msat in total LSP fees",
422+
lsp.node_id,
423+
total_fee_msat
424+
);
425+
match client
426+
.lsps2_send_buy_request(
427+
Some(amount_msat),
428+
opening_params,
429+
Some(&lsp.node_id),
430+
)
431+
.await
432+
{
433+
Ok(lease) => Ok((lease, total_fee_msat, lsp)),
434+
Err(error) => {
435+
log_warn!(
436+
client.logger,
437+
"Failed negotiating LSPS2 payment lease with LSP {}, trying the next candidate: {}",
438+
lsp.node_id,
439+
error
440+
);
441+
Err(error)
442+
},
443+
}
444+
}
445+
},
446+
)
447+
.await
405448
}
406449

407450
async fn acquire_variable_lease(
@@ -434,12 +477,7 @@ where
434477
) -> Result<(PaymentLease, u64, LspConfig), Error> {
435478
let mut attempt = 1;
436479
loop {
437-
let result = self
438-
.negotiate_variable_lease_once(
439-
max_proportional_lsp_fee_limit_ppm_msat,
440-
connection_manager,
441-
)
442-
.await;
480+
let result = self.negotiate_variable_lease_once(connection_manager).await;
443481
match result {
444482
Err(error) if should_retry_lease_negotiation(error, attempt) => {
445483
log_warn!(
@@ -456,12 +494,11 @@ where
456494
}
457495

458496
async fn negotiate_variable_lease_once(
459-
self: &Arc<Self>, max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
460-
connection_manager: &Arc<ConnectionManager<L>>,
497+
self: &Arc<Self>, connection_manager: &Arc<ConnectionManager<L>>,
461498
) -> Result<(PaymentLease, u64, LspConfig), Error> {
462499
let all_offers = self.gather_lsps2_offers(connection_manager).await?;
463500
let mut rejected_for_fee = false;
464-
let (cheapest_lsp, min_prop_fee_ppm_msat, min_opening_params) = all_offers
501+
let mut candidates = all_offers
465502
.into_iter()
466503
.flat_map(|(lsp, resp)| {
467504
resp.opening_fee_params_menu.into_iter().map(move |params| (lsp.clone(), params))
@@ -478,29 +515,52 @@ where
478515
rejected_for_fee |= !allowed;
479516
allowed
480517
})
481-
.min_by_key(|(_, ppm, _)| *ppm)
482-
.ok_or_else(|| {
483-
if rejected_for_fee {
484-
log_error!(
485-
self.logger,
486-
"Failed to request inbound JIT channel as all LSP offers exceed our configured fee limit"
487-
);
488-
return Error::LiquidityFeeTooHigh;
489-
}
518+
.collect::<Vec<_>>();
519+
candidates.sort_unstable_by_key(|(_, ppm, _)| *ppm);
520+
if candidates.is_empty() {
521+
return Err(if rejected_for_fee {
522+
log_error!(
523+
self.logger,
524+
"Failed to request inbound JIT channel as all LSP offers exceed our configured fee limit"
525+
);
526+
Error::LiquidityFeeTooHigh
527+
} else {
490528
log_error!(self.logger, "Failed to handle response from liquidity service",);
491529
Error::LiquidityRequestFailed
492-
})?;
493-
log_debug!(
494-
self.logger,
495-
"Choosing cheapest liquidity offer from LSP {}, will pay {}ppm msat in proportional LSP fees",
496-
cheapest_lsp.node_id,
497-
min_prop_fee_ppm_msat
498-
);
499-
500-
let negotiated_lease = self
501-
.lsps2_send_buy_request(None, min_opening_params, Some(&cheapest_lsp.node_id))
502-
.await?;
503-
Ok((negotiated_lease, min_prop_fee_ppm_msat, cheapest_lsp))
530+
});
531+
}
532+
533+
try_lease_candidates(
534+
candidates,
535+
|(lsp, _, _)| lsp.node_id,
536+
|(lsp, proportional_fee_ppm_msat, opening_params)| {
537+
let client = Arc::clone(self);
538+
async move {
539+
log_debug!(
540+
client.logger,
541+
"Choosing liquidity offer from LSP {}, will pay {}ppm msat in proportional LSP fees",
542+
lsp.node_id,
543+
proportional_fee_ppm_msat
544+
);
545+
match client
546+
.lsps2_send_buy_request(None, opening_params, Some(&lsp.node_id))
547+
.await
548+
{
549+
Ok(lease) => Ok((lease, proportional_fee_ppm_msat, lsp)),
550+
Err(error) => {
551+
log_warn!(
552+
client.logger,
553+
"Failed negotiating LSPS2 payment lease with LSP {}, trying the next candidate: {}",
554+
lsp.node_id,
555+
error
556+
);
557+
Err(error)
558+
},
559+
}
560+
}
561+
},
562+
)
563+
.await
504564
}
505565

506566
fn schedule_fixed_lease_refill(
@@ -1039,6 +1099,32 @@ mod tests {
10391099
assert!(!should_retry_lease_negotiation(Error::LiquidityFeeTooHigh, 1));
10401100
assert!(!should_retry_lease_negotiation(Error::LiquiditySourceUnavailable, 1));
10411101
}
1102+
1103+
#[tokio::test]
1104+
async fn lease_negotiation_fails_over_between_lsps() {
1105+
let candidates = vec![(1, 10), (1, 20), (2, 30)];
1106+
let attempted_lsps = Arc::new(Mutex::new(Vec::new()));
1107+
let attempted_lsps_ref = Arc::clone(&attempted_lsps);
1108+
let result = try_lease_candidates(
1109+
candidates,
1110+
|candidate| candidate.0,
1111+
move |candidate| {
1112+
let attempted_lsps = Arc::clone(&attempted_lsps_ref);
1113+
async move {
1114+
attempted_lsps.lock().unwrap().push(candidate.0);
1115+
if candidate.0 == 1 {
1116+
Err(())
1117+
} else {
1118+
Ok(candidate.1)
1119+
}
1120+
}
1121+
},
1122+
)
1123+
.await;
1124+
1125+
assert_eq!(result, Ok(30));
1126+
assert_eq!(*attempted_lsps.lock().unwrap(), vec![1, 2]);
1127+
}
10421128
}
10431129

10441130
pub(crate) mod router;

0 commit comments

Comments
 (0)