You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
584 lines
21 KiB
584 lines
21 KiB
// Copyright (c) 2013-2022 The btcsuite developers |
|
|
|
package musig2 |
|
|
|
import ( |
|
"fmt" |
|
|
|
"lol.mleku.dev/chk" |
|
"next.orly.dev/pkg/crypto/ec" |
|
"next.orly.dev/pkg/crypto/ec/schnorr" |
|
) |
|
|
|
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 secret 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", |
|
) |
|
// ErrAlreadyHaveAllNonces is called when RegisterPubNonce is called too |
|
// many times for a given signing session. |
|
// |
|
// ErrAlreadyHaveAllNonces is returned when a caller attempts to |
|
// register a signer, once we already have the total set of known |
|
// signers. |
|
ErrAlreadyHaveAllNonces = 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 is returned if a caller attempts to obtain an |
|
// early nonce when it wasn't specified |
|
ErrNotEnoughSigners = fmt.Errorf("not enough 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") |
|
// 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") |
|
// ErrTaprootInternalKeyUnavailable is returned when a user attempts to |
|
// obtain the |
|
ErrTaprootInternalKeyUnavailable = fmt.Errorf("taproot tweak not used") |
|
ErrNoEarlyNonce = fmt.Errorf("no early nonce available") |
|
) |
|
|
|
// 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.SecretKey |
|
// pubKey is our even-y coordinate public key. |
|
pubKey *btcec.PublicKey |
|
// combinedKey is the aggregated public key. |
|
combinedKey *AggregateKey |
|
// 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 |
|
// 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. |
|
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 |
|
// 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 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 |
|
// 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. |
|
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 } |
|
} |
|
|
|
// 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 } |
|
} |
|
|
|
// 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 } |
|
} |
|
|
|
// 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.SecretKey, shouldSort bool, |
|
ctxOpts ...ContextOption, |
|
) (*Context, error) { |
|
|
|
// First, parse the set of optional context options. |
|
opts := defaultContextOptions() |
|
for _, option := range ctxOpts { |
|
option(opts) |
|
} |
|
pubKey := signingKey.PubKey() |
|
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(); chk.T(err) { |
|
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) |
|
default: |
|
return nil, ErrSignersNotSpecified |
|
} |
|
// 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 { |
|
var err error |
|
ctx.sessionNonce, err = GenNonces( |
|
WithPublicKey(ctx.pubKey), |
|
WithNonceSecretKeyAux(signingKey), |
|
) |
|
if err != nil { |
|
return nil, err |
|
} |
|
} |
|
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 c.opts.keySet { |
|
if key.IsEqual(c.pubKey) { |
|
keyFound = true |
|
break |
|
} |
|
} |
|
if !keyFound { |
|
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. |
|
c.keysHash = keyHashFingerprint(c.opts.keySet, c.shouldSort) |
|
c.uniqueKeyIndex = secondUniqueKeyIndex( |
|
c.opts.keySet, c.shouldSort, |
|
) |
|
keyAggOpts := []KeyAggOption{ |
|
WithKeysHash(c.keysHash), |
|
WithUniqueKeyIndex(c.uniqueKeyIndex), |
|
} |
|
switch { |
|
case c.opts.bip86Tweak: |
|
keyAggOpts = append( |
|
keyAggOpts, WithBIP86KeyTweak(), |
|
) |
|
case c.opts.taprootTweak != nil: |
|
keyAggOpts = append( |
|
keyAggOpts, WithTaprootKeyTweak(c.opts.taprootTweak), |
|
) |
|
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. |
|
var err error |
|
c.combinedKey, _, _, err = AggregateKeys( |
|
c.opts.keySet, c.shouldSort, keyAggOpts..., |
|
) |
|
if err != nil { |
|
return err |
|
} |
|
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(); chk.T(err) { |
|
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, 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. |
|
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.opts.keySet)) |
|
copy(keys, c.opts.keySet) |
|
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 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 |
|
} |
|
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 |
|
// 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 { |
|
opts *sessionOptions |
|
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(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 { |
|
// 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( |
|
WithPublicKey(c.pubKey), |
|
WithNonceSecretKeyAux(c.signingKey), |
|
WithNonceCombinedKeyAux(c.combinedKey.FinalKey), |
|
) |
|
if err != nil { |
|
return nil, err |
|
} |
|
} |
|
s := &Session{ |
|
opts: opts, |
|
ctx: c, |
|
localNonces: localNonces, |
|
pubNonces: make([][PubNonceSize]byte, 0, c.opts.numSigners), |
|
sigs: make([]*PartialSignature, 0, c.opts.numSigners), |
|
} |
|
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) == s.ctx.opts.numSigners |
|
if haveAllNonces { |
|
return false, ErrAlreadyHaveAllNonces |
|
} |
|
// 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) == s.ctx.opts.numSigners |
|
// 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) { |
|
|
|
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 |
|
} |
|
switch { |
|
case s.ctx.opts.bip86Tweak: |
|
signOpts = append( |
|
signOpts, WithBip86SignTweak(), |
|
) |
|
case s.ctx.opts.taprootTweak != nil: |
|
signOpts = append( |
|
signOpts, WithTaprootSignTweak(s.ctx.opts.taprootTweak), |
|
) |
|
case len(s.ctx.opts.tweaks) != 0: |
|
signOpts = append(signOpts, WithTweaks(s.ctx.opts.tweaks...)) |
|
} |
|
partialSig, err := Sign( |
|
s.localNonces.SecNonce, s.ctx.signingKey, *s.combinedNonce, |
|
s.ctx.opts.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.msg = msg |
|
s.ourSig = partialSig |
|
s.sigs = append(s.sigs, partialSig) |
|
return partialSig, nil |
|
} |
|
|
|
// 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) { |
|
// 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.opts.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.opts.keySet) |
|
// If we have all the signatures, then we can combine them all into the |
|
// final signature. |
|
if haveAllSigs { |
|
var combineOpts []CombineOption |
|
switch { |
|
case s.ctx.opts.bip86Tweak: |
|
combineOpts = append( |
|
combineOpts, WithBip86TweakedCombine( |
|
s.msg, s.ctx.opts.keySet, |
|
s.ctx.shouldSort, |
|
), |
|
) |
|
case s.ctx.opts.taprootTweak != nil: |
|
combineOpts = append( |
|
combineOpts, WithTaprootTweakedCombine( |
|
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.opts.keySet, |
|
s.ctx.opts.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. |
|
// |
|
// TODO(roasbef): allow skipping? |
|
if !finalSig.Verify(s.msg[:], s.ctx.combinedKey.FinalKey) { |
|
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 }
|
|
|