Repository: lightningnetwork/lnd
Pull request #7345 (2023-01-20 04:02:39)

[6/?] - lnwallet+chancloser: update channel state machine and co-op close for musig2 flow
## Change Description


In this PR, we update the channel state machine to be aware of the new musig2 flow. Along the way, we update some util functions to ensure we can use them to generate scripts/transactions for both segwit v0 and v1 channels. 

In this new flow, each time we send a signature, we sig a partial sig along with the nonce that we used to sign. Upon receiving that signature, the remote party will use the nonce to complete their local session (for that state).

Each time a signature is received, a party will generate another nonce that'll be used to create their next local state. This next local/verification nonce is then sent alongside the revoke_and_ack message. Upon receiving the revocation, the party that originally sent the sig, can then use the new local nonce to prep the session for the next state.

We also update the co-op close state machine to be proper taproot aware. This involves setting the new nonce and partial signatures fields in the shutdown and closing signed messages. The other big change here is that we'll short circuit most of the co-op close flow. Rather than attempt a "negotiation", we'll just have the responder accept w/e the initiator sends, as they're the ones that pay the fees in the end.

---

In the next PR, we'll hook the new reservation flow up to the funding manager, and fill out the other functionality needed for the other sub-systems to be aware of the new channel type. 

## Steps to Test
Steps for reviewers to follow to test the change.

## Pull Request Checklist
### Testing
- [ ] Your PR passes all CI checks.
- [ ] Tests covering the positive and negative (error paths) are included.
- [ ] Bug fixes contain tests triggering the bug to prevent regressions.

### Code Style and Documentation
- [ ] The change obeys the [Code Documentation and Commenting](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md#CodeDocumentation) guidelines, and lines wrap at 80.
- [ ] Commits follow the [Ideal Git Commit Structure](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md#IdealGitCommitStructure).
- [ ] Any new logging statements use an appropriate subsystem and logging level.
- [ ]  [There is a change description in the release notes](https://github.com/lightningnetwork/lnd/tree/master/docs/release-notes), or `[skip ci]` in the commit message for small changes.

📝 Please see our  [Contribution Guidelines](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md) for further guidance.

lightninglabs-deploy: @crypt-iq: review reminder
@roasbeef, remember to re-request review from reviewers when ready
lightninglabs-deploy: @crypt-iq: review reminder
@roasbeef, remember to re-request review from reviewers when ready
lightninglabs-deploy: @crypt-iq: review reminder
@ellemouton: review reminder
@roasbeef, remember to re-request review from reviewers when ready
lightninglabs-deploy: @roasbeef, remember to re-request review from reviewers when ready
lightninglabs-deploy: @crypt-iq: review reminder
@roasbeef, remember to re-request review from reviewers when ready
lightninglabs-deploy: @crypt-iq: review reminder
@roasbeef, remember to re-request review from reviewers when ready
lightninglabs-deploy: @crypt-iq: review reminder
@roasbeef, remember to re-request review from reviewers when ready
lightninglabs-deploy: @crypt-iq: review reminder
@roasbeef, remember to re-request review from reviewers when ready
lightninglabs-deploy: @roasbeef, remember to re-request review from reviewers when ready
lightninglabs-deploy: @roasbeef, remember to re-request review from reviewers when ready
lightninglabs-deploy: @roasbeef, remember to re-request review from reviewers when ready
lightninglabs-deploy: @roasbeef, remember to re-request review from reviewers when ready
lightninglabs-deploy: @roasbeef, remember to re-request review from reviewers when ready
lightninglabs-deploy: @yyforyongyu: review reminder
@crypt-iq: review reminder
@ellemouton: review reminder
@roasbeef, remember to re-request review from reviewers when ready
Roasbeef: Pushed up a new version, added tests for co-op close, and also tests for channel re-establish, PTAL!

Patch:
diff --git a/channeldb/channel.go b/channeldb/channel.go
index 77840070f..1e4341aa8 100644
--- a/channeldb/channel.go
+++ b/channeldb/channel.go
@@ -2,6 +2,7 @@ package channeldb
 
 import (
 	"bytes"
+	"crypto/hmac"
 	"crypto/sha256"
 	"encoding/binary"
 	"errors"
@@ -13,6 +14,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"
@@ -1369,6 +1371,65 @@ func (c *OpenChannel) SecondCommitmentPoint() (*btcec.PublicKey, error) {
 	return input.ComputeCommitmentPoint(revocation[:]), nil
 }
 
+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 shachain.Producer) (shachain.Producer, error) { //nolint:lll
+	// In order to obtain the revocation root hash to create the taproot
+	// revocation, we'll encode the producer into a buffer, then use that
+	// to derive the shachain root needed.
+	var rootHashBuf bytes.Buffer
+	if err := revRoot.Encode(&rootHashBuf); err != nil {
+		return nil, fmt.Errorf("unable to encode producer: %w", err)
+	}
+
+	revRootHash := chainhash.HashH(rootHashBuf.Bytes())
+
+	// 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)
+	if _, err := taprootRevHmac.Write(revRootHash[:]); err != nil {
+		return nil, err
+	}
+
+	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,
+	)
+}
+
+// NewMusigVerificationNonce generates the local or verification nonce for
+// another musig2 session. In order to permit our implementation to not have to
+// write any secret nonce state to disk, we'll use the _next_ shachain
+// pre-image as our primary randomness source. When used to generate the nonce
+// again to broadcast our commitment hte current height will be used.
+func NewMusigVerificationNonce(pubKey *btcec.PublicKey, targetHeight uint64,
+	shaGen shachain.Producer) (*musig2.Nonces, error) {
+
+	// Now that we know what height we need, we'll grab the shachain
+	// pre-image at the target destination.
+	nextPreimage, err := shaGen.AtIndex(targetHeight)
+	if err != nil {
+		return nil, err
+	}
+
+	shaChainRand := musig2.WithCustomRand(bytes.NewBuffer(nextPreimage[:]))
+	pubKeyOpt := musig2.WithPublicKey(pubKey)
+
+	return musig2.GenNonces(pubKeyOpt, shaChainRand)
+}
+
 // 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
@@ -1440,6 +1501,30 @@ func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) {
 		}
 	}
 
+	// If this is a taproot channel, then we'll need to generate our next
+	// verification nonce to send to the remote party. They'll use this to
+	// sign the next update to our commitment transaction.
+	var nextTaprootNonce *lnwire.Musig2Nonce
+	if c.ChanType.IsTaproot() {
+		taprootRevProducer, err := DeriveMusig2Shachain(
+			c.RevocationProducer,
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		nextNonce, err := NewMusigVerificationNonce(
+			c.LocalChanCfg.MultiSigKey.PubKey,
+			nextLocalCommitHeight, taprootRevProducer,
+		)
+		if err != nil {
+			return nil, fmt.Errorf("unable to gen next "+
+				"nonce: %w", err)
+		}
+
+		nextTaprootNonce = (*lnwire.Musig2Nonce)(&nextNonce.PubNonce)
+	}
+
 	return &lnwire.ChannelReestablish{
 		ChanID: lnwire.NewChanIDFromOutPoint(
 			&c.FundingOutpoint,
@@ -1450,6 +1535,7 @@ func (c *OpenChannel) ChanSyncMsg() (*lnwire.ChannelReestablish, error) {
 		LocalUnrevokedCommitPoint: input.ComputeCommitmentPoint(
 			currentCommitSecret[:],
 		),
+		LocalNonce: nextTaprootNonce,
 	}, nil
 }
 
@@ -3776,8 +3862,6 @@ func putChanRevocationState(chanBucket kvdb.RwBucket, channel *OpenChannel) erro
 		return err
 	}
 
-	// TODO(roasbeef): don't keep producer on disk
-
 	// If the next revocation is present, which is only the case after the
 	// FundingLocked message has been sent, then we'll write it to disk.
 	if channel.RemoteNextRevocation != nil {
diff --git a/htlcswitch/link.go b/htlcswitch/link.go
index bb800f5e4..959792881 100644
--- a/htlcswitch/link.go
+++ b/htlcswitch/link.go
@@ -698,7 +698,6 @@ func (l *channelLink) syncChanStates() error {
 		return fmt.Errorf("unable to generate chan sync message for "+
 			"ChannelPoint(%v)", l.channel.ChannelPoint())
 	}
-
 	if err := l.cfg.Peer.SendMessage(true, localChanSyncMsg); err != nil {
 		return fmt.Errorf("unable to send chan sync message for "+
 			"ChannelPoint(%v): %v", l.channel.ChannelPoint(), err)
@@ -738,6 +737,13 @@ func (l *channelLink) syncChanStates() error {
 				l.ChanID(), nextRevocation,
 			)
 
+			// If this is a taproot channel, then we'll send the
+			// very same nonce that we sent above, as they should
+			// take the latest verification nonce we send.
+			if chanState.ChanType.IsTaproot() {
+				fundingLockedMsg.NextLocalNonce = localChanSyncMsg.LocalNonce //nolint:lll
+			}
+
 			// For channels that negotiated the option-scid-alias
 			// feature bit, ensure that we send over the alias in
 			// the funding_locked message. We'll send the first
@@ -1929,7 +1935,10 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) {
 		// We just received a new updates to our local commitment
 		// chain, validate this new commitment, closing the link if
 		// invalid.
-		err = l.channel.ReceiveNewCommitment(msg.CommitSig, msg.HtlcSigs)
+		err = l.channel.ReceiveNewCommitment(&lnwallet.CommitSigs{
+			CommitSig: msg.CommitSig,
+			HtlcSigs:  msg.HtlcSigs,
+		})
 		if err != nil {
 			// If we were unable to reconstruct their proposed
 			// commitment, then we'll examine the type of error. If
@@ -2259,7 +2268,7 @@ func (l *channelLink) updateCommitTx() error {
 		return nil
 	}
 
-	theirCommitSig, htlcSigs, pendingHTLCs, err := l.channel.SignNextCommitment()
+	newCommit, err := l.channel.SignNextCommitment()
 	if err == lnwallet.ErrNoWindow {
 		l.cfg.PendingCommitTicker.Resume()
 		l.log.Trace("PendingCommitTicker resumed")
@@ -2291,7 +2300,7 @@ func (l *channelLink) updateCommitTx() error {
 	// pending).
 	newUpdate := &contractcourt.ContractUpdate{
 		HtlcKey: contractcourt.RemotePendingHtlcSet,
-		Htlcs:   pendingHTLCs,
+		Htlcs:   newCommit.PendingHTLCs,
 	}
 	err = l.cfg.NotifyContractUpdate(newUpdate)
 	if err != nil {
@@ -2307,8 +2316,8 @@ func (l *channelLink) updateCommitTx() error {
 
 	commitSig := &lnwire.CommitSig{
 		ChanID:    l.ChanID(),
-		CommitSig: theirCommitSig,
-		HtlcSigs:  htlcSigs,
+		CommitSig: newCommit.CommitSig,
+		HtlcSigs:  newCommit.HtlcSigs,
 	}
 	l.cfg.Peer.SendMessage(false, commitSig)
 
diff --git a/input/musig2_session_manager.go b/input/musig2_session_manager.go
index 3bf26004a..4b1d412d1 100644
--- a/input/musig2_session_manager.go
+++ b/input/musig2_session_manager.go
@@ -78,7 +78,7 @@ func (m *MusigSessionManager) MuSig2CreateSession(bipVersion MuSig2Version,
 	// Create a signing context and session with the given private key and
 	// list of all known signer public keys.
 	musigContext, musigSession, err := MuSig2CreateContext(
-		bipVersion, privKey, allSignerPubKeys, tweaks,
+		bipVersion, privKey, allSignerPubKeys, tweaks, sessionOpts...,
 	)
 	if err != nil {
 		return nil, fmt.Errorf("error creating signing context: %w",
diff --git a/lnwallet/chancloser/chancloser.go b/lnwallet/chancloser/chancloser.go
index 448e196c8..a383097eb 100644
--- a/lnwallet/chancloser/chancloser.go
+++ b/lnwallet/chancloser/chancloser.go
@@ -4,9 +4,9 @@ import (
 	"bytes"
 	"fmt"
 
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
 	"github.com/btcsuite/btcd/btcutil"
 	"github.com/btcsuite/btcd/chaincfg"
-	"github.com/btcsuite/btcd/chaincfg/chainhash"
 	"github.com/btcsuite/btcd/txscript"
 	"github.com/btcsuite/btcd/wire"
 	"github.com/davecgh/go-spew/spew"
@@ -94,75 +94,16 @@ const (
 	defaultMaxFeeMultiplier = 3
 )
 
-// Channel abstracts away from the core channel state machine by exposing an
-// interface that requires only the methods we need to carry out the channel
-// closing process.
-type Channel interface {
-	// ChannelPoint returns the channel point of the target channel.
-	ChannelPoint() *wire.OutPoint
-
-	// MarkCoopBroadcasted persistently marks that the channel close
-	// transaction has been broadcast.
-	MarkCoopBroadcasted(*wire.MsgTx, bool) error
-
-	// IsInitiator returns true we are the initiator of the channel.
-	IsInitiator() bool
-
-	// ShortChanID returns the scid of the channel.
-	ShortChanID() lnwire.ShortChannelID
-
-	// AbsoluteThawHeight returns the absolute thaw height of the channel.
-	// If the channel is pending, or an unconfirmed zero conf channel, then
-	// an error should be returned.
-	AbsoluteThawHeight() (uint32, error)
-
-	// LocalBalanceDust returns true if when creating a co-op close
-	// transaction, the balance of the local party will be dust after
-	// accounting for any anchor outputs.
-	LocalBalanceDust() bool
-
-	// RemoteBalanceDust returns true if when creating a co-op close
-	// transaction, the balance of the remote party will be dust after
-	// accounting for any anchor outputs.
-	RemoteBalanceDust() bool
-
-	// RemoteUpfrontShutdownScript returns the upfront shutdown script of
-	// the remote party. If the remote party didn't specify such a script,
-	// an empty delivery address should be returned.
-	RemoteUpfrontShutdownScript() lnwire.DeliveryAddress
-
-	// CreateCloseProposal creates a new co-op close proposal in the form
-	// of a valid signature, the chainhash of the final txid, and our final
-	// balance in the created state.
-	CreateCloseProposal(proposedFee btcutil.Amount, localDeliveryScript []byte,
-		remoteDeliveryScript []byte) (input.Signature, *chainhash.Hash,
-		btcutil.Amount, error)
-
-	// CompleteCooperativeClose persistently "completes" the cooperative
-	// close by producing a fully signed co-op close transaction.
-	CompleteCooperativeClose(localSig, remoteSig input.Signature,
-		localDeliveryScript, remoteDeliveryScript []byte,
-		proposedFee btcutil.Amount) (*wire.MsgTx, btcutil.Amount, error)
-}
-
-// CoopFeeEstimator is used to estimate the fee of a co-op close transaction.
-type CoopFeeEstimator interface {
-	// EstimateFee estimates an _absolute_ fee for a co-op close transaction
-	// given the local+remote tx outs (for the co-op close transaction),
-	// channel type, and ideal fee rate. If a passed TxOut is nil, then
-	// that indicates that an output is dust on the co-op close transaction
-	// _before_ fees are accounted for.
-	EstimateFee(chanType channeldb.ChannelType,
-		localTxOut, remoteTxOut *wire.TxOut,
-		idealFeeRate chainfee.SatPerKWeight) btcutil.Amount
-}
-
 // ChanCloseCfg holds all the items that a ChanCloser requires to carry out its
 // duties.
 type ChanCloseCfg struct {
 	// Channel is the channel that should be closed.
 	Channel Channel
 
+	// MusigSession is used to handle generating musig2 nonces, and also
+	// creating the proper set of closing options for taproot channels.
+	MusigSession MusigSession
+
 	// BroadcastTx broadcasts the passed transaction to the network.
 	BroadcastTx func(*wire.MsgTx, string) error
 
@@ -367,19 +308,35 @@ func (c *ChanCloser) initChanShutdown() (*lnwire.Shutdown, error) {
 	// closing script.
 	shutdown := lnwire.NewShutdown(c.cid, c.localDeliveryScript)
 
-	// Before closing, we'll attempt to send a disable update for the channel.
-	// We do so before closing the channel as otherwise the current edge policy
-	// won't be retrievable from the graph.
+	// If this is a taproot channel, then we'll need to also generate a
+	// nonce that'll be used sign the co-op close transaction offer.
+	if c.cfg.Channel.ChanType().IsTaproot() {
+		firstClosingNonce, err := c.cfg.MusigSession.ClosingNonce()
+		if err != nil {
+			return nil, err
+		}
+
+		shutdown.ShutdownNonce = (*lnwire.ShutdownNonce)(
+			&firstClosingNonce.PubNonce,
+		)
+
+		chancloserLog.Infof("Initiating shutdown w/ nonce: %v",
+			spew.Sdump(firstClosingNonce.PubNonce))
+	}
+
+	// Before closing, we'll attempt to send a disable update for the
+	// channel.  We do so before closing the channel as otherwise the
+	// current edge policy won't be retrievable from the graph.
 	if err := c.cfg.DisableChannel(c.chanPoint); err != nil {
 		chancloserLog.Warnf("Unable to disable channel %v on close: %v",
 			c.chanPoint, err)
 	}
 
-	// Before continuing, mark the channel as cooperatively closed with a nil
-	// txn. Even though we haven't negotiated the final txn, this guarantees
-	// that our listchannels rpc will be externally consistent, and reflect
-	// that the channel is being shutdown by the time the closing request
-	// returns.
+	// Before continuing, mark the channel as cooperatively closed with a
+	// nil txn. Even though we haven't negotiated the final txn, this
+	// guarantees that our listchannels rpc will be externally consistent,
+	// and reflect that the channel is being shutdown by the time the
+	// closing request returns.
 	err := c.cfg.Channel.MarkCoopBroadcasted(nil, c.locallyInitiated)
 	if err != nil {
 		return nil, err
@@ -448,6 +405,7 @@ func (c *ChanCloser) CloseRequest() *htlcswitch.ChanClose {
 // NOTE: This method will PANIC if the underlying channel implementation isn't
 // the desired type.
 func (c *ChanCloser) Channel() *lnwallet.LightningChannel {
+	// TODO(roasbeef): remove this
 	return c.cfg.Channel.(*lnwallet.LightningChannel)
 }
 
@@ -522,8 +480,9 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
 		// as otherwise, this is an attempted invalid state transition.
 		shutdownMsg, ok := msg.(*lnwire.Shutdown)
 		if !ok {
-			return nil, false, fmt.Errorf("expected lnwire.Shutdown, instead "+
-				"have %v", spew.Sdump(msg))
+			return nil, false, fmt.Errorf("expected "+
+				"lnwire.Shutdown, instead have %v",
+				spew.Sdump(msg))
 		}
 
 		// As we're the responder to this shutdown (the other party
@@ -571,6 +530,20 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
 			return nil, false, err
 		}
 
+		// If this is a taproot channel, then we'll want to stash the
+		// remote nonces so we can properly create a new musig
+		// session for signing.
+		if c.cfg.Channel.ChanType().IsTaproot() {
+			if shutdownMsg.ShutdownNonce == nil {
+				return nil, false, fmt.Errorf("shutdown " +
+					"nonce not populated")
+			}
+
+			c.cfg.MusigSession.InitRemoteNonce(&musig2.Nonces{
+				PubNonce: *shutdownMsg.ShutdownNonce,
+			})
+		}
+
 		chancloserLog.Infof("ChannelPoint(%v): responding to shutdown",
 			c.chanPoint)
 
@@ -588,7 +561,8 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
 		if chanInitiator {
 			closeSigned, err := c.proposeCloseSigned(c.idealFeeSat)
 			if err != nil {
-				return nil, false, err
+				return nil, false, fmt.Errorf("unable to sign "+
+					"new co op close offer: %w", err)
 			}
 			msgsToSend = append(msgsToSend, closeSigned)
 		}
@@ -628,10 +602,24 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
 		// closing transaction should look like.
 		c.state = closeFeeNegotiation
 
-		// Now that we know their desried delivery script, we can
+		// Now that we know their desired delivery script, we can
 		// compute what our max/ideal fee will be.
 		c.initFeeBaseline()
 
+		// If this is a taproot channel, then we'll want to stash the
+		// local+remote nonces so we can properly create a new musig
+		// session for signing.
+		if c.cfg.Channel.ChanType().IsTaproot() {
+			if shutdownMsg.ShutdownNonce == nil {
+				return nil, false, fmt.Errorf("shutdown " +
+					"nonce not populated")
+			}
+
+			c.cfg.MusigSession.InitRemoteNonce(&musig2.Nonces{
+				PubNonce: *shutdownMsg.ShutdownNonce,
+			})
+		}
+
 		chancloserLog.Infof("ChannelPoint(%v): shutdown response received, "+
 			"entering fee negotiation", c.chanPoint)
 
@@ -641,7 +629,8 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
 		if c.cfg.Channel.IsInitiator() {
 			closeSigned, err := c.proposeCloseSigned(c.idealFeeSat)
 			if err != nil {
-				return nil, false, err
+				return nil, false, fmt.Errorf("unable to sign "+
+					"new co op close offer: %w", err)
 			}
 
 			return []lnwire.Message{closeSigned}, false, nil
@@ -661,14 +650,66 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
 				"instead have %v", spew.Sdump(msg))
 		}
 
-		// We'll compare the proposed total fee, to what we've proposed during
-		// the negotiations. If it doesn't match any of our prior offers, then
-		// we'll attempt to ratchet the fee closer to
+		// If this is a taproot channel, then it MUST have a partial
+		// signature set at this point.
+		isTaproot := c.cfg.Channel.ChanType().IsTaproot()
+		if isTaproot && closeSignedMsg.PartialSig == nil {
+			return nil, false, fmt.Errorf("partial sig not set " +
+				"for taproot chan")
+		}
+
+		isInitiator := c.cfg.Channel.IsInitiator()
+
+		// We'll compare the proposed total fee, to what we've proposed
+		// during the negotiations. If it doesn't match any of our
+		// prior offers, then we'll attempt to ratchet the fee closer
+		// to our ideal fee.
 		remoteProposedFee := closeSignedMsg.FeeSatoshis
-		if _, ok := c.priorFeeOffers[remoteProposedFee]; !ok {
-			// We'll now attempt to ratchet towards a fee deemed acceptable by
-			// both parties, factoring in our ideal fee rate, and the last
-			// proposed fee by both sides.
+
+		_, feeMatchesOffer := c.priorFeeOffers[remoteProposedFee]
+		switch {
+		// For taproot channels, since nonces are involved, we can't do
+		// the existing co-op close negotiation process without going
+		// to a fully round based model. Rather than do this, we'll
+		// just accept the very first offer by the initiator.
+		case isTaproot && !isInitiator:
+			chancloserLog.Infof("ChannelPoint(%v) accepting "+
+				"initiator fee of %v", c.chanPoint,
+				remoteProposedFee)
+
+			// To auto-accept the initiators proposal, we'll just
+			// send back a signature w/ the same offer. We don't
+			// send the message here, as we can drop down and
+			// finalize the closure and broadcast, then echo back
+			// to Alice the final signature.
+			_, err := c.proposeCloseSigned(remoteProposedFee)
+			if err != nil {
+				return nil, false, fmt.Errorf("unable to sign "+
+					"new co op close offer: %w", err)
+			}
+
+		// Otherwise, if we are the initiator, and we just sent a
+		// signature for a taproot channel, then we'll ensure that the
+		// fee rate matches up exactly.
+		case isTaproot && isInitiator && !feeMatchesOffer:
+			return nil, false, fmt.Errorf("fee rate for "+
+				"taproot channels was not accepted: "+
+				"sent %v, got %v",
+				c.idealFeeSat, remoteProposedFee)
+
+		// If we're the initiator of the taproot channel, and we had
+		// our fee echo'd back, then it's all good, and we can proceed
+		// with final broadcast.
+		case isTaproot && isInitiator && feeMatchesOffer:
+			break
+
+		// Otherwise, if this is a normal segwit v0 channel, and the
+		// fee doesn't match our offer, then we'll try to "negotiate" a
+		// new fee.
+		case !feeMatchesOffer:
+			// We'll now attempt to ratchet towards a fee deemed
+			// acceptable by both parties, factoring in our ideal
+			// fee rate, and the last proposed fee by both sides.
 			feeProposal := calcCompromiseFee(c.chanPoint, c.idealFeeSat,
 				c.lastFeeProposal, remoteProposedFee,
 			)
@@ -678,17 +719,19 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
 					c.maxFee)
 			}
 
-			// With our new fee proposal calculated, we'll craft a new close
-			// signed signature to send to the other party so we can continue
-			// the fee negotiation process.
+			// With our new fee proposal calculated, we'll craft a
+			// new close signed signature to send to the other
+			// party so we can continue the fee negotiation
+			// process.
 			closeSigned, err := c.proposeCloseSigned(feeProposal)
 			if err != nil {
-				return nil, false, err
+				return nil, false, fmt.Errorf("unable to sign "+
+					"new co op close offer: %w", err)
 			}
 
-			// If the compromise fee doesn't match what the peer proposed, then
-			// we'll return this latest close signed message so we can continue
-			// negotiation.
+			// If the compromise fee doesn't match what the peer
+			// proposed, then we'll return this latest close signed
+			// message so we can continue negotiation.
 			if feeProposal != remoteProposedFee {
 				chancloserLog.Debugf("ChannelPoint(%v): close tx fee "+
 					"disagreement, continuing negotiation", c.chanPoint)
@@ -699,38 +742,56 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
 		chancloserLog.Infof("ChannelPoint(%v) fee of %v accepted, ending "+
 			"negotiation", c.chanPoint, remoteProposedFee)
 
-		// Otherwise, we've agreed on a fee for the closing transaction! We'll
-		// craft the final closing transaction so we can broadcast it to the
-		// network.
-		matchingSig := c.priorFeeOffers[remoteProposedFee].Signature
-		localSig, err := matchingSig.ToSignature()
-		if err != nil {
-			return nil, false, err
-		}
-
-		remoteSig, err := closeSignedMsg.Signature.ToSignature()
-		if err != nil {
-			return nil, false, err
+		// Otherwise, we've agreed on a fee for the closing
+		// transaction! We'll craft the final closing transaction so we
+		// can broadcast it to the network.
+		var (
+			localSig, remoteSig input.Signature
+			closeOpts           []lnwallet.ChanCloseOpt
+			err                 error
+		)
+		matchingSig := c.priorFeeOffers[remoteProposedFee]
+		if c.cfg.Channel.ChanType().IsTaproot() {
+			muSession := c.cfg.MusigSession
+			localSig, remoteSig, closeOpts, err = muSession.CombineClosingOpts( //nolint:lll
+				*matchingSig.PartialSig,
+				*closeSignedMsg.PartialSig,
+			)
+			if err != nil {
+				return nil, false, err
+			}
+		} else {
+			localSig, err = matchingSig.Signature.ToSignature()
+			if err != nil {
+				return nil, false, err
+			}
+			remoteSig, err = closeSignedMsg.Signature.ToSignature()
+			if err != nil {
+				return nil, false, err
+			}
 		}
 
 		closeTx, _, err := c.cfg.Channel.CompleteCooperativeClose(
-			localSig, remoteSig, c.localDeliveryScript, c.remoteDeliveryScript,
-			remoteProposedFee,
+			localSig, remoteSig, c.localDeliveryScript,
+			c.remoteDeliveryScript, remoteProposedFee, closeOpts...,
 		)
 		if err != nil {
 			return nil, false, err
 		}
 		c.closingTx = closeTx
 
-		// Before publishing the closing tx, we persist it to the database,
-		// such that it can be republished if something goes wrong.
-		err = c.cfg.Channel.MarkCoopBroadcasted(closeTx, c.locallyInitiated)
+		// Before publishing the closing tx, we persist it to the
+		// database, such that it can be republished if something goes
+		// wrong.
+		err = c.cfg.Channel.MarkCoopBroadcasted(
+			closeTx, c.locallyInitiated,
+		)
 		if err != nil {
 			return nil, false, err
 		}
 
-		// With the closing transaction crafted, we'll now broadcast it to the
-		// network.
+		// With the closing transaction crafted, we'll now broadcast it
+		// to the network.
 		chancloserLog.Infof("Broadcasting cooperative close tx: %v",
 			newLogClosure(func() string {
 				return spew.Sdump(closeTx)
@@ -747,10 +808,11 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
 			return nil, false, err
 		}
 
-		// Finally, we'll transition to the closeFinished state, and also
-		// return the final close signed message we sent. Additionally, we
-		// return true for the second argument to indicate we're finished with
-		// the channel closing negotiation.
+		// Finally, we'll transition to the closeFinished state, and
+		// also return the final close signed message we sent.
+		// Additionally, we return true for the second argument to
+		// indicate we're finished with the channel closing
+		// negotiation.
 		c.state = closeFinished
 		matchingOffer := c.priorFeeOffers[remoteProposedFee]
 		return []lnwire.Message{matchingOffer}, true, nil
@@ -778,8 +840,23 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
 // transaction for a channel based on the prior fee negotiations and our current
 // compromise fee.
 func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (*lnwire.ClosingSigned, error) {
+	var (
+		closeOpts []lnwallet.ChanCloseOpt
+		err       error
+	)
+
+	// If this is a taproot channel, then we'll include the musig session
+	// generated for the next co-op close negotiation round.
+	if c.cfg.Channel.ChanType().IsTaproot() {
+		closeOpts, err = c.cfg.MusigSession.ProposalClosingOpts()
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	rawSig, _, _, err := c.cfg.Channel.CreateCloseProposal(
 		fee, c.localDeliveryScript, c.remoteDeliveryScript,
+		closeOpts...,
 	)
 	if err != nil {
 		return nil, err
@@ -788,21 +865,42 @@ func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (*lnwire.ClosingSign
 	// We'll note our last signature and proposed fee so when the remote
 	// party responds we'll be able to decide if we've agreed on fees or
 	// not.
-	c.lastFeeProposal = fee
+	var (
+		parsedSig  lnwire.Sig
+		partialSig *lnwire.PartialSigWithNonce
+	)
+	if c.cfg.Channel.ChanType().IsTaproot() {
+		musig, ok := rawSig.(*lnwallet.MusigPartialSig)
+		if !ok {
+			return nil, fmt.Errorf("expected MusigPartialSig, "+
+				"got %T", rawSig)
+		}
 
-	parsedSig, err := lnwire.NewSigFromSignature(rawSig)
-	if err != nil {
-		return nil, err
+		partialSig = musig.ToWireSig()
+	} else {
+		parsedSig, err = lnwire.NewSigFromSignature(rawSig)
+		if err != nil {
+			return nil, err
+		}
 	}
 
+	c.lastFeeProposal = fee
+
 	chancloserLog.Infof("ChannelPoint(%v): proposing fee of %v sat to "+
 		"close chan", c.chanPoint, int64(fee))
 
-	// We'll assemble a ClosingSigned message using this information and return
-	// it to the caller so we can kick off the final stage of the channel
-	// closure process.
+	// We'll assemble a ClosingSigned message using this information and
+	// return it to the caller so we can kick off the final stage of the
+	// channel closure process.
 	closeSignedMsg := lnwire.NewClosingSigned(c.cid, fee, parsedSig)
 
+	// For musig2 channels, the main sig is blank, and instead we'll send
+	// over a partial signature which'll be combine donce our offer is
+	// accepted.
+	if partialSig != nil {
+		closeSignedMsg.PartialSig = &partialSig.PartialSig
+	}
+
 	// We'll also save this close signed, in the case that the remote party
 	// accepts our offer. This way, we don't have to re-sign.
 	c.priorFeeOffers[fee] = closeSignedMsg
diff --git a/lnwallet/chancloser/interface.go b/lnwallet/chancloser/interface.go
new file mode 100644
index 000000000..9d588d521
--- /dev/null
+++ b/lnwallet/chancloser/interface.go
@@ -0,0 +1,110 @@
+package chancloser
+
+import (
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
+	"github.com/btcsuite/btcd/btcutil"
+	"github.com/btcsuite/btcd/chaincfg/chainhash"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightningnetwork/lnd/channeldb"
+	"github.com/lightningnetwork/lnd/input"
+	"github.com/lightningnetwork/lnd/lnwallet"
+	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
+	"github.com/lightningnetwork/lnd/lnwire"
+)
+
+// CoopFeeEstimator is used to estimate the fee of a co-op close transaction.
+type CoopFeeEstimator interface {
+	// EstimateFee estimates an _absolute_ fee for a co-op close transaction
+	// given the local+remote tx outs (for the co-op close transaction),
+	// channel type, and ideal fee rate. If a passed TxOut is nil, then
+	// that indicates that an output is dust on the co-op close transaction
+	// _before_ fees are accounted for.
+	EstimateFee(chanType channeldb.ChannelType,
+		localTxOut, remoteTxOut *wire.TxOut,
+		idealFeeRate chainfee.SatPerKWeight) btcutil.Amount
+}
+
+// Channel abstracts away from the core channel state machine by exposing an
+// interface that requires only the methods we need to carry out the channel
+// closing process.
+type Channel interface { //nolint:interfacebloat
+	// ChannelPoint returns the channel point of the target channel.
+	ChannelPoint() *wire.OutPoint
+
+	// MarkCoopBroadcasted persistently marks that the channel close
+	// transaction has been broadcast.
+	MarkCoopBroadcasted(*wire.MsgTx, bool) error
+
+	// IsInitiator returns true we are the initiator of the channel.
+	IsInitiator() bool
+
+	// ShortChanID returns the scid of the channel.
+	ShortChanID() lnwire.ShortChannelID
+
+	// ChanType returns the channel type of the channel.
+	ChanType() channeldb.ChannelType
+
+	// FundingTxOut returns the funding output of the channel.
+	FundingTxOut() *wire.TxOut
+
+	// AbsoluteThawHeight returns the absolute thaw height of the channel.
+	// If the channel is pending, or an unconfirmed zero conf channel, then
+	// an error should be returned.
+	AbsoluteThawHeight() (uint32, error)
+
+	// LocalBalanceDust returns true if when creating a co-op close
+	// transaction, the balance of the local party will be dust after
+	// accounting for any anchor outputs.
+	LocalBalanceDust() bool
+
+	// RemoteBalanceDust returns true if when creating a co-op close
+	// transaction, the balance of the remote party will be dust after
+	// accounting for any anchor outputs.
+	RemoteBalanceDust() bool
+
+	// RemoteUpfrontShutdownScript returns the upfront shutdown script of
+	// the remote party. If the remote party didn't specify such a script,
+	// an empty delivery address should be returned.
+	RemoteUpfrontShutdownScript() lnwire.DeliveryAddress
+
+	// CreateCloseProposal creates a new co-op close proposal in the form
+	// of a valid signature, the chainhash of the final txid, and our final
+	// balance in the created state.
+	CreateCloseProposal(proposedFee btcutil.Amount,
+		localDeliveryScript []byte, remoteDeliveryScript []byte,
+		closeOpt ...lnwallet.ChanCloseOpt,
+	) (
+		input.Signature, *chainhash.Hash, btcutil.Amount, error)
+
+	// CompleteCooperativeClose persistently "completes" the cooperative
+	// close by producing a fully signed co-op close transaction.
+	CompleteCooperativeClose(localSig, remoteSig input.Signature,
+		localDeliveryScript, remoteDeliveryScript []byte,
+		proposedFee btcutil.Amount, closeOpt ...lnwallet.ChanCloseOpt,
+	) (*wire.MsgTx, btcutil.Amount, error)
+}
+
+// MusigSession is an interface that abstracts away the details of the musig2
+// session details. A session is used to generate the necessary closing options
+// needed to close a channel cooperatively.
+type MusigSession interface {
+	// ProposalClosingOpts generates the set of closing options needed to
+	// generate a new musig2 proposal signature.
+	ProposalClosingOpts() ([]lnwallet.ChanCloseOpt, error)
+
+	// CombineClosingOpts returns the options that should be used when
+	// combining the final musig partial signature. The method also maps
+	// the lnwire partial signatures into an input.Signature that can be
+	// used more generally.
+	CombineClosingOpts(localSig, remoteSig lnwire.PartialSig,
+	) (input.Signature, input.Signature, []lnwallet.ChanCloseOpt, error)
+
+	// ClosingNonce generates the nonce we'll use to generate the musig2
+	// partial signatures for the co-op close transaction.
+	ClosingNonce() (*musig2.Nonces, error)
+
+	// InitRemoteNonce saves the remote nonce the party sent during their
+	// shutdown message so it can be used later to generate and verify
+	// signatures.
+	InitRemoteNonce(nonce *musig2.Nonces)
+}
diff --git a/lnwallet/channel.go b/lnwallet/channel.go
index e63fdce4c..4fa9d1b65 100644
--- a/lnwallet/channel.go
+++ b/lnwallet/channel.go
@@ -13,6 +13,7 @@ import (
 	"github.com/btcsuite/btcd/blockchain"
 	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btcd/btcec/v2/ecdsa"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
 	"github.com/btcsuite/btcd/btcutil"
 	"github.com/btcsuite/btcd/btcutil/txsort"
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -25,8 +26,10 @@ import (
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/channeldb/models"
 	"github.com/lightningnetwork/lnd/input"
+	"github.com/lightningnetwork/lnd/keychain"
 	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
 	"github.com/lightningnetwork/lnd/lnwire"
+	"github.com/lightningnetwork/lnd/shachain"
 )
 
 var (
@@ -1317,9 +1320,56 @@ type LightningChannel struct {
 	// log is a channel-specific logging instance.
 	log btclog.Logger
 
+	// taprootNonceProducer is used to generate a shachain tree for the
+	// purpose of generating verification nonces for taproot channels.
+	taprootNonceProducer shachain.Producer
+
+	// musigSessions holds the current musig2 pair session for the channel.
+	musigSessions *MusigPairSession
+
+	// pendingVerificationNonce is the initial verification nonce generated
+	// for musig2 channels when the state machine is intiated. Once we know
+	// the verification nonce of the remote party, then we can start to use
+	// the channel as normal.
+	pendingVerificationNonce *musig2.Nonces
+
+	// fundingOutput is the funding output (script+value).
+	fundingOutput wire.TxOut
+
 	sync.RWMutex
 }
 
+// ChannelOpt is a functional option that lets callers modify how a new channel
+// is created.
+type ChannelOpt func(*channelOpts)
+
+// WithLocalMusigNonces is used to bind an existing verification/local nonce to
+// a new channel.
+func WithLocalMusigNonces(nonce *musig2.Nonces) ChannelOpt {
+	return func(o *channelOpts) {
+		o.localNonce = nonce
+	}
+}
+
+// WithRemoteMusigNonces is used to bind the remote party's local/verification
+// nonce to a new channel.
+func WithRemoteMusigNonces(nonces *musig2.Nonces) ChannelOpt {
+	return func(o *channelOpts) {
+		o.remoteNonce = nonces
+	}
+}
+
+// channelOpts is the set of options used to create a new channel.
+type channelOpts struct {
+	localNonce  *musig2.Nonces
+	remoteNonce *musig2.Nonces
+}
+
+// defaultChannelOpts returns the set of default options for a new channel.
+func defaultChannelOpts() *channelOpts {
+	return &channelOpts{}
+}
+
 // NewLightningChannel creates a new, active payment channel given an
 // implementation of the chain notifier, channel database, and the current
 // settled channel state. Throughout state transitions, then channel will
@@ -1327,7 +1377,12 @@ type LightningChannel struct {
 // manner.
 func NewLightningChannel(signer input.Signer,
 	state *channeldb.OpenChannel,
-	sigPool *SigPool) (*LightningChannel, error) {
+	sigPool *SigPool, chanOpts ...ChannelOpt) (*LightningChannel, error) {
+
+	opts := defaultChannelOpts()
+	for _, optFunc := range chanOpts {
+		optFunc(opts)
+	}
 
 	localCommit := state.LocalCommitment
 	remoteCommit := state.RemoteCommitment
@@ -1343,26 +1398,46 @@ func NewLightningChannel(signer input.Signer,
 
 	logPrefix := fmt.Sprintf("ChannelPoint(%v):", state.FundingOutpoint)
 
+	taprootNonceProducer, err := channeldb.DeriveMusig2Shachain(
+		state.RevocationProducer,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("unable to derive shachain: %w", err)
+	}
+
 	lc := &LightningChannel{
-		Signer:            signer,
-		sigPool:           sigPool,
-		currentHeight:     localCommit.CommitHeight,
-		remoteCommitChain: newCommitmentChain(),
-		localCommitChain:  newCommitmentChain(),
-		channelState:      state,
-		commitBuilder:     NewCommitmentBuilder(state),
-		localUpdateLog:    localUpdateLog,
-		remoteUpdateLog:   remoteUpdateLog,
-		ChanPoint:         &state.FundingOutpoint,
-		Capacity:          state.Capacity,
-		LocalFundingKey:   state.LocalChanCfg.MultiSigKey.PubKey,
-		RemoteFundingKey:  state.RemoteChanCfg.MultiSigKey.PubKey,
-		log:               build.NewPrefixLog(logPrefix, walletLog),
+		Signer:               signer,
+		sigPool:              sigPool,
+		currentHeight:        localCommit.CommitHeight,
+		remoteCommitChain:    newCommitmentChain(),
+		localCommitChain:     newCommitmentChain(),
+		channelState:         state,
+		commitBuilder:        NewCommitmentBuilder(state),
+		localUpdateLog:       localUpdateLog,
+		remoteUpdateLog:      remoteUpdateLog,
+		ChanPoint:            &state.FundingOutpoint,
+		Capacity:             state.Capacity,
+		LocalFundingKey:      state.LocalChanCfg.MultiSigKey.PubKey,
+		RemoteFundingKey:     state.RemoteChanCfg.MultiSigKey.PubKey,
+		taprootNonceProducer: taprootNonceProducer,
+		log:                  build.NewPrefixLog(logPrefix, walletLog),
+	}
+
+	// At this point, we may already have nonces that were passed in, so
+	// we'll check that now as this lets us skip some steps later.
+	if opts.localNonce != nil {
+		lc.pendingVerificationNonce = opts.localNonce
+	}
+	if lc.pendingVerificationNonce != nil && opts.remoteNonce != nil {
+		err := lc.InitRemoteMusigNonces(opts.remoteNonce)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	// With the main channel struct reconstructed, we'll now restore the
 	// commitment state in memory and also the update logs themselves.
-	err := lc.restoreCommitState(&localCommit, &remoteCommit)
+	err = lc.restoreCommitState(&localCommit, &remoteCommit)
 	if err != nil {
 		return nil, err
 	}
@@ -1380,29 +1455,48 @@ func NewLightningChannel(signer input.Signer,
 // createSignDesc derives the SignDescriptor for commitment transactions from
 // other fields on the LightningChannel.
 func (lc *LightningChannel) createSignDesc() error {
-	localKey := lc.channelState.LocalChanCfg.MultiSigKey.PubKey.
-		SerializeCompressed()
-	remoteKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey.
-		SerializeCompressed()
 
-	multiSigScript, err := input.GenMultiSigScript(localKey, remoteKey)
-	if err != nil {
-		return err
+	var (
+		fundingPkScript, multiSigScript []byte
+		err                             error
+	)
+
+	chanState := lc.channelState
+	localKey := chanState.LocalChanCfg.MultiSigKey.PubKey
+	remoteKey := chanState.RemoteChanCfg.MultiSigKey.PubKey
+
+	if chanState.ChanType.IsTaproot() {
+		fundingPkScript, _, err = input.GenTaprootFundingScript(
+			localKey, remoteKey, int64(lc.channelState.Capacity),
+		)
+		if err != nil {
+			return err
+		}
+	} else {
+		multiSigScript, err = input.GenMultiSigScript(
+			localKey.SerializeCompressed(),
+			remoteKey.SerializeCompressed(),
+		)
+		if err != nil {
+			return err
+		}
+
+		fundingPkScript, err = input.WitnessScriptHash(multiSigScript)
+		if err != nil {
+			return err
+		}
 	}
 
-	fundingPkScript, err := input.WitnessScriptHash(multiSigScript)
-	if err != nil {
-		return err
+	lc.fundingOutput = wire.TxOut{
+		PkScript: fundingPkScript,
+		Value:    int64(lc.channelState.Capacity),
 	}
 	lc.signDesc = &input.SignDescriptor{
 		KeyDesc:       lc.channelState.LocalChanCfg.MultiSigKey,
 		WitnessScript: multiSigScript,
-		Output: &wire.TxOut{
-			PkScript: fundingPkScript,
-			Value:    int64(lc.channelState.Capacity),
-		},
-		HashType:   txscript.SigHashAll,
-		InputIndex: 0,
+		Output:        &lc.fundingOutput,
+		HashType:      txscript.SigHashAll,
+		InputIndex:    0,
 	}
 
 	return nil
@@ -3219,7 +3313,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
 
 		// If the HTLC isn't dust, then we'll create an empty sign job
 		// to add to the batch momentarily.
-		sigJob := SignJob{}
+		var sigJob SignJob
 		sigJob.Cancel = cancelChan
 		sigJob.Resp = make(chan SignJobResp, 1)
 
@@ -3246,21 +3340,35 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
 			return nil, nil, err
 		}
 
+		// Construct a full hash cache as we may be signing a segwit v1
+		// sighash.
+		txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex]
+		prevFetcher := txscript.NewCannedPrevOutputFetcher(
+			txOut.PkScript, int64(htlc.Amount.ToSatoshis()),
+		)
+		hashCache := txscript.NewTxSigHashes(sigJob.Tx, prevFetcher)
+
 		// Finally, we'll generate a sign descriptor to generate a
 		// signature to give to the remote party for this commitment
 		// transaction. Note we use the raw HTLC amount.
-		txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex]
 		sigJob.SignDesc = input.SignDescriptor{
-			KeyDesc:       localChanCfg.HtlcBasePoint,
-			SingleTweak:   keyRing.LocalHtlcKeyTweak,
-			WitnessScript: htlc.theirWitnessScript,
-			Output:        txOut,
-			HashType:      sigHashType,
-			SigHashes:     input.NewTxSigHashesV0Only(sigJob.Tx),
-			InputIndex:    0,
+			KeyDesc:           localChanCfg.HtlcBasePoint,
+			SingleTweak:       keyRing.LocalHtlcKeyTweak,
+			WitnessScript:     htlc.theirWitnessScript,
+			Output:            txOut,
+			PrevOutputFetcher: prevFetcher,
+			HashType:          sigHashType,
+			SigHashes:         hashCache,
+			InputIndex:        0,
 		}
 		sigJob.OutputIndex = htlc.remoteOutputIndex
 
+		// If this is a taproot channel, then we'll need to set the
+		// method type to ensure we generate a valid signature.
+		if chanType.IsTaproot() {
+			sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod //nolint:lll
+		}
+
 		sigBatch = append(sigBatch, sigJob)
 	}
 	for _, htlc := range remoteCommitView.outgoingHTLCs {
@@ -3300,21 +3408,35 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
 			return nil, nil, err
 		}
 
+		// Construct a full hash cache as we may be signing a segwit v1
+		// sighash.
+		txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex]
+		prevFetcher := txscript.NewCannedPrevOutputFetcher(
+			txOut.PkScript, int64(htlc.Amount.ToSatoshis()),
+		)
+		hashCache := txscript.NewTxSigHashes(sigJob.Tx, prevFetcher)
+
 		// Finally, we'll generate a sign descriptor to generate a
 		// signature to give to the remote party for this commitment
 		// transaction. Note we use the raw HTLC amount.
-		txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex]
 		sigJob.SignDesc = input.SignDescriptor{
-			KeyDesc:       localChanCfg.HtlcBasePoint,
-			SingleTweak:   keyRing.LocalHtlcKeyTweak,
-			WitnessScript: htlc.theirWitnessScript,
-			Output:        txOut,
-			HashType:      sigHashType,
-			SigHashes:     input.NewTxSigHashesV0Only(sigJob.Tx),
-			InputIndex:    0,
+			KeyDesc:           localChanCfg.HtlcBasePoint,
+			SingleTweak:       keyRing.LocalHtlcKeyTweak,
+			WitnessScript:     htlc.theirWitnessScript,
+			Output:            txOut,
+			PrevOutputFetcher: prevFetcher,
+			HashType:          sigHashType,
+			SigHashes:         hashCache,
+			InputIndex:        0,
 		}
 		sigJob.OutputIndex = htlc.remoteOutputIndex
 
+		// If this is a taproot channel, then we'll need to set the
+		// method type to ensure we generate a valid signature.
+		if chanType.IsTaproot() {
+			sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod //nolint:lll
+		}
+
 		sigBatch = append(sigBatch, sigJob)
 	}
 
@@ -3718,6 +3840,34 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter,
 	return nil
 }
 
+// CommitSigs holds the set of related signatures for a new commitment
+// transaction state.
+type CommitSigs struct {
+	// CommitSig is the normal commitment signature. This will only be a
+	// non-zero commitment signature for non-taproot channels.
+	CommitSig lnwire.Sig
+
+	// HtlcSigs is the set of signatures for all HTLCs in the commitment
+	// transaction. Depending on the channel type, these will either be
+	// ECDSA or Schnorr signatures.
+	HtlcSigs []lnwire.Sig
+
+	// PartialSig is the musig2 partial signature for taproot commitment
+	// transactions.
+	PartialSig *lnwire.PartialSigWithNonce
+}
+
+// NewCommitState wraps the various signatures needed to properly
+// propose/accept a new commitment state. This includes the signer's nonce for
+// musig2 channels.
+type NewCommitState struct {
+	*CommitSigs
+
+	// PendingHTLCs is the set of new/pending HTLCs produced by this
+	// commitment state.
+	PendingHTLCs []channeldb.HTLC
+}
+
 // SignNextCommitment signs a new commitment which includes any previous
 // unsettled HTLCs, any new HTLCs, and any modifications to prior HTLCs
 // committed in previous commitment updates. Signing a new commitment
@@ -3729,9 +3879,7 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter,
 // any). The HTLC signatures are sorted according to the BIP 69 order of the
 // HTLC's on the commitment transaction. Finally, the new set of pending HTLCs
 // for the remote party's commitment are also returned.
-func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig,
-	[]channeldb.HTLC, error) {
-
+func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
 	lc.Lock()
 	defer lc.Unlock()
 
@@ -3744,8 +3892,9 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig,
 	}
 
 	var (
-		sig      lnwire.Sig
-		htlcSigs []lnwire.Sig
+		sig        lnwire.Sig
+		partialSig *lnwire.PartialSigWithNonce
+		htlcSigs   []lnwire.Sig
 	)
 
 	// If we're awaiting for an ACK to a commitment signature, or if we
@@ -3757,7 +3906,7 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig,
 	if unacked || commitPoint == nil {
 		lc.log.Tracef("waiting for remote ack=%v, nil "+
 			"RemoteNextRevocation: %v", unacked, commitPoint == nil)
-		return sig, htlcSigs, nil, ErrNoWindow
+		return nil, ErrNoWindow
 	}
 
 	// Determine the last update on the remote log that has been locked in.
@@ -3772,7 +3921,7 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig,
 		remoteACKedIndex, lc.localUpdateLog.logIndex, true, nil, nil,
 	)
 	if err != nil {
-		return sig, htlcSigs, nil, err
+		return nil, err
 	}
 
 	// Grab the next commitment point for the remote party. This will be
@@ -3795,7 +3944,7 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig,
 		remoteACKedIndex, remoteHtlcIndex, keyRing,
 	)
 	if err != nil {
-		return sig, htlcSigs, nil, err
+		return nil, err
 	}
 
 	lc.log.Tracef("extending remote chain to height %v, "+
@@ -3826,23 +3975,45 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig,
 		&lc.channelState.RemoteChanCfg, newCommitView,
 	)
 	if err != nil {
-		return sig, htlcSigs, nil, err
+		return nil, err
 	}
 	lc.sigPool.SubmitSignBatch(sigBatch)
 
 	// While the jobs are being carried out, we'll Sign their version of
 	// the new commitment transaction while we're waiting for the rest of
 	// the HTLC signatures to be processed.
-	lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(newCommitView.txn)
-	rawSig, err := lc.Signer.SignOutputRaw(newCommitView.txn, lc.signDesc)
-	if err != nil {
-		close(cancelChan)
-		return sig, htlcSigs, nil, err
-	}
-	sig, err = lnwire.NewSigFromSignature(rawSig)
-	if err != nil {
-		close(cancelChan)
-		return sig, htlcSigs, nil, err
+	//
+	// TODO(roasbeef): abstract into CommitSigner interface?
+	if lc.channelState.ChanType.IsTaproot() {
+		// In this case, we'll send out a partial signature as this is
+		// a musig2 channel. The encoded normal ECDSA signature will be
+		// just blank.
+		remoteSession := lc.musigSessions.RemoteSession
+		musig, err := remoteSession.SignCommit(
+			newCommitView.txn,
+		)
+		if err != nil {
+			close(cancelChan)
+			return nil, err
+		}
+
+		partialSig = musig.ToWireSig()
+	} else {
+		lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(
+			newCommitView.txn,
+		)
+		rawSig, err := lc.Signer.SignOutputRaw(
+			newCommitView.txn, lc.signDesc,
+		)
+		if err != nil {
+			close(cancelChan)
+			return nil, err
+		}
+		sig, err = lnwire.NewSigFromSignature(rawSig)
+		if err != nil {
+			close(cancelChan)
+			return nil, err
+		}
 	}
 
 	// We'll need to send over the signatures to the remote party in the
@@ -3862,7 +4033,7 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig,
 		// jobs.
 		if jobResp.Err != nil {
 			close(cancelChan)
-			return sig, htlcSigs, nil, jobResp.Err
+			return nil, jobResp.Err
 		}
 
 		htlcSigs = append(htlcSigs, jobResp.Sig)
@@ -3873,11 +4044,11 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig,
 	// can retransmit it if necessary.
 	commitDiff, err := lc.createCommitDiff(newCommitView, sig, htlcSigs)
 	if err != nil {
-		return sig, htlcSigs, nil, err
+		return nil, err
 	}
 	err = lc.channelState.AppendRemoteCommitChain(commitDiff)
 	if err != nil {
-		return sig, htlcSigs, nil, err
+		return nil, err
 	}
 
 	// TODO(roasbeef): check that one eclair bug
@@ -3888,7 +4059,14 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig,
 	// latest commitment update.
 	lc.remoteCommitChain.addCommitment(newCommitView)
 
-	return sig, htlcSigs, commitDiff.Commitment.Htlcs, nil
+	return &NewCommitState{
+		CommitSigs: &CommitSigs{
+			CommitSig:  sig,
+			HtlcSigs:   htlcSigs,
+			PartialSig: partialSig,
+		},
+		PendingHTLCs: commitDiff.Commitment.Htlcs,
+	}, nil
 }
 
 // ProcessChanSyncMsg processes a ChannelReestablish message sent by the remote
@@ -3955,6 +4133,25 @@ func (lc *LightningChannel) ProcessChanSyncMsg(
 		}
 	}
 
+	// If this is a taproot channel, then we expect that the remote party
+	// has sent the next verification nonce. If they haven't, then we'll
+	// bail out, otherwise we'll init our local session then continue as
+	// normal.
+	switch {
+	case lc.channelState.ChanType.IsTaproot() && msg.LocalNonce == nil:
+		return nil, nil, nil, fmt.Errorf("remote verification nonce " +
+			"not sent")
+
+	case lc.channelState.ChanType.IsTaproot() && msg.LocalNonce != nil:
+		err := lc.InitRemoteMusigNonces(&musig2.Nonces{
+			PubNonce: *msg.LocalNonce,
+		})
+		if err != nil {
+			return nil, nil, nil, fmt.Errorf("unable to init "+
+				"remote nonce: %w", err)
+		}
+	}
+
 	// If we detect that this is is a restored channel, then we can skip a
 	// portion of the verification, as we already know that we're unable to
 	// proceed with any updates.
@@ -4032,12 +4229,12 @@ func (lc *LightningChannel) ProcessChanSyncMsg(
 			"while we have %v, we owe them a revocation",
 			msg.RemoteCommitTailHeight, localTailHeight)
 
-		revocationMsg, err := lc.generateRevocation(
-			localTailHeight - 1,
-		)
+		heightToRetransmit := localTailHeight - 1
+		revocationMsg, err := lc.generateRevocation(heightToRetransmit)
 		if err != nil {
 			return nil, nil, nil, err
 		}
+
 		updates = append(updates, revocationMsg)
 
 		// Next, as a precaution, we'll check a special edge case. If
@@ -4046,19 +4243,22 @@ func (lc *LightningChannel) ProcessChanSyncMsg(
 		// revocation, but also initiate a state transition to re-sync
 		// them.
 		if lc.OweCommitment() {
-			commitSig, htlcSigs, _, err := lc.SignNextCommitment()
+			newCommit, err := lc.SignNextCommitment()
 			switch {
 
 			// If we signed this state, then we'll accumulate
 			// another update to send over.
 			case err == nil:
-				updates = append(updates, &lnwire.CommitSig{
+				commitSig := &lnwire.CommitSig{
 					ChanID: lnwire.NewChanIDFromOutPoint(
 						&lc.channelState.FundingOutpoint,
 					),
-					CommitSig: commitSig,
-					HtlcSigs:  htlcSigs,
-				})
+					CommitSig:  newCommit.CommitSig,
+					HtlcSigs:   newCommit.HtlcSigs,
+					PartialSig: newCommit.PartialSig,
+				}
+
+				updates = append(updates, commitSig)
 
 			// If we get a failure due to not knowing their next
 			// point, then this is fine as they'll either send
@@ -4138,6 +4338,9 @@ func (lc *LightningChannel) ProcessChanSyncMsg(
 		// With the batch of updates accumulated, we'll now re-send the
 		// original CommitSig message required to re-sync their remote
 		// commitment chain with our local version of their chain.
+		//
+		// TODO(roasbeef): need to re-sign commitment states w/
+		// fresh nonce
 		commitUpdates = append(commitUpdates, commitDiff.CommitSig)
 
 		// NOTE: If a revocation is not owed, then updates is empty.
@@ -4366,11 +4569,33 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment,
 					return nil, err
 				}
 
+				htlcAmt := int64(htlc.Amount.ToSatoshis())
+
+				if chanType.IsTaproot() {
+					// TODO(roasbeef): add abstraction in
+					// front
+					prevFetcher := txscript.NewCannedPrevOutputFetcher( //nolint:lll
+						htlc.ourPkScript, htlcAmt,
+					)
+					hashCache := txscript.NewTxSigHashes(
+						successTx, prevFetcher,
+					)
+					tapLeaf := txscript.NewBaseTapLeaf(
+						htlc.ourWitnessScript,
+					)
+
+					return txscript.CalcTapscriptSignaturehash( //nolint:lll
+						hashCache, sigHashType,
+						successTx, 0, prevFetcher,
+						tapLeaf,
+					)
+				}
+
 				hashCache := input.NewTxSigHashesV0Only(successTx)
 				sigHash, err := txscript.CalcWitnessSigHash(
 					htlc.ourWitnessScript, hashCache,
 					sigHashType, successTx, 0,
-					int64(htlc.Amount.ToSatoshis()),
+					htlcAmt,
 				)
 				if err != nil {
 					return nil, err
@@ -4385,6 +4610,13 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment,
 					"signatures")
 			}
 
+			// If this is a taproot channel, then we'll convert it
+			// to a schnorr signature, so we can get correct type
+			// from ToSignature below.
+			if chanType.IsTaproot() {
+				htlcSigs[i].ForceSchnorr()
+			}
+
 			// With the sighash generated, we'll also store the
 			// signature so it can be written to disk if this state
 			// is valid.
@@ -4414,18 +4646,43 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment,
 				timeoutTx, err := CreateHtlcTimeoutTx(
 					chanType, isLocalInitiator, op,
 					outputAmt, htlc.Timeout,
-					uint32(localChanCfg.CsvDelay), leaseExpiry,
-					keyRing.RevocationKey, keyRing.ToLocalKey,
+					uint32(localChanCfg.CsvDelay),
+					leaseExpiry, keyRing.RevocationKey,
+					keyRing.ToLocalKey,
 				)
 				if err != nil {
 					return nil, err
 				}
 
-				hashCache := input.NewTxSigHashesV0Only(timeoutTx)
+				htlcAmt := int64(htlc.Amount.ToSatoshis())
+
+				if chanType.IsTaproot() {
+					// TODO(roasbeef): add abstraction in
+					// front
+					prevFetcher := txscript.NewCannedPrevOutputFetcher( //nolint:lll
+						htlc.ourPkScript, htlcAmt,
+					)
+					hashCache := txscript.NewTxSigHashes(
+						timeoutTx, prevFetcher,
+					)
+					tapLeaf := txscript.NewBaseTapLeaf(
+						htlc.ourWitnessScript,
+					)
+
+					return txscript.CalcTapscriptSignaturehash( //nolint:lll
+						hashCache, sigHashType,
+						timeoutTx, 0, prevFetcher,
+						tapLeaf,
+					)
+				}
+
+				hashCache := input.NewTxSigHashesV0Only(
+					timeoutTx,
+				)
 				sigHash, err := txscript.CalcWitnessSigHash(
 					htlc.ourWitnessScript, hashCache,
 					sigHashType, timeoutTx, 0,
-					int64(htlc.Amount.ToSatoshis()),
+					htlcAmt,
 				)
 				if err != nil {
 					return nil, err
@@ -4440,6 +4697,13 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment,
 					"signatures")
 			}
 
+			// If this is a taproot channel, then we'll convert it
+			// to a schnorr signature, so we can get correct type
+			// from ToSignature below.
+			if chanType.IsTaproot() {
+				htlcSigs[i].ForceSchnorr()
+			}
+
 			// With the sighash generated, we'll also store the
 			// signature so it can be written to disk if this state
 			// is valid.
@@ -4447,6 +4711,7 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment,
 			if err != nil {
 				return nil, err
 			}
+
 			htlc.sig = sig
 
 		default:
@@ -4500,6 +4765,22 @@ func (i *InvalidCommitSigError) Error() string {
 // error interface.
 var _ error = (*InvalidCommitSigError)(nil)
 
+// InvalidPartialCommitSigError is used when we encounter an invalid musig2
+// partial signature.
+type InvalidPartialCommitSigError struct {
+	InvalidCommitSigError
+
+	*invalidPartialSigError
+}
+
+// Error returns a detailed error string including the exact transaction that
+// caused an invalid partial commit sig signature.
+func (i *InvalidPartialCommitSigError) Error() string {
+	return fmt.Sprintf("rejected commitment: commit_height=%v, "+
+		"commit_tx=%x -- %v", i.commitHeight, i.commitTx,
+		i.invalidPartialSigError)
+}
+
 // InvalidHtlcSigError is a struct that implements the error interface to
 // report a failure to validate an htlc signature from a remote peer. We'll use
 // the items in this struct to generate a rich error message for the remote
@@ -4537,9 +4818,9 @@ var _ error = (*InvalidCommitSigError)(nil)
 // to our local commitment chain. Once we send a revocation for our prior
 // state, then this newly added commitment becomes our current accepted channel
 // state.
-func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig,
-	htlcSigs []lnwire.Sig) error {
-
+//
+//nolint:funlen
+func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error {
 	lc.Lock()
 	defer lc.Unlock()
 
@@ -4610,30 +4891,15 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig,
 		}),
 	)
 
-	// Construct the sighash of the commitment transaction corresponding to
-	// this newly proposed state update.
-	localCommitTx := localCommitmentView.txn
-	multiSigScript := lc.signDesc.WitnessScript
-	hashCache := input.NewTxSigHashesV0Only(localCommitTx)
-	sigHash, err := txscript.CalcWitnessSigHash(
-		multiSigScript, hashCache, txscript.SigHashAll,
-		localCommitTx, 0, int64(lc.channelState.Capacity),
-	)
-	if err != nil {
-		// TODO(roasbeef): fetchview has already mutated the HTLCs...
-		//  * need to either roll-back, or make pure
-		return err
-	}
-
 	// As an optimization, we'll generate a series of jobs for the worker
-	// pool to verify each of the HTLc signatures presented. Once
+	// pool to verify each of the HTLC signatures presented. Once
 	// generated, we'll submit these jobs to the worker pool.
 	var leaseExpiry uint32
 	if lc.channelState.ChanType.HasLeaseExpiration() {
 		leaseExpiry = lc.channelState.ThawHeight
 	}
 	verifyJobs, err := genHtlcSigValidationJobs(
-		localCommitmentView, keyRing, htlcSigs,
+		localCommitmentView, keyRing, commitSigs.HtlcSigs,
 		lc.channelState.ChanType, lc.channelState.IsInitiator,
 		leaseExpiry, &lc.channelState.LocalChanCfg,
 		&lc.channelState.RemoteChanCfg,
@@ -4645,29 +4911,104 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig,
 	cancelChan := make(chan struct{})
 	verifyResps := lc.sigPool.SubmitVerifyBatch(verifyJobs, cancelChan)
 
+	localCommitTx := localCommitmentView.txn
+
 	// While the HTLC verification jobs are proceeding asynchronously,
 	// we'll ensure that the newly constructed commitment state has a valid
 	// signature.
-	verifyKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey
+	//
+	// To do that we'll, construct the sighash of the commitment
+	// transaction corresponding to this newly proposed state update.  If
+	// this is a taproot channel, then in order to validate the sighash,
+	// we'll need to call into the relevant tapscript methods.
+	if lc.channelState.ChanType.IsTaproot() {
+		localSession := lc.musigSessions.LocalSession
+
+		// As we want to ensure we never write nonces to disk, we'll
+		// use the shachain state to generate a nonce for our next
+		// local state. Similar to generateRevocation, we do height + 2
+		// (next height + 1) here, as this is for the _next_ local
+		// state, and we're about to accept height + 1.
+		localCtrNonce := WithLocalCounterNonce(
+			nextHeight+1, lc.taprootNonceProducer,
+		)
+		nextVerificationNonce, err := localSession.VerifyCommitSig(
+			localCommitTx, commitSigs.PartialSig, localCtrNonce,
+		)
+		if err != nil {
+			close(cancelChan)
 
-	cSig, err := commitSig.ToSignature()
-	if err != nil {
-		return err
-	}
-	if !cSig.Verify(sigHash, verifyKey) {
-		close(cancelChan)
+			var sigErr invalidPartialSigError
+			if errors.As(err, &sigErr) {
+				// If we fail to validate their commitment
+				// signature, we'll generate a special error to
+				// send over the protocol. We'll include the
+				// exact signature and commitment we failed to
+				// verify against in order to aide debugging.
+				var txBytes bytes.Buffer
+				_ = localCommitTx.Serialize(&txBytes)
+				return &InvalidPartialCommitSigError{
+					invalidPartialSigError: &sigErr,
+					InvalidCommitSigError: InvalidCommitSigError{ //nolint:lll
+						commitHeight: nextHeight,
+						commitTx:     txBytes.Bytes(),
+					},
+				}
+			}
+
+			return err
+		}
+
+		// Now that we have the next verification nonce for our local
+		// session, we'll refresh it to yield a new session we'll use
+		// for the next incoming signature.
+		newLocalSession, err := lc.musigSessions.LocalSession.Refresh(
+			nextVerificationNonce,
+		)
+		if err != nil {
+			return err
+		}
+		lc.musigSessions.LocalSession = newLocalSession
+	} else {
+		multiSigScript := lc.signDesc.WitnessScript
+		prevFetcher := txscript.NewCannedPrevOutputFetcher(
+			multiSigScript, int64(lc.channelState.Capacity),
+		)
+		hashCache := txscript.NewTxSigHashes(localCommitTx, prevFetcher)
+
+		sigHash, err := txscript.CalcWitnessSigHash(
+			multiSigScript, hashCache, txscript.SigHashAll,
+			localCommitTx, 0, int64(lc.channelState.Capacity),
+		)
+		if err != nil {
+			// TODO(roasbeef): fetchview has already mutated the
+			// HTLCs...  * need to either roll-back, or make pure
+			return err
+		}
+
+		verifyKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey
 
-		// If we fail to validate their commitment signature, we'll
-		// generate a special error to send over the protocol. We'll
-		// include the exact signature and commitment we failed to
-		// verify against in order to aide debugging.
-		var txBytes bytes.Buffer
-		localCommitTx.Serialize(&txBytes)
-		return &InvalidCommitSigError{
-			commitHeight: nextHeight,
-			commitSig:    commitSig.ToSignatureBytes(),
-			sigHash:      sigHash,
-			commitTx:     txBytes.Bytes(),
+		cSig, err := commitSigs.CommitSig.ToSignature()
+		if err != nil {
+			return err
+		}
+		if !cSig.Verify(sigHash, verifyKey) {
+			close(cancelChan)
+
+			// If we fail to validate their commitment signature,
+			// we'll generate a special error to send over the
+			// protocol. We'll include the exact signature and
+			// commitment we failed to verify against in order to
+			// aide debugging.
+			var txBytes bytes.Buffer
+			_ = localCommitTx.Serialize(&txBytes)
+
+			return &InvalidCommitSigError{
+				commitHeight: nextHeight,
+				commitSig:    commitSigs.CommitSig.ToSignatureBytes(), //nolint:lll
+				sigHash:      sigHash,
+				commitTx:     txBytes.Bytes(),
+			}
 		}
 	}
 
@@ -4704,8 +5045,21 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig,
 	}
 
 	// The signature checks out, so we can now add the new commitment to
-	// our local commitment chain.
-	localCommitmentView.sig = commitSig.ToSignatureBytes()
+	// our local commitment chain. For regular channels, we can just
+	// serialize the ECDSA sig. For taproot channels, we'll serialize the
+	// partial sig that includes the nonce that was used for signing.
+	if lc.channelState.ChanType.IsTaproot() {
+		var sigBytes [lnwire.PartialSigWithNonceLen]byte
+		b := bytes.NewBuffer(sigBytes[0:0])
+		if err := commitSigs.PartialSig.Encode(b); err != nil {
+			return err
+		}
+
+		localCommitmentView.sig = sigBytes[:]
+	} else {
+		localCommitmentView.sig = commitSigs.CommitSig.ToSignatureBytes() //nolint:lll
+	}
+
 	lc.localCommitChain.addCommitment(localCommitmentView)
 
 	return nil
@@ -5112,6 +5466,24 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
 		return nil, nil, nil, nil, err
 	}
 
+	// Now that we have a new verification nonce from them, we can refresh
+	// our remote musig2 session which allows us to create another state.
+	if lc.channelState.ChanType.IsTaproot() {
+		if revMsg.LocalNonce == nil {
+			return nil, nil, nil, nil, fmt.Errorf("next " +
+				"revocation nonce not set")
+		}
+		newRemoteSession, err := lc.musigSessions.RemoteSession.Refresh(
+			&musig2.Nonces{
+				PubNonce: *revMsg.LocalNonce,
+			},
+		)
+		if err != nil {
+			return nil, nil, nil, nil, err
+		}
+		lc.musigSessions.RemoteSession = newRemoteSession
+	}
+
 	// At this point, the revocation has been accepted, and we've rotated
 	// the current revocation key+hash for the remote party. Therefore we
 	// sync now to ensure the revocation producer state is consistent with
@@ -5723,7 +6095,7 @@ func (lc *LightningChannel) RemoteUpfrontShutdownScript() lnwire.DeliveryAddress
 // AbsoluteThawHeight determines a frozen channel's absolute thaw height. If
 // the channel is not frozen, then 0 is returned.
 //
-// An error is returned if the channel is penidng, or is an unconfirmed zero
+// An error is returned if the channel is pending, or is an unconfirmed zero
 // conf channel.
 func (lc *LightningChannel) AbsoluteThawHeight() (uint32, error) {
 	return lc.channelState.AbsoluteThawHeight()
@@ -5737,30 +6109,103 @@ func (lc *LightningChannel) getSignedCommitTx() (*wire.MsgTx, error) {
 	localCommit := lc.channelState.LocalCommitment
 	commitTx := localCommit.CommitTx.Copy()
 
-	theirSig, err := ecdsa.ParseDERSignature(localCommit.CommitSig)
-	if err != nil {
-		return nil, err
-	}
+	ourKey := lc.channelState.LocalChanCfg.MultiSigKey
+	theirKey := lc.channelState.RemoteChanCfg.MultiSigKey
 
-	// With this, we then generate the full witness so the caller can
-	// broadcast a fully signed transaction.
-	lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(commitTx)
-	ourSig, err := lc.Signer.SignOutputRaw(commitTx, lc.signDesc)
-	if err != nil {
-		return nil, err
-	}
+	var witness wire.TxWitness
+	switch {
+	// If this is a taproot channel, then we'll need to re-derive the nonce
+	// we need to generate a new signature
+	case lc.channelState.ChanType.IsTaproot():
+		// First, we'll need to re-derive the local nonce we sent to
+		// the remote party to create this musig session. We pass in
+		// the same height here as we're generating the nonce needed
+		// for the _current_ state.
+		localNonce, err := channeldb.NewMusigVerificationNonce(
+			ourKey.PubKey, lc.currentHeight,
+			lc.taprootNonceProducer,
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		// Now that we have the local nonce, we'll re-create the musig
+		// session we had for this height.
+		musigSession := NewPartialMusigSession(
+			*localNonce, ourKey, theirKey, lc.Signer,
+			&lc.fundingOutput, LocalMusigCommit,
+		)
 
-	// With the final signature generated, create the witness stack
-	// required to spend from the multi-sig output.
-	ourKey := lc.channelState.LocalChanCfg.MultiSigKey.PubKey.
-		SerializeCompressed()
-	theirKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey.
-		SerializeCompressed()
+		var remoteSig lnwire.PartialSigWithNonce
+		err = remoteSig.Decode(
+			bytes.NewReader(localCommit.CommitSig),
+		)
+		if err != nil {
+			return nil, fmt.Errorf("unable to decode remote "+
+				"partial sig: %w", err)
+		}
 
-	commitTx.TxIn[0].Witness = input.SpendMultiSig(
-		lc.signDesc.WitnessScript, ourKey,
-		ourSig, theirKey, theirSig,
-	)
+		// Next, we'll manually finalize the session with the signing
+		// nonce we got from the remote party which is embedded in the
+		// signature we have.
+		err = musigSession.FinalizeSession(musig2.Nonces{
+			PubNonce: remoteSig.Nonce,
+		})
+		if err != nil {
+			return nil, fmt.Errorf("unable to finalize musig "+
+				"session: %w", err)
+		}
+
+		// Now that the session has been finalized, we can generate our
+		// half of the signature for the state. We don't capture the
+		// sig as it's stored within the session.
+		if _, err := musigSession.SignCommit(commitTx); err != nil {
+			return nil, err
+		}
+
+		// The final step is now to combine this signature we generated
+		// above, with the remote party's signature. We only need to
+		// pass the remote sig, as the local sig was already cached in
+		// the session.
+		var partialSig MusigPartialSig
+		partialSig.FromWireSig(&remoteSig)
+		finalSig, err := musigSession.CombineSigs(partialSig.sig)
+		if err != nil {
+			return nil, fmt.Errorf("unable to combine musig "+
+				"partial sigs: %w", err)
+		}
+
+		// The witness is the single keyspend schnorr sig.
+		witness = wire.TxWitness{
+			finalSig.Serialize(),
+		}
+
+	// Otherwise, the final witness we generate will be a normal p2wsh
+	// multi-sig spend.
+	default:
+		theirSig, err := ecdsa.ParseDERSignature(localCommit.CommitSig)
+		if err != nil {
+			return nil, err
+		}
+
+		// With this, we then generate the full witness so the caller
+		// can broadcast a fully signed transaction.
+		lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(commitTx)
+		ourSig, err := lc.Signer.SignOutputRaw(commitTx, lc.signDesc)
+		if err != nil {
+			return nil, err
+		}
+
+		// With the final signature generated, create the witness stack
+		// required to spend from the multi-sig output.
+		witness = input.SpendMultiSig(
+			lc.signDesc.WitnessScript,
+			ourKey.PubKey.SerializeCompressed(), ourSig,
+			theirKey.PubKey.SerializeCompressed(), theirSig,
+		)
+	}
+
+	commitTx.TxIn[0].Witness = witness
 
 	return commitTx, nil
 }
@@ -6657,6 +7102,30 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
 	}, nil
 }
 
+// chanCloseOpt is a functional option that can be used to modify the co-op
+// close process.
+type chanCloseOpt struct {
+	musigSession *MusigSession
+}
+
+// ChanCloseOpt is a closure type that cen be used to modify the set of default
+// options.
+type ChanCloseOpt func(*chanCloseOpt)
+
+// defaultCloseOpts is the default set of close options.
+func defaultCloseOpts() *chanCloseOpt {
+	return &chanCloseOpt{}
+}
+
+// WithCoopCloseMusigSession can be used to apply an existing musig2 session to
+// the cooperative close process. If specified, then a musig2 co-op close
+// (single sig keyspend) will be used.
+func WithCoopCloseMusigSession(session *MusigSession) ChanCloseOpt {
+	return func(opts *chanCloseOpt) {
+		opts.musigSession = session
+	}
+}
+
 // CreateCloseProposal is used by both parties in a cooperative channel close
 // workflow to generate proposed close transactions and signatures. This method
 // should only be executed once all pending HTLCs (if any) on the channel have
@@ -6668,8 +7137,8 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
 // TODO(roasbeef): caller should initiate signal to reject all incoming HTLCs,
 // settle any in flight.
 func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
-	localDeliveryScript []byte,
-	remoteDeliveryScript []byte) (input.Signature, *chainhash.Hash,
+	localDeliveryScript []byte, remoteDeliveryScript []byte,
+	closeOpts ...ChanCloseOpt) (input.Signature, *chainhash.Hash,
 	btcutil.Amount, error) {
 
 	lc.Lock()
@@ -6681,6 +7150,11 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
 		return nil, nil, 0, ErrChanClosing
 	}
 
+	opts := defaultCloseOpts()
+	for _, optFunc := range closeOpts {
+		optFunc(opts)
+	}
+
 	// Get the final balances after subtracting the proposed fee, taking
 	// care not to persist the adjusted balance, as the feeRate may change
 	// during the channel closing process.
@@ -6706,14 +7180,25 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
 		return nil, nil, 0, err
 	}
 
-	// Finally, sign the completed cooperative closure transaction. As the
-	// initiator we'll simply send our signature over to the remote party,
-	// using the generated txid to be notified once the closure transaction
-	// has been confirmed.
-	lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(closeTx)
-	sig, err := lc.Signer.SignOutputRaw(closeTx, lc.signDesc)
-	if err != nil {
-		return nil, nil, 0, err
+	// If we have a co-op close musig session, then this is a taproot
+	// channel, so we'll generate a _partial_ signature.
+	var sig input.Signature
+	if opts.musigSession != nil {
+		sig, err = opts.musigSession.SignCommit(closeTx)
+		if err != nil {
+			return nil, nil, 0, err
+		}
+	} else {
+		// For regular channels we'll, sign the completed cooperative
+		// closure transaction. As the initiator we'll simply send our
+		// signature over to the remote party, using the generated txid
+		// to be notified once the closure transaction has been
+		// confirmed.
+		lc.signDesc.SigHashes = input.NewTxSigHashesV0Only(closeTx)
+		sig, err = lc.Signer.SignOutputRaw(closeTx, lc.signDesc)
+		if err != nil {
+			return nil, nil, 0, err
+		}
 	}
 
 	// As everything checks out, indicate in the channel status that a
@@ -6734,7 +7219,8 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
 func (lc *LightningChannel) CompleteCooperativeClose(
 	localSig, remoteSig input.Signature,
 	localDeliveryScript, remoteDeliveryScript []byte,
-	proposedFee btcutil.Amount) (*wire.MsgTx, btcutil.Amount, error) {
+	proposedFee btcutil.Amount,
+	closeOpts ...ChanCloseOpt) (*wire.MsgTx, btcutil.Amount, error) {
 
 	lc.Lock()
 	defer lc.Unlock()
@@ -6745,6 +7231,11 @@ func (lc *LightningChannel) CompleteCooperativeClose(
 		return nil, 0, ErrChanClosing
 	}
 
+	opts := defaultCloseOpts()
+	for _, optFunc := range closeOpts {
+		optFunc(opts)
+	}
+
 	// Get the final balances after subtracting the proposed fee.
 	ourBalance, theirBalance, err := CoopCloseBalance(
 		lc.channelState.ChanType, lc.channelState.IsInitiator,
@@ -6767,31 +7258,60 @@ func (lc *LightningChannel) CompleteCooperativeClose(
 	// consensus rules such as being too big, or having any value with a
 	// negative output.
 	tx := btcutil.NewTx(closeTx)
+	prevOut := lc.signDesc.Output
 	if err := blockchain.CheckTransactionSanity(tx); err != nil {
 		return nil, 0, err
 	}
-	hashCache := input.NewTxSigHashesV0Only(closeTx)
-
-	// Finally, construct the witness stack minding the order of the
-	// pubkeys+sigs on the stack.
-	ourKey := lc.channelState.LocalChanCfg.MultiSigKey.PubKey.
-		SerializeCompressed()
-	theirKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey.
-		SerializeCompressed()
-	witness := input.SpendMultiSig(
-		lc.signDesc.WitnessScript, ourKey, localSig, theirKey,
-		remoteSig,
+
+	prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
+		prevOut.PkScript, prevOut.Value,
 	)
-	closeTx.TxIn[0].Witness = witness
+	hashCache := txscript.NewTxSigHashes(closeTx, prevOutputFetcher)
+
+	// Next, we'll complete the co-op close transaction. Depending on the
+	// set of options, we'll either do a regular p2wsh spend, or construct
+	// the final schnorr signature from a set of partial sigs.
+	if opts.musigSession != nil {
+		// For taproot channels, we'll use the attached session to
+		// combine the two partial signatures into a proper schnorr
+		// signature.
+		remotePartialSig, ok := remoteSig.(*MusigPartialSig)
+		if !ok {
+			return nil, 0, fmt.Errorf("expected MusigPartialSig, "+
+				"got %T", remoteSig)
+		}
+
+		finalSchnorrSig, err := opts.musigSession.CombineSigs(
+			remotePartialSig.sig,
+		)
+		if err != nil {
+			return nil, 0, fmt.Errorf("unable to combine "+
+				"final co-op close sig: %w", err)
+		}
+
+		// The witness for a keyspend is just the signature itself.
+		closeTx.TxIn[0].Witness = wire.TxWitness{
+			finalSchnorrSig.Serialize(),
+		}
+	} else {
+		// For regular channels, we'll need to , construct the witness
+		// stack minding the order of the pubkeys+sigs on the stack.
+		ourKey := lc.channelState.LocalChanCfg.MultiSigKey.PubKey.
+			SerializeCompressed()
+		theirKey := lc.channelState.RemoteChanCfg.MultiSigKey.PubKey.
+			SerializeCompressed()
+		witness := input.SpendMultiSig(
+			lc.signDesc.WitnessScript, ourKey, localSig, theirKey,
+			remoteSig,
+		)
+		closeTx.TxIn[0].Witness = witness
+	}
 
 	// Validate the finalized transaction to ensure the output script is
 	// properly met, and that the remote peer supplied a valid signature.
-	prevOut := lc.signDesc.Output
 	vm, err := txscript.NewEngine(
 		prevOut.PkScript, closeTx, 0, txscript.StandardVerifyFlags, nil,
-		hashCache, prevOut.Value, txscript.NewCannedPrevOutputFetcher(
-			prevOut.PkScript, prevOut.Value,
-		),
+		hashCache, prevOut.Value, prevOutputFetcher,
 	)
 	if err != nil {
 		return nil, 0, err
@@ -7255,8 +7775,9 @@ func (lc *LightningChannel) generateRevocation(height uint64) (*lnwire.RevokeAnd
 	// revocation.
 	//
 	// Put simply in the window slides to the left by one.
+	revHeight := height + 2
 	nextCommitSecret, err := lc.channelState.RevocationProducer.AtIndex(
-		height + 2,
+		revHeight,
 	)
 	if err != nil {
 		return nil, err
@@ -7267,6 +7788,21 @@ func (lc *LightningChannel) generateRevocation(height uint64) (*lnwire.RevokeAnd
 		&lc.channelState.FundingOutpoint,
 	)
 
+	// If this is a taproot channel, then we also need to generate the
+	// verification nonce for this target state.
+	if lc.channelState.ChanType.IsTaproot() {
+		nextVerificationNonce, err := channeldb.NewMusigVerificationNonce( //nolint:lll
+			lc.channelState.LocalChanCfg.MultiSigKey.PubKey,
+			revHeight, lc.taprootNonceProducer,
+		)
+		if err != nil {
+			return nil, err
+		}
+		revocationMsg.LocalNonce = (*lnwire.Musig2Nonce)(
+			&nextVerificationNonce.PubNonce,
+		)
+	}
+
 	return revocationMsg, nil
 }
 
@@ -7451,11 +7987,23 @@ func (lc *LightningChannel) IdealCommitFeeRate(netFeeRate, minRelayFeeRate,
 	return absoluteMaxFee
 }
 
-// RemoteNextRevocation returns the channelState's RemoteNextRevocation.
+// RemoteNextRevocation returns the channelState's RemoteNextRevocation. For
+// musig2 channels, until a nonce pair is processed by the remote party, a nil
+// public key is returned.
+//
+// TODO(roasbeef): revisit, maybe just make a more general method instead?
 func (lc *LightningChannel) RemoteNextRevocation() *btcec.PublicKey {
 	lc.RLock()
 	defer lc.RUnlock()
 
+	if !lc.channelState.ChanType.IsTaproot() {
+		return lc.channelState.RemoteNextRevocation
+	}
+
+	if lc.musigSessions == nil {
+		return nil
+	}
+
 	return lc.channelState.RemoteNextRevocation
 }
 
@@ -7635,3 +8183,98 @@ func (lc *LightningChannel) unsignedLocalUpdates(remoteMessageIndex,
 
 	return localPeerUpdates
 }
+
+// GenMusigNonces generates the verification nonce to start off a new musig2
+// channel session.
+func (lc *LightningChannel) GenMusigNonces() (*musig2.Nonces, error) {
+	lc.Lock()
+	defer lc.Unlock()
+
+	var err error
+
+	// We pass in the current height+1 as this'll be the set of
+	// verification nonces we'll send to the party to create our _next_
+	// state.
+	lc.pendingVerificationNonce, err = channeldb.NewMusigVerificationNonce(
+		lc.channelState.LocalChanCfg.MultiSigKey.PubKey,
+		lc.currentHeight+1, lc.taprootNonceProducer,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return lc.pendingVerificationNonce, nil
+}
+
+// HasRemoteNonces returns true if the channel has a remote nonce pair.
+func (lc *LightningChannel) HasRemoteNonces() bool {
+	return lc.musigSessions != nil
+}
+
+// InitRemoteMusigNonces processes the remote musig nonces sent by the remote
+// party. This should be called upon connection re-establishment, after we've
+// generated our own nonces. Once this method returns a nil error, then the
+// channel can be used to sign commitment states.
+func (lc *LightningChannel) InitRemoteMusigNonces(remoteNonce *musig2.Nonces,
+) error {
+
+	lc.Lock()
+	defer lc.Unlock()
+
+	if lc.pendingVerificationNonce == nil {
+		return fmt.Errorf("pending verification nonce is not set")
+	}
+
+	// Now that we have the set of local and remote nonces, we can generate
+	// a new pair of musig sessions for our local commitment and the
+	// commitment of the remote party.
+	localNonce := lc.pendingVerificationNonce
+
+	localChanCfg := lc.channelState.LocalChanCfg
+	remoteChanCfg := lc.channelState.RemoteChanCfg
+
+	// TODO(roasbeef): propagate rename of signing and verification nonces
+
+	sessionCfg := &MusigSessionCfg{
+		LocalKey:    localChanCfg.MultiSigKey,
+		RemoteKey:   remoteChanCfg.MultiSigKey,
+		LocalNonce:  *localNonce,
+		RemoteNonce: *remoteNonce,
+		Signer:      lc.Signer,
+		InputTxOut:  &lc.fundingOutput,
+	}
+	lc.musigSessions = NewMusigPairSession(
+		sessionCfg,
+	)
+
+	lc.pendingVerificationNonce = nil
+
+	return nil
+}
+
+// ChanType returns the channel type.
+func (lc *LightningChannel) ChanType() channeldb.ChannelType {
+	lc.RLock()
+	defer lc.RUnlock()
+
+	return lc.channelState.ChanType
+}
+
+// FundingTxOut returns the funding output of the channel.
+func (lc *LightningChannel) FundingTxOut() *wire.TxOut {
+	lc.RLock()
+	defer lc.RUnlock()
+
+	return &lc.fundingOutput
+}
+
+// MultiSigKeys returns the set of multi-sig keys for an channel.
+func (lc *LightningChannel) MultiSigKeys() (keychain.KeyDescriptor,
+	keychain.KeyDescriptor) {
+
+	lc.RLock()
+	defer lc.RUnlock()
+
+	return lc.channelState.LocalChanCfg.MultiSigKey,
+		lc.channelState.RemoteChanCfg.MultiSigKey
+}
diff --git a/lnwallet/chanvalidate/validate.go b/lnwallet/chanvalidate/validate.go
index c349ea974..5cf5bbf10 100644
--- a/lnwallet/chanvalidate/validate.go
+++ b/lnwallet/chanvalidate/validate.go
@@ -186,10 +186,14 @@ func Validate(ctx *Context) (*wire.OutPoint, error) {
 	// If we reach this point, then all other checks have succeeded, so
 	// we'll now attempt a full Script VM execution to ensure that we're
 	// able to close the channel using this initial state.
+	prevFetcher := txscript.NewCannedPrevOutputFetcher(
+		ctx.MultiSigPkScript, fundingValue,
+	)
+	commitTx := ctx.CommitCtx.FullySignedCommitTx
+	hashCache := txscript.NewTxSigHashes(commitTx, prevFetcher)
 	vm, err := txscript.NewEngine(
-		ctx.MultiSigPkScript, ctx.CommitCtx.FullySignedCommitTx,
-		0, txscript.StandardVerifyFlags, nil, nil, fundingValue,
-		txscript.NewCannedPrevOutputFetcher(ctx.MultiSigPkScript, 0),
+		ctx.MultiSigPkScript, commitTx, 0, txscript.StandardVerifyFlags,
+		nil, hashCache, fundingValue, prevFetcher,
 	)
 	if err != nil {
 		return nil, err
diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go
index 996e3486d..22d5c3bb6 100644
--- a/lnwallet/commitment.go
+++ b/lnwallet/commitment.go
@@ -410,26 +410,47 @@ func SecondLevelHtlcScript(chanType channeldb.ChannelType, initiator bool,
 
 	var (
 		witnessScript []byte
+		pkScript      []byte
 		err           error
 	)
 	switch {
+	// For taproot channels, the pkScript is a segwit v1 p2tr output.
+	case chanType.IsTaproot():
+		taprootOutputKey, err := input.TaprootSecondLevelHtlcScript(
+			revocationKey, delayKey, csvDelay,
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		pkScript, err = input.PayToTaprootScript(taprootOutputKey)
+		if err != nil {
+			return nil, err
+		}
+
 	// 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():
 		witnessScript, err = input.LeaseSecondLevelHtlcScript(
 			revocationKey, delayKey, csvDelay, leaseExpiry,
 		)
 
+		pkScript, err = input.WitnessScriptHash(witnessScript)
+		if err != nil {
+			return nil, err
+		}
+
 	default:
 		witnessScript, err = input.SecondLevelHtlcScript(
 			revocationKey, delayKey, csvDelay,
 		)
-	}
-	if err != nil {
-		return nil, err
-	}
 
-	pkScript, err := input.WitnessScriptHash(witnessScript)
+		pkScript, err = input.WitnessScriptHash(witnessScript)
+		if err != nil {
+			return nil, err
+		}
+	}
 	if err != nil {
 		return nil, err
 	}
@@ -969,12 +990,11 @@ func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool,
 	return ourBalance, theirBalance, nil
 }
 
-// genHtlcScript generates the proper P2WSH public key scripts for the HTLC
-// output modified by two-bits denoting if this is an incoming HTLC, and if the
-// HTLC is being applied to their commitment transaction or ours.
-func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool,
-	timeout uint32, rHash [32]byte,
-	keyRing *CommitmentKeyRing) ([]byte, []byte, error) {
+// genSegwitV0HtlcScript generates the HTLC scripts for a normal segwit v0
+// channel.
+func genSegwitV0HtlcScript(chanType channeldb.ChannelType,
+	isIncoming, ourCommit bool, timeout uint32, rHash [32]byte,
+	keyRing *CommitmentKeyRing) (*ScriptInfo, error) {
 
 	var (
 		witnessScript []byte
@@ -1028,17 +1048,156 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool,
 		)
 	}
 	if err != nil {
-		return nil, nil, err
+		return nil, err
 	}
 
 	// Now that we have the redeem scripts, create the P2WSH public key
 	// script for the output itself.
 	htlcP2WSH, err := input.WitnessScriptHash(witnessScript)
+	if err != nil {
+		return nil, err
+	}
+
+	return &ScriptInfo{
+		PkScript:      htlcP2WSH,
+		WitnessScript: witnessScript,
+	}, nil
+}
+
+// genTaprootHtlcScript generates the HTLC scripts for a taproot+musig2
+// channel.
+func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32,
+	rHash [32]byte, keyRing *CommitmentKeyRing) (*ScriptInfo, error) {
+
+	var (
+		taprootKey        *btcec.PublicKey
+		secondLevelScript []byte
+	)
+
+	// Generate the proper redeem scripts for the HTLC output modified by
+	// two-bits denoting if this is an incoming HTLC, and if the HTLC is
+	// being applied to their commitment transaction or ours.
+	switch {
+	// The HTLC is paying to us, and being applied to our commitment
+	// transaction. So we need to use the receiver's version of HTLC the
+	// script.
+	case isIncoming && ourCommit:
+		scriptTree, err := input.ReceiverHTLCScriptTaproot(
+			timeout, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey,
+			keyRing.RevocationKey, rHash[:],
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		taprootKey = scriptTree.TaprootKey
+
+		// As this is an HTLC on our commitment transaction, the second
+		// level path we care about here is the success path.
+		// Therefore, we'll grab the tapLeaf corresponding to the
+		// success path.
+		secondLevelScript = scriptTree.SuccessTapLeaf.Script
+
+	// We're being paid via an HTLC by the remote party, and the HTLC is
+	// being added to their commitment transaction, so we use the sender's
+	// version of the HTLC script.
+	case isIncoming && !ourCommit:
+		scriptTree, err := input.SenderHTLCScriptTaproot(
+			keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey,
+			keyRing.RevocationKey, rHash[:],
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		taprootKey = scriptTree.TaprootKey
+
+		// In this case, this is an incoming HTLC on the commitment
+		// transaction of the remote party, so we'll return the timeout
+		// tapleaf since that's the second level spend they need in the
+		// case of a broadcast.
+		secondLevelScript = scriptTree.TimeoutTapLeaf.Script
+
+	// We're sending an HTLC which is being added to our commitment
+	// transaction. Therefore, we need to use the sender's version of the
+	// HTLC script.
+	case !isIncoming && ourCommit:
+		scriptTree, err := input.SenderHTLCScriptTaproot(
+			keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey,
+			keyRing.RevocationKey, rHash[:],
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		taprootKey = scriptTree.TaprootKey
+
+		// This is an outgoing HTLC on our commitment transaction, so
+		// we need to be able to generate/verify signatures for the
+		// timeout path.
+		secondLevelScript = scriptTree.TimeoutTapLeaf.Script
+
+	// Finally, we're paying the remote party via an HTLC, which is being
+	// added to their commitment transaction. Therefore, we use the
+	// receiver's version of the HTLC script.
+	case !isIncoming && !ourCommit:
+		scriptTree, err := input.ReceiverHTLCScriptTaproot(
+			timeout, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey,
+			keyRing.RevocationKey, rHash[:],
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		taprootKey = scriptTree.TaprootKey
+
+		// This is an outgoing HTLC on the remote party's commitment
+		// transaction. In this case if they go on chain, they'll need
+		// the second level success spend, so we grab that tapscript
+		// path.
+		secondLevelScript = scriptTree.SuccessTapLeaf.Script
+	}
+
+	// Now that we have the redeem scripts, create the P2TR public key
+	// script for the output itself.
+	p2trOutput, err := input.PayToTaprootScript(taprootKey)
+	if err != nil {
+		return nil, err
+	}
+
+	return &ScriptInfo{
+		PkScript:      p2trOutput,
+		WitnessScript: secondLevelScript,
+	}, nil
+}
+
+// genHtlcScript generates the proper P2WSH public key scripts for the HTLC
+// output modified by two-bits denoting if this is an incoming HTLC, and if the
+// HTLC is being applied to their commitment transaction or ours.
+func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool,
+	timeout uint32, rHash [32]byte,
+	keyRing *CommitmentKeyRing) ([]byte, []byte, error) {
+
+	var (
+		scriptInfo *ScriptInfo
+		err        error
+	)
+
+	if !chanType.IsTaproot() {
+		scriptInfo, err = genSegwitV0HtlcScript(
+			chanType, isIncoming, ourCommit, timeout, rHash,
+			keyRing,
+		)
+	} else {
+		scriptInfo, err = genTaprootHtlcScript(
+			isIncoming, ourCommit, timeout, rHash, keyRing,
+		)
+	}
 	if err != nil {
 		return nil, nil, err
 	}
 
-	return htlcP2WSH, witnessScript, nil
+	return scriptInfo.PkScript, scriptInfo.WitnessScript, nil
 }
 
 // addHTLC adds a new HTLC to the passed commitment transaction. One of four
@@ -1055,7 +1214,7 @@ func addHTLC(commitTx *wire.MsgTx, ourCommit bool,
 	timeout := paymentDesc.Timeout
 	rHash := paymentDesc.RHash
 
-	p2wsh, witnessScript, err := genHtlcScript(
+	witnessProgram, witnessScript, err := genHtlcScript(
 		chanType, isIncoming, ourCommit, timeout, rHash, keyRing,
 	)
 	if err != nil {
@@ -1064,15 +1223,15 @@ func addHTLC(commitTx *wire.MsgTx, ourCommit bool,
 
 	// Add the new HTLC outputs to the respective commitment transactions.
 	amountPending := int64(paymentDesc.Amount.ToSatoshis())
-	commitTx.AddTxOut(wire.NewTxOut(amountPending, p2wsh))
+	commitTx.AddTxOut(wire.NewTxOut(amountPending, witnessProgram))
 
 	// Store the pkScript of this particular PaymentDescriptor so we can
 	// quickly locate it within the commitment transaction later.
 	if ourCommit {
-		paymentDesc.ourPkScript = p2wsh
+		paymentDesc.ourPkScript = witnessProgram
 		paymentDesc.ourWitnessScript = witnessScript
 	} else {
-		paymentDesc.theirPkScript = p2wsh
+		paymentDesc.theirPkScript = witnessProgram
 		paymentDesc.theirWitnessScript = witnessScript
 	}
 
diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go
index 37f4d98c3..89cc36a96 100644
--- a/lnwallet/musig_session.go
+++ b/lnwallet/musig_session.go
@@ -2,15 +2,12 @@ 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"
@@ -19,17 +16,18 @@ import (
 	"github.com/lightningnetwork/lnd/shachain"
 )
 
-// commitType is an enum that denotes if this is the local or remote
+// MusigCommitType is an enum that denotes if this is the local or remote
 // commitment.
-type commitType uint8
+type MusigCommitType uint8
 
 const (
-	// localCommit denotes that this a session for the local commitment.
-	localCommit commitType = iota
+	// LocalMusigCommit denotes that this a session for the local
+	// commitment.
+	LocalMusigCommit MusigCommitType = iota
 
-	// remoteCommit denotes that this is a session for the remote
+	// RemoteMusigCommit denotes that this is a session for the remote
 	// commitment.
-	remoteCommit
+	RemoteMusigCommit
 )
 
 var (
@@ -198,7 +196,7 @@ type MusigSession struct {
 
 	// commitType tracks if this is the session for the local or remote
 	// commitment.
-	commitType commitType
+	commitType MusigCommitType
 }
 
 // NewPartialMusigSession creates a new musig2 session given only the
@@ -207,7 +205,7 @@ type MusigSession struct {
 func NewPartialMusigSession(verificationNonce musig2.Nonces,
 	localKey, remoteKey keychain.KeyDescriptor,
 	signer input.MuSig2Signer, inputTxOut *wire.TxOut,
-	commitType commitType) *MusigSession {
+	commitType MusigCommitType) *MusigSession {
 
 	signerKeys := []*btcec.PublicKey{localKey.PubKey, remoteKey.PubKey}
 
@@ -243,7 +241,7 @@ func (m *MusigSession) FinalizeSession(signingNonce musig2.Nonces) error {
 	// If we're making a session for the remote commitment, then the nonce
 	// we use to sign is actually will be the signing nonce for the
 	// session, and their nonce the verification nonce.
-	case remoteCommit:
+	case RemoteMusigCommit:
 		localNonce = m.nonces.SigningNonce
 		remoteNonce = m.nonces.VerificationNonce
 
@@ -251,7 +249,7 @@ func (m *MusigSession) FinalizeSession(signingNonce musig2.Nonces) error {
 	// commitment (to broadcast), so now our verification nonce is the one
 	// we've already generated, and we want to bind their new signing
 	// nonce.
-	case localCommit:
+	case LocalMusigCommit:
 		localNonce = m.nonces.VerificationNonce
 		remoteNonce = m.nonces.SigningNonce
 	}
@@ -307,7 +305,7 @@ func (m *MusigSession) SignCommit(tx *wire.MsgTx) (*MusigPartialSig, error) {
 	switch {
 	// If we already have a session, then we don't need to finalize as this
 	// was done up front (symmetric nonce case, like for co-op close).
-	case m.session == nil && m.commitType == remoteCommit:
+	case m.session == nil && m.commitType == RemoteMusigCommit:
 		// Before we can sign a new commitment, we'll need to generate
 		// a fresh nonce that'll be sent along side our signature. With
 		// the nonce in hand, we can finalize the session.
@@ -407,6 +405,23 @@ func WithLocalCounterNonce(targetHeight uint64,
 	}
 }
 
+// invalidPartialSigError is used to return additional debug information to a
+// caller that encounters an invalid partial sig.
+type invalidPartialSigError struct {
+	partialSig        []byte
+	sigHash           []byte
+	signingNonce      [musig2.PubNonceSize]byte
+	verificationNonce [musig2.PubNonceSize]byte
+}
+
+// Error returns the error string for the partial sig error.
+func (i invalidPartialSigError) Error() string {
+	return fmt.Sprintf("invalid partial sig: partial_sig=%x, "+
+		"sig_hash=%x, signing_nonce=%x, verification_nonce=%x",
+		i.partialSig, i.sigHash, i.signingNonce[:],
+		i.verificationNonce[:])
+}
+
 // VerifyCommitSig attempts to verify the passed partial signature against the
 // passed commitment transaction. A keyspend sighash is assumed to generate the
 // signed message. As we never re-use nonces, a new verification nonce (our
@@ -449,8 +464,17 @@ func (m *MusigSession) VerifyCommitSig(commitTx *wire.MsgTx,
 	walletLog.Infof("Verifying new musig2 sig for session=%x, nonce=%s",
 		m.session.SessionID[:], m.nonces.String())
 
+	if partialSig == nil {
+		return nil, fmt.Errorf("partial sig not set")
+	}
+
 	if !partialSig.Verify(sigHash, m.remoteKey.PubKey) {
-		return nil, fmt.Errorf("invalid partial commit sig")
+		return nil, &invalidPartialSigError{
+			partialSig:        partialSig.Serialize(),
+			sigHash:           sigHash,
+			verificationNonce: m.nonces.VerificationNonce.PubNonce,
+			signingNonce:      m.nonces.SigningNonce.PubNonce,
+		}
 	}
 
 	nonceOpts := []musig2.NonceGenOption{
@@ -535,11 +559,11 @@ func NewMusigPairSession(cfg *MusigSessionCfg) *MusigPairSession {
 	// the local+remote party.
 	localSession := NewPartialMusigSession(
 		cfg.LocalNonce, cfg.LocalKey, cfg.RemoteKey,
-		cfg.Signer, cfg.InputTxOut, localCommit,
+		cfg.Signer, cfg.InputTxOut, LocalMusigCommit,
 	)
 	remoteSession := NewPartialMusigSession(
 		cfg.RemoteNonce, cfg.LocalKey, cfg.RemoteKey,
-		cfg.Signer, cfg.InputTxOut, remoteCommit,
+		cfg.Signer, cfg.InputTxOut, RemoteMusigCommit,
 	)
 
 	return &MusigPairSession{
@@ -548,27 +572,3 @@ 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/revocation_producer.go b/lnwallet/revocation_producer.go
index 5945c2faf..0bf490d2f 100644
--- a/lnwallet/revocation_producer.go
+++ b/lnwallet/revocation_producer.go
@@ -3,6 +3,7 @@
 package lnwallet
 
 import (
+	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/keychain"
 	"github.com/lightningnetwork/lnd/shachain"
 )
@@ -50,7 +51,7 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation,
 	// Once we have the root, we can then generate our shachain producer
 	// and from that generate the per-commitment point.
 	shaChainRoot := shachain.NewRevocationProducer(revRoot)
-	taprootShaChainRoot, err := deriveMusig2Shachain(revRoot)
+	taprootShaChainRoot, err := channeldb.DeriveMusig2Shachain(shaChainRoot)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/lnwallet/sigpool.go b/lnwallet/sigpool.go
index 0ebc3a931..2424757f9 100644
--- a/lnwallet/sigpool.go
+++ b/lnwallet/sigpool.go
@@ -113,8 +113,6 @@ type SignJobResp struct {
 	Err error
 }
 
-// TODO(roasbeef); fix description
-
 // SigPool is a struct that is meant to allow the current channel state
 // machine to parallelize all signature generation and verification. This
 // struct is needed as _each_ HTLC when creating a commitment transaction
@@ -206,7 +204,11 @@ func (s *SigPool) poolWorker() {
 				}
 			}
 
+			// Use the sig mapper to go from the input.Signature
+			// into the serialized lnwire.Sig that we'll send
+			// across the wire.
 			sig, err := lnwire.NewSigFromSignature(rawSig)
+
 			select {
 			case sigMsg.Resp <- SignJobResp{
 				Sig: sig,
diff --git a/lnwallet/transactions.go b/lnwallet/transactions.go
index f8bbf83b3..f66312a39 100644
--- a/lnwallet/transactions.go
+++ b/lnwallet/transactions.go
@@ -41,9 +41,13 @@ var (
 // state transition to create another output which actually allows redemption
 // or revocation of an HTLC.
 //
-// In order to spend the HTLC output, the witness for the passed transaction
-// should be:
+// In order to spend the segwit v0 HTLC output, the witness for the passed
+// transaction should be:
 //   - <0> <sender sig> <recvr sig> <preimage>
+//
+// In order to spend the segwit v1 (taproot) HTLC output, the witness for the
+// passed transaction should be:
+//   - <sender sig> <receiver sig> <preimage> <success_script> <control_block>
 func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool,
 	htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, csvDelay,
 	leaseExpiry uint32, revocationKey, delayKey *btcec.PublicKey) (
@@ -62,10 +66,12 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool,
 	}
 	successTx.AddTxIn(txin)
 
+	var pkScript []byte
+
 	// Next, we'll generate the script used as the output for all second
 	// level HTLC which forces a covenant w.r.t what can be done with all
 	// HTLC outputs.
-	script, err := SecondLevelHtlcScript(
+	scriptInfo, err := SecondLevelHtlcScript(
 		chanType, initiator, revocationKey, delayKey, csvDelay,
 		leaseExpiry,
 	)
@@ -73,11 +79,13 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool,
 		return nil, err
 	}
 
+	pkScript = scriptInfo.PkScript
+
 	// Finally, the output is simply the amount of the HTLC (minus the
 	// required fees), paying to the timeout script.
 	successTx.AddTxOut(&wire.TxOut{
 		Value:    int64(htlcAmt),
-		PkScript: script.PkScript,
+		PkScript: pkScript,
 	})
 
 	return successTx, nil
@@ -92,9 +100,13 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool,
 // transaction is locked with an absolute lock-time so the sender can only
 // attempt to claim the output using it after the lock time has passed.
 //
-// In order to spend the HTLC output, the witness for the passed transaction
-// should be:
-// * <0> <sender sig> <receiver sig> <0>
+// In order to spend the HTLC output for segwit v0, the witness for the passed
+// transaction should be:
+//   - <0> <sender sig> <receiver sig> <0>
+//
+// In order to spend the HTLC output for segwit v1, then witness for the passed
+// transaction should be:
+//   - <sender sig> <receiver sig> <timeout_script> <control_block>
 //
 // NOTE: The passed amount for the HTLC should take into account the required
 // fee rate at the time the HTLC was created. The fee should be able to
@@ -121,10 +133,12 @@ func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool,
 	}
 	timeoutTx.AddTxIn(txin)
 
+	var pkScript []byte
+
 	// Next, we'll generate the script used as the output for all second
 	// level HTLC which forces a covenant w.r.t what can be done with all
 	// HTLC outputs.
-	script, err := SecondLevelHtlcScript(
+	scriptInfo, err := SecondLevelHtlcScript(
 		chanType, initiator, revocationKey, delayKey, csvDelay,
 		leaseExpiry,
 	)
@@ -132,11 +146,13 @@ func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool,
 		return nil, err
 	}
 
+	pkScript = scriptInfo.PkScript
+
 	// Finally, the output is simply the amount of the HTLC (minus the
 	// required fees), paying to the regular second level HTLC script.
 	timeoutTx.AddTxOut(&wire.TxOut{
 		Value:    int64(htlcAmt),
-		PkScript: script.PkScript,
+		PkScript: pkScript,
 	})
 
 	return timeoutTx, nil
diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go
index 4f8f23fa7..33e3e250a 100644
--- a/lnwallet/wallet.go
+++ b/lnwallet/wallet.go
@@ -2373,27 +2373,43 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel,
 	if err != nil {
 		return err
 	}
-	signedCommitTx, err := channel.getSignedCommitTx()
-	if err != nil {
-		return err
-	}
+
+	localKey := channelState.LocalChanCfg.MultiSigKey.PubKey
+	remoteKey := channelState.RemoteChanCfg.MultiSigKey.PubKey
 
 	// We'll also need the multi-sig witness script itself so the
 	// chanvalidate package can check it for correctness against the
 	// funding transaction, and also commitment validity.
-	localKey := channelState.LocalChanCfg.MultiSigKey.PubKey
-	remoteKey := channelState.RemoteChanCfg.MultiSigKey.PubKey
-	witnessScript, err := input.GenMultiSigScript(
-		localKey.SerializeCompressed(),
-		remoteKey.SerializeCompressed(),
-	)
-	if err != nil {
-		return err
+	var fundingScript []byte
+	if channelState.ChanType.IsTaproot() {
+		fundingScript, _, err = input.GenTaprootFundingScript(
+			localKey, remoteKey, int64(channel.Capacity),
+		)
+		if err != nil {
+			return err
+		}
+	} else {
+		witnessScript, err := input.GenMultiSigScript(
+			localKey.SerializeCompressed(),
+			remoteKey.SerializeCompressed(),
+		)
+		if err != nil {
+			return err
+		}
+		fundingScript, err = input.WitnessScriptHash(witnessScript)
+		if err != nil {
+			return err
+		}
 	}
-	pkScript, err := input.WitnessScriptHash(witnessScript)
+
+	signedCommitTx, err := channel.getSignedCommitTx()
 	if err != nil {
 		return err
 	}
+	commitCtx := &chanvalidate.CommitmentContext{
+		Value:               channel.Capacity,
+		FullySignedCommitTx: signedCommitTx,
+	}
 
 	// Finally, we'll pass in all the necessary context needed to fully
 	// validate that this channel is indeed what we expect, and can be
@@ -2402,12 +2418,9 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel,
 		Locator: &chanvalidate.OutPointChanLocator{
 			ChanPoint: channelState.FundingOutpoint,
 		},
-		MultiSigPkScript: pkScript,
+		MultiSigPkScript: fundingScript,
 		FundingTx:        fundingTx,
-		CommitCtx: &chanvalidate.CommitmentContext{
-			Value:               channel.Capacity,
-			FullySignedCommitTx: signedCommitTx,
-		},
+		CommitCtx:        commitCtx,
 	})
 	if err != nil {
 		return err
diff --git a/peer/brontide.go b/peer/brontide.go
index d8ff01481..2330cc464 100644
--- a/peer/brontide.go
+++ b/peer/brontide.go
@@ -2945,6 +2945,7 @@ func (p *Brontide) createChanCloser(channel *lnwallet.LightningChannel,
 	chanCloser := chancloser.NewChanCloser(
 		chancloser.ChanCloseCfg{
 			Channel:      channel,
+			MusigSession: NewMusigChanCloser(channel),
 			FeeEstimator: &chancloser.SimpleCoopFeeEstimator{},
 			BroadcastTx:  p.cfg.Wallet.PublishTransaction,
 			DisableChannel: func(op wire.OutPoint) error {
diff --git a/peer/musig_chan_closer.go b/peer/musig_chan_closer.go
new file mode 100644
index 000000000..3e9b65497
--- /dev/null
+++ b/peer/musig_chan_closer.go
@@ -0,0 +1,129 @@
+package peer
+
+import (
+	"fmt"
+
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
+	"github.com/lightningnetwork/lnd/input"
+	"github.com/lightningnetwork/lnd/lnwallet"
+	"github.com/lightningnetwork/lnd/lnwallet/chancloser"
+	"github.com/lightningnetwork/lnd/lnwire"
+)
+
+// MusigChanCloser is an adapter over the normal channel state machine that
+// allows the chan closer to handle the musig2 details of closing taproot
+// channels.
+type MusigChanCloser struct {
+	channel *lnwallet.LightningChannel
+
+	musigSession *lnwallet.MusigSession
+
+	localNonce  *musig2.Nonces
+	remoteNonce *musig2.Nonces
+}
+
+// NewMusigChanCloser creates a new musig chan closer from a normal channel.
+func NewMusigChanCloser(channel *lnwallet.LightningChannel) *MusigChanCloser {
+	return &MusigChanCloser{
+		channel: channel,
+	}
+}
+
+// ProposalClosingOpts returns the options that should be used when
+// generating a new co-op close signature.
+func (m *MusigChanCloser) ProposalClosingOpts() (
+	[]lnwallet.ChanCloseOpt, error) {
+
+	switch {
+	case m.localNonce == nil:
+		return nil, fmt.Errorf("local nonce not generated")
+
+	case m.remoteNonce == nil:
+		return nil, fmt.Errorf("remote nonce not generated")
+	}
+
+	localKey, remoteKey := m.channel.MultiSigKeys()
+	m.musigSession = lnwallet.NewPartialMusigSession(
+		*m.localNonce, localKey, remoteKey,
+		m.channel.Signer, m.channel.FundingTxOut(),
+		lnwallet.RemoteMusigCommit,
+	)
+
+	err := m.musigSession.FinalizeSession(*m.remoteNonce)
+	if err != nil {
+		return nil, err
+	}
+
+	return []lnwallet.ChanCloseOpt{
+		lnwallet.WithCoopCloseMusigSession(m.musigSession),
+	}, nil
+}
+
+// CombineClosingOpts returns the options that should be used when combining
+// the final musig partial signature. The method also maps the lnwire partial
+// signatures into an input.Signature that can be used more generally.
+func (m *MusigChanCloser) CombineClosingOpts(localSig,
+	remoteSig lnwire.PartialSig) (input.Signature, input.Signature,
+	[]lnwallet.ChanCloseOpt, error) {
+
+	if m.musigSession == nil {
+		return nil, nil, nil, fmt.Errorf("musig session not created")
+	}
+
+	// We'll convert the wire partial signatures into an input.Signature
+	// compliant struct so we can pass it into the final combination
+	// function.
+	localPartialSig := &lnwire.PartialSigWithNonce{
+		PartialSig: localSig,
+		Nonce:      m.localNonce.PubNonce,
+	}
+	remotePartialSig := &lnwire.PartialSigWithNonce{
+		PartialSig: remoteSig,
+		Nonce:      m.remoteNonce.PubNonce,
+	}
+
+	localMuSig := new(lnwallet.MusigPartialSig).FromWireSig(
+		localPartialSig,
+	)
+	remoteMuSig := new(lnwallet.MusigPartialSig).FromWireSig(
+		remotePartialSig,
+	)
+
+	opts := []lnwallet.ChanCloseOpt{
+		lnwallet.WithCoopCloseMusigSession(m.musigSession),
+	}
+
+	// For taproot channels, we'll need to pass along the session so the
+	// final combined signature can be created.
+	return localMuSig, remoteMuSig, opts, nil
+}
+
+// ClosingNonce returns the nonce that should be used when generating the our
+// partial signature for the remote party.
+func (m *MusigChanCloser) ClosingNonce() (*musig2.Nonces, error) {
+	if m.localNonce != nil {
+		return m.localNonce, nil
+	}
+
+	localKey, _ := m.channel.MultiSigKeys()
+	nonce, err := musig2.GenNonces(
+		musig2.WithPublicKey(localKey.PubKey),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	m.localNonce = nonce
+
+	return nonce, nil
+}
+
+// InitRemoteNonce saves the remote nonce the party sent during their shutdown
+// message so it can be used later to generate and verify signatures.
+func (m *MusigChanCloser) InitRemoteNonce(nonce *musig2.Nonces) {
+	m.remoteNonce = nonce
+}
+
+// A compile-time assertion to ensure MusigChanCloser implements the
+// chancloser.MusigSession interface.
+var _ chancloser.MusigSession = (*MusigChanCloser)(nil)


Test patch:
diff --git a/contractcourt/breacharbiter_test.go b/contractcourt/breacharbiter_test.go
index 6d17135dc..becc4883b 100644
--- a/contractcourt/breacharbiter_test.go
+++ b/contractcourt/breacharbiter_test.go
@@ -978,7 +978,7 @@ func initBreachedState(t *testing.T) (*BreachArbiter,
 	if _, err := bob.ReceiveHTLC(htlc); err != nil {
 		t.Fatalf("bob unable to recv add htlc: %v", err)
 	}
-	if err := forceStateTransition(alice, bob); err != nil {
+	if err := lnwallet.ForceStateTransition(alice, bob); err != nil {
 		t.Fatalf("Can't update the channel state: %v", err)
 	}
 
@@ -996,7 +996,7 @@ func initBreachedState(t *testing.T) (*BreachArbiter,
 	if _, err := bob.ReceiveHTLC(htlc2); err != nil {
 		t.Fatalf("bob unable to recv add htlc: %v", err)
 	}
-	if err := forceStateTransition(alice, bob); err != nil {
+	if err := lnwallet.ForceStateTransition(alice, bob); err != nil {
 		t.Fatalf("Can't update the channel state: %v", err)
 	}
 
@@ -2436,45 +2436,3 @@ func createHTLC(data int, amount lnwire.MilliSatoshi) (*lnwire.UpdateAddHTLC, [3
 		Expiry:      uint32(5),
 	}, returnPreimage
 }
-
-// forceStateTransition executes the necessary interaction between the two
-// commitment state machines to transition to a new state locking in any
-// pending updates.
-// TODO(conner) remove code duplication
-func forceStateTransition(chanA, chanB *lnwallet.LightningChannel) error {
-	aliceSig, aliceHtlcSigs, _, err := chanA.SignNextCommitment()
-	if err != nil {
-		return err
-	}
-	if err = chanB.ReceiveNewCommitment(aliceSig, aliceHtlcSigs); err != nil {
-		return err
-	}
-
-	bobRevocation, _, _, err := chanB.RevokeCurrentCommitment()
-	if err != nil {
-		return err
-	}
-	bobSig, bobHtlcSigs, _, err := chanB.SignNextCommitment()
-	if err != nil {
-		return err
-	}
-
-	_, _, _, _, err = chanA.ReceiveRevocation(bobRevocation)
-	if err != nil {
-		return err
-	}
-	if err := chanA.ReceiveNewCommitment(bobSig, bobHtlcSigs); err != nil {
-		return err
-	}
-
-	aliceRevocation, _, _, err := chanA.RevokeCurrentCommitment()
-	if err != nil {
-		return err
-	}
-	_, _, _, _, err = chanB.ReceiveRevocation(aliceRevocation)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
diff --git a/contractcourt/chain_watcher_test.go b/contractcourt/chain_watcher_test.go
index 74559dbcf..489a60518 100644
--- a/contractcourt/chain_watcher_test.go
+++ b/contractcourt/chain_watcher_test.go
@@ -145,7 +145,7 @@ func TestChainWatcherRemoteUnilateralClosePendingCommit(t *testing.T) {
 
 	// With the HTLC added, we'll now manually initiate a state transition
 	// from Alice to Bob.
-	_, _, _, err = aliceChannel.SignNextCommitment()
+	_, err = aliceChannel.SignNextCommitment()
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/htlcswitch/link_isolated_test.go b/htlcswitch/link_isolated_test.go
index 7204a5873..6b616d2d8 100644
--- a/htlcswitch/link_isolated_test.go
+++ b/htlcswitch/link_isolated_test.go
@@ -93,14 +93,14 @@ func (l *linkTestContext) receiveHtlcAliceToBob() {
 func (l *linkTestContext) sendCommitSigBobToAlice(expHtlcs int) {
 	l.t.Helper()
 
-	sig, htlcSigs, _, err := l.bobChannel.SignNextCommitment()
+	sigs, err := l.bobChannel.SignNextCommitment()
 	if err != nil {
 		l.t.Fatalf("error signing commitment: %v", err)
 	}
 
 	commitSig := &lnwire.CommitSig{
-		CommitSig: sig,
-		HtlcSigs:  htlcSigs,
+		CommitSig: sigs.CommitSig,
+		HtlcSigs:  sigs.HtlcSigs,
 	}
 
 	if len(commitSig.HtlcSigs) != expHtlcs {
@@ -141,9 +141,10 @@ func (l *linkTestContext) receiveCommitSigAliceToBob(expHtlcs int) {
 
 	comSig := l.receiveCommitSigAlice(expHtlcs)
 
-	err := l.bobChannel.ReceiveNewCommitment(
-		comSig.CommitSig, comSig.HtlcSigs,
-	)
+	err := l.bobChannel.ReceiveNewCommitment(&lnwallet.CommitSigs{
+		CommitSig: comSig.CommitSig,
+		HtlcSigs:  comSig.HtlcSigs,
+	})
 	if err != nil {
 		l.t.Fatalf("bob failed receiving commitment: %v", err)
 	}
diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go
index 79eafb80e..ba1cc3871 100644
--- a/htlcswitch/link_test.go
+++ b/htlcswitch/link_test.go
@@ -1954,8 +1954,10 @@ func handleStateUpdate(link *channelLink,
 
 	// Let the remote channel receive the commit sig, and
 	// respond with a revocation + commitsig.
-	err := remoteChannel.ReceiveNewCommitment(
-		commitSig.CommitSig, commitSig.HtlcSigs)
+	err := remoteChannel.ReceiveNewCommitment(&lnwallet.CommitSigs{
+		CommitSig: commitSig.CommitSig,
+		HtlcSigs:  commitSig.HtlcSigs,
+	})
 	if err != nil {
 		return err
 	}
@@ -1966,13 +1968,13 @@ func handleStateUpdate(link *channelLink,
 	}
 	link.HandleChannelUpdate(remoteRev)
 
-	remoteSig, remoteHtlcSigs, _, err := remoteChannel.SignNextCommitment()
+	remoteSigs, err := remoteChannel.SignNextCommitment()
 	if err != nil {
 		return err
 	}
 	commitSig = &lnwire.CommitSig{
-		CommitSig: remoteSig,
-		HtlcSigs:  remoteHtlcSigs,
+		CommitSig: remoteSigs.CommitSig,
+		HtlcSigs:  remoteSigs.HtlcSigs,
 	}
 	link.HandleChannelUpdate(commitSig)
 
@@ -2017,14 +2019,14 @@ func updateState(batchTick chan time.Time, link *channelLink,
 
 	// The remote is triggering the state update, emulate this by
 	// signing and sending CommitSig to the link.
-	remoteSig, remoteHtlcSigs, _, err := remoteChannel.SignNextCommitment()
+	remoteSigs, err := remoteChannel.SignNextCommitment()
 	if err != nil {
 		return err
 	}
 
 	commitSig := &lnwire.CommitSig{
-		CommitSig: remoteSig,
-		HtlcSigs:  remoteHtlcSigs,
+		CommitSig: remoteSigs.CommitSig,
+		HtlcSigs:  remoteSigs.HtlcSigs,
 	}
 	link.HandleChannelUpdate(commitSig)
 
@@ -2057,8 +2059,10 @@ func updateState(batchTick chan time.Time, link *channelLink,
 		return fmt.Errorf("expected CommitSig, got %T", msg)
 	}
 
-	err = remoteChannel.ReceiveNewCommitment(
-		commitSig.CommitSig, commitSig.HtlcSigs)
+	err = remoteChannel.ReceiveNewCommitment(&lnwallet.CommitSigs{
+		CommitSig: commitSig.CommitSig,
+		HtlcSigs:  commitSig.HtlcSigs,
+	})
 	if err != nil {
 		return err
 	}
@@ -3131,7 +3135,10 @@ func TestChannelLinkTrimCircuitsRemoteCommit(t *testing.T) {
 			t.Fatalf("alice did not send commitment signature")
 		}
 
-		err := bobChan.ReceiveNewCommitment(sig.CommitSig, sig.HtlcSigs)
+		err := bobChan.ReceiveNewCommitment(&lnwallet.CommitSigs{
+			CommitSig: sig.CommitSig,
+			HtlcSigs:  sig.HtlcSigs,
+		})
 		if err != nil {
 			t.Fatalf("unable to receive new commitment: %v", err)
 		}
@@ -4719,9 +4726,10 @@ func TestChannelLinkNoEmptySig(t *testing.T) {
 	ctx.sendCommitSigBobToAlice(1)
 
 	// Now send Bob the signature from Alice covering both htlcs.
-	err = bobChannel.ReceiveNewCommitment(
-		commitSigAlice.CommitSig, commitSigAlice.HtlcSigs,
-	)
+	err = bobChannel.ReceiveNewCommitment(&lnwallet.CommitSigs{
+		CommitSig: commitSigAlice.CommitSig,
+		HtlcSigs:  commitSigAlice.HtlcSigs,
+	})
 	require.NoError(t, err, "bob failed receiving commitment")
 
 	// Both Alice and Bob revoke their previous commitment txes.
@@ -5323,8 +5331,7 @@ func TestChannelLinkFail(t *testing.T) {
 
 				// Sign a commitment that will include
 				// signature for the HTLC just sent.
-				sig, htlcSigs, _, err :=
-					remoteChannel.SignNextCommitment()
+				sigs, err := remoteChannel.SignNextCommitment()
 				if err != nil {
 					t.Fatalf("error signing commitment: %v",
 						err)
@@ -5333,8 +5340,8 @@ func TestChannelLinkFail(t *testing.T) {
 				// Remove the HTLC sig, such that the commit
 				// sig will be invalid.
 				commitSig := &lnwire.CommitSig{
-					CommitSig: sig,
-					HtlcSigs:  htlcSigs[1:],
+					CommitSig: sigs.CommitSig,
+					HtlcSigs:  sigs.HtlcSigs[1:],
 				}
 
 				c.HandleChannelUpdate(commitSig)
@@ -5365,8 +5372,7 @@ func TestChannelLinkFail(t *testing.T) {
 
 				// Sign a commitment that will include
 				// signature for the HTLC just sent.
-				sig, htlcSigs, _, err :=
-					remoteChannel.SignNextCommitment()
+				sigs, err := remoteChannel.SignNextCommitment()
 				if err != nil {
 					t.Fatalf("error signing commitment: %v",
 						err)
@@ -5374,7 +5380,7 @@ func TestChannelLinkFail(t *testing.T) {
 
 				// Flip a bit on the signature, rendering it
 				// invalid.
-				sigCopy := sig.Copy()
+				sigCopy := sigs.CommitSig.Copy()
 				copyBytes := sigCopy.RawBytes()
 				copyBytes[19] ^= 1
 				modifiedSig, err := lnwire.NewSigFromWireECDSA(
@@ -5383,7 +5389,7 @@ func TestChannelLinkFail(t *testing.T) {
 				require.NoError(t, err)
 				commitSig := &lnwire.CommitSig{
 					CommitSig: modifiedSig,
-					HtlcSigs:  htlcSigs,
+					HtlcSigs:  sigs.HtlcSigs,
 				}
 
 				c.HandleChannelUpdate(commitSig)
diff --git a/lnwallet/chancloser/chancloser_test.go b/lnwallet/chancloser/chancloser_test.go
index 6a9c8a2f0..e6c2b773d 100644
--- a/lnwallet/chancloser/chancloser_test.go
+++ b/lnwallet/chancloser/chancloser_test.go
@@ -4,7 +4,10 @@ import (
 	"bytes"
 	"fmt"
 	"testing"
+	"time"
 
+	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
 	"github.com/btcsuite/btcd/btcutil"
 	"github.com/btcsuite/btcd/chaincfg"
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -12,6 +15,9 @@ import (
 	"github.com/btcsuite/btcd/wire"
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/input"
+	"github.com/lightningnetwork/lnd/keychain"
+	"github.com/lightningnetwork/lnd/lnutils"
+	"github.com/lightningnetwork/lnd/lnwallet"
 	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
 	"github.com/lightningnetwork/lnd/lnwire"
 	"github.com/stretchr/testify/require"
@@ -135,6 +141,9 @@ type mockChannel struct {
 	chanPoint wire.OutPoint
 	initiator bool
 	scid      lnwire.ShortChannelID
+	chanType  channeldb.ChannelType
+	localKey  keychain.KeyDescriptor
+	remoteKey keychain.KeyDescriptor
 }
 
 func (m *mockChannel) ChannelPoint() *wire.OutPoint {
@@ -162,15 +171,26 @@ func (m *mockChannel) RemoteUpfrontShutdownScript() lnwire.DeliveryAddress {
 }
 
 func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount,
-	localScript, remoteScript []byte,
+	localScript, remoteScript []byte, _ ...lnwallet.ChanCloseOpt,
 ) (input.Signature, *chainhash.Hash, btcutil.Amount, error) {
 
+	if m.chanType.IsTaproot() {
+		return lnwallet.NewMusigPartialSig(
+			&musig2.PartialSignature{
+				S: new(btcec.ModNScalar),
+				R: new(btcec.PublicKey),
+			},
+			lnwire.Musig2Nonce{}, lnwire.Musig2Nonce{}, nil,
+		), nil, 0, nil
+	}
+
 	return nil, nil, 0, nil
 }
 
 func (m *mockChannel) CompleteCooperativeClose(localSig,
 	remoteSig input.Signature, localScript, remoteScript []byte,
-	proposedFee btcutil.Amount) (*wire.MsgTx, btcutil.Amount, error) {
+	proposedFee btcutil.Amount,
+	_ ...lnwallet.ChanCloseOpt) (*wire.MsgTx, btcutil.Amount, error) {
 
 	return nil, 0, nil
 }
@@ -183,6 +203,76 @@ func (m *mockChannel) RemoteBalanceDust() bool {
 	return false
 }
 
+func (m *mockChannel) ChanType() channeldb.ChannelType {
+	return m.chanType
+}
+
+func (m *mockChannel) FundingTxOut() *wire.TxOut {
+	return nil
+}
+
+func (m *mockChannel) MultiSigKeys() (
+	keychain.KeyDescriptor, keychain.KeyDescriptor) {
+
+	return m.localKey, m.remoteKey
+}
+
+func newMockTaprootChan(t *testing.T, initiator bool) *mockChannel {
+	taprootBits := channeldb.SimpleTaprootFeatureBit |
+		channeldb.AnchorOutputsBit |
+		channeldb.ZeroHtlcTxFeeBit |
+		channeldb.SingleFunderTweaklessBit
+
+	localKey, err := btcec.NewPrivateKey()
+	require.NoError(t, err)
+
+	remoteKey, err := btcec.NewPrivateKey()
+	require.NoError(t, err)
+
+	return &mockChannel{
+		chanPoint: wire.OutPoint{
+			Hash:  chainhash.Hash{},
+			Index: 0,
+		},
+		initiator: initiator,
+		chanType:  taprootBits,
+		localKey: keychain.KeyDescriptor{
+			PubKey: localKey.PubKey(),
+		},
+		remoteKey: keychain.KeyDescriptor{
+			PubKey: remoteKey.PubKey(),
+		},
+	}
+}
+
+type mockMusigSession struct {
+}
+
+func newMockMusigSession() *mockMusigSession {
+	return &mockMusigSession{}
+}
+
+func (m *mockMusigSession) ProposalClosingOpts() ([]lnwallet.ChanCloseOpt,
+	error) {
+
+	return nil, nil
+}
+
+func (m *mockMusigSession) CombineClosingOpts(localSig,
+	remoteSig lnwire.PartialSig,
+) (input.Signature, input.Signature, []lnwallet.ChanCloseOpt, error) {
+
+	return &lnwallet.MusigPartialSig{}, &lnwallet.MusigPartialSig{}, nil,
+		nil
+}
+
+func (m *mockMusigSession) ClosingNonce() (*musig2.Nonces, error) {
+	return &musig2.Nonces{}, nil
+}
+
+func (m *mockMusigSession) InitRemoteNonce(nonce *musig2.Nonces) {
+}
+
 type mockCoopFeeEstimator struct {
 	targetFee btcutil.Amount
 }
@@ -377,3 +467,130 @@ func TestParseUpfrontShutdownAddress(t *testing.T) {
 		})
 	}
 }
+
+func assertType[T any](t *testing.T, typ any) T {
+	value, ok := typ.(T)
+	require.True(t, ok)
+
+	return value
+}
+
+// TestTaprootFastClose tests that we are able to properly execute a fast close
+// (skip negotiation) for taproot channels.
+func TestTaprootFastClose(t *testing.T) {
+	t.Parallel()
+
+	aliceChan := newMockTaprootChan(t, true)
+	bobChan := newMockTaprootChan(t, false)
+
+	broadcastSignal := make(chan struct{}, 2)
+
+	idealFee := chainfee.SatPerKWeight(506)
+
+	// Next, we'll make a channel for Alice and Bob, with Alice being the
+	// initiator.
+	aliceCloser := NewChanCloser(
+		ChanCloseCfg{
+			Channel:      aliceChan,
+			MusigSession: newMockMusigSession(),
+			BroadcastTx: func(_ *wire.MsgTx, _ string) error {
+				broadcastSignal <- struct{}{}
+				return nil
+			},
+			MaxFee:       chainfee.SatPerKWeight(1000),
+			FeeEstimator: &SimpleCoopFeeEstimator{},
+			DisableChannel: func(wire.OutPoint) error {
+				return nil
+			},
+		}, nil, idealFee, 0, nil, true,
+	)
+	aliceCloser.initFeeBaseline()
+
+	bobCloser := NewChanCloser(
+		ChanCloseCfg{
+			Channel:      bobChan,
+			MusigSession: newMockMusigSession(),
+			MaxFee:       chainfee.SatPerKWeight(1000),
+			BroadcastTx: func(_ *wire.MsgTx, _ string) error {
+				broadcastSignal <- struct{}{}
+				return nil
+			},
+			FeeEstimator: &SimpleCoopFeeEstimator{},
+			DisableChannel: func(wire.OutPoint) error {
+				return nil
+			},
+		}, nil, idealFee, 0, nil, false,
+	)
+	bobCloser.initFeeBaseline()
+
+	// With our set up complete, we'll now initialize the shutdown
+	// procedure kicked off by Alice.
+	msg, err := aliceCloser.ShutdownChan()
+	require.NoError(t, err)
+	require.NotNil(t, msg)
+
+	// Bob will then process this message. As he's the responder, he should
+	// only send the shutdown message back to Alice.
+	bobMsgs, closeFinished, err := bobCloser.ProcessCloseMsg(msg)
+	require.NoError(t, err)
+	require.False(t, closeFinished)
+	require.Len(t, bobMsgs, 1)
+	require.IsType(t, &lnwire.Shutdown{}, bobMsgs[0])
+
+	// Alice should process the shutdown message, and create a closing
+	// signed of her own.
+	aliceMsgs, closeFinished, err := aliceCloser.ProcessCloseMsg(bobMsgs[0])
+	require.NoError(t, err)
+	require.False(t, closeFinished)
+	require.Len(t, aliceMsgs, 1)
+	require.IsType(t, &lnwire.ClosingSigned{}, aliceMsgs[0])
+
+	// Next, Bob will process the closing signed message, and send back a
+	// new one that should match exactly the offer Alice sent.
+	bobMsgs, closeFinished, err = bobCloser.ProcessCloseMsg(aliceMsgs[0])
+	require.NoError(t, err)
+	require.True(t, closeFinished)
+	require.Len(t, aliceMsgs, 1)
+	require.IsType(t, &lnwire.ClosingSigned{}, bobMsgs[0])
+
+	// At this point, Bob has accepted the offer, so he can broadcast the
+	// closing transaction, and considers the channel closed.
+	_, err = lnutils.RecvOrTimeout(broadcastSignal, time.Second*1)
+	require.NoError(t, err)
+
+	// Bob's fee proposal should exactly match Alice's initial fee.
+	aliceOffer := assertType[*lnwire.ClosingSigned](t, aliceMsgs[0])
+	bobOffer := assertType[*lnwire.ClosingSigned](t, bobMsgs[0])
+	require.Equal(t, aliceOffer.FeeSatoshis, bobOffer.FeeSatoshis)
+
+	// If we modify Bob's offer, and try to have Alice process it, then she
+	// should reject it.
+	ogOffer := bobOffer.FeeSatoshis
+	bobOffer.FeeSatoshis /= 2
+
+	_, _, err = aliceCloser.ProcessCloseMsg(bobOffer)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "was not accepted")
+
+	// We'll now restore the original offer before passing it on to Alice.
+	bobOffer.FeeSatoshis = ogOffer
+
+	// If we use the original offer, then Alice should accept this message,
+	// and finalize the shutdown process. We expect a message here as Alice
+	// will echo back the final message.
+	aliceMsgs, closeFinished, err = aliceCloser.ProcessCloseMsg(bobMsgs[0])
+	require.NoError(t, err)
+	require.True(t, closeFinished)
+	require.Len(t, aliceMsgs, 1)
+	require.IsType(t, &lnwire.ClosingSigned{}, aliceMsgs[0])
+
+	// Alice should now also broadcast her closing transaction.
+	_, err = lnutils.RecvOrTimeout(broadcastSignal, time.Second*1)
+	require.NoError(t, err)
+
+	// Finally, Bob will process Alice's echo message, and conclude.
+	bobMsgs, closeFinished, err = bobCloser.ProcessCloseMsg(aliceMsgs[0])
+	require.NoError(t, err)
+	require.True(t, closeFinished)
+	require.Len(t, bobMsgs, 0)
+}
diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go
index 93bf4c747..276b378bc 100644
--- a/lnwallet/channel_test.go
+++ b/lnwallet/channel_test.go
@@ -13,6 +13,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/chaincfg/chainhash"
 	"github.com/btcsuite/btcd/txscript"
@@ -59,7 +60,8 @@ func assertOutputExistsByValue(t *testing.T, commitTx *wire.MsgTx,
 
 // testAddSettleWorkflow tests a simple channel scenario where Alice and Bob
 // add, the settle an HTLC between themselves.
-func testAddSettleWorkflow(t *testing.T, tweakless,
+func testAddSettleWorkflow(t *testing.T, tweakless bool,
+	chanTypeModifier channeldb.ChannelType,
 	storeFinalHtlcResolutions bool) {
 
 	// Create a test channel which will be used for the duration of this
@@ -70,6 +72,10 @@ func testAddSettleWorkflow(t *testing.T, tweakless,
 		chanType = channeldb.SingleFunderBit
 	}
 
+	if chanTypeModifier != 0 {
+		chanType |= chanTypeModifier
+	}
+
 	aliceChannel, bobChannel, err := CreateTestChannels(
 		t, chanType,
 		channeldb.OptionStoreFinalHtlcResolutions(
@@ -100,13 +106,13 @@ func testAddSettleWorkflow(t *testing.T, tweakless,
 	// we expect the messages to be ordered, Bob will receive the HTLC we
 	// just sent before he receives this signature, so the signature will
 	// cover the HTLC.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "alice unable to sign commitment")
 
 	// Bob receives this signature message, and checks that this covers the
 	// state he has in his remote log. This includes the HTLC just sent
 	// from Alice.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "bob unable to process alice's new commitment")
 
 	// Bob revokes his prior commitment given to him by Alice, since he now
@@ -118,7 +124,7 @@ func testAddSettleWorkflow(t *testing.T, tweakless,
 	// This signature will cover the HTLC, since Bob will first send the
 	// revocation just created. The revocation also acks every received
 	// HTLC up to the point where Alice sent here signature.
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "bob unable to sign alice's commitment")
 
 	// Alice then processes this revocation, sending her own revocation for
@@ -138,7 +144,7 @@ func testAddSettleWorkflow(t *testing.T, tweakless,
 	// Alice then processes bob's signature, and since she just received
 	// the revocation, she expect this signature to cover everything up to
 	// the point where she sent her signature, including the HTLC.
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "alice unable to process bob's new commitment")
 
 	// Alice then generates a revocation for bob.
@@ -191,17 +197,26 @@ func testAddSettleWorkflow(t *testing.T, tweakless,
 			aliceChannel.currentHeight, 1)
 	}
 
+	aliceChanState := aliceChannel.channelState
+	bobChanState := bobChannel.channelState
+
 	// Both commitment transactions should have three outputs, and one of
 	// them should be exactly the amount of the HTLC.
-	if len(aliceChannel.channelState.LocalCommitment.CommitTx.TxOut) != 3 {
+	numOutputs := 3
+	if chanTypeModifier.HasAnchors() {
+		// In this case we expect two extra outputs as both sides need
+		// an anchor output.
+		numOutputs = 5
+	}
+	if len(aliceChanState.LocalCommitment.CommitTx.TxOut) != numOutputs {
 		t.Fatalf("alice should have three commitment outputs, instead "+
 			"have %v",
-			len(aliceChannel.channelState.LocalCommitment.CommitTx.TxOut))
+			len(aliceChanState.LocalCommitment.CommitTx.TxOut))
 	}
-	if len(bobChannel.channelState.LocalCommitment.CommitTx.TxOut) != 3 {
+	if len(bobChanState.LocalCommitment.CommitTx.TxOut) != numOutputs {
 		t.Fatalf("bob should have three commitment outputs, instead "+
 			"have %v",
-			len(bobChannel.channelState.LocalCommitment.CommitTx.TxOut))
+			len(bobChanState.LocalCommitment.CommitTx.TxOut))
 	}
 	assertOutputExistsByValue(t,
 		aliceChannel.channelState.LocalCommitment.CommitTx,
@@ -222,14 +237,14 @@ func testAddSettleWorkflow(t *testing.T, tweakless,
 		t.Fatalf("alice unable to accept settle of outbound htlc: %v", err)
 	}
 
-	bobSig2, bobHtlcSigs2, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err = bobChannel.SignNextCommitment()
 	require.NoError(t, err, "bob unable to sign settle commitment")
-	err = aliceChannel.ReceiveNewCommitment(bobSig2, bobHtlcSigs2)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "alice unable to process bob's new commitment")
 
 	aliceRevocation2, _, _, err := aliceChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "alice unable to generate revocation")
-	aliceSig2, aliceHtlcSigs2, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "alice unable to sign new commitment")
 
 	fwdPkg, _, _, _, err = bobChannel.ReceiveRevocation(aliceRevocation2)
@@ -243,7 +258,7 @@ func testAddSettleWorkflow(t *testing.T, tweakless,
 			"should forward none", len(fwdPkg.SettleFails))
 	}
 
-	err = bobChannel.ReceiveNewCommitment(aliceSig2, aliceHtlcSigs2)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "bob unable to process alice's new commitment")
 
 	bobRevocation2, _, finalHtlcs, err := bobChannel.
@@ -350,13 +365,29 @@ func TestSimpleAddSettleWorkflow(t *testing.T) {
 
 	for _, tweakless := range []bool{true, false} {
 		tweakless := tweakless
+
 		t.Run(fmt.Sprintf("tweakless=%v", tweakless), func(t *testing.T) {
-			testAddSettleWorkflow(t, tweakless, false)
+			testAddSettleWorkflow(t, tweakless, 0, false)
 		})
 	}
 
+	t.Run("anchors", func(t *testing.T) { //nolint:paralleltest
+		testAddSettleWorkflow(
+			t, true,
+			channeldb.AnchorOutputsBit|channeldb.ZeroHtlcTxFeeBit,
+			false,
+		)
+	})
+
+	t.Run("taproot", func(t *testing.T) { //nolint:paralleltest
+		testAddSettleWorkflow(
+			t, true, channeldb.SimpleTaprootFeatureBit, false,
+		)
+	})
+
+	//nolint:paralleltest
 	t.Run("storeFinalHtlcResolutions=true", func(t *testing.T) {
-		testAddSettleWorkflow(t, false, true)
+		testAddSettleWorkflow(t, false, 0, true)
 	})
 }
 
@@ -417,10 +448,10 @@ func TestChannelZeroAddLocalHeight(t *testing.T) {
 
 	// Bob should send a commitment signature to Alice.
 	// <----sig------
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err)
 
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err)
 
 	// Alice should reply with a revocation.
@@ -451,12 +482,12 @@ func TestChannelZeroAddLocalHeight(t *testing.T) {
 
 	// Bob should now send a commitment signature to Alice.
 	// <----sig-----
-	bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment()
+	bobNewCommit, err = bobChannel.SignNextCommitment()
 	require.NoError(t, err)
 
 	// Alice should accept the commitment. Previously she would
 	// force close here.
-	err = newAliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = newAliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err)
 }
 
@@ -596,10 +627,10 @@ func testCommitHTLCSigTieBreak(t *testing.T, restart bool) {
 	// tie-breaking for commitment sorting won't affect the commitment
 	// signed by Alice because received HTLC scripts commit to the CLTV
 	// directly, so the outputs will have different scriptPubkeys.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign alice's commitment")
 
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive alice's commitment")
 
 	bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment()
@@ -611,19 +642,20 @@ func testCommitHTLCSigTieBreak(t *testing.T, restart bool) {
 	// the offered HTLC scripts he adds for Alice will need to have the
 	// tie-breaking applied because the CLTV is not committed, but instead
 	// implicit via the construction of the second-level transactions.
-	bobSig, bobHtlcSigs, bobHtlcs, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign bob's commitment")
 
-	if len(bobHtlcs) != numHtlcs {
-		t.Fatalf("expected %d htlcs, got: %v", numHtlcs, len(bobHtlcs))
+	if len(bobNewCommit.PendingHTLCs) != numHtlcs {
+		t.Fatalf("expected %d htlcs, got: %v", numHtlcs,
+			len(bobNewCommit.PendingHTLCs))
 	}
 
 	// Ensure that our HTLCs appear in the reverse order from which they
 	// were added by inspecting each's outpoint index. We expect the output
 	// indexes to be in descending order, i.e. the first HTLC added had the
 	// highest CLTV and should end up last.
-	lastIndex := bobHtlcs[0].OutputIndex
-	for i, htlc := range bobHtlcs[1:] {
+	lastIndex := bobNewCommit.PendingHTLCs[0].OutputIndex
+	for i, htlc := range bobNewCommit.PendingHTLCs[1:] {
 		if htlc.OutputIndex >= lastIndex {
 			t.Fatalf("htlc %d output index %d is not descending",
 				i, htlc.OutputIndex)
@@ -657,7 +689,7 @@ func testCommitHTLCSigTieBreak(t *testing.T, restart bool) {
 
 	// Finally, have Alice validate the signatures to ensure that she is
 	// expecting the signatures in the proper order.
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive bob's commitment")
 }
 
@@ -1433,17 +1465,19 @@ func TestHTLCSigNumber(t *testing.T) {
 	// ===================================================================
 	aliceChannel, bobChannel := createChanWithHTLC(aboveDust, aboveDust)
 
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "Error signing next commitment")
 
-	if len(aliceHtlcSigs) != 2 {
+	if len(aliceNewCommit.HtlcSigs) != 2 {
 		t.Fatalf("expected 2 htlc sig, instead got %v",
-			len(aliceHtlcSigs))
+			len(aliceNewCommit.HtlcSigs))
 	}
 
 	// Now discard one signature from the htlcSig slice. Bob should reject
 	// the commitment because of this.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs[1:])
+	aliceNewCommitCopy := *aliceNewCommit
+	aliceNewCommitCopy.HtlcSigs = aliceNewCommitCopy.HtlcSigs[1:]
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommitCopy.CommitSigs)
 	if err == nil {
 		t.Fatalf("Expected Bob to reject signatures")
 	}
@@ -1454,17 +1488,19 @@ func TestHTLCSigNumber(t *testing.T) {
 	// ===================================================================
 	aliceChannel, bobChannel = createChanWithHTLC(aboveDust)
 
-	aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "Error signing next commitment")
 
-	if len(aliceHtlcSigs) != 1 {
+	if len(aliceNewCommit.HtlcSigs) != 1 {
 		t.Fatalf("expected 1 htlc sig, instead got %v",
-			len(aliceHtlcSigs))
+			len(aliceNewCommit.HtlcSigs))
 	}
 
 	// Now just give Bob an empty htlcSig slice. He should reject the
 	// commitment because of this.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, []lnwire.Sig{})
+	aliceCommitCopy := *aliceNewCommit.CommitSigs
+	aliceCommitCopy.HtlcSigs = []lnwire.Sig{}
+	err = bobChannel.ReceiveNewCommitment(&aliceCommitCopy)
 	if err == nil {
 		t.Fatalf("Expected Bob to reject signatures")
 	}
@@ -1474,17 +1510,17 @@ func TestHTLCSigNumber(t *testing.T) {
 	// ==============================================================
 	aliceChannel, bobChannel = createChanWithHTLC(belowDust)
 
-	aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "Error signing next commitment")
 
 	// Since the HTLC is below Bob's dust limit, Alice won't need to send
 	// any signatures for this HTLC.
-	if len(aliceHtlcSigs) != 0 {
+	if len(aliceNewCommit.HtlcSigs) != 0 {
 		t.Fatalf("expected no htlc sigs, instead got %v",
-			len(aliceHtlcSigs))
+			len(aliceNewCommit.HtlcSigs))
 	}
 
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "Bob failed receiving commitment")
 
 	// ================================================================
@@ -1492,17 +1528,17 @@ func TestHTLCSigNumber(t *testing.T) {
 	// ================================================================
 	aliceChannel, bobChannel = createChanWithHTLC(aboveDust)
 
-	aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "Error signing next commitment")
 
 	// Since the HTLC is above Bob's dust limit, Alice should send a
 	// signature for this HTLC.
-	if len(aliceHtlcSigs) != 1 {
+	if len(aliceNewCommit.HtlcSigs) != 1 {
 		t.Fatalf("expected 1 htlc sig, instead got %v",
-			len(aliceHtlcSigs))
+			len(aliceNewCommit.HtlcSigs))
 	}
 
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "Bob failed receiving commitment")
 
 	// ====================================================================
@@ -1513,20 +1549,22 @@ func TestHTLCSigNumber(t *testing.T) {
 
 	// Alice should produce only one signature, since one HTLC is below
 	// dust.
-	aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "Error signing next commitment")
 
-	if len(aliceHtlcSigs) != 1 {
+	if len(aliceNewCommit.HtlcSigs) != 1 {
 		t.Fatalf("expected 1 htlc sig, instead got %v",
-			len(aliceHtlcSigs))
+			len(aliceNewCommit.HtlcSigs))
 	}
 
 	// Add an extra signature.
-	aliceHtlcSigs = append(aliceHtlcSigs, aliceHtlcSigs[0])
+	aliceNewCommit.HtlcSigs = append(
+		aliceNewCommit.HtlcSigs, aliceNewCommit.HtlcSigs[0],
+	)
 
 	// Bob should reject these signatures since they don't match the number
 	// of HTLCs above dust.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	if err == nil {
 		t.Fatalf("Expected Bob to reject signatures")
 	}
@@ -2268,13 +2306,13 @@ func TestUpdateFeeFail(t *testing.T) {
 
 	// Alice sends signature for commitment that does not cover any fee
 	// update.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "alice unable to sign commitment")
 
 	// Bob verifies this commit, meaning that he checks that it is
 	// consistent everything he has received. This should fail, since he got
 	// the fee update, but Alice never sent it.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	if err == nil {
 		t.Fatalf("expected bob to fail receiving alice's signature")
 	}
@@ -2316,15 +2354,15 @@ func TestUpdateFeeConcurrentSig(t *testing.T) {
 	}
 
 	// Alice signs a commitment, and sends this to bob.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommits, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "alice unable to sign commitment")
 
 	// At the same time, Bob signs a commitment.
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommits, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "bob unable to sign alice's commitment")
 
 	// ...that Alice receives.
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommits.CommitSigs)
 	require.NoError(t, err, "alice unable to process bob's new commitment")
 
 	// Now let Bob receive the fee update + commitment that Alice sent.
@@ -2335,7 +2373,7 @@ func TestUpdateFeeConcurrentSig(t *testing.T) {
 	// Bob receives this signature message, and verifies that it is
 	// consistent with the state he had for Alice, including the received
 	// HTLC and fee update.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommits.CommitSigs)
 	require.NoError(t, err, "bob unable to process alice's new commitment")
 
 	if chainfee.SatPerKWeight(bobChannel.channelState.LocalCommitment.FeePerKw) == fee {
@@ -2392,13 +2430,13 @@ func TestUpdateFeeSenderCommits(t *testing.T) {
 	// Alice signs a commitment, which will cover everything sent to Bob
 	// (the HTLC and the fee update), and everything acked by Bob (nothing
 	// so far).
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommits, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "alice unable to sign commitment")
 
 	// Bob receives this signature message, and verifies that it is
 	// consistent with the state he had for Alice, including the received
 	// HTLC and fee update.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommits.CommitSigs)
 	require.NoError(t, err, "bob unable to process alice's new commitment")
 
 	if chainfee.SatPerKWeight(
@@ -2422,7 +2460,7 @@ func TestUpdateFeeSenderCommits(t *testing.T) {
 
 	// Bob commits to all updates he has received from Alice. This includes
 	// the HTLC he received, and the fee update.
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "bob unable to sign alice's commitment")
 
 	// Alice receives the revocation of the old one, and can now assume
@@ -2433,7 +2471,7 @@ func TestUpdateFeeSenderCommits(t *testing.T) {
 
 	// Alice receives new signature from Bob, and assumes this covers the
 	// changes.
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "alice unable to process bob's new commitment")
 
 	if chainfee.SatPerKWeight(
@@ -2501,12 +2539,12 @@ func TestUpdateFeeReceiverCommits(t *testing.T) {
 	// Bob commits to every change he has sent since last time (none). He
 	// does not commit to the received HTLC and fee update, since Alice
 	// cannot know if he has received them.
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "alice unable to sign commitment")
 
 	// Alice receives this signature message, and verifies that it is
 	// consistent with the remote state, not including any of the updates.
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "bob unable to process alice's new commitment")
 
 	// Alice can revoke the prior commitment she had, this will ack
@@ -2522,12 +2560,12 @@ func TestUpdateFeeReceiverCommits(t *testing.T) {
 	// Alice will sign next commitment. Since she sent the revocation, she
 	// also ack'ed everything received, but in this case this is nothing.
 	// Since she sent the two updates, this signature will cover those two.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "bob unable to sign alice's commitment")
 
 	// Bob gets the signature for the new commitment from Alice. He assumes
 	// this covers everything received from alice, including the two updates.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "alice unable to process bob's new commitment")
 
 	if chainfee.SatPerKWeight(
@@ -2552,7 +2590,7 @@ func TestUpdateFeeReceiverCommits(t *testing.T) {
 
 	// Bob will send a new signature, which will cover what he just acked:
 	// the HTLC and fee update.
-	bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment()
+	bobNewCommit, err = bobChannel.SignNextCommitment()
 	require.NoError(t, err, "alice unable to sign commitment")
 
 	// Alice receives revocation from Bob, and can now be sure that Bob
@@ -2562,7 +2600,7 @@ func TestUpdateFeeReceiverCommits(t *testing.T) {
 
 	// Alice will receive the signature from Bob, which will cover what was
 	// just acked by his revocation.
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "alice unable to process bob's new commitment")
 
 	if chainfee.SatPerKWeight(
@@ -2642,7 +2680,7 @@ func TestUpdateFeeMultipleUpdates(t *testing.T) {
 	// Alice signs a commitment, which will cover everything sent to Bob
 	// (the HTLC and the fee update), and everything acked by Bob (nothing
 	// so far).
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "alice unable to sign commitment")
 
 	bobChannel.ReceiveUpdateFee(fee1)
@@ -2652,7 +2690,7 @@ func TestUpdateFeeMultipleUpdates(t *testing.T) {
 	// Bob receives this signature message, and verifies that it is
 	// consistent with the state he had for Alice, including the received
 	// HTLC and fee update.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "bob unable to process alice's new commitment")
 
 	if chainfee.SatPerKWeight(
@@ -2688,7 +2726,7 @@ func TestUpdateFeeMultipleUpdates(t *testing.T) {
 
 	// Bob commits to all updates he has received from Alice. This includes
 	// the HTLC he received, and the fee update.
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "bob unable to sign alice's commitment")
 
 	// Alice receives the revocation of the old one, and can now assume that
@@ -2699,7 +2737,8 @@ func TestUpdateFeeMultipleUpdates(t *testing.T) {
 
 	// Alice receives new signature from Bob, and assumes this covers the
 	// changes.
-	if err := aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs); err != nil {
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
+	if err != nil {
 		t.Fatalf("alice unable to process bob's new commitment: %v", err)
 	}
 
@@ -2776,6 +2815,23 @@ func assertNoChanSyncNeeded(t *testing.T, aliceChannel *LightningChannel,
 		t.Fatalf("line #%v: unable to produce chan sync msg: %v",
 			line, err)
 	}
+	bobChanSyncMsg, err := bobChannel.channelState.ChanSyncMsg()
+	if err != nil {
+		t.Fatalf("line #%v: unable to produce chan sync msg: %v",
+			line, err)
+	}
+
+	// For taproot channels, simulate the link/peer binding the generated
+	// nonces.
+	if aliceChannel.channelState.ChanType.IsTaproot() {
+		aliceChannel.pendingVerificationNonce = &musig2.Nonces{
+			PubNonce: *aliceChanSyncMsg.LocalNonce,
+		}
+		bobChannel.pendingVerificationNonce = &musig2.Nonces{
+			PubNonce: *bobChanSyncMsg.LocalNonce,
+		}
+	}
+
 	bobMsgsToSend, _, _, err := bobChannel.ProcessChanSyncMsg(aliceChanSyncMsg)
 	if err != nil {
 		t.Fatalf("line #%v: unable to process ChannelReestablish "+
@@ -2786,11 +2842,6 @@ func assertNoChanSyncNeeded(t *testing.T, aliceChannel *LightningChannel,
 			"instead wants to send: %v", line, spew.Sdump(bobMsgsToSend))
 	}
 
-	bobChanSyncMsg, err := bobChannel.channelState.ChanSyncMsg()
-	if err != nil {
-		t.Fatalf("line #%v: unable to produce chan sync msg: %v",
-			line, err)
-	}
 	aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg(bobChanSyncMsg)
 	if err != nil {
 		t.Fatalf("line #%v: unable to process ChannelReestablish "+
@@ -2991,7 +3042,7 @@ func TestChanSyncOweCommitment(t *testing.T) {
 	// Now we'll begin the core of the test itself. Alice will extend a new
 	// commitment to Bob, but the connection drops before Bob can process
 	// it.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
 
 	// Bob doesn't get this message so upon reconnection, they need to
@@ -3058,20 +3109,22 @@ func TestChanSyncOweCommitment(t *testing.T) {
 			t.Fatalf("expected a CommitSig message, instead have %v",
 				spew.Sdump(aliceMsgsToSend[4]))
 		}
-		if commitSigMsg.CommitSig != aliceSig {
-			t.Fatalf("commit sig msgs don't match: expected %x got %x",
-				aliceSig, commitSigMsg.CommitSig)
+		if commitSigMsg.CommitSig != aliceNewCommit.CommitSig {
+			t.Fatalf("commit sig msgs don't match: expected "+
+				"%x got %x",
+				aliceNewCommit.CommitSig,
+				commitSigMsg.CommitSig)
 		}
-		if len(commitSigMsg.HtlcSigs) != len(aliceHtlcSigs) {
+		if len(commitSigMsg.HtlcSigs) != len(aliceNewCommit.HtlcSigs) {
 			t.Fatalf("wrong number of htlc sigs: expected %v, got %v",
-				len(aliceHtlcSigs), len(commitSigMsg.HtlcSigs))
+				len(aliceNewCommit.HtlcSigs),
+				len(commitSigMsg.HtlcSigs))
 		}
 		for i, htlcSig := range commitSigMsg.HtlcSigs {
-			if htlcSig != aliceHtlcSigs[i] {
+			if htlcSig != aliceNewCommit.HtlcSigs[i] {
 				t.Fatalf("htlc sig msgs don't match: "+
 					"expected %x got %x",
-					aliceHtlcSigs[i],
-					htlcSig)
+					aliceNewCommit.HtlcSigs[i], htlcSig)
 			}
 		}
 	}
@@ -3101,15 +3154,15 @@ func TestChanSyncOweCommitment(t *testing.T) {
 	// At this point, we should be able to resume the prior state update
 	// without any issues, resulting in Alice settling the 3 htlc's, and
 	// adding one of her own.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "bob unable to process alice's commitment")
 	bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "unable to revoke bob commitment")
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "bob unable to sign commitment")
 	_, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation)
 	require.NoError(t, err, "alice unable to recv revocation")
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "alice unable to rev bob's commitment")
 	aliceRevocation, _, _, err := aliceChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "alice unable to revoke commitment")
@@ -3249,12 +3302,13 @@ func TestChanSyncOweCommitmentPendingRemote(t *testing.T) {
 			t.Fatalf("unable to settle htlc: %v", err)
 		}
 
-		aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+		aliceNewCommit, err := aliceChannel.SignNextCommitment()
 		if err != nil {
 			t.Fatalf("unable to sign commitment: %v", err)
 		}
-
-		err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+		err = bobChannel.ReceiveNewCommitment(
+			aliceNewCommit.CommitSigs,
+		)
 		if err != nil {
 			t.Fatalf("unable to receive commitment: %v", err)
 		}
@@ -3280,16 +3334,17 @@ func TestChanSyncOweCommitmentPendingRemote(t *testing.T) {
 	require.NoError(t, err, "unable to restart bob")
 
 	// Bob signs the commitment he owes.
-	bobCommit, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
 
 	// This commitment is expected to contain no htlcs anymore.
-	if len(bobHtlcSigs) != 0 {
-		t.Fatalf("no htlcs expected, but got %v", len(bobHtlcSigs))
+	if len(bobNewCommit.HtlcSigs) != 0 {
+		t.Fatalf("no htlcs expected, but got %v",
+			len(bobNewCommit.HtlcSigs))
 	}
 
 	// Get Alice to revoke and trigger Bob to compact his logs.
-	err = aliceChannel.ReceiveNewCommitment(bobCommit, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -3303,19 +3358,14 @@ func TestChanSyncOweCommitmentPendingRemote(t *testing.T) {
 	}
 }
 
-// TestChanSyncOweRevocation tests that if Bob restarts (and then Alice) before
-// he receiver's Alice's RevokeAndAck message, then Alice concludes that she
-// needs to re-send the RevokeAndAck. After the revocation has been sent, both
-// nodes should be able to successfully complete another state transition.
-func TestChanSyncOweRevocation(t *testing.T) {
-	t.Parallel()
-
+// testChanSyncOweRevocation is the internal version of
+// TestChanSyncOweRevocation that is parameterized based on the type of channel
+// being used in the test.
+func testChanSyncOweRevocation(t *testing.T, chanType channeldb.ChannelType) {
 	// Create a test channel which will be used for the duration of this
 	// unittest. The channel will be funded evenly with Alice having 5 BTC,
 	// and Bob having 5 BTC.
-	aliceChannel, bobChannel, err := CreateTestChannels(
-		t, channeldb.SingleFunderTweaklessBit,
-	)
+	aliceChannel, bobChannel, err := CreateTestChannels(t, chanType)
 	require.NoError(t, err, "unable to create test channels")
 
 	chanID := lnwire.NewChanIDFromOutPoint(
@@ -3354,19 +3404,19 @@ func TestChanSyncOweRevocation(t *testing.T) {
 	//
 	// Alice signs the next state, then Bob receives and sends his
 	// revocation message.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "bob unable to process alice's commitment")
 
 	bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "unable to revoke bob commitment")
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "bob unable to sign commitment")
 
 	_, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation)
 	require.NoError(t, err, "alice unable to recv revocation")
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "alice unable to rev bob's commitment")
 
 	// At this point, we'll simulate the connection breaking down by Bob's
@@ -3382,8 +3432,23 @@ func TestChanSyncOweRevocation(t *testing.T) {
 	bobSyncMsg, err := bobChannel.channelState.ChanSyncMsg()
 	require.NoError(t, err, "unable to produce chan sync msg")
 
+	// For taproot channels, simulate the link/peer binding the generated
+	// nonces.
+	if chanType.IsTaproot() {
+		aliceChannel.pendingVerificationNonce = &musig2.Nonces{
+			PubNonce: *aliceSyncMsg.LocalNonce,
+		}
+		bobChannel.pendingVerificationNonce = &musig2.Nonces{
+			PubNonce: *bobSyncMsg.LocalNonce,
+		}
+	}
+
 	assertAliceOwesRevoke := func() {
-		aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg(bobSyncMsg)
+		t.Helper()
+
+		aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg(
+			bobSyncMsg,
+		)
 		if err != nil {
 			t.Fatalf("unable to process chan sync msg: %v", err)
 		}
@@ -3407,7 +3472,8 @@ func TestChanSyncOweRevocation(t *testing.T) {
 		}
 		if !reflect.DeepEqual(expectedRevocation, aliceReRevoke) {
 			t.Fatalf("wrong re-revocation: expected %v, got %v",
-				expectedRevocation, aliceReRevoke)
+				spew.Sdump(expectedRevocation),
+				spew.Sdump(aliceReRevoke))
 		}
 	}
 
@@ -3427,11 +3493,24 @@ func TestChanSyncOweRevocation(t *testing.T) {
 	// revocation message to Bob.
 	aliceChannel, err = restartChannel(aliceChannel)
 	require.NoError(t, err, "unable to restart alice")
+
+	// For taproot channels, simulate the link/peer binding the generated
+	// nonces.
+	if chanType.IsTaproot() {
+		aliceChannel.pendingVerificationNonce = &musig2.Nonces{
+			PubNonce: *aliceSyncMsg.LocalNonce,
+		}
+		bobChannel.pendingVerificationNonce = &musig2.Nonces{
+			PubNonce: *bobSyncMsg.LocalNonce,
+		}
+	}
+
 	assertAliceOwesRevoke()
 
 	// TODO(roasbeef): restart bob too???
 
-	// We'll continue by then allowing bob to process Alice's revocation message.
+	// We'll continue by then allowing bob to process Alice's revocation
+	// message.
 	_, _, _, _, err = bobChannel.ReceiveRevocation(aliceRevocation)
 	require.NoError(t, err, "bob unable to recv revocation")
 
@@ -3460,19 +3539,33 @@ func TestChanSyncOweRevocation(t *testing.T) {
 	assertNoChanSyncNeeded(t, aliceChannel, bobChannel)
 }
 
-// TestChanSyncOweRevocationAndCommit tests that if Alice initiates a state
-// transition with Bob and Bob sends both a RevokeAndAck and CommitSig message
-// but Alice doesn't receive them before the connection dies, then he'll
-// retransmit them both.
-func TestChanSyncOweRevocationAndCommit(t *testing.T) {
+// TestChanSyncOweRevocation tests that if Bob restarts (and then Alice) before
+// he receiver's Alice's RevokeAndAck message, then Alice concludes that she
+// needs to re-send the RevokeAndAck. After the revocation has been sent, both
+// nodes should be able to successfully complete another state transition.
+func TestChanSyncOweRevocation(t *testing.T) {
 	t.Parallel()
 
+	t.Run("tweakless", func(t *testing.T) { //nolint:paralleltest
+		testChanSyncOweRevocation(t, channeldb.SingleFunderTweaklessBit)
+	})
+	t.Run("taproot", func(t *testing.T) { //nolint:paralleltest
+		taprootBits := channeldb.SimpleTaprootFeatureBit |
+			channeldb.AnchorOutputsBit |
+			channeldb.ZeroHtlcTxFeeBit |
+			channeldb.SingleFunderTweaklessBit
+
+		testChanSyncOweRevocation(t, taprootBits)
+	})
+}
+
+func testChanSyncOweRevocationAndCommit(t *testing.T,
+	chanType channeldb.ChannelType) {
+
 	// Create a test channel which will be used for the duration of this
 	// unittest. The channel will be funded evenly with Alice having 5 BTC,
 	// and Bob having 5 BTC.
-	aliceChannel, bobChannel, err := CreateTestChannels(
-		t, channeldb.SingleFunderTweaklessBit,
-	)
+	aliceChannel, bobChannel, err := CreateTestChannels(t, chanType)
 	require.NoError(t, err, "unable to create test channels")
 
 	htlcAmt := lnwire.NewMSatFromSatoshis(20000)
@@ -3504,16 +3597,16 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) {
 
 	// Progressing the exchange: Alice will send her signature, Bob will
 	// receive, send a revocation and also a signature for Alice's state.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommits, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommits.CommitSigs)
 	require.NoError(t, err, "bob unable to process alice's commitment")
 
 	// Bob generates the revoke and sig message, but the messages don't
 	// reach Alice before the connection dies.
 	bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "unable to revoke bob commitment")
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "bob unable to sign commitment")
 
 	// If we now attempt to resync, then Alice should conclude that she
@@ -3524,6 +3617,17 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) {
 	bobSyncMsg, err := bobChannel.channelState.ChanSyncMsg()
 	require.NoError(t, err, "unable to produce chan sync msg")
 
+	// For taproot channels, simulate the link/peer binding the generated
+	// nonces.
+	if chanType.IsTaproot() {
+		aliceChannel.pendingVerificationNonce = &musig2.Nonces{
+			PubNonce: *aliceSyncMsg.LocalNonce,
+		}
+		bobChannel.pendingVerificationNonce = &musig2.Nonces{
+			PubNonce: *bobSyncMsg.LocalNonce,
+		}
+	}
+
 	aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg(bobSyncMsg)
 	require.NoError(t, err, "unable to process chan sync msg")
 	if len(aliceMsgsToSend) != 0 {
@@ -3532,7 +3636,11 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) {
 	}
 
 	assertBobSendsRevokeAndCommit := func() {
-		bobMsgsToSend, _, _, err := bobChannel.ProcessChanSyncMsg(aliceSyncMsg)
+		t.Helper()
+
+		bobMsgsToSend, _, _, err := bobChannel.ProcessChanSyncMsg(
+			aliceSyncMsg,
+		)
 		if err != nil {
 			t.Fatalf("unable to process chan sync msg: %v", err)
 		}
@@ -3542,32 +3650,40 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) {
 		}
 		bobReRevoke, ok := bobMsgsToSend[0].(*lnwire.RevokeAndAck)
 		if !ok {
-			t.Fatalf("expected bob to re-send revoke, instead sending: %v",
-				spew.Sdump(bobMsgsToSend[0]))
+			t.Fatalf("expected bob to re-send revoke, instead "+
+				"sending: %v", spew.Sdump(bobMsgsToSend[0]))
 		}
 		if !reflect.DeepEqual(bobReRevoke, bobRevocation) {
-			t.Fatalf("revocation msgs don't match: expected %v, got %v",
-				bobRevocation, bobReRevoke)
+			t.Fatalf("revocation msgs don't match: expected %v, "+
+				"got %v", bobRevocation, bobReRevoke)
 		}
 
 		bobReCommitSigMsg, ok := bobMsgsToSend[1].(*lnwire.CommitSig)
 		if !ok {
-			t.Fatalf("expected bob to re-send commit sig, instead sending: %v",
+			t.Fatalf("expected bob to re-send commit sig, "+
+				"instead sending: %v",
 				spew.Sdump(bobMsgsToSend[1]))
 		}
-		if bobReCommitSigMsg.CommitSig != bobSig {
-			t.Fatalf("commit sig msgs don't match: expected %x got %x",
-				bobSig, bobReCommitSigMsg.CommitSig)
+		if bobReCommitSigMsg.CommitSig != bobNewCommit.CommitSig {
+			t.Fatalf("commit sig msgs don't match: expected %x "+
+				"got %x",
+				bobNewCommit.CommitSigs.CommitSig,
+				bobReCommitSigMsg.CommitSig)
 		}
-		if len(bobReCommitSigMsg.HtlcSigs) != len(bobHtlcSigs) {
-			t.Fatalf("wrong number of htlc sigs: expected %v, got %v",
-				len(bobHtlcSigs), len(bobReCommitSigMsg.HtlcSigs))
+		if len(bobReCommitSigMsg.HtlcSigs) !=
+			len(bobNewCommit.HtlcSigs) {
+
+			t.Fatalf("wrong number of htlc sigs: expected %v, "+
+				"got %v",
+				len(bobNewCommit.HtlcSigs),
+				len(bobReCommitSigMsg.HtlcSigs))
 		}
 		for i, htlcSig := range bobReCommitSigMsg.HtlcSigs {
-			if htlcSig != aliceHtlcSigs[i] {
+			if htlcSig != bobNewCommit.HtlcSigs[i] {
 				t.Fatalf("htlc sig msgs don't match: "+
 					"expected %x got %x",
-					bobHtlcSigs[i], htlcSig)
+					htlcSig,
+					bobNewCommit.HtlcSigs[i])
 			}
 		}
 	}
@@ -3580,35 +3696,61 @@ func TestChanSyncOweRevocationAndCommit(t *testing.T) {
 	// Bob. He should still re-send the exact same set of messages.
 	bobChannel, err = restartChannel(bobChannel)
 	require.NoError(t, err, "unable to restart channel")
+
+	// For taproot channels, simulate the link/peer binding the generated
+	// nonces.
+	if chanType.IsTaproot() {
+		aliceChannel.pendingVerificationNonce = &musig2.Nonces{
+			PubNonce: *aliceSyncMsg.LocalNonce,
+		}
+		bobChannel.pendingVerificationNonce = &musig2.Nonces{
+			PubNonce: *bobSyncMsg.LocalNonce,
+		}
+	}
+
 	assertBobSendsRevokeAndCommit()
 
 	// We'll now finish the state transition by having Alice process both
 	// messages, and send her final revocation.
 	_, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation)
 	require.NoError(t, err, "alice unable to recv revocation")
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
-	require.NoError(t, err, "alice unable to rev bob's commitment")
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
+	require.NoError(t, err, "alice unable to recv bob's commitment")
 	aliceRevocation, _, _, err := aliceChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "alice unable to revoke commitment")
 	_, _, _, _, err = bobChannel.ReceiveRevocation(aliceRevocation)
 	require.NoError(t, err, "bob unable to recv revocation")
 }
 
-// TestChanSyncOweRevocationAndCommitForceTransition tests that if Alice
-// initiates a state transition with Bob, but Alice fails to receive his
-// RevokeAndAck and the connection dies before Bob sends his CommitSig message,
-// then Bob will re-send her RevokeAndAck message. Bob will also send and
-// _identical_ CommitSig as he detects his commitment chain is ahead of
-// Alice's.
-func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) {
+// TestChanSyncOweRevocationAndCommit tests that if Alice initiates a state
+// transition with Bob and Bob sends both a RevokeAndAck and CommitSig message
+// but Alice doesn't receive them before the connection dies, then he'll
+// retransmit them both.
+func TestChanSyncOweRevocationAndCommit(t *testing.T) {
 	t.Parallel()
 
+	t.Run("tweakless", func(t *testing.T) { //nolint:paralleltest
+		testChanSyncOweRevocationAndCommit(
+			t, channeldb.SingleFunderTweaklessBit,
+		)
+	})
+	t.Run("taproot", func(t *testing.T) { //nolint:paralleltest
+		taprootBits := channeldb.SimpleTaprootFeatureBit |
+			channeldb.AnchorOutputsBit |
+			channeldb.ZeroHtlcTxFeeBit |
+			channeldb.SingleFunderTweaklessBit
+
+		testChanSyncOweRevocationAndCommit(t, taprootBits)
+	})
+}
+
+func testChanSyncOweRevocationAndCommitForceTransition(t *testing.T,
+	chanType channeldb.ChannelType) {
+
 	// Create a test channel which will be used for the duration of this
 	// unittest. The channel will be funded evenly with Alice having 5 BTC,
 	// and Bob having 5 BTC.
-	aliceChannel, bobChannel, err := CreateTestChannels(
-		t, channeldb.SingleFunderTweaklessBit,
-	)
+	aliceChannel, bobChannel, err := CreateTestChannels(t, chanType)
 	require.NoError(t, err, "unable to create test channels")
 
 	htlcAmt := lnwire.NewMSatFromSatoshis(20000)
@@ -3649,10 +3791,10 @@ func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) {
 	require.NoError(t, err, "unable to recv bob's htlc")
 
 	// Bob signs the new state update, and sends the signature to Alice.
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "bob unable to sign commitment")
 
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "alice unable to rev bob's commitment")
 
 	// Alice revokes her current state, but doesn't immediately send a
@@ -3673,9 +3815,9 @@ func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) {
 
 	// Progressing the exchange: Alice will send her signature, with Bob
 	// processing the new state locally.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommits, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommits.CommitSigs)
 	require.NoError(t, err, "bob unable to process alice's commitment")
 
 	// Bob then sends his revocation message, but before Alice can process
@@ -3692,6 +3834,17 @@ func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) {
 	bobSyncMsg, err := bobChannel.channelState.ChanSyncMsg()
 	require.NoError(t, err, "unable to produce chan sync msg")
 
+	// For taproot channels, simulate the link/peer binding the generated
+	// nonces.
+	if chanType.IsTaproot() {
+		aliceChannel.pendingVerificationNonce = &musig2.Nonces{
+			PubNonce: *aliceSyncMsg.LocalNonce,
+		}
+		bobChannel.pendingVerificationNonce = &musig2.Nonces{
+			PubNonce: *bobSyncMsg.LocalNonce,
+		}
+	}
+
 	aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg(bobSyncMsg)
 	require.NoError(t, err, "unable to process chan sync msg")
 	if len(aliceMsgsToSend) != 0 {
@@ -3766,14 +3919,29 @@ func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) {
 		}
 	}
 
+	// If this is a taproot channel, then we'll also have Bob generate their
+	// current nonce, and also process Alice's.
+	if chanType.IsTaproot() {
+		_, err = bobChannel.GenMusigNonces()
+		require.NoError(t, err)
+
+		aliceNonces, err := aliceChannel.GenMusigNonces()
+		require.NoError(t, err)
+
+		err = bobChannel.InitRemoteMusigNonces(aliceNonces)
+		require.NoError(t, err)
+	}
+
 	// Now, we'll continue the exchange, sending Bob's revocation and
 	// signature message to Alice, ending with Alice sending her revocation
 	// message to Bob.
 	_, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation)
 	require.NoError(t, err, "alice unable to recv revocation")
-	err = aliceChannel.ReceiveNewCommitment(
-		bobSigMsg.CommitSig, bobSigMsg.HtlcSigs,
-	)
+	err = aliceChannel.ReceiveNewCommitment(&CommitSigs{
+		CommitSig:  bobSigMsg.CommitSig,
+		HtlcSigs:   bobSigMsg.HtlcSigs,
+		PartialSig: bobSigMsg.PartialSig,
+	})
 	require.NoError(t, err, "alice unable to rev bob's commitment")
 	aliceRevocation, _, _, err = aliceChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "alice unable to revoke commitment")
@@ -3781,6 +3949,32 @@ func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) {
 	require.NoError(t, err, "bob unable to recv revocation")
 }
 
+// TestChanSyncOweRevocationAndCommitForceTransition tests that if Alice
+// initiates a state transition with Bob, but Alice fails to receive his
+// RevokeAndAck and the connection dies before Bob sends his CommitSig message,
+// then Bob will re-send her RevokeAndAck message. Bob will also send and
+// _identical_ CommitSig as he detects his commitment chain is ahead of
+// Alice's.
+func TestChanSyncOweRevocationAndCommitForceTransition(t *testing.T) {
+	t.Parallel()
+
+	t.Run("tweakless", func(t *testing.T) { //nolint:paralleltest
+		testChanSyncOweRevocationAndCommitForceTransition(
+			t, channeldb.SingleFunderTweaklessBit,
+		)
+	})
+	t.Run("taproot", func(t *testing.T) { //nolint:paralleltest
+		taprootBits := channeldb.SimpleTaprootFeatureBit |
+			channeldb.AnchorOutputsBit |
+			channeldb.ZeroHtlcTxFeeBit |
+			channeldb.SingleFunderTweaklessBit
+
+		testChanSyncOweRevocationAndCommitForceTransition(
+			t, taprootBits,
+		)
+	})
+}
+
 // TestChanSyncFailure tests the various scenarios during channel sync where we
 // should be able to detect that the channels cannot be synced because of
 // invalid state.
@@ -3858,11 +4052,11 @@ func TestChanSyncFailure(t *testing.T) {
 			t.Fatalf("unable to recv bob's htlc: %v", err)
 		}
 
-		aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+		aliceNewCommit, err := aliceChannel.SignNextCommitment()
 		if err != nil {
 			t.Fatalf("unable to sign next commit: %v", err)
 		}
-		err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+		err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 		if err != nil {
 			t.Fatalf("unable to receive commit sig: %v", err)
 		}
@@ -4078,7 +4272,7 @@ func TestChannelRetransmissionFeeUpdate(t *testing.T) {
 
 	// Now, Alice will send a new commitment to Bob, but we'll simulate a
 	// connection failure, so Bob doesn't get her signature.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
 
 	// Restart both channels to simulate a connection restart.
@@ -4137,19 +4331,20 @@ func TestChannelRetransmissionFeeUpdate(t *testing.T) {
 		t.Fatalf("expected a CommitSig message, instead have %v",
 			spew.Sdump(aliceMsgsToSend[1]))
 	}
-	if commitSigMsg.CommitSig != aliceSig {
+	if commitSigMsg.CommitSig != aliceNewCommit.CommitSig {
 		t.Fatalf("commit sig msgs don't match: expected %x got %x",
-			aliceSig, commitSigMsg.CommitSig)
+			aliceNewCommit.CommitSig, commitSigMsg.CommitSig)
 	}
-	if len(commitSigMsg.HtlcSigs) != len(aliceHtlcSigs) {
+	if len(commitSigMsg.HtlcSigs) != len(aliceNewCommit.HtlcSigs) {
 		t.Fatalf("wrong number of htlc sigs: expected %v, got %v",
-			len(aliceHtlcSigs), len(commitSigMsg.HtlcSigs))
+			len(aliceNewCommit.HtlcSigs),
+			len(commitSigMsg.HtlcSigs))
 	}
 	for i, htlcSig := range commitSigMsg.HtlcSigs {
-		if htlcSig != aliceHtlcSigs[i] {
+		if htlcSig != aliceNewCommit.HtlcSigs[i] {
 			t.Fatalf("htlc sig msgs don't match: "+
 				"expected %x got %x",
-				aliceHtlcSigs[i], htlcSig)
+				aliceNewCommit.HtlcSigs[i], htlcSig)
 		}
 	}
 
@@ -4159,15 +4354,15 @@ func TestChannelRetransmissionFeeUpdate(t *testing.T) {
 		t.Fatalf("unable to update fee for Bob's channel: %v", err)
 	}
 
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "bob unable to process alice's commitment")
 	bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "unable to revoke bob commitment")
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "bob unable to sign commitment")
 	_, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation)
 	require.NoError(t, err, "alice unable to recv revocation")
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "alice unable to rev bob's commitment")
 	aliceRevocation, _, _, err := aliceChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "alice unable to revoke commitment")
@@ -4304,7 +4499,7 @@ func TestFeeUpdateOldDiskFormat(t *testing.T) {
 
 	// Now, Alice will send a new commitment to Bob, but we'll simulate a
 	// connection failure, so Bob doesn't get the signature.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommitSig, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
 
 	// Before restarting Alice, to mimic the old format, we fetch the
@@ -4357,15 +4552,15 @@ func TestFeeUpdateOldDiskFormat(t *testing.T) {
 
 	// We send Alice's commitment signatures, and finish the state
 	// transition.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommitSig.CommitSigs)
 	require.NoError(t, err, "bob unable to process alice's commitment")
 	bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "unable to revoke bob commitment")
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommitSigs, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "bob unable to sign commitment")
 	_, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation)
 	require.NoError(t, err, "alice unable to recv revocation")
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommitSigs.CommitSigs)
 	require.NoError(t, err, "alice unable to rev bob's commitment")
 	aliceRevocation, _, _, err := aliceChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "alice unable to revoke commitment")
@@ -4996,7 +5191,7 @@ func TestSignCommitmentFailNotLockedIn(t *testing.T) {
 
 	// If we now try to initiate a state update, then it should fail as
 	// Alice is unable to actually create a new state.
-	_, _, _, err = aliceChannel.SignNextCommitment()
+	_, err = aliceChannel.SignNextCommitment()
 	if err != ErrNoWindow {
 		t.Fatalf("expected ErrNoWindow, instead have: %v", err)
 	}
@@ -5034,11 +5229,11 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) {
 
 	// We'll now manually initiate a state transition between Alice and
 	// bob.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	if err != nil {
 		t.Fatal(err)
 	}
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -5063,12 +5258,12 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) {
 
 	// Now, have Bob initiate a transition to lock in the Adds sent by
 	// Alice.
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -5108,11 +5303,11 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) {
 
 	// We'll now initiate another state transition, but this time Bob will
 	// lead.
-	bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment()
+	bobNewCommit, err = bobChannel.SignNextCommitment()
 	if err != nil {
 		t.Fatal(err)
 	}
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -5143,11 +5338,11 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) {
 
 	// Now, begin another state transition led by Alice, and fail the second
 	// HTLC part-way through the dance.
-	aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	if err != nil {
 		t.Fatal(err)
 	}
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -5207,11 +5402,11 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) {
 
 	// Have Alice initiate a state transition, which does not include the
 	// HTLCs just re-added to the channel state.
-	aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	if err != nil {
 		t.Fatal(err)
 	}
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -5236,12 +5431,12 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) {
 	}
 
 	// Now initiate a final update from Bob to lock in the final Fail.
-	bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment()
+	bobNewCommit, err = bobChannel.SignNextCommitment()
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -5268,11 +5463,11 @@ func TestLockedInHtlcForwardingSkipAfterRestart(t *testing.T) {
 
 	// Finally, have Bob initiate a state transition that locks in the Fail
 	// added after the restart.
-	aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	if err != nil {
 		t.Fatal(err)
 	}
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -5326,19 +5521,21 @@ func TestInvalidCommitSigError(t *testing.T) {
 	}
 
 	// Alice will now attempt to initiate a state transition.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign new commit")
 
 	// Before the signature gets to Bob, we'll mutate it, such that the
 	// signature is now actually invalid.
-	aliceSigCopy := aliceSig.Copy()
+	aliceSigCopy := aliceNewCommit.CommitSig.Copy()
 	aliceSigCopyBytes := aliceSigCopy.RawBytes()
-	aliceSigCopyBytes[0] ^= 88
-	aliceSig, err = lnwire.NewSigFromWireECDSA(aliceSigCopyBytes)
+	aliceSigCopyBytes[0] ^= 80
+	aliceNewCommit.CommitSig, err = lnwire.NewSigFromWireECDSA(
+		aliceSigCopyBytes,
+	)
 	require.NoError(t, err)
 
 	// Bob should reject this new state, and return the proper error.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	if err == nil {
 		t.Fatalf("bob accepted invalid state but shouldn't have")
 	}
@@ -5530,7 +5727,7 @@ func TestChannelUnilateralClosePendingCommit(t *testing.T) {
 
 	// With the HTLC added, we'll now manually initiate a state transition
 	// from Alice to Bob.
-	_, _, _, err = aliceChannel.SignNextCommitment()
+	_, err = aliceChannel.SignNextCommitment()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -5770,9 +5967,9 @@ func TestMaxAcceptedHTLCs(t *testing.T) {
 	}
 
 	// Add a commitment to Bob's commitment chain.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign next commitment")
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to recv new commitment")
 
 	// The next HTLC should fail with ErrMaxHTLCNumber. The index is incremented
@@ -5875,18 +6072,18 @@ func TestMaxAsynchronousHtlcs(t *testing.T) {
 		t.Fatalf("unable to receive fail htlc: %v", err)
 	}
 
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign next commitment")
 
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive new commitment")
 
 	// Cover the HTLC referenced with id equal to numHTLCs-1 with a new
 	// signature (step 3).
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign next commitment")
 
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive new commitment")
 
 	// Both sides exchange revocations as in step 4 & 5.
@@ -5913,10 +6110,10 @@ func TestMaxAsynchronousHtlcs(t *testing.T) {
 
 	// Receiving the commitment should succeed as in step 7 since space was
 	// made.
-	aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign next commitment")
 
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive new commitment")
 }
 
@@ -6566,11 +6763,11 @@ func TestChannelRestoreUpdateLogs(t *testing.T) {
 	}
 
 	// Let Alice sign a new state, which will include the HTLC just sent.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
 
 	// Bob receives this commitment signature, and revokes his old state.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive commitment")
 	bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "unable to revoke commitment")
@@ -6596,7 +6793,7 @@ func TestChannelRestoreUpdateLogs(t *testing.T) {
 	// and remote commit chains are updated in an async fashion. Since the
 	// remote chain was updated with the latest state (since Bob sent the
 	// revocation earlier) we can keep advancing the remote commit chain.
-	aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
 
 	// After Alice has signed this commitment, her local commitment will
@@ -6740,9 +6937,9 @@ func TestChannelRestoreUpdateLogsFailedHTLC(t *testing.T) {
 	restoreAndAssert(t, aliceChannel, 1, 0, 0, 0)
 
 	// Bob sends a signature.
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive commitment")
 
 	// When Alice receives Bob's new commitment, the logs will stay the
@@ -6767,9 +6964,9 @@ func TestChannelRestoreUpdateLogsFailedHTLC(t *testing.T) {
 
 	// Now send a signature from Alice. This will give Bob a new commitment
 	// where the HTLC is removed.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive commitment")
 
 	// When sending a new commitment, Alice will add a pending commit to
@@ -6834,7 +7031,7 @@ func TestDuplicateFailRejection(t *testing.T) {
 
 	// We'll now have Bob sign a new commitment to lock in the HTLC fail
 	// for Alice.
-	_, _, _, err = bobChannel.SignNextCommitment()
+	_, err = bobChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commit")
 
 	// We'll now force a restart for Bob and Alice, so we can test the
@@ -6901,7 +7098,7 @@ func TestDuplicateSettleRejection(t *testing.T) {
 
 	// We'll now have Bob sign a new commitment to lock in the HTLC fail
 	// for Alice.
-	_, _, _, err = bobChannel.SignNextCommitment()
+	_, err = bobChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commit")
 
 	// We'll now force a restart for Bob and Alice, so we can test the
@@ -6985,7 +7182,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) {
 	}
 
 	// Let Alice sign a new state, which will include the HTLC just sent.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
 
 	// The HTLC should only be on the pending remote commitment, so the
@@ -6995,7 +7192,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) {
 	)
 
 	// Bob receives this commitment signature, and revokes his old state.
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive commitment")
 	bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "unable to revoke commitment")
@@ -7017,7 +7214,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) {
 
 	// Now let Bob send the commitment signature making the HTLC lock in on
 	// Alice's commitment.
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
 
 	// At this stage Bob has a pending remote commitment. Make sure
@@ -7025,7 +7222,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) {
 	// heights.
 	bobChannel = restoreAndAssertCommitHeights(t, bobChannel, true, 0, 1, 1)
 
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive commitment")
 	aliceRevocation, _, _, err := aliceChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "unable to revoke commitment")
@@ -7055,7 +7252,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) {
 
 	// Send a new signature from Alice to Bob, making Alice have a pending
 	// remote commitment.
-	aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
 
 	// A restoration should keep the add heights iof the first HTLC, and
@@ -7067,7 +7264,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) {
 		t, aliceChannel, false, 1, 0, 2,
 	)
 
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive commitment")
 	bobRevocation, _, _, err = bobChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "unable to revoke commitment")
@@ -7094,7 +7291,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) {
 
 	// Sign a new state for Alice, making Bob have a pending remote
 	// commitment.
-	bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment()
+	bobNewCommit, err = bobChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
 
 	// The signing of a new commitment for Alice should have given the new
@@ -7103,7 +7300,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) {
 	bobChannel = restoreAndAssertCommitHeights(t, bobChannel, true, 1, 2, 2)
 
 	// Alice should receive the commitment and send over a revocation.
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive commitment")
 	aliceRevocation, _, _, err = aliceChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "unable to revoke commitment")
@@ -7131,7 +7328,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) {
 	require.NoError(t, err, "unable to recv htlc cancel")
 
 	// Now Bob signs for the fail update.
-	bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment()
+	bobNewCommit, err = bobChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commitment")
 
 	// Bob has a pending commitment for Alice, it shouldn't affect the add
@@ -7140,7 +7337,7 @@ func TestChannelRestoreCommitHeight(t *testing.T) {
 	_ = restoreAndAssertCommitHeights(t, bobChannel, true, 1, 2, 2)
 
 	// Alice receives commitment, sends revocation.
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive commitment")
 	_, _, _, err = aliceChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "unable to revoke commitment")
@@ -7192,15 +7389,15 @@ func TestForceCloseBorkedState(t *testing.T) {
 	// Do the commitment dance until Bob sends a revocation so Alice is
 	// able to receive the revocation, and then also make a new state
 	// herself.
-	aliceSigs, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commit")
-	err = bobChannel.ReceiveNewCommitment(aliceSigs, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive commitment")
 	revokeMsg, _, _, err := bobChannel.RevokeCurrentCommitment()
 	require.NoError(t, err, "unable to revoke bob commitment")
-	bobSigs, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err, "unable to sign commit")
-	err = aliceChannel.ReceiveNewCommitment(bobSigs, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err, "unable to receive commitment")
 
 	// Now that we have a new Alice channel, we'll force close once to
@@ -7232,7 +7429,7 @@ func TestForceCloseBorkedState(t *testing.T) {
 	// We manually advance the commitment tail here since the above
 	// ReceiveRevocation call will fail before it's actually advanced.
 	aliceChannel.remoteCommitChain.advanceTail()
-	_, _, _, err = aliceChannel.SignNextCommitment()
+	_, err = aliceChannel.SignNextCommitment()
 	if err != channeldb.ErrChanBorked {
 		t.Fatalf("sign commitment should have failed: %v", err)
 	}
@@ -7501,11 +7698,11 @@ func TestChannelFeeRateFloor(t *testing.T) {
 	}
 
 	// Check that alice can still sign commitments.
-	sig, htlcSigs, _, err := alice.SignNextCommitment()
+	aliceNewCommit, err := alice.SignNextCommitment()
 	require.NoError(t, err, "alice unable to sign commitment")
 
 	// Check that bob can still receive commitments.
-	err = bob.ReceiveNewCommitment(sig, htlcSigs)
+	err = bob.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	if err != nil {
 		t.Fatalf("bob unable to process alice's new commitment: %v",
 			err)
@@ -8808,9 +9005,9 @@ func TestChannelUnsignedAckedFailure(t *testing.T) {
 
 	// Bob should send a commitment signature to Alice.
 	// <----sig------
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err)
 
 	// Alice should reply with a revocation.
@@ -8823,7 +9020,7 @@ func TestChannelUnsignedAckedFailure(t *testing.T) {
 	// Alice should sign the next commitment and go down before
 	// sending it.
 	// -----sig-----X
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err)
 
 	newAliceChannel, err := NewLightningChannel(
@@ -8834,7 +9031,7 @@ func TestChannelUnsignedAckedFailure(t *testing.T) {
 
 	// Bob receives Alice's signature.
 	// -----sig----->
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err)
 
 	// Bob revokes his current commitment and sends a revocation
@@ -8857,9 +9054,9 @@ func TestChannelUnsignedAckedFailure(t *testing.T) {
 	// Bob sends the final signature to Alice and Alice should not
 	// reject it, given that we properly restore the unsigned acked
 	// updates and therefore our update log is structured correctly.
-	bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment()
+	bobNewCommit, err = bobChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = newAliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = newAliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err)
 }
 
@@ -8918,9 +9115,9 @@ func TestChannelLocalUnsignedUpdatesFailure(t *testing.T) {
 
 	// Alice should send a commitment signature to Bob.
 	// -----sig---->
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err)
 
 	// Bob should reply with a revocation and Alice should save the fail as
@@ -8942,9 +9139,9 @@ func TestChannelLocalUnsignedUpdatesFailure(t *testing.T) {
 
 	// Bob sends the final signature and Alice should not reject it.
 	// <----sig-----
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = newAliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = newAliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err)
 }
 
@@ -9004,9 +9201,9 @@ func TestChannelSignedAckRegression(t *testing.T) {
 	require.NoError(t, err)
 
 	// -----sig---->
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err)
 
 	// <----rev-----
@@ -9016,9 +9213,9 @@ func TestChannelSignedAckRegression(t *testing.T) {
 	require.NoError(t, err)
 
 	// <----sig-----
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err)
 
 	// Create an HTLC that Alice will send to Bob.
@@ -9031,9 +9228,9 @@ func TestChannelSignedAckRegression(t *testing.T) {
 	require.NoError(t, err)
 
 	// -----sig---->
-	aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err)
 
 	// <----rev-----
@@ -9061,11 +9258,11 @@ func TestChannelSignedAckRegression(t *testing.T) {
 	// Bob should no longer fail to sign this commitment due to faulty
 	// update logs.
 	// <----sig-----
-	bobSig, bobHtlcSigs, _, err = newBobChannel.SignNextCommitment()
+	bobNewCommit, err = newBobChannel.SignNextCommitment()
 	require.NoError(t, err)
 
 	// Alice should receive the new commitment without hiccups.
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err)
 }
 
@@ -9135,9 +9332,9 @@ func TestIsChannelClean(t *testing.T) {
 	// removed from both commitments.
 
 	// ---sig--->
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err)
 	assertCleanOrDirty(false, aliceChannel, bobChannel, t)
 
@@ -9149,9 +9346,9 @@ func TestIsChannelClean(t *testing.T) {
 	assertCleanOrDirty(false, aliceChannel, bobChannel, t)
 
 	// <---sig---
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err)
 	assertCleanOrDirty(false, aliceChannel, bobChannel, t)
 
@@ -9170,9 +9367,9 @@ func TestIsChannelClean(t *testing.T) {
 	assertCleanOrDirty(false, aliceChannel, bobChannel, t)
 
 	// <---sig---
-	bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment()
+	bobNewCommit, err = bobChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err)
 	assertCleanOrDirty(false, aliceChannel, bobChannel, t)
 
@@ -9184,9 +9381,9 @@ func TestIsChannelClean(t *testing.T) {
 	assertCleanOrDirty(false, aliceChannel, bobChannel, t)
 
 	// ---sig--->
-	aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err)
 	assertCleanOrDirty(false, aliceChannel, bobChannel, t)
 
@@ -9208,9 +9405,9 @@ func TestIsChannelClean(t *testing.T) {
 	assertCleanOrDirty(false, aliceChannel, bobChannel, t)
 
 	// ---sig--->
-	aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
+	aliceNewCommit, err = aliceChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err)
 	assertCleanOrDirty(false, aliceChannel, bobChannel, t)
 
@@ -9222,9 +9419,9 @@ func TestIsChannelClean(t *testing.T) {
 	assertCleanOrDirty(false, aliceChannel, bobChannel, t)
 
 	// <---sig---
-	bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment()
+	bobNewCommit, err = bobChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err)
 	assertCleanOrDirty(false, aliceChannel, bobChannel, t)
 
@@ -9359,9 +9556,9 @@ func testGetDustSum(t *testing.T, chantype channeldb.ChannelType) {
 	checkDust(bobChannel, htlc2Amt, htlc2Amt)
 
 	// Alice signs for this HTLC and neither perspective should change.
-	aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
+	aliceNewCommit, err := aliceChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
+	err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
 	require.NoError(t, err)
 	checkDust(aliceChannel, htlc2Amt, htlc1Amt+htlc2Amt)
 	checkDust(bobChannel, htlc2Amt, htlc2Amt)
@@ -9378,9 +9575,9 @@ func testGetDustSum(t *testing.T, chantype channeldb.ChannelType) {
 
 	// The rest of the dance is completed and neither perspective should
 	// change.
-	bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
+	bobNewCommit, err := bobChannel.SignNextCommitment()
 	require.NoError(t, err)
-	err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
+	err = aliceChannel.ReceiveNewCommitment(bobNewCommit.CommitSigs)
 	require.NoError(t, err)
 	aliceRevocation, _, _, err := aliceChannel.RevokeCurrentCommitment()
 	require.NoError(t, err)
diff --git a/lnwallet/revocation_producer_itest.go b/lnwallet/revocation_producer_itest.go
index 824597176..a1b6a0551 100644
--- a/lnwallet/revocation_producer_itest.go
+++ b/lnwallet/revocation_producer_itest.go
@@ -4,6 +4,7 @@ package lnwallet
 
 import (
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
+	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/keychain"
 	"github.com/lightningnetwork/lnd/shachain"
 )
@@ -46,7 +47,9 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation,
 		}
 		shaChainRoot := shachain.NewRevocationProducer(*revRoot)
 
-		taprootShaChainRoot, err := deriveMusig2Shachain(*revRoot)
+		taprootShaChainRoot, err := channeldb.DeriveMusig2Shachain(
+			shaChainRoot,
+		)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -81,7 +84,9 @@ func (l *LightningWallet) nextRevocationProducer(res *ChannelReservation,
 	// Once we have the root, we can then generate our shachain producer
 	// and from that generate the per-commitment point.
 	shaChainRoot := shachain.NewRevocationProducer(revRoot)
-	taprootShaChainRoot, err := deriveMusig2Shachain(revRoot)
+	taprootShaChainRoot, err := channeldb.DeriveMusig2Shachain(
+		shaChainRoot,
+	)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go
index d28511feb..e33d563c3 100644
--- a/lnwallet/test_utils.go
+++ b/lnwallet/test_utils.go
@@ -418,6 +418,27 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType,
 // network by populating the initial revocation windows of the passed
 // commitment state machines.
 func initRevocationWindows(chanA, chanB *LightningChannel) error {
+	// If these are taproot chanenls, then we need to also simulate sending
+	// either FundingLocked or ChannelReestablish by calling
+	// InitRemoteMusigNonces for both sides.
+	if chanA.channelState.ChanType.IsTaproot() {
+		chanANonces, err := chanA.GenMusigNonces()
+		if err != nil {
+			return err
+		}
+		chanBNonces, err := chanB.GenMusigNonces()
+		if err != nil {
+			return err
+		}
+
+		if err := chanA.InitRemoteMusigNonces(chanBNonces); err != nil {
+			return err
+		}
+		if err := chanB.InitRemoteMusigNonces(chanANonces); err != nil {
+			return err
+		}
+	}
+
 	aliceNextRevoke, err := chanA.NextRevocationKey()
 	if err != nil {
 		return err
@@ -495,11 +516,12 @@ func calcStaticFee(chanType channeldb.ChannelType, numHTLCs int) btcutil.Amount
 // pending updates. This method is useful when testing interactions between two
 // live state machines.
 func ForceStateTransition(chanA, chanB *LightningChannel) error {
-	aliceSig, aliceHtlcSigs, _, err := chanA.SignNextCommitment()
+	aliceNewCommit, err := chanA.SignNextCommitment()
 	if err != nil {
 		return err
 	}
-	if err = chanB.ReceiveNewCommitment(aliceSig, aliceHtlcSigs); err != nil {
+	err = chanB.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
+	if err != nil {
 		return err
 	}
 
@@ -507,15 +529,17 @@ func ForceStateTransition(chanA, chanB *LightningChannel) error {
 	if err != nil {
 		return err
 	}
-	bobSig, bobHtlcSigs, _, err := chanB.SignNextCommitment()
+	bobNewCommit, err := chanB.SignNextCommitment()
 	if err != nil {
 		return err
 	}
 
-	if _, _, _, _, err := chanA.ReceiveRevocation(bobRevocation); err != nil {
+	_, _, _, _, err = chanA.ReceiveRevocation(bobRevocation)
+	if err != nil {
 		return err
 	}
-	if err := chanA.ReceiveNewCommitment(bobSig, bobHtlcSigs); err != nil {
+	err = chanA.ReceiveNewCommitment(bobNewCommit.CommitSigs)
+	if err != nil {
 		return err
 	}
 
@@ -523,7 +547,8 @@ func ForceStateTransition(chanA, chanB *LightningChannel) error {
 	if err != nil {
 		return err
 	}
-	if _, _, _, _, err := chanB.ReceiveRevocation(aliceRevocation); err != nil {
+	_, _, _, _, err = chanB.ReceiveRevocation(aliceRevocation)
+	if err != nil {
 		return err
 	}
 
diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go
index 329366bbb..a51eb726f 100644
--- a/lnwallet/transactions_test.go
+++ b/lnwallet/transactions_test.go
@@ -285,10 +285,10 @@ func testVectors(t *testing.T, chanType channeldb.ChannelType, test testCase) {
 
 	// Execute commit dance to arrive at the point where the local node has
 	// received the test commitment and the remote signature.
-	localSig, localHtlcSigs, _, err := localChannel.SignNextCommitment()
+	localNewCommit, err := localChannel.SignNextCommitment()
 	require.NoError(t, err, "local unable to sign commitment")
 
-	err = remoteChannel.ReceiveNewCommitment(localSig, localHtlcSigs)
+	err = remoteChannel.ReceiveNewCommitment(localNewCommit.CommitSigs)
 	require.NoError(t, err)
 
 	revMsg, _, _, err := remoteChannel.RevokeCurrentCommitment()
@@ -297,16 +297,24 @@ func testVectors(t *testing.T, chanType channeldb.ChannelType, test testCase) {
 	_, _, _, _, err = localChannel.ReceiveRevocation(revMsg)
 	require.NoError(t, err)
 
-	remoteSig, remoteHtlcSigs, _, err := remoteChannel.SignNextCommitment()
+	remoteNewCommit, err := remoteChannel.SignNextCommitment()
 	require.NoError(t, err)
 
-	require.Equal(t, test.RemoteSigHex, hex.EncodeToString(remoteSig.ToSignatureBytes()))
+	require.Equal(
+		t, test.RemoteSigHex,
+		hex.EncodeToString(
+			remoteNewCommit.CommitSig.ToSignatureBytes(),
+		),
+	)
 
-	for i, sig := range remoteHtlcSigs {
-		require.Equal(t, test.HtlcDescs[i].RemoteSigHex, hex.EncodeToString(sig.ToSignatureBytes()))
+	for i, sig := range remoteNewCommit.HtlcSigs {
+		require.Equal(
+			t, test.HtlcDescs[i].RemoteSigHex,
+			hex.EncodeToString(sig.ToSignatureBytes()),
+		)
 	}
 
-	err = localChannel.ReceiveNewCommitment(remoteSig, remoteHtlcSigs)
+	err = localChannel.ReceiveNewCommitment(remoteNewCommit.CommitSigs)
 	require.NoError(t, err)
 
 	_, _, _, err = localChannel.RevokeCurrentCommitment()
@@ -321,7 +329,10 @@ func testVectors(t *testing.T, chanType channeldb.ChannelType, test testCase) {
 	var txBytes bytes.Buffer
 	require.NoError(t, forceCloseSum.CloseTx.Serialize(&txBytes))
 
-	require.Equal(t, test.ExpectedCommitmentTxHex, hex.EncodeToString(txBytes.Bytes()))
+	require.Equal(
+		t, test.ExpectedCommitmentTxHex,
+		hex.EncodeToString(txBytes.Bytes()),
+	)
 
 	// Obtain the second level transactions that the local node's channel
 	// state machine has produced. Store them in a map indexed by commit tx