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
161 changes: 150 additions & 11 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; 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,25 +5616,51 @@ 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. 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,
)
}

/// Sends a payment to the route found using the provided [`RouteParameters`], retrying failed
/// payment paths based on the provided `Retry`.
///
Expand Down Expand Up @@ -6115,6 +6164,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;
Comment on lines +6218 to +6219
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.

Nit/correctness: The multiplication fee_proportional_millionths as u64 * amount_msat can overflow u64 for extreme (but valid) values of the two operands. The channel forwarding code at channel.rs:11335 uses checked_mul for the same fee calculation:

let fee = amt_to_forward.checked_mul(config.forwarding_fee_proportional_millionths as u64)
    .and_then(|prop_fee| (prop_fee / 1000000).checked_add(config.forwarding_fee_base_msat as u64));

Consider using checked arithmetic here to be consistent and avoid a wrapping fee (which would cause a confusing async payment failure):

Suggested change
let forwarding_fee = forwarding_info.fee_base_msat as u64
+ (forwarding_info.fee_proportional_millionths as u64 * amount_msat) / 1000000;
let forwarding_fee = (forwarding_info.fee_proportional_millionths as u64)
.checked_mul(amount_msat)
.and_then(|prop_fee| (prop_fee / 1_000_000)
.checked_add(forwarding_info.fee_base_msat as u64))
.ok_or(RetryableSendFailure::RouteNotFound)?;

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
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