Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 140 additions & 13 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Copy link
Copy Markdown
Collaborator

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, but secp256k1::PublicKey::from_slice expects 33 bytes for a compressed key. This unwrap() 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:

Suggested change
(PublicKey::from_slice(&[2; 32]).unwrap(), MIN_FINAL_CLTV_EXPIRY_DELTA as u32)
(PublicKey::from_slice(&[2; 33]).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
Expand All @@ -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]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 send_spontaneous_payment for more context, similar to how send_payment_with_route references send_payment:

Suggested change
#[rustfmt::skip]
/// Sends a spontaneous payment along a given route. See
/// [`Self::send_spontaneous_payment`] for more info.
///
/// LDK will not automatically retry this payment, though it may be manually re-sent after an
/// [`Event::PaymentFailed`] is generated.

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)
}
Comment thread
Ferryx349 marked this conversation as resolved.
Expand Down Expand Up @@ -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,
);
Comment thread
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;
}
Comment thread
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.
Expand Down Expand Up @@ -9773,7 +9898,8 @@ impl<
ComplFunc: FnOnce(
Option<u64>,
bool,
) -> (Option<MonitorUpdateCompletionAction>, Option<RAAMonitorUpdateBlockingAction>),
)
-> (Option<MonitorUpdateCompletionAction>, Option<RAAMonitorUpdateBlockingAction>),
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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>,
Expand Down Expand Up @@ -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>,
Expand Down
124 changes: 124 additions & 0 deletions lightning/src/ln/payment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading