Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lightning/src/ln/channelmanager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3894,7 +3894,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
return;
}
mem::drop(channel_state_lock);
let retry = if let Some(payment_params_data) = payment_params {
let mut retry = if let Some(payment_params_data) = payment_params {
let path_last_hop = path.last().expect("Outbound payments must have had a valid path");
Some(RouteParameters {
payment_params: payment_params_data.clone(),
Expand DownExpand Up@@ -3930,6 +3930,9 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// TODO: If we decided to blame ourselves (or one of our channels) in
// process_onion_failure we should close that channel as it implies our
// next-hop is needlessly blaming us!
if let Some(scid) = short_channel_id {
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
}
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
Expand DownExpand Up@@ -3959,14 +3962,16 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// ChannelDetails.
// TODO: For non-temporary failures, we really should be closing the
// channel here as we apparently can't relay through them anyway.
let scid = path.first().unwrap().short_channel_id;
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
rejected_by_dest: path.len() == 1,
network_update: None,
all_paths_failed,
path: path.clone(),
short_channel_id: Some(path.first().unwrap().short_channel_id),
short_channel_id: Some(scid),
retry,
#[cfg(test)]
error_code: Some(*failure_code),
Expand Down
5 changes: 4 additions & 1 deletion lightning/src/ln/functional_test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1492,7 +1492,7 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
let mut events = node.node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
let expected_payment_id = match events.pop().unwrap() {
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update,
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update, short_channel_id,
#[cfg(test)]
error_code,
#[cfg(test)]
Expand All@@ -1502,6 +1502,9 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
assert!(retry.is_some(), "expected retry.is_some()");
assert_eq!(retry.as_ref().unwrap().final_value_msat, path.last().unwrap().fee_msat, "Retry amount should match last hop in path");
assert_eq!(retry.as_ref().unwrap().payment_params.payee_pubkey, path.last().unwrap().pubkey, "Retry payee node_id should match last hop in path");
if let Some(scid) = short_channel_id {
assert!(retry.as_ref().unwrap().payment_params.previously_failed_channels.contains(&scid));
}

#[cfg(test)]
{
Expand Down
54 changes: 48 additions & 6 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,6 +240,11 @@ pub struct PaymentParameters {
///
/// Default value: 1
pub max_channel_saturation_power_of_half: u8,

/// A list of SCIDs which this payment was previously attempted over and which caused the
/// payment to fail. Future attempts for the same payment shouldn't be relayed through any of
/// these SCIDs.
pub previously_failed_channels: Vec<u64>,
}

impl_writeable_tlv_based!(PaymentParameters, {
Expand All@@ -250,6 +255,7 @@ impl_writeable_tlv_based!(PaymentParameters, {
(4, route_hints, vec_type),
(5, max_channel_saturation_power_of_half, (default_value, 1)),
(6, expiry_time, option),
(7, previously_failed_channels, vec_type),
});

impl PaymentParameters {
Expand All@@ -263,6 +269,7 @@ impl PaymentParameters {
max_total_cltv_expiry_delta: DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA,
max_path_count: DEFAULT_MAX_PATH_COUNT,
max_channel_saturation_power_of_half: 1,
previously_failed_channels: Vec::new(),
}
}

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

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

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

let payment_failed_on_this_channel =
payment_params.previously_failed_channels.contains(&short_channel_id);

// If HTLC minimum is larger than the amount we're going to transfer, we shouldn't
// bother considering this channel. If retrying with recommended_value_msat may
// allow us to hit the HTLC minimum limit, set htlc_minimum_limit so that we go
// around again with a higher amount.
if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && may_overpay_to_meet_path_minimum_msat {
if !contributes_sufficient_value || exceeds_max_path_length ||
exceeds_cltv_delta_limit || payment_failed_on_this_channel {
// Path isn't useful, ignore it and move on.
} else if may_overpay_to_meet_path_minimum_msat {
hit_minimum_limit = true;
} else if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && over_path_minimum_msat {
} else if over_path_minimum_msat {
// Note that low contribution here (limited by available_liquidity_msat)
// might violate htlc_minimum_msat on the hops which are next along the
// payment path (upstream to the payee). To avoid that, we recompute
Expand DownExpand Up@@ -1993,6 +2004,8 @@ mod tests {
use prelude::*;
use sync::{self, Arc};

use core::convert::TryInto;

fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey,
features: InitFeatures, outbound_capacity_msat: u64) -> channelmanager::ChannelDetails {
channelmanager::ChannelDetails {
Expand DownExpand Up@@ -5573,6 +5586,35 @@ mod tests {
}
}

#[test]
fn avoids_recently_failed_paths() {
// Ensure that the router always avoids all of the `previously_failed_channels` channels by
// randomly inserting channels into it until we can't find a route anymore.
let (secp_ctx, network, _, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
let network_graph = network.read_only();

let scorer = test_utils::TestScorer::with_penalty(0);
let mut payment_params = PaymentParameters::from_node_id(nodes[6]).with_route_hints(last_hops(&nodes))
.with_max_path_count(1);
let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
let random_seed_bytes = keys_manager.get_secure_random_bytes();

// We should be able to find a route initially, and then after we fail a few random
// channels eventually we won't be able to any longer.
assert!(get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes).is_ok());
loop {
if let Ok(route) = get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes) {
Comment thread
TheBlueMatt marked this conversation as resolved.
for chan in route.paths[0].iter() {
assert!(!payment_params.previously_failed_channels.contains(&chan.short_channel_id));
}
let victim = (u64::from_ne_bytes(random_seed_bytes[0..8].try_into().unwrap()) as usize)
Comment thread
tnull marked this conversation as resolved.
% route.paths[0].len();
payment_params.previously_failed_channels.push(route.paths[0][victim].short_channel_id);
} else { break; }
}
}

#[test]
fn limits_path_length() {
let (secp_ctx, network, _, _, logger) = build_line_graph();
Expand Down
60 changes: 42 additions & 18 deletions lightning/src/routing/scoring.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,6 +394,25 @@ pub struct ProbabilisticScoringParameters {
///
/// Default value: 250 msat
pub anti_probing_penalty_msat: u64,

/// This penalty is applied when the amount we're attempting to send over a channel exceeds our
/// current estimate of the channel's available liquidity.
///
/// Note that in this case all other penalties, including the
/// [`liquidity_penalty_multiplier_msat`] and [`amount_penalty_multiplier_msat`]-based
/// penalties, as well as the [`base_penalty_msat`] and the [`anti_probing_penalty_msat`], if
/// applicable, are still included in the overall penalty.
///
/// If you wish to avoid creating paths with such channels entirely, setting this to a value of
/// `u64::max_value()` will guarantee that.
///
/// Default value: 1_0000_0000_000 msat (1 Bitcoin)
Comment thread
tnull marked this conversation as resolved.
///
/// [`liquidity_penalty_multiplier_msat`]: Self::liquidity_penalty_multiplier_msat
/// [`amount_penalty_multiplier_msat`]: Self::amount_penalty_multiplier_msat
/// [`base_penalty_msat`]: Self::base_penalty_msat
/// [`anti_probing_penalty_msat`]: Self::anti_probing_penalty_msat
pub considered_impossible_penalty_msat: u64,
}

/// Accounting for channel liquidity balance uncertainty.
Expand DownExpand Up@@ -522,6 +541,7 @@ impl ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 0,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 0,
considered_impossible_penalty_msat: 0,
}
}

Expand All@@ -543,6 +563,7 @@ impl Default for ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 256,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 250,
considered_impossible_penalty_msat: 1_0000_0000_000,
Comment thread
tnull marked this conversation as resolved.
}
}
}
Expand DownExpand Up@@ -620,17 +641,12 @@ impl<L: Deref<Target = u64>, T: Time, U: Deref<Target = T>> DirectedChannelLiqui
if amount_msat <= min_liquidity_msat {
0
} else if amount_msat >= max_liquidity_msat {
if amount_msat > max_liquidity_msat {
u64::max_value()
} else if max_liquidity_msat != self.capacity_msat {
// Avoid using the failed channel on retry.
u64::max_value()
} else {
// Equivalent to hitting the else clause below with the amount equal to the
// effective capacity and without any certainty on the liquidity upper bound.
let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
}
// Equivalent to hitting the else clause below with the amount equal to the effective
// capacity and without any certainty on the liquidity upper bound, plus the
// impossibility penalty.
Comment on lines +644 to +646

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment needs to be updated now since it's not only when equal to the effective capacity. Seems like the earlier change that added this now removed logic is what caused calculates_log10_without_overflowing_u64_max_value to not exercise the correct code path as mentioned earlier on that test (i.e., it doesn't hit the negative_log10_times_2048 case below.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the comment differently - I read the comment to say "the calculation we're doing here is equivalent to..." which is still true, no?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, you're right!

let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
.saturating_add(params.considered_impossible_penalty_msat)
Comment on lines +647 to +649

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to do the max of these two rather than adding? Is the idea that we want this to be >= anything given in the else clause?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that's the idea. I suppose we could do a max. Originally I had it not adding the combined_penalty call at all but that seemed to brittle to deal with so switched to adding. I don't honestly have a strong opinion between adding and max, either way the docs can tell users what's going on, but for max i kinda worry users will acidentally set it too low and get no penalty here, which seems strange?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They would also need to set the other params lower, though, since max would select the combined penalty over the considered_impossible_penalty_msat if the latter were too low. So it's all kinda relative. Don't feel too strongly either though note that max may make debugging a little easier as otherwise you may need to mentally subtract some combined penalty if it is not obvious.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong? I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong?

Plus base and amount penalty, FWIW.

I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Sure we can leave it. Though one thing I just realized is that either way now the penalty will be variable across channels depending on the amount. Maybe less so when using max but maybe that's an argument in favor of adding. That way if left to choose only channels exceeding the maximum liquidity, we'd prefer ones that would otherwise be penalized less.

} else {
let numerator = (max_liquidity_msat - amount_msat).saturating_add(1);
let denominator = (max_liquidity_msat - min_liquidity_msat).saturating_add(1);
Expand DownExpand Up@@ -1624,7 +1640,7 @@ mod tests {
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 102_400, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 47);
let usage = ChannelUsage { amount_msat: 1_024_000, ..usage };
let usage = ChannelUsage { amount_msat: 1_023_999, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

let usage = ChannelUsage {
Expand DownExpand Up@@ -1654,6 +1670,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger)
Expand DownExpand Up@@ -1745,6 +1762,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand DownExpand Up@@ -1811,6 +1829,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand All@@ -1820,10 +1839,10 @@ mod tests {
let usage = ChannelUsage {
amount_msat: 0,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_000) },
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_024) },
};
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

scorer.payment_path_failed(&payment_path_for_amount(768).iter().collect::<Vec<_>>(), 42);
Expand DownExpand Up@@ -1867,20 +1886,20 @@ mod tests {
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

// Fully decay liquidity upper bound.
SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());
}

#[test]
Expand DownExpand Up@@ -1965,6 +1984,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2001,6 +2021,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2171,7 +2192,10 @@ mod tests {
fn accounts_for_inflight_htlc_usage() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters::default();
let params = ProbabilisticScoringParameters {
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
let source = source_node_id();
let target = target_node_id();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lightning/src/ln/channelmanager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3894,7 +3894,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
return;
}
mem::drop(channel_state_lock);
let retry = if let Some(payment_params_data) = payment_params {
let mut retry = if let Some(payment_params_data) = payment_params {
let path_last_hop = path.last().expect("Outbound payments must have had a valid path");
Some(RouteParameters {
payment_params: payment_params_data.clone(),
Expand DownExpand Up@@ -3930,6 +3930,9 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// TODO: If we decided to blame ourselves (or one of our channels) in
// process_onion_failure we should close that channel as it implies our
// next-hop is needlessly blaming us!
if let Some(scid) = short_channel_id {
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
}
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
Expand DownExpand Up@@ -3959,14 +3962,16 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// ChannelDetails.
// TODO: For non-temporary failures, we really should be closing the
// channel here as we apparently can't relay through them anyway.
let scid = path.first().unwrap().short_channel_id;
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
rejected_by_dest: path.len() == 1,
network_update: None,
all_paths_failed,
path: path.clone(),
short_channel_id: Some(path.first().unwrap().short_channel_id),
short_channel_id: Some(scid),
retry,
#[cfg(test)]
error_code: Some(*failure_code),
Expand Down
5 changes: 4 additions & 1 deletion lightning/src/ln/functional_test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1492,7 +1492,7 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
let mut events = node.node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
let expected_payment_id = match events.pop().unwrap() {
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update,
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update, short_channel_id,
#[cfg(test)]
error_code,
#[cfg(test)]
Expand All@@ -1502,6 +1502,9 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
assert!(retry.is_some(), "expected retry.is_some()");
assert_eq!(retry.as_ref().unwrap().final_value_msat, path.last().unwrap().fee_msat, "Retry amount should match last hop in path");
assert_eq!(retry.as_ref().unwrap().payment_params.payee_pubkey, path.last().unwrap().pubkey, "Retry payee node_id should match last hop in path");
if let Some(scid) = short_channel_id {
assert!(retry.as_ref().unwrap().payment_params.previously_failed_channels.contains(&scid));
}

#[cfg(test)]
{
Expand Down
54 changes: 48 additions & 6 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,6 +240,11 @@ pub struct PaymentParameters {
///
/// Default value: 1
pub max_channel_saturation_power_of_half: u8,

/// A list of SCIDs which this payment was previously attempted over and which caused the
/// payment to fail. Future attempts for the same payment shouldn't be relayed through any of
/// these SCIDs.
pub previously_failed_channels: Vec<u64>,
}

impl_writeable_tlv_based!(PaymentParameters, {
Expand All@@ -250,6 +255,7 @@ impl_writeable_tlv_based!(PaymentParameters, {
(4, route_hints, vec_type),
(5, max_channel_saturation_power_of_half, (default_value, 1)),
(6, expiry_time, option),
(7, previously_failed_channels, vec_type),
});

impl PaymentParameters {
Expand All@@ -263,6 +269,7 @@ impl PaymentParameters {
max_total_cltv_expiry_delta: DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA,
max_path_count: DEFAULT_MAX_PATH_COUNT,
max_channel_saturation_power_of_half: 1,
previously_failed_channels: Vec::new(),
}
}

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

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

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

let payment_failed_on_this_channel =
payment_params.previously_failed_channels.contains(&short_channel_id);

// If HTLC minimum is larger than the amount we're going to transfer, we shouldn't
// bother considering this channel. If retrying with recommended_value_msat may
// allow us to hit the HTLC minimum limit, set htlc_minimum_limit so that we go
// around again with a higher amount.
if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && may_overpay_to_meet_path_minimum_msat {
if !contributes_sufficient_value || exceeds_max_path_length ||
exceeds_cltv_delta_limit || payment_failed_on_this_channel {
// Path isn't useful, ignore it and move on.
} else if may_overpay_to_meet_path_minimum_msat {
hit_minimum_limit = true;
} else if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && over_path_minimum_msat {
} else if over_path_minimum_msat {
// Note that low contribution here (limited by available_liquidity_msat)
// might violate htlc_minimum_msat on the hops which are next along the
// payment path (upstream to the payee). To avoid that, we recompute
Expand DownExpand Up@@ -1993,6 +2004,8 @@ mod tests {
use prelude::*;
use sync::{self, Arc};

use core::convert::TryInto;

fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey,
features: InitFeatures, outbound_capacity_msat: u64) -> channelmanager::ChannelDetails {
channelmanager::ChannelDetails {
Expand DownExpand Up@@ -5573,6 +5586,35 @@ mod tests {
}
}

#[test]
fn avoids_recently_failed_paths() {
// Ensure that the router always avoids all of the `previously_failed_channels` channels by
// randomly inserting channels into it until we can't find a route anymore.
let (secp_ctx, network, _, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
let network_graph = network.read_only();

let scorer = test_utils::TestScorer::with_penalty(0);
let mut payment_params = PaymentParameters::from_node_id(nodes[6]).with_route_hints(last_hops(&nodes))
.with_max_path_count(1);
let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
let random_seed_bytes = keys_manager.get_secure_random_bytes();

// We should be able to find a route initially, and then after we fail a few random
// channels eventually we won't be able to any longer.
assert!(get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes).is_ok());
loop {
if let Ok(route) = get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes) {
Comment thread
TheBlueMatt marked this conversation as resolved.
for chan in route.paths[0].iter() {
assert!(!payment_params.previously_failed_channels.contains(&chan.short_channel_id));
}
let victim = (u64::from_ne_bytes(random_seed_bytes[0..8].try_into().unwrap()) as usize)
Comment thread
tnull marked this conversation as resolved.
% route.paths[0].len();
payment_params.previously_failed_channels.push(route.paths[0][victim].short_channel_id);
} else { break; }
}
}

#[test]
fn limits_path_length() {
let (secp_ctx, network, _, _, logger) = build_line_graph();
Expand Down
60 changes: 42 additions & 18 deletions lightning/src/routing/scoring.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,6 +394,25 @@ pub struct ProbabilisticScoringParameters {
///
/// Default value: 250 msat
pub anti_probing_penalty_msat: u64,

/// This penalty is applied when the amount we're attempting to send over a channel exceeds our
/// current estimate of the channel's available liquidity.
///
/// Note that in this case all other penalties, including the
/// [`liquidity_penalty_multiplier_msat`] and [`amount_penalty_multiplier_msat`]-based
/// penalties, as well as the [`base_penalty_msat`] and the [`anti_probing_penalty_msat`], if
/// applicable, are still included in the overall penalty.
///
/// If you wish to avoid creating paths with such channels entirely, setting this to a value of
/// `u64::max_value()` will guarantee that.
///
/// Default value: 1_0000_0000_000 msat (1 Bitcoin)
Comment thread
tnull marked this conversation as resolved.
///
/// [`liquidity_penalty_multiplier_msat`]: Self::liquidity_penalty_multiplier_msat
/// [`amount_penalty_multiplier_msat`]: Self::amount_penalty_multiplier_msat
/// [`base_penalty_msat`]: Self::base_penalty_msat
/// [`anti_probing_penalty_msat`]: Self::anti_probing_penalty_msat
pub considered_impossible_penalty_msat: u64,
}

/// Accounting for channel liquidity balance uncertainty.
Expand DownExpand Up@@ -522,6 +541,7 @@ impl ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 0,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 0,
considered_impossible_penalty_msat: 0,
}
}

Expand All@@ -543,6 +563,7 @@ impl Default for ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 256,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 250,
considered_impossible_penalty_msat: 1_0000_0000_000,
Comment thread
tnull marked this conversation as resolved.
}
}
}
Expand DownExpand Up@@ -620,17 +641,12 @@ impl<L: Deref<Target = u64>, T: Time, U: Deref<Target = T>> DirectedChannelLiqui
if amount_msat <= min_liquidity_msat {
0
} else if amount_msat >= max_liquidity_msat {
if amount_msat > max_liquidity_msat {
u64::max_value()
} else if max_liquidity_msat != self.capacity_msat {
// Avoid using the failed channel on retry.
u64::max_value()
} else {
// Equivalent to hitting the else clause below with the amount equal to the
// effective capacity and without any certainty on the liquidity upper bound.
let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
}
// Equivalent to hitting the else clause below with the amount equal to the effective
// capacity and without any certainty on the liquidity upper bound, plus the
// impossibility penalty.
Comment on lines +644 to +646

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment needs to be updated now since it's not only when equal to the effective capacity. Seems like the earlier change that added this now removed logic is what caused calculates_log10_without_overflowing_u64_max_value to not exercise the correct code path as mentioned earlier on that test (i.e., it doesn't hit the negative_log10_times_2048 case below.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the comment differently - I read the comment to say "the calculation we're doing here is equivalent to..." which is still true, no?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, you're right!

let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
.saturating_add(params.considered_impossible_penalty_msat)
Comment on lines +647 to +649

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to do the max of these two rather than adding? Is the idea that we want this to be >= anything given in the else clause?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that's the idea. I suppose we could do a max. Originally I had it not adding the combined_penalty call at all but that seemed to brittle to deal with so switched to adding. I don't honestly have a strong opinion between adding and max, either way the docs can tell users what's going on, but for max i kinda worry users will acidentally set it too low and get no penalty here, which seems strange?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They would also need to set the other params lower, though, since max would select the combined penalty over the considered_impossible_penalty_msat if the latter were too low. So it's all kinda relative. Don't feel too strongly either though note that max may make debugging a little easier as otherwise you may need to mentally subtract some combined penalty if it is not obvious.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong? I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong?

Plus base and amount penalty, FWIW.

I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Sure we can leave it. Though one thing I just realized is that either way now the penalty will be variable across channels depending on the amount. Maybe less so when using max but maybe that's an argument in favor of adding. That way if left to choose only channels exceeding the maximum liquidity, we'd prefer ones that would otherwise be penalized less.

} else {
let numerator = (max_liquidity_msat - amount_msat).saturating_add(1);
let denominator = (max_liquidity_msat - min_liquidity_msat).saturating_add(1);
Expand DownExpand Up@@ -1624,7 +1640,7 @@ mod tests {
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 102_400, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 47);
let usage = ChannelUsage { amount_msat: 1_024_000, ..usage };
let usage = ChannelUsage { amount_msat: 1_023_999, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

let usage = ChannelUsage {
Expand DownExpand Up@@ -1654,6 +1670,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger)
Expand DownExpand Up@@ -1745,6 +1762,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand DownExpand Up@@ -1811,6 +1829,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand All@@ -1820,10 +1839,10 @@ mod tests {
let usage = ChannelUsage {
amount_msat: 0,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_000) },
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_024) },
};
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

scorer.payment_path_failed(&payment_path_for_amount(768).iter().collect::<Vec<_>>(), 42);
Expand DownExpand Up@@ -1867,20 +1886,20 @@ mod tests {
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

// Fully decay liquidity upper bound.
SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());
}

#[test]
Expand DownExpand Up@@ -1965,6 +1984,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2001,6 +2021,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2171,7 +2192,10 @@ mod tests {
fn accounts_for_inflight_htlc_usage() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters::default();
let params = ProbabilisticScoringParameters {
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
let source = source_node_id();
let target = target_node_id();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lightning/src/ln/channelmanager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3894,7 +3894,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
return;
}
mem::drop(channel_state_lock);
let retry = if let Some(payment_params_data) = payment_params {
let mut retry = if let Some(payment_params_data) = payment_params {
let path_last_hop = path.last().expect("Outbound payments must have had a valid path");
Some(RouteParameters {
payment_params: payment_params_data.clone(),
Expand DownExpand Up@@ -3930,6 +3930,9 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// TODO: If we decided to blame ourselves (or one of our channels) in
// process_onion_failure we should close that channel as it implies our
// next-hop is needlessly blaming us!
if let Some(scid) = short_channel_id {
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
}
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
Expand DownExpand Up@@ -3959,14 +3962,16 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// ChannelDetails.
// TODO: For non-temporary failures, we really should be closing the
// channel here as we apparently can't relay through them anyway.
let scid = path.first().unwrap().short_channel_id;
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
rejected_by_dest: path.len() == 1,
network_update: None,
all_paths_failed,
path: path.clone(),
short_channel_id: Some(path.first().unwrap().short_channel_id),
short_channel_id: Some(scid),
retry,
#[cfg(test)]
error_code: Some(*failure_code),
Expand Down
5 changes: 4 additions & 1 deletion lightning/src/ln/functional_test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1492,7 +1492,7 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
let mut events = node.node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
let expected_payment_id = match events.pop().unwrap() {
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update,
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update, short_channel_id,
#[cfg(test)]
error_code,
#[cfg(test)]
Expand All@@ -1502,6 +1502,9 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
assert!(retry.is_some(), "expected retry.is_some()");
assert_eq!(retry.as_ref().unwrap().final_value_msat, path.last().unwrap().fee_msat, "Retry amount should match last hop in path");
assert_eq!(retry.as_ref().unwrap().payment_params.payee_pubkey, path.last().unwrap().pubkey, "Retry payee node_id should match last hop in path");
if let Some(scid) = short_channel_id {
assert!(retry.as_ref().unwrap().payment_params.previously_failed_channels.contains(&scid));
}

#[cfg(test)]
{
Expand Down
54 changes: 48 additions & 6 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,6 +240,11 @@ pub struct PaymentParameters {
///
/// Default value: 1
pub max_channel_saturation_power_of_half: u8,

/// A list of SCIDs which this payment was previously attempted over and which caused the
/// payment to fail. Future attempts for the same payment shouldn't be relayed through any of
/// these SCIDs.
pub previously_failed_channels: Vec<u64>,
}

impl_writeable_tlv_based!(PaymentParameters, {
Expand All@@ -250,6 +255,7 @@ impl_writeable_tlv_based!(PaymentParameters, {
(4, route_hints, vec_type),
(5, max_channel_saturation_power_of_half, (default_value, 1)),
(6, expiry_time, option),
(7, previously_failed_channels, vec_type),
});

impl PaymentParameters {
Expand All@@ -263,6 +269,7 @@ impl PaymentParameters {
max_total_cltv_expiry_delta: DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA,
max_path_count: DEFAULT_MAX_PATH_COUNT,
max_channel_saturation_power_of_half: 1,
previously_failed_channels: Vec::new(),
}
}

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

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

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

let payment_failed_on_this_channel =
payment_params.previously_failed_channels.contains(&short_channel_id);

// If HTLC minimum is larger than the amount we're going to transfer, we shouldn't
// bother considering this channel. If retrying with recommended_value_msat may
// allow us to hit the HTLC minimum limit, set htlc_minimum_limit so that we go
// around again with a higher amount.
if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && may_overpay_to_meet_path_minimum_msat {
if !contributes_sufficient_value || exceeds_max_path_length ||
exceeds_cltv_delta_limit || payment_failed_on_this_channel {
// Path isn't useful, ignore it and move on.
} else if may_overpay_to_meet_path_minimum_msat {
hit_minimum_limit = true;
} else if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && over_path_minimum_msat {
} else if over_path_minimum_msat {
// Note that low contribution here (limited by available_liquidity_msat)
// might violate htlc_minimum_msat on the hops which are next along the
// payment path (upstream to the payee). To avoid that, we recompute
Expand DownExpand Up@@ -1993,6 +2004,8 @@ mod tests {
use prelude::*;
use sync::{self, Arc};

use core::convert::TryInto;

fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey,
features: InitFeatures, outbound_capacity_msat: u64) -> channelmanager::ChannelDetails {
channelmanager::ChannelDetails {
Expand DownExpand Up@@ -5573,6 +5586,35 @@ mod tests {
}
}

#[test]
fn avoids_recently_failed_paths() {
// Ensure that the router always avoids all of the `previously_failed_channels` channels by
// randomly inserting channels into it until we can't find a route anymore.
let (secp_ctx, network, _, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
let network_graph = network.read_only();

let scorer = test_utils::TestScorer::with_penalty(0);
let mut payment_params = PaymentParameters::from_node_id(nodes[6]).with_route_hints(last_hops(&nodes))
.with_max_path_count(1);
let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
let random_seed_bytes = keys_manager.get_secure_random_bytes();

// We should be able to find a route initially, and then after we fail a few random
// channels eventually we won't be able to any longer.
assert!(get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes).is_ok());
loop {
if let Ok(route) = get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes) {
Comment thread
TheBlueMatt marked this conversation as resolved.
for chan in route.paths[0].iter() {
assert!(!payment_params.previously_failed_channels.contains(&chan.short_channel_id));
}
let victim = (u64::from_ne_bytes(random_seed_bytes[0..8].try_into().unwrap()) as usize)
Comment thread
tnull marked this conversation as resolved.
% route.paths[0].len();
payment_params.previously_failed_channels.push(route.paths[0][victim].short_channel_id);
} else { break; }
}
}

#[test]
fn limits_path_length() {
let (secp_ctx, network, _, _, logger) = build_line_graph();
Expand Down
60 changes: 42 additions & 18 deletions lightning/src/routing/scoring.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,6 +394,25 @@ pub struct ProbabilisticScoringParameters {
///
/// Default value: 250 msat
pub anti_probing_penalty_msat: u64,

/// This penalty is applied when the amount we're attempting to send over a channel exceeds our
/// current estimate of the channel's available liquidity.
///
/// Note that in this case all other penalties, including the
/// [`liquidity_penalty_multiplier_msat`] and [`amount_penalty_multiplier_msat`]-based
/// penalties, as well as the [`base_penalty_msat`] and the [`anti_probing_penalty_msat`], if
/// applicable, are still included in the overall penalty.
///
/// If you wish to avoid creating paths with such channels entirely, setting this to a value of
/// `u64::max_value()` will guarantee that.
///
/// Default value: 1_0000_0000_000 msat (1 Bitcoin)
Comment thread
tnull marked this conversation as resolved.
///
/// [`liquidity_penalty_multiplier_msat`]: Self::liquidity_penalty_multiplier_msat
/// [`amount_penalty_multiplier_msat`]: Self::amount_penalty_multiplier_msat
/// [`base_penalty_msat`]: Self::base_penalty_msat
/// [`anti_probing_penalty_msat`]: Self::anti_probing_penalty_msat
pub considered_impossible_penalty_msat: u64,
}

/// Accounting for channel liquidity balance uncertainty.
Expand DownExpand Up@@ -522,6 +541,7 @@ impl ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 0,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 0,
considered_impossible_penalty_msat: 0,
}
}

Expand All@@ -543,6 +563,7 @@ impl Default for ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 256,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 250,
considered_impossible_penalty_msat: 1_0000_0000_000,
Comment thread
tnull marked this conversation as resolved.
}
}
}
Expand DownExpand Up@@ -620,17 +641,12 @@ impl<L: Deref<Target = u64>, T: Time, U: Deref<Target = T>> DirectedChannelLiqui
if amount_msat <= min_liquidity_msat {
0
} else if amount_msat >= max_liquidity_msat {
if amount_msat > max_liquidity_msat {
u64::max_value()
} else if max_liquidity_msat != self.capacity_msat {
// Avoid using the failed channel on retry.
u64::max_value()
} else {
// Equivalent to hitting the else clause below with the amount equal to the
// effective capacity and without any certainty on the liquidity upper bound.
let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
}
// Equivalent to hitting the else clause below with the amount equal to the effective
// capacity and without any certainty on the liquidity upper bound, plus the
// impossibility penalty.
Comment on lines +644 to +646

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment needs to be updated now since it's not only when equal to the effective capacity. Seems like the earlier change that added this now removed logic is what caused calculates_log10_without_overflowing_u64_max_value to not exercise the correct code path as mentioned earlier on that test (i.e., it doesn't hit the negative_log10_times_2048 case below.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the comment differently - I read the comment to say "the calculation we're doing here is equivalent to..." which is still true, no?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, you're right!

let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
.saturating_add(params.considered_impossible_penalty_msat)
Comment on lines +647 to +649

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to do the max of these two rather than adding? Is the idea that we want this to be >= anything given in the else clause?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that's the idea. I suppose we could do a max. Originally I had it not adding the combined_penalty call at all but that seemed to brittle to deal with so switched to adding. I don't honestly have a strong opinion between adding and max, either way the docs can tell users what's going on, but for max i kinda worry users will acidentally set it too low and get no penalty here, which seems strange?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They would also need to set the other params lower, though, since max would select the combined penalty over the considered_impossible_penalty_msat if the latter were too low. So it's all kinda relative. Don't feel too strongly either though note that max may make debugging a little easier as otherwise you may need to mentally subtract some combined penalty if it is not obvious.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong? I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong?

Plus base and amount penalty, FWIW.

I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Sure we can leave it. Though one thing I just realized is that either way now the penalty will be variable across channels depending on the amount. Maybe less so when using max but maybe that's an argument in favor of adding. That way if left to choose only channels exceeding the maximum liquidity, we'd prefer ones that would otherwise be penalized less.

} else {
let numerator = (max_liquidity_msat - amount_msat).saturating_add(1);
let denominator = (max_liquidity_msat - min_liquidity_msat).saturating_add(1);
Expand DownExpand Up@@ -1624,7 +1640,7 @@ mod tests {
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 102_400, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 47);
let usage = ChannelUsage { amount_msat: 1_024_000, ..usage };
let usage = ChannelUsage { amount_msat: 1_023_999, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

let usage = ChannelUsage {
Expand DownExpand Up@@ -1654,6 +1670,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger)
Expand DownExpand Up@@ -1745,6 +1762,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand DownExpand Up@@ -1811,6 +1829,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand All@@ -1820,10 +1839,10 @@ mod tests {
let usage = ChannelUsage {
amount_msat: 0,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_000) },
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_024) },
};
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

scorer.payment_path_failed(&payment_path_for_amount(768).iter().collect::<Vec<_>>(), 42);
Expand DownExpand Up@@ -1867,20 +1886,20 @@ mod tests {
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

// Fully decay liquidity upper bound.
SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());
}

#[test]
Expand DownExpand Up@@ -1965,6 +1984,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2001,6 +2021,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2171,7 +2192,10 @@ mod tests {
fn accounts_for_inflight_htlc_usage() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters::default();
let params = ProbabilisticScoringParameters {
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
let source = source_node_id();
let target = target_node_id();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lightning/src/ln/channelmanager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3894,7 +3894,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
return;
}
mem::drop(channel_state_lock);
let retry = if let Some(payment_params_data) = payment_params {
let mut retry = if let Some(payment_params_data) = payment_params {
let path_last_hop = path.last().expect("Outbound payments must have had a valid path");
Some(RouteParameters {
payment_params: payment_params_data.clone(),
Expand DownExpand Up@@ -3930,6 +3930,9 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// TODO: If we decided to blame ourselves (or one of our channels) in
// process_onion_failure we should close that channel as it implies our
// next-hop is needlessly blaming us!
if let Some(scid) = short_channel_id {
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
}
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
Expand DownExpand Up@@ -3959,14 +3962,16 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// ChannelDetails.
// TODO: For non-temporary failures, we really should be closing the
// channel here as we apparently can't relay through them anyway.
let scid = path.first().unwrap().short_channel_id;
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
rejected_by_dest: path.len() == 1,
network_update: None,
all_paths_failed,
path: path.clone(),
short_channel_id: Some(path.first().unwrap().short_channel_id),
short_channel_id: Some(scid),
retry,
#[cfg(test)]
error_code: Some(*failure_code),
Expand Down
5 changes: 4 additions & 1 deletion lightning/src/ln/functional_test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1492,7 +1492,7 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
let mut events = node.node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
let expected_payment_id = match events.pop().unwrap() {
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update,
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update, short_channel_id,
#[cfg(test)]
error_code,
#[cfg(test)]
Expand All@@ -1502,6 +1502,9 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
assert!(retry.is_some(), "expected retry.is_some()");
assert_eq!(retry.as_ref().unwrap().final_value_msat, path.last().unwrap().fee_msat, "Retry amount should match last hop in path");
assert_eq!(retry.as_ref().unwrap().payment_params.payee_pubkey, path.last().unwrap().pubkey, "Retry payee node_id should match last hop in path");
if let Some(scid) = short_channel_id {
assert!(retry.as_ref().unwrap().payment_params.previously_failed_channels.contains(&scid));
}

#[cfg(test)]
{
Expand Down
54 changes: 48 additions & 6 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,6 +240,11 @@ pub struct PaymentParameters {
///
/// Default value: 1
pub max_channel_saturation_power_of_half: u8,

/// A list of SCIDs which this payment was previously attempted over and which caused the
/// payment to fail. Future attempts for the same payment shouldn't be relayed through any of
/// these SCIDs.
pub previously_failed_channels: Vec<u64>,
}

impl_writeable_tlv_based!(PaymentParameters, {
Expand All@@ -250,6 +255,7 @@ impl_writeable_tlv_based!(PaymentParameters, {
(4, route_hints, vec_type),
(5, max_channel_saturation_power_of_half, (default_value, 1)),
(6, expiry_time, option),
(7, previously_failed_channels, vec_type),
});

impl PaymentParameters {
Expand All@@ -263,6 +269,7 @@ impl PaymentParameters {
max_total_cltv_expiry_delta: DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA,
max_path_count: DEFAULT_MAX_PATH_COUNT,
max_channel_saturation_power_of_half: 1,
previously_failed_channels: Vec::new(),
}
}

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

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

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

let payment_failed_on_this_channel =
payment_params.previously_failed_channels.contains(&short_channel_id);

// If HTLC minimum is larger than the amount we're going to transfer, we shouldn't
// bother considering this channel. If retrying with recommended_value_msat may
// allow us to hit the HTLC minimum limit, set htlc_minimum_limit so that we go
// around again with a higher amount.
if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && may_overpay_to_meet_path_minimum_msat {
if !contributes_sufficient_value || exceeds_max_path_length ||
exceeds_cltv_delta_limit || payment_failed_on_this_channel {
// Path isn't useful, ignore it and move on.
} else if may_overpay_to_meet_path_minimum_msat {
hit_minimum_limit = true;
} else if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && over_path_minimum_msat {
} else if over_path_minimum_msat {
// Note that low contribution here (limited by available_liquidity_msat)
// might violate htlc_minimum_msat on the hops which are next along the
// payment path (upstream to the payee). To avoid that, we recompute
Expand DownExpand Up@@ -1993,6 +2004,8 @@ mod tests {
use prelude::*;
use sync::{self, Arc};

use core::convert::TryInto;

fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey,
features: InitFeatures, outbound_capacity_msat: u64) -> channelmanager::ChannelDetails {
channelmanager::ChannelDetails {
Expand DownExpand Up@@ -5573,6 +5586,35 @@ mod tests {
}
}

#[test]
fn avoids_recently_failed_paths() {
// Ensure that the router always avoids all of the `previously_failed_channels` channels by
// randomly inserting channels into it until we can't find a route anymore.
let (secp_ctx, network, _, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
let network_graph = network.read_only();

let scorer = test_utils::TestScorer::with_penalty(0);
let mut payment_params = PaymentParameters::from_node_id(nodes[6]).with_route_hints(last_hops(&nodes))
.with_max_path_count(1);
let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
let random_seed_bytes = keys_manager.get_secure_random_bytes();

// We should be able to find a route initially, and then after we fail a few random
// channels eventually we won't be able to any longer.
assert!(get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes).is_ok());
loop {
if let Ok(route) = get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes) {
Comment thread
TheBlueMatt marked this conversation as resolved.
for chan in route.paths[0].iter() {
assert!(!payment_params.previously_failed_channels.contains(&chan.short_channel_id));
}
let victim = (u64::from_ne_bytes(random_seed_bytes[0..8].try_into().unwrap()) as usize)
Comment thread
tnull marked this conversation as resolved.
% route.paths[0].len();
payment_params.previously_failed_channels.push(route.paths[0][victim].short_channel_id);
} else { break; }
}
}

#[test]
fn limits_path_length() {
let (secp_ctx, network, _, _, logger) = build_line_graph();
Expand Down
60 changes: 42 additions & 18 deletions lightning/src/routing/scoring.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,6 +394,25 @@ pub struct ProbabilisticScoringParameters {
///
/// Default value: 250 msat
pub anti_probing_penalty_msat: u64,

/// This penalty is applied when the amount we're attempting to send over a channel exceeds our
/// current estimate of the channel's available liquidity.
///
/// Note that in this case all other penalties, including the
/// [`liquidity_penalty_multiplier_msat`] and [`amount_penalty_multiplier_msat`]-based
/// penalties, as well as the [`base_penalty_msat`] and the [`anti_probing_penalty_msat`], if
/// applicable, are still included in the overall penalty.
///
/// If you wish to avoid creating paths with such channels entirely, setting this to a value of
/// `u64::max_value()` will guarantee that.
///
/// Default value: 1_0000_0000_000 msat (1 Bitcoin)
Comment thread
tnull marked this conversation as resolved.
///
/// [`liquidity_penalty_multiplier_msat`]: Self::liquidity_penalty_multiplier_msat
/// [`amount_penalty_multiplier_msat`]: Self::amount_penalty_multiplier_msat
/// [`base_penalty_msat`]: Self::base_penalty_msat
/// [`anti_probing_penalty_msat`]: Self::anti_probing_penalty_msat
pub considered_impossible_penalty_msat: u64,
}

/// Accounting for channel liquidity balance uncertainty.
Expand DownExpand Up@@ -522,6 +541,7 @@ impl ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 0,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 0,
considered_impossible_penalty_msat: 0,
}
}

Expand All@@ -543,6 +563,7 @@ impl Default for ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 256,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 250,
considered_impossible_penalty_msat: 1_0000_0000_000,
Comment thread
tnull marked this conversation as resolved.
}
}
}
Expand DownExpand Up@@ -620,17 +641,12 @@ impl<L: Deref<Target = u64>, T: Time, U: Deref<Target = T>> DirectedChannelLiqui
if amount_msat <= min_liquidity_msat {
0
} else if amount_msat >= max_liquidity_msat {
if amount_msat > max_liquidity_msat {
u64::max_value()
} else if max_liquidity_msat != self.capacity_msat {
// Avoid using the failed channel on retry.
u64::max_value()
} else {
// Equivalent to hitting the else clause below with the amount equal to the
// effective capacity and without any certainty on the liquidity upper bound.
let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
}
// Equivalent to hitting the else clause below with the amount equal to the effective
// capacity and without any certainty on the liquidity upper bound, plus the
// impossibility penalty.
Comment on lines +644 to +646

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment needs to be updated now since it's not only when equal to the effective capacity. Seems like the earlier change that added this now removed logic is what caused calculates_log10_without_overflowing_u64_max_value to not exercise the correct code path as mentioned earlier on that test (i.e., it doesn't hit the negative_log10_times_2048 case below.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the comment differently - I read the comment to say "the calculation we're doing here is equivalent to..." which is still true, no?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, you're right!

let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
.saturating_add(params.considered_impossible_penalty_msat)
Comment on lines +647 to +649

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to do the max of these two rather than adding? Is the idea that we want this to be >= anything given in the else clause?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that's the idea. I suppose we could do a max. Originally I had it not adding the combined_penalty call at all but that seemed to brittle to deal with so switched to adding. I don't honestly have a strong opinion between adding and max, either way the docs can tell users what's going on, but for max i kinda worry users will acidentally set it too low and get no penalty here, which seems strange?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They would also need to set the other params lower, though, since max would select the combined penalty over the considered_impossible_penalty_msat if the latter were too low. So it's all kinda relative. Don't feel too strongly either though note that max may make debugging a little easier as otherwise you may need to mentally subtract some combined penalty if it is not obvious.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong? I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong?

Plus base and amount penalty, FWIW.

I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Sure we can leave it. Though one thing I just realized is that either way now the penalty will be variable across channels depending on the amount. Maybe less so when using max but maybe that's an argument in favor of adding. That way if left to choose only channels exceeding the maximum liquidity, we'd prefer ones that would otherwise be penalized less.

} else {
let numerator = (max_liquidity_msat - amount_msat).saturating_add(1);
let denominator = (max_liquidity_msat - min_liquidity_msat).saturating_add(1);
Expand DownExpand Up@@ -1624,7 +1640,7 @@ mod tests {
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 102_400, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 47);
let usage = ChannelUsage { amount_msat: 1_024_000, ..usage };
let usage = ChannelUsage { amount_msat: 1_023_999, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

let usage = ChannelUsage {
Expand DownExpand Up@@ -1654,6 +1670,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger)
Expand DownExpand Up@@ -1745,6 +1762,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand DownExpand Up@@ -1811,6 +1829,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand All@@ -1820,10 +1839,10 @@ mod tests {
let usage = ChannelUsage {
amount_msat: 0,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_000) },
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_024) },
};
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

scorer.payment_path_failed(&payment_path_for_amount(768).iter().collect::<Vec<_>>(), 42);
Expand DownExpand Up@@ -1867,20 +1886,20 @@ mod tests {
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

// Fully decay liquidity upper bound.
SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());
}

#[test]
Expand DownExpand Up@@ -1965,6 +1984,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2001,6 +2021,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2171,7 +2192,10 @@ mod tests {
fn accounts_for_inflight_htlc_usage() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters::default();
let params = ProbabilisticScoringParameters {
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
let source = source_node_id();
let target = target_node_id();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lightning/src/ln/channelmanager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3894,7 +3894,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
return;
}
mem::drop(channel_state_lock);
let retry = if let Some(payment_params_data) = payment_params {
let mut retry = if let Some(payment_params_data) = payment_params {
let path_last_hop = path.last().expect("Outbound payments must have had a valid path");
Some(RouteParameters {
payment_params: payment_params_data.clone(),
Expand DownExpand Up@@ -3930,6 +3930,9 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// TODO: If we decided to blame ourselves (or one of our channels) in
// process_onion_failure we should close that channel as it implies our
// next-hop is needlessly blaming us!
if let Some(scid) = short_channel_id {
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
}
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
Expand DownExpand Up@@ -3959,14 +3962,16 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// ChannelDetails.
// TODO: For non-temporary failures, we really should be closing the
// channel here as we apparently can't relay through them anyway.
let scid = path.first().unwrap().short_channel_id;
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
rejected_by_dest: path.len() == 1,
network_update: None,
all_paths_failed,
path: path.clone(),
short_channel_id: Some(path.first().unwrap().short_channel_id),
short_channel_id: Some(scid),
retry,
#[cfg(test)]
error_code: Some(*failure_code),
Expand Down
5 changes: 4 additions & 1 deletion lightning/src/ln/functional_test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1492,7 +1492,7 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
let mut events = node.node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
let expected_payment_id = match events.pop().unwrap() {
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update,
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update, short_channel_id,
#[cfg(test)]
error_code,
#[cfg(test)]
Expand All@@ -1502,6 +1502,9 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
assert!(retry.is_some(), "expected retry.is_some()");
assert_eq!(retry.as_ref().unwrap().final_value_msat, path.last().unwrap().fee_msat, "Retry amount should match last hop in path");
assert_eq!(retry.as_ref().unwrap().payment_params.payee_pubkey, path.last().unwrap().pubkey, "Retry payee node_id should match last hop in path");
if let Some(scid) = short_channel_id {
assert!(retry.as_ref().unwrap().payment_params.previously_failed_channels.contains(&scid));
}

#[cfg(test)]
{
Expand Down
54 changes: 48 additions & 6 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,6 +240,11 @@ pub struct PaymentParameters {
///
/// Default value: 1
pub max_channel_saturation_power_of_half: u8,

/// A list of SCIDs which this payment was previously attempted over and which caused the
/// payment to fail. Future attempts for the same payment shouldn't be relayed through any of
/// these SCIDs.
pub previously_failed_channels: Vec<u64>,
}

impl_writeable_tlv_based!(PaymentParameters, {
Expand All@@ -250,6 +255,7 @@ impl_writeable_tlv_based!(PaymentParameters, {
(4, route_hints, vec_type),
(5, max_channel_saturation_power_of_half, (default_value, 1)),
(6, expiry_time, option),
(7, previously_failed_channels, vec_type),
});

impl PaymentParameters {
Expand All@@ -263,6 +269,7 @@ impl PaymentParameters {
max_total_cltv_expiry_delta: DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA,
max_path_count: DEFAULT_MAX_PATH_COUNT,
max_channel_saturation_power_of_half: 1,
previously_failed_channels: Vec::new(),
}
}

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

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

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

let payment_failed_on_this_channel =
payment_params.previously_failed_channels.contains(&short_channel_id);

// If HTLC minimum is larger than the amount we're going to transfer, we shouldn't
// bother considering this channel. If retrying with recommended_value_msat may
// allow us to hit the HTLC minimum limit, set htlc_minimum_limit so that we go
// around again with a higher amount.
if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && may_overpay_to_meet_path_minimum_msat {
if !contributes_sufficient_value || exceeds_max_path_length ||
exceeds_cltv_delta_limit || payment_failed_on_this_channel {
// Path isn't useful, ignore it and move on.
} else if may_overpay_to_meet_path_minimum_msat {
hit_minimum_limit = true;
} else if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && over_path_minimum_msat {
} else if over_path_minimum_msat {
// Note that low contribution here (limited by available_liquidity_msat)
// might violate htlc_minimum_msat on the hops which are next along the
// payment path (upstream to the payee). To avoid that, we recompute
Expand DownExpand Up@@ -1993,6 +2004,8 @@ mod tests {
use prelude::*;
use sync::{self, Arc};

use core::convert::TryInto;

fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey,
features: InitFeatures, outbound_capacity_msat: u64) -> channelmanager::ChannelDetails {
channelmanager::ChannelDetails {
Expand DownExpand Up@@ -5573,6 +5586,35 @@ mod tests {
}
}

#[test]
fn avoids_recently_failed_paths() {
// Ensure that the router always avoids all of the `previously_failed_channels` channels by
// randomly inserting channels into it until we can't find a route anymore.
let (secp_ctx, network, _, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
let network_graph = network.read_only();

let scorer = test_utils::TestScorer::with_penalty(0);
let mut payment_params = PaymentParameters::from_node_id(nodes[6]).with_route_hints(last_hops(&nodes))
.with_max_path_count(1);
let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
let random_seed_bytes = keys_manager.get_secure_random_bytes();

// We should be able to find a route initially, and then after we fail a few random
// channels eventually we won't be able to any longer.
assert!(get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes).is_ok());
loop {
if let Ok(route) = get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes) {
Comment thread
TheBlueMatt marked this conversation as resolved.
for chan in route.paths[0].iter() {
assert!(!payment_params.previously_failed_channels.contains(&chan.short_channel_id));
}
let victim = (u64::from_ne_bytes(random_seed_bytes[0..8].try_into().unwrap()) as usize)
Comment thread
tnull marked this conversation as resolved.
% route.paths[0].len();
payment_params.previously_failed_channels.push(route.paths[0][victim].short_channel_id);
} else { break; }
}
}

#[test]
fn limits_path_length() {
let (secp_ctx, network, _, _, logger) = build_line_graph();
Expand Down
60 changes: 42 additions & 18 deletions lightning/src/routing/scoring.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,6 +394,25 @@ pub struct ProbabilisticScoringParameters {
///
/// Default value: 250 msat
pub anti_probing_penalty_msat: u64,

/// This penalty is applied when the amount we're attempting to send over a channel exceeds our
/// current estimate of the channel's available liquidity.
///
/// Note that in this case all other penalties, including the
/// [`liquidity_penalty_multiplier_msat`] and [`amount_penalty_multiplier_msat`]-based
/// penalties, as well as the [`base_penalty_msat`] and the [`anti_probing_penalty_msat`], if
/// applicable, are still included in the overall penalty.
///
/// If you wish to avoid creating paths with such channels entirely, setting this to a value of
/// `u64::max_value()` will guarantee that.
///
/// Default value: 1_0000_0000_000 msat (1 Bitcoin)
Comment thread
tnull marked this conversation as resolved.
///
/// [`liquidity_penalty_multiplier_msat`]: Self::liquidity_penalty_multiplier_msat
/// [`amount_penalty_multiplier_msat`]: Self::amount_penalty_multiplier_msat
/// [`base_penalty_msat`]: Self::base_penalty_msat
/// [`anti_probing_penalty_msat`]: Self::anti_probing_penalty_msat
pub considered_impossible_penalty_msat: u64,
}

/// Accounting for channel liquidity balance uncertainty.
Expand DownExpand Up@@ -522,6 +541,7 @@ impl ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 0,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 0,
considered_impossible_penalty_msat: 0,
}
}

Expand All@@ -543,6 +563,7 @@ impl Default for ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 256,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 250,
considered_impossible_penalty_msat: 1_0000_0000_000,
Comment thread
tnull marked this conversation as resolved.
}
}
}
Expand DownExpand Up@@ -620,17 +641,12 @@ impl<L: Deref<Target = u64>, T: Time, U: Deref<Target = T>> DirectedChannelLiqui
if amount_msat <= min_liquidity_msat {
0
} else if amount_msat >= max_liquidity_msat {
if amount_msat > max_liquidity_msat {
u64::max_value()
} else if max_liquidity_msat != self.capacity_msat {
// Avoid using the failed channel on retry.
u64::max_value()
} else {
// Equivalent to hitting the else clause below with the amount equal to the
// effective capacity and without any certainty on the liquidity upper bound.
let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
}
// Equivalent to hitting the else clause below with the amount equal to the effective
// capacity and without any certainty on the liquidity upper bound, plus the
// impossibility penalty.
Comment on lines +644 to +646

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment needs to be updated now since it's not only when equal to the effective capacity. Seems like the earlier change that added this now removed logic is what caused calculates_log10_without_overflowing_u64_max_value to not exercise the correct code path as mentioned earlier on that test (i.e., it doesn't hit the negative_log10_times_2048 case below.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the comment differently - I read the comment to say "the calculation we're doing here is equivalent to..." which is still true, no?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, you're right!

let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
.saturating_add(params.considered_impossible_penalty_msat)
Comment on lines +647 to +649

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to do the max of these two rather than adding? Is the idea that we want this to be >= anything given in the else clause?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that's the idea. I suppose we could do a max. Originally I had it not adding the combined_penalty call at all but that seemed to brittle to deal with so switched to adding. I don't honestly have a strong opinion between adding and max, either way the docs can tell users what's going on, but for max i kinda worry users will acidentally set it too low and get no penalty here, which seems strange?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They would also need to set the other params lower, though, since max would select the combined penalty over the considered_impossible_penalty_msat if the latter were too low. So it's all kinda relative. Don't feel too strongly either though note that max may make debugging a little easier as otherwise you may need to mentally subtract some combined penalty if it is not obvious.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong? I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong?

Plus base and amount penalty, FWIW.

I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Sure we can leave it. Though one thing I just realized is that either way now the penalty will be variable across channels depending on the amount. Maybe less so when using max but maybe that's an argument in favor of adding. That way if left to choose only channels exceeding the maximum liquidity, we'd prefer ones that would otherwise be penalized less.

} else {
let numerator = (max_liquidity_msat - amount_msat).saturating_add(1);
let denominator = (max_liquidity_msat - min_liquidity_msat).saturating_add(1);
Expand DownExpand Up@@ -1624,7 +1640,7 @@ mod tests {
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 102_400, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 47);
let usage = ChannelUsage { amount_msat: 1_024_000, ..usage };
let usage = ChannelUsage { amount_msat: 1_023_999, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

let usage = ChannelUsage {
Expand DownExpand Up@@ -1654,6 +1670,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger)
Expand DownExpand Up@@ -1745,6 +1762,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand DownExpand Up@@ -1811,6 +1829,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand All@@ -1820,10 +1839,10 @@ mod tests {
let usage = ChannelUsage {
amount_msat: 0,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_000) },
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_024) },
};
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

scorer.payment_path_failed(&payment_path_for_amount(768).iter().collect::<Vec<_>>(), 42);
Expand DownExpand Up@@ -1867,20 +1886,20 @@ mod tests {
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

// Fully decay liquidity upper bound.
SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());
}

#[test]
Expand DownExpand Up@@ -1965,6 +1984,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2001,6 +2021,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2171,7 +2192,10 @@ mod tests {
fn accounts_for_inflight_htlc_usage() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters::default();
let params = ProbabilisticScoringParameters {
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
let source = source_node_id();
let target = target_node_id();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lightning/src/ln/channelmanager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3894,7 +3894,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
return;
}
mem::drop(channel_state_lock);
let retry = if let Some(payment_params_data) = payment_params {
let mut retry = if let Some(payment_params_data) = payment_params {
let path_last_hop = path.last().expect("Outbound payments must have had a valid path");
Some(RouteParameters {
payment_params: payment_params_data.clone(),
Expand DownExpand Up@@ -3930,6 +3930,9 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// TODO: If we decided to blame ourselves (or one of our channels) in
// process_onion_failure we should close that channel as it implies our
// next-hop is needlessly blaming us!
if let Some(scid) = short_channel_id {
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
}
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
Expand DownExpand Up@@ -3959,14 +3962,16 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// ChannelDetails.
// TODO: For non-temporary failures, we really should be closing the
// channel here as we apparently can't relay through them anyway.
let scid = path.first().unwrap().short_channel_id;
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
rejected_by_dest: path.len() == 1,
network_update: None,
all_paths_failed,
path: path.clone(),
short_channel_id: Some(path.first().unwrap().short_channel_id),
short_channel_id: Some(scid),
retry,
#[cfg(test)]
error_code: Some(*failure_code),
Expand Down
5 changes: 4 additions & 1 deletion lightning/src/ln/functional_test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1492,7 +1492,7 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
let mut events = node.node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
let expected_payment_id = match events.pop().unwrap() {
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update,
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update, short_channel_id,
#[cfg(test)]
error_code,
#[cfg(test)]
Expand All@@ -1502,6 +1502,9 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
assert!(retry.is_some(), "expected retry.is_some()");
assert_eq!(retry.as_ref().unwrap().final_value_msat, path.last().unwrap().fee_msat, "Retry amount should match last hop in path");
assert_eq!(retry.as_ref().unwrap().payment_params.payee_pubkey, path.last().unwrap().pubkey, "Retry payee node_id should match last hop in path");
if let Some(scid) = short_channel_id {
assert!(retry.as_ref().unwrap().payment_params.previously_failed_channels.contains(&scid));
}

#[cfg(test)]
{
Expand Down
54 changes: 48 additions & 6 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,6 +240,11 @@ pub struct PaymentParameters {
///
/// Default value: 1
pub max_channel_saturation_power_of_half: u8,

/// A list of SCIDs which this payment was previously attempted over and which caused the
/// payment to fail. Future attempts for the same payment shouldn't be relayed through any of
/// these SCIDs.
pub previously_failed_channels: Vec<u64>,
}

impl_writeable_tlv_based!(PaymentParameters, {
Expand All@@ -250,6 +255,7 @@ impl_writeable_tlv_based!(PaymentParameters, {
(4, route_hints, vec_type),
(5, max_channel_saturation_power_of_half, (default_value, 1)),
(6, expiry_time, option),
(7, previously_failed_channels, vec_type),
});

impl PaymentParameters {
Expand All@@ -263,6 +269,7 @@ impl PaymentParameters {
max_total_cltv_expiry_delta: DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA,
max_path_count: DEFAULT_MAX_PATH_COUNT,
max_channel_saturation_power_of_half: 1,
previously_failed_channels: Vec::new(),
}
}

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

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

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

let payment_failed_on_this_channel =
payment_params.previously_failed_channels.contains(&short_channel_id);

// If HTLC minimum is larger than the amount we're going to transfer, we shouldn't
// bother considering this channel. If retrying with recommended_value_msat may
// allow us to hit the HTLC minimum limit, set htlc_minimum_limit so that we go
// around again with a higher amount.
if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && may_overpay_to_meet_path_minimum_msat {
if !contributes_sufficient_value || exceeds_max_path_length ||
exceeds_cltv_delta_limit || payment_failed_on_this_channel {
// Path isn't useful, ignore it and move on.
} else if may_overpay_to_meet_path_minimum_msat {
hit_minimum_limit = true;
} else if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && over_path_minimum_msat {
} else if over_path_minimum_msat {
// Note that low contribution here (limited by available_liquidity_msat)
// might violate htlc_minimum_msat on the hops which are next along the
// payment path (upstream to the payee). To avoid that, we recompute
Expand DownExpand Up@@ -1993,6 +2004,8 @@ mod tests {
use prelude::*;
use sync::{self, Arc};

use core::convert::TryInto;

fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey,
features: InitFeatures, outbound_capacity_msat: u64) -> channelmanager::ChannelDetails {
channelmanager::ChannelDetails {
Expand DownExpand Up@@ -5573,6 +5586,35 @@ mod tests {
}
}

#[test]
fn avoids_recently_failed_paths() {
// Ensure that the router always avoids all of the `previously_failed_channels` channels by
// randomly inserting channels into it until we can't find a route anymore.
let (secp_ctx, network, _, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
let network_graph = network.read_only();

let scorer = test_utils::TestScorer::with_penalty(0);
let mut payment_params = PaymentParameters::from_node_id(nodes[6]).with_route_hints(last_hops(&nodes))
.with_max_path_count(1);
let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
let random_seed_bytes = keys_manager.get_secure_random_bytes();

// We should be able to find a route initially, and then after we fail a few random
// channels eventually we won't be able to any longer.
assert!(get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes).is_ok());
loop {
if let Ok(route) = get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes) {
Comment thread
TheBlueMatt marked this conversation as resolved.
for chan in route.paths[0].iter() {
assert!(!payment_params.previously_failed_channels.contains(&chan.short_channel_id));
}
let victim = (u64::from_ne_bytes(random_seed_bytes[0..8].try_into().unwrap()) as usize)
Comment thread
tnull marked this conversation as resolved.
% route.paths[0].len();
payment_params.previously_failed_channels.push(route.paths[0][victim].short_channel_id);
} else { break; }
}
}

#[test]
fn limits_path_length() {
let (secp_ctx, network, _, _, logger) = build_line_graph();
Expand Down
60 changes: 42 additions & 18 deletions lightning/src/routing/scoring.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,6 +394,25 @@ pub struct ProbabilisticScoringParameters {
///
/// Default value: 250 msat
pub anti_probing_penalty_msat: u64,

/// This penalty is applied when the amount we're attempting to send over a channel exceeds our
/// current estimate of the channel's available liquidity.
///
/// Note that in this case all other penalties, including the
/// [`liquidity_penalty_multiplier_msat`] and [`amount_penalty_multiplier_msat`]-based
/// penalties, as well as the [`base_penalty_msat`] and the [`anti_probing_penalty_msat`], if
/// applicable, are still included in the overall penalty.
///
/// If you wish to avoid creating paths with such channels entirely, setting this to a value of
/// `u64::max_value()` will guarantee that.
///
/// Default value: 1_0000_0000_000 msat (1 Bitcoin)
Comment thread
tnull marked this conversation as resolved.
///
/// [`liquidity_penalty_multiplier_msat`]: Self::liquidity_penalty_multiplier_msat
/// [`amount_penalty_multiplier_msat`]: Self::amount_penalty_multiplier_msat
/// [`base_penalty_msat`]: Self::base_penalty_msat
/// [`anti_probing_penalty_msat`]: Self::anti_probing_penalty_msat
pub considered_impossible_penalty_msat: u64,
}

/// Accounting for channel liquidity balance uncertainty.
Expand DownExpand Up@@ -522,6 +541,7 @@ impl ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 0,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 0,
considered_impossible_penalty_msat: 0,
}
}

Expand All@@ -543,6 +563,7 @@ impl Default for ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 256,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 250,
considered_impossible_penalty_msat: 1_0000_0000_000,
Comment thread
tnull marked this conversation as resolved.
}
}
}
Expand DownExpand Up@@ -620,17 +641,12 @@ impl<L: Deref<Target = u64>, T: Time, U: Deref<Target = T>> DirectedChannelLiqui
if amount_msat <= min_liquidity_msat {
0
} else if amount_msat >= max_liquidity_msat {
if amount_msat > max_liquidity_msat {
u64::max_value()
} else if max_liquidity_msat != self.capacity_msat {
// Avoid using the failed channel on retry.
u64::max_value()
} else {
// Equivalent to hitting the else clause below with the amount equal to the
// effective capacity and without any certainty on the liquidity upper bound.
let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
}
// Equivalent to hitting the else clause below with the amount equal to the effective
// capacity and without any certainty on the liquidity upper bound, plus the
// impossibility penalty.
Comment on lines +644 to +646

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment needs to be updated now since it's not only when equal to the effective capacity. Seems like the earlier change that added this now removed logic is what caused calculates_log10_without_overflowing_u64_max_value to not exercise the correct code path as mentioned earlier on that test (i.e., it doesn't hit the negative_log10_times_2048 case below.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the comment differently - I read the comment to say "the calculation we're doing here is equivalent to..." which is still true, no?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, you're right!

let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
.saturating_add(params.considered_impossible_penalty_msat)
Comment on lines +647 to +649

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to do the max of these two rather than adding? Is the idea that we want this to be >= anything given in the else clause?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that's the idea. I suppose we could do a max. Originally I had it not adding the combined_penalty call at all but that seemed to brittle to deal with so switched to adding. I don't honestly have a strong opinion between adding and max, either way the docs can tell users what's going on, but for max i kinda worry users will acidentally set it too low and get no penalty here, which seems strange?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They would also need to set the other params lower, though, since max would select the combined penalty over the considered_impossible_penalty_msat if the latter were too low. So it's all kinda relative. Don't feel too strongly either though note that max may make debugging a little easier as otherwise you may need to mentally subtract some combined penalty if it is not obvious.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong? I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong?

Plus base and amount penalty, FWIW.

I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Sure we can leave it. Though one thing I just realized is that either way now the penalty will be variable across channels depending on the amount. Maybe less so when using max but maybe that's an argument in favor of adding. That way if left to choose only channels exceeding the maximum liquidity, we'd prefer ones that would otherwise be penalized less.

} else {
let numerator = (max_liquidity_msat - amount_msat).saturating_add(1);
let denominator = (max_liquidity_msat - min_liquidity_msat).saturating_add(1);
Expand DownExpand Up@@ -1624,7 +1640,7 @@ mod tests {
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 102_400, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 47);
let usage = ChannelUsage { amount_msat: 1_024_000, ..usage };
let usage = ChannelUsage { amount_msat: 1_023_999, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

let usage = ChannelUsage {
Expand DownExpand Up@@ -1654,6 +1670,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger)
Expand DownExpand Up@@ -1745,6 +1762,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand DownExpand Up@@ -1811,6 +1829,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand All@@ -1820,10 +1839,10 @@ mod tests {
let usage = ChannelUsage {
amount_msat: 0,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_000) },
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_024) },
};
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

scorer.payment_path_failed(&payment_path_for_amount(768).iter().collect::<Vec<_>>(), 42);
Expand DownExpand Up@@ -1867,20 +1886,20 @@ mod tests {
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

// Fully decay liquidity upper bound.
SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());
}

#[test]
Expand DownExpand Up@@ -1965,6 +1984,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2001,6 +2021,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2171,7 +2192,10 @@ mod tests {
fn accounts_for_inflight_htlc_usage() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters::default();
let params = ProbabilisticScoringParameters {
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
let source = source_node_id();
let target = target_node_id();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lightning/src/ln/channelmanager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3894,7 +3894,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
return;
}
mem::drop(channel_state_lock);
let retry = if let Some(payment_params_data) = payment_params {
let mut retry = if let Some(payment_params_data) = payment_params {
let path_last_hop = path.last().expect("Outbound payments must have had a valid path");
Some(RouteParameters {
payment_params: payment_params_data.clone(),
Expand DownExpand Up@@ -3930,6 +3930,9 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// TODO: If we decided to blame ourselves (or one of our channels) in
// process_onion_failure we should close that channel as it implies our
// next-hop is needlessly blaming us!
if let Some(scid) = short_channel_id {
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
}
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
Expand DownExpand Up@@ -3959,14 +3962,16 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// ChannelDetails.
// TODO: For non-temporary failures, we really should be closing the
// channel here as we apparently can't relay through them anyway.
let scid = path.first().unwrap().short_channel_id;
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
rejected_by_dest: path.len() == 1,
network_update: None,
all_paths_failed,
path: path.clone(),
short_channel_id: Some(path.first().unwrap().short_channel_id),
short_channel_id: Some(scid),
retry,
#[cfg(test)]
error_code: Some(*failure_code),
Expand Down
5 changes: 4 additions & 1 deletion lightning/src/ln/functional_test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1492,7 +1492,7 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
let mut events = node.node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
let expected_payment_id = match events.pop().unwrap() {
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update,
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update, short_channel_id,
#[cfg(test)]
error_code,
#[cfg(test)]
Expand All@@ -1502,6 +1502,9 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
assert!(retry.is_some(), "expected retry.is_some()");
assert_eq!(retry.as_ref().unwrap().final_value_msat, path.last().unwrap().fee_msat, "Retry amount should match last hop in path");
assert_eq!(retry.as_ref().unwrap().payment_params.payee_pubkey, path.last().unwrap().pubkey, "Retry payee node_id should match last hop in path");
if let Some(scid) = short_channel_id {
assert!(retry.as_ref().unwrap().payment_params.previously_failed_channels.contains(&scid));
}

#[cfg(test)]
{
Expand Down
54 changes: 48 additions & 6 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,6 +240,11 @@ pub struct PaymentParameters {
///
/// Default value: 1
pub max_channel_saturation_power_of_half: u8,

/// A list of SCIDs which this payment was previously attempted over and which caused the
/// payment to fail. Future attempts for the same payment shouldn't be relayed through any of
/// these SCIDs.
pub previously_failed_channels: Vec<u64>,
}

impl_writeable_tlv_based!(PaymentParameters, {
Expand All@@ -250,6 +255,7 @@ impl_writeable_tlv_based!(PaymentParameters, {
(4, route_hints, vec_type),
(5, max_channel_saturation_power_of_half, (default_value, 1)),
(6, expiry_time, option),
(7, previously_failed_channels, vec_type),
});

impl PaymentParameters {
Expand All@@ -263,6 +269,7 @@ impl PaymentParameters {
max_total_cltv_expiry_delta: DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA,
max_path_count: DEFAULT_MAX_PATH_COUNT,
max_channel_saturation_power_of_half: 1,
previously_failed_channels: Vec::new(),
}
}

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

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

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

let payment_failed_on_this_channel =
payment_params.previously_failed_channels.contains(&short_channel_id);

// If HTLC minimum is larger than the amount we're going to transfer, we shouldn't
// bother considering this channel. If retrying with recommended_value_msat may
// allow us to hit the HTLC minimum limit, set htlc_minimum_limit so that we go
// around again with a higher amount.
if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && may_overpay_to_meet_path_minimum_msat {
if !contributes_sufficient_value || exceeds_max_path_length ||
exceeds_cltv_delta_limit || payment_failed_on_this_channel {
// Path isn't useful, ignore it and move on.
} else if may_overpay_to_meet_path_minimum_msat {
hit_minimum_limit = true;
} else if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && over_path_minimum_msat {
} else if over_path_minimum_msat {
// Note that low contribution here (limited by available_liquidity_msat)
// might violate htlc_minimum_msat on the hops which are next along the
// payment path (upstream to the payee). To avoid that, we recompute
Expand DownExpand Up@@ -1993,6 +2004,8 @@ mod tests {
use prelude::*;
use sync::{self, Arc};

use core::convert::TryInto;

fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey,
features: InitFeatures, outbound_capacity_msat: u64) -> channelmanager::ChannelDetails {
channelmanager::ChannelDetails {
Expand DownExpand Up@@ -5573,6 +5586,35 @@ mod tests {
}
}

#[test]
fn avoids_recently_failed_paths() {
// Ensure that the router always avoids all of the `previously_failed_channels` channels by
// randomly inserting channels into it until we can't find a route anymore.
let (secp_ctx, network, _, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
let network_graph = network.read_only();

let scorer = test_utils::TestScorer::with_penalty(0);
let mut payment_params = PaymentParameters::from_node_id(nodes[6]).with_route_hints(last_hops(&nodes))
.with_max_path_count(1);
let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
let random_seed_bytes = keys_manager.get_secure_random_bytes();

// We should be able to find a route initially, and then after we fail a few random
// channels eventually we won't be able to any longer.
assert!(get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes).is_ok());
loop {
if let Ok(route) = get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes) {
Comment thread
TheBlueMatt marked this conversation as resolved.
for chan in route.paths[0].iter() {
assert!(!payment_params.previously_failed_channels.contains(&chan.short_channel_id));
}
let victim = (u64::from_ne_bytes(random_seed_bytes[0..8].try_into().unwrap()) as usize)
Comment thread
tnull marked this conversation as resolved.
% route.paths[0].len();
payment_params.previously_failed_channels.push(route.paths[0][victim].short_channel_id);
} else { break; }
}
}

#[test]
fn limits_path_length() {
let (secp_ctx, network, _, _, logger) = build_line_graph();
Expand Down
60 changes: 42 additions & 18 deletions lightning/src/routing/scoring.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,6 +394,25 @@ pub struct ProbabilisticScoringParameters {
///
/// Default value: 250 msat
pub anti_probing_penalty_msat: u64,

/// This penalty is applied when the amount we're attempting to send over a channel exceeds our
/// current estimate of the channel's available liquidity.
///
/// Note that in this case all other penalties, including the
/// [`liquidity_penalty_multiplier_msat`] and [`amount_penalty_multiplier_msat`]-based
/// penalties, as well as the [`base_penalty_msat`] and the [`anti_probing_penalty_msat`], if
/// applicable, are still included in the overall penalty.
///
/// If you wish to avoid creating paths with such channels entirely, setting this to a value of
/// `u64::max_value()` will guarantee that.
///
/// Default value: 1_0000_0000_000 msat (1 Bitcoin)
Comment thread
tnull marked this conversation as resolved.
///
/// [`liquidity_penalty_multiplier_msat`]: Self::liquidity_penalty_multiplier_msat
/// [`amount_penalty_multiplier_msat`]: Self::amount_penalty_multiplier_msat
/// [`base_penalty_msat`]: Self::base_penalty_msat
/// [`anti_probing_penalty_msat`]: Self::anti_probing_penalty_msat
pub considered_impossible_penalty_msat: u64,
}

/// Accounting for channel liquidity balance uncertainty.
Expand DownExpand Up@@ -522,6 +541,7 @@ impl ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 0,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 0,
considered_impossible_penalty_msat: 0,
}
}

Expand All@@ -543,6 +563,7 @@ impl Default for ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 256,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 250,
considered_impossible_penalty_msat: 1_0000_0000_000,
Comment thread
tnull marked this conversation as resolved.
}
}
}
Expand DownExpand Up@@ -620,17 +641,12 @@ impl<L: Deref<Target = u64>, T: Time, U: Deref<Target = T>> DirectedChannelLiqui
if amount_msat <= min_liquidity_msat {
0
} else if amount_msat >= max_liquidity_msat {
if amount_msat > max_liquidity_msat {
u64::max_value()
} else if max_liquidity_msat != self.capacity_msat {
// Avoid using the failed channel on retry.
u64::max_value()
} else {
// Equivalent to hitting the else clause below with the amount equal to the
// effective capacity and without any certainty on the liquidity upper bound.
let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
}
// Equivalent to hitting the else clause below with the amount equal to the effective
// capacity and without any certainty on the liquidity upper bound, plus the
// impossibility penalty.
Comment on lines +644 to +646

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment needs to be updated now since it's not only when equal to the effective capacity. Seems like the earlier change that added this now removed logic is what caused calculates_log10_without_overflowing_u64_max_value to not exercise the correct code path as mentioned earlier on that test (i.e., it doesn't hit the negative_log10_times_2048 case below.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the comment differently - I read the comment to say "the calculation we're doing here is equivalent to..." which is still true, no?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, you're right!

let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
.saturating_add(params.considered_impossible_penalty_msat)
Comment on lines +647 to +649

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to do the max of these two rather than adding? Is the idea that we want this to be >= anything given in the else clause?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that's the idea. I suppose we could do a max. Originally I had it not adding the combined_penalty call at all but that seemed to brittle to deal with so switched to adding. I don't honestly have a strong opinion between adding and max, either way the docs can tell users what's going on, but for max i kinda worry users will acidentally set it too low and get no penalty here, which seems strange?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They would also need to set the other params lower, though, since max would select the combined penalty over the considered_impossible_penalty_msat if the latter were too low. So it's all kinda relative. Don't feel too strongly either though note that max may make debugging a little easier as otherwise you may need to mentally subtract some combined penalty if it is not obvious.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong? I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong?

Plus base and amount penalty, FWIW.

I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Sure we can leave it. Though one thing I just realized is that either way now the penalty will be variable across channels depending on the amount. Maybe less so when using max but maybe that's an argument in favor of adding. That way if left to choose only channels exceeding the maximum liquidity, we'd prefer ones that would otherwise be penalized less.

} else {
let numerator = (max_liquidity_msat - amount_msat).saturating_add(1);
let denominator = (max_liquidity_msat - min_liquidity_msat).saturating_add(1);
Expand DownExpand Up@@ -1624,7 +1640,7 @@ mod tests {
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 102_400, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 47);
let usage = ChannelUsage { amount_msat: 1_024_000, ..usage };
let usage = ChannelUsage { amount_msat: 1_023_999, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

let usage = ChannelUsage {
Expand DownExpand Up@@ -1654,6 +1670,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger)
Expand DownExpand Up@@ -1745,6 +1762,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand DownExpand Up@@ -1811,6 +1829,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand All@@ -1820,10 +1839,10 @@ mod tests {
let usage = ChannelUsage {
amount_msat: 0,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_000) },
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_024) },
};
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

scorer.payment_path_failed(&payment_path_for_amount(768).iter().collect::<Vec<_>>(), 42);
Expand DownExpand Up@@ -1867,20 +1886,20 @@ mod tests {
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

// Fully decay liquidity upper bound.
SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());
}

#[test]
Expand DownExpand Up@@ -1965,6 +1984,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2001,6 +2021,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2171,7 +2192,10 @@ mod tests {
fn accounts_for_inflight_htlc_usage() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters::default();
let params = ProbabilisticScoringParameters {
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
let source = source_node_id();
let target = target_node_id();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lightning/src/ln/channelmanager.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3894,7 +3894,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
return;
}
mem::drop(channel_state_lock);
let retry = if let Some(payment_params_data) = payment_params {
let mut retry = if let Some(payment_params_data) = payment_params {
let path_last_hop = path.last().expect("Outbound payments must have had a valid path");
Some(RouteParameters {
payment_params: payment_params_data.clone(),
Expand DownExpand Up@@ -3930,6 +3930,9 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// TODO: If we decided to blame ourselves (or one of our channels) in
// process_onion_failure we should close that channel as it implies our
// next-hop is needlessly blaming us!
if let Some(scid) = short_channel_id {
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
}
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
Expand DownExpand Up@@ -3959,14 +3962,16 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
// ChannelDetails.
// TODO: For non-temporary failures, we really should be closing the
// channel here as we apparently can't relay through them anyway.
let scid = path.first().unwrap().short_channel_id;
retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
events::Event::PaymentPathFailed {
payment_id: Some(payment_id),
payment_hash: payment_hash.clone(),
rejected_by_dest: path.len() == 1,
network_update: None,
all_paths_failed,
path: path.clone(),
short_channel_id: Some(path.first().unwrap().short_channel_id),
short_channel_id: Some(scid),
retry,
#[cfg(test)]
error_code: Some(*failure_code),
Expand Down
5 changes: 4 additions & 1 deletion lightning/src/ln/functional_test_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1492,7 +1492,7 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
let mut events = node.node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
let expected_payment_id = match events.pop().unwrap() {
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update,
Event::PaymentPathFailed { payment_hash, rejected_by_dest, path, retry, payment_id, network_update, short_channel_id,
#[cfg(test)]
error_code,
#[cfg(test)]
Expand All@@ -1502,6 +1502,9 @@ pub fn expect_payment_failed_conditions<'a, 'b, 'c, 'd, 'e>(
assert!(retry.is_some(), "expected retry.is_some()");
assert_eq!(retry.as_ref().unwrap().final_value_msat, path.last().unwrap().fee_msat, "Retry amount should match last hop in path");
assert_eq!(retry.as_ref().unwrap().payment_params.payee_pubkey, path.last().unwrap().pubkey, "Retry payee node_id should match last hop in path");
if let Some(scid) = short_channel_id {
assert!(retry.as_ref().unwrap().payment_params.previously_failed_channels.contains(&scid));
}

#[cfg(test)]
{
Expand Down
54 changes: 48 additions & 6 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -240,6 +240,11 @@ pub struct PaymentParameters {
///
/// Default value: 1
pub max_channel_saturation_power_of_half: u8,

/// A list of SCIDs which this payment was previously attempted over and which caused the
/// payment to fail. Future attempts for the same payment shouldn't be relayed through any of
/// these SCIDs.
pub previously_failed_channels: Vec<u64>,
}

impl_writeable_tlv_based!(PaymentParameters, {
Expand All@@ -250,6 +255,7 @@ impl_writeable_tlv_based!(PaymentParameters, {
(4, route_hints, vec_type),
(5, max_channel_saturation_power_of_half, (default_value, 1)),
(6, expiry_time, option),
(7, previously_failed_channels, vec_type),
});

impl PaymentParameters {
Expand All@@ -263,6 +269,7 @@ impl PaymentParameters {
max_total_cltv_expiry_delta: DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA,
max_path_count: DEFAULT_MAX_PATH_COUNT,
max_channel_saturation_power_of_half: 1,
previously_failed_channels: Vec::new(),
}
}

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

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

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

let payment_failed_on_this_channel =
payment_params.previously_failed_channels.contains(&short_channel_id);

// If HTLC minimum is larger than the amount we're going to transfer, we shouldn't
// bother considering this channel. If retrying with recommended_value_msat may
// allow us to hit the HTLC minimum limit, set htlc_minimum_limit so that we go
// around again with a higher amount.
if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && may_overpay_to_meet_path_minimum_msat {
if !contributes_sufficient_value || exceeds_max_path_length ||
exceeds_cltv_delta_limit || payment_failed_on_this_channel {
// Path isn't useful, ignore it and move on.
} else if may_overpay_to_meet_path_minimum_msat {
hit_minimum_limit = true;
} else if contributes_sufficient_value && doesnt_exceed_max_path_length &&
doesnt_exceed_cltv_delta_limit && over_path_minimum_msat {
} else if over_path_minimum_msat {
// Note that low contribution here (limited by available_liquidity_msat)
// might violate htlc_minimum_msat on the hops which are next along the
// payment path (upstream to the payee). To avoid that, we recompute
Expand DownExpand Up@@ -1993,6 +2004,8 @@ mod tests {
use prelude::*;
use sync::{self, Arc};

use core::convert::TryInto;

fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey,
features: InitFeatures, outbound_capacity_msat: u64) -> channelmanager::ChannelDetails {
channelmanager::ChannelDetails {
Expand DownExpand Up@@ -5573,6 +5586,35 @@ mod tests {
}
}

#[test]
fn avoids_recently_failed_paths() {
// Ensure that the router always avoids all of the `previously_failed_channels` channels by
// randomly inserting channels into it until we can't find a route anymore.
let (secp_ctx, network, _, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
let network_graph = network.read_only();

let scorer = test_utils::TestScorer::with_penalty(0);
let mut payment_params = PaymentParameters::from_node_id(nodes[6]).with_route_hints(last_hops(&nodes))
.with_max_path_count(1);
let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
let random_seed_bytes = keys_manager.get_secure_random_bytes();

// We should be able to find a route initially, and then after we fail a few random
// channels eventually we won't be able to any longer.
assert!(get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes).is_ok());
loop {
if let Ok(route) = get_route(&our_id, &payment_params, &network_graph, None, 100, 0, Arc::clone(&logger), &scorer, &random_seed_bytes) {
Comment thread
TheBlueMatt marked this conversation as resolved.
for chan in route.paths[0].iter() {
assert!(!payment_params.previously_failed_channels.contains(&chan.short_channel_id));
}
let victim = (u64::from_ne_bytes(random_seed_bytes[0..8].try_into().unwrap()) as usize)
Comment thread
tnull marked this conversation as resolved.
% route.paths[0].len();
payment_params.previously_failed_channels.push(route.paths[0][victim].short_channel_id);
} else { break; }
}
}

#[test]
fn limits_path_length() {
let (secp_ctx, network, _, _, logger) = build_line_graph();
Expand Down
60 changes: 42 additions & 18 deletions lightning/src/routing/scoring.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -394,6 +394,25 @@ pub struct ProbabilisticScoringParameters {
///
/// Default value: 250 msat
pub anti_probing_penalty_msat: u64,

/// This penalty is applied when the amount we're attempting to send over a channel exceeds our
/// current estimate of the channel's available liquidity.
///
/// Note that in this case all other penalties, including the
/// [`liquidity_penalty_multiplier_msat`] and [`amount_penalty_multiplier_msat`]-based
/// penalties, as well as the [`base_penalty_msat`] and the [`anti_probing_penalty_msat`], if
/// applicable, are still included in the overall penalty.
///
/// If you wish to avoid creating paths with such channels entirely, setting this to a value of
/// `u64::max_value()` will guarantee that.
///
/// Default value: 1_0000_0000_000 msat (1 Bitcoin)
Comment thread
tnull marked this conversation as resolved.
///
/// [`liquidity_penalty_multiplier_msat`]: Self::liquidity_penalty_multiplier_msat
/// [`amount_penalty_multiplier_msat`]: Self::amount_penalty_multiplier_msat
/// [`base_penalty_msat`]: Self::base_penalty_msat
/// [`anti_probing_penalty_msat`]: Self::anti_probing_penalty_msat
pub considered_impossible_penalty_msat: u64,
}

/// Accounting for channel liquidity balance uncertainty.
Expand DownExpand Up@@ -522,6 +541,7 @@ impl ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 0,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 0,
considered_impossible_penalty_msat: 0,
}
}

Expand All@@ -543,6 +563,7 @@ impl Default for ProbabilisticScoringParameters {
amount_penalty_multiplier_msat: 256,
manual_node_penalties: HashMap::new(),
anti_probing_penalty_msat: 250,
considered_impossible_penalty_msat: 1_0000_0000_000,
Comment thread
tnull marked this conversation as resolved.
}
}
}
Expand DownExpand Up@@ -620,17 +641,12 @@ impl<L: Deref<Target = u64>, T: Time, U: Deref<Target = T>> DirectedChannelLiqui
if amount_msat <= min_liquidity_msat {
0
} else if amount_msat >= max_liquidity_msat {
if amount_msat > max_liquidity_msat {
u64::max_value()
} else if max_liquidity_msat != self.capacity_msat {
// Avoid using the failed channel on retry.
u64::max_value()
} else {
// Equivalent to hitting the else clause below with the amount equal to the
// effective capacity and without any certainty on the liquidity upper bound.
let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
}
// Equivalent to hitting the else clause below with the amount equal to the effective
// capacity and without any certainty on the liquidity upper bound, plus the
// impossibility penalty.
Comment on lines +644 to +646

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment needs to be updated now since it's not only when equal to the effective capacity. Seems like the earlier change that added this now removed logic is what caused calculates_log10_without_overflowing_u64_max_value to not exercise the correct code path as mentioned earlier on that test (i.e., it doesn't hit the negative_log10_times_2048 case below.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read the comment differently - I read the comment to say "the calculation we're doing here is equivalent to..." which is still true, no?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, you're right!

let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
.saturating_add(params.considered_impossible_penalty_msat)
Comment on lines +647 to +649

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to do the max of these two rather than adding? Is the idea that we want this to be >= anything given in the else clause?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that's the idea. I suppose we could do a max. Originally I had it not adding the combined_penalty call at all but that seemed to brittle to deal with so switched to adding. I don't honestly have a strong opinion between adding and max, either way the docs can tell users what's going on, but for max i kinda worry users will acidentally set it too low and get no penalty here, which seems strange?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They would also need to set the other params lower, though, since max would select the combined penalty over the considered_impossible_penalty_msat if the latter were too low. So it's all kinda relative. Don't feel too strongly either though note that max may make debugging a little easier as otherwise you may need to mentally subtract some combined penalty if it is not obvious.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong? I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sure, I just meant it'd be easy for a user to look at the field and think "okay, let me pick something a bit higher than the liquidity offset, forget to multiply by 2 or whatever, and end up with a penalty equal to the liquidity penalty, which seems wrong?

Plus base and amount penalty, FWIW.

I dunno, I'm happy to mentally convert when we're debugging. Unless you feel strongly I'd suggest we leave it.

Sure we can leave it. Though one thing I just realized is that either way now the penalty will be variable across channels depending on the amount. Maybe less so when using max but maybe that's an argument in favor of adding. That way if left to choose only channels exceeding the maximum liquidity, we'd prefer ones that would otherwise be penalized less.

} else {
let numerator = (max_liquidity_msat - amount_msat).saturating_add(1);
let denominator = (max_liquidity_msat - min_liquidity_msat).saturating_add(1);
Expand DownExpand Up@@ -1624,7 +1640,7 @@ mod tests {
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 102_400, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 47);
let usage = ChannelUsage { amount_msat: 1_024_000, ..usage };
let usage = ChannelUsage { amount_msat: 1_023_999, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

let usage = ChannelUsage {
Expand DownExpand Up@@ -1654,6 +1670,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger)
Expand DownExpand Up@@ -1745,6 +1762,7 @@ mod tests {
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand DownExpand Up@@ -1811,6 +1829,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
Expand All@@ -1820,10 +1839,10 @@ mod tests {
let usage = ChannelUsage {
amount_msat: 0,
inflight_htlc_msat: 0,
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_000) },
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_024) },
};
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);

scorer.payment_path_failed(&payment_path_for_amount(768).iter().collect::<Vec<_>>(), 42);
Expand DownExpand Up@@ -1867,20 +1886,20 @@ mod tests {
let usage = ChannelUsage { amount_msat: 1_023, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

// Fully decay liquidity upper bound.
SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());

SinceEpoch::advance(Duration::from_secs(10));
let usage = ChannelUsage { amount_msat: 0, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 0);
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());
}

#[test]
Expand DownExpand Up@@ -1965,6 +1984,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2001,6 +2021,7 @@ mod tests {
let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params.clone(), &network_graph, &logger);
Expand DownExpand Up@@ -2171,7 +2192,10 @@ mod tests {
fn accounts_for_inflight_htlc_usage() {
let logger = TestLogger::new();
let network_graph = network_graph(&logger);
let params = ProbabilisticScoringParameters::default();
let params = ProbabilisticScoringParameters {
considered_impossible_penalty_msat: u64::max_value(),
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
let source = source_node_id();
let target = target_node_id();
Expand Down