Skip to content

fix: add timeout to payment status polling loop - #55

Open
lucianocasalunga wants to merge 1 commit into
lnbits:mainfrom
lucianocasalunga:fix/payment-poll-timeout
Open

lucianocasalunga wants to merge 1 commit into
lnbits:mainfrom
lucianocasalunga:fix/payment-poll-timeout

Conversation

@lucianocasalunga

Copy link
Copy Markdown

Problem

The _process_invoice function polls LNbits for payment confirmation in a while loop with exponential backoff (max interval: 60 s). If the underlying Lightning node never transitions a payment from pending to success or failed — e.g. due to LND mission-control cache poisoning, a hung HTLC, or an internal node state issue — the loop runs indefinitely.

Because NWC requests are processed serially inside the ws.recv() loop, one stuck payment blocks all subsequent pay_invoice requests until the process is manually restarted.

Observed scenario

LND's mission-control cache poisoned a route to a destination node. check_transaction_status kept returning pending (the payment was never forwarded — 0 HTLCs attempted). The NWC worker blocked for the entire duration of the process lifetime.

Fix

Adds a PAYMENT_STATUS_POLL_TIMEOUT_SECONDS = 90.0 constant and a deadline check inside the polling loop. After 90 seconds of waiting for a pending payment, the function returns an INTERNAL error instead of blocking forever. The timeout constant is easily adjustable.

# before: loop ran until success or failed (no bound on pending state)
while wait_for_preimage:
    ...
    if payment_status.failed:
        return PAYMENT_FAILED
    await asyncio.sleep(poll_interval)

# after: adds deadline guard
deadline = asyncio.get_event_loop().time() + PAYMENT_STATUS_POLL_TIMEOUT_SECONDS
while wait_for_preimage:
    ...
    if payment_status.failed:
        return PAYMENT_FAILED
    if asyncio.get_event_loop().time() >= deadline:
        return INTERNAL ("Payment timed out waiting for confirmation.")
    await asyncio.sleep(poll_interval)

Notes

  • The PAYMENT_FAILED branch (added in a prior fix) already handles the case where LND explicitly marks a payment as failed. This PR handles the complementary case where LND never marks it at all.
  • 90 s was chosen as a conservative upper bound — typical Lightning payments settle in < 10 s. Adjust PAYMENT_STATUS_POLL_TIMEOUT_SECONDS as needed.
  • No behavioral change for payments that succeed or fail normally.

The `_process_invoice` function polls LNbits for payment confirmation in a
`while` loop with exponential backoff (max 60s per interval). If the
underlying node never transitions a payment from pending to success/failed —
e.g. due to mission-control poisoning, LND internal state corruption, or a
node restart — the loop runs indefinitely, blocking the entire NWC request
queue (which is processed serially).

Adds a `PAYMENT_STATUS_POLL_TIMEOUT_SECONDS = 90.0` constant and a deadline
check inside the loop. After 90 seconds the request returns an `INTERNAL`
error instead of hanging forever. The timeout is configurable via the
constant.

This was observed in production: a poisoned LND mission-control cache caused
`check_transaction_status` to always return `pending`, which blocked all
subsequent NWC pay_invoice requests until the container was manually
restarted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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