Merge pull request #2576 from valentinewallace/2023-09-fix-outbound-bp-fail-ev
authorMatt Corallo <649246+TheBlueMatt@users.noreply.github.com>
Mon, 25 Sep 2023 16:56:03 +0000 (16:56 +0000)
committerGitHub <noreply@github.com>
Mon, 25 Sep 2023 16:56:03 +0000 (16:56 +0000)
Fix `PaymentPathFailed::payment_failed_permanently` on blinded path fail

1  2 
lightning/src/ln/onion_route_tests.rs
lightning/src/ln/onion_utils.rs
lightning/src/ln/outbound_payment.rs

index 52ae2908022c7894300b4a42824de38f3ab5e39e,c124360d1ee47f2112c03fe9227bab4ec4455d07..ca7d33f50c032907cef80d3629fd943e813ba4a7
@@@ -645,8 -645,34 +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 {
@@@ -1052,7 -1078,7 +1078,7 @@@ macro_rules! get_phantom_route 
                (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)
        }
  }}
index 52cd3ca96d94760f71fcc1d354dc60e79360f427,390826e4f3d2938b32a69855b8b70eab2d476e3e..9af3de07ff4e7356ac65fe2069b3ff209761156a
@@@ -426,7 -426,7 +426,7 @@@ pub(super) fn build_first_hop_failure_p
  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)]
@@@ -444,7 -444,14 +444,14 @@@ pub(super) fn process_onion_failure<T: 
        } = 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)]
@@@ -824,7 -856,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),
@@@ -1014,27 -1046,27 +1046,27 @@@ mod tests 
                                        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,
index 023412e1afb56cdcdd0a2d9195064e5744ccab6c,a4d5c3a389e4e476dabcfb735aeed05f106e84ce..86c31aac6c606aad848a0bc1f12e283e09334c73
@@@ -391,7 -391,7 +391,7 @@@ pub enum RetryableSendFailure 
  /// 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.
@@@ -465,18 -465,6 +465,18 @@@ pub(super) enum Bolt12PaymentError 
        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
@@@ -1115,7 -1103,6 +1115,7 @@@ impl OutboundPayments 
                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,
@@@ -1863,7 -1851,6 +1864,7 @@@ mod tests 
                                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,