Skip to content

Skip abandoned and unavailable channels when paying and when creating invoices - #759

Merged
cdecker merged 3 commits into
mainfrom
2026m10-skip-broken-jit
Oct 26, 2026
Merged

cdecker merged 3 commits into
mainfrom
2026m10-skip-broken-jit

Conversation

@cdecker

@cdecker cdecker commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

One rule decides everywhere whether a channel may be sent over, received over, advertised as a routehint, or awaited:

available(channel) :=
    state == CHANNELD_NORMAL
    && (max_to_us_msat > 0 || short_channel_id.is_some())

max_to_us_msat is the monotonically increasing high-water mark of the balance we ever held. For zeroconf JIT channels — which may not have a short_channel_id yet — a non-zero gauge signals that the incoming JIT payment that caused the channel to be opened was claimed, so the LSP server has no reason to have dropped the channel and awaiting it can succeed. A channel that never held funds and was never confirmed is a dangling channel open, typically an abandoned JIT channel: it will never come back, yet we currently await it until the wait deadline expires and fail otherwise payable payments (gl#870, gl#871). The current balance (spendable_msat) stays secondary: drained-but-confirmed channels remain available.

 trampolinepay channel collection
+  skip if !is_channel_available        # abandoned channels are never awaited
   skip if spendable_msat <= 0/min_htlc # allocation filters, unchanged

 get_receivable_capacity (invoice JIT negotiation check)
-  filter max_to_us_msat > 0
+  filter is_channel_available          # now also keeps confirmed receive-only channels

 get_routehints (invoices)
-  filter peer_connected && CHANNELD_NORMAL
+  filter peer_connected && is_channel_available

 AwaitableChannel billboard poll
+  return Err if state != CHANNELD_NORMAL  # fail fast, don't spin to the deadline
   poll "Channel ready"/"Reconnected, and reestablished" every 250ms

The rule lives once in channels::is_channel_available, with primitive arguments so the internal plugin can reuse it after a submodule bump. Pre-v23.05gl1 version shims and the get_version machinery in awaitables are dropped — nodes older than v25.12gl1 are not in use anymore.

Evidence

  • Before: gl#870, session 7490976188727296: a node with a healthy 2,575 sat channel plus an abandoned JIT channel (zero gauge, zeroconf, never confirmed) failed a 2,327 sat invoice — payable on the healthy channel alone — because the wait also waited for the abandoned channel, until the deadline expired and the payment failed as a timeout.
    After: the abandoned channel is skipped at selection, and a channel that leaves CHANNELD_NORMAL mid-wait fails its await immediately.

  • Unit coverage for the rule's truth table:

$ cargo test -p gl-plugin --lib
test channels::tests::test_availability_truth_table ... ok
test result: ok. 35 passed; 0 failed

Merge Danger

Door: two-way — pure plugin-side channel selection, no persisted state, protocol, or wire format changes.

Blast Radius: invoice creation (JIT negotiation decision, routehints), trampoline pay channel selection, and channel reestablishment waits. Three visible behavior shifts:

  1. Confirmed receive-only channels (have an scid, never held funds) now count toward receivable capacity — invoices on such nodes skip JIT negotiation instead of opening a superfluous new channel.
  2. Channels outside CHANNELD_NORMAL are skipped even when they show spendable capacity (e.g. currently closing ones) — payments no longer block on channels that can no longer carry them.
  3. Awaiting a channel whose peer is simply offline is unchanged: once confirmed or funded, channels are still awaited as before.

cdecker added 3 commits October 24, 2026 13:10
Introduce `channels::is_channel_available`, a single rule that decides
whether a channel can be used to send or receive, advertised as a
routehint, or awaited for (re-)establishment:

    available := state == CHANNELD_NORMAL &&
                 (max_to_us_msat > 0 || short_channel_id.is_some())

`max_to_us_msat` is the monotonically increasing high-water mark of the
balance we have ever held. For zeroconf JIT channels, which may not have
a `short_channel_id` yet, a non-zero gauge signals that the incoming
JIT payment that opened the channel was claimed, so the LSP server has
no reason to have dropped the channel and awaiting it can succeed. A
channel that never held funds and was never confirmed is a dangling
channel open (typically an abandoned JIT channel): it will never come
back, yet we currently await it until the wait deadline expires and
fail otherwise perfectly payable invoices (gl#870, gl#871).

The current balance (`spendable_msat`) is deliberately secondary: a
drained but confirmed channel stays available.
Apply `channels::is_channel_available` to all channel selection sites:

 - `get_receivable_capacity`: the invoice-side JIT check now counts
   zeroconf channels whose JIT payment was claimed (gauge > 0) and
   confirmed incoming-only channels (scid), but never dangling,
   never-funded channel opens the LSP may have dropped.

 - `get_routehints`: unavailable channels never become routehints, so
   incoming payments are not routed into channels that may have been
   abandoned by their LSP server.

 - trampoline pay: drop unavailable channels before spawning awaitable
   tasks; awaiting an abandoned channel used to stall the payment for
   the full `AWAIT_CHANNELS_TIMEOUT_SEC` since `reestablished_channels`
   joins on every channel. The pre-existing `spendable_msat` filters
   remain for allocation purposes.

This replaces the ad-hoc per-site filters (which used the current
balance or `max_to_us_msat` alone, inconsistently) with the single
availability rule (gl#871).
The billboard step of `AwaitableChannel` looped until the caller's wait
deadline whenever a channel would never report "Channel ready" /
"Reconnected, and reestablished", e.g. because it was closed or
abandoned while the wait was in flight. Check the channel state on each
billboard poll and return an error as soon as the channel is not (or no
longer) in `CHANNELD_NORMAL`, so such channels are dropped from the
wait immediately instead of stalling it until the deadline (gl#871).

While at it remove the `v23.05gl1` version shims and the now unused
`get_version` bookkeeping: nodes older than v25.12gl1 are not in use
anymore.
@cdecker
cdecker requested a review from nepet October 26, 2026 12:11
@cdecker
cdecker merged commit 1d721b7 into main Oct 26, 2026
16 checks passed
@cdecker
cdecker deleted the 2026m10-skip-broken-jit branch October 26, 2026 13:38
@ksedgwic

Copy link
Copy Markdown

is_channel_available also excludes channels in CHANNELD_AWAITING_SPLICE, which CLN treats as fully usable.

A channel sits in that state from the splice commitment until splice_locked, and lightningd adds HTLCs in it: channel_state_can_add_htlc (lightningd/channel.h) returns true for both CHANNELD_NORMAL and CHANNELD_AWAITING_SPLICE, and the native routehint path enables a local channel in either state (common/gossmods_listpeerchannels.c). Every call site here passes ch.state == ChannelState::CHANNELD_NORMAL, so a channel mid-splice is skipped for the whole confirmation window.

  • trampolinepay selection: new with this PR. There was no state check before, so a splicing channel could send. Now it is skipped, and if it is the only channel with balance the payment fails.
  • AwaitableChannel: a channel that enters the splice state while awaited now fails with "will not become ready".
  • Routehints and receivable capacity: already required CHANNELD_NORMAL before this PR, so unchanged. An invoice created during a splice gets no hint for that channel.

Suggestion: pass matches!(ch.state, ChannelState::CHANNELD_NORMAL | ChannelState::CHANNELD_AWAITING_SPLICE) at the four call sites and rename the parameter from state_is_normal to state_can_add_htlc. The abandoned-JIT case is unaffected, since a zero-conf channel cannot splice before it confirms.

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