Skip to content

Fix LSPS2 variable-amount JIT fee ceiling to cover the full payment - #1095

Open
ajaysehwal wants to merge 1 commit into
lightningdevkit:mainfrom
ajaysehwal:fix/lsps2-variable-amount-fee-ceiling
Open

Fix LSPS2 variable-amount JIT fee ceiling to cover the full payment#1095
ajaysehwal wants to merge 1 commit into
lightningdevkit:mainfrom
ajaysehwal:fix/lsps2-variable-amount-fee-ceiling

Conversation

@ajaysehwal

@ajaysehwal ajaysehwal commented Sep 11, 2026

Copy link
Copy Markdown

Summary

Fixes a bug in EventHandler::lsps2_max_total_opening_fee_msat where the admissible-fee ceiling for the variable-amount LSPS2 JIT-channel flow (receive_variable_amount_via_jit_channel[_for_hash]) was computed from the post-skim PaymentClaimable::amount_msat instead of the amount the payer actually sent.

Per bLIP-52:

opening_fee = max(min_fee_msat, ceil(payment_size_msat * proportional / 1_000_000))

payment_size_msat is what the payer sent; the LSP forwards payment_size_msat - opening_fee, which is the amount_msat we observe in PaymentClaimable. The buggy code computed the ceiling as compute_opening_fee(amount_msat, 0, max_prop_fee) — over the already-discounted amount — which is a strictly smaller base than the fee was actually charged against. As a result, for every channel_opening_fee_ppm > 0 the computed ceiling ends up below the LSP's correctly-computed fee, and fail_claimable_payment() is called, after the LSP has already opened and funded the JIT channel.

Example (channel_opening_fee_ppm = 10_000, i.e. 1%)

msat
Payer sends 1,000,000
LSP's correct fee — ceil(1,000,000 × 10,000 / 1,000,000) 10,000
amount_msat delivered to us (post-skim) 990,000
Ceiling computed on mainceil(990,000 × 10,000 / 1,000,000) 9,900
Check: 10,000 > 9,900 refused

Because the fixed-amount JIT flow stores an absolute max_total_opening_fee_msat cap (unaffected by this bug), none of the existing integration tests — which all use receive_via_jit_channel[_for_hash] — exercise the broken branch.

Fix

Reconstruct the pre-skim payment size the same way the handler already does a few lines below (when recording the invoice amount): amount_msat.saturating_add(counterparty_skimmed_fee_msat).

fn lsps2_max_total_opening_fee_msat(
    payment_metadata: &[u8], amount_msat: u64, counterparty_skimmed_fee_msat: u64,
) -> Option<u64> {
    ...
    let payment_size_msat = amount_msat.saturating_add(counterparty_skimmed_fee_msat);
    compute_opening_fee(payment_size_msat, 0, max_prop_fee)
    ...
}

min_fee_msat remains hardcoded to 0 here (pre-existing, out of scope for this PR — LSPS2Parameters doesn't currently carry the agreed min_fee_msat for the variable-amount flow; happy to follow up separately if useful).

Testing

Unit tests (src/event.rs, all passing on this branch, cargo test --lib):

  • lsps2_proportional_fee_limit_admits_the_agreed_fee — reproduces the bug across a range of payment sizes (100k–1B msat). Confirmed this fails on unmodified main (ceiling 9900 rejects the agreed 1% fee) and passes with the fix; also asserts the recomputed ceiling matches the LSP's fee exactly, not just as an upper bound.
  • lsps2_proportional_fee_limit_of_zero_admits_no_skimmed_fee — zero-ppm / zero-skim regression guard.
  • lsps2_absolute_fee_limit_ignores_skimmed_fee_argumentmax_total_opening_fee_msat, when set, stays an unconditional cap and doesn't get the skimmed-fee argument threaded into it.
  • lsps2_proportional_fee_limit_saturates_instead_of_panicking_on_overflowu64::MAX-adjacent inputs don't panic; an internal compute_opening_fee overflow fails closed (None → payment refused) rather than wrapping.

I manually reverted just the fix (keeping the new tests) and confirmed exactly lsps2_proportional_fee_limit_admits_the_agreed_fee fails, while the other three pass either way — they pin separate properties, not this specific reconstruction bug.

Integration test (tests/integration_tests_rust.rs):

  • lsps2_variable_amount_jit_channel_with_proportional_fee_succeeds — mirrors lsps2_client_service_integration but drives the flow through receive_variable_amount_via_jit_channel + send_using_amount end-to-end (service/client/payer nodes, real channel open, real PaymentForwarded/PaymentReceived), asserting the payment succeeds and counterparty_skimmed_fee_msat matches the LSP's fee. This test compiles and is believed correct (built directly from the working, executed lsps2_client_service_integration template with matching macros/helpers), but I wasn't able to execute it in my sandbox — the corepc-node/electrsd downloaded bitcoind/electrs binaries fail macOS code-signature validation there (unrelated to this repo). Happy to have CI confirm, or iterate if it needs adjustment.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 11, 2026

Copy link
Copy Markdown

I've assigned @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@ajaysehwal
ajaysehwal force-pushed the fix/lsps2-variable-amount-fee-ceiling branch from 8cc9b50 to 288f8b8 Compare September 11, 2026 16:04
@ajaysehwal
ajaysehwal force-pushed the fix/lsps2-variable-amount-fee-ceiling branch from e15a80e to 288f8b8 Compare September 11, 2026 16:53
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.

2 participants