Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b9f483c
feature: add taproot final to feature bit manager
Roasbeef Oct 29, 2025
8b8fe41
input: add production taproot witness types for final channels
Roasbeef Jun 24, 2025
be14f95
input: add production taproot HTLC succeed input constructor
Roasbeef Jun 24, 2025
4b61173
channeldb: add production taproot channel type support
Roasbeef Jun 24, 2025
47abccc
contractcourt: add channel type support to HTLC resolvers
Roasbeef Jun 24, 2025
3c87ca8
contractcourt: implement production taproot witness selection
Roasbeef Jun 24, 2025
6cc86cf
contractcourt: integrate production taproot support in nursery
Roasbeef Jun 24, 2025
ce61879
input: thread script options through taproot HTLC functions
Roasbeef Jun 24, 2025
5b8d632
lnwallet: integrate production script options in commitment generation
Roasbeef Jun 24, 2025
67f4583
lnrpc+rpcserver: add production taproot commitment type to RPC interface
Roasbeef Jun 24, 2025
fd3391f
funding: add production taproot channel negotiation support
Roasbeef Jun 24, 2025
6c432c4
itest+input: add production taproot channel integration tests
Roasbeef Jun 24, 2025
56d79e2
watchtower: prepare infrastructure for production taproot support
Roasbeef Jun 24, 2025
f27ced2
lnrpc/walletrpc: add witness types for taproot chans final
Roasbeef Oct 30, 2025
853c6eb
itest: extend relevant itests to cover taproot chans final
Roasbeef Oct 30, 2025
2e49e32
fixup! itest: extend relevant itests to cover taproot chans final
Roasbeef Oct 30, 2025
11ea871
lnwire: add local_nonces field to revoke_and_ack
Roasbeef Nov 12, 2025
27e1992
lnwallet: add support for local nonces map in revoke_and_ack
Roasbeef Nov 12, 2025
9630f6f
multi: use feature bits to pick which taproot nonce field to use
Roasbeef Dec 6, 2025
bb1b9fc
fixup! itest: extend relevant itests to cover taproot chans final
Roasbeef Dec 6, 2025
d0c2313
cmd/commands: add taproot-final to lncli open command
Roasbeef Jan 3, 2026
625ff13
peer: pass in remote peer's feature bits when loading new channel
Roasbeef Jan 3, 2026
536ed54
multi: add custom nonce rand support to MuSig2 sessions
Roasbeef Feb 21, 2026
4beb78e
lnwallet: add taproot channel test vector generator
Roasbeef Feb 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 58 additions & 11 deletions channeldb/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,11 @@ const (
// level tapscript commitment. This MUST be set along with the
// SimpleTaprootFeatureBit.
TapscriptRootBit ChannelType = 1 << 11

// TaprootFinalBit indicates that this is a MuSig2 channel using the
// final/production taproot scripts and feature bits 80/81. This MUST
// be set along with the SimpleTaprootFeatureBit.
TaprootFinalBit ChannelType = 1 << 12
)

// IsSingleFunder returns true if the channel type if one of the known single
Expand Down Expand Up @@ -488,6 +493,12 @@ func (c ChannelType) HasTapscriptRoot() bool {
return c&TapscriptRootBit == TapscriptRootBit
}

// IsTaprootFinal returns true if the channel is using final/production taproot
// scripts and feature bits.
func (c ChannelType) IsTaprootFinal() bool {
return c&TaprootFinalBit == TaprootFinalBit
}

// ChannelStateBounds are the parameters from OpenChannel and AcceptChannel
// that are responsible for providing bounds on the state space of the abstract
// channel state. These values must be remembered for normal channel operation
Expand Down Expand Up @@ -1741,6 +1752,26 @@ func NewMusigVerificationNonce(pubKey *btcec.PublicKey, targetHeight uint64,
return musig2.GenNonces(pubKeyOpt, shaChainRand)
}

// chanSyncCfg holds configuration options for ChanSyncMsg.
type chanSyncCfg struct {
// taprootNonceType specifies which nonce format to use when
// constructing the ChannelReestablish message for the peer.
taprootNonceType lnwire.TaprootNonceType
}

// ChanSyncOpt is a functional option that can be used to modify the behavior of
// ChanSyncMsg.
type ChanSyncOpt func(*chanSyncCfg)

// WithChanSyncNonceType specifies which nonce format to use when constructing
// the ChannelReestablish message. This is determined by the peer's advertised
// feature bits.
func WithChanSyncNonceType(nonceType lnwire.TaprootNonceType) ChanSyncOpt {
return func(cfg *chanSyncCfg) {
cfg.taprootNonceType = nonceType
}
}

// ChanSyncMsg returns the ChannelReestablish message that should be sent upon
// reconnection with the remote peer that we're maintaining this channel with.
// The information contained within this message is necessary to re-sync our
Expand All @@ -1756,7 +1787,16 @@ func NewMusigVerificationNonce(pubKey *btcec.PublicKey, targetHeight uint64,
// If this is a restored channel, having status ChanStatusRestored, then we'll
// modify our typical chan sync message to ensure they force close even if
// we're on the very first state.
func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) {
func (c *OpenChannel) ChanSyncMsg(
opts ...ChanSyncOpt) (*lnwire.ChannelReestablish, error) {

cfg := &chanSyncCfg{
taprootNonceType: lnwire.TaprootNonceTypeLegacy,
}
for _, opt := range opts {
opt(cfg)
}

c.Lock()
defer c.Unlock()

Expand Down Expand Up @@ -1836,17 +1876,24 @@ func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) {
"nonce: %w", err)
}

// Populate the legacy LocalNonce field for backwards compatibility.
nextTaprootNonce = lnwire.SomeMusig2Nonce(nextNonce.PubNonce)

// Also populate the new LocalNonces field. For channel
// re-establishment, we'll key our nonce by the funding txid.
fundingTxid := c.FundingOutpoint.Hash
noncesMap := make(map[chainhash.Hash]lnwire.Musig2Nonce)
noncesMap[fundingTxid] = nextNonce.PubNonce
nextLocalNonces = lnwire.SomeLocalNonces(
lnwire.LocalNoncesData{NoncesMap: noncesMap},
)
nonce := nextNonce.PubNonce

// Set the appropriate nonce field based on the peer's feature
// bits. If they support the final taproot channel feature bits,
// we use the map-based LocalNonces field. Otherwise, we use
// the legacy single LocalNonce field.
switch cfg.taprootNonceType {
case lnwire.TaprootNonceTypeLegacy:
nextTaprootNonce = lnwire.SomeMusig2Nonce(nonce)

case lnwire.TaprootNonceTypeMap:
noncesMap := make(map[chainhash.Hash]lnwire.Musig2Nonce)
noncesMap[fundingTxid] = nonce
nextLocalNonces = lnwire.SomeLocalNonces(
lnwire.LocalNoncesData{NoncesMap: noncesMap},
)
}
}

return &lnwire.ChannelReestablish{
Expand Down
13 changes: 8 additions & 5 deletions cmd/commands/cmd_open_channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ Signed base64 encoded PSBT or hex encoded raw wire TX (or path to file): `
// of memory issues or other weird errors.
psbtMaxFileSize = 1024 * 1024

channelTypeTweakless = "tweakless"
channelTypeAnchors = "anchors"
channelTypeSimpleTaproot = "taproot"
channelTypeTweakless = "tweakless"
channelTypeAnchors = "anchors"
channelTypeSimpleTaproot = "taproot"
channelTypeSimpleTaprootFinal = "taproot-final"
)

// TODO(roasbeef): change default number of confirmations.
Expand Down Expand Up @@ -253,9 +254,9 @@ var openChannelCommand = cli.Command{
cli.StringFlag{
Name: "channel_type",
Usage: fmt.Sprintf("(optional) the type of channel to "+
"propose to the remote peer (%q, %q, %q)",
"propose to the remote peer (%q, %q, %q, %q)",
channelTypeTweakless, channelTypeAnchors,
channelTypeSimpleTaproot),
channelTypeSimpleTaproot, channelTypeSimpleTaprootFinal),
},
cli.BoolFlag{
Name: "zero_conf",
Expand Down Expand Up @@ -446,6 +447,8 @@ func openChannel(ctx *cli.Context) error {
req.CommitmentType = lnrpc.CommitmentType_ANCHORS
case channelTypeSimpleTaproot:
req.CommitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT
case channelTypeSimpleTaprootFinal:
req.CommitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT_FINAL
default:
return fmt.Errorf("unsupported channel type %v", channelType)
}
Expand Down
27 changes: 23 additions & 4 deletions contractcourt/channel_arbitrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2466,8 +2466,13 @@ func (c *ChannelArbitrator) prepContractResolutions(
continue
}

var chanType channeldb.ChannelType
if chanState != nil {
chanType = chanState.ChanType
}

resolver := newSuccessResolver(
resolution, height, htlc, resolverCfg,
resolution, height, htlc, chanType, resolverCfg,
)
if chanState != nil {
resolver.SupplementState(chanState)
Expand All @@ -2494,8 +2499,13 @@ func (c *ChannelArbitrator) prepContractResolutions(
continue
}

var chanType channeldb.ChannelType
if chanState != nil {
chanType = chanState.ChanType
}

resolver := newTimeoutResolver(
resolution, height, htlc, resolverCfg,
resolution, height, htlc, chanType, resolverCfg,
)
if chanState != nil {
resolver.SupplementState(chanState)
Expand Down Expand Up @@ -2534,8 +2544,13 @@ func (c *ChannelArbitrator) prepContractResolutions(
continue
}

var chanType channeldb.ChannelType
if chanState != nil {
chanType = chanState.ChanType
}

resolver := newIncomingContestResolver(
resolution, height, htlc,
resolution, height, htlc, chanType,
resolverCfg,
)
if chanState != nil {
Expand Down Expand Up @@ -2566,8 +2581,12 @@ func (c *ChannelArbitrator) prepContractResolutions(
continue
}

var chanType channeldb.ChannelType
if chanState != nil {
chanType = chanState.ChanType
}
resolver := newOutgoingContestResolver(
resolution, height, htlc, resolverCfg,
resolution, height, htlc, chanType, resolverCfg,
)
if chanState != nil {
resolver.SupplementState(chanState)
Expand Down
12 changes: 10 additions & 2 deletions contractcourt/commit_sweep_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,11 +506,19 @@ func (c *commitSweepResolver) decideWitnessType() (input.WitnessType, error) {
// commitment tweak to discern which type of commitment this is.
var witnessType input.WitnessType
switch {
// The local delayed output for a taproot channel.
// The local delayed output for a final taproot channel.
case isLocalCommitTx && c.chanType.IsTaprootFinal():
witnessType = input.TaprootLocalCommitSpendFinal

// The local delayed output for a staging taproot channel.
case isLocalCommitTx && c.chanType.IsTaproot():
witnessType = input.TaprootLocalCommitSpend

// The CSV 1 delayed output for a taproot channel.
// The CSV 1 delayed output for a final taproot channel.
case !isLocalCommitTx && c.chanType.IsTaprootFinal():
witnessType = input.TaprootRemoteCommitSpendFinal

// The CSV 1 delayed output for a staging taproot channel.
case !isLocalCommitTx && c.chanType.IsTaproot():
witnessType = input.TaprootRemoteCommitSpend

Expand Down
4 changes: 2 additions & 2 deletions contractcourt/htlc_incoming_contest_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ type htlcIncomingContestResolver struct {
// newIncomingContestResolver instantiates a new incoming htlc contest resolver.
func newIncomingContestResolver(
res lnwallet.IncomingHtlcResolution, broadcastHeight uint32,
htlc channeldb.HTLC, resCfg ResolverConfig) *htlcIncomingContestResolver {
htlc channeldb.HTLC, chanType channeldb.ChannelType, resCfg ResolverConfig) *htlcIncomingContestResolver {

success := newSuccessResolver(
res, broadcastHeight, htlc, resCfg,
res, broadcastHeight, htlc, chanType, resCfg,
)

return &htlcIncomingContestResolver{
Expand Down
4 changes: 2 additions & 2 deletions contractcourt/htlc_outgoing_contest_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ type htlcOutgoingContestResolver struct {
// resolver.
func newOutgoingContestResolver(res lnwallet.OutgoingHtlcResolution,
broadcastHeight uint32, htlc channeldb.HTLC,
resCfg ResolverConfig) *htlcOutgoingContestResolver {
chanType channeldb.ChannelType, resCfg ResolverConfig) *htlcOutgoingContestResolver {

timeout := newTimeoutResolver(
res, broadcastHeight, htlc, resCfg,
res, broadcastHeight, htlc, chanType, resCfg,
)

return &htlcOutgoingContestResolver{
Expand Down
28 changes: 25 additions & 3 deletions contractcourt/htlc_success_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ type htlcSuccessResolver struct {
// htlc contains information on the htlc that we are resolving on-chain.
htlc channeldb.HTLC

// chanType denotes the type of channel the HTLC belongs to.
chanType channeldb.ChannelType

// currentReport stores the current state of the resolver for reporting
// over the rpc interface. This should only be reported in case we have
// a non-nil SignDetails on the htlcResolution, otherwise the nursery
Expand All @@ -67,13 +70,14 @@ type htlcSuccessResolver struct {
// newSuccessResolver instanties a new htlc success resolver.
func newSuccessResolver(res lnwallet.IncomingHtlcResolution,
broadcastHeight uint32, htlc channeldb.HTLC,
resCfg ResolverConfig) *htlcSuccessResolver {
chanType channeldb.ChannelType, resCfg ResolverConfig) *htlcSuccessResolver {

h := &htlcSuccessResolver{
contractResolverKit: *newContractResolverKit(resCfg),
htlcResolution: res,
broadcastHeight: broadcastHeight,
htlc: htlc,
chanType: chanType,
}

h.initReport()
Expand Down Expand Up @@ -409,6 +413,11 @@ func (h *htlcSuccessResolver) isTaproot() bool {
)
}

// isTaprootFinal returns true if the htlc output is from a final taproot channel.
func (h *htlcSuccessResolver) isTaprootFinal() bool {
return h.chanType.IsTaprootFinal()
}

// sweepRemoteCommitOutput creates a sweep request to sweep the HTLC output on
// the remote commitment via the direct preimage-spend.
func (h *htlcSuccessResolver) sweepRemoteCommitOutput() error {
Expand All @@ -417,7 +426,18 @@ func (h *htlcSuccessResolver) sweepRemoteCommitOutput() error {
// sweeping transaction, and generate a witness.
var inp input.Input

if h.isTaproot() {
if h.isTaprootFinal() {
inp = lnutils.Ptr(input.MakeTaprootHtlcSucceedInputFinal(
&h.htlcResolution.ClaimOutpoint,
&h.htlcResolution.SweepSignDesc,
h.htlcResolution.Preimage[:],
h.broadcastHeight,
h.htlcResolution.CsvDelay,
input.WithResolutionBlob(
h.htlcResolution.ResolutionBlob,
),
))
} else if h.isTaproot() {
inp = lnutils.Ptr(input.MakeTaprootHtlcSucceedInput(
&h.htlcResolution.ClaimOutpoint,
&h.htlcResolution.SweepSignDesc,
Expand Down Expand Up @@ -562,7 +582,9 @@ func (h *htlcSuccessResolver) sweepSuccessTxOutput() error {
// Let the sweeper sweep the second-level output now that the
// CSV/CLTV locks have expired.
var witType input.StandardWitnessType
if h.isTaproot() {
if h.isTaprootFinal() {
witType = input.TaprootHtlcAcceptedSuccessSecondLevelFinal
} else if h.isTaproot() {
witType = input.TaprootHtlcAcceptedSuccessSecondLevel
} else {
witType = input.HtlcAcceptedSuccessSecondLevel
Expand Down
19 changes: 16 additions & 3 deletions contractcourt/htlc_timeout_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ type htlcTimeoutResolver struct {
// htlc contains information on the htlc that we are resolving on-chain.
htlc channeldb.HTLC

// chanType denotes the type of channel the HTLC belongs to.
chanType channeldb.ChannelType

// currentReport stores the current state of the resolver for reporting
// over the rpc interface. This should only be reported in case we have
// a non-nil SignDetails on the htlcResolution, otherwise the nursery
Expand All @@ -70,13 +73,14 @@ type htlcTimeoutResolver struct {
// newTimeoutResolver instantiates a new timeout htlc resolver.
func newTimeoutResolver(res lnwallet.OutgoingHtlcResolution,
broadcastHeight uint32, htlc channeldb.HTLC,
resCfg ResolverConfig) *htlcTimeoutResolver {
chanType channeldb.ChannelType, resCfg ResolverConfig) *htlcTimeoutResolver {

h := &htlcTimeoutResolver{
contractResolverKit: *newContractResolverKit(resCfg),
htlcResolution: res,
broadcastHeight: broadcastHeight,
htlc: htlc,
chanType: chanType,
}

h.initReport()
Expand All @@ -92,6 +96,11 @@ func (h *htlcTimeoutResolver) isTaproot() bool {
)
}

// isTaprootFinal returns true if the htlc output is from a final taproot channel.
func (h *htlcTimeoutResolver) isTaprootFinal() bool {
return h.chanType.IsTaprootFinal()
}

// outpoint returns the outpoint of the HTLC output we're attempting to sweep.
func (h *htlcTimeoutResolver) outpoint() wire.OutPoint {
// The primary key for this resolver will be the outpoint of the HTLC
Expand Down Expand Up @@ -515,7 +524,9 @@ func (h *htlcTimeoutResolver) resolveSecondLevelTxLegacy() error {
// are resolved via this path.
func (h *htlcTimeoutResolver) sweepDirectHtlcOutput() error {
var htlcWitnessType input.StandardWitnessType
if h.isTaproot() {
if h.isTaprootFinal() {
htlcWitnessType = input.TaprootHtlcOfferedRemoteTimeoutFinal
} else if h.isTaproot() {
htlcWitnessType = input.TaprootHtlcOfferedRemoteTimeout
} else {
htlcWitnessType = input.HtlcOfferedRemoteTimeout
Expand Down Expand Up @@ -1030,7 +1041,9 @@ func (h *htlcTimeoutResolver) sweepTimeoutTxOutput() error {
}

var witType input.StandardWitnessType
if h.isTaproot() {
if h.isTaprootFinal() {
witType = input.TaprootHtlcOfferedTimeoutSecondLevelFinal
} else if h.isTaproot() {
witType = input.TaprootHtlcOfferedTimeoutSecondLevel
} else {
witType = input.HtlcOfferedTimeoutSecondLevel
Expand Down
Loading
Loading