From b509aa3d5f546acf305fe2ab713edd5480e385e9 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 16:59:49 -0800 Subject: [PATCH 1/9] channeldb: add new db type for taproot channels --- channeldb/channel.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/channeldb/channel.go b/channeldb/channel.go index d71f17047db..77840070ffc 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -295,6 +295,10 @@ const ( // ScidAliasFeatureBit indicates that the scid-alias feature bit was // negotiated during the lifetime of this channel. ScidAliasFeatureBit ChannelType = 1 << 9 + + // SimpleTaprootFeatureBit indicates that the simple-taproot-channels + // feature bit was negotiated during the lifetime of the channel. + SimpleTaprootFeatureBit ChannelType = 1 << 10 ) // IsSingleFunder returns true if the channel type if one of the known single @@ -360,6 +364,11 @@ func (c ChannelType) HasScidAliasFeature() bool { return c&ScidAliasFeatureBit == ScidAliasFeatureBit } +// IsTaproot returns true if the channel is using taproot features. +func (c ChannelType) IsTaproot() bool { + return c&SimpleTaprootFeatureBit == SimpleTaprootFeatureBit +} + // ChannelConstraints represents a set of constraints meant to allow a node to // limit their exposure, enact flow control and ensure that all HTLCs are // economically relevant. This struct will be mirrored for both sides of the From 83af2788b825a3fee67827fbe1c0cdfaa73b2136 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 17:02:32 -0800 Subject: [PATCH 2/9] lnwallet: add new CommitmentTypeSimpleTaproot chan type In this commit, we add a new wallet level channel type, along with the new fields we'll need to accept from both parties within the contribution messages. In this case, we now have a local nonce, along with the internal musig session. --- lnwallet/reservation.go | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 90a1201cf2d..373bff4c360 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -42,6 +43,11 @@ const ( // guarantee that the channel initiator has no incentives to close a // leased channel before its maturity date. CommitmentTypeScriptEnforcedLease + + // CommitmentTypeSimpleTaproot is the base commitment type for the + // channels that use a musig2 funding output and the tapscript tree + // where relevant for the commitment transaciton pk scripts. + CommitmentTypeSimpleTaproot ) // HasStaticRemoteKey returns whether the commitment type supports remote @@ -50,7 +56,8 @@ func (c CommitmentType) HasStaticRemoteKey() bool { switch c { case CommitmentTypeTweakless, CommitmentTypeAnchorsZeroFeeHtlcTx, - CommitmentTypeScriptEnforcedLease: + CommitmentTypeScriptEnforcedLease, + CommitmentTypeSimpleTaproot: return true default: return false @@ -61,13 +68,19 @@ func (c CommitmentType) HasStaticRemoteKey() bool { func (c CommitmentType) HasAnchors() bool { switch c { case CommitmentTypeAnchorsZeroFeeHtlcTx, - CommitmentTypeScriptEnforcedLease: + CommitmentTypeScriptEnforcedLease, + CommitmentTypeSimpleTaproot: return true default: return false } } +// IsTaproot returns true if the channel type is a taproot channel. +func (c CommitmentType) IsTaproot() bool { + return c == CommitmentTypeSimpleTaproot +} + // String returns the name of the CommitmentType. func (c CommitmentType) String() string { switch c { @@ -79,6 +92,8 @@ func (c CommitmentType) String() string { return "anchors-zero-fee-second-level" case CommitmentTypeScriptEnforcedLease: return "script-enforced-lease" + case CommitmentTypeSimpleTaproot: + return "simple-taproot" default: return "invalid" } @@ -117,6 +132,11 @@ type ChannelContribution struct { // UpfrontShutdown is an optional address to which the channel should be // paid out to on cooperative close. UpfrontShutdown lnwire.DeliveryAddress + + // LocalNonce is populated if the channel type is a simple taproot + // channel. This stores the public (and secret) nonce that will be used + // to generate commitments for the local party. + LocalNonce *musig2.Nonces } // toChanConfig returns the raw channel configuration generated by a node's @@ -200,6 +220,8 @@ type ChannelReservation struct { // nextRevocationKeyLoc stores the key locator information for this // channel. nextRevocationKeyLoc keychain.KeyLocator + + musigSessions *MusigPairSession } // NewChannelReservation creates a new channel reservation. This function is @@ -372,6 +394,10 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, chanType |= channeldb.FrozenBit } + if req.CommitType == CommitmentTypeSimpleTaproot { + chanType |= channeldb.SimpleTaprootFeatureBit + } + if req.ZeroConf { chanType |= channeldb.ZeroConfBit } @@ -454,6 +480,15 @@ func (r *ChannelReservation) IsZeroConf() bool { return r.partialState.IsZeroConf() } +// IsTaproot returns if the reservation's underlying partial channel state is a +// taproot channel. +func (r *ChannelReservation) IsTaproot() bool { + r.RLock() + defer r.RUnlock() + + return r.partialState.ChanType.IsTaproot() +} + // CommitConstraints takes the constraints that the remote party specifies for // the type of commitments that we can generate for them. These constraints // include several parameters that serve as flow control restricting the amount From 4cd3eec0e2fee477b283ca0bc1dc94a983a9de9e Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 17:05:39 -0800 Subject: [PATCH 3/9] lnwallet: update CommitScriptToSelf to add taproot awareness --- lnwallet/commitment.go | 78 ++++++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index c4f4d931a40..fcf68ec35cf 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -202,36 +202,76 @@ func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool, selfKey, revokeKey *btcec.PublicKey, csvDelay, leaseExpiry uint32) ( *ScriptInfo, error) { - var ( - toLocalRedeemScript []byte - err error - ) switch { + // For taproot scripts, we'll need to make a slightly modified script + // where a NUMS key is used to force a script path reveal of either the + // revocation or the CSV timeout. + // + // Our "redeem" script here is just the taproot witness program. + case chanType.IsTaproot(): + toLocalOutputKey, err := input.TaprootCommitScriptToSelf( + csvDelay, selfKey, revokeKey, + ) + if err != nil { + return nil, fmt.Errorf("unable to generate taproot "+ + "key: %w", err) + } + + toLocalPkScript, err := input.PayToTaprootScript( + toLocalOutputKey, + ) + if err != nil { + return nil, fmt.Errorf("unable to gen taproot "+ + "pkscript: %w", err) + } + + return &ScriptInfo{ + PkScript: toLocalPkScript, + }, nil + // If we are the initiator of a leased channel, then we have an - // additional CLTV requirement in addition to the usual CSV requirement. + // additional CLTV requirement in addition to the usual CSV + // requirement. case initiator && chanType.HasLeaseExpiration(): - toLocalRedeemScript, err = input.LeaseCommitScriptToSelf( + toLocalRedeemScript, err := input.LeaseCommitScriptToSelf( selfKey, revokeKey, csvDelay, leaseExpiry, ) + if err != nil { + return nil, err + } + + toLocalScriptHash, err := input.WitnessScriptHash( + toLocalRedeemScript, + ) + if err != nil { + return nil, err + } + + return &ScriptInfo{ + PkScript: toLocalScriptHash, + WitnessScript: toLocalRedeemScript, + }, nil default: - toLocalRedeemScript, err = input.CommitScriptToSelf( + toLocalRedeemScript, err := input.CommitScriptToSelf( csvDelay, selfKey, revokeKey, ) - } - if err != nil { - return nil, err - } + if err != nil { + return nil, err + } - toLocalScriptHash, err := input.WitnessScriptHash(toLocalRedeemScript) - if err != nil { - return nil, err - } + toLocalScriptHash, err := input.WitnessScriptHash( + toLocalRedeemScript, + ) + if err != nil { + return nil, err + } - return &ScriptInfo{ - PkScript: toLocalScriptHash, - WitnessScript: toLocalRedeemScript, - }, nil + return &ScriptInfo{ + PkScript: toLocalScriptHash, + WitnessScript: toLocalRedeemScript, + }, nil + } } // CommitScriptToRemote derives the appropriate to_remote script based on the From 96c48ba9e4a26efe11c2bd3ed3d0fb98470597a2 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 17:05:58 -0800 Subject: [PATCH 4/9] lnwallet: update CommitScriptToRemote to add taproot awareness --- lnwallet/commitment.go | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index fcf68ec35cf..5ae50758904 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -280,7 +280,8 @@ func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool, // script for. The second return value is the CSV delay of the output script, // what must be satisfied in order to spend the output. func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, - key *btcec.PublicKey, leaseExpiry uint32) (*ScriptInfo, uint32, error) { + remoteKey *btcec.PublicKey, + leaseExpiry uint32) (*ScriptInfo, uint32, error) { switch { // If we are not the initiator of a leased channel, then the remote @@ -288,7 +289,7 @@ func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, // CSV requirement. case chanType.HasLeaseExpiration() && !initiator: script, err := input.LeaseCommitScriptToRemoteConfirmed( - key, leaseExpiry, + remoteKey, leaseExpiry, ) if err != nil { return nil, 0, err @@ -304,10 +305,30 @@ func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, WitnessScript: script, }, 1, nil + // For taproot channels, we'll use a slightly different format, where + // 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(): + toRemoteKey, err := input.TaprootCommitScriptToRemote( + remoteKey, + ) + if err != nil { + return nil, 0, err + } + + toRemotePkScript, err := input.PayToTaprootScript(toRemoteKey) + if err != nil { + return nil, 0, err + } + + return &ScriptInfo{ + PkScript: toRemotePkScript, + }, 1, nil + // If this channel type has anchors, we derive the delayed to_remote // script. case chanType.HasAnchors(): - script, err := input.CommitScriptToRemoteConfirmed(key) + script, err := input.CommitScriptToRemoteConfirmed(remoteKey) if err != nil { return nil, 0, err } @@ -324,7 +345,7 @@ func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, default: // Otherwise the to_remote will be a simple p2wkh. - p2wkh, err := input.CommitScriptUnencumbered(key) + p2wkh, err := input.CommitScriptUnencumbered(remoteKey) if err != nil { return nil, 0, err } @@ -458,9 +479,12 @@ func HtlcSuccessFee(chanType channeldb.ChannelType, return 0 } + // TODO(roasbeef): fee is still off here? + if chanType.HasAnchors() { return feePerKw.FeeForWeight(input.HtlcSuccessWeightConfirmed) } + return feePerKw.FeeForWeight(input.HtlcSuccessWeight) } From 7225071a91a095bdfa3679e94f13c0a032a462e8 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 17:06:38 -0800 Subject: [PATCH 5/9] lnwallet: update CommitScriptAnchors to add taproot awareness We also update some of the resolutions (even though they aren't hooked up yet), as they need to be able to properly re-create the set of scripts. --- contractcourt/chain_watcher.go | 2 +- lnwallet/channel.go | 43 +++++++++++++---- lnwallet/commitment.go | 88 +++++++++++++++++++++++++++------- 3 files changed, 106 insertions(+), 27 deletions(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 99472e0c1fc..7fd45e1c244 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -834,7 +834,7 @@ func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail, // Create an AnchorResolution for the breached state. anchorRes, err := lnwallet.NewAnchorResolution( - c.cfg.chanState, commitSpend.SpendingTx, + c.cfg.chanState, commitSpend.SpendingTx, nil, ) if err != nil { return false, fmt.Errorf("unable to create anchor "+ diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 6efd0b62e27..9dfb6db6ac3 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -5945,7 +5945,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si } anchorResolution, err := NewAnchorResolution( - chanState, commitTxBroadcast, + chanState, commitTxBroadcast, keyRing, ) if err != nil { return nil, err @@ -6642,7 +6642,7 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, } anchorResolution, err := NewAnchorResolution( - chanState, commitTx, + chanState, commitTx, keyRing, ) if err != nil { return nil, err @@ -6826,7 +6826,7 @@ type AnchorResolutions struct { // NewAnchorResolutions returns a set of anchor resolutions wrapped in the // struct AnchorResolutions. Because we have no view on the mempool, we can -// only blindly anchor all of these txes down. Caller needs to check the +// only blindly anchor all of these txes down. The caller needs to check the // returned values against nil to decide whether there exists an anchor // resolution for local/remote/pending remote commitment txes. func (lc *LightningChannel) NewAnchorResolutions() (*AnchorResolutions, @@ -6835,11 +6835,23 @@ func (lc *LightningChannel) NewAnchorResolutions() (*AnchorResolutions, lc.Lock() defer lc.Unlock() - resolutions := &AnchorResolutions{} + var resolutions AnchorResolutions // Add anchor for local commitment tx, if any. + revocation, err := lc.channelState.RevocationProducer.AtIndex( + lc.currentHeight, + ) + if err != nil { + return nil, err + } + localCommitPoint := input.ComputeCommitmentPoint(revocation[:]) + localKeyRing := DeriveCommitmentKeys( + localCommitPoint, true, lc.channelState.ChanType, + &lc.channelState.LocalChanCfg, &lc.channelState.RemoteChanCfg, + ) localRes, err := NewAnchorResolution( lc.channelState, lc.channelState.LocalCommitment.CommitTx, + localKeyRing, ) if err != nil { return nil, err @@ -6847,8 +6859,14 @@ func (lc *LightningChannel) NewAnchorResolutions() (*AnchorResolutions, resolutions.Local = localRes // Add anchor for remote commitment tx, if any. + remoteKeyRing := DeriveCommitmentKeys( + lc.channelState.RemoteCurrentRevocation, false, + lc.channelState.ChanType, &lc.channelState.LocalChanCfg, + &lc.channelState.RemoteChanCfg, + ) remoteRes, err := NewAnchorResolution( lc.channelState, lc.channelState.RemoteCommitment.CommitTx, + remoteKeyRing, ) if err != nil { return nil, err @@ -6862,9 +6880,15 @@ func (lc *LightningChannel) NewAnchorResolutions() (*AnchorResolutions, } if remotePendingCommit != nil { + pendingRemoteKeyRing := DeriveCommitmentKeys( + lc.channelState.RemoteNextRevocation, false, + lc.channelState.ChanType, &lc.channelState.LocalChanCfg, + &lc.channelState.RemoteChanCfg, + ) remotePendingRes, err := NewAnchorResolution( lc.channelState, remotePendingCommit.Commitment.CommitTx, + pendingRemoteKeyRing, ) if err != nil { return nil, err @@ -6872,13 +6896,14 @@ func (lc *LightningChannel) NewAnchorResolutions() (*AnchorResolutions, resolutions.RemotePending = remotePendingRes } - return resolutions, nil + return &resolutions, nil } // NewAnchorResolution returns the information that is required to sweep the // local anchor. func NewAnchorResolution(chanState *channeldb.OpenChannel, - commitTx *wire.MsgTx) (*AnchorResolution, error) { + commitTx *wire.MsgTx, + keyRing *CommitmentKeyRing) (*AnchorResolution, error) { // Return nil resolution if the channel has no anchors. if !chanState.ChanType.HasAnchors() { @@ -6887,7 +6912,8 @@ func NewAnchorResolution(chanState *channeldb.OpenChannel, // Derive our local anchor script. localAnchor, _, err := CommitScriptAnchors( - &chanState.LocalChanCfg, &chanState.RemoteChanCfg, + chanState.ChanType, &chanState.LocalChanCfg, + &chanState.RemoteChanCfg, keyRing, ) if err != nil { return nil, err @@ -7239,7 +7265,8 @@ func (lc *LightningChannel) generateRevocation(height uint64) (*lnwire.RevokeAnd revocationMsg.NextRevocationKey = input.ComputeCommitmentPoint(nextCommitSecret[:]) revocationMsg.ChanID = lnwire.NewChanIDFromOutPoint( - &lc.channelState.FundingOutpoint) + &lc.channelState.FundingOutpoint, + ) return revocationMsg, nil } diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 5ae50758904..996e3486de0 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -490,37 +490,89 @@ func HtlcSuccessFee(chanType channeldb.ChannelType, // CommitScriptAnchors return the scripts to use for the local and remote // anchor. -func CommitScriptAnchors(localChanCfg, - remoteChanCfg *channeldb.ChannelConfig) (*ScriptInfo, - *ScriptInfo, error) { +func CommitScriptAnchors(chanType channeldb.ChannelType, + localChanCfg, remoteChanCfg *channeldb.ChannelConfig, + keyRing *CommitmentKeyRing) (*ScriptInfo, *ScriptInfo, error) { - // Helper to create anchor ScriptInfo from key. - anchorScript := func(key *btcec.PublicKey) (*ScriptInfo, error) { - script, err := input.CommitScriptAnchor(key) - if err != nil { - return nil, err + var ( + anchorScript func(key *btcec.PublicKey) (*ScriptInfo, error) + keySelector func(*channeldb.ChannelConfig, + bool) *btcec.PublicKey + ) + + switch { + // For taproot channels, the anchor is slightly different: the top + // level key is now the (relative) local delay and remote public key, + // since these are fully revealed once the commitment hits the chain. + case chanType.IsTaproot(): + anchorScript = func(key *btcec.PublicKey) (*ScriptInfo, error) { + anchorKey, err := input.TaprootOutputKeyAnchor(key) + if err != nil { + return nil, err + } + + anchorPkScript, err := input.PayToTaprootScript( + anchorKey, + ) + if err != nil { + return nil, err + } + + return &ScriptInfo{ + PkScript: anchorPkScript, + }, nil } - scriptHash, err := input.WitnessScriptHash(script) - if err != nil { - return nil, err + keySelector = func(cfg *channeldb.ChannelConfig, + local bool) *btcec.PublicKey { + + if local { + return keyRing.ToLocalKey + } + + return keyRing.ToRemoteKey } - return &ScriptInfo{ - PkScript: scriptHash, - WitnessScript: script, - }, nil + // For normal channels we'll use the multi-sig keys since those are + // revealed when the channel closes + default: + // For normal channels, we'll create a p2wsh script based on + // the target key. + anchorScript = func(key *btcec.PublicKey) (*ScriptInfo, error) { + script, err := input.CommitScriptAnchor(key) + if err != nil { + return nil, err + } + + scriptHash, err := input.WitnessScriptHash(script) + if err != nil { + return nil, err + } + + return &ScriptInfo{ + PkScript: scriptHash, + WitnessScript: script, + }, nil + } + + // For the existing channels, we'll always select the multi-sig + // key from the party's channel config. + keySelector = func(cfg *channeldb.ChannelConfig, + _ bool) *btcec.PublicKey { + + return cfg.MultiSigKey.PubKey + } } // Get the script used for the anchor output spendable by the local // node. - localAnchor, err := anchorScript(localChanCfg.MultiSigKey.PubKey) + localAnchor, err := anchorScript(keySelector(localChanCfg, true)) if err != nil { return nil, nil, err } // And the anchor spendable by the remote node. - remoteAnchor, err := anchorScript(remoteChanCfg.MultiSigKey.PubKey) + remoteAnchor, err := anchorScript(keySelector(remoteChanCfg, false)) if err != nil { return nil, nil, err } @@ -848,7 +900,7 @@ func CreateCommitTx(chanType channeldb.ChannelType, // If this channel type has anchors, we'll also add those. if chanType.HasAnchors() { localAnchor, remoteAnchor, err := CommitScriptAnchors( - localChanCfg, remoteChanCfg, + chanType, localChanCfg, remoteChanCfg, keyRing, ) if err != nil { return nil, err From 88d1384cd688d357c1d69374f4da9f7ce245d63f Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 17:08:39 -0800 Subject: [PATCH 6/9] lnwallet: set new musig2 bool for chanfunding.Request in handleFundingReserveRequest --- lnwallet/wallet.go | 1 + 1 file changed, 1 insertion(+) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index fec421cc02b..1edc286d267 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -851,6 +851,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg TaprootPubkey, true, DefaultAccountName, ) }, + Musig2: req.CommitType == CommitmentTypeSimpleTaproot, } fundingIntent, err = req.ChanFunder.ProvisionChannel( fundingReq, From 08324692c72c4ee4c05b42bd7bea06159f6b1cb1 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 17:15:12 -0800 Subject: [PATCH 7/9] lnwallet: integrate new taproot channels into internal funding flow In this commit, we build on all the prior commits and integrate the new taproot channels into the existing internal funding flow. Along the way, we do some refactoring to unify things like signing and verifying incoming commitment transaction signatures. For our local nonce, we use the existing functional option type to derive the nonce based on the initial shachain pre-image we'll use as our revocation. --- lnwallet/channel.go | 1 - lnwallet/musig_session.go | 27 +++ lnwallet/reservation.go | 2 +- lnwallet/revocation_producer.go | 22 +- lnwallet/revocation_producer_itest.go | 29 ++- lnwallet/wallet.go | 334 +++++++++++++++++++------- 6 files changed, 311 insertions(+), 104 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 9dfb6db6ac3..e63fdce4c20 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2588,7 +2588,6 @@ func createBreachRetribution(revokedLog *channeldb.RevocationLog, // Read the amounts from the breach transaction. theirAmt = spendTx.TxOut[theirOutpoint.Index].Value - } else { // Otherwise, we check to see if the revocation log // contains remote parties' output amount. Due to a diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go index 7683eca962e..37f4d98c3e4 100644 --- a/lnwallet/musig_session.go +++ b/lnwallet/musig_session.go @@ -2,12 +2,15 @@ package lnwallet import ( "bytes" + "crypto/hmac" + "crypto/sha256" "fmt" "io" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/input" @@ -545,3 +548,27 @@ func NewMusigPairSession(cfg *MusigSessionCfg) *MusigPairSession { signer: cfg.Signer, } } + +var ( + // taprootRevRootKey is the key used to derive the revocation root for + // the taproot nonces. This is done via HMAC of the existing revocation + // root. + taprootRevRootKey = []byte("taproot-rev-root") +) + +// deriveMusig2Shachain derives a shachain producer for the taproot channel +// from normal shachain revocation root. +func deriveMusig2Shachain(revRoot chainhash.Hash) (shachain.Producer, error) { + // For taproot channel types, we'll also generate a distinct shachain + // root using the same seed information. We'll use this to generate + // verification nonces for the channel. We'll bind with this a simple + // hmac. + taprootRevHmac := hmac.New(sha256.New, taprootRevRootKey) + taprootRevRoot := taprootRevHmac.Sum(nil) + + // Once we have the root, we can then generate our shachain producer + // and from that generate the per-commitment point. + return shachain.NewRevocationProducerFromBytes( + taprootRevRoot, + ) +} diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 373bff4c360..7bb864a9ced 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -46,7 +46,7 @@ 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 transaciton pk scripts. + // where relevant for the commitment transaction pk scripts. CommitmentTypeSimpleTaproot ) diff --git a/lnwallet/revocation_producer.go b/lnwallet/revocation_producer.go index 413e33d0af9..5945c2fafc1 100644 --- a/lnwallet/revocation_producer.go +++ b/lnwallet/revocation_producer.go @@ -8,17 +8,19 @@ import ( ) // nextRevocationProducer creates a new revocation producer, deriving the -// revocation root by applying ECDH to a new key from our revocation root family -// and the multisig key we use for the channel. +// revocation root by applying ECDH to a new key from our revocation root +// family and the multisig key we use for the channel. For taproot channels a +// related shachain revocation root is also returned. func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, - keyRing keychain.KeyRing) (shachain.Producer, error) { + keyRing keychain.KeyRing, +) (shachain.Producer, shachain.Producer, error) { // Derive the next key in the revocation root family. nextRevocationKeyDesc, err := keyRing.DeriveNextKey( keychain.KeyFamilyRevocationRoot, ) if err != nil { - return nil, err + return nil, nil, err } // If the DeriveNextKey call returns the first key with Index 0, we need @@ -29,7 +31,7 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, keychain.KeyFamilyRevocationRoot, ) if err != nil { - return nil, err + return nil, nil, err } } @@ -42,10 +44,16 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, nextRevocationKeyDesc, res.ourContribution.MultiSigKey.PubKey, ) if err != nil { - return nil, err + return nil, nil, err } // Once we have the root, we can then generate our shachain producer // and from that generate the per-commitment point. - return shachain.NewRevocationProducer(revRoot), nil + shaChainRoot := shachain.NewRevocationProducer(revRoot) + taprootShaChainRoot, err := deriveMusig2Shachain(revRoot) + if err != nil { + return nil, nil, err + } + + return shaChainRoot, taprootShaChainRoot, nil } diff --git a/lnwallet/revocation_producer_itest.go b/lnwallet/revocation_producer_itest.go index d4b04152b56..82459717669 100644 --- a/lnwallet/revocation_producer_itest.go +++ b/lnwallet/revocation_producer_itest.go @@ -12,14 +12,15 @@ import ( // revocation root by applying ECDH to a new key from our revocation root family // and the multisig key we use for the channel. func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, - keyRing keychain.KeyRing) (shachain.Producer, error) { + keyRing keychain.KeyRing, +) (shachain.Producer, shachain.Producer, error) { // Derive the next key in the revocation root family. nextRevocationKeyDesc, err := keyRing.DeriveNextKey( keychain.KeyFamilyRevocationRoot, ) if err != nil { - return nil, err + return nil, nil, err } // Within our itests, we want to make sure we can still restore channel @@ -34,17 +35,23 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, if res.pendingChanID == itestLegacyFormatChanID { revocationRoot, err := l.DerivePrivKey(nextRevocationKeyDesc) if err != nil { - return nil, err + return nil, nil, err } // Once we have the root, we can then generate our shachain // producer and from that generate the per-commitment point. revRoot, err := chainhash.NewHash(revocationRoot.Serialize()) if err != nil { - return nil, err + return nil, nil, err } + shaChainRoot := shachain.NewRevocationProducer(*revRoot) - return shachain.NewRevocationProducer(*revRoot), nil + taprootShaChainRoot, err := deriveMusig2Shachain(*revRoot) + if err != nil { + return nil, nil, err + } + + return shaChainRoot, taprootShaChainRoot, nil } // If the DeriveNextKey call returns the first key with Index 0, we need @@ -55,7 +62,7 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, keychain.KeyFamilyRevocationRoot, ) if err != nil { - return nil, err + return nil, nil, err } } @@ -68,10 +75,16 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation, nextRevocationKeyDesc, res.ourContribution.MultiSigKey.PubKey, ) if err != nil { - return nil, err + return nil, nil, err } // Once we have the root, we can then generate our shachain producer // and from that generate the per-commitment point. - return shachain.NewRevocationProducer(revRoot), nil + shaChainRoot := shachain.NewRevocationProducer(revRoot) + taprootShaChainRoot, err := deriveMusig2Shachain(revRoot) + if err != nil { + return nil, nil, err + } + + return shaChainRoot, taprootShaChainRoot, nil } diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 1edc286d267..4f8f23fa70b 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/btcutil/txsort" @@ -1270,8 +1271,11 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation, } // With the above keys created, we'll also need to initialize our - // revocation tree state, and from that generate the per-commitment point. - producer, err := l.nextRevocationProducer(reservation, keyRing) + // revocation tree state, and from that generate the per-commitment + // point. + producer, taprootNonceProducer, err := l.nextRevocationProducer( + reservation, keyRing, + ) if err != nil { return err } @@ -1287,6 +1291,34 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation, reservation.partialState.RevocationProducer = producer reservation.ourContribution.ChannelConstraints = l.Cfg.DefaultConstraints + // If taproot channels are active, then we'll generate our verification + // nonce here. We'll use this nonce to verify the signature for our + // local commitment transaction. If we need to force close, then this + // is also what'll be used to sign that transaction. + if reservation.partialState.ChanType.IsTaproot() { + firstNoncePreimage, err := taprootNonceProducer.AtIndex(0) + if err != nil { + return err + } + + // As we'd like the local nonce we send over to be generated + // deterministically, we'll provide a custom reader that + // actually just uses our sha-chain pre-image as the primary + // randomness source. + shaChainRand := musig2.WithCustomRand( + bytes.NewBuffer(firstNoncePreimage[:]), + ) + pubKeyOpt := musig2.WithPublicKey( + reservation.ourContribution.MultiSigKey.PubKey, + ) + reservation.ourContribution.LocalNonce, err = musig2.GenNonces( + pubKeyOpt, shaChainRand, + ) + if err != nil { + return err + } + } + return nil } @@ -1543,8 +1575,94 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { }) } -// handleChanPointReady continues the funding process once the channel point -// is known and the funding transaction can be completed. +// genMusigSession generates a new musig2 pair session that we can use to sign +// the commitment transaction for the remote party, and verify their incoming +// partial signature. +func genMusigSession(ourContribution, theirContribution *ChannelContribution, + signer input.MuSig2Signer, + fundingOutput *wire.TxOut) *MusigPairSession { + + return NewMusigPairSession(&MusigSessionCfg{ + LocalKey: ourContribution.MultiSigKey, + RemoteKey: theirContribution.MultiSigKey, + LocalNonce: *ourContribution.LocalNonce, + RemoteNonce: *theirContribution.LocalNonce, + Signer: signer, + InputTxOut: fundingOutput, + }) +} + +// signCommitTx generates a valid input.Signature to send to the remote party +// for their version of the commitment transaction. For regular channels, this +// will be a normal ECDSA signature. For taproot channels, this will instead be +// a musig2 partial signature that also includes the nonce used to generate it. +func (l *LightningWallet) signCommitTx(pendingReservation *ChannelReservation, + commitTx *wire.MsgTx, fundingOutput *wire.TxOut, + fundingWitnessScript []byte) (input.Signature, error) { + + ourContribution := pendingReservation.ourContribution + theirContribution := pendingReservation.theirContribution + + var ( + sigTheirCommit input.Signature + err error + ) + switch { + // For regular channels, we can just send over a normal ECDSA signature + // w/o any extra steps. + case !pendingReservation.partialState.ChanType.IsTaproot(): + ourKey := ourContribution.MultiSigKey + signDesc := input.SignDescriptor{ + WitnessScript: fundingWitnessScript, + KeyDesc: ourKey, + Output: fundingOutput, + HashType: txscript.SigHashAll, + SigHashes: input.NewTxSigHashesV0Only( + commitTx, + ), + InputIndex: 0, + } + sigTheirCommit, err = l.Cfg.Signer.SignOutputRaw( + commitTx, &signDesc, + ) + if err != nil { + return nil, err + } + + // If this is a taproot channel, then we'll need to create an initial + // musig2 session here as we'll be sending over a _partial_ signature. + default: + // We're now ready to sign the first commitment. However, we'll + // only create the session if that hasn't been done already. + if pendingReservation.musigSessions == nil { + musigSessions := genMusigSession( + ourContribution, theirContribution, + l.Cfg.Signer, fundingOutput, + ) + pendingReservation.musigSessions = musigSessions + } + + // Now that we have the funding outpoint, we'll generate a + // musig2 signature for their version of the commitment + // transaction. We use the remote session as this is for the + // remote commitment transaction. + musigSessions := pendingReservation.musigSessions + partialSig, err := musigSessions.RemoteSession.SignCommit( + commitTx, + ) + if err != nil { + return nil, fmt.Errorf("unable to sign "+ + "commitment: %w", err) + } + + sigTheirCommit = partialSig + } + + return sigTheirCommit, nil +} + +// handleChanPointReady continues the funding process once the channel point is +// known and the funding transaction can be completed. func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { l.limboMtx.Lock() pendingReservation, ok := l.fundingLimbo[req.pendingFundingID] @@ -1554,6 +1672,7 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { "funding state") return } + ourContribution := pendingReservation.ourContribution theirContribution := pendingReservation.theirContribution chanPoint := pendingReservation.partialState.FundingOutpoint @@ -1694,26 +1813,22 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { fundingIntent := pendingReservation.fundingIntent fundingWitnessScript, fundingOutput, err := fundingIntent.FundingOutput() if err != nil { - req.err <- fmt.Errorf("unable to obtain funding output") + req.err <- fmt.Errorf("unable to obtain funding "+ + "output: %w", err) return } // Generate a signature for their version of the initial commitment // transaction. - ourKey := ourContribution.MultiSigKey - signDesc := input.SignDescriptor{ - WitnessScript: fundingWitnessScript, - KeyDesc: ourKey, - Output: fundingOutput, - HashType: txscript.SigHashAll, - SigHashes: input.NewTxSigHashesV0Only(theirCommitTx), - InputIndex: 0, - } - sigTheirCommit, err := l.Cfg.Signer.SignOutputRaw(theirCommitTx, &signDesc) + sigTheirCommit, err := l.signCommitTx( + pendingReservation, theirCommitTx, fundingOutput, + fundingWitnessScript, + ) if err != nil { req.err <- err return } + pendingReservation.ourCommitmentSig = sigTheirCommit req.err <- nil @@ -1838,6 +1953,89 @@ func (l *LightningWallet) verifyFundingInputs(fundingTx *wire.MsgTx, return nil } +// verifyCommitSig verifies an incoming signature for our version of the +// commitment transaction. For normal channels, this will verify that the ECDSA +// signature is valid. For taproot channels, we'll verify that their partial +// signature is valid, so it can properly be combined with our eventual +// signature when we need to broadcast. +func (l *LightningWallet) verifyCommitSig(res *ChannelReservation, + commitSig input.Signature, commitTx *wire.MsgTx) error { + + localKey := res.ourContribution.MultiSigKey.PubKey + remoteKey := res.theirContribution.MultiSigKey.PubKey + channelValue := int64(res.partialState.Capacity) + + switch { + // If this isn't a taproot channel, then we'll construct a segwit v0 + // p2wsh sighash. + case !res.partialState.ChanType.IsTaproot(): + hashCache := input.NewTxSigHashesV0Only(commitTx) + witnessScript, _, err := input.GenFundingPkScript( + localKey.SerializeCompressed(), + remoteKey.SerializeCompressed(), channelValue, + ) + if err != nil { + return err + } + + sigHash, err := txscript.CalcWitnessSigHash( + witnessScript, hashCache, txscript.SigHashAll, + commitTx, 0, channelValue, + ) + if err != nil { + return err + } + + // Verify that we've received a valid signature from the remote + // party for our version of the commitment transaction. + if !commitSig.Verify(sigHash, remoteKey) { + return fmt.Errorf("counterparty's commitment " + + "signature is invalid") + } + + return nil + + // Otherwise for taproot channels, we'll compute the segwit v1 sighash, + // which is slightly different. + default: + // First, check to see if we've generated the musig session + // already. If we're the responder in the funding flow, we may + // not have generated it already. + if res.musigSessions == nil { + _, fundingOutput, err := input.GenTaprootFundingScript( + localKey, remoteKey, channelValue, + ) + if err != nil { + return err + } + + res.musigSessions = genMusigSession( + res.ourContribution, res.theirContribution, + l.Cfg.Signer, fundingOutput, + ) + } + + // For the musig2 based channels, we'll use the generated local + // musig2 session to verify the signature. + localSession := res.musigSessions.LocalSession + + // At this point, the commitment signature passed in should + // actually be a wrapped musig2 signature, so we'll do a type + // asset to the get the signature we actually need. + partialSig, ok := commitSig.(*MusigPartialSig) + if !ok { + return fmt.Errorf("expected *musig2.PartialSignature, "+ + "got: %T", commitSig) + } + + _, err := localSession.VerifyCommitSig( + commitTx, partialSig.ToWireSig(), + ) + + return err + } +} + // handleFundingCounterPartySigs is the final step in the channel reservation // workflow. During this step, we validate *all* the received signatures for // inputs to the funding transaction. If any of these are invalid, we bail, @@ -1880,44 +2078,15 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs // commitment transaction. res.theirCommitmentSig = msg.theirCommitmentSig commitTx := res.partialState.LocalCommitment.CommitTx - ourKey := res.ourContribution.MultiSigKey - theirKey := res.theirContribution.MultiSigKey - - // Re-generate both the witnessScript and p2sh output. We sign the - // witnessScript script, but include the p2sh output as the subscript - // for verification. - witnessScript, _, err := input.GenFundingPkScript( - ourKey.PubKey.SerializeCompressed(), - theirKey.PubKey.SerializeCompressed(), - int64(res.partialState.Capacity), - ) - if err != nil { - msg.err <- err - msg.completeChan <- nil - return - } - // Next, create the spending scriptSig, and then verify that the script - // is complete, allowing us to spend from the funding transaction. - channelValue := int64(res.partialState.Capacity) - hashCache := input.NewTxSigHashesV0Only(commitTx) - sigHash, err := txscript.CalcWitnessSigHash( - witnessScript, hashCache, txscript.SigHashAll, commitTx, - 0, channelValue, - ) + err := l.verifyCommitSig(res, msg.theirCommitmentSig, commitTx) if err != nil { - msg.err <- err + msg.err <- fmt.Errorf("counterparty's commitment signature is "+ + "invalid: %w", err) msg.completeChan <- nil return } - // Verify that we've received a valid signature from the remote party - // for our version of the commitment transaction. - if !msg.theirCommitmentSig.Verify(sigHash, theirKey.PubKey) { - msg.err <- fmt.Errorf("counterparty's commitment signature is invalid") - msg.completeChan <- nil - return - } theirCommitSigBytes := msg.theirCommitmentSig.Serialize() res.partialState.LocalCommitment.CommitSig = theirCommitSigBytes @@ -1996,6 +2165,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { defer pendingReservation.Unlock() chanState := pendingReservation.partialState + chanType := pendingReservation.partialState.ChanType chanState.FundingOutpoint = *req.fundingOutpoint fundingTxIn := wire.NewTxIn(req.fundingOutpoint, nil, nil) @@ -2014,7 +2184,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { pendingReservation.theirContribution.ChannelConfig, pendingReservation.ourContribution.FirstCommitmentPoint, pendingReservation.theirContribution.FirstCommitmentPoint, - *fundingTxIn, pendingReservation.partialState.ChanType, + *fundingTxIn, chanType, pendingReservation.partialState.IsInitiator, leaseExpiry, ) if err != nil { @@ -2050,13 +2220,10 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { walletLog.Debugf("Remote commit tx for ChannelPoint(%v): %v", req.fundingOutpoint, spew.Sdump(theirCommitTx)) - channelValue := int64(pendingReservation.partialState.Capacity) - hashCache := input.NewTxSigHashesV0Only(ourCommitTx) - theirKey := pendingReservation.theirContribution.MultiSigKey - ourKey := pendingReservation.ourContribution.MultiSigKey - witnessScript, _, err := input.GenFundingPkScript( - ourKey.PubKey.SerializeCompressed(), - theirKey.PubKey.SerializeCompressed(), channelValue, + // With both commitment transactions created, we'll now verify their + // signature on our commitment. + err = l.verifyCommitSig( + pendingReservation, req.theirCommitmentSig, ourCommitTx, ) if err != nil { req.err <- err @@ -2064,53 +2231,46 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { return } - sigHash, err := txscript.CalcWitnessSigHash( - witnessScript, hashCache, txscript.SigHashAll, ourCommitTx, 0, - channelValue, + theirCommitSigBytes := req.theirCommitmentSig.Serialize() + chanState.LocalCommitment.CommitSig = theirCommitSigBytes + + channelValue := int64(pendingReservation.partialState.Capacity) + theirKey := pendingReservation.theirContribution.MultiSigKey + ourKey := pendingReservation.ourContribution.MultiSigKey + + var ( + fundingWitnessScript []byte + fundingTxOut *wire.TxOut ) + if chanType.IsTaproot() { + fundingWitnessScript, fundingTxOut, err = input.GenTaprootFundingScript( //nolint:lll + ourKey.PubKey, theirKey.PubKey, channelValue, + ) + } else { + fundingWitnessScript, fundingTxOut, err = input.GenFundingPkScript( //nolint:lll + ourKey.PubKey.SerializeCompressed(), + theirKey.PubKey.SerializeCompressed(), channelValue, + ) + } if err != nil { req.err <- err req.completeChan <- nil return } - // Verify that we've received a valid signature from the remote party - // for our version of the commitment transaction. - if !req.theirCommitmentSig.Verify(sigHash, theirKey.PubKey) { - req.err <- fmt.Errorf("counterparty's commitment signature " + - "is invalid") - req.completeChan <- nil - return - } - theirCommitSigBytes := req.theirCommitmentSig.Serialize() - chanState.LocalCommitment.CommitSig = theirCommitSigBytes - // With their signature for our version of the commitment transactions // verified, we can now generate a signature for their version, // allowing the funding transaction to be safely broadcast. - p2wsh, err := input.WitnessScriptHash(witnessScript) - if err != nil { - req.err <- err - req.completeChan <- nil - return - } - signDesc := input.SignDescriptor{ - WitnessScript: witnessScript, - KeyDesc: ourKey, - Output: &wire.TxOut{ - PkScript: p2wsh, - Value: channelValue, - }, - HashType: txscript.SigHashAll, - SigHashes: input.NewTxSigHashesV0Only(theirCommitTx), - InputIndex: 0, - } - sigTheirCommit, err := l.Cfg.Signer.SignOutputRaw(theirCommitTx, &signDesc) + sigTheirCommit, err := l.signCommitTx( + pendingReservation, theirCommitTx, fundingTxOut, + fundingWitnessScript, + ) if err != nil { req.err <- err req.completeChan <- nil return } + pendingReservation.ourCommitmentSig = sigTheirCommit _, bestHeight, err := l.Cfg.ChainIO.GetBestBlock() From b4fa4330999cdb919b341eb08fe20f2437e809eb Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 19 Jan 2023 17:16:21 -0800 Subject: [PATCH 8/9] lnwallet: add test case for musig2 channel funding --- lnwallet/test/test_interface.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go index e27e12ded9d..563a975f84c 100644 --- a/lnwallet/test/test_interface.go +++ b/lnwallet/test/test_interface.go @@ -927,7 +927,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, // by having Alice immediately process his contribution. err = aliceChanReservation.ProcessContribution(bobContribution) if err != nil { - t.Fatalf("alice unable to process bob's contribution") + t.Fatalf("alice unable to process bob's contribution: %v", err) } assertContributionInitPopulated(t, bobChanReservation.TheirContribution()) @@ -2760,6 +2760,19 @@ var walletTests = []walletTestCase{ ) }, }, + { + name: "single funding workflow musig2", + test: func(miner *rpctest.Harness, alice, + bob *lnwallet.LightningWallet, t *testing.T) { + + testSingleFunderReservationWorkflow( + miner, alice, bob, t, + lnwallet.CommitmentTypeSimpleTaproot, nil, + nil, [32]byte{}, 0, + ) + }, + }, + // TODO(roasbeef): add musig2 external funding { name: "single funding workflow external funding tx", test: testSingleFunderExternalFundingTx, From 2d4df803498983a881556abd4d60b91260db2345 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 7 Jun 2023 17:20:54 -0700 Subject: [PATCH 9/9] peer: soft-disable towers with taproot channels In this commit, we modify the starting logic to note attempt to add a tower client for taproot channels. Instead, we'll just log that this isn't available yet. --- peer/brontide.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/peer/brontide.go b/peer/brontide.go index 9ea85edd149..d8ff0148142 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -956,9 +956,23 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint, // okay if the clients are disabled altogether and these values are nil, // as the link will check for nilness before using either. var towerClient htlcswitch.TowerClient - if chanType.HasAnchors() { + switch { + case chanType.IsTaproot(): + // Leave the tower client as nil for now until the tower client + // has support for taproot channels. + // + // If the user has activated the tower client, then add a log + // to explain that any taproot channel updates wil not be + // backed up to a tower. + if p.cfg.TowerClient != nil { + p.log.Debugf("Updates for channel %s will not be "+ + "backed up to a watchtower as watchtowers "+ + "are not yet taproot channel compatible", + chanPoint) + } + case chanType.HasAnchors(): towerClient = p.cfg.AnchorTowerClient - } else { + default: towerClient = p.cfg.TowerClient }