funding: add production taproot channel negotiation support
What changed, and why it matters
This commit adds support for negotiating a new, finalized version of Taproot payment channels in the Lightning Network Daemon (LND). It is a feature addition that lets two LND nodes agree to use the production-ready Taproot channel format when both sides advertise the new feature bits. The change also includes tests and fixes a small consistency check in existing tests. There is no direct evidence in the commit that this fixes an active security vulnerability.
Treat as a normal feature/correctness commit. Reviewers should verify that final and staging feature bits cannot be confused during negotiation and that the new TaprootFinal commitment type is handled consistently across the wallet and RPC layers before release.
Security signals we found
New feature-bit negotiation paths for production Taproot channels
Implicit negotiation now prefers final feature bits over staging bits
Added missing ScidAliasOptional feature check in staging taproot + scid + zero-conf branch
Test-only hardening: proto enum value consistency check for commitment types
Evidence from the diff
The patch extends funding/commitment_type_negotiation.go to recognize final (production) Taproot channel feature bits (SimpleTaprootChannelsRequiredFinal / OptionalFinal) alongside the existing staging bits. It adds explicit-negotiation branches for plain, SCID-alias, zero-conf, and combined SCID+zero-conf final Taproot channel types, and updates implicit negotiation to prefer final over staging when both peers support it. It also adds a missing ScidAliasOptional check in one staging branch and adds unit tests. manager_test.go gains a new enum entry and a value-consistency assertion. The change is defensive/protocol-correctness work rather than a clear vulnerability fix.
Changed components
funding/commitment_type_negotiation.gofunding/commitment_type_negotiation_test.gofunding/manager_test.goInspect captured patch +290 / −6
diff --git a/funding/commitment_type_negotiation.go b/funding/commitment_type_negotiation.go
index 817709c..2d8702c 100644
--- a/funding/commitment_type_negotiation.go
+++ b/funding/commitment_type_negotiation.go
@@ -240,7 +240,22 @@ func explicitNegotiateCommitmentType(channelType lnwire.ChannelType, local,
}
return lnwallet.CommitmentTypeTweakless, nil
- // Simple taproot channels only.
+ // Simple taproot channels only (final feature bits).
+ case channelFeatures.OnlyContains(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ ):
+
+ if !hasFeatures(
+ local, remote,
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ ) {
+
+ return 0, errUnsupportedChannelType
+ }
+
+ return lnwallet.CommitmentTypeSimpleTaprootFinal, nil
+
+ // Simple taproot channels only (staging feature bits).
case channelFeatures.OnlyContains(
lnwire.SimpleTaprootChannelsRequiredStaging,
):
@@ -255,7 +270,24 @@ func explicitNegotiateCommitmentType(channelType lnwire.ChannelType, local,
return lnwallet.CommitmentTypeSimpleTaproot, nil
- // Simple taproot channels with scid only.
+ // Simple taproot channels with scid only (final feature bits).
+ case channelFeatures.OnlyContains(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ lnwire.ScidAliasRequired,
+ ):
+
+ if !hasFeatures(
+ local, remote,
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ScidAliasOptional,
+ ) {
+
+ return 0, errUnsupportedChannelType
+ }
+
+ return lnwallet.CommitmentTypeSimpleTaprootFinal, nil
+
+ // Simple taproot channels with scid only (staging feature bits).
case channelFeatures.OnlyContains(
lnwire.SimpleTaprootChannelsRequiredStaging,
lnwire.ScidAliasRequired,
@@ -272,7 +304,24 @@ func explicitNegotiateCommitmentType(channelType lnwire.ChannelType, local,
return lnwallet.CommitmentTypeSimpleTaproot, nil
- // Simple taproot channels with zero conf only.
+ // Simple taproot channels with zero conf only (final feature bits).
+ case channelFeatures.OnlyContains(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ lnwire.ZeroConfRequired,
+ ):
+
+ if !hasFeatures(
+ local, remote,
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ZeroConfOptional,
+ ) {
+
+ return 0, errUnsupportedChannelType
+ }
+
+ return lnwallet.CommitmentTypeSimpleTaprootFinal, nil
+
+ // Simple taproot channels with zero conf only (staging feature bits).
case channelFeatures.OnlyContains(
lnwire.SimpleTaprootChannelsRequiredStaging,
lnwire.ZeroConfRequired,
@@ -289,7 +338,27 @@ func explicitNegotiateCommitmentType(channelType lnwire.ChannelType, local,
return lnwallet.CommitmentTypeSimpleTaproot, nil
- // Simple taproot channels with scid and zero conf.
+ // Simple taproot channels with scid and zero conf (final feature bits).
+ case channelFeatures.OnlyContains(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ lnwire.ZeroConfRequired,
+ lnwire.ScidAliasRequired,
+ ):
+
+ if !hasFeatures(
+ local, remote,
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ZeroConfOptional,
+ lnwire.ScidAliasOptional,
+ ) {
+
+ return 0, errUnsupportedChannelType
+ }
+
+ return lnwallet.CommitmentTypeSimpleTaprootFinal, nil
+
+ // Simple taproot channels with scid and zero conf (staging feature
+ // bits).
case channelFeatures.OnlyContains(
lnwire.SimpleTaprootChannelsRequiredStaging,
lnwire.ZeroConfRequired,
@@ -300,6 +369,7 @@ func explicitNegotiateCommitmentType(channelType lnwire.ChannelType, local,
local, remote,
lnwire.SimpleTaprootChannelsOptionalStaging,
lnwire.ZeroConfOptional,
+ lnwire.ScidAliasOptional,
) {
return 0, errUnsupportedChannelType
@@ -391,6 +461,27 @@ 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 b9e9f59..4f7432f 100644
--- a/funding/commitment_type_negotiation_test.go
+++ b/funding/commitment_type_negotiation_test.go
@@ -307,6 +307,189 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
expectsChanType: nil,
expectsErr: nil,
},
+
+ // Test cases for final taproot channels with explicit negotiation.
+ {
+ name: "explicit simple taproot final only",
+ channelFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ ),
+ localFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ remoteFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ expectsCommitType: lnwallet.CommitmentTypeSimpleTaprootFinal,
+ expectsChanType: (*lnwire.ChannelType)(
+ lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ ),
+ ),
+ expectsErr: nil,
+ },
+ {
+ name: "explicit simple taproot final with scid alias",
+ channelFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ lnwire.ScidAliasRequired,
+ ),
+ localFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ScidAliasOptional,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ remoteFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ScidAliasOptional,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ expectsCommitType: lnwallet.CommitmentTypeSimpleTaprootFinal,
+ expectsChanType: (*lnwire.ChannelType)(
+ lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ lnwire.ScidAliasRequired,
+ ),
+ ),
+ scidAlias: true,
+ expectsErr: nil,
+ },
+ {
+ name: "explicit simple taproot final with zero conf",
+ channelFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ lnwire.ZeroConfRequired,
+ ),
+ localFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ZeroConfOptional,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ remoteFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ZeroConfOptional,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ expectsCommitType: lnwallet.CommitmentTypeSimpleTaprootFinal,
+ expectsChanType: (*lnwire.ChannelType)(
+ lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ lnwire.ZeroConfRequired,
+ ),
+ ),
+ zeroConf: true,
+ expectsErr: nil,
+ },
+ {
+ name: "explicit simple taproot final with scid alias and zero conf",
+ channelFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ lnwire.ScidAliasRequired,
+ lnwire.ZeroConfRequired,
+ ),
+ localFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ScidAliasOptional,
+ lnwire.ZeroConfOptional,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ remoteFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ScidAliasOptional,
+ lnwire.ZeroConfOptional,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ expectsCommitType: lnwallet.CommitmentTypeSimpleTaprootFinal,
+ expectsChanType: (*lnwire.ChannelType)(
+ lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ lnwire.ScidAliasRequired,
+ lnwire.ZeroConfRequired,
+ ),
+ ),
+ scidAlias: true,
+ zeroConf: true,
+ expectsErr: nil,
+ },
+ {
+ name: "explicit simple taproot final missing remote support",
+ channelFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ ),
+ localFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ remoteFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalStaging,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ expectsErr: errUnsupportedChannelType,
+ },
+
+ // Test cases for implicit negotiation preferring final over staging.
+ {
+ name: "implicit final taproot preferred over staging",
+ channelFeatures: nil,
+ localFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.SimpleTaprootChannelsOptionalStaging,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ remoteFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.SimpleTaprootChannelsOptionalStaging,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ expectsCommitType: lnwallet.CommitmentTypeSimpleTaprootFinal,
+ expectsChanType: (*lnwire.ChannelType)(
+ lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ ),
+ ),
+ expectsErr: nil,
+ },
+ {
+ name: "implicit staging taproot when final not supported",
+ channelFeatures: nil,
+ localFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.SimpleTaprootChannelsOptionalStaging,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ remoteFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalStaging,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ expectsCommitType: lnwallet.CommitmentTypeSimpleTaproot,
+ expectsChanType: (*lnwire.ChannelType)(
+ lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsRequiredStaging,
+ ),
+ ),
+ expectsErr: nil,
+ },
+ {
+ name: "implicit final taproot only",
+ channelFeatures: nil,
+ localFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ remoteFeatures: lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsOptionalFinal,
+ lnwire.ExplicitChannelTypeOptional,
+ ),
+ expectsCommitType: lnwallet.CommitmentTypeSimpleTaprootFinal,
+ expectsChanType: (*lnwire.ChannelType)(
+ lnwire.NewRawFeatureVector(
+ lnwire.SimpleTaprootChannelsRequiredFinal,
+ ),
+ ),
+ expectsErr: nil,
+ },
}
for _, testCase := range testCases {
diff --git a/funding/manager_test.go b/funding/manager_test.go
index fdd448d..f731739 100644
--- a/funding/manager_test.go
+++ b/funding/manager_test.go
@@ -4815,14 +4815,24 @@ func TestCommitmentTypeFundmaxSanityCheck(t *testing.T) {
"SCRIPT_ENFORCED_LEASE": 4,
"SIMPLE_TAPROOT": 5,
"SIMPLE_TAPROOT_OVERLAY": 6,
+ "SIMPLE_TAPROOT_FINAL": 7,
}
- for commitmentType := range lnrpc.CommitmentType_value {
- if _, ok := allCommitmentTypes[commitmentType]; !ok {
+ for commitmentType, protoValue := range lnrpc.CommitmentType_value {
+ expectedValue, ok := allCommitmentTypes[commitmentType]
+ if !ok {
t.Fatalf("Commitment type %s hasn't been considered "+
"in the context of the --fundmax flag for "+
"channel openings.", commitmentType)
}
+
+ // Verify the proto enum integer values match to catch
+ // accidental renumbering.
+ if int(protoValue) != expectedValue {
+ t.Fatalf("Commitment type %s has proto value %d "+
+ "but expected %d", commitmentType,
+ protoValue, expectedValue)
+ }
}
}
Why this scored 19/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.