From 3ddbf19b84607a800a802a6eb21117e7bef19a54 Mon Sep 17 00:00:00 2001 From: martonp Date: Sun, 23 Feb 2025 19:04:07 -0500 Subject: [PATCH] musig2: Add adaptor signature support to MuSig2 protocol This commit adds comprehensive support for adaptor signatures in the MuSig2 protocol, including: - New methods for generating and setting adaptor points - Support for creating partial adaptor signatures - Methods to adapt final signatures and recover adaptor secrets - Updated signature verification and combination logic - Added test cases for adaptor signature functionality across different signer configurations --- btcec/schnorr/musig2/context.go | 215 +++++++++++++++++++++++++++++- btcec/schnorr/musig2/sign.go | 73 +++++++--- btcec/schnorr/musig2/sign_test.go | 117 ++++++++++++++++ 3 files changed, 385 insertions(+), 20 deletions(-) diff --git a/btcec/schnorr/musig2/context.go b/btcec/schnorr/musig2/context.go index 8e6b7154..e26ce6f1 100644 --- a/btcec/schnorr/musig2/context.go +++ b/btcec/schnorr/musig2/context.go @@ -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 +} diff --git a/btcec/schnorr/musig2/sign.go b/btcec/schnorr/musig2/sign.go index 9204611d..e60e5ecd 100644 --- a/btcec/schnorr/musig2/sign.go +++ b/btcec/schnorr/musig2/sign.go @@ -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. diff --git a/btcec/schnorr/musig2/sign_test.go b/btcec/schnorr/musig2/sign_test.go index a967cfe4..05ba5be1 100644 --- a/btcec/schnorr/musig2/sign_test.go +++ b/btcec/schnorr/musig2/sign_test.go @@ -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[:])) + }) + } +}