@@ -1265,7 +1265,11 @@ impl<'a> PaymentPath<'a> {
12651265 // Note that this function is not aware of the available_liquidity limit, and thus does not
12661266 // support increasing the value being transferred beyond what was selected during the initial
12671267 // routing passes.
1268- fn update_value_and_recompute_fees ( & mut self , value_msat : u64 ) {
1268+ //
1269+ // Returns the amount that this path contributes to the total payment value, which may be greater
1270+ // than `value_msat` if we had to overpay to meet the final node's `htlc_minimum_msat`.
1271+ fn update_value_and_recompute_fees ( & mut self , value_msat : u64 ) -> u64 {
1272+ let mut extra_contribution_msat = 0 ;
12691273 let mut total_fee_paid_msat = 0 as u64 ;
12701274 for i in ( 0 ..self . hops . len ( ) ) . rev ( ) {
12711275 let last_hop = i == self . hops . len ( ) - 1 ;
@@ -1280,6 +1284,7 @@ impl<'a> PaymentPath<'a> {
12801284
12811285 let cur_hop = & mut self . hops . get_mut ( i) . unwrap ( ) . 0 ;
12821286 cur_hop. next_hops_fee_msat = total_fee_paid_msat;
1287+ cur_hop. path_penalty_msat += extra_contribution_msat;
12831288 // Overpay in fees if we can't save these funds due to htlc_minimum_msat.
12841289 // We try to account for htlc_minimum_msat in scoring (add_entry!), so that nodes don't
12851290 // set it too high just to maliciously take more fees by exploiting this
@@ -1295,8 +1300,15 @@ impl<'a> PaymentPath<'a> {
12951300 // Also, this can't be exploited more heavily than *announce a free path and fail
12961301 // all payments*.
12971302 cur_hop_transferred_amount_msat += extra_fees_msat;
1298- total_fee_paid_msat += extra_fees_msat;
1299- cur_hop_fees_msat += extra_fees_msat;
1303+
1304+ // We remember and return the extra fees on the final hop to allow accounting for
1305+ // them in the path's value contribution.
1306+ if last_hop {
1307+ extra_contribution_msat = extra_fees_msat;
1308+ } else {
1309+ total_fee_paid_msat += extra_fees_msat;
1310+ cur_hop_fees_msat += extra_fees_msat;
1311+ }
13001312 }
13011313
13021314 if last_hop {
@@ -1324,6 +1336,7 @@ impl<'a> PaymentPath<'a> {
13241336 }
13251337 }
13261338 }
1339+ value_msat + extra_contribution_msat
13271340 }
13281341}
13291342
@@ -2330,8 +2343,8 @@ where L::Target: Logger {
23302343 // recompute the fees again, so that if that's the case, we match the currently
23312344 // underpaid htlc_minimum_msat with fees.
23322345 debug_assert_eq ! ( payment_path. get_value_msat( ) , value_contribution_msat) ;
2333- value_contribution_msat = cmp:: min ( value_contribution_msat, final_value_msat) ;
2334- payment_path. update_value_and_recompute_fees ( value_contribution_msat ) ;
2346+ let desired_value_contribution = cmp:: min ( value_contribution_msat, final_value_msat) ;
2347+ value_contribution_msat = payment_path. update_value_and_recompute_fees ( desired_value_contribution ) ;
23352348
23362349 // Since a path allows to transfer as much value as
23372350 // the smallest channel it has ("bottleneck"), we should recompute
@@ -7521,6 +7534,67 @@ mod tests {
75217534 assert_eq ! ( err, "Failed to find a path to the given destination" ) ;
75227535 } else { panic ! ( ) }
75237536 }
7537+
7538+ #[ test]
7539+ fn path_contribution_includes_min_htlc_overpay ( ) {
7540+ // Previously, the fuzzer hit a debug panic because we wouldn't include the amount overpaid to
7541+ // meet a last hop's min_htlc in the total collected paths value. We now include this value and
7542+ // also penalize hops along the overpaying path to ensure that it gets deprioritized in path
7543+ // selection, both tested here.
7544+ let secp_ctx = Secp256k1 :: new ( ) ;
7545+ let logger = Arc :: new ( ln_test_utils:: TestLogger :: new ( ) ) ;
7546+ let network_graph = Arc :: new ( NetworkGraph :: new ( Network :: Testnet , Arc :: clone ( & logger) ) ) ;
7547+ let scorer = ProbabilisticScorer :: new ( ProbabilisticScoringDecayParameters :: default ( ) , network_graph. clone ( ) , logger. clone ( ) ) ;
7548+ let keys_manager = ln_test_utils:: TestKeysInterface :: new ( & [ 0u8 ; 32 ] , Network :: Testnet ) ;
7549+ let random_seed_bytes = keys_manager. get_secure_random_bytes ( ) ;
7550+ let config = UserConfig :: default ( ) ;
7551+
7552+ // Values are taken from the fuzz input that uncovered this panic.
7553+ let amt_msat = 562_0000 ;
7554+ let ( _, our_id, _, nodes) = get_nodes ( & secp_ctx) ;
7555+ let first_hops = vec ! [
7556+ get_channel_details(
7557+ Some ( 83 ) , nodes[ 0 ] , channelmanager:: provided_init_features( & config) , 2199_0000 ,
7558+ ) ,
7559+ ] ;
7560+
7561+ let htlc_mins = [ 49_0000 , 1125_0000 ] ;
7562+ let payment_params = {
7563+ let blinded_path = BlindedPath {
7564+ introduction_node_id : nodes[ 0 ] ,
7565+ blinding_point : ln_test_utils:: pubkey ( 42 ) ,
7566+ blinded_hops : vec ! [
7567+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 42 as u8 ) , encrypted_payload: Vec :: new( ) } ,
7568+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 42 as u8 ) , encrypted_payload: Vec :: new( ) }
7569+ ] ,
7570+ } ;
7571+ let mut blinded_hints = Vec :: new ( ) ;
7572+ for htlc_min in htlc_mins. iter ( ) {
7573+ blinded_hints. push ( ( BlindedPayInfo {
7574+ fee_base_msat : 0 ,
7575+ fee_proportional_millionths : 0 ,
7576+ htlc_minimum_msat : * htlc_min,
7577+ htlc_maximum_msat : * htlc_min * 100 ,
7578+ cltv_expiry_delta : 10 ,
7579+ features : BlindedHopFeatures :: empty ( ) ,
7580+ } , blinded_path. clone ( ) ) ) ;
7581+ }
7582+ let bolt12_features: Bolt12InvoiceFeatures = channelmanager:: provided_invoice_features ( & config) . to_context ( ) ;
7583+ PaymentParameters :: blinded ( blinded_hints. clone ( ) )
7584+ . with_bolt12_features ( bolt12_features. clone ( ) ) . unwrap ( )
7585+ } ;
7586+
7587+ let netgraph = network_graph. read_only ( ) ;
7588+ let route_params = RouteParameters :: from_payment_params_and_value (
7589+ payment_params, amt_msat) ;
7590+ let route = get_route (
7591+ & our_id, & route_params, & netgraph, Some ( & first_hops. iter ( ) . collect :: < Vec < _ > > ( ) ) ,
7592+ Arc :: clone ( & logger) , & scorer, & ProbabilisticScoringFeeParameters :: default ( ) ,
7593+ & random_seed_bytes
7594+ ) . unwrap ( ) ;
7595+ assert_eq ! ( route. paths. len( ) , 1 ) ;
7596+ assert_eq ! ( route. get_total_amount( ) , amt_msat) ;
7597+ }
75247598}
75257599
75267600#[ cfg( all( any( test, ldk_bench) , not( feature = "no-std" ) ) ) ]
0 commit comments