X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Fln%2Foutbound_payment.rs;h=c2d0a6b62c9d11285439645a7a168d410ad9d76f;hb=72a7da8d5175f656d6e4b73b7ab7f5b2ee9a89f5;hp=44415af3d4d2afd7f8ad125cbf65856a5a535721;hpb=defa2f6811c64b3c9fc3e8f375ccf4f0c4f09c3d;p=rust-lightning diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 44415af3..c2d0a6b6 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -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, session_privs: HashSet<[u8; 32]>, payment_hash: PaymentHash, payment_secret: Option, @@ -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; + +/// Storing minimal payment attempts information required for determining if a outbound payment can +/// be retried. +pub(crate) struct PaymentAttemptsUsingTime { + /// 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 PaymentAttemptsUsingTime { + pub(crate) fn new() -> Self { + PaymentAttemptsUsingTime { + count: 0, + first_attempted_at: T::now() + } + } +} + +impl Display for PaymentAttemptsUsingTime { + 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( + pub(super) fn send_payment_with_route( &self, route: &Route, payment_hash: PaymentHash, payment_secret: &Option, - 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, &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, 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( + pub(super) fn send_spontaneous_payment( &self, route: &Route, payment_preimage: Option, 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 where - K::Target: KeysInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, F: Fn(&Vec, &Option, &PaymentHash, &Option, u64, u32, PaymentId, &Option, [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( - &self, route: &Route, payment_id: PaymentId, keys_manager: &K, best_block_height: u32, + pub(super) fn retry_payment_with_route( + &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, &Option, &PaymentHash, &Option, u64, u32, PaymentId, &Option, [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( - &self, hops: Vec, probing_cookie_secret: [u8; 32], keys_manager: &K, - best_block_height: u32, send_payment_along_path: F + pub(super) fn send_probe( + &self, hops: Vec, 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, &Option, &PaymentHash, &Option, u64, u32, PaymentId, &Option, [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( + pub(super) fn test_add_new_pending_payment( &self, payment_hash: PaymentHash, payment_secret: Option, payment_id: PaymentId, - route: &Route, keys_manager: &K, best_block_height: u32 - ) -> Result, 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, 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( + pub(super) fn add_new_pending_payment( &self, payment_hash: PaymentHash, payment_secret: Option, payment_id: PaymentId, - route: &Route, keys_manager: &K, best_block_height: u32 - ) -> Result, PaymentSendFailure> where K::Target: KeysInterface { + 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() { - 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,14 +557,14 @@ impl OutboundPayments { } } - fn send_payment_internal( + fn send_payment_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]>, keys_manager: &K, best_block_height: u32, + 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, &Option, &PaymentHash, &Option, u64, u32, PaymentId, &Option, [u8; 32]) -> Result<(), APIError> { @@ -461,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 { @@ -542,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(()) @@ -553,20 +663,30 @@ impl OutboundPayments { } #[cfg(test)] - pub(super) fn test_send_payment_internal( + pub(super) fn test_send_payment_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]>, keys_manager: &K, best_block_height: u32, + 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, &Option, &PaymentHash, &Option, u64, u32, PaymentId, &Option, [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( @@ -683,12 +803,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 @@ -697,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() { @@ -707,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 { @@ -722,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 { @@ -749,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(), @@ -768,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 { @@ -828,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), },