diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9920be84e6b..c8bc0fdf920 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -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] + pub fn send_spontaneous_payment_with_route( + &self, mut route: Route, payment_preimage: Option, + recipient_onion: RecipientOnionFields, payment_id: PaymentId + ) -> Result { + 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) } @@ -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 { + 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, + ); + + 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; + } + 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, bool, - ) -> (Option, Option), + ) + -> (Option, Option), >( &self, prev_hop: &HTLCPreviousHopData, payment_preimage: PaymentPreimage, payment_info: Option, attribution_data: Option, @@ -9811,7 +9937,8 @@ impl< ComplFunc: FnOnce( Option, bool, - ) -> (Option, Option), + ) + -> (Option, Option), >( &self, prev_hop: HTLCClaimSource, payment_preimage: PaymentPreimage, payment_info: Option, attribution_data: Option, diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index e80fcea33aa..aa1615150c8 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -5946,3 +5946,127 @@ fn bolt11_multi_node_mpp_with_retry() { panic!("{payment_sent_b:?}"); } } + +#[test] +fn test_circular_payment_rebalance() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + let node_c_id = nodes[2].node.get_our_node_id(); + + let chan_1 = create_announced_chan_between_nodes(&nodes, 0, 1); + let _chan_2 = create_announced_chan_between_nodes(&nodes, 1, 2); + let chan_3 = create_announced_chan_between_nodes(&nodes, 2, 0); + + let out_chan_id = chan_1.2; + let in_chan_id = chan_3.2; + + let amount_msat = 10_000; + + // Test 1: Same channel for both in/out + let same_chan_err = nodes[0].node.send_circular_payment( + out_chan_id, + out_chan_id, + amount_msat, + PaymentId([1; 32]), + ); + assert_eq!(same_chan_err.unwrap_err(), RetryableSendFailure::RouteNotFound); + + // Test 2: Channel not found + let fake_chan_id = ChannelId([99; 32]); + let missing_chan_err = nodes[0].node.send_circular_payment( + fake_chan_id, + in_chan_id, + amount_msat, + PaymentId([2; 32]), + ); + assert_eq!(missing_chan_err.unwrap_err(), RetryableSendFailure::RouteNotFound); + + let missing_chan_err2 = nodes[0].node.send_circular_payment( + out_chan_id, + fake_chan_id, + amount_msat, + PaymentId([3; 32]), + ); + assert_eq!(missing_chan_err2.unwrap_err(), RetryableSendFailure::RouteNotFound); + + // Test 3: Happy path + let payment_id = PaymentId([42; 32]); + let _hash = nodes[0] + .node + .send_circular_payment(out_chan_id, in_chan_id, amount_msat, payment_id) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + // Route should be 0 -> 1 -> 2 -> 0. + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + + // Forward 0 -> 1 + let node_1_msgs = remove_first_msg_event_to_node(&node_b_id, &mut events); + let send_event_1 = SendEvent::from_event(node_1_msgs); + nodes[1].node.handle_update_add_htlc(node_a_id, &send_event_1.msgs[0]); + do_commitment_signed_dance(&nodes[1], &nodes[0], &send_event_1.commitment_msg, false, true); + + // Forward 1 -> 2 + expect_and_process_pending_htlcs(&nodes[1], false); + check_added_monitors(&nodes[1], 1); + let mut events_1 = nodes[1].node.get_and_clear_pending_msg_events(); + let node_2_msgs = remove_first_msg_event_to_node(&node_c_id, &mut events_1); + let send_event_2 = SendEvent::from_event(node_2_msgs); + nodes[2].node.handle_update_add_htlc(node_b_id, &send_event_2.msgs[0]); + do_commitment_signed_dance(&nodes[2], &nodes[1], &send_event_2.commitment_msg, false, true); + + // Forward 2 -> 0 + expect_and_process_pending_htlcs(&nodes[2], false); + check_added_monitors(&nodes[2], 1); + let mut events_2 = nodes[2].node.get_and_clear_pending_msg_events(); + let node_0_msgs = remove_first_msg_event_to_node(&node_a_id, &mut events_2); + let send_event_3 = SendEvent::from_event(node_0_msgs); + nodes[0].node.handle_update_add_htlc(node_c_id, &send_event_3.msgs[0]); + do_commitment_signed_dance(&nodes[0], &nodes[2], &send_event_3.commitment_msg, false, true); + + // Now node 0 should process it and claim it. + expect_and_process_pending_htlcs(&nodes[0], false); + let claim_events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(claim_events.len(), 1); + let preimage = if let Event::PaymentClaimable { + purpose: PaymentPurpose::SpontaneousPayment(preimage), + .. + } = claim_events[0] + { + preimage + } else { + panic!("Expected PaymentClaimable SpontaneousPayment"); + }; + + let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2], &nodes[0]]]; + claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, preimage)); +} + +#[test] +fn test_circular_payment_no_route() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + let chan_1 = create_announced_chan_between_nodes(&nodes, 0, 1); + let chan_2 = create_announced_chan_between_nodes(&nodes, 0, 2); + + let out_chan_id = chan_1.2; + let in_chan_id = chan_2.2; + + let amount_msat = 10_000; + let no_route_err = nodes[0].node.send_circular_payment( + out_chan_id, + in_chan_id, + amount_msat, + PaymentId([5; 32]), + ); + assert_eq!(no_route_err.unwrap_err(), RetryableSendFailure::RouteNotFound); +}