@@ -2502,7 +2502,8 @@ where L::Target: Logger {
25022502 // Returns the contribution amount of $candidate if the channel caused an update to `targets`.
25032503 ( $candidate: expr, $next_hops_fee_msat: expr,
25042504 $next_hops_value_contribution: expr, $next_hops_path_htlc_minimum_msat: expr,
2505- $next_hops_path_penalty_msat: expr, $next_hops_cltv_delta: expr, $next_hops_path_length: expr ) => { {
2505+ $next_hops_path_penalty_msat: expr, $next_hops_cltv_delta: expr, $next_hops_path_length: expr,
2506+ $allow_first_hop_route_convergence: expr ) => { {
25062507 // We "return" whether we updated the path at the end, and how much we can route via
25072508 // this channel, via this:
25082509 let mut hop_contribution_amt_msat = None ;
@@ -2559,7 +2560,12 @@ where L::Target: Logger {
25592560
25602561 let value_contribution_msat = cmp:: min( available_value_contribution_msat, $next_hops_value_contribution) ;
25612562 // Verify the liquidity offered by this channel complies to the minimal contribution.
2562- let contributes_sufficient_value = value_contribution_msat >= minimal_value_contribution_msat;
2563+ // For first hops, we allow skipping this if their aggregate capacity meets the
2564+ // threshold (they converge immediately, so no real fragmentation occurs).
2565+ // We still require >= 1 to avoid division by zero in cost calculation.
2566+ let is_first_hop = matches!( $candidate, CandidateRouteHop :: FirstHop ( _) ) ;
2567+ let contributes_sufficient_value = value_contribution_msat >= minimal_value_contribution_msat
2568+ || ( $allow_first_hop_route_convergence && is_first_hop && value_contribution_msat >= 1 ) ;
25632569 // Includes paying fees for the use of the following channels.
25642570 let amount_to_transfer_over_msat: u64 = match value_contribution_msat. checked_add( $next_hops_fee_msat) {
25652571 Some ( result) => result,
@@ -2894,12 +2900,18 @@ where L::Target: Logger {
28942900 add_entry!( candidate, fee_to_target_msat,
28952901 $next_hops_value_contribution,
28962902 next_hops_path_htlc_minimum_msat, next_hops_path_penalty_msat,
2897- $next_hops_cltv_delta, $next_hops_path_length) ;
2903+ $next_hops_cltv_delta, $next_hops_path_length, false ) ;
28982904 }
28992905 }
29002906 }
29012907 if is_first_hop_target {
29022908 if let Some ( ( first_channels, peer_node_counter) ) = first_hop_targets. get( & $node_id) {
2909+ // Check aggregate capacity to this peer for the fragmentation limit.
2910+ let aggregate_capacity_to_peer: u64 = first_channels. iter( )
2911+ . map( |details| details. next_outbound_htlc_limit_msat)
2912+ . sum( ) ;
2913+ let aggregate_meets_threshold = aggregate_capacity_to_peer >= minimal_value_contribution_msat;
2914+
29032915 for details in first_channels {
29042916 debug_assert_eq!( * peer_node_counter, $node_counter) ;
29052917 let candidate = CandidateRouteHop :: FirstHop ( FirstHopCandidate {
@@ -2909,7 +2921,8 @@ where L::Target: Logger {
29092921 add_entry!( & candidate, fee_to_target_msat,
29102922 $next_hops_value_contribution,
29112923 next_hops_path_htlc_minimum_msat, next_hops_path_penalty_msat,
2912- $next_hops_cltv_delta, $next_hops_path_length) ;
2924+ $next_hops_cltv_delta, $next_hops_path_length,
2925+ aggregate_meets_threshold) ;
29132926 }
29142927 }
29152928 }
@@ -2937,7 +2950,8 @@ where L::Target: Logger {
29372950 $next_hops_value_contribution,
29382951 next_hops_path_htlc_minimum_msat,
29392952 next_hops_path_penalty_msat,
2940- $next_hops_cltv_delta, $next_hops_path_length) ;
2953+ $next_hops_cltv_delta, $next_hops_path_length,
2954+ false ) ;
29412955 }
29422956 }
29432957 }
@@ -3066,7 +3080,7 @@ where L::Target: Logger {
30663080 CandidateRouteHop :: Blinded ( BlindedPathCandidate { source_node_counter, source_node_id, hint, hint_idx } )
30673081 } ;
30683082 if let Some ( hop_used_msat) = add_entry ! ( & candidate,
3069- 0 , path_value_msat, 0 , 0_u64 , 0 , 0 )
3083+ 0 , path_value_msat, 0 , 0_u64 , 0 , 0 , false )
30703084 {
30713085 blind_intros_added. insert ( source_node_id, ( hop_used_msat, candidate) ) ;
30723086 } else { continue }
@@ -3084,6 +3098,13 @@ where L::Target: Logger {
30843098 sort_first_hop_channels (
30853099 first_channels, & used_liquidities, recommended_value_msat, our_node_pubkey
30863100 ) ;
3101+
3102+ // Check aggregate capacity to this peer for the fragmentation limit.
3103+ let aggregate_capacity_to_peer: u64 = first_channels. iter ( )
3104+ . map ( |details| details. next_outbound_htlc_limit_msat )
3105+ . sum ( ) ;
3106+ let aggregate_meets_threshold = aggregate_capacity_to_peer >= minimal_value_contribution_msat;
3107+
30873108 for details in first_channels {
30883109 let first_hop_candidate = CandidateRouteHop :: FirstHop ( FirstHopCandidate {
30893110 details, payer_node_id : & our_node_id, payer_node_counter,
@@ -3096,7 +3117,7 @@ where L::Target: Logger {
30963117 let path_min = candidate. htlc_minimum_msat ( ) . saturating_add (
30973118 compute_fees_saturating ( candidate. htlc_minimum_msat ( ) , candidate. fees ( ) ) ) ;
30983119 add_entry ! ( & first_hop_candidate, blinded_path_fee, path_contribution_msat, path_min,
3099- 0_u64 , candidate. cltv_expiry_delta( ) , 0 ) ;
3120+ 0_u64 , candidate. cltv_expiry_delta( ) , 0 , aggregate_meets_threshold ) ;
31003121 }
31013122 }
31023123 }
@@ -6865,6 +6886,57 @@ mod tests {
68656886 }
68666887 }
68676888
6889+ #[ test]
6890+ fn first_hop_aggregate_capacity_overrides_fragmentation_heuristic ( ) {
6891+ // The fragmentation heuristic requires each channel to contribute at least
6892+ // `payment_amount / max_path_count`. However, for first hops to the same peer,
6893+ // this is overly restrictive since all channels converge immediately.
6894+ //
6895+ // Here we test that the aggregate capacity across all first-hop channels to a
6896+ // peer is used for the fragmentation check, not individual channel capacities.
6897+ //
6898+ // Setup:
6899+ // payment_amount = 49_737_000 msat
6900+ // min_contribution = payment_amount / 10 = 4_973_700 msat
6901+ // channel_1 = 2_180_500 msat (below threshold, would be rejected individually)
6902+ // channel_2 = 47_557_520 msat (above threshold, but insufficient alone)
6903+ // aggregate = 49_738_020 msat (sufficient for payment)
6904+
6905+ let secp_ctx = Secp256k1 :: new ( ) ;
6906+ let ( _, our_id, _, nodes) = get_nodes ( & secp_ctx) ;
6907+ let logger = Arc :: new ( ln_test_utils:: TestLogger :: new ( ) ) ;
6908+ let network_graph = NetworkGraph :: new ( Network :: Testnet , Arc :: clone ( & logger) ) ;
6909+ let scorer = ln_test_utils:: TestScorer :: new ( ) ;
6910+ let config = UserConfig :: default ( ) ;
6911+ let payment_params = PaymentParameters :: from_node_id ( nodes[ 0 ] , 42 )
6912+ . with_bolt11_features ( channelmanager:: provided_bolt11_invoice_features ( & config) )
6913+ . unwrap ( ) ;
6914+ let random_seed_bytes = [ 42 ; 32 ] ;
6915+
6916+ let payment_amt = 49_737_000 ;
6917+ let small_channel_capacity = 2_180_500 ;
6918+ let large_channel_capacity = 47_557_520 ;
6919+
6920+ let route_params = RouteParameters :: from_payment_params_and_value (
6921+ payment_params. clone ( ) , payment_amt) ;
6922+ let route = get_route ( & our_id, & route_params, & network_graph. read_only ( ) , Some ( & [
6923+ & get_channel_details ( Some ( 1 ) , nodes[ 0 ] , channelmanager:: provided_init_features ( & config) , small_channel_capacity) ,
6924+ & get_channel_details ( Some ( 2 ) , nodes[ 0 ] , channelmanager:: provided_init_features ( & config) , large_channel_capacity) ,
6925+ ] ) , Arc :: clone ( & logger) , & scorer, & Default :: default ( ) , & random_seed_bytes) . unwrap ( ) ;
6926+
6927+ assert_eq ! ( route. paths. len( ) , 2 , "Expected 2 paths" ) ;
6928+
6929+ let total_sent: u64 = route. paths . iter ( )
6930+ . map ( |path| path. hops . last ( ) . unwrap ( ) . fee_msat )
6931+ . sum ( ) ;
6932+ assert_eq ! ( total_sent, payment_amt) ;
6933+
6934+ let scids: std:: collections:: HashSet < u64 > = route. paths . iter ( )
6935+ . map ( |path| path. hops [ 0 ] . short_channel_id )
6936+ . collect ( ) ;
6937+ assert ! ( scids. contains( & 1 ) && scids. contains( & 2 ) , "Both channels should be used" ) ;
6938+ }
6939+
68686940 #[ test]
68696941 fn prefers_shorter_route_with_higher_fees ( ) {
68706942 let ( secp_ctx, network_graph, _, _, logger) = build_graph ( ) ;
@@ -7966,7 +8038,9 @@ mod tests {
79668038 if let Err ( LightningError { err, .. } ) = get_route ( & nodes[ 0 ] , & route_params, & netgraph,
79678039 Some ( & first_hops. iter ( ) . collect :: < Vec < _ > > ( ) ) , Arc :: clone ( & logger) , & scorer,
79688040 & Default :: default ( ) , & random_seed_bytes) {
7969- assert_eq ! ( err, "Failed to find a path to the given destination" ) ;
8041+ assert ! ( err == "Failed to find a path to the given destination" ||
8042+ err == "Failed to find a sufficient route to the given destination" ,
8043+ "Unexpected error: {}" , err) ;
79708044 } else { panic ! ( "Expected error" ) }
79718045
79728046 // Sending an exact amount accounting for the blinded path fee works.
0 commit comments