Remove AllPathsFailed outbounds at send_payment_internal callsites instead
[rust-lightning] / lightning / src / ln / outbound_payment.rs
index 08aeae5c58bce3bc9e1c355482f9b01d370da98f..c2d0a6b62c9d11285439645a7a168d410ad9d76f 100644 (file)
@@ -13,18 +13,24 @@ use bitcoin::hashes::Hash;
 use bitcoin::hashes::sha256::Hash as Sha256;
 use bitcoin::secp256k1::{self, Secp256k1, SecretKey};
 
-use crate::chain::keysinterface::{EntropySource, KeysInterface, NodeSigner, Recipient};
+use crate::chain::keysinterface::{EntropySource, NodeSigner, Recipient};
 use crate::ln::{PaymentHash, PaymentPreimage, PaymentSecret};
-use crate::ln::channelmanager::{HTLCSource, IDEMPOTENCY_TIMEOUT_TICKS, PaymentId};
+use crate::ln::channelmanager::{HTLCSource, IDEMPOTENCY_TIMEOUT_TICKS, MIN_HTLC_RELAY_HOLDING_CELL_MILLIS, PaymentId};
 use crate::ln::msgs::DecodeError;
 use crate::ln::onion_utils::HTLCFailReason;
 use crate::routing::router::{PaymentParameters, Route, RouteHop, RouteParameters, RoutePath};
 use crate::util::errors::APIError;
 use crate::util::events;
 use crate::util::logger::Logger;
+use crate::util::time::Time;
+#[cfg(all(not(feature = "no-std"), test))]
+use crate::util::time::tests::SinceEpoch;
 
 use core::cmp;
+use core::fmt::{self, Display, Formatter};
 use core::ops::Deref;
+use core::time::Duration;
+
 use crate::prelude::*;
 use crate::sync::Mutex;
 
@@ -35,6 +41,9 @@ pub(crate) enum PendingOutboundPayment {
                session_privs: HashSet<[u8; 32]>,
        },
        Retryable {
+               retry_strategy: Retry,
+               attempts: PaymentAttempts,
+               route_params: Option<RouteParameters>,
                session_privs: HashSet<[u8; 32]>,
                payment_hash: PaymentHash,
                payment_secret: Option<PaymentSecret>,
@@ -60,7 +69,7 @@ pub(crate) enum PendingOutboundPayment {
        /// `PaymentPathFailed` events with `all_paths_failed` can be pending at once, confusing a
        /// downstream event handler as to when a payment has actually failed.
        ///
-       /// (1) https://github.com/lightningdevkit/rust-lightning/issues/1164
+       /// (1) <https://github.com/lightningdevkit/rust-lightning/issues/1164>
        Abandoned {
                session_privs: HashSet<[u8; 32]>,
                payment_hash: PaymentHash,
@@ -68,6 +77,22 @@ pub(crate) enum PendingOutboundPayment {
 }
 
 impl PendingOutboundPayment {
+       fn increment_attempts(&mut self) {
+               if let PendingOutboundPayment::Retryable { attempts, .. } = self {
+                       attempts.count += 1;
+               }
+       }
+       fn is_retryable_now(&self) -> bool {
+               if let PendingOutboundPayment::Retryable { retry_strategy, attempts, .. } = self {
+                       return retry_strategy.is_retryable_now(&attempts)
+               }
+               false
+       }
+       pub fn insert_previously_failed_scid(&mut self, scid: u64) {
+               if let PendingOutboundPayment::Retryable { route_params: Some(params), .. } = self {
+                       params.payment_params.previously_failed_channels.push(scid);
+               }
+       }
        pub(super) fn is_fulfilled(&self) -> bool {
                match self {
                        PendingOutboundPayment::Fulfilled { .. } => true,
@@ -182,6 +207,78 @@ impl PendingOutboundPayment {
        }
 }
 
+/// Strategies available to retry payment path failures.
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub enum Retry {
+       /// Max number of attempts to retry payment.
+       ///
+       /// Note that this is the number of *path* failures, not full payment retries. For multi-path
+       /// payments, if this is less than the total number of paths, we will never even retry all of the
+       /// payment's paths.
+       Attempts(usize),
+       #[cfg(not(feature = "no-std"))]
+       /// Time elapsed before abandoning retries for a payment.
+       Timeout(core::time::Duration),
+}
+
+impl Retry {
+       pub(crate) fn is_retryable_now(&self, attempts: &PaymentAttempts) -> bool {
+               match (self, attempts) {
+                       (Retry::Attempts(max_retry_count), PaymentAttempts { count, .. }) => {
+                               max_retry_count > count
+                       },
+                       #[cfg(all(not(feature = "no-std"), not(test)))]
+                       (Retry::Timeout(max_duration), PaymentAttempts { first_attempted_at, .. }) =>
+                               *max_duration >= std::time::Instant::now().duration_since(*first_attempted_at),
+                       #[cfg(all(not(feature = "no-std"), test))]
+                       (Retry::Timeout(max_duration), PaymentAttempts { first_attempted_at, .. }) =>
+                               *max_duration >= SinceEpoch::now().duration_since(*first_attempted_at),
+               }
+       }
+}
+
+pub(crate) type PaymentAttempts = PaymentAttemptsUsingTime<ConfiguredTime>;
+
+/// Storing minimal payment attempts information required for determining if a outbound payment can
+/// be retried.
+pub(crate) struct PaymentAttemptsUsingTime<T: Time> {
+       /// This count will be incremented only after the result of the attempt is known. When it's 0,
+       /// it means the result of the first attempt is not known yet.
+       pub(crate) count: usize,
+       /// This field is only used when retry is `Retry::Timeout` which is only build with feature std
+       first_attempted_at: T
+}
+
+#[cfg(not(any(feature = "no-std", test)))]
+type ConfiguredTime = std::time::Instant;
+#[cfg(feature = "no-std")]
+type ConfiguredTime = crate::util::time::Eternity;
+#[cfg(all(not(feature = "no-std"), test))]
+type ConfiguredTime = SinceEpoch;
+
+impl<T: Time> PaymentAttemptsUsingTime<T> {
+       pub(crate) fn new() -> Self {
+               PaymentAttemptsUsingTime {
+                       count: 0,
+                       first_attempted_at: T::now()
+               }
+       }
+}
+
+impl<T: Time> Display for PaymentAttemptsUsingTime<T> {
+       fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
+               #[cfg(feature = "no-std")]
+               return write!(f, "attempts: {}", self.count);
+               #[cfg(not(feature = "no-std"))]
+               return write!(
+                       f,
+                       "attempts: {}, duration: {}s",
+                       self.count,
+                       T::now().duration_since(self.first_attempted_at).as_secs()
+               );
+       }
+}
+
 /// If a payment fails to send, it can be in one of several states. This enum is returned as the
 /// Err() type describing which state the payment is in, see the description of individual enum
 /// states for more.
@@ -271,47 +368,56 @@ impl OutboundPayments {
                }
        }
 
-       pub(super) fn send_payment_with_route<K: Deref, F>(
+       pub(super) fn send_payment_with_route<ES: Deref, NS: Deref, F>(
                &self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option<PaymentSecret>,
-               payment_id: PaymentId, keys_manager: &K, best_block_height: u32, send_payment_along_path: F
+               payment_id: PaymentId, entropy_source: &ES, node_signer: &NS, best_block_height: u32,
+               send_payment_along_path: F
        ) -> Result<(), PaymentSendFailure>
        where
-               K::Target: KeysInterface,
+               ES::Target: EntropySource,
+               NS::Target: NodeSigner,
                F: Fn(&Vec<RouteHop>, &Option<PaymentParameters>, &PaymentHash, &Option<PaymentSecret>, u64,
                   u32, PaymentId, &Option<PaymentPreimage>, [u8; 32]) -> Result<(), APIError>
        {
-               let onion_session_privs = self.add_new_pending_payment(payment_hash, *payment_secret, payment_id, route, keys_manager, best_block_height)?;
-               self.send_payment_internal(route, payment_hash, payment_secret, None, payment_id, None, onion_session_privs, keys_manager, best_block_height, send_payment_along_path)
+               let onion_session_privs = self.add_new_pending_payment(payment_hash, *payment_secret, payment_id, route, Retry::Attempts(0), None, entropy_source, best_block_height)?;
+               self.send_payment_internal(route, payment_hash, payment_secret, None, payment_id, None,
+                       onion_session_privs, node_signer, best_block_height, send_payment_along_path)
+                       .map_err(|e| { self.remove_outbound_if_all_failed(payment_id, &e); e })
        }
 
-       pub(super) fn send_spontaneous_payment<K: Deref, F>(
+       pub(super) fn send_spontaneous_payment<ES: Deref, NS: Deref, F>(
                &self, route: &Route, payment_preimage: Option<PaymentPreimage>, payment_id: PaymentId,
-               keys_manager: &K, best_block_height: u32, send_payment_along_path: F
+               entropy_source: &ES, node_signer: &NS, best_block_height: u32, send_payment_along_path: F
        ) -> Result<PaymentHash, PaymentSendFailure>
        where
-               K::Target: KeysInterface,
+               ES::Target: EntropySource,
+               NS::Target: NodeSigner,
                F: Fn(&Vec<RouteHop>, &Option<PaymentParameters>, &PaymentHash, &Option<PaymentSecret>, u64,
                   u32, PaymentId, &Option<PaymentPreimage>, [u8; 32]) -> Result<(), APIError>
        {
                let preimage = match payment_preimage {
                        Some(p) => p,
-                       None => PaymentPreimage(keys_manager.get_secure_random_bytes()),
+                       None => PaymentPreimage(entropy_source.get_secure_random_bytes()),
                };
                let payment_hash = PaymentHash(Sha256::hash(&preimage.0).into_inner());
-               let onion_session_privs = self.add_new_pending_payment(payment_hash, None, payment_id, &route, keys_manager, best_block_height)?;
+               let onion_session_privs = self.add_new_pending_payment(payment_hash, None, payment_id, &route, Retry::Attempts(0), None, entropy_source, best_block_height)?;
 
-               match self.send_payment_internal(route, payment_hash, &None, Some(preimage), payment_id, None, onion_session_privs, keys_manager, best_block_height, send_payment_along_path) {
+               match self.send_payment_internal(route, payment_hash, &None, Some(preimage), payment_id, None, onion_session_privs, node_signer, best_block_height, send_payment_along_path) {
                        Ok(()) => Ok(payment_hash),
-                       Err(e) => Err(e)
+                       Err(e) => {
+                               self.remove_outbound_if_all_failed(payment_id, &e);
+                               Err(e)
+                       }
                }
        }
 
-       pub(super) fn retry_payment_with_route<K: Deref, F>(
-               &self, route: &Route, payment_id: PaymentId, keys_manager: &K, best_block_height: u32,
+       pub(super) fn retry_payment_with_route<ES: Deref, NS: Deref, F>(
+               &self, route: &Route, payment_id: PaymentId, entropy_source: &ES, node_signer: &NS, best_block_height: u32,
                send_payment_along_path: F
        ) -> Result<(), PaymentSendFailure>
        where
-               K::Target: KeysInterface,
+               ES::Target: EntropySource,
+               NS::Target: NodeSigner,
                F: Fn(&Vec<RouteHop>, &Option<PaymentParameters>, &PaymentHash, &Option<PaymentSecret>, u64,
                   u32, PaymentId, &Option<PaymentPreimage>, [u8; 32]) -> Result<(), APIError>
        {
@@ -326,7 +432,7 @@ impl OutboundPayments {
 
                let mut onion_session_privs = Vec::with_capacity(route.paths.len());
                for _ in 0..route.paths.len() {
-                       onion_session_privs.push(keys_manager.get_secure_random_bytes());
+                       onion_session_privs.push(entropy_source.get_secure_random_bytes());
                }
 
                let (total_msat, payment_hash, payment_secret) = {
@@ -372,19 +478,20 @@ impl OutboundPayments {
                                        })),
                        }
                };
-               self.send_payment_internal(route, payment_hash, &payment_secret, None, payment_id, Some(total_msat), onion_session_privs, keys_manager, best_block_height, send_payment_along_path)
+               self.send_payment_internal(route, payment_hash, &payment_secret, None, payment_id, Some(total_msat), onion_session_privs, node_signer, best_block_height, send_payment_along_path)
        }
 
-       pub(super) fn send_probe<K: Deref, F>(
-               &self, hops: Vec<RouteHop>, probing_cookie_secret: [u8; 32], keys_manager: &K,
-               best_block_height: u32, send_payment_along_path: F
+       pub(super) fn send_probe<ES: Deref, NS: Deref, F>(
+               &self, hops: Vec<RouteHop>, probing_cookie_secret: [u8; 32], entropy_source: &ES,
+               node_signer: &NS, best_block_height: u32, send_payment_along_path: F
        ) -> Result<(PaymentHash, PaymentId), PaymentSendFailure>
        where
-               K::Target: KeysInterface,
+               ES::Target: EntropySource,
+               NS::Target: NodeSigner,
                F: Fn(&Vec<RouteHop>, &Option<PaymentParameters>, &PaymentHash, &Option<PaymentSecret>, u64,
                   u32, PaymentId, &Option<PaymentPreimage>, [u8; 32]) -> Result<(), APIError>
        {
-               let payment_id = PaymentId(keys_manager.get_secure_random_bytes());
+               let payment_id = PaymentId(entropy_source.get_secure_random_bytes());
 
                let payment_hash = probing_cookie_from_id(&payment_id, probing_cookie_secret);
 
@@ -395,29 +502,33 @@ impl OutboundPayments {
                }
 
                let route = Route { paths: vec![hops], payment_params: None };
-               let onion_session_privs = self.add_new_pending_payment(payment_hash, None, payment_id, &route, keys_manager, best_block_height)?;
+               let onion_session_privs = self.add_new_pending_payment(payment_hash, None, payment_id, &route, Retry::Attempts(0), None, entropy_source, best_block_height)?;
 
-               match self.send_payment_internal(&route, payment_hash, &None, None, payment_id, None, onion_session_privs, keys_manager, best_block_height, send_payment_along_path) {
+               match self.send_payment_internal(&route, payment_hash, &None, None, payment_id, None, onion_session_privs, node_signer, best_block_height, send_payment_along_path) {
                        Ok(()) => Ok((payment_hash, payment_id)),
-                       Err(e) => Err(e)
+                       Err(e) => {
+                               self.remove_outbound_if_all_failed(payment_id, &e);
+                               Err(e)
+                       }
                }
        }
 
        #[cfg(test)]
-       pub(super) fn test_add_new_pending_payment<K: Deref>(
+       pub(super) fn test_add_new_pending_payment<ES: Deref>(
                &self, payment_hash: PaymentHash, payment_secret: Option<PaymentSecret>, payment_id: PaymentId,
-               route: &Route, keys_manager: &K, best_block_height: u32
-       ) -> Result<Vec<[u8; 32]>, PaymentSendFailure> where K::Target: KeysInterface {
-               self.add_new_pending_payment(payment_hash, payment_secret, payment_id, route, keys_manager, best_block_height)
+               route: &Route, retry_strategy: Retry, entropy_source: &ES, best_block_height: u32
+       ) -> Result<Vec<[u8; 32]>, PaymentSendFailure> where ES::Target: EntropySource {
+               self.add_new_pending_payment(payment_hash, payment_secret, payment_id, route, retry_strategy, None, entropy_source, best_block_height)
        }
 
-       fn add_new_pending_payment<K: Deref>(
+       pub(super) fn add_new_pending_payment<ES: Deref>(
                &self, payment_hash: PaymentHash, payment_secret: Option<PaymentSecret>, payment_id: PaymentId,
-               route: &Route, keys_manager: &K, best_block_height: u32
-       ) -> Result<Vec<[u8; 32]>, PaymentSendFailure> where K::Target: KeysInterface {
+               route: &Route, retry_strategy: Retry, route_params: Option<RouteParameters>,
+               entropy_source: &ES, best_block_height: u32
+       ) -> Result<Vec<[u8; 32]>, PaymentSendFailure> where ES::Target: EntropySource {
                let mut onion_session_privs = Vec::with_capacity(route.paths.len());
                for _ in 0..route.paths.len() {
-                       onion_session_privs.push(keys_manager.get_secure_random_bytes());
+                       onion_session_privs.push(entropy_source.get_secure_random_bytes());
                }
 
                let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap();
@@ -425,6 +536,9 @@ impl OutboundPayments {
                        hash_map::Entry::Occupied(_) => Err(PaymentSendFailure::DuplicatePayment),
                        hash_map::Entry::Vacant(entry) => {
                                let payment = entry.insert(PendingOutboundPayment::Retryable {
+                                       retry_strategy,
+                                       attempts: PaymentAttempts::new(),
+                                       route_params,
                                        session_privs: HashSet::new(),
                                        pending_amt_msat: 0,
                                        pending_fee_msat: Some(0),
@@ -443,13 +557,14 @@ impl OutboundPayments {
                }
        }
 
-       fn send_payment_internal<K: Deref, F>(
+       fn send_payment_internal<NS: Deref, F>(
                &self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option<PaymentSecret>,
                keysend_preimage: Option<PaymentPreimage>, payment_id: PaymentId, recv_value_msat: Option<u64>,
-               onion_session_privs: Vec<[u8; 32]>, keys_manager: &K, best_block_height: u32,
-               send_payment_along_path: F) -> Result<(), PaymentSendFailure>
+               onion_session_privs: Vec<[u8; 32]>, node_signer: &NS, best_block_height: u32,
+               send_payment_along_path: F
+       ) -> Result<(), PaymentSendFailure>
        where
-               K::Target: KeysInterface,
+               NS::Target: NodeSigner,
                F: Fn(&Vec<RouteHop>, &Option<PaymentParameters>, &PaymentHash, &Option<PaymentSecret>, u64,
                   u32, PaymentId, &Option<PaymentPreimage>, [u8; 32]) -> Result<(), APIError>
        {
@@ -460,7 +575,7 @@ impl OutboundPayments {
                        return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError{err: "Payment secret is required for multi-path payments".to_string()}));
                }
                let mut total_value = 0;
-               let our_node_id = keys_manager.get_node_id(Recipient::Node).unwrap(); // TODO no unwrap
+               let our_node_id = node_signer.get_node_id(Recipient::Node).unwrap(); // TODO no unwrap
                let mut path_errs = Vec::with_capacity(route.paths.len());
                'path_check: for path in route.paths.iter() {
                        if path.len() < 1 || path.len() > 20 {
@@ -541,10 +656,6 @@ impl OutboundPayments {
                                } else { None },
                        })
                } else if has_err {
-                       // If we failed to send any paths, we should remove the new PaymentId from the
-                       // `pending_outbound_payments` map, as the user isn't expected to `abandon_payment`.
-                       let removed = self.pending_outbound_payments.lock().unwrap().remove(&payment_id).is_some();
-                       debug_assert!(removed, "We should always have a pending payment to remove here");
                        Err(PaymentSendFailure::AllFailedResendSafe(results.drain(..).map(|r| r.unwrap_err()).collect()))
                } else {
                        Ok(())
@@ -552,25 +663,36 @@ impl OutboundPayments {
        }
 
        #[cfg(test)]
-       pub(super) fn test_send_payment_internal<K: Deref, F>(
+       pub(super) fn test_send_payment_internal<NS: Deref, F>(
                &self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option<PaymentSecret>,
                keysend_preimage: Option<PaymentPreimage>, payment_id: PaymentId, recv_value_msat: Option<u64>,
-               onion_session_privs: Vec<[u8; 32]>, keys_manager: &K, best_block_height: u32,
-               send_payment_along_path: F) -> Result<(), PaymentSendFailure>
+               onion_session_privs: Vec<[u8; 32]>, node_signer: &NS, best_block_height: u32,
+               send_payment_along_path: F
+       ) -> Result<(), PaymentSendFailure>
        where
-               K::Target: KeysInterface,
+               NS::Target: NodeSigner,
                F: Fn(&Vec<RouteHop>, &Option<PaymentParameters>, &PaymentHash, &Option<PaymentSecret>, u64,
                   u32, PaymentId, &Option<PaymentPreimage>, [u8; 32]) -> Result<(), APIError>
        {
                self.send_payment_internal(route, payment_hash, payment_secret, keysend_preimage, payment_id,
-                       recv_value_msat, onion_session_privs, keys_manager, best_block_height,
+                       recv_value_msat, onion_session_privs, node_signer, best_block_height,
                        send_payment_along_path)
+                       .map_err(|e| { self.remove_outbound_if_all_failed(payment_id, &e); e })
+       }
+
+       // If we failed to send any paths, we should remove the new PaymentId from the
+       // `pending_outbound_payments` map, as the user isn't expected to `abandon_payment`.
+       fn remove_outbound_if_all_failed(&self, payment_id: PaymentId, err: &PaymentSendFailure) {
+               if let &PaymentSendFailure::AllFailedResendSafe(_) = err {
+                       let removed = self.pending_outbound_payments.lock().unwrap().remove(&payment_id).is_some();
+                       debug_assert!(removed, "We should always have a pending payment to remove here");
+               }
        }
 
        pub(super) fn claim_htlc<L: Deref>(
                &self, payment_id: PaymentId, payment_preimage: PaymentPreimage, session_priv: SecretKey,
-               path: Vec<RouteHop>, from_onchain: bool, pending_events: &Mutex<Vec<events::Event>>, logger: &L)
-        where L::Target: Logger {
+               path: Vec<RouteHop>, from_onchain: bool, pending_events: &Mutex<Vec<events::Event>>, logger: &L
+       ) where L::Target: Logger {
                let mut session_priv_bytes = [0; 32];
                session_priv_bytes.copy_from_slice(&session_priv[..]);
                let mut outbounds = self.pending_outbound_payments.lock().unwrap();
@@ -679,14 +801,20 @@ impl OutboundPayments {
                &self, source: &HTLCSource, payment_hash: &PaymentHash, onion_error: &HTLCFailReason,
                path: &Vec<RouteHop>, session_priv: &SecretKey, payment_id: &PaymentId,
                payment_params: &Option<PaymentParameters>, probing_cookie_secret: [u8; 32],
-               secp_ctx: &Secp256k1<secp256k1::All>, pending_events: &Mutex<Vec<events::Event>>, logger: &L)
-       where L::Target: Logger {
+               secp_ctx: &Secp256k1<secp256k1::All>, pending_events: &Mutex<Vec<events::Event>>, logger: &L
+       ) where L::Target: Logger {
+               #[cfg(test)]
+               let (network_update, short_channel_id, payment_retryable, onion_error_code, onion_error_data) = onion_error.decode_onion_failure(secp_ctx, logger, &source);
+               #[cfg(not(test))]
+               let (network_update, short_channel_id, payment_retryable, _, _) = onion_error.decode_onion_failure(secp_ctx, logger, &source);
+
                let mut session_priv_bytes = [0; 32];
                session_priv_bytes.copy_from_slice(&session_priv[..]);
                let mut outbounds = self.pending_outbound_payments.lock().unwrap();
                let mut all_paths_failed = false;
                let mut full_failure_ev = None;
-               if let hash_map::Entry::Occupied(mut payment) = outbounds.entry(*payment_id) {
+               let mut pending_retry_ev = None;
+               let attempts_remaining = if let hash_map::Entry::Occupied(mut payment) = outbounds.entry(*payment_id) {
                        if !payment.get_mut().remove(&session_priv_bytes, Some(&path)) {
                                log_trace!(logger, "Received duplicative fail for HTLC with payment_hash {}", log_bytes!(payment_hash.0));
                                return
@@ -695,6 +823,10 @@ impl OutboundPayments {
                                log_trace!(logger, "Received failure of HTLC with payment_hash {} after payment completion", log_bytes!(payment_hash.0));
                                return
                        }
+                       let is_retryable_now = payment.get().is_retryable_now();
+                       if let Some(scid) = short_channel_id {
+                               payment.get_mut().insert_previously_failed_scid(scid);
+                       }
                        if payment.get().remaining_parts() == 0 {
                                all_paths_failed = true;
                                if payment.get().abandoned() {
@@ -705,10 +837,12 @@ impl OutboundPayments {
                                        payment.remove();
                                }
                        }
+                       is_retryable_now
                } else {
                        log_trace!(logger, "Received duplicative fail for HTLC with payment_hash {}", log_bytes!(payment_hash.0));
                        return
-               }
+               };
+               core::mem::drop(outbounds);
                let mut retry = if let Some(payment_params_data) = payment_params {
                        let path_last_hop = path.last().expect("Outbound payments must have had a valid path");
                        Some(RouteParameters {
@@ -720,11 +854,6 @@ impl OutboundPayments {
                log_trace!(logger, "Failing outbound payment HTLC with payment_hash {}", log_bytes!(payment_hash.0));
 
                let path_failure = {
-       #[cfg(test)]
-                       let (network_update, short_channel_id, payment_retryable, onion_error_code, onion_error_data) = onion_error.decode_onion_failure(secp_ctx, logger, &source);
-       #[cfg(not(test))]
-                       let (network_update, short_channel_id, payment_retryable, _, _) = onion_error.decode_onion_failure(secp_ctx, logger, &source);
-
                        if payment_is_probe(payment_hash, &payment_id, probing_cookie_secret) {
                                if !payment_retryable {
                                        events::Event::ProbeSuccessful {
@@ -747,6 +876,12 @@ impl OutboundPayments {
                                if let Some(scid) = short_channel_id {
                                        retry.as_mut().map(|r| r.payment_params.previously_failed_channels.push(scid));
                                }
+                               if payment_retryable && attempts_remaining && retry.is_some() {
+                                       debug_assert!(full_failure_ev.is_none());
+                                       pending_retry_ev = Some(events::Event::PendingHTLCsForwardable {
+                                               time_forwardable: Duration::from_millis(MIN_HTLC_RELAY_HOLDING_CELL_MILLIS),
+                                       });
+                               }
                                events::Event::PaymentPathFailed {
                                        payment_id: Some(*payment_id),
                                        payment_hash: payment_hash.clone(),
@@ -766,6 +901,7 @@ impl OutboundPayments {
                let mut pending_events = pending_events.lock().unwrap();
                pending_events.push(path_failure);
                if let Some(ev) = full_failure_ev { pending_events.push(ev); }
+               if let Some(ev) = pending_retry_ev { pending_events.push(ev); }
        }
 
        pub(super) fn abandon_payment(&self, payment_id: PaymentId) -> Option<events::Event> {
@@ -826,8 +962,11 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
                (0, session_privs, required),
                (1, pending_fee_msat, option),
                (2, payment_hash, required),
+               (not_written, retry_strategy, (static_value, Retry::Attempts(0))),
                (4, payment_secret, option),
+               (not_written, attempts, (static_value, PaymentAttempts::new())),
                (6, total_msat, required),
+               (not_written, route_params, (static_value, None)),
                (8, pending_amt_msat, required),
                (10, starting_block_height, required),
        },