funding: require explicit taproot channel negotiation
What changed, and why it matters
This change stops LND from automatically opening Taproot Lightning channels when both peers merely advertise support for them. Instead, Taproot channels now require an explicit channel type request. The commit message explains this is because public Taproot channel announcements are not yet supported, so implicit negotiation could accidentally create channels that cannot be publicly announced. This is a behavior-hardening change rather than a fix for a known active exploit.
Treat as a protocol-behavior hardening commit. Review whether any production peers relied on implicit Taproot negotiation and ensure release notes document the change. No urgent security patch action is indicated by the diff alone.
Security signals we found
Behavior change in channel type negotiation
Removes automatic selection of newer channel type based on feature bits
Prevents unintended Taproot channel opens when public announcement unsupported
No memory safety, cryptographic, or input validation fix visible in diff
Evidence from the diff
The patch removes the implicit negotiation branches that selected SimpleTaprootChannelsOptionalFinal/Staging when both peers supported them. Now implicitNegotiateCommitmentType only considers anchors and legacy/static-remote-key paths, and Taproot must be requested via explicit channel type negotiation. Tests are updated to assert that anchor commitments are preferred over Taproot in implicit negotiation, and that Taproot-only feature sets fall back to legacy rather than selecting Taproot.
Changed components
funding/commitment_type_negotiation.gofunding/commitment_type_negotiation_test.goInspect captured patch +24 / −40
diff --git a/funding/commitment_type_negotiation.go b/funding/commitment_type_negotiation.go
index 2d8702c..080f4c8 100644
--- a/funding/commitment_type_negotiation.go
+++ b/funding/commitment_type_negotiation.go
@@ -455,33 +455,17 @@ func explicitNegotiateCommitmentType(channelType lnwire.ChannelType, local,
}
// implicitNegotiateCommitmentType negotiates the commitment type of a channel
-// implicitly by choosing the latest type supported by the local and remote
-// features.
+// implicitly by choosing the latest non-taproot type supported by the local and
+// remote features. Taproot channels must be requested explicitly, keeping
+// implicit opens on channel types that can be used for both public and private
+// channels.
+//
+// TODO(yy): Revisit implicit taproot negotiation once public taproot channel
+// announcements are supported.
func implicitNegotiateCommitmentType(local,
remote *lnwire.FeatureVector) (*lnwire.ChannelType,
lnwallet.CommitmentType) {
- // Taproot channels are checked before anchors intentionally: when both
- // peers support taproot, we prefer the newer channel type. Production
- // (final) feature bits take priority over staging bits.
- if hasFeatures(local, remote, lnwire.SimpleTaprootChannelsOptionalFinal) { //nolint:ll
- chanType := lnwire.ChannelType(*lnwire.NewRawFeatureVector(
- lnwire.SimpleTaprootChannelsRequiredFinal,
- ))
-
- return &chanType, lnwallet.CommitmentTypeSimpleTaprootFinal
- }
-
- // If both peers are signalling support for simple taproot channels with
- // staging feature bits, use those.
- if hasFeatures(local, remote, lnwire.SimpleTaprootChannelsOptionalStaging) { //nolint:ll
- chanType := lnwire.ChannelType(*lnwire.NewRawFeatureVector(
- lnwire.SimpleTaprootChannelsRequiredStaging,
- ))
-
- return &chanType, lnwallet.CommitmentTypeSimpleTaproot
- }
-
// If both peers are signalling support for anchor commitments with
// zero-fee HTLC transactions, we'll use this type.
if hasFeatures(local, remote, lnwire.AnchorsZeroFeeHtlcTxOptional) {
diff --git a/funding/commitment_type_negotiation_test.go b/funding/commitment_type_negotiation_test.go
index 5da4e05..48b1a62 100644
--- a/funding/commitment_type_negotiation_test.go
+++ b/funding/commitment_type_negotiation_test.go
@@ -432,33 +432,36 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
expectsErr: errUnsupportedChannelType,
},
- // Test cases for implicit negotiation preferring
- // final over staging.
+ // Test cases for implicit negotiation ignoring taproot feature
+ // bits. Taproot channels require an explicit channel type.
{
- name: "implicit final taproot preferred " +
- "over staging",
+ //nolint:ll
+ name: "implicit anchors preferred over taproot",
channelFeatures: nil,
localFeatures: lnwire.NewRawFeatureVector(
+ lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.SimpleTaprootChannelsOptionalFinal,
lnwire.SimpleTaprootChannelsOptionalStaging,
lnwire.ExplicitChannelTypeOptional,
),
remoteFeatures: lnwire.NewRawFeatureVector(
+ lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.SimpleTaprootChannelsOptionalFinal,
lnwire.SimpleTaprootChannelsOptionalStaging,
lnwire.ExplicitChannelTypeOptional,
),
- expectsCommitType: lnwallet.CommitmentTypeSimpleTaprootFinal, //nolint:ll
+ expectsCommitType: lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx, //nolint:ll
expectsChanType: (*lnwire.ChannelType)(
lnwire.NewRawFeatureVector(
- lnwire.SimpleTaprootChannelsRequiredFinal, //nolint:ll
+ lnwire.StaticRemoteKeyRequired,
+ lnwire.AnchorsZeroFeeHtlcTxRequired,
),
),
expectsErr: nil,
},
{
- name: "implicit staging taproot when final " +
- "not supported",
+ //nolint:ll
+ name: "implicit ignores staging taproot without anchors",
channelFeatures: nil,
localFeatures: lnwire.NewRawFeatureVector(
lnwire.SimpleTaprootChannelsOptionalFinal,
@@ -469,16 +472,15 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
lnwire.SimpleTaprootChannelsOptionalStaging,
lnwire.ExplicitChannelTypeOptional,
),
- expectsCommitType: lnwallet.CommitmentTypeSimpleTaproot,
+ expectsCommitType: lnwallet.CommitmentTypeLegacy,
expectsChanType: (*lnwire.ChannelType)(
- lnwire.NewRawFeatureVector(
- lnwire.SimpleTaprootChannelsRequiredStaging, //nolint:ll
- ),
+ lnwire.NewRawFeatureVector(),
),
expectsErr: nil,
},
{
- name: "implicit final taproot only",
+ //nolint:ll
+ name: "implicit ignores final taproot without anchors",
channelFeatures: nil,
localFeatures: lnwire.NewRawFeatureVector(
lnwire.SimpleTaprootChannelsOptionalFinal,
@@ -488,11 +490,9 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
lnwire.SimpleTaprootChannelsOptionalFinal,
lnwire.ExplicitChannelTypeOptional,
),
- expectsCommitType: lnwallet.CommitmentTypeSimpleTaprootFinal, //nolint:ll
+ expectsCommitType: lnwallet.CommitmentTypeLegacy,
expectsChanType: (*lnwire.ChannelType)(
- lnwire.NewRawFeatureVector(
- lnwire.SimpleTaprootChannelsRequiredFinal, //nolint:ll
- ),
+ lnwire.NewRawFeatureVector(),
),
expectsErr: nil,
},
Why this scored 35/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.