mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-09-08 23:01:53 +02:00
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.
This commit is contained in:
@@ -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)
|
||||
|
Reference in New Issue
Block a user