Skip to content

Commit a2886a2

Browse files
get_route: fix path_min when adding first_hop<>route_hint candidates
Previously, we would add a candidate hop to the list of potential hops even though its available contribution wasn't sufficient to meet the next hop's min_htlc. We'd subsequently build an invalid path using this hop and hit a debug assertion.
1 parent 041c45d commit a2886a2

File tree

1 file changed

+121
-5
lines changed

1 file changed

+121
-5
lines changed

lightning/src/routing/router.rs

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2163,11 +2163,15 @@ 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+
// The next channel will need to relay this channel's min_htlc *plus* the fees taken by
2167+
// this route hint's source node to forward said min over this channel.
2168+
aggregate_next_hops_path_htlc_minimum_msat = {
2169+
let curr_htlc_min = cmp::max(
2170+
candidate.htlc_minimum_msat(), aggregate_next_hops_path_htlc_minimum_msat
2171+
);
2172+
let curr_htlc_min_fee = if let Some(val) = compute_fees(curr_htlc_min, hop.fees) { val } else { break };
2173+
if let Some(min) = curr_htlc_min.checked_add(curr_htlc_min_fee) { min } else { break }
2174+
};
21712175

21722176
if idx == route.0.len() - 1 {
21732177
// The last hop in this iterator is the first hop in
@@ -7185,6 +7189,118 @@ mod tests {
71857189
assert_eq!(err, "Failed to find a path to the given destination");
71867190
} else { panic!() }
71877191
}
7192+
7193+
#[test]
7194+
fn min_htlc_overpay_violates_max_htlc() {
7195+
// Test that if overpaying to meet a later hop's min_htlc and causes us to violate an earlier
7196+
// hop's max_htlc, we don't consider that candidate hop valid. Previously we would add this hop
7197+
// to `targets` and build an invalid path with it, and subsquently hit a debug panic asserting
7198+
// that the used liquidity for a hop was less than its available liquidity limit.
7199+
let secp_ctx = Secp256k1::new();
7200+
let logger = Arc::new(ln_test_utils::TestLogger::new());
7201+
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger)));
7202+
let scorer = ln_test_utils::TestScorer::new();
7203+
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
7204+
let random_seed_bytes = keys_manager.get_secure_random_bytes();
7205+
let config = UserConfig::default();
7206+
7207+
// Values are taken from the fuzz input that uncovered this panic.
7208+
let amt_msat = 7_4009_8048;
7209+
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
7210+
let first_hop_outbound_capacity = 2_7345_2000;
7211+
let first_hops = vec![get_channel_details(
7212+
Some(200), nodes[0], channelmanager::provided_init_features(&config),
7213+
first_hop_outbound_capacity
7214+
)];
7215+
7216+
let base_fee = 1_6778_3453;
7217+
let htlc_min = 2_5165_8240;
7218+
let payment_params = {
7219+
let route_hint = RouteHint(vec![RouteHintHop {
7220+
src_node_id: nodes[0],
7221+
short_channel_id: 42,
7222+
fees: RoutingFees {
7223+
base_msat: base_fee,
7224+
proportional_millionths: 0,
7225+
},
7226+
cltv_expiry_delta: 10,
7227+
htlc_minimum_msat: Some(htlc_min),
7228+
htlc_maximum_msat: None,
7229+
}]);
7230+
7231+
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+
7236+
let netgraph = network_graph.read_only();
7237+
let route_params = RouteParameters::from_payment_params_and_value(
7238+
payment_params, amt_msat);
7239+
if let Err(LightningError { err, .. }) = get_route(
7240+
&our_id, &route_params, &netgraph, Some(&first_hops.iter().collect::<Vec<_>>()),
7241+
Arc::clone(&logger), &scorer, &(), &random_seed_bytes
7242+
) {
7243+
assert_eq!(err, "Failed to find a path to the given destination");
7244+
} else { panic!() }
7245+
}
7246+
7247+
#[test]
7248+
fn previously_used_liquidity_violates_max_htlc() {
7249+
// Test that if a candidate first_hop<>route_hint_src_node channel does not have enough
7250+
// contribution amount to cover the next hop's min_htlc plus fees, we will not consider that
7251+
// candidate. In this case, the candidate does not have enough due to a previous path taking up
7252+
// some of its liquidity. Previously we would construct an invalid path and hit a debug panic
7253+
// asserting that the used liquidity for a hop was less than its available liquidity limit.
7254+
let secp_ctx = Secp256k1::new();
7255+
let logger = Arc::new(ln_test_utils::TestLogger::new());
7256+
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger)));
7257+
let scorer = ln_test_utils::TestScorer::new();
7258+
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
7259+
let random_seed_bytes = keys_manager.get_secure_random_bytes();
7260+
let config = UserConfig::default();
7261+
7262+
// Values are taken from the fuzz input that uncovered this panic.
7263+
let amt_msat = 52_4288;
7264+
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
7265+
let first_hops = vec![get_channel_details(
7266+
Some(161), nodes[0], channelmanager::provided_init_features(&config), 486_4000
7267+
), get_channel_details(
7268+
Some(122), nodes[0], channelmanager::provided_init_features(&config), 179_5000
7269+
)];
7270+
7271+
let base_fees = [0, 425_9840, 0, 0];
7272+
let htlc_mins = [1_4392, 19_7401, 1027, 6_5535];
7273+
let payment_params = {
7274+
let mut route_hints = Vec::new();
7275+
for (idx, (base_fee, htlc_min)) in base_fees.iter().zip(htlc_mins.iter()).enumerate() {
7276+
route_hints.push(RouteHint(vec![RouteHintHop {
7277+
src_node_id: nodes[0],
7278+
short_channel_id: 42 + idx as u64,
7279+
fees: RoutingFees {
7280+
base_msat: *base_fee,
7281+
proportional_millionths: 0,
7282+
},
7283+
cltv_expiry_delta: 10,
7284+
htlc_minimum_msat: Some(*htlc_min),
7285+
htlc_maximum_msat: Some(htlc_min * 100),
7286+
}]));
7287+
}
7288+
PaymentParameters::from_node_id(nodes[1], 42)
7289+
.with_route_hints(route_hints).unwrap()
7290+
.with_bolt11_features(channelmanager::provided_invoice_features(&config)).unwrap()
7291+
};
7292+
7293+
let netgraph = network_graph.read_only();
7294+
let route_params = RouteParameters::from_payment_params_and_value(
7295+
payment_params, amt_msat);
7296+
7297+
let route = get_route(
7298+
&our_id, &route_params, &netgraph, Some(&first_hops.iter().collect::<Vec<_>>()),
7299+
Arc::clone(&logger), &scorer, &(), &random_seed_bytes
7300+
).unwrap();
7301+
assert_eq!(route.paths.len(), 1);
7302+
assert_eq!(route.get_total_amount(), amt_msat);
7303+
}
71887304
}
71897305

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

0 commit comments

Comments
 (0)