@@ -1787,10 +1787,11 @@ where L::Target: Logger {
17871787 // might violate htlc_minimum_msat on the hops which are next along the
17881788 // payment path (upstream to the payee). To avoid that, we recompute
17891789 // path fees knowing the final path contribution after constructing it.
1790- let path_htlc_minimum_msat = cmp:: max(
1791- compute_fees_saturating( $next_hops_path_htlc_minimum_msat, $candidate. fees( ) )
1792- . saturating_add( $next_hops_path_htlc_minimum_msat) ,
1793- $candidate. htlc_minimum_msat( ) ) ;
1790+ let curr_min = cmp:: max(
1791+ $next_hops_path_htlc_minimum_msat, $candidate. htlc_minimum_msat( )
1792+ ) ;
1793+ let path_htlc_minimum_msat = compute_fees_saturating( curr_min, $candidate. fees( ) )
1794+ . saturating_add( curr_min) ;
17941795 let hm_entry = dist. entry( $src_node_id) ;
17951796 let old_entry = hm_entry. or_insert_with( || {
17961797 // If there was previously no known way to access the source node
@@ -7354,6 +7355,78 @@ mod tests {
73547355 assert_eq ! ( route. paths. len( ) , 1 ) ;
73557356 assert_eq ! ( route. get_total_amount( ) , amt_msat) ;
73567357 }
7358+
7359+ #[ test]
7360+ fn candidate_path_min ( ) {
7361+ // Test that if a candidate first_hop<>network_node channel does not have enough contribution
7362+ // amount to cover the next channel's min htlc plus fees, we will not consider that candidate.
7363+ // Previously, we were storing RouteGraphNodes with a path_min that did not include fees, and
7364+ // would add a connecting first_hop node that did not have enough contribution amount, leading
7365+ // to a debug panic upon invalid path construction.
7366+ let secp_ctx = Secp256k1 :: new ( ) ;
7367+ let logger = Arc :: new ( ln_test_utils:: TestLogger :: new ( ) ) ;
7368+ let network_graph = Arc :: new ( NetworkGraph :: new ( Network :: Testnet , Arc :: clone ( & logger) ) ) ;
7369+ let gossip_sync = P2PGossipSync :: new ( network_graph. clone ( ) , None , logger. clone ( ) ) ;
7370+ let scorer = ProbabilisticScorer :: new ( ProbabilisticScoringDecayParameters :: default ( ) , network_graph. clone ( ) , logger. clone ( ) ) ;
7371+ let keys_manager = ln_test_utils:: TestKeysInterface :: new ( & [ 0u8 ; 32 ] , Network :: Testnet ) ;
7372+ let random_seed_bytes = keys_manager. get_secure_random_bytes ( ) ;
7373+ let config = UserConfig :: default ( ) ;
7374+
7375+ // Values are taken from the fuzz input that uncovered this panic.
7376+ let amt_msat = 7_4009_8048 ;
7377+ let ( _, our_id, privkeys, nodes) = get_nodes ( & secp_ctx) ;
7378+ let first_hops = vec ! [ get_channel_details(
7379+ Some ( 200 ) , nodes[ 0 ] , channelmanager:: provided_init_features( & config) , 2_7345_2000
7380+ ) ] ;
7381+
7382+ add_channel ( & gossip_sync, & secp_ctx, & privkeys[ 0 ] , & privkeys[ 6 ] , ChannelFeatures :: from_le_bytes ( id_to_feature_flags ( 6 ) ) , 6 ) ;
7383+ update_channel ( & gossip_sync, & secp_ctx, & privkeys[ 0 ] , UnsignedChannelUpdate {
7384+ chain_hash : genesis_block ( Network :: Testnet ) . header . block_hash ( ) ,
7385+ short_channel_id : 6 ,
7386+ timestamp : 1 ,
7387+ flags : 0 ,
7388+ cltv_expiry_delta : ( 6 << 4 ) | 0 ,
7389+ htlc_minimum_msat : 0 ,
7390+ htlc_maximum_msat : MAX_VALUE_MSAT ,
7391+ fee_base_msat : 0 ,
7392+ fee_proportional_millionths : 0 ,
7393+ excess_data : Vec :: new ( )
7394+ } ) ;
7395+ add_or_update_node ( & gossip_sync, & secp_ctx, & privkeys[ 0 ] , NodeFeatures :: from_le_bytes ( id_to_feature_flags ( 1 ) ) , 0 ) ;
7396+
7397+ let htlc_min = 2_5165_8240 ;
7398+ let blinded_hints = vec ! [
7399+ ( BlindedPayInfo {
7400+ fee_base_msat: 1_6778_3453 ,
7401+ fee_proportional_millionths: 0 ,
7402+ htlc_minimum_msat: htlc_min,
7403+ htlc_maximum_msat: htlc_min * 100 ,
7404+ cltv_expiry_delta: 10 ,
7405+ features: BlindedHopFeatures :: empty( ) ,
7406+ } , BlindedPath {
7407+ introduction_node_id: nodes[ 0 ] ,
7408+ blinding_point: ln_test_utils:: pubkey( 42 ) ,
7409+ blinded_hops: vec![
7410+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 42 as u8 ) , encrypted_payload: Vec :: new( ) } ,
7411+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 42 as u8 ) , encrypted_payload: Vec :: new( ) }
7412+ ] ,
7413+ } )
7414+ ] ;
7415+ let bolt12_features: Bolt12InvoiceFeatures = channelmanager:: provided_invoice_features ( & config) . to_context ( ) ;
7416+ let payment_params = PaymentParameters :: blinded ( blinded_hints. clone ( ) )
7417+ . with_bolt12_features ( bolt12_features. clone ( ) ) . unwrap ( ) ;
7418+ let route_params = RouteParameters :: from_payment_params_and_value (
7419+ payment_params, amt_msat) ;
7420+ let netgraph = network_graph. read_only ( ) ;
7421+
7422+ if let Err ( LightningError { err, .. } ) = get_route (
7423+ & our_id, & route_params, & netgraph, Some ( & first_hops. iter ( ) . collect :: < Vec < _ > > ( ) ) ,
7424+ Arc :: clone ( & logger) , & scorer, & ProbabilisticScoringFeeParameters :: default ( ) ,
7425+ & random_seed_bytes
7426+ ) {
7427+ assert_eq ! ( err, "Failed to find a path to the given destination" ) ;
7428+ } else { panic ! ( ) }
7429+ }
73577430}
73587431
73597432#[ cfg( all( any( test, ldk_bench) , not( feature = "no-std" ) ) ) ]
0 commit comments