Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix PaymentPathFailed::payment_failed_permanently on blinded path fail #2576

30 changes: 28 additions & 2 deletions lightning/src/ln/onion_route_tests.rs
Original file line number Diff line number Diff 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 number Diff line number Diff 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 Down Expand 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
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.

Copy link
Contributor Author

@valentinewallace valentinewallace Sep 20, 2023

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 Down Expand 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 Down Expand 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 number Diff line number Diff 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 Down Expand 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 Down Expand 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