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 ;
@@ -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 ) ]
76104pub ( 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
10511137pub ( crate ) mod router;
0 commit comments