Skip to content

Commit 825cb3a

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 ffef4e9 commit 825cb3a

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;
@@ -72,6 +73,33 @@ pub(crate) struct JitInvoiceResponse {
7273
pub(crate) allow_mpp: bool,
7374
}
7475

76+
async fn try_lease_candidates<T, K, R, E, KF, AF, Fut>(
77+
candidates: Vec<T>, candidate_key: KF, mut attempt: AF,
78+
) -> Result<R, E>
79+
where
80+
K: Copy + Eq + Hash,
81+
KF: Fn(&T) -> K,
82+
AF: FnMut(T) -> Fut,
83+
Fut: Future<Output = Result<R, E>>,
84+
{
85+
let mut failed_candidates = HashSet::new();
86+
let mut last_error = None;
87+
for candidate in candidates {
88+
let key = candidate_key(&candidate);
89+
if failed_candidates.contains(&key) {
90+
continue;
91+
}
92+
match attempt(candidate).await {
93+
Ok(result) => return Ok(result),
94+
Err(error) => {
95+
failed_candidates.insert(key);
96+
last_error = Some(error);
97+
},
98+
}
99+
}
100+
Err(last_error.expect("lease candidates are non-empty"))
101+
}
102+
75103
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76104
pub(crate) enum JitInvoiceRequest {
77105
Fixed { amount_msat: u64, absolute_expiry: Option<u64> },
@@ -324,13 +352,7 @@ where
324352
) -> Result<(PaymentLease, u64, LspConfig), Error> {
325353
let mut attempt = 1;
326354
loop {
327-
let result = self
328-
.negotiate_fixed_lease_once(
329-
amount_msat,
330-
max_total_lsp_fee_limit_msat,
331-
connection_manager,
332-
)
333-
.await;
355+
let result = self.negotiate_fixed_lease_once(amount_msat, connection_manager).await;
334356
match result {
335357
Err(error) if should_retry_lease_negotiation(error, attempt) => {
336358
log_warn!(
@@ -347,11 +369,10 @@ where
347369
}
348370

349371
async fn negotiate_fixed_lease_once(
350-
self: &Arc<Self>, amount_msat: u64, max_total_lsp_fee_limit_msat: Option<u64>,
351-
connection_manager: &Arc<ConnectionManager<L>>,
372+
self: &Arc<Self>, amount_msat: u64, connection_manager: &Arc<ConnectionManager<L>>,
352373
) -> Result<(PaymentLease, u64, LspConfig), Error> {
353374
let all_offers = self.gather_lsps2_offers(connection_manager).await?;
354-
let (cheapest_lsp, min_total_fee_msat, min_opening_params) = all_offers
375+
let mut candidates = all_offers
355376
.into_iter()
356377
.flat_map(|(lsp, resp)| {
357378
resp.opening_fee_params_menu
@@ -375,11 +396,12 @@ where
375396
.map(|fee| (lsp, fee, params))
376397
}
377398
})
378-
.min_by_key(|(_, fee, _)| *fee)
379-
.ok_or_else(|| {
380-
log_error!(self.logger, "Failed to handle response from liquidity service",);
381-
Error::LiquidityRequestFailed
382-
})?;
399+
.collect::<Vec<_>>();
400+
candidates.sort_unstable_by_key(|(_, fee, _)| *fee);
401+
let min_total_fee_msat = candidates.first().map(|(_, fee, _)| *fee).ok_or_else(|| {
402+
log_error!(self.logger, "Failed to handle response from liquidity service",);
403+
Error::LiquidityRequestFailed
404+
})?;
383405

384406
if let Some(max_total_lsp_fee_limit_msat) = self.config.lsps2_max_total_lsp_fee_limit_msat {
385407
if min_total_fee_msat > max_total_lsp_fee_limit_msat {
@@ -389,23 +411,44 @@ where
389411
);
390412
return Err(Error::LiquidityFeeTooHigh);
391413
}
414+
candidates.retain(|(_, fee, _)| *fee <= max_total_lsp_fee_limit_msat);
392415
}
393416

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

411454
async fn acquire_variable_lease(
@@ -438,12 +481,7 @@ where
438481
) -> Result<(PaymentLease, u64, LspConfig), Error> {
439482
let mut attempt = 1;
440483
loop {
441-
let result = self
442-
.negotiate_variable_lease_once(
443-
max_proportional_lsp_fee_limit_ppm_msat,
444-
connection_manager,
445-
)
446-
.await;
484+
let result = self.negotiate_variable_lease_once(connection_manager).await;
447485
match result {
448486
Err(error) if should_retry_lease_negotiation(error, attempt) => {
449487
log_warn!(
@@ -460,12 +498,11 @@ where
460498
}
461499

462500
async fn negotiate_variable_lease_once(
463-
self: &Arc<Self>, max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
464-
connection_manager: &Arc<ConnectionManager<L>>,
501+
self: &Arc<Self>, connection_manager: &Arc<ConnectionManager<L>>,
465502
) -> Result<(PaymentLease, u64, LspConfig), Error> {
466503
let all_offers = self.gather_lsps2_offers(connection_manager).await?;
467504
let mut rejected_for_fee = false;
468-
let (cheapest_lsp, min_prop_fee_ppm_msat, min_opening_params) = all_offers
505+
let mut candidates = all_offers
469506
.into_iter()
470507
.flat_map(|(lsp, resp)| {
471508
resp.opening_fee_params_menu.into_iter().map(move |params| (lsp.clone(), params))
@@ -482,29 +519,52 @@ where
482519
rejected_for_fee |= !allowed;
483520
allowed
484521
})
485-
.min_by_key(|(_, ppm, _)| *ppm)
486-
.ok_or_else(|| {
487-
if rejected_for_fee {
488-
log_error!(
489-
self.logger,
490-
"Failed to request inbound JIT channel as all LSP offers exceed our configured fee limit"
491-
);
492-
return Error::LiquidityFeeTooHigh;
493-
}
522+
.collect::<Vec<_>>();
523+
candidates.sort_unstable_by_key(|(_, ppm, _)| *ppm);
524+
if candidates.is_empty() {
525+
return Err(if rejected_for_fee {
526+
log_error!(
527+
self.logger,
528+
"Failed to request inbound JIT channel as all LSP offers exceed our configured fee limit"
529+
);
530+
Error::LiquidityFeeTooHigh
531+
} else {
494532
log_error!(self.logger, "Failed to handle response from liquidity service",);
495533
Error::LiquidityRequestFailed
496-
})?;
497-
log_debug!(
498-
self.logger,
499-
"Choosing cheapest liquidity offer from LSP {}, will pay {}ppm msat in proportional LSP fees",
500-
cheapest_lsp.node_id,
501-
min_prop_fee_ppm_msat
502-
);
503-
504-
let negotiated_lease = self
505-
.lsps2_send_buy_request(None, min_opening_params, Some(&cheapest_lsp.node_id))
506-
.await?;
507-
Ok((negotiated_lease, min_prop_fee_ppm_msat, cheapest_lsp))
534+
});
535+
}
536+
537+
try_lease_candidates(
538+
candidates,
539+
|(lsp, _, _)| lsp.node_id,
540+
|(lsp, proportional_fee_ppm_msat, opening_params)| {
541+
let client = Arc::clone(self);
542+
async move {
543+
log_debug!(
544+
client.logger,
545+
"Choosing liquidity offer from LSP {}, will pay {}ppm msat in proportional LSP fees",
546+
lsp.node_id,
547+
proportional_fee_ppm_msat
548+
);
549+
match client
550+
.lsps2_send_buy_request(None, opening_params, Some(&lsp.node_id))
551+
.await
552+
{
553+
Ok(lease) => Ok((lease, proportional_fee_ppm_msat, lsp)),
554+
Err(error) => {
555+
log_warn!(
556+
client.logger,
557+
"Failed negotiating LSPS2 payment lease with LSP {}, trying the next candidate: {}",
558+
lsp.node_id,
559+
error
560+
);
561+
Err(error)
562+
},
563+
}
564+
}
565+
},
566+
)
567+
.await
508568
}
509569

510570
fn schedule_fixed_lease_refill(
@@ -1046,6 +1106,32 @@ mod tests {
10461106
assert!(!should_retry_lease_negotiation(Error::LiquidityFeeTooHigh, 1));
10471107
assert!(!should_retry_lease_negotiation(Error::LiquiditySourceUnavailable, 1));
10481108
}
1109+
1110+
#[tokio::test]
1111+
async fn lease_negotiation_fails_over_between_lsps() {
1112+
let candidates = vec![(1, 10), (1, 20), (2, 30)];
1113+
let attempted_lsps = Arc::new(Mutex::new(Vec::new()));
1114+
let attempted_lsps_ref = Arc::clone(&attempted_lsps);
1115+
let result = try_lease_candidates(
1116+
candidates,
1117+
|candidate| candidate.0,
1118+
move |candidate| {
1119+
let attempted_lsps = Arc::clone(&attempted_lsps_ref);
1120+
async move {
1121+
attempted_lsps.lock().unwrap().push(candidate.0);
1122+
if candidate.0 == 1 {
1123+
Err(())
1124+
} else {
1125+
Ok(candidate.1)
1126+
}
1127+
}
1128+
},
1129+
)
1130+
.await;
1131+
1132+
assert_eq!(result, Ok(30));
1133+
assert_eq!(*attempted_lsps.lock().unwrap(), vec![1, 2]);
1134+
}
10491135
}
10501136

10511137
pub(crate) mod router;

0 commit comments

Comments
 (0)