From 20c842b4969f6de53cba703cfa959153ac10cb80 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 28 Aug 2023 15:03:04 +0200 Subject: [PATCH] Add preflight probing capabilities 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 | 92 +++++++++++++++++++++++++++- lightning/src/ln/channelmanager.rs | 90 ++++++++++++++++++++++++++- lightning/src/ln/outbound_payment.rs | 14 ++++- 3 files changed, 192 insertions(+), 4 deletions(-) diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs index e5cf41b1b..6fa23d912 100644 --- a/lightning-invoice/src/payment.rs +++ b/lightning-invoice/src/payment.rs @@ -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( 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( + invoice: &Bolt11Invoice, channelmanager: &ChannelManager, + liquidity_limit_multiplier: Option, +) -> Result, ProbingError> +where + M::Target: chain::Watch<::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( + invoice: &Bolt11Invoice, amount_msat: u64, channelmanager: &ChannelManager, + liquidity_limit_multiplier: Option, +) -> Result, ProbingError> +where + M::Target: chain::Watch<::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. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e2d7c90f7..1b971dce4 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -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, + ) -> Result, 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, + ) -> Result, 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::>(); + 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, &Transaction) -> Result>( diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 0d6586e40..583c54056 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -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 -- 2.39.5