Repository: lightningnetwork/lnd
Pull request #7340 (2023-01-19 04:15:09)

[4/?] - input+lnwallet: prepare input package for funding logic, add new MusigSession abstraction 
## Change Description

Depends on https://github.com/lightningnetwork/lnd/pull/7333 and https://github.com/lightningnetwork/lnd/pull/7331. 

Only the last 3 commits are new. We'll also want to synchronize with #6974 as both the PRs update the deps to use the latest version of musig2. 

In this PR, we first do a small refactoring to extract the musig2 session logic into a new package + struct. This allows use to re-use it later in tests w/o creating the entire wallet system. 

Building on this, we then do some small prep to update the `chanfunding` package to be ready for the new `musig2` channels. 

The final change in this PR adds a new struct to encapsulate the new musig2 session logic for channels. A session can be created as soon as the verification nonce for the local party is known. At any time, we'll have two sessions: one for the remote commitment and one for the local commitment. A session can be finalized once the signing nonce (sent by the remote party) is known. When generating a signature, we'll use the remote party's verification nonce (the remote session), then generate a fresh nonce to complete the session. Similarly, when accepting a new state, we'll use the remote party's signing nonce to finalize the session, then verify the incoming signature. 

For our local session, we'll use a counter-based system to generate the nonces we send to the remote party. The underlying counter here is the commitment height, which is then used by the existing `shachain` producer to generate fresh, but deterministic randomness. This lets us not have to store the secret nonce to disk, instead we'll just re-generate the nonce and use the sig we have on disk to combine, then create the final witness to broadcast.

The next PR in this series uses this PR, and its dependents to implement the funding logic within the wallet itself (reservations). 

## Steps to Test

## 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: @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: @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
Roasbeef: Initial set of review comment addressed. Will request review again after I put up the unit tests. 
Roasbeef: Unit tests added, PTAL! 

Patch:
diff --git a/blockcache/blockcache.go b/blockcache/blockcache.go
index 6c1f5f74c..09d5d3395 100644
--- a/blockcache/blockcache.go
+++ b/blockcache/blockcache.go
@@ -14,7 +14,7 @@ import (
 // BlockCache is an lru cache for blocks.
 type BlockCache struct {
 	Cache     *lru.Cache[wire.InvVect, *neutrino.CacheableBlock]
-	HashMutex *multimutex.HashMutex
+	HashMutex *multimutex.Mutex[lntypes.Hash]
 }
 
 // NewBlockCache creates a new BlockCache with the given maximum capacity.
@@ -23,7 +23,7 @@ func NewBlockCache(capacity uint64) *BlockCache {
 		Cache: lru.NewCache[wire.InvVect, *neutrino.CacheableBlock](
 			capacity,
 		),
-		HashMutex: multimutex.NewHashMutex(),
+		HashMutex: multimutex.NewMutex[lntypes.Hash](),
 	}
 }
 
diff --git a/discovery/gossiper.go b/discovery/gossiper.go
index 71fc2ee8a..e41e3c03e 100644
--- a/discovery/gossiper.go
+++ b/discovery/gossiper.go
@@ -447,7 +447,7 @@ type AuthenticatedGossiper struct {
 	// goroutine per channel ID. This is done to ensure that when
 	// the gossiper is handling an announcement, the db state stays
 	// consistent between when the DB is first read until it's written.
-	channelMtx *multimutex.Mutex
+	channelMtx *multimutex.Mutex[uint64]
 
 	recentRejects *lru.Cache[rejectCacheKey, *cachedReject]
 
@@ -493,7 +493,7 @@ func New(cfg Config, selfKeyDesc *keychain.KeyDescriptor) *AuthenticatedGossiper
 		prematureChannelUpdates: lru.NewCache[uint64, *cachedNetworkMsg]( //nolint: lll
 			maxPrematureUpdates,
 		),
-		channelMtx: multimutex.NewMutex(),
+		channelMtx: multimutex.NewMutex[uint64](),
 		recentRejects: lru.NewCache[rejectCacheKey, *cachedReject](
 			maxRejectedUpdates,
 		),
diff --git a/htlcswitch/payment_result.go b/htlcswitch/payment_result.go
index 8d6cb5b3a..cd982b8bb 100644
--- a/htlcswitch/payment_result.go
+++ b/htlcswitch/payment_result.go
@@ -93,14 +93,14 @@ type networkResultStore struct {
 	// paymentIDMtx is a multimutex used to make sure the database and
 	// result subscribers map is consistent for each payment ID in case of
 	// concurrent callers.
-	paymentIDMtx *multimutex.Mutex
+	paymentIDMtx *multimutex.Mutex[uint64]
 }
 
 func newNetworkResultStore(db kvdb.Backend) *networkResultStore {
 	return &networkResultStore{
 		backend:      db,
 		results:      make(map[uint64][]chan *networkResult),
-		paymentIDMtx: multimutex.NewMutex(),
+		paymentIDMtx: multimutex.NewMutex[uint64](),
 	}
 }
 
diff --git a/input/musig2.go b/input/musig2.go
index 1ae73d8ce..d2ff0c1d4 100644
--- a/input/musig2.go
+++ b/input/musig2.go
@@ -48,9 +48,13 @@ type MuSig2Signer interface {
 	// public key of the local signing key. If nonces of other parties are
 	// already known, they can be submitted as well to reduce the number of
 	// method calls necessary later on.
+	//
+	// The set of sessionOpts are _optional_ and allow a caller to modify
+	// the generated sessions. As an example the local nonce might already
+	// be generated ahead of time.
 	MuSig2CreateSession(MuSig2Version, keychain.KeyLocator,
-		[]*btcec.PublicKey, *MuSig2Tweaks,
-		[][musig2.PubNonceSize]byte) (*MuSig2SessionInfo, error)
+		[]*btcec.PublicKey, *MuSig2Tweaks, [][musig2.PubNonceSize]byte,
+		...musig2.SessionOption) (*MuSig2SessionInfo, error)
 
 	// MuSig2RegisterNonces registers one or more public nonces of other
 	// signing participants for a session identified by its ID. This method
@@ -374,15 +378,20 @@ func combineKeysV040(allSignerPubKeys []*btcec.PublicKey, sortKeys bool,
 
 // MuSig2CreateContext creates a new MuSig2 signing context.
 func MuSig2CreateContext(bipVersion MuSig2Version, privKey *btcec.PrivateKey,
-	allSignerPubKeys []*btcec.PublicKey,
-	tweaks *MuSig2Tweaks) (MuSig2Context, MuSig2Session, error) {
+	allSignerPubKeys []*btcec.PublicKey, tweaks *MuSig2Tweaks,
+	sessionOpts ...musig2.SessionOption,
+) (MuSig2Context, MuSig2Session, error) {
 
 	switch bipVersion {
 	case MuSig2Version040:
-		return createContextV040(privKey, allSignerPubKeys, tweaks)
+		return createContextV040(
+			privKey, allSignerPubKeys, tweaks, sessionOpts...,
+		)
 
 	case MuSig2Version100RC2:
-		return createContextV100RC2(privKey, allSignerPubKeys, tweaks)
+		return createContextV100RC2(
+			privKey, allSignerPubKeys, tweaks, sessionOpts...,
+		)
 
 	default:
 		return nil, nil, fmt.Errorf("unknown MuSig2 version: <%d>",
@@ -393,8 +402,9 @@ func MuSig2CreateContext(bipVersion MuSig2Version, privKey *btcec.PrivateKey,
 // createContextV100RC2 implements the MuSig2CreateContext logic for the MuSig2
 // BIP draft version 1.0.0rc2.
 func createContextV100RC2(privKey *btcec.PrivateKey,
-	allSignerPubKeys []*btcec.PublicKey,
-	tweaks *MuSig2Tweaks) (*musig2.Context, *musig2.Session, error) {
+	allSignerPubKeys []*btcec.PublicKey, tweaks *MuSig2Tweaks,
+	sessionOpts ...musig2.SessionOption,
+) (*musig2.Context, *musig2.Session, error) {
 
 	// The context keeps track of all signing keys and our local key.
 	allOpts := append(
@@ -409,7 +419,7 @@ func createContextV100RC2(privKey *btcec.PrivateKey,
 			"context: %v", err)
 	}
 
-	muSigSession, err := muSigContext.NewSession()
+	muSigSession, err := muSigContext.NewSession(sessionOpts...)
 	if err != nil {
 		return nil, nil, fmt.Errorf("error creating MuSig2 signing "+
 			"session: %v", err)
@@ -421,9 +431,9 @@ func createContextV100RC2(privKey *btcec.PrivateKey,
 // createContextV040 implements the MuSig2CreateContext logic for the MuSig2 BIP
 // draft version 0.4.0.
 func createContextV040(privKey *btcec.PrivateKey,
-	allSignerPubKeys []*btcec.PublicKey,
-	tweaks *MuSig2Tweaks) (*musig2v040.Context, *musig2v040.Session,
-	error) {
+	allSignerPubKeys []*btcec.PublicKey, tweaks *MuSig2Tweaks,
+	sessionOpts ...musig2.SessionOption,
+) (*musig2v040.Context, *musig2v040.Session, error) {
 
 	// The context keeps track of all signing keys and our local key.
 	allOpts := append(
diff --git a/input/musig2_session_manager.go b/input/musig2_session_manager.go
new file mode 100644
index 000000000..3bf26004a
--- /dev/null
+++ b/input/musig2_session_manager.go
@@ -0,0 +1,303 @@
+package input
+
+import (
+	"crypto/sha256"
+	"fmt"
+
+	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
+	"github.com/lightningnetwork/lnd/keychain"
+	"github.com/lightningnetwork/lnd/multimutex"
+)
+
+// MuSig2State is a struct that holds on to the internal signing session state
+// of a MuSig2 session.
+type MuSig2State struct {
+	// MuSig2SessionInfo is the associated meta information of the signing
+	// session.
+	MuSig2SessionInfo
+
+	// context is the signing context responsible for keeping track of the
+	// public keys involved in the signing process.
+	context MuSig2Context
+
+	// session is the signing session responsible for keeping track of the
+	// nonces and partial signatures involved in the signing process.
+	session MuSig2Session
+}
+
+// PrivKeyFetcher is used to fetch a private key that matches a given key desc.
+type PrivKeyFetcher func(*keychain.KeyDescriptor) (*btcec.PrivateKey, error)
+
+// MusigSessionMusigSessionManager houses the state needed to manage concurrent
+// musig sessions. Each session is identified by a unique session ID which is
+// used by callers to interact with a given session.
+type MusigSessionManager struct {
+	keyFetcher PrivKeyFetcher
+
+	sessionMtx *multimutex.Mutex[MuSig2SessionID]
+
+	musig2Sessions map[MuSig2SessionID]*MuSig2State
+}
+
+// NewMusigSessionManager creates a new musig manager given an abstract key
+// fetcher.
+func NewMusigSessionManager(keyFetcher PrivKeyFetcher) *MusigSessionManager {
+	return &MusigSessionManager{
+		keyFetcher:     keyFetcher,
+		musig2Sessions: make(map[MuSig2SessionID]*MuSig2State),
+		sessionMtx:     multimutex.NewMutex[MuSig2SessionID](),
+	}
+}
+
+// MuSig2CreateSession creates a new MuSig2 signing session using the local key
+// identified by the key locator. The complete list of all public keys of all
+// signing parties must be provided, including the public key of the local
+// signing key. If nonces of other parties are already known, they can be
+// submitted as well to reduce the number of method calls necessary later on.
+//
+// The set of sessionOpts are _optional_ and allow a caller to modify the
+// generated sessions. As an example the local nonce might already be generated
+// ahead of time.
+func (m *MusigSessionManager) MuSig2CreateSession(bipVersion MuSig2Version,
+	keyLoc keychain.KeyLocator, allSignerPubKeys []*btcec.PublicKey,
+	tweaks *MuSig2Tweaks, otherSignerNonces [][musig2.PubNonceSize]byte,
+	sessionOpts ...musig2.SessionOption) (*MuSig2SessionInfo, error) {
+
+	// We need to derive the private key for signing. In the remote signing
+	// setup, this whole RPC call will be forwarded to the signing
+	// instance, which requires it to be stateful.
+	privKey, err := m.keyFetcher(&keychain.KeyDescriptor{
+		KeyLocator: keyLoc,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("error deriving private key: %w", err)
+	}
+
+	// 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,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error creating signing context: %w",
+			err)
+	}
+
+	// Add all nonces we might've learned so far.
+	haveAllNonces := false
+	for _, otherSignerNonce := range otherSignerNonces {
+		haveAllNonces, err = musigSession.RegisterPubNonce(
+			otherSignerNonce,
+		)
+		if err != nil {
+			return nil, fmt.Errorf("error registering other "+
+				"signer public nonce: %v", err)
+		}
+	}
+
+	// Register the new session.
+	combinedKey, err := musigContext.CombinedKey()
+	if err != nil {
+		return nil, fmt.Errorf("error getting combined key: %w", err)
+	}
+	session := &MuSig2State{
+		MuSig2SessionInfo: MuSig2SessionInfo{
+			SessionID: NewMuSig2SessionID(
+				combinedKey, musigSession.PublicNonce(),
+			),
+			Version:       bipVersion,
+			PublicNonce:   musigSession.PublicNonce(),
+			CombinedKey:   combinedKey,
+			TaprootTweak:  tweaks.HasTaprootTweak(),
+			HaveAllNonces: haveAllNonces,
+		},
+		context: musigContext,
+		session: musigSession,
+	}
+
+	// The internal key is only calculated if we are using a taproot tweak
+	// and need to know it for a potential script spend.
+	if tweaks.HasTaprootTweak() {
+		internalKey, err := musigContext.TaprootInternalKey()
+		if err != nil {
+			return nil, fmt.Errorf("error getting internal key: %w",
+				err)
+		}
+		session.TaprootInternalKey = internalKey
+	}
+
+	// Since we generate new nonces for every session, there is no way that
+	// a session with the same ID already exists. So even if we call the API
+	// twice with the same signers, we still get a new ID.
+	//
+	// We'll use just all zeroes as the session ID for the mutex, as this
+	// is a "global" action.
+	m.sessionMtx.Lock(MuSig2SessionID{})
+	m.musig2Sessions[session.SessionID] = session
+	m.sessionMtx.Unlock(MuSig2SessionID{})
+
+	return &session.MuSig2SessionInfo, nil
+}
+
+// MuSig2Sign creates a partial signature using the local signing key
+// that was specified when the session was created. This can only be
+// called when all public nonces of all participants are known and have
+// been registered with the session. If this node isn't responsible for
+// combining all the partial signatures, then the cleanup parameter
+// should be set, indicating that the session can be removed from memory
+// once the signature was produced.
+func (m *MusigSessionManager) MuSig2Sign(sessionID MuSig2SessionID,
+	msg [sha256.Size]byte, cleanUp bool) (*musig2.PartialSignature, error) {
+
+	// We hold the lock during the whole operation, we don't want any
+	// interference with calls that might come through in parallel for the
+	// same session.
+	m.sessionMtx.Lock(sessionID)
+	defer m.sessionMtx.Unlock(sessionID)
+
+	session, ok := m.musig2Sessions[sessionID]
+	if !ok {
+		return nil, fmt.Errorf("session with ID %x not found",
+			sessionID[:])
+	}
+
+	// We can only sign once we have all other signer's nonces.
+	if !session.HaveAllNonces {
+		return nil, fmt.Errorf("only have %d of %d required nonces",
+			session.session.NumRegisteredNonces(),
+			len(session.context.SigningKeys()))
+	}
+
+	// Create our own partial signature with the local signing key.
+	partialSig, err := MuSig2Sign(session.session, msg, true)
+	if err != nil {
+		return nil, fmt.Errorf("error signing with local key: %w", err)
+	}
+
+	// Clean up our local state if requested.
+	if cleanUp {
+		delete(m.musig2Sessions, sessionID)
+	}
+
+	return partialSig, nil
+}
+
+// MuSig2CombineSig combines the given partial signature(s) with the
+// local one, if it already exists. Once a partial signature of all
+// participants is registered, the final signature will be combined and
+// returned.
+func (m *MusigSessionManager) MuSig2CombineSig(sessionID MuSig2SessionID,
+	partialSigs []*musig2.PartialSignature) (*schnorr.Signature, bool,
+	error) {
+
+	// We hold the lock during the whole operation, we don't want any
+	// interference with calls that might come through in parallel for the
+	// same session.
+	m.sessionMtx.Lock(sessionID)
+	defer m.sessionMtx.Unlock(sessionID)
+
+	session, ok := m.musig2Sessions[sessionID]
+	if !ok {
+		return nil, false, fmt.Errorf("session with ID %x not found",
+			sessionID[:])
+	}
+
+	// Make sure we don't exceed the number of expected partial signatures
+	// as that would indicate something is wrong with the signing setup.
+	if session.HaveAllSigs {
+		return nil, true, fmt.Errorf("already have all partial" +
+			"signatures")
+	}
+
+	// Add all sigs we got so far.
+	var (
+		finalSig *schnorr.Signature
+		err      error
+	)
+	for _, otherPartialSig := range partialSigs {
+		session.HaveAllSigs, err = MuSig2CombineSig(
+			session.session, otherPartialSig,
+		)
+		if err != nil {
+			return nil, false, fmt.Errorf("error combining "+
+				"partial signature: %w", err)
+		}
+	}
+
+	// If we have all partial signatures, we should be able to get the
+	// complete signature now. We also remove this session from memory since
+	// there is nothing more left to do.
+	if session.HaveAllSigs {
+		finalSig = session.session.FinalSig()
+		delete(m.musig2Sessions, sessionID)
+	}
+
+	return finalSig, session.HaveAllSigs, nil
+}
+
+// MuSig2Cleanup removes a session from memory to free up resources.
+func (m *MusigSessionManager) MuSig2Cleanup(sessionID MuSig2SessionID) error {
+	// We hold the lock during the whole operation, we don't want any
+	// interference with calls that might come through in parallel for the
+	// same session.
+	m.sessionMtx.Lock(sessionID)
+	defer m.sessionMtx.Unlock(sessionID)
+
+	_, ok := m.musig2Sessions[sessionID]
+	if !ok {
+		return fmt.Errorf("session with ID %x not found", sessionID[:])
+	}
+
+	delete(m.musig2Sessions, sessionID)
+
+	return nil
+}
+
+// MuSig2RegisterNonces registers one or more public nonces of other signing
+// participants for a session identified by its ID. This method returns true
+// once we have all nonces for all other signing participants.
+func (m *MusigSessionManager) MuSig2RegisterNonces(sessionID MuSig2SessionID,
+	otherSignerNonces [][musig2.PubNonceSize]byte) (bool, error) {
+
+	// We hold the lock during the whole operation, we don't want any
+	// interference with calls that might come through in parallel for the
+	// same session.
+	m.sessionMtx.Lock(sessionID)
+	defer m.sessionMtx.Unlock(sessionID)
+
+	session, ok := m.musig2Sessions[sessionID]
+	if !ok {
+		return false, fmt.Errorf("session with ID %x not found",
+			sessionID[:])
+	}
+
+	// Make sure we don't exceed the number of expected nonces as that would
+	// indicate something is wrong with the signing setup.
+	if session.HaveAllNonces {
+		return true, fmt.Errorf("already have all nonces")
+	}
+
+	numSigners := len(session.context.SigningKeys())
+	remainingNonces := numSigners - session.session.NumRegisteredNonces()
+	if len(otherSignerNonces) > remainingNonces {
+		return false, fmt.Errorf("only %d other nonces remaining but "+
+			"trying to register %d more", remainingNonces,
+			len(otherSignerNonces))
+	}
+
+	// Add all nonces we've learned so far.
+	var err error
+	for _, otherSignerNonce := range otherSignerNonces {
+		session.HaveAllNonces, err = session.session.RegisterPubNonce(
+			otherSignerNonce,
+		)
+		if err != nil {
+			return false, fmt.Errorf("error registering other "+
+				"signer public nonce: %v", err)
+		}
+	}
+
+	return session.HaveAllNonces, nil
+}
diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go
index 8305dbb55..2c6f9c012 100644
--- a/lnwallet/btcwallet/btcwallet.go
+++ b/lnwallet/btcwallet/btcwallet.go
@@ -109,8 +109,7 @@ type BtcWallet struct {
 
 	blockCache *blockcache.BlockCache
 
-	musig2Sessions    map[input.MuSig2SessionID]*muSig2State
-	musig2SessionsMtx sync.Mutex
+	*input.MusigSessionManager
 }
 
 // A compile time check to ensure that BtcWallet implements the
@@ -172,16 +171,21 @@ func New(cfg Config, blockCache *blockcache.BlockCache) (*BtcWallet, error) {
 		}
 	}
 
-	return &BtcWallet{
-		cfg:            &cfg,
-		wallet:         wallet,
-		db:             wallet.Database(),
-		chain:          cfg.ChainSource,
-		netParams:      cfg.NetParams,
-		chainKeyScope:  chainKeyScope,
-		blockCache:     blockCache,
-		musig2Sessions: make(map[input.MuSig2SessionID]*muSig2State),
-	}, nil
+	finalWallet := &BtcWallet{
+		cfg:           &cfg,
+		wallet:        wallet,
+		db:            wallet.Database(),
+		chain:         cfg.ChainSource,
+		netParams:     cfg.NetParams,
+		chainKeyScope: chainKeyScope,
+		blockCache:    blockCache,
+	}
+
+	finalWallet.MusigSessionManager = input.NewMusigSessionManager(
+		finalWallet.fetchPrivKey,
+	)
+
+	return finalWallet, nil
 }
 
 // loaderCfg holds optional wallet loader configuration.
diff --git a/lnwallet/btcwallet/signer.go b/lnwallet/btcwallet/signer.go
index da624c726..986416476 100644
--- a/lnwallet/btcwallet/signer.go
+++ b/lnwallet/btcwallet/signer.go
@@ -1,13 +1,11 @@
 package btcwallet
 
 import (
-	"crypto/sha256"
 	"fmt"
 
 	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btcd/btcec/v2/ecdsa"
 	"github.com/btcsuite/btcd/btcec/v2/schnorr"
-	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
 	"github.com/btcsuite/btcd/btcutil"
 	"github.com/btcsuite/btcd/btcutil/hdkeychain"
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -469,267 +467,6 @@ func (b *BtcWallet) ComputeInputScript(tx *wire.MsgTx,
 	}, nil
 }
 
-// muSig2State is a struct that holds on to the internal signing session state
-// of a MuSig2 session.
-type muSig2State struct {
-	// MuSig2SessionInfo is the associated meta information of the signing
-	// session.
-	input.MuSig2SessionInfo
-
-	// context is the signing context responsible for keeping track of the
-	// public keys involved in the signing process.
-	context input.MuSig2Context
-
-	// session is the signing session responsible for keeping track of the
-	// nonces and partial signatures involved in the signing process.
-	session input.MuSig2Session
-}
-
-// MuSig2CreateSession creates a new MuSig2 signing session using the local
-// key identified by the key locator. The complete list of all public keys of
-// all signing parties must be provided, including the public key of the local
-// signing key. If nonces of other parties are already known, they can be
-// submitted as well to reduce the number of method calls necessary later on.
-func (b *BtcWallet) MuSig2CreateSession(bipVersion input.MuSig2Version,
-	keyLoc keychain.KeyLocator, allSignerPubKeys []*btcec.PublicKey,
-	tweaks *input.MuSig2Tweaks,
-	otherSignerNonces [][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo,
-	error) {
-
-	// We need to derive the private key for signing. In the remote signing
-	// setup, this whole RPC call will be forwarded to the signing
-	// instance, which requires it to be stateful.
-	privKey, err := b.fetchPrivKey(&keychain.KeyDescriptor{
-		KeyLocator: keyLoc,
-	})
-	if err != nil {
-		return nil, fmt.Errorf("error deriving private key: %w", err)
-	}
-
-	// Create a signing context and session with the given private key and
-	// list of all known signer public keys.
-	muSigContext, muSigSession, err := input.MuSig2CreateContext(
-		bipVersion, privKey, allSignerPubKeys, tweaks,
-	)
-	if err != nil {
-		return nil, fmt.Errorf("error creating signing context: %w",
-			err)
-	}
-
-	// Add all nonces we might've learned so far.
-	haveAllNonces := false
-	for _, otherSignerNonce := range otherSignerNonces {
-		haveAllNonces, err = muSigSession.RegisterPubNonce(
-			otherSignerNonce,
-		)
-		if err != nil {
-			return nil, fmt.Errorf("error registering other "+
-				"signer public nonce: %w", err)
-		}
-	}
-
-	// Register the new session.
-	combinedKey, err := muSigContext.CombinedKey()
-	if err != nil {
-		return nil, fmt.Errorf("error getting combined key: %w", err)
-	}
-	session := &muSig2State{
-		MuSig2SessionInfo: input.MuSig2SessionInfo{
-			SessionID: input.NewMuSig2SessionID(
-				combinedKey, muSigSession.PublicNonce(),
-			),
-			Version:       bipVersion,
-			PublicNonce:   muSigSession.PublicNonce(),
-			CombinedKey:   combinedKey,
-			TaprootTweak:  tweaks.HasTaprootTweak(),
-			HaveAllNonces: haveAllNonces,
-		},
-		context: muSigContext,
-		session: muSigSession,
-	}
-
-	// The internal key is only calculated if we are using a taproot tweak
-	// and need to know it for a potential script spend.
-	if tweaks.HasTaprootTweak() {
-		internalKey, err := muSigContext.TaprootInternalKey()
-		if err != nil {
-			return nil, fmt.Errorf("error getting internal key: %w",
-				err)
-		}
-		session.TaprootInternalKey = internalKey
-	}
-
-	// Since we generate new nonces for every session, there is no way that
-	// a session with the same ID already exists. So even if we call the API
-	// twice with the same signers, we still get a new ID.
-	b.musig2SessionsMtx.Lock()
-	b.musig2Sessions[session.SessionID] = session
-	b.musig2SessionsMtx.Unlock()
-
-	return &session.MuSig2SessionInfo, nil
-}
-
-// MuSig2RegisterNonces registers one or more public nonces of other signing
-// participants for a session identified by its ID. This method returns true
-// once we have all nonces for all other signing participants.
-func (b *BtcWallet) MuSig2RegisterNonces(sessionID input.MuSig2SessionID,
-	otherSignerNonces [][musig2.PubNonceSize]byte) (bool, error) {
-
-	// We hold the lock during the whole operation, we don't want any
-	// interference with calls that might come through in parallel for the
-	// same session.
-	b.musig2SessionsMtx.Lock()
-	defer b.musig2SessionsMtx.Unlock()
-
-	session, ok := b.musig2Sessions[sessionID]
-	if !ok {
-		return false, fmt.Errorf("session with ID %x not found",
-			sessionID[:])
-	}
-
-	// Make sure we don't exceed the number of expected nonces as that would
-	// indicate something is wrong with the signing setup.
-	if session.HaveAllNonces {
-		return true, fmt.Errorf("already have all nonces")
-	}
-
-	numSigners := len(session.context.SigningKeys())
-	remainingNonces := numSigners - session.session.NumRegisteredNonces()
-	if len(otherSignerNonces) > remainingNonces {
-		return false, fmt.Errorf("only %d other nonces remaining but "+
-			"trying to register %d more", remainingNonces,
-			len(otherSignerNonces))
-	}
-
-	// Add all nonces we've learned so far.
-	var err error
-	for _, otherSignerNonce := range otherSignerNonces {
-		session.HaveAllNonces, err = session.session.RegisterPubNonce(
-			otherSignerNonce,
-		)
-		if err != nil {
-			return false, fmt.Errorf("error registering other "+
-				"signer public nonce: %w", err)
-		}
-	}
-
-	return session.HaveAllNonces, nil
-}
-
-// MuSig2Sign creates a partial signature using the local signing key
-// that was specified when the session was created. This can only be
-// called when all public nonces of all participants are known and have
-// been registered with the session. If this node isn't responsible for
-// combining all the partial signatures, then the cleanup parameter
-// should be set, indicating that the session can be removed from memory
-// once the signature was produced.
-func (b *BtcWallet) MuSig2Sign(sessionID input.MuSig2SessionID,
-	msg [sha256.Size]byte, cleanUp bool) (*musig2.PartialSignature, error) {
-
-	// We hold the lock during the whole operation, we don't want any
-	// interference with calls that might come through in parallel for the
-	// same session.
-	b.musig2SessionsMtx.Lock()
-	defer b.musig2SessionsMtx.Unlock()
-
-	session, ok := b.musig2Sessions[sessionID]
-	if !ok {
-		return nil, fmt.Errorf("session with ID %x not found",
-			sessionID[:])
-	}
-
-	// We can only sign once we have all other signer's nonces.
-	if !session.HaveAllNonces {
-		return nil, fmt.Errorf("only have %d of %d required nonces",
-			session.session.NumRegisteredNonces(),
-			len(session.context.SigningKeys()))
-	}
-
-	// Create our own partial signature with the local signing key.
-	partialSig, err := input.MuSig2Sign(session.session, msg, true)
-	if err != nil {
-		return nil, fmt.Errorf("error signing with local key: %w", err)
-	}
-
-	// Clean up our local state if requested.
-	if cleanUp {
-		delete(b.musig2Sessions, sessionID)
-	}
-
-	return partialSig, nil
-}
-
-// MuSig2CombineSig combines the given partial signature(s) with the
-// local one, if it already exists. Once a partial signature of all
-// participants is registered, the final signature will be combined and
-// returned.
-func (b *BtcWallet) MuSig2CombineSig(sessionID input.MuSig2SessionID,
-	partialSigs []*musig2.PartialSignature) (*schnorr.Signature, bool,
-	error) {
-
-	// We hold the lock during the whole operation, we don't want any
-	// interference with calls that might come through in parallel for the
-	// same session.
-	b.musig2SessionsMtx.Lock()
-	defer b.musig2SessionsMtx.Unlock()
-
-	session, ok := b.musig2Sessions[sessionID]
-	if !ok {
-		return nil, false, fmt.Errorf("session with ID %x not found",
-			sessionID[:])
-	}
-
-	// Make sure we don't exceed the number of expected partial signatures
-	// as that would indicate something is wrong with the signing setup.
-	if session.HaveAllSigs {
-		return nil, true, fmt.Errorf("already have all partial" +
-			"signatures")
-	}
-
-	// Add all sigs we got so far.
-	var (
-		finalSig *schnorr.Signature
-		err      error
-	)
-	for _, otherPartialSig := range partialSigs {
-		session.HaveAllSigs, err = input.MuSig2CombineSig(
-			session.session, otherPartialSig,
-		)
-		if err != nil {
-			return nil, false, fmt.Errorf("error combining "+
-				"partial signature: %w", err)
-		}
-	}
-
-	// If we have all partial signatures, we should be able to get the
-	// complete signature now. We also remove this session from memory since
-	// there is nothing more left to do.
-	if session.HaveAllSigs {
-		finalSig = session.session.FinalSig()
-		delete(b.musig2Sessions, sessionID)
-	}
-
-	return finalSig, session.HaveAllSigs, nil
-}
-
-// MuSig2Cleanup removes a session from memory to free up resources.
-func (b *BtcWallet) MuSig2Cleanup(sessionID input.MuSig2SessionID) error {
-	// We hold the lock during the whole operation, we don't want any
-	// interference with calls that might come through in parallel for the
-	// same session.
-	b.musig2SessionsMtx.Lock()
-	defer b.musig2SessionsMtx.Unlock()
-
-	_, ok := b.musig2Sessions[sessionID]
-	if !ok {
-		return fmt.Errorf("session with ID %x not found", sessionID[:])
-	}
-
-	delete(b.musig2Sessions, sessionID)
-
-	return nil
-}
-
 // A compile time check to ensure that BtcWallet implements the Signer
 // interface.
 var _ input.Signer = (*BtcWallet)(nil)
diff --git a/lnwallet/chanfunding/assembler.go b/lnwallet/chanfunding/assembler.go
index 4e6e62d25..f569b42c8 100644
--- a/lnwallet/chanfunding/assembler.go
+++ b/lnwallet/chanfunding/assembler.go
@@ -78,6 +78,11 @@ type Request struct {
 	// ChangeAddr is a closure that will provide the Assembler with a
 	// change address for the funding transaction if needed.
 	ChangeAddr func() (btcutil.Address, error)
+
+	// Musig2 if true, then musig2 will be used to generate the funding
+	// output. By definition, this'll also use segwit v1 (taproot) for the
+	// funding output.
+	Musig2 bool
 }
 
 // Intent is returned by an Assembler and represents the base functionality the
diff --git a/lnwallet/chanfunding/canned_assembler.go b/lnwallet/chanfunding/canned_assembler.go
index 603d90fe2..cb9472f51 100644
--- a/lnwallet/chanfunding/canned_assembler.go
+++ b/lnwallet/chanfunding/canned_assembler.go
@@ -35,6 +35,11 @@ type ShimIntent struct {
 	// a normal channel. Until this height, it's considered frozen, so it
 	// can only be cooperatively closed by the responding party.
 	thawHeight uint32
+
+	// musig2 determines if the funding output should use musig2 to
+	// generate an aggregate key to use as the taproot-native multi-sig
+	// output.
+	musig2 bool
 }
 
 // FundingOutput returns the witness script, and the output that creates the
@@ -48,6 +53,19 @@ func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) {
 	}
 
 	totalAmt := s.localFundingAmt + s.remoteFundingAmt
+
+	// If musig2 is active, then we'll return a single aggregated key
+	// rather than using the "existing" funding script.
+	if s.musig2 {
+		// Similar to the existing p2wsh script, we'll always ensure
+		// the keys are sorted before use.
+		return input.GenTaprootFundingScript(
+			s.localKey.PubKey,
+			s.remoteKey,
+			int64(totalAmt),
+		)
+	}
+
 	return input.GenFundingPkScript(
 		s.localKey.PubKey.SerializeCompressed(),
 		s.remoteKey.SerializeCompressed(),
@@ -171,13 +189,20 @@ type CannedAssembler struct {
 	// a normal channel. Until this height, it's considered frozen, so it
 	// can only be cooperatively closed by the responding party.
 	thawHeight uint32
+
+	// musig2 determines if the funding output should use musig2 to
+	// generate an aggregate key to use as the taproot-native multi-sig
+	// output.
+	musig2 bool
 }
 
 // NewCannedAssembler creates a new CannedAssembler from the material required
 // to construct a funding output and channel point.
+//
+// TODO(roasbeef): pass in chan type instead?
 func NewCannedAssembler(thawHeight uint32, chanPoint wire.OutPoint,
 	fundingAmt btcutil.Amount, localKey *keychain.KeyDescriptor,
-	remoteKey *btcec.PublicKey, initiator bool) *CannedAssembler {
+	remoteKey *btcec.PublicKey, initiator, musig2 bool) *CannedAssembler {
 
 	return &CannedAssembler{
 		initiator:  initiator,
@@ -186,6 +211,7 @@ func NewCannedAssembler(thawHeight uint32, chanPoint wire.OutPoint,
 		fundingAmt: fundingAmt,
 		chanPoint:  chanPoint,
 		thawHeight: thawHeight,
+		musig2:     musig2,
 	}
 }
 
@@ -207,6 +233,7 @@ func (c *CannedAssembler) ProvisionChannel(req *Request) (Intent, error) {
 		remoteKey:  c.remoteKey,
 		chanPoint:  &c.chanPoint,
 		thawHeight: c.thawHeight,
+		musig2:     c.musig2,
 	}
 
 	if c.initiator {
diff --git a/lnwallet/chanfunding/psbt_assembler.go b/lnwallet/chanfunding/psbt_assembler.go
index 8632b175d..5f0bb88e7 100644
--- a/lnwallet/chanfunding/psbt_assembler.go
+++ b/lnwallet/chanfunding/psbt_assembler.go
@@ -523,6 +523,7 @@ func (p *PsbtAssembler) ProvisionChannel(req *Request) (Intent, error) {
 	intent := &PsbtIntent{
 		ShimIntent: ShimIntent{
 			localFundingAmt: p.fundingAmt,
+			musig2:          req.Musig2,
 		},
 		State:         PsbtShimRegistered,
 		BasePsbt:      p.basePsbt,
diff --git a/lnwallet/chanfunding/wallet_assembler.go b/lnwallet/chanfunding/wallet_assembler.go
index c81c1cb53..b78ea0deb 100644
--- a/lnwallet/chanfunding/wallet_assembler.go
+++ b/lnwallet/chanfunding/wallet_assembler.go
@@ -353,6 +353,7 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
 			ShimIntent: ShimIntent{
 				localFundingAmt:  localContributionAmt,
 				remoteFundingAmt: r.RemoteAmt,
+				musig2:           r.Musig2,
 			},
 			InputCoins: selectedCoins,
 			coinLocker: w.cfg.CoinLocker,
diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go
new file mode 100644
index 000000000..7683eca96
--- /dev/null
+++ b/lnwallet/musig_session.go
@@ -0,0 +1,547 @@
+package lnwallet
+
+import (
+	"bytes"
+	"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/txscript"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightningnetwork/lnd/input"
+	"github.com/lightningnetwork/lnd/keychain"
+	"github.com/lightningnetwork/lnd/lnwire"
+	"github.com/lightningnetwork/lnd/shachain"
+)
+
+// commitType is an enum that denotes if this is the local or remote
+// commitment.
+type commitType uint8
+
+const (
+	// localCommit denotes that this a session for the local commitment.
+	localCommit commitType = iota
+
+	// remoteCommit denotes that this is a session for the remote
+	// commitment.
+	remoteCommit
+)
+
+var (
+	// ErrSessionNotFinalized is returned when the SignCommit method is
+	// called for a local commitment, without the session being finalized
+	// (missing nonce).
+	ErrSessionNotFinalized = fmt.Errorf("musig2 session not finalized")
+)
+
+// MusigPartialSig is a wrapper around the base musig2.PartialSignature type
+// that also includes information about the set of nonces used, and also the
+// signer. This allows us to implement the input.Signature interface, as that
+// requires the ability to perform abstract verification based on a public key.
+type MusigPartialSig struct {
+	// sig is the actual musig2 partial signature.
+	sig *musig2.PartialSignature
+
+	// signerNonce is the nonce used by the signer to generate the partial
+	// signature.
+	signerNonce lnwire.Musig2Nonce
+
+	// combinedNonce is the combined nonce of all signers.
+	combinedNonce lnwire.Musig2Nonce
+
+	// signerKeys is the set of public keys of all signers.
+	signerKeys []*btcec.PublicKey
+}
+
+// NewMusigPartialSig creates a new musig partial signature.
+func NewMusigPartialSig(sig *musig2.PartialSignature,
+	signerNonce, combinedNonce lnwire.Musig2Nonce,
+	signerKeys []*btcec.PublicKey) *MusigPartialSig {
+
+	return &MusigPartialSig{
+		sig:           sig,
+		signerNonce:   signerNonce,
+		combinedNonce: combinedNonce,
+		signerKeys:    signerKeys,
+	}
+}
+
+// FromWireSig maps a wire partial sig to this internal type that we'll use to
+// perform signature validation.
+func (p *MusigPartialSig) FromWireSig(sig *lnwire.PartialSigWithNonce,
+) *MusigPartialSig {
+
+	p.sig = &musig2.PartialSignature{
+		S: &sig.Sig,
+	}
+	p.signerNonce = sig.Nonce
+
+	return p
+}
+
+// ToWireSig maps the partial signature to something that we can use to write
+// out for the wire protocol.
+func (p *MusigPartialSig) ToWireSig() *lnwire.PartialSigWithNonce {
+	return &lnwire.PartialSigWithNonce{
+		PartialSig: lnwire.NewPartialSig(*p.sig.S),
+		Nonce:      p.signerNonce,
+	}
+}
+
+// Serialize serializes the musig2 partial signature. The serializing includes
+// the signer's public nonce _and_ the partial signature. The final signature
+// is always 98 bytes in length.
+func (p *MusigPartialSig) Serialize() []byte {
+	var b bytes.Buffer
+
+	_ = p.ToWireSig().Encode(&b)
+
+	return b.Bytes()
+}
+
+// ToSchnorrShell converts the musig partial signature to a regular schnorr.
+// This schnorr signature uses a zero value for the 'r' field, so we're just
+// only using the last 32-bytes of the signature. This is useful when we need
+// to convert an HTLC schnorr signature into something we can send using the
+// existing messages.
+func (p *MusigPartialSig) ToSchnorrShell() *schnorr.Signature {
+	var zeroVal btcec.FieldVal
+	return schnorr.NewSignature(&zeroVal, p.sig.S)
+}
+
+// FromSchnorrShell takes a schnorr signature and parses out the last 32 bytes
+// as a normal musig2 partial signature.
+func (p *MusigPartialSig) FromSchnorrShell(sig *schnorr.Signature) {
+	var (
+		partialS      btcec.ModNScalar
+		partialSBytes [32]byte
+	)
+	copy(partialSBytes[:], sig.Serialize()[32:])
+	partialS.SetBytes(&partialSBytes)
+
+	p.sig = &musig2.PartialSignature{
+		S: &partialS,
+	}
+}
+
+// Verify attempts to verify the partial musig2 signature using the passed
+// message and signer public key.
+//
+// NOTE: This implements the input.Signature interface.
+func (p *MusigPartialSig) Verify(msg []byte, pub *btcec.PublicKey) bool {
+	var m [32]byte
+	copy(m[:], msg)
+
+	return p.sig.Verify(
+		p.signerNonce, p.combinedNonce, p.signerKeys, pub, m,
+		musig2.WithSortedKeys(), musig2.WithBip86SignTweak(),
+	)
+}
+
+// MusigNoncePair holds the two nonces needed to sign/verify a new commitment
+// state. The signer nonce is the nonce used by the signer (remote nonce), and
+// the verification nonce, the nonce used by the verifier (local nonce).
+type MusigNoncePair struct {
+	// SigningNonce is the nonce used by the signer to sign the commitment.
+	SigningNonce musig2.Nonces
+
+	// VerificationNonce is the nonce used by the verifier to verify the
+	// commitment.
+	VerificationNonce musig2.Nonces
+}
+
+// String returns a string representation of the MusigNoncePair.
+func (n *MusigNoncePair) String() string {
+	return fmt.Sprintf("NoncePair(verification_nonce=%x, "+
+		"signing_nonce=%x)", n.VerificationNonce.PubNonce[:],
+		n.SigningNonce.PubNonce[:])
+}
+
+// MusigSession abstracts over the details of a logical musig session. A single
+// session is used for each commitment transactions. The sessions use a JIT
+// nonce style, wherein part of the session can be created using only the
+// verifier nonce. Once a new state is signed, then the signer nonce is
+// generated. Similarly, the verifier then uses the received signer nonce to
+// complete the session and verify the incoming signature.
+type MusigSession struct {
+	// session is the backing musig2 session. We'll use this to interact
+	// with the musig2 signer.
+	session *input.MuSig2SessionInfo
+
+	// combinedNonce is the combined nonce of all signers.
+	combinedNonce lnwire.Musig2Nonce
+
+	// nonces is the set of nonces that'll be used to generate/verify the
+	// next commitment.
+	nonces MusigNoncePair
+
+	// inputTxOut is the funding input.
+	inputTxOut *wire.TxOut
+
+	// signerKeys is the set of public keys of all signers.
+	signerKeys []*btcec.PublicKey
+
+	// remoteKey is the key desc of the remote key.
+	remoteKey keychain.KeyDescriptor
+
+	// localKey is the key desc of the local key.
+	localKey keychain.KeyDescriptor
+
+	// signer is the signer that'll be used to interact with the musig
+	// session.
+	signer input.MuSig2Signer
+
+	// commitType tracks if this is the session for the local or remote
+	// commitment.
+	commitType commitType
+}
+
+// NewPartialMusigSession creates a new musig2 session given only the
+// verification nonce (local nonce), and the other information that has already
+// been bound to the session.
+func NewPartialMusigSession(verificationNonce musig2.Nonces,
+	localKey, remoteKey keychain.KeyDescriptor,
+	signer input.MuSig2Signer, inputTxOut *wire.TxOut,
+	commitType commitType) *MusigSession {
+
+	signerKeys := []*btcec.PublicKey{localKey.PubKey, remoteKey.PubKey}
+
+	nonces := MusigNoncePair{
+		VerificationNonce: verificationNonce,
+	}
+
+	return &MusigSession{
+		nonces:     nonces,
+		remoteKey:  remoteKey,
+		localKey:   localKey,
+		inputTxOut: inputTxOut,
+		signerKeys: signerKeys,
+		signer:     signer,
+		commitType: commitType,
+	}
+}
+
+// FinalizeSession finalizes the session given the signer nonce.  This is
+// called before signing or verifying a new commitment.
+func (m *MusigSession) FinalizeSession(signingNonce musig2.Nonces) error {
+	var (
+		localNonce, remoteNonce musig2.Nonces
+		err                     error
+	)
+
+	// First, we'll stash the freshly generated signing nonce. Depending on
+	// who's commitment we're handling, this'll either be our generated
+	// nonce, or the one we just got from the remote party.
+	m.nonces.SigningNonce = signingNonce
+
+	switch m.commitType {
+	// 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:
+		localNonce = m.nonces.SigningNonce
+		remoteNonce = m.nonces.VerificationNonce
+
+	// Otherwise, we're generating/receiving a signature for our local
+	// 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:
+		localNonce = m.nonces.VerificationNonce
+		remoteNonce = m.nonces.SigningNonce
+	}
+
+	tweakDesc := input.MuSig2Tweaks{
+		TaprootBIP0086Tweak: true,
+	}
+	m.session, err = m.signer.MuSig2CreateSession(
+		input.MuSig2Version100RC2, m.localKey.KeyLocator, m.signerKeys,
+		&tweakDesc, [][musig2.PubNonceSize]byte{remoteNonce.PubNonce},
+		musig2.WithPreGeneratedNonce(&localNonce),
+	)
+	if err != nil {
+		return err
+	}
+
+	// We'll need the raw combined nonces later to be able to verify
+	// partial signatures, and also combine partial signatures, so we'll
+	// generate it now ourselves.
+	aggNonce, err := musig2.AggregateNonces([][musig2.PubNonceSize]byte{
+		m.nonces.SigningNonce.PubNonce,
+		m.nonces.VerificationNonce.PubNonce,
+	})
+	if err != nil {
+		return nil
+	}
+
+	m.combinedNonce = aggNonce
+
+	return nil
+}
+
+// taprootKeyspendSighash generates the sighash for a taproot key spend. As
+// this is a musig2 channel output, the keyspend is the only path we can take.
+func taprootKeyspendSighash(tx *wire.MsgTx, pkScript []byte,
+	value int64) ([]byte, error) {
+
+	prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
+		pkScript, value,
+	)
+
+	sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)
+
+	return txscript.CalcTaprootSignatureHash(
+		sigHashes, txscript.SigHashDefault, tx, 0, prevOutputFetcher,
+	)
+}
+
+// SignCommit signs the passed commitment w/ the current signing (relative
+// remote) nonce. Given nonces should only ever be used once, once the method
+// returns a new nonce is returned, w/ the existing nonce blanked out.
+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:
+		// 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.
+		txHash := tx.TxHash()
+		signingNonce, err := musig2.GenNonces(
+			musig2.WithPublicKey(m.localKey.PubKey),
+			musig2.WithNonceAuxInput(txHash[:]),
+		)
+		if err != nil {
+			return nil, err
+		}
+		if err := m.FinalizeSession(*signingNonce); err != nil {
+			return nil, err
+		}
+
+	// Otherwise, we're trying to make a new commitment transaction without
+	// an active session, so we'll error out.
+	case m.session == nil:
+		return nil, ErrSessionNotFinalized
+	}
+
+	// Next we can sign, we'll need to generate the sighash for their
+	// commitment transaction.
+	sigHash, err := taprootKeyspendSighash(
+		tx, m.inputTxOut.PkScript, m.inputTxOut.Value,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	// Now that we have our session created, we'll use it to generate the
+	// initial partial signature over our sighash.
+	var sigHashMsg [32]byte
+	copy(sigHashMsg[:], sigHash)
+
+	walletLog.Infof("Generating new musig2 sig for session=%x, nonces=%s",
+		m.session.SessionID[:], m.nonces.String())
+
+	sig, err := m.signer.MuSig2Sign(
+		m.session.SessionID, sigHashMsg, false,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewMusigPartialSig(
+		sig, m.session.PublicNonce, m.combinedNonce, m.signerKeys,
+	), nil
+}
+
+// Refresh is called once we receive a new verification nonce from the remote
+// party after sending a signature. This nonce will be coupled within the
+// revoke-and-ack message of the remote party.
+func (m *MusigSession) Refresh(verificationNonce *musig2.Nonces,
+) (*MusigSession, error) {
+
+	return NewPartialMusigSession(
+		*verificationNonce, m.localKey, m.remoteKey, m.signer,
+		m.inputTxOut, m.commitType,
+	), nil
+}
+
+// VerificationNonce returns the current verification nonce for the session.
+func (m *MusigSession) VerificationNonce() *musig2.Nonces {
+	return &m.nonces.VerificationNonce
+}
+
+// musigSessionOpts is a set of options that can be used to modify calls to the
+// musig session.
+type musigSessionOpts struct {
+	// customRand is an optional custom random source that can be used to
+	// generate nonces via a counter scheme.
+	customRand io.Reader
+}
+
+// defaultMusigSessionOpts returns the default set of options for the musig
+// session.
+func defaultMusigSessionOpts() *musigSessionOpts {
+	return &musigSessionOpts{}
+}
+
+// MusigSessionOpt is a functional option that can be used to modify calls to
+// the musig session.
+type MusigSessionOpt func(*musigSessionOpts)
+
+// WithLocalCounterNonce is used to generate local nonces based on the shachain
+// producer and the current height. This allows us to not have to write secret
+// nonce state to disk. Instead, we can use this to derive the nonce we need to
+// sign and broadcast our own commitment transaction.
+func WithLocalCounterNonce(targetHeight uint64,
+	shaGen shachain.Producer) MusigSessionOpt {
+
+	return func(opt *musigSessionOpts) {
+		nextPreimage, _ := shaGen.AtIndex(targetHeight)
+
+		opt.customRand = bytes.NewBuffer(nextPreimage[:])
+	}
+}
+
+// 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
+// relative local nonce) returned to transmit to the remote party, which allows
+// them to generate another signature.
+func (m *MusigSession) VerifyCommitSig(commitTx *wire.MsgTx,
+	sig *lnwire.PartialSigWithNonce,
+	musigOpts ...MusigSessionOpt) (*musig2.Nonces, error) {
+
+	opts := defaultMusigSessionOpts()
+	for _, optFunc := range musigOpts {
+		optFunc(opts)
+	}
+
+	// Before we can verify the signature, we'll need to finalize the
+	// session by binding the remote party's provided signing nonce.
+	if err := m.FinalizeSession(musig2.Nonces{
+		PubNonce: sig.Nonce,
+	}); err != nil {
+		return nil, err
+	}
+
+	// When we verify a commitment signature, we always assume that we're
+	// verifying a signature on our local commitment. Therefore, we'll use:
+	// their remote nonce, and also public key.
+	partialSig := NewMusigPartialSig(
+		&musig2.PartialSignature{S: &sig.Sig},
+		m.nonces.SigningNonce.PubNonce, m.combinedNonce, m.signerKeys,
+	)
+
+	// With the partial sig loaded with the proper context, we'll now
+	// generate the sighash that the remote party should have signed.
+	sigHash, err := taprootKeyspendSighash(
+		commitTx, m.inputTxOut.PkScript, m.inputTxOut.Value,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	walletLog.Infof("Verifying new musig2 sig for session=%x, nonce=%s",
+		m.session.SessionID[:], m.nonces.String())
+
+	if !partialSig.Verify(sigHash, m.remoteKey.PubKey) {
+		return nil, fmt.Errorf("invalid partial commit sig")
+	}
+
+	nonceOpts := []musig2.NonceGenOption{
+		musig2.WithPublicKey(m.localKey.PubKey),
+	}
+	if opts.customRand != nil {
+		nonceOpts = append(
+			nonceOpts, musig2.WithCustomRand(opts.customRand),
+		)
+	}
+
+	// At this point, we know that their signature is valid, so we'll
+	// generate another verification nonce for them, so they can generate a
+	// new state transition.
+	nextVerificationNonce, err := musig2.GenNonces(nonceOpts...)
+	if err != nil {
+		return nil, fmt.Errorf("unable to gen new nonce: %w", err)
+	}
+
+	return nextVerificationNonce, nil
+}
+
+// CombineSigs combines the passed partial signatures into a valid schnorr
+// signature.
+func (m *MusigSession) CombineSigs(sigs ...*musig2.PartialSignature,
+) (*schnorr.Signature, error) {
+
+	sig, _, err := m.signer.MuSig2CombineSig(
+		m.session.SessionID, sigs,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return sig, nil
+}
+
+// MusigSessionCfg is used to create a new musig2 pair session. It contains the
+// keys for both parties, as well as their initial verification nonces.
+type MusigSessionCfg struct {
+	// LocalKey is a key desc for the local key.
+	LocalKey keychain.KeyDescriptor
+
+	// RemoteKey is a key desc for the remote key.
+	RemoteKey keychain.KeyDescriptor
+
+	// LocalNonce is the local party's initial verification nonce.
+	LocalNonce musig2.Nonces
+
+	// RemoteNonce is the remote party's initial verification nonce.
+	RemoteNonce musig2.Nonces
+
+	// Signer is the signer that will be used to generate the session.
+	Signer input.MuSig2Signer
+
+	// InputTxOut is the output that we're signing for. This will be the
+	// funding input.
+	InputTxOut *wire.TxOut
+}
+
+// MusigPairSession houses the two musig2 sessions needed to do funding and
+// drive forward the state machine.  The local session is used to verify
+// incoming commitment states. The remote session is used to propose new
+// commitment states to the remote party.
+type MusigPairSession struct {
+	// LocalSession is the local party's musig2 session.
+	LocalSession *MusigSession
+
+	// RemoteSession is the remote party's musig2 session.
+	RemoteSession *MusigSession
+
+	// signer is the signer that will be used to drive the session.
+	signer input.MuSig2Signer
+}
+
+// NewMusigPairSession creates a new musig2 pair session.
+func NewMusigPairSession(cfg *MusigSessionCfg) *MusigPairSession {
+	// Given the config passed in, we'll now create our two sessions: one
+	// for the local commit, and one for the remote commit.
+	//
+	// Both sessions will be created using only the verification nonce for
+	// the local+remote party.
+	localSession := NewPartialMusigSession(
+		cfg.LocalNonce, cfg.LocalKey, cfg.RemoteKey,
+		cfg.Signer, cfg.InputTxOut, localCommit,
+	)
+	remoteSession := NewPartialMusigSession(
+		cfg.RemoteNonce, cfg.LocalKey, cfg.RemoteKey,
+		cfg.Signer, cfg.InputTxOut, remoteCommit,
+	)
+
+	return &MusigPairSession{
+		LocalSession:  localSession,
+		RemoteSession: remoteSession,
+		signer:        cfg.Signer,
+	}
+}
diff --git a/lnwallet/rpcwallet/rpcwallet.go b/lnwallet/rpcwallet/rpcwallet.go
index 5ccffbef9..d40dc338c 100644
--- a/lnwallet/rpcwallet/rpcwallet.go
+++ b/lnwallet/rpcwallet/rpcwallet.go
@@ -655,9 +655,8 @@ func (r *RPCKeyRing) ComputeInputScript(tx *wire.MsgTx,
 // submitted as well to reduce the number of method calls necessary later on.
 func (r *RPCKeyRing) MuSig2CreateSession(bipVersion input.MuSig2Version,
 	keyLoc keychain.KeyLocator, pubKeys []*btcec.PublicKey,
-	tweaks *input.MuSig2Tweaks,
-	otherNonces [][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo,
-	error) {
+	tweaks *input.MuSig2Tweaks, otherNonces [][musig2.PubNonceSize]byte,
+	sessionOpts ...musig2.SessionOption) (*input.MuSig2SessionInfo, error) {
 
 	apiVersion, err := signrpc.MarshalMuSig2Version(bipVersion)
 	if err != nil {
diff --git a/multimutex/hash_mutex.go b/multimutex/hash_mutex.go
deleted file mode 100644
index 4a65394d1..000000000
--- a/multimutex/hash_mutex.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package multimutex
-
-import (
-	"fmt"
-	"sync"
-
-	"github.com/lightningnetwork/lnd/lntypes"
-)
-
-// HashMutex is a struct that keeps track of a set of mutexes with a given hash.
-// It can be used for making sure only one goroutine gets given the mutex per
-// hash.
-type HashMutex struct {
-	// mutexes is a map of hashes to a cntMutex. The cntMutex for
-	// a given hash will hold the mutex to be used by all
-	// callers requesting access for the hash, in addition to
-	// the count of callers.
-	mutexes map[lntypes.Hash]*cntMutex
-
-	// mapMtx is used to give synchronize concurrent access
-	// to the mutexes map.
-	mapMtx sync.Mutex
-}
-
-// NewHashMutex creates a new Mutex.
-func NewHashMutex() *HashMutex {
-	return &HashMutex{
-		mutexes: make(map[lntypes.Hash]*cntMutex),
-	}
-}
-
-// Lock locks the mutex by the given hash. If the mutex is already
-// locked by this hash, Lock blocks until the mutex is available.
-func (c *HashMutex) Lock(hash lntypes.Hash) {
-	c.mapMtx.Lock()
-	mtx, ok := c.mutexes[hash]
-	if ok {
-		// If the mutex already existed in the map, we
-		// increment its counter, to indicate that there
-		// now is one more goroutine waiting for it.
-		mtx.cnt++
-	} else {
-		// If it was not in the map, it means no other
-		// goroutine has locked the mutex for this hash,
-		// and we can create a new mutex with count 1
-		// and add it to the map.
-		mtx = &cntMutex{
-			cnt: 1,
-		}
-		c.mutexes[hash] = mtx
-	}
-	c.mapMtx.Unlock()
-
-	// Acquire the mutex for this hash.
-	mtx.Lock()
-}
-
-// Unlock unlocks the mutex by the given hash. It is a run-time
-// error if the mutex is not locked by the hash on entry to Unlock.
-func (c *HashMutex) Unlock(hash lntypes.Hash) {
-	// Since we are done with all the work for this
-	// update, we update the map to reflect that.
-	c.mapMtx.Lock()
-
-	mtx, ok := c.mutexes[hash]
-	if !ok {
-		// The mutex not existing in the map means
-		// an unlock for an hash not currently locked
-		// was attempted.
-		panic(fmt.Sprintf("double unlock for hash %v",
-			hash))
-	}
-
-	// Decrement the counter. If the count goes to
-	// zero, it means this caller was the last one
-	// to wait for the mutex, and we can delete it
-	// from the map. We can do this safely since we
-	// are under the mapMtx, meaning that all other
-	// goroutines waiting for the mutex already
-	// have incremented it, or will create a new
-	// mutex when they get the mapMtx.
-	mtx.cnt--
-	if mtx.cnt == 0 {
-		delete(c.mutexes, hash)
-	}
-	c.mapMtx.Unlock()
-
-	// Unlock the mutex for this hash.
-	mtx.Unlock()
-}
diff --git a/multimutex/multimutex.go b/multimutex/multimutex.go
index e37c88d51..4180f3e53 100644
--- a/multimutex/multimutex.go
+++ b/multimutex/multimutex.go
@@ -5,51 +5,48 @@ import (
 	"sync"
 )
 
-// cntMutex is a struct that wraps a counter and a mutex, and is used
-// to keep track of the number of goroutines waiting for access to the
+// cntMutex is a struct that wraps a counter and a mutex, and is used to keep
+// track of the number of goroutines waiting for access to the
 // mutex, such that we can forget about it when the counter is zero.
 type cntMutex struct {
 	cnt int
 	sync.Mutex
 }
 
-// Mutex is a struct that keeps track of a set of mutexes with
-// a given ID. It can be used for making sure only one goroutine
-// gets given the mutex per ID.
-type Mutex struct {
-	// mutexes is a map of IDs to a cntMutex. The cntMutex for
-	// a given ID will hold the mutex to be used by all
-	// callers requesting access for the ID, in addition to
-	// the count of callers.
-	mutexes map[uint64]*cntMutex
+// Mutex is a struct that keeps track of a set of mutexes with a given ID. It
+// can be used for making sure only one goroutine gets given the mutex per ID.
+type Mutex[T comparable] struct {
+	// mutexes is a map of IDs to a cntMutex. The cntMutex for a given ID
+	// will hold the mutex to be used by all callers requesting access for
+	// the ID, in addition to the count of callers.
+	mutexes map[T]*cntMutex
 
-	// mapMtx is used to give synchronize concurrent access
-	// to the mutexes map.
+	// mapMtx is used to give synchronize concurrent access to the mutexes
+	// map.
 	mapMtx sync.Mutex
 }
 
 // NewMutex creates a new Mutex.
-func NewMutex() *Mutex {
-	return &Mutex{
-		mutexes: make(map[uint64]*cntMutex),
+func NewMutex[T comparable]() *Mutex[T] {
+	return &Mutex[T]{
+		mutexes: make(map[T]*cntMutex),
 	}
 }
 
-// Lock locks the mutex by the given ID. If the mutex is already
-// locked by this ID, Lock blocks until the mutex is available.
-func (c *Mutex) Lock(id uint64) {
+// Lock locks the mutex by the given ID. If the mutex is already locked by this
+// ID, Lock blocks until the mutex is available.
+func (c *Mutex[T]) Lock(id T) {
 	c.mapMtx.Lock()
 	mtx, ok := c.mutexes[id]
 	if ok {
-		// If the mutex already existed in the map, we
-		// increment its counter, to indicate that there
-		// now is one more goroutine waiting for it.
+		// If the mutex already existed in the map, we increment its
+		// counter, to indicate that there now is one more goroutine
+		// waiting for it.
 		mtx.cnt++
 	} else {
-		// If it was not in the map, it means no other
-		// goroutine has locked the mutex for this ID,
-		// and we can create a new mutex with count 1
-		// and add it to the map.
+		// If it was not in the map, it means no other goroutine has
+		// locked the mutex for this ID, and we can create a new mutex
+		// with count 1 and add it to the map.
 		mtx = &cntMutex{
 			cnt: 1,
 		}
@@ -61,30 +58,26 @@ func (c *Mutex) Lock(id uint64) {
 	mtx.Lock()
 }
 
-// Unlock unlocks the mutex by the given ID. It is a run-time
-// error if the mutex is not locked by the ID on entry to Unlock.
-func (c *Mutex) Unlock(id uint64) {
-	// Since we are done with all the work for this
-	// update, we update the map to reflect that.
+// Unlock unlocks the mutex by the given ID. It is a run-time error if the
+// mutex is not locked by the ID on entry to Unlock.
+func (c *Mutex[T]) Unlock(id T) {
+	// Since we are done with all the work for this update, we update the
+	// map to reflect that.
 	c.mapMtx.Lock()
 
 	mtx, ok := c.mutexes[id]
 	if !ok {
-		// The mutex not existing in the map means
-		// an unlock for an ID not currently locked
-		// was attempted.
+		// The mutex not existing in the map means an unlock for an ID
+		// not currently locked was attempted.
 		panic(fmt.Sprintf("double unlock for id %v",
 			id))
 	}
 
-	// Decrement the counter. If the count goes to
-	// zero, it means this caller was the last one
-	// to wait for the mutex, and we can delete it
-	// from the map. We can do this safely since we
-	// are under the mapMtx, meaning that all other
-	// goroutines waiting for the mutex already
-	// have incremented it, or will create a new
-	// mutex when they get the mapMtx.
+	// Decrement the counter. If the count goes to zero, it means this
+	// caller was the last one to wait for the mutex, and we can delete it
+	// from the map. We can do this safely since we are under the mapMtx,
+	// meaning that all other goroutines waiting for the mutex already have
+	// incremented it, or will create a new mutex when they get the mapMtx.
 	mtx.cnt--
 	if mtx.cnt == 0 {
 		delete(c.mutexes, id)
diff --git a/routing/control_tower.go b/routing/control_tower.go
index a0c5b1df7..d2cbc6bbf 100644
--- a/routing/control_tower.go
+++ b/routing/control_tower.go
@@ -132,7 +132,7 @@ type controlTower struct {
 	// paymentsMtx provides synchronization on the payment level to ensure
 	// that no race conditions occur in between updating the database and
 	// sending a notification.
-	paymentsMtx *multimutex.HashMutex
+	paymentsMtx *multimutex.Mutex[lntypes.Hash]
 }
 
 // NewControlTower creates a new instance of the controlTower.
@@ -143,7 +143,7 @@ func NewControlTower(db *channeldb.PaymentControl) ControlTower {
 			map[uint64]*controlTowerSubscriberImpl,
 		),
 		subscribers: make(map[lntypes.Hash][]*controlTowerSubscriberImpl),
-		paymentsMtx: multimutex.NewHashMutex(),
+		paymentsMtx: multimutex.NewMutex[lntypes.Hash](),
 	}
 }
 
diff --git a/routing/router.go b/routing/router.go
index eea5189d9..5ecce6646 100644
--- a/routing/router.go
+++ b/routing/router.go
@@ -437,7 +437,7 @@ type ChannelRouter struct {
 	// channelEdgeMtx is a mutex we use to make sure we process only one
 	// ChannelEdgePolicy at a time for a given channelID, to ensure
 	// consistency between the various database accesses.
-	channelEdgeMtx *multimutex.Mutex
+	channelEdgeMtx *multimutex.Mutex[uint64]
 
 	// statTicker is a resumable ticker that logs the router's progress as
 	// it discovers channels or receives updates.
@@ -475,7 +475,7 @@ func New(cfg Config) (*ChannelRouter, error) {
 		networkUpdates:    make(chan *routingMsg),
 		topologyClients:   &lnutils.SyncMap[uint64, *topologyClient]{},
 		ntfnClientUpdates: make(chan *topologyClientUpdate),
-		channelEdgeMtx:    multimutex.NewMutex(),
+		channelEdgeMtx:    multimutex.NewMutex[uint64](),
 		selfNode:          selfNode,
 		statTicker:        ticker.New(defaultStatInterval),
 		stats:             new(routerStats),
diff --git a/rpcserver.go b/rpcserver.go
index cc8b95e06..18e576cef 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -1847,10 +1847,12 @@ func newFundingShimAssembler(chanPointShim *lnrpc.ChanPointShim, initiator bool,
 
 	// With all the parts assembled, we can now make the canned assembler
 	// to pass into the wallet.
+	//
+	// TODO(roasbeef): update to support musig2
 	return chanfunding.NewCannedAssembler(
 		chanPointShim.ThawHeight, *chanPoint,
 		btcutil.Amount(chanPointShim.Amt), &localKeyDesc,
-		remoteKey, initiator,
+		remoteKey, initiator, false,
 	), nil
 }
 
diff --git a/watchtower/wtmock/signer.go b/watchtower/wtmock/signer.go
index 6454871ce..8b8056493 100644
--- a/watchtower/wtmock/signer.go
+++ b/watchtower/wtmock/signer.go
@@ -71,7 +71,8 @@ func (s *MockSigner) ComputeInputScript(tx *wire.MsgTx,
 // submitted as well to reduce the number of method calls necessary later on.
 func (s *MockSigner) MuSig2CreateSession(input.MuSig2Version,
 	keychain.KeyLocator, []*btcec.PublicKey, *input.MuSig2Tweaks,
-	[][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, error) {
+	[][musig2.PubNonceSize]byte,
+	...musig2.SessionOption) (*input.MuSig2SessionInfo, error) {
 
 	return nil, nil
 }


Test patch:
diff --git a/contractcourt/breacharbiter_test.go b/contractcourt/breacharbiter_test.go
index 263ab45ea..6d17135dc 100644
--- a/contractcourt/breacharbiter_test.go
+++ b/contractcourt/breacharbiter_test.go
@@ -2112,7 +2112,7 @@ func createTestArbiter(t *testing.T, contractBreaches chan *ContractBreachEvent,
 	})
 
 	aliceKeyPriv, _ := btcec.PrivKeyFromBytes(channels.AlicesPrivKey)
-	signer := &mock.SingleSigner{Privkey: aliceKeyPriv}
+	signer := input.NewMockSigner([]*btcec.PrivateKey{aliceKeyPriv}, nil)
 
 	// Assemble our test arbiter.
 	notifier := mock.MakeMockSpendNotifier()
@@ -2339,8 +2339,10 @@ func createInitChannels(t *testing.T, revocationWindow int) (
 		Packager:                channeldb.NewChannelPackager(shortChanID),
 	}
 
-	aliceSigner := &mock.SingleSigner{Privkey: aliceKeyPriv}
-	bobSigner := &mock.SingleSigner{Privkey: bobKeyPriv}
+	aliceSigner := input.NewMockSigner(
+		[]*btcec.PrivateKey{aliceKeyPriv}, nil,
+	)
+	bobSigner := input.NewMockSigner([]*btcec.PrivateKey{bobKeyPriv}, nil)
 
 	alicePool := lnwallet.NewSigPool(1, aliceSigner)
 	channelAlice, err := lnwallet.NewLightningChannel(
diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go
index e1bd6783a..a94ea9d76 100644
--- a/htlcswitch/test_utils.go
+++ b/htlcswitch/test_utils.go
@@ -31,7 +31,6 @@ import (
 	"github.com/lightningnetwork/lnd/kvdb"
 	"github.com/lightningnetwork/lnd/lnpeer"
 	"github.com/lightningnetwork/lnd/lntest/channels"
-	"github.com/lightningnetwork/lnd/lntest/mock"
 	"github.com/lightningnetwork/lnd/lntest/wait"
 	"github.com/lightningnetwork/lnd/lntypes"
 	"github.com/lightningnetwork/lnd/lnwallet"
@@ -336,8 +335,12 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte,
 		return nil, nil, err
 	}
 
-	aliceSigner := &mock.SingleSigner{Privkey: aliceKeyPriv}
-	bobSigner := &mock.SingleSigner{Privkey: bobKeyPriv}
+	aliceSigner := input.NewMockSigner(
+		[]*btcec.PrivateKey{aliceKeyPriv}, nil,
+	)
+	bobSigner := input.NewMockSigner(
+		[]*btcec.PrivateKey{bobKeyPriv}, nil,
+	)
 
 	alicePool := lnwallet.NewSigPool(runtime.NumCPU(), aliceSigner)
 	channelAlice, err := lnwallet.NewLightningChannel(
diff --git a/input/test_utils.go b/input/test_utils.go
index 35f372c43..682dfdcd4 100644
--- a/input/test_utils.go
+++ b/input/test_utils.go
@@ -2,14 +2,12 @@ package input
 
 import (
 	"bytes"
-	"crypto/sha256"
 	"encoding/hex"
 	"fmt"
 
 	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btcd/btcec/v2/ecdsa"
 	"github.com/btcsuite/btcd/btcec/v2/schnorr"
-	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
 	"github.com/btcsuite/btcd/btcutil"
 	"github.com/btcsuite/btcd/chaincfg"
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -51,6 +49,26 @@ var (
 type MockSigner struct {
 	Privkeys  []*btcec.PrivateKey
 	NetParams *chaincfg.Params
+
+	*MusigSessionManager
+}
+
+// NewMockSigner returns a new instance of the MockSigner given a set of
+// backing private keys.
+func NewMockSigner(privKeys []*btcec.PrivateKey,
+	netParams *chaincfg.Params) *MockSigner {
+
+	signer := &MockSigner{
+		Privkeys:  privKeys,
+		NetParams: netParams,
+	}
+
+	keyFetcher := func(*keychain.KeyDescriptor) (*btcec.PrivateKey, error) {
+		return signer.Privkeys[0], nil
+	}
+	signer.MusigSessionManager = NewMusigSessionManager(keyFetcher)
+
+	return signer
 }
 
 // SignOutputRaw generates a signature for the passed transaction according to
@@ -74,18 +92,14 @@ func (m *MockSigner) SignOutputRaw(tx *wire.MsgTx,
 
 	// In case of a taproot output any signature is always a Schnorr
 	// signature, based on the new tapscript sighash algorithm.
-	//
-	// TODO(roasbeef): should conslidate with btcwallet/signer.go
 	if txscript.IsPayToTaproot(signDesc.Output.PkScript) {
 		sigHashes := txscript.NewTxSigHashes(
 			tx, signDesc.PrevOutputFetcher,
 		)
 
-		witnessScript := signDesc.WitnessScript
-
 		// Are we spending a script path or the key path? The API is
-		// slightly different, so we need to account for that to get the
-		// raw signature.
+		// slightly different, so we need to account for that to get
+		// the raw signature.
 		var (
 			rawSig []byte
 			err    error
@@ -109,7 +123,7 @@ func (m *MockSigner) SignOutputRaw(tx *wire.MsgTx,
 		case TaprootScriptSpendSignMethod:
 			leaf := txscript.TapLeaf{
 				LeafVersion: txscript.BaseLeafVersion,
-				Script:      witnessScript,
+				Script:      signDesc.WitnessScript,
 			}
 			rawSig, err = txscript.RawTxInTapscriptSignature(
 				tx, sigHashes, signDesc.InputIndex,
@@ -121,6 +135,10 @@ func (m *MockSigner) SignOutputRaw(tx *wire.MsgTx,
 			}
 		}
 
+		// The signature returned above might have a sighash flag
+		// attached if a non-default type was used. We'll slice this
+		// off if it exists to ensure we can properly parse the raw
+		// signature.
 		sig, err := schnorr.ParseSignature(
 			rawSig[:schnorr.SignatureSize],
 		)
@@ -195,55 +213,6 @@ func (m *MockSigner) ComputeInputScript(tx *wire.MsgTx, signDesc *SignDescriptor
 	}
 }
 
-// MuSig2CreateSession creates a new MuSig2 signing session using the local
-// key identified by the key locator. The complete list of all public keys of
-// all signing parties must be provided, including the public key of the local
-// signing key. If nonces of other parties are already known, they can be
-// submitted as well to reduce the number of method calls necessary later on.
-func (m *MockSigner) MuSig2CreateSession(MuSig2Version, keychain.KeyLocator,
-	[]*btcec.PublicKey, *MuSig2Tweaks,
-	[][musig2.PubNonceSize]byte) (*MuSig2SessionInfo, error) {
-
-	return nil, nil
-}
-
-// MuSig2RegisterNonces registers one or more public nonces of other signing
-// participants for a session identified by its ID. This method returns true
-// once we have all nonces for all other signing participants.
-func (m *MockSigner) MuSig2RegisterNonces(MuSig2SessionID,
-	[][musig2.PubNonceSize]byte) (bool, error) {
-
-	return false, nil
-}
-
-// MuSig2Sign creates a partial signature using the local signing key
-// that was specified when the session was created. This can only be
-// called when all public nonces of all participants are known and have
-// been registered with the session. If this node isn't responsible for
-// combining all the partial signatures, then the cleanup parameter
-// should be set, indicating that the session can be removed from memory
-// once the signature was produced.
-func (m *MockSigner) MuSig2Sign(MuSig2SessionID,
-	[sha256.Size]byte, bool) (*musig2.PartialSignature, error) {
-
-	return nil, nil
-}
-
-// MuSig2CombineSig combines the given partial signature(s) with the
-// local one, if it already exists. Once a partial signature of all
-// participants is registered, the final signature will be combined and
-// returned.
-func (m *MockSigner) MuSig2CombineSig(MuSig2SessionID,
-	[]*musig2.PartialSignature) (*schnorr.Signature, bool, error) {
-
-	return nil, false, nil
-}
-
-// MuSig2Cleanup removes a session from memory to free up resources.
-func (m *MockSigner) MuSig2Cleanup(MuSig2SessionID) error {
-	return nil
-}
-
 // findKey searches through all stored private keys and returns one
 // corresponding to the hashed pubkey if it can be found. The public key may
 // either correspond directly to the private key or to the private key with a
@@ -252,13 +221,15 @@ func (m *MockSigner) findKey(needleHash160 []byte, singleTweak []byte,
 	doubleTweak *btcec.PrivateKey) *btcec.PrivateKey {
 
 	for _, privkey := range m.Privkeys {
-		// First check whether public key is directly derived from private key.
+		// First check whether public key is directly derived from
+		// private key.
 		hash160 := btcutil.Hash160(privkey.PubKey().SerializeCompressed())
 		if bytes.Equal(hash160, needleHash160) {
 			return privkey
 		}
 
-		// Otherwise check if public key is derived from tweaked private key.
+		// Otherwise check if public key is derived from tweaked
+		// private key.
 		switch {
 		case singleTweak != nil:
 			privkey = TweakPrivKey(privkey, singleTweak)
diff --git a/lntest/mock/signer.go b/lntest/mock/signer.go
index ddcef3598..7ce6cf4e0 100644
--- a/lntest/mock/signer.go
+++ b/lntest/mock/signer.go
@@ -57,7 +57,8 @@ func (d *DummySigner) ComputeInputScript(tx *wire.MsgTx,
 // submitted as well to reduce the number of method calls necessary later on.
 func (d *DummySigner) MuSig2CreateSession(input.MuSig2Version,
 	keychain.KeyLocator, []*btcec.PublicKey, *input.MuSig2Tweaks,
-	[][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, error) {
+	[][musig2.PubNonceSize]byte,
+	...musig2.SessionOption) (*input.MuSig2SessionInfo, error) {
 
 	return nil, nil
 }
@@ -196,7 +197,8 @@ func (s *SingleSigner) SignMessage(keyLoc keychain.KeyLocator,
 // submitted as well to reduce the number of method calls necessary later on.
 func (s *SingleSigner) MuSig2CreateSession(input.MuSig2Version,
 	keychain.KeyLocator, []*btcec.PublicKey, *input.MuSig2Tweaks,
-	[][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, error) {
+	[][musig2.PubNonceSize]byte,
+	...musig2.SessionOption) (*input.MuSig2SessionInfo, error) {
 
 	return nil, nil
 }
diff --git a/lnwallet/musig_session_test.go b/lnwallet/musig_session_test.go
new file mode 100644
index 000000000..fc144e90e
--- /dev/null
+++ b/lnwallet/musig_session_test.go
@@ -0,0 +1,273 @@
+package lnwallet
+
+import (
+	"testing"
+
+	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightningnetwork/lnd/input"
+	"github.com/lightningnetwork/lnd/keychain"
+	"github.com/stretchr/testify/require"
+)
+
+// nodeType is an enum that represents the two nodes in our test harness.
+type nodeType uint8
+
+const (
+	// nodeAlice is the node that initiates the session.
+	nodeAlice nodeType = iota
+
+	// nodeBob is the node that responds to the session.
+	nodeBob
+)
+
+type muSessionHarness struct {
+	aliceCommit *wire.MsgTx
+	bobCommit   *wire.MsgTx
+
+	aliceSession *MusigPairSession
+
+	bobSession *MusigPairSession
+
+	t *testing.T
+}
+
+func (h *muSessionHarness) selectSession(nodeName nodeType) *MusigPairSession {
+	var targetSession *MusigPairSession
+	switch nodeName {
+	case nodeAlice:
+		targetSession = h.aliceSession
+	case nodeBob:
+		targetSession = h.bobSession
+	}
+
+	return targetSession
+}
+
+func (h *muSessionHarness) refreshSession(nodeName nodeType,
+	nextNonce *musig2.Nonces, revoke bool) {
+
+	var session *MusigPairSession
+	switch nodeName {
+	case nodeAlice:
+		session = h.aliceSession
+	case nodeBob:
+		session = h.bobSession
+	}
+
+	var err error
+
+	// If this isn't in response to a revoke, then we just signed, so we'll
+	// refresh our local session with the newly generated verification
+	// nonce.
+	if !revoke {
+		session.LocalSession, err = session.LocalSession.Refresh(
+			nextNonce,
+		)
+	} else {
+		session.RemoteSession, err = session.RemoteSession.Refresh(
+			nextNonce,
+		)
+	}
+	require.NoError(h.t, err)
+}
+
+// SignCommitment signs a new remote commitment. This is equivalent to sending
+// a CommitSig message on the normal LN protocol.
+func (h *muSessionHarness) SignCommitment(nodeName nodeType) *MusigPartialSig {
+	targetSession := h.selectSession(nodeName)
+
+	sig, err := targetSession.RemoteSession.SignCommit(h.bobCommit)
+	require.NoError(h.t, err)
+
+	return sig
+}
+
+// VerifyAndSignCommitment verifies a remote commitment, then signs a new
+// commitment. This combines receiving a signature, then sending a revoke
+// message.
+func (h *muSessionHarness) VerifyAndSignCommitment(nodeName nodeType,
+	sig *MusigPartialSig) (*MusigPartialSig, *musig2.Nonces) {
+
+	muSession := h.selectSession(nodeName)
+
+	// Verify the commitment transaction from the remote party. The nonce
+	// returned will be sent along side the "revoke and ack" message in the
+	// actual p2p protocol.
+	nextVerificationNonce, err := muSession.LocalSession.VerifyCommitSig(
+		h.bobCommit, sig.ToWireSig(),
+	)
+	require.NoError(h.t, err)
+
+	// As we've just used our verification nonce to verify the remote sign,
+	// we'll refresh our local session with the new nonce.
+	h.refreshSession(nodeName, nextVerificationNonce, false)
+
+	// Next, sign a new version of the commitment for the remote party.
+	// This uses a JIT nonce that'll be sent along side the signature, and
+	// consumes the verification nonce of the remote party.
+	remoteSig, err := muSession.RemoteSession.SignCommit(h.aliceCommit)
+	require.NoError(h.t, err)
+
+	return remoteSig, nextVerificationNonce
+}
+
+// VerifyCommitment verifies a remote commitment, then sends a nonce. This is
+// equivalent to verifying a new incoming commitment, then sending a revoke
+// message.
+func (h *muSessionHarness) VerifyCommitment(nodeName nodeType,
+	sig *MusigPartialSig, nextNonce *musig2.Nonces) *musig2.Nonces {
+
+	muSession := h.selectSession(nodeName)
+
+	// We'll now verify the incoming signature, then refresh our local
+	// session as we've used up our prior verification nonce.
+	nextVerificationNonce, err := muSession.LocalSession.VerifyCommitSig(
+		h.aliceCommit, sig.ToWireSig(),
+	)
+	require.NoError(h.t, err)
+	h.refreshSession(nodeName, nextVerificationNonce, false)
+
+	// The packaged nonce is the remote party's new verification nonce, so
+	// we'll refresh their remote commitment: we just got the revocation
+	// and the sig in the same message.
+	h.refreshSession(nodeName, nextNonce, true)
+
+	return nextVerificationNonce
+}
+
+// ProcessVerificationNonce processes a verification nonce from the remote.
+// This is equivalent to receiving the revoke from a remote party after you
+// kicked off the commitment dance.
+func (h *muSessionHarness) ProcessVerificationNonce(nodeName nodeType,
+	nextNonce *musig2.Nonces) {
+
+	h.refreshSession(nodeName, nextNonce, true)
+}
+
+func newMuSessionHarness(t *testing.T) *muSessionHarness {
+	aliceCommit := wire.NewMsgTx(2)
+	aliceCommit.AddTxIn(&wire.TxIn{
+		PreviousOutPoint: wire.OutPoint{
+			Index: 1,
+		},
+	})
+
+	bobCommit := wire.NewMsgTx(2)
+	bobCommit.AddTxIn(&wire.TxIn{
+		PreviousOutPoint: wire.OutPoint{
+			Index: 2,
+		},
+	})
+
+	alicePriv, alicePub := btcec.PrivKeyFromBytes(testWalletPrivKey)
+	aliceSigner := input.NewMockSigner([]*btcec.PrivateKey{alicePriv}, nil)
+
+	aliceVerificationNonce, err := musig2.GenNonces(
+		musig2.WithPublicKey(alicePub),
+	)
+	require.NoError(t, err)
+
+	bobPriv, bobPub := btcec.PrivKeyFromBytes(bobsPrivKey)
+	bobSigner := input.NewMockSigner([]*btcec.PrivateKey{bobPriv}, nil)
+
+	bobVerificationNonce, err := musig2.GenNonces(
+		musig2.WithPublicKey(bobPub),
+	)
+	require.NoError(t, err)
+
+	inputTxOut := &wire.TxOut{
+		Value:    1000,
+		PkScript: testHdSeed[:],
+	}
+
+	aliceSession := NewMusigPairSession(&MusigSessionCfg{
+		LocalKey: keychain.KeyDescriptor{
+			PubKey: alicePub,
+		},
+		RemoteKey: keychain.KeyDescriptor{
+			PubKey: bobPub,
+		},
+		LocalNonce:  *aliceVerificationNonce,
+		RemoteNonce: *bobVerificationNonce,
+		Signer:      aliceSigner,
+		InputTxOut:  inputTxOut,
+	})
+
+	bobSession := NewMusigPairSession(&MusigSessionCfg{
+		LocalKey: keychain.KeyDescriptor{
+			PubKey: bobPub,
+		},
+		RemoteKey: keychain.KeyDescriptor{
+			PubKey: alicePub,
+		},
+		LocalNonce:  *bobVerificationNonce,
+		RemoteNonce: *aliceVerificationNonce,
+		Signer:      bobSigner,
+		InputTxOut:  inputTxOut,
+	})
+
+	return &muSessionHarness{
+		aliceCommit:  aliceCommit,
+		aliceSession: aliceSession,
+		bobCommit:    bobCommit,
+		bobSession:   bobSession,
+		t:            t,
+	}
+}
+
+// TestMusigSession tests that we're able to send and receive signatures for
+// the set of asymmetric musig sessions. This tests proper nonce rotation and
+// signature verification.
+func TestMusigSesssion(t *testing.T) {
+	t.Parallel()
+
+	// First, we'll make a new musig session between Alice and Bob. This is
+	// 4 sessions total, as both sides maintain a session for their local
+	// commitment, and one for the remote commitment.
+	muSessions := newMuSessionHarness(t)
+
+	t.Run("session_round_trips", func(t *testing.T) { //nolint:paralleltest
+		const numRounds = 10
+		for i := 0; i < numRounds; i++ {
+			// We'll now simulate a full commitment dance.
+			//
+			// To start, Alice will sign a new commitment for Bob's
+			// remote commitment.
+			aliceSig := muSessions.SignCommitment(nodeAlice)
+
+			// Bob will then verify Alice's signature, and sign a
+			// new commitment for Alice.
+			bobSig, bobNonce := muSessions.VerifyAndSignCommitment(
+				nodeBob, aliceSig,
+			)
+
+			// Next Alice will process Bob's signature, and then
+			// generate a new verification nonce to he can sign the
+			// next commitment.
+			aliceNonce := muSessions.VerifyCommitment(
+				nodeAlice, bobSig, bobNonce,
+			)
+
+			// To conclude the commitment dance, Bob will process
+			// Alice's new verification nonce.
+			muSessions.ProcessVerificationNonce(nodeBob, aliceNonce)
+
+			// Modify the commitments after each round to simulate
+			// the LN protocol commitment randomness structure
+			// (sequence+locktime change each state, etc).
+			muSessions.aliceCommit.TxIn[0].PreviousOutPoint.Index++
+			muSessions.bobCommit.TxIn[0].PreviousOutPoint.Index++
+		}
+	})
+
+	t.Run("no_finalize_error", func(t *testing.T) { //nolint:paralleltest
+		// If a local party attempts to sign for their local commitment
+		// without finalizing first, they'll get this error.
+		_, err := muSessions.aliceSession.LocalSession.SignCommit(
+			muSessions.aliceCommit,
+		)
+		require.ErrorIs(t, err, ErrSessionNotFinalized)
+	})
+}
diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go
index 737f1d417..e27e12ded 100644
--- a/lnwallet/test/test_interface.go
+++ b/lnwallet/test/test_interface.go
@@ -2974,11 +2974,11 @@ func testSingleFunderExternalFundingTx(miner *rpctest.Harness,
 	thawHeight := uint32(200)
 	aliceExternalFunder := chanfunding.NewCannedAssembler(
 		thawHeight, *chanPoint, btcutil.Amount(chanAmt), &aliceFundingKey,
-		bobFundingKey.PubKey, true,
+		bobFundingKey.PubKey, true, false,
 	)
 	bobShimIntent, err := chanfunding.NewCannedAssembler(
 		thawHeight, *chanPoint, btcutil.Amount(chanAmt), &bobFundingKey,
-		aliceFundingKey.PubKey, false,
+		aliceFundingKey.PubKey, false, false,
 	).ProvisionChannel(&chanfunding.Request{
 		LocalAmt: btcutil.Amount(chanAmt),
 		MinConfs: 1,
diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go
index f506ffa6a..d28511feb 100644
--- a/lnwallet/test_utils.go
+++ b/lnwallet/test_utils.go
@@ -343,8 +343,8 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType,
 		Packager:                channeldb.NewChannelPackager(shortChanID),
 	}
 
-	aliceSigner := &input.MockSigner{Privkeys: aliceKeys}
-	bobSigner := &input.MockSigner{Privkeys: bobKeys}
+	aliceSigner := input.NewMockSigner(aliceKeys, nil)
+	bobSigner := input.NewMockSigner(bobKeys, nil)
 
 	// TODO(roasbeef): make mock version of pre-image store
 
diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go
index b51a3f903..329366bbb 100644
--- a/lnwallet/transactions_test.go
+++ b/lnwallet/transactions_test.go
@@ -534,9 +534,9 @@ func testSpendValidation(t *testing.T, tweakless bool) {
 	remoteCommitTweak := input.SingleTweakBytes(commitPoint, aliceKeyPub)
 	localCommitTweak := input.SingleTweakBytes(commitPoint, bobKeyPub)
 
-	aliceSelfOutputSigner := &input.MockSigner{
-		Privkeys: []*btcec.PrivateKey{aliceKeyPriv},
-	}
+	aliceSelfOutputSigner := input.NewMockSigner(
+		[]*btcec.PrivateKey{aliceKeyPriv}, nil,
+	)
 
 	// Calculate the dust limit we'll use for the test.
 	dustLimit := DustLimitForSize(input.UnknownWitnessSize)
@@ -626,7 +626,7 @@ func testSpendValidation(t *testing.T, tweakless bool) {
 		t.Fatalf("spend from delay output is invalid: %v", err)
 	}
 
-	localSigner := &input.MockSigner{Privkeys: []*btcec.PrivateKey{bobKeyPriv}}
+	localSigner := input.NewMockSigner([]*btcec.PrivateKey{bobKeyPriv}, nil)
 
 	// Next, we'll test bob spending with the derived revocation key to
 	// simulate the scenario when Alice broadcasts this commitment
@@ -941,15 +941,15 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp
 	}
 
 	// Create mock signers that can sign for the keys that are used.
-	localSigner := &input.MockSigner{Privkeys: []*btcec.PrivateKey{
+	localSigner := input.NewMockSigner([]*btcec.PrivateKey{
 		tc.localPaymentBasepointSecret, tc.localDelayedPaymentBasepointSecret,
 		tc.localFundingPrivkey, localDummy1, localDummy2,
-	}}
+	}, nil)
 
-	remoteSigner := &input.MockSigner{Privkeys: []*btcec.PrivateKey{
+	remoteSigner := input.NewMockSigner([]*btcec.PrivateKey{
 		tc.remoteFundingPrivkey, tc.remoteRevocationBasepointSecret,
 		tc.remotePaymentBasepointSecret, remoteDummy1, remoteDummy2,
-	}}
+	}, nil)
 
 	remotePool := NewSigPool(1, remoteSigner)
 	channelRemote, err := NewLightningChannel(
diff --git a/peer/test_utils.go b/peer/test_utils.go
index a997411df..bf61640f9 100644
--- a/peer/test_utils.go
+++ b/peer/test_utils.go
@@ -276,8 +276,12 @@ func createTestPeer(t *testing.T, notifier chainntnfs.ChainNotifier,
 		return nil, nil, err
 	}
 
-	aliceSigner := &mock.SingleSigner{Privkey: aliceKeyPriv}
-	bobSigner := &mock.SingleSigner{Privkey: bobKeyPriv}
+	aliceSigner := input.NewMockSigner(
+		[]*btcec.PrivateKey{aliceKeyPriv}, nil,
+	)
+	bobSigner := input.NewMockSigner(
+		[]*btcec.PrivateKey{bobKeyPriv}, nil,
+	)
 
 	alicePool := lnwallet.NewSigPool(1, aliceSigner)
 	channelAlice, err := lnwallet.NewLightningChannel(