Skip to content

Commit f1741a8

Browse files
get_route: fix path value contribution to include min htlc overpay
Previously, the fuzzer hit a debug panic because we wouldn't include the amount overpaid to meet a last hop's min_htlc in the total collected paths value. We now include this value and also penalize hops along the overpaying path to ensure that it gets deprioritized in path selection.
1 parent 31b032f commit f1741a8

File tree

1 file changed

+79
-5
lines changed

1 file changed

+79
-5
lines changed

lightning/src/routing/router.rs

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)