From 8fc99fba00b7d7b23467394b4f27036a8840fe2f Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 27 Apr 2022 22:20:33 +0200 Subject: [PATCH] input+btcwallet: add MuSig2 signing operations With this commit we add the high-level MuSig2 signing methods to the btcwallet which will later be exposed through an RPC interface. --- input/musig2.go | 228 ++++++++++++++++++++++++++++ lnwallet/btcwallet/btcwallet.go | 19 ++- lnwallet/btcwallet/signer.go | 253 ++++++++++++++++++++++++++++++++ 3 files changed, 493 insertions(+), 7 deletions(-) create mode 100644 input/musig2.go diff --git a/input/musig2.go b/input/musig2.go new file mode 100644 index 000000000..a7a0d78e4 --- /dev/null +++ b/input/musig2.go @@ -0,0 +1,228 @@ +package input + +import ( + "bytes" + "crypto/sha256" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/lightningnetwork/lnd/keychain" +) + +const ( + // MuSig2PartialSigSize is the size of a MuSig2 partial signature. + // Because a partial signature is just the s value, this corresponds to + // the length of a scalar. + MuSig2PartialSigSize = 32 +) + +// MuSig2SessionID is a type for a session ID that is just a hash of the MuSig2 +// combined key and the local public nonces. +type MuSig2SessionID [sha256.Size]byte + +// MuSig2Signer is an interface that declares all methods that a MuSig2 +// compatible signer needs to implement. +type MuSig2Signer interface { + // MuSig2CreateSession creates a new MuSig2 signing session using the + // local key identified by the key locator. The complete list of all + // public keys of all signing parties must be provided, including the + // public key of the local signing key. If nonces of other parties are + // already known, they can be submitted as well to reduce the number of + // method calls necessary later on. + MuSig2CreateSession(keychain.KeyLocator, []*btcec.PublicKey, + *MuSig2Tweaks, [][musig2.PubNonceSize]byte) (*MuSig2SessionInfo, + error) + + // MuSig2RegisterNonces registers one or more public nonces of other + // signing participants for a session identified by its ID. This method + // returns true once we have all nonces for all other signing + // participants. + MuSig2RegisterNonces(MuSig2SessionID, + [][musig2.PubNonceSize]byte) (bool, error) + + // MuSig2Sign creates a partial signature using the local signing key + // that was specified when the session was created. This can only be + // called when all public nonces of all participants are known and have + // been registered with the session. If this node isn't responsible for + // combining all the partial signatures, then the cleanup parameter + // should be set, indicating that the session can be removed from memory + // once the signature was produced. + MuSig2Sign(MuSig2SessionID, [sha256.Size]byte, + bool) (*musig2.PartialSignature, error) + + // MuSig2CombineSig combines the given partial signature(s) with the + // local one, if it already exists. Once a partial signature of all + // participants is registered, the final signature will be combined and + // returned. + MuSig2CombineSig(MuSig2SessionID, + []*musig2.PartialSignature) (*schnorr.Signature, bool, error) +} + +// MuSig2SessionInfo is a struct for keeping track of a signing session +// information in memory. +type MuSig2SessionInfo struct { + // SessionID is the wallet's internal unique ID of this session. The ID + // is the hash over the combined public key and the local public nonces. + SessionID [32]byte + + // PublicNonce contains the public nonce of the local signer session. + PublicNonce [musig2.PubNonceSize]byte + + // CombinedKey is the combined public key with all tweaks applied to it. + CombinedKey *btcec.PublicKey + + // TaprootTweak indicates whether a taproot tweak (BIP-0086 or script + // path) was used. The TaprootInternalKey will only be set if this is + // set to true. + TaprootTweak bool + + // TaprootInternalKey is the raw combined public key without any tweaks + // applied to it. This is only set if TaprootTweak is true. + TaprootInternalKey *btcec.PublicKey + + // HaveAllNonces indicates whether this session already has all nonces + // of all other signing participants registered. + HaveAllNonces bool + + // HaveAllSigs indicates whether this session already has all partial + // signatures of all other signing participants registered. + HaveAllSigs bool +} + +// MuSig2Tweaks is a struct that contains all tweaks that can be applied to a +// MuSig2 combined public key. +type MuSig2Tweaks struct { + // GenericTweaks is a list of normal tweaks to apply to the combined + // public key (and to the private key when signing). + GenericTweaks []musig2.KeyTweakDesc + + // TaprootBIP0086Tweak indicates that 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 aggregated key before the tweak will be used as the + // internal key. If this is set to true then TaprootTweak will be + // ignored. + TaprootBIP0086Tweak bool + + // TaprootTweak specifies that 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. Will be ignored if TaprootBIP0086Tweak is set to true. + TaprootTweak []byte +} + +// HasTaprootTweak returns true if either a taproot BIP0086 tweak or a taproot +// script root tweak is set. +func (t *MuSig2Tweaks) HasTaprootTweak() bool { + return t.TaprootBIP0086Tweak || len(t.TaprootTweak) > 0 +} + +// ToContextOptions converts the tweak descriptor to context options. +func (t *MuSig2Tweaks) ToContextOptions() []musig2.ContextOption { + var tweakOpts []musig2.ContextOption + if len(t.GenericTweaks) > 0 { + tweakOpts = append(tweakOpts, musig2.WithTweakedContext( + t.GenericTweaks..., + )) + } + + // The BIP0086 tweak and the taproot script tweak are mutually + // exclusive. + if t.TaprootBIP0086Tweak { + tweakOpts = append(tweakOpts, musig2.WithBip86TweakCtx()) + } else if len(t.TaprootTweak) > 0 { + tweakOpts = append(tweakOpts, musig2.WithTaprootTweakCtx( + t.TaprootTweak, + )) + } + + return tweakOpts +} + +// MuSig2CombineKeys combines the given set of public keys into a single +// combined MuSig2 combined public key, applying the given tweaks. +func MuSig2CombineKeys(allSignerPubKeys []*btcec.PublicKey, + tweaks *MuSig2Tweaks) (*musig2.AggregateKey, error) { + + // Convert the tweak options into the appropriate MuSig2 API functional + // options. + var keyAggOpts []musig2.KeyAggOption + switch { + case tweaks.TaprootBIP0086Tweak: + keyAggOpts = append(keyAggOpts, musig2.WithBIP86KeyTweak()) + case len(tweaks.TaprootTweak) > 0: + keyAggOpts = append(keyAggOpts, musig2.WithTaprootKeyTweak( + tweaks.TaprootTweak, + )) + case len(tweaks.GenericTweaks) > 0: + keyAggOpts = append(keyAggOpts, musig2.WithKeyTweaks( + tweaks.GenericTweaks..., + )) + } + + // Then we'll use this information to compute the aggregated public key. + combinedKey, _, _, err := musig2.AggregateKeys( + allSignerPubKeys, true, keyAggOpts..., + ) + return combinedKey, err +} + +// NewMuSig2SessionID returns the unique ID of a MuSig2 session by using the +// combined key and the local public nonces and hashing that data. +func NewMuSig2SessionID(combinedKey *btcec.PublicKey, + publicNonces [musig2.PubNonceSize]byte) MuSig2SessionID { + + // We hash the data to save some bytes in memory. + hash := sha256.New() + _, _ = hash.Write(combinedKey.SerializeCompressed()) + _, _ = hash.Write(publicNonces[:]) + + id := MuSig2SessionID{} + copy(id[:], hash.Sum(nil)) + return id +} + +// SerializePartialSignature encodes the partial signature to a fixed size byte +// array. +func SerializePartialSignature( + sig *musig2.PartialSignature) ([MuSig2PartialSigSize]byte, error) { + + var ( + buf bytes.Buffer + result [MuSig2PartialSigSize]byte + ) + if err := sig.Encode(&buf); err != nil { + return result, fmt.Errorf("error encoding partial signature: "+ + "%v", err) + } + + if buf.Len() != MuSig2PartialSigSize { + return result, fmt.Errorf("invalid partial signature length, "+ + "got %d wanted %d", buf.Len(), MuSig2PartialSigSize) + } + + copy(result[:], buf.Bytes()) + + return result, nil +} + +// DeserializePartialSignature decodes a partial signature from a byte slice. +func DeserializePartialSignature(scalarBytes []byte) (*musig2.PartialSignature, + error) { + + if len(scalarBytes) != MuSig2PartialSigSize { + return nil, fmt.Errorf("invalid partial signature length, got "+ + "%d wanted %d", len(scalarBytes), MuSig2PartialSigSize) + } + + sig := &musig2.PartialSignature{} + if err := sig.Decode(bytes.NewReader(scalarBytes)); err != nil { + return nil, fmt.Errorf("error decoding partial signature: %v", + err) + } + + return sig, nil +} diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go index 41bec9410..c6b8b1a46 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -24,6 +24,7 @@ import ( "github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/wtxmgr" "github.com/lightningnetwork/lnd/blockcache" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwallet" @@ -98,6 +99,9 @@ type BtcWallet struct { chainKeyScope waddrmgr.KeyScope blockCache *blockcache.BlockCache + + musig2Sessions map[input.MuSig2SessionID]*muSig2State + musig2SessionsMtx sync.Mutex } // A compile time check to ensure that BtcWallet implements the @@ -160,13 +164,14 @@ func New(cfg Config, blockCache *blockcache.BlockCache) (*BtcWallet, error) { } return &BtcWallet{ - cfg: &cfg, - wallet: wallet, - db: wallet.Database(), - chain: cfg.ChainSource, - netParams: cfg.NetParams, - chainKeyScope: chainKeyScope, - blockCache: blockCache, + cfg: &cfg, + wallet: wallet, + db: wallet.Database(), + chain: cfg.ChainSource, + netParams: cfg.NetParams, + chainKeyScope: chainKeyScope, + blockCache: blockCache, + musig2Sessions: make(map[input.MuSig2SessionID]*muSig2State), }, nil } diff --git a/lnwallet/btcwallet/signer.go b/lnwallet/btcwallet/signer.go index a205109b5..a52805b7d 100644 --- a/lnwallet/btcwallet/signer.go +++ b/lnwallet/btcwallet/signer.go @@ -1,11 +1,13 @@ package btcwallet import ( + "crypto/sha256" "fmt" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -451,6 +453,257 @@ func (b *BtcWallet) ComputeInputScript(tx *wire.MsgTx, }, nil } +// muSig2State is a struct that holds on to the internal signing session state +// of a MuSig2 session. +type muSig2State struct { + // MuSig2SessionInfo is the associated meta information of the signing + // session. + input.MuSig2SessionInfo + + // context is the signing context responsible for keeping track of the + // public keys involved in the signing process. + context *musig2.Context + + // session is the signing session responsible for keeping track of the + // nonces and partial signatures involved in the signing process. + session *musig2.Session +} + +// MuSig2CreateSession creates a new MuSig2 signing session using the local +// key identified by the key locator. The complete list of all public keys of +// all signing parties must be provided, including the public key of the local +// signing key. If nonces of other parties are already known, they can be +// submitted as well to reduce the number of method calls necessary later on. +func (b *BtcWallet) MuSig2CreateSession(keyLoc keychain.KeyLocator, + allSignerPubKeys []*btcec.PublicKey, tweaks *input.MuSig2Tweaks, + otherSignerNonces [][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, + error) { + + // We need to derive the private key for signing. In the remote signing + // setup, this whole RPC call will be forwarded to the signing + // instance, which requires it to be stateful. + privKey, err := b.fetchPrivKey(&keychain.KeyDescriptor{ + KeyLocator: keyLoc, + }) + if err != nil { + return nil, fmt.Errorf("error deriving private key: %v", err) + } + + // The context keeps track of all signing keys and our local key. + allOpts := append( + []musig2.ContextOption{ + musig2.WithKnownSigners(allSignerPubKeys), + }, + tweaks.ToContextOptions()..., + ) + musigContext, err := musig2.NewContext(privKey, true, allOpts...) + if err != nil { + return nil, fmt.Errorf("error creating MuSig2 signing "+ + "context: %v", err) + } + + // The session keeps track of the own and other nonces. + musigSession, err := musigContext.NewSession() + if err != nil { + return nil, fmt.Errorf("error creating MuSig2 signing "+ + "session: %v", err) + } + + // Add all nonces we might've learned so far. + haveAllNonces := false + for _, otherSignerNonce := range otherSignerNonces { + haveAllNonces, err = musigSession.RegisterPubNonce( + otherSignerNonce, + ) + if err != nil { + return nil, fmt.Errorf("error registering other "+ + "signer public nonce: %v", err) + } + } + + // Register the new session. + combinedKey, err := musigContext.CombinedKey() + if err != nil { + return nil, fmt.Errorf("error getting combined key: %v", err) + } + session := &muSig2State{ + MuSig2SessionInfo: input.MuSig2SessionInfo{ + SessionID: input.NewMuSig2SessionID( + combinedKey, musigSession.PublicNonce(), + ), + PublicNonce: musigSession.PublicNonce(), + CombinedKey: combinedKey, + TaprootTweak: tweaks.HasTaprootTweak(), + HaveAllNonces: haveAllNonces, + }, + context: musigContext, + session: musigSession, + } + + // The internal key is only calculated if we are using a taproot tweak + // and need to know it for a potential script spend. + if tweaks.HasTaprootTweak() { + internalKey, err := musigContext.TaprootInternalKey() + if err != nil { + return nil, fmt.Errorf("error getting internal key: %v", + err) + } + session.TaprootInternalKey = internalKey + } + + // Since we generate new nonces for every session, there is no way that + // a session with the same ID already exists. So even if we call the API + // twice with the same signers, we still get a new ID. + b.musig2SessionsMtx.Lock() + b.musig2Sessions[session.SessionID] = session + b.musig2SessionsMtx.Unlock() + + return &session.MuSig2SessionInfo, nil +} + +// MuSig2RegisterNonces registers one or more public nonces of other signing +// participants for a session identified by its ID. This method returns true +// once we have all nonces for all other signing participants. +func (b *BtcWallet) MuSig2RegisterNonces(sessionID input.MuSig2SessionID, + otherSignerNonces [][musig2.PubNonceSize]byte) (bool, error) { + + // We hold the lock during the whole operation, we don't want any + // interference with calls that might come through in parallel for the + // same session. + b.musig2SessionsMtx.Lock() + defer b.musig2SessionsMtx.Unlock() + + session, ok := b.musig2Sessions[sessionID] + if !ok { + return false, fmt.Errorf("session with ID %x not found", + sessionID[:]) + } + + // Make sure we don't exceed the number of expected nonces as that would + // indicate something is wrong with the signing setup. + if session.HaveAllNonces { + return true, fmt.Errorf("already have all nonces") + } + + numSigners := len(session.context.SigningKeys()) + remainingNonces := numSigners - session.session.NumRegisteredNonces() + if len(otherSignerNonces) > remainingNonces { + return false, fmt.Errorf("only %d other nonces remaining but "+ + "trying to register %d more", remainingNonces, + len(otherSignerNonces)) + } + + // Add all nonces we've learned so far. + var err error + for _, otherSignerNonce := range otherSignerNonces { + session.HaveAllNonces, err = session.session.RegisterPubNonce( + otherSignerNonce, + ) + if err != nil { + return false, fmt.Errorf("error registering other "+ + "signer public nonce: %v", err) + } + } + + return session.HaveAllNonces, nil +} + +// MuSig2Sign creates a partial signature using the local signing key +// that was specified when the session was created. This can only be +// called when all public nonces of all participants are known and have +// been registered with the session. If this node isn't responsible for +// combining all the partial signatures, then the cleanup parameter +// should be set, indicating that the session can be removed from memory +// once the signature was produced. +func (b *BtcWallet) MuSig2Sign(sessionID input.MuSig2SessionID, + msg [sha256.Size]byte, cleanUp bool) (*musig2.PartialSignature, error) { + + // We hold the lock during the whole operation, we don't want any + // interference with calls that might come through in parallel for the + // same session. + b.musig2SessionsMtx.Lock() + defer b.musig2SessionsMtx.Unlock() + + session, ok := b.musig2Sessions[sessionID] + if !ok { + return nil, fmt.Errorf("session with ID %x not found", + sessionID[:]) + } + + // We can only sign once we have all other signer's nonces. + if !session.HaveAllNonces { + return nil, fmt.Errorf("only have %d of %d required nonces", + session.session.NumRegisteredNonces(), + len(session.context.SigningKeys())) + } + + // Create our own partial signature with the local signing key. + partialSig, err := session.session.Sign(msg, musig2.WithSortedKeys()) + if err != nil { + return nil, fmt.Errorf("error signing with local key: %v", err) + } + + // Clean up our local state if requested. + if cleanUp { + delete(b.musig2Sessions, sessionID) + } + + return partialSig, nil +} + +// MuSig2CombineSig combines the given partial signature(s) with the +// local one, if it already exists. Once a partial signature of all +// participants is registered, the final signature will be combined and +// returned. +func (b *BtcWallet) MuSig2CombineSig(sessionID input.MuSig2SessionID, + partialSigs []*musig2.PartialSignature) (*schnorr.Signature, bool, + error) { + + // We hold the lock during the whole operation, we don't want any + // interference with calls that might come through in parallel for the + // same session. + b.musig2SessionsMtx.Lock() + defer b.musig2SessionsMtx.Unlock() + + session, ok := b.musig2Sessions[sessionID] + if !ok { + return nil, false, fmt.Errorf("session with ID %x not found", + sessionID[:]) + } + + // Make sure we don't exceed the number of expected partial signatures + // as that would indicate something is wrong with the signing setup. + if session.HaveAllSigs { + return nil, true, fmt.Errorf("already have all partial" + + "signatures") + } + + // Add all sigs we got so far. + var ( + finalSig *schnorr.Signature + err error + ) + for _, otherPartialSig := range partialSigs { + session.HaveAllSigs, err = session.session.CombineSig( + otherPartialSig, + ) + if err != nil { + return nil, false, fmt.Errorf("error combining "+ + "partial signature: %v", err) + } + } + + // If we have all partial signatures, we should be able to get the + // complete signature now. We also remove this session from memory since + // there is nothing more left to do. + if session.HaveAllSigs { + finalSig = session.session.FinalSig() + delete(b.musig2Sessions, sessionID) + } + + return finalSig, session.HaveAllSigs, nil +} + // A compile time check to ensure that BtcWallet implements the Signer // interface. var _ input.Signer = (*BtcWallet)(nil)