This commit is contained in:
Marton 2025-03-11 10:05:58 +08:00 committed by GitHub
commit 9cbd944a0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 385 additions and 20 deletions

View file

@ -3,6 +3,7 @@
package musig2
import (
"crypto/rand"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
@ -52,6 +53,27 @@ var (
// required combined nonces.
ErrCombinedNonceUnavailable = fmt.Errorf("missing combined nonce")
// ErrFinalSigUnavailable is returned when a caller attempts to adapt a
// final signature, but the final signature is not available.
ErrFinalSigUnavailable = fmt.Errorf("final signature not available")
// ErrAdaptorSecretUnavailable is returned when a caller attempts to adapt a
// final signature, but the adaptor secret key is not available.
ErrAdaptorSecretUnavailable = fmt.Errorf("adaptor secret key not available")
// ErrAdaptorPointUnavailable is returned when a caller attempts to recover
// the adaptor signature from a valid signature, but the adaptor point
// is not available.
ErrAdaptorPointUnavailable = fmt.Errorf("adaptor point not available")
// ErrInvalidAdaptorPoint is returned when a caller attempts to provide
// an adaptor point that is incompatible with the combined nonce.
ErrInvalidAdaptorPoint = fmt.Errorf("invalid adaptor point")
// ErrAlreadySigned is returned when a caller attempts to provide an
// adaptor point, but a signature was already signed.
ErrAlreadySigned = fmt.Errorf("already signed")
// ErrTaprootInternalKeyUnavailable is returned when a user attempts to
// obtain the
ErrTaprootInternalKeyUnavailable = fmt.Errorf("taproot tweak not used")
@ -99,7 +121,7 @@ type ContextOption func(*contextOptions)
// contextOptions houses the set of functional options that can be used to
// musig2 signing protocol.
type contextOptions struct {
// tweaks is the set of optinoal tweaks to apply to the combined public
// tweaks is the set of optional tweaks to apply to the combined public
// key.
tweaks []KeyTweakDesc
@ -439,6 +461,9 @@ type Session struct {
msg [32]byte
adaptorSecret *btcec.ModNScalar
adaptorPoint *btcec.JacobianPoint
ourSig *PartialSignature
sigs []*PartialSignature
@ -548,6 +573,129 @@ func (s *Session) RegisterPubNonce(nonce [PubNonceSize]byte) (bool, error) {
return haveAllNonces, nil
}
// GenerateAdaptorSecret generates an adaptor secret key. This must be called
// after the public nonces have been registered for all signers, because
// the validity of the adaptor secret key depends on the combined nonce. It
// must also be called before any partial signatures are generated or provided,
// because the validation of the signatures depends on the adaptor point.
func (s *Session) GenerateAdaptor(msg [32]byte) (*btcec.ModNScalar, *btcec.JacobianPoint, error) {
if s.combinedNonce == nil {
return nil, nil, ErrCombinedNonceUnavailable
}
if len(s.sigs) != 0 {
return nil, nil, ErrAlreadySigned
}
nonce, _, err := computeSigningNonce(
*s.combinedNonce, s.ctx.combinedKey.FinalKey, msg,
)
if err != nil {
return nil, nil, err
}
// The adaptor point added to the combined nonce must be even.
// We keep generating random secrets until we find one that works.
for {
var secretB [32]byte
_, err := rand.Read(secretB[:])
if err != nil {
return nil, nil, err
}
var secret btcec.ModNScalar
secret.SetBytes(&secretB)
var adaptorPoint btcec.JacobianPoint
btcec.ScalarBaseMultNonConst(&secret, &adaptorPoint)
adaptorPoint.ToAffine()
var adaptedNonce btcec.JacobianPoint
btcec.AddNonConst(&adaptorPoint, nonce, &adaptedNonce)
adaptedNonce.ToAffine()
if !adaptedNonce.Y.IsOdd() {
s.adaptorSecret = &secret
s.adaptorPoint = &adaptorPoint
return &secret, &adaptorPoint, nil
}
}
}
// SetAdaptorSecret sets the adaptor secret key for the session. This must be
// called after the public nonces have been registered for all signers, because
// the validity of the adaptor secret key depends on the combined nonce. It
// must also be called before any partial signatures are generated or provided,
// because the validation of the signatures depends on the adaptor point.
func (s *Session) SetAdaptorSecret(msg [32]byte, adaptorSecret *btcec.ModNScalar) error {
if s.combinedNonce == nil {
return ErrCombinedNonceUnavailable
}
if len(s.sigs) != 0 {
return ErrAlreadySigned
}
nonce, _, err := computeSigningNonce(
*s.combinedNonce, s.ctx.combinedKey.FinalKey, msg,
)
if err != nil {
return err
}
var adaptorPoint btcec.JacobianPoint
btcec.ScalarBaseMultNonConst(adaptorSecret, &adaptorPoint)
adaptorPoint.ToAffine()
var adaptedNonce btcec.JacobianPoint
btcec.AddNonConst(&adaptorPoint, nonce, &adaptedNonce)
adaptedNonce.ToAffine()
if adaptedNonce.Y.IsOdd() {
return ErrInvalidAdaptorPoint
}
s.adaptorSecret = adaptorSecret
s.adaptorPoint = &adaptorPoint
return nil
}
// SetAdaptorPoint sets the adaptor point for the session. This is called by
// other signers after the adaptor point has been generated by the signer that
// knows the adaptor secret key. It must be called after all the public nonces
// have been registered, and before any partial signatures are generated or
// provided.
func (s *Session) SetAdaptorPoint(msg [32]byte, adaptorPoint *btcec.JacobianPoint) error {
if s.combinedNonce == nil {
return ErrCombinedNonceUnavailable
}
if len(s.sigs) != 0 {
return ErrAlreadySigned
}
// Verify that the adaptor point added to the combined nonce will result in
// a point with even y-coordinate.
nonce, _, err := computeSigningNonce(
*s.combinedNonce, s.ctx.combinedKey.FinalKey, msg,
)
if err != nil {
return err
}
var adaptedNonce btcec.JacobianPoint
btcec.AddNonConst(adaptorPoint, nonce, &adaptedNonce)
adaptedNonce.ToAffine()
if adaptedNonce.Y.IsOdd() {
return ErrInvalidAdaptorPoint
}
s.adaptorPoint = adaptorPoint
return nil
}
// Sign generates a partial signature for the target message, using the target
// context. If this method is called more than once per context, then an error
// is returned, as that means a nonce was re-used.
@ -579,6 +727,10 @@ func (s *Session) Sign(msg [32]byte,
signOpts = append(signOpts, WithTweaks(s.ctx.opts.tweaks...))
}
if s.adaptorPoint != nil {
signOpts = append(signOpts, WithAdaptorSign(s.adaptorPoint))
}
partialSig, err := Sign(
s.localNonces.SecNonce, s.ctx.signingKey, *s.combinedNonce,
s.ctx.opts.keySet, msg, signOpts...,
@ -647,14 +799,21 @@ func (s *Session) CombineSig(sig *PartialSignature) (bool, error) {
)
}
if s.adaptorPoint != nil {
combineOpts = append(combineOpts, WithAdaptorCombine(s.msg, s.adaptorPoint))
}
finalSig := CombineSigs(s.ourSig.R, s.sigs, combineOpts...)
// We'll also verify the signature at this point to ensure it's
// valid.
// valid. For adaptor signatures, verification is done when the
// signature is adapted using the secret key.
//
// TODO(roasbef): allow skipping?
if !finalSig.Verify(s.msg[:], s.ctx.combinedKey.FinalKey) {
return false, ErrFinalSigInvalid
if s.adaptorPoint == nil {
if !finalSig.Verify(s.msg[:], s.ctx.combinedKey.FinalKey) {
return false, ErrFinalSigInvalid
}
}
s.finalSig = finalSig
@ -667,3 +826,51 @@ func (s *Session) CombineSig(sig *PartialSignature) (bool, error) {
func (s *Session) FinalSig() *schnorr.Signature {
return s.finalSig
}
// AdaptFinalSig adapts the final signature with the provided adaptor secret
// key, and returns a valid signature.
func (s *Session) AdaptFinalSig(adaptorSecret *btcec.ModNScalar) (*schnorr.Signature, error) {
if s.finalSig == nil {
return nil, ErrFinalSigUnavailable
}
if s.adaptorSecret == nil {
return nil, ErrAdaptorSecretUnavailable
}
sigB := s.finalSig.Serialize()
r := new(btcec.FieldVal)
r.SetByteSlice(sigB[0:32])
sigS := new(btcec.ModNScalar)
sigS.SetByteSlice(sigB[32:64])
sigS.Add(adaptorSecret)
adaptedSig := schnorr.NewSignature(r, sigS)
if !adaptedSig.Verify(s.msg[:], s.ctx.combinedKey.FinalKey) {
return nil, ErrFinalSigInvalid
}
return adaptedSig, nil
}
// RecoverAdaptorSecret recovers the adaptor secret key from a valid signature
// created by the signer that knows the adaptor secret key.
func (s *Session) RecoverAdaptorSecret(sig *schnorr.Signature) (*btcec.ModNScalar, error) {
if s.finalSig == nil {
return nil, ErrFinalSigUnavailable
}
if s.adaptorPoint == nil {
return nil, ErrAdaptorPointUnavailable
}
finalSigB := s.finalSig.Serialize()
finalSigS := new(btcec.ModNScalar)
finalSigS.SetByteSlice(finalSigB[32:64])
sigSB := sig.Serialize()
sigS := new(btcec.ModNScalar)
sigS.SetByteSlice(sigSB[32:64])
sigS.Add(finalSigS.Negate())
return sigS, nil
}

View file

@ -134,6 +134,10 @@ type signOptions struct {
// 86 style, where we don't expect an actual tweak and instead just
// commit to the public key itself.
bip86Tweak bool
// adaptorPoint is used to tweak the nonce in the commitment in order
// to create a partial adaptor signature.
adaptorPoint *btcec.JacobianPoint
}
// defaultSignOptions returns the default set of signing operations.
@ -181,6 +185,14 @@ func WithTaprootSignTweak(scriptRoot []byte) SignOption {
}
}
// WithAdaptorSign allows a caller to specify an adaptor point that should be
// used to create a partial adaptor signature.
func WithAdaptorSign(adaptorPoint *btcec.JacobianPoint) SignOption {
return func(o *signOptions) {
o.adaptorPoint = adaptorPoint
}
}
// WithBip86SignTweak allows a caller to specify a tweak that should be used in
// a bip 340 manner when signing, factoring in BIP 86 as well. This differs
// from WithTaprootSignTweak as no true script root will be committed to,
@ -320,6 +332,19 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey,
return nil, err
}
nonce.ToAffine()
var adaptedNonce *btcec.JacobianPoint
if opts.adaptorPoint != nil {
adaptedNonce = new(btcec.JacobianPoint)
btcec.AddNonConst(nonce, opts.adaptorPoint, adaptedNonce)
adaptedNonce.ToAffine()
} else {
adaptedNonce = nonce
}
adaptedNonceKey := btcec.NewPublicKey(&adaptedNonce.X, &adaptedNonce.Y)
// Next we'll parse out our two secret nonces, which we'll be using in
// the core signing process below.
var k1, k2 btcec.ModNScalar
@ -330,13 +355,9 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey,
return nil, ErrSecretNonceZero
}
nonce.ToAffine()
nonceKey := btcec.NewPublicKey(&nonce.X, &nonce.Y)
// If the nonce R has an odd y coordinate, then we'll negate both our
// secret nonces.
if nonce.Y.IsOdd() {
if adaptedNonce.Y.IsOdd() {
k1.Negate()
k2.Negate()
}
@ -368,7 +389,7 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey,
// nonce, combined public key and also the message:
// * e = H(tag=ChallengeHashTag, R || Q || m) mod n
var challengeMsg bytes.Buffer
challengeMsg.Write(schnorr.SerializePubKey(nonceKey))
challengeMsg.Write(schnorr.SerializePubKey(adaptedNonceKey))
challengeMsg.Write(schnorr.SerializePubKey(combinedKey.FinalKey))
challengeMsg.Write(msg[:])
challengeBytes := chainhash.TaggedHash(
@ -386,7 +407,7 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey,
s := new(btcec.ModNScalar)
s.Add(&k1).Add(k2.Mul(nonceBlinder)).Add(e.Mul(a).Mul(&privKeyScalar))
sig := NewPartialSignature(s, nonceKey)
sig := NewPartialSignature(s, adaptedNonceKey)
// If we're not in fast sign mode, then we'll also validate our partial
// signature.
@ -500,6 +521,21 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte,
btcec.ScalarMultNonConst(&nonceBlinder, &r2J, &r2J)
btcec.AddNonConst(&r1J, &r2J, &nonce)
var adaptedNonce *btcec.JacobianPoint
if opts.adaptorPoint != nil {
adaptedNonce = new(btcec.JacobianPoint)
btcec.AddNonConst(&nonce, opts.adaptorPoint, adaptedNonce)
} else {
adaptedNonce = &nonce
}
// If the nonce is the infinity point we set it to the Generator.
if *adaptedNonce == infinityPoint {
btcec.GeneratorJacobian(adaptedNonce)
} else {
adaptedNonce.ToAffine()
}
// Next, we'll parse out the set of public nonces this signer used to
// generate the signature.
pubNonce1J, err := btcec.ParseJacobian(
@ -515,13 +551,6 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte,
return err
}
// If the nonce is the infinity point we set it to the Generator.
if nonce == infinityPoint {
btcec.GeneratorJacobian(&nonce)
} else {
nonce.ToAffine()
}
// We'll perform a similar aggregation and blinding operator as we did
// above for the combined nonces: R' = R_1' + b*R_2'.
var pubNonceJ btcec.JacobianPoint
@ -533,7 +562,7 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte,
// If the combined nonce used in the challenge hash has an odd y
// coordinate, then we'll negate our final public nonce.
if nonce.Y.IsOdd() {
if adaptedNonce.Y.IsOdd() {
pubNonceJ.Y.Negate(1)
pubNonceJ.Y.Normalize()
}
@ -543,7 +572,7 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte,
// * e = H(tag=ChallengeHashTag, R || Q || m) mod n
var challengeMsg bytes.Buffer
challengeMsg.Write(schnorr.SerializePubKey(btcec.NewPublicKey(
&nonce.X, &nonce.Y,
&adaptedNonce.X, &adaptedNonce.Y,
)))
challengeMsg.Write(schnorr.SerializePubKey(combinedKey.FinalKey))
challengeMsg.Write(msg[:])
@ -606,6 +635,8 @@ type combineOptions struct {
combinedKey *btcec.PublicKey
tweakAcc *btcec.ModNScalar
adaptorPub *btcec.JacobianPoint
}
// defaultCombineOptions returns the default set of signing operations.
@ -632,6 +663,16 @@ func WithTweakedCombine(msg [32]byte, keys []*btcec.PublicKey,
}
}
// WithAdaptorCombine is a functional option that allows callers to specify
// that the signature was produced using an adaptor point. In order to properly
// combine the partial signatures, the caller must specify the adaptor point.
func WithAdaptorCombine(msg [32]byte, adaptorPub *btcec.JacobianPoint) CombineOption {
return func(o *combineOptions) {
o.msg = msg
o.adaptorPub = adaptorPub
}
}
// WithTaprootTweakedCombine is similar to the WithTweakedCombine option, but
// assumes a BIP 341 context where the final tweaked key is to be used as the
// output key, where the internal key is the aggregated key pre-tweak.

View file

@ -4,6 +4,7 @@ package musig2
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
@ -389,3 +390,119 @@ func TestMusig2SignCombine(t *testing.T) {
})
}
}
func TestMusig2SignCombineAdaptor(t *testing.T) {
// Pre-generate 10 deterministic private keys
privKeys := make([]*btcec.PrivateKey, 10)
pubKeys := make([]*btcec.PublicKey, 10)
for i := 0; i < 10; i++ {
seed := sha256.Sum256([]byte(fmt.Sprintf("privkey_%d", i)))
privKey, _ := btcec.PrivKeyFromBytes(seed[:])
privKeys[i] = privKey
pubKeys[i] = privKey.PubKey()
}
// Helper function to run test with different number of signers and context options
runTest := func(t *testing.T, numSigners int, ctxOpt ContextOption) {
contexts := make([]*Context, numSigners)
sessions := make([]*Session, numSigners)
// Create context for each signer
for i := 0; i < numSigners; i++ {
ctx, err := NewContext(
privKeys[i], true,
WithNumSigners(numSigners),
ctxOpt,
)
require.NoError(t, err)
contexts[i] = ctx
// Register all other public keys
for j := 0; j < numSigners; j++ {
if j != i {
_, err = contexts[i].RegisterSigner(pubKeys[j])
require.NoError(t, err)
}
}
// Create session
sess, err := contexts[i].NewSession()
require.NoError(t, err)
sessions[i] = sess
}
// Exchange public nonces
for i := 0; i < numSigners; i++ {
for j := 0; j < numSigners; j++ {
if j != i {
_, err := sessions[i].RegisterPubNonce(sessions[j].PublicNonce())
require.NoError(t, err)
}
}
}
// Message to sign
msg := [32]byte{1, 2, 3, 4}
// Signer 0 generates and uses the adaptor secret
adaptorSecret, adaptorPoint, err := sessions[0].GenerateAdaptor(msg)
require.NoError(t, err)
// No-op, but test SetAdaptorSecret
err = sessions[0].SetAdaptorSecret(msg, adaptorSecret)
require.NoError(t, err)
// All other signers set the adaptor point
for i := 1; i < numSigners; i++ {
err = sessions[i].SetAdaptorPoint(msg, adaptorPoint)
require.NoError(t, err)
}
// Each signer generates a partial signature
partialSigs := make([]*PartialSignature, numSigners)
for i := 0; i < numSigners; i++ {
sig, err := sessions[i].Sign(msg)
require.NoError(t, err)
partialSigs[i] = sig
}
// Each signer combines all partial signatures
for i := 0; i < numSigners; i++ {
for j := 0; j < numSigners; j++ {
if j != i {
_, err := sessions[i].CombineSig(partialSigs[j])
require.NoError(t, err)
}
}
}
// Signer 0 adapts the final signature
validSig, err := sessions[0].AdaptFinalSig(adaptorSecret)
require.NoError(t, err)
// Verify the final signature is valid under the combined key
combinedKey, _ := contexts[0].CombinedKey()
require.True(t, validSig.Verify(msg[:], combinedKey),
"Final signature verification failed")
// All other signers recover the adaptor secret
for i := 1; i < numSigners; i++ {
recoveredSecret, err := sessions[i].RecoverAdaptorSecret(validSig)
require.NoError(t, err)
require.Equal(t, adaptorSecret.Bytes(), recoveredSecret.Bytes(),
"Recovered secret mismatch for signer %d", i)
}
}
// Run test for 2-10 signers with different context options
for numSigners := 2; numSigners <= 10; numSigners++ {
t.Run(fmt.Sprintf("BIP86_%d_signers", numSigners), func(t *testing.T) {
runTest(t, numSigners, WithBip86TweakCtx())
})
scriptRoot := sha256.Sum256([]byte("test_script_root"))
t.Run(fmt.Sprintf("Taproot_%d_signers", numSigners), func(t *testing.T) {
runTest(t, numSigners, WithTaprootTweakCtx(scriptRoot[:]))
})
}
}