diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go index 04f9e74ad0..e469f5f0f4 100644 --- a/lnwallet/musig_session.go +++ b/lnwallet/musig_session.go @@ -296,7 +296,7 @@ func taprootKeyspendSighash(tx *wire.MsgTx, pkScript []byte, func (m *MusigSession) SignCommit(tx *wire.MsgTx) (*MusigPartialSig, error) { // 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). - if m.session == nil && m.commitType == localCommit { + if m.session == nil { // 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. diff --git a/lnwallet/musig_session_test.go b/lnwallet/musig_session_test.go new file mode 100644 index 0000000000..ff12dd5edd --- /dev/null +++ b/lnwallet/musig_session_test.go @@ -0,0 +1,247 @@ +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.aliceCommit, 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.bobCommit) + 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{}) + + bobCommit := wire.NewMsgTx(2) + bobCommit.AddTxIn(&wire.TxIn{}) + + 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) + + 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) + } +}