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