@@ -1234,7 +1234,11 @@ impl<'a> PaymentPath<'a> {
12341234 // Note that this function is not aware of the available_liquidity limit, and thus does not
12351235 // support increasing the value being transferred beyond what was selected during the initial
12361236 // 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 ;
12381242 let mut total_fee_paid_msat = 0 as u64 ;
12391243 for i in ( 0 ..self . hops . len ( ) ) . rev ( ) {
12401244 let last_hop = i == self . hops . len ( ) - 1 ;
@@ -1249,6 +1253,7 @@ impl<'a> PaymentPath<'a> {
12491253
12501254 let cur_hop = & mut self . hops . get_mut ( i) . unwrap ( ) . 0 ;
12511255 cur_hop. next_hops_fee_msat = total_fee_paid_msat;
1256+ cur_hop. path_penalty_msat += extra_contribution_msat;
12521257 // Overpay in fees if we can't save these funds due to htlc_minimum_msat.
12531258 // We try to account for htlc_minimum_msat in scoring (add_entry!), so that nodes don't
12541259 // set it too high just to maliciously take more fees by exploiting this
@@ -1264,8 +1269,15 @@ impl<'a> PaymentPath<'a> {
12641269 // Also, this can't be exploited more heavily than *announce a free path and fail
12651270 // all payments*.
12661271 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+ }
12691281 }
12701282
12711283 if last_hop {
@@ -1293,6 +1305,7 @@ impl<'a> PaymentPath<'a> {
12931305 }
12941306 }
12951307 }
1308+ value_msat + extra_contribution_msat
12961309 }
12971310}
12981311
@@ -2289,8 +2302,8 @@ where L::Target: Logger {
22892302 // recompute the fees again, so that if that's the case, we match the currently
22902303 // underpaid htlc_minimum_msat with fees.
22912304 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 ) ;
22942307
22952308 // Since a path allows to transfer as much value as
22962309 // the smallest channel it has ("bottleneck"), we should recompute
@@ -7427,6 +7440,67 @@ mod tests {
74277440 assert_eq ! ( err, "Failed to find a path to the given destination" ) ;
74287441 } else { panic ! ( ) }
74297442 }
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+ }
74307504}
74317505
74327506#[ cfg( all( any( test, ldk_bench) , not( feature = "no-std" ) ) ) ]
0 commit comments