-
Notifications
You must be signed in to change notification settings - Fork 457
Add 'send_circular_payment' helper and 'send_spontaneous_payment_with… #4616
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -114,7 +114,7 @@ use crate::onion_message::messenger::{ | |||||||||||||
| use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; | ||||||||||||||
| use crate::routing::gossip::NodeId; | ||||||||||||||
| use crate::routing::router::{ | ||||||||||||||
| BlindedTail, FixedRouter, InFlightHtlcs, Path, Payee, PaymentParameters, Route, | ||||||||||||||
| BlindedTail, FixedRouter, InFlightHtlcs, Path, Payee, PaymentParameters, Route, RouteHop, | ||||||||||||||
| RouteParameters, RouteParametersConfig, Router, | ||||||||||||||
| }; | ||||||||||||||
| use crate::sign::ecdsa::EcdsaChannelSigner; | ||||||||||||||
|
|
@@ -5582,6 +5582,29 @@ impl< | |||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| fn route_params_for_fixed_route(route: &mut Route) -> RouteParameters { | ||||||||||||||
| let params = route.route_params.clone().unwrap_or_else(|| { | ||||||||||||||
| let (payee_node_id, cltv_delta) = route | ||||||||||||||
| .paths | ||||||||||||||
| .first() | ||||||||||||||
| .and_then(|path| { | ||||||||||||||
| path.hops.last().map(|hop| (hop.pubkey, hop.cltv_expiry_delta as u32)) | ||||||||||||||
| }) | ||||||||||||||
| .unwrap_or_else(|| { | ||||||||||||||
| (PublicKey::from_slice(&[2; 32]).unwrap(), MIN_FINAL_CLTV_EXPIRY_DELTA as u32) | ||||||||||||||
| }); | ||||||||||||||
| let dummy_payment_params = PaymentParameters::from_node_id(payee_node_id, cltv_delta); | ||||||||||||||
| RouteParameters::from_payment_params_and_value( | ||||||||||||||
| dummy_payment_params, | ||||||||||||||
| route.get_total_amount(), | ||||||||||||||
| ) | ||||||||||||||
| }); | ||||||||||||||
| if route.route_params.is_none() { | ||||||||||||||
| route.route_params = Some(params.clone()); | ||||||||||||||
| } | ||||||||||||||
| params | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// Sends a payment along a given route. See [`Self::send_payment`] for more info. | ||||||||||||||
| /// | ||||||||||||||
| /// LDK will not automatically retry this payment, though it may be manually re-sent after an | ||||||||||||||
|
|
@@ -5593,21 +5616,33 @@ impl< | |||||||||||||
| ) -> Result<(), RetryableSendFailure> { | ||||||||||||||
| let best_block_height = self.best_block.read().unwrap().height; | ||||||||||||||
| let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); | ||||||||||||||
| let route_params = route.route_params.clone().unwrap_or_else(|| { | ||||||||||||||
| // Create a dummy route params since they're a required parameter but unused in this case | ||||||||||||||
| let (payee_node_id, cltv_delta) = route.paths.first() | ||||||||||||||
| .and_then(|path| path.hops.last().map(|hop| (hop.pubkey, hop.cltv_expiry_delta as u32))) | ||||||||||||||
| .unwrap_or_else(|| (PublicKey::from_slice(&[2; 32]).unwrap(), MIN_FINAL_CLTV_EXPIRY_DELTA as u32)); | ||||||||||||||
| let dummy_payment_params = PaymentParameters::from_node_id(payee_node_id, cltv_delta); | ||||||||||||||
| RouteParameters::from_payment_params_and_value(dummy_payment_params, route.get_total_amount()) | ||||||||||||||
| }); | ||||||||||||||
| if route.route_params.is_none() { route.route_params = Some(route_params.clone()); } | ||||||||||||||
| let route_params = Self::route_params_for_fixed_route(&mut route); | ||||||||||||||
| let router = FixedRouter::new(route); | ||||||||||||||
| let logger = | ||||||||||||||
| WithContext::for_payment(&self.logger, None, None, Some(payment_hash), payment_id); | ||||||||||||||
| self.pending_outbound_payments | ||||||||||||||
| .send_payment(payment_hash, recipient_onion, payment_id, Retry::Attempts(0), | ||||||||||||||
| route_params, &&router, self.list_usable_channels(), || self.compute_inflight_htlcs(), | ||||||||||||||
| route_params, &router, self.list_usable_channels(), || self.compute_inflight_htlcs(), | ||||||||||||||
| &self.entropy_source, &self.node_signer, best_block_height, | ||||||||||||||
| &self.pending_events, |args| self.send_payment_along_path(args), &logger) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// Sends a spontaneous payment along a given route. | ||||||||||||||
| #[rustfmt::skip] | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a new public API method but the documentation is very sparse. At minimum it should mention no auto-retry behavior and reference
Suggested change
|
||||||||||||||
| pub fn send_spontaneous_payment_with_route( | ||||||||||||||
| &self, mut route: Route, payment_preimage: Option<PaymentPreimage>, | ||||||||||||||
| recipient_onion: RecipientOnionFields, payment_id: PaymentId | ||||||||||||||
| ) -> Result<PaymentHash, RetryableSendFailure> { | ||||||||||||||
| let best_block_height = self.best_block.read().unwrap().height; | ||||||||||||||
| let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); | ||||||||||||||
| let route_params = Self::route_params_for_fixed_route(&mut route); | ||||||||||||||
| let router = FixedRouter::new(route); | ||||||||||||||
| let payment_hash = payment_preimage.map(|preimage| preimage.into()); | ||||||||||||||
| let logger = | ||||||||||||||
| WithContext::for_payment(&self.logger, None, None, payment_hash, payment_id); | ||||||||||||||
| self.pending_outbound_payments | ||||||||||||||
| .send_spontaneous_payment(payment_preimage, recipient_onion, payment_id, Retry::Attempts(0), | ||||||||||||||
| route_params, &router, self.list_usable_channels(), || self.compute_inflight_htlcs(), | ||||||||||||||
| &self.entropy_source, &self.node_signer, best_block_height, | ||||||||||||||
| &self.pending_events, |args| self.send_payment_along_path(args), &logger) | ||||||||||||||
| } | ||||||||||||||
|
Ferryx349 marked this conversation as resolved.
|
||||||||||||||
|
|
@@ -6115,6 +6150,96 @@ impl< | |||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// Performs a circular rebalancing payment: funds exit our node over `outbound_channel_id`, | ||||||||||||||
| /// traverse the Lightning Network, and re-enter our node through `inbound_channel_id`. | ||||||||||||||
| /// | ||||||||||||||
| /// This is a convenient helper for moving liquidity between two of our channels without | ||||||||||||||
| /// requiring a counterparty invoice. It is equivalent to constructing an appropriate circular | ||||||||||||||
| /// [`Route`] and sending a spontaneous (keysend) payment over it. | ||||||||||||||
| /// | ||||||||||||||
| /// # How it works | ||||||||||||||
| /// | ||||||||||||||
| /// The router finds a path from our node to the `inbound_channel_id`'s counterparty, forced to | ||||||||||||||
| /// start with `outbound_channel_id`. We then manually append a final hop back to ourselves over | ||||||||||||||
| /// the `inbound_channel_id`. The route is sent as a spontaneous payment. | ||||||||||||||
| /// | ||||||||||||||
| /// # Limitations | ||||||||||||||
| /// | ||||||||||||||
| /// - Only single-path routing (no MPP support) is currently available. | ||||||||||||||
| /// - The payment is not recorded by the `Scorer`. | ||||||||||||||
| /// | ||||||||||||||
| /// # Errors | ||||||||||||||
| /// | ||||||||||||||
| /// Returns [`RetryableSendFailure::RouteNotFound`] if channel validation fails or no route can be | ||||||||||||||
| /// found. Payment-level errors (e.g. HTLC failures mid-flight) are reported asynchronously | ||||||||||||||
| /// via [`Event::PaymentFailed`]. | ||||||||||||||
| /// | ||||||||||||||
| /// [`Route`]: crate::routing::router::Route | ||||||||||||||
| /// [`Event::PaymentFailed`]: crate::events::Event::PaymentFailed | ||||||||||||||
| pub fn send_circular_payment( | ||||||||||||||
| &self, outbound_channel_id: ChannelId, inbound_channel_id: ChannelId, amount_msat: u64, | ||||||||||||||
| payment_id: PaymentId, | ||||||||||||||
| ) -> Result<PaymentHash, RetryableSendFailure> { | ||||||||||||||
| if outbound_channel_id == inbound_channel_id { | ||||||||||||||
| return Err(RetryableSendFailure::RouteNotFound); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| let usable_channels = self.list_usable_channels(); | ||||||||||||||
| let out_chan = usable_channels | ||||||||||||||
| .iter() | ||||||||||||||
| .find(|c| c.channel_id == outbound_channel_id) | ||||||||||||||
| .ok_or(RetryableSendFailure::RouteNotFound)?; | ||||||||||||||
|
|
||||||||||||||
| let in_chan = usable_channels | ||||||||||||||
| .iter() | ||||||||||||||
| .find(|c| c.channel_id == inbound_channel_id) | ||||||||||||||
| .ok_or(RetryableSendFailure::RouteNotFound)?; | ||||||||||||||
|
|
||||||||||||||
| let our_node_id = self.get_our_node_id(); | ||||||||||||||
| let forwarding_info = in_chan | ||||||||||||||
| .counterparty | ||||||||||||||
| .forwarding_info | ||||||||||||||
| .as_ref() | ||||||||||||||
| .ok_or(RetryableSendFailure::RouteNotFound)?; | ||||||||||||||
| let forwarding_fee = forwarding_info.fee_base_msat as u64 | ||||||||||||||
| + (forwarding_info.fee_proportional_millionths as u64 * amount_msat) / 1000000; | ||||||||||||||
| let cltv_expiry_delta = forwarding_info.cltv_expiry_delta as u32; | ||||||||||||||
|
|
||||||||||||||
| let route_params = RouteParameters::from_payment_params_and_value( | ||||||||||||||
| PaymentParameters::from_node_id(in_chan.counterparty.node_id, cltv_expiry_delta), | ||||||||||||||
| amount_msat + forwarding_fee, | ||||||||||||||
| ); | ||||||||||||||
|
Ferryx349 marked this conversation as resolved.
|
||||||||||||||
|
|
||||||||||||||
| let first_hops: [&ChannelDetails; 1] = [out_chan]; | ||||||||||||||
| let inflight_htlcs = self.compute_inflight_htlcs(); | ||||||||||||||
| let mut route = self | ||||||||||||||
| .router | ||||||||||||||
| .find_route(&our_node_id, &route_params, Some(&first_hops), inflight_htlcs) | ||||||||||||||
| .map_err(|_| RetryableSendFailure::RouteNotFound)?; | ||||||||||||||
| let inbound_scid = | ||||||||||||||
| in_chan.get_inbound_payment_scid().ok_or(RetryableSendFailure::RouteNotFound)?; | ||||||||||||||
| let last_hop = RouteHop { | ||||||||||||||
| pubkey: our_node_id, | ||||||||||||||
| node_features: NodeFeatures::empty(), | ||||||||||||||
| short_channel_id: inbound_scid, | ||||||||||||||
| channel_features: ChannelFeatures::empty(), | ||||||||||||||
| fee_msat: amount_msat, | ||||||||||||||
| cltv_expiry_delta: MIN_FINAL_CLTV_EXPIRY_DELTA as u32, | ||||||||||||||
| maybe_announced_channel: in_chan.is_announced, | ||||||||||||||
| }; | ||||||||||||||
| for path in route.paths.iter_mut() { | ||||||||||||||
| if let Some(prev_last) = path.hops.last_mut() { | ||||||||||||||
| prev_last.fee_msat = forwarding_fee; | ||||||||||||||
| prev_last.cltv_expiry_delta = cltv_expiry_delta; | ||||||||||||||
| } | ||||||||||||||
|
Ferryx349 marked this conversation as resolved.
|
||||||||||||||
| path.hops.push(last_hop.clone()); | ||||||||||||||
| } | ||||||||||||||
| let preimage = PaymentPreimage(self.entropy_source.get_secure_random_bytes()); | ||||||||||||||
| let onion = RecipientOnionFields::spontaneous_empty(amount_msat); | ||||||||||||||
| route.route_params = None; | ||||||||||||||
| self.send_spontaneous_payment_with_route(route, Some(preimage), onion, payment_id) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// Send a payment that is probing the given route for liquidity. We calculate the | ||||||||||||||
| /// [`PaymentHash`] of probes based on a static secret and a random [`PaymentId`], which allows | ||||||||||||||
| /// us to easily discern them from real payments. | ||||||||||||||
|
|
@@ -9773,7 +9898,8 @@ impl< | |||||||||||||
| ComplFunc: FnOnce( | ||||||||||||||
| Option<u64>, | ||||||||||||||
| bool, | ||||||||||||||
| ) -> (Option<MonitorUpdateCompletionAction>, Option<RAAMonitorUpdateBlockingAction>), | ||||||||||||||
| ) | ||||||||||||||
| -> (Option<MonitorUpdateCompletionAction>, Option<RAAMonitorUpdateBlockingAction>), | ||||||||||||||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. got these after doing fmt in channelmanager... |
||||||||||||||
| >( | ||||||||||||||
| &self, prev_hop: &HTLCPreviousHopData, payment_preimage: PaymentPreimage, | ||||||||||||||
| payment_info: Option<PaymentClaimDetails>, attribution_data: Option<AttributionData>, | ||||||||||||||
|
|
@@ -9811,7 +9937,8 @@ impl< | |||||||||||||
| ComplFunc: FnOnce( | ||||||||||||||
| Option<u64>, | ||||||||||||||
| bool, | ||||||||||||||
| ) -> (Option<MonitorUpdateCompletionAction>, Option<RAAMonitorUpdateBlockingAction>), | ||||||||||||||
| ) | ||||||||||||||
| -> (Option<MonitorUpdateCompletionAction>, Option<RAAMonitorUpdateBlockingAction>), | ||||||||||||||
| >( | ||||||||||||||
| &self, prev_hop: HTLCClaimSource, payment_preimage: PaymentPreimage, | ||||||||||||||
| payment_info: Option<PaymentClaimDetails>, attribution_data: Option<AttributionData>, | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug (pre-existing, preserved during refactor):
[2; 32]produces a 32-byte array, butsecp256k1::PublicKey::from_sliceexpects 33 bytes for a compressed key. Thisunwrap()will always panic if reached.Every other usage in the codebase uses
[2; 33]. This code path is only hit when the route has zero paths (so it's currently unreachable in practice), but it's still a latent panic that should be fixed while this code is being moved: