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 } ;
99use std:: future:: Future ;
10+ use std:: hash:: Hash ;
1011use std:: ops:: Deref ;
1112use std:: sync:: { Arc , Mutex , RwLock } ;
1213use 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 ) ]
72100enum 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
12531352pub ( crate ) mod state;
0 commit comments