Skip to content

fix: LSPS4 HTLC forwarding race condition and retry mechanism#4

Closed
martinsaposnic wants to merge 1 commit into
lsp-0.2.0from
fix/lsps4-htlc-forwarding-race
Closed

fix: LSPS4 HTLC forwarding race condition and retry mechanism#4
martinsaposnic wants to merge 1 commit into
lsp-0.2.0from
fix/lsps4-htlc-forwarding-race

Conversation

@martinsaposnic

Copy link
Copy Markdown

Summary

Fixes two bugs causing ~6.7% LSPS4 payment failures on staging (2/30 L402 payments failed):

  1. forward_intercepted_htlc lies about success: Returns Ok when channel is not live (peer_disconnected flag set). LSPS4 removes HTLC from store, then LDK's send_htlc rejects with "Cannot send while disconnected". HTLC silently lost. (22 occurrences in staging logs.)

  2. peer_connected race + short expiry: peer_connected fires before ChannelReestablish completes, so is_usable is always false. HTLCs forwarded at this point always fail silently. Combined with 10s expiry, payments arriving while peer was offline couldn't survive the webhook round-trip (webhook -> merchant wake -> TCP -> noise -> reestablish).

Changes

  • channelmanager.rs: forward_intercepted_htlc returns Err(ChannelUnavailable) when !is_live
  • service.rs: execute_htlc_actions only removes HTLC from store on Ok; on Err, keeps/re-inserts for retry
  • service.rs: peer_connected skips forwarding when no channels are is_usable
  • service.rs: New try_forward_pending_htlcs() - periodic retry for stored HTLCs (called from 5s poll loop in ldk-node)
  • htlc_store.rs: New get_all_peer_node_ids() helper
  • service.rs: HTLC_EXPIRY_THRESHOLD_SECS bumped from 10 to 30

How it works now

  1. HTLC intercepted, peer offline -> stored, webhook sent (unchanged)
  2. Peer reconnects -> peer_connected skips forwarding (channels not usable yet)
  3. ChannelReestablish completes -> channels become is_usable
  4. Next 5s poll tick -> try_forward_pending_htlcs finds stored HTLCs, sees usable channel, forwards
  5. If forward fails (channel went down again) -> HTLC stays in store for next retry
  6. After 30s with no successful forward -> HTLC expired, failed back to payer for retry

Test plan

  • cargo check -p lightning passes
  • cargo check -p lightning-liquidity passes
  • Existing lsps4_client_service_integration test passes
  • New lsps4_htlc_retry_after_reconnect integration test in ldk-node (disconnect client, send payment, reconnect, verify retry forwards successfully)
  • Deploy to staging and run 30 L402 payments - expect 0 failures

forward_intercepted_htlc was returning Ok when the channel was not live
(peer_disconnected flag set), causing LSPS4 to remove the HTLC from the
store. LDK's send_htlc would then reject with "Cannot send while
disconnected", silently losing the HTLC.

Additionally, peer_connected fires before ChannelReestablish completes,
so channels are never is_usable at that point. Combined with a 10s HTLC
expiry, payments arriving while the peer was offline had almost no
chance of being forwarded in time.

Changes:
- forward_intercepted_htlc: return Err when channel is not live
- execute_htlc_actions: only remove HTLC from store on Ok; on Err,
  keep/re-insert for retry
- peer_connected: skip forwarding when no channels are is_usable
- New try_forward_pending_htlcs(): periodic retry for stored HTLCs,
  called from the 5s poll loop
- New get_all_peer_node_ids() on HTLCStore
- Bump HTLC_EXPIRY_THRESHOLD_SECS from 10 to 30
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