Skip to content

openingd: fail open_channel at receipt when both initial balances <= their reserve - #9480

Open
Amperstrand wants to merge 1 commit into
ElementsProject:masterfrom
Amperstrand:pr9475-open-channel-reserve-must
Open

openingd: fail open_channel at receipt when both initial balances <= their reserve#9480
Amperstrand wants to merge 1 commit into
ElementsProject:masterfrom
Amperstrand:pr9475-open-channel-reserve-must

Conversation

@Amperstrand

@Amperstrand Amperstrand commented Sep 5, 2026

Copy link
Copy Markdown

BOLT #2 requires the receiving node to fail the channel if both
to_local and to_remote of the initial commitment transaction are
<= the opener's channel_reserve_satoshis — a receiving-node MUST
under open_channel receipt handling, not a general
channel-establishment rule. CLN implements the comparison, but in
initial_commit_tx() (common/initial_commit_tx.c, whose FIXME says
it should be in #2), so it only fires at funding_created receipt —
after accept_channel has already gone out (#9475).

This projects the initial balances at open_channel receipt (funder
to_local = funding − push − base fee − 2×330 anchor outputs; accepter
to_remote = push) and fails the negotiation before accept_channel
is sent, using the same fee math as initial_commit_tx()
(commit_tx_base_fee + the 660-sat anchor correction).

Notes for reviewers:

  • The misplaced initial_commit_tx() check stays: it remains the
    authoritative backstop at funding_created, and covers the funder
    side's own construction.
  • An in-suite test would need a raw-wire opener: a stock fundchannel
    reserve is pre-checked with the reserve doubled ("Not opening
    because if they used the same setting as us channel capacity with
    funding 100000sat, reserves 44000sat/44000sat, … channel capacity is
    9935sat, which is below 10000sat" on the reporter's 100k funding;
    43000sat passes), which blocks every shape that trips this check. I
    validated with a ~300-line BOLT8 wire peer (noise-XK initiator,
    spec-vector-pinned) driving the reporter's exact parameters instead:
    pre-fix accept_channel, post-fix rejection citing the projected
    balances (78778000msat / 20000000msat); boundary shifts
    86,100→77,092 at feerate 2000 and the static_remotekey (724-weight)
    path lands at 78,551/78,552, both exactly on the projection. The complete wire-peer source, per-cell results, and boundary math are in this gist: https://gist.github.com/Amperstrand/c2078688f93800cf7723cad2670c8044
  • Reserves between the two initial balances (only the accepter's
    balance below reserve) still lock in spendable-zero channels — that
    is spec-legal (only the MAY "considers channel_reserve_satoshis
    too large" would catch it); a follow-up could discuss a cap like
    lnd's 20%-of-capacity bound (lnwallet/reservation.go: channelCapacity / 5) or eclair's max-reserve-to-funding-ratio = 0.05.
reporter's exact params, pre- vs post-fix wire (v26.06.7) — expand
pre-fix  (100k funding, 20k push, 87k reserve, anchors, feerate 500):
         accept_channel sent — both initial balances (78,778 / 20,000) below the 87k reserve
post-fix: "kind": "wire_error", "data": "You gave bad parameters: Their channel reserve 87000sat
         is not exceeded by either initial balance (78778000msat, 20000000msat)"
boundary shift at feerate 2000 — anchors and static_remotekey paths both land on the BOLT3 projection — expand
c5-r77091  accepted=YES          c5-r77092  rejected (new check cites 77092000msat)
c5-r86091  rejected (new check)  c5-r86100  rejected (aggregate capacity check, unchanged)
c5na-r78551 accepted=YES (724-weight path)  c5na-r78552 rejected (cites 78552000msat)

Fixes #9475

…their reserve

BOLT ElementsProject#2 requires the receiving node to fail the channel if both
to_local and to_remote of the initial commitment transaction are
<= the opener's channel_reserve_satoshis (a receiving-node MUST
under open_channel receipt handling). CLN implements the comparison,
but in initial_commit_tx() (common/initial_commit_tx.c, whose FIXME
says it should be in ElementsProject#2), so it only fires at funding_created
receipt -- after accept_channel has already gone out.

Project the initial balances at open_channel receipt (funder
to_local = funding - push - base fee - 2x330 anchor outputs; accepter
to_remote = push) and fail the negotiation before accept_channel is
sent, using the same fee math as initial_commit_tx()
(commit_tx_base_fee + the 660-sat anchor correction). The misplaced
check stays as the authoritative backstop at funding_created.

An in-suite test would need a raw-wire opener: a stock fundchannel
reserve is pre-checked with the reserve doubled ('Not opening because
if they used the same setting as us ... below 10000sat'), which
blocks every shape that trips this check. Validated with a BOLT8
wire peer driving the reporter's exact parameters (100k funding,
20k push, 87k reserve: pre-fix accept_channel, post-fix rejection
citing the projected balances 78778000msat / 20000000msat).

Changelog-Fixes: ElementsProject#9475
Fixes: ElementsProject#9475
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.

Failure to reject absurdly high channel reserves

1 participant