input+wallet: extract musig2 session management into new module

In this commit, we extract the musig2 session management into a new
module. This allows us to re-use the session logic elsewhere in unit
tests so we don't need to instantiate the entire wallet.
This commit is contained in:
Olaoluwa Osuntokun 2023-01-18 19:29:41 -08:00
parent 4733da5ccb
commit 9a65806c09
No known key found for this signature in database
GPG key ID: 3BBD59E99B280306
13 changed files with 402 additions and 371 deletions

View file

@ -2112,7 +2112,7 @@ func createTestArbiter(t *testing.T, contractBreaches chan *ContractBreachEvent,
}) })
aliceKeyPriv, _ := btcec.PrivKeyFromBytes(channels.AlicesPrivKey) aliceKeyPriv, _ := btcec.PrivKeyFromBytes(channels.AlicesPrivKey)
signer := &mock.SingleSigner{Privkey: aliceKeyPriv} signer := input.NewMockSigner([]*btcec.PrivateKey{aliceKeyPriv}, nil)
// Assemble our test arbiter. // Assemble our test arbiter.
notifier := mock.MakeMockSpendNotifier() notifier := mock.MakeMockSpendNotifier()
@ -2339,8 +2339,10 @@ func createInitChannels(t *testing.T, revocationWindow int) (
Packager: channeldb.NewChannelPackager(shortChanID), Packager: channeldb.NewChannelPackager(shortChanID),
} }
aliceSigner := &mock.SingleSigner{Privkey: aliceKeyPriv} aliceSigner := input.NewMockSigner(
bobSigner := &mock.SingleSigner{Privkey: bobKeyPriv} []*btcec.PrivateKey{aliceKeyPriv}, nil,
)
bobSigner := input.NewMockSigner([]*btcec.PrivateKey{bobKeyPriv}, nil)
alicePool := lnwallet.NewSigPool(1, aliceSigner) alicePool := lnwallet.NewSigPool(1, aliceSigner)
channelAlice, err := lnwallet.NewLightningChannel( channelAlice, err := lnwallet.NewLightningChannel(

View file

@ -32,7 +32,6 @@ import (
"github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lnpeer"
"github.com/lightningnetwork/lnd/lntest/channels" "github.com/lightningnetwork/lnd/lntest/channels"
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
@ -337,8 +336,12 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte,
return nil, nil, err return nil, nil, err
} }
aliceSigner := &mock.SingleSigner{Privkey: aliceKeyPriv} aliceSigner := input.NewMockSigner(
bobSigner := &mock.SingleSigner{Privkey: bobKeyPriv} []*btcec.PrivateKey{aliceKeyPriv}, nil,
)
bobSigner := input.NewMockSigner(
[]*btcec.PrivateKey{bobKeyPriv}, nil,
)
alicePool := lnwallet.NewSigPool(runtime.NumCPU(), aliceSigner) alicePool := lnwallet.NewSigPool(runtime.NumCPU(), aliceSigner)
channelAlice, err := lnwallet.NewLightningChannel( channelAlice, err := lnwallet.NewLightningChannel(

View file

@ -48,9 +48,13 @@ type MuSig2Signer interface {
// public key of the local signing key. If nonces of other parties are // 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 // already known, they can be submitted as well to reduce the number of
// method calls necessary later on. // method calls necessary later on.
//
// The set of sessionOpts are _optional_ and allow a caller to modify
// the generated sessions. As an example the local nonce might already
// be generated ahead of time.
MuSig2CreateSession(MuSig2Version, keychain.KeyLocator, MuSig2CreateSession(MuSig2Version, keychain.KeyLocator,
[]*btcec.PublicKey, *MuSig2Tweaks, []*btcec.PublicKey, *MuSig2Tweaks, [][musig2.PubNonceSize]byte,
[][musig2.PubNonceSize]byte) (*MuSig2SessionInfo, error) ...musig2.SessionOption) (*MuSig2SessionInfo, error)
// MuSig2RegisterNonces registers one or more public nonces of other // MuSig2RegisterNonces registers one or more public nonces of other
// signing participants for a session identified by its ID. This method // signing participants for a session identified by its ID. This method
@ -374,15 +378,20 @@ func combineKeysV040(allSignerPubKeys []*btcec.PublicKey, sortKeys bool,
// MuSig2CreateContext creates a new MuSig2 signing context. // MuSig2CreateContext creates a new MuSig2 signing context.
func MuSig2CreateContext(bipVersion MuSig2Version, privKey *btcec.PrivateKey, func MuSig2CreateContext(bipVersion MuSig2Version, privKey *btcec.PrivateKey,
allSignerPubKeys []*btcec.PublicKey, allSignerPubKeys []*btcec.PublicKey, tweaks *MuSig2Tweaks,
tweaks *MuSig2Tweaks) (MuSig2Context, MuSig2Session, error) { sessionOpts ...musig2.SessionOption,
) (MuSig2Context, MuSig2Session, error) {
switch bipVersion { switch bipVersion {
case MuSig2Version040: case MuSig2Version040:
return createContextV040(privKey, allSignerPubKeys, tweaks) return createContextV040(
privKey, allSignerPubKeys, tweaks, sessionOpts...,
)
case MuSig2Version100RC2: case MuSig2Version100RC2:
return createContextV100RC2(privKey, allSignerPubKeys, tweaks) return createContextV100RC2(
privKey, allSignerPubKeys, tweaks, sessionOpts...,
)
default: default:
return nil, nil, fmt.Errorf("unknown MuSig2 version: <%d>", return nil, nil, fmt.Errorf("unknown MuSig2 version: <%d>",
@ -393,8 +402,9 @@ func MuSig2CreateContext(bipVersion MuSig2Version, privKey *btcec.PrivateKey,
// createContextV100RC2 implements the MuSig2CreateContext logic for the MuSig2 // createContextV100RC2 implements the MuSig2CreateContext logic for the MuSig2
// BIP draft version 1.0.0rc2. // BIP draft version 1.0.0rc2.
func createContextV100RC2(privKey *btcec.PrivateKey, func createContextV100RC2(privKey *btcec.PrivateKey,
allSignerPubKeys []*btcec.PublicKey, allSignerPubKeys []*btcec.PublicKey, tweaks *MuSig2Tweaks,
tweaks *MuSig2Tweaks) (*musig2.Context, *musig2.Session, error) { sessionOpts ...musig2.SessionOption,
) (*musig2.Context, *musig2.Session, error) {
// The context keeps track of all signing keys and our local key. // The context keeps track of all signing keys and our local key.
allOpts := append( allOpts := append(
@ -409,7 +419,7 @@ func createContextV100RC2(privKey *btcec.PrivateKey,
"context: %v", err) "context: %v", err)
} }
muSigSession, err := muSigContext.NewSession() muSigSession, err := muSigContext.NewSession(sessionOpts...)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("error creating MuSig2 signing "+ return nil, nil, fmt.Errorf("error creating MuSig2 signing "+
"session: %v", err) "session: %v", err)
@ -421,9 +431,9 @@ func createContextV100RC2(privKey *btcec.PrivateKey,
// createContextV040 implements the MuSig2CreateContext logic for the MuSig2 BIP // createContextV040 implements the MuSig2CreateContext logic for the MuSig2 BIP
// draft version 0.4.0. // draft version 0.4.0.
func createContextV040(privKey *btcec.PrivateKey, func createContextV040(privKey *btcec.PrivateKey,
allSignerPubKeys []*btcec.PublicKey, allSignerPubKeys []*btcec.PublicKey, tweaks *MuSig2Tweaks,
tweaks *MuSig2Tweaks) (*musig2v040.Context, *musig2v040.Session, sessionOpts ...musig2.SessionOption,
error) { ) (*musig2v040.Context, *musig2v040.Session, error) {
// The context keeps track of all signing keys and our local key. // The context keeps track of all signing keys and our local key.
allOpts := append( allOpts := append(

View file

@ -0,0 +1,298 @@
package input
import (
"crypto/sha256"
"fmt"
"sync"
"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"
)
// 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.
MuSig2SessionInfo
// context is the signing context responsible for keeping track of the
// public keys involved in the signing process.
context MuSig2Context
// session is the signing session responsible for keeping track of the
// nonces and partial signatures involved in the signing process.
session MuSig2Session
}
// PrivKeyFetcher is used to fetch a private key that matches a given key desc.
type PrivKeyFetcher func(*keychain.KeyDescriptor) (*btcec.PrivateKey, error)
// MusigSessionMusigSessionManager houses the state needed to manage concurrent
// musig sessions. Each session is identified by a unique session ID which is
// used by callers to interact with a given session.
type MusigSessionManager struct {
sync.Mutex
keyFetcher PrivKeyFetcher
musig2Sessions map[MuSig2SessionID]*MuSig2State
}
// NewMusigSessionManager creates a new musig manager given an abstract key
// fetcher.
func NewMusigSessionManager(keyFetcher PrivKeyFetcher) *MusigSessionManager {
return &MusigSessionManager{
keyFetcher: keyFetcher,
}
}
// 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.
//
// The set of sessionOpts are _optional_ and allow a caller to modify the
// generated sessions. As an example the local nonce might already be generated
// ahead of time.
func (m *MusigSessionManager) MuSig2CreateSession(bipVersion MuSig2Version,
keyLoc keychain.KeyLocator, allSignerPubKeys []*btcec.PublicKey,
tweaks *MuSig2Tweaks, otherSignerNonces [][musig2.PubNonceSize]byte,
sessionOpts ...musig2.SessionOption) (*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 := m.keyFetcher(&keychain.KeyDescriptor{
KeyLocator: keyLoc,
})
if err != nil {
return nil, fmt.Errorf("error deriving private key: %v", err)
}
// Create a signing context and session with the given private key and
// list of all known signer public keys.
musigContext, musigSession, err := MuSig2CreateContext(
bipVersion, privKey, allSignerPubKeys, tweaks,
)
if err != nil {
return nil, fmt.Errorf("error creating signing context: %w",
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: MuSig2SessionInfo{
SessionID: NewMuSig2SessionID(
combinedKey, musigSession.PublicNonce(),
),
Version: bipVersion,
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.
m.Lock()
m.musig2Sessions[session.SessionID] = session
m.Unlock()
return &session.MuSig2SessionInfo, 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 (m *MusigSessionManager) MuSig2Sign(sessionID 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.
m.Lock()
defer m.Unlock()
session, ok := m.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 := MuSig2Sign(session.session, msg, true)
if err != nil {
return nil, fmt.Errorf("error signing with local key: %w", err)
}
// Clean up our local state if requested.
if cleanUp {
delete(m.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 (m *MusigSessionManager) MuSig2CombineSig(sessionID 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.
m.Lock()
defer m.Unlock()
session, ok := m.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 = MuSig2CombineSig(
session.session, otherPartialSig,
)
if err != nil {
return nil, false, fmt.Errorf("error combining "+
"partial signature: %w", 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(m.musig2Sessions, sessionID)
}
return finalSig, session.HaveAllSigs, nil
}
// MuSig2Cleanup removes a session from memory to free up resources.
func (m *MusigSessionManager) MuSig2Cleanup(sessionID MuSig2SessionID) 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.
m.Lock()
defer m.Unlock()
_, ok := m.musig2Sessions[sessionID]
if !ok {
return fmt.Errorf("session with ID %x not found", sessionID[:])
}
delete(m.musig2Sessions, sessionID)
return 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 (m *MusigSessionManager) MuSig2RegisterNonces(sessionID 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.
m.Lock()
defer m.Unlock()
session, ok := m.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
}

View file

@ -2,14 +2,12 @@ package input
import ( import (
"bytes" "bytes"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "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"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
@ -51,6 +49,26 @@ var (
type MockSigner struct { type MockSigner struct {
Privkeys []*btcec.PrivateKey Privkeys []*btcec.PrivateKey
NetParams *chaincfg.Params NetParams *chaincfg.Params
*MusigSessionManager
}
// NewMockSigner returns a new instance of the MockSigner given a set of
// backing private keys.
func NewMockSigner(privKeys []*btcec.PrivateKey,
netParams *chaincfg.Params) *MockSigner {
signer := &MockSigner{
Privkeys: privKeys,
NetParams: netParams,
}
keyFetcher := func(*keychain.KeyDescriptor) (*btcec.PrivateKey, error) {
return signer.Privkeys[0], nil
}
signer.MusigSessionManager = NewMusigSessionManager(keyFetcher)
return signer
} }
// SignOutputRaw generates a signature for the passed transaction according to // SignOutputRaw generates a signature for the passed transaction according to
@ -74,18 +92,14 @@ func (m *MockSigner) SignOutputRaw(tx *wire.MsgTx,
// In case of a taproot output any signature is always a Schnorr // In case of a taproot output any signature is always a Schnorr
// signature, based on the new tapscript sighash algorithm. // signature, based on the new tapscript sighash algorithm.
//
// TODO(roasbeef): should conslidate with btcwallet/signer.go
if txscript.IsPayToTaproot(signDesc.Output.PkScript) { if txscript.IsPayToTaproot(signDesc.Output.PkScript) {
sigHashes := txscript.NewTxSigHashes( sigHashes := txscript.NewTxSigHashes(
tx, signDesc.PrevOutputFetcher, tx, signDesc.PrevOutputFetcher,
) )
witnessScript := signDesc.WitnessScript
// Are we spending a script path or the key path? The API is // Are we spending a script path or the key path? The API is
// slightly different, so we need to account for that to get the // slightly different, so we need to account for that to get
// raw signature. // the raw signature.
var ( var (
rawSig []byte rawSig []byte
err error err error
@ -109,7 +123,7 @@ func (m *MockSigner) SignOutputRaw(tx *wire.MsgTx,
case TaprootScriptSpendSignMethod: case TaprootScriptSpendSignMethod:
leaf := txscript.TapLeaf{ leaf := txscript.TapLeaf{
LeafVersion: txscript.BaseLeafVersion, LeafVersion: txscript.BaseLeafVersion,
Script: witnessScript, Script: signDesc.WitnessScript,
} }
rawSig, err = txscript.RawTxInTapscriptSignature( rawSig, err = txscript.RawTxInTapscriptSignature(
tx, sigHashes, signDesc.InputIndex, tx, sigHashes, signDesc.InputIndex,
@ -121,6 +135,10 @@ func (m *MockSigner) SignOutputRaw(tx *wire.MsgTx,
} }
} }
// The signature returned above might have a sighash flag
// attached if a non-default type was used. We'll slice this
// off if it exists to ensure we can properly parse the raw
// signature.
sig, err := schnorr.ParseSignature( sig, err := schnorr.ParseSignature(
rawSig[:schnorr.SignatureSize], rawSig[:schnorr.SignatureSize],
) )
@ -195,55 +213,6 @@ func (m *MockSigner) ComputeInputScript(tx *wire.MsgTx, signDesc *SignDescriptor
} }
} }
// 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 (m *MockSigner) MuSig2CreateSession(MuSig2Version, keychain.KeyLocator,
[]*btcec.PublicKey, *MuSig2Tweaks,
[][musig2.PubNonceSize]byte) (*MuSig2SessionInfo, error) {
return nil, 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 (m *MockSigner) MuSig2RegisterNonces(MuSig2SessionID,
[][musig2.PubNonceSize]byte) (bool, error) {
return false, 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 (m *MockSigner) MuSig2Sign(MuSig2SessionID,
[sha256.Size]byte, bool) (*musig2.PartialSignature, error) {
return nil, 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 (m *MockSigner) MuSig2CombineSig(MuSig2SessionID,
[]*musig2.PartialSignature) (*schnorr.Signature, bool, error) {
return nil, false, nil
}
// MuSig2Cleanup removes a session from memory to free up resources.
func (m *MockSigner) MuSig2Cleanup(MuSig2SessionID) error {
return nil
}
// findKey searches through all stored private keys and returns one // findKey searches through all stored private keys and returns one
// corresponding to the hashed pubkey if it can be found. The public key may // corresponding to the hashed pubkey if it can be found. The public key may
// either correspond directly to the private key or to the private key with a // either correspond directly to the private key or to the private key with a
@ -252,13 +221,15 @@ func (m *MockSigner) findKey(needleHash160 []byte, singleTweak []byte,
doubleTweak *btcec.PrivateKey) *btcec.PrivateKey { doubleTweak *btcec.PrivateKey) *btcec.PrivateKey {
for _, privkey := range m.Privkeys { for _, privkey := range m.Privkeys {
// First check whether public key is directly derived from private key. // First check whether public key is directly derived from
// private key.
hash160 := btcutil.Hash160(privkey.PubKey().SerializeCompressed()) hash160 := btcutil.Hash160(privkey.PubKey().SerializeCompressed())
if bytes.Equal(hash160, needleHash160) { if bytes.Equal(hash160, needleHash160) {
return privkey return privkey
} }
// Otherwise check if public key is derived from tweaked private key. // Otherwise check if public key is derived from tweaked
// private key.
switch { switch {
case singleTweak != nil: case singleTweak != nil:
privkey = TweakPrivKey(privkey, singleTweak) privkey = TweakPrivKey(privkey, singleTweak)

View file

@ -57,7 +57,8 @@ func (d *DummySigner) ComputeInputScript(tx *wire.MsgTx,
// submitted as well to reduce the number of method calls necessary later on. // submitted as well to reduce the number of method calls necessary later on.
func (d *DummySigner) MuSig2CreateSession(input.MuSig2Version, func (d *DummySigner) MuSig2CreateSession(input.MuSig2Version,
keychain.KeyLocator, []*btcec.PublicKey, *input.MuSig2Tweaks, keychain.KeyLocator, []*btcec.PublicKey, *input.MuSig2Tweaks,
[][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, error) { [][musig2.PubNonceSize]byte,
...musig2.SessionOption) (*input.MuSig2SessionInfo, error) {
return nil, nil return nil, nil
} }
@ -196,7 +197,8 @@ func (s *SingleSigner) SignMessage(keyLoc keychain.KeyLocator,
// submitted as well to reduce the number of method calls necessary later on. // submitted as well to reduce the number of method calls necessary later on.
func (s *SingleSigner) MuSig2CreateSession(input.MuSig2Version, func (s *SingleSigner) MuSig2CreateSession(input.MuSig2Version,
keychain.KeyLocator, []*btcec.PublicKey, *input.MuSig2Tweaks, keychain.KeyLocator, []*btcec.PublicKey, *input.MuSig2Tweaks,
[][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, error) { [][musig2.PubNonceSize]byte,
...musig2.SessionOption) (*input.MuSig2SessionInfo, error) {
return nil, nil return nil, nil
} }

View file

@ -104,8 +104,7 @@ type BtcWallet struct {
blockCache *blockcache.BlockCache blockCache *blockcache.BlockCache
musig2Sessions map[input.MuSig2SessionID]*muSig2State *input.MusigSessionManager
musig2SessionsMtx sync.Mutex
} }
// A compile time check to ensure that BtcWallet implements the // A compile time check to ensure that BtcWallet implements the
@ -167,16 +166,21 @@ func New(cfg Config, blockCache *blockcache.BlockCache) (*BtcWallet, error) {
} }
} }
return &BtcWallet{ finalWallet := &BtcWallet{
cfg: &cfg, cfg: &cfg,
wallet: wallet, wallet: wallet,
db: wallet.Database(), db: wallet.Database(),
chain: cfg.ChainSource, chain: cfg.ChainSource,
netParams: cfg.NetParams, netParams: cfg.NetParams,
chainKeyScope: chainKeyScope, chainKeyScope: chainKeyScope,
blockCache: blockCache, blockCache: blockCache,
musig2Sessions: make(map[input.MuSig2SessionID]*muSig2State), }
}, nil
finalWallet.MusigSessionManager = input.NewMusigSessionManager(
finalWallet.fetchPrivKey,
)
return finalWallet, nil
} }
// loaderCfg holds optional wallet loader configuration. // loaderCfg holds optional wallet loader configuration.

View file

@ -1,13 +1,11 @@
package btcwallet package btcwallet
import ( import (
"crypto/sha256"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "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"
"github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
@ -469,267 +467,6 @@ func (b *BtcWallet) ComputeInputScript(tx *wire.MsgTx,
}, nil }, 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 input.MuSig2Context
// session is the signing session responsible for keeping track of the
// nonces and partial signatures involved in the signing process.
session input.MuSig2Session
}
// 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(bipVersion input.MuSig2Version,
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: %w", err)
}
// Create a signing context and session with the given private key and
// list of all known signer public keys.
muSigContext, muSigSession, err := input.MuSig2CreateContext(
bipVersion, privKey, allSignerPubKeys, tweaks,
)
if err != nil {
return nil, fmt.Errorf("error creating signing context: %w",
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: %w", err)
}
}
// Register the new session.
combinedKey, err := muSigContext.CombinedKey()
if err != nil {
return nil, fmt.Errorf("error getting combined key: %w", err)
}
session := &muSig2State{
MuSig2SessionInfo: input.MuSig2SessionInfo{
SessionID: input.NewMuSig2SessionID(
combinedKey, muSigSession.PublicNonce(),
),
Version: bipVersion,
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: %w",
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: %w", 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 := input.MuSig2Sign(session.session, msg, true)
if err != nil {
return nil, fmt.Errorf("error signing with local key: %w", 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 = input.MuSig2CombineSig(
session.session, otherPartialSig,
)
if err != nil {
return nil, false, fmt.Errorf("error combining "+
"partial signature: %w", 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
}
// MuSig2Cleanup removes a session from memory to free up resources.
func (b *BtcWallet) MuSig2Cleanup(sessionID input.MuSig2SessionID) 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()
_, ok := b.musig2Sessions[sessionID]
if !ok {
return fmt.Errorf("session with ID %x not found", sessionID[:])
}
delete(b.musig2Sessions, sessionID)
return nil
}
// A compile time check to ensure that BtcWallet implements the Signer // A compile time check to ensure that BtcWallet implements the Signer
// interface. // interface.
var _ input.Signer = (*BtcWallet)(nil) var _ input.Signer = (*BtcWallet)(nil)

View file

@ -656,9 +656,8 @@ func (r *RPCKeyRing) ComputeInputScript(tx *wire.MsgTx,
// submitted as well to reduce the number of method calls necessary later on. // submitted as well to reduce the number of method calls necessary later on.
func (r *RPCKeyRing) MuSig2CreateSession(bipVersion input.MuSig2Version, func (r *RPCKeyRing) MuSig2CreateSession(bipVersion input.MuSig2Version,
keyLoc keychain.KeyLocator, pubKeys []*btcec.PublicKey, keyLoc keychain.KeyLocator, pubKeys []*btcec.PublicKey,
tweaks *input.MuSig2Tweaks, tweaks *input.MuSig2Tweaks, otherNonces [][musig2.PubNonceSize]byte,
otherNonces [][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, sessionOpts ...musig2.SessionOption) (*input.MuSig2SessionInfo, error) {
error) {
apiVersion, err := signrpc.MarshalMuSig2Version(bipVersion) apiVersion, err := signrpc.MarshalMuSig2Version(bipVersion)
if err != nil { if err != nil {

View file

@ -343,8 +343,8 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType,
Packager: channeldb.NewChannelPackager(shortChanID), Packager: channeldb.NewChannelPackager(shortChanID),
} }
aliceSigner := &input.MockSigner{Privkeys: aliceKeys} aliceSigner := input.NewMockSigner(aliceKeys, nil)
bobSigner := &input.MockSigner{Privkeys: bobKeys} bobSigner := input.NewMockSigner(bobKeys, nil)
// TODO(roasbeef): make mock version of pre-image store // TODO(roasbeef): make mock version of pre-image store

View file

@ -587,9 +587,9 @@ func testSpendValidation(t *testing.T, tweakless bool) {
remoteCommitTweak := input.SingleTweakBytes(commitPoint, aliceKeyPub) remoteCommitTweak := input.SingleTweakBytes(commitPoint, aliceKeyPub)
localCommitTweak := input.SingleTweakBytes(commitPoint, bobKeyPub) localCommitTweak := input.SingleTweakBytes(commitPoint, bobKeyPub)
aliceSelfOutputSigner := &input.MockSigner{ aliceSelfOutputSigner := input.NewMockSigner(
Privkeys: []*btcec.PrivateKey{aliceKeyPriv}, []*btcec.PrivateKey{aliceKeyPriv}, nil,
} )
// Calculate the dust limit we'll use for the test. // Calculate the dust limit we'll use for the test.
dustLimit := DustLimitForSize(input.UnknownWitnessSize) dustLimit := DustLimitForSize(input.UnknownWitnessSize)
@ -679,7 +679,7 @@ func testSpendValidation(t *testing.T, tweakless bool) {
t.Fatalf("spend from delay output is invalid: %v", err) t.Fatalf("spend from delay output is invalid: %v", err)
} }
localSigner := &input.MockSigner{Privkeys: []*btcec.PrivateKey{bobKeyPriv}} localSigner := input.NewMockSigner([]*btcec.PrivateKey{bobKeyPriv}, nil)
// Next, we'll test bob spending with the derived revocation key to // Next, we'll test bob spending with the derived revocation key to
// simulate the scenario when Alice broadcasts this commitment // simulate the scenario when Alice broadcasts this commitment
@ -994,15 +994,15 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp
} }
// Create mock signers that can sign for the keys that are used. // Create mock signers that can sign for the keys that are used.
localSigner := &input.MockSigner{Privkeys: []*btcec.PrivateKey{ localSigner := input.NewMockSigner([]*btcec.PrivateKey{
tc.localPaymentBasepointSecret, tc.localDelayedPaymentBasepointSecret, tc.localPaymentBasepointSecret, tc.localDelayedPaymentBasepointSecret,
tc.localFundingPrivkey, localDummy1, localDummy2, tc.localFundingPrivkey, localDummy1, localDummy2,
}} }, nil)
remoteSigner := &input.MockSigner{Privkeys: []*btcec.PrivateKey{ remoteSigner := input.NewMockSigner([]*btcec.PrivateKey{
tc.remoteFundingPrivkey, tc.remoteRevocationBasepointSecret, tc.remoteFundingPrivkey, tc.remoteRevocationBasepointSecret,
tc.remotePaymentBasepointSecret, remoteDummy1, remoteDummy2, tc.remotePaymentBasepointSecret, remoteDummy1, remoteDummy2,
}} }, nil)
remotePool := NewSigPool(1, remoteSigner) remotePool := NewSigPool(1, remoteSigner)
channelRemote, err := NewLightningChannel( channelRemote, err := NewLightningChannel(

View file

@ -276,8 +276,12 @@ func createTestPeer(t *testing.T, notifier chainntnfs.ChainNotifier,
return nil, nil, err return nil, nil, err
} }
aliceSigner := &mock.SingleSigner{Privkey: aliceKeyPriv} aliceSigner := input.NewMockSigner(
bobSigner := &mock.SingleSigner{Privkey: bobKeyPriv} []*btcec.PrivateKey{aliceKeyPriv}, nil,
)
bobSigner := input.NewMockSigner(
[]*btcec.PrivateKey{bobKeyPriv}, nil,
)
alicePool := lnwallet.NewSigPool(1, aliceSigner) alicePool := lnwallet.NewSigPool(1, aliceSigner)
channelAlice, err := lnwallet.NewLightningChannel( channelAlice, err := lnwallet.NewLightningChannel(

View file

@ -71,7 +71,8 @@ func (s *MockSigner) ComputeInputScript(tx *wire.MsgTx,
// submitted as well to reduce the number of method calls necessary later on. // submitted as well to reduce the number of method calls necessary later on.
func (s *MockSigner) MuSig2CreateSession(input.MuSig2Version, func (s *MockSigner) MuSig2CreateSession(input.MuSig2Version,
keychain.KeyLocator, []*btcec.PublicKey, *input.MuSig2Tweaks, keychain.KeyLocator, []*btcec.PublicKey, *input.MuSig2Tweaks,
[][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, error) { [][musig2.PubNonceSize]byte,
...musig2.SessionOption) (*input.MuSig2SessionInfo, error) {
return nil, nil return nil, nil
} }