This commit is contained in:
Boris Nagaev 2026-08-12 12:22:13 +00:00 committed by GitHub
commit 89b034c4f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 192 additions and 10 deletions

View file

@ -1239,7 +1239,10 @@ func (s *loopInSwap) setState(state loopdb.SwapState) {
}
// sharedSecretFromHash derives the shared secret from the swap hash using the
// swap.KeyFamily family and zero as index.
// swap.KeyFamily family and zero as index. The swap hash is first interpreted
// with btcec.PrivKeyFromBytes semantics, so the derived ephemeral pubkey uses
// the same modulo-N normalization as the internal-key code paths that consume
// the resulting shared secret.
func sharedSecretFromHash(ctx context.Context, signer lndclient.SignerClient,
hash lntypes.Hash) ([32]byte, error) {

View file

@ -6,7 +6,6 @@ import (
"fmt"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/test"
@ -28,9 +27,12 @@ func TestStaticAddressScript(t *testing.T) {
clientPrivKey, clientPubKey := test.CreateKey(1)
serverPrivKey, serverPubKey := test.CreateKey(2)
var clientKey, serverKey [32]byte
copy(clientKey[:], clientPrivKey.Serialize())
copy(serverKey[:], serverPrivKey.Serialize())
// Keys used for the Musig2 session.
privKeys := []*btcec.PrivateKey{clientPrivKey, serverPrivKey}
pubKeys := []*btcec.PublicKey{clientPubKey, serverPubKey}
privKeys := [][32]byte{clientKey, serverKey}
// Create a new static address.
staticAddress, err := NewStaticAddress(
@ -91,7 +93,7 @@ func TestStaticAddressScript(t *testing.T) {
}
sig, err := utils.MuSig2Sign(
version, privKeys, pubKeys, tweak, msg,
version, privKeys, tweak, msg,
)
require.NoError(t, err)

View file

@ -4,15 +4,44 @@ import (
"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/input"
)
// MuSig2Sign will create a MuSig2 signature for the passed message using the
// passed private keys.
func MuSig2Sign(version input.MuSig2Version, privKeys []*btcec.PrivateKey,
pubKeys []*btcec.PublicKey, tweaks *input.MuSig2Tweaks,
msg [32]byte) ([]byte, error) {
// passed raw private keys. Raw keys are interpreted with
// btcec.PrivKeyFromBytes semantics, which normalize 32-byte inputs modulo the
// secp256k1 group order instead of rejecting out-of-range values. It expects
// at least two signing keys.
func MuSig2Sign(version input.MuSig2Version, keys [][32]byte,
tweaks *input.MuSig2Tweaks, msg [32]byte) ([]byte, error) {
privKeys := make([]*btcec.PrivateKey, len(keys))
pubKeys := make([]*btcec.PublicKey, len(keys))
// First parse the raw private keys and also create the corresponding
// public keys. This preserves the same normalization semantics used
// when these raw keys are turned into pubkeys elsewhere in the protocol.
for i, key := range keys {
privKeys[i], pubKeys[i] = btcec.PrivKeyFromBytes(key[:])
// MuSig2 v0.4 expects x-only public keys.
if version == input.MuSig2Version040 {
pubKey := pubKeys[i].SerializeCompressed()
xOnlyPubKey, err := schnorr.ParsePubKey(pubKey[1:])
if err != nil {
return nil, fmt.Errorf("error parsing x-only "+
"pubkey: %v", err)
}
pubKeys[i] = xOnlyPubKey
}
}
if len(privKeys) < 2 {
return nil, fmt.Errorf("need at least two signing keys")
}
// Next we'll create MuSig2 sessions for each individual private
// signing key.
@ -74,7 +103,7 @@ func MuSig2Sign(version input.MuSig2Version, privKeys []*btcec.PrivateKey,
}
if !haveAllSigs {
return nil, fmt.Errorf("combinging MuSig2 signatures " +
return nil, fmt.Errorf("combining MuSig2 signatures " +
"failed")
}

148
utils/musig_test.go Normal file
View file

@ -0,0 +1,148 @@
package utils
import (
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/input"
"github.com/stretchr/testify/require"
)
// rawKeys returns serialized private keys for the given test seeds.
func rawKeys(seeds ...int32) [][32]byte {
keys := make([][32]byte, len(seeds))
for i, seed := range seeds {
privKey, _ := test.CreateKey(seed)
copy(keys[i][:], privKey.Serialize())
}
return keys
}
// signerPubKeys returns signer public keys derived from the given seeds in the
// format expected by the given MuSig2 version.
func signerPubKeys(t *testing.T, version input.MuSig2Version,
seeds ...int32) []*btcec.PublicKey {
t.Helper()
pubKeys := make([]*btcec.PublicKey, len(seeds))
for i, seed := range seeds {
_, pubKey := test.CreateKey(seed)
if version == input.MuSig2Version040 {
var err error
pubKey, err = schnorr.ParsePubKey(
schnorr.SerializePubKey(pubKey),
)
require.NoError(t, err)
}
pubKeys[i] = pubKey
}
return pubKeys
}
// hasOddY returns true if the compressed serialization of the public key uses
// the odd-Y prefix.
func hasOddY(pubKey *btcec.PublicKey) bool {
return pubKey.SerializeCompressed()[0] == 0x03
}
// TestMuSig2SignRejectsSingleSigner ensures the helper fails fast with a clear
// error instead of entering an invalid one-party MuSig2 flow.
func TestMuSig2SignRejectsSingleSigner(t *testing.T) {
_, err := MuSig2Sign(
input.MuSig2Version100RC2,
rawKeys(1),
&input.MuSig2Tweaks{},
[32]byte{},
)
require.ErrorContains(t, err, "need at least two signing keys")
}
// TestMuSig2SignSupportsVersions verifies the helper works with the supported
// MuSig2 versions used in Loop.
func TestMuSig2SignSupportsVersions(t *testing.T) {
t.Parallel()
tweaks := &input.MuSig2Tweaks{}
msg := [32]byte{1}
testCases := []struct {
name string
version input.MuSig2Version
seeds []int32
oddYSigner int32
}{
{
name: testVersionName(input.MuSig2Version040),
version: input.MuSig2Version040,
seeds: []int32{1, 2},
},
{
name: testVersionName(input.MuSig2Version100RC2),
version: input.MuSig2Version100RC2,
seeds: []int32{1, 2},
},
{
name: testVersionName(input.MuSig2Version040) +
" odd Y",
version: input.MuSig2Version040,
seeds: []int32{5, 1},
oddYSigner: 5,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
if testCase.oddYSigner != 0 {
_, oddYKey := test.CreateKey(
testCase.oddYSigner,
)
require.True(t, hasOddY(oddYKey))
}
keys := rawKeys(testCase.seeds...)
pubKeys := signerPubKeys(
t, testCase.version, testCase.seeds...,
)
sigBytes, err := MuSig2Sign(
testCase.version, keys, tweaks, msg,
)
require.NoError(t, err)
require.Len(t, sigBytes, 64)
sig, err := schnorr.ParseSignature(sigBytes)
require.NoError(t, err)
combinedKey, err := input.MuSig2CombineKeys(
testCase.version, pubKeys, true, tweaks,
)
require.NoError(t, err)
require.True(
t, sig.Verify(msg[:], combinedKey.FinalKey),
)
})
}
}
// testVersionName returns a stable subtest name for a MuSig2 version.
func testVersionName(version input.MuSig2Version) string {
switch version {
case input.MuSig2Version040:
return "MuSig2 0.4"
case input.MuSig2Version100RC2:
return "MuSig2 1.0RC2"
default:
return "unknown"
}
}