lnwallet: integrate production script options in commitment generation
What changed, and why it matters
This commit adds support for a new 'production' version of taproot Lightning channels. It makes sure the wallet picks the correct, optimized Bitcoin scripts when building commitment transactions for this new channel type, while keeping older staging taproot and legacy channels unchanged. There is no direct evidence in the commit that this fixes an active security vulnerability; it reads more like feature completion for a new channel type.
Treat as a feature/integration commit rather than an urgent security patch. Reviewers should verify that IsTaprootFinal() correctly identifies only the intended production taproot channels and that WithProdScripts() is applied consistently across all commitment and HTLC paths, including any other call sites not shown in this diff.
Security signals we found
New channel commitment type added (CommitmentTypeSimpleTaprootFinal)
Script-generation option selection now depends on IsTaprootFinal()
Production taproot scripts use OP_CHECKSIGVERIFY per commit message
Backward compatibility explicitly maintained for staging taproot and legacy channels
No explicit security bug, CVE, or vulnerability described in commit or references
Evidence from the diff
The patch introduces CommitmentTypeSimpleTaprootFinal and propagates input.WithProdScripts() into local/remote commitment script trees and HTLC script generation when chanType.IsTaprootFinal() is true. It touches channel.go, commitment.go, and reservation.go. The change is gated on the new channel type, so it only affects channels negotiated with the final taproot feature bits. The commit message frames this as consistency/integration work rather than a security fix.
Changed components
lnwallet/channel.golnwallet/commitment.golnwallet/reservation.goTaproot channel commitment transaction generationHTLC script tree constructionInspect captured patch +65 / −12
diff --git a/lnwallet/channel.go b/lnwallet/channel.go
index 27016e6..2f0cfae 100644
--- a/lnwallet/channel.go
+++ b/lnwallet/channel.go
@@ -7569,10 +7569,16 @@ func newOutgoingHtlcResolution(signer input.Signer,
return nil, err
}
} else {
+ // Determine script options based on channel type.
+ var scriptOpts []input.TaprootScriptOpt
+ if chanType.IsTaprootFinal() {
+ scriptOpts = append(scriptOpts, input.WithProdScripts())
+ }
+
//nolint:ll
secondLevelScriptTree, err := input.TaprootSecondLevelScriptTree(
keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay,
- secondLevelAuxLeaf,
+ secondLevelAuxLeaf, scriptOpts...,
)
if err != nil {
return nil, err
@@ -7933,10 +7939,16 @@ func newIncomingHtlcResolution(signer input.Signer,
return nil, err
}
} else {
+ // Determine script options based on channel type.
+ var scriptOpts []input.TaprootScriptOpt
+ if chanType.IsTaprootFinal() {
+ scriptOpts = append(scriptOpts, input.WithProdScripts())
+ }
+
//nolint:ll
secondLevelScriptTree, err := input.TaprootSecondLevelScriptTree(
keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay,
- secondLevelAuxLeaf,
+ secondLevelAuxLeaf, scriptOpts...,
)
if err != nil {
return nil, err
diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go
index ab20d9a..74348ee 100644
--- a/lnwallet/commitment.go
+++ b/lnwallet/commitment.go
@@ -236,8 +236,14 @@ func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool,
//
// Our "redeem" script here is just the taproot witness program.
case chanType.IsTaproot():
+ // Determine script options based on channel type.
+ var scriptOpts []input.TaprootScriptOpt
+ if chanType.IsTaprootFinal() {
+ scriptOpts = append(scriptOpts, input.WithProdScripts())
+ }
+
return input.NewLocalCommitScriptTree(
- csvDelay, selfKey, revokeKey, auxLeaf,
+ csvDelay, selfKey, revokeKey, auxLeaf, scriptOpts...,
)
// If we are the initiator of a leased channel, then we have an
@@ -320,8 +326,14 @@ func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool,
// we use a NUMS key to force the remote party to take a script path,
// with the sole tap leaf enforcing the 1 CSV delay.
case chanType.IsTaproot():
+ // Determine script options based on channel type.
+ var scriptOpts []input.TaprootScriptOpt
+ if chanType.IsTaprootFinal() {
+ scriptOpts = append(scriptOpts, input.WithProdScripts())
+ }
+
toRemoteScriptTree, err := input.NewRemoteCommitScriptTree(
- remoteKey, auxLeaf,
+ remoteKey, auxLeaf, scriptOpts...,
)
if err != nil {
return nil, 0, err
@@ -426,8 +438,14 @@ func SecondLevelHtlcScript(chanType channeldb.ChannelType, initiator bool,
switch {
// For taproot channels, the pkScript is a segwit v1 p2tr output.
case chanType.IsTaproot():
+ // Determine script options based on channel type.
+ var scriptOpts []input.TaprootScriptOpt
+ if chanType.IsTaprootFinal() {
+ scriptOpts = append(scriptOpts, input.WithProdScripts())
+ }
+
return input.TaprootSecondLevelScriptTree(
- revocationKey, delayKey, csvDelay, auxLeaf,
+ revocationKey, delayKey, csvDelay, auxLeaf, scriptOpts...,
)
// If we are the initiator of a leased channel, then we have an
@@ -1165,7 +1183,7 @@ func genSegwitV0HtlcScript(chanType channeldb.ChannelType,
// channel.
func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
timeout uint32, rHash [32]byte, keyRing *CommitmentKeyRing,
- auxLeaf input.AuxTapLeaf) (*input.HtlcScriptTree, error) {
+ auxLeaf input.AuxTapLeaf, opts ...input.TaprootScriptOpt) (*input.HtlcScriptTree, error) {
var (
htlcScriptTree *input.HtlcScriptTree
@@ -1182,7 +1200,7 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
case isIncoming && whoseCommit.IsLocal():
htlcScriptTree, err = input.ReceiverHTLCScriptTaproot(
timeout, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey,
- keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf,
+ keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf, opts...,
)
// We're being paid via an HTLC by the remote party, and the HTLC is
@@ -1191,7 +1209,7 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
case isIncoming && whoseCommit.IsRemote():
htlcScriptTree, err = input.SenderHTLCScriptTaproot(
keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey,
- keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf,
+ keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf, opts...,
)
// We're sending an HTLC which is being added to our commitment
@@ -1200,7 +1218,7 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
case !isIncoming && whoseCommit.IsLocal():
htlcScriptTree, err = input.SenderHTLCScriptTaproot(
keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey,
- keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf,
+ keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf, opts...,
)
// Finally, we're paying the remote party via an HTLC, which is being
@@ -1209,7 +1227,7 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
case !isIncoming && whoseCommit.IsRemote():
htlcScriptTree, err = input.ReceiverHTLCScriptTaproot(
timeout, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey,
- keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf,
+ keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf, opts...,
)
}
@@ -1234,8 +1252,14 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming bool,
)
}
+ // Determine script options based on channel type.
+ var scriptOpts []input.TaprootScriptOpt
+ if chanType.IsTaprootFinal() {
+ scriptOpts = append(scriptOpts, input.WithProdScripts())
+ }
+
return GenTaprootHtlcScript(
- isIncoming, whoseCommit, timeout, rHash, keyRing, auxLeaf,
+ isIncoming, whoseCommit, timeout, rHash, keyRing, auxLeaf, scriptOpts...,
)
}
diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go
index a8a0cac..908759b 100644
--- a/lnwallet/reservation.go
+++ b/lnwallet/reservation.go
@@ -49,9 +49,16 @@ const (
// CommitmentTypeSimpleTaproot is the base commitment type for the
// channels that use a musig2 funding output and the tapscript tree
- // where relevant for the commitment transaction pk scripts.
+ // where relevant for the commitment transaction pk scripts. This is
+ // the staging version using feature bits 180/181.
CommitmentTypeSimpleTaproot
+ // CommitmentTypeSimpleTaprootFinal is the production commitment type for
+ // taproot channels that use a musig2 funding output and the tapscript tree
+ // where relevant for the commitment transaction pk scripts. This uses the
+ // final feature bits 80/81 and production scripts.
+ CommitmentTypeSimpleTaprootFinal
+
// CommitmentTypeSimpleTaprootOverlay builds on the existing
// CommitmentTypeSimpleTaproot type but layers on a special overlay
// protocol.
@@ -66,6 +73,7 @@ func (c CommitmentType) HasStaticRemoteKey() bool {
CommitmentTypeAnchorsZeroFeeHtlcTx,
CommitmentTypeScriptEnforcedLease,
CommitmentTypeSimpleTaproot,
+ CommitmentTypeSimpleTaprootFinal,
CommitmentTypeSimpleTaprootOverlay:
return true
@@ -81,6 +89,7 @@ func (c CommitmentType) HasAnchors() bool {
case CommitmentTypeAnchorsZeroFeeHtlcTx,
CommitmentTypeScriptEnforcedLease,
CommitmentTypeSimpleTaproot,
+ CommitmentTypeSimpleTaprootFinal,
CommitmentTypeSimpleTaprootOverlay:
return true
@@ -93,6 +102,7 @@ func (c CommitmentType) HasAnchors() bool {
// IsTaproot returns true if the channel type is a taproot channel.
func (c CommitmentType) IsTaproot() bool {
return c == CommitmentTypeSimpleTaproot ||
+ c == CommitmentTypeSimpleTaprootFinal ||
c == CommitmentTypeSimpleTaprootOverlay
}
@@ -109,6 +119,8 @@ func (c CommitmentType) String() string {
return "script-enforced-lease"
case CommitmentTypeSimpleTaproot:
return "simple-taproot"
+ case CommitmentTypeSimpleTaprootFinal:
+ return "simple-taproot-final"
case CommitmentTypeSimpleTaprootOverlay:
return "simple-taproot-overlay"
default:
@@ -441,6 +453,11 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
if req.CommitType.IsTaproot() {
chanType |= channeldb.SimpleTaprootFeatureBit
+
+ // Set the final bit if this is the production taproot version.
+ if req.CommitType == CommitmentTypeSimpleTaprootFinal {
+ chanType |= channeldb.TaprootFinalBit
+ }
}
if req.ZeroConf {
Why this scored 30/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.