Skip to content

funding+lnwallet: reject absurdly high channel feerates - #11151

Open
Roasbeef wants to merge 3 commits into
lightningnetwork:masterfrom
Roasbeef:reject-absurd-feerates
Open

funding+lnwallet: reject absurdly high channel feerates#11151
Roasbeef wants to merge 3 commits into
lightningnetwork:masterfrom
Roasbeef:reject-absurd-feerates

Conversation

@Roasbeef

Copy link
Copy Markdown
Member

Fixes #11149.

In this PR, we add the two BOLT-02 checks that bound the commitment feerate a
channel initiator can impose on us. Today we implement neither, so a peer can
open a channel at a feerate_per_kw so high that the initial commitment leaves
both sides at or under their channel reserve, i.e. a channel that can never be
used. The reporter's example: 2 BTC of capacity at feerate_per_kw = 2_750_000
lands to_local at 9k sat and to_remote at 0, against a 20k sat reserve.

Worth being clear on the framing, since it came up in the issue: all channels
are single funder today, so the initiator proposing an absurd rate is mostly
burning its own capacity. It isn't purely self-harm though. The open still costs
us a pending channel slot on the way in, and if we let it through we end up with
a confirmed channel that can never route and has to be closed on chain. So this
is spec compliance plus a bit of cheap resource hygiene, not a fund-loss bug.

The reserve check

lnwallet now fails the reservation when both to_local and to_remote on the
initial commitment are less than or equal to the reserve each side has to
maintain. The check lives in handleSingleContribution rather than in
NewChannelReservation, as that's the first point where both reserves are
known: the one the initiator imposes on us rides in on open_channel, while the
one we impose on them is only settled once the channel acceptor (which can
override our default) has had its say.

The feerate cap

BOLT-02 leaves "unreasonably large" up to each implementation. We take the same
approach as CLN
and Eclair
and cap at 10x our own estimate, sampled at the same conf target (3) we'd use
for a channel we open ourselves.

A pure multiple of our estimate is brittle on its own: on a quiet chain, or when
the estimator falls back to the relay fee, 10x of a very small number is still a
very small number, and we'd start turning away channels opened at a rate that's
high relative to our estimate yet perfectly sane in absolute terms. So the bound
is floored at 100 sat/vbyte, which no realistic mempool needs a commitment to
beat. If the estimator errors out entirely we fall back to that floor rather
than failing the flow over a transient hiccup.

Note that this shouldn't disturb anchor channels either way: we already cap our
own anchor commitments at --max-commit-fee-rate-anchors (10 sat/vbyte by
default), well under the floor.

Follow-up

The new check reports its error over req.err to match the code around it.
That channel-of-errors pattern in lnwallet/wallet.go is due for a pass to
fn.Future, same as we did in the gossiper, but I've kept that out of here so
the baseline behavior lands on its own.

@Roasbeef
Roasbeef force-pushed the reject-absurd-feerates branch from bc34fc1 to df165b4 Compare August 31, 2026 23:17
@github-actions github-actions Bot added the severity-critical Requires expert review - security/consensus critical label Aug 31, 2026
@github-actions

Copy link
Copy Markdown

🔴 PR Severity: CRITICAL

file classification | 7 files | 410 lines changed

🔴 Critical (4 files)
  • funding/manager.go - funding/* — channel funding workflow coordination
  • lnwallet/errors.go - lnwallet/* — wallet operations / error definitions
  • lnwallet/reservation.go - lnwallet/* — channel funding reservation and commitment logic
  • lnwallet/wallet.go - lnwallet/* — core wallet operations
🟢 Low (3 files)
  • funding/manager_test.go - test-only change
  • lnwallet/reservation_test.go - test-only change (new file)
  • docs/release-notes/release-notes-0.22.0.md - release notes

Analysis

This PR modifies funding/manager.go, lnwallet/errors.go, lnwallet/reservation.go, and lnwallet/wallet.go — all part of the channel-funding and wallet-reservation code paths, which fall under the CRITICAL category (funding/*, lnwallet/*) since they govern channel funding coordination, reservation state, and commitment-transaction handling.

Excluding test files and docs, the change touches 4 non-test Go files across two distinct critical packages (funding and lnwallet) totaling ~123 lines — below the >20 files / >500 lines bump thresholds, but it does span multiple critical packages. Since CRITICAL is already the highest tier, no further bump applies. This PR should get expert review given it touches wallet reservation and funding-manager error handling.


To override, add a severity-override-{critical,high,medium,low} label.

In this commit, we add the BOLT-02 check that the receiver of open_channel
must fail the channel if both to_local and to_remote on the initial
commitment are less than or equal to the channel reserve. If neither side
starts out above its reserve, then neither side can ever add an HTLC
without dipping below it, so the channel is dead on arrival.

We run the check in handleSingleContribution rather than in
NewChannelReservation, as that's the first point where both reserves are
known: the one the initiator imposes on us arrives in open_channel, while
the one we impose on them is only settled once the channel acceptor (which
may override our default) has had its say.
In this commit, we add the other half of the BOLT-02 requirement here: the
receiver MUST fail the channel if it considers feerate_per_kw unreasonably
large. The spec leaves the threshold up to us, so we take the same approach
as CLN and Eclair and cap at 10x our own estimate for the same conf target
(3) we'd use for a channel we open ourselves.

A pure multiple of our estimate is a bit brittle on its own: on a quiet
chain, or when the estimator falls back to the relay fee, 10x of a very
small number is still a very small number, and we'd start turning away
channels opened at a rate that's perfectly sane in absolute terms. So we
floor the bound at 100 sat/vbyte, which no realistic mempool needs a
commitment to beat. If the estimator errors out entirely we fall back to
that floor rather than failing the flow.

Since all channels are single funder today, an initiator that proposes an
absurd rate mostly burns its own capacity. But it doesn't only do that: the
open still costs us a pending channel slot, and left unchecked it lands us
with a confirmed channel that can never be used and has to be closed on
chain. Rejecting up front is cheaper for both of us.
@Roasbeef
Roasbeef force-pushed the reject-absurd-feerates branch from df165b4 to 5730887 Compare August 31, 2026 23:40
@ziggie1984
ziggie1984 self-requested a review September 1, 2026 01:37

@Lrifton92 Lrifton92 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the two BOLT-02 checks end to end — logic looks correct and the wiring is in the right place. Nice touch flooring the feerate bound at minCommitFeeRateCap and falling back to it on an EstimateFeePerKW error in maxRemoteCommitFeeRate; that neatly avoids the 10x-a-relay-fee-fallback false-reject on a quiet chain, and the sat/kw conversion + uint32 feerate mean neither the cast nor the x10 can overflow.

One thing worth spelling out for future readers: validateInitialBalances overlaps the existing !initiator && theirBalance <= 2*defaultDust guard in NewChannelReservation (reservation.go:386), which fires earlier and returns ErrFunderBalanceDust. So ErrBalancesBelowReserve only surfaces in the (2*dust, reserve] band — which does cover the reporter's 9k-sat example, so it's additive, but for the exact feerate_per_kw=2_750_000 case the feerate cap trips first and you'll see ErrCommitFeeRateTooLarge instead. Might be worth a sentence in the PR body clarifying the layering of the three error paths.

Also: the reserve check is only invoked from handleSingleContribution, not the dual-funder handleContributionMsg path. Correct for single-funder-only inbound today, but might deserve a // TODO when v2 inbound lands. Non-blocking — LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

severity-critical Requires expert review - security/consensus critical

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failure to reject absurdly high channel feerates

2 participants