Skip to content

Commit 93e645d

Browse files
committed
Track channels which a given payment part failed to traverse
When an HTLC fails, we currently rely on the scorer learning the failed channel and assigning an infinite (`u64::max_value()`) penalty to the channel so as to avoid retrying over the exact same path (if there's only one available path). This is common when trying to pay a mobile client behind an LSP if the mobile client is currently offline. This leads to the scorer being overly conservative in some cases - returning `u64::max_value()` when a given path hasn't been tried for a given payment may not be the best decision, even if that channel failed 50 minutes ago. By tracking channels which failed on a payment part level and explicitly refusing to route over them we can relax the requirements on the scorer, allowing it to make different decisions on how to treat channels that failed relatively recently without causing payments to retry the same path forever. This does have the drawback that it could allow two separate part of a payment to traverse the same path even though that path just failed, however this should only occur if the payment is going to fail anyway, at least as long as the scorer is properly learning. Closes lightningdevkit#1241, superseding lightningdevkit#1252.
1 parent 5cca9a0 commit 93e645d

File tree

3 files changed

+59
-9
lines changed

3 files changed

+59
-9
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3894,7 +3894,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
38943894
return;
38953895
}
38963896
mem::drop(channel_state_lock);
3897-
let retry = if let Some(payment_params_data) = payment_params {
3897+
let mut retry = if let Some(payment_params_data) = payment_params {
38983898
let path_last_hop = path.last().expect("Outbound payments must have had a valid path");
38993899
Some(RouteParameters {
39003900
payment_params: payment_params_data.clone(),
@@ -3930,6 +3930,9 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
39303930
// TODO: If we decided to blame ourselves (or one of our channels) in
39313931
// process_onion_failure we should close that channel as it implies our
39323932
// next-hop is needlessly blaming us!
3933+
if let Some(scid) = short_channel_id {
3934+
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
3935+
}
39333936
events::Event::PaymentPathFailed {
39343937
payment_id: Some(payment_id),
39353938
payment_hash: payment_hash.clone(),
@@ -3959,14 +3962,16 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
39593962
// ChannelDetails.
39603963
// TODO: For non-temporary failures, we really should be closing the
39613964
// channel here as we apparently can't relay through them anyway.
3965+
let scid = path.first().unwrap().short_channel_id;
3966+
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
39623967
events::Event::PaymentPathFailed {
39633968
payment_id: Some(payment_id),
39643969
payment_hash: payment_hash.clone(),
39653970
rejected_by_dest: path.len() == 1,
39663971
network_update: None,
39673972
all_paths_failed,
39683973
path: path.clone(),
3969-
short_channel_id: Some(path.first().unwrap().short_channel_id),
3974+
short_channel_id: Some(scid),
39703975
retry,
39713976
#[cfg(test)]
39723977
error_code: Some(*failure_code),

lightning/src/ln/functional_test_utils.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1492,7 +1492,7 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
14921492
let mut events = node.node.get_and_clear_pending_events();
14931493
assert_eq!(events.len(), 1);
14941494
let expected_payment_id = match events.pop().unwrap() {
1495-
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update,
1495+
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update, short_channel_id,
14961496
#[cfg(test)]
14971497
error_code,
14981498
#[cfg(test)]
@@ -1502,6 +1502,9 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
15021502
assert!(retry.is_some(), "expected retry.is_some()");
15031503
assert_eq!(retry.as_ref().unwrap().final_value_msat, path.last().unwrap().fee_msat, "Retry amount should match last hop in path");
15041504
assert_eq!(retry.as_ref().unwrap().payment_params.payee_pubkey, path.last().unwrap().pubkey, "Retry payee node_id should match last hop in path");
1505+
if let Some(scid) = short_channel_id {
1506+
assert!(retry.as_ref().unwrap().payment_params.previously_failed_channels.contains(&scid));
1507+
}
15051508

15061509
#[cfg(test)]
15071510
{

lightning/src/routing/router.rs

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ pub struct PaymentParameters {
240240
///
241241
/// Default value: 1
242242
pub max_channel_saturation_power_of_half: u8,
243+
244+
/// A list of SCIDs which this payment was previously attempted over and which caused the
245+
/// payment to fail. Future attempts for the same payment shouldn't be relayed through any of
246+
/// these SCIDs.
247+
pub previously_failed_channels: Vec<u64>,
243248
}
244249

245250
impl_writeable_tlv_based!(PaymentParameters, {
@@ -250,6 +255,7 @@ impl_writeable_tlv_based!(PaymentParameters, {
250255
(4, route_hints, vec_type),
251256
(5, max_channel_saturation_power_of_half, (default_value, 1)),
252257
(6, expiry_time, option),
258+
(7, previously_failed_channels, vec_type),
253259
});
254260

255261
impl PaymentParameters {
@@ -263,6 +269,7 @@ impl PaymentParameters {
263269
max_total_cltv_expiry_delta: DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA,
264270
max_path_count: DEFAULT_MAX_PATH_COUNT,
265271
max_channel_saturation_power_of_half: 1,
272+
previously_failed_channels: Vec::new(),
266273
}
267274
}
268275

@@ -1002,7 +1009,7 @@ where L::Target: Logger {
10021009
let contributes_sufficient_value = available_value_contribution_msat >= minimal_value_contribution_msat;
10031010
// Do not consider candidate hops that would exceed the maximum path length.
10041011
let path_length_to_node = $next_hops_path_length + 1;
1005-
let doesnt_exceed_max_path_length = path_length_to_node <= MAX_PATH_LENGTH_ESTIMATE;
1012+
let exceeds_max_path_length = path_length_to_node > MAX_PATH_LENGTH_ESTIMATE;
10061013

10071014
// Do not consider candidates that exceed the maximum total cltv expiry limit.
10081015
// In order to already account for some of the privacy enhancing random CLTV
@@ -1013,7 +1020,7 @@ where L::Target: Logger {
10131020
.unwrap_or(payment_params.max_total_cltv_expiry_delta - final_cltv_expiry_delta);
10141021
let hop_total_cltv_delta = ($next_hops_cltv_delta as u32)
10151022
.saturating_add($candidate.cltv_expiry_delta());
1016-
let doesnt_exceed_cltv_delta_limit = hop_total_cltv_delta <= max_total_cltv_expiry_delta;
1023+
let exceeds_cltv_delta_limit = hop_total_cltv_delta > max_total_cltv_expiry_delta;
10171024

10181025
let value_contribution_msat = cmp::min(available_value_contribution_msat, $next_hops_value_contribution);
10191026
// Includes paying fees for the use of the following channels.
@@ -1033,15 +1040,19 @@ where L::Target: Logger {
10331040
(amount_to_transfer_over_msat < $next_hops_path_htlc_minimum_msat &&
10341041
recommended_value_msat > $next_hops_path_htlc_minimum_msat));
10351042

1043+
let payment_failed_on_this_channel =
1044+
payment_params.previously_failed_channels.contains(&short_channel_id);
1045+
10361046
// If HTLC minimum is larger than the amount we're going to transfer, we shouldn't
10371047
// bother considering this channel. If retrying with recommended_value_msat may
10381048
// allow us to hit the HTLC minimum limit, set htlc_minimum_limit so that we go
10391049
// around again with a higher amount.
1040-
if contributes_sufficient_value && doesnt_exceed_max_path_length &&
1041-
doesnt_exceed_cltv_delta_limit && may_overpay_to_meet_path_minimum_msat {
1050+
if !contributes_sufficient_value || exceeds_max_path_length ||
1051+
exceeds_cltv_delta_limit || payment_failed_on_this_channel {
1052+
// Path isn't useful, ignore it and move on.
1053+
} else if may_overpay_to_meet_path_minimum_msat {
10421054
hit_minimum_limit = true;
1043-
} else if contributes_sufficient_value && doesnt_exceed_max_path_length &&
1044-
doesnt_exceed_cltv_delta_limit && over_path_minimum_msat {
1055+
} else if over_path_minimum_msat {
10451056
// Note that low contribution here (limited by available_liquidity_msat)
10461057
// might violate htlc_minimum_msat on the hops which are next along the
10471058
// payment path (upstream to the payee). To avoid that, we recompute
@@ -1993,6 +2004,8 @@ mod tests {
19932004
use prelude::*;
19942005
use sync::{self, Arc};
19952006

2007+
use core::convert::TryInto;
2008+
19962009
fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey,
19972010
features: InitFeatures, outbound_capacity_msat: u64) -> channelmanager::ChannelDetails {
19982011
channelmanager::ChannelDetails {
@@ -5573,6 +5586,35 @@ mod tests {
55735586
}
55745587
}
55755588

5589+
#[test]
5590+
fn avoids_recently_failed_paths() {
5591+
// Ensure that the router always avoids all of the `previously_failed_channels` channels by
5592+
// randomly inserting channels into it until we can't find a route anymore.
5593+
let (secp_ctx, network, _, _, logger) = build_graph();
5594+
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
5595+
let network_graph = network.read_only();
5596+
5597+
let scorer = test_utils::TestScorer::with_penalty(0);
5598+
let mut payment_params = PaymentParameters::from_node_id(nodes[6]).with_route_hints(last_hops(&nodes))
5599+
.with_max_path_count(1);
5600+
let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
5601+
let random_seed_bytes = keys_manager.get_secure_random_bytes();
5602+
5603+
// We should be able to find a route initially, and then after we fail a few random
5604+
// channels eventually we won't be able to any longer.
5605+
assert!(get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes).is_ok());
5606+
loop {
5607+
if let Ok(route) = get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes) {
5608+
for chan in route.paths[0].iter() {
5609+
assert!(!payment_params.previously_failed_channels.contains(&chan.short_channel_id));
5610+
}
5611+
let victim = (u64::from_ne_bytes(random_seed_bytes[0..8].try_into().unwrap()) as usize)
5612+
% route.paths[0].len();
5613+
payment_params.previously_failed_channels.push(route.paths[0][victim].short_channel_id);
5614+
} else { break; }
5615+
}
5616+
}
5617+
55765618
#[test]
55775619
fn limits_path_length() {
55785620
let (secp_ctx, network, _, _, logger) = build_line_graph();

0 commit comments

Comments
 (0)