From 5d6e41aefb6fdc5d0c5f6206efcc0dd62c92ae1d Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 3 Aug 2023 14:44:50 +0200 Subject: [PATCH 1/3] Add `send_payment_probe` method We add the capability to send a payment probe given an invoice. --- bindings/ldk_node.udl | 3 ++ src/builder.rs | 1 + src/error.rs | 3 ++ src/lib.rs | 77 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 3 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 47c776315..a24a78696 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -72,6 +72,8 @@ interface LDKNode { [Throws=NodeError] PaymentHash send_spontaneous_payment(u64 amount_msat, PublicKey node_id); [Throws=NodeError] + void send_payment_probe([ByRef]Invoice invoice); + [Throws=NodeError] Invoice receive_payment(u64 amount_msat, [ByRef]string description, u32 expiry_secs); [Throws=NodeError] Invoice receive_variable_amount_payment([ByRef]string description, u32 expiry_secs); @@ -94,6 +96,7 @@ enum NodeError { "ConnectionFailed", "InvoiceCreationFailed", "PaymentSendingFailed", + "ProbeSendingFailed", "ChannelCreationFailed", "ChannelClosingFailed", "ChannelConfigUpdateFailed", diff --git a/src/builder.rs b/src/builder.rs index 3c02815ba..bc5575ddf 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -730,6 +730,7 @@ fn build_with_store_internal( gossip_source, kv_store, logger, + router, scorer, peer_store, payment_store, diff --git a/src/error.rs b/src/error.rs index 8a188e48f..54ad0cdfd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,6 +15,8 @@ pub enum Error { InvoiceCreationFailed, /// Sending a payment has failed. PaymentSendingFailed, + /// Sending a payment probe has failed. + ProbeSendingFailed, /// A channel could not be opened. ChannelCreationFailed, /// A channel could not be closed. @@ -72,6 +74,7 @@ impl fmt::Display for Error { Self::ConnectionFailed => write!(f, "Network connection closed."), Self::InvoiceCreationFailed => write!(f, "Failed to create invoice."), Self::PaymentSendingFailed => write!(f, "Failed to send the given payment."), + Self::ProbeSendingFailed => write!(f, "Failed to send the given payment probe."), Self::ChannelCreationFailed => write!(f, "Failed to create channel."), Self::ChannelClosingFailed => write!(f, "Failed to close channel."), Self::ChannelConfigUpdateFailed => write!(f, "Failed to update channel config."), diff --git a/src/lib.rs b/src/lib.rs index fa835e627..7990cb1cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,11 +118,11 @@ use io::KVStore; use payment_store::PaymentStore; pub use payment_store::{PaymentDetails, PaymentDirection, PaymentStatus}; use peer_store::{PeerInfo, PeerStore}; -use types::{ChainMonitor, ChannelManager, KeysManager, NetworkGraph, PeerManager, Scorer}; +use types::{ChainMonitor, ChannelManager, KeysManager, NetworkGraph, PeerManager, Router, Scorer}; pub use types::{ChannelDetails, ChannelId, PeerDetails, UserChannelId}; use wallet::Wallet; -use logger::{log_error, log_info, log_trace, FilesystemLogger, Logger}; +use logger::{log_debug, log_error, log_info, log_trace, FilesystemLogger, Logger}; use lightning::chain::keysinterface::EntropySource; use lightning::chain::Confirm; @@ -136,7 +136,7 @@ use lightning_background_processor::process_events_async; use lightning_transaction_sync::EsploraSyncClient; -use lightning::routing::router::{PaymentParameters, RouteParameters}; +use lightning::routing::router::{PaymentParameters, RouteParameters, Router as LdkRouter}; use lightning_invoice::{payment, Currency, Invoice}; use bitcoin::hashes::sha256::Hash as Sha256; @@ -284,6 +284,7 @@ pub struct Node { gossip_source: Arc, kv_store: Arc, logger: Arc, + router: Arc, scorer: Arc>, peer_store: Arc>>, payment_store: Arc>>, @@ -1287,6 +1288,76 @@ impl Node { } } + /// Sends payment probes over all paths of a route that would be used to pay the given invoice. + /// + /// 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. + pub fn send_payment_probe(&self, invoice: &Invoice) -> Result<(), Error> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let amount_msat = if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { + invoice_amount_msat + } else { + log_error!(self.logger, "Failed to send probe as no amount was given in the invoice."); + return Err(Error::InvalidAmount); + }; + + let expiry_time = invoice.duration_since_epoch().saturating_add(invoice.expiry_time()); + 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.as_secs()) + .with_route_hints(invoice.route_hints()); + if let Some(features) = invoice.features() { + payment_params = payment_params.with_features(features.clone()); + } + let route_params = RouteParameters { payment_params, final_value_msat: amount_msat }; + + self.send_payment_probe_internal(route_params) + } + + fn send_payment_probe_internal(&self, route_params: RouteParameters) -> Result<(), Error> { + let payer = self.channel_manager.get_our_node_id(); + let first_hops = self.channel_manager.list_usable_channels(); + let inflight_htlcs = self.channel_manager.compute_inflight_htlcs(); + + let route = self + .router + .find_route( + &payer, + &route_params, + Some(&first_hops.iter().collect::>()), + &inflight_htlcs, + ) + .map_err(|e| { + log_error!(self.logger, "Failed to find path for payment probe: {:?}", e); + Error::ProbeSendingFailed + })?; + + for path in route.paths { + if path.hops.len() + path.blinded_tail.as_ref().map_or(0, |t| t.hops.len()) < 2 { + log_debug!( + self.logger, + "Skipped sending payment probe over path with less than two hops." + ); + continue; + } + + self.channel_manager.send_probe(path).map_err(|e| { + log_error!(self.logger, "Failed to send payment probe: {:?}", e); + Error::ProbeSendingFailed + })?; + } + + Ok(()) + } + /// Returns a payable invoice that can be used to request and receive a payment of the amount /// given. pub fn receive_payment( From e5b69ca3ccf2b196e2d5da6d9a3916348ac19d4e Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 3 Aug 2023 14:50:23 +0200 Subject: [PATCH 2/3] Add a `send_spontaneous_payment_probe` method We add the capability to send a payment probe given an amount and a payee node id. --- bindings/ldk_node.udl | 2 ++ src/lib.rs | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index a24a78696..75a6374bf 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -74,6 +74,8 @@ interface LDKNode { [Throws=NodeError] void send_payment_probe([ByRef]Invoice invoice); [Throws=NodeError] + void send_spontaneous_payment_probe(u64 amount_msat, PublicKey node_id); + [Throws=NodeError] Invoice receive_payment(u64 amount_msat, [ByRef]string description, u32 expiry_secs); [Throws=NodeError] Invoice receive_variable_amount_payment([ByRef]string description, u32 expiry_secs); diff --git a/src/lib.rs b/src/lib.rs index 7990cb1cd..a4f2713cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1322,6 +1322,29 @@ impl Node { self.send_payment_probe_internal(route_params) } + /// Sends payment probes over all paths of a route that would be used to pay the given + /// amount to the given `node_id`. + /// + /// 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. + pub fn send_spontaneous_payment_probe( + &self, amount_msat: u64, node_id: PublicKey, + ) -> Result<(), Error> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let payment_params = + PaymentParameters::from_node_id(node_id, self.config.default_cltv_expiry_delta); + + let route_params = RouteParameters { payment_params, final_value_msat: amount_msat }; + + self.send_payment_probe_internal(route_params) + } + fn send_payment_probe_internal(&self, route_params: RouteParameters) -> Result<(), Error> { let payer = self.channel_manager.get_our_node_id(); let first_hops = self.channel_manager.list_usable_channels(); From 07364b8ca59382ff1b9e93ccb8dd2f39b68dfcdf Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 3 Aug 2023 14:07:00 +0200 Subject: [PATCH 3/3] Fix typo in docs --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index a4f2713cf..96d642829 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1036,7 +1036,7 @@ impl Node { .map_err(|_| Error::ChannelConfigUpdateFailed) } - /// Send a payement given an invoice. + /// Send a payment given an invoice. pub fn send_payment(&self, invoice: &Invoice) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() {