@@ -1234,7 +1234,11 @@ impl<'a> PaymentPath<'a> {
1234
1234
// Note that this function is not aware of the available_liquidity limit, and thus does not
1235
1235
// support increasing the value being transferred beyond what was selected during the initial
1236
1236
// routing passes.
1237
- fn update_value_and_recompute_fees ( & mut self , value_msat : u64 ) {
1237
+ //
1238
+ // Returns the amount that this path contributes to the total payment value, which may be greater
1239
+ // than `value_msat` if we had to overpay to meet the final node's `htlc_minimum_msat`.
1240
+ fn update_value_and_recompute_fees ( & mut self , value_msat : u64 ) -> u64 {
1241
+ let mut extra_contribution_msat = 0 ;
1238
1242
let mut total_fee_paid_msat = 0 as u64 ;
1239
1243
for i in ( 0 ..self . hops . len ( ) ) . rev ( ) {
1240
1244
let last_hop = i == self . hops . len ( ) - 1 ;
@@ -1249,6 +1253,7 @@ impl<'a> PaymentPath<'a> {
1249
1253
1250
1254
let cur_hop = & mut self . hops . get_mut ( i) . unwrap ( ) . 0 ;
1251
1255
cur_hop. next_hops_fee_msat = total_fee_paid_msat;
1256
+ cur_hop. path_penalty_msat += extra_contribution_msat;
1252
1257
// Overpay in fees if we can't save these funds due to htlc_minimum_msat.
1253
1258
// We try to account for htlc_minimum_msat in scoring (add_entry!), so that nodes don't
1254
1259
// set it too high just to maliciously take more fees by exploiting this
@@ -1264,8 +1269,15 @@ impl<'a> PaymentPath<'a> {
1264
1269
// Also, this can't be exploited more heavily than *announce a free path and fail
1265
1270
// all payments*.
1266
1271
cur_hop_transferred_amount_msat += extra_fees_msat;
1267
- total_fee_paid_msat += extra_fees_msat;
1268
- cur_hop_fees_msat += extra_fees_msat;
1272
+
1273
+ // We remember and return the extra fees on the final hop to allow accounting for
1274
+ // them in the path's value contribution.
1275
+ if last_hop {
1276
+ extra_contribution_msat = extra_fees_msat;
1277
+ } else {
1278
+ total_fee_paid_msat += extra_fees_msat;
1279
+ cur_hop_fees_msat += extra_fees_msat;
1280
+ }
1269
1281
}
1270
1282
1271
1283
if last_hop {
@@ -1293,6 +1305,7 @@ impl<'a> PaymentPath<'a> {
1293
1305
}
1294
1306
}
1295
1307
}
1308
+ value_msat + extra_contribution_msat
1296
1309
}
1297
1310
}
1298
1311
@@ -2289,8 +2302,8 @@ where L::Target: Logger {
2289
2302
// recompute the fees again, so that if that's the case, we match the currently
2290
2303
// underpaid htlc_minimum_msat with fees.
2291
2304
debug_assert_eq ! ( payment_path. get_value_msat( ) , value_contribution_msat) ;
2292
- value_contribution_msat = cmp:: min ( value_contribution_msat, final_value_msat) ;
2293
- payment_path. update_value_and_recompute_fees ( value_contribution_msat ) ;
2305
+ let desired_value_contribution = cmp:: min ( value_contribution_msat, final_value_msat) ;
2306
+ value_contribution_msat = payment_path. update_value_and_recompute_fees ( desired_value_contribution ) ;
2294
2307
2295
2308
// Since a path allows to transfer as much value as
2296
2309
// the smallest channel it has ("bottleneck"), we should recompute
@@ -7427,6 +7440,67 @@ mod tests {
7427
7440
assert_eq ! ( err, "Failed to find a path to the given destination" ) ;
7428
7441
} else { panic ! ( ) }
7429
7442
}
7443
+
7444
+ #[ test]
7445
+ fn path_contribution_includes_min_htlc_overpay ( ) {
7446
+ // Previously, the fuzzer hit a debug panic because we wouldn't include the amount overpaid to
7447
+ // meet a last hop's min_htlc in the total collected paths value. We now include this value and
7448
+ // also penalize hops along the overpaying path to ensure that it gets deprioritized in path
7449
+ // selection, both tested here.
7450
+ let secp_ctx = Secp256k1 :: new ( ) ;
7451
+ let logger = Arc :: new ( ln_test_utils:: TestLogger :: new ( ) ) ;
7452
+ let network_graph = Arc :: new ( NetworkGraph :: new ( Network :: Testnet , Arc :: clone ( & logger) ) ) ;
7453
+ let scorer = ProbabilisticScorer :: new ( ProbabilisticScoringDecayParameters :: default ( ) , network_graph. clone ( ) , logger. clone ( ) ) ;
7454
+ let keys_manager = ln_test_utils:: TestKeysInterface :: new ( & [ 0u8 ; 32 ] , Network :: Testnet ) ;
7455
+ let random_seed_bytes = keys_manager. get_secure_random_bytes ( ) ;
7456
+ let config = UserConfig :: default ( ) ;
7457
+
7458
+ // Values are taken from the fuzz input that uncovered this panic.
7459
+ let amt_msat = 562_0000 ;
7460
+ let ( _, our_id, _, nodes) = get_nodes ( & secp_ctx) ;
7461
+ let first_hops = vec ! [
7462
+ get_channel_details(
7463
+ Some ( 83 ) , nodes[ 0 ] , channelmanager:: provided_init_features( & config) , 2199_0000 ,
7464
+ ) ,
7465
+ ] ;
7466
+
7467
+ let htlc_mins = [ 49_0000 , 1125_0000 ] ;
7468
+ let payment_params = {
7469
+ let blinded_path = BlindedPath {
7470
+ introduction_node_id : nodes[ 0 ] ,
7471
+ blinding_point : ln_test_utils:: pubkey ( 42 ) ,
7472
+ blinded_hops : vec ! [
7473
+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 42 as u8 ) , encrypted_payload: Vec :: new( ) } ,
7474
+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 42 as u8 ) , encrypted_payload: Vec :: new( ) }
7475
+ ] ,
7476
+ } ;
7477
+ let mut blinded_hints = Vec :: new ( ) ;
7478
+ for htlc_min in htlc_mins {
7479
+ blinded_hints. push ( ( BlindedPayInfo {
7480
+ fee_base_msat : 0 ,
7481
+ fee_proportional_millionths : 0 ,
7482
+ htlc_minimum_msat : htlc_min,
7483
+ htlc_maximum_msat : htlc_min * 100 ,
7484
+ cltv_expiry_delta : 10 ,
7485
+ features : BlindedHopFeatures :: empty ( ) ,
7486
+ } , blinded_path. clone ( ) ) ) ;
7487
+ }
7488
+ let bolt12_features: Bolt12InvoiceFeatures = channelmanager:: provided_invoice_features ( & config) . to_context ( ) ;
7489
+ PaymentParameters :: blinded ( blinded_hints. clone ( ) )
7490
+ . with_bolt12_features ( bolt12_features. clone ( ) ) . unwrap ( )
7491
+ } ;
7492
+
7493
+ let netgraph = network_graph. read_only ( ) ;
7494
+ let route_params = RouteParameters :: from_payment_params_and_value (
7495
+ payment_params, amt_msat) ;
7496
+ let route = get_route (
7497
+ & our_id, & route_params, & netgraph, Some ( & first_hops. iter ( ) . collect :: < Vec < _ > > ( ) ) ,
7498
+ Arc :: clone ( & logger) , & scorer, & ProbabilisticScoringFeeParameters :: default ( ) ,
7499
+ & random_seed_bytes
7500
+ ) . unwrap ( ) ;
7501
+ assert_eq ! ( route. paths. len( ) , 1 ) ;
7502
+ assert_eq ! ( route. get_total_amount( ) , amt_msat) ;
7503
+ }
7430
7504
}
7431
7505
7432
7506
#[ cfg( all( any( test, ldk_bench) , not( feature = "no-std" ) ) ) ]
0 commit comments