Skip to content

Commit d8aff41

Browse files
committed
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.
1 parent 6d09642 commit d8aff41

File tree

4 files changed

+151
-3
lines changed

4 files changed

+151
-3
lines changed

lightning-invoice/src/payment.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use lightning::chain;
1717
use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator};
1818
use lightning::sign::{NodeSigner, SignerProvider, EntropySource};
1919
use lightning::ln::PaymentHash;
20-
use lightning::ln::channelmanager::{ChannelManager, PaymentId, Retry, RetryableSendFailure, RecipientOnionFields};
20+
use lightning::ln::channelmanager::{ChannelManager, PaymentId, Retry, RetryableSendFailure, RecipientOnionFields, ProbeSendFailure};
2121
use lightning::routing::router::{PaymentParameters, RouteParameters, Router};
2222
use lightning::util::logger::Logger;
2323

@@ -163,6 +163,46 @@ fn pay_invoice_using_amount<P: Deref>(
163163
payer.send_payment(payment_hash, recipient_onion, payment_id, route_params, retry_strategy)
164164
}
165165

166+
/// Sends payment probes over all paths of a route that would be used to pay the given invoice.
167+
///
168+
/// See [`ChannelManager::send_preflight_probes`] for more information.
169+
pub fn preflight_probe_invoice<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>(
170+
invoice: &Bolt11Invoice, channelmanager: &ChannelManager<M, T, ES, NS, SP, F, R, L>
171+
) -> Result<(), PaymentError>
172+
where
173+
M::Target: chain::Watch<<SP::Target as SignerProvider>::Signer>,
174+
T::Target: BroadcasterInterface,
175+
ES::Target: EntropySource,
176+
NS::Target: NodeSigner,
177+
SP::Target: SignerProvider,
178+
F::Target: FeeEstimator,
179+
R::Target: Router,
180+
L::Target: Logger,
181+
{
182+
let amount_msat = if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() {
183+
invoice_amount_msat
184+
} else {
185+
return Err(PaymentError::Invoice("Failed to send probe as no amount was given in the invoice."));
186+
};
187+
188+
let expiry_time = invoice.duration_since_epoch().saturating_add(invoice.expiry_time());
189+
let mut payment_params = PaymentParameters::from_node_id(
190+
invoice.recover_payee_pub_key(),
191+
invoice.min_final_cltv_expiry_delta() as u32,
192+
)
193+
.with_expiry_time(expiry_time.as_secs())
194+
.with_route_hints(invoice.route_hints())
195+
.map_err(|_| PaymentError::Invoice("Failed to send probe as given route hints are invalid"))?;
196+
if let Some(features) = invoice.features() {
197+
payment_params = payment_params
198+
.with_bolt11_features(features.clone())
199+
.map_err(|_| PaymentError::Invoice("Failed to send probe as given features are invalid"))?;
200+
}
201+
let route_params = RouteParameters { payment_params, final_value_msat: amount_msat };
202+
203+
channelmanager.send_preflight_probes(route_params).map_err(PaymentError::ProbeSending)
204+
}
205+
166206
fn expiry_time_from_unix_epoch(invoice: &Bolt11Invoice) -> Duration {
167207
invoice.signed_invoice.raw_invoice.data.timestamp.0 + invoice.expiry_time()
168208
}
@@ -174,6 +214,8 @@ pub enum PaymentError {
174214
Invoice(&'static str),
175215
/// An error occurring when sending a payment.
176216
Sending(RetryableSendFailure),
217+
/// An error occurring when sending a payment probe.
218+
ProbeSending(ProbeSendFailure),
177219
}
178220

179221
/// A trait defining behavior of a [`Bolt11Invoice`] payer.

lightning/src/ln/channelmanager.rs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ use core::time::Duration;
7777
use core::ops::Deref;
7878

7979
// Re-export this for use in the public API.
80-
pub use crate::ln::outbound_payment::{PaymentSendFailure, Retry, RetryableSendFailure, RecipientOnionFields};
80+
pub use crate::ln::outbound_payment::{PaymentSendFailure, ProbeSendFailure, Retry, RetryableSendFailure, RecipientOnionFields};
8181
use crate::ln::script::ShutdownScript;
8282

8383
// We hold various information about HTLC relay in the HTLC objects in Channel itself:
@@ -3452,6 +3452,88 @@ where
34523452
outbound_payment::payment_is_probe(payment_hash, payment_id, self.probing_cookie_secret)
34533453
}
34543454

3455+
/// Sends payment probes over all paths of a route that would be used to pay the given
3456+
/// amount to the given `node_id`.
3457+
///
3458+
/// See [`ChannelManager::send_preflight_probes`] for more information.
3459+
pub fn send_spontaneous_preflight_probes(
3460+
&self, amount_msat: u64, node_id: PublicKey, cltv_expiry_delta: u32
3461+
) -> Result<(), ProbeSendFailure> {
3462+
let payment_params =
3463+
PaymentParameters::from_node_id(node_id, cltv_expiry_delta);
3464+
3465+
let route_params = RouteParameters { payment_params, final_value_msat: amount_msat };
3466+
3467+
self.send_preflight_probes(route_params)
3468+
}
3469+
3470+
/// Sends payment probes over all paths of a route that would be used to pay a route found
3471+
/// according to the given [`RouteParameters`].
3472+
///
3473+
/// This may be used to send "pre-flight" probes, i.e., to train our scorer before conducting
3474+
/// the actual payment. Note this is only useful if there likely is sufficient time for the
3475+
/// probe to settle before sending out the actual payment, e.g., when waiting for user
3476+
/// confirmation in a wallet UI.
3477+
///
3478+
/// Otherwise, there is a chance the probe could take up some liquidity needed to complete the
3479+
/// actual payment. Users should therefore be cautious and might avoid sending probes if
3480+
/// liquidity is scarce and/or they don't expect the probe to return before they send the
3481+
/// payment. To mitigate this issue, channels with available liquidity less than the required
3482+
/// amount times [`UserConfig::preflight_probing_liquidity_limit_multiplier`] won't be used to send
3483+
/// pre-flight probes.
3484+
pub fn send_preflight_probes(&self, route_params: RouteParameters) -> Result<(), ProbeSendFailure> {
3485+
let payer = self.get_our_node_id();
3486+
let usable_channels = self.list_usable_channels();
3487+
let first_hops = usable_channels.iter().collect::<Vec<_>>();
3488+
let inflight_htlcs = self.compute_inflight_htlcs();
3489+
3490+
let route = self
3491+
.router
3492+
.find_route(&payer, &route_params, Some(&first_hops), inflight_htlcs)
3493+
.map_err(|e| {
3494+
log_error!(self.logger, "Failed to find path for payment probe: {:?}", e);
3495+
ProbeSendFailure::RouteNotFound
3496+
})?;
3497+
3498+
let mut used_liquidity_map = HashMap::with_capacity(first_hops.len());
3499+
for path in route.paths {
3500+
if path.hops.len() + path.blinded_tail.as_ref().map_or(0, |t| t.hops.len()) < 2 {
3501+
log_debug!(
3502+
self.logger,
3503+
"Skipped sending payment probe over path with less than two hops."
3504+
);
3505+
continue;
3506+
}
3507+
3508+
if let Some(first_path_hop) = path.hops.first() {
3509+
if let Some(first_hop) = first_hops.iter().find(|h| {
3510+
h.get_outbound_payment_scid() == Some(first_path_hop.short_channel_id)
3511+
}) {
3512+
let path_value = path.final_value_msat() + path.fee_msat();
3513+
let used_liquidity =
3514+
used_liquidity_map.entry(first_path_hop.short_channel_id).or_insert(0);
3515+
3516+
if first_hop.next_outbound_htlc_limit_msat
3517+
< (*used_liquidity + path_value)
3518+
* self.default_configuration.preflight_probing_liquidity_limit_multiplier
3519+
{
3520+
log_debug!(self.logger, "Skipped sending payment probe to avoid putting channel {} under the liquidity limit.", first_path_hop.short_channel_id);
3521+
continue;
3522+
} else {
3523+
*used_liquidity += path_value;
3524+
}
3525+
}
3526+
}
3527+
3528+
self.send_probe(path).map_err(|e| {
3529+
log_error!(self.logger, "Failed to send pre-flight probe: {:?}", e);
3530+
ProbeSendFailure::SendingFailed(e)
3531+
})?;
3532+
}
3533+
3534+
Ok(())
3535+
}
3536+
34553537
/// Handles the generation of a funding transaction, optionally (for tests) with a function
34563538
/// which checks the correctness of the funding transaction given the associated channel.
34573539
fn funding_transaction_generated_intern<FundingOutput: Fn(&OutboundV1Channel<SP>, &Transaction) -> Result<OutPoint, APIError>>(

lightning/src/ln/outbound_payment.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ pub enum RetryableSendFailure {
336336
/// is in, see the description of individual enum states for more.
337337
///
338338
/// [`ChannelManager::send_payment_with_route`]: crate::ln::channelmanager::ChannelManager::send_payment_with_route
339-
#[derive(Clone, Debug)]
339+
#[derive(Clone, Debug, PartialEq, Eq)]
340340
pub enum PaymentSendFailure {
341341
/// A parameter which was passed to send_payment was invalid, preventing us from attempting to
342342
/// send the payment at all.
@@ -401,6 +401,18 @@ pub enum PaymentSendFailure {
401401
},
402402
}
403403

404+
/// Indicates that we failed to send a payment probe. Further errors may be surfaced later via
405+
/// [`Event::ProbeFailed`].
406+
///
407+
/// [`Event::ProbeFailed`]: crate::events::Event::ProbeFailed
408+
#[derive(Clone, Debug, PartialEq, Eq)]
409+
pub enum ProbeSendFailure {
410+
/// We were unable to find a route to the destination.
411+
RouteNotFound,
412+
/// We failed to send the payment probes.
413+
SendingFailed(PaymentSendFailure),
414+
}
415+
404416
/// Information which is provided, encrypted, to the payment recipient when sending HTLCs.
405417
///
406418
/// This should generally be constructed with data communicated to us from the recipient (via a

lightning/src/util/config.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,17 @@ pub struct UserConfig {
764764
///
765765
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
766766
pub accept_mpp_keysend: bool,
767+
/// The liquidity factor by which we filter our outgoing channels used for sending pre-flight probes.
768+
///
769+
/// Channels with available liquidity less than the required amount times this value won't be
770+
/// used to send pre-flight probes.
771+
///
772+
/// See [`ChannelManager::send_preflight_probes`] for more information.
773+
///
774+
/// Default value: 3
775+
///
776+
/// [`ChannelManager::send_preflight_probes`]: crate::ln::channelmanager::ChannelManager::send_preflight_probes
777+
pub preflight_probing_liquidity_limit_multiplier: u64,
767778
}
768779

769780
impl Default for UserConfig {
@@ -777,6 +788,7 @@ impl Default for UserConfig {
777788
manually_accept_inbound_channels: false,
778789
accept_intercept_htlcs: false,
779790
accept_mpp_keysend: false,
791+
preflight_probing_liquidity_limit_multiplier: 3,
780792
}
781793
}
782794
}

0 commit comments

Comments
 (0)