X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Fln%2Foutbound_payment.rs;h=b82cc436a114e224c266709c7fb6adfc06013eec;hb=1059ac3c28d0f7bac70bdc9be6d85e936fa6a358;hp=e154b3f0d73d9736eb7afc567d768117e8ff8202;hpb=cbeaeb708f413b1abadc7a3dadb2f7cf0a531eae;p=rust-lightning diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index e154b3f0..b82cc436 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -18,16 +18,18 @@ use crate::events::{self, PaymentFailureReason}; use crate::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::ln::channelmanager::{ChannelDetails, EventCompletionAction, HTLCSource, PaymentId}; use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason}; +use crate::offers::invoice::Bolt12Invoice; use crate::routing::router::{InFlightHtlcs, Path, PaymentParameters, Route, RouteParameters, Router}; use crate::util::errors::APIError; use crate::util::logger::Logger; use crate::util::time::Time; -#[cfg(all(not(feature = "no-std"), test))] +#[cfg(all(feature = "std", test))] use crate::util::time::tests::SinceEpoch; use crate::util::ser::ReadableArgs; use core::fmt::{self, Display, Formatter}; use core::ops::Deref; +use core::time::Duration; use crate::prelude::*; use crate::sync::Mutex; @@ -38,12 +40,6 @@ use crate::sync::Mutex; /// [`ChannelManager::timer_tick_occurred`]: crate::ln::channelmanager::ChannelManager::timer_tick_occurred pub(crate) const IDEMPOTENCY_TIMEOUT_TICKS: u8 = 7; -/// The number of ticks of [`ChannelManager::timer_tick_occurred`] until an invoice request without -/// a response is timed out. -/// -/// [`ChannelManager::timer_tick_occurred`]: crate::ln::channelmanager::ChannelManager::timer_tick_occurred -const INVOICE_REQUEST_TIMEOUT_TICKS: u8 = 3; - /// Stores the session_priv for each part of a payment that is still pending. For versions 0.0.102 /// and later, also stores information for retrying the payment. pub(crate) enum PendingOutboundPayment { @@ -51,7 +47,16 @@ pub(crate) enum PendingOutboundPayment { session_privs: HashSet<[u8; 32]>, }, AwaitingInvoice { - timer_ticks_without_response: u8, + expiration: StaleExpiration, + retry_strategy: Retry, + max_total_routing_fee_msat: Option, + }, + InvoiceReceived { + payment_hash: PaymentHash, + retry_strategy: Retry, + // Note this field is currently just replicated from AwaitingInvoice but not actually + // used anywhere. + max_total_routing_fee_msat: Option, }, Retryable { retry_strategy: Option, @@ -70,6 +75,7 @@ pub(crate) enum PendingOutboundPayment { total_msat: u64, /// Our best known block height at the time this payment was initiated. starting_block_height: u32, + remaining_max_total_routing_fee_msat: Option, }, /// When a pending payment is fulfilled, we continue tracking it until all pending HTLCs have /// been resolved. This ensures we don't look up pending payments in ChannelMonitors on restart @@ -152,6 +158,7 @@ impl PendingOutboundPayment { match self { PendingOutboundPayment::Legacy { .. } => None, PendingOutboundPayment::AwaitingInvoice { .. } => None, + PendingOutboundPayment::InvoiceReceived { payment_hash, .. } => Some(*payment_hash), PendingOutboundPayment::Retryable { payment_hash, .. } => Some(*payment_hash), PendingOutboundPayment::Fulfilled { payment_hash, .. } => *payment_hash, PendingOutboundPayment::Abandoned { payment_hash, .. } => Some(*payment_hash), @@ -165,10 +172,8 @@ impl PendingOutboundPayment { PendingOutboundPayment::Retryable { session_privs, .. } | PendingOutboundPayment::Fulfilled { session_privs, .. } | PendingOutboundPayment::Abandoned { session_privs, .. } => session_privs, - PendingOutboundPayment::AwaitingInvoice { .. } => { - debug_assert!(false); - return; - }, + PendingOutboundPayment::AwaitingInvoice { .. } | + PendingOutboundPayment::InvoiceReceived { .. } => { debug_assert!(false); return; }, }); let payment_hash = self.payment_hash(); *self = PendingOutboundPayment::Fulfilled { session_privs, payment_hash, timer_ticks_without_htlcs: 0 }; @@ -183,6 +188,12 @@ impl PendingOutboundPayment { payment_hash: *payment_hash, reason: Some(reason) }; + } else if let PendingOutboundPayment::InvoiceReceived { payment_hash, .. } = self { + *self = PendingOutboundPayment::Abandoned { + session_privs: HashSet::new(), + payment_hash: *payment_hash, + reason: Some(reason) + }; } } @@ -195,17 +206,23 @@ impl PendingOutboundPayment { PendingOutboundPayment::Abandoned { session_privs, .. } => { session_privs.remove(session_priv) }, - PendingOutboundPayment::AwaitingInvoice { .. } => { - debug_assert!(false); - false - }, + PendingOutboundPayment::AwaitingInvoice { .. } | + PendingOutboundPayment::InvoiceReceived { .. } => { debug_assert!(false); false }, }; if remove_res { - if let PendingOutboundPayment::Retryable { ref mut pending_amt_msat, ref mut pending_fee_msat, .. } = self { - let path = path.expect("Fulfilling a payment should always come with a path"); + if let PendingOutboundPayment::Retryable { + ref mut pending_amt_msat, ref mut pending_fee_msat, + ref mut remaining_max_total_routing_fee_msat, .. + } = self { + let path = path.expect("Removing a failed payment should always come with a path"); *pending_amt_msat -= path.final_value_msat(); + let path_fee_msat = path.fee_msat(); if let Some(fee_msat) = pending_fee_msat.as_mut() { - *fee_msat -= path.fee_msat(); + *fee_msat -= path_fee_msat; + } + + if let Some(max_total_routing_fee_msat) = remaining_max_total_routing_fee_msat.as_mut() { + *max_total_routing_fee_msat = max_total_routing_fee_msat.saturating_add(path_fee_msat); } } } @@ -217,20 +234,26 @@ impl PendingOutboundPayment { PendingOutboundPayment::Legacy { session_privs } | PendingOutboundPayment::Retryable { session_privs, .. } => { session_privs.insert(session_priv) - } - PendingOutboundPayment::AwaitingInvoice { .. } => { - debug_assert!(false); - false - }, + }, + PendingOutboundPayment::AwaitingInvoice { .. } | + PendingOutboundPayment::InvoiceReceived { .. } => { debug_assert!(false); false }, PendingOutboundPayment::Fulfilled { .. } => false, PendingOutboundPayment::Abandoned { .. } => false, }; if insert_res { - if let PendingOutboundPayment::Retryable { ref mut pending_amt_msat, ref mut pending_fee_msat, .. } = self { - *pending_amt_msat += path.final_value_msat(); - if let Some(fee_msat) = pending_fee_msat.as_mut() { - *fee_msat += path.fee_msat(); - } + if let PendingOutboundPayment::Retryable { + ref mut pending_amt_msat, ref mut pending_fee_msat, + ref mut remaining_max_total_routing_fee_msat, .. + } = self { + *pending_amt_msat += path.final_value_msat(); + let path_fee_msat = path.fee_msat(); + if let Some(fee_msat) = pending_fee_msat.as_mut() { + *fee_msat += path_fee_msat; + } + + if let Some(max_total_routing_fee_msat) = remaining_max_total_routing_fee_msat.as_mut() { + *max_total_routing_fee_msat = max_total_routing_fee_msat.saturating_sub(path_fee_msat); + } } } insert_res @@ -245,6 +268,7 @@ impl PendingOutboundPayment { session_privs.len() }, PendingOutboundPayment::AwaitingInvoice { .. } => 0, + PendingOutboundPayment::InvoiceReceived { .. } => 0, } } } @@ -257,8 +281,8 @@ pub enum Retry { /// Each attempt may be multiple HTLCs along multiple paths if the router decides to split up a /// retry, and may retry multiple failed HTLCs at once if they failed around the same time and /// were retried along a route from a single call to [`Router::find_route_with_id`]. - Attempts(usize), - #[cfg(not(feature = "no-std"))] + Attempts(u32), + #[cfg(feature = "std")] /// Time elapsed before abandoning retries for a payment. At least one attempt at payment is made; /// see [`PaymentParameters::expiry_time`] to avoid any attempt at payment after a specific time. /// @@ -266,16 +290,29 @@ pub enum Retry { Timeout(core::time::Duration), } +#[cfg(not(feature = "std"))] +impl_writeable_tlv_based_enum!(Retry, + ; + (0, Attempts) +); + +#[cfg(feature = "std")] +impl_writeable_tlv_based_enum!(Retry, + ; + (0, Attempts), + (2, Timeout) +); + 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)))] + #[cfg(all(feature = "std", not(test)))] (Retry::Timeout(max_duration), PaymentAttempts { first_attempted_at, .. }) => *max_duration >= crate::util::time::MonotonicTime::now().duration_since(*first_attempted_at), - #[cfg(all(not(feature = "no-std"), test))] + #[cfg(all(feature = "std", test))] (Retry::Timeout(max_duration), PaymentAttempts { first_attempted_at, .. }) => *max_duration >= SinceEpoch::now().duration_since(*first_attempted_at), } @@ -299,29 +336,29 @@ pub(crate) type PaymentAttempts = PaymentAttemptsUsingTime; 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, + pub(crate) count: u32, /// This field is only used when retry is `Retry::Timeout` which is only build with feature std - #[cfg(not(feature = "no-std"))] + #[cfg(feature = "std")] first_attempted_at: T, - #[cfg(feature = "no-std")] + #[cfg(not(feature = "std"))] phantom: core::marker::PhantomData, } -#[cfg(not(any(feature = "no-std", test)))] -type ConfiguredTime = crate::util::time::MonotonicTime; -#[cfg(feature = "no-std")] +#[cfg(not(feature = "std"))] type ConfiguredTime = crate::util::time::Eternity; -#[cfg(all(not(feature = "no-std"), test))] +#[cfg(all(feature = "std", not(test)))] +type ConfiguredTime = crate::util::time::MonotonicTime; +#[cfg(all(feature = "std", test))] type ConfiguredTime = SinceEpoch; impl PaymentAttemptsUsingTime { pub(crate) fn new() -> Self { PaymentAttemptsUsingTime { count: 0, - #[cfg(not(feature = "no-std"))] + #[cfg(feature = "std")] first_attempted_at: T::now(), - #[cfg(feature = "no-std")] + #[cfg(not(feature = "std"))] phantom: core::marker::PhantomData, } } @@ -329,9 +366,9 @@ impl PaymentAttemptsUsingTime { impl Display for PaymentAttemptsUsingTime { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { - #[cfg(feature = "no-std")] + #[cfg(not(feature = "std"))] return write!(f, "attempts: {}", self.count); - #[cfg(not(feature = "no-std"))] + #[cfg(feature = "std")] return write!( f, "attempts: {}, duration: {}s", @@ -341,6 +378,22 @@ impl Display for PaymentAttemptsUsingTime { } } +/// How long before a [`PendingOutboundPayment::AwaitingInvoice`] should be considered stale and +/// candidate for removal in [`OutboundPayments::remove_stale_payments`]. +#[derive(Clone, Copy)] +pub(crate) enum StaleExpiration { + /// Number of times [`OutboundPayments::remove_stale_payments`] is called. + TimerTicks(u64), + /// Duration since the Unix epoch. + AbsoluteTimeout(core::time::Duration), +} + +impl_writeable_tlv_based_enum!(StaleExpiration, + ; + (0, TimerTicks), + (2, AbsoluteTimeout) +); + /// Indicates an immediate error on [`ChannelManager::send_payment`]. Further errors may be /// surfaced later via [`Event::PaymentPathFailed`] and [`Event::PaymentFailed`]. /// @@ -370,7 +423,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. @@ -435,6 +488,27 @@ pub enum PaymentSendFailure { }, } +/// An error when attempting to pay a BOLT 12 invoice. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) enum Bolt12PaymentError { + /// The invoice was not requested. + UnexpectedInvoice, + /// Payment for an invoice with the corresponding [`PaymentId`] was already initiated. + 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 @@ -531,10 +605,26 @@ impl RecipientOnionFields { /// Note that if this field is non-empty, it will contain strictly increasing TLVs, each /// represented by a `(u64, Vec)` for its type number and serialized value respectively. /// This is validated when setting this field using [`Self::with_custom_tlvs`]. + #[cfg(not(c_bindings))] pub fn custom_tlvs(&self) -> &Vec<(u64, Vec)> { &self.custom_tlvs } + /// Gets the custom TLVs that will be sent or have been received. + /// + /// Custom TLVs allow sending extra application-specific data with a payment. They provide + /// additional flexibility on top of payment metadata, as while other implementations may + /// require `payment_metadata` to reflect metadata provided in an invoice, custom TLVs + /// do not have this restriction. + /// + /// Note that if this field is non-empty, it will contain strictly increasing TLVs, each + /// represented by a `(u64, Vec)` for its type number and serialized value respectively. + /// This is validated when setting this field using [`Self::with_custom_tlvs`]. + #[cfg(c_bindings)] + pub fn custom_tlvs(&self) -> Vec<(u64, Vec)> { + self.custom_tlvs.clone() + } + /// When we have received some HTLC(s) towards an MPP payment, as we receive further HTLC(s) we /// have to make sure that some fields match exactly across the parts. For those that aren't /// required to match, if they don't match we should remove them so as to not expose data @@ -638,7 +728,7 @@ impl OutboundPayments { { let preimage = payment_preimage .unwrap_or_else(|| PaymentPreimage(entropy_source.get_secure_random_bytes())); - let payment_hash = PaymentHash(Sha256::hash(&preimage.0).into_inner()); + let payment_hash = PaymentHash(Sha256::hash(&preimage.0).to_byte_array()); self.send_payment_internal(payment_id, payment_hash, recipient_onion, Some(preimage), retry_strategy, route_params, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, send_payment_along_path) @@ -657,7 +747,7 @@ impl OutboundPayments { { let preimage = payment_preimage .unwrap_or_else(|| PaymentPreimage(entropy_source.get_secure_random_bytes())); - let payment_hash = PaymentHash(Sha256::hash(&preimage.0).into_inner()); + let payment_hash = PaymentHash(Sha256::hash(&preimage.0).to_byte_array()); let onion_session_privs = self.add_new_pending_payment(payment_hash, recipient_onion.clone(), payment_id, Some(preimage), &route, None, None, entropy_source, best_block_height)?; @@ -672,6 +762,54 @@ impl OutboundPayments { } } + pub(super) fn send_payment_for_bolt12_invoice( + &self, invoice: &Bolt12Invoice, payment_id: PaymentId, router: &R, + first_hops: Vec, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, + best_block_height: u32, logger: &L, + pending_events: &Mutex)>>, + send_payment_along_path: SP, + ) -> Result<(), Bolt12PaymentError> + where + R::Target: Router, + ES::Target: EntropySource, + NS::Target: NodeSigner, + L::Target: Logger, + IH: Fn() -> InFlightHtlcs, + SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, + { + let payment_hash = invoice.payment_hash(); + let max_total_routing_fee_msat; + match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { + hash_map::Entry::Occupied(entry) => match entry.get() { + PendingOutboundPayment::AwaitingInvoice { retry_strategy, max_total_routing_fee_msat: max_total_fee, .. } => { + max_total_routing_fee_msat = *max_total_fee; + *entry.into_mut() = PendingOutboundPayment::InvoiceReceived { + payment_hash, + retry_strategy: *retry_strategy, + max_total_routing_fee_msat, + }; + }, + _ => return Err(Bolt12PaymentError::DuplicateInvoice), + }, + hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice), + }; + + let pay_params = PaymentParameters::from_bolt12_invoice(&invoice); + let amount_msat = invoice.amount_msats(); + let mut route_params = RouteParameters::from_payment_params_and_value(pay_params, amount_msat); + if let Some(max_fee_msat) = max_total_routing_fee_msat { + route_params.max_total_routing_fee_msat = Some(max_fee_msat); + } + + self.find_route_and_send_payment( + payment_hash, payment_id, route_params, router, first_hops, &inflight_htlcs, + entropy_source, node_signer, best_block_height, logger, pending_events, + &send_payment_along_path + ); + + Ok(()) + } + 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, @@ -693,11 +831,12 @@ impl OutboundPayments { let mut retry_id_route_params = None; for (pmt_id, pmt) in outbounds.iter_mut() { if pmt.is_auto_retryable_now() { - if let PendingOutboundPayment::Retryable { pending_amt_msat, total_msat, payment_params: Some(params), payment_hash, .. } = pmt { + if let PendingOutboundPayment::Retryable { pending_amt_msat, total_msat, payment_params: Some(params), payment_hash, remaining_max_total_routing_fee_msat, .. } = pmt { if pending_amt_msat < total_msat { retry_id_route_params = Some((*payment_hash, *pmt_id, RouteParameters { final_value_msat: *total_msat - *pending_amt_msat, payment_params: params.clone(), + max_total_routing_fee_msat: *remaining_max_total_routing_fee_msat, })); break } @@ -706,7 +845,7 @@ impl OutboundPayments { } core::mem::drop(outbounds); if let Some((payment_hash, payment_id, route_params)) = retry_id_route_params { - self.retry_payment_internal(payment_hash, payment_id, route_params, router, first_hops(), &inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, &send_payment_along_path) + self.find_route_and_send_payment(payment_hash, payment_id, route_params, router, first_hops(), &inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, &send_payment_along_path) } else { break } } @@ -763,7 +902,7 @@ impl OutboundPayments { } } - let route = router.find_route_with_id( + let mut route = router.find_route_with_id( &node_signer.get_node_id(Recipient::Node).unwrap(), &route_params, Some(&first_hops.iter().collect::>()), inflight_htlcs(), payment_hash, payment_id, @@ -773,6 +912,12 @@ impl OutboundPayments { RetryableSendFailure::RouteNotFound })?; + if route.route_params.as_ref() != Some(&route_params) { + debug_assert!(false, + "Routers are expected to return a Route which includes the requested RouteParameters"); + route.route_params = Some(route_params.clone()); + } + let onion_session_privs = self.add_new_pending_payment(payment_hash, recipient_onion.clone(), payment_id, keysend_preimage, &route, Some(retry_strategy), Some(route_params.payment_params.clone()), entropy_source, best_block_height) @@ -792,7 +937,7 @@ impl OutboundPayments { Ok(()) } - fn retry_payment_internal( + fn find_route_and_send_payment( &self, payment_hash: PaymentHash, payment_id: PaymentId, route_params: RouteParameters, router: &R, first_hops: Vec, inflight_htlcs: &IH, entropy_source: &ES, node_signer: &NS, best_block_height: u32, logger: &L, @@ -814,7 +959,7 @@ impl OutboundPayments { } } - let route = match router.find_route_with_id( + let mut route = match router.find_route_with_id( &node_signer.get_node_id(Recipient::Node).unwrap(), &route_params, Some(&first_hops.iter().collect::>()), inflight_htlcs(), payment_hash, payment_id, @@ -826,6 +971,13 @@ impl OutboundPayments { return } }; + + if route.route_params.as_ref() != Some(&route_params) { + debug_assert!(false, + "Routers are expected to return a Route which includes the requested RouteParameters"); + route.route_params = Some(route_params.clone()); + } + for path in route.paths.iter() { if path.hops.len() == 0 { log_error!(logger, "Unusable path in route (path.hops.len() must be at least 1"); @@ -834,12 +986,6 @@ impl OutboundPayments { } } - const RETRY_OVERFLOW_PERCENTAGE: u64 = 10; - let mut onion_session_privs = Vec::with_capacity(route.paths.len()); - for _ in 0..route.paths.len() { - onion_session_privs.push(entropy_source.get_secure_random_bytes()); - } - macro_rules! abandon_with_entry { ($payment: expr, $reason: expr) => { $payment.get_mut().mark_abandoned($reason); @@ -855,26 +1001,49 @@ impl OutboundPayments { } } } - let (total_msat, recipient_onion, keysend_preimage) = { + let (total_msat, recipient_onion, keysend_preimage, onion_session_privs) = { let mut outbounds = self.pending_outbound_payments.lock().unwrap(); match outbounds.entry(payment_id) { hash_map::Entry::Occupied(mut payment) => { - let res = match payment.get() { + match payment.get() { PendingOutboundPayment::Retryable { total_msat, keysend_preimage, payment_secret, payment_metadata, custom_tlvs, pending_amt_msat, .. } => { + const RETRY_OVERFLOW_PERCENTAGE: u64 = 10; let retry_amt_msat = route.get_total_amount(); if retry_amt_msat + *pending_amt_msat > *total_msat * (100 + RETRY_OVERFLOW_PERCENTAGE) / 100 { log_error!(logger, "retry_amt_msat of {} will put pending_amt_msat (currently: {}) more than 10% over total_payment_amt_msat of {}", retry_amt_msat, pending_amt_msat, total_msat); abandon_with_entry!(payment, PaymentFailureReason::UnexpectedError); return } - (*total_msat, RecipientOnionFields { - payment_secret: *payment_secret, - payment_metadata: payment_metadata.clone(), - custom_tlvs: custom_tlvs.clone(), - }, *keysend_preimage) + + if !payment.get().is_retryable_now() { + log_error!(logger, "Retries exhausted for payment id {}", &payment_id); + abandon_with_entry!(payment, PaymentFailureReason::RetriesExhausted); + return + } + + let total_msat = *total_msat; + let recipient_onion = RecipientOnionFields { + payment_secret: *payment_secret, + payment_metadata: payment_metadata.clone(), + custom_tlvs: custom_tlvs.clone(), + }; + let keysend_preimage = *keysend_preimage; + + let mut onion_session_privs = Vec::with_capacity(route.paths.len()); + for _ in 0..route.paths.len() { + onion_session_privs.push(entropy_source.get_secure_random_bytes()); + } + + for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.iter()) { + assert!(payment.get_mut().insert(*session_priv_bytes, path)); + } + + payment.get_mut().increment_attempts(); + + (total_msat, recipient_onion, keysend_preimage, onion_session_privs) }, PendingOutboundPayment::Legacy { .. } => { log_error!(logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102"); @@ -884,6 +1053,22 @@ impl OutboundPayments { log_error!(logger, "Payment not yet sent"); return }, + PendingOutboundPayment::InvoiceReceived { payment_hash, retry_strategy, .. } => { + let total_amount = route_params.final_value_msat; + let recipient_onion = RecipientOnionFields { + payment_secret: None, + payment_metadata: None, + custom_tlvs: vec![], + }; + let retry_strategy = Some(*retry_strategy); + let payment_params = Some(route_params.payment_params.clone()); + let (retryable_payment, onion_session_privs) = self.create_pending_payment( + *payment_hash, recipient_onion.clone(), None, &route, + retry_strategy, payment_params, entropy_source, best_block_height + ); + *payment.into_mut() = retryable_payment; + (total_amount, recipient_onion, None, onion_session_privs) + }, PendingOutboundPayment::Fulfilled { .. } => { log_error!(logger, "Payment already completed"); return @@ -892,17 +1077,7 @@ impl OutboundPayments { log_error!(logger, "Payment already abandoned (with some HTLCs still pending)"); return }, - }; - if !payment.get().is_retryable_now() { - log_error!(logger, "Retries exhausted for payment id {}", &payment_id); - abandon_with_entry!(payment, PaymentFailureReason::RetriesExhausted); - return - } - payment.get_mut().increment_attempts(); - for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.iter()) { - assert!(payment.get_mut().insert(*session_priv_bytes, path)); } - res }, hash_map::Entry::Vacant(_) => { log_error!(logger, "Payment with ID {} not found", &payment_id); @@ -936,14 +1111,14 @@ impl OutboundPayments { match err { PaymentSendFailure::AllFailedResendSafe(errs) => { Self::push_path_failed_evs_and_scids(payment_id, payment_hash, &mut route_params, route.paths, errs.into_iter().map(|e| Err(e)), logger, pending_events); - self.retry_payment_internal(payment_hash, payment_id, route_params, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, send_payment_along_path); + self.find_route_and_send_payment(payment_hash, payment_id, route_params, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, send_payment_along_path); }, PaymentSendFailure::PartialFailure { failed_paths_retry: Some(mut retry), results, .. } => { Self::push_path_failed_evs_and_scids(payment_id, payment_hash, &mut retry, route.paths, results.into_iter(), logger, pending_events); // 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. - self.retry_payment_internal(payment_hash, payment_id, retry, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, send_payment_along_path); + self.find_route_and_send_payment(payment_hash, payment_id, retry, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, logger, pending_events, send_payment_along_path); }, PaymentSendFailure::PartialFailure { failed_paths_retry: None, .. } => { // This may happen if we send a payment and some paths fail, but only due to a temporary @@ -1006,6 +1181,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); @@ -1017,7 +1193,7 @@ impl OutboundPayments { 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(), @@ -1056,48 +1232,70 @@ impl OutboundPayments { keysend_preimage: Option, route: &Route, retry_strategy: Option, payment_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(entropy_source.get_secure_random_bytes()); - } - let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap(); match pending_outbounds.entry(payment_id) { hash_map::Entry::Occupied(_) => Err(PaymentSendFailure::DuplicatePayment), hash_map::Entry::Vacant(entry) => { - let payment = entry.insert(PendingOutboundPayment::Retryable { - retry_strategy, - attempts: PaymentAttempts::new(), - payment_params, - session_privs: HashSet::new(), - pending_amt_msat: 0, - pending_fee_msat: Some(0), - payment_hash, - payment_secret: recipient_onion.payment_secret, - payment_metadata: recipient_onion.payment_metadata, - keysend_preimage, - custom_tlvs: recipient_onion.custom_tlvs, - starting_block_height: best_block_height, - total_msat: route.get_total_amount(), - }); - - for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.iter()) { - assert!(payment.insert(*session_priv_bytes, path)); - } - + let (payment, onion_session_privs) = self.create_pending_payment( + payment_hash, recipient_onion, keysend_preimage, route, retry_strategy, + payment_params, entropy_source, best_block_height + ); + entry.insert(payment); Ok(onion_session_privs) }, } } - #[allow(unused)] - pub(super) fn add_new_awaiting_invoice(&self, payment_id: PaymentId) -> Result<(), ()> { + fn create_pending_payment( + &self, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, + keysend_preimage: Option, route: &Route, retry_strategy: Option, + payment_params: Option, entropy_source: &ES, best_block_height: u32 + ) -> (PendingOutboundPayment, Vec<[u8; 32]>) + 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(entropy_source.get_secure_random_bytes()); + } + + let mut payment = PendingOutboundPayment::Retryable { + retry_strategy, + attempts: PaymentAttempts::new(), + payment_params, + session_privs: HashSet::new(), + pending_amt_msat: 0, + pending_fee_msat: Some(0), + payment_hash, + payment_secret: recipient_onion.payment_secret, + payment_metadata: recipient_onion.payment_metadata, + keysend_preimage, + custom_tlvs: recipient_onion.custom_tlvs, + starting_block_height: best_block_height, + total_msat: route.get_total_amount(), + remaining_max_total_routing_fee_msat: + route.route_params.as_ref().and_then(|p| p.max_total_routing_fee_msat), + }; + + for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.iter()) { + assert!(payment.insert(*session_priv_bytes, path)); + } + + (payment, onion_session_privs) + } + + pub(super) fn add_new_awaiting_invoice( + &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, + max_total_routing_fee_msat: Option + ) -> Result<(), ()> { let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap(); match pending_outbounds.entry(payment_id) { hash_map::Entry::Occupied(_) => Err(()), hash_map::Entry::Vacant(entry) => { entry.insert(PendingOutboundPayment::AwaitingInvoice { - timer_ticks_without_response: 0, + expiration, + retry_strategy, + max_total_routing_fee_msat, }); Ok(()) @@ -1118,7 +1316,9 @@ impl OutboundPayments { if route.paths.len() < 1 { return Err(PaymentSendFailure::ParameterError(APIError::InvalidRoute{err: "There must be at least one path to send over".to_owned()})); } - if recipient_onion.payment_secret.is_none() && route.paths.len() > 1 { + if recipient_onion.payment_secret.is_none() && route.paths.len() > 1 + && !route.paths.iter().any(|p| p.blinded_tail.is_some()) + { return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError{err: "Payment secret is required for multi-path payments".to_owned()})); } let mut total_value = 0; @@ -1129,10 +1329,6 @@ impl OutboundPayments { path_errs.push(Err(APIError::InvalidRoute{err: "Path didn't go anywhere/had bogus size".to_owned()})); continue 'path_check; } - if path.blinded_tail.is_some() { - path_errs.push(Err(APIError::InvalidRoute{err: "Sending to blinded paths isn't supported yet".to_owned()})); - continue 'path_check; - } let dest_hop_idx = if path.blinded_tail.is_some() && path.blinded_tail.as_ref().unwrap().hops.len() > 1 { usize::max_value() } else { path.hops.len() - 1 }; for (idx, hop) in path.hops.iter().enumerate() { @@ -1141,6 +1337,13 @@ impl OutboundPayments { continue 'path_check; } } + for (i, hop) in path.hops.iter().enumerate() { + // Check for duplicate channel_id in the remaining hops of the path + if path.hops.iter().skip(i + 1).any(|other_hop| other_hop.short_channel_id == hop.short_channel_id) { + path_errs.push(Err(APIError::InvalidRoute{err: "Path went through the same channel twice".to_owned()})); + continue 'path_check; + } + } total_value += path.final_value_msat(); path_errs.push(Ok(())); } @@ -1181,29 +1384,44 @@ impl OutboundPayments { } let mut has_ok = false; let mut has_err = false; - let mut pending_amt_unsent = 0; + let mut has_unsent = false; + let mut total_ok_fees_msat = 0; + let mut total_ok_amt_sent_msat = 0; for (res, path) in results.iter().zip(route.paths.iter()) { - if res.is_ok() { has_ok = true; } + if res.is_ok() { + has_ok = true; + total_ok_fees_msat += path.fee_msat(); + total_ok_amt_sent_msat += path.final_value_msat(); + } if res.is_err() { has_err = true; } if let &Err(APIError::MonitorUpdateInProgress) = res { // MonitorUpdateInProgress is inherently unsafe to retry, so we call it a // PartialFailure. has_err = true; has_ok = true; + total_ok_fees_msat += path.fee_msat(); + total_ok_amt_sent_msat += path.final_value_msat(); } else if res.is_err() { - pending_amt_unsent += path.final_value_msat(); + has_unsent = true; } } if has_err && has_ok { Err(PaymentSendFailure::PartialFailure { results, payment_id, - failed_paths_retry: if pending_amt_unsent != 0 { - if let Some(payment_params) = route.route_params.as_ref().map(|p| p.payment_params.clone()) { - Some(RouteParameters { - payment_params: payment_params, - final_value_msat: pending_amt_unsent, - }) + failed_paths_retry: if has_unsent { + if let Some(route_params) = &route.route_params { + let mut route_params = route_params.clone(); + // We calculate the leftover fee budget we're allowed to spend by + // subtracting the used fee from the total fee budget. + route_params.max_total_routing_fee_msat = route_params + .max_total_routing_fee_msat.map(|m| m.saturating_sub(total_ok_fees_msat)); + + // We calculate the remaining target amount by subtracting the succeded + // path values. + route_params.final_value_msat = route_params.final_value_msat + .saturating_sub(total_ok_amt_sent_msat); + Some(route_params) } else { None } } else { None }, }) @@ -1252,7 +1470,7 @@ impl OutboundPayments { let mut pending_events = pending_events.lock().unwrap(); if let hash_map::Entry::Occupied(mut payment) = outbounds.entry(payment_id) { if !payment.get().is_fulfilled() { - let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).into_inner()); + let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array()); log_info!(logger, "Payment with id {} and hash {} sent!", payment_id, payment_hash); let fee_paid_msat = payment.get().get_pending_fee_msat(); pending_events.push_back((events::Event::PaymentSent { @@ -1272,7 +1490,7 @@ impl OutboundPayments { // TODO: We should have a second monitor event that informs us of payments // irrevocably fulfilled. if payment.get_mut().remove(&session_priv_bytes, Some(&path)) { - let payment_hash = Some(PaymentHash(Sha256::hash(&payment_preimage.0).into_inner())); + let payment_hash = Some(PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array())); pending_events.push_back((events::Event::PaymentPathSuccessful { payment_id, payment_hash, @@ -1311,11 +1529,12 @@ impl OutboundPayments { } pub(super) fn remove_stale_payments( - &self, pending_events: &Mutex)>>) + &self, duration_since_epoch: Duration, + pending_events: &Mutex)>>) { let mut pending_outbound_payments = self.pending_outbound_payments.lock().unwrap(); let mut pending_events = pending_events.lock().unwrap(); - pending_outbound_payments.retain(|payment_id, payment| { + pending_outbound_payments.retain(|payment_id, payment| match payment { // If an outbound payment was completed, and no pending HTLCs remain, we should remove it // from the map. However, if we did that immediately when the last payment HTLC is claimed, // this could race the user making a duplicate send_payment call and our idempotency @@ -1323,7 +1542,7 @@ impl OutboundPayments { // removal. This should be more than sufficient to ensure the idempotency of any // `send_payment` calls that were made at the same time the `PaymentSent` event was being // processed. - if let PendingOutboundPayment::Fulfilled { session_privs, timer_ticks_without_htlcs, .. } = payment { + PendingOutboundPayment::Fulfilled { session_privs, timer_ticks_without_htlcs, .. } => { let mut no_remaining_entries = session_privs.is_empty(); if no_remaining_entries { for (ev, _) in pending_events.iter() { @@ -1347,17 +1566,31 @@ impl OutboundPayments { *timer_ticks_without_htlcs = 0; true } - } else if let PendingOutboundPayment::AwaitingInvoice { timer_ticks_without_response, .. } = payment { - *timer_ticks_without_response += 1; - if *timer_ticks_without_response <= INVOICE_REQUEST_TIMEOUT_TICKS { - true - } else { + }, + PendingOutboundPayment::AwaitingInvoice { expiration, .. } => { + let is_stale = match expiration { + StaleExpiration::AbsoluteTimeout(absolute_expiry) => { + *absolute_expiry <= duration_since_epoch + }, + StaleExpiration::TimerTicks(timer_ticks_remaining) => { + if *timer_ticks_remaining > 0 { + *timer_ticks_remaining -= 1; + false + } else { + true + } + }, + }; + if is_stale { pending_events.push_back( (events::Event::InvoiceRequestFailed { payment_id: *payment_id }, None) ); false + } else { + true } - } else { true } + }, + _ => true, }); } @@ -1370,10 +1603,11 @@ impl OutboundPayments { ) -> 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); @@ -1414,8 +1648,8 @@ impl OutboundPayments { 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 @@ -1424,7 +1658,7 @@ impl OutboundPayments { is_retryable_now = false; } if payment.get().remaining_parts() == 0 { - if let PendingOutboundPayment::Abandoned { payment_hash, reason, .. }= payment.get() { + if let PendingOutboundPayment::Abandoned { payment_hash, reason, .. } = payment.get() { if !payment_is_probe { full_failure_ev = Some(events::Event::PaymentFailed { payment_id: *payment_id, @@ -1445,7 +1679,7 @@ impl OutboundPayments { 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(), @@ -1469,7 +1703,7 @@ impl OutboundPayments { 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, @@ -1536,7 +1770,7 @@ fn probing_cookie_from_id(payment_id: &PaymentId, probing_cookie_secret: [u8; 32 let mut preimage = [0u8; 64]; preimage[..32].copy_from_slice(&probing_cookie_secret); preimage[32..].copy_from_slice(&payment_id.0); - PaymentHash(Sha256::hash(&preimage).into_inner()) + PaymentHash(Sha256::hash(&preimage).to_byte_array()) } impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, @@ -1562,6 +1796,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (8, pending_amt_msat, required), (9, custom_tlvs, optional_vec), (10, starting_block_height, required), + (11, remaining_max_total_routing_fee_msat, option), (not_written, retry_strategy, (static_value, None)), (not_written, attempts, (static_value, PaymentAttempts::new())), }, @@ -1571,7 +1806,14 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (2, payment_hash, required), }, (5, AwaitingInvoice) => { - (0, timer_ticks_without_response, required), + (0, expiration, required), + (2, retry_strategy, required), + (4, max_total_routing_fee_msat, option), + }, + (7, InvoiceReceived) => { + (0, payment_hash, required), + (2, retry_strategy, required), + (4, max_total_routing_fee_msat, option), }, ); @@ -1580,12 +1822,18 @@ mod tests { use bitcoin::network::constants::Network; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use core::time::Duration; + use crate::events::{Event, PathFailure, PaymentFailureReason}; use crate::ln::PaymentHash; use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; use crate::ln::features::{ChannelFeatures, NodeFeatures}; use crate::ln::msgs::{ErrorAction, LightningError}; - use crate::ln::outbound_payment::{INVOICE_REQUEST_TIMEOUT_TICKS, OutboundPayments, Retry, RetryableSendFailure}; + use crate::ln::outbound_payment::{Bolt12PaymentError, OutboundPayments, Retry, RetryableSendFailure, StaleExpiration}; + #[cfg(feature = "std")] + use crate::offers::invoice::DEFAULT_RELATIVE_EXPIRY; + use crate::offers::offer::OfferBuilder; + use crate::offers::test_utils::*; use crate::routing::gossip::NetworkGraph; use crate::routing::router::{InFlightHtlcs, Path, PaymentParameters, Route, RouteHop, RouteParameters}; use crate::sync::{Arc, Mutex, RwLock}; @@ -1628,7 +1876,7 @@ mod tests { let logger = test_utils::TestLogger::new(); let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); let scorer = RwLock::new(test_utils::TestScorer::new()); - let router = test_utils::TestRouter::new(network_graph, &scorer); + let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); let secp_ctx = Secp256k1::new(); let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); @@ -1644,7 +1892,7 @@ mod tests { PaymentId([0; 32]), None, &Route { paths: vec![], route_params: None }, Some(Retry::Attempts(1)), Some(expired_route_params.payment_params.clone()), &&keys_manager, 0).unwrap(); - outbound_payments.retry_payment_internal( + outbound_payments.find_route_and_send_payment( PaymentHash([0; 32]), PaymentId([0; 32]), expired_route_params, &&router, vec![], &|| InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &&logger, &pending_events, &|_| Ok(())); @@ -1672,7 +1920,7 @@ mod tests { let logger = test_utils::TestLogger::new(); let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); let scorer = RwLock::new(test_utils::TestScorer::new()); - let router = test_utils::TestRouter::new(network_graph, &scorer); + let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); let secp_ctx = Secp256k1::new(); let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); @@ -1688,7 +1936,7 @@ mod tests { PaymentId([0; 32]), None, &Route { paths: vec![], route_params: None }, Some(Retry::Attempts(1)), Some(route_params.payment_params.clone()), &&keys_manager, 0).unwrap(); - outbound_payments.retry_payment_internal( + outbound_payments.find_route_and_send_payment( PaymentHash([0; 32]), PaymentId([0; 32]), route_params, &&router, vec![], &|| InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &&logger, &pending_events, &|_| Ok(())); @@ -1711,7 +1959,7 @@ mod tests { let logger = test_utils::TestLogger::new(); let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); let scorer = RwLock::new(test_utils::TestScorer::new()); - let router = test_utils::TestRouter::new(network_graph, &scorer); + let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); let secp_ctx = Secp256k1::new(); let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); @@ -1728,13 +1976,16 @@ 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()), }; router.expect_find_route(route_params.clone(), Ok(route.clone())); let mut route_params_w_failed_scid = route_params.clone(); route_params_w_failed_scid.payment_params.previously_failed_channels.push(failed_scid); - router.expect_find_route(route_params_w_failed_scid, Ok(route.clone())); + let mut route_w_failed_scid = route.clone(); + route_w_failed_scid.route_params = Some(route_params_w_failed_scid.clone()); + router.expect_find_route(route_params_w_failed_scid, Ok(route_w_failed_scid)); router.expect_find_route(route_params.clone(), Ok(route.clone())); router.expect_find_route(route_params.clone(), Ok(route.clone())); @@ -1784,22 +2035,82 @@ mod tests { } #[test] - fn removes_stale_awaiting_invoice() { + fn removes_stale_awaiting_invoice_using_absolute_timeout() { let pending_events = Mutex::new(VecDeque::new()); let outbound_payments = OutboundPayments::new(); let payment_id = PaymentId([0; 32]); + let absolute_expiry = 100; + let tick_interval = 10; + let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(absolute_expiry)); assert!(!outbound_payments.has_pending_payments()); - assert!(outbound_payments.add_new_awaiting_invoice(payment_id).is_ok()); + assert!( + outbound_payments.add_new_awaiting_invoice( + payment_id, expiration, Retry::Attempts(0), None + ).is_ok() + ); assert!(outbound_payments.has_pending_payments()); - for _ in 0..INVOICE_REQUEST_TIMEOUT_TICKS { - outbound_payments.remove_stale_payments(&pending_events); + for seconds_since_epoch in (0..absolute_expiry).step_by(tick_interval) { + let duration_since_epoch = Duration::from_secs(seconds_since_epoch); + outbound_payments.remove_stale_payments(duration_since_epoch, &pending_events); + + assert!(outbound_payments.has_pending_payments()); + assert!(pending_events.lock().unwrap().is_empty()); + } + + let duration_since_epoch = Duration::from_secs(absolute_expiry); + outbound_payments.remove_stale_payments(duration_since_epoch, &pending_events); + + assert!(!outbound_payments.has_pending_payments()); + assert!(!pending_events.lock().unwrap().is_empty()); + assert_eq!( + pending_events.lock().unwrap().pop_front(), + Some((Event::InvoiceRequestFailed { payment_id }, None)), + ); + assert!(pending_events.lock().unwrap().is_empty()); + + assert!( + outbound_payments.add_new_awaiting_invoice( + payment_id, expiration, Retry::Attempts(0), None + ).is_ok() + ); + assert!(outbound_payments.has_pending_payments()); + + assert!( + outbound_payments.add_new_awaiting_invoice( + payment_id, expiration, Retry::Attempts(0), None + ).is_err() + ); + } + + #[test] + fn removes_stale_awaiting_invoice_using_timer_ticks() { + let pending_events = Mutex::new(VecDeque::new()); + let outbound_payments = OutboundPayments::new(); + let payment_id = PaymentId([0; 32]); + let timer_ticks = 3; + let expiration = StaleExpiration::TimerTicks(timer_ticks); + + assert!(!outbound_payments.has_pending_payments()); + assert!( + outbound_payments.add_new_awaiting_invoice( + payment_id, expiration, Retry::Attempts(0), None + ).is_ok() + ); + assert!(outbound_payments.has_pending_payments()); + + for i in 0..timer_ticks { + let duration_since_epoch = Duration::from_secs(i * 60); + outbound_payments.remove_stale_payments(duration_since_epoch, &pending_events); + assert!(outbound_payments.has_pending_payments()); assert!(pending_events.lock().unwrap().is_empty()); } - outbound_payments.remove_stale_payments(&pending_events); + let duration_since_epoch = Duration::from_secs(timer_ticks * 60); + outbound_payments.remove_stale_payments(duration_since_epoch, &pending_events); + assert!(!outbound_payments.has_pending_payments()); assert!(!pending_events.lock().unwrap().is_empty()); assert_eq!( @@ -1808,10 +2119,18 @@ mod tests { ); assert!(pending_events.lock().unwrap().is_empty()); - assert!(outbound_payments.add_new_awaiting_invoice(payment_id).is_ok()); + assert!( + outbound_payments.add_new_awaiting_invoice( + payment_id, expiration, Retry::Attempts(0), None + ).is_ok() + ); assert!(outbound_payments.has_pending_payments()); - assert!(outbound_payments.add_new_awaiting_invoice(payment_id).is_err()); + assert!( + outbound_payments.add_new_awaiting_invoice( + payment_id, expiration, Retry::Attempts(0), None + ).is_err() + ); } #[test] @@ -1819,9 +2138,14 @@ mod tests { let pending_events = Mutex::new(VecDeque::new()); let outbound_payments = OutboundPayments::new(); let payment_id = PaymentId([0; 32]); + let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); assert!(!outbound_payments.has_pending_payments()); - assert!(outbound_payments.add_new_awaiting_invoice(payment_id).is_ok()); + assert!( + outbound_payments.add_new_awaiting_invoice( + payment_id, expiration, Retry::Attempts(0), None + ).is_ok() + ); assert!(outbound_payments.has_pending_payments()); outbound_payments.abandon_payment( @@ -1835,4 +2159,264 @@ mod tests { ); assert!(pending_events.lock().unwrap().is_empty()); } + + #[cfg(feature = "std")] + #[test] + fn fails_sending_payment_for_expired_bolt12_invoice() { + let logger = test_utils::TestLogger::new(); + let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); + let scorer = RwLock::new(test_utils::TestScorer::new()); + let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); + let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); + + let pending_events = Mutex::new(VecDeque::new()); + let outbound_payments = OutboundPayments::new(); + let payment_id = PaymentId([0; 32]); + let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); + + assert!( + outbound_payments.add_new_awaiting_invoice( + payment_id, expiration, Retry::Attempts(0), None + ).is_ok() + ); + assert!(outbound_payments.has_pending_payments()); + + let created_at = now() - DEFAULT_RELATIVE_EXPIRY; + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), created_at).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + assert_eq!( + outbound_payments.send_payment_for_bolt12_invoice( + &invoice, payment_id, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, + &&keys_manager, 0, &&logger, &pending_events, |_| panic!() + ), + Ok(()), + ); + assert!(!outbound_payments.has_pending_payments()); + + let payment_hash = invoice.payment_hash(); + let reason = Some(PaymentFailureReason::PaymentExpired); + + assert!(!pending_events.lock().unwrap().is_empty()); + assert_eq!( + pending_events.lock().unwrap().pop_front(), + Some((Event::PaymentFailed { payment_id, payment_hash, reason }, None)), + ); + assert!(pending_events.lock().unwrap().is_empty()); + } + + #[test] + fn fails_finding_route_for_bolt12_invoice() { + let logger = test_utils::TestLogger::new(); + let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); + let scorer = RwLock::new(test_utils::TestScorer::new()); + let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); + let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); + + let pending_events = Mutex::new(VecDeque::new()); + let outbound_payments = OutboundPayments::new(); + let payment_id = PaymentId([0; 32]); + let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); + + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + assert!( + outbound_payments.add_new_awaiting_invoice( + payment_id, expiration, Retry::Attempts(0), + Some(invoice.amount_msats() / 100 + 50_000) + ).is_ok() + ); + assert!(outbound_payments.has_pending_payments()); + + router.expect_find_route( + RouteParameters::from_payment_params_and_value( + PaymentParameters::from_bolt12_invoice(&invoice), + invoice.amount_msats(), + ), + Err(LightningError { err: String::new(), action: ErrorAction::IgnoreError }), + ); + + assert_eq!( + outbound_payments.send_payment_for_bolt12_invoice( + &invoice, payment_id, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, + &&keys_manager, 0, &&logger, &pending_events, |_| panic!() + ), + Ok(()), + ); + assert!(!outbound_payments.has_pending_payments()); + + let payment_hash = invoice.payment_hash(); + let reason = Some(PaymentFailureReason::RouteNotFound); + + assert!(!pending_events.lock().unwrap().is_empty()); + assert_eq!( + pending_events.lock().unwrap().pop_front(), + Some((Event::PaymentFailed { payment_id, payment_hash, reason }, None)), + ); + assert!(pending_events.lock().unwrap().is_empty()); + } + + #[test] + fn fails_paying_for_bolt12_invoice() { + let logger = test_utils::TestLogger::new(); + let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); + let scorer = RwLock::new(test_utils::TestScorer::new()); + let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); + let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); + + let pending_events = Mutex::new(VecDeque::new()); + let outbound_payments = OutboundPayments::new(); + let payment_id = PaymentId([0; 32]); + let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); + + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + assert!( + outbound_payments.add_new_awaiting_invoice( + payment_id, expiration, Retry::Attempts(0), + Some(invoice.amount_msats() / 100 + 50_000) + ).is_ok() + ); + assert!(outbound_payments.has_pending_payments()); + + let route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::from_bolt12_invoice(&invoice), + invoice.amount_msats(), + ); + router.expect_find_route( + route_params.clone(), Ok(Route { paths: vec![], route_params: Some(route_params) }) + ); + + assert_eq!( + outbound_payments.send_payment_for_bolt12_invoice( + &invoice, payment_id, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, + &&keys_manager, 0, &&logger, &pending_events, |_| panic!() + ), + Ok(()), + ); + assert!(!outbound_payments.has_pending_payments()); + + let payment_hash = invoice.payment_hash(); + let reason = Some(PaymentFailureReason::UnexpectedError); + + assert!(!pending_events.lock().unwrap().is_empty()); + assert_eq!( + pending_events.lock().unwrap().pop_front(), + Some((Event::PaymentFailed { payment_id, payment_hash, reason }, None)), + ); + assert!(pending_events.lock().unwrap().is_empty()); + } + + #[test] + fn sends_payment_for_bolt12_invoice() { + let logger = test_utils::TestLogger::new(); + let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); + let scorer = RwLock::new(test_utils::TestScorer::new()); + let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); + let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); + + let pending_events = Mutex::new(VecDeque::new()); + let outbound_payments = OutboundPayments::new(); + let payment_id = PaymentId([0; 32]); + let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); + + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let route_params = RouteParameters { + payment_params: PaymentParameters::from_bolt12_invoice(&invoice), + final_value_msat: invoice.amount_msats(), + max_total_routing_fee_msat: Some(1234), + }; + router.expect_find_route( + route_params.clone(), + Ok(Route { + paths: vec![ + Path { + hops: vec![ + RouteHop { + pubkey: recipient_pubkey(), + node_features: NodeFeatures::empty(), + short_channel_id: 42, + channel_features: ChannelFeatures::empty(), + fee_msat: invoice.amount_msats(), + cltv_expiry_delta: 0, + maybe_announced_channel: true, + } + ], + blinded_tail: None, + } + ], + route_params: Some(route_params), + }) + ); + + assert!(!outbound_payments.has_pending_payments()); + assert_eq!( + outbound_payments.send_payment_for_bolt12_invoice( + &invoice, payment_id, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, + &&keys_manager, 0, &&logger, &pending_events, |_| panic!() + ), + Err(Bolt12PaymentError::UnexpectedInvoice), + ); + assert!(!outbound_payments.has_pending_payments()); + assert!(pending_events.lock().unwrap().is_empty()); + + assert!( + outbound_payments.add_new_awaiting_invoice( + payment_id, expiration, Retry::Attempts(0), Some(1234) + ).is_ok() + ); + assert!(outbound_payments.has_pending_payments()); + + assert_eq!( + outbound_payments.send_payment_for_bolt12_invoice( + &invoice, payment_id, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, + &&keys_manager, 0, &&logger, &pending_events, |_| Ok(()) + ), + Ok(()), + ); + assert!(outbound_payments.has_pending_payments()); + assert!(pending_events.lock().unwrap().is_empty()); + + assert_eq!( + outbound_payments.send_payment_for_bolt12_invoice( + &invoice, payment_id, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, + &&keys_manager, 0, &&logger, &pending_events, |_| panic!() + ), + Err(Bolt12PaymentError::DuplicateInvoice), + ); + assert!(outbound_payments.has_pending_payments()); + assert!(pending_events.lock().unwrap().is_empty()); + } }