Skip to content

[v0.20.x-branch] Backport #11212: multi: stop opening legacy channels - #11253

Closed
github-actions[bot] wants to merge 7 commits into
v0.20.x-branchfrom
backport-11212-to-v0.20.x-branch
Closed

github-actions[bot] wants to merge 7 commits into
v0.20.x-branchfrom
backport-11212-to-v0.20.x-branch

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Backport of #11212


Change Description

In this PR, we stop opening and accepting new channels that use the legacy
commitment type. Channels that already use it are untouched: they keep working,
and can be operated, force closed and cooperatively closed exactly as before.
Only opening new ones is refused. The code that drives the type stays in place
and gets removed separately in 0.22, which keeps this small enough to backport
to 0.21 and 0.20.

Fixes #11195.

The legacy type was removed from the
spec

in 2024, and lnd is the last implementation still creating them. Its tweaked
to_remote output is why funds in such a channel cannot be recovered
unilaterally after data loss, which is the actual harm reported in the issue.

Where this sits

Channel commitments have gone through four generations, each superseding the
one before it:

LEGACY  ->  static remote key  ->  anchors  ->  taproot

LEGACY tweaks the to_remote output with a per-commitment point. Static
remote key (option_static_remotekey) leaves that output untweaked, which is
what makes it recoverable from the seed alone. Anchors
(option_anchors_zero_fee_htlc_tx) adds the anchor outputs that allow fee
bumping after a force close. Taproot moves the funding output to musig2.

This PR removes the first generation from the set we will open, and nothing
else. The second is dealt with in #11216.

The hole

An empty channel_type TLV in open_channel asks for exactly the legacy type,
and the branch handling it performed no feature check whatsoever, since the
type predates feature bits entirely. Any peer could obtain a legacy channel
from us regardless of what either side signalled, and we would do the same to
them. That is what TestFundingManagerRejectLegacyChanType now covers,
adapted from the reproduction in #11195: a peer advertising both
option_static_remotekey and option_anchors_zero_fee_htlc_tx sends an empty
channel type, and we reply with an error instead of echoing it back in
accept_channel.

negotiateCommitmentType in funding/commitment_type_negotiation.go is the
only place a commitment type for a new channel gets chosen, for both the
initiator (handleInitFundingMsg) and the responder
(fundeeProcessOpenChannel), so the change lives there. Implicit negotiation
still falls back to the static remote key type and only fails once there is
nothing left below it.

We also found that the funding manager's own test nodes signalled no features
at all, which means they had been negotiating legacy channels among
themselves. They now default to static remote key.

Scope

Only the legacy type. The static remote key type is still spec-valid, it is
what replaced legacy, and its to_remote is untweaked so the recovery problem
above does not apply to it. Refusing it as well would mean refusing channels
from any peer that cannot do anchors, which is not something to do in a patch
release. That belongs with the code removal in 0.22.

The rejection is a lnwire.FundingError (ErrChanTypeDeprecated) rather than
a plain error, because failFundingFlow only forwards the text of whitelisted
error types and flattens everything else into "funding failed due to internal
error", which would tell the remote nothing. Its string is kept as terse as
the ones beside it: what the remote needs to know is that the type is not
acceptable, and naming our replacement on the wire would only add another
fingerprint.

OpenChannel rejects commitment_type LEGACY up front so the caller gets a
clear error before we touch the wallet or the peer. The enum value itself has
to stay, since CommitmentType is also the reporting type for ListChannels,
ClosedChannels, PendingChannels and the channel acceptor, and channels we
already have still report it.

The restriction is documented on the API itself: the LEGACY enum value notes
that it is only reported for channels that already exist, and the
commitment_type fields of OpenChannelRequest and BatchOpenChannel note
that it is turned down as an input. The regenerated code carries no change
beyond the comments.

The dev build only protocol.legacy.committweak option is deprecated and made
a no-op. It stopped the node from signalling option_static_remotekey, which
now leaves no commitment type left to negotiate at all.

See each commit message for a detailed description w.r.t the incremental
changes.

Steps to Test

Three code paths could produce a legacy channel, and each has a test:

Path Test
Responder: peer sends an empty channel_type TestFundingManagerRejectLegacyChanType
Initiator, implicit fall back TestCommitmentTypeNegotiation/default_legacy_rejected
Initiator, explicit RPC request itest legacy_chan_type_rejected, TestCommitmentTypeNegotiation/explicit_legacy_rejected
go test ./funding/...
make itest-only icase="legacy_chan_type_rejected" backend=btcd

Two tests used to open legacy channels and now use static remote key instead.
The watchtower one opted in deliberately; the remote signer one did not, and
was itself an instance of the bug above. Both produce the same commitment
shape and the same fees, so no assertion moved. Re-ran those plus everything
else the funding path touches:

make itest-only icase='(legacy_chan_type_rejected|revoked_close_retribution_altruist|psbt_outbound|basic_flow|batch_channel_funding|channel_fundmax_error)' backend=btcd

All nine pass. make lint is clean, unit tests pass, and each commit builds on
its own under both the default and dev build tags.

Pull Request Checklist

Testing

  • Your PR passes all CI checks.
  • Tests covering the positive and negative (error paths) are included.
  • Bug fixes contain tests triggering the bug to prevent regressions.

Code Style and Documentation


Backport notes

All seven commits were cherry-picked from #11212. The 0.21.4 release notes
commit was left out. Six of them are identical to their parents; the
funding: never open a legacy channel commit is adapted because 0.20.x still
has implicit commitment type negotiation (#11064 is deliberately not
backported here, the explicit channel type feature bit is still optional on
this branch).

  • lnwire/error.go: ErrChanTypeRequired from funding: require explicit channel type in all negotiations  #11064 is not added.
    ErrChanTypeDeprecated keeps value 4, with a comment explaining the gap.
    Only the error text is ever sent to the peer, so the number is a local
    detail.
  • funding/commitment_type_negotiation.go: the implicit/explicit switch is
    kept. implicitNegotiateCommitmentType now returns an error instead of
    falling back to the legacy type, and every caller propagates it. The
    empty channel type rejection and ErrDeprecatedChanType are identical to
    the parent.
  • funding/manager.go: two call sites the parent never touched. The fundee
    path maps ErrDeprecatedChanType to the wire ErrChanTypeDeprecated, so
    a peer lacking static remote key learns why it was refused instead of
    getting the generic internal error. The accept_channel check on the funder
    side handles the new error return.
  • funding/manager_test.go: TestFundingManagerRejectLegacyChanType also
    signals ExplicitChannelTypeOptional, since on this branch an empty
    channel type is only honored as-is on the explicit path.
  • funding/commitment_type_negotiation_test.go: the "implicit legacy" case
    becomes "implicit legacy rejected". The taproot default cases the parent
    touched do not exist on 0.20.
  • itest/lnd_funding_test.go: the "simple taproot final" test entry seen in
    the parent's context does not exist on 0.20.
  • lnrpc: the added comments are identical, attached to 0.20's wording of
    the commitment_type field description. Generated code regenerated with
    make rpc, comments only.

Verified locally: every commit builds under the default and dev tags,
go test ./funding/ ./lnwire/ pass, lint reports nothing on the added lines,
and the funding itests are being re-run against btcd (an earlier run used
stale binaries and is not counted).

@github-actions

Copy link
Copy Markdown
Author

Please cherry-pick the changes locally and resolve any conflicts.

git fetch origin backport-11212-to-v0.20.x-branch
git worktree add --checkout .worktree/backport-11212-to-v0.20.x-branch backport-11212-to-v0.20.x-branch
cd .worktree/backport-11212-to-v0.20.x-branch
git reset --hard HEAD^
git cherry-pick -x 1e3b0747d9b640e817b02a2062b73313f8da15e6 98a0d345a2301629c73c473450864aeeee5d646e 6ea22314c3ad5cc246471e93eb1e6f4856451867 a03747961b0b97ca12bae39452383116cf4ad5ec 03d8b09dca48caf5a6361d05c40c46672853d35e 76c18acbd63bf2868be7663f15d4d30f9471ac70 a5556e0525c2a105778d7e515faf161f535dcd93 9b634bfd0929ecc0f0e0ddc0b6aa85a5234a06b6
git push --force-with-lease

@github-actions github-actions Bot mentioned this pull request Sep 24, 2026
7 of 9 tasks
In this commit, we add ErrChanTypeDeprecated so that a peer whose channel
type we reject learns the actual reason. failFundingFlow only forwards the
text of whitelisted error types and flattens everything else into a generic
"funding failed due to internal error", which tells the remote nothing.

The string is kept as terse as the ones around it. What the remote needs to
know is that the type isn't acceptable; which type to use instead is our
local policy, and spelling it out on the wire would only add another
fingerprint for no gain.

(cherry picked from commit 1e3b074)
In this commit, we turn the dev build only committweak option into a no-op.
It stopped the node from signalling option_static_remotekey, which left the
legacy commitment type as the only one it could negotiate. Since we no
longer open channels of that type, such a node would be unable to negotiate
any commitment type at all.

The field is kept so that configs which still set it continue to parse.

(cherry picked from commit 98a0d34)
In this commit, we stop opening and accepting new channels using the legacy
commitment type, which was removed from the spec in 2024. Its tweaked
to_remote output is why funds in such a channel cannot be recovered
unilaterally after data loss. Channels that already use it are untouched:
they keep working and can be operated, force closed and cooperatively closed
exactly as before.

An empty channel_type asks for precisely this type, and that branch performed
no feature check whatsoever, since the type predates feature bits entirely.
Any peer could obtain a legacy channel from us no matter what either side
signalled, which is what TestFundingManagerRejectLegacyChanType now covers.

Implicit negotiation keeps falling back to the static remote key type, and
only fails once there is nothing left below it.

The funding manager's own test nodes signalled no features at all, which
means they were negotiating legacy channels among themselves. They now
default to static remote key.

(cherry picked from commit 6ea2231)
In this commit, we turn down a request for the legacy commitment type before
we touch the wallet or the peer, so the caller gets a clear error instead of
one from deep inside the funding flow.

The LEGACY enum value itself has to stay: CommitmentType is also the
reporting type for ListChannels, ClosedChannels, PendingChannels and the
channel acceptor, and channels we already have still report it.

(cherry picked from commit a037479)
In this commit, we move the two tests that still opened legacy channels over
to static remote key. Both produce the same commitment shape and the same
fees, so no assertion moves.

The watchtower case opted into legacy deliberately. The remote signer one did
not: neither of its nodes runs with committweak, so both support static
remote key, and the channel only came out legacy because an empty
channel_type was accepted without any check. That is the bug this PR fixes,
sitting in our own suite.

The legacy node arguments and the unreferenced CfgLegacy go away with them,
since committweak no longer does anything.

(cherry picked from commit 03d8b09)
In this commit, we spell out that existing legacy channels are not affected
and can still be operated and closed, since that is the question an operator
reading these notes will have. Only opening new ones is refused.

(cherry picked from commit 76c18ac)
In this commit, we document the restriction on the API itself, so callers
can discover it without reading the release notes. The LEGACY enum value
notes that it is only ever reported for channels that already exist, and the
commitment_type fields of OpenChannelRequest and BatchOpenChannel note that
it is turned down as an input.

Worth spelling out on those fields that an empty channel type on the wire
asks for the legacy type as well, so there is no way to request it at all.

The generated code carries no change beyond the comments.

(cherry picked from commit 9b634bf)
@ziggie1984

Copy link
Copy Markdown
Collaborator

Not going to backport this PR into the 20.x line, because the surrounding code will have gaps regarding enum values which result form other PRs that should not be backported, so to keep this clean I am not disabling legacy chanenls on the 20.x line.

@ziggie1984 ziggie1984 closed this Sep 24, 2026
@github-actions github-actions Bot added the severity-critical Requires expert review - security/consensus critical label Sep 24, 2026
@github-actions

Copy link
Copy Markdown
Author

🔴 PR Severity: CRITICAL

automated classification | 8 files | 163 lines changed (excluding tests/auto-generated)

🔴 Critical (3 files)
  • funding/commitment_type_negotiation.go - funding/* channel funding workflow coordination
  • funding/manager.go - funding/* channel funding workflow coordination
  • lnwire/error.go - lnwire/* Lightning wire protocol messages
  • rpcserver.go - core server coordination
🟠 High (2 files)
  • lnrpc/lightning.proto - lnrpc/* RPC/API definition change
  • lnrpc/lightning.swagger.json - lnrpc/* generated API surface reflecting the proto change
🟡 Medium (1 file)
  • lncfg/protocol_legacy_on.go - lncfg/* config flag handling
🟢 Low (2 files, plus tests/generated excluded from counting)
  • docs/release-notes/release-notes-0.20.5.md - release notes
  • funding/commitment_type_negotiation_test.go, funding/manager_test.go, itest/lnd_funding_test.go, itest/lnd_remote_signer_test.go, itest/lnd_watchtower_test.go, lntest/node/config.go, lntest/utils.go - test-only changes
  • lnrpc/lightning.pb.go - auto-generated from the .proto change

Analysis

This PR disables legacy (pre-anchor) channel types and touches funding/commitment_type_negotiation.go and funding/manager.go directly, both under funding/* (channel funding workflow coordination — CRITICAL), plus lnwire/error.go (wire protocol error handling — CRITICAL) and rpcserver.go (core server coordination — CRITICAL). It also touches multiple distinct critical packages (funding, lnwire, rpcserver), which would independently justify a severity bump, but the classification is already at the top tier. The lnrpc/lightning.proto/.swagger.json changes add a new API field and are HIGH per the lnrpc/* rule. File/line counts (8 files, 163 lines excluding tests and generated code) are below the automatic-bump thresholds, but the nature of the changed packages alone warrants expert review of the commitment-type negotiation and funding-manager logic.


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

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

Labels

no-changelog severity-critical Requires expert review - security/consensus critical

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant