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