Skip to content

Commit 33eaf98

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 4e5f74a commit 33eaf98

File tree

3 files changed

+59
-9
lines changed

3 files changed

+59
-9
lines changed

lightning/src/ln/channelmanager.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -3867,7 +3867,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
38673867
return;
38683868
}
38693869
mem::drop(channel_state_lock);
3870-
let retry = if let Some(payment_params_data) = payment_params {
3870+
let mut retry = if let Some(payment_params_data) = payment_params {
38713871
let path_last_hop = path.last().expect("Outbound payments must have had a valid path");
38723872
Some(RouteParameters {
38733873
payment_params: payment_params_data.clone(),
@@ -3903,6 +3903,9 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
39033903
// TODO: If we decided to blame ourselves (or one of our channels) in
39043904
// process_onion_failure we should close that channel as it implies our
39053905
// next-hop is needlessly blaming us!
3906+
if let Some(scid) = short_channel_id {
3907+
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
3908+
}
39063909
events::Event::PaymentPathFailed {
39073910
payment_id: Some(payment_id),
39083911
payment_hash: payment_hash.clone(),
@@ -3932,14 +3935,16 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
39323935
// ChannelDetails.
39333936
// TODO: For non-temporary failures, we really should be closing the
39343937
// channel here as we apparently can't relay through them anyway.
3938+
let scid = path.first().unwrap().short_channel_id;
3939+
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
39353940
events::Event::PaymentPathFailed {
39363941
payment_id: Some(payment_id),
39373942
payment_hash: payment_hash.clone(),
39383943
rejected_by_dest: path.len() == 1,
39393944
network_update: None,
39403945
all_paths_failed,
39413946
path: path.clone(),
3942-
short_channel_id: Some(path.first().unwrap().short_channel_id),
3947+
short_channel_id: Some(scid),
39433948
retry,
39443949
#[cfg(test)]
39453950
error_code: Some(*failure_code),

lightning/src/ln/functional_test_utils.rs

+4-1
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

+48-6
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,11 @@ pub struct PaymentParameters {
225225
/// The maximum number of paths that may be used by (MPP) payments.
226226
/// Defaults to [`DEFAULT_MAX_PATH_COUNT`].
227227
pub max_path_count: u8,
228+
229+
/// A list of SCIDs which this payment was previously attempted over and which caused the
230+
/// payment to fail. Future attempts for the same payment shouldn't be relayed through any of
231+
/// these SCIDs.
232+
pub previously_failed_channels: Vec<u64>,
228233
}
229234

230235
impl_writeable_tlv_based!(PaymentParameters, {
@@ -233,6 +238,7 @@ impl_writeable_tlv_based!(PaymentParameters, {
233238
(2, features, option),
234239
(3, max_path_count, (default_value, DEFAULT_MAX_PATH_COUNT)),
235240
(4, route_hints, vec_type),
241+
(5, previously_failed_channels, vec_type),
236242
(6, expiry_time, option),
237243
});
238244

@@ -246,6 +252,7 @@ impl PaymentParameters {
246252
expiry_time: None,
247253
max_total_cltv_expiry_delta: DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA,
248254
max_path_count: DEFAULT_MAX_PATH_COUNT,
255+
previously_failed_channels: Vec::new(),
249256
}
250257
}
251258

@@ -956,7 +963,7 @@ where L::Target: Logger {
956963
let contributes_sufficient_value = available_value_contribution_msat >= minimal_value_contribution_msat;
957964
// Do not consider candidate hops that would exceed the maximum path length.
958965
let path_length_to_node = $next_hops_path_length + 1;
959-
let doesnt_exceed_max_path_length = path_length_to_node <= MAX_PATH_LENGTH_ESTIMATE;
966+
let exceeds_max_path_length = path_length_to_node > MAX_PATH_LENGTH_ESTIMATE;
960967

961968
// Do not consider candidates that exceed the maximum total cltv expiry limit.
962969
// In order to already account for some of the privacy enhancing random CLTV
@@ -967,7 +974,7 @@ where L::Target: Logger {
967974
.unwrap_or(payment_params.max_total_cltv_expiry_delta - final_cltv_expiry_delta);
968975
let hop_total_cltv_delta = ($next_hops_cltv_delta as u32)
969976
.saturating_add($candidate.cltv_expiry_delta());
970-
let doesnt_exceed_cltv_delta_limit = hop_total_cltv_delta <= max_total_cltv_expiry_delta;
977+
let exceeds_cltv_delta_limit = hop_total_cltv_delta > max_total_cltv_expiry_delta;
971978

972979
let value_contribution_msat = cmp::min(available_value_contribution_msat, $next_hops_value_contribution);
973980
// Includes paying fees for the use of the following channels.
@@ -987,15 +994,19 @@ where L::Target: Logger {
987994
(amount_to_transfer_over_msat < $next_hops_path_htlc_minimum_msat &&
988995
recommended_value_msat > $next_hops_path_htlc_minimum_msat));
989996

997+
let payment_failed_on_this_channel =
998+
payment_params.previously_failed_channels.contains(&short_channel_id);
999+
9901000
// If HTLC minimum is larger than the amount we're going to transfer, we shouldn't
9911001
// bother considering this channel. If retrying with recommended_value_msat may
9921002
// allow us to hit the HTLC minimum limit, set htlc_minimum_limit so that we go
9931003
// around again with a higher amount.
994-
if contributes_sufficient_value && doesnt_exceed_max_path_length &&
995-
doesnt_exceed_cltv_delta_limit && may_overpay_to_meet_path_minimum_msat {
1004+
if !contributes_sufficient_value || exceeds_max_path_length ||
1005+
exceeds_cltv_delta_limit || payment_failed_on_this_channel {
1006+
// Path isn't useful, ignore it and move on.
1007+
} else if may_overpay_to_meet_path_minimum_msat {
9961008
hit_minimum_limit = true;
997-
} else if contributes_sufficient_value && doesnt_exceed_max_path_length &&
998-
doesnt_exceed_cltv_delta_limit && over_path_minimum_msat {
1009+
} else if over_path_minimum_msat {
9991010
// Note that low contribution here (limited by available_liquidity_msat)
10001011
// might violate htlc_minimum_msat on the hops which are next along the
10011012
// payment path (upstream to the payee). To avoid that, we recompute
@@ -1915,6 +1926,8 @@ mod tests {
19151926
use prelude::*;
19161927
use sync::{self, Arc};
19171928

1929+
use core::convert::TryInto;
1930+
19181931
fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey,
19191932
features: InitFeatures, outbound_capacity_msat: u64) -> channelmanager::ChannelDetails {
19201933
channelmanager::ChannelDetails {
@@ -5492,6 +5505,35 @@ mod tests {
54925505
}
54935506
}
54945507

5508+
#[test]
5509+
fn avoids_recently_failed_paths() {
5510+
// Ensure that the router always avoids all of the `previously_failed_channels` channels by
5511+
// randomly inserting channels into it until we can't find a route anymore.
5512+
let (secp_ctx, network, _, _, logger) = build_graph();
5513+
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
5514+
let network_graph = network.read_only();
5515+
5516+
let scorer = test_utils::TestScorer::with_penalty(0);
5517+
let mut payment_params = PaymentParameters::from_node_id(nodes[6]).with_route_hints(last_hops(&nodes))
5518+
.with_max_path_count(1);
5519+
let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
5520+
let random_seed_bytes = keys_manager.get_secure_random_bytes();
5521+
5522+
// We should be able to find a route initially, and then after we fail a few random
5523+
// channels eventually we won't be able to any longer.
5524+
assert!(get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes).is_ok());
5525+
loop {
5526+
if let Ok(route) = get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes) {
5527+
for chan in route.paths[0].iter() {
5528+
assert!(!payment_params.previously_failed_channels.contains(&chan.short_channel_id));
5529+
}
5530+
let victim = (u64::from_ne_bytes(random_seed_bytes[0..8].try_into().unwrap()) as usize)
5531+
% route.paths[0].len();
5532+
payment_params.previously_failed_channels.push(route.paths[0][victim].short_channel_id);
5533+
} else { break; }
5534+
}
5535+
}
5536+
54955537
#[test]
54965538
fn limits_path_length() {
54975539
let (secp_ctx, network, _, _, logger) = build_line_graph();

0 commit comments

Comments
 (0)