Skip to content

musig2: Add adaptor signature support to MuSig2 protocol #2325

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 197 additions & 4 deletions btcec/schnorr/musig2/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package musig2

import (
"crypto/rand"
"fmt"

"github.com/btcsuite/btcd/btcec/v2"
Expand Down Expand Up @@ -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 unavailable")

// 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 unavailable")

// 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 unavailable")

// 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")
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -439,6 +461,9 @@ type Session struct {

msg [32]byte

adaptorSecret *btcec.ModNScalar
adaptorPoint *btcec.JacobianPoint

ourSig *PartialSignature
sigs []*PartialSignature

Expand Down Expand Up @@ -548,6 +573,115 @@ func (s *Session) RegisterPubNonce(nonce [PubNonceSize]byte) (bool, error) {
return haveAllNonces, nil
}

// GenerateAdaptor generates an adaptor secret key and point. 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) {

// The adaptor point added to the combined nonce must be even.
// We keep generating random secrets until we find one that works.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to do this in a loop? Can't we just take the inverse of the secret to get an even y coordinate if the first draw produces an odd one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A loop is needed because we want the adaptor point added to the combined nonce to be even, not just for the adaptor point to be even.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I understand that. But the evenness should be deterministic. So wouldn't it be possible to just invert the secret if we find out that (after adding the adaptor point to the nonce) it's not even, we can invert it instead of drawing a new value?
It's just that a for loop with no defined exit condition feels suboptimal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I completely missed your message.

Inverting the secret will not work, you can try this test function here:

func TestGenerateAdaptor(t *testing.T) {
	var nonceB [32]byte
	_, err := rand.Read(nonceB[:])
	if err != nil {
		t.Fatalf("failed to generate random nonce: %v", err)
	}

	nonceScalar := btcec.ModNScalar{}
	nonceScalar.SetBytes(&nonceB)
	nonce := btcec.JacobianPoint{}
	btcec.ScalarBaseMultNonConst(&nonceScalar, &nonce)

	createAdaptedNonce := func(secret btcec.ModNScalar) btcec.JacobianPoint {
		var adaptorPoint btcec.JacobianPoint
		btcec.ScalarBaseMultNonConst(&secret, &adaptorPoint)

		combinedNonce := btcec.JacobianPoint{}
		btcec.AddNonConst(&adaptorPoint, &nonce, &combinedNonce)
		combinedNonce.ToAffine()

		return combinedNonce
	}

	unchanged := 0

	for i := 0; i < 1000; i++ {
		var secretB [32]byte
		_, err := rand.Read(secretB[:])
		if err != nil {
			t.Fatalf("failed to generate random secret: %v", err)
		}
		secret := btcec.ModNScalar{}
		secret.SetBytes(&secretB)

		combinedNonceBeforeNegation := createAdaptedNonce(secret)
		secret.Negate()
		combinedNonceAfterNegation := createAdaptedNonce(secret)

		if combinedNonceBeforeNegation.Y.IsOdd() == combinedNonceAfterNegation.Y.IsOdd() {
			unchanged++
		}
	}

	t.Logf("Y-coordinate evenness unchanged %d times", unchanged)
}

There's no deterministic effect of the evenness of one of the points to the combined point. Maybe just incrementing the secret until we find a valid one, rather than choosing a new random number, would be more performant, but I'm not aware of a technique to avoid trying different points until we encounter 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)

err = s.SetAdaptorSecret(msg, &secret)
if err == ErrInvalidAdaptorPoint {
continue
} else if err != nil {
return nil, nil, err
}

return &secret, s.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.
Expand Down Expand Up @@ -579,6 +713,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...,
Expand Down Expand Up @@ -650,11 +788,14 @@ func (s *Session) CombineSig(sig *PartialSignature) (bool, error) {
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
Expand All @@ -667,3 +808,55 @@ 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() (
*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(s.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
}
Loading
Loading