Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
8ce6327
feature: add taproot final to feature bit manager
Roasbeef Oct 29, 2025
fc2e01a
input: add production taproot witness types for final channels
Roasbeef Jun 24, 2025
0720abc
input: add production taproot HTLC succeed input constructor
Roasbeef Jun 24, 2025
749a8e4
channeldb: add production taproot channel type support
Roasbeef Jun 24, 2025
6b0c3f5
contractcourt: add channel type support to HTLC resolvers
Roasbeef Jun 24, 2025
269d2c3
contractcourt: implement production taproot witness selection
Roasbeef Jun 24, 2025
f9a8598
contractcourt: integrate production taproot support in nursery
Roasbeef Jun 24, 2025
60b3d2c
input: thread script options through taproot HTLC functions
Roasbeef Jun 24, 2025
2e0343c
lnwallet: integrate production script options in commitment generation
Roasbeef Jun 24, 2025
4ee8bd5
lnrpc+rpcserver: add production taproot commitment type to RPC interface
Roasbeef Jun 24, 2025
27ae0cb
funding: add production taproot channel negotiation support
Roasbeef Jun 24, 2025
d15b961
itest+input: add production taproot channel integration tests
Roasbeef Jun 24, 2025
597af82
watchtower: prepare infrastructure for production taproot support
Roasbeef Jun 24, 2025
d86b840
lnrpc/walletrpc: add witness types for taproot chans final
Roasbeef Oct 30, 2025
5bbfdc6
itest: extend relevant itests to cover taproot chans final
Roasbeef Oct 30, 2025
09acced
lnwire: add local_nonces field to revoke_and_ack
Roasbeef Nov 12, 2025
55ab4b0
lnwallet: add support for local nonces map in revoke_and_ack
Roasbeef Nov 12, 2025
5ffcd82
multi: use feature bits to pick which taproot nonce field to use
Roasbeef Dec 6, 2025
29de2c8
cmd/commands: add taproot-final to lncli open command
Roasbeef Jan 3, 2026
08c42b1
multi: add custom nonce rand support to MuSig2 sessions
Roasbeef Feb 21, 2026
38c415a
lnwallet: add taproot channel test vector generator
Roasbeef Feb 21, 2026
fa97946
lnwallet: emit actual MuSig2 partial sigs and nonces in test vectors
Roasbeef Mar 9, 2026
b78de44
lnwallet: fix HTLC sig-to-transaction mapping in test vector generator
Roasbeef Mar 9, 2026
745bdc1
lnwallet: fix HTLC trimming test case to use dust_limit for zero-fee …
Roasbeef Mar 9, 2026
77da917
lnwallet: add 3rd-party signature verification for taproot test vectors
Roasbeef Mar 9, 2026
70f189f
lnwallet: regenerate taproot channel test vectors
Roasbeef Mar 9, 2026
2148445
lnwallet: add secret nonce stashing to MusigSession for test vectors
Roasbeef Mar 12, 2026
4c225dd
lnwallet: add MuSig2 secret nonces and partial sig replay to test vec…
Roasbeef Mar 12, 2026
50981df
lnwallet: regenerate taproot test vectors with secret nonces
Roasbeef Mar 12, 2026
63450b8
lnwallet: use BIP-340 nonce derivation for HTLC sigs in test vectors
Roasbeef Mar 16, 2026
1866770
lnwallet: regenerate test vectors with BIP-340 HTLC signatures
Roasbeef Mar 16, 2026
1d7b5bb
lnrpc: regenerate protobuf files for Go 1.26 compatibility
Roasbeef Mar 26, 2026
a279700
lnwallet: fix fundingTxid scope in TestChanSyncTaprootLocalNonces
Roasbeef Mar 26, 2026
65d04f3
multi: fix linter issues
Roasbeef Mar 26, 2026
7a18fba
watchtower: add production taproot channel support to justice kit
Roasbeef Mar 28, 2026
086f692
itest: extend watchtower breach test to cover production taproot chan…
Roasbeef Mar 28, 2026
890636a
multi: fix linter issues
Roasbeef Apr 1, 2026
91e35c4
docs/release-notes: add release note for production taproot channels
Roasbeef Apr 7, 2026
aaf7c29
lnwallet: return error from AggregateNonces in MusigSession
Roasbeef Apr 8, 2026
98c086b
chanbackup: add SimpleTaprootFinalVersion for production taproot backups
Roasbeef Apr 8, 2026
fd3b638
multi: fix lint and itest failures for production taproot channels
Roasbeef Apr 10, 2026
85bba2c
multi: add SCB restore support for production taproot channels
Roasbeef Apr 13, 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
13 changes: 12 additions & 1 deletion chanbackup/single.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ const (
// channel with a top level tapscript commitment.
TapscriptRootVersion = 6

// SimpleTaprootFinalVersion is a version that denotes this channel is
// using the production musig2 based taproot commitment format with
// final scripts (OP_CHECKSIGVERIFY instead of OP_CHECKSIG + OP_DROP).
SimpleTaprootFinalVersion = 7

// closeTxVersionMask is the byte mask used that is ORed to version byte
// on wire indicating that the backup has CloseTxInputs.
closeTxVersionMask = 1 << 7
Expand Down Expand Up @@ -94,7 +99,8 @@ func DecodeVersion(encoded byte) (SingleBackupVersion, bool) {
// IsTaproot returns if this is a backup of a taproot channel. This will also be
// true for simple taproot overlay channels when a version is added.
func (v SingleBackupVersion) IsTaproot() bool {
return v == SimpleTaprootVersion || v == TapscriptRootVersion
return v == SimpleTaprootVersion || v == TapscriptRootVersion ||
v == SimpleTaprootFinalVersion
}

// HasTapscriptRoot returns true if the channel is using a top level tapscript
Expand Down Expand Up @@ -302,6 +308,9 @@ func NewSingle(channel *channeldb.OpenChannel,
}

switch {
case channel.ChanType.IsTaprootFinal():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks like this introduces a new SCB version for SIMPLE_TAPROOT_FINAL, but chanrestore.openChannelShell() still does not map version 7 back to a ChannelType. That means newly created backups for final taproot channels can be serialized/deserialized here, but restore/DLP will still fail once they reach chanrestore.

This we can simply add a case chanbackup.SimpleTaprootFinalVersion here (same as SimpleTaprootVersion, plus channeldb.TaprootFinalBit),

case chanbackup.SimpleTaprootVersion:

So we can properly reconstruct the channel type, and add an SCB restore itest for SIMPLE_TAPROOT_FINAL.

single.Version = SimpleTaprootFinalVersion

case channel.ChanType.IsTaproot():
if channel.ChanType.HasTapscriptRoot() {
single.Version = TapscriptRootVersion
Expand Down Expand Up @@ -351,6 +360,7 @@ func (s *Single) Serialize(w io.Writer) error {
case ScriptEnforcedLeaseVersion:
case SimpleTaprootVersion:
case TapscriptRootVersion:
case SimpleTaprootFinalVersion:
default:
return fmt.Errorf("unable to serialize w/ unknown "+
"version: %v", s.Version)
Expand Down Expand Up @@ -585,6 +595,7 @@ func (s *Single) Deserialize(r io.Reader) error {
case ScriptEnforcedLeaseVersion:
case SimpleTaprootVersion:
case TapscriptRootVersion:
case SimpleTaprootFinalVersion:
default:
return fmt.Errorf("unable to de-serialize w/ unknown "+
"version: %v", s.Version)
Expand Down
37 changes: 26 additions & 11 deletions channeldb/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,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 @@ -513,6 +518,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 @@ -1903,6 +1914,7 @@ func NewMusigVerificationNonce(pubKey *btcec.PublicKey, targetHeight uint64,
// 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) {

c.Lock()
defer c.Unlock()

Expand Down Expand Up @@ -1982,18 +1994,21 @@ 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

// Final taproot channels use the map-based LocalNonces
// field keyed by funding TXID. Staging channels use the
// legacy single LocalNonce field.
if c.ChanType.IsTaprootFinal() {
noncesMap := make(map[chainhash.Hash]lnwire.Musig2Nonce)
noncesMap[fundingTxid] = nonce
nextLocalNonces = lnwire.SomeLocalNonces(
lnwire.LocalNoncesData{NoncesMap: noncesMap},
)
} else {
nextTaprootNonce = lnwire.SomeMusig2Nonce(nonce)
}
}

return &lnwire.ChannelReestablish{
Expand Down
7 changes: 7 additions & 0 deletions chanrestore.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,13 @@ func (c *chanDBRestorer) openChannelShell(backup chanbackup.Single) (
chanType |= channeldb.SimpleTaprootFeatureBit
chanType |= channeldb.TapscriptRootBit

case chanbackup.SimpleTaprootFinalVersion:
chanType = channeldb.ZeroHtlcTxFeeBit
chanType |= channeldb.AnchorOutputsBit
chanType |= channeldb.SingleFunderTweaklessBit
chanType |= channeldb.SimpleTaprootFeatureBit
chanType |= channeldb.TaprootFinalBit

default:
return nil, fmt.Errorf("unknown Single version: %w", err)
}
Expand Down
14 changes: 9 additions & 5 deletions cmd/commands/cmd_open_channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,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 @@ -254,9 +255,10 @@ 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 @@ -447,6 +449,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
2 changes: 1 addition & 1 deletion contractcourt/chain_arbitrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ type ChainArbitratorConfig struct {
IncubateOutputs func(wire.OutPoint,
fn.Option[lnwallet.OutgoingHtlcResolution],
fn.Option[lnwallet.IncomingHtlcResolution],
uint32, fn.Option[int32]) error
uint32, fn.Option[int32], ...IncubateOption) error

// PreimageDB is a global store of all known pre-images. We'll use this
// to decide if we should broadcast a commitment transaction to claim
Expand Down
18 changes: 14 additions & 4 deletions contractcourt/channel_arbitrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2434,6 +2434,13 @@ func (c *ChannelArbitrator) prepContractResolutions(
return htlcResolvers, nil
}

// Determine the channel type once before the resolution loop so we
// don't repeat the nil check on every iteration.
var chanType channeldb.ChannelType
if chanState != nil {
chanType = chanState.ChanType
}

// For each HTLC, we'll either act immediately, meaning we'll instantly
// fail the HTLC, or we'll act only once the transaction has been
// confirmed, in which case we'll need an HTLC resolver.
Expand All @@ -2460,7 +2467,8 @@ func (c *ChannelArbitrator) prepContractResolutions(
}

resolver := newSuccessResolver(
resolution, height, htlc, resolverCfg,
resolution, height, htlc, chanType,
resolverCfg,
)
if chanState != nil {
resolver.SupplementState(chanState)
Expand Down Expand Up @@ -2488,7 +2496,8 @@ func (c *ChannelArbitrator) prepContractResolutions(
}

resolver := newTimeoutResolver(
resolution, height, htlc, resolverCfg,
resolution, height, htlc, chanType,
resolverCfg,
)
if chanState != nil {
resolver.SupplementState(chanState)
Expand Down Expand Up @@ -2528,7 +2537,7 @@ func (c *ChannelArbitrator) prepContractResolutions(
}

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

resolver := newOutgoingContestResolver(
resolution, height, htlc, resolverCfg,
resolution, height, htlc, chanType,
resolverCfg,
)
if chanState != nil {
resolver.SupplementState(chanState)
Expand Down
3 changes: 2 additions & 1 deletion contractcourt/channel_arbitrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,8 @@ func createTestChannelArbitrator(t *testing.T, log ArbitratorLog,
IncubateOutputs: func(wire.OutPoint,
fn.Option[lnwallet.OutgoingHtlcResolution],
fn.Option[lnwallet.IncomingHtlcResolution],
uint32, fn.Option[int32]) error {
uint32, fn.Option[int32],
...IncubateOption) error {

incubateChan <- struct{}{}
return nil
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 @@ -470,11 +470,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
5 changes: 3 additions & 2 deletions contractcourt/htlc_incoming_contest_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ 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
3 changes: 2 additions & 1 deletion contractcourt/htlc_outgoing_contest_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ type htlcOutgoingContestResolver struct {
// resolver.
func newOutgoingContestResolver(res lnwallet.OutgoingHtlcResolution,
broadcastHeight uint32, htlc channeldb.HTLC,
chanType channeldb.ChannelType,
resCfg ResolverConfig) *htlcOutgoingContestResolver {

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

return &htlcOutgoingContestResolver{
Expand Down
46 changes: 42 additions & 4 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
Comment thread
yyforyongyu marked this conversation as resolved.

// 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,15 @@ type htlcSuccessResolver struct {
// newSuccessResolver instanties a new htlc success resolver.
func newSuccessResolver(res lnwallet.IncomingHtlcResolution,
broadcastHeight uint32, htlc channeldb.HTLC,
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 @@ -373,6 +378,17 @@ func (h *htlcSuccessResolver) HtlcPoint() wire.OutPoint {
return h.htlcResolution.HtlcPoint()
}

// SupplementState allows the user of a ContractResolver to supplement it with
// state required for the proper resolution of a contract. This restores the
// channel type which is needed to select the correct witness type for
// production taproot channels after restart.
//
// NOTE: Part of the ContractResolver interface.
func (h *htlcSuccessResolver) SupplementState(state *channeldb.OpenChannel) {
h.htlcLeaseResolver.SupplementState(state)
h.chanType = state.ChanType
}

// SupplementDeadline does nothing for an incoming htlc resolver.
//
// NOTE: Part of the htlcContractResolver interface.
Expand Down Expand Up @@ -409,6 +425,12 @@ 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 +439,19 @@ func (h *htlcSuccessResolver) sweepRemoteCommitOutput() error {
// sweeping transaction, and generate a witness.
var inp input.Input

if h.isTaproot() {
switch {
case 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,
),
))
case h.isTaproot():
inp = lnutils.Ptr(input.MakeTaprootHtlcSucceedInput(
&h.htlcResolution.ClaimOutpoint,
&h.htlcResolution.SweepSignDesc,
Expand All @@ -428,7 +462,7 @@ func (h *htlcSuccessResolver) sweepRemoteCommitOutput() error {
h.htlcResolution.ResolutionBlob,
),
))
} else {
default:
inp = lnutils.Ptr(input.MakeHtlcSucceedInput(
&h.htlcResolution.ClaimOutpoint,
&h.htlcResolution.SweepSignDesc,
Expand Down Expand Up @@ -562,9 +596,12 @@ 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() {
switch {
case h.isTaprootFinal():
witType = input.TaprootHtlcAcceptedSuccessSecondLevelFinal
case h.isTaproot():
witType = input.TaprootHtlcAcceptedSuccessSecondLevel
} else {
default:
witType = input.HtlcAcceptedSuccessSecondLevel
}
inp := h.makeSweepInput(
Expand Down Expand Up @@ -634,6 +671,7 @@ func (h *htlcSuccessResolver) resolveLegacySuccessTx() error {
h.ChanPoint, fn.None[lnwallet.OutgoingHtlcResolution](),
fn.Some(h.htlcResolution),
h.broadcastHeight, fn.Some(int32(h.htlc.RefundTimeout)),
WithChanType(h.chanType),
)
if err != nil {
return err
Expand Down
Loading
Loading