Skip to content

Commit 5712376

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 8d912f5 commit 5712376

1 file changed

Lines changed: 135 additions & 36 deletions

File tree

  • src/liquidity/client/lsps2

src/liquidity/client/lsps2/mod.rs

Lines changed: 135 additions & 36 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::HashMap;
8+
use std::collections::{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 @@ fn should_retry_lease_negotiation(error: Error, attempt: usize) -> bool {
6869
error == Error::LiquidityRequestFailed && attempt < LEASE_NEGOTIATION_MAX_ATTEMPTS
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(Debug, PartialEq, Eq)]
72100
enum JitInvoiceRequest {
73101
Fixed { amount_msat: u64, max_total_fee_msat: Option<u64> },
@@ -453,7 +481,7 @@ where
453481
connection_manager: &Arc<ConnectionManager<L>>,
454482
) -> Result<(PaymentLease, u64, LspConfig), Error> {
455483
let all_offers = self.gather_lsps2_offers(connection_manager).await?;
456-
let (cheapest_lsp, min_total_fee_msat, min_opening_params) = all_offers
484+
let mut candidates = all_offers
457485
.into_iter()
458486
.flat_map(|(lsp, resp)| {
459487
resp.opening_fee_params_menu
@@ -477,11 +505,12 @@ where
477505
.map(|fee| (lsp, fee, params))
478506
}
479507
})
480-
.min_by_key(|(_, fee, _)| *fee)
481-
.ok_or_else(|| {
482-
log_error!(self.logger, "Failed to handle response from liquidity service",);
483-
Error::LiquidityRequestFailed
484-
})?;
508+
.collect::<Vec<_>>();
509+
candidates.sort_unstable_by_key(|(_, fee, _)| *fee);
510+
let min_total_fee_msat = candidates.first().map(|(_, fee, _)| *fee).ok_or_else(|| {
511+
log_error!(self.logger, "Failed to handle response from liquidity service",);
512+
Error::LiquidityRequestFailed
513+
})?;
485514

486515
if let Some(max_total_lsp_fee_limit_msat) = max_total_lsp_fee_limit_msat {
487516
if min_total_fee_msat > max_total_lsp_fee_limit_msat {
@@ -491,23 +520,44 @@ where
491520
);
492521
return Err(Error::LiquidityFeeTooHigh);
493522
}
523+
candidates.retain(|(_, fee, _)| *fee <= max_total_lsp_fee_limit_msat);
494524
}
495525

496-
log_debug!(
497-
self.logger,
498-
"Choosing cheapest liquidity offer from LSP {}, will pay {}msat in total LSP fees",
499-
cheapest_lsp.node_id,
500-
min_total_fee_msat
501-
);
502-
503-
let negotiated_lease = self
504-
.lsps2_send_buy_request(
505-
Some(amount_msat),
506-
min_opening_params,
507-
Some(&cheapest_lsp.node_id),
508-
)
509-
.await?;
510-
Ok((negotiated_lease, min_total_fee_msat, cheapest_lsp))
526+
try_lease_candidates(
527+
candidates,
528+
|(lsp, _, _)| lsp.node_id,
529+
|(lsp, total_fee_msat, opening_params)| {
530+
let client = Arc::clone(self);
531+
async move {
532+
log_debug!(
533+
client.logger,
534+
"Choosing liquidity offer from LSP {}, will pay {}msat in total LSP fees",
535+
lsp.node_id,
536+
total_fee_msat
537+
);
538+
match client
539+
.lsps2_send_buy_request(
540+
Some(amount_msat),
541+
opening_params,
542+
Some(&lsp.node_id),
543+
)
544+
.await
545+
{
546+
Ok(lease) => Ok((lease, total_fee_msat, lsp)),
547+
Err(error) => {
548+
log_warn!(
549+
client.logger,
550+
"Failed negotiating LSPS2 payment lease with LSP {}, trying the next candidate: {}",
551+
lsp.node_id,
552+
error
553+
);
554+
Err(error)
555+
},
556+
}
557+
}
558+
},
559+
)
560+
.await
511561
}
512562

513563
async fn acquire_variable_lease(
@@ -582,7 +632,7 @@ where
582632
connection_manager: &Arc<ConnectionManager<L>>,
583633
) -> Result<(PaymentLease, u64, LspConfig), Error> {
584634
let all_offers = self.gather_lsps2_offers(connection_manager).await?;
585-
let (cheapest_lsp, min_prop_fee_ppm_msat, min_opening_params) = all_offers
635+
let mut candidates = all_offers
586636
.into_iter()
587637
.flat_map(|(lsp, resp)| {
588638
resp.opening_fee_params_menu.into_iter().map(move |params| (lsp.clone(), params))
@@ -591,8 +641,10 @@ where
591641
let ppm = params.proportional as u64;
592642
(lsp, ppm, params)
593643
})
594-
.min_by_key(|(_, ppm, _)| *ppm)
595-
.ok_or_else(|| {
644+
.collect::<Vec<_>>();
645+
candidates.sort_unstable_by_key(|(_, ppm, _)| *ppm);
646+
let min_prop_fee_ppm_msat =
647+
candidates.first().map(|(_, ppm, _)| *ppm).ok_or_else(|| {
596648
log_error!(self.logger, "Failed to handle response from liquidity service",);
597649
Error::LiquidityRequestFailed
598650
})?;
@@ -608,19 +660,40 @@ where
608660
);
609661
return Err(Error::LiquidityFeeTooHigh);
610662
}
663+
candidates.retain(|(_, ppm, _)| *ppm <= max_proportional_lsp_fee_limit_ppm_msat);
611664
}
612665

613-
log_debug!(
614-
self.logger,
615-
"Choosing cheapest liquidity offer from LSP {}, will pay {}ppm msat in proportional LSP fees",
616-
cheapest_lsp.node_id,
617-
min_prop_fee_ppm_msat
618-
);
619-
620-
let negotiated_lease = self
621-
.lsps2_send_buy_request(None, min_opening_params, Some(&cheapest_lsp.node_id))
622-
.await?;
623-
Ok((negotiated_lease, min_prop_fee_ppm_msat, cheapest_lsp))
666+
try_lease_candidates(
667+
candidates,
668+
|(lsp, _, _)| lsp.node_id,
669+
|(lsp, proportional_fee_ppm_msat, opening_params)| {
670+
let client = Arc::clone(self);
671+
async move {
672+
log_debug!(
673+
client.logger,
674+
"Choosing liquidity offer from LSP {}, will pay {}ppm msat in proportional LSP fees",
675+
lsp.node_id,
676+
proportional_fee_ppm_msat
677+
);
678+
match client
679+
.lsps2_send_buy_request(None, opening_params, Some(&lsp.node_id))
680+
.await
681+
{
682+
Ok(lease) => Ok((lease, proportional_fee_ppm_msat, lsp)),
683+
Err(error) => {
684+
log_warn!(
685+
client.logger,
686+
"Failed negotiating LSPS2 payment lease with LSP {}, trying the next candidate: {}",
687+
lsp.node_id,
688+
error
689+
);
690+
Err(error)
691+
},
692+
}
693+
}
694+
},
695+
)
696+
.await
624697
}
625698

626699
fn schedule_fixed_lease_refill(
@@ -1248,6 +1321,32 @@ mod tests {
12481321
assert!(!should_retry_lease_negotiation(Error::LiquidityFeeTooHigh, 1));
12491322
assert!(!should_retry_lease_negotiation(Error::LiquiditySourceUnavailable, 1));
12501323
}
1324+
1325+
#[tokio::test]
1326+
async fn lease_negotiation_fails_over_between_lsps() {
1327+
let candidates = vec![(1, 10), (1, 20), (2, 30)];
1328+
let attempted_lsps = Arc::new(Mutex::new(Vec::new()));
1329+
let attempted_lsps_ref = Arc::clone(&attempted_lsps);
1330+
let result = try_lease_candidates(
1331+
candidates,
1332+
|candidate| candidate.0,
1333+
move |candidate| {
1334+
let attempted_lsps = Arc::clone(&attempted_lsps_ref);
1335+
async move {
1336+
attempted_lsps.lock().unwrap().push(candidate.0);
1337+
if candidate.0 == 1 {
1338+
Err(())
1339+
} else {
1340+
Ok(candidate.1)
1341+
}
1342+
}
1343+
},
1344+
)
1345+
.await;
1346+
1347+
assert_eq!(result, Ok(30));
1348+
assert_eq!(*attempted_lsps.lock().unwrap(), vec![1, 2]);
1349+
}
12511350
}
12521351

12531352
pub(crate) mod state;

0 commit comments

Comments
 (0)