@@ -1819,10 +1819,11 @@ where L::Target: Logger {
18191819 // might violate htlc_minimum_msat on the hops which are next along the
18201820 // payment path (upstream to the payee). To avoid that, we recompute
18211821 // path fees knowing the final path contribution after constructing it.
1822- let path_htlc_minimum_msat = cmp:: max(
1823- compute_fees_saturating( $next_hops_path_htlc_minimum_msat, $candidate. fees( ) )
1824- . saturating_add( $next_hops_path_htlc_minimum_msat) ,
1825- $candidate. htlc_minimum_msat( ) ) ;
1822+ let curr_min = cmp:: max(
1823+ $next_hops_path_htlc_minimum_msat, $candidate. htlc_minimum_msat( )
1824+ ) ;
1825+ let path_htlc_minimum_msat = compute_fees_saturating( curr_min, $candidate. fees( ) )
1826+ . saturating_add( curr_min) ;
18261827 let hm_entry = dist. entry( $src_node_id) ;
18271828 let old_entry = hm_entry. or_insert_with( || {
18281829 // If there was previously no known way to access the source node
@@ -7448,6 +7449,78 @@ mod tests {
74487449 assert_eq ! ( route. paths. len( ) , 1 ) ;
74497450 assert_eq ! ( route. get_total_amount( ) , amt_msat) ;
74507451 }
7452+
7453+ #[ test]
7454+ fn candidate_path_min ( ) {
7455+ // Test that if a candidate first_hop<>network_node channel does not have enough contribution
7456+ // amount to cover the next channel's min htlc plus fees, we will not consider that candidate.
7457+ // Previously, we were storing RouteGraphNodes with a path_min that did not include fees, and
7458+ // would add a connecting first_hop node that did not have enough contribution amount, leading
7459+ // to a debug panic upon invalid path construction.
7460+ let secp_ctx = Secp256k1 :: new ( ) ;
7461+ let logger = Arc :: new ( ln_test_utils:: TestLogger :: new ( ) ) ;
7462+ let network_graph = Arc :: new ( NetworkGraph :: new ( Network :: Testnet , Arc :: clone ( & logger) ) ) ;
7463+ let gossip_sync = P2PGossipSync :: new ( network_graph. clone ( ) , None , logger. clone ( ) ) ;
7464+ let scorer = ProbabilisticScorer :: new ( ProbabilisticScoringDecayParameters :: default ( ) , network_graph. clone ( ) , logger. clone ( ) ) ;
7465+ let keys_manager = ln_test_utils:: TestKeysInterface :: new ( & [ 0u8 ; 32 ] , Network :: Testnet ) ;
7466+ let random_seed_bytes = keys_manager. get_secure_random_bytes ( ) ;
7467+ let config = UserConfig :: default ( ) ;
7468+
7469+ // Values are taken from the fuzz input that uncovered this panic.
7470+ let amt_msat = 7_4009_8048 ;
7471+ let ( _, our_id, privkeys, nodes) = get_nodes ( & secp_ctx) ;
7472+ let first_hops = vec ! [ get_channel_details(
7473+ Some ( 200 ) , nodes[ 0 ] , channelmanager:: provided_init_features( & config) , 2_7345_2000
7474+ ) ] ;
7475+
7476+ add_channel ( & gossip_sync, & secp_ctx, & privkeys[ 0 ] , & privkeys[ 6 ] , ChannelFeatures :: from_le_bytes ( id_to_feature_flags ( 6 ) ) , 6 ) ;
7477+ update_channel ( & gossip_sync, & secp_ctx, & privkeys[ 0 ] , UnsignedChannelUpdate {
7478+ chain_hash : genesis_block ( Network :: Testnet ) . header . block_hash ( ) ,
7479+ short_channel_id : 6 ,
7480+ timestamp : 1 ,
7481+ flags : 0 ,
7482+ cltv_expiry_delta : ( 6 << 4 ) | 0 ,
7483+ htlc_minimum_msat : 0 ,
7484+ htlc_maximum_msat : MAX_VALUE_MSAT ,
7485+ fee_base_msat : 0 ,
7486+ fee_proportional_millionths : 0 ,
7487+ excess_data : Vec :: new ( )
7488+ } ) ;
7489+ add_or_update_node ( & gossip_sync, & secp_ctx, & privkeys[ 0 ] , NodeFeatures :: from_le_bytes ( id_to_feature_flags ( 1 ) ) , 0 ) ;
7490+
7491+ let htlc_min = 2_5165_8240 ;
7492+ let blinded_hints = vec ! [
7493+ ( BlindedPayInfo {
7494+ fee_base_msat: 1_6778_3453 ,
7495+ fee_proportional_millionths: 0 ,
7496+ htlc_minimum_msat: htlc_min,
7497+ htlc_maximum_msat: htlc_min * 100 ,
7498+ cltv_expiry_delta: 10 ,
7499+ features: BlindedHopFeatures :: empty( ) ,
7500+ } , BlindedPath {
7501+ introduction_node_id: nodes[ 0 ] ,
7502+ blinding_point: ln_test_utils:: pubkey( 42 ) ,
7503+ blinded_hops: vec![
7504+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 42 as u8 ) , encrypted_payload: Vec :: new( ) } ,
7505+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 42 as u8 ) , encrypted_payload: Vec :: new( ) }
7506+ ] ,
7507+ } )
7508+ ] ;
7509+ let bolt12_features: Bolt12InvoiceFeatures = channelmanager:: provided_invoice_features ( & config) . to_context ( ) ;
7510+ let payment_params = PaymentParameters :: blinded ( blinded_hints. clone ( ) )
7511+ . with_bolt12_features ( bolt12_features. clone ( ) ) . unwrap ( ) ;
7512+ let route_params = RouteParameters :: from_payment_params_and_value (
7513+ payment_params, amt_msat) ;
7514+ let netgraph = network_graph. read_only ( ) ;
7515+
7516+ if let Err ( LightningError { err, .. } ) = get_route (
7517+ & our_id, & route_params, & netgraph, Some ( & first_hops. iter ( ) . collect :: < Vec < _ > > ( ) ) ,
7518+ Arc :: clone ( & logger) , & scorer, & ProbabilisticScoringFeeParameters :: default ( ) ,
7519+ & random_seed_bytes
7520+ ) {
7521+ assert_eq ! ( err, "Failed to find a path to the given destination" ) ;
7522+ } else { panic ! ( ) }
7523+ }
74517524}
74527525
74537526#[ cfg( all( any( test, ldk_bench) , not( feature = "no-std" ) ) ) ]
0 commit comments