Skip to content

Commit 6b917c6

Browse files
Router: account for min HTLC overpay violating another hop's max_htlc
See new test, the fuzzer found a panic in this case.
1 parent 2f6a922 commit 6b917c6

File tree

1 file changed

+61
-0
lines changed

1 file changed

+61
-0
lines changed

lightning/src/routing/router.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,16 @@ impl<'a> PaymentPath<'a> {
12941294
}
12951295
}
12961296
}
1297+
fn violates_max_htlc(&self) -> bool {
1298+
let mut hop_transferred_amount = self.get_value_msat() + self.get_total_fee_paid_msat();
1299+
for (hop, _) in self.hops.iter() {
1300+
if hop_transferred_amount > hop.candidate.effective_capacity().as_msat() {
1301+
return true
1302+
}
1303+
hop_transferred_amount = hop_transferred_amount.saturating_sub(hop.fee_msat);
1304+
}
1305+
false
1306+
}
12971307
}
12981308

12991309
#[inline(always)]
@@ -2285,6 +2295,7 @@ where L::Target: Logger {
22852295
debug_assert_eq!(payment_path.get_value_msat(), value_contribution_msat);
22862296
value_contribution_msat = cmp::min(value_contribution_msat, final_value_msat);
22872297
payment_path.update_value_and_recompute_fees(value_contribution_msat);
2298+
if payment_path.violates_max_htlc() { break 'path_construction }
22882299

22892300
// Since a path allows to transfer as much value as
22902301
// the smallest channel it has ("bottleneck"), we should recompute
@@ -7185,6 +7196,56 @@ mod tests {
71857196
assert_eq!(err, "Failed to find a path to the given destination");
71867197
} else { panic!() }
71877198
}
7199+
7200+
#[test]
7201+
fn min_htlc_overpay_violates_max_htlc() {
7202+
// Test that if overpaying to meet a later hop's min_htlc and causes us to violate an earlier
7203+
// hop's max_htlc, we discard that invalid path. Previously we would consider the path to be
7204+
// valid and hit a debug panic asserting that the used liquidity for a hop was less than its
7205+
// available liquidity limit.
7206+
let secp_ctx = Secp256k1::new();
7207+
let logger = Arc::new(ln_test_utils::TestLogger::new());
7208+
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger)));
7209+
let scorer = ln_test_utils::TestScorer::new();
7210+
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
7211+
let random_seed_bytes = keys_manager.get_secure_random_bytes();
7212+
let config = UserConfig::default();
7213+
7214+
// Values are taken from the fuzz input that uncovered this panic.
7215+
let amt_msat = 7_4009_8048;
7216+
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
7217+
let first_hop_outbound_capacity = 2_7345_2000;
7218+
let first_hops = vec![get_channel_details(
7219+
Some(200), nodes[0], channelmanager::provided_init_features(&config),
7220+
first_hop_outbound_capacity
7221+
)];
7222+
7223+
let route_hint = RouteHint(vec![RouteHintHop {
7224+
src_node_id: nodes[0],
7225+
short_channel_id: 44,
7226+
fees: RoutingFees {
7227+
base_msat: 1_6778_3453,
7228+
proportional_millionths: 0,
7229+
},
7230+
cltv_expiry_delta: 10,
7231+
htlc_minimum_msat: Some(2_5165_8240),
7232+
htlc_maximum_msat: Some(251_6582_4000),
7233+
}]);
7234+
7235+
let payment_params = PaymentParameters::from_node_id(nodes[1], 42)
7236+
.with_route_hints(vec![route_hint]).unwrap()
7237+
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();
7238+
7239+
let netgraph = network_graph.read_only();
7240+
let route_params = RouteParameters::from_payment_params_and_value(
7241+
payment_params, amt_msat);
7242+
if let Err(LightningError { err, .. }) = get_route(
7243+
&our_id, &route_params, &netgraph, Some(&first_hops.iter().collect::<Vec<_>>()),
7244+
Arc::clone(&logger), &scorer, &(), &random_seed_bytes
7245+
) {
7246+
assert_eq!(err, "Failed to find a path to the given destination");
7247+
} else { panic!() }
7248+
}
71887249
}
71897250

71907251
#[cfg(all(any(test, ldk_bench), not(feature = "no-std")))]

0 commit comments

Comments
 (0)