btcd/btcec/schnorr/musig2/sign.go
Olaoluwa Osuntokun bb7ba7b1fc
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.
2022-04-28 16:19:42 -07:00

423 lines
13 KiB
Go

// 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)
}