diff --git a/input/musig2.go b/input/musig2.go index 2fc689d77..0d3a9abbe 100644 --- a/input/musig2.go +++ b/input/musig2.go @@ -145,6 +145,26 @@ func (t *MuSig2Tweaks) ToContextOptions() []musig2.ContextOption { return tweakOpts } +// MuSig2ParsePubKeys parses a list of raw public keys as the signing keys of a +// MuSig2 signing session. +func MuSig2ParsePubKeys(rawPubKeys [][]byte) ([]*btcec.PublicKey, error) { + allSignerPubKeys := make([]*btcec.PublicKey, len(rawPubKeys)) + if len(rawPubKeys) < 2 { + return nil, fmt.Errorf("need at least two signing public keys") + } + + for idx, pubKeyBytes := range rawPubKeys { + pubKey, err := schnorr.ParsePubKey(pubKeyBytes) + if err != nil { + return nil, fmt.Errorf("error parsing signer public "+ + "key %d: %v", idx, err) + } + allSignerPubKeys[idx] = pubKey + } + + return allSignerPubKeys, nil +} + // MuSig2CombineKeys combines the given set of public keys into a single // combined MuSig2 combined public key, applying the given tweaks. func MuSig2CombineKeys(allSignerPubKeys []*btcec.PublicKey, @@ -173,6 +193,64 @@ func MuSig2CombineKeys(allSignerPubKeys []*btcec.PublicKey, return combinedKey, err } +// MuSig2CreateContext creates a new MuSig2 signing context. +func MuSig2CreateContext(privKey *btcec.PrivateKey, + allSignerPubKeys []*btcec.PublicKey, + tweaks *MuSig2Tweaks) (*musig2.Context, *musig2.Session, error) { + + // The context keeps track of all signing keys and our local key. + allOpts := append( + []musig2.ContextOption{ + musig2.WithKnownSigners(allSignerPubKeys), + }, + tweaks.ToContextOptions()..., + ) + muSigContext, err := musig2.NewContext(privKey, true, allOpts...) + if err != nil { + return nil, nil, fmt.Errorf("error creating MuSig2 signing "+ + "context: %v", err) + } + + muSigSession, err := muSigContext.NewSession() + if err != nil { + return nil, nil, fmt.Errorf("error creating MuSig2 signing "+ + "session: %v", err) + } + + return muSigContext, muSigSession, nil +} + +// MuSig2Sign calls the Sign() method on the given versioned signing session and +// returns the result in the most recent version of the MuSig2 API. +func MuSig2Sign(session *musig2.Session, msg [32]byte, + withSortedKeys bool) (*musig2.PartialSignature, error) { + + var opts []musig2.SignOption + if withSortedKeys { + opts = append(opts, musig2.WithSortedKeys()) + } + partialSig, err := session.Sign(msg, opts...) + if err != nil { + return nil, fmt.Errorf("error signing with local key: %v", err) + } + + return partialSig, nil +} + +// MuSig2CombineSig calls the CombineSig() method on the given versioned signing +// session and returns the result in the most recent version of the MuSig2 API. +func MuSig2CombineSig(session *musig2.Session, + otherPartialSig *musig2.PartialSignature) (bool, error) { + + haveAllSigs, err := session.CombineSig(otherPartialSig) + if err != nil { + return false, fmt.Errorf("error combining partial signature: "+ + "%v", err) + } + + return haveAllSigs, nil +} + // NewMuSig2SessionID returns the unique ID of a MuSig2 session by using the // combined key and the local public nonces and hashing that data. func NewMuSig2SessionID(combinedKey *btcec.PublicKey, diff --git a/input/musig2_test.go b/input/musig2_test.go new file mode 100644 index 000000000..c16f96cf3 --- /dev/null +++ b/input/musig2_test.go @@ -0,0 +1,122 @@ +package input + +import ( + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/stretchr/testify/require" +) + +var ( + hexDecode = func(keyStr string) []byte { + keyBytes, _ := hex.DecodeString(keyStr) + return keyBytes + } + dummyPubKey1, _ = btcec.ParsePubKey(hexDecode( + "02ec95e4e8ad994861b95fc5986eedaac24739e5ea3d0634db4c8ccd44cd" + + "a126ea", + )) + dummyPubKey2, _ = btcec.ParsePubKey(hexDecode( + "0356167ba3e54ac542e86e906d4186aba9ca0b9df45001c62b753d33fe06" + + "f5b4e8", + )) + dummyPubKey3, _ = btcec.ParsePubKey(hexDecode( + "02a9b0e1777e35d4620061a9fb0e614bf0254a50dea4f872babf6d44bf4d" + + "8ee7c6", + )) + + testVector040Key1, _ = schnorr.ParsePubKey(hexDecode( + "F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE0" + + "36F9", + )) + testVector040Key2, _ = schnorr.ParsePubKey(hexDecode( + "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502B" + + "A659", + )) + testVector040Key3, _ = schnorr.ParsePubKey(hexDecode( + "3590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038" + + "CA66", + )) + + bip86Tweak = &MuSig2Tweaks{TaprootBIP0086Tweak: true} +) + +func TestMuSig2CombineKeys(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + keys []*btcec.PublicKey + tweak *MuSig2Tweaks + expectedErr string + expectedFinalKey string + expectedPreTweakKey string + }{{ + name: "v0.4.0 two dummy keys BIP86", + keys: []*btcec.PublicKey{dummyPubKey1, dummyPubKey2}, + tweak: bip86Tweak, + expectedFinalKey: "03b54fb320a8fc3589e86a1559c6aaa774fbab4e4d" + + "9fbf31e2fd836b661ac6a132", + expectedPreTweakKey: "0279c76a15dcf6786058a571e4022b78633e1bf" + + "8a7a4ca440bcbbeeaea772228a2", + }, { + name: "v0.4.0 three dummy keys BIP86", + keys: []*btcec.PublicKey{ + dummyPubKey1, dummyPubKey2, dummyPubKey3, + }, + tweak: bip86Tweak, + expectedFinalKey: "03fa8195d584b195476f20e2fe978fd7312f4b08f2" + + "777f080bcdfc9350603cd6e7", + expectedPreTweakKey: "03e615b8aad4ed10544537bc48b1d6600e15773" + + "476a675c6cbba6808f21b1988e5", + }, { + name: "v0.4.0 three test vector keys BIP86", + keys: []*btcec.PublicKey{ + testVector040Key1, testVector040Key2, testVector040Key3, + }, + tweak: bip86Tweak, + expectedFinalKey: "025b257b4e785d61157ef5303051f45184bd5cb47b" + + "c4b4069ed4dd4536459cb83b", + expectedPreTweakKey: "02d70cd69a2647f7390973df48cbfa2ccc407b8" + + "b2d60b08c5f1641185c7998a290", + }, { + name: "v0.4.0 three test vector keys BIP86 reverse order", + keys: []*btcec.PublicKey{ + testVector040Key3, testVector040Key2, testVector040Key1, + }, + tweak: bip86Tweak, + expectedFinalKey: "025b257b4e785d61157ef5303051f45184bd5cb47b" + + "c4b4069ed4dd4536459cb83b", + expectedPreTweakKey: "02d70cd69a2647f7390973df48cbfa2ccc407b8" + + "b2d60b08c5f1641185c7998a290", + }} + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + + res, err := MuSig2CombineKeys(tc.keys, tc.tweak) + + if tc.expectedErr != "" { + require.ErrorContains(tt, err, tc.expectedErr) + return + } + + require.NoError(tt, err) + + finalKey := res.FinalKey.SerializeCompressed() + preTweakKey := res.PreTweakedKey.SerializeCompressed() + require.Equal( + tt, tc.expectedFinalKey, + hex.EncodeToString(finalKey), + ) + require.Equal( + tt, tc.expectedPreTweakKey, + hex.EncodeToString(preTweakKey), + ) + }) + } +} diff --git a/lnrpc/signrpc/signer_server.go b/lnrpc/signrpc/signer_server.go index 7c098f257..3cebae57a 100644 --- a/lnrpc/signrpc/signer_server.go +++ b/lnrpc/signrpc/signer_server.go @@ -826,18 +826,10 @@ func (s *Server) MuSig2CombineKeys(_ context.Context, // Parse the public keys of all signing participants. This must also // include our own, local key. - allSignerPubKeys := make([]*btcec.PublicKey, len(in.AllSignerPubkeys)) - if len(in.AllSignerPubkeys) < 2 { - return nil, fmt.Errorf("need at least two signing public keys") - } - - for idx, pubKeyBytes := range in.AllSignerPubkeys { - pubKey, err := schnorr.ParsePubKey(pubKeyBytes) - if err != nil { - return nil, fmt.Errorf("error parsing signer public "+ - "key %d: %v", idx, err) - } - allSignerPubKeys[idx] = pubKey + allSignerPubKeys, err := input.MuSig2ParsePubKeys(in.AllSignerPubkeys) + if err != nil { + return nil, fmt.Errorf("error parsing all signer public "+ + "keys: %w", err) } // Are there any tweaks to apply to the combined public key? @@ -887,18 +879,10 @@ func (s *Server) MuSig2CreateSession(_ context.Context, // Parse the public keys of all signing participants. This must also // include our own, local key. - allSignerPubKeys := make([]*btcec.PublicKey, len(in.AllSignerPubkeys)) - if len(in.AllSignerPubkeys) < 2 { - return nil, fmt.Errorf("need at least two signing public keys") - } - - for idx, pubKeyBytes := range in.AllSignerPubkeys { - pubKey, err := schnorr.ParsePubKey(pubKeyBytes) - if err != nil { - return nil, fmt.Errorf("error parsing signer public "+ - "key %d: %v", idx, err) - } - allSignerPubKeys[idx] = pubKey + allSignerPubKeys, err := input.MuSig2ParsePubKeys(in.AllSignerPubkeys) + if err != nil { + return nil, fmt.Errorf("error parsing all signer public "+ + "keys: %w", err) } // We participate a nonce ourselves, so we can't have more nonces than diff --git a/lnwallet/btcwallet/signer.go b/lnwallet/btcwallet/signer.go index 07aa5b8d1..23e970e99 100644 --- a/lnwallet/btcwallet/signer.go +++ b/lnwallet/btcwallet/signer.go @@ -503,24 +503,14 @@ func (b *BtcWallet) MuSig2CreateSession(keyLoc keychain.KeyLocator, return nil, fmt.Errorf("error deriving private key: %v", err) } - // The context keeps track of all signing keys and our local key. - allOpts := append( - []musig2.ContextOption{ - musig2.WithKnownSigners(allSignerPubKeys), - }, - tweaks.ToContextOptions()..., + // Create a signing context with the given private key and list of all + // known signer public keys. + muSigContext, muSigSession, err := input.MuSig2CreateContext( + privKey, allSignerPubKeys, tweaks, ) - muSigContext, err := musig2.NewContext(privKey, true, allOpts...) if err != nil { - return nil, fmt.Errorf("error creating MuSig2 signing "+ - "context: %v", err) - } - - // The session keeps track of the own and other nonces. - muSigSession, err := muSigContext.NewSession() - if err != nil { - return nil, fmt.Errorf("error creating MuSig2 signing "+ - "session: %v", err) + return nil, fmt.Errorf("error creating signing context: %v", + err) } // Add all nonces we might've learned so far. @@ -652,7 +642,7 @@ func (b *BtcWallet) MuSig2Sign(sessionID input.MuSig2SessionID, } // Create our own partial signature with the local signing key. - partialSig, err := session.session.Sign(msg, musig2.WithSortedKeys()) + partialSig, err := input.MuSig2Sign(session.session, msg, true) if err != nil { return nil, fmt.Errorf("error signing with local key: %v", err) } @@ -698,8 +688,8 @@ func (b *BtcWallet) MuSig2CombineSig(sessionID input.MuSig2SessionID, err error ) for _, otherPartialSig := range partialSigs { - session.HaveAllSigs, err = session.session.CombineSig( - otherPartialSig, + session.HaveAllSigs, err = input.MuSig2CombineSig( + session.session, otherPartialSig, ) if err != nil { return nil, false, fmt.Errorf("error combining "+