Skip to content

funding: require explicit channel type in all negotiations - #11064

Open
NishantBansal2003 wants to merge 2 commits into
lightningnetwork:masterfrom
NishantBansal2003:explicit-chan-type
Open

funding: require explicit channel type in all negotiations #11064
NishantBansal2003 wants to merge 2 commits into
lightningnetwork:masterfrom
NishantBansal2003:explicit-chan-type

Conversation

@NishantBansal2003

@NishantBansal2003 NishantBansal2003 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Currently, BOLT assumes ExplicitChannelType, and LND sends it as required (#9637). However, when receiving an OpenChannel from peer, LND doesn’t enforce an explicit ChannelType and falls back to implicit negotiation, even though both peers have negotiated ExplicitChannelType. According to BOLT 2, I think we should fail the funding flow if ChannelType is omitted from the received OpenChannel message and echo the same channel type (if valid) in AcceptChannel.

So, we should remove implicit negotiation entirely. If the RPC caller doesn’t request a channel type, a default should be derived from both peers' features and signaled explicitly.

This bug was discovered while fuzzing the funding flow using the smite

@github-actions github-actions Bot added the severity-critical Requires expert review - security/consensus critical label Aug 12, 2026
@github-actions

Copy link
Copy Markdown

🔴 PR Severity: CRITICAL

gh pr view | 2 files | 126 lines changed

🔴 Critical (1 file)
  • funding/manager.go - modifies channel funding workflow coordination (enforces explicit channel type negotiation per BOLT 2)
🟢 Low (1 file)
  • funding/manager_test.go - test-only change, excluded from severity calculation

Analysis

This PR changes funding/manager.go, which coordinates the channel funding workflow — a category that always requires expert review due to its direct impact on channel open/funding correctness and security. The change enforces that ChannelType must be explicitly set when both peers have negotiated ExplicitChannelType, closing a gap where LND would otherwise silently fall back to implicit channel type negotiation. The change is small (15 non-test lines) and scoped to a single file, so no severity bump was applied beyond the inherent CRITICAL classification of the funding/* package.


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

@ziggie1984

Copy link
Copy Markdown
Collaborator

We should remove implicit channel-type negotiation instead of gating this validation on option_channel_type. The feature is now ASSUMED, and BOLT 2 unconditionally requires channel_type in both open_channel and accept_channel. Implementations may therefore stop advertising bits 44/45, causing this condition to evaluate to false and incorrectly re-enable the obsolete implicit fallback.

Please require msg.ChannelType unconditionally here and remove the implicit wire-negotiation paths. LND may retain a local default-selection helper when the RPC caller does not request a specific type, but the selected type must always be sent explicitly and echoed by the peer.

@ziggie1984

Copy link
Copy Markdown
Collaborator

When removing the implicit negotiation paths, we should keep the backward-compatibility implications in mind. In particular, removing implicit negotiation should be separate from removing the option_channel_type advertisement.

Continuing to advertise bit 44 allows older LND versions that understand explicit channel types to enter their explicit path and send/echo channel_type. If we stop advertising the bit at the same time, those versions may fall back to implicit behavior: an older funder may omit channel_type, while an older fundee may not echo the type sent by the new node. Both flows would then fail against the new strict validation.

I suggest always requiring, sending, and echoing channel_type, while temporarily retaining ExplicitChannelTypeRequired in our feature set for compatibility. The remaining implicit helper can be converted into a local default-type selector rather than used as wire-level negotiation. Existing channels are unaffected because their commitment type is persisted and not renegotiated.

@NishantBansal2003 NishantBansal2003 changed the title funding: enforce explicit channel type when negotiated funding: require explicit channel type in all negotiations Aug 13, 2026
@NishantBansal2003

Copy link
Copy Markdown
Contributor Author

We should remove implicit channel-type negotiation instead of gating this validation on option_channel_type. The feature is now ASSUMED, and BOLT 2 unconditionally requires channel_type in both open_channel and accept_channel. Implementations may therefore stop advertising bits 44/45, causing this condition to evaluate to false and incorrectly re-enable the obsolete implicit fallback.

Wanted to do that in the first place, since all the other implementations currently do this, but I was unsure why it wasn’t done in: #9637. Anyway, in the latest commit, I did the following:

  • Removed all implicit channel type negotiation paths and made the channel type required in both open_channel and accept_channel
  • If the RPC caller does not specify a channel type, we fall back to the default channel type, which is how the implicit negotiation worked previously.
  • The channel type in our reservation should always be present, so I removed all other implicit or nil channel type paths.

NishantBansal2003 added a commit to NishantBansal2003/smite that referenced this pull request Aug 13, 2026
LND allows `open_channel` with an omitted `channel_type`,
violating BOLT 2 even though it signals the required
`option_channel_type` feature bit. This will be tracked
upstream and will be suppressed until fixed.

see: lightningnetwork/lnd#11064

Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
NishantBansal2003 added a commit to NishantBansal2003/smite that referenced this pull request Aug 13, 2026
LND allows `open_channel` with an omitted `channel_type`,
violating BOLT 2 even though it signals the required
`option_channel_type` feature bit. This will be tracked
upstream and will be suppressed until fixed.

see: lightningnetwork/lnd#11064

Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>

@MPins MPins left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agreed with @ziggie1984 .

The flip side is that the advertisement is now a prerequisite of this change rather than an independent feature flag: dropping that line silently breaks the funding flow against older peers, so a short note there would be cheap insurance:

	// NOTE: Funding requires an explicit channel type, and older peers only
	// enter their explicit path when we advertise this. Removing this bit
	// breaks the funding flow against them until those versions age out.
	lnwire.ExplicitChannelTypeRequired: {
		SetInit:    {}, // I
		SetNodeAnn: {}, // N
	},

Comment thread funding/manager.go Outdated
Comment thread funding/manager.go Outdated
Comment thread funding/manager_test.go
@MPins

MPins commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

I've been building a coverage-guided fuzz harness for the funding manager, and added oracles for BOLT 2's channel_type requirements — checking what lnd emits against the spec text, rather than against what the lnd on the other end expects. Against master it reaches three divergences, all fixed by this PR:

  1. Peer omits channel_type while option_channel_type is negotiated. BOLT 2 says the receiver must fail the channel. Master instead falls back to its default selection and puts that invented type in accept_channel — which a conforming funder must reject, the echo having to match what it sent.

  2. Peer advertises neither bit 44 nor 45 but sends channel_type, as current BOLT 2 requires of it, the field no longer being feature-gated. Master accepts the channel and omits the echo entirely.

  3. Same peer, lnd funding. Master's own open_channel goes out with no channel_type.

For now these are logged rather than fatal, so the fuzzer can keep running against master — it otherwise stops within seconds and reaches nothing else. The must-reject check for case 1 is commented out as well; the case still surfaces,
but through the echo oracle, as the invented type in accept_channel. Each spot carries a TODO to restore the hard assertion once this lands. The remaining channel_type oracles stay armed — master already enforces the funder-side
checks on accept_channel, and those pass.

All three pass with this branch applied.

@MPins MPins left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 🎉

Verified the test fix does what it was meant to: with the check removed, the package now fails as expected, instead of panicking in copyPubKey and taking the binary down.

Two non-code leftovers before this lands: missing the release notes and the PR description, which still describe the conditional design "I think we should fail the funding flow if ChannelType is omitted when both peers have negotiated ExplicitChannelType".

Comment thread funding/commitment_type_negotiation.go Outdated
// channel. If a desiredChanType is provided, explicit negotiation for said type
// will be attempted if the set of both local and remote features support it.
// Otherwise, implicit negotiation will be attempted.
// Otherwise, a default type is selected based on feature compatibility,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we update the RPC-facing docs as part of this change? funding/manager.go:291-293 still says ChannelType is observed only when both sides support explicit negotiation, and both commitment_type fields in lnrpc/lightning.proto (around lines 2239 and 2412) say the field is only used if the remote advertises it. With option_channel_type now assumed, those statements are false: a requested type is validated regardless of bits 44/45, while nil/UNKNOWN selects a default that is sent explicitly. Suggested wording: "The commitment type to request. If UNKNOWN, lnd selects a default from both peers' supported features; the selected type is always sent explicitly." Please regenerate lightning.pb.go and lightning.swagger.json with make rpc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in c961526

I'll squash it and update the commit message to multi: require explicit channel type in all negotiations once the PR is finalized

Comment thread funding/commitment_type_negotiation.go Outdated
@@ -18,61 +18,30 @@ var (
// negotiateCommitmentType negotiates the commitment type of a newly opened

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Small pre-existing doc typo while we're touching this helper: the comment above names errUnsupportedCommitmentType, but the variable is errUnsupportedChannelType.

Comment thread funding/manager.go
"public channel")
log.Error(err)
msg.Err <- err
// Since we always negotiate an explicit channel type now, chanType is

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The nearby comment above negotiateCommitmentType is now stale/misleading: This is dependent on *both* us and the remote peer are signaling the proper feature bit. A caller-supplied type comes from msg.ChannelType and is then validated against its underlying features; only a nil request uses feature-based default selection, and bit 44/45 is not a gate. Could we rewrite it to: Before initializing the reservation, validate the caller-requested channel type or select a default from the peers' mutually supported commitment features.

Comment thread funding/manager_test.go Outdated

// Set up feature bits for channel type negotiation.
featureBits := []lnwire.FeatureBit{
lnwire.ExplicitChannelTypeOptional,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This test still sets ExplicitChannelTypeOptional, so it doesn't exercise the main compatibility case added by this PR: a peer omits bit 44/45 but sends a valid channel_type. Could we remove this bit here and keep only the underlying static-remotekey/anchors features? That would also let us update the stale comment in testNormalWorkflow around lines 1601-1602, which currently says both parties must support explicit channel-type negotiation.

Comment thread funding/manager.go
// using explicit negotiation.
// format we can use with this peer. This is dependent on the channel
// type sent by the funder and the feature bits both peers are signaling
chanType, commitType, err := negotiateCommitmentType(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: this comment is missing its terminating period — // type sent by the funder and the feature bits both peers are signaling. Its rewritten counterpart in handleInitFundingMsg (the Before initializing the reservation, ... block) ends properly, so this is the odd one out. Easy to fold into the squash.

// Otherwise, implicit negotiation will be attempted.
// Otherwise, a default type is selected based on feature compatibility,
// particularly when the RPC caller does not request a specific channel type.
//

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This doc comment is the one place that still carries the vocabulary the PR removes, and it's the entry point people will read first. Two things:

  • "explicit negotiation for said type will be attempted if the set of both local and remote features support it" — nothing is negotiated here anymore. A caller-supplied type is validated against the commitment features both peers support; bits 44/45 are not consulted at all.
  • "particularly when the RPC caller does not request a specific channel type" — that isn't a particular case, it's the only path that reaches default selection, so the qualifier implies there are others.

Suggested rewrite:

// negotiateCommitmentType determines the commitment type of a newly opened
// channel. If desiredChanType is provided, it is validated against the
// commitment features supported by both peers. Otherwise, a default type is
// selected from those features.
//
// The returned ChannelType is always non-nil and is always signaled on the
// wire. An error is only returned if desiredChanType is not supported.

Comment thread funding/commitment_type_negotiation.go Outdated
// channels.
// selectDefaultChannelType selects a default channel type by choosing the
// latest non-taproot type supported by the local and remote features.
// Taproot channels must be requested explicitly, keeping default selections

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The rename reads well — dropping "negotiate" is right, since this never touches the peer, and "default" lines up with the RPC wording ("If UNKNOWN, lnd selects a default..."). Two leftovers in the doc, both inherited from implicitNegotiateCommitmentType:

  • "the latest non-taproot type" — "latest" reads temporally; what's meant is a preference ordering. "the most preferred non-taproot type" is clearer.
  • "keeping default selections on channel types that can be used for both public and private channels" is a slightly garbled edit of "keeping implicit opens on ...". Maybe: "so that defaults stay on channel types usable for both public and private channels."

expectsChanType: nil,
expectsErr: nil,
expectsChanType: (*lnwire.ChannelType)(
lnwire.NewRawFeatureVector(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Follow-on from removing the bit in TestFundingManagerAcceptChanType: the ExplicitChannelTypeOptional entries in this table's localFeatures/remoteFeatures are now inert, and leaving them in suggests the bit still steers the outcome.

After this change nothing in the funding path reads bit 44/45. The single read in the tree was the hasFeatures(local, remote, lnwire.ExplicitChannelTypeOptional) call this PR deletes; grepping ExplicitChannelType outside lnwire/features.go now finds only feature/default_sets.go (a write into our advertised set) and feature/deps.go (dependency declarations). These vectors are the arguments to negotiateCommitmentType, which no longer looks at the bit.

I removed all 35 occurrences from this file's vectors locally and all 20 subtests still pass, so this is a pure signal-clarity change. It matters most on this first case, whose name — "explicit missing remote negotiation feature" — describes a condition the code no longer branches on; something like "explicit type without remote negotiation bit" would also convey that it now succeeds and signals the type.

To be explicit about scope: this is only about the test vectors. The advertisement in feature/default_sets.go must stay, since older lnd still gates its own explicit path on seeing the bit from us.

@ziggie1984

Copy link
Copy Markdown
Collaborator

Looking good can you squash the commits

@ziggie1984 ziggie1984 added the severity-override-medium Manual override to medium label Aug 25, 2026

@ziggie1984 ziggie1984 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM (pending fixup commit history), nice contribution thank you !!!

Move from optional implicit negotiation to mandatory explicit
channel type in OpenChannel and AcceptChannel. The returned
ChannelType is now always non-nil.

Channel type is required in all negotiations now. We only
fallback to a default channel type when the RPC caller does
not explicitly specify one.

Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
@github-actions github-actions Bot added severity-medium Focused review required and removed severity-critical Requires expert review - security/consensus critical labels Aug 26, 2026
@github-actions

Copy link
Copy Markdown

⚠️ Severity changed: CRITICALMEDIUM (manual override applied via severity-override-medium label)

🟡 PR Severity: MEDIUM

gh pr view | 6 files (excl. tests/generated) | 320 lines changed

🔴 Critical (2 files)
  • funding/manager.go - channel funding workflow coordination; enforces explicit channel_type on open_channel/accept_channel and removes implicit negotiation fallback paths
  • funding/commitment_type_negotiation.go - core commitment/channel-type negotiation logic per BOLT 2
🟠 High (3 files)
  • feature/default_sets.go - feature bit set changes related to ExplicitChannelType
  • lnrpc/lightning.proto - RPC/API definition change
  • lnrpc/lightning.swagger.json - generated swagger spec reflecting the proto change
🟢 Low / excluded (4 files)
  • docs/release-notes/release-notes-0.22.0.md - release notes
  • funding/commitment_type_negotiation_test.go - test-only change
  • funding/manager_test.go - test-only change
  • lnrpc/lightning.pb.go - auto-generated from proto

Analysis

By file content alone, this PR would classify as CRITICAL: it modifies funding/manager.go and funding/commitment_type_negotiation.go, which govern channel funding and channel-type negotiation (BOLT 2 open_channel/accept_channel handling). A maintainer has applied a severity-override-medium label, which takes precedence over the automatic classification per the bot's override rule, so this PR is recorded as MEDIUM. Reviewers should be aware the underlying change touches critical funding-workflow code regardless of the override.


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

@GeorgeTsagk GeorgeTsagk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, two non-blocking nits inline.

Verified against current BOLT 2: rejecting open_channel without channel_type, requiring the echo in accept_channel, and always signaling the type are all mandatory since option_channel_type became ASSUMED. Keeping the ExplicitChannelTypeRequired advertisement is the right compat bridge for older LND peers that only enter their explicit path when they see the bit.

funding, feature, chanacceptor, and lnwire unit tests pass locally on this branch. The three CI failures are unrelated flakes (neutrino wallet test under race, etcd client test, htlc_timeout_resolver_extract_preimage_local itest), all in subsystems this PR does not touch.

Comment thread funding/manager.go
Comment on lines +1585 to +1592
// Enforce BOLT-02: The funder MUST set the channel_type in
// open_channel. Reject if it's omitted.
if msg.ChannelType == nil {
err := errors.New("channel type required but not provided")
f.failFundingFlow(peer, cid, err)

return
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The peer never learns why we rejected here: failFundingFlow downgrades non-whitelisted errors to "funding failed due to internal error" (manager.go:982). Since this check exists to flag non-conforming peers, an actionable reason helps interop debugging. Consider adding an lnwire.FundingError enum value (e.g. ErrChanTypeRequired = 3, "channel type required") and returning it here.

Comment thread funding/manager.go
return
}

// Enforce BOLT-02: The funder MUST set the channel_type in

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: this runs after OpenChannelPredicate.Accept (manager.go:1579), so spec-violating opens still reach external acceptors before being rejected. Protocol validation should precede user-defined policy; suggest moving this check above the acceptor call, next to the chain-hash and sync-state checks.

@ziggie1984

Copy link
Copy Markdown
Collaborator

@NishantBansal2003 pls address the Nits from George.

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

Labels

channels severity-medium Focused review required severity-override-medium Manual override to medium

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

4 participants