Skip to content

Commit 905387b

Browse files
Router: discard invalid PaymentPaths that violate a 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 905387b

File tree

1 file changed

+137
-2
lines changed

1 file changed

+137
-2
lines changed

lightning/src/routing/router.rs

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2300,12 +2300,19 @@ where L::Target: Logger {
23002300
.chain(payment_path.hops.iter().map(|(hop, _)| &hop.node_id));
23012301
for (prev_hop, (hop, _)) in prev_hop_iter.zip(payment_path.hops.iter()) {
23022302
let spent_on_hop_msat = value_contribution_msat + hop.next_hops_fee_msat;
2303+
let hop_max_msat = max_htlc_from_capacity(
2304+
hop.candidate.effective_capacity(), channel_saturation_pow_half
2305+
);
2306+
let used_liquidity_candidate = used_liquidities
2307+
.get(&hop.candidate.id(*prev_hop < hop.node_id))
2308+
.map_or(spent_on_hop_msat, |used_liquidity_msat|
2309+
used_liquidity_msat + spent_on_hop_msat);
2310+
if used_liquidity_candidate > hop_max_msat { break 'path_construction }
2311+
23032312
let used_liquidity_msat = used_liquidities
23042313
.entry(hop.candidate.id(*prev_hop < hop.node_id))
23052314
.and_modify(|used_liquidity_msat| *used_liquidity_msat += spent_on_hop_msat)
23062315
.or_insert(spent_on_hop_msat);
2307-
let hop_capacity = hop.candidate.effective_capacity();
2308-
let hop_max_msat = max_htlc_from_capacity(hop_capacity, channel_saturation_pow_half);
23092316
if *used_liquidity_msat == hop_max_msat {
23102317
// If this path used all of this channel's available liquidity, we know
23112318
// this path will not be selected again in the next loop iteration.
@@ -7185,6 +7192,134 @@ mod tests {
71857192
assert_eq!(err, "Failed to find a path to the given destination");
71867193
} else { panic!() }
71877194
}
7195+
7196+
#[test]
7197+
fn min_htlc_overpay_violates_max_htlc() {
7198+
// Test that if overpaying to meet a later hop's min_htlc and causes us to violate an earlier
7199+
// hop's max_htlc, we discard that invalid path. Previously we would consider the path to be
7200+
// valid and hit a debug panic asserting that the used liquidity for a hop was less than its
7201+
// available liquidity limit.
7202+
let secp_ctx = Secp256k1::new();
7203+
let logger = Arc::new(ln_test_utils::TestLogger::new());
7204+
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger)));
7205+
let scorer = ln_test_utils::TestScorer::new();
7206+
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
7207+
let random_seed_bytes = keys_manager.get_secure_random_bytes();
7208+
let config = UserConfig::default();
7209+
7210+
// Values are taken from the fuzz input that uncovered this panic.
7211+
let amt_msat = 7_4009_8048;
7212+
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
7213+
let first_hop_outbound_capacity = 2_7345_2000;
7214+
let first_hops = vec![get_channel_details(
7215+
Some(200), nodes[0], channelmanager::provided_init_features(&config),
7216+
first_hop_outbound_capacity
7217+
)];
7218+
7219+
let route_hint = RouteHint(vec![RouteHintHop {
7220+
src_node_id: nodes[0],
7221+
short_channel_id: 44,
7222+
fees: RoutingFees {
7223+
base_msat: 1_6778_3453,
7224+
proportional_millionths: 0,
7225+
},
7226+
cltv_expiry_delta: 10,
7227+
htlc_minimum_msat: Some(2_5165_8240),
7228+
htlc_maximum_msat: Some(251_6582_4000),
7229+
}]);
7230+
7231+
let payment_params = PaymentParameters::from_node_id(nodes[1], 42)
7232+
.with_route_hints(vec![route_hint]).unwrap()
7233+
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();
7234+
7235+
let netgraph = network_graph.read_only();
7236+
let route_params = RouteParameters::from_payment_params_and_value(
7237+
payment_params, amt_msat);
7238+
if let Err(LightningError { err, .. }) = get_route(
7239+
&our_id, &route_params, &netgraph, Some(&first_hops.iter().collect::<Vec<_>>()),
7240+
Arc::clone(&logger), &scorer, &(), &random_seed_bytes
7241+
) {
7242+
assert_eq!(err, "Failed to find a path to the given destination");
7243+
} else { panic!() }
7244+
}
7245+
7246+
#[test]
7247+
fn previously_used_liquidity_violates_max_htlc() {
7248+
// Test that if a previously found path causes us to violate a newly found path hop's max_htlc,
7249+
// we will discard that invalid path. Previously we would consider the path to be valid and hit
7250+
// a debug panic asserting that the used liquidity for a hop was less than its available
7251+
// liquidity limit.
7252+
let secp_ctx = Secp256k1::new();
7253+
let logger = Arc::new(ln_test_utils::TestLogger::new());
7254+
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger)));
7255+
let scorer = ln_test_utils::TestScorer::new();
7256+
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
7257+
let random_seed_bytes = keys_manager.get_secure_random_bytes();
7258+
let config = UserConfig::default();
7259+
7260+
// Values are taken from the fuzz input that uncovered this panic.
7261+
let amt_msat = 52_4288;
7262+
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
7263+
let first_hops = vec![get_channel_details(
7264+
Some(161), nodes[0], channelmanager::provided_init_features(&config), 486_4000
7265+
), get_channel_details(
7266+
Some(122), nodes[0], channelmanager::provided_init_features(&config), 179_5000
7267+
)];
7268+
7269+
let route_hints = vec![RouteHint(vec![RouteHintHop {
7270+
src_node_id: nodes[0],
7271+
short_channel_id: 42,
7272+
fees: RoutingFees {
7273+
base_msat: 0,
7274+
proportional_millionths: 0,
7275+
},
7276+
cltv_expiry_delta: 10,
7277+
htlc_minimum_msat: Some(1_4392),
7278+
htlc_maximum_msat: Some(143_9200),
7279+
}]), RouteHint(vec![RouteHintHop {
7280+
src_node_id: nodes[0],
7281+
short_channel_id: 43,
7282+
fees: RoutingFees {
7283+
base_msat: 425_9840,
7284+
proportional_millionths: 0,
7285+
},
7286+
cltv_expiry_delta: 10,
7287+
htlc_minimum_msat: Some(19_7401),
7288+
htlc_maximum_msat: Some(1974_0100),
7289+
}]), RouteHint(vec![RouteHintHop {
7290+
src_node_id: nodes[0],
7291+
short_channel_id: 44,
7292+
fees: RoutingFees {
7293+
base_msat: 0,
7294+
proportional_millionths: 0,
7295+
},
7296+
cltv_expiry_delta: 10,
7297+
htlc_minimum_msat: Some(1027),
7298+
htlc_maximum_msat: Some(10_2700),
7299+
}]), RouteHint(vec![RouteHintHop {
7300+
src_node_id: nodes[0],
7301+
short_channel_id: 45,
7302+
fees: RoutingFees {
7303+
base_msat: 0,
7304+
proportional_millionths: 0,
7305+
},
7306+
cltv_expiry_delta: 10,
7307+
htlc_minimum_msat: Some(6_5535),
7308+
htlc_maximum_msat: Some(655_3500),
7309+
}])];
7310+
7311+
let payment_params = PaymentParameters::from_node_id(nodes[1], 42)
7312+
.with_route_hints(route_hints).unwrap()
7313+
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap();
7314+
7315+
let netgraph = network_graph.read_only();
7316+
let route_params = RouteParameters::from_payment_params_and_value(
7317+
payment_params, amt_msat);
7318+
assert!(get_route(
7319+
&our_id, &route_params, &netgraph, Some(&first_hops.iter().collect::<Vec<_>>()),
7320+
Arc::clone(&logger), &scorer, &(), &random_seed_bytes
7321+
).is_ok());
7322+
}
71887323
}
71897324

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

0 commit comments

Comments
 (0)