}, || 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 {
(get_route(
&$nodes[0].node.get_our_node_id(), &route_params, &network_graph,
Some(&$nodes[0].node.list_usable_channels().iter().collect::<Vec<_>>()),
- $nodes[0].logger, &scorer, &(), &[0u8; 32]
+ $nodes[0].logger, &scorer, &Default::default(), &[0u8; 32]
).unwrap(), phantom_route_hint.phantom_scid)
}
}}
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)]
} = 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;
// 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;
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
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
}
};
} 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,
is_permanent: false,
});
}
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 {
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)]
// 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)]
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),
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0 // We fill in the payloads manually instead of generating them from RouteHops.
+ short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, // We fill in the payloads manually instead of generating them from RouteHops.
},
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0 // We fill in the payloads manually instead of generating them from RouteHops.
+ short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, // We fill in the payloads manually instead of generating them from RouteHops.
},
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0 // We fill in the payloads manually instead of generating them from RouteHops.
+ short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, // We fill in the payloads manually instead of generating them from RouteHops.
},
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0 // We fill in the payloads manually instead of generating them from RouteHops.
+ short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, // We fill in the payloads manually instead of generating them from RouteHops.
},
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0 // We fill in the payloads manually instead of generating them from RouteHops.
+ short_channel_id: 0, fee_msat: 0, cltv_expiry_delta: 0, maybe_announced_channel: true, // We fill in the payloads manually instead of generating them from RouteHops.
},
], blinded_tail: None }],
route_params: None,
/// is in, see the description of individual enum states for more.
///
/// [`ChannelManager::send_payment_with_route`]: crate::ln::channelmanager::ChannelManager::send_payment_with_route
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PaymentSendFailure {
/// A parameter which was passed to send_payment was invalid, preventing us from attempting to
/// send the payment at all.
DuplicateInvoice,
}
+/// Indicates that we failed to send a payment probe. Further errors may be surfaced later via
+/// [`Event::ProbeFailed`].
+///
+/// [`Event::ProbeFailed`]: crate::events::Event::ProbeFailed
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum ProbeSendFailure {
+ /// We were unable to find a route to the destination.
+ RouteNotFound,
+ /// We failed to send the payment probes.
+ SendingFailed(PaymentSendFailure),
+}
+
/// Information which is provided, encrypted, to the payment recipient when sending HTLCs.
///
/// This should generally be constructed with data communicated to us from the recipient (via a
F: Fn(SendAlongPathArgs) -> Result<(), APIError>,
{
let payment_id = PaymentId(entropy_source.get_secure_random_bytes());
+ let payment_secret = PaymentSecret(entropy_source.get_secure_random_bytes());
let payment_hash = probing_cookie_from_id(&payment_id, probing_cookie_secret);
let route = Route { paths: vec![path], route_params: None };
let onion_session_privs = self.add_new_pending_payment(payment_hash,
- RecipientOnionFields::spontaneous_empty(), payment_id, None, &route, None, None,
+ RecipientOnionFields::secret_only(payment_secret), payment_id, None, &route, None, None,
entropy_source, best_block_height)?;
match self.pay_route_internal(&route, payment_hash, RecipientOnionFields::spontaneous_empty(),
) -> 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);
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
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(),
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,
channel_features: ChannelFeatures::empty(),
fee_msat: 0,
cltv_expiry_delta: 0,
+ maybe_announced_channel: true,
}], blinded_tail: None }],
route_params: Some(route_params.clone()),
};
channel_features: ChannelFeatures::empty(),
fee_msat: invoice.amount_msats(),
cltv_expiry_delta: 0,
+ maybe_announced_channel: true,
}
],
blinded_tail: None,