X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Fln%2Foutbound_payment.rs;h=8867b2e96ae14c3e75bb960d5dd9fa97ed10d746;hb=ccf92157620da45032d75f06b5972eaf142c1ce3;hp=a4def55cfeb5edb8dd19390334a0caaa7a1e9258;hpb=948ebc67f01a6b17c04687c8c4b35431d01192e3;p=rust-lightning diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index a4def55c..8867b2e9 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -15,10 +15,10 @@ use bitcoin::secp256k1::{self, Secp256k1, SecretKey}; 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::{ChannelDetails, 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::routing::router::{InFlightHtlcs, PaymentParameters, Route, RouteHop, RouteParameters, RoutePath, Router}; use crate::util::errors::APIError; use crate::util::events; use crate::util::logger::Logger; @@ -29,6 +29,7 @@ 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; @@ -40,6 +41,9 @@ pub(crate) enum PendingOutboundPayment { session_privs: HashSet<[u8; 32]>, }, Retryable { + retry_strategy: Retry, + attempts: PaymentAttempts, + route_params: Option, session_privs: HashSet<[u8; 32]>, payment_hash: PaymentHash, payment_secret: Option, @@ -73,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, @@ -187,6 +207,46 @@ 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), + } + } +} + +#[cfg(feature = "std")] +pub(super) fn has_expired(route_params: &RouteParameters) -> bool { + if let Some(expiry_time) = route_params.payment_params.expiry_time { + if let Ok(elapsed) = std::time::SystemTime::UNIX_EPOCH.elapsed() { + return elapsed > core::time::Duration::from_secs(expiry_time) + } + } + false +} + pub(crate) type PaymentAttempts = PaymentAttemptsUsingTime; /// Storing minimal payment attempts information required for determining if a outbound payment can @@ -318,6 +378,25 @@ impl OutboundPayments { } } + pub(super) fn send_payment( + &self, payment_hash: PaymentHash, payment_secret: &Option, payment_id: PaymentId, + retry_strategy: Retry, route_params: RouteParameters, router: &R, + first_hops: Vec, inflight_htlcs: InFlightHtlcs, entropy_source: &ES, + node_signer: &NS, best_block_height: u32, send_payment_along_path: F + ) -> Result<(), PaymentSendFailure> + where + R::Target: Router, + ES::Target: EntropySource, + NS::Target: NodeSigner, + F: Fn(&Vec, &Option, &PaymentHash, &Option, u64, + u32, PaymentId, &Option, [u8; 32]) -> Result<(), APIError>, + { + self.pay_internal(payment_id, Some((payment_hash, payment_secret, retry_strategy)), + route_params, router, first_hops, inflight_htlcs, entropy_source, 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_payment_with_route( &self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option, payment_id: PaymentId, entropy_source: &ES, node_signer: &NS, best_block_height: u32, @@ -329,8 +408,10 @@ impl OutboundPayments { F: Fn(&Vec, &Option, &PaymentHash, &Option, u64, u32, PaymentId, &Option, [u8; 32]) -> Result<(), APIError> { - let onion_session_privs = self.add_new_pending_payment(payment_hash, *payment_secret, payment_id, route, 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) + 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.pay_route_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( @@ -348,11 +429,124 @@ impl OutboundPayments { 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, entropy_source, 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, node_signer, best_block_height, send_payment_along_path) { + match self.pay_route_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 check_retry_payments( + &self, router: &R, first_hops: FH, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, + best_block_height: u32, logger: &L, send_payment_along_path: SP, + ) + where + R::Target: Router, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP: Fn(&Vec, &Option, &PaymentHash, &Option, u64, + u32, PaymentId, &Option, [u8; 32]) -> Result<(), APIError>, + IH: Fn() -> InFlightHtlcs, + FH: Fn() -> Vec, + L::Target: Logger, + { + loop { + let mut outbounds = self.pending_outbound_payments.lock().unwrap(); + let mut retry_id_route_params = None; + for (pmt_id, pmt) in outbounds.iter_mut() { + if pmt.is_retryable_now() { + if let PendingOutboundPayment::Retryable { pending_amt_msat, total_msat, route_params: Some(params), .. } = pmt { + if pending_amt_msat < total_msat { + retry_id_route_params = Some((*pmt_id, params.clone())); + pmt.increment_attempts(); + break + } + } + } + } + if let Some((payment_id, route_params)) = retry_id_route_params { + core::mem::drop(outbounds); + if let Err(e) = self.pay_internal(payment_id, None, route_params, router, first_hops(), inflight_htlcs(), entropy_source, node_signer, best_block_height, &send_payment_along_path) { + log_trace!(logger, "Errored retrying payment: {:?}", e); + } + } else { break } + } + } + + fn pay_internal( + &self, payment_id: PaymentId, + initial_send_info: Option<(PaymentHash, &Option, Retry)>, + route_params: RouteParameters, router: &R, first_hops: Vec, + inflight_htlcs: InFlightHtlcs, entropy_source: &ES, node_signer: &NS, best_block_height: u32, + send_payment_along_path: &F + ) -> Result<(), PaymentSendFailure> + where + R::Target: Router, + ES::Target: EntropySource, + NS::Target: NodeSigner, + F: Fn(&Vec, &Option, &PaymentHash, &Option, u64, + u32, PaymentId, &Option, [u8; 32]) -> Result<(), APIError> + { + #[cfg(feature = "std")] { + if has_expired(&route_params) { + return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError { + err: format!("Invoice expired for payment id {}", log_bytes!(payment_id.0)), + })) + } + } + + let route = router.find_route( + &node_signer.get_node_id(Recipient::Node).unwrap(), &route_params, + Some(&first_hops.iter().collect::>()), &inflight_htlcs + ).map_err(|e| PaymentSendFailure::ParameterError(APIError::APIMisuseError { + err: format!("Failed to find a route for payment {}: {:?}", log_bytes!(payment_id.0), e), // TODO: add APIError::RouteNotFound + }))?; + + let res = if let Some((payment_hash, payment_secret, retry_strategy)) = initial_send_info { + let onion_session_privs = self.add_new_pending_payment(payment_hash, *payment_secret, payment_id, &route, retry_strategy, Some(route_params.clone()), entropy_source, best_block_height)?; + self.pay_route_internal(&route, payment_hash, payment_secret, None, payment_id, None, onion_session_privs, node_signer, best_block_height, send_payment_along_path) + } else { + self.retry_payment_with_route(&route, payment_id, entropy_source, node_signer, best_block_height, send_payment_along_path) + }; + match res { + Err(PaymentSendFailure::AllFailedResendSafe(_)) => { + let mut outbounds = self.pending_outbound_payments.lock().unwrap(); + if let Some(payment) = outbounds.get_mut(&payment_id) { + let retryable = payment.is_retryable_now(); + if retryable { + payment.increment_attempts(); + } else { return res } + } else { return res } + core::mem::drop(outbounds); + self.pay_internal(payment_id, None, route_params, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, send_payment_along_path) + }, + Err(PaymentSendFailure::PartialFailure { failed_paths_retry: Some(retry), results, .. }) => { + let mut outbounds = self.pending_outbound_payments.lock().unwrap(); + if let Some(payment) = outbounds.get_mut(&payment_id) { + let retryable = payment.is_retryable_now(); + if retryable { + payment.increment_attempts(); + } else { return Err(PaymentSendFailure::PartialFailure { failed_paths_retry: Some(retry), results, payment_id }) } + } else { return Err(PaymentSendFailure::PartialFailure { failed_paths_retry: Some(retry), results, payment_id }) } + core::mem::drop(outbounds); + + // Some paths were sent, even if we failed to send the full MPP value our recipient may + // misbehave and claim the funds, at which point we have to consider the payment sent, so + // return `Ok()` here, ignoring any retry errors. + let _ = self.pay_internal(payment_id, None, retry, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, send_payment_along_path); + Ok(()) + }, + Err(PaymentSendFailure::PartialFailure { failed_paths_retry: None, .. }) => { + // This may happen if we send a payment and some paths fail, but only due to a temporary + // monitor failure or the like, implying they're really in-flight, but we haven't sent the + // initial HTLC-Add messages yet. + Ok(()) + }, + res => res, } } @@ -423,7 +617,7 @@ impl OutboundPayments { })), } }; - 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) + self.pay_route_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( @@ -447,25 +641,29 @@ 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, entropy_source, 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, node_signer, best_block_height, send_payment_along_path) { + match self.pay_route_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( &self, payment_hash: PaymentHash, payment_secret: Option, payment_id: PaymentId, - route: &Route, entropy_source: &ES, best_block_height: u32 + route: &Route, retry_strategy: Retry, entropy_source: &ES, best_block_height: u32 ) -> Result, PaymentSendFailure> where ES::Target: EntropySource { - self.add_new_pending_payment(payment_hash, payment_secret, payment_id, route, entropy_source, best_block_height) + self.add_new_pending_payment(payment_hash, payment_secret, payment_id, route, retry_strategy, None, entropy_source, best_block_height) } pub(super) fn add_new_pending_payment( &self, payment_hash: PaymentHash, payment_secret: Option, payment_id: PaymentId, - route: &Route, entropy_source: &ES, best_block_height: u32 + route: &Route, retry_strategy: Retry, route_params: Option, + entropy_source: &ES, best_block_height: u32 ) -> Result, PaymentSendFailure> where ES::Target: EntropySource { let mut onion_session_privs = Vec::with_capacity(route.paths.len()); for _ in 0..route.paths.len() { @@ -477,6 +675,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), @@ -495,11 +696,11 @@ impl OutboundPayments { } } - fn send_payment_internal( + fn pay_route_internal( &self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option, keysend_preimage: Option, payment_id: PaymentId, recv_value_msat: Option, onion_session_privs: Vec<[u8; 32]>, node_signer: &NS, best_block_height: u32, - send_payment_along_path: F + send_payment_along_path: &F ) -> Result<(), PaymentSendFailure> where NS::Target: NodeSigner, @@ -594,10 +795,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(()) @@ -616,9 +813,19 @@ impl OutboundPayments { F: Fn(&Vec, &Option, &PaymentHash, &Option, u64, u32, PaymentId, &Option, [u8; 32]) -> Result<(), APIError> { - self.send_payment_internal(route, payment_hash, payment_secret, keysend_preimage, payment_id, + self.pay_route_internal(route, payment_hash, payment_secret, keysend_preimage, payment_id, recv_value_msat, onion_session_privs, node_signer, best_block_height, - send_payment_along_path) + &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( @@ -735,12 +942,18 @@ impl OutboundPayments { payment_params: &Option, probing_cookie_secret: [u8; 32], secp_ctx: &Secp256k1, pending_events: &Mutex>, 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 @@ -749,6 +962,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() { @@ -759,10 +976,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 { @@ -774,11 +993,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 { @@ -801,6 +1015,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(), @@ -820,6 +1040,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 { @@ -880,8 +1101,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), }, @@ -890,3 +1114,99 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (2, payment_hash, required), }, ); + +#[cfg(test)] +mod tests { + use bitcoin::blockdata::constants::genesis_block; + use bitcoin::network::constants::Network; + use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + + use crate::ln::PaymentHash; + use crate::ln::channelmanager::{PaymentId, PaymentSendFailure}; + use crate::ln::msgs::{ErrorAction, LightningError}; + use crate::ln::outbound_payment::{OutboundPayments, Retry}; + use crate::routing::gossip::NetworkGraph; + use crate::routing::router::{InFlightHtlcs, PaymentParameters, Route, RouteParameters}; + use crate::sync::Arc; + use crate::util::errors::APIError; + use crate::util::test_utils; + + #[test] + #[cfg(feature = "std")] + fn fails_paying_after_expiration() { + do_fails_paying_after_expiration(false); + do_fails_paying_after_expiration(true); + } + #[cfg(feature = "std")] + fn do_fails_paying_after_expiration(on_retry: bool) { + let outbound_payments = OutboundPayments::new(); + let logger = test_utils::TestLogger::new(); + let genesis_hash = genesis_block(Network::Testnet).header.block_hash(); + let network_graph = Arc::new(NetworkGraph::new(genesis_hash, &logger)); + let router = test_utils::TestRouter::new(network_graph); + let secp_ctx = Secp256k1::new(); + let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); + + let past_expiry_time = std::time::SystemTime::UNIX_EPOCH.elapsed().unwrap().as_secs() - 2; + let payment_params = PaymentParameters::from_node_id( + PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap())) + .with_expiry_time(past_expiry_time); + let expired_route_params = RouteParameters { + payment_params, + final_value_msat: 0, + final_cltv_expiry_delta: 0, + }; + let err = if on_retry { + outbound_payments.pay_internal( + PaymentId([0; 32]), None, expired_route_params, &&router, vec![], InFlightHtlcs::new(), + &&keys_manager, &&keys_manager, 0, &|_, _, _, _, _, _, _, _, _| Ok(())).unwrap_err() + } else { + outbound_payments.send_payment( + PaymentHash([0; 32]), &None, PaymentId([0; 32]), Retry::Attempts(0), expired_route_params, + &&router, vec![], InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, |_, _, _, _, _, _, _, _, _| Ok(())).unwrap_err() + }; + if let PaymentSendFailure::ParameterError(APIError::APIMisuseError { err }) = err { + assert!(err.contains("Invoice expired")); + } else { panic!("Unexpected error"); } + } + + #[test] + fn find_route_error() { + do_find_route_error(false); + do_find_route_error(true); + } + fn do_find_route_error(on_retry: bool) { + let outbound_payments = OutboundPayments::new(); + let logger = test_utils::TestLogger::new(); + let genesis_hash = genesis_block(Network::Testnet).header.block_hash(); + let network_graph = Arc::new(NetworkGraph::new(genesis_hash, &logger)); + let router = test_utils::TestRouter::new(network_graph); + let secp_ctx = Secp256k1::new(); + let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); + + router.expect_find_route(Err(LightningError { err: String::new(), action: ErrorAction::IgnoreError })); + + let payment_params = PaymentParameters::from_node_id( + PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap())); + let route_params = RouteParameters { + payment_params, + final_value_msat: 0, + final_cltv_expiry_delta: 0, + }; + let err = if on_retry { + outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), None, PaymentId([0; 32]), + &Route { paths: vec![], payment_params: None }, Retry::Attempts(1), Some(route_params.clone()), + &&keys_manager, 0).unwrap(); + outbound_payments.pay_internal( + PaymentId([0; 32]), None, route_params, &&router, vec![], InFlightHtlcs::new(), + &&keys_manager, &&keys_manager, 0, &|_, _, _, _, _, _, _, _, _| Ok(())).unwrap_err() + } else { + outbound_payments.send_payment( + PaymentHash([0; 32]), &None, PaymentId([0; 32]), Retry::Attempts(0), route_params, + &&router, vec![], InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, |_, _, _, _, _, _, _, _, _| Ok(())).unwrap_err() + }; + if let PaymentSendFailure::ParameterError(APIError::APIMisuseError { err }) = err { + assert!(err.contains("Failed to find a route")); + } else { panic!("Unexpected error"); } + } +}