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 ;
@@ -68,6 +69,33 @@ pub(crate) struct JitInvoiceResponse {
6869 pub ( crate ) allow_mpp : bool ,
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( Clone , Copy , Debug , PartialEq , Eq ) ]
72100pub ( crate ) enum JitInvoiceRequest {
73101 Fixed { amount_msat : u64 , absolute_expiry : Option < u64 > } ,
@@ -320,13 +348,7 @@ where
320348 ) -> Result < ( PaymentLease , u64 , LspConfig ) , Error > {
321349 let mut attempt = 1 ;
322350 loop {
323- let result = self
324- . negotiate_fixed_lease_once (
325- amount_msat,
326- max_total_lsp_fee_limit_msat,
327- connection_manager,
328- )
329- . await ;
351+ let result = self . negotiate_fixed_lease_once ( amount_msat, connection_manager) . await ;
330352 match result {
331353 Err ( error) if should_retry_lease_negotiation ( error, attempt) => {
332354 log_warn ! (
@@ -343,11 +365,10 @@ where
343365 }
344366
345367 async fn negotiate_fixed_lease_once (
346- self : & Arc < Self > , amount_msat : u64 , max_total_lsp_fee_limit_msat : Option < u64 > ,
347- connection_manager : & Arc < ConnectionManager < L > > ,
368+ self : & Arc < Self > , amount_msat : u64 , connection_manager : & Arc < ConnectionManager < L > > ,
348369 ) -> Result < ( PaymentLease , u64 , LspConfig ) , Error > {
349370 let all_offers = self . gather_lsps2_offers ( connection_manager) . await ?;
350- let ( cheapest_lsp , min_total_fee_msat , min_opening_params ) = all_offers
371+ let mut candidates = all_offers
351372 . into_iter ( )
352373 . flat_map ( |( lsp, resp) | {
353374 resp. opening_fee_params_menu
@@ -371,11 +392,12 @@ where
371392 . map ( |fee| ( lsp, fee, params) )
372393 }
373394 } )
374- . min_by_key ( |( _, fee, _) | * fee)
375- . ok_or_else ( || {
376- log_error ! ( self . logger, "Failed to handle response from liquidity service" , ) ;
377- Error :: LiquidityRequestFailed
378- } ) ?;
395+ . collect :: < Vec < _ > > ( ) ;
396+ candidates. sort_unstable_by_key ( |( _, fee, _) | * fee) ;
397+ let min_total_fee_msat = candidates. first ( ) . map ( |( _, fee, _) | * fee) . ok_or_else ( || {
398+ log_error ! ( self . logger, "Failed to handle response from liquidity service" , ) ;
399+ Error :: LiquidityRequestFailed
400+ } ) ?;
379401
380402 if let Some ( max_total_lsp_fee_limit_msat) = self . config . lsps2_max_total_lsp_fee_limit_msat {
381403 if min_total_fee_msat > max_total_lsp_fee_limit_msat {
@@ -385,23 +407,44 @@ where
385407 ) ;
386408 return Err ( Error :: LiquidityFeeTooHigh ) ;
387409 }
410+ candidates. retain ( |( _, fee, _) | * fee <= max_total_lsp_fee_limit_msat) ;
388411 }
389412
390- log_debug ! (
391- self . logger,
392- "Choosing cheapest liquidity offer from LSP {}, will pay {}msat in total LSP fees" ,
393- cheapest_lsp. node_id,
394- min_total_fee_msat
395- ) ;
396-
397- let negotiated_lease = self
398- . lsps2_send_buy_request (
399- Some ( amount_msat) ,
400- min_opening_params,
401- Some ( & cheapest_lsp. node_id ) ,
402- )
403- . await ?;
404- Ok ( ( negotiated_lease, min_total_fee_msat, cheapest_lsp) )
413+ try_lease_candidates (
414+ candidates,
415+ |( lsp, _, _) | lsp. node_id ,
416+ |( lsp, total_fee_msat, opening_params) | {
417+ let client = Arc :: clone ( self ) ;
418+ async move {
419+ log_debug ! (
420+ client. logger,
421+ "Choosing liquidity offer from LSP {}, will pay {}msat in total LSP fees" ,
422+ lsp. node_id,
423+ total_fee_msat
424+ ) ;
425+ match client
426+ . lsps2_send_buy_request (
427+ Some ( amount_msat) ,
428+ opening_params,
429+ Some ( & lsp. node_id ) ,
430+ )
431+ . await
432+ {
433+ Ok ( lease) => Ok ( ( lease, total_fee_msat, lsp) ) ,
434+ Err ( error) => {
435+ log_warn ! (
436+ client. logger,
437+ "Failed negotiating LSPS2 payment lease with LSP {}, trying the next candidate: {}" ,
438+ lsp. node_id,
439+ error
440+ ) ;
441+ Err ( error)
442+ } ,
443+ }
444+ }
445+ } ,
446+ )
447+ . await
405448 }
406449
407450 async fn acquire_variable_lease (
@@ -434,12 +477,7 @@ where
434477 ) -> Result < ( PaymentLease , u64 , LspConfig ) , Error > {
435478 let mut attempt = 1 ;
436479 loop {
437- let result = self
438- . negotiate_variable_lease_once (
439- max_proportional_lsp_fee_limit_ppm_msat,
440- connection_manager,
441- )
442- . await ;
480+ let result = self . negotiate_variable_lease_once ( connection_manager) . await ;
443481 match result {
444482 Err ( error) if should_retry_lease_negotiation ( error, attempt) => {
445483 log_warn ! (
@@ -456,12 +494,11 @@ where
456494 }
457495
458496 async fn negotiate_variable_lease_once (
459- self : & Arc < Self > , max_proportional_lsp_fee_limit_ppm_msat : Option < u64 > ,
460- connection_manager : & Arc < ConnectionManager < L > > ,
497+ self : & Arc < Self > , connection_manager : & Arc < ConnectionManager < L > > ,
461498 ) -> Result < ( PaymentLease , u64 , LspConfig ) , Error > {
462499 let all_offers = self . gather_lsps2_offers ( connection_manager) . await ?;
463500 let mut rejected_for_fee = false ;
464- let ( cheapest_lsp , min_prop_fee_ppm_msat , min_opening_params ) = all_offers
501+ let mut candidates = all_offers
465502 . into_iter ( )
466503 . flat_map ( |( lsp, resp) | {
467504 resp. opening_fee_params_menu . into_iter ( ) . map ( move |params| ( lsp. clone ( ) , params) )
@@ -478,29 +515,52 @@ where
478515 rejected_for_fee |= !allowed;
479516 allowed
480517 } )
481- . min_by_key ( |( _, ppm, _) | * ppm)
482- . ok_or_else ( || {
483- if rejected_for_fee {
484- log_error ! (
485- self . logger,
486- "Failed to request inbound JIT channel as all LSP offers exceed our configured fee limit"
487- ) ;
488- return Error :: LiquidityFeeTooHigh ;
489- }
518+ . collect :: < Vec < _ > > ( ) ;
519+ candidates. sort_unstable_by_key ( |( _, ppm, _) | * ppm) ;
520+ if candidates. is_empty ( ) {
521+ return Err ( if rejected_for_fee {
522+ log_error ! (
523+ self . logger,
524+ "Failed to request inbound JIT channel as all LSP offers exceed our configured fee limit"
525+ ) ;
526+ Error :: LiquidityFeeTooHigh
527+ } else {
490528 log_error ! ( self . logger, "Failed to handle response from liquidity service" , ) ;
491529 Error :: LiquidityRequestFailed
492- } ) ?;
493- log_debug ! (
494- self . logger,
495- "Choosing cheapest liquidity offer from LSP {}, will pay {}ppm msat in proportional LSP fees" ,
496- cheapest_lsp. node_id,
497- min_prop_fee_ppm_msat
498- ) ;
499-
500- let negotiated_lease = self
501- . lsps2_send_buy_request ( None , min_opening_params, Some ( & cheapest_lsp. node_id ) )
502- . await ?;
503- Ok ( ( negotiated_lease, min_prop_fee_ppm_msat, cheapest_lsp) )
530+ } ) ;
531+ }
532+
533+ try_lease_candidates (
534+ candidates,
535+ |( lsp, _, _) | lsp. node_id ,
536+ |( lsp, proportional_fee_ppm_msat, opening_params) | {
537+ let client = Arc :: clone ( self ) ;
538+ async move {
539+ log_debug ! (
540+ client. logger,
541+ "Choosing liquidity offer from LSP {}, will pay {}ppm msat in proportional LSP fees" ,
542+ lsp. node_id,
543+ proportional_fee_ppm_msat
544+ ) ;
545+ match client
546+ . lsps2_send_buy_request ( None , opening_params, Some ( & lsp. node_id ) )
547+ . await
548+ {
549+ Ok ( lease) => Ok ( ( lease, proportional_fee_ppm_msat, lsp) ) ,
550+ Err ( error) => {
551+ log_warn ! (
552+ client. logger,
553+ "Failed negotiating LSPS2 payment lease with LSP {}, trying the next candidate: {}" ,
554+ lsp. node_id,
555+ error
556+ ) ;
557+ Err ( error)
558+ } ,
559+ }
560+ }
561+ } ,
562+ )
563+ . await
504564 }
505565
506566 fn schedule_fixed_lease_refill (
@@ -1039,6 +1099,32 @@ mod tests {
10391099 assert ! ( !should_retry_lease_negotiation( Error :: LiquidityFeeTooHigh , 1 ) ) ;
10401100 assert ! ( !should_retry_lease_negotiation( Error :: LiquiditySourceUnavailable , 1 ) ) ;
10411101 }
1102+
1103+ #[ tokio:: test]
1104+ async fn lease_negotiation_fails_over_between_lsps ( ) {
1105+ let candidates = vec ! [ ( 1 , 10 ) , ( 1 , 20 ) , ( 2 , 30 ) ] ;
1106+ let attempted_lsps = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
1107+ let attempted_lsps_ref = Arc :: clone ( & attempted_lsps) ;
1108+ let result = try_lease_candidates (
1109+ candidates,
1110+ |candidate| candidate. 0 ,
1111+ move |candidate| {
1112+ let attempted_lsps = Arc :: clone ( & attempted_lsps_ref) ;
1113+ async move {
1114+ attempted_lsps. lock ( ) . unwrap ( ) . push ( candidate. 0 ) ;
1115+ if candidate. 0 == 1 {
1116+ Err ( ( ) )
1117+ } else {
1118+ Ok ( candidate. 1 )
1119+ }
1120+ }
1121+ } ,
1122+ )
1123+ . await ;
1124+
1125+ assert_eq ! ( result, Ok ( 30 ) ) ;
1126+ assert_eq ! ( * attempted_lsps. lock( ) . unwrap( ) , vec![ 1 , 2 ] ) ;
1127+ }
10421128}
10431129
10441130pub ( crate ) mod router;
0 commit comments