Skip to content

Add helpers for circular rebalancing#4616

Open
Ferryx349 wants to merge 1 commit into
lightningdevkit:mainfrom
Ferryx349:cir-reb
Open

Add helpers for circular rebalancing#4616
Ferryx349 wants to merge 1 commit into
lightningdevkit:mainfrom
Ferryx349:cir-reb

Conversation

@Ferryx349
Copy link
Copy Markdown

@Ferryx349 Ferryx349 commented May 17, 2026

Fixes- #3791
This PR adds two new methods to ChannelManager:

send_circular_payment

A helper for circular channel rebalancing. It:

  1. Validates that two provided channel IDs are distinct and usable.
  2. Calls find_route targeting the inbound channel's counterparty, with first_hops restricted to only the outbound channel to force the router to use it as the first hop.
  3. Manually appends a final RouteHop back to our own node over the inbound channel's SCID, with fee_msat set to the full payment amount as required for the last hop and the preceding hop's fee_msat adjusted to counterparty's forwarding fee.
  4. Sends result as a spontaneous payment via send_spontaneous_payment_with_route.

The existing guard in pay_route_internal already permits our node as the last hop in a path ("simple rebalance loop to us"), so no changes to the payment sending internals were needed.

Tests

  1. test_circular_payment_rebalance: covers the happy path (0→1→2→0), same-channel error, and channel-not-found error.
  2. test_circular_payment_no_route: verifies RouteNotFound is returned when no path exists between the two counterparties.

This is my first contribution to LDK. I tried to follow the patterns established by send_payment_with_route and send_spontaneous_preflight_probes as closely as possible. Happy to revise anything that doesn't match project conventions.

CC: @tnull

@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented May 17, 2026

👋 I see @wpaulino was un-assigned.
If you'd like another reviewer assignment, please click here.

@ldk-reviews-bot ldk-reviews-bot requested a review from wpaulino May 17, 2026 03:32
Comment thread lightning/src/ln/channelmanager.rs
Comment thread lightning/src/ln/channelmanager.rs Outdated
Comment thread lightning/src/ln/channelmanager.rs Outdated
Comment thread lightning/src/ln/channelmanager.rs
@ldk-claude-review-bot
Copy link
Copy Markdown
Collaborator

ldk-claude-review-bot commented May 17, 2026

Review Summary

New Issues Found

Inline comment posted:

  1. lightning/src/ln/channelmanager.rs:6218-6219 — Potential integer overflow in forwarding fee calculation. The unchecked fee_proportional_millionths * amount_msat can overflow u64. The channel forwarding code uses checked_mul for the same operation.

Prior Comments Status

The issues from the previous review pass appear to have been addressed:

  • cltv_expiry_delta is now correctly updated on prev_last (line 6247)
  • forwarding_info None case returns early with an error (line 6217)
  • forwarding_info.cltv_expiry_delta is used in PaymentParameters (line 6220, 6223)
  • DRY issue resolved via route_params_for_fixed_route helper
  • [2; 32] bug fixed to [2; 33] (line 5594)

The prior comments about doc quality on send_spontaneous_payment_with_route and the prev_last.cltv_expiry_delta update still apply if they haven't been explicitly acknowledged/resolved by the author.

Cross-cutting notes

  • The &&router to &router change in send_payment_with_route (line 5625) is a harmless cleanup — both work due to the blanket impl<T: Router + ?Sized, R: Deref<Target = T>> Router for R at router.rs:293.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 17, 2026

Codecov Report

❌ Patch coverage is 94.23077% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.43%. Comparing base (946ee09) to head (e6dfc3a).
⚠️ Report is 24 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channelmanager.rs 94.23% 2 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4616      +/-   ##
==========================================
+ Coverage   86.11%   86.43%   +0.32%     
==========================================
  Files         157      158       +1     
  Lines      108841   109420     +579     
  Branches   108841   109420     +579     
==========================================
+ Hits        93725    94578     +853     
+ Misses      12497    12299     -198     
+ Partials     2619     2543      -76     
Flag Coverage Δ
fuzzing-fake-hashes 5.07% <0.00%> (?)
fuzzing-real-hashes 22.73% <7.69%> (?)
tests 86.16% <94.23%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread lightning/src/ln/channelmanager.rs Outdated
Comment thread lightning/src/ln/channelmanager.rs Outdated
Comment thread lightning/src/ln/channelmanager.rs Outdated
…_route' to ChannelManager

Signed-off-by: ABHAY PANDEY <pandeyabhay967@gmail.com>
Comment on lines +6218 to +6219
let forwarding_fee = forwarding_info.fee_base_msat as u64
+ (forwarding_info.fee_proportional_millionths as u64 * amount_msat) / 1000000;
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)?;

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 1st Reminder

Hey @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@Ferryx349 Ferryx349 changed the title Add 'send_circular_payment' helper and 'send_spontaneous_payment_with… Add helpers for circular rebalancing May 19, 2026
@TheBlueMatt TheBlueMatt removed the request for review from wpaulino May 19, 2026 15:49
/// [`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,
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.

Should the inbound/outbound edges be a list?

prev_last.fee_msat = forwarding_fee;
prev_last.cltv_expiry_delta = cltv_expiry_delta;
}
path.hops.push(last_hop.clone());
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.

It might be marginally simpler to do a find_route to a dummy recipient pubkey with the last-hop pre-filled in the route params so we don't have to manually build a last-hop and push.

};
for path in route.paths.iter_mut() {
if let Some(prev_last) = path.hops.last_mut() {
prev_last.fee_msat = forwarding_fee;
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 may break the path cause we have to compute an additional proportional fee on previous hops.

/// 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(
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.

Adding this method is fine, but I'm not actually sure we want the rebalance "payment" to be a spontaneous payment - it maybe should be its own class of payment and generate a different set of events (or, maybe, if we don't want yet more events, at least result in a flag in the payment claimed/sent events being set that marks it as a rebalance payment).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants