@@ -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
@@ -7448,6 +7449,78 @@ mod tests {
7448
7449
assert_eq ! ( route. paths. len( ) , 1 ) ;
7449
7450
assert_eq ! ( route. get_total_amount( ) , amt_msat) ;
7450
7451
}
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
+ }
7451
7524
}
7452
7525
7453
7526
#[ cfg( all( any( test, ldk_bench) , not( feature = "no-std" ) ) ) ]
0 commit comments