From 9a65806c09316e8ab5764b7ffebb7473354400d8 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 18 Jan 2023 19:29:41 -0800 Subject: [PATCH] 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. --- contractcourt/breacharbiter_test.go | 8 +- htlcswitch/test_utils.go | 9 +- input/musig2.go | 34 ++-- input/musig2_session_manager.go | 298 ++++++++++++++++++++++++++++ input/test_utils.go | 91 +++------ lntest/mock/signer.go | 6 +- lnwallet/btcwallet/btcwallet.go | 28 +-- lnwallet/btcwallet/signer.go | 263 ------------------------ lnwallet/rpcwallet/rpcwallet.go | 5 +- lnwallet/test_utils.go | 4 +- lnwallet/transactions_test.go | 16 +- peer/test_utils.go | 8 +- watchtower/wtmock/signer.go | 3 +- 13 files changed, 402 insertions(+), 371 deletions(-) create mode 100644 input/musig2_session_manager.go diff --git a/contractcourt/breacharbiter_test.go b/contractcourt/breacharbiter_test.go index 263ab45ea..6d17135dc 100644 --- a/contractcourt/breacharbiter_test.go +++ b/contractcourt/breacharbiter_test.go @@ -2112,7 +2112,7 @@ func createTestArbiter(t *testing.T, contractBreaches chan *ContractBreachEvent, }) aliceKeyPriv, _ := btcec.PrivKeyFromBytes(channels.AlicesPrivKey) - signer := &mock.SingleSigner{Privkey: aliceKeyPriv} + signer := input.NewMockSigner([]*btcec.PrivateKey{aliceKeyPriv}, nil) // Assemble our test arbiter. notifier := mock.MakeMockSpendNotifier() @@ -2339,8 +2339,10 @@ func createInitChannels(t *testing.T, revocationWindow int) ( Packager: channeldb.NewChannelPackager(shortChanID), } - aliceSigner := &mock.SingleSigner{Privkey: aliceKeyPriv} - bobSigner := &mock.SingleSigner{Privkey: bobKeyPriv} + aliceSigner := input.NewMockSigner( + []*btcec.PrivateKey{aliceKeyPriv}, nil, + ) + bobSigner := input.NewMockSigner([]*btcec.PrivateKey{bobKeyPriv}, nil) alicePool := lnwallet.NewSigPool(1, aliceSigner) channelAlice, err := lnwallet.NewLightningChannel( diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index b03aff417..649570d2c 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -32,7 +32,6 @@ import ( "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lntest/channels" - "github.com/lightningnetwork/lnd/lntest/mock" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" @@ -337,8 +336,12 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte, return nil, nil, err } - aliceSigner := &mock.SingleSigner{Privkey: aliceKeyPriv} - bobSigner := &mock.SingleSigner{Privkey: bobKeyPriv} + aliceSigner := input.NewMockSigner( + []*btcec.PrivateKey{aliceKeyPriv}, nil, + ) + bobSigner := input.NewMockSigner( + []*btcec.PrivateKey{bobKeyPriv}, nil, + ) alicePool := lnwallet.NewSigPool(runtime.NumCPU(), aliceSigner) channelAlice, err := lnwallet.NewLightningChannel( diff --git a/input/musig2.go b/input/musig2.go index 1ae73d8ce..d2ff0c1d4 100644 --- a/input/musig2.go +++ b/input/musig2.go @@ -48,9 +48,13 @@ type MuSig2Signer interface { // 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. MuSig2CreateSession(MuSig2Version, keychain.KeyLocator, - []*btcec.PublicKey, *MuSig2Tweaks, - [][musig2.PubNonceSize]byte) (*MuSig2SessionInfo, error) + []*btcec.PublicKey, *MuSig2Tweaks, [][musig2.PubNonceSize]byte, + ...musig2.SessionOption) (*MuSig2SessionInfo, error) // MuSig2RegisterNonces registers one or more public nonces of other // 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. func MuSig2CreateContext(bipVersion MuSig2Version, privKey *btcec.PrivateKey, - allSignerPubKeys []*btcec.PublicKey, - tweaks *MuSig2Tweaks) (MuSig2Context, MuSig2Session, error) { + allSignerPubKeys []*btcec.PublicKey, tweaks *MuSig2Tweaks, + sessionOpts ...musig2.SessionOption, +) (MuSig2Context, MuSig2Session, error) { switch bipVersion { case MuSig2Version040: - return createContextV040(privKey, allSignerPubKeys, tweaks) + return createContextV040( + privKey, allSignerPubKeys, tweaks, sessionOpts..., + ) case MuSig2Version100RC2: - return createContextV100RC2(privKey, allSignerPubKeys, tweaks) + return createContextV100RC2( + privKey, allSignerPubKeys, tweaks, sessionOpts..., + ) default: 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 // BIP draft version 1.0.0rc2. func createContextV100RC2(privKey *btcec.PrivateKey, - allSignerPubKeys []*btcec.PublicKey, - tweaks *MuSig2Tweaks) (*musig2.Context, *musig2.Session, error) { + allSignerPubKeys []*btcec.PublicKey, tweaks *MuSig2Tweaks, + sessionOpts ...musig2.SessionOption, +) (*musig2.Context, *musig2.Session, error) { // The context keeps track of all signing keys and our local key. allOpts := append( @@ -409,7 +419,7 @@ func createContextV100RC2(privKey *btcec.PrivateKey, "context: %v", err) } - muSigSession, err := muSigContext.NewSession() + muSigSession, err := muSigContext.NewSession(sessionOpts...) if err != nil { return nil, nil, fmt.Errorf("error creating MuSig2 signing "+ "session: %v", err) @@ -421,9 +431,9 @@ func createContextV100RC2(privKey *btcec.PrivateKey, // createContextV040 implements the MuSig2CreateContext logic for the MuSig2 BIP // draft version 0.4.0. func createContextV040(privKey *btcec.PrivateKey, - allSignerPubKeys []*btcec.PublicKey, - tweaks *MuSig2Tweaks) (*musig2v040.Context, *musig2v040.Session, - error) { + allSignerPubKeys []*btcec.PublicKey, tweaks *MuSig2Tweaks, + sessionOpts ...musig2.SessionOption, +) (*musig2v040.Context, *musig2v040.Session, error) { // The context keeps track of all signing keys and our local key. allOpts := append( diff --git a/input/musig2_session_manager.go b/input/musig2_session_manager.go new file mode 100644 index 000000000..3572614fb --- /dev/null +++ b/input/musig2_session_manager.go @@ -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 +} diff --git a/input/test_utils.go b/input/test_utils.go index 35f372c43..682dfdcd4 100644 --- a/input/test_utils.go +++ b/input/test_utils.go @@ -2,14 +2,12 @@ package input import ( "bytes" - "crypto/sha256" "encoding/hex" "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/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -51,6 +49,26 @@ var ( type MockSigner struct { Privkeys []*btcec.PrivateKey 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 @@ -74,18 +92,14 @@ func (m *MockSigner) SignOutputRaw(tx *wire.MsgTx, // In case of a taproot output any signature is always a Schnorr // signature, based on the new tapscript sighash algorithm. - // - // TODO(roasbeef): should conslidate with btcwallet/signer.go if txscript.IsPayToTaproot(signDesc.Output.PkScript) { sigHashes := txscript.NewTxSigHashes( tx, signDesc.PrevOutputFetcher, ) - witnessScript := signDesc.WitnessScript - // 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 - // raw signature. + // slightly different, so we need to account for that to get + // the raw signature. var ( rawSig []byte err error @@ -109,7 +123,7 @@ func (m *MockSigner) SignOutputRaw(tx *wire.MsgTx, case TaprootScriptSpendSignMethod: leaf := txscript.TapLeaf{ LeafVersion: txscript.BaseLeafVersion, - Script: witnessScript, + Script: signDesc.WitnessScript, } rawSig, err = txscript.RawTxInTapscriptSignature( 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( 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 // 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 @@ -252,13 +221,15 @@ func (m *MockSigner) findKey(needleHash160 []byte, singleTweak []byte, doubleTweak *btcec.PrivateKey) *btcec.PrivateKey { 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()) if bytes.Equal(hash160, needleHash160) { 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 { case singleTweak != nil: privkey = TweakPrivKey(privkey, singleTweak) diff --git a/lntest/mock/signer.go b/lntest/mock/signer.go index ddcef3598..7ce6cf4e0 100644 --- a/lntest/mock/signer.go +++ b/lntest/mock/signer.go @@ -57,7 +57,8 @@ func (d *DummySigner) ComputeInputScript(tx *wire.MsgTx, // submitted as well to reduce the number of method calls necessary later on. func (d *DummySigner) MuSig2CreateSession(input.MuSig2Version, keychain.KeyLocator, []*btcec.PublicKey, *input.MuSig2Tweaks, - [][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, error) { + [][musig2.PubNonceSize]byte, + ...musig2.SessionOption) (*input.MuSig2SessionInfo, error) { 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. func (s *SingleSigner) MuSig2CreateSession(input.MuSig2Version, keychain.KeyLocator, []*btcec.PublicKey, *input.MuSig2Tweaks, - [][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, error) { + [][musig2.PubNonceSize]byte, + ...musig2.SessionOption) (*input.MuSig2SessionInfo, error) { return nil, nil } diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go index 1535d434b..ac7716ccd 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -104,8 +104,7 @@ type BtcWallet struct { blockCache *blockcache.BlockCache - musig2Sessions map[input.MuSig2SessionID]*muSig2State - musig2SessionsMtx sync.Mutex + *input.MusigSessionManager } // 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{ - cfg: &cfg, - wallet: wallet, - db: wallet.Database(), - chain: cfg.ChainSource, - netParams: cfg.NetParams, - chainKeyScope: chainKeyScope, - blockCache: blockCache, - musig2Sessions: make(map[input.MuSig2SessionID]*muSig2State), - }, nil + finalWallet := &BtcWallet{ + cfg: &cfg, + wallet: wallet, + db: wallet.Database(), + chain: cfg.ChainSource, + netParams: cfg.NetParams, + chainKeyScope: chainKeyScope, + blockCache: blockCache, + } + + finalWallet.MusigSessionManager = input.NewMusigSessionManager( + finalWallet.fetchPrivKey, + ) + + return finalWallet, nil } // loaderCfg holds optional wallet loader configuration. diff --git a/lnwallet/btcwallet/signer.go b/lnwallet/btcwallet/signer.go index da624c726..986416476 100644 --- a/lnwallet/btcwallet/signer.go +++ b/lnwallet/btcwallet/signer.go @@ -1,13 +1,11 @@ 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" @@ -469,267 +467,6 @@ 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 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 // interface. var _ input.Signer = (*BtcWallet)(nil) diff --git a/lnwallet/rpcwallet/rpcwallet.go b/lnwallet/rpcwallet/rpcwallet.go index 2dc9ae33c..92dfa6445 100644 --- a/lnwallet/rpcwallet/rpcwallet.go +++ b/lnwallet/rpcwallet/rpcwallet.go @@ -656,9 +656,8 @@ func (r *RPCKeyRing) ComputeInputScript(tx *wire.MsgTx, // submitted as well to reduce the number of method calls necessary later on. func (r *RPCKeyRing) MuSig2CreateSession(bipVersion input.MuSig2Version, keyLoc keychain.KeyLocator, pubKeys []*btcec.PublicKey, - tweaks *input.MuSig2Tweaks, - otherNonces [][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, - error) { + tweaks *input.MuSig2Tweaks, otherNonces [][musig2.PubNonceSize]byte, + sessionOpts ...musig2.SessionOption) (*input.MuSig2SessionInfo, error) { apiVersion, err := signrpc.MarshalMuSig2Version(bipVersion) if err != nil { diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go index f506ffa6a..d28511feb 100644 --- a/lnwallet/test_utils.go +++ b/lnwallet/test_utils.go @@ -343,8 +343,8 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType, Packager: channeldb.NewChannelPackager(shortChanID), } - aliceSigner := &input.MockSigner{Privkeys: aliceKeys} - bobSigner := &input.MockSigner{Privkeys: bobKeys} + aliceSigner := input.NewMockSigner(aliceKeys, nil) + bobSigner := input.NewMockSigner(bobKeys, nil) // TODO(roasbeef): make mock version of pre-image store diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index 92b22d5e0..e2263fccb 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -587,9 +587,9 @@ func testSpendValidation(t *testing.T, tweakless bool) { remoteCommitTweak := input.SingleTweakBytes(commitPoint, aliceKeyPub) localCommitTweak := input.SingleTweakBytes(commitPoint, bobKeyPub) - aliceSelfOutputSigner := &input.MockSigner{ - Privkeys: []*btcec.PrivateKey{aliceKeyPriv}, - } + aliceSelfOutputSigner := input.NewMockSigner( + []*btcec.PrivateKey{aliceKeyPriv}, nil, + ) // Calculate the dust limit we'll use for the test. 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) } - 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 // 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. - localSigner := &input.MockSigner{Privkeys: []*btcec.PrivateKey{ + localSigner := input.NewMockSigner([]*btcec.PrivateKey{ tc.localPaymentBasepointSecret, tc.localDelayedPaymentBasepointSecret, tc.localFundingPrivkey, localDummy1, localDummy2, - }} + }, nil) - remoteSigner := &input.MockSigner{Privkeys: []*btcec.PrivateKey{ + remoteSigner := input.NewMockSigner([]*btcec.PrivateKey{ tc.remoteFundingPrivkey, tc.remoteRevocationBasepointSecret, tc.remotePaymentBasepointSecret, remoteDummy1, remoteDummy2, - }} + }, nil) remotePool := NewSigPool(1, remoteSigner) channelRemote, err := NewLightningChannel( diff --git a/peer/test_utils.go b/peer/test_utils.go index d6c16b076..add15cf19 100644 --- a/peer/test_utils.go +++ b/peer/test_utils.go @@ -276,8 +276,12 @@ func createTestPeer(t *testing.T, notifier chainntnfs.ChainNotifier, return nil, nil, err } - aliceSigner := &mock.SingleSigner{Privkey: aliceKeyPriv} - bobSigner := &mock.SingleSigner{Privkey: bobKeyPriv} + aliceSigner := input.NewMockSigner( + []*btcec.PrivateKey{aliceKeyPriv}, nil, + ) + bobSigner := input.NewMockSigner( + []*btcec.PrivateKey{bobKeyPriv}, nil, + ) alicePool := lnwallet.NewSigPool(1, aliceSigner) channelAlice, err := lnwallet.NewLightningChannel( diff --git a/watchtower/wtmock/signer.go b/watchtower/wtmock/signer.go index 6454871ce..8b8056493 100644 --- a/watchtower/wtmock/signer.go +++ b/watchtower/wtmock/signer.go @@ -71,7 +71,8 @@ func (s *MockSigner) ComputeInputScript(tx *wire.MsgTx, // submitted as well to reduce the number of method calls necessary later on. func (s *MockSigner) MuSig2CreateSession(input.MuSig2Version, keychain.KeyLocator, []*btcec.PublicKey, *input.MuSig2Tweaks, - [][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, error) { + [][musig2.PubNonceSize]byte, + ...musig2.SessionOption) (*input.MuSig2SessionInfo, error) { return nil, nil }