Add preflight probing capabilities
authorElias Rohrer <dev@tnull.de>
Mon, 28 Aug 2023 13:03:04 +0000 (15:03 +0200)
committerElias Rohrer <dev@tnull.de>
Mon, 18 Sep 2023 13:08:27 +0000 (15:08 +0200)
We add a `ChannelManager::send_preflight_probes` method that can be used
to send pre-flight probes given some [`RouteParameters`]. Additionally,
we add convenience methods in for spontaneous probes and send pre-flight
probes for a given invoice.

As pre-flight probes might take up some of the available liquidity, we
here introduce that channels whose available liquidity is less than the
required amount times
`UserConfig::preflight_probing_liquidity_limit_multiplier` won't be used
to send pre-flight probes.

This commit is a more or less a carbon copy of the pre-flight
probing code recently added to LDK Node.

lightning-invoice/src/payment.rs
lightning/src/ln/channelmanager.rs
lightning/src/ln/outbound_payment.rs

index e5cf41b1b5c78eff03a273c06a21333e14e3f819..6fa23d91258af8841cb71a7a6534af738e3d4c89 100644 (file)
@@ -9,7 +9,7 @@
 
 //! Convenient utilities for paying Lightning invoices.
 
-use crate::Bolt11Invoice;
+use crate::{Bolt11Invoice, Vec};
 
 use bitcoin_hashes::Hash;
 
@@ -17,7 +17,7 @@ use lightning::chain;
 use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator};
 use lightning::sign::{NodeSigner, SignerProvider, EntropySource};
 use lightning::ln::PaymentHash;
-use lightning::ln::channelmanager::{ChannelManager, PaymentId, Retry, RetryableSendFailure, RecipientOnionFields};
+use lightning::ln::channelmanager::{ChannelManager, PaymentId, Retry, RetryableSendFailure, RecipientOnionFields, ProbeSendFailure};
 use lightning::routing::router::{PaymentParameters, RouteParameters, Router};
 use lightning::util::logger::Logger;
 
@@ -163,6 +163,85 @@ fn pay_invoice_using_amount<P: Deref>(
        payer.send_payment(payment_hash, recipient_onion, payment_id, route_params, retry_strategy)
 }
 
+/// Sends payment probes over all paths of a route that would be used to pay the given invoice.
+///
+/// See [`ChannelManager::send_preflight_probes`] for more information.
+pub fn preflight_probe_invoice<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
+       invoice: &Bolt11Invoice, channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>,
+       liquidity_limit_multiplier: Option<u64>,
+) -> Result<Vec<(PaymentHash, PaymentId)>, ProbingError>
+where
+               M::Target: chain::Watch<<SP::Target as SignerProvider>::Signer>,
+               T::Target: BroadcasterInterface,
+               ES::Target: EntropySource,
+               NS::Target: NodeSigner,
+               SP::Target: SignerProvider,
+               F::Target: FeeEstimator,
+               R::Target: Router,
+               L::Target: Logger,
+{
+       let amount_msat = if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() {
+               invoice_amount_msat
+       } else {
+               return Err(ProbingError::Invoice("Failed to send probe as no amount was given in the invoice."));
+       };
+
+       let mut payment_params = PaymentParameters::from_node_id(
+               invoice.recover_payee_pub_key(),
+               invoice.min_final_cltv_expiry_delta() as u32,
+       )
+       .with_expiry_time(expiry_time_from_unix_epoch(invoice).as_secs())
+       .with_route_hints(invoice.route_hints())
+       .unwrap();
+
+       if let Some(features) = invoice.features() {
+               payment_params = payment_params.with_bolt11_features(features.clone()).unwrap();
+       }
+       let route_params = RouteParameters { payment_params, final_value_msat: amount_msat };
+
+       channelmanager.send_preflight_probes(route_params, liquidity_limit_multiplier)
+               .map_err(ProbingError::Sending)
+}
+
+/// Sends payment probes over all paths of a route that would be used to pay the given zero-value
+/// invoice using the given amount.
+///
+/// See [`ChannelManager::send_preflight_probes`] for more information.
+pub fn preflight_probe_zero_value_invoice<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
+       invoice: &Bolt11Invoice, amount_msat: u64, channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>,
+       liquidity_limit_multiplier: Option<u64>,
+) -> Result<Vec<(PaymentHash, PaymentId)>, ProbingError>
+where
+               M::Target: chain::Watch<<SP::Target as SignerProvider>::Signer>,
+               T::Target: BroadcasterInterface,
+               ES::Target: EntropySource,
+               NS::Target: NodeSigner,
+               SP::Target: SignerProvider,
+               F::Target: FeeEstimator,
+               R::Target: Router,
+               L::Target: Logger,
+{
+       if invoice.amount_milli_satoshis().is_some() {
+               return Err(ProbingError::Invoice("amount unexpected"));
+       }
+
+       let mut payment_params = PaymentParameters::from_node_id(
+               invoice.recover_payee_pub_key(),
+               invoice.min_final_cltv_expiry_delta() as u32,
+       )
+       .with_expiry_time(expiry_time_from_unix_epoch(invoice).as_secs())
+       .with_route_hints(invoice.route_hints())
+       .unwrap();
+
+       if let Some(features) = invoice.features() {
+               payment_params = payment_params.with_bolt11_features(features.clone()).unwrap();
+       }
+       let route_params = RouteParameters { payment_params, final_value_msat: amount_msat };
+
+       channelmanager.send_preflight_probes(route_params, liquidity_limit_multiplier)
+               .map_err(ProbingError::Sending)
+}
+
 fn expiry_time_from_unix_epoch(invoice: &Bolt11Invoice) -> Duration {
        invoice.signed_invoice.raw_invoice.data.timestamp.0 + invoice.expiry_time()
 }
@@ -176,6 +255,15 @@ pub enum PaymentError {
        Sending(RetryableSendFailure),
 }
 
+/// An error that may occur when sending a payment probe.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum ProbingError {
+       /// An error resulting from the provided [`Bolt11Invoice`].
+       Invoice(&'static str),
+       /// An error occurring when sending a payment probe.
+       Sending(ProbeSendFailure),
+}
+
 /// A trait defining behavior of a [`Bolt11Invoice`] payer.
 ///
 /// Useful for unit testing internal methods.
index e2d7c90f783b5e9a93d92f80ecff3c21cf741285..1b971dce497a390e29fa060c801376e129ce9bca 100644 (file)
@@ -77,7 +77,7 @@ use core::time::Duration;
 use core::ops::Deref;
 
 // Re-export this for use in the public API.
-pub use crate::ln::outbound_payment::{PaymentSendFailure, Retry, RetryableSendFailure, RecipientOnionFields};
+pub use crate::ln::outbound_payment::{PaymentSendFailure, ProbeSendFailure, Retry, RetryableSendFailure, RecipientOnionFields};
 use crate::ln::script::ShutdownScript;
 
 // We hold various information about HTLC relay in the HTLC objects in Channel itself:
@@ -3488,6 +3488,94 @@ where
                outbound_payment::payment_is_probe(payment_hash, payment_id, self.probing_cookie_secret)
        }
 
+       /// Sends payment probes over all paths of a route that would be used to pay the given
+       /// amount to the given `node_id`.
+       ///
+       /// See [`ChannelManager::send_preflight_probes`] for more information.
+       pub fn send_spontaneous_preflight_probes(
+               &self, node_id: PublicKey, amount_msat: u64, final_cltv_expiry_delta: u32, 
+               liquidity_limit_multiplier: Option<u64>,
+       ) -> Result<Vec<(PaymentHash, PaymentId)>, ProbeSendFailure> {
+               let payment_params =
+                       PaymentParameters::from_node_id(node_id, final_cltv_expiry_delta);
+
+               let route_params = RouteParameters { payment_params, final_value_msat: amount_msat };
+
+               self.send_preflight_probes(route_params, liquidity_limit_multiplier)
+       }
+
+       /// Sends payment probes over all paths of a route that would be used to pay a route found
+       /// according to the given [`RouteParameters`].
+       ///
+       /// This may be used to send "pre-flight" probes, i.e., to train our scorer before conducting
+       /// the actual payment. Note this is only useful if there likely is sufficient time for the
+       /// probe to settle before sending out the actual payment, e.g., when waiting for user
+       /// confirmation in a wallet UI.
+       ///
+       /// Otherwise, there is a chance the probe could take up some liquidity needed to complete the
+       /// actual payment. Users should therefore be cautious and might avoid sending probes if
+       /// liquidity is scarce and/or they don't expect the probe to return before they send the
+       /// payment. To mitigate this issue, channels with available liquidity less than the required
+       /// amount times the given `liquidity_limit_multiplier` won't be used to send pre-flight
+       /// probes. If `None` is given as `liquidity_limit_multiplier`, it defaults to `3`.
+       pub fn send_preflight_probes(
+               &self, route_params: RouteParameters, liquidity_limit_multiplier: Option<u64>,
+       ) -> Result<Vec<(PaymentHash, PaymentId)>, ProbeSendFailure> {
+               let liquidity_limit_multiplier = liquidity_limit_multiplier.unwrap_or(3);
+
+               let payer = self.get_our_node_id();
+               let usable_channels = self.list_usable_channels();
+               let first_hops = usable_channels.iter().collect::<Vec<_>>();
+               let inflight_htlcs = self.compute_inflight_htlcs();
+
+               let route = self
+                       .router
+                       .find_route(&payer, &route_params, Some(&first_hops), inflight_htlcs)
+                       .map_err(|e| {
+                               log_error!(self.logger, "Failed to find path for payment probe: {:?}", e);
+                               ProbeSendFailure::RouteNotFound
+                       })?;
+
+               let mut used_liquidity_map = HashMap::with_capacity(first_hops.len());
+
+               let mut res = Vec::new();
+               for path in route.paths {
+                       if path.hops.len() < 2 {
+                               log_debug!(
+                                       self.logger,
+                                       "Skipped sending payment probe over path with less than two hops."
+                               );
+                               continue;
+                       }
+
+                       if let Some(first_path_hop) = path.hops.first() {
+                               if let Some(first_hop) = first_hops.iter().find(|h| {
+                                       h.get_outbound_payment_scid() == Some(first_path_hop.short_channel_id)
+                               }) {
+                                       let path_value = path.final_value_msat() + path.fee_msat();
+                                       let used_liquidity =
+                                               used_liquidity_map.entry(first_path_hop.short_channel_id).or_insert(0);
+
+                                       if first_hop.next_outbound_htlc_limit_msat
+                                               < (*used_liquidity + path_value) * liquidity_limit_multiplier
+                                       {
+                                               log_debug!(self.logger, "Skipped sending payment probe to avoid putting channel {} under the liquidity limit.", first_path_hop.short_channel_id);
+                                               continue;
+                                       } else {
+                                               *used_liquidity += path_value;
+                                       }
+                               }
+                       }
+
+                       res.push(self.send_probe(path).map_err(|e| {
+                               log_error!(self.logger, "Failed to send pre-flight probe: {:?}", e);
+                               ProbeSendFailure::SendingFailed(e)
+                       })?);
+               }
+
+               Ok(res)
+       }
+
        /// Handles the generation of a funding transaction, optionally (for tests) with a function
        /// which checks the correctness of the funding transaction given the associated channel.
        fn funding_transaction_generated_intern<FundingOutput: Fn(&OutboundV1Channel<SP>, &Transaction) -> Result<OutPoint, APIError>>(
index 0d6586e40f405386bab08737e8e05398ce1d5b51..583c54056fbe3f547bb05f91783ccab3ee8c39d4 100644 (file)
@@ -391,7 +391,7 @@ pub enum RetryableSendFailure {
 /// is in, see the description of individual enum states for more.
 ///
 /// [`ChannelManager::send_payment_with_route`]: crate::ln::channelmanager::ChannelManager::send_payment_with_route
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum PaymentSendFailure {
        /// A parameter which was passed to send_payment was invalid, preventing us from attempting to
        /// send the payment at all.
@@ -465,6 +465,18 @@ pub(super) enum Bolt12PaymentError {
        DuplicateInvoice,
 }
 
+/// Indicates that we failed to send a payment probe. Further errors may be surfaced later via
+/// [`Event::ProbeFailed`].
+///
+/// [`Event::ProbeFailed`]: crate::events::Event::ProbeFailed
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum ProbeSendFailure {
+       /// We were unable to find a route to the destination.
+       RouteNotFound,
+       /// We failed to send the payment probes.
+       SendingFailed(PaymentSendFailure),
+}
+
 /// Information which is provided, encrypted, to the payment recipient when sending HTLCs.
 ///
 /// This should generally be constructed with data communicated to us from the recipient (via a