Skip to content

LSPS4: budget forwards by next_outbound_htlc_limit_msat - #31

Open
martinsaposnic wants to merge 1 commit into
lsp-0.2.7from
lsp-0.2.7-lsps4-htlc-limit
Open

martinsaposnic wants to merge 1 commit into
lsp-0.2.7from
lsp-0.2.7-lsps4-htlc-limit

Conversation

@martinsaposnic

Copy link
Copy Markdown

Bug

calculate_htlc_actions_for_peer compared each HTLC with outbound_capacity_msat. That figure does not count the commitment fee the funder (LSP) pays for one more HTLC output, or dust exposure and in-flight limits. The largest HTLC LDK will really send is next_outbound_htlc_limit_msat, and it is lower.

An HTLC in that gap went to forward_intercepted_htlc, which returned Ok. Then LDK failed it back with temporary_channel_failure, and no splice was requested. In regtest, the 4th 9,800 sat payment into a 50k sat JIT channel fails every time, on old and new LSP images alike.

Fix

Use next_outbound_htlc_limit_msat in the capacity map. HTLCs in the gap now get SpliceChannel (or OpenChannel) and are held until the timer forwards them.

Not changed

  • Forwarded amounts and the skim.
  • The inbound >= expected outbound check from bfc1b29 (it runs before this code).
  • Splice/open sizing (ldk-node adds channel_over_provisioning_ppm, 500k in lightning-node, so after a splice the limit covers the HTLC).

Tests

  • htlc_above_send_limit_but_within_capacity_gets_liquidity: fails before the fix (Expected SpliceChannel, got None), passes after. It asserts the HTLC is still pending and not failed back.
  • htlc_at_send_limit_is_forwarded (control): the largest forward that fits is still forwarded and reaches the client.
  • All lightning-liquidity tests pass.

Known limit

Later HTLCs in one batch subtract only their amount, not the extra commitment fee each one adds. Such a batch can still overshoot and fail back. No funds move in that case, and the sender retries.

calculate_htlc_actions_for_peer compared each HTLC against the channel's
outbound_capacity_msat. That figure ignores the commitment fee the funder
pays for one more HTLC output (and dust exposure / in-flight limits), so
the largest HTLC LDK will send is next_outbound_htlc_limit_msat, which is
lower. An HTLC in that gap was "forwarded": forward_intercepted_htlc
returned Ok, then LDK failed it back with temporary_channel_failure and no
splice was ever requested. Seen in regtest on the 4th 9,800 sat payment
into a 50k sat JIT channel, every time.

Use next_outbound_htlc_limit_msat so such HTLCs get a splice/open instead.
Forwarded amounts and the inbound >= outbound check are unchanged.
@martinsaposnic

Copy link
Copy Markdown
Author

Verification

Unit/integration (#31):

  • New htlc_above_send_limit_but_within_capacity_gets_liquidity: fails on lsp-0.2.7 (Expected SpliceChannel, got None), passes with the fix. It asserts the HTLC stays pending and is not failed back after process_pending_htlc_forwards.
  • New control htlc_at_send_limit_is_forwarded: the largest forward that fits is still forwarded and the update_add_htlc reaches the client.
  • All lightning-liquidity tests pass (97 unit + all LSPS integration suites), and the 6 existing LSPS4 security regression tests (lying onion, etc.) still pass.

Local e2e A/B (regtest stack, same script, fresh compose down -v each time):
setup-all (2 JIT payments) + matrix spec (1) + checkout-and-pay.sh (4th payment into the same 50k JIT channel):

LSP image setup-all matrix 4th payment
before lightningdevkit#934 pass pass fail (TemporaryChannelFailure)
lightningdevkit#934 pass pass fail (TemporaryChannelFailure)
this PR pass pass pass

LSP log for the 4th payment: outbound_capacity: 9800000msat, next_outbound_htlc_limit: 9260000msat. The forward is 9,800,000, so LSPS4 requests a splice. In regtest the splice hit "Insufficient confirmed UTXOs", and ldk-node fell back to OpenChannel. The held HTLC was forwarded once (9.8M for 10M inbound, the same 2% skim) and claimed. Later payments used the new channel.

CI: build + e2e (workspace, workspace-preview, legacy-0.18.0, legacy-0.15.0) pass.

Fund-safety analysis

  • The fix only changes decisions for HTLCs that LDK would always fail. send_htlc rejects amount_msat > next_outbound_htlc_limit_msat with HTLCMaximum (lightning/src/ln/channel.rs ~12715). That is the same field LSPS4 now reads, and it is checked in every channel state (during a pending splice LDK uses the min across funding scopes). So the fix never turns a forward that could succeed into a splice/open.
  • Amounts are unchanged: the same amount_to_forward_msat and skim. The inbound >= expected outbound check from bfc1b29 runs before this code and is untouched.
  • Cost when it triggers: the existing splice/open path, sized amt + 50% over-provisioning (min 50k sat), with the existing per-peer cooldown. Before, these HTLCs failed. An attacker gains nothing new: triggering liquidity already needed an HTLC above capacity, and the bfc1b29 check makes them pay inbound >= outbound. The threshold only moves by the commitment-fee gap (540 sat in regtest).
  • Known limit (not a regression): later HTLCs in one batch subtract only their amount, not the extra commitment fee per HTLC. Such a batch can still overshoot and fail back with no funds moved, the same as today.

Upstream LSPS2

Not affected by this bug class. Upstream LSPS2 (v0.2.7 lsps2/service.rs) has no capacity check: each JIT invoice gets a fresh channel sized amt + channel_over_provisioning_ppm (ldk-node), and it never decides "forward vs. add liquidity" on a reused channel.

Relation to the bfc1b29 security fix

None. bfc1b29 did not touch capacity code. The outbound_capacity_msat budget dates from the original LSPS4 commit (3a8b11c).

@martinsaposnic
martinsaposnic marked this pull request as ready for review October 13, 2026 14:43
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.

1 participant