From 1a65f1ccf02541f3ec19f8761ff269f0a7172d11 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 1 Mar 2022 18:39:37 -0800 Subject: [PATCH 01/16] btcec/schnorr/musig2: add key musig2 key aggregation routines In this commit, we add the set of key aggregation routines for musig2. This includes the main public key aggregation method, as well as the aggregation coefficient which is used to compute "mu" when signing. The logic in this implementation is based on the musig2 paper, as well as this spec: https://github.com/ElementsProject/secp256k1-zkp/blob/master/doc/musig-spec.mediawiki. --- btcec/schnorr/musig2/keys.go | 166 +++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 btcec/schnorr/musig2/keys.go diff --git a/btcec/schnorr/musig2/keys.go b/btcec/schnorr/musig2/keys.go new file mode 100644 index 0000000000..f69cc31063 --- /dev/null +++ b/btcec/schnorr/musig2/keys.go @@ -0,0 +1,166 @@ +// Copyright 2013-2022 The btcsuite developers + +package musig2 + +import ( + "bytes" + "sort" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" +) + +var ( + // KeyAggTagList is the tagged hash tag used to compute the hash of the + // list of sorted public keys. + KeyAggTagList = []byte("KeyAgg list") + + // KeyAggTagCoeff is the tagged hash tag used to compute the key + // aggregation coefficient for each key. + KeyAggTagCoeff = []byte("KeyAgg coefficient") +) + +// sortableKeys defines a type of slice of public keys that implements the sort +// interface for BIP 340 keys. +type sortableKeys []*btcec.PublicKey + +// Less reports whether the element with index i must sort before the element +// with index j. +func (s sortableKeys) Less(i, j int) bool { + keyIBytes := schnorr.SerializePubKey(s[i]) + keyJBytes := schnorr.SerializePubKey(s[j]) + + return bytes.Compare(keyIBytes, keyJBytes) == -1 +} + +// Swap swaps the elements with indexes i and j. +func (s sortableKeys) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Len is the number of elements in the collection. +func (s sortableKeys) Len() int { + return len(s) +} + +// sortKeys takes a set of schnorr public keys and returns a new slice that is +// a copy of the keys sorted in lexicographical order bytes on the x-only +// pubkey serialization. +func sortKeys(keys []*btcec.PublicKey) []*btcec.PublicKey { + keySet := sortableKeys(keys) + if sort.IsSorted(keySet) { + return keys + } + + sort.Sort(keySet) + return keySet +} + +// keyHashFingerprint computes the tagged hash of the series of (sorted) public +// keys passed as input. This is used to compute the aggregation coefficient +// for each key. The final computation is: +// * H(tag=KeyAgg list, pk1 || pk2..) +func keyHashFingerprint(keys []*btcec.PublicKey, sort bool) []byte { + var keyBytes bytes.Buffer + + if sort { + keys = sortKeys(keys) + } + + for _, key := range keys { + keyBytes.Write(schnorr.SerializePubKey(key)) + } + + h := chainhash.TaggedHash(KeyAggTagList, keyBytes.Bytes()) + return h[:] +} + +// isSecondKey returns true if the passed public key is the second key in the +// (sorted) keySet passed. +func isSecondKey(keySet []*btcec.PublicKey, targetKey *btcec.PublicKey) bool { + // For this comparison, we want to compare the raw serialized version + // instead of the full pubkey, as it's possible we're dealing with a + // pubkey that _actually_ has an odd y coordinate. + equalBytes := func(a, b *btcec.PublicKey) bool { + return bytes.Equal( + schnorr.SerializePubKey(a), + schnorr.SerializePubKey(b), + ) + } + + for i := range keySet { + if !equalBytes(keySet[i], keySet[0]) { + return equalBytes(keySet[i], targetKey) + } + } + + return false +} + +// aggregationCoefficient computes the key aggregation coefficient for the +// specified target key. The coefficient is computed as: +// * H(tag=KeyAgg coefficient, keyHashFingerprint(pks) || pk) +func aggregationCoefficient(keySet []*btcec.PublicKey, + targetKey *btcec.PublicKey, sort bool) *btcec.ModNScalar { + + var mu btcec.ModNScalar + + // If this is the second key, then this coefficient is just one. + // + // TODO(roasbeef): use intermediate cache to keep track of the second + // key, can just store an index, otherwise this is O(n^2) + if isSecondKey(keySet, targetKey) { + return mu.SetInt(1) + } + + // Otherwise, we'll compute the full finger print hash for this given + // key and then use that to compute the coefficient tagged hash: + // * H(tag=KeyAgg coefficient, keyHashFingerprint(pks, pk) || pk) + var coefficientBytes bytes.Buffer + coefficientBytes.Write(keyHashFingerprint(keySet, sort)) + coefficientBytes.Write(schnorr.SerializePubKey(targetKey)) + + muHash := chainhash.TaggedHash(KeyAggTagCoeff, coefficientBytes.Bytes()) + + mu.SetByteSlice(muHash[:]) + + return &mu +} + +// TODO(roasbeef): make proper IsEven func + +// AggregateKeys takes a list of possibly unsorted keys and returns a single +// aggregated key as specified by the musig2 key aggregation algorithm. +func AggregateKeys(keys []*btcec.PublicKey, sort bool) *btcec.PublicKey { + // Sort the set of public key so we know we're working with them in + // sorted order for all the routines below. + if sort { + keys = sortKeys(keys) + } + + // For each key, we'll compute the intermediate blinded key: a_i*P_i, + // where a_i is the aggregation coefficient for that key, and P_i is + // the key itself, then accumulate that (addition) into the main final + // key: P = P_1 + P_2 ... P_N. + var finalKeyJ btcec.JacobianPoint + for _, key := range keys { + // Port the key over to Jacobian coordinates as we need it in + // this format for the routines below. + var keyJ btcec.JacobianPoint + key.AsJacobian(&keyJ) + + // Compute the aggregation coefficient for the key, then + // multiply it by the key itself: P_i' = a_i*P_i. + var tweakedKeyJ btcec.JacobianPoint + a := aggregationCoefficient(keys, key, sort) + btcec.ScalarMultNonConst(a, &keyJ, &tweakedKeyJ) + + // Finally accumulate this into the final key in an incremental + // fashion. + btcec.AddNonConst(&finalKeyJ, &tweakedKeyJ, &finalKeyJ) + } + + finalKeyJ.ToAffine() + return btcec.NewPublicKey(&finalKeyJ.X, &finalKeyJ.Y) +} From 8343e462a62e6aa6c5a19e4bf5a1e7eb8e94609a Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 1 Mar 2022 18:42:43 -0800 Subject: [PATCH 02/16] btcec/schnorr/musig2: add nonce generation & aggregation funcs In this commit, we add the ability to generate the secret+public nonces, as well as combine a series of nonces into a single combined nonce (which is used when doing multi signing). --- btcec/schnorr/musig2/nonces.go | 208 +++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 btcec/schnorr/musig2/nonces.go diff --git a/btcec/schnorr/musig2/nonces.go b/btcec/schnorr/musig2/nonces.go new file mode 100644 index 0000000000..7f9339d217 --- /dev/null +++ b/btcec/schnorr/musig2/nonces.go @@ -0,0 +1,208 @@ +// Copyright 2013-2016 The btcsuite developers +// Copyright (c) 2015-2021 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package musig2 + +import ( + "crypto/rand" + + "github.com/btcsuite/btcd/btcec/v2" +) + +const ( + // PubNonceSize is the size of the public nonces. Each public nonce is + // serialized the full compressed encoding, which uses 32 bytes for each + // nonce. + PubNonceSize = 66 + + // SecNonceSize is the size of the secret nonces for musig2. The secret + // nonces are the corresponding private keys to the public nonce points. + SecNonceSize = 64 +) + +// Nonces holds the public and secret nonces required for musig2. +// +// TODO(roasbeef): methods on this to help w/ parsing, etc? +type Nonces struct { + // PubNonce holds the two 33-byte compressed encoded points that serve + // as the public set of nonces. + PubNonce [PubNonceSize]byte + + // SecNonce holds the two 32-byte scalar values that are the private + // keys to the two public nonces. + SecNonce [SecNonceSize]byte +} + +// secNonceToPubNonce takes our two secrete nonces, and produces their two +// corresponding EC points, serialized in compressed format. +func secNonceToPubNonce(secNonce *[SecNonceSize]byte) [PubNonceSize]byte { + var k1Mod, k2Mod btcec.ModNScalar + k1Mod.SetByteSlice(secNonce[:btcec.PrivKeyBytesLen]) + k2Mod.SetByteSlice(secNonce[btcec.PrivKeyBytesLen:]) + + var r1, r2 btcec.JacobianPoint + btcec.ScalarBaseMultNonConst(&k1Mod, &r1) + btcec.ScalarBaseMultNonConst(&k2Mod, &r2) + + // Next, we'll convert the key in jacobian format to a normal public + // key expressed in affine coordinates. + r1.ToAffine() + r2.ToAffine() + r1Pub := btcec.NewPublicKey(&r1.X, &r1.Y) + r2Pub := btcec.NewPublicKey(&r2.X, &r2.Y) + + var pubNonce [PubNonceSize]byte + + // The public nonces are serialized as: R1 || R2, where both keys are + // serialized in compressed format. + copy(pubNonce[:], r1Pub.SerializeCompressed()) + copy( + pubNonce[btcec.PubKeyBytesLenCompressed:], + r2Pub.SerializeCompressed(), + ) + + return pubNonce +} + +// NonceGenOption is a function option that allows callers to modify how nonce +// generation happens. +type NonceGenOption func(*nonceGenOpts) + +// nonceGenOpts is the set of options that control how nonce generation happens. +type nonceGenOpts struct { + randReader func(b []byte) (int, error) +} + +// defaultNonceGenOpts returns the default set of nonce generation options. +func defaultNonceGenOpts() *nonceGenOpts { + return &nonceGenOpts{ + // By default, we always use the crypto/rand reader, but the + // caller is able to specify their own generation, which can be + // useful for deterministic tests. + randReader: rand.Read, + } +} + +// GenNonces generates the secret nonces, as well as the public nonces which +// correspond to an EC point generated using the secret nonce as a private key. +func GenNonces(options ...NonceGenOption) (*Nonces, error) { + opts := defaultNonceGenOpts() + for _, opt := range options { + opt(opts) + } + + // Generate two 32-byte random values that'll be the private keys to + // the public nonces. + var k1, k2 [32]byte + if _, err := opts.randReader(k1[:]); err != nil { + return nil, err + } + if _, err := opts.randReader(k2[:]); err != nil { + return nil, err + } + + var nonces Nonces + + var k1Mod, k2Mod btcec.ModNScalar + k1Mod.SetBytes(&k1) + k2Mod.SetBytes(&k2) + + // The secret nonces are serialized as the concatenation of the two 32 + // byte secret nonce values. + k1Mod.PutBytesUnchecked(nonces.SecNonce[:]) + k2Mod.PutBytesUnchecked(nonces.SecNonce[btcec.PrivKeyBytesLen:]) + + // Next, we'll generate R_1 = k_1*G and R_2 = k_2*G. Along the way we + // need to map our nonce values into mod n scalars so we can work with + // the btcec API. + nonces.PubNonce = secNonceToPubNonce(nonces.SecNonce) + + return &nonces, nil +} + +// AggregateNonces aggregates the set of a pair of public nonces for each party +// into a single aggregated nonces to be used for multi-signing. +func AggregateNonces(pubNonces [][PubNonceSize]byte) ([PubNonceSize]byte, error) { + // combineNonces is a helper function that aggregates (adds) up a + // series of nonces encoded in compressed format. It uses a slicing + // function to extra 33 bytes at a time from the packed 2x public + // nonces. + type nonceSlicer func([PubNonceSize]byte) []byte + combineNonces := func(slicer nonceSlicer) (*btcec.PublicKey, error) { + // Convert the set of nonces into jacobian coordinates we can + // use to accumulate them all into each other. + pubNonceJs := make([]*btcec.JacobianPoint, len(pubNonces)) + for i, pubNonceBytes := range pubNonces { + // Using the slicer, extract just the bytes we need to + // decode. + var nonceJ btcec.JacobianPoint + pubNonce, err := btcec.ParsePubKey( + slicer(pubNonceBytes), + ) + if err != nil { + return nil, err + } + + pubNonce.AsJacobian(&nonceJ) + pubNonceJs[i] = &nonceJ + } + + // Now that we have the set of complete nonces, we'll aggregate + // them: R = R_i + R_i+1 + ... + R_i+n. + var aggregateNonce btcec.JacobianPoint + for _, pubNonceJ := range pubNonceJs { + btcec.AddNonConst( + &aggregateNonce, pubNonceJ, &aggregateNonce, + ) + } + + // Now that we've aggregated all the points, we need to check + // if this point is the point at infinity, if so, then we'll + // just return the generator. At a later step, the malicious + // party will be detected. + if aggregateNonce == infinityPoint { + // TODO(roasbeef): better way to get the generator w/ + // the new API? -- via old curve params instead? + var generator btcec.JacobianPoint + one := new(btcec.ModNScalar).SetInt(1) + btcec.ScalarBaseMultNonConst(one, &generator) + + generator.ToAffine() + return btcec.NewPublicKey( + &generator.X, &generator.Y, + ), nil + } + + aggregateNonce.ToAffine() + return btcec.NewPublicKey( + &aggregateNonce.X, &aggregateNonce.Y, + ), nil + } + + // The final nonce public nonce is actually two nonces, one that + // aggregate the first nonce of all the parties, and the other that + // aggregates the second nonce of all the parties. + var finalNonce [PubNonceSize]byte + combinedNonce1, err := combineNonces(func(n [PubNonceSize]byte) []byte { + return n[:btcec.PubKeyBytesLenCompressed] + }) + if err != nil { + return finalNonce, err + } + combinedNonce2, err := combineNonces(func(n [PubNonceSize]byte) []byte { + return n[btcec.PubKeyBytesLenCompressed:] + }) + if err != nil { + return finalNonce, err + } + + copy(finalNonce[:], combinedNonce1.SerializeCompressed()) + copy( + finalNonce[btcec.PubKeyBytesLenCompressed:], + combinedNonce2.SerializeCompressed(), + ) + + return finalNonce, nil +} From bb7ba7b1fc64f71fed963e043274a49b7ea60449 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 1 Mar 2022 18:43:49 -0800 Subject: [PATCH 03/16] btcec/schnorr/musig2: add partial sig generation, validation, and combination In this commit, we build on the prior two commits by adding the ability to generate partial musig2 signatures, validate them individually, and finally combine them into a single signature. Much of the logic here is unoptimized, and will be optimized in a later commit. In addition, we also want to eventually have a nicer API to support the book keeping necessary during multi signing. --- btcec/schnorr/musig2/nonces.go | 2 +- btcec/schnorr/musig2/sign.go | 423 +++++++++++++++++++++++++++++++++ 2 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 btcec/schnorr/musig2/sign.go diff --git a/btcec/schnorr/musig2/nonces.go b/btcec/schnorr/musig2/nonces.go index 7f9339d217..bebc327124 100644 --- a/btcec/schnorr/musig2/nonces.go +++ b/btcec/schnorr/musig2/nonces.go @@ -117,7 +117,7 @@ func GenNonces(options ...NonceGenOption) (*Nonces, error) { // Next, we'll generate R_1 = k_1*G and R_2 = k_2*G. Along the way we // need to map our nonce values into mod n scalars so we can work with // the btcec API. - nonces.PubNonce = secNonceToPubNonce(nonces.SecNonce) + nonces.PubNonce = secNonceToPubNonce(&nonces.SecNonce) return &nonces, nil } diff --git a/btcec/schnorr/musig2/sign.go b/btcec/schnorr/musig2/sign.go new file mode 100644 index 0000000000..3bce9f1561 --- /dev/null +++ b/btcec/schnorr/musig2/sign.go @@ -0,0 +1,423 @@ +// Copyright 2013-2016 The btcsuite developers +// Copyright (c) 2015-2021 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package musig2 + +import ( + "bytes" + "fmt" + + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" +) + +var ( + // NonceBlindTag is that tag used to construct the value b, which + // blinds the second public nonce of each party. + NonceBlindTag = []byte("MuSig/noncecoef") + + // ChallengeHashTag is the tag used to construct the challenge hash + ChallengeHashTag = []byte("BIP0340/challenge") + + // ErrNoncePointAtInfinity is returned if during signing, the fully + // combined public nonce is the point at infinity. + ErrNoncePointAtInfinity = fmt.Errorf("signing nonce is the infinity " + + "point") + + // ErrPrivKeyZero is returned when the private key for signing is + // actually zero. + ErrPrivKeyZero = fmt.Errorf("priv key is zero") + + // ErrPartialSigInvalid is returned when a partial is found to be + // invalid. + ErrPartialSigInvalid = fmt.Errorf("partial signature is invalid") + + // ErrSecretNonceZero is returned when a secret nonce is passed in a + // zero. + ErrSecretNonceZero = fmt.Errorf("secret nonce is blank") +) + +// infinityPoint is the jacobian representation of the point at infinity. +var infinityPoint btcec.JacobianPoint + +// PartialSignature reprints a partial (s-only) musig2 multi-signature. This +// isn't a valid schnorr signature by itself, as it needs to be aggregated +// along with the other partial signatures to be completed. +type PartialSignature struct { + S *btcec.ModNScalar + + R *btcec.PublicKey +} + +// NewPartialSignature returns a new instances of the partial sig struct. +func NewPartialSignature(s *btcec.ModNScalar, + r *btcec.PublicKey) PartialSignature { + + return PartialSignature{ + S: s, + R: r, + } +} + +// SignOption is a functional option argument that allows callers to modify the +// way we generate musig2 schnorr signatures. +type SignOption func(*signOptions) + +// signOptions houses the set of functional options that can be used to modify +// the method used to generate the musig2 partial signature. +type signOptions struct { + // fastSign determines if we'll skip the check at the end of the + // routine where we attempt to verify the produced signature. + fastSign bool + + // sortKeys determines if the set of keys should be sorted before doing + // key aggregation. + sortKeys bool +} + +// defaultSignOptions returns the default set of signing operations. +func defaultSignOptions() *signOptions { + return &signOptions{} +} + +// WithFastSign forces signing to skip the extra verification step at the end. +// Performance sensitive applications may opt to use this option to speed up +// the signing operation. +func WithFastSign() SignOption { + return func(o *signOptions) { + o.fastSign = true + } +} + +// WithSortedKeys determines if the set of signing public keys are to be sorted +// or not before doing key aggregation. +func WithSortedKeys() SignOption { + return func(o *signOptions) { + o.sortKeys = 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 +// infinity. +func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, + combinedNonce [PubNonceSize]byte, pubKeys []*btcec.PublicKey, + msg [32]byte, signOpts ...SignOption) (*PartialSignature, error) { + + // First, parse the set of optional signing options. + opts := defaultSignOptions() + for _, option := range signOpts { + option(opts) + } + + // Next, we'll parse the public nonces into R1 and R2. + r1, err := btcec.ParsePubKey( + combinedNonce[:btcec.PubKeyBytesLenCompressed], + ) + if err != nil { + return nil, err + } + r2, err := btcec.ParsePubKey( + combinedNonce[btcec.PubKeyBytesLenCompressed:], + ) + if err != nil { + return nil, err + } + + // Next we'll construct the aggregated public key based on the set of + // signers. + combinedKey := AggregateKeys(pubKeys, opts.sortKeys) + + // Next we'll compute the value b, that blinds our second public + // nonce: + // * b = h(tag=NonceBlindTag, combinedNonce || combinedKey || m). + var ( + nonceMsgBuf bytes.Buffer + nonceBlinder btcec.ModNScalar + ) + nonceMsgBuf.Write(combinedNonce[:]) + nonceMsgBuf.Write(schnorr.SerializePubKey(combinedKey)) + nonceMsgBuf.Write(msg[:]) + nonceBlindHash := chainhash.TaggedHash( + NonceBlindTag, nonceMsgBuf.Bytes(), + ) + nonceBlinder.SetByteSlice(nonceBlindHash[:]) + + var nonce, r1J, r2J btcec.JacobianPoint + r1.AsJacobian(&r1J) + r2.AsJacobian(&r2J) + + // With our nonce blinding value, we'll now combine both the public + // nonces, using the blinding factor to tweak the second nonce: + // * R = R_1 + b*R_2 + btcec.ScalarMultNonConst(&nonceBlinder, &r2J, &r2J) + btcec.AddNonConst(&r1J, &r2J, &nonce) + + // If the combined nonce it eh point at infinity, then we'll bail out. + if nonce == infinityPoint { + return nil, ErrNoncePointAtInfinity + } + + // 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 + k1.SetByteSlice(secNonce[:btcec.PrivKeyBytesLen]) + k2.SetByteSlice(secNonce[btcec.PrivKeyBytesLen:]) + + if k1.IsZero() || k2.IsZero() { + 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() { + k1.Negate() + k2.Negate() + } + + privKeyScalar := privKey.Key + if privKeyScalar.IsZero() { + return nil, ErrPrivKeyZero + } + + // If the y coordinate of the public key is odd xor the y coordinate of + // the combined public key is odd, then we'll negate the private key. + pubKey := privKey.PubKey() + pubKeyYIsOdd := func() bool { + pubKeyBytes := pubKey.SerializeCompressed() + return pubKeyBytes[0] == secp.PubKeyFormatCompressedOdd + }() + combinedKeyYIsOdd := func() bool { + combinedKeyBytes := combinedKey.SerializeCompressed() + return combinedKeyBytes[0] == secp.PubKeyFormatCompressedOdd + }() + if pubKeyYIsOdd != combinedKeyYIsOdd { + privKeyScalar.Negate() + } + + // Next we'll create the challenge hash that commits to the combined + // 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(combinedKey)) + challengeMsg.Write(msg[:]) + challengeBytes := chainhash.TaggedHash( + ChallengeHashTag, challengeMsg.Bytes(), + ) + var e btcec.ModNScalar + e.SetByteSlice(challengeBytes[:]) + + // Next, we'll compute mu, our aggregation coefficient for the key that + // we're signing with. + mu := aggregationCoefficient(pubKeys, pubKey, opts.sortKeys) + + // With mu constructed, we can finally generate our partial signature + // as: s = (k1_1 + b*k_2 + e*mu*d) mod n. + s := new(btcec.ModNScalar) + s.Add(&k1).Add(k2.Mul(&nonceBlinder)).Add(e.Mul(mu).Mul(&privKeyScalar)) + + sig := NewPartialSignature(s, nonceKey) + + // If we're not in fast sign mode, then we'll also validate our partial + // signature. + if !opts.fastSign { + pubNonce := secNonceToPubNonce(&secNonce) + sigValid := sig.Verify( + pubNonce, combinedNonce, pubKeys, pubKey, msg, + signOpts..., + ) + if !sigValid { + return nil, fmt.Errorf("sig is invalid!") + } + } + + return &sig, nil +} + +// Verify implements partial signature verification given the public nonce for +// the signer, aggregate nonce, signer set and finally the message being +// signed. +func (p *PartialSignature) Verify(pubNonce [PubNonceSize]byte, + combinedNonce [PubNonceSize]byte, keySet []*btcec.PublicKey, + signingKey *btcec.PublicKey, msg [32]byte, signOpts ...SignOption) bool { + + pubKey := schnorr.SerializePubKey(signingKey) + return verifyPartialSig( + p, pubNonce, combinedNonce, keySet, pubKey, msg, signOpts..., + ) == nil +} + +// verifyPartialSig attempts to verify a partial schnorr signature given the +// necessary parameters. This is the internal version of Verify that returns +// detailed errors. signed. +func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte, + combinedNonce [PubNonceSize]byte, keySet []*btcec.PublicKey, + pubKey []byte, msg [32]byte, signOpts ...SignOption) error { + + opts := defaultSignOptions() + for _, option := range signOpts { + option(opts) + } + + // First we'll map the internal partial signature back into something + // we can manipulate. + s := partialSig.S + + // Next we'll parse out the two public nonces into something we can + // use. + // + // TODO(roasbeef): consolidate, new method + r1, err := btcec.ParsePubKey( + combinedNonce[:btcec.PubKeyBytesLenCompressed], + ) + if err != nil { + return err + } + r2, err := btcec.ParsePubKey( + combinedNonce[btcec.PubKeyBytesLenCompressed:], + ) + if err != nil { + return err + } + + // Next we'll construct the aggregated public key based on the set of + // signers. + combinedKey := AggregateKeys(keySet, opts.sortKeys) + + // Next we'll compute the value b, that blinds our second public + // nonce: + // * b = h(tag=NonceBlindTag, combinedNonce || combinedKey || m). + var ( + nonceMsgBuf bytes.Buffer + nonceBlinder btcec.ModNScalar + ) + nonceMsgBuf.Write(combinedNonce[:]) + nonceMsgBuf.Write(schnorr.SerializePubKey(combinedKey)) + nonceMsgBuf.Write(msg[:]) + nonceBlindHash := chainhash.TaggedHash(NonceBlindTag, nonceMsgBuf.Bytes()) + nonceBlinder.SetByteSlice(nonceBlindHash[:]) + + var nonce, r1J, r2J btcec.JacobianPoint + r1.AsJacobian(&r1J) + r2.AsJacobian(&r2J) + + // With our nonce blinding value, we'll now combine both the public + // nonces, using the blinding factor to tweak the second nonce: + // * R = R_1 + b*R_2 + btcec.ScalarMultNonConst(&nonceBlinder, &r2J, &r2J) + btcec.AddNonConst(&r1J, &r2J, &nonce) + + // Next, we'll parse out the set of public nonces this signer used to + // generate the signature. + pubNonce1, err := btcec.ParsePubKey( + pubNonce[:btcec.PubKeyBytesLenCompressed], + ) + if err != nil { + return err + } + pubNonce2, err := btcec.ParsePubKey( + pubNonce[btcec.PubKeyBytesLenCompressed:], + ) + if err != nil { + return err + } + + // 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, pubNonce1J, pubNonce2J btcec.JacobianPoint + pubNonce1.AsJacobian(&pubNonce1J) + pubNonce2.AsJacobian(&pubNonce2J) + btcec.ScalarMultNonConst(&nonceBlinder, &pubNonce2J, &pubNonce2J) + btcec.AddNonConst(&pubNonce1J, &pubNonce2J, &pubNonceJ) + + nonce.ToAffine() + + // If the combined nonce used in the challenge hash has an odd y + // coordinate, then we'll negate our final public nonce. + // + // TODO(roasbeef): make into func + if nonce.Y.IsOdd() { + pubNonceJ.ToAffine() + pubNonceJ.Y.Negate(1) + pubNonceJ.Y.Normalize() + } + + // Next we'll create the challenge hash that commits to the combined + // 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(btcec.NewPublicKey( + &nonce.X, &nonce.Y, + ))) + challengeMsg.Write(schnorr.SerializePubKey(combinedKey)) + challengeMsg.Write(msg[:]) + challengeBytes := chainhash.TaggedHash( + ChallengeHashTag, challengeMsg.Bytes(), + ) + var e btcec.ModNScalar + e.SetByteSlice(challengeBytes[:]) + + signingKey, err := schnorr.ParsePubKey(pubKey) + if err != nil { + return err + } + + // Next, we'll compute mu, our aggregation coefficient for the key that + // we're signing with. + mu := aggregationCoefficient(keySet, signingKey, opts.sortKeys) + + // If the combined key has an odd y coordinate, then we'll negate the + // signer key. + var signKeyJ btcec.JacobianPoint + signingKey.AsJacobian(&signKeyJ) + combinedKeyBytes := combinedKey.SerializeCompressed() + if combinedKeyBytes[0] == secp.PubKeyFormatCompressedOdd { + signKeyJ.ToAffine() + signKeyJ.Y.Negate(1) + signKeyJ.Y.Normalize() + } + + // In the final set, we'll check that: s*G == R' + e*mu*P. + var sG, rP btcec.JacobianPoint + btcec.ScalarBaseMultNonConst(s, &sG) + btcec.ScalarMultNonConst(e.Mul(mu), &signKeyJ, &rP) + btcec.AddNonConst(&rP, &pubNonceJ, &rP) + + sG.ToAffine() + rP.ToAffine() + + if sG != rP { + return ErrPartialSigInvalid + } + + return nil +} + +// 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, + partialSigs []*PartialSignature) *schnorr.Signature { + + var combinedSig btcec.ModNScalar + for _, partialSig := range partialSigs { + combinedSig.Add(partialSig.S) + } + + // TODO(roasbeef): less verbose way to get the x coord... + var nonceJ btcec.JacobianPoint + combinedNonce.AsJacobian(&nonceJ) + nonceJ.ToAffine() + + return schnorr.NewSignature(&nonceJ.X, &combinedSig) +} From d25f072e715ba4d32469d1fd680e807fa0bfcba6 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 1 Mar 2022 18:48:09 -0800 Subject: [PATCH 04/16] btcec/schnorr/musig2: add test vectors from secp256k1-zkp In this commit, we add test vectors which are extracted from the secp256k1-zkp/ codebase and match up with the current draft specification. --- btcec/schnorr/musig2/musig2_test.go | 252 ++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 btcec/schnorr/musig2/musig2_test.go diff --git a/btcec/schnorr/musig2/musig2_test.go b/btcec/schnorr/musig2/musig2_test.go new file mode 100644 index 0000000000..0b56f916ea --- /dev/null +++ b/btcec/schnorr/musig2/musig2_test.go @@ -0,0 +1,252 @@ +// Copyright 2013-2016 The btcsuite developers +// Copyright (c) 2015-2021 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package musig2 + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" +) + +// TestMuSig2SgnTestVectors tests that this implementation of musig2 matches +// the secp256k1-zkp test vectors. +func TestMuSig2SignTestVectors(t *testing.T) { + t.Parallel() +} + +var ( + key1Bytes, _ = hex.DecodeString("F9308A019258C31049344F85F89D5229B53" + + "1C845836F99B08601F113BCE036F9") + key2Bytes, _ = hex.DecodeString("DFF1D77F2A671C5F36183726DB2341BE58F" + + "EAE1DA2DECED843240F7B502BA659") + key3Bytes, _ = hex.DecodeString("3590A94E768F8E1815C2F24B4D80A8E3149" + + "316C3518CE7B7AD338368D038CA66") + + testKeys = [][]byte{key1Bytes, key2Bytes, key3Bytes} + + keyCombo1, _ = hex.DecodeString("E5830140512195D74C8307E39637CBE5FB730EBEAB80EC514CF88A877CEEEE0B") + keyCombo2, _ = hex.DecodeString("D70CD69A2647F7390973DF48CBFA2CCC407B8B2D60B08C5F1641185C7998A290") + keyCombo3, _ = hex.DecodeString("81A8B093912C9E481408D09776CEFB48AEB8B65481B6BAAFB3C5810106717BEB") + keyCombo4, _ = hex.DecodeString("2EB18851887E7BDC5E830E89B19DDBC28078F1FA88AAD0AD01CA06FE4F80210B") +) + +// TestMuSig2KeyAggTestVectors tests that this implementation of musig2 key +// aggregation lines up with the secp256k1-zkp test vectors. +func TestMuSig2KeyAggTestVectors(t *testing.T) { + t.Parallel() + + testCases := []struct { + keyOrder []int + expectedKey []byte + }{ + // Keys in backwards lexicographical order. + { + keyOrder: []int{0, 1, 2}, + expectedKey: keyCombo1, + }, + + // Keys in sorted order. + { + keyOrder: []int{2, 1, 0}, + expectedKey: keyCombo2, + }, + + // Only the first key. + { + keyOrder: []int{0, 0, 0}, + expectedKey: keyCombo3, + }, + + // Duplicate the first key and second keys. + { + keyOrder: []int{0, 0, 1, 1}, + expectedKey: keyCombo4, + }, + } + for i, testCase := range testCases { + testName := fmt.Sprintf("%v", testCase.keyOrder) + t.Run(testName, func(t *testing.T) { + var keys []*btcec.PublicKey + for _, keyIndex := range testCase.keyOrder { + keyBytes := testKeys[keyIndex] + pub, err := schnorr.ParsePubKey(keyBytes) + if err != nil { + t.Fatalf("unable to parse pubkeys: %v", err) + } + + keys = append(keys, pub) + } + + combinedKey := AggregateKeys(keys, false) + combinedKeyBytes := schnorr.SerializePubKey(combinedKey) + if !bytes.Equal(combinedKeyBytes, testCase.expectedKey) { + t.Fatalf("case: #%v, invalid aggregation: "+ + "expected %x, got %x", i, testCase.expectedKey, + combinedKeyBytes) + } + }) + } +} + +func mustParseHex(str string) []byte { + b, err := hex.DecodeString(str) + if err != nil { + panic(fmt.Errorf("unable to parse hex: %v", err)) + } + + return b +} + +func parseKey(xHex string) *btcec.PublicKey { + xB, err := hex.DecodeString(xHex) + if err != nil { + panic(err) + } + + var x, y btcec.FieldVal + x.SetByteSlice(xB) + if !btcec.DecompressY(&x, false, &y) { + panic("x not on curve") + } + y.Normalize() + + return btcec.NewPublicKey(&x, &y) +} + +var ( + signSetPrivKey, _ = btcec.PrivKeyFromBytes( + mustParseHex("7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671"), + ) + signSetPubKey, _ = schnorr.ParsePubKey(schnorr.SerializePubKey(signSetPrivKey.PubKey())) + + signTestMsg = mustParseHex("F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF") + + signSetKey2, _ = schnorr.ParsePubKey( + mustParseHex("F9308A019258C31049344F85F89D5229B531C845836F99B086" + + "01F113BCE036F9"), + ) + signSetKey3, _ = schnorr.ParsePubKey( + mustParseHex("DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843" + + "240F7B502BA659"), + ) + + signSetKeys = []*btcec.PublicKey{signSetPubKey, signSetKey2, signSetKey3} +) + +// TestMuSig2SigningTestVectors tests that the musig2 implementation produces +// the same set of signatures. +func TestMuSig2SigningTestVectors(t *testing.T) { + t.Parallel() + + var aggregatedNonce [PubNonceSize]byte + copy( + aggregatedNonce[:], + mustParseHex("028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61"), + ) + copy( + aggregatedNonce[33:], + mustParseHex("037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9"), + ) + + var secNonce [SecNonceSize]byte + copy(secNonce[:], mustParseHex("508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61")) + copy(secNonce[32:], mustParseHex("FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F7")) + + testCases := []struct { + keyOrder []int + expectedPartialSig []byte + }{ + { + keyOrder: []int{0, 1, 2}, + expectedPartialSig: mustParseHex("68537CC5234E505BD14061F8DA9E90C220A181855FD8BDB7F127BB12403B4D3B"), + }, + { + keyOrder: []int{1, 0, 2}, + expectedPartialSig: mustParseHex("2DF67BFFF18E3DE797E13C6475C963048138DAEC5CB20A357CECA7C8424295EA"), + }, + + { + keyOrder: []int{1, 2, 0}, + expectedPartialSig: mustParseHex("0D5B651E6DE34A29A12DE7A8B4183B4AE6A7F7FBE15CDCAFA4A3D1BCAABC7517"), + }, + } + + var msg [32]byte + copy(msg[:], signTestMsg) + + for _, testCase := range testCases { + testName := fmt.Sprintf("%v", testCase.keyOrder) + t.Run(testName, func(t *testing.T) { + keySet := make([]*btcec.PublicKey, 0, len(testCase.keyOrder)) + for _, keyIndex := range testCase.keyOrder { + keySet = append(keySet, signSetKeys[keyIndex]) + } + + partialSig, err := Sign( + secNonce, signSetPrivKey, aggregatedNonce, keySet, msg, + ) + if err != nil { + t.Fatalf("unable to generate partial sig: %v", err) + } + + var partialSigBytes [32]byte + partialSig.S.PutBytesUnchecked(partialSigBytes[:]) + + if !bytes.Equal(partialSigBytes[:], testCase.expectedPartialSig) { + t.Fatalf("sigs don't match: expected %x, got %x", + testCase.expectedPartialSig, partialSigBytes, + ) + } + }) + } +} + +type signer struct { + privKey *btcec.PrivateKey + pubKey *btcec.PublicKey + + nonces *Nonces + + partialSig *PartialSignature +} + +type signerSet []signer + +func (s signerSet) keys() []*btcec.PublicKey { + keys := make([]*btcec.PublicKey, len(s)) + for i := 0; i < len(s); i++ { + keys[i] = s[i].pubKey + } + + return keys +} + +func (s signerSet) partialSigs() []*PartialSignature { + sigs := make([]*PartialSignature, len(s)) + for i := 0; i < len(s); i++ { + sigs[i] = s[i].partialSig + } + + return sigs +} + +func (s signerSet) pubNonces() [][PubNonceSize]byte { + nonces := make([][PubNonceSize]byte, len(s)) + for i := 0; i < len(s); i++ { + nonces[i] = s[i].nonces.PubNonce + } + + return nonces +} + +func (s signerSet) combinedKey() *btcec.PublicKey { + return AggregateKeys(s.keys(), false) +} From 69a42a35663bfabd0219c0194b1a9468d6bf494d Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 1 Mar 2022 18:49:05 -0800 Subject: [PATCH 05/16] btcec/schnorr/musig2: add multi-party signing test case w/ 100 signers In this commit, we add a final test case that exercises the act of generating partial signatures amongst 100 signers, combining them into a single signature, and finally verifying to make sure the final signature is valid. --- btcec/schnorr/musig2/musig2_test.go | 76 +++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/btcec/schnorr/musig2/musig2_test.go b/btcec/schnorr/musig2/musig2_test.go index 0b56f916ea..8e3d3931c5 100644 --- a/btcec/schnorr/musig2/musig2_test.go +++ b/btcec/schnorr/musig2/musig2_test.go @@ -250,3 +250,79 @@ func (s signerSet) pubNonces() [][PubNonceSize]byte { func (s signerSet) combinedKey() *btcec.PublicKey { return AggregateKeys(s.keys(), false) } + +// TestMuSigMultiParty tests that for a given set of 100 signers, we're able to +// properly generate valid sub signatures, which ultimately can be combined +// into a single valid signature. +func TestMuSigMultiParty(t *testing.T) { + t.Parallel() + + // First generate the private key, public key, and nonce for each + // signer. + numSigners := 100 + signers := make(signerSet, 0, numSigners) + for i := 0; i < numSigners; i++ { + privKey, err := btcec.NewPrivateKey() + if err != nil { + t.Fatalf("unable to gen priv key: %v", err) + } + + pubKey, err := schnorr.ParsePubKey( + schnorr.SerializePubKey(privKey.PubKey()), + ) + if err != nil { + t.Fatalf("unable to gen key: %v", err) + } + + nonces, err := GenNonces() + if err != nil { + t.Fatalf("unable to gen nonces: %v", err) + } + + signers = append(signers, signer{ + privKey: privKey, + pubKey: pubKey, + nonces: nonces, + }) + } + + msg := sha256.Sum256([]byte("let's get taprooty")) + + // With the set of nonces generated, we can now generate our aggregate + // nonce. + combinedNonce, err := AggregateNonces(signers.pubNonces()) + if err != nil { + t.Fatalf("unable to gen nonces: %v", err) + } + + signerKeys := signers.keys() + combinedKey := signers.combinedKey() + + // Now for the signing phase: each signer will generate their partial + // signature and stash that away. We'll also store the final nonce as + // well, since we'll need that to validate the signature. + var finalNonce *btcec.PublicKey + for i := range signers { + signer := signers[i] + partialSig, err := Sign( + signer.nonces.SecNonce, signer.privKey, combinedNonce, + signerKeys, msg, + ) + if err != nil { + t.Fatalf("unable to generate partial sig: %v", err) + } + + signers[i].partialSig = partialSig + + if finalNonce == nil { + finalNonce = partialSig.R + } + } + + // Finally we'll combined all the nonces, and ensure that it validates + // as a single schnorr signature. + finalSig := CombineSigs(finalNonce, signers.partialSigs()) + if !finalSig.Verify(msg[:], combinedKey) { + t.Fatalf("final sig is invalid!") + } +} From 4b46b2298a690c4153a073c903c0a0d381796a35 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 9 Mar 2022 18:25:21 -0800 Subject: [PATCH 06/16] btcec/schnorr/musig2: add benchmarks --- btcec/schnorr/musig2/bench_test.go | 314 +++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 btcec/schnorr/musig2/bench_test.go diff --git a/btcec/schnorr/musig2/bench_test.go b/btcec/schnorr/musig2/bench_test.go new file mode 100644 index 0000000000..8bb42247c5 --- /dev/null +++ b/btcec/schnorr/musig2/bench_test.go @@ -0,0 +1,314 @@ +// Copyright 2013-2016 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package musig2 + +import ( + "encoding/hex" + "fmt" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +var ( + testPrivBytes = hexToModNScalar("9e0699c91ca1e3b7e3c9ba71eb71c89890872be97576010fe593fbf3fd57e66d") + + testMsg = hexToBytes("c301ba9de5d6053caad9f5eb46523f007702add2c62fa39de03146a36b8026b7") +) + +func hexToBytes(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic("invalid hex in source file: " + s) + } + return b +} + +func hexToModNScalar(s string) *btcec.ModNScalar { + b, err := hex.DecodeString(s) + if err != nil { + panic("invalid hex in source file: " + s) + } + var scalar btcec.ModNScalar + if overflow := scalar.SetByteSlice(b); overflow { + panic("hex in source file overflows mod N scalar: " + s) + } + return &scalar +} + +func genSigner(t *testing.B) signer { + privKey, err := btcec.NewPrivateKey() + if err != nil { + t.Fatalf("unable to gen priv key: %v", err) + } + + pubKey, err := schnorr.ParsePubKey( + schnorr.SerializePubKey(privKey.PubKey()), + ) + if err != nil { + t.Fatalf("unable to gen key: %v", err) + } + + nonces, err := GenNonces() + if err != nil { + t.Fatalf("unable to gen nonces: %v", err) + } + + return signer{ + privKey: privKey, + pubKey: pubKey, + nonces: nonces, + } +} + +var ( + testSig *PartialSignature + testErr error +) + +// BenchmarkPartialSign benchmarks how long it takes to generate a partial +// signature factoring in if the keys are sorted and also if we're in fast sign +// mode. +func BenchmarkPartialSign(b *testing.B) { + privKey := secp256k1.NewPrivateKey(testPrivBytes) + + for _, numSigners := range []int{10, 100} { + for _, fastSign := range []bool{true, false} { + for _, sortKeys := range []bool{true, false} { + name := fmt.Sprintf("num_signers=%v/fast_sign=%v/sort=%v", + numSigners, fastSign, sortKeys) + + nonces, err := GenNonces() + if err != nil { + b.Fatalf("unable to generate nonces: %v", err) + } + + signers := make(signerSet, numSigners) + for i := 0; i < numSigners; i++ { + signers[i] = genSigner(b) + } + + combinedNonce, err := AggregateNonces(signers.pubNonces()) + if err != nil { + b.Fatalf("unable to generate combined nonce: %v", err) + } + + var sig *PartialSignature + + var msg [32]byte + copy(msg[:], testMsg[:]) + + keys := signers.keys() + + b.Run(name, func(b *testing.B) { + var signOpts []SignOption + if fastSign { + signOpts = append(signOpts, WithFastSign()) + } + if sortKeys { + signOpts = append(signOpts, WithSortedKeys()) + } + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + sig, err = Sign( + nonces.SecNonce, privKey, combinedNonce, + keys, msg, signOpts..., + ) + } + + testSig = sig + testErr = err + }) + } + } + } +} + +// TODO(roasbeef): add impact of sorting ^ + +var sigOk bool + +// BenchmarkPartialVerify benchmarks how long it takes to verify a partial +// signature. +func BenchmarkPartialVerify(b *testing.B) { + privKey := secp256k1.NewPrivateKey(testPrivBytes) + + for _, numSigners := range []int{10, 100} { + for _, sortKeys := range []bool{true, false} { + name := fmt.Sprintf("sort_keys=%v/num_signers=%v", + sortKeys, numSigners) + + nonces, err := GenNonces() + if err != nil { + b.Fatalf("unable to generate nonces: %v", err) + } + + signers := make(signerSet, numSigners) + for i := 0; i < numSigners; i++ { + signers[i] = genSigner(b) + } + + combinedNonce, err := AggregateNonces( + signers.pubNonces(), + ) + if err != nil { + b.Fatalf("unable to generate combined "+ + "nonce: %v", err) + } + + var sig *PartialSignature + + var msg [32]byte + copy(msg[:], testMsg[:]) + + b.ReportAllocs() + b.ResetTimer() + + sig, err = Sign( + nonces.SecNonce, privKey, combinedNonce, + signers.keys(), msg, WithFastSign(), + ) + + keys := signers.keys() + pubKey := privKey.PubKey() + + b.Run(name, func(b *testing.B) { + var signOpts []SignOption + if sortKeys { + signOpts = append( + signOpts, WithSortedKeys(), + ) + } + + b.ResetTimer() + b.ReportAllocs() + + var ok bool + for i := 0; i < b.N; i++ { + ok = sig.Verify( + nonces.PubNonce, combinedNonce, + keys, pubKey, msg, + ) + } + sigOk = ok + }) + + } + } +} + +var finalSchnorrSig *schnorr.Signature + +// BenchmarkCombineSigs benchmarks how long it takes to combine a set amount of +// signatures. +func BenchmarkCombineSigs(b *testing.B) { + + for _, numSigners := range []int{10, 100} { + signers := make(signerSet, numSigners) + for i := 0; i < numSigners; i++ { + signers[i] = genSigner(b) + } + + combinedNonce, err := AggregateNonces(signers.pubNonces()) + if err != nil { + b.Fatalf("unable to generate combined nonce: %v", err) + } + + var msg [32]byte + copy(msg[:], testMsg[:]) + + var finalNonce *btcec.PublicKey + for i := range signers { + signer := signers[i] + partialSig, err := Sign( + signer.nonces.SecNonce, signer.privKey, + combinedNonce, signers.keys(), msg, + ) + if err != nil { + b.Fatalf("unable to generate partial sig: %v", + err) + } + + signers[i].partialSig = partialSig + + if finalNonce == nil { + finalNonce = partialSig.R + } + } + + sigs := signers.partialSigs() + + name := fmt.Sprintf("num_signers=%v", numSigners) + b.Run(name, func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + finalSig := CombineSigs(finalNonce, sigs) + + finalSchnorrSig = finalSig + }) + } +} + +var testNonce [PubNonceSize]byte + +// BenchmarkAggregateNonces benchmarks how long it takes to combine nonces. +func BenchmarkAggregateNonces(b *testing.B) { + for _, numSigners := range []int{10, 100} { + signers := make(signerSet, numSigners) + for i := 0; i < numSigners; i++ { + signers[i] = genSigner(b) + } + + nonces := signers.pubNonces() + + name := fmt.Sprintf("num_signers=%v", numSigners) + b.Run(name, func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + pubNonce, err := AggregateNonces(nonces) + if err != nil { + b.Fatalf("unable to generate nonces: %v", err) + } + + testNonce = pubNonce + }) + } +} + +var testKey *btcec.PublicKey + +// BenchmarkAggregateKeys benchmarks how long it takes to aggregate public +// keys. +func BenchmarkAggregateKeys(b *testing.B) { + for _, numSigners := range []int{10, 100} { + for _, sortKeys := range []bool{true, false} { + signers := make(signerSet, numSigners) + for i := 0; i < numSigners; i++ { + signers[i] = genSigner(b) + } + + signerKeys := signers.keys() + + name := fmt.Sprintf("num_signers=%v/sort_keys=%v", + numSigners, sortKeys) + + b.Run(name, func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + + aggKey := AggregateKeys(signerKeys, sortKeys) + + testKey = aggKey + }) + } + } +} From e85e7c3ac72eb14617e781d70bc057a5f4d49b44 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 10 Mar 2022 18:47:52 -0800 Subject: [PATCH 07/16] btcec/schnorr/musig2: optimize signing+verification In this commit, we optimize signing+verification mainly by only computing values once, and reducing allocations when possible. The following optimizations have been implemented: * Use a single buffer allocation in keyHashFingerprint to avoid dynamic buffer growth+re-sizing * Remove the isSecondKey computation and replace that with a single routine that computes the index of the second unique key. * Optimize keyHashFingerprint usage by only computing it once during signing +verification. A further optimization is possible: use the x coordinate of a key for comparisons instead of computing the full sexualision. We need to do the latter atm, as the X() method of the public key struct will allocate more memory as it allocate and sets the buffer in place. The final benchmarks of before and after this commit: benchmark old ns/op new ns/op delta BenchmarkPartialSign/num_signers=10/fast_sign=true/sort=true-8 1227374 1194047 -2.72% BenchmarkPartialSign/num_signers=10/fast_sign=true/sort=false-8 1217743 1191468 -2.16% BenchmarkPartialSign/num_signers=10/fast_sign=false/sort=true-8 2755544 2698827 -2.06% BenchmarkPartialSign/num_signers=10/fast_sign=false/sort=false-8 2754749 2694547 -2.19% BenchmarkPartialSign/num_signers=100/fast_sign=true/sort=true-8 12382654 10561204 -14.71% BenchmarkPartialSign/num_signers=100/fast_sign=true/sort=false-8 12260134 10315376 -15.86% BenchmarkPartialSign/num_signers=100/fast_sign=false/sort=true-8 24832061 22009935 -11.36% BenchmarkPartialSign/num_signers=100/fast_sign=false/sort=false-8 24650086 21022833 -14.71% BenchmarkPartialVerify/sort_keys=true/num_signers=10-8 1485787 1473377 -0.84% BenchmarkPartialVerify/sort_keys=false/num_signers=10-8 1447275 1465139 +1.23% BenchmarkPartialVerify/sort_keys=true/num_signers=100-8 12503482 10672618 -14.64% BenchmarkPartialVerify/sort_keys=false/num_signers=100-8 12388289 10581398 -14.59% BenchmarkCombineSigs/num_signers=10-8 0.00 0.00 +0.00% BenchmarkCombineSigs/num_signers=100-8 0.00 0.00 -1.95% BenchmarkAggregateNonces/num_signers=10-8 0.00 0.00 -0.76% BenchmarkAggregateNonces/num_signers=100-8 0.00 0.00 +1.13% BenchmarkAggregateKeys/num_signers=10/sort_keys=true-8 0.00 0.00 -0.09% BenchmarkAggregateKeys/num_signers=10/sort_keys=false-8 0.00 0.01 +559.94% BenchmarkAggregateKeys/num_signers=100/sort_keys=true-8 0.01 0.01 -11.30% BenchmarkAggregateKeys/num_signers=100/sort_keys=false-8 0.01 0.01 -11.66% benchmark old allocs new allocs delta BenchmarkPartialSign/num_signers=10/fast_sign=true/sort=true-8 458 269 -41.27% BenchmarkPartialSign/num_signers=10/fast_sign=true/sort=false-8 409 222 -45.72% BenchmarkPartialSign/num_signers=10/fast_sign=false/sort=true-8 892 524 -41.26% BenchmarkPartialSign/num_signers=10/fast_sign=false/sort=false-8 841 467 -44.47% BenchmarkPartialSign/num_signers=100/fast_sign=true/sort=true-8 14366 3089 -78.50% BenchmarkPartialSign/num_signers=100/fast_sign=true/sort=false-8 13143 1842 -85.98% BenchmarkPartialSign/num_signers=100/fast_sign=false/sort=true-8 27596 4964 -82.01% BenchmarkPartialSign/num_signers=100/fast_sign=false/sort=false-8 26309 3707 -85.91% BenchmarkPartialVerify/sort_keys=true/num_signers=10-8 430 243 -43.49% BenchmarkPartialVerify/sort_keys=false/num_signers=10-8 430 243 -43.49% BenchmarkPartialVerify/sort_keys=true/num_signers=100-8 13164 1863 -85.85% BenchmarkPartialVerify/sort_keys=false/num_signers=100-8 13164 1863 -85.85% BenchmarkCombineSigs/num_signers=10-8 0 0 +0.00% BenchmarkCombineSigs/num_signers=100-8 0 0 +0.00% BenchmarkAggregateNonces/num_signers=10-8 0 0 +0.00% BenchmarkAggregateNonces/num_signers=100-8 0 0 +0.00% BenchmarkAggregateKeys/num_signers=10/sort_keys=true-8 0 0 +0.00% BenchmarkAggregateKeys/num_signers=10/sort_keys=false-8 0 0 +0.00% BenchmarkAggregateKeys/num_signers=100/sort_keys=true-8 0 0 +0.00% BenchmarkAggregateKeys/num_signers=100/sort_keys=false-8 0 0 +0.00% benchmark old bytes new bytes delta BenchmarkPartialSign/num_signers=10/fast_sign=true/sort=true-8 27854 14878 -46.59% BenchmarkPartialSign/num_signers=10/fast_sign=true/sort=false-8 25508 12605 -50.58% BenchmarkPartialSign/num_signers=10/fast_sign=false/sort=true-8 54982 29476 -46.39% BenchmarkPartialSign/num_signers=10/fast_sign=false/sort=false-8 52581 26805 -49.02% BenchmarkPartialSign/num_signers=100/fast_sign=true/sort=true-8 1880138 166996 -91.12% BenchmarkPartialSign/num_signers=100/fast_sign=true/sort=false-8 1820561 106295 -94.16% BenchmarkPartialSign/num_signers=100/fast_sign=false/sort=true-8 3706291 275344 -92.57% BenchmarkPartialSign/num_signers=100/fast_sign=false/sort=false-8 3642725 214122 -94.12% BenchmarkPartialVerify/sort_keys=true/num_signers=10-8 26995 14078 -47.85% BenchmarkPartialVerify/sort_keys=false/num_signers=10-8 26980 14078 -47.82% BenchmarkPartialVerify/sort_keys=true/num_signers=100-8 1822043 107767 -94.09% BenchmarkPartialVerify/sort_keys=false/num_signers=100-8 1822046 107752 -94.09% BenchmarkCombineSigs/num_signers=10-8 0 0 +0.00% BenchmarkCombineSigs/num_signers=100-8 0 0 +0.00% BenchmarkAggregateNonces/num_signers=10-8 0 0 +0.00% BenchmarkAggregateNonces/num_signers=100-8 0 0 +0.00% BenchmarkAggregateKeys/num_signers=10/sort_keys=true-8 0 0 +0.00% BenchmarkAggregateKeys/num_signers=10/sort_keys=false-8 0 0 +0.00% BenchmarkAggregateKeys/num_signers=100/sort_keys=true-8 0 0 +0.00% BenchmarkAggregateKeys/num_signers=100/sort_keys=false-8 0 0 +0.00% --- btcec/schnorr/musig2/bench_test.go | 7 +- btcec/schnorr/musig2/keys.go | 134 ++++++++++++++++++++-------- btcec/schnorr/musig2/musig2_test.go | 10 ++- btcec/schnorr/musig2/sign.go | 27 ++++-- 4 files changed, 134 insertions(+), 44 deletions(-) diff --git a/btcec/schnorr/musig2/bench_test.go b/btcec/schnorr/musig2/bench_test.go index 8bb42247c5..6d008641a7 100644 --- a/btcec/schnorr/musig2/bench_test.go +++ b/btcec/schnorr/musig2/bench_test.go @@ -301,11 +301,16 @@ func BenchmarkAggregateKeys(b *testing.B) { name := fmt.Sprintf("num_signers=%v/sort_keys=%v", numSigners, sortKeys) + uniqueKeyIndex := secondUniqueKeyIndex(signerKeys) + b.Run(name, func(b *testing.B) { b.ResetTimer() b.ReportAllocs() - aggKey := AggregateKeys(signerKeys, sortKeys) + aggKey := AggregateKeys( + signerKeys, sortKeys, + WithUniqueKeyIndex(uniqueKeyIndex), + ) testKey = aggKey }) diff --git a/btcec/schnorr/musig2/keys.go b/btcec/schnorr/musig2/keys.go index f69cc31063..2d0f26e022 100644 --- a/btcec/schnorr/musig2/keys.go +++ b/btcec/schnorr/musig2/keys.go @@ -28,6 +28,7 @@ type sortableKeys []*btcec.PublicKey // Less reports whether the element with index i must sort before the element // with index j. func (s sortableKeys) Less(i, j int) bool { + // TODO(roasbeef): more efficient way to compare... keyIBytes := schnorr.SerializePubKey(s[i]) keyJBytes := schnorr.SerializePubKey(s[j]) @@ -62,12 +63,14 @@ func sortKeys(keys []*btcec.PublicKey) []*btcec.PublicKey { // for each key. The final computation is: // * H(tag=KeyAgg list, pk1 || pk2..) func keyHashFingerprint(keys []*btcec.PublicKey, sort bool) []byte { - var keyBytes bytes.Buffer - if sort { keys = sortKeys(keys) } + // We'll create a single buffer and slice into that so the bytes buffer + // doesn't continually need to grow the underlying buffer. + keyAggBuf := make([]byte, 32*len(keys)) + keyBytes := bytes.NewBuffer(keyAggBuf[0:0]) for _, key := range keys { keyBytes.Write(schnorr.SerializePubKey(key)) } @@ -76,69 +79,128 @@ func keyHashFingerprint(keys []*btcec.PublicKey, sort bool) []byte { return h[:] } -// isSecondKey returns true if the passed public key is the second key in the -// (sorted) keySet passed. -func isSecondKey(keySet []*btcec.PublicKey, targetKey *btcec.PublicKey) bool { - // For this comparison, we want to compare the raw serialized version - // instead of the full pubkey, as it's possible we're dealing with a - // pubkey that _actually_ has an odd y coordinate. - equalBytes := func(a, b *btcec.PublicKey) bool { - return bytes.Equal( - schnorr.SerializePubKey(a), - schnorr.SerializePubKey(b), - ) - } - - for i := range keySet { - if !equalBytes(keySet[i], keySet[0]) { - return equalBytes(keySet[i], targetKey) - } - } - - return false +// keyBytesEqual returns true if two keys are the same from the PoV of BIP +// 340's 32-byte x-only public keys. +func keyBytesEqual(a, b *btcec.PublicKey) bool { + return bytes.Equal( + schnorr.SerializePubKey(a), + schnorr.SerializePubKey(b), + ) } // aggregationCoefficient computes the key aggregation coefficient for the // specified target key. The coefficient is computed as: // * H(tag=KeyAgg coefficient, keyHashFingerprint(pks) || pk) func aggregationCoefficient(keySet []*btcec.PublicKey, - targetKey *btcec.PublicKey, sort bool) *btcec.ModNScalar { + targetKey *btcec.PublicKey, keysHash []byte, + secondKeyIdx int) *btcec.ModNScalar { var mu btcec.ModNScalar // If this is the second key, then this coefficient is just one. - // - // TODO(roasbeef): use intermediate cache to keep track of the second - // key, can just store an index, otherwise this is O(n^2) - if isSecondKey(keySet, targetKey) { + if secondKeyIdx != -1 && keyBytesEqual(keySet[secondKeyIdx], targetKey) { return mu.SetInt(1) } // Otherwise, we'll compute the full finger print hash for this given // key and then use that to compute the coefficient tagged hash: // * H(tag=KeyAgg coefficient, keyHashFingerprint(pks, pk) || pk) - var coefficientBytes bytes.Buffer - coefficientBytes.Write(keyHashFingerprint(keySet, sort)) - coefficientBytes.Write(schnorr.SerializePubKey(targetKey)) + var coefficientBytes [64]byte + copy(coefficientBytes[:], keysHash[:]) + copy(coefficientBytes[32:], schnorr.SerializePubKey(targetKey)) - muHash := chainhash.TaggedHash(KeyAggTagCoeff, coefficientBytes.Bytes()) + muHash := chainhash.TaggedHash(KeyAggTagCoeff, coefficientBytes[:]) mu.SetByteSlice(muHash[:]) return &mu } -// TODO(roasbeef): make proper IsEven func +// secondUniqueKeyIndex returns the index of the second unique key. If all keys +// are the same, then a value of -1 is returned. +func secondUniqueKeyIndex(keySet []*btcec.PublicKey) int { + // Find the first key that isn't the same as the very first key (second + // unique key). + for i := range keySet { + if !keyBytesEqual(keySet[i], keySet[0]) { + return i + } + } + + // A value of negative one is used to indicate that all the keys in the + // sign set are actually equal, which in practice actually makes musig2 + // useless, but we need a value to distinguish this case. + return -1 +} + +// KeyAggOption is a functional option argument that allows callers to specify +// more or less information that has been pre-computed to the main routine. +type KeyAggOption func(*keyAggOption) + +// keyAggOption houses the set of functional options that modify key +// aggregation. +type keyAggOption struct { + // keyHash is the output of keyHashFingerprint for a given set of keys. + keyHash []byte + + // uniqueKeyIndex is the pre-computed index of the second unique key. + uniqueKeyIndex *int +} + +// WithKeysHash allows key aggregation to be optimize, by allowing the caller +// to specify the hash of all the keys. +func WithKeysHash(keyHash []byte) KeyAggOption { + return func(o *keyAggOption) { + o.keyHash = keyHash + } +} + +// WithUniqueKeyIndex allows the caller to specify the index of the second +// unique key. +func WithUniqueKeyIndex(idx int) KeyAggOption { + return func(o *keyAggOption) { + i := idx + o.uniqueKeyIndex = &i + } +} + +// defaultKeyAggOptions returns the set of default arguments for key +// aggregation. +func defaultKeyAggOptions() *keyAggOption { + return &keyAggOption{} +} // AggregateKeys takes a list of possibly unsorted keys and returns a single -// aggregated key as specified by the musig2 key aggregation algorithm. -func AggregateKeys(keys []*btcec.PublicKey, sort bool) *btcec.PublicKey { +// aggregated key as specified by the musig2 key aggregation algorithm. A nil +// value can be passed for keyHash, which causes this function to re-derive it. +func AggregateKeys(keys []*btcec.PublicKey, sort bool, + keyOpts ...KeyAggOption) *btcec.PublicKey { + + // First, parse the set of optional signing options. + opts := defaultKeyAggOptions() + for _, option := range keyOpts { + option(opts) + } + // Sort the set of public key so we know we're working with them in // sorted order for all the routines below. if sort { keys = sortKeys(keys) } + // The caller may provide the hash of all the keys as an optimization + // during signing, as it already needs to be computed. + if opts.keyHash == nil { + opts.keyHash = keyHashFingerprint(keys, sort) + } + + // A caller may also specify the unique key index themselves so we + // don't need to re-compute it. + if opts.uniqueKeyIndex == nil { + idx := secondUniqueKeyIndex(keys) + opts.uniqueKeyIndex = &idx + } + // For each key, we'll compute the intermediate blinded key: a_i*P_i, // where a_i is the aggregation coefficient for that key, and P_i is // the key itself, then accumulate that (addition) into the main final @@ -153,7 +215,9 @@ func AggregateKeys(keys []*btcec.PublicKey, sort bool) *btcec.PublicKey { // Compute the aggregation coefficient for the key, then // multiply it by the key itself: P_i' = a_i*P_i. var tweakedKeyJ btcec.JacobianPoint - a := aggregationCoefficient(keys, key, sort) + a := aggregationCoefficient( + keys, key, opts.keyHash, *opts.uniqueKeyIndex, + ) btcec.ScalarMultNonConst(a, &keyJ, &tweakedKeyJ) // Finally accumulate this into the final key in an incremental diff --git a/btcec/schnorr/musig2/musig2_test.go b/btcec/schnorr/musig2/musig2_test.go index 8e3d3931c5..edbff0b838 100644 --- a/btcec/schnorr/musig2/musig2_test.go +++ b/btcec/schnorr/musig2/musig2_test.go @@ -85,7 +85,10 @@ func TestMuSig2KeyAggTestVectors(t *testing.T) { keys = append(keys, pub) } - combinedKey := AggregateKeys(keys, false) + uniqueKeyIndex := secondUniqueKeyIndex(keys) + combinedKey := AggregateKeys( + keys, false, WithUniqueKeyIndex(uniqueKeyIndex), + ) combinedKeyBytes := schnorr.SerializePubKey(combinedKey) if !bytes.Equal(combinedKeyBytes, testCase.expectedKey) { t.Fatalf("case: #%v, invalid aggregation: "+ @@ -248,7 +251,10 @@ func (s signerSet) pubNonces() [][PubNonceSize]byte { } func (s signerSet) combinedKey() *btcec.PublicKey { - return AggregateKeys(s.keys(), false) + uniqueKeyIndex := secondUniqueKeyIndex(s.keys()) + return AggregateKeys( + s.keys(), false, WithUniqueKeyIndex(uniqueKeyIndex), + ) } // TestMuSigMultiParty tests that for a given set of 100 signers, we're able to diff --git a/btcec/schnorr/musig2/sign.go b/btcec/schnorr/musig2/sign.go index 3bce9f1561..11edcf8469 100644 --- a/btcec/schnorr/musig2/sign.go +++ b/btcec/schnorr/musig2/sign.go @@ -130,9 +130,17 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, return nil, err } + // Compute the hash of all the keys here as we'll need it do aggregrate + // the keys and also at the final step of signing. + keysHash := keyHashFingerprint(pubKeys, opts.sortKeys) + // Next we'll construct the aggregated public key based on the set of // signers. - combinedKey := AggregateKeys(pubKeys, opts.sortKeys) + uniqueKeyIndex := secondUniqueKeyIndex(pubKeys) + combinedKey := AggregateKeys( + pubKeys, opts.sortKeys, WithKeysHash(keysHash), + WithUniqueKeyIndex(uniqueKeyIndex), + ) // Next we'll compute the value b, that blinds our second public // nonce: @@ -220,7 +228,7 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, // Next, we'll compute mu, our aggregation coefficient for the key that // we're signing with. - mu := aggregationCoefficient(pubKeys, pubKey, opts.sortKeys) + mu := aggregationCoefficient(pubKeys, pubKey, keysHash, uniqueKeyIndex) // With mu constructed, we can finally generate our partial signature // as: s = (k1_1 + b*k_2 + e*mu*d) mod n. @@ -291,9 +299,18 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte, return err } + // Compute the hash of all the keys here as we'll need it do aggregrate + // the keys and also at the final step of verification. + keysHash := keyHashFingerprint(keySet, opts.sortKeys) + + uniqueKeyIndex := secondUniqueKeyIndex(keySet) + // Next we'll construct the aggregated public key based on the set of // signers. - combinedKey := AggregateKeys(keySet, opts.sortKeys) + combinedKey := AggregateKeys( + keySet, opts.sortKeys, + WithKeysHash(keysHash), WithUniqueKeyIndex(uniqueKeyIndex), + ) // Next we'll compute the value b, that blinds our second public // nonce: @@ -345,8 +362,6 @@ 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. - // - // TODO(roasbeef): make into func if nonce.Y.IsOdd() { pubNonceJ.ToAffine() pubNonceJ.Y.Negate(1) @@ -375,7 +390,7 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte, // Next, we'll compute mu, our aggregation coefficient for the key that // we're signing with. - mu := aggregationCoefficient(keySet, signingKey, opts.sortKeys) + mu := aggregationCoefficient(keySet, signingKey, keysHash, uniqueKeyIndex) // If the combined key has an odd y coordinate, then we'll negate the // signer key. From 743cbc840313efc82241252ba7e350bdfdae39b7 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 11 Mar 2022 19:00:33 -0800 Subject: [PATCH 08/16] btcec/schnorr/musig2: add safer signing API with Session+Context In this commit, we introduce an easier to use API for musig2 signing in the Session and Context structs. The Context struct represents a particular musig2 signing context which is defined by the set of signers. The struct can be serialized to disk as it contains no volatile information. A given context can be kept for each signer in the final set. The Session struct represents an ephemeral musig2 signing session. It handles nonce generation, key aggregation, nonce combination, signature combination, and final sig verification all in one API. The API also protects against nonce generation by not exposing nonces to the end user and also attempting to catch nonce re-use (assuming no process forking) across sessions. --- btcec/schnorr/musig2/bench_test.go | 40 ++-- btcec/schnorr/musig2/context.go | 305 ++++++++++++++++++++++++++++ btcec/schnorr/musig2/keys.go | 8 +- btcec/schnorr/musig2/musig2_test.go | 126 ++++++++---- btcec/schnorr/musig2/nonces.go | 13 +- btcec/schnorr/musig2/sign.go | 45 +++- btcec/schnorr/signature.go | 5 +- 7 files changed, 459 insertions(+), 83 deletions(-) create mode 100644 btcec/schnorr/musig2/context.go diff --git a/btcec/schnorr/musig2/bench_test.go b/btcec/schnorr/musig2/bench_test.go index 6d008641a7..c02ccf3e41 100644 --- a/btcec/schnorr/musig2/bench_test.go +++ b/btcec/schnorr/musig2/bench_test.go @@ -1,4 +1,4 @@ -// Copyright 2013-2016 The btcsuite developers +// Copyright 2013-2022 The btcsuite developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -11,7 +11,6 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" - "github.com/decred/dcrd/dcrec/secp256k1/v4" ) var ( @@ -74,19 +73,12 @@ var ( // signature factoring in if the keys are sorted and also if we're in fast sign // mode. func BenchmarkPartialSign(b *testing.B) { - privKey := secp256k1.NewPrivateKey(testPrivBytes) - for _, numSigners := range []int{10, 100} { for _, fastSign := range []bool{true, false} { for _, sortKeys := range []bool{true, false} { name := fmt.Sprintf("num_signers=%v/fast_sign=%v/sort=%v", numSigners, fastSign, sortKeys) - nonces, err := GenNonces() - if err != nil { - b.Fatalf("unable to generate nonces: %v", err) - } - signers := make(signerSet, numSigners) for i := 0; i < numSigners; i++ { signers[i] = genSigner(b) @@ -118,9 +110,12 @@ func BenchmarkPartialSign(b *testing.B) { for i := 0; i < b.N; i++ { sig, err = Sign( - nonces.SecNonce, privKey, combinedNonce, - keys, msg, signOpts..., + signers[0].nonces.SecNonce, signers[0].privKey, + combinedNonce, keys, msg, signOpts..., ) + if err != nil { + b.Fatalf("unable to generate sig: %v", err) + } } testSig = sig @@ -138,18 +133,11 @@ var sigOk bool // BenchmarkPartialVerify benchmarks how long it takes to verify a partial // signature. func BenchmarkPartialVerify(b *testing.B) { - privKey := secp256k1.NewPrivateKey(testPrivBytes) - for _, numSigners := range []int{10, 100} { for _, sortKeys := range []bool{true, false} { name := fmt.Sprintf("sort_keys=%v/num_signers=%v", sortKeys, numSigners) - nonces, err := GenNonces() - if err != nil { - b.Fatalf("unable to generate nonces: %v", err) - } - signers := make(signerSet, numSigners) for i := 0; i < numSigners; i++ { signers[i] = genSigner(b) @@ -172,12 +160,15 @@ func BenchmarkPartialVerify(b *testing.B) { b.ResetTimer() sig, err = Sign( - nonces.SecNonce, privKey, combinedNonce, - signers.keys(), msg, WithFastSign(), + signers[0].nonces.SecNonce, signers[0].privKey, + combinedNonce, signers.keys(), msg, ) + if err != nil { + b.Fatalf("unable to generate sig: %v", err) + } keys := signers.keys() - pubKey := privKey.PubKey() + pubKey := signers[0].pubKey b.Run(name, func(b *testing.B) { var signOpts []SignOption @@ -193,9 +184,12 @@ func BenchmarkPartialVerify(b *testing.B) { var ok bool for i := 0; i < b.N; i++ { ok = sig.Verify( - nonces.PubNonce, combinedNonce, + signers[0].nonces.PubNonce, combinedNonce, keys, pubKey, msg, ) + if !ok { + b.Fatalf("generated invalid sig!") + } } sigOk = ok }) @@ -301,7 +295,7 @@ func BenchmarkAggregateKeys(b *testing.B) { name := fmt.Sprintf("num_signers=%v/sort_keys=%v", numSigners, sortKeys) - uniqueKeyIndex := secondUniqueKeyIndex(signerKeys) + uniqueKeyIndex := secondUniqueKeyIndex(signerKeys, false) b.Run(name, func(b *testing.B) { b.ResetTimer() diff --git a/btcec/schnorr/musig2/context.go b/btcec/schnorr/musig2/context.go new file mode 100644 index 0000000000..22bd2b236f --- /dev/null +++ b/btcec/schnorr/musig2/context.go @@ -0,0 +1,305 @@ +// Copyright (c) 2013-2022 The btcsuite developers + +package musig2 + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" +) + +var ( + // ErrSignerNotInKeySet is returned when a the private key for a signer + // isn't included in the set of signing public keys. + ErrSignerNotInKeySet = fmt.Errorf("signing key is not found in key" + + " set") + + // ErrAlredyHaveAllNonces is called when RegisterPubNonce is called too + // many times for a given signing session. + ErrAlredyHaveAllNonces = fmt.Errorf("already have all nonces") + + // ErrAlredyHaveAllSigs is called when CombineSig is called too many + // times for a given signing session. + ErrAlredyHaveAllSigs = fmt.Errorf("already have all sigs") + + // ErrSigningContextReuse is returned if a user attempts to sign using + // the same signing context more than once. + ErrSigningContextReuse = fmt.Errorf("nonce already used") + + // ErrFinalSigInvalid is returned when the combined signature turns out + // to be invalid. + ErrFinalSigInvalid = fmt.Errorf("final signature is invalid") + + // ErrCombinedNonceUnavailable is returned when a caller attempts to + // sign a partial signature, without first having collected all the + // required combined nonces. + ErrCombinedNonceUnavailable = fmt.Errorf("missing combined nonce") +) + +// Context is a managed signing context for musig2. It takes care of things +// like securely generating secret nonces, aggregating keys and nonces, etc. +type Context struct { + // signingKey is the key we'll use for signing. + signingKey *btcec.PrivateKey + + // pubKey is our even-y coordinate public key. + pubKey *btcec.PublicKey + + // keySet is the set of all signers. + keySet []*btcec.PublicKey + + // combinedKey is the aggregated public key. + combinedKey *btcec.PublicKey + + // uniqueKeyIndex is the index of the second unique key in the keySet. + // This is used to speed up signing and verification computations. + uniqueKeyIndex int + + // keysHash is the hash of all the keys as defined in musig2. + keysHash []byte + + // shouldSort keeps track of if the public keys should be sorted before + // any operations. + shouldSort bool +} + +// NewContext creates a new signing context with the passed singing key and set +// of public keys for each of the other signers. +// +// NOTE: This struct should be used over the raw Sign API whenever possible. +func NewContext(signingKey *btcec.PrivateKey, + signers []*btcec.PublicKey, shouldSort bool) (*Context, error) { + + // As a sanity check, make sure the signing key is actually amongst the sit + // of signers. + // + // TODO(roasbeef): instead have pass all the _other_ signers? + pubKey, err := schnorr.ParsePubKey( + schnorr.SerializePubKey(signingKey.PubKey()), + ) + if err != nil { + return nil, err + } + + var keyFound bool + for _, key := range signers { + if key.IsEqual(pubKey) { + keyFound = true + break + } + } + if !keyFound { + return nil, ErrSignerNotInKeySet + } + + // Now that we know that we're actually a signer, we'll generate the + // key hash finger print and second unique key index so we can speed up + // signing later. + keysHash := keyHashFingerprint(signers, shouldSort) + uniqueKeyIndex := secondUniqueKeyIndex(signers, shouldSort) + + // Next, we'll use this information to compute the aggregated public + // key that'll be used for signing in practice. + combinedKey := AggregateKeys( + signers, shouldSort, WithKeysHash(keysHash), + WithUniqueKeyIndex(uniqueKeyIndex), + ) + + return &Context{ + signingKey: signingKey, + pubKey: pubKey, + keySet: signers, + combinedKey: combinedKey, + uniqueKeyIndex: uniqueKeyIndex, + keysHash: keysHash, + shouldSort: shouldSort, + }, nil +} + +// CombinedKey returns the combined public key that will be used to generate +// multi-signatures against. +func (c *Context) CombinedKey() btcec.PublicKey { + return *c.combinedKey +} + +// PubKey returns the public key of the signer of this session. +func (c *Context) PubKey() btcec.PublicKey { + return *c.pubKey +} + +// SigningKeys returns the set of keys used for signing. +func (c *Context) SigningKeys() []*btcec.PublicKey { + keys := make([]*btcec.PublicKey, len(c.keySet)) + copy(keys, c.keySet) + + return keys +} + +// Session represents a musig2 signing session. A new instance should be +// created each time a multi-signature is needed. The session struct handles +// nonces management, incremental partial sig vitrifaction, as well as final +// signature combination. Errors are returned when unsafe behavior such as +// nonce re-use is attempted. +// +// NOTE: This struct should be used over the raw Sign API whenever possible. +type Session struct { + ctx *Context + + localNonces *Nonces + + pubNonces [][PubNonceSize]byte + + combinedNonce *[PubNonceSize]byte + + msg [32]byte + + ourSig *PartialSignature + sigs []*PartialSignature + + finalSig *schnorr.Signature +} + +// NewSession creates a new musig2 signing session. +func (c *Context) NewSession() (*Session, error) { + localNonces, err := GenNonces() + if err != nil { + return nil, err + } + + s := &Session{ + ctx: c, + localNonces: localNonces, + pubNonces: make([][PubNonceSize]byte, 0, len(c.keySet)), + sigs: make([]*PartialSignature, 0, len(c.keySet)), + } + + s.pubNonces = append(s.pubNonces, localNonces.PubNonce) + + return s, nil +} + +// PublicNonce returns the public nonce for a signer. This should be sent to +// other parties before signing begins, so they can compute the aggregated +// public nonce. +func (s *Session) PublicNonce() [PubNonceSize]byte { + return s.localNonces.PubNonce +} + +// NumRegisteredNonces returns the total number of nonces that have been +// regsitered so far. +func (s *Session) NumRegisteredNonces() int { + return len(s.pubNonces) +} + +// RegisterPubNonce should be called for each public nonce from the set of +// signers. This method returns true once all the public nonces have been +// accounted for. +func (s *Session) RegisterPubNonce(nonce [PubNonceSize]byte) (bool, error) { + // If we already have all the nonces, then this method was called too many + // times. + haveAllNonces := len(s.pubNonces) == len(s.ctx.keySet) + if haveAllNonces { + return false, nil + } + + // Add this nonce and check again if we already have tall the nonces we + // need. + s.pubNonces = append(s.pubNonces, nonce) + haveAllNonces = len(s.pubNonces) == len(s.ctx.keySet) + + // If we have all the nonces, then we can go ahead and combine them + // now. + if haveAllNonces { + combinedNonce, err := AggregateNonces(s.pubNonces) + if err != nil { + return false, err + } + + s.combinedNonce = &combinedNonce + } + + return haveAllNonces, 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. +func (s *Session) Sign(msg [32]byte, + signOpts ...SignOption) (*PartialSignature, error) { + + s.msg = msg + + switch { + // If no local nonce is present, then this means we already signed, so + // we'll return an error to prevent nonce re-use. + case s.localNonces == nil: + return nil, ErrSigningContextReuse + + // We also need to make sure we have the combined nonce, otherwise this + // funciton was called too early. + case s.combinedNonce == nil: + return nil, ErrCombinedNonceUnavailable + } + + partialSig, err := Sign( + s.localNonces.SecNonce, s.ctx.signingKey, *s.combinedNonce, + s.ctx.keySet, msg, signOpts..., + ) + + // Now that we've generated our signature, we'll make sure to blank out + // our signing nonce. + s.localNonces = nil + + if err != nil { + return nil, err + } + + s.ourSig = partialSig + s.sigs = append(s.sigs, partialSig) + + return partialSig, nil +} + +// CombineSigs buffers a partial signature received from a signing party. The +// method returns true once all the signatures are available, and can be +// combined into the final signature. +func (s *Session) CombineSig(sig *PartialSignature) (bool, error) { + // First check if we already have all the signatures we need. We + // already accumulated our own signature when we generated the sig. + haveAllSigs := len(s.sigs) == len(s.ctx.keySet) + if haveAllSigs { + return false, ErrAlredyHaveAllSigs + } + + // TODO(roasbeef): incremental check for invalid sig, or just detect at + // the very end? + + // Accumulate this sig, and check again if we have all the sigs we + // need. + s.sigs = append(s.sigs, sig) + haveAllSigs = len(s.sigs) == len(s.ctx.keySet) + + // If we have all the signatures, then we can combine them all into the + // final signature. + if haveAllSigs { + finalSig := CombineSigs(s.ourSig.R, s.sigs) + + // We'll also verify the signature at this point to ensure it's + // valid. + // + // TODO(roasbef): allow skipping? + if !finalSig.Verify(s.msg[:], s.ctx.combinedKey) { + return false, ErrFinalSigInvalid + } + + s.finalSig = finalSig + } + + return haveAllSigs, nil +} + +// FinalSig returns the final combined multi-signature, if present. +func (s *Session) FinalSig() *schnorr.Signature { + return s.finalSig +} diff --git a/btcec/schnorr/musig2/keys.go b/btcec/schnorr/musig2/keys.go index 2d0f26e022..7c756697eb 100644 --- a/btcec/schnorr/musig2/keys.go +++ b/btcec/schnorr/musig2/keys.go @@ -118,7 +118,11 @@ func aggregationCoefficient(keySet []*btcec.PublicKey, // secondUniqueKeyIndex returns the index of the second unique key. If all keys // are the same, then a value of -1 is returned. -func secondUniqueKeyIndex(keySet []*btcec.PublicKey) int { +func secondUniqueKeyIndex(keySet []*btcec.PublicKey, sort bool) int { + if sort { + keySet = sortKeys(keySet) + } + // Find the first key that isn't the same as the very first key (second // unique key). for i := range keySet { @@ -197,7 +201,7 @@ func AggregateKeys(keys []*btcec.PublicKey, sort bool, // A caller may also specify the unique key index themselves so we // don't need to re-compute it. if opts.uniqueKeyIndex == nil { - idx := secondUniqueKeyIndex(keys) + idx := secondUniqueKeyIndex(keys, sort) opts.uniqueKeyIndex = &idx } diff --git a/btcec/schnorr/musig2/musig2_test.go b/btcec/schnorr/musig2/musig2_test.go index edbff0b838..0c8af2fca0 100644 --- a/btcec/schnorr/musig2/musig2_test.go +++ b/btcec/schnorr/musig2/musig2_test.go @@ -1,7 +1,4 @@ -// Copyright 2013-2016 The btcsuite developers -// Copyright (c) 2015-2021 The Decred developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. +// Copyright 2013-2022 The btcsuite developers package musig2 @@ -10,6 +7,7 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "sync" "testing" "github.com/btcsuite/btcd/btcec/v2" @@ -85,7 +83,7 @@ func TestMuSig2KeyAggTestVectors(t *testing.T) { keys = append(keys, pub) } - uniqueKeyIndex := secondUniqueKeyIndex(keys) + uniqueKeyIndex := secondUniqueKeyIndex(keys, false) combinedKey := AggregateKeys( keys, false, WithUniqueKeyIndex(uniqueKeyIndex), ) @@ -194,7 +192,8 @@ func TestMuSig2SigningTestVectors(t *testing.T) { } partialSig, err := Sign( - secNonce, signSetPrivKey, aggregatedNonce, keySet, msg, + secNonce, signSetPrivKey, aggregatedNonce, + keySet, msg, ) if err != nil { t.Fatalf("unable to generate partial sig: %v", err) @@ -251,7 +250,7 @@ func (s signerSet) pubNonces() [][PubNonceSize]byte { } func (s signerSet) combinedKey() *btcec.PublicKey { - uniqueKeyIndex := secondUniqueKeyIndex(s.keys()) + uniqueKeyIndex := secondUniqueKeyIndex(s.keys(), false) return AggregateKeys( s.keys(), false, WithUniqueKeyIndex(uniqueKeyIndex), ) @@ -263,10 +262,11 @@ func (s signerSet) combinedKey() *btcec.PublicKey { func TestMuSigMultiParty(t *testing.T) { t.Parallel() - // First generate the private key, public key, and nonce for each - // signer. - numSigners := 100 - signers := make(signerSet, 0, numSigners) + const numSigners = 100 + + // First generate the set of signers along with their public keys. + signerKeys := make([]*btcec.PrivateKey, numSigners) + signSet := make([]*btcec.PublicKey, numSigners) for i := 0; i < numSigners; i++ { privKey, err := btcec.NewPrivateKey() if err != nil { @@ -280,55 +280,105 @@ func TestMuSigMultiParty(t *testing.T) { t.Fatalf("unable to gen key: %v", err) } - nonces, err := GenNonces() + signerKeys[i] = privKey + signSet[i] = pubKey + } + + var combinedKey *btcec.PublicKey + + // Now that we have all the signers, we'll make a new context, then + // generate a new session for each of them(which handles nonce + // generation). + signers := make([]*Session, numSigners) + for i, signerKey := range signerKeys { + signCtx, err := NewContext(signerKey, signSet, false) if err != nil { - t.Fatalf("unable to gen nonces: %v", err) + t.Fatalf("unable to generate context: %v", err) } - signers = append(signers, signer{ - privKey: privKey, - pubKey: pubKey, - nonces: nonces, - }) + if combinedKey == nil { + k := signCtx.CombinedKey() + combinedKey = &k + } + + session, err := signCtx.NewSession() + if err != nil { + t.Fatalf("unable to generate new session: %v", err) + } + signers[i] = session } - msg := sha256.Sum256([]byte("let's get taprooty")) + // Next, in the pre-signing phase, we'll send all the nonces to each + // signer. + var wg sync.WaitGroup + for i, signCtx := range signers { + signCtx := signCtx - // With the set of nonces generated, we can now generate our aggregate - // nonce. - combinedNonce, err := AggregateNonces(signers.pubNonces()) - if err != nil { - t.Fatalf("unable to gen nonces: %v", err) + wg.Add(1) + go func(idx int, signer *Session) { + defer wg.Done() + + for j, otherCtx := range signers { + if idx == j { + continue + } + + nonce := otherCtx.PublicNonce() + haveAll, err := signer.RegisterPubNonce(nonce) + if err != nil { + t.Fatalf("unable to add public nonce") + } + + if j == len(signers)-1 && !haveAll { + t.Fatalf("all public nonces should have been detected") + } + } + }(i, signCtx) } - signerKeys := signers.keys() - combinedKey := signers.combinedKey() + wg.Wait() + + msg := sha256.Sum256([]byte("let's get taprooty")) - // Now for the signing phase: each signer will generate their partial - // signature and stash that away. We'll also store the final nonce as - // well, since we'll need that to validate the signature. - var finalNonce *btcec.PublicKey + // In the final step, we'll use the first signer as our combiner, and + // generate a signature for each signer, and then accumulate that with + // the combiner. + combiner := signers[0] for i := range signers { signer := signers[i] - partialSig, err := Sign( - signer.nonces.SecNonce, signer.privKey, combinedNonce, - signerKeys, msg, - ) + partialSig, err := signer.Sign(msg) if err != nil { t.Fatalf("unable to generate partial sig: %v", err) } - signers[i].partialSig = partialSig + // We don't need to combine the signature for the very first + // signer, as it already has that partial signature. + if i != 0 { + haveAll, err := combiner.CombineSig(partialSig) + if err != nil { + t.Fatalf("unable to combine sigs: %v", err) + } - if finalNonce == nil { - finalNonce = partialSig.R + if i == len(signers)-1 && !haveAll { + t.Fatalf("final sig wasn't reconstructed") + } } } // Finally we'll combined all the nonces, and ensure that it validates // as a single schnorr signature. - finalSig := CombineSigs(finalNonce, signers.partialSigs()) + finalSig := combiner.FinalSig() if !finalSig.Verify(msg[:], combinedKey) { t.Fatalf("final sig is invalid!") } + + // Verify that if we try to sign again with any of the existing + // signers, then we'll get an error as the nonces have already been + // used. + for _, signer := range signers { + _, err := signer.Sign(msg) + if err != ErrSigningContextReuse { + t.Fatalf("expected to get signing context reuse") + } + } } diff --git a/btcec/schnorr/musig2/nonces.go b/btcec/schnorr/musig2/nonces.go index bebc327124..4fd76450d9 100644 --- a/btcec/schnorr/musig2/nonces.go +++ b/btcec/schnorr/musig2/nonces.go @@ -1,7 +1,4 @@ -// Copyright 2013-2016 The btcsuite developers -// Copyright (c) 2015-2021 The Decred developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. +// Copyright 2013-2022 The btcsuite developers package musig2 @@ -22,6 +19,10 @@ const ( SecNonceSize = 64 ) +// zeroSecNonce is a secret nonce that's all zeroes. This is used to check that +// we're not attempting to re-use a nonce, and also protect callers from it. +var zeroSecNonce [SecNonceSize]byte + // Nonces holds the public and secret nonces required for musig2. // // TODO(roasbeef): methods on this to help w/ parsing, etc? @@ -37,7 +38,7 @@ type Nonces struct { // secNonceToPubNonce takes our two secrete nonces, and produces their two // corresponding EC points, serialized in compressed format. -func secNonceToPubNonce(secNonce *[SecNonceSize]byte) [PubNonceSize]byte { +func secNonceToPubNonce(secNonce [SecNonceSize]byte) [PubNonceSize]byte { var k1Mod, k2Mod btcec.ModNScalar k1Mod.SetByteSlice(secNonce[:btcec.PrivKeyBytesLen]) k2Mod.SetByteSlice(secNonce[btcec.PrivKeyBytesLen:]) @@ -117,7 +118,7 @@ func GenNonces(options ...NonceGenOption) (*Nonces, error) { // Next, we'll generate R_1 = k_1*G and R_2 = k_2*G. Along the way we // need to map our nonce values into mod n scalars so we can work with // the btcec API. - nonces.PubNonce = secNonceToPubNonce(&nonces.SecNonce) + nonces.PubNonce = secNonceToPubNonce(nonces.SecNonce) return &nonces, nil } diff --git a/btcec/schnorr/musig2/sign.go b/btcec/schnorr/musig2/sign.go index 11edcf8469..df3caf23ed 100644 --- a/btcec/schnorr/musig2/sign.go +++ b/btcec/schnorr/musig2/sign.go @@ -1,13 +1,11 @@ -// Copyright 2013-2016 The btcsuite developers -// Copyright (c) 2015-2021 The Decred developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. +// Copyright 2013-2022 The btcsuite developers package musig2 import ( "bytes" "fmt" + "io" secp "github.com/decred/dcrd/dcrec/secp256k1/v4" @@ -64,6 +62,34 @@ func NewPartialSignature(s *btcec.ModNScalar, } } +// Encode writes a serialized version of the partial signature to the passed +// io.Writer +func (p *PartialSignature) Encode(w io.Writer) error { + var sBytes [32]byte + p.S.PutBytes(&sBytes) + + if _, err := w.Write(sBytes[:]); err != nil { + return err + } + + return nil +} + +// Decode attempts to parse a serialized PartialSignature stored in the passed +// io reader. +func (p *PartialSignature) Decode(r io.Reader) error { + p.S = new(btcec.ModNScalar) + + var sBytes [32]byte + if _, err := io.ReadFull(r, sBytes[:]); err != nil { + return nil + } + + p.S.SetBytes(&sBytes) + + return nil +} + // SignOption is a functional option argument that allows callers to modify the // way we generate musig2 schnorr signatures. type SignOption func(*signOptions) @@ -130,13 +156,13 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, return nil, err } - // Compute the hash of all the keys here as we'll need it do aggregrate + // Compute the hash of all the keys here as we'll need it do aggregate // the keys and also at the final step of signing. keysHash := keyHashFingerprint(pubKeys, opts.sortKeys) // Next we'll construct the aggregated public key based on the set of // signers. - uniqueKeyIndex := secondUniqueKeyIndex(pubKeys) + uniqueKeyIndex := secondUniqueKeyIndex(pubKeys, opts.sortKeys) combinedKey := AggregateKeys( pubKeys, opts.sortKeys, WithKeysHash(keysHash), WithUniqueKeyIndex(uniqueKeyIndex), @@ -240,7 +266,7 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, // If we're not in fast sign mode, then we'll also validate our partial // signature. if !opts.fastSign { - pubNonce := secNonceToPubNonce(&secNonce) + pubNonce := secNonceToPubNonce(secNonce) sigValid := sig.Verify( pubNonce, combinedNonce, pubKeys, pubKey, msg, signOpts..., @@ -299,11 +325,10 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte, return err } - // Compute the hash of all the keys here as we'll need it do aggregrate + // Compute the hash of all the keys here as we'll need it do aggregate // the keys and also at the final step of verification. keysHash := keyHashFingerprint(keySet, opts.sortKeys) - - uniqueKeyIndex := secondUniqueKeyIndex(keySet) + uniqueKeyIndex := secondUniqueKeyIndex(keySet, opts.sortKeys) // Next we'll construct the aggregated public key based on the set of // signers. diff --git a/btcec/schnorr/signature.go b/btcec/schnorr/signature.go index ed4b9cb5bf..f4532c7d09 100644 --- a/btcec/schnorr/signature.go +++ b/btcec/schnorr/signature.go @@ -1,7 +1,4 @@ -// Copyright (c) 2013-2017 The btcsuite developers -// Copyright (c) 2015-2021 The Decred developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. +// Copyright (c) 2013-2022 The btcsuite developers package schnorr From 08187eb78602d01e430ed31de70ba6e090c4ecb3 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 30 Mar 2022 22:52:39 -0500 Subject: [PATCH 09/16] btcec/schnorr/musig2: add support for tweaked aggregated keys In this commit, we add support for signing with tweaked aggregated keys. Such signing is required when signing for a taproot output key that actually commits to a script tree root, or was generated using BIP 86. A series of new functional arguments (that can likely be de-dup'd using Go's new type params), have been added to allow callers to optionally flip on this new behavior. --- btcec/schnorr/musig2/bench_test.go | 2 +- btcec/schnorr/musig2/context.go | 61 +++++++++- btcec/schnorr/musig2/keys.go | 123 +++++++++++++++++++- btcec/schnorr/musig2/musig2_test.go | 102 +++++++++++++++-- btcec/schnorr/musig2/sign.go | 171 ++++++++++++++++++++++++---- 5 files changed, 417 insertions(+), 42 deletions(-) diff --git a/btcec/schnorr/musig2/bench_test.go b/btcec/schnorr/musig2/bench_test.go index c02ccf3e41..eaa6b21b3c 100644 --- a/btcec/schnorr/musig2/bench_test.go +++ b/btcec/schnorr/musig2/bench_test.go @@ -301,7 +301,7 @@ func BenchmarkAggregateKeys(b *testing.B) { b.ResetTimer() b.ReportAllocs() - aggKey := AggregateKeys( + aggKey, _, _, _ := AggregateKeys( signerKeys, sortKeys, WithUniqueKeyIndex(uniqueKeyIndex), ) diff --git a/btcec/schnorr/musig2/context.go b/btcec/schnorr/musig2/context.go index 22bd2b236f..3a3b29280c 100644 --- a/btcec/schnorr/musig2/context.go +++ b/btcec/schnorr/musig2/context.go @@ -59,17 +59,51 @@ type Context struct { // keysHash is the hash of all the keys as defined in musig2. keysHash []byte + // tweaks is a set of optional tweak values that affect the final + // combined public key. + tweaks []KeyTweakDesc + // shouldSort keeps track of if the public keys should be sorted before // any operations. shouldSort bool } +// ContextOption is a functional option argument that allows callers to modify +// the musig2 signing is done within a context. +type ContextOption func(*contextOptions) + +// contextOptions houses the set of functional options that can be used to +// musig2 signing protocol. +type contextOptions struct { + tweaks []KeyTweakDesc +} + +// defaultContextOptions returns the default context options. +func defaultContextOptions() *contextOptions { + return &contextOptions{} +} + +// WithTweakedContext specifies that within the context, the aggregated public +// key should be tweaked with the specified tweaks. +func WithTweakedContext(tweaks []KeyTweakDesc) ContextOption { + return func(o *contextOptions) { + o.tweaks = tweaks + } +} + // NewContext creates a new signing context with the passed singing key and set // of public keys for each of the other signers. // // NOTE: This struct should be used over the raw Sign API whenever possible. func NewContext(signingKey *btcec.PrivateKey, - signers []*btcec.PublicKey, shouldSort bool) (*Context, error) { + signers []*btcec.PublicKey, shouldSort bool, + ctxOpts ...ContextOption) (*Context, error) { + + // First, parse the set of optional context options. + opts := defaultContextOptions() + for _, option := range ctxOpts { + option(opts) + } // As a sanity check, make sure the signing key is actually amongst the sit // of signers. @@ -101,10 +135,14 @@ func NewContext(signingKey *btcec.PrivateKey, // Next, we'll use this information to compute the aggregated public // key that'll be used for signing in practice. - combinedKey := AggregateKeys( + combinedKey, _, _, err := AggregateKeys( signers, shouldSort, WithKeysHash(keysHash), WithUniqueKeyIndex(uniqueKeyIndex), + WithKeyTweaks(opts.tweaks...), ) + if err != nil { + return nil, err + } return &Context{ signingKey: signingKey, @@ -113,6 +151,7 @@ func NewContext(signingKey *btcec.PrivateKey, combinedKey: combinedKey, uniqueKeyIndex: uniqueKeyIndex, keysHash: keysHash, + tweaks: opts.tweaks, shouldSort: shouldSort, }, nil } @@ -160,6 +199,8 @@ type Session struct { finalSig *schnorr.Signature } +// TODO(roasbeef): optional arg to allow parsing in pre-generated nonces + // NewSession creates a new musig2 signing session. func (c *Context) NewSession() (*Session, error) { localNonces, err := GenNonces() @@ -242,6 +283,10 @@ func (s *Session) Sign(msg [32]byte, return nil, ErrCombinedNonceUnavailable } + if len(s.ctx.tweaks) != 0 { + signOpts = append(signOpts, WithTweaks(s.ctx.tweaks...)) + } + partialSig, err := Sign( s.localNonces.SecNonce, s.ctx.signingKey, *s.combinedNonce, s.ctx.keySet, msg, signOpts..., @@ -283,7 +328,17 @@ func (s *Session) CombineSig(sig *PartialSignature) (bool, error) { // If we have all the signatures, then we can combine them all into the // final signature. if haveAllSigs { - finalSig := CombineSigs(s.ourSig.R, s.sigs) + var combineOpts []CombineOption + if len(s.ctx.tweaks) != 0 { + combineOpts = append( + combineOpts, WithTweakedCombine( + s.msg, s.ctx.keySet, s.ctx.tweaks, + s.ctx.shouldSort, + ), + ) + } + + finalSig := CombineSigs(s.ourSig.R, s.sigs, combineOpts...) // We'll also verify the signature at this point to ensure it's // valid. diff --git a/btcec/schnorr/musig2/keys.go b/btcec/schnorr/musig2/keys.go index 7c756697eb..35c37d9309 100644 --- a/btcec/schnorr/musig2/keys.go +++ b/btcec/schnorr/musig2/keys.go @@ -4,8 +4,11 @@ package musig2 import ( "bytes" + "fmt" "sort" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -19,6 +22,10 @@ var ( // KeyAggTagCoeff is the tagged hash tag used to compute the key // aggregation coefficient for each key. KeyAggTagCoeff = []byte("KeyAgg coefficient") + + // ErrTweakedKeyIsInfinity is returned if while tweaking a key, we end + // up with the point at infinity. + ErrTweakedKeyIsInfinity = fmt.Errorf("tweaked key is infinity point") ) // sortableKeys defines a type of slice of public keys that implements the sort @@ -137,6 +144,18 @@ func secondUniqueKeyIndex(keySet []*btcec.PublicKey, sort bool) int { return -1 } +// KeyTweakDesc describes a tweak to be applied to the aggregated public key +// generation and signing process. The IsXOnly specifies if the target key +// should be converted to an x-only public key before tweaking. +type KeyTweakDesc struct { + // Tweak is the 32-byte value that will modify the public key. + Tweak [32]byte + + // IsXOnly if true, then the public key will be mapped to an x-only key + // before the tweaking operation is applied. + IsXOnly bool +} + // KeyAggOption is a functional option argument that allows callers to specify // more or less information that has been pre-computed to the main routine. type KeyAggOption func(*keyAggOption) @@ -149,6 +168,10 @@ type keyAggOption struct { // uniqueKeyIndex is the pre-computed index of the second unique key. uniqueKeyIndex *int + + // tweaks specifies a series of tweaks to be applied to the aggregated + // public key> + tweaks []KeyTweakDesc } // WithKeysHash allows key aggregation to be optimize, by allowing the caller @@ -168,17 +191,92 @@ func WithUniqueKeyIndex(idx int) KeyAggOption { } } +// WithKeyTweaks allows a caller to specify a series of 32-byte tweaks that +// should be applied to the final aggregated public key. +func WithKeyTweaks(tweaks ...KeyTweakDesc) KeyAggOption { + return func(o *keyAggOption) { + o.tweaks = tweaks + } +} + // defaultKeyAggOptions returns the set of default arguments for key // aggregation. func defaultKeyAggOptions() *keyAggOption { return &keyAggOption{} } +// hasEvenY returns true if the affine representation of the passed jacobian +// point has an even y coordinate. +// +// TODO(roasbeef): double check, can just check the y coord even not jacobian? +func hasEvenY(pJ btcec.JacobianPoint) bool { + pJ.ToAffine() + p := btcec.NewPublicKey(&pJ.X, &pJ.Y) + keyBytes := p.SerializeCompressed() + return keyBytes[0] == secp.PubKeyFormatCompressedEven +} + +// tweakKey applies a tweaks to the passed public key using the specified +// tweak. The parityAcc and tweakAcc are returned (in that order) which +// includes the accumulate ration of the parity factor and the tweak multiplied +// by the parity factor. The xOnly bool specifies if this is to be an x-only +// tweak or not. +func tweakKey(keyJ btcec.JacobianPoint, parityAcc btcec.ModNScalar, tweak [32]byte, + tweakAcc btcec.ModNScalar, + xOnly bool) (btcec.JacobianPoint, btcec.ModNScalar, btcec.ModNScalar, error) { + + // First we'll compute the new parity factor for this key. If the key has + // an odd y coordinate (not even), then we'll need to negate it (multiply + // by -1 mod n, in this case). + var parityFactor btcec.ModNScalar + if xOnly && !hasEvenY(keyJ) { + parityFactor.SetInt(1).Negate() + } else { + parityFactor.SetInt(1) + } + + // Next, map the tweak into a mod n integer so we can use it for + // manipulations below. + tweakInt := new(btcec.ModNScalar) + tweakInt.SetBytes(&tweak) + + // Next, we'll compute: Q_i = g*Q + t*G, where g is our parityFactor and t + // is the tweakInt above. We'll space things out a bit to make it easier to + // follow. + // + // First compute t*G: + var tweakedGenerator btcec.JacobianPoint + btcec.ScalarBaseMultNonConst(tweakInt, &tweakedGenerator) + + // Next compute g*Q: + btcec.ScalarMultNonConst(&parityFactor, &keyJ, &keyJ) + + // Finally add both of them together to get our final + // tweaked point. + btcec.AddNonConst(&tweakedGenerator, &keyJ, &keyJ) + + // As a sanity check, make sure that we didn't just end up with the + // point at infinity. + if keyJ == infinityPoint { + return keyJ, parityAcc, tweakAcc, ErrTweakedKeyIsInfinity + } + + // As a final wrap up step, we'll accumulate the parity + // factor and also this tweak into the final set of accumulators. + parityAcc.Mul(&parityFactor) + tweakAcc.Mul(&parityFactor).Add(tweakInt) + + return keyJ, parityAcc, tweakAcc, nil +} + // AggregateKeys takes a list of possibly unsorted keys and returns a single // aggregated key as specified by the musig2 key aggregation algorithm. A nil // value can be passed for keyHash, which causes this function to re-derive it. +// In addition to the combined public key, the parity accumulator and the tweak +// accumulator are returned as well. func AggregateKeys(keys []*btcec.PublicKey, sort bool, - keyOpts ...KeyAggOption) *btcec.PublicKey { + keyOpts ...KeyAggOption) ( + *btcec.PublicKey, *btcec.ModNScalar, *btcec.ModNScalar, error) { // First, parse the set of optional signing options. opts := defaultKeyAggOptions() @@ -229,6 +327,27 @@ func AggregateKeys(keys []*btcec.PublicKey, sort bool, btcec.AddNonConst(&finalKeyJ, &tweakedKeyJ, &finalKeyJ) } + var ( + err error + tweakAcc btcec.ModNScalar + parityAcc btcec.ModNScalar + ) + parityAcc.SetInt(1) + + // In this case we have a set of tweaks, so we'll incrementally apply + // each one, until we have our final tweaked key, and the related + // accumulators. + for i := 1; i <= len(opts.tweaks); i++ { + finalKeyJ, parityAcc, tweakAcc, err = tweakKey( + finalKeyJ, parityAcc, opts.tweaks[i-1].Tweak, tweakAcc, + opts.tweaks[i-1].IsXOnly, + ) + if err != nil { + return nil, nil, nil, err + } + } + finalKeyJ.ToAffine() - return btcec.NewPublicKey(&finalKeyJ.X, &finalKeyJ.Y) + key := btcec.NewPublicKey(&finalKeyJ.X, &finalKeyJ.Y) + return key, &parityAcc, &tweakAcc, nil } diff --git a/btcec/schnorr/musig2/musig2_test.go b/btcec/schnorr/musig2/musig2_test.go index 0c8af2fca0..d64ca9e9b8 100644 --- a/btcec/schnorr/musig2/musig2_test.go +++ b/btcec/schnorr/musig2/musig2_test.go @@ -84,7 +84,7 @@ func TestMuSig2KeyAggTestVectors(t *testing.T) { } uniqueKeyIndex := secondUniqueKeyIndex(keys, false) - combinedKey := AggregateKeys( + combinedKey, _, _, _ := AggregateKeys( keys, false, WithUniqueKeyIndex(uniqueKeyIndex), ) combinedKeyBytes := schnorr.SerializePubKey(combinedKey) @@ -164,6 +164,7 @@ func TestMuSig2SigningTestVectors(t *testing.T) { testCases := []struct { keyOrder []int expectedPartialSig []byte + tweak *KeyTweakDesc }{ { keyOrder: []int{0, 1, 2}, @@ -173,27 +174,63 @@ func TestMuSig2SigningTestVectors(t *testing.T) { keyOrder: []int{1, 0, 2}, expectedPartialSig: mustParseHex("2DF67BFFF18E3DE797E13C6475C963048138DAEC5CB20A357CECA7C8424295EA"), }, - { keyOrder: []int{1, 2, 0}, expectedPartialSig: mustParseHex("0D5B651E6DE34A29A12DE7A8B4183B4AE6A7F7FBE15CDCAFA4A3D1BCAABC7517"), }, + { + keyOrder: []int{1, 2, 0}, + expectedPartialSig: mustParseHex("5e24c7496b565debc3b9639e6f1304a21597f9603d3ab05b4913641775e1375b"), + tweak: &KeyTweakDesc{ + Tweak: [32]byte{ + 0xE8, 0xF7, 0x91, 0xFF, 0x92, 0x25, 0xA2, 0xAF, + 0x01, 0x02, 0xAF, 0xFF, 0x4A, 0x9A, 0x72, 0x3D, + 0x96, 0x12, 0xA6, 0x82, 0xA2, 0x5E, 0xBE, 0x79, + 0x80, 0x2B, 0x26, 0x3C, 0xDF, 0xCD, 0x83, 0xBB, + }, + IsXOnly: true, + }, + }, + { + keyOrder: []int{1, 2, 0}, + expectedPartialSig: mustParseHex("78408ddcab4813d1394c97d493ef1084195c1d4b52e63ecd7bc5991644e44ddd"), + tweak: &KeyTweakDesc{ + Tweak: [32]byte{ + 0xE8, 0xF7, 0x91, 0xFF, 0x92, 0x25, 0xA2, 0xAF, + 0x01, 0x02, 0xAF, 0xFF, 0x4A, 0x9A, 0x72, 0x3D, + 0x96, 0x12, 0xA6, 0x82, 0xA2, 0x5E, 0xBE, 0x79, + 0x80, 0x2B, 0x26, 0x3C, 0xDF, 0xCD, 0x83, 0xBB, + }, + IsXOnly: false, + }, + }, } var msg [32]byte copy(msg[:], signTestMsg) for _, testCase := range testCases { - testName := fmt.Sprintf("%v", testCase.keyOrder) + testName := fmt.Sprintf("%v/tweak=%v", testCase.keyOrder, testCase.tweak != nil) + if testCase.tweak != nil { + testName += fmt.Sprintf("/x_only=%v", testCase.tweak.IsXOnly) + } + t.Run(testName, func(t *testing.T) { keySet := make([]*btcec.PublicKey, 0, len(testCase.keyOrder)) for _, keyIndex := range testCase.keyOrder { keySet = append(keySet, signSetKeys[keyIndex]) } + var opts []SignOption + if testCase.tweak != nil { + opts = append( + opts, WithTweaks(*testCase.tweak), + ) + } + partialSig, err := Sign( secNonce, signSetPrivKey, aggregatedNonce, - keySet, msg, + keySet, msg, opts..., ) if err != nil { t.Fatalf("unable to generate partial sig: %v", err) @@ -251,17 +288,14 @@ func (s signerSet) pubNonces() [][PubNonceSize]byte { func (s signerSet) combinedKey() *btcec.PublicKey { uniqueKeyIndex := secondUniqueKeyIndex(s.keys(), false) - return AggregateKeys( + key, _, _, _ := AggregateKeys( s.keys(), false, WithUniqueKeyIndex(uniqueKeyIndex), ) + return key } -// TestMuSigMultiParty tests that for a given set of 100 signers, we're able to -// properly generate valid sub signatures, which ultimately can be combined -// into a single valid signature. -func TestMuSigMultiParty(t *testing.T) { - t.Parallel() - +// testMultiPartySign executes a multi-party signing context w/ 100 signers. +func testMultiPartySign(t *testing.T, tweaks ...KeyTweakDesc) { const numSigners = 100 // First generate the set of signers along with their public keys. @@ -286,12 +320,19 @@ func TestMuSigMultiParty(t *testing.T) { var combinedKey *btcec.PublicKey + var ctxOpts []ContextOption + if len(tweaks) != 0 { + ctxOpts = append(ctxOpts, WithTweakedContext(tweaks)) + } + // Now that we have all the signers, we'll make a new context, then // generate a new session for each of them(which handles nonce // generation). signers := make([]*Session, numSigners) for i, signerKey := range signerKeys { - signCtx, err := NewContext(signerKey, signSet, false) + signCtx, err := NewContext( + signerKey, signSet, false, ctxOpts..., + ) if err != nil { t.Fatalf("unable to generate context: %v", err) } @@ -382,3 +423,40 @@ func TestMuSigMultiParty(t *testing.T) { } } } + +// TestMuSigMultiParty tests that for a given set of 100 signers, we're able to +// properly generate valid sub signatures, which ultimately can be combined +// into a single valid signature. +func TestMuSigMultiParty(t *testing.T) { + t.Parallel() + + testTweak := [32]byte{ + 0xE8, 0xF7, 0x91, 0xFF, 0x92, 0x25, 0xA2, 0xAF, + 0x01, 0x02, 0xAF, 0xFF, 0x4A, 0x9A, 0x72, 0x3D, + 0x96, 0x12, 0xA6, 0x82, 0xA2, 0x5E, 0xBE, 0x79, + 0x80, 0x2B, 0x26, 0x3C, 0xDF, 0xCD, 0x83, 0xBB, + } + + t.Run("no_tweak", func(t *testing.T) { + t.Parallel() + + testMultiPartySign(t) + }) + + t.Run("tweaked", func(t *testing.T) { + t.Parallel() + + testMultiPartySign(t, KeyTweakDesc{ + Tweak: testTweak, + }) + }) + + t.Run("tweaked_x_only", func(t *testing.T) { + t.Parallel() + + testMultiPartySign(t, KeyTweakDesc{ + Tweak: testTweak, + IsXOnly: true, + }) + }) +} diff --git a/btcec/schnorr/musig2/sign.go b/btcec/schnorr/musig2/sign.go index df3caf23ed..f7b7b13d55 100644 --- a/btcec/schnorr/musig2/sign.go +++ b/btcec/schnorr/musig2/sign.go @@ -104,6 +104,11 @@ type signOptions struct { // sortKeys determines if the set of keys should be sorted before doing // key aggregation. sortKeys bool + + // tweaks specifies a series of tweaks to be applied to the aggregated + // public key, which also partially carries over into the signing + // process. + tweaks []KeyTweakDesc } // defaultSignOptions returns the default set of signing operations. @@ -128,6 +133,14 @@ func WithSortedKeys() SignOption { } } +// WithTweaks determines if the aggregated public key used should apply a +// series of tweaks before key aggregation. +func WithTweaks(tweaks ...KeyTweakDesc) SignOption { + return func(o *signOptions) { + o.tweaks = tweaks + } +} + // 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 @@ -163,10 +176,14 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, // Next we'll construct the aggregated public key based on the set of // signers. uniqueKeyIndex := secondUniqueKeyIndex(pubKeys, opts.sortKeys) - combinedKey := AggregateKeys( + combinedKey, parityAcc, _, err := AggregateKeys( pubKeys, opts.sortKeys, WithKeysHash(keysHash), WithUniqueKeyIndex(uniqueKeyIndex), + WithKeyTweaks(opts.tweaks...), ) + if err != nil { + return nil, err + } // Next we'll compute the value b, that blinds our second public // nonce: @@ -224,8 +241,6 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, return nil, ErrPrivKeyZero } - // If the y coordinate of the public key is odd xor the y coordinate of - // the combined public key is odd, then we'll negate the private key. pubKey := privKey.PubKey() pubKeyYIsOdd := func() bool { pubKeyBytes := pubKey.SerializeCompressed() @@ -235,13 +250,27 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, combinedKeyBytes := combinedKey.SerializeCompressed() return combinedKeyBytes[0] == secp.PubKeyFormatCompressedOdd }() - if pubKeyYIsOdd != combinedKeyYIsOdd { - privKeyScalar.Negate() + + // Next we'll compute our two parity factors for Q the combined public + // key, and P, the public key we're signing with. If the keys are odd, + // then we'll negate them. + parityCombinedKey := new(btcec.ModNScalar).SetInt(1) + paritySignKey := new(btcec.ModNScalar).SetInt(1) + if combinedKeyYIsOdd { + parityCombinedKey.Negate() } + if pubKeyYIsOdd { + paritySignKey.Negate() + } + + // Before we sign below, we'll multiply by our various parity factors + // to ensure that the signing key is properly negated (if necessary): + // * d = gvâ‹…gaccvâ‹…gpâ‹…d' + privKeyScalar.Mul(parityCombinedKey).Mul(paritySignKey).Mul(parityAcc) // Next we'll create the challenge hash that commits to the combined - // nonce, combined public key and also the message: * e = - // H(tag=ChallengeHashTag, R || Q || m) mod n + // 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(combinedKey)) @@ -252,14 +281,14 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, var e btcec.ModNScalar e.SetByteSlice(challengeBytes[:]) - // Next, we'll compute mu, our aggregation coefficient for the key that + // Next, we'll compute a, our aggregation coefficient for the key that // we're signing with. - mu := aggregationCoefficient(pubKeys, pubKey, keysHash, uniqueKeyIndex) + a := aggregationCoefficient(pubKeys, pubKey, keysHash, uniqueKeyIndex) // With mu constructed, we can finally generate our partial signature - // as: s = (k1_1 + b*k_2 + e*mu*d) mod n. + // as: s = (k1_1 + b*k_2 + e*a*d) mod n. s := new(btcec.ModNScalar) - s.Add(&k1).Add(k2.Mul(&nonceBlinder)).Add(e.Mul(mu).Mul(&privKeyScalar)) + s.Add(&k1).Add(k2.Mul(&nonceBlinder)).Add(e.Mul(a).Mul(&privKeyScalar)) sig := NewPartialSignature(s, nonceKey) @@ -332,10 +361,14 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte, // Next we'll construct the aggregated public key based on the set of // signers. - combinedKey := AggregateKeys( + combinedKey, parityAcc, _, err := AggregateKeys( keySet, opts.sortKeys, WithKeysHash(keysHash), WithUniqueKeyIndex(uniqueKeyIndex), + WithKeyTweaks(opts.tweaks...), ) + if err != nil { + return err + } // Next we'll compute the value b, that blinds our second public // nonce: @@ -413,25 +446,33 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte, return err } - // Next, we'll compute mu, our aggregation coefficient for the key that + // Next, we'll compute a, our aggregation coefficient for the key that // we're signing with. - mu := aggregationCoefficient(keySet, signingKey, keysHash, uniqueKeyIndex) + a := aggregationCoefficient(keySet, signingKey, keysHash, uniqueKeyIndex) - // If the combined key has an odd y coordinate, then we'll negate the - // signer key. - var signKeyJ btcec.JacobianPoint - signingKey.AsJacobian(&signKeyJ) + // If the combined key has an odd y coordinate, then we'll negate + // parity factor for the signing key. + paritySignKey := new(btcec.ModNScalar).SetInt(1) combinedKeyBytes := combinedKey.SerializeCompressed() if combinedKeyBytes[0] == secp.PubKeyFormatCompressedOdd { - signKeyJ.ToAffine() - signKeyJ.Y.Negate(1) - signKeyJ.Y.Normalize() + paritySignKey.Negate() } - // In the final set, we'll check that: s*G == R' + e*mu*P. + // Next, we'll construct the final parity factor by multiplying the + // sign key parity factor with the accumulated parity factor for all + // the keys. + finalParityFactor := paritySignKey.Mul(parityAcc) + + // Now we'll multiply the parity factor by our signing key, which'll + // take care of the amount of negation needed. + var signKeyJ btcec.JacobianPoint + signingKey.AsJacobian(&signKeyJ) + btcec.ScalarMultNonConst(finalParityFactor, &signKeyJ, &signKeyJ) + + // In the final set, we'll check that: s*G == R' + e*a*P. var sG, rP btcec.JacobianPoint btcec.ScalarBaseMultNonConst(s, &sG) - btcec.ScalarMultNonConst(e.Mul(mu), &signKeyJ, &rP) + btcec.ScalarMultNonConst(e.Mul(a), &signKeyJ, &rP) btcec.AddNonConst(&rP, &pubNonceJ, &rP) sG.ToAffine() @@ -444,16 +485,98 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte, return nil } +// CombineOption is a functional option argument that allows callers to modify the +// way we combine musig2 schnorr signatures. +type CombineOption func(*combineOptions) + +// combineOptions houses the set of functional options that can be used to +// modify the method used to combine the musig2 partial signatures. +type combineOptions struct { + msg [32]byte + + combinedKey *btcec.PublicKey + + tweakAcc *btcec.ModNScalar +} + +// defaultCombineOptions returns the default set of signing operations. +func defaultCombineOptions() *combineOptions { + return &combineOptions{} +} + +// WithTweakedCombine is a functional option that allows callers to specify +// that the signature was produced using a tweaked aggregated public key. In +// order to properly aggregate the partial signatures, the caller must specify +// enough information to reconstruct the challenge, and also the final +// accumulated tweak value. +func WithTweakedCombine(msg [32]byte, keys []*btcec.PublicKey, + tweaks []KeyTweakDesc, sort bool) CombineOption { + + return func(o *combineOptions) { + combinedKey, _, tweakAcc, _ := AggregateKeys( + keys, sort, WithKeyTweaks(tweaks...), + ) + + o.msg = msg + o.combinedKey = combinedKey + 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, - partialSigs []*PartialSignature) *schnorr.Signature { + partialSigs []*PartialSignature, + combineOpts ...CombineOption) *schnorr.Signature { + // First, parse the set of optional combine options. + opts := defaultCombineOptions() + for _, option := range combineOpts { + option(opts) + } + + // If signer keys and tweaks are specified, then we need to carry out + // some intermediate steps before we can combine the signature. + var tweakProduct *btcec.ModNScalar + if opts.combinedKey != nil && opts.tweakAcc != nil { + // Next, we'll construct the parity factor of the combined key, + // negating it if the combined key has an even y coordinate. + parityFactor := new(btcec.ModNScalar).SetInt(1) + combinedKeyBytes := opts.combinedKey.SerializeCompressed() + if combinedKeyBytes[0] == secp.PubKeyFormatCompressedOdd { + parityFactor.Negate() + } + + // Next we'll reconstruct e the challenge has based on the + // nonce and combined public key. + // * e = H(tag=ChallengeHashTag, R || Q || m) mod n + var challengeMsg bytes.Buffer + challengeMsg.Write(schnorr.SerializePubKey(combinedNonce)) + challengeMsg.Write(schnorr.SerializePubKey(opts.combinedKey)) + challengeMsg.Write(opts.msg[:]) + challengeBytes := chainhash.TaggedHash( + ChallengeHashTag, challengeMsg.Bytes(), + ) + var e btcec.ModNScalar + e.SetByteSlice(challengeBytes[:]) + + tweakProduct = new(btcec.ModNScalar).Set(&e) + tweakProduct.Mul(opts.tweakAcc).Mul(parityFactor) + } + + // Finally, the tweak factor also needs to be re-computed as well. var combinedSig btcec.ModNScalar for _, partialSig := range partialSigs { combinedSig.Add(partialSig.S) } + // If the tweak product was set above, then we'll need to add the value + // at the very end in order to produce a valid signature under the + // final tweaked key. + if tweakProduct != nil { + combinedSig.Add(tweakProduct) + } + // TODO(roasbeef): less verbose way to get the x coord... var nonceJ btcec.JacobianPoint combinedNonce.AsJacobian(&nonceJ) From f7168c86633d4737a0e0c3af51d83a6e559199f7 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 31 Mar 2022 19:27:34 -0500 Subject: [PATCH 10/16] schnorr/musig2: add native support for taproot output key tweaking In this commit, we add a series of new options and methods to make it easier to use the package in the context of a taproot output that commits to a script root or some other value. Before this series of changes, the API was hard to use in this context as the taproot tweak actually includes the internal public key, which in this case is the aggregated public key. So you actually needed to call that API w/o the tweak, get that, then recompute the tweak itself. To make things easier in the taproot context, we've added a series of new options that'll return the aggregated key before any tweaks (to be used as the internal key), and also handle computing the BIP 341 tweak value for the caller. --- btcec/go.mod | 2 +- btcec/go.sum | 4 +- btcec/schnorr/musig2/bench_test.go | 2 +- btcec/schnorr/musig2/context.go | 93 +++++++++++++++++++++++------ btcec/schnorr/musig2/keys.go | 71 ++++++++++++++++++++-- btcec/schnorr/musig2/musig2_test.go | 30 ++++++---- btcec/schnorr/musig2/sign.go | 90 +++++++++++++++++++++++----- 7 files changed, 242 insertions(+), 50 deletions(-) diff --git a/btcec/go.mod b/btcec/go.mod index bc8cf721e3..8c7cc1d63c 100644 --- a/btcec/go.mod +++ b/btcec/go.mod @@ -3,7 +3,7 @@ module github.com/btcsuite/btcd/btcec/v2 go 1.17 require ( - github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0 + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 ) diff --git a/btcec/go.sum b/btcec/go.sum index 00af17624d..0004a783bb 100644 --- a/btcec/go.sum +++ b/btcec/go.sum @@ -1,5 +1,5 @@ -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0 h1:MSskdM4/xJYcFzy0altH/C/xHopifpWzHUi1JeVI34Q= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= diff --git a/btcec/schnorr/musig2/bench_test.go b/btcec/schnorr/musig2/bench_test.go index eaa6b21b3c..70d7e931ce 100644 --- a/btcec/schnorr/musig2/bench_test.go +++ b/btcec/schnorr/musig2/bench_test.go @@ -306,7 +306,7 @@ func BenchmarkAggregateKeys(b *testing.B) { WithUniqueKeyIndex(uniqueKeyIndex), ) - testKey = aggKey + testKey = aggKey.FinalKey }) } } diff --git a/btcec/schnorr/musig2/context.go b/btcec/schnorr/musig2/context.go index 3a3b29280c..bfb9d874e9 100644 --- a/btcec/schnorr/musig2/context.go +++ b/btcec/schnorr/musig2/context.go @@ -35,6 +35,10 @@ var ( // sign a partial signature, without first having collected all the // required combined nonces. ErrCombinedNonceUnavailable = fmt.Errorf("missing combined nonce") + + // ErrTaprootInternalKeyUnavailable is returned when a user attempts to + // obtain the + ErrTaprootInternalKeyUnavailable = fmt.Errorf("taproot tweak not used") ) // Context is a managed signing context for musig2. It takes care of things @@ -50,7 +54,7 @@ type Context struct { keySet []*btcec.PublicKey // combinedKey is the aggregated public key. - combinedKey *btcec.PublicKey + combinedKey *AggregateKey // uniqueKeyIndex is the index of the second unique key in the keySet. // This is used to speed up signing and verification computations. @@ -59,9 +63,8 @@ type Context struct { // keysHash is the hash of all the keys as defined in musig2. keysHash []byte - // tweaks is a set of optional tweak values that affect the final - // combined public key. - tweaks []KeyTweakDesc + // opts is the set of options for the context. + opts *contextOptions // shouldSort keeps track of if the public keys should be sorted before // any operations. @@ -75,7 +78,18 @@ 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 + // key. tweaks []KeyTweakDesc + + // taprootTweak specifies the taproot tweak. If specified, then we'll + // use this as the script root for the BIP 341 taproot (x-only) tweak. + // Normally we'd just apply the raw 32 byte tweak, but for taproot, we + // first need to compute the aggregated key before tweaking, and then + // use it as the internal key. This is required as the taproot tweak + // also commits to the public key, which in this case is the aggregated + // key before the tweak. + taprootTweak []byte } // defaultContextOptions returns the default context options. @@ -85,12 +99,22 @@ func defaultContextOptions() *contextOptions { // WithTweakedContext specifies that within the context, the aggregated public // key should be tweaked with the specified tweaks. -func WithTweakedContext(tweaks []KeyTweakDesc) ContextOption { +func WithTweakedContext(tweaks ...KeyTweakDesc) ContextOption { return func(o *contextOptions) { o.tweaks = tweaks } } +// WithTaprootTweakCtx specifies that within this context, the final key should +// use the taproot tweak as defined in BIP 341: outputKey = internalKey + +// h_tapTweak(internalKey || scriptRoot). In this case, the aggreaged key +// before the tweak will be used as the internal key. +func WithTaprootTweakCtx(scriptRoot []byte) ContextOption { + return func(o *contextOptions) { + o.taprootTweak = scriptRoot + } +} + // NewContext creates a new signing context with the passed singing key and set // of public keys for each of the other signers. // @@ -133,12 +157,21 @@ func NewContext(signingKey *btcec.PrivateKey, keysHash := keyHashFingerprint(signers, shouldSort) uniqueKeyIndex := secondUniqueKeyIndex(signers, shouldSort) + keyAggOpts := []KeyAggOption{ + WithKeysHash(keysHash), WithUniqueKeyIndex(uniqueKeyIndex), + } + if opts.taprootTweak != nil { + keyAggOpts = append( + keyAggOpts, WithTaprootKeyTweak(opts.taprootTweak), + ) + } else if len(opts.tweaks) != 0 { + keyAggOpts = append(keyAggOpts, WithKeyTweaks(opts.tweaks...)) + } + // Next, we'll use this information to compute the aggregated public // key that'll be used for signing in practice. combinedKey, _, _, err := AggregateKeys( - signers, shouldSort, WithKeysHash(keysHash), - WithUniqueKeyIndex(uniqueKeyIndex), - WithKeyTweaks(opts.tweaks...), + signers, shouldSort, keyAggOpts..., ) if err != nil { return nil, err @@ -151,15 +184,15 @@ func NewContext(signingKey *btcec.PrivateKey, combinedKey: combinedKey, uniqueKeyIndex: uniqueKeyIndex, keysHash: keysHash, - tweaks: opts.tweaks, + opts: opts, shouldSort: shouldSort, }, nil } // CombinedKey returns the combined public key that will be used to generate // multi-signatures against. -func (c *Context) CombinedKey() btcec.PublicKey { - return *c.combinedKey +func (c *Context) CombinedKey() *btcec.PublicKey { + return c.combinedKey.FinalKey } // PubKey returns the public key of the signer of this session. @@ -175,6 +208,19 @@ func (c *Context) SigningKeys() []*btcec.PublicKey { return keys } +// TaprootInternalKey returns the internal taproot key, which is the aggregated +// key _before_ the tweak is applied. If a taproot tweak was specified, then +// CombinedKey() will return the fully tweaked output key, with this method +// 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 { + return nil, ErrTaprootInternalKeyUnavailable + } + + return c.combinedKey.PreTweakedKey, nil +} + // Session represents a musig2 signing session. A new instance should be // created each time a multi-signature is needed. The session struct handles // nonces management, incremental partial sig vitrifaction, as well as final @@ -241,7 +287,7 @@ func (s *Session) RegisterPubNonce(nonce [PubNonceSize]byte) (bool, error) { // times. haveAllNonces := len(s.pubNonces) == len(s.ctx.keySet) if haveAllNonces { - return false, nil + return false, ErrAlredyHaveAllNonces } // Add this nonce and check again if we already have tall the nonces we @@ -283,8 +329,12 @@ func (s *Session) Sign(msg [32]byte, return nil, ErrCombinedNonceUnavailable } - if len(s.ctx.tweaks) != 0 { - signOpts = append(signOpts, WithTweaks(s.ctx.tweaks...)) + if s.ctx.opts.taprootTweak != nil { + signOpts = append( + signOpts, WithTaprootSignTweak(s.ctx.opts.taprootTweak), + ) + } else if len(s.ctx.opts.tweaks) != 0 { + signOpts = append(signOpts, WithTweaks(s.ctx.opts.tweaks...)) } partialSig, err := Sign( @@ -306,7 +356,7 @@ func (s *Session) Sign(msg [32]byte, return partialSig, nil } -// CombineSigs buffers a partial signature received from a signing party. The +// CombineSig buffers a partial signature received from a signing party. The // method returns true once all the signatures are available, and can be // combined into the final signature. func (s *Session) CombineSig(sig *PartialSignature) (bool, error) { @@ -329,10 +379,17 @@ func (s *Session) CombineSig(sig *PartialSignature) (bool, error) { // final signature. if haveAllSigs { var combineOpts []CombineOption - if len(s.ctx.tweaks) != 0 { + if 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 { combineOpts = append( combineOpts, WithTweakedCombine( - s.msg, s.ctx.keySet, s.ctx.tweaks, + s.msg, s.ctx.keySet, s.ctx.opts.tweaks, s.ctx.shouldSort, ), ) @@ -344,7 +401,7 @@ func (s *Session) CombineSig(sig *PartialSignature) (bool, error) { // valid. // // TODO(roasbef): allow skipping? - if !finalSig.Verify(s.msg[:], s.ctx.combinedKey) { + if !finalSig.Verify(s.msg[:], s.ctx.combinedKey.FinalKey) { return false, ErrFinalSigInvalid } diff --git a/btcec/schnorr/musig2/keys.go b/btcec/schnorr/musig2/keys.go index 35c37d9309..c13af98463 100644 --- a/btcec/schnorr/musig2/keys.go +++ b/btcec/schnorr/musig2/keys.go @@ -170,8 +170,12 @@ type keyAggOption struct { uniqueKeyIndex *int // tweaks specifies a series of tweaks to be applied to the aggregated - // public key> + // public key. tweaks []KeyTweakDesc + + // taprootTweak controls if the tweaks above should be applied in a BIP + // 340 style. + taprootTweak bool } // WithKeysHash allows key aggregation to be optimize, by allowing the caller @@ -199,6 +203,29 @@ func WithKeyTweaks(tweaks ...KeyTweakDesc) KeyAggOption { } } +// WithTaprootKeyTweak specifies that within this context, the final key should +// use the taproot tweak as defined in BIP 341: outputKey = internalKey + +// h_tapTweak(internalKey || scriptRoot). In this case, the aggregated key +// before the tweak will be used as the internal key. +// +// This option should be used instead of WithKeyTweaks when the aggregated key +// is intended to be used as a taproot output key that commits to a script +// root. +func WithTaprootKeyTweak(scriptRoot []byte) KeyAggOption { + return func(o *keyAggOption) { + var tweak [32]byte + copy(tweak[:], scriptRoot[:]) + + o.tweaks = []KeyTweakDesc{ + { + Tweak: tweak, + IsXOnly: true, + }, + } + o.taprootTweak = true + } +} + // defaultKeyAggOptions returns the set of default arguments for key // aggregation. func defaultKeyAggOptions() *keyAggOption { @@ -269,6 +296,19 @@ func tweakKey(keyJ btcec.JacobianPoint, parityAcc btcec.ModNScalar, tweak [32]by return keyJ, parityAcc, tweakAcc, nil } +// AggregateKey is a final aggregated key along with a possible version of the +// key without any tweaks applied. +type AggregateKey struct { + // FinalKey is the final aggregated key which may include one or more + // tweaks applied to it. + FinalKey *btcec.PublicKey + + // PreTweakedKey is the aggregated *before* any tweaks have been + // applied. This should be used as the internal key in taproot + // contexts. + PreTweakedKey *btcec.PublicKey +} + // AggregateKeys takes a list of possibly unsorted keys and returns a single // aggregated key as specified by the musig2 key aggregation algorithm. A nil // value can be passed for keyHash, which causes this function to re-derive it. @@ -276,7 +316,7 @@ func tweakKey(keyJ btcec.JacobianPoint, parityAcc btcec.ModNScalar, tweak [32]by // accumulator are returned as well. func AggregateKeys(keys []*btcec.PublicKey, sort bool, keyOpts ...KeyAggOption) ( - *btcec.PublicKey, *btcec.ModNScalar, *btcec.ModNScalar, error) { + *AggregateKey, *btcec.ModNScalar, *btcec.ModNScalar, error) { // First, parse the set of optional signing options. opts := defaultKeyAggOptions() @@ -327,6 +367,25 @@ func AggregateKeys(keys []*btcec.PublicKey, sort bool, btcec.AddNonConst(&finalKeyJ, &tweakedKeyJ, &finalKeyJ) } + // 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. + 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 { + // 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[:], + ) + opts.tweaks[0].Tweak = *tapTweakHash + } + var ( err error tweakAcc btcec.ModNScalar @@ -348,6 +407,10 @@ func AggregateKeys(keys []*btcec.PublicKey, sort bool, } finalKeyJ.ToAffine() - key := btcec.NewPublicKey(&finalKeyJ.X, &finalKeyJ.Y) - return key, &parityAcc, &tweakAcc, nil + finalKey := btcec.NewPublicKey(&finalKeyJ.X, &finalKeyJ.Y) + + return &AggregateKey{ + PreTweakedKey: combinedKey, + FinalKey: finalKey, + }, &parityAcc, &tweakAcc, nil } diff --git a/btcec/schnorr/musig2/musig2_test.go b/btcec/schnorr/musig2/musig2_test.go index d64ca9e9b8..bfd7c0818d 100644 --- a/btcec/schnorr/musig2/musig2_test.go +++ b/btcec/schnorr/musig2/musig2_test.go @@ -87,7 +87,7 @@ func TestMuSig2KeyAggTestVectors(t *testing.T) { combinedKey, _, _, _ := AggregateKeys( keys, false, WithUniqueKeyIndex(uniqueKeyIndex), ) - combinedKeyBytes := schnorr.SerializePubKey(combinedKey) + combinedKeyBytes := schnorr.SerializePubKey(combinedKey.FinalKey) if !bytes.Equal(combinedKeyBytes, testCase.expectedKey) { t.Fatalf("case: #%v, invalid aggregation: "+ "expected %x, got %x", i, testCase.expectedKey, @@ -291,11 +291,13 @@ func (s signerSet) combinedKey() *btcec.PublicKey { key, _, _, _ := AggregateKeys( s.keys(), false, WithUniqueKeyIndex(uniqueKeyIndex), ) - return key + return key.FinalKey } // testMultiPartySign executes a multi-party signing context w/ 100 signers. -func testMultiPartySign(t *testing.T, tweaks ...KeyTweakDesc) { +func testMultiPartySign(t *testing.T, taprootTweak []byte, + tweaks ...KeyTweakDesc) { + const numSigners = 100 // First generate the set of signers along with their public keys. @@ -321,8 +323,11 @@ func testMultiPartySign(t *testing.T, tweaks ...KeyTweakDesc) { var combinedKey *btcec.PublicKey var ctxOpts []ContextOption - if len(tweaks) != 0 { - ctxOpts = append(ctxOpts, WithTweakedContext(tweaks)) + switch { + case taprootTweak != nil: + ctxOpts = append(ctxOpts, WithTaprootTweakCtx(taprootTweak)) + case len(tweaks) != 0: + ctxOpts = append(ctxOpts, WithTweakedContext(tweaks...)) } // Now that we have all the signers, we'll make a new context, then @@ -338,8 +343,7 @@ func testMultiPartySign(t *testing.T, tweaks ...KeyTweakDesc) { } if combinedKey == nil { - k := signCtx.CombinedKey() - combinedKey = &k + combinedKey = signCtx.CombinedKey() } session, err := signCtx.NewSession() @@ -440,13 +444,13 @@ func TestMuSigMultiParty(t *testing.T) { t.Run("no_tweak", func(t *testing.T) { t.Parallel() - testMultiPartySign(t) + testMultiPartySign(t, nil) }) t.Run("tweaked", func(t *testing.T) { t.Parallel() - testMultiPartySign(t, KeyTweakDesc{ + testMultiPartySign(t, nil, KeyTweakDesc{ Tweak: testTweak, }) }) @@ -454,9 +458,15 @@ func TestMuSigMultiParty(t *testing.T) { t.Run("tweaked_x_only", func(t *testing.T) { t.Parallel() - testMultiPartySign(t, KeyTweakDesc{ + testMultiPartySign(t, nil, KeyTweakDesc{ Tweak: testTweak, IsXOnly: true, }) }) + + t.Run("taproot_tweaked_x_only", func(t *testing.T) { + t.Parallel() + + testMultiPartySign(t, testTweak[:]) + }) } diff --git a/btcec/schnorr/musig2/sign.go b/btcec/schnorr/musig2/sign.go index f7b7b13d55..16c5cd2fac 100644 --- a/btcec/schnorr/musig2/sign.go +++ b/btcec/schnorr/musig2/sign.go @@ -109,6 +109,14 @@ type signOptions struct { // public key, which also partially carries over into the signing // process. tweaks []KeyTweakDesc + + // taprootTweak specifies a taproot specific tweak. of the tweaks + // specified above. Normally we'd just apply the raw 32 byte tweak, but + // for taproot, we first need to compute the aggregated key before + // tweaking, and then use it as the internal key. This is required as + // the taproot tweak also commits to the public key, which in this case + // is the aggregated key before the tweak. + taprootTweak []byte } // defaultSignOptions returns the default set of signing operations. @@ -141,6 +149,21 @@ func WithTweaks(tweaks ...KeyTweakDesc) SignOption { } } +// WithTaprootSignTweak allows a caller to specify a tweak that should be used +// in a bip 340 manner when signing. This differs from WithTweaks as the tweak +// will be assumed to always be x-only and the intermediate aggregate key +// before tweaking will be used to generate part of the tweak (as the taproot +// tweak also commits 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 is actually +// committing to a script path, or some other data. +func WithTaprootSignTweak(scriptRoot []byte) SignOption { + return func(o *signOptions) { + o.taprootTweak = scriptRoot + } +} + // 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 @@ -172,14 +195,23 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, // Compute the hash of all the keys here as we'll need it do aggregate // the keys and also at the final step of signing. keysHash := keyHashFingerprint(pubKeys, opts.sortKeys) + uniqueKeyIndex := secondUniqueKeyIndex(pubKeys, opts.sortKeys) + + keyAggOpts := []KeyAggOption{ + WithKeysHash(keysHash), WithUniqueKeyIndex(uniqueKeyIndex), + } + if opts.taprootTweak != nil { + keyAggOpts = append( + keyAggOpts, WithTaprootKeyTweak(opts.taprootTweak), + ) + } else { + keyAggOpts = append(keyAggOpts, WithKeyTweaks(opts.tweaks...)) + } // Next we'll construct the aggregated public key based on the set of // signers. - uniqueKeyIndex := secondUniqueKeyIndex(pubKeys, opts.sortKeys) combinedKey, parityAcc, _, err := AggregateKeys( - pubKeys, opts.sortKeys, WithKeysHash(keysHash), - WithUniqueKeyIndex(uniqueKeyIndex), - WithKeyTweaks(opts.tweaks...), + pubKeys, opts.sortKeys, keyAggOpts..., ) if err != nil { return nil, err @@ -193,7 +225,7 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, nonceBlinder btcec.ModNScalar ) nonceMsgBuf.Write(combinedNonce[:]) - nonceMsgBuf.Write(schnorr.SerializePubKey(combinedKey)) + nonceMsgBuf.Write(schnorr.SerializePubKey(combinedKey.FinalKey)) nonceMsgBuf.Write(msg[:]) nonceBlindHash := chainhash.TaggedHash( NonceBlindTag, nonceMsgBuf.Bytes(), @@ -247,7 +279,7 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, return pubKeyBytes[0] == secp.PubKeyFormatCompressedOdd }() combinedKeyYIsOdd := func() bool { - combinedKeyBytes := combinedKey.SerializeCompressed() + combinedKeyBytes := combinedKey.FinalKey.SerializeCompressed() return combinedKeyBytes[0] == secp.PubKeyFormatCompressedOdd }() @@ -273,7 +305,7 @@ func Sign(secNonce [SecNonceSize]byte, privKey *btcec.PrivateKey, // * e = H(tag=ChallengeHashTag, R || Q || m) mod n var challengeMsg bytes.Buffer challengeMsg.Write(schnorr.SerializePubKey(nonceKey)) - challengeMsg.Write(schnorr.SerializePubKey(combinedKey)) + challengeMsg.Write(schnorr.SerializePubKey(combinedKey.FinalKey)) challengeMsg.Write(msg[:]) challengeBytes := chainhash.TaggedHash( ChallengeHashTag, challengeMsg.Bytes(), @@ -359,12 +391,21 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte, keysHash := keyHashFingerprint(keySet, opts.sortKeys) uniqueKeyIndex := secondUniqueKeyIndex(keySet, opts.sortKeys) + keyAggOpts := []KeyAggOption{ + WithKeysHash(keysHash), WithUniqueKeyIndex(uniqueKeyIndex), + } + if opts.taprootTweak != nil { + keyAggOpts = append( + keyAggOpts, WithTaprootKeyTweak(opts.taprootTweak), + ) + } else { + keyAggOpts = append(keyAggOpts, WithKeyTweaks(opts.tweaks...)) + } + // Next we'll construct the aggregated public key based on the set of // signers. combinedKey, parityAcc, _, err := AggregateKeys( - keySet, opts.sortKeys, - WithKeysHash(keysHash), WithUniqueKeyIndex(uniqueKeyIndex), - WithKeyTweaks(opts.tweaks...), + keySet, opts.sortKeys, keyAggOpts..., ) if err != nil { return err @@ -378,7 +419,7 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte, nonceBlinder btcec.ModNScalar ) nonceMsgBuf.Write(combinedNonce[:]) - nonceMsgBuf.Write(schnorr.SerializePubKey(combinedKey)) + nonceMsgBuf.Write(schnorr.SerializePubKey(combinedKey.FinalKey)) nonceMsgBuf.Write(msg[:]) nonceBlindHash := chainhash.TaggedHash(NonceBlindTag, nonceMsgBuf.Bytes()) nonceBlinder.SetByteSlice(nonceBlindHash[:]) @@ -433,7 +474,7 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte, challengeMsg.Write(schnorr.SerializePubKey(btcec.NewPublicKey( &nonce.X, &nonce.Y, ))) - challengeMsg.Write(schnorr.SerializePubKey(combinedKey)) + challengeMsg.Write(schnorr.SerializePubKey(combinedKey.FinalKey)) challengeMsg.Write(msg[:]) challengeBytes := chainhash.TaggedHash( ChallengeHashTag, challengeMsg.Bytes(), @@ -453,7 +494,7 @@ func verifyPartialSig(partialSig *PartialSignature, pubNonce [PubNonceSize]byte, // If the combined key has an odd y coordinate, then we'll negate // parity factor for the signing key. paritySignKey := new(btcec.ModNScalar).SetInt(1) - combinedKeyBytes := combinedKey.SerializeCompressed() + combinedKeyBytes := combinedKey.FinalKey.SerializeCompressed() if combinedKeyBytes[0] == secp.PubKeyFormatCompressedOdd { paritySignKey.Negate() } @@ -518,7 +559,28 @@ func WithTweakedCombine(msg [32]byte, keys []*btcec.PublicKey, ) o.msg = msg - o.combinedKey = combinedKey + o.combinedKey = combinedKey.FinalKey + o.tweakAcc = tweakAcc + } +} + +// 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. +// +// This option should be used over WithTweakedCombine when attempting to +// aggregate signatures for a top-level taproot keysepnd, where the output key +// commits to a script root. +func WithTaprootTweakedCombine(msg [32]byte, keys []*btcec.PublicKey, + scriptRoot []byte, sort bool) CombineOption { + + return func(o *combineOptions) { + combinedKey, _, tweakAcc, _ := AggregateKeys( + keys, sort, WithTaprootKeyTweak(scriptRoot), + ) + + o.msg = msg + o.combinedKey = combinedKey.FinalKey o.tweakAcc = tweakAcc } } From 9d0d52708ac9a0cdc13b08e9c75df9e58905fbb4 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 1 Apr 2022 12:35:25 -0500 Subject: [PATCH 11/16] 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 bfb9d874e9..2461f962ab 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 c13af98463..e61a22f2e5 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 bfd7c0818d..1ff3151300 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 16c5cd2fac..4f9439fe6a 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, From 65e4fc0dea1758ed5439b651aad4622135e23690 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 26 Apr 2022 21:33:10 -0700 Subject: [PATCH 12/16] btcec/schnorr/musig2: update nonce generation to support optional inputs In this commit, we update the nonce generation to support optional parameters defined in the latest BIP draft. These parameters are optional, but if specified my mitigate the effect of weak randomness when generating the nonce. Given the protocol doesn't require signers to prove how they generate their nonces, this update is mainly to ensure strict spec compliance, and is effectively optional. --- btcec/go.mod | 1 + btcec/go.sum | 2 + btcec/schnorr/musig2/nonces.go | 206 ++++++++++++++++++++++++++++++--- 3 files changed, 194 insertions(+), 15 deletions(-) diff --git a/btcec/go.mod b/btcec/go.mod index 8c7cc1d63c..a5e8c3abef 100644 --- a/btcec/go.mod +++ b/btcec/go.mod @@ -6,6 +6,7 @@ require ( github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 + golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5 ) require github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect diff --git a/btcec/go.sum b/btcec/go.sum index 0004a783bb..bd484d5ed4 100644 --- a/btcec/go.sum +++ b/btcec/go.sum @@ -6,3 +6,5 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5 h1:rxKZ2gOnYxjfmakvUUqh9Gyb6KXfrj7JWTxORTYqb0E= +golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= diff --git a/btcec/schnorr/musig2/nonces.go b/btcec/schnorr/musig2/nonces.go index 4fd76450d9..c0ece86fa4 100644 --- a/btcec/schnorr/musig2/nonces.go +++ b/btcec/schnorr/musig2/nonces.go @@ -3,9 +3,14 @@ package musig2 import ( + "bytes" "crypto/rand" + "encoding/binary" + "io" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" ) const ( @@ -19,6 +24,18 @@ const ( SecNonceSize = 64 ) +var ( + // NonceAuxTag is the tag used to optionally mix in the secret key with + // the set of aux randomness. + NonceAuxTag = []byte("MuSig/aux") + + // NonceGenTag is used to generate the value (from a set of required an + // optional field) that will be used as the part of the secret nonce. + NonceGenTag = []byte("Musig/nonce") + + byteOrder = binary.BigEndian +) + // zeroSecNonce is a secret nonce that's all zeroes. This is used to check that // we're not attempting to re-use a nonce, and also protect callers from it. var zeroSecNonce [SecNonceSize]byte @@ -71,21 +88,173 @@ func secNonceToPubNonce(secNonce [SecNonceSize]byte) [PubNonceSize]byte { // generation happens. type NonceGenOption func(*nonceGenOpts) -// nonceGenOpts is the set of options that control how nonce generation happens. +// nonceGenOpts is the set of options that control how nonce generation +// happens. type nonceGenOpts struct { - randReader func(b []byte) (int, error) + // randReader is what we'll use to generate a set of random bytes. If + // unspecified, then the normal crypto/rand rand.Read method will be + // used in place. + randReader io.Reader + + // secretKey is an optional argument that's used to further augment the + // generated nonce by xor'ing it with this secret key. + secretKey []byte + + // combinedKey is an optional argument that if specified, will be + // combined along with the nonce generation. + combinedKey []byte + + // msg is an optional argument that will be mixed into the nonce + // derivation algorithm. + msg []byte + + // auxInput is an optional argument that will be mixed into the nonce + // derivation algorithm. + auxInput []byte +} + +// cryptoRandAdapter is an adapter struct that allows us to pass in the package +// level Read function from crypto/rand into a context that accepts an +// io.Reader. +type cryptoRandAdapter struct { +} + +// Read implements the io.Reader interface for the crypto/rand package. By +// default, we always use the crypto/rand reader, but the caller is able to +// specify their own generation, which can be useful for deterministic tests. +func (c *cryptoRandAdapter) Read(p []byte) (n int, err error) { + return rand.Read(p) } // defaultNonceGenOpts returns the default set of nonce generation options. func defaultNonceGenOpts() *nonceGenOpts { return &nonceGenOpts{ - // By default, we always use the crypto/rand reader, but the - // caller is able to specify their own generation, which can be - // useful for deterministic tests. - randReader: rand.Read, + randReader: &cryptoRandAdapter{}, + } +} + +// WithCustomRand allows a caller to use a custom random number generator in +// place for crypto/rand. This should only really be used to generate +// determinstic tests. +func WithCustomRand(r io.Reader) NonceGenOption { + return func(o *nonceGenOpts) { + o.randReader = r + } +} + +// WithNonceSecretKeyAux allows a caller to optionally specify a secret key +// that should be used to augment the randomness used to generate the nonces. +func WithNonceSecretKeyAux(secKey *btcec.PrivateKey) NonceGenOption { + return func(o *nonceGenOpts) { + o.secretKey = secKey.Serialize() + } +} + +// WithNonceCombinedKeyAux allows a caller to optionally specify the combined +// key used in this signing session to further augment the randomness used to +// generate nonces. +func WithNonceCombinedKeyAux(combinedKey *btcec.PublicKey) NonceGenOption { + return func(o *nonceGenOpts) { + o.combinedKey = schnorr.SerializePubKey(combinedKey) } } +// WithNonceMessageAux allows a caller to optionally specify a message to be +// mixed into the randomness generated to create the nonce. +func WithNonceMessageAux(msg [32]byte) NonceGenOption { + return func(o *nonceGenOpts) { + o.msg = msg[:] + } +} + +// WithNonceAuxInput is a set of auxiliary randomness, similar to BIP 340 that +// can be used to further augment the nonce generation process. +func WithNonceAuxInput(aux []byte) NonceGenOption { + return func(o *nonceGenOpts) { + o.auxInput = aux + } +} + +// lengthWriter is a function closure that allows a caller to control how the +// length prefix of a byte slice is written. +type lengthWriter func(w io.Writer, b []byte) error + +// uint8Writer is an implementation of lengthWriter that writes the length of +// the byte slice using 1 byte. +func uint8Writer(w io.Writer, b []byte) error { + return binary.Write(w, byteOrder, uint8(len(b))) +} + +// uint8Writer is an implementation of lengthWriter that writes the length of +// the byte slice using 4 bytes. +func uint32Writer(w io.Writer, b []byte) error { + return binary.Write(w, byteOrder, uint32(len(b))) +} + +// writeBytesPrefix is used to write out: len(b) || b, to the passed io.Writer. +// The lengthWriter function closure is used to allow the caller to specify the +// precise byte packing of the length. +func writeBytesPrefix(w io.Writer, b []byte, lenWriter lengthWriter) error { + // Write out the length of the byte first, followed by the set of bytes + // itself. + if err := lenWriter(w, b); err != nil { + return err + } + + if _, err := w.Write(b); err != nil { + return err + } + + return nil +} + +// genNonceAuxBytes writes out the full byte string used to derive a secret +// nonce based on some initial randomness as well as the series of optional +// fields. The byte string used for derivation is: +// * tagged_hash("MuSig/nonce", rand || len(aggpk) || aggpk || len(m) +// || m || len(in) || in || i). +// +// where i is the ith secret nonce being generated. +func genNonceAuxBytes(rand []byte, i int, + opts *nonceGenOpts) (*chainhash.Hash, error) { + + var w bytes.Buffer + + // First, write out the randomness generated in the prior step. + if _, err := w.Write(rand); err != nil { + return nil, err + } + + // Next, we'll write out: len(aggpk) || aggpk. + err := writeBytesPrefix(&w, opts.combinedKey, uint8Writer) + if err != nil { + return nil, err + } + + // Next, we'll write out the length prefixed message. + err = writeBytesPrefix(&w, opts.msg, uint8Writer) + if err != nil { + return nil, err + } + + // Finally we'll write out the auxiliary input. + err = writeBytesPrefix(&w, opts.auxInput, uint32Writer) + if err != nil { + return nil, err + } + + // Next we'll write out the interaction/index number which will + // uniquely generate two nonces given the rest of the possibly static + // parameters. + if err := binary.Write(&w, byteOrder, uint8(i)); err != nil { + return nil, err + } + + // With the message buffer complete, we'll now derive the tagged hash + // using our set of params. + return chainhash.TaggedHash(NonceGenTag, w.Bytes()), nil +} + // GenNonces generates the secret nonces, as well as the public nonces which // correspond to an EC point generated using the secret nonce as a private key. func GenNonces(options ...NonceGenOption) (*Nonces, error) { @@ -94,24 +263,31 @@ func GenNonces(options ...NonceGenOption) (*Nonces, error) { opt(opts) } - // Generate two 32-byte random values that'll be the private keys to - // the public nonces. - var k1, k2 [32]byte - if _, err := opts.randReader(k1[:]); err != nil { + // First, we'll start out by generating 32 random bytes drawn from our + // CSPRNG. + var randBytes [32]byte + if _, err := opts.randReader.Read(randBytes[:]); err != nil { return nil, err } - if _, err := opts.randReader(k2[:]); err != nil { + + // Using our randomness and the set of optional params, generate our + // two secret nonces: k1 and k2. + k1, err := genNonceAuxBytes(randBytes[:], 1, opts) + if err != nil { + return nil, err + } + k2, err := genNonceAuxBytes(randBytes[:], 2, opts) + if err != nil { return nil, err } - - var nonces Nonces var k1Mod, k2Mod btcec.ModNScalar - k1Mod.SetBytes(&k1) - k2Mod.SetBytes(&k2) + k1Mod.SetBytes((*[32]byte)(k1)) + k2Mod.SetBytes((*[32]byte)(k2)) // The secret nonces are serialized as the concatenation of the two 32 // byte secret nonce values. + var nonces Nonces k1Mod.PutBytesUnchecked(nonces.SecNonce[:]) k2Mod.PutBytesUnchecked(nonces.SecNonce[btcec.PrivKeyBytesLen:]) From 55c8cab7694a55be8bd30975d2142ecdb5bb1615 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 26 Apr 2022 21:33:34 -0700 Subject: [PATCH 13/16] btcec/schnorr/musig2: add new key tweak combination test vectors --- btcec/schnorr/musig2/musig2_test.go | 106 ++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 23 deletions(-) diff --git a/btcec/schnorr/musig2/musig2_test.go b/btcec/schnorr/musig2/musig2_test.go index 1ff3151300..71378f61ba 100644 --- a/btcec/schnorr/musig2/musig2_test.go +++ b/btcec/schnorr/musig2/musig2_test.go @@ -142,6 +142,23 @@ var ( signSetKeys = []*btcec.PublicKey{signSetPubKey, signSetKey2, signSetKey3} ) +func formatTweakParity(tweaks []KeyTweakDesc) string { + var s string + for _, tweak := range tweaks { + s += fmt.Sprintf("%v/", tweak.IsXOnly) + } + + // Snip off that last '/'. + s = s[:len(s)-1] + + return s +} + +func genTweakParity(tweak KeyTweakDesc, isXOnly bool) KeyTweakDesc { + tweak.IsXOnly = isXOnly + return tweak +} + // TestMuSig2SigningTestVectors tests that the musig2 implementation produces // the same set of signatures. func TestMuSig2SigningTestVectors(t *testing.T) { @@ -161,10 +178,43 @@ func TestMuSig2SigningTestVectors(t *testing.T) { copy(secNonce[:], mustParseHex("508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61")) copy(secNonce[32:], mustParseHex("FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F7")) + tweak1 := KeyTweakDesc{ + Tweak: [32]byte{ + 0xE8, 0xF7, 0x91, 0xFF, 0x92, 0x25, 0xA2, 0xAF, + 0x01, 0x02, 0xAF, 0xFF, 0x4A, 0x9A, 0x72, 0x3D, + 0x96, 0x12, 0xA6, 0x82, 0xA2, 0x5E, 0xBE, 0x79, + 0x80, 0x2B, 0x26, 0x3C, 0xDF, 0xCD, 0x83, 0xBB, + }, + } + tweak2 := KeyTweakDesc{ + Tweak: [32]byte{ + 0xae, 0x2e, 0xa7, 0x97, 0xcc, 0xf, 0xe7, 0x2a, + 0xc5, 0xb9, 0x7b, 0x97, 0xf3, 0xc6, 0x95, 0x7d, + 0x7e, 0x41, 0x99, 0xa1, 0x67, 0xa5, 0x8e, 0xb0, + 0x8b, 0xca, 0xff, 0xda, 0x70, 0xac, 0x4, 0x55, + }, + } + tweak3 := KeyTweakDesc{ + Tweak: [32]byte{ + 0xf5, 0x2e, 0xcb, 0xc5, 0x65, 0xb3, 0xd8, 0xbe, + 0xa2, 0xdf, 0xd5, 0xb7, 0x5a, 0x4f, 0x45, 0x7e, + 0x54, 0x36, 0x98, 0x9, 0x32, 0x2e, 0x41, 0x20, + 0x83, 0x16, 0x26, 0xf2, 0x90, 0xfa, 0x87, 0xe0, + }, + } + tweak4 := KeyTweakDesc{ + Tweak: [32]byte{ + 0x19, 0x69, 0xad, 0x73, 0xcc, 0x17, 0x7f, 0xa0, + 0xb4, 0xfc, 0xed, 0x6d, 0xf1, 0xf7, 0xbf, 0x99, + 0x7, 0xe6, 0x65, 0xfd, 0xe9, 0xba, 0x19, 0x6a, + 0x74, 0xfe, 0xd0, 0xa3, 0xcf, 0x5a, 0xef, 0x9d, + }, + } + testCases := []struct { keyOrder []int expectedPartialSig []byte - tweak *KeyTweakDesc + tweaks []KeyTweakDesc }{ { keyOrder: []int{0, 1, 2}, @@ -178,30 +228,40 @@ func TestMuSig2SigningTestVectors(t *testing.T) { keyOrder: []int{1, 2, 0}, expectedPartialSig: mustParseHex("0D5B651E6DE34A29A12DE7A8B4183B4AE6A7F7FBE15CDCAFA4A3D1BCAABC7517"), }, + + // A single x-only tweak. { keyOrder: []int{1, 2, 0}, expectedPartialSig: mustParseHex("5e24c7496b565debc3b9639e6f1304a21597f9603d3ab05b4913641775e1375b"), - tweak: &KeyTweakDesc{ - Tweak: [32]byte{ - 0xE8, 0xF7, 0x91, 0xFF, 0x92, 0x25, 0xA2, 0xAF, - 0x01, 0x02, 0xAF, 0xFF, 0x4A, 0x9A, 0x72, 0x3D, - 0x96, 0x12, 0xA6, 0x82, 0xA2, 0x5E, 0xBE, 0x79, - 0x80, 0x2B, 0x26, 0x3C, 0xDF, 0xCD, 0x83, 0xBB, - }, - IsXOnly: true, - }, + tweaks: []KeyTweakDesc{genTweakParity(tweak1, true)}, }, + + // A single ordinary tweak. { keyOrder: []int{1, 2, 0}, expectedPartialSig: mustParseHex("78408ddcab4813d1394c97d493ef1084195c1d4b52e63ecd7bc5991644e44ddd"), - tweak: &KeyTweakDesc{ - Tweak: [32]byte{ - 0xE8, 0xF7, 0x91, 0xFF, 0x92, 0x25, 0xA2, 0xAF, - 0x01, 0x02, 0xAF, 0xFF, 0x4A, 0x9A, 0x72, 0x3D, - 0x96, 0x12, 0xA6, 0x82, 0xA2, 0x5E, 0xBE, 0x79, - 0x80, 0x2B, 0x26, 0x3C, 0xDF, 0xCD, 0x83, 0xBB, - }, - IsXOnly: false, + tweaks: []KeyTweakDesc{genTweakParity(tweak1, false)}, + }, + + // An ordinary tweak then an x-only tweak. + { + keyOrder: []int{1, 2, 0}, + expectedPartialSig: mustParseHex("C3A829A81480E36EC3AB052964509A94EBF34210403D16B226A6F16EC85B7357"), + tweaks: []KeyTweakDesc{ + genTweakParity(tweak1, false), + genTweakParity(tweak2, true), + }, + }, + + // Four tweaks, in the order: x-only, ordinary, x-only, ordinary. + { + keyOrder: []int{1, 2, 0}, + expectedPartialSig: mustParseHex("8C4473C6A382BD3C4AD7BE59818DA5ED7CF8CEC4BC21996CFDA08BB4316B8BC7"), + tweaks: []KeyTweakDesc{ + genTweakParity(tweak1, true), + genTweakParity(tweak2, false), + genTweakParity(tweak3, true), + genTweakParity(tweak4, false), }, }, } @@ -210,9 +270,9 @@ func TestMuSig2SigningTestVectors(t *testing.T) { copy(msg[:], signTestMsg) for _, testCase := range testCases { - testName := fmt.Sprintf("%v/tweak=%v", testCase.keyOrder, testCase.tweak != nil) - if testCase.tweak != nil { - testName += fmt.Sprintf("/x_only=%v", testCase.tweak.IsXOnly) + testName := fmt.Sprintf("%v/tweak=%v", testCase.keyOrder, len(testCase.tweaks) != 0) + if len(testCase.tweaks) != 0 { + testName += fmt.Sprintf("/x_only=%v", formatTweakParity(testCase.tweaks)) } t.Run(testName, func(t *testing.T) { @@ -222,9 +282,9 @@ func TestMuSig2SigningTestVectors(t *testing.T) { } var opts []SignOption - if testCase.tweak != nil { + if len(testCase.tweaks) != 0 { opts = append( - opts, WithTweaks(*testCase.tweak), + opts, WithTweaks(testCase.tweaks...), ) } From 953e2dd94aa7590d793cbf9c074c0ee6ebbdf6cc Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 27 Apr 2022 16:47:11 -0700 Subject: [PATCH 14/16] btcec/schnorr/musig2: enable early nonce generation w/ a context In this commit, we enable early nonce generation, allowing callers to obtain generated nonces before the total set of signers is actually known. This type of nonce generation is useful for contexts like LN funding when we want to minimize the round trips and send nonces before we know the pubkey of the other party. --- btcec/schnorr/musig2/context.go | 346 ++++++++++++++++++++++------ btcec/schnorr/musig2/musig2_test.go | 187 ++++++++++++++- btcec/schnorr/musig2/nonces.go | 2 +- 3 files changed, 467 insertions(+), 68 deletions(-) diff --git a/btcec/schnorr/musig2/context.go b/btcec/schnorr/musig2/context.go index 2461f962ab..298fe96179 100644 --- a/btcec/schnorr/musig2/context.go +++ b/btcec/schnorr/musig2/context.go @@ -10,6 +10,12 @@ import ( ) var ( + // ErrSignersNotSpecified is returned when a caller attempts to create + // a context without specifying either the total number of signers, or + // the complete set of singers. + ErrSignersNotSpecified = fmt.Errorf("total number of signers or all " + + "signers must be known") + // ErrSignerNotInKeySet is returned when a the private key for a signer // isn't included in the set of signing public keys. ErrSignerNotInKeySet = fmt.Errorf("signing key is not found in key" + @@ -19,6 +25,16 @@ var ( // many times for a given signing session. ErrAlredyHaveAllNonces = fmt.Errorf("already have all nonces") + // ErrNotEnoughSigners is returned when a caller attempts to create a + // session from a context, but before all the required signers are + // known. + ErrNotEnoughSigners = fmt.Errorf("not enough signers") + + // ErrAlredyHaveAllNonces is returned when a caller attempts to + // register a signer, once we already have the total set of known + // signers. + ErrAlreadyHaveAllSigners = fmt.Errorf("all signers registered") + // ErrAlredyHaveAllSigs is called when CombineSig is called too many // times for a given signing session. ErrAlredyHaveAllSigs = fmt.Errorf("already have all sigs") @@ -39,6 +55,10 @@ var ( // ErrTaprootInternalKeyUnavailable is returned when a user attempts to // obtain the ErrTaprootInternalKeyUnavailable = fmt.Errorf("taproot tweak not used") + + // ErrNotEnoughSigners is returned if a caller attempts to obtain an + // early nonce when it wasn't specified + ErrNoEarlyNonce = fmt.Errorf("no early nonce available") ) // Context is a managed signing context for musig2. It takes care of things @@ -50,9 +70,6 @@ type Context struct { // pubKey is our even-y coordinate public key. pubKey *btcec.PublicKey - // keySet is the set of all signers. - keySet []*btcec.PublicKey - // combinedKey is the aggregated public key. combinedKey *AggregateKey @@ -69,6 +86,10 @@ type Context struct { // shouldSort keeps track of if the public keys should be sorted before // any operations. shouldSort bool + + // sessionNonce will be populated if the earlyNonce option is true. + // After the first session is created, this nonce will be blanked out. + sessionNonce *Nonces } // ContextOption is a functional option argument that allows callers to modify @@ -94,6 +115,17 @@ type contextOptions struct { // bip86Tweak if true, then the weak will just be // h_tapTweak(internalKey) as there is no true script root. bip86Tweak bool + + // keySet is the complete set of signers for this context. + keySet []*btcec.PublicKey + + // numSigners is the total number of signers that will eventually be a + // part of the context. + numSigners int + + // earlyNonce determines if a nonce should be generated during context + // creation, to be automatically passed to the created session. + earlyNonce bool } // defaultContextOptions returns the default context options. @@ -129,12 +161,43 @@ func WithBip86TweakCtx() ContextOption { } } +// WithKnownSigners is an optional parameter that should be used if a session +// can be created as soon as all the singers are known. +func WithKnownSigners(signers []*btcec.PublicKey) ContextOption { + return func(o *contextOptions) { + o.keySet = signers + o.numSigners = len(signers) + } +} + +// WithNumSigners is a functional option used to specify that a context should +// be created without knowing all the signers. Instead the total number of +// signers is specified to ensure that a session can only be created once all +// the signers are known. +// +// NOTE: Either WithKnownSigners or WithNumSigners MUST be specified. +func WithNumSigners(n int) ContextOption { + return func(o *contextOptions) { + o.numSigners = n + } +} + +// WithEarlyNonceGen allow a caller to specify that a nonce should be generated +// early, before the session is created. This should be used in protocols that +// require some partial nonce exchange before all the signers are known. +// +// NOTE: This option must only be specified with the WithNumSigners option. +func WithEarlyNonceGen() ContextOption { + return func(o *contextOptions) { + o.earlyNonce = true + } +} + // NewContext creates a new signing context with the passed singing key and set // of public keys for each of the other signers. // // NOTE: This struct should be used over the raw Sign API whenever possible. -func NewContext(signingKey *btcec.PrivateKey, - signers []*btcec.PublicKey, shouldSort bool, +func NewContext(signingKey *btcec.PrivateKey, shouldSort bool, ctxOpts ...ContextOption) (*Context, error) { // First, parse the set of optional context options. @@ -143,10 +206,6 @@ func NewContext(signingKey *btcec.PrivateKey, option(opts) } - // As a sanity check, make sure the signing key is actually amongst the sit - // of signers. - // - // TODO(roasbeef): instead have pass all the _other_ signers? pubKey, err := schnorr.ParsePubKey( schnorr.SerializePubKey(signingKey.PubKey()), ) @@ -154,64 +213,156 @@ func NewContext(signingKey *btcec.PrivateKey, return nil, err } + ctx := &Context{ + signingKey: signingKey, + pubKey: pubKey, + opts: opts, + shouldSort: shouldSort, + } + + switch { + + // We know all the signers, so we can compute the aggregated key, along + // with all the other intermediate state we need to do signing and + // verification. + case opts.keySet != nil: + if err := ctx.combineSignerKeys(); err != nil { + return nil, err + } + + // The total signers are known, so we add ourselves, and skip key + // aggregation. + case opts.numSigners != 0: + // Otherwise, we'll add ourselves as the only known signer, and + // await further calls to RegisterSigner before a session can + // be created. + opts.keySet = make([]*btcec.PublicKey, 0, opts.numSigners) + opts.keySet = append(opts.keySet, pubKey) + + // If early nonce generation is specified, then we'll generate + // the nonce now to pass in to the session once all the callers + // are known. + if opts.earlyNonce { + ctx.sessionNonce, err = GenNonces() + if err != nil { + return nil, err + } + } + + default: + return nil, ErrSignersNotSpecified + } + + return ctx, nil +} + +// combineSignerKeys is used to compute the aggregated signer key once all the +// signers are known. +func (c *Context) combineSignerKeys() error { + // As a sanity check, make sure the signing key is actually + // amongst the sit of signers. var keyFound bool - for _, key := range signers { - if key.IsEqual(pubKey) { + for _, key := range c.opts.keySet { + if key.IsEqual(c.pubKey) { keyFound = true break } } if !keyFound { - return nil, ErrSignerNotInKeySet + return ErrSignerNotInKeySet } - // Now that we know that we're actually a signer, we'll generate the - // key hash finger print and second unique key index so we can speed up - // signing later. - keysHash := keyHashFingerprint(signers, shouldSort) - uniqueKeyIndex := secondUniqueKeyIndex(signers, shouldSort) + // Now that we know that we're actually a signer, we'll + // generate the key hash finger print and second unique key + // index so we can speed up signing later. + c.keysHash = keyHashFingerprint(c.opts.keySet, c.shouldSort) + c.uniqueKeyIndex = secondUniqueKeyIndex( + c.opts.keySet, c.shouldSort, + ) keyAggOpts := []KeyAggOption{ - WithKeysHash(keysHash), WithUniqueKeyIndex(uniqueKeyIndex), + WithKeysHash(c.keysHash), + WithUniqueKeyIndex(c.uniqueKeyIndex), } switch { - case opts.bip86Tweak: + case c.opts.bip86Tweak: keyAggOpts = append( keyAggOpts, WithBIP86KeyTweak(), ) - case opts.taprootTweak != nil: + case c.opts.taprootTweak != nil: keyAggOpts = append( - keyAggOpts, WithTaprootKeyTweak(opts.taprootTweak), + keyAggOpts, WithTaprootKeyTweak(c.opts.taprootTweak), ) - case len(opts.tweaks) != 0: - keyAggOpts = append(keyAggOpts, WithKeyTweaks(opts.tweaks...)) + case len(c.opts.tweaks) != 0: + keyAggOpts = append(keyAggOpts, WithKeyTweaks(c.opts.tweaks...)) } - // Next, we'll use this information to compute the aggregated public - // key that'll be used for signing in practice. - combinedKey, _, _, err := AggregateKeys( - signers, shouldSort, keyAggOpts..., + // Next, we'll use this information to compute the aggregated + // public key that'll be used for signing in practice. + var err error + c.combinedKey, _, _, err = AggregateKeys( + c.opts.keySet, c.shouldSort, keyAggOpts..., ) if err != nil { - return nil, err + return err } - return &Context{ - signingKey: signingKey, - pubKey: pubKey, - keySet: signers, - combinedKey: combinedKey, - uniqueKeyIndex: uniqueKeyIndex, - keysHash: keysHash, - opts: opts, - shouldSort: shouldSort, - }, nil + return nil +} + +// EarlySessionNonce returns the early session nonce, if available. +func (c *Context) EarlySessionNonce() (*Nonces, error) { + if c.sessionNonce == nil { + return nil, ErrNoEarlyNonce + } + + return c.sessionNonce, nil +} + +// RegisterSigner allows a caller to register a signer after the context has +// been created. This will be used in scenarios where the total number of +// signers is known, but nonce exchange needs to happen before all the signers +// are known. +// +// A bool is returned which indicates if all the signers have been registered. +// +// NOTE: If the set of keys are not to be sorted during signing, then the +// ordering each key is registered with MUST match the desired ordering. +func (c *Context) RegisterSigner(pub *btcec.PublicKey) (bool, error) { + haveAllSigners := len(c.opts.keySet) == c.opts.numSigners + if haveAllSigners { + return false, ErrAlreadyHaveAllSigners + } + + c.opts.keySet = append(c.opts.keySet, pub) + + // If we have the expected number of signers at this point, then we can + // generate the aggregated key and other necessary information. + haveAllSigners = len(c.opts.keySet) == c.opts.numSigners + if haveAllSigners { + if err := c.combineSignerKeys(); err != nil { + return false, err + } + } + + return haveAllSigners, nil +} + +// NumRegisteredSigners returns the total number of registered signers. +func (c *Context) NumRegisteredSigners() int { + return len(c.opts.keySet) } // CombinedKey returns the combined public key that will be used to generate // multi-signatures against. -func (c *Context) CombinedKey() *btcec.PublicKey { - return c.combinedKey.FinalKey +func (c *Context) CombinedKey() (*btcec.PublicKey, error) { + // If the caller hasn't registered all the signers at this point, then + // the combined key won't be available. + if c.combinedKey == nil { + return nil, ErrNotEnoughSigners + } + + return c.combinedKey.FinalKey, nil } // PubKey returns the public key of the signer of this session. @@ -221,8 +372,8 @@ func (c *Context) PubKey() btcec.PublicKey { // SigningKeys returns the set of keys used for signing. func (c *Context) SigningKeys() []*btcec.PublicKey { - keys := make([]*btcec.PublicKey, len(c.keySet)) - copy(keys, c.keySet) + keys := make([]*btcec.PublicKey, len(c.opts.keySet)) + copy(keys, c.opts.keySet) return keys } @@ -230,9 +381,15 @@ func (c *Context) SigningKeys() []*btcec.PublicKey { // TaprootInternalKey returns the internal taproot key, which is the aggregated // key _before_ the tweak is applied. If a taproot tweak was specified, then // CombinedKey() will return the fully tweaked output key, with this method -// returning the internal key. If a taproot tweak wasn't speciifed, then this +// returning the internal key. If a taproot tweak wasn't specified, then this // method will return an error. func (c *Context) TaprootInternalKey() (*btcec.PublicKey, error) { + // If the caller hasn't registered all the signers at this point, then + // the combined key won't be available. + if c.combinedKey == nil { + return nil, ErrNotEnoughSigners + } + if c.opts.taprootTweak == nil && !c.opts.bip86Tweak { return nil, ErrTaprootInternalKeyUnavailable } @@ -240,6 +397,30 @@ func (c *Context) TaprootInternalKey() (*btcec.PublicKey, error) { return c.combinedKey.PreTweakedKey, nil } +// SessionOption is a functional option argument that allows callers to modify +// the musig2 signing is done within a session. +type SessionOption func(*sessionOptions) + +// sessionOptions houses the set of functional options that can be used to +// modify the musig2 signing protocol. +type sessionOptions struct { + externalNonce *Nonces +} + +// defaultSessionOptions returns the default session options. +func defaultSessionOptions() *sessionOptions { + return &sessionOptions{} +} + +// WithPreGeneratedNonce allows a caller to start a session using a nonce +// they've generated themselves. This may be useful in protocols where all the +// signer keys may not be known before nonce exchange needs to occur. +func WithPreGeneratedNonce(nonce *Nonces) SessionOption { + return func(o *sessionOptions) { + o.externalNonce = nonce + } +} + // Session represents a musig2 signing session. A new instance should be // created each time a multi-signature is needed. The session struct handles // nonces management, incremental partial sig vitrifaction, as well as final @@ -248,6 +429,8 @@ func (c *Context) TaprootInternalKey() (*btcec.PublicKey, error) { // // NOTE: This struct should be used over the raw Sign API whenever possible. type Session struct { + opts *sessionOptions + ctx *Context localNonces *Nonces @@ -264,20 +447,52 @@ type Session struct { finalSig *schnorr.Signature } -// TODO(roasbeef): optional arg to allow parsing in pre-generated nonces - // NewSession creates a new musig2 signing session. -func (c *Context) NewSession() (*Session, error) { - localNonces, err := GenNonces() - if err != nil { - return nil, err +func (c *Context) NewSession(options ...SessionOption) (*Session, error) { + opts := defaultSessionOptions() + for _, opt := range options { + opt(opts) + } + + // At this point we verify that we know of all the signers, as + // otherwise we can't proceed with the session. This check is intended + // to catch misuse of the API wherein a caller forgets to register the + // remaining signers if they're doing nonce generation ahead of time. + if len(c.opts.keySet) != c.opts.numSigners { + return nil, ErrNotEnoughSigners + } + + // If an early nonce was specified, then we'll automatically add the + // corresponding session option for the caller. + var localNonces *Nonces + if c.sessionNonce != nil { + // Apply the early nonce to the session, and also blank out the + // session nonce on the context to ensure it isn't ever re-used + // for another session. + localNonces = c.sessionNonce + c.sessionNonce = nil + } else if opts.externalNonce != nil { + // Otherwise if there's a custom nonce passed in via the + // session options, then use that instead. + localNonces = opts.externalNonce + } + + // Now that we know we have enough signers, we'll either use the caller + // specified nonce, or generate a fresh set. + var err error + if localNonces == nil { + localNonces, err = GenNonces() + if err != nil { + return nil, err + } } s := &Session{ + opts: opts, ctx: c, localNonces: localNonces, - pubNonces: make([][PubNonceSize]byte, 0, len(c.keySet)), - sigs: make([]*PartialSignature, 0, len(c.keySet)), + pubNonces: make([][PubNonceSize]byte, 0, c.opts.numSigners), + sigs: make([]*PartialSignature, 0, c.opts.numSigners), } s.pubNonces = append(s.pubNonces, localNonces.PubNonce) @@ -302,9 +517,9 @@ func (s *Session) NumRegisteredNonces() int { // signers. This method returns true once all the public nonces have been // accounted for. func (s *Session) RegisterPubNonce(nonce [PubNonceSize]byte) (bool, error) { - // If we already have all the nonces, then this method was called too many - // times. - haveAllNonces := len(s.pubNonces) == len(s.ctx.keySet) + // If we already have all the nonces, then this method was called too + // many times. + haveAllNonces := len(s.pubNonces) == s.ctx.opts.numSigners if haveAllNonces { return false, ErrAlredyHaveAllNonces } @@ -312,7 +527,7 @@ func (s *Session) RegisterPubNonce(nonce [PubNonceSize]byte) (bool, error) { // Add this nonce and check again if we already have tall the nonces we // need. s.pubNonces = append(s.pubNonces, nonce) - haveAllNonces = len(s.pubNonces) == len(s.ctx.keySet) + haveAllNonces = len(s.pubNonces) == s.ctx.opts.numSigners // If we have all the nonces, then we can go ahead and combine them // now. @@ -334,8 +549,6 @@ func (s *Session) RegisterPubNonce(nonce [PubNonceSize]byte) (bool, error) { func (s *Session) Sign(msg [32]byte, signOpts ...SignOption) (*PartialSignature, error) { - s.msg = msg - switch { // If no local nonce is present, then this means we already signed, so // we'll return an error to prevent nonce re-use. @@ -363,7 +576,7 @@ func (s *Session) Sign(msg [32]byte, partialSig, err := Sign( s.localNonces.SecNonce, s.ctx.signingKey, *s.combinedNonce, - s.ctx.keySet, msg, signOpts..., + s.ctx.opts.keySet, msg, signOpts..., ) // Now that we've generated our signature, we'll make sure to blank out @@ -374,6 +587,8 @@ func (s *Session) Sign(msg [32]byte, return nil, err } + s.msg = msg + s.ourSig = partialSig s.sigs = append(s.sigs, partialSig) @@ -386,7 +601,7 @@ func (s *Session) Sign(msg [32]byte, func (s *Session) CombineSig(sig *PartialSignature) (bool, error) { // First check if we already have all the signatures we need. We // already accumulated our own signature when we generated the sig. - haveAllSigs := len(s.sigs) == len(s.ctx.keySet) + haveAllSigs := len(s.sigs) == len(s.ctx.opts.keySet) if haveAllSigs { return false, ErrAlredyHaveAllSigs } @@ -397,7 +612,7 @@ func (s *Session) CombineSig(sig *PartialSignature) (bool, error) { // Accumulate this sig, and check again if we have all the sigs we // need. s.sigs = append(s.sigs, sig) - haveAllSigs = len(s.sigs) == len(s.ctx.keySet) + haveAllSigs = len(s.sigs) == len(s.ctx.opts.keySet) // If we have all the signatures, then we can combine them all into the // final signature. @@ -407,21 +622,22 @@ func (s *Session) CombineSig(sig *PartialSignature) (bool, error) { case s.ctx.opts.bip86Tweak: combineOpts = append( combineOpts, WithBip86TweakedCombine( - s.msg, s.ctx.keySet, s.ctx.shouldSort, + s.msg, s.ctx.opts.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, + s.msg, s.ctx.opts.keySet, + s.ctx.opts.taprootTweak, s.ctx.shouldSort, ), ) case len(s.ctx.opts.tweaks) != 0: combineOpts = append( combineOpts, WithTweakedCombine( - s.msg, s.ctx.keySet, s.ctx.opts.tweaks, - s.ctx.shouldSort, + s.msg, s.ctx.opts.keySet, + s.ctx.opts.tweaks, s.ctx.shouldSort, ), ) } diff --git a/btcec/schnorr/musig2/musig2_test.go b/btcec/schnorr/musig2/musig2_test.go index 71378f61ba..df3f78c93c 100644 --- a/btcec/schnorr/musig2/musig2_test.go +++ b/btcec/schnorr/musig2/musig2_test.go @@ -6,6 +6,7 @@ import ( "bytes" "crypto/sha256" "encoding/hex" + "errors" "fmt" "sync" "testing" @@ -392,20 +393,25 @@ func testMultiPartySign(t *testing.T, taprootTweak []byte, ctxOpts = append(ctxOpts, WithTweakedContext(tweaks...)) } + ctxOpts = append(ctxOpts, WithKnownSigners(signSet)) + // Now that we have all the signers, we'll make a new context, then // generate a new session for each of them(which handles nonce // generation). signers := make([]*Session, numSigners) for i, signerKey := range signerKeys { signCtx, err := NewContext( - signerKey, signSet, false, ctxOpts..., + signerKey, false, ctxOpts..., ) if err != nil { t.Fatalf("unable to generate context: %v", err) } if combinedKey == nil { - combinedKey = signCtx.CombinedKey() + combinedKey, err = signCtx.CombinedKey() + if err != nil { + t.Fatalf("combined key not available: %v", err) + } } session, err := signCtx.NewSession() @@ -538,3 +544,180 @@ func TestMuSigMultiParty(t *testing.T) { testMultiPartySign(t, []byte{}) }) } + +// TestMuSigEarlyNonce tests that for protocols where nonces need to be +// exchagned before all signers are known, the context API works as expected. +func TestMuSigEarlyNonce(t *testing.T) { + t.Parallel() + + privKey1, err := btcec.NewPrivateKey() + if err != nil { + t.Fatalf("unable to gen priv key: %v", err) + } + privKey2, err := btcec.NewPrivateKey() + if err != nil { + t.Fatalf("unable to gen priv key: %v", err) + } + + // If we try to make a context, with just the private key and sorting + // value, we should get an error. + _, err = NewContext(privKey1, true) + if !errors.Is(err, ErrSignersNotSpecified) { + t.Fatalf("unexpected ctx error: %v", err) + } + + numSigners := 2 + + ctx1, err := NewContext( + privKey1, true, WithNumSigners(numSigners), WithEarlyNonceGen(), + ) + if err != nil { + t.Fatalf("unable to make ctx: %v", err) + } + pubKey1 := ctx1.PubKey() + + ctx2, err := NewContext( + privKey2, true, WithNumSigners(numSigners), WithEarlyNonceGen(), + ) + if err != nil { + t.Fatalf("unable to make ctx: %v", err) + } + pubKey2 := ctx2.PubKey() + + // At this point, the combined key shouldn't be available for both + // signers, since we only know of the sole signers. + if _, err := ctx1.CombinedKey(); !errors.Is(err, ErrNotEnoughSigners) { + t.Fatalf("unepxected error: %v", err) + } + if _, err := ctx2.CombinedKey(); !errors.Is(err, ErrNotEnoughSigners) { + t.Fatalf("unepxected error: %v", err) + } + + // The early nonces _should_ be available at this point. + nonce1, err := ctx1.EarlySessionNonce() + if err != nil { + t.Fatalf("session nonce not available: %v", err) + } + nonce2, err := ctx2.EarlySessionNonce() + if err != nil { + t.Fatalf("session nonce not available: %v", err) + } + + // The number of registered signers should still be 1 for both parties. + if ctx1.NumRegisteredSigners() != 1 { + t.Fatalf("expected 1 signer, instead have: %v", + ctx1.NumRegisteredSigners()) + } + if ctx2.NumRegisteredSigners() != 1 { + t.Fatalf("expected 1 signer, instead have: %v", + ctx2.NumRegisteredSigners()) + } + + // If we try to make a session, we should get an error since we dn't + // have all the signers yet. + if _, err := ctx1.NewSession(); !errors.Is(err, ErrNotEnoughSigners) { + t.Fatalf("unexpected session key error: %v", err) + } + + // The combined key should also be unavailable as well. + if _, err := ctx1.CombinedKey(); !errors.Is(err, ErrNotEnoughSigners) { + t.Fatalf("unexpected combined key error: %v", err) + } + + // We'll now register the other signer for both parties. + done, err := ctx1.RegisterSigner(&pubKey2) + if err != nil { + t.Fatalf("unable to register signer: %v", err) + } + if !done { + t.Fatalf("signer 1 doesn't have all keys") + } + done, err = ctx2.RegisterSigner(&pubKey1) + if err != nil { + t.Fatalf("unable to register signer: %v", err) + } + if !done { + t.Fatalf("signer 2 doesn't have all keys") + } + + // If we try to register the signer again, we should get an error. + _, err = ctx2.RegisterSigner(&pubKey1) + if !errors.Is(err, ErrAlreadyHaveAllSigners) { + t.Fatalf("should not be able to register too many signers") + } + + // We should be able to create the session at this point. + session1, err := ctx1.NewSession() + if err != nil { + t.Fatalf("unable to create new session: %v", err) + } + session2, err := ctx2.NewSession() + if err != nil { + t.Fatalf("unable to create new session: %v", err) + } + + msg := sha256.Sum256([]byte("let's get taprooty, LN style")) + + // If we try to sign before we have the combined nonce, we shoudl get + // an error. + _, err = session1.Sign(msg) + if !errors.Is(err, ErrCombinedNonceUnavailable) { + t.Fatalf("unable to gen sig: %v", err) + } + + // Now we can exchange nonces to continue with the rest of the signing + // process as normal. + done, err = session1.RegisterPubNonce(nonce2.PubNonce) + if err != nil { + t.Fatalf("unable to register nonce: %v", err) + } + if !done { + t.Fatalf("signer 1 doesn't have all nonces") + } + done, err = session2.RegisterPubNonce(nonce1.PubNonce) + if err != nil { + t.Fatalf("unable to register nonce: %v", err) + } + if !done { + t.Fatalf("signer 2 doesn't have all nonces") + } + + // Registering the nonce again should error out. + _, err = session2.RegisterPubNonce(nonce1.PubNonce) + if !errors.Is(err, ErrAlredyHaveAllNonces) { + t.Fatalf("shouldn't be able to register nonces twice") + } + + // Sign the message and combine the two partial sigs into one. + _, err = session1.Sign(msg) + if err != nil { + t.Fatalf("unable to gen sig: %v", err) + } + sig2, err := session2.Sign(msg) + if err != nil { + t.Fatalf("unable to gen sig: %v", err) + } + done, err = session1.CombineSig(sig2) + if err != nil { + t.Fatalf("unable to combine sig: %v", err) + } + if !done { + t.Fatalf("all sigs should be known now: %v", err) + } + + // If we try to combine another sig, then we should get an error. + _, err = session1.CombineSig(sig2) + if !errors.Is(err, ErrAlredyHaveAllSigs) { + t.Fatalf("shouldn't be able to combine again") + } + + // Finally, verify that the final signature is valid. + combinedKey, err := ctx1.CombinedKey() + if err != nil { + t.Fatalf("unexpected combined key error: %v", err) + } + finalSig := session1.FinalSig() + if !finalSig.Verify(msg[:], combinedKey) { + t.Fatalf("final sig is invalid!") + } +} diff --git a/btcec/schnorr/musig2/nonces.go b/btcec/schnorr/musig2/nonces.go index c0ece86fa4..5a7fbd4b3d 100644 --- a/btcec/schnorr/musig2/nonces.go +++ b/btcec/schnorr/musig2/nonces.go @@ -185,7 +185,7 @@ func uint8Writer(w io.Writer, b []byte) error { return binary.Write(w, byteOrder, uint8(len(b))) } -// uint8Writer is an implementation of lengthWriter that writes the length of +// uint32Writer is an implementation of lengthWriter that writes the length of // the byte slice using 4 bytes. func uint32Writer(w io.Writer, b []byte) error { return binary.Write(w, byteOrder, uint32(len(b))) From ba20c75aaffee0e09cbfdd7b204add18485bd1a8 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 27 Apr 2022 16:51:53 -0700 Subject: [PATCH 15/16] btcec/schnorr/musig2: pass in aux info during nonce generation --- btcec/schnorr/musig2/context.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/btcec/schnorr/musig2/context.go b/btcec/schnorr/musig2/context.go index 298fe96179..19cef34fa9 100644 --- a/btcec/schnorr/musig2/context.go +++ b/btcec/schnorr/musig2/context.go @@ -481,7 +481,13 @@ func (c *Context) NewSession(options ...SessionOption) (*Session, error) { // specified nonce, or generate a fresh set. var err error if localNonces == nil { - localNonces, err = GenNonces() + // At this point we need to generate a fresh nonce. We'll pass + // in some auxiliary information to strengthen the nonce + // generated. + localNonces, err = GenNonces( + WithNonceSecretKeyAux(c.signingKey), + WithNonceCombinedKeyAux(c.combinedKey.FinalKey), + ) if err != nil { return nil, err } From 1da361b04e7c4a90c719f56762fa556cce995201 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 28 Apr 2022 16:06:39 -0700 Subject: [PATCH 16/16] btcec/schnorr/musig2: add optional json dump command to gen test vectors --- btcec/schnorr/musig2/musig2_test.go | 118 ++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 7 deletions(-) diff --git a/btcec/schnorr/musig2/musig2_test.go b/btcec/schnorr/musig2/musig2_test.go index df3f78c93c..c9ae74d7be 100644 --- a/btcec/schnorr/musig2/musig2_test.go +++ b/btcec/schnorr/musig2/musig2_test.go @@ -6,8 +6,11 @@ import ( "bytes" "crypto/sha256" "encoding/hex" + "encoding/json" "errors" + "flag" "fmt" + "io/ioutil" "sync" "testing" @@ -15,12 +18,6 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr" ) -// TestMuSig2SgnTestVectors tests that this implementation of musig2 matches -// the secp256k1-zkp test vectors. -func TestMuSig2SignTestVectors(t *testing.T) { - t.Parallel() -} - var ( key1Bytes, _ = hex.DecodeString("F9308A019258C31049344F85F89D5229B53" + "1C845836F99B08601F113BCE036F9") @@ -37,11 +34,27 @@ var ( keyCombo4, _ = hex.DecodeString("2EB18851887E7BDC5E830E89B19DDBC28078F1FA88AAD0AD01CA06FE4F80210B") ) +const ( + keyAggTestVectorName = "key_agg_vectors.json" + + signTestVectorName = "sign_vectors.json" +) + +var dumpJson = flag.Bool("dumpjson", false, "if true, a JSON version of the "+ + "test vectors will be written to the cwd") + +type jsonKeyAggTestCase struct { + Keys []string `json:"keys"` + ExpectedKey string `json:"expected_key"` +} + // TestMuSig2KeyAggTestVectors tests that this implementation of musig2 key // aggregation lines up with the secp256k1-zkp test vectors. func TestMuSig2KeyAggTestVectors(t *testing.T) { t.Parallel() + var jsonCases []jsonKeyAggTestCase + testCases := []struct { keyOrder []int expectedKey []byte @@ -73,7 +86,10 @@ func TestMuSig2KeyAggTestVectors(t *testing.T) { for i, testCase := range testCases { testName := fmt.Sprintf("%v", testCase.keyOrder) t.Run(testName, func(t *testing.T) { - var keys []*btcec.PublicKey + var ( + keys []*btcec.PublicKey + strKeys []string + ) for _, keyIndex := range testCase.keyOrder { keyBytes := testKeys[keyIndex] pub, err := schnorr.ParsePubKey(keyBytes) @@ -82,8 +98,14 @@ func TestMuSig2KeyAggTestVectors(t *testing.T) { } keys = append(keys, pub) + strKeys = append(strKeys, hex.EncodeToString(keyBytes)) } + jsonCases = append(jsonCases, jsonKeyAggTestCase{ + Keys: strKeys, + ExpectedKey: hex.EncodeToString(testCase.expectedKey), + }) + uniqueKeyIndex := secondUniqueKeyIndex(keys, false) combinedKey, _, _, _ := AggregateKeys( keys, false, WithUniqueKeyIndex(uniqueKeyIndex), @@ -96,6 +118,22 @@ func TestMuSig2KeyAggTestVectors(t *testing.T) { } }) } + + if *dumpJson { + jsonBytes, err := json.Marshal(jsonCases) + if err != nil { + t.Fatalf("unable to encode json: %v", err) + } + + var formattedJson bytes.Buffer + json.Indent(&formattedJson, jsonBytes, "", "\t") + err = ioutil.WriteFile( + keyAggTestVectorName, formattedJson.Bytes(), 0644, + ) + if err != nil { + t.Fatalf("unable to write file: %v", err) + } + } } func mustParseHex(str string) []byte { @@ -160,11 +198,37 @@ func genTweakParity(tweak KeyTweakDesc, isXOnly bool) KeyTweakDesc { return tweak } +type jsonTweak struct { + Tweak string `json:"tweak"` + XOnly bool `json:"x_only"` +} + +type tweakSignCase struct { + Keys []string `json:"keys"` + Tweaks []jsonTweak `json:"tweaks,omitempty"` + + ExpectedSig string `json:"expected_sig"` +} + +type jsonSignTestCase struct { + SecNonce string `json:"secret_nonce"` + AggNonce string `json:"agg_nonce"` + SigningKey string `json:"signing_key"` + Msg string `json:"msg"` + + TestCases []tweakSignCase `json:"test_cases"` +} + // TestMuSig2SigningTestVectors tests that the musig2 implementation produces // the same set of signatures. func TestMuSig2SigningTestVectors(t *testing.T) { t.Parallel() + var jsonCases jsonSignTestCase + + jsonCases.SigningKey = hex.EncodeToString(signSetPrivKey.Serialize()) + jsonCases.Msg = hex.EncodeToString(signTestMsg) + var aggregatedNonce [PubNonceSize]byte copy( aggregatedNonce[:], @@ -175,10 +239,14 @@ func TestMuSig2SigningTestVectors(t *testing.T) { mustParseHex("037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9"), ) + jsonCases.AggNonce = hex.EncodeToString(aggregatedNonce[:]) + var secNonce [SecNonceSize]byte copy(secNonce[:], mustParseHex("508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61")) copy(secNonce[32:], mustParseHex("FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F7")) + jsonCases.SecNonce = hex.EncodeToString(secNonce[:]) + tweak1 := KeyTweakDesc{ Tweak: [32]byte{ 0xE8, 0xF7, 0x91, 0xFF, 0x92, 0x25, 0xA2, 0xAF, @@ -277,9 +345,15 @@ func TestMuSig2SigningTestVectors(t *testing.T) { } t.Run(testName, func(t *testing.T) { + var strKeys []string keySet := make([]*btcec.PublicKey, 0, len(testCase.keyOrder)) for _, keyIndex := range testCase.keyOrder { keySet = append(keySet, signSetKeys[keyIndex]) + strKeys = append( + strKeys, hex.EncodeToString( + schnorr.SerializePubKey(signSetKeys[keyIndex]), + ), + ) } var opts []SignOption @@ -289,6 +363,14 @@ func TestMuSig2SigningTestVectors(t *testing.T) { ) } + var jsonTweaks []jsonTweak + for _, tweak := range testCase.tweaks { + jsonTweaks = append(jsonTweaks, jsonTweak{ + Tweak: hex.EncodeToString(tweak.Tweak[:]), + XOnly: tweak.IsXOnly, + }) + } + partialSig, err := Sign( secNonce, signSetPrivKey, aggregatedNonce, keySet, msg, opts..., @@ -305,8 +387,30 @@ func TestMuSig2SigningTestVectors(t *testing.T) { testCase.expectedPartialSig, partialSigBytes, ) } + + jsonCases.TestCases = append(jsonCases.TestCases, tweakSignCase{ + Keys: strKeys, + Tweaks: jsonTweaks, + ExpectedSig: hex.EncodeToString(testCase.expectedPartialSig), + }) }) } + + if *dumpJson { + jsonBytes, err := json.Marshal(jsonCases) + if err != nil { + t.Fatalf("unable to encode json: %v", err) + } + + var formattedJson bytes.Buffer + json.Indent(&formattedJson, jsonBytes, "", "\t") + err = ioutil.WriteFile( + signTestVectorName, formattedJson.Bytes(), 0644, + ) + if err != nil { + t.Fatalf("unable to write file: %v", err) + } + } } type signer struct {