Skip to content

Commit 46a5414

Browse files
Router: discard candidate hops that violate an upstream hop's max_htlc
See new tests, the fuzzer found a debug panic in two of these cases, one where we overpay to meet a hop's min_htlc and one where a previously found path is already taking up some of a hop's liquidity.
1 parent 09e4c3d commit 46a5414

File tree

1 file changed

+135
-5
lines changed

1 file changed

+135
-5
lines changed

lightning/src/routing/router.rs

Lines changed: 135 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2163,11 +2163,13 @@ where L::Target: Logger {
21632163
.map_or(None, |inc| inc.checked_add(aggregate_next_hops_fee_msat));
21642164
aggregate_next_hops_fee_msat = if let Some(val) = hops_fee { val } else { break; };
21652165

2166-
let hop_htlc_minimum_msat = candidate.htlc_minimum_msat();
2167-
let hop_htlc_minimum_msat_inc = if let Some(val) = compute_fees(aggregate_next_hops_path_htlc_minimum_msat, hop.fees) { val } else { break; };
2168-
let hops_path_htlc_minimum = aggregate_next_hops_path_htlc_minimum_msat
2169-
.checked_add(hop_htlc_minimum_msat_inc);
2170-
aggregate_next_hops_path_htlc_minimum_msat = if let Some(val) = hops_path_htlc_minimum { cmp::max(hop_htlc_minimum_msat, val) } else { break; };
2166+
aggregate_next_hops_path_htlc_minimum_msat = {
2167+
let curr_htlc_min = cmp::max(
2168+
candidate.htlc_minimum_msat(), aggregate_next_hops_path_htlc_minimum_msat
2169+
);
2170+
let curr_htlc_min_fee = if let Some(val) = compute_fees(curr_htlc_min, hop.fees) { val } else { break };
2171+
if let Some(min) = curr_htlc_min.checked_add(curr_htlc_min_fee) { min } else { break }
2172+
};
21712173

21722174
if idx == route.0.len() - 1 {
21732175
// The last hop in this iterator is the first hop in
@@ -7185,6 +7187,134 @@ mod tests {
71857187
assert_eq!(err, "Failed to find a path to the given destination");
71867188
} else { panic!() }
71877189
}
7190+
7191+
#[test]
7192+
fn min_htlc_overpay_violates_max_htlc() {
7193+
// Test that if overpaying to meet a later hop's min_htlc and causes us to violate an earlier
7194+
// hop's max_htlc, we discard that invalid path. Previously we would consider the path to be
7195+
// valid and hit a debug panic asserting that the used liquidity for a hop was less than its
7196+
// available liquidity limit.
7197+
let secp_ctx = Secp256k1::new();
7198+
let logger = Arc::new(ln_test_utils::TestLogger::new());
7199+
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger)));
7200+
let scorer = ln_test_utils::TestScorer::new();
7201+
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
7202+
let random_seed_bytes = keys_manager.get_secure_random_bytes();
7203+
let config = UserConfig::default();
7204+
7205+
// Values are taken from the fuzz input that uncovered this panic.
7206+
let amt_msat = 7_4009_8048;
7207+
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
7208+
let first_hop_outbound_capacity = 2_7345_2000;
7209+
let first_hops = vec![get_channel_details(
7210+
Some(200), nodes[0], channelmanager::provided_init_features(&config),
7211+
first_hop_outbound_capacity
7212+
)];
7213+
7214+
let route_hint = RouteHint(vec![RouteHintHop {
7215+
src_node_id: nodes[0],
7216+
short_channel_id: 44,
7217+
fees: RoutingFees {
7218+
base_msat: 1_6778_3453,
7219+
proportional_millionths: 0,
7220+
},
7221+
cltv_expiry_delta: 10,
7222+
htlc_minimum_msat: Some(2_5165_8240),
7223+
htlc_maximum_msat: None,
7224+
}]);
7225+
7226+
let payment_params = PaymentParameters::from_node_id(nodes[1], 42)
7227+
.with_route_hints(vec![route_hint]).unwrap()
7228+
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();
7229+
7230+
let netgraph = network_graph.read_only();
7231+
let route_params = RouteParameters::from_payment_params_and_value(
7232+
payment_params, amt_msat);
7233+
if let Err(LightningError { err, .. }) = get_route(
7234+
&our_id, &route_params, &netgraph, Some(&first_hops.iter().collect::<Vec<_>>()),
7235+
Arc::clone(&logger), &scorer, &(), &random_seed_bytes
7236+
) {
7237+
assert_eq!(err, "Failed to find a path to the given destination");
7238+
} else { panic!() }
7239+
}
7240+
7241+
#[test]
7242+
fn previously_used_liquidity_violates_max_htlc() {
7243+
// Test that if a candidate hop would cause us to violate an upstream hop's available
7244+
// contribution amount due to a previously found path, we will not consider that candidate in
7245+
// path construction. Previously we would construct an invalid path and hit a debug panic
7246+
// asserting that the used liquidity for a hop was less than its available liquidity limit.
7247+
let secp_ctx = Secp256k1::new();
7248+
let logger = Arc::new(ln_test_utils::TestLogger::new());
7249+
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger)));
7250+
let scorer = ln_test_utils::TestScorer::new();
7251+
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
7252+
let random_seed_bytes = keys_manager.get_secure_random_bytes();
7253+
let config = UserConfig::default();
7254+
7255+
// Values are taken from the fuzz input that uncovered this panic.
7256+
let amt_msat = 52_4288;
7257+
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
7258+
let first_hops = vec![get_channel_details(
7259+
Some(161), nodes[0], channelmanager::provided_init_features(&config), 486_4000
7260+
), get_channel_details(
7261+
Some(122), nodes[0], channelmanager::provided_init_features(&config), 179_5000
7262+
)];
7263+
7264+
let route_hints = vec![RouteHint(vec![RouteHintHop {
7265+
src_node_id: nodes[0],
7266+
short_channel_id: 42,
7267+
fees: RoutingFees {
7268+
base_msat: 0,
7269+
proportional_millionths: 0,
7270+
},
7271+
cltv_expiry_delta: 10,
7272+
htlc_minimum_msat: Some(1_4392),
7273+
htlc_maximum_msat: Some(143_9200),
7274+
}]), RouteHint(vec![RouteHintHop {
7275+
src_node_id: nodes[0],
7276+
short_channel_id: 43,
7277+
fees: RoutingFees {
7278+
base_msat: 425_9840,
7279+
proportional_millionths: 0,
7280+
},
7281+
cltv_expiry_delta: 10,
7282+
htlc_minimum_msat: Some(19_7401),
7283+
htlc_maximum_msat: Some(1974_0100),
7284+
}]), RouteHint(vec![RouteHintHop {
7285+
src_node_id: nodes[0],
7286+
short_channel_id: 44,
7287+
fees: RoutingFees {
7288+
base_msat: 0,
7289+
proportional_millionths: 0,
7290+
},
7291+
cltv_expiry_delta: 10,
7292+
htlc_minimum_msat: Some(1027),
7293+
htlc_maximum_msat: Some(10_2700),
7294+
}]), RouteHint(vec![RouteHintHop {
7295+
src_node_id: nodes[0],
7296+
short_channel_id: 45,
7297+
fees: RoutingFees {
7298+
base_msat: 0,
7299+
proportional_millionths: 0,
7300+
},
7301+
cltv_expiry_delta: 10,
7302+
htlc_minimum_msat: Some(6_5535),
7303+
htlc_maximum_msat: Some(655_3500),
7304+
}])];
7305+
7306+
let payment_params = PaymentParameters::from_node_id(nodes[1], 42)
7307+
.with_route_hints(route_hints).unwrap()
7308+
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();
7309+
7310+
let netgraph = network_graph.read_only();
7311+
let route_params = RouteParameters::from_payment_params_and_value(
7312+
payment_params, amt_msat);
7313+
assert!(get_route(
7314+
&our_id, &route_params, &netgraph, Some(&first_hops.iter().collect::<Vec<_>>()),
7315+
Arc::clone(&logger), &scorer, &(), &random_seed_bytes
7316+
).is_ok());
7317+
}
71887318
}
71897319

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

0 commit comments

Comments
 (0)