From 9d0d52708ac9a0cdc13b08e9c75df9e58905fbb4 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 1 Apr 2022 12:35:25 -0500 Subject: [PATCH] btcec/schnorr/musig2: add explicit support for BIP 86 multi-signing In this commit, we add a series of new functional optinos to make signing for an aggregated key where the final taproot output key was derived using BIP 86. This can be used in cases where no script path shuold be allowed, and only an n-of-n multi-sig should be used. --- btcec/schnorr/musig2/context.go | 45 +++++++++++++++++---- btcec/schnorr/musig2/keys.go | 41 ++++++++++++++++++- btcec/schnorr/musig2/musig2_test.go | 8 ++++ btcec/schnorr/musig2/sign.go | 61 ++++++++++++++++++++++++++--- 4 files changed, 141 insertions(+), 14 deletions(-) diff --git a/btcec/schnorr/musig2/context.go b/btcec/schnorr/musig2/context.go index bfb9d874..2461f962 100644 --- a/btcec/schnorr/musig2/context.go +++ b/btcec/schnorr/musig2/context.go @@ -90,6 +90,10 @@ type contextOptions struct { // also commits to the public key, which in this case is the aggregated // key before the tweak. taprootTweak []byte + + // bip86Tweak if true, then the weak will just be + // h_tapTweak(internalKey) as there is no true script root. + bip86Tweak bool } // defaultContextOptions returns the default context options. @@ -115,6 +119,16 @@ func WithTaprootTweakCtx(scriptRoot []byte) ContextOption { } } +// WithBip86TweakCtx specifies that within this context, the final key should +// use the taproot tweak as defined in BIP 341, with the BIP 86 modification: +// outputKey = internalKey + h_tapTweak(internalKey)*G. In this case, the +// aggreaged key before the tweak will be used as the internal key. +func WithBip86TweakCtx() ContextOption { + return func(o *contextOptions) { + o.bip86Tweak = true + } +} + // NewContext creates a new signing context with the passed singing key and set // of public keys for each of the other signers. // @@ -160,11 +174,16 @@ func NewContext(signingKey *btcec.PrivateKey, keyAggOpts := []KeyAggOption{ WithKeysHash(keysHash), WithUniqueKeyIndex(uniqueKeyIndex), } - if opts.taprootTweak != nil { + switch { + case opts.bip86Tweak: + keyAggOpts = append( + keyAggOpts, WithBIP86KeyTweak(), + ) + case opts.taprootTweak != nil: keyAggOpts = append( keyAggOpts, WithTaprootKeyTweak(opts.taprootTweak), ) - } else if len(opts.tweaks) != 0 { + case len(opts.tweaks) != 0: keyAggOpts = append(keyAggOpts, WithKeyTweaks(opts.tweaks...)) } @@ -214,7 +233,7 @@ func (c *Context) SigningKeys() []*btcec.PublicKey { // returning the internal key. If a taproot tweak wasn't speciifed, then this // method will return an error. func (c *Context) TaprootInternalKey() (*btcec.PublicKey, error) { - if c.opts.taprootTweak == nil { + if c.opts.taprootTweak == nil && !c.opts.bip86Tweak { return nil, ErrTaprootInternalKeyUnavailable } @@ -329,11 +348,16 @@ func (s *Session) Sign(msg [32]byte, return nil, ErrCombinedNonceUnavailable } - if s.ctx.opts.taprootTweak != nil { + switch { + case s.ctx.opts.bip86Tweak: + signOpts = append( + signOpts, WithBip86SignTweak(), + ) + case s.ctx.opts.taprootTweak != nil: signOpts = append( signOpts, WithTaprootSignTweak(s.ctx.opts.taprootTweak), ) - } else if len(s.ctx.opts.tweaks) != 0 { + case len(s.ctx.opts.tweaks) != 0: signOpts = append(signOpts, WithTweaks(s.ctx.opts.tweaks...)) } @@ -379,14 +403,21 @@ func (s *Session) CombineSig(sig *PartialSignature) (bool, error) { // final signature. if haveAllSigs { var combineOpts []CombineOption - if s.ctx.opts.taprootTweak != nil { + switch { + case s.ctx.opts.bip86Tweak: + combineOpts = append( + combineOpts, WithBip86TweakedCombine( + s.msg, s.ctx.keySet, s.ctx.shouldSort, + ), + ) + case s.ctx.opts.taprootTweak != nil: combineOpts = append( combineOpts, WithTaprootTweakedCombine( s.msg, s.ctx.keySet, s.ctx.opts.taprootTweak, s.ctx.shouldSort, ), ) - } else if len(s.ctx.opts.tweaks) != 0 { + case len(s.ctx.opts.tweaks) != 0: combineOpts = append( combineOpts, WithTweakedCombine( s.msg, s.ctx.keySet, s.ctx.opts.tweaks, diff --git a/btcec/schnorr/musig2/keys.go b/btcec/schnorr/musig2/keys.go index c13af984..e61a22f2 100644 --- a/btcec/schnorr/musig2/keys.go +++ b/btcec/schnorr/musig2/keys.go @@ -176,6 +176,11 @@ type keyAggOption struct { // taprootTweak controls if the tweaks above should be applied in a BIP // 340 style. taprootTweak bool + + // bip86Tweak specifies that the taproot tweak should be done in a BIP + // 86 style, where we don't expect an actual tweak and instead just + // commit to the public key itself. + bip86Tweak bool } // WithKeysHash allows key aggregation to be optimize, by allowing the caller @@ -226,6 +231,22 @@ func WithTaprootKeyTweak(scriptRoot []byte) KeyAggOption { } } +// WithBIP86KeyTweak specifies that then during key aggregation, the BIP 86 +// tweak which just commits to the hash of the serialized public key should be +// used. This option should be used when signing with a key that was derived +// using BIP 86. +func WithBIP86KeyTweak() KeyAggOption { + return func(o *keyAggOption) { + o.tweaks = []KeyTweakDesc{ + { + IsXOnly: true, + }, + } + o.taprootTweak = true + o.bip86Tweak = true + } +} + // defaultKeyAggOptions returns the set of default arguments for key // aggregation. func defaultKeyAggOptions() *keyAggOption { @@ -370,18 +391,34 @@ func AggregateKeys(keys []*btcec.PublicKey, sort bool, // We'll copy over the key at this point, since this represents the // aggregated key before any tweaks have been applied. This'll be used // as the internal key for script path proofs. + finalKeyJ.ToAffine() combinedKey := btcec.NewPublicKey(&finalKeyJ.X, &finalKeyJ.Y) // At this point, if this is a taproot tweak, then we'll modify the // base tweak value to use the BIP 341 tweak value. if opts.taprootTweak { + // Emulate the same behavior as txscript.ComputeTaprootOutputKey + // which only operates on the x-only public key. + key, _ := schnorr.ParsePubKey(schnorr.SerializePubKey( + combinedKey, + )) + + // We only use the actual tweak bytes if we're not committing + // to a BIP-0086 key only spend output. Otherwise, we just + // commit to the internal key and an empty byte slice as the + // root hash. + tweakBytes := []byte{} + if !opts.bip86Tweak { + tweakBytes = opts.tweaks[0].Tweak[:] + } + // Compute the taproot key tagged hash of: // h_tapTweak(internalKey || scriptRoot). We only do this for // the first one, as you can only specify a single tweak when // using the taproot mode with this API. tapTweakHash := chainhash.TaggedHash( - chainhash.TagTapTweak, schnorr.SerializePubKey(combinedKey), - opts.tweaks[0].Tweak[:], + chainhash.TagTapTweak, schnorr.SerializePubKey(key), + tweakBytes, ) opts.tweaks[0].Tweak = *tapTweakHash } diff --git a/btcec/schnorr/musig2/musig2_test.go b/btcec/schnorr/musig2/musig2_test.go index bfd7c081..1ff31513 100644 --- a/btcec/schnorr/musig2/musig2_test.go +++ b/btcec/schnorr/musig2/musig2_test.go @@ -324,6 +324,8 @@ func testMultiPartySign(t *testing.T, taprootTweak []byte, var ctxOpts []ContextOption switch { + case len(taprootTweak) == 0: + ctxOpts = append(ctxOpts, WithBip86TweakCtx()) case taprootTweak != nil: ctxOpts = append(ctxOpts, WithTaprootTweakCtx(taprootTweak)) case len(tweaks) != 0: @@ -469,4 +471,10 @@ func TestMuSigMultiParty(t *testing.T) { testMultiPartySign(t, testTweak[:]) }) + + t.Run("taproot_bip_86", func(t *testing.T) { + t.Parallel() + + testMultiPartySign(t, []byte{}) + }) } diff --git a/btcec/schnorr/musig2/sign.go b/btcec/schnorr/musig2/sign.go index 16c5cd2f..4f9439fe 100644 --- a/btcec/schnorr/musig2/sign.go +++ b/btcec/schnorr/musig2/sign.go @@ -117,6 +117,11 @@ type signOptions struct { // the taproot tweak also commits to the public key, which in this case // is the aggregated key before the tweak. taprootTweak []byte + + // bip86Tweak specifies that the taproot tweak should be done in a BIP + // 86 style, where we don't expect an actual tweak and instead just + // commit to the public key itself. + bip86Tweak bool } // defaultSignOptions returns the default set of signing operations. @@ -164,6 +169,20 @@ func WithTaprootSignTweak(scriptRoot []byte) SignOption { } } +// 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, +// instead we just commit to the internal key. +// +// This option should be used in the taproot context to create a valid +// signature for the keypath spend for taproot, when the output key was +// generated using BIP 86. +func WithBip86SignTweak() SignOption { + return func(o *signOptions) { + o.bip86Tweak = true + } +} + // Sign generates a musig2 partial signature given the passed key set, secret // nonce, public nonce, and private keys. This method returns an error if the // generated nonces are either too large, or end up mapping to the point at @@ -200,11 +219,16 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, keyAggOpts := []KeyAggOption{ WithKeysHash(keysHash), WithUniqueKeyIndex(uniqueKeyIndex), } - if opts.taprootTweak != nil { + switch { + case opts.bip86Tweak: + keyAggOpts = append( + keyAggOpts, WithBIP86KeyTweak(), + ) + case opts.taprootTweak != nil: keyAggOpts = append( keyAggOpts, WithTaprootKeyTweak(opts.taprootTweak), ) - } else { + case len(opts.tweaks) != 0: keyAggOpts = append(keyAggOpts, WithKeyTweaks(opts.tweaks...)) } @@ -394,11 +418,16 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte, keyAggOpts := []KeyAggOption{ WithKeysHash(keysHash), WithUniqueKeyIndex(uniqueKeyIndex), } - if opts.taprootTweak != nil { + switch { + case opts.bip86Tweak: + keyAggOpts = append( + keyAggOpts, WithBIP86KeyTweak(), + ) + case opts.taprootTweak != nil: keyAggOpts = append( keyAggOpts, WithTaprootKeyTweak(opts.taprootTweak), ) - } else { + case len(opts.tweaks) != 0: keyAggOpts = append(keyAggOpts, WithKeyTweaks(opts.tweaks...)) } @@ -569,7 +598,7 @@ func WithTweakedCombine(msg [32]byte, keys []*btcec.PublicKey, // output key, where the internal key is the aggregated key pre-tweak. // // This option should be used over WithTweakedCombine when attempting to -// aggregate signatures for a top-level taproot keysepnd, where the output key +// aggregate signatures for a top-level taproot keyspend, where the output key // commits to a script root. func WithTaprootTweakedCombine(msg [32]byte, keys []*btcec.PublicKey, scriptRoot []byte, sort bool) CombineOption { @@ -585,6 +614,28 @@ func WithTaprootTweakedCombine(msg [32]byte, keys []*btcec.PublicKey, } } +// WithBip86TweakedCombine is similar to the WithTaprootTweakedCombine option, +// but assumes a BIP 341 + BIP 86 context where the final tweaked key is to be +// used as the output key, where the internal key is the aggregated key +// pre-tweak. +// +// This option should be used over WithTaprootTweakedCombine when attempting to +// aggregate signatures for a top-level taproot keyspend, where the output key +// was generated using BIP 86. +func WithBip86TweakedCombine(msg [32]byte, keys []*btcec.PublicKey, + sort bool) CombineOption { + + return func(o *combineOptions) { + combinedKey, _, tweakAcc, _ := AggregateKeys( + keys, sort, WithBIP86KeyTweak(), + ) + + o.msg = msg + o.combinedKey = combinedKey.FinalKey + o.tweakAcc = tweakAcc + } +} + // CombineSigs combines the set of public keys given the final aggregated // nonce, and the series of partial signatures for each nonce. func CombineSigs(combinedNonce *btcec.PublicKey,