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
30 changes: 28 additions & 2 deletions lightning/src/ln/onion_route_tests.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,8 +645,34 @@ fn test_onion_failure() {
}, || nodes[2].node.fail_htlc_backwards(&payment_hash), false, None,
Some(NetworkUpdate::NodeFailure { node_id: route.paths[0].hops[1].pubkey, is_permanent: true }),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in UPDATE onion failure", 200, &nodes,
&route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
run_onion_failure_test_with_fail_intercept("0-length channel update in intermediate node UPDATE onion failure",
100, &nodes, &route, &payment_hash, &payment_secret, |msg| {
msg.amount_msat -= 1;
}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
failuremsg: vec![
0x10, 0x7, // UPDATE|7
0x0, 0x0 // 0-len channel update
],
pad: vec![0; 255 - 4 /* 4-byte error message */],
hmac: [0; 32],
};
let um = onion_utils::gen_um_from_shared_secret(&onion_keys[0].shared_secret.as_ref());
let mut hmac = HmacEngine::<Sha256>::new(&um);
hmac.input(&decoded_err_packet.encode()[32..]);
decoded_err_packet.hmac = Hmac::from_engine(hmac).into_inner();
msg.reason = onion_utils::encrypt_failure_packet(
&onion_keys[0].shared_secret.as_ref(), &decoded_err_packet.encode()[..])
}, || {}, true, Some(0x1000|7),
Some(NetworkUpdate::ChannelFailure {
short_channel_id: channels[1].0.contents.short_channel_id,
is_permanent: false,
}),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in final node UPDATE onion failure",
200, &nodes, &route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
Expand Down
62 changes: 47 additions & 15 deletions lightning/src/ln/onion_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -426,7 +426,7 @@ pub(super) fn build_first_hop_failure_packet(shared_secret: &[u8], failure_type:
pub(crate) struct DecodedOnionFailure {
pub(crate) network_update: Option<NetworkUpdate>,
pub(crate) short_channel_id: Option<u64>,
pub(crate) payment_retryable: bool,
pub(crate) payment_failed_permanently: bool,
#[cfg(test)]
pub(crate) onion_error_code: Option<u16>,
#[cfg(test)]
Expand All@@ -444,7 +444,14 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} = htlc_source {
(path, session_priv, first_hop_htlc_msat)
} else { unreachable!() };
let mut res = None;

// Learnings from the HTLC failure to inform future payment retries and scoring.
struct FailureLearnings {
network_update: Option<NetworkUpdate>,
short_channel_id: Option<u64>,
payment_failed_permanently: bool,
}
let mut res: Option<FailureLearnings> = None;
let mut htlc_msat = *first_hop_htlc_msat;
let mut error_code_ret = None;
let mut error_packet_ret = None;
Expand All@@ -467,11 +474,33 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// Got an error from within a blinded route.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
is_from_final_node = false;
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
},
};

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
let num_blinded_hops = path.blinded_tail.as_ref().map_or(0, |bt| bt.hops.len());
// For 1-hop blinded paths, the final `path.hops` entry is the recipient.
is_from_final_node = route_hop_idx + 1 == path.hops.len() && num_blinded_hops <= 1;
let failing_route_hop = if is_from_final_node { route_hop } else {
match path.hops.get(route_hop_idx + 1) {
Some(hop) => hop,
None => {
// The failing hop is within a multi-hop blinded path.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
}
}
};

let amt_to_forward = htlc_msat - route_hop.fee_msat;
htlc_msat = amt_to_forward;

Expand All@@ -483,11 +512,6 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
chacha.process(&packet_decrypted, &mut decryption_tmp[..]);
packet_decrypted = decryption_tmp;

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
is_from_final_node = route_hop_idx + 1 == path.hops.len();
let failing_route_hop = if is_from_final_node { route_hop } else { &path.hops[route_hop_idx + 1] };

let err_packet = match msgs::DecodedOnionErrorPacket::read(&mut Cursor::new(&packet_decrypted)) {
Ok(p) => p,
Err(_) => return
Expand All@@ -507,7 +531,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
is_permanent: true,
});
let short_channel_id = Some(route_hop.short_channel_id);
res = Some((network_update, short_channel_id, !is_from_final_node));
res = Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently: is_from_final_node
});
return
}
};
Expand DownExpand Up@@ -615,8 +641,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} else {
// The node in question intentionally encoded a 0-length channel update. This is
// likely due to https://github.com/ElementsProject/lightning/issues/6200.
short_channel_id = Some(failing_route_hop.short_channel_id);
network_update = Some(NetworkUpdate::ChannelFailure {
short_channel_id: route_hop.short_channel_id,
short_channel_id: failing_route_hop.short_channel_id,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should remove the network update entirely. Even with perm set to false this will still mark the channel disabled even though it isn't.

@valentinewallacevalentinewallaceSep 20, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We only mark the channel as disabled if is_permanent is set. It's nice to set it because then we can more easily keep the behavior of defaulting to NetworkUpdate::NodeFailure on a totally bogus channel update.

is_permanent: false,
});
}
Expand DownExpand Up@@ -659,7 +686,10 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
short_channel_id = Some(route_hop.short_channel_id);
}

res = Some((network_update, short_channel_id, !(error_code & PERM == PERM && is_from_final_node)));
res = Some(FailureLearnings {
network_update, short_channel_id,
payment_failed_permanently: error_code & PERM == PERM && is_from_final_node
});

let (description, title) = errors::get_onion_error_description(error_code);
if debug_field_size > 0 && err_packet.failuremsg.len() >= 4 + debug_field_size {
Expand All@@ -668,9 +698,11 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
log_info!(logger, "Onion Error[from {}: {}({:#x})] {}", route_hop.pubkey, title, error_code, description);
}
}).expect("Route that we sent via spontaneously grew invalid keys in the middle of it?");
if let Some((network_update, short_channel_id, payment_retryable)) = res {
if let Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently
}) = res {
DecodedOnionFailure {
network_update, short_channel_id, payment_retryable,
network_update, short_channel_id, payment_failed_permanently,
#[cfg(test)]
onion_error_code: error_code_ret,
#[cfg(test)]
Expand All@@ -680,7 +712,7 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// only not set either packet unparseable or hmac does not match with any
// payment not retryable only when garbage is from the final node
DecodedOnionFailure {
network_update: None, short_channel_id: None, payment_retryable: !is_from_final_node,
network_update: None, short_channel_id: None, payment_failed_permanently: is_from_final_node,
#[cfg(test)]
onion_error_code: None,
#[cfg(test)]
Expand DownExpand Up@@ -824,7 +856,7 @@ impl HTLCFailReason {
if let &HTLCSource::OutboundRoute { ref path, .. } = htlc_source {
DecodedOnionFailure {
network_update: None,
payment_retryable: true,
payment_failed_permanently: false,
short_channel_id: Some(path.hops[0].short_channel_id),
#[cfg(test)]
onion_error_code: Some(*failure_code),
Expand Down
13 changes: 7 additions & 6 deletions lightning/src/ln/outbound_payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1484,10 +1484,11 @@ impl OutboundPayments {
) -> bool where L::Target: Logger {
#[cfg(test)]
let DecodedOnionFailure {
network_update, short_channel_id, payment_retryable, onion_error_code, onion_error_data
network_update, short_channel_id, payment_failed_permanently, onion_error_code,
onion_error_data
} = onion_error.decode_onion_failure(secp_ctx, logger, &source);
#[cfg(not(test))]
let DecodedOnionFailure { network_update, short_channel_id, payment_retryable } =
let DecodedOnionFailure { network_update, short_channel_id, payment_failed_permanently } =
onion_error.decode_onion_failure(secp_ctx, logger, &source);

let payment_is_probe = payment_is_probe(payment_hash, &payment_id, probing_cookie_secret);
Expand DownExpand Up@@ -1528,8 +1529,8 @@ impl OutboundPayments {
payment.get_mut().insert_previously_failed_scid(scid);
}

if payment_is_probe || !is_retryable_now || !payment_retryable {
let reason = if !payment_retryable {
if payment_is_probe || !is_retryable_now || payment_failed_permanently {
let reason = if payment_failed_permanently {
PaymentFailureReason::RecipientRejected
} else {
PaymentFailureReason::RetriesExhausted
Expand DownExpand Up@@ -1559,7 +1560,7 @@ impl OutboundPayments {

let path_failure = {
if payment_is_probe {
if !payment_retryable {
if payment_failed_permanently {
events::Event::ProbeSuccessful {
payment_id: *payment_id,
payment_hash: payment_hash.clone(),
Expand All@@ -1583,7 +1584,7 @@ impl OutboundPayments {
events::Event::PaymentPathFailed {
payment_id: Some(*payment_id),
payment_hash: payment_hash.clone(),
payment_failed_permanently: !payment_retryable,
payment_failed_permanently,
failure: events::PathFailure::OnPath { network_update },
path: path.clone(),
short_channel_id,
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
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;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Fix `PaymentPathFailed::payment_failed_permanently` on blinded path fail by valentinewallace · Pull Request #2576 · lightningdevkit/rust-lightning · GitHub
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
30 changes: 28 additions & 2 deletions lightning/src/ln/onion_route_tests.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,8 +645,34 @@ fn test_onion_failure() {
}, || nodes[2].node.fail_htlc_backwards(&payment_hash), false, None,
Some(NetworkUpdate::NodeFailure { node_id: route.paths[0].hops[1].pubkey, is_permanent: true }),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in UPDATE onion failure", 200, &nodes,
&route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
run_onion_failure_test_with_fail_intercept("0-length channel update in intermediate node UPDATE onion failure",
100, &nodes, &route, &payment_hash, &payment_secret, |msg| {
msg.amount_msat -= 1;
}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
failuremsg: vec![
0x10, 0x7, // UPDATE|7
0x0, 0x0 // 0-len channel update
],
pad: vec![0; 255 - 4 /* 4-byte error message */],
hmac: [0; 32],
};
let um = onion_utils::gen_um_from_shared_secret(&onion_keys[0].shared_secret.as_ref());
let mut hmac = HmacEngine::<Sha256>::new(&um);
hmac.input(&decoded_err_packet.encode()[32..]);
decoded_err_packet.hmac = Hmac::from_engine(hmac).into_inner();
msg.reason = onion_utils::encrypt_failure_packet(
&onion_keys[0].shared_secret.as_ref(), &decoded_err_packet.encode()[..])
}, || {}, true, Some(0x1000|7),
Some(NetworkUpdate::ChannelFailure {
short_channel_id: channels[1].0.contents.short_channel_id,
is_permanent: false,
}),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in final node UPDATE onion failure",
200, &nodes, &route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
Expand Down
62 changes: 47 additions & 15 deletions lightning/src/ln/onion_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -426,7 +426,7 @@ pub(super) fn build_first_hop_failure_packet(shared_secret: &[u8], failure_type:
pub(crate) struct DecodedOnionFailure {
pub(crate) network_update: Option<NetworkUpdate>,
pub(crate) short_channel_id: Option<u64>,
pub(crate) payment_retryable: bool,
pub(crate) payment_failed_permanently: bool,
#[cfg(test)]
pub(crate) onion_error_code: Option<u16>,
#[cfg(test)]
Expand All@@ -444,7 +444,14 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} = htlc_source {
(path, session_priv, first_hop_htlc_msat)
} else { unreachable!() };
let mut res = None;

// Learnings from the HTLC failure to inform future payment retries and scoring.
struct FailureLearnings {
network_update: Option<NetworkUpdate>,
short_channel_id: Option<u64>,
payment_failed_permanently: bool,
}
let mut res: Option<FailureLearnings> = None;
let mut htlc_msat = *first_hop_htlc_msat;
let mut error_code_ret = None;
let mut error_packet_ret = None;
Expand All@@ -467,11 +474,33 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// Got an error from within a blinded route.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
is_from_final_node = false;
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
},
};

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
let num_blinded_hops = path.blinded_tail.as_ref().map_or(0, |bt| bt.hops.len());
// For 1-hop blinded paths, the final `path.hops` entry is the recipient.
is_from_final_node = route_hop_idx + 1 == path.hops.len() && num_blinded_hops <= 1;
let failing_route_hop = if is_from_final_node { route_hop } else {
match path.hops.get(route_hop_idx + 1) {
Some(hop) => hop,
None => {
// The failing hop is within a multi-hop blinded path.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
}
}
};

let amt_to_forward = htlc_msat - route_hop.fee_msat;
htlc_msat = amt_to_forward;

Expand All@@ -483,11 +512,6 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
chacha.process(&packet_decrypted, &mut decryption_tmp[..]);
packet_decrypted = decryption_tmp;

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
is_from_final_node = route_hop_idx + 1 == path.hops.len();
let failing_route_hop = if is_from_final_node { route_hop } else { &path.hops[route_hop_idx + 1] };

let err_packet = match msgs::DecodedOnionErrorPacket::read(&mut Cursor::new(&packet_decrypted)) {
Ok(p) => p,
Err(_) => return
Expand All@@ -507,7 +531,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
is_permanent: true,
});
let short_channel_id = Some(route_hop.short_channel_id);
res = Some((network_update, short_channel_id, !is_from_final_node));
res = Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently: is_from_final_node
});
return
}
};
Expand DownExpand Up@@ -615,8 +641,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} else {
// The node in question intentionally encoded a 0-length channel update. This is
// likely due to https://github.com/ElementsProject/lightning/issues/6200.
short_channel_id = Some(failing_route_hop.short_channel_id);
network_update = Some(NetworkUpdate::ChannelFailure {
short_channel_id: route_hop.short_channel_id,
short_channel_id: failing_route_hop.short_channel_id,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should remove the network update entirely. Even with perm set to false this will still mark the channel disabled even though it isn't.

@valentinewallacevalentinewallaceSep 20, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We only mark the channel as disabled if is_permanent is set. It's nice to set it because then we can more easily keep the behavior of defaulting to NetworkUpdate::NodeFailure on a totally bogus channel update.

is_permanent: false,
});
}
Expand DownExpand Up@@ -659,7 +686,10 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
short_channel_id = Some(route_hop.short_channel_id);
}

res = Some((network_update, short_channel_id, !(error_code & PERM == PERM && is_from_final_node)));
res = Some(FailureLearnings {
network_update, short_channel_id,
payment_failed_permanently: error_code & PERM == PERM && is_from_final_node
});

let (description, title) = errors::get_onion_error_description(error_code);
if debug_field_size > 0 && err_packet.failuremsg.len() >= 4 + debug_field_size {
Expand All@@ -668,9 +698,11 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
log_info!(logger, "Onion Error[from {}: {}({:#x})] {}", route_hop.pubkey, title, error_code, description);
}
}).expect("Route that we sent via spontaneously grew invalid keys in the middle of it?");
if let Some((network_update, short_channel_id, payment_retryable)) = res {
if let Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently
}) = res {
DecodedOnionFailure {
network_update, short_channel_id, payment_retryable,
network_update, short_channel_id, payment_failed_permanently,
#[cfg(test)]
onion_error_code: error_code_ret,
#[cfg(test)]
Expand All@@ -680,7 +712,7 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// only not set either packet unparseable or hmac does not match with any
// payment not retryable only when garbage is from the final node
DecodedOnionFailure {
network_update: None, short_channel_id: None, payment_retryable: !is_from_final_node,
network_update: None, short_channel_id: None, payment_failed_permanently: is_from_final_node,
#[cfg(test)]
onion_error_code: None,
#[cfg(test)]
Expand DownExpand Up@@ -824,7 +856,7 @@ impl HTLCFailReason {
if let &HTLCSource::OutboundRoute { ref path, .. } = htlc_source {
DecodedOnionFailure {
network_update: None,
payment_retryable: true,
payment_failed_permanently: false,
short_channel_id: Some(path.hops[0].short_channel_id),
#[cfg(test)]
onion_error_code: Some(*failure_code),
Expand Down
13 changes: 7 additions & 6 deletions lightning/src/ln/outbound_payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1484,10 +1484,11 @@ impl OutboundPayments {
) -> bool where L::Target: Logger {
#[cfg(test)]
let DecodedOnionFailure {
network_update, short_channel_id, payment_retryable, onion_error_code, onion_error_data
network_update, short_channel_id, payment_failed_permanently, onion_error_code,
onion_error_data
} = onion_error.decode_onion_failure(secp_ctx, logger, &source);
#[cfg(not(test))]
let DecodedOnionFailure { network_update, short_channel_id, payment_retryable } =
let DecodedOnionFailure { network_update, short_channel_id, payment_failed_permanently } =
onion_error.decode_onion_failure(secp_ctx, logger, &source);

let payment_is_probe = payment_is_probe(payment_hash, &payment_id, probing_cookie_secret);
Expand DownExpand Up@@ -1528,8 +1529,8 @@ impl OutboundPayments {
payment.get_mut().insert_previously_failed_scid(scid);
}

if payment_is_probe || !is_retryable_now || !payment_retryable {
let reason = if !payment_retryable {
if payment_is_probe || !is_retryable_now || payment_failed_permanently {
let reason = if payment_failed_permanently {
PaymentFailureReason::RecipientRejected
} else {
PaymentFailureReason::RetriesExhausted
Expand DownExpand Up@@ -1559,7 +1560,7 @@ impl OutboundPayments {

let path_failure = {
if payment_is_probe {
if !payment_retryable {
if payment_failed_permanently {
events::Event::ProbeSuccessful {
payment_id: *payment_id,
payment_hash: payment_hash.clone(),
Expand All@@ -1583,7 +1584,7 @@ impl OutboundPayments {
events::Event::PaymentPathFailed {
payment_id: Some(*payment_id),
payment_hash: payment_hash.clone(),
payment_failed_permanently: !payment_retryable,
payment_failed_permanently,
failure: events::PathFailure::OnPath { network_update },
path: path.clone(),
short_channel_id,
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix `PaymentPathFailed::payment_failed_permanently` on blinded path fail by valentinewallace · Pull Request #2576 · lightningdevkit/rust-lightning · GitHub
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
30 changes: 28 additions & 2 deletions lightning/src/ln/onion_route_tests.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,8 +645,34 @@ fn test_onion_failure() {
}, || nodes[2].node.fail_htlc_backwards(&payment_hash), false, None,
Some(NetworkUpdate::NodeFailure { node_id: route.paths[0].hops[1].pubkey, is_permanent: true }),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in UPDATE onion failure", 200, &nodes,
&route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
run_onion_failure_test_with_fail_intercept("0-length channel update in intermediate node UPDATE onion failure",
100, &nodes, &route, &payment_hash, &payment_secret, |msg| {
msg.amount_msat -= 1;
}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
failuremsg: vec![
0x10, 0x7, // UPDATE|7
0x0, 0x0 // 0-len channel update
],
pad: vec![0; 255 - 4 /* 4-byte error message */],
hmac: [0; 32],
};
let um = onion_utils::gen_um_from_shared_secret(&onion_keys[0].shared_secret.as_ref());
let mut hmac = HmacEngine::<Sha256>::new(&um);
hmac.input(&decoded_err_packet.encode()[32..]);
decoded_err_packet.hmac = Hmac::from_engine(hmac).into_inner();
msg.reason = onion_utils::encrypt_failure_packet(
&onion_keys[0].shared_secret.as_ref(), &decoded_err_packet.encode()[..])
}, || {}, true, Some(0x1000|7),
Some(NetworkUpdate::ChannelFailure {
short_channel_id: channels[1].0.contents.short_channel_id,
is_permanent: false,
}),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in final node UPDATE onion failure",
200, &nodes, &route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
Expand Down
62 changes: 47 additions & 15 deletions lightning/src/ln/onion_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -426,7 +426,7 @@ pub(super) fn build_first_hop_failure_packet(shared_secret: &[u8], failure_type:
pub(crate) struct DecodedOnionFailure {
pub(crate) network_update: Option<NetworkUpdate>,
pub(crate) short_channel_id: Option<u64>,
pub(crate) payment_retryable: bool,
pub(crate) payment_failed_permanently: bool,
#[cfg(test)]
pub(crate) onion_error_code: Option<u16>,
#[cfg(test)]
Expand All@@ -444,7 +444,14 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} = htlc_source {
(path, session_priv, first_hop_htlc_msat)
} else { unreachable!() };
let mut res = None;

// Learnings from the HTLC failure to inform future payment retries and scoring.
struct FailureLearnings {
network_update: Option<NetworkUpdate>,
short_channel_id: Option<u64>,
payment_failed_permanently: bool,
}
let mut res: Option<FailureLearnings> = None;
let mut htlc_msat = *first_hop_htlc_msat;
let mut error_code_ret = None;
let mut error_packet_ret = None;
Expand All@@ -467,11 +474,33 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// Got an error from within a blinded route.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
is_from_final_node = false;
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
},
};

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
let num_blinded_hops = path.blinded_tail.as_ref().map_or(0, |bt| bt.hops.len());
// For 1-hop blinded paths, the final `path.hops` entry is the recipient.
is_from_final_node = route_hop_idx + 1 == path.hops.len() && num_blinded_hops <= 1;
let failing_route_hop = if is_from_final_node { route_hop } else {
match path.hops.get(route_hop_idx + 1) {
Some(hop) => hop,
None => {
// The failing hop is within a multi-hop blinded path.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
}
}
};

let amt_to_forward = htlc_msat - route_hop.fee_msat;
htlc_msat = amt_to_forward;

Expand All@@ -483,11 +512,6 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
chacha.process(&packet_decrypted, &mut decryption_tmp[..]);
packet_decrypted = decryption_tmp;

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
is_from_final_node = route_hop_idx + 1 == path.hops.len();
let failing_route_hop = if is_from_final_node { route_hop } else { &path.hops[route_hop_idx + 1] };

let err_packet = match msgs::DecodedOnionErrorPacket::read(&mut Cursor::new(&packet_decrypted)) {
Ok(p) => p,
Err(_) => return
Expand All@@ -507,7 +531,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
is_permanent: true,
});
let short_channel_id = Some(route_hop.short_channel_id);
res = Some((network_update, short_channel_id, !is_from_final_node));
res = Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently: is_from_final_node
});
return
}
};
Expand DownExpand Up@@ -615,8 +641,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} else {
// The node in question intentionally encoded a 0-length channel update. This is
// likely due to https://github.com/ElementsProject/lightning/issues/6200.
short_channel_id = Some(failing_route_hop.short_channel_id);
network_update = Some(NetworkUpdate::ChannelFailure {
short_channel_id: route_hop.short_channel_id,
short_channel_id: failing_route_hop.short_channel_id,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should remove the network update entirely. Even with perm set to false this will still mark the channel disabled even though it isn't.

@valentinewallacevalentinewallaceSep 20, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We only mark the channel as disabled if is_permanent is set. It's nice to set it because then we can more easily keep the behavior of defaulting to NetworkUpdate::NodeFailure on a totally bogus channel update.

is_permanent: false,
});
}
Expand DownExpand Up@@ -659,7 +686,10 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
short_channel_id = Some(route_hop.short_channel_id);
}

res = Some((network_update, short_channel_id, !(error_code & PERM == PERM && is_from_final_node)));
res = Some(FailureLearnings {
network_update, short_channel_id,
payment_failed_permanently: error_code & PERM == PERM && is_from_final_node
});

let (description, title) = errors::get_onion_error_description(error_code);
if debug_field_size > 0 && err_packet.failuremsg.len() >= 4 + debug_field_size {
Expand All@@ -668,9 +698,11 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
log_info!(logger, "Onion Error[from {}: {}({:#x})] {}", route_hop.pubkey, title, error_code, description);
}
}).expect("Route that we sent via spontaneously grew invalid keys in the middle of it?");
if let Some((network_update, short_channel_id, payment_retryable)) = res {
if let Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently
}) = res {
DecodedOnionFailure {
network_update, short_channel_id, payment_retryable,
network_update, short_channel_id, payment_failed_permanently,
#[cfg(test)]
onion_error_code: error_code_ret,
#[cfg(test)]
Expand All@@ -680,7 +712,7 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// only not set either packet unparseable or hmac does not match with any
// payment not retryable only when garbage is from the final node
DecodedOnionFailure {
network_update: None, short_channel_id: None, payment_retryable: !is_from_final_node,
network_update: None, short_channel_id: None, payment_failed_permanently: is_from_final_node,
#[cfg(test)]
onion_error_code: None,
#[cfg(test)]
Expand DownExpand Up@@ -824,7 +856,7 @@ impl HTLCFailReason {
if let &HTLCSource::OutboundRoute { ref path, .. } = htlc_source {
DecodedOnionFailure {
network_update: None,
payment_retryable: true,
payment_failed_permanently: false,
short_channel_id: Some(path.hops[0].short_channel_id),
#[cfg(test)]
onion_error_code: Some(*failure_code),
Expand Down
13 changes: 7 additions & 6 deletions lightning/src/ln/outbound_payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1484,10 +1484,11 @@ impl OutboundPayments {
) -> bool where L::Target: Logger {
#[cfg(test)]
let DecodedOnionFailure {
network_update, short_channel_id, payment_retryable, onion_error_code, onion_error_data
network_update, short_channel_id, payment_failed_permanently, onion_error_code,
onion_error_data
} = onion_error.decode_onion_failure(secp_ctx, logger, &source);
#[cfg(not(test))]
let DecodedOnionFailure { network_update, short_channel_id, payment_retryable } =
let DecodedOnionFailure { network_update, short_channel_id, payment_failed_permanently } =
onion_error.decode_onion_failure(secp_ctx, logger, &source);

let payment_is_probe = payment_is_probe(payment_hash, &payment_id, probing_cookie_secret);
Expand DownExpand Up@@ -1528,8 +1529,8 @@ impl OutboundPayments {
payment.get_mut().insert_previously_failed_scid(scid);
}

if payment_is_probe || !is_retryable_now || !payment_retryable {
let reason = if !payment_retryable {
if payment_is_probe || !is_retryable_now || payment_failed_permanently {
let reason = if payment_failed_permanently {
PaymentFailureReason::RecipientRejected
} else {
PaymentFailureReason::RetriesExhausted
Expand DownExpand Up@@ -1559,7 +1560,7 @@ impl OutboundPayments {

let path_failure = {
if payment_is_probe {
if !payment_retryable {
if payment_failed_permanently {
events::Event::ProbeSuccessful {
payment_id: *payment_id,
payment_hash: payment_hash.clone(),
Expand All@@ -1583,7 +1584,7 @@ impl OutboundPayments {
events::Event::PaymentPathFailed {
payment_id: Some(*payment_id),
payment_hash: payment_hash.clone(),
payment_failed_permanently: !payment_retryable,
payment_failed_permanently,
failure: events::PathFailure::OnPath { network_update },
path: path.clone(),
short_channel_id,
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix `PaymentPathFailed::payment_failed_permanently` on blinded path fail by valentinewallace · Pull Request #2576 · lightningdevkit/rust-lightning · GitHub
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
30 changes: 28 additions & 2 deletions lightning/src/ln/onion_route_tests.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,8 +645,34 @@ fn test_onion_failure() {
}, || nodes[2].node.fail_htlc_backwards(&payment_hash), false, None,
Some(NetworkUpdate::NodeFailure { node_id: route.paths[0].hops[1].pubkey, is_permanent: true }),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in UPDATE onion failure", 200, &nodes,
&route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
run_onion_failure_test_with_fail_intercept("0-length channel update in intermediate node UPDATE onion failure",
100, &nodes, &route, &payment_hash, &payment_secret, |msg| {
msg.amount_msat -= 1;
}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
failuremsg: vec![
0x10, 0x7, // UPDATE|7
0x0, 0x0 // 0-len channel update
],
pad: vec![0; 255 - 4 /* 4-byte error message */],
hmac: [0; 32],
};
let um = onion_utils::gen_um_from_shared_secret(&onion_keys[0].shared_secret.as_ref());
let mut hmac = HmacEngine::<Sha256>::new(&um);
hmac.input(&decoded_err_packet.encode()[32..]);
decoded_err_packet.hmac = Hmac::from_engine(hmac).into_inner();
msg.reason = onion_utils::encrypt_failure_packet(
&onion_keys[0].shared_secret.as_ref(), &decoded_err_packet.encode()[..])
}, || {}, true, Some(0x1000|7),
Some(NetworkUpdate::ChannelFailure {
short_channel_id: channels[1].0.contents.short_channel_id,
is_permanent: false,
}),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in final node UPDATE onion failure",
200, &nodes, &route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
Expand Down
62 changes: 47 additions & 15 deletions lightning/src/ln/onion_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -426,7 +426,7 @@ pub(super) fn build_first_hop_failure_packet(shared_secret: &[u8], failure_type:
pub(crate) struct DecodedOnionFailure {
pub(crate) network_update: Option<NetworkUpdate>,
pub(crate) short_channel_id: Option<u64>,
pub(crate) payment_retryable: bool,
pub(crate) payment_failed_permanently: bool,
#[cfg(test)]
pub(crate) onion_error_code: Option<u16>,
#[cfg(test)]
Expand All@@ -444,7 +444,14 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} = htlc_source {
(path, session_priv, first_hop_htlc_msat)
} else { unreachable!() };
let mut res = None;

// Learnings from the HTLC failure to inform future payment retries and scoring.
struct FailureLearnings {
network_update: Option<NetworkUpdate>,
short_channel_id: Option<u64>,
payment_failed_permanently: bool,
}
let mut res: Option<FailureLearnings> = None;
let mut htlc_msat = *first_hop_htlc_msat;
let mut error_code_ret = None;
let mut error_packet_ret = None;
Expand All@@ -467,11 +474,33 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// Got an error from within a blinded route.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
is_from_final_node = false;
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
},
};

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
let num_blinded_hops = path.blinded_tail.as_ref().map_or(0, |bt| bt.hops.len());
// For 1-hop blinded paths, the final `path.hops` entry is the recipient.
is_from_final_node = route_hop_idx + 1 == path.hops.len() && num_blinded_hops <= 1;
let failing_route_hop = if is_from_final_node { route_hop } else {
match path.hops.get(route_hop_idx + 1) {
Some(hop) => hop,
None => {
// The failing hop is within a multi-hop blinded path.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
}
}
};

let amt_to_forward = htlc_msat - route_hop.fee_msat;
htlc_msat = amt_to_forward;

Expand All@@ -483,11 +512,6 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
chacha.process(&packet_decrypted, &mut decryption_tmp[..]);
packet_decrypted = decryption_tmp;

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
is_from_final_node = route_hop_idx + 1 == path.hops.len();
let failing_route_hop = if is_from_final_node { route_hop } else { &path.hops[route_hop_idx + 1] };

let err_packet = match msgs::DecodedOnionErrorPacket::read(&mut Cursor::new(&packet_decrypted)) {
Ok(p) => p,
Err(_) => return
Expand All@@ -507,7 +531,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
is_permanent: true,
});
let short_channel_id = Some(route_hop.short_channel_id);
res = Some((network_update, short_channel_id, !is_from_final_node));
res = Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently: is_from_final_node
});
return
}
};
Expand DownExpand Up@@ -615,8 +641,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} else {
// The node in question intentionally encoded a 0-length channel update. This is
// likely due to https://github.com/ElementsProject/lightning/issues/6200.
short_channel_id = Some(failing_route_hop.short_channel_id);
network_update = Some(NetworkUpdate::ChannelFailure {
short_channel_id: route_hop.short_channel_id,
short_channel_id: failing_route_hop.short_channel_id,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should remove the network update entirely. Even with perm set to false this will still mark the channel disabled even though it isn't.

@valentinewallacevalentinewallaceSep 20, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We only mark the channel as disabled if is_permanent is set. It's nice to set it because then we can more easily keep the behavior of defaulting to NetworkUpdate::NodeFailure on a totally bogus channel update.

is_permanent: false,
});
}
Expand DownExpand Up@@ -659,7 +686,10 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
short_channel_id = Some(route_hop.short_channel_id);
}

res = Some((network_update, short_channel_id, !(error_code & PERM == PERM && is_from_final_node)));
res = Some(FailureLearnings {
network_update, short_channel_id,
payment_failed_permanently: error_code & PERM == PERM && is_from_final_node
});

let (description, title) = errors::get_onion_error_description(error_code);
if debug_field_size > 0 && err_packet.failuremsg.len() >= 4 + debug_field_size {
Expand All@@ -668,9 +698,11 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
log_info!(logger, "Onion Error[from {}: {}({:#x})] {}", route_hop.pubkey, title, error_code, description);
}
}).expect("Route that we sent via spontaneously grew invalid keys in the middle of it?");
if let Some((network_update, short_channel_id, payment_retryable)) = res {
if let Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently
}) = res {
DecodedOnionFailure {
network_update, short_channel_id, payment_retryable,
network_update, short_channel_id, payment_failed_permanently,
#[cfg(test)]
onion_error_code: error_code_ret,
#[cfg(test)]
Expand All@@ -680,7 +712,7 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// only not set either packet unparseable or hmac does not match with any
// payment not retryable only when garbage is from the final node
DecodedOnionFailure {
network_update: None, short_channel_id: None, payment_retryable: !is_from_final_node,
network_update: None, short_channel_id: None, payment_failed_permanently: is_from_final_node,
#[cfg(test)]
onion_error_code: None,
#[cfg(test)]
Expand DownExpand Up@@ -824,7 +856,7 @@ impl HTLCFailReason {
if let &HTLCSource::OutboundRoute { ref path, .. } = htlc_source {
DecodedOnionFailure {
network_update: None,
payment_retryable: true,
payment_failed_permanently: false,
short_channel_id: Some(path.hops[0].short_channel_id),
#[cfg(test)]
onion_error_code: Some(*failure_code),
Expand Down
13 changes: 7 additions & 6 deletions lightning/src/ln/outbound_payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1484,10 +1484,11 @@ impl OutboundPayments {
) -> bool where L::Target: Logger {
#[cfg(test)]
let DecodedOnionFailure {
network_update, short_channel_id, payment_retryable, onion_error_code, onion_error_data
network_update, short_channel_id, payment_failed_permanently, onion_error_code,
onion_error_data
} = onion_error.decode_onion_failure(secp_ctx, logger, &source);
#[cfg(not(test))]
let DecodedOnionFailure { network_update, short_channel_id, payment_retryable } =
let DecodedOnionFailure { network_update, short_channel_id, payment_failed_permanently } =
onion_error.decode_onion_failure(secp_ctx, logger, &source);

let payment_is_probe = payment_is_probe(payment_hash, &payment_id, probing_cookie_secret);
Expand DownExpand Up@@ -1528,8 +1529,8 @@ impl OutboundPayments {
payment.get_mut().insert_previously_failed_scid(scid);
}

if payment_is_probe || !is_retryable_now || !payment_retryable {
let reason = if !payment_retryable {
if payment_is_probe || !is_retryable_now || payment_failed_permanently {
let reason = if payment_failed_permanently {
PaymentFailureReason::RecipientRejected
} else {
PaymentFailureReason::RetriesExhausted
Expand DownExpand Up@@ -1559,7 +1560,7 @@ impl OutboundPayments {

let path_failure = {
if payment_is_probe {
if !payment_retryable {
if payment_failed_permanently {
events::Event::ProbeSuccessful {
payment_id: *payment_id,
payment_hash: payment_hash.clone(),
Expand All@@ -1583,7 +1584,7 @@ impl OutboundPayments {
events::Event::PaymentPathFailed {
payment_id: Some(*payment_id),
payment_hash: payment_hash.clone(),
payment_failed_permanently: !payment_retryable,
payment_failed_permanently,
failure: events::PathFailure::OnPath { network_update },
path: path.clone(),
short_channel_id,
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Fix `PaymentPathFailed::payment_failed_permanently` on blinded path fail by valentinewallace · Pull Request #2576 · lightningdevkit/rust-lightning · GitHub
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
30 changes: 28 additions & 2 deletions lightning/src/ln/onion_route_tests.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,8 +645,34 @@ fn test_onion_failure() {
}, || nodes[2].node.fail_htlc_backwards(&payment_hash), false, None,
Some(NetworkUpdate::NodeFailure { node_id: route.paths[0].hops[1].pubkey, is_permanent: true }),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in UPDATE onion failure", 200, &nodes,
&route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
run_onion_failure_test_with_fail_intercept("0-length channel update in intermediate node UPDATE onion failure",
100, &nodes, &route, &payment_hash, &payment_secret, |msg| {
msg.amount_msat -= 1;
}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
failuremsg: vec![
0x10, 0x7, // UPDATE|7
0x0, 0x0 // 0-len channel update
],
pad: vec![0; 255 - 4 /* 4-byte error message */],
hmac: [0; 32],
};
let um = onion_utils::gen_um_from_shared_secret(&onion_keys[0].shared_secret.as_ref());
let mut hmac = HmacEngine::<Sha256>::new(&um);
hmac.input(&decoded_err_packet.encode()[32..]);
decoded_err_packet.hmac = Hmac::from_engine(hmac).into_inner();
msg.reason = onion_utils::encrypt_failure_packet(
&onion_keys[0].shared_secret.as_ref(), &decoded_err_packet.encode()[..])
}, || {}, true, Some(0x1000|7),
Some(NetworkUpdate::ChannelFailure {
short_channel_id: channels[1].0.contents.short_channel_id,
is_permanent: false,
}),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in final node UPDATE onion failure",
200, &nodes, &route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
Expand Down
62 changes: 47 additions & 15 deletions lightning/src/ln/onion_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -426,7 +426,7 @@ pub(super) fn build_first_hop_failure_packet(shared_secret: &[u8], failure_type:
pub(crate) struct DecodedOnionFailure {
pub(crate) network_update: Option<NetworkUpdate>,
pub(crate) short_channel_id: Option<u64>,
pub(crate) payment_retryable: bool,
pub(crate) payment_failed_permanently: bool,
#[cfg(test)]
pub(crate) onion_error_code: Option<u16>,
#[cfg(test)]
Expand All@@ -444,7 +444,14 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} = htlc_source {
(path, session_priv, first_hop_htlc_msat)
} else { unreachable!() };
let mut res = None;

// Learnings from the HTLC failure to inform future payment retries and scoring.
struct FailureLearnings {
network_update: Option<NetworkUpdate>,
short_channel_id: Option<u64>,
payment_failed_permanently: bool,
}
let mut res: Option<FailureLearnings> = None;
let mut htlc_msat = *first_hop_htlc_msat;
let mut error_code_ret = None;
let mut error_packet_ret = None;
Expand All@@ -467,11 +474,33 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// Got an error from within a blinded route.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
is_from_final_node = false;
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
},
};

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
let num_blinded_hops = path.blinded_tail.as_ref().map_or(0, |bt| bt.hops.len());
// For 1-hop blinded paths, the final `path.hops` entry is the recipient.
is_from_final_node = route_hop_idx + 1 == path.hops.len() && num_blinded_hops <= 1;
let failing_route_hop = if is_from_final_node { route_hop } else {
match path.hops.get(route_hop_idx + 1) {
Some(hop) => hop,
None => {
// The failing hop is within a multi-hop blinded path.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
}
}
};

let amt_to_forward = htlc_msat - route_hop.fee_msat;
htlc_msat = amt_to_forward;

Expand All@@ -483,11 +512,6 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
chacha.process(&packet_decrypted, &mut decryption_tmp[..]);
packet_decrypted = decryption_tmp;

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
is_from_final_node = route_hop_idx + 1 == path.hops.len();
let failing_route_hop = if is_from_final_node { route_hop } else { &path.hops[route_hop_idx + 1] };

let err_packet = match msgs::DecodedOnionErrorPacket::read(&mut Cursor::new(&packet_decrypted)) {
Ok(p) => p,
Err(_) => return
Expand All@@ -507,7 +531,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
is_permanent: true,
});
let short_channel_id = Some(route_hop.short_channel_id);
res = Some((network_update, short_channel_id, !is_from_final_node));
res = Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently: is_from_final_node
});
return
}
};
Expand DownExpand Up@@ -615,8 +641,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} else {
// The node in question intentionally encoded a 0-length channel update. This is
// likely due to https://github.com/ElementsProject/lightning/issues/6200.
short_channel_id = Some(failing_route_hop.short_channel_id);
network_update = Some(NetworkUpdate::ChannelFailure {
short_channel_id: route_hop.short_channel_id,
short_channel_id: failing_route_hop.short_channel_id,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should remove the network update entirely. Even with perm set to false this will still mark the channel disabled even though it isn't.

@valentinewallacevalentinewallaceSep 20, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We only mark the channel as disabled if is_permanent is set. It's nice to set it because then we can more easily keep the behavior of defaulting to NetworkUpdate::NodeFailure on a totally bogus channel update.

is_permanent: false,
});
}
Expand DownExpand Up@@ -659,7 +686,10 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
short_channel_id = Some(route_hop.short_channel_id);
}

res = Some((network_update, short_channel_id, !(error_code & PERM == PERM && is_from_final_node)));
res = Some(FailureLearnings {
network_update, short_channel_id,
payment_failed_permanently: error_code & PERM == PERM && is_from_final_node
});

let (description, title) = errors::get_onion_error_description(error_code);
if debug_field_size > 0 && err_packet.failuremsg.len() >= 4 + debug_field_size {
Expand All@@ -668,9 +698,11 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
log_info!(logger, "Onion Error[from {}: {}({:#x})] {}", route_hop.pubkey, title, error_code, description);
}
}).expect("Route that we sent via spontaneously grew invalid keys in the middle of it?");
if let Some((network_update, short_channel_id, payment_retryable)) = res {
if let Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently
}) = res {
DecodedOnionFailure {
network_update, short_channel_id, payment_retryable,
network_update, short_channel_id, payment_failed_permanently,
#[cfg(test)]
onion_error_code: error_code_ret,
#[cfg(test)]
Expand All@@ -680,7 +712,7 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// only not set either packet unparseable or hmac does not match with any
// payment not retryable only when garbage is from the final node
DecodedOnionFailure {
network_update: None, short_channel_id: None, payment_retryable: !is_from_final_node,
network_update: None, short_channel_id: None, payment_failed_permanently: is_from_final_node,
#[cfg(test)]
onion_error_code: None,
#[cfg(test)]
Expand DownExpand Up@@ -824,7 +856,7 @@ impl HTLCFailReason {
if let &HTLCSource::OutboundRoute { ref path, .. } = htlc_source {
DecodedOnionFailure {
network_update: None,
payment_retryable: true,
payment_failed_permanently: false,
short_channel_id: Some(path.hops[0].short_channel_id),
#[cfg(test)]
onion_error_code: Some(*failure_code),
Expand Down
13 changes: 7 additions & 6 deletions lightning/src/ln/outbound_payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1484,10 +1484,11 @@ impl OutboundPayments {
) -> bool where L::Target: Logger {
#[cfg(test)]
let DecodedOnionFailure {
network_update, short_channel_id, payment_retryable, onion_error_code, onion_error_data
network_update, short_channel_id, payment_failed_permanently, onion_error_code,
onion_error_data
} = onion_error.decode_onion_failure(secp_ctx, logger, &source);
#[cfg(not(test))]
let DecodedOnionFailure { network_update, short_channel_id, payment_retryable } =
let DecodedOnionFailure { network_update, short_channel_id, payment_failed_permanently } =
onion_error.decode_onion_failure(secp_ctx, logger, &source);

let payment_is_probe = payment_is_probe(payment_hash, &payment_id, probing_cookie_secret);
Expand DownExpand Up@@ -1528,8 +1529,8 @@ impl OutboundPayments {
payment.get_mut().insert_previously_failed_scid(scid);
}

if payment_is_probe || !is_retryable_now || !payment_retryable {
let reason = if !payment_retryable {
if payment_is_probe || !is_retryable_now || payment_failed_permanently {
let reason = if payment_failed_permanently {
PaymentFailureReason::RecipientRejected
} else {
PaymentFailureReason::RetriesExhausted
Expand DownExpand Up@@ -1559,7 +1560,7 @@ impl OutboundPayments {

let path_failure = {
if payment_is_probe {
if !payment_retryable {
if payment_failed_permanently {
events::Event::ProbeSuccessful {
payment_id: *payment_id,
payment_hash: payment_hash.clone(),
Expand All@@ -1583,7 +1584,7 @@ impl OutboundPayments {
events::Event::PaymentPathFailed {
payment_id: Some(*payment_id),
payment_hash: payment_hash.clone(),
payment_failed_permanently: !payment_retryable,
payment_failed_permanently,
failure: events::PathFailure::OnPath { network_update },
path: path.clone(),
short_channel_id,
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix `PaymentPathFailed::payment_failed_permanently` on blinded path fail by valentinewallace · Pull Request #2576 · lightningdevkit/rust-lightning · GitHub
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
30 changes: 28 additions & 2 deletions lightning/src/ln/onion_route_tests.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,8 +645,34 @@ fn test_onion_failure() {
}, || nodes[2].node.fail_htlc_backwards(&payment_hash), false, None,
Some(NetworkUpdate::NodeFailure { node_id: route.paths[0].hops[1].pubkey, is_permanent: true }),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in UPDATE onion failure", 200, &nodes,
&route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
run_onion_failure_test_with_fail_intercept("0-length channel update in intermediate node UPDATE onion failure",
100, &nodes, &route, &payment_hash, &payment_secret, |msg| {
msg.amount_msat -= 1;
}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
failuremsg: vec![
0x10, 0x7, // UPDATE|7
0x0, 0x0 // 0-len channel update
],
pad: vec![0; 255 - 4 /* 4-byte error message */],
hmac: [0; 32],
};
let um = onion_utils::gen_um_from_shared_secret(&onion_keys[0].shared_secret.as_ref());
let mut hmac = HmacEngine::<Sha256>::new(&um);
hmac.input(&decoded_err_packet.encode()[32..]);
decoded_err_packet.hmac = Hmac::from_engine(hmac).into_inner();
msg.reason = onion_utils::encrypt_failure_packet(
&onion_keys[0].shared_secret.as_ref(), &decoded_err_packet.encode()[..])
}, || {}, true, Some(0x1000|7),
Some(NetworkUpdate::ChannelFailure {
short_channel_id: channels[1].0.contents.short_channel_id,
is_permanent: false,
}),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in final node UPDATE onion failure",
200, &nodes, &route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
Expand Down
62 changes: 47 additions & 15 deletions lightning/src/ln/onion_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -426,7 +426,7 @@ pub(super) fn build_first_hop_failure_packet(shared_secret: &[u8], failure_type:
pub(crate) struct DecodedOnionFailure {
pub(crate) network_update: Option<NetworkUpdate>,
pub(crate) short_channel_id: Option<u64>,
pub(crate) payment_retryable: bool,
pub(crate) payment_failed_permanently: bool,
#[cfg(test)]
pub(crate) onion_error_code: Option<u16>,
#[cfg(test)]
Expand All@@ -444,7 +444,14 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} = htlc_source {
(path, session_priv, first_hop_htlc_msat)
} else { unreachable!() };
let mut res = None;

// Learnings from the HTLC failure to inform future payment retries and scoring.
struct FailureLearnings {
network_update: Option<NetworkUpdate>,
short_channel_id: Option<u64>,
payment_failed_permanently: bool,
}
let mut res: Option<FailureLearnings> = None;
let mut htlc_msat = *first_hop_htlc_msat;
let mut error_code_ret = None;
let mut error_packet_ret = None;
Expand All@@ -467,11 +474,33 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// Got an error from within a blinded route.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
is_from_final_node = false;
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
},
};

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
let num_blinded_hops = path.blinded_tail.as_ref().map_or(0, |bt| bt.hops.len());
// For 1-hop blinded paths, the final `path.hops` entry is the recipient.
is_from_final_node = route_hop_idx + 1 == path.hops.len() && num_blinded_hops <= 1;
let failing_route_hop = if is_from_final_node { route_hop } else {
match path.hops.get(route_hop_idx + 1) {
Some(hop) => hop,
None => {
// The failing hop is within a multi-hop blinded path.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
}
}
};

let amt_to_forward = htlc_msat - route_hop.fee_msat;
htlc_msat = amt_to_forward;

Expand All@@ -483,11 +512,6 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
chacha.process(&packet_decrypted, &mut decryption_tmp[..]);
packet_decrypted = decryption_tmp;

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
is_from_final_node = route_hop_idx + 1 == path.hops.len();
let failing_route_hop = if is_from_final_node { route_hop } else { &path.hops[route_hop_idx + 1] };

let err_packet = match msgs::DecodedOnionErrorPacket::read(&mut Cursor::new(&packet_decrypted)) {
Ok(p) => p,
Err(_) => return
Expand All@@ -507,7 +531,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
is_permanent: true,
});
let short_channel_id = Some(route_hop.short_channel_id);
res = Some((network_update, short_channel_id, !is_from_final_node));
res = Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently: is_from_final_node
});
return
}
};
Expand DownExpand Up@@ -615,8 +641,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} else {
// The node in question intentionally encoded a 0-length channel update. This is
// likely due to https://github.com/ElementsProject/lightning/issues/6200.
short_channel_id = Some(failing_route_hop.short_channel_id);
network_update = Some(NetworkUpdate::ChannelFailure {
short_channel_id: route_hop.short_channel_id,
short_channel_id: failing_route_hop.short_channel_id,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should remove the network update entirely. Even with perm set to false this will still mark the channel disabled even though it isn't.

@valentinewallacevalentinewallaceSep 20, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We only mark the channel as disabled if is_permanent is set. It's nice to set it because then we can more easily keep the behavior of defaulting to NetworkUpdate::NodeFailure on a totally bogus channel update.

is_permanent: false,
});
}
Expand DownExpand Up@@ -659,7 +686,10 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
short_channel_id = Some(route_hop.short_channel_id);
}

res = Some((network_update, short_channel_id, !(error_code & PERM == PERM && is_from_final_node)));
res = Some(FailureLearnings {
network_update, short_channel_id,
payment_failed_permanently: error_code & PERM == PERM && is_from_final_node
});

let (description, title) = errors::get_onion_error_description(error_code);
if debug_field_size > 0 && err_packet.failuremsg.len() >= 4 + debug_field_size {
Expand All@@ -668,9 +698,11 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
log_info!(logger, "Onion Error[from {}: {}({:#x})] {}", route_hop.pubkey, title, error_code, description);
}
}).expect("Route that we sent via spontaneously grew invalid keys in the middle of it?");
if let Some((network_update, short_channel_id, payment_retryable)) = res {
if let Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently
}) = res {
DecodedOnionFailure {
network_update, short_channel_id, payment_retryable,
network_update, short_channel_id, payment_failed_permanently,
#[cfg(test)]
onion_error_code: error_code_ret,
#[cfg(test)]
Expand All@@ -680,7 +712,7 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// only not set either packet unparseable or hmac does not match with any
// payment not retryable only when garbage is from the final node
DecodedOnionFailure {
network_update: None, short_channel_id: None, payment_retryable: !is_from_final_node,
network_update: None, short_channel_id: None, payment_failed_permanently: is_from_final_node,
#[cfg(test)]
onion_error_code: None,
#[cfg(test)]
Expand DownExpand Up@@ -824,7 +856,7 @@ impl HTLCFailReason {
if let &HTLCSource::OutboundRoute { ref path, .. } = htlc_source {
DecodedOnionFailure {
network_update: None,
payment_retryable: true,
payment_failed_permanently: false,
short_channel_id: Some(path.hops[0].short_channel_id),
#[cfg(test)]
onion_error_code: Some(*failure_code),
Expand Down
13 changes: 7 additions & 6 deletions lightning/src/ln/outbound_payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1484,10 +1484,11 @@ impl OutboundPayments {
) -> bool where L::Target: Logger {
#[cfg(test)]
let DecodedOnionFailure {
network_update, short_channel_id, payment_retryable, onion_error_code, onion_error_data
network_update, short_channel_id, payment_failed_permanently, onion_error_code,
onion_error_data
} = onion_error.decode_onion_failure(secp_ctx, logger, &source);
#[cfg(not(test))]
let DecodedOnionFailure { network_update, short_channel_id, payment_retryable } =
let DecodedOnionFailure { network_update, short_channel_id, payment_failed_permanently } =
onion_error.decode_onion_failure(secp_ctx, logger, &source);

let payment_is_probe = payment_is_probe(payment_hash, &payment_id, probing_cookie_secret);
Expand DownExpand Up@@ -1528,8 +1529,8 @@ impl OutboundPayments {
payment.get_mut().insert_previously_failed_scid(scid);
}

if payment_is_probe || !is_retryable_now || !payment_retryable {
let reason = if !payment_retryable {
if payment_is_probe || !is_retryable_now || payment_failed_permanently {
let reason = if payment_failed_permanently {
PaymentFailureReason::RecipientRejected
} else {
PaymentFailureReason::RetriesExhausted
Expand DownExpand Up@@ -1559,7 +1560,7 @@ impl OutboundPayments {

let path_failure = {
if payment_is_probe {
if !payment_retryable {
if payment_failed_permanently {
events::Event::ProbeSuccessful {
payment_id: *payment_id,
payment_hash: payment_hash.clone(),
Expand All@@ -1583,7 +1584,7 @@ impl OutboundPayments {
events::Event::PaymentPathFailed {
payment_id: Some(*payment_id),
payment_hash: payment_hash.clone(),
payment_failed_permanently: !payment_retryable,
payment_failed_permanently,
failure: events::PathFailure::OnPath { network_update },
path: path.clone(),
short_channel_id,
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix `PaymentPathFailed::payment_failed_permanently` on blinded path fail by valentinewallace · Pull Request #2576 · lightningdevkit/rust-lightning · GitHub
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
30 changes: 28 additions & 2 deletions lightning/src/ln/onion_route_tests.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,8 +645,34 @@ fn test_onion_failure() {
}, || nodes[2].node.fail_htlc_backwards(&payment_hash), false, None,
Some(NetworkUpdate::NodeFailure { node_id: route.paths[0].hops[1].pubkey, is_permanent: true }),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in UPDATE onion failure", 200, &nodes,
&route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
run_onion_failure_test_with_fail_intercept("0-length channel update in intermediate node UPDATE onion failure",
100, &nodes, &route, &payment_hash, &payment_secret, |msg| {
msg.amount_msat -= 1;
}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
failuremsg: vec![
0x10, 0x7, // UPDATE|7
0x0, 0x0 // 0-len channel update
],
pad: vec![0; 255 - 4 /* 4-byte error message */],
hmac: [0; 32],
};
let um = onion_utils::gen_um_from_shared_secret(&onion_keys[0].shared_secret.as_ref());
let mut hmac = HmacEngine::<Sha256>::new(&um);
hmac.input(&decoded_err_packet.encode()[32..]);
decoded_err_packet.hmac = Hmac::from_engine(hmac).into_inner();
msg.reason = onion_utils::encrypt_failure_packet(
&onion_keys[0].shared_secret.as_ref(), &decoded_err_packet.encode()[..])
}, || {}, true, Some(0x1000|7),
Some(NetworkUpdate::ChannelFailure {
short_channel_id: channels[1].0.contents.short_channel_id,
is_permanent: false,
}),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in final node UPDATE onion failure",
200, &nodes, &route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
Expand Down
62 changes: 47 additions & 15 deletions lightning/src/ln/onion_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -426,7 +426,7 @@ pub(super) fn build_first_hop_failure_packet(shared_secret: &[u8], failure_type:
pub(crate) struct DecodedOnionFailure {
pub(crate) network_update: Option<NetworkUpdate>,
pub(crate) short_channel_id: Option<u64>,
pub(crate) payment_retryable: bool,
pub(crate) payment_failed_permanently: bool,
#[cfg(test)]
pub(crate) onion_error_code: Option<u16>,
#[cfg(test)]
Expand All@@ -444,7 +444,14 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} = htlc_source {
(path, session_priv, first_hop_htlc_msat)
} else { unreachable!() };
let mut res = None;

// Learnings from the HTLC failure to inform future payment retries and scoring.
struct FailureLearnings {
network_update: Option<NetworkUpdate>,
short_channel_id: Option<u64>,
payment_failed_permanently: bool,
}
let mut res: Option<FailureLearnings> = None;
let mut htlc_msat = *first_hop_htlc_msat;
let mut error_code_ret = None;
let mut error_packet_ret = None;
Expand All@@ -467,11 +474,33 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// Got an error from within a blinded route.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
is_from_final_node = false;
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
},
};

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
let num_blinded_hops = path.blinded_tail.as_ref().map_or(0, |bt| bt.hops.len());
// For 1-hop blinded paths, the final `path.hops` entry is the recipient.
is_from_final_node = route_hop_idx + 1 == path.hops.len() && num_blinded_hops <= 1;
let failing_route_hop = if is_from_final_node { route_hop } else {
match path.hops.get(route_hop_idx + 1) {
Some(hop) => hop,
None => {
// The failing hop is within a multi-hop blinded path.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
}
}
};

let amt_to_forward = htlc_msat - route_hop.fee_msat;
htlc_msat = amt_to_forward;

Expand All@@ -483,11 +512,6 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
chacha.process(&packet_decrypted, &mut decryption_tmp[..]);
packet_decrypted = decryption_tmp;

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
is_from_final_node = route_hop_idx + 1 == path.hops.len();
let failing_route_hop = if is_from_final_node { route_hop } else { &path.hops[route_hop_idx + 1] };

let err_packet = match msgs::DecodedOnionErrorPacket::read(&mut Cursor::new(&packet_decrypted)) {
Ok(p) => p,
Err(_) => return
Expand All@@ -507,7 +531,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
is_permanent: true,
});
let short_channel_id = Some(route_hop.short_channel_id);
res = Some((network_update, short_channel_id, !is_from_final_node));
res = Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently: is_from_final_node
});
return
}
};
Expand DownExpand Up@@ -615,8 +641,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} else {
// The node in question intentionally encoded a 0-length channel update. This is
// likely due to https://github.com/ElementsProject/lightning/issues/6200.
short_channel_id = Some(failing_route_hop.short_channel_id);
network_update = Some(NetworkUpdate::ChannelFailure {
short_channel_id: route_hop.short_channel_id,
short_channel_id: failing_route_hop.short_channel_id,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should remove the network update entirely. Even with perm set to false this will still mark the channel disabled even though it isn't.

@valentinewallacevalentinewallaceSep 20, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We only mark the channel as disabled if is_permanent is set. It's nice to set it because then we can more easily keep the behavior of defaulting to NetworkUpdate::NodeFailure on a totally bogus channel update.

is_permanent: false,
});
}
Expand DownExpand Up@@ -659,7 +686,10 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
short_channel_id = Some(route_hop.short_channel_id);
}

res = Some((network_update, short_channel_id, !(error_code & PERM == PERM && is_from_final_node)));
res = Some(FailureLearnings {
network_update, short_channel_id,
payment_failed_permanently: error_code & PERM == PERM && is_from_final_node
});

let (description, title) = errors::get_onion_error_description(error_code);
if debug_field_size > 0 && err_packet.failuremsg.len() >= 4 + debug_field_size {
Expand All@@ -668,9 +698,11 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
log_info!(logger, "Onion Error[from {}: {}({:#x})] {}", route_hop.pubkey, title, error_code, description);
}
}).expect("Route that we sent via spontaneously grew invalid keys in the middle of it?");
if let Some((network_update, short_channel_id, payment_retryable)) = res {
if let Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently
}) = res {
DecodedOnionFailure {
network_update, short_channel_id, payment_retryable,
network_update, short_channel_id, payment_failed_permanently,
#[cfg(test)]
onion_error_code: error_code_ret,
#[cfg(test)]
Expand All@@ -680,7 +712,7 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// only not set either packet unparseable or hmac does not match with any
// payment not retryable only when garbage is from the final node
DecodedOnionFailure {
network_update: None, short_channel_id: None, payment_retryable: !is_from_final_node,
network_update: None, short_channel_id: None, payment_failed_permanently: is_from_final_node,
#[cfg(test)]
onion_error_code: None,
#[cfg(test)]
Expand DownExpand Up@@ -824,7 +856,7 @@ impl HTLCFailReason {
if let &HTLCSource::OutboundRoute { ref path, .. } = htlc_source {
DecodedOnionFailure {
network_update: None,
payment_retryable: true,
payment_failed_permanently: false,
short_channel_id: Some(path.hops[0].short_channel_id),
#[cfg(test)]
onion_error_code: Some(*failure_code),
Expand Down
13 changes: 7 additions & 6 deletions lightning/src/ln/outbound_payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1484,10 +1484,11 @@ impl OutboundPayments {
) -> bool where L::Target: Logger {
#[cfg(test)]
let DecodedOnionFailure {
network_update, short_channel_id, payment_retryable, onion_error_code, onion_error_data
network_update, short_channel_id, payment_failed_permanently, onion_error_code,
onion_error_data
} = onion_error.decode_onion_failure(secp_ctx, logger, &source);
#[cfg(not(test))]
let DecodedOnionFailure { network_update, short_channel_id, payment_retryable } =
let DecodedOnionFailure { network_update, short_channel_id, payment_failed_permanently } =
onion_error.decode_onion_failure(secp_ctx, logger, &source);

let payment_is_probe = payment_is_probe(payment_hash, &payment_id, probing_cookie_secret);
Expand DownExpand Up@@ -1528,8 +1529,8 @@ impl OutboundPayments {
payment.get_mut().insert_previously_failed_scid(scid);
}

if payment_is_probe || !is_retryable_now || !payment_retryable {
let reason = if !payment_retryable {
if payment_is_probe || !is_retryable_now || payment_failed_permanently {
let reason = if payment_failed_permanently {
PaymentFailureReason::RecipientRejected
} else {
PaymentFailureReason::RetriesExhausted
Expand DownExpand Up@@ -1559,7 +1560,7 @@ impl OutboundPayments {

let path_failure = {
if payment_is_probe {
if !payment_retryable {
if payment_failed_permanently {
events::Event::ProbeSuccessful {
payment_id: *payment_id,
payment_hash: payment_hash.clone(),
Expand All@@ -1583,7 +1584,7 @@ impl OutboundPayments {
events::Event::PaymentPathFailed {
payment_id: Some(*payment_id),
payment_hash: payment_hash.clone(),
payment_failed_permanently: !payment_retryable,
payment_failed_permanently,
failure: events::PathFailure::OnPath { network_update },
path: path.clone(),
short_channel_id,
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Fix `PaymentPathFailed::payment_failed_permanently` on blinded path fail by valentinewallace · Pull Request #2576 · lightningdevkit/rust-lightning · GitHub
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
30 changes: 28 additions & 2 deletions lightning/src/ln/onion_route_tests.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -645,8 +645,34 @@ fn test_onion_failure() {
}, || nodes[2].node.fail_htlc_backwards(&payment_hash), false, None,
Some(NetworkUpdate::NodeFailure { node_id: route.paths[0].hops[1].pubkey, is_permanent: true }),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in UPDATE onion failure", 200, &nodes,
&route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
run_onion_failure_test_with_fail_intercept("0-length channel update in intermediate node UPDATE onion failure",
100, &nodes, &route, &payment_hash, &payment_secret, |msg| {
msg.amount_msat -= 1;
}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
failuremsg: vec![
0x10, 0x7, // UPDATE|7
0x0, 0x0 // 0-len channel update
],
pad: vec![0; 255 - 4 /* 4-byte error message */],
hmac: [0; 32],
};
let um = onion_utils::gen_um_from_shared_secret(&onion_keys[0].shared_secret.as_ref());
let mut hmac = HmacEngine::<Sha256>::new(&um);
hmac.input(&decoded_err_packet.encode()[32..]);
decoded_err_packet.hmac = Hmac::from_engine(hmac).into_inner();
msg.reason = onion_utils::encrypt_failure_packet(
&onion_keys[0].shared_secret.as_ref(), &decoded_err_packet.encode()[..])
}, || {}, true, Some(0x1000|7),
Some(NetworkUpdate::ChannelFailure {
short_channel_id: channels[1].0.contents.short_channel_id,
is_permanent: false,
}),
Some(channels[1].0.contents.short_channel_id));
run_onion_failure_test_with_fail_intercept("0-length channel update in final node UPDATE onion failure",
200, &nodes, &route, &payment_hash, &payment_secret, |_msg| {}, |msg| {
let session_priv = SecretKey::from_slice(&[3; 32]).unwrap();
let onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv).unwrap();
let mut decoded_err_packet = msgs::DecodedOnionErrorPacket {
Expand Down
62 changes: 47 additions & 15 deletions lightning/src/ln/onion_utils.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -426,7 +426,7 @@ pub(super) fn build_first_hop_failure_packet(shared_secret: &[u8], failure_type:
pub(crate) struct DecodedOnionFailure {
pub(crate) network_update: Option<NetworkUpdate>,
pub(crate) short_channel_id: Option<u64>,
pub(crate) payment_retryable: bool,
pub(crate) payment_failed_permanently: bool,
#[cfg(test)]
pub(crate) onion_error_code: Option<u16>,
#[cfg(test)]
Expand All@@ -444,7 +444,14 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} = htlc_source {
(path, session_priv, first_hop_htlc_msat)
} else { unreachable!() };
let mut res = None;

// Learnings from the HTLC failure to inform future payment retries and scoring.
struct FailureLearnings {
network_update: Option<NetworkUpdate>,
short_channel_id: Option<u64>,
payment_failed_permanently: bool,
}
let mut res: Option<FailureLearnings> = None;
let mut htlc_msat = *first_hop_htlc_msat;
let mut error_code_ret = None;
let mut error_packet_ret = None;
Expand All@@ -467,11 +474,33 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// Got an error from within a blinded route.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
is_from_final_node = false;
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
},
};

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
let num_blinded_hops = path.blinded_tail.as_ref().map_or(0, |bt| bt.hops.len());
// For 1-hop blinded paths, the final `path.hops` entry is the recipient.
is_from_final_node = route_hop_idx + 1 == path.hops.len() && num_blinded_hops <= 1;
let failing_route_hop = if is_from_final_node { route_hop } else {
match path.hops.get(route_hop_idx + 1) {
Some(hop) => hop,
None => {
// The failing hop is within a multi-hop blinded path.
error_code_ret = Some(BADONION | PERM | 24); // invalid_onion_blinding
error_packet_ret = Some(vec![0; 32]);
res = Some(FailureLearnings {
network_update: None, short_channel_id: None, payment_failed_permanently: false
});
return
}
}
};

let amt_to_forward = htlc_msat - route_hop.fee_msat;
htlc_msat = amt_to_forward;

Expand All@@ -483,11 +512,6 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
chacha.process(&packet_decrypted, &mut decryption_tmp[..]);
packet_decrypted = decryption_tmp;

// The failing hop includes either the inbound channel to the recipient or the outbound channel
// from the current hop (i.e., the next hop's inbound channel).
is_from_final_node = route_hop_idx + 1 == path.hops.len();
let failing_route_hop = if is_from_final_node { route_hop } else { &path.hops[route_hop_idx + 1] };

let err_packet = match msgs::DecodedOnionErrorPacket::read(&mut Cursor::new(&packet_decrypted)) {
Ok(p) => p,
Err(_) => return
Expand All@@ -507,7 +531,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
is_permanent: true,
});
let short_channel_id = Some(route_hop.short_channel_id);
res = Some((network_update, short_channel_id, !is_from_final_node));
res = Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently: is_from_final_node
});
return
}
};
Expand DownExpand Up@@ -615,8 +641,9 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
} else {
// The node in question intentionally encoded a 0-length channel update. This is
// likely due to https://github.com/ElementsProject/lightning/issues/6200.
short_channel_id = Some(failing_route_hop.short_channel_id);
network_update = Some(NetworkUpdate::ChannelFailure {
short_channel_id: route_hop.short_channel_id,
short_channel_id: failing_route_hop.short_channel_id,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should remove the network update entirely. Even with perm set to false this will still mark the channel disabled even though it isn't.

@valentinewallacevalentinewallaceSep 20, 2023

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

We only mark the channel as disabled if is_permanent is set. It's nice to set it because then we can more easily keep the behavior of defaulting to NetworkUpdate::NodeFailure on a totally bogus channel update.

is_permanent: false,
});
}
Expand DownExpand Up@@ -659,7 +686,10 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
short_channel_id = Some(route_hop.short_channel_id);
}

res = Some((network_update, short_channel_id, !(error_code & PERM == PERM && is_from_final_node)));
res = Some(FailureLearnings {
network_update, short_channel_id,
payment_failed_permanently: error_code & PERM == PERM && is_from_final_node
});

let (description, title) = errors::get_onion_error_description(error_code);
if debug_field_size > 0 && err_packet.failuremsg.len() >= 4 + debug_field_size {
Expand All@@ -668,9 +698,11 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
log_info!(logger, "Onion Error[from {}: {}({:#x})] {}", route_hop.pubkey, title, error_code, description);
}
}).expect("Route that we sent via spontaneously grew invalid keys in the middle of it?");
if let Some((network_update, short_channel_id, payment_retryable)) = res {
if let Some(FailureLearnings {
network_update, short_channel_id, payment_failed_permanently
}) = res {
DecodedOnionFailure {
network_update, short_channel_id, payment_retryable,
network_update, short_channel_id, payment_failed_permanently,
#[cfg(test)]
onion_error_code: error_code_ret,
#[cfg(test)]
Expand All@@ -680,7 +712,7 @@ pub(super) fn process_onion_failure<T: secp256k1::Signing, L: Deref>(
// only not set either packet unparseable or hmac does not match with any
// payment not retryable only when garbage is from the final node
DecodedOnionFailure {
network_update: None, short_channel_id: None, payment_retryable: !is_from_final_node,
network_update: None, short_channel_id: None, payment_failed_permanently: is_from_final_node,
#[cfg(test)]
onion_error_code: None,
#[cfg(test)]
Expand DownExpand Up@@ -824,7 +856,7 @@ impl HTLCFailReason {
if let &HTLCSource::OutboundRoute { ref path, .. } = htlc_source {
DecodedOnionFailure {
network_update: None,
payment_retryable: true,
payment_failed_permanently: false,
short_channel_id: Some(path.hops[0].short_channel_id),
#[cfg(test)]
onion_error_code: Some(*failure_code),
Expand Down
13 changes: 7 additions & 6 deletions lightning/src/ln/outbound_payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1484,10 +1484,11 @@ impl OutboundPayments {
) -> bool where L::Target: Logger {
#[cfg(test)]
let DecodedOnionFailure {
network_update, short_channel_id, payment_retryable, onion_error_code, onion_error_data
network_update, short_channel_id, payment_failed_permanently, onion_error_code,
onion_error_data
} = onion_error.decode_onion_failure(secp_ctx, logger, &source);
#[cfg(not(test))]
let DecodedOnionFailure { network_update, short_channel_id, payment_retryable } =
let DecodedOnionFailure { network_update, short_channel_id, payment_failed_permanently } =
onion_error.decode_onion_failure(secp_ctx, logger, &source);

let payment_is_probe = payment_is_probe(payment_hash, &payment_id, probing_cookie_secret);
Expand DownExpand Up@@ -1528,8 +1529,8 @@ impl OutboundPayments {
payment.get_mut().insert_previously_failed_scid(scid);
}

if payment_is_probe || !is_retryable_now || !payment_retryable {
let reason = if !payment_retryable {
if payment_is_probe || !is_retryable_now || payment_failed_permanently {
let reason = if payment_failed_permanently {
PaymentFailureReason::RecipientRejected
} else {
PaymentFailureReason::RetriesExhausted
Expand DownExpand Up@@ -1559,7 +1560,7 @@ impl OutboundPayments {

let path_failure = {
if payment_is_probe {
if !payment_retryable {
if payment_failed_permanently {
events::Event::ProbeSuccessful {
payment_id: *payment_id,
payment_hash: payment_hash.clone(),
Expand All@@ -1583,7 +1584,7 @@ impl OutboundPayments {
events::Event::PaymentPathFailed {
payment_id: Some(*payment_id),
payment_hash: payment_hash.clone(),
payment_failed_permanently: !payment_retryable,
payment_failed_permanently,
failure: events::PathFailure::OnPath { network_update },
path: path.clone(),
short_channel_id,
Expand Down