From a2f7350237056a4fdf15359d5cda4d5544551225 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 18 Jan 2023 19:29:41 -0800 Subject: [PATCH 1/6] 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 263ab45ea0a..6d17135dc5a 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 e1bd6783af9..a94ea9d7660 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -31,7 +31,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" @@ -336,8 +335,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 1ae73d8ce37..d2ff0c1d416 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 00000000000..3572614fb85 --- /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 35f372c4341..682dfdcd4e8 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 ddcef3598b4..7ce6cf4e060 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 8305dbb55f9..2c6f9c012ef 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -109,8 +109,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 @@ -172,16 +171,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 da624c72659..98641647690 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 5ccffbef960..d40dc338ca0 100644 --- a/lnwallet/rpcwallet/rpcwallet.go +++ b/lnwallet/rpcwallet/rpcwallet.go @@ -655,9 +655,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 f506ffa6a06..d28511feb63 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 b51a3f903b1..329366bbb30 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -534,9 +534,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) @@ -626,7 +626,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 @@ -941,15 +941,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 a997411df7d..bf61640f9ee 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 6454871ceb4..8b805649373 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 } From 02a0671dd6342327db8d02a8c30f7e672373d5a4 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 18 Jan 2023 19:33:50 -0800 Subject: [PATCH 2/6] lnwallet/chanfunding: update assemblers to support musig2 In this commit, we update the set of intents and assemblers to recognize musig2. For this change, we use a new bool, `musig2`, then use that to determine if we need to use the new taproot funding scripts or not. --- lnwallet/chanfunding/assembler.go | 5 ++++ lnwallet/chanfunding/canned_assembler.go | 29 +++++++++++++++++++++++- lnwallet/chanfunding/psbt_assembler.go | 1 + lnwallet/chanfunding/wallet_assembler.go | 1 + lnwallet/test/test_interface.go | 4 ++-- rpcserver.go | 4 +++- 6 files changed, 40 insertions(+), 4 deletions(-) diff --git a/lnwallet/chanfunding/assembler.go b/lnwallet/chanfunding/assembler.go index 4e6e62d259f..f569b42c83e 100644 --- a/lnwallet/chanfunding/assembler.go +++ b/lnwallet/chanfunding/assembler.go @@ -78,6 +78,11 @@ type Request struct { // ChangeAddr is a closure that will provide the Assembler with a // change address for the funding transaction if needed. ChangeAddr func() (btcutil.Address, error) + + // Musig2 if true, then musig2 will be used to generate the funding + // output. By definition, this'll also use segwit v1 (taproot) for the + // funding output. + Musig2 bool } // Intent is returned by an Assembler and represents the base functionality the diff --git a/lnwallet/chanfunding/canned_assembler.go b/lnwallet/chanfunding/canned_assembler.go index 603d90fe273..cb9472f5172 100644 --- a/lnwallet/chanfunding/canned_assembler.go +++ b/lnwallet/chanfunding/canned_assembler.go @@ -35,6 +35,11 @@ type ShimIntent struct { // a normal channel. Until this height, it's considered frozen, so it // can only be cooperatively closed by the responding party. thawHeight uint32 + + // musig2 determines if the funding output should use musig2 to + // generate an aggregate key to use as the taproot-native multi-sig + // output. + musig2 bool } // FundingOutput returns the witness script, and the output that creates the @@ -48,6 +53,19 @@ func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) { } totalAmt := s.localFundingAmt + s.remoteFundingAmt + + // If musig2 is active, then we'll return a single aggregated key + // rather than using the "existing" funding script. + if s.musig2 { + // Similar to the existing p2wsh script, we'll always ensure + // the keys are sorted before use. + return input.GenTaprootFundingScript( + s.localKey.PubKey, + s.remoteKey, + int64(totalAmt), + ) + } + return input.GenFundingPkScript( s.localKey.PubKey.SerializeCompressed(), s.remoteKey.SerializeCompressed(), @@ -171,13 +189,20 @@ type CannedAssembler struct { // a normal channel. Until this height, it's considered frozen, so it // can only be cooperatively closed by the responding party. thawHeight uint32 + + // musig2 determines if the funding output should use musig2 to + // generate an aggregate key to use as the taproot-native multi-sig + // output. + musig2 bool } // NewCannedAssembler creates a new CannedAssembler from the material required // to construct a funding output and channel point. +// +// TODO(roasbeef): pass in chan type instead? func NewCannedAssembler(thawHeight uint32, chanPoint wire.OutPoint, fundingAmt btcutil.Amount, localKey *keychain.KeyDescriptor, - remoteKey *btcec.PublicKey, initiator bool) *CannedAssembler { + remoteKey *btcec.PublicKey, initiator, musig2 bool) *CannedAssembler { return &CannedAssembler{ initiator: initiator, @@ -186,6 +211,7 @@ func NewCannedAssembler(thawHeight uint32, chanPoint wire.OutPoint, fundingAmt: fundingAmt, chanPoint: chanPoint, thawHeight: thawHeight, + musig2: musig2, } } @@ -207,6 +233,7 @@ func (c *CannedAssembler) ProvisionChannel(req *Request) (Intent, error) { remoteKey: c.remoteKey, chanPoint: &c.chanPoint, thawHeight: c.thawHeight, + musig2: c.musig2, } if c.initiator { diff --git a/lnwallet/chanfunding/psbt_assembler.go b/lnwallet/chanfunding/psbt_assembler.go index 8632b175d91..5f0bb88e7b8 100644 --- a/lnwallet/chanfunding/psbt_assembler.go +++ b/lnwallet/chanfunding/psbt_assembler.go @@ -523,6 +523,7 @@ func (p *PsbtAssembler) ProvisionChannel(req *Request) (Intent, error) { intent := &PsbtIntent{ ShimIntent: ShimIntent{ localFundingAmt: p.fundingAmt, + musig2: req.Musig2, }, State: PsbtShimRegistered, BasePsbt: p.basePsbt, diff --git a/lnwallet/chanfunding/wallet_assembler.go b/lnwallet/chanfunding/wallet_assembler.go index c81c1cb53c3..b78ea0deb5a 100644 --- a/lnwallet/chanfunding/wallet_assembler.go +++ b/lnwallet/chanfunding/wallet_assembler.go @@ -353,6 +353,7 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) { ShimIntent: ShimIntent{ localFundingAmt: localContributionAmt, remoteFundingAmt: r.RemoteAmt, + musig2: r.Musig2, }, InputCoins: selectedCoins, coinLocker: w.cfg.CoinLocker, diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go index 737f1d417ae..e27e12ded9d 100644 --- a/lnwallet/test/test_interface.go +++ b/lnwallet/test/test_interface.go @@ -2974,11 +2974,11 @@ func testSingleFunderExternalFundingTx(miner *rpctest.Harness, thawHeight := uint32(200) aliceExternalFunder := chanfunding.NewCannedAssembler( thawHeight, *chanPoint, btcutil.Amount(chanAmt), &aliceFundingKey, - bobFundingKey.PubKey, true, + bobFundingKey.PubKey, true, false, ) bobShimIntent, err := chanfunding.NewCannedAssembler( thawHeight, *chanPoint, btcutil.Amount(chanAmt), &bobFundingKey, - aliceFundingKey.PubKey, false, + aliceFundingKey.PubKey, false, false, ).ProvisionChannel(&chanfunding.Request{ LocalAmt: btcutil.Amount(chanAmt), MinConfs: 1, diff --git a/rpcserver.go b/rpcserver.go index cc8b95e06c2..18e576cefe6 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1847,10 +1847,12 @@ func newFundingShimAssembler(chanPointShim *lnrpc.ChanPointShim, initiator bool, // With all the parts assembled, we can now make the canned assembler // to pass into the wallet. + // + // TODO(roasbeef): update to support musig2 return chanfunding.NewCannedAssembler( chanPointShim.ThawHeight, *chanPoint, btcutil.Amount(chanPointShim.Amt), &localKeyDesc, - remoteKey, initiator, + remoteKey, initiator, false, ), nil } From 3b178e18df4b3a4e46b6391de1a9dd8aaf74f579 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 18 Jan 2023 19:57:26 -0800 Subject: [PATCH 3/6] lnwallet: add new structs to abstract over the asymmetric musig commitment state In this commit, we add a series of abstractions that'll allow us to easily do funding and also state updates for the new taproot channels. A partial session is defined by the knowledge of a verification nonce. Once the remote party sends a signature, we learn of their signing nonce, and can then complete a session. By using a JIT nonce approach, we ensure that the signer can generate their nonces randomly and also at the very last step to avoid having to maintain state. For our local nonces, we also have an option to use a counter based nonce derived from the shachain instead of fully random nonces. This allows us to not have to store ay additional state. Instead, when we need to go to broadcast, we can just regenerate the nonce then use that to broadcast. --- lnwallet/musig_session.go | 547 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 547 insertions(+) create mode 100644 lnwallet/musig_session.go diff --git a/lnwallet/musig_session.go b/lnwallet/musig_session.go new file mode 100644 index 00000000000..7683eca962e --- /dev/null +++ b/lnwallet/musig_session.go @@ -0,0 +1,547 @@ +package lnwallet + +import ( + "bytes" + "fmt" + "io" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/shachain" +) + +// commitType is an enum that denotes if this is the local or remote +// commitment. +type commitType uint8 + +const ( + // localCommit denotes that this a session for the local commitment. + localCommit commitType = iota + + // remoteCommit denotes that this is a session for the remote + // commitment. + remoteCommit +) + +var ( + // ErrSessionNotFinalized is returned when the SignCommit method is + // called for a local commitment, without the session being finalized + // (missing nonce). + ErrSessionNotFinalized = fmt.Errorf("musig2 session not finalized") +) + +// MusigPartialSig is a wrapper around the base musig2.PartialSignature type +// that also includes information about the set of nonces used, and also the +// signer. This allows us to implement the input.Signature interface, as that +// requires the ability to perform abstract verification based on a public key. +type MusigPartialSig struct { + // sig is the actual musig2 partial signature. + sig *musig2.PartialSignature + + // signerNonce is the nonce used by the signer to generate the partial + // signature. + signerNonce lnwire.Musig2Nonce + + // combinedNonce is the combined nonce of all signers. + combinedNonce lnwire.Musig2Nonce + + // signerKeys is the set of public keys of all signers. + signerKeys []*btcec.PublicKey +} + +// NewMusigPartialSig creates a new musig partial signature. +func NewMusigPartialSig(sig *musig2.PartialSignature, + signerNonce, combinedNonce lnwire.Musig2Nonce, + signerKeys []*btcec.PublicKey) *MusigPartialSig { + + return &MusigPartialSig{ + sig: sig, + signerNonce: signerNonce, + combinedNonce: combinedNonce, + signerKeys: signerKeys, + } +} + +// FromWireSig maps a wire partial sig to this internal type that we'll use to +// perform signature validation. +func (p *MusigPartialSig) FromWireSig(sig *lnwire.PartialSigWithNonce, +) *MusigPartialSig { + + p.sig = &musig2.PartialSignature{ + S: &sig.Sig, + } + p.signerNonce = sig.Nonce + + return p +} + +// ToWireSig maps the partial signature to something that we can use to write +// out for the wire protocol. +func (p *MusigPartialSig) ToWireSig() *lnwire.PartialSigWithNonce { + return &lnwire.PartialSigWithNonce{ + PartialSig: lnwire.NewPartialSig(*p.sig.S), + Nonce: p.signerNonce, + } +} + +// Serialize serializes the musig2 partial signature. The serializing includes +// the signer's public nonce _and_ the partial signature. The final signature +// is always 98 bytes in length. +func (p *MusigPartialSig) Serialize() []byte { + var b bytes.Buffer + + _ = p.ToWireSig().Encode(&b) + + return b.Bytes() +} + +// ToSchnorrShell converts the musig partial signature to a regular schnorr. +// This schnorr signature uses a zero value for the 'r' field, so we're just +// only using the last 32-bytes of the signature. This is useful when we need +// to convert an HTLC schnorr signature into something we can send using the +// existing messages. +func (p *MusigPartialSig) ToSchnorrShell() *schnorr.Signature { + var zeroVal btcec.FieldVal + return schnorr.NewSignature(&zeroVal, p.sig.S) +} + +// FromSchnorrShell takes a schnorr signature and parses out the last 32 bytes +// as a normal musig2 partial signature. +func (p *MusigPartialSig) FromSchnorrShell(sig *schnorr.Signature) { + var ( + partialS btcec.ModNScalar + partialSBytes [32]byte + ) + copy(partialSBytes[:], sig.Serialize()[32:]) + partialS.SetBytes(&partialSBytes) + + p.sig = &musig2.PartialSignature{ + S: &partialS, + } +} + +// Verify attempts to verify the partial musig2 signature using the passed +// message and signer public key. +// +// NOTE: This implements the input.Signature interface. +func (p *MusigPartialSig) Verify(msg []byte, pub *btcec.PublicKey) bool { + var m [32]byte + copy(m[:], msg) + + return p.sig.Verify( + p.signerNonce, p.combinedNonce, p.signerKeys, pub, m, + musig2.WithSortedKeys(), musig2.WithBip86SignTweak(), + ) +} + +// MusigNoncePair holds the two nonces needed to sign/verify a new commitment +// state. The signer nonce is the nonce used by the signer (remote nonce), and +// the verification nonce, the nonce used by the verifier (local nonce). +type MusigNoncePair struct { + // SigningNonce is the nonce used by the signer to sign the commitment. + SigningNonce musig2.Nonces + + // VerificationNonce is the nonce used by the verifier to verify the + // commitment. + VerificationNonce musig2.Nonces +} + +// String returns a string representation of the MusigNoncePair. +func (n *MusigNoncePair) String() string { + return fmt.Sprintf("NoncePair(verification_nonce=%x, "+ + "signing_nonce=%x)", n.VerificationNonce.PubNonce[:], + n.SigningNonce.PubNonce[:]) +} + +// MusigSession abstracts over the details of a logical musig session. A single +// session is used for each commitment transactions. The sessions use a JIT +// nonce style, wherein part of the session can be created using only the +// verifier nonce. Once a new state is signed, then the signer nonce is +// generated. Similarly, the verifier then uses the received signer nonce to +// complete the session and verify the incoming signature. +type MusigSession struct { + // session is the backing musig2 session. We'll use this to interact + // with the musig2 signer. + session *input.MuSig2SessionInfo + + // combinedNonce is the combined nonce of all signers. + combinedNonce lnwire.Musig2Nonce + + // nonces is the set of nonces that'll be used to generate/verify the + // next commitment. + nonces MusigNoncePair + + // inputTxOut is the funding input. + inputTxOut *wire.TxOut + + // signerKeys is the set of public keys of all signers. + signerKeys []*btcec.PublicKey + + // remoteKey is the key desc of the remote key. + remoteKey keychain.KeyDescriptor + + // localKey is the key desc of the local key. + localKey keychain.KeyDescriptor + + // signer is the signer that'll be used to interact with the musig + // session. + signer input.MuSig2Signer + + // commitType tracks if this is the session for the local or remote + // commitment. + commitType commitType +} + +// NewPartialMusigSession creates a new musig2 session given only the +// verification nonce (local nonce), and the other information that has already +// been bound to the session. +func NewPartialMusigSession(verificationNonce musig2.Nonces, + localKey, remoteKey keychain.KeyDescriptor, + signer input.MuSig2Signer, inputTxOut *wire.TxOut, + commitType commitType) *MusigSession { + + signerKeys := []*btcec.PublicKey{localKey.PubKey, remoteKey.PubKey} + + nonces := MusigNoncePair{ + VerificationNonce: verificationNonce, + } + + return &MusigSession{ + nonces: nonces, + remoteKey: remoteKey, + localKey: localKey, + inputTxOut: inputTxOut, + signerKeys: signerKeys, + signer: signer, + commitType: commitType, + } +} + +// FinalizeSession finalizes the session given the signer nonce. This is +// called before signing or verifying a new commitment. +func (m *MusigSession) FinalizeSession(signingNonce musig2.Nonces) error { + var ( + localNonce, remoteNonce musig2.Nonces + err error + ) + + // First, we'll stash the freshly generated signing nonce. Depending on + // who's commitment we're handling, this'll either be our generated + // nonce, or the one we just got from the remote party. + m.nonces.SigningNonce = signingNonce + + switch m.commitType { + // If we're making a session for the remote commitment, then the nonce + // we use to sign is actually will be the signing nonce for the + // session, and their nonce the verification nonce. + case remoteCommit: + localNonce = m.nonces.SigningNonce + remoteNonce = m.nonces.VerificationNonce + + // Otherwise, we're generating/receiving a signature for our local + // commitment (to broadcast), so now our verification nonce is the one + // we've already generated, and we want to bind their new signing + // nonce. + case localCommit: + localNonce = m.nonces.VerificationNonce + remoteNonce = m.nonces.SigningNonce + } + + tweakDesc := input.MuSig2Tweaks{ + TaprootBIP0086Tweak: true, + } + m.session, err = m.signer.MuSig2CreateSession( + input.MuSig2Version100RC2, m.localKey.KeyLocator, m.signerKeys, + &tweakDesc, [][musig2.PubNonceSize]byte{remoteNonce.PubNonce}, + musig2.WithPreGeneratedNonce(&localNonce), + ) + if err != nil { + return err + } + + // We'll need the raw combined nonces later to be able to verify + // partial signatures, and also combine partial signatures, so we'll + // generate it now ourselves. + aggNonce, err := musig2.AggregateNonces([][musig2.PubNonceSize]byte{ + m.nonces.SigningNonce.PubNonce, + m.nonces.VerificationNonce.PubNonce, + }) + if err != nil { + return nil + } + + m.combinedNonce = aggNonce + + return nil +} + +// taprootKeyspendSighash generates the sighash for a taproot key spend. As +// this is a musig2 channel output, the keyspend is the only path we can take. +func taprootKeyspendSighash(tx *wire.MsgTx, pkScript []byte, + value int64) ([]byte, error) { + + prevOutputFetcher := txscript.NewCannedPrevOutputFetcher( + pkScript, value, + ) + + sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher) + + return txscript.CalcTaprootSignatureHash( + sigHashes, txscript.SigHashDefault, tx, 0, prevOutputFetcher, + ) +} + +// SignCommit signs the passed commitment w/ the current signing (relative +// remote) nonce. Given nonces should only ever be used once, once the method +// returns a new nonce is returned, w/ the existing nonce blanked out. +func (m *MusigSession) SignCommit(tx *wire.MsgTx) (*MusigPartialSig, error) { + switch { + // If we already have a session, then we don't need to finalize as this + // was done up front (symmetric nonce case, like for co-op close). + case m.session == nil && m.commitType == remoteCommit: + // Before we can sign a new commitment, we'll need to generate + // a fresh nonce that'll be sent along side our signature. With + // the nonce in hand, we can finalize the session. + txHash := tx.TxHash() + signingNonce, err := musig2.GenNonces( + musig2.WithPublicKey(m.localKey.PubKey), + musig2.WithNonceAuxInput(txHash[:]), + ) + if err != nil { + return nil, err + } + if err := m.FinalizeSession(*signingNonce); err != nil { + return nil, err + } + + // Otherwise, we're trying to make a new commitment transaction without + // an active session, so we'll error out. + case m.session == nil: + return nil, ErrSessionNotFinalized + } + + // Next we can sign, we'll need to generate the sighash for their + // commitment transaction. + sigHash, err := taprootKeyspendSighash( + tx, m.inputTxOut.PkScript, m.inputTxOut.Value, + ) + if err != nil { + return nil, err + } + + // Now that we have our session created, we'll use it to generate the + // initial partial signature over our sighash. + var sigHashMsg [32]byte + copy(sigHashMsg[:], sigHash) + + walletLog.Infof("Generating new musig2 sig for session=%x, nonces=%s", + m.session.SessionID[:], m.nonces.String()) + + sig, err := m.signer.MuSig2Sign( + m.session.SessionID, sigHashMsg, false, + ) + if err != nil { + return nil, err + } + + return NewMusigPartialSig( + sig, m.session.PublicNonce, m.combinedNonce, m.signerKeys, + ), nil +} + +// Refresh is called once we receive a new verification nonce from the remote +// party after sending a signature. This nonce will be coupled within the +// revoke-and-ack message of the remote party. +func (m *MusigSession) Refresh(verificationNonce *musig2.Nonces, +) (*MusigSession, error) { + + return NewPartialMusigSession( + *verificationNonce, m.localKey, m.remoteKey, m.signer, + m.inputTxOut, m.commitType, + ), nil +} + +// VerificationNonce returns the current verification nonce for the session. +func (m *MusigSession) VerificationNonce() *musig2.Nonces { + return &m.nonces.VerificationNonce +} + +// musigSessionOpts is a set of options that can be used to modify calls to the +// musig session. +type musigSessionOpts struct { + // customRand is an optional custom random source that can be used to + // generate nonces via a counter scheme. + customRand io.Reader +} + +// defaultMusigSessionOpts returns the default set of options for the musig +// session. +func defaultMusigSessionOpts() *musigSessionOpts { + return &musigSessionOpts{} +} + +// MusigSessionOpt is a functional option that can be used to modify calls to +// the musig session. +type MusigSessionOpt func(*musigSessionOpts) + +// WithLocalCounterNonce is used to generate local nonces based on the shachain +// producer and the current height. This allows us to not have to write secret +// nonce state to disk. Instead, we can use this to derive the nonce we need to +// sign and broadcast our own commitment transaction. +func WithLocalCounterNonce(targetHeight uint64, + shaGen shachain.Producer) MusigSessionOpt { + + return func(opt *musigSessionOpts) { + nextPreimage, _ := shaGen.AtIndex(targetHeight) + + opt.customRand = bytes.NewBuffer(nextPreimage[:]) + } +} + +// VerifyCommitSig attempts to verify the passed partial signature against the +// passed commitment transaction. A keyspend sighash is assumed to generate the +// signed message. As we never re-use nonces, a new verification nonce (our +// relative local nonce) returned to transmit to the remote party, which allows +// them to generate another signature. +func (m *MusigSession) VerifyCommitSig(commitTx *wire.MsgTx, + sig *lnwire.PartialSigWithNonce, + musigOpts ...MusigSessionOpt) (*musig2.Nonces, error) { + + opts := defaultMusigSessionOpts() + for _, optFunc := range musigOpts { + optFunc(opts) + } + + // Before we can verify the signature, we'll need to finalize the + // session by binding the remote party's provided signing nonce. + if err := m.FinalizeSession(musig2.Nonces{ + PubNonce: sig.Nonce, + }); err != nil { + return nil, err + } + + // When we verify a commitment signature, we always assume that we're + // verifying a signature on our local commitment. Therefore, we'll use: + // their remote nonce, and also public key. + partialSig := NewMusigPartialSig( + &musig2.PartialSignature{S: &sig.Sig}, + m.nonces.SigningNonce.PubNonce, m.combinedNonce, m.signerKeys, + ) + + // With the partial sig loaded with the proper context, we'll now + // generate the sighash that the remote party should have signed. + sigHash, err := taprootKeyspendSighash( + commitTx, m.inputTxOut.PkScript, m.inputTxOut.Value, + ) + if err != nil { + return nil, err + } + + walletLog.Infof("Verifying new musig2 sig for session=%x, nonce=%s", + m.session.SessionID[:], m.nonces.String()) + + if !partialSig.Verify(sigHash, m.remoteKey.PubKey) { + return nil, fmt.Errorf("invalid partial commit sig") + } + + nonceOpts := []musig2.NonceGenOption{ + musig2.WithPublicKey(m.localKey.PubKey), + } + if opts.customRand != nil { + nonceOpts = append( + nonceOpts, musig2.WithCustomRand(opts.customRand), + ) + } + + // At this point, we know that their signature is valid, so we'll + // generate another verification nonce for them, so they can generate a + // new state transition. + nextVerificationNonce, err := musig2.GenNonces(nonceOpts...) + if err != nil { + return nil, fmt.Errorf("unable to gen new nonce: %w", err) + } + + return nextVerificationNonce, nil +} + +// CombineSigs combines the passed partial signatures into a valid schnorr +// signature. +func (m *MusigSession) CombineSigs(sigs ...*musig2.PartialSignature, +) (*schnorr.Signature, error) { + + sig, _, err := m.signer.MuSig2CombineSig( + m.session.SessionID, sigs, + ) + if err != nil { + return nil, err + } + + return sig, nil +} + +// MusigSessionCfg is used to create a new musig2 pair session. It contains the +// keys for both parties, as well as their initial verification nonces. +type MusigSessionCfg struct { + // LocalKey is a key desc for the local key. + LocalKey keychain.KeyDescriptor + + // RemoteKey is a key desc for the remote key. + RemoteKey keychain.KeyDescriptor + + // LocalNonce is the local party's initial verification nonce. + LocalNonce musig2.Nonces + + // RemoteNonce is the remote party's initial verification nonce. + RemoteNonce musig2.Nonces + + // Signer is the signer that will be used to generate the session. + Signer input.MuSig2Signer + + // InputTxOut is the output that we're signing for. This will be the + // funding input. + InputTxOut *wire.TxOut +} + +// MusigPairSession houses the two musig2 sessions needed to do funding and +// drive forward the state machine. The local session is used to verify +// incoming commitment states. The remote session is used to propose new +// commitment states to the remote party. +type MusigPairSession struct { + // LocalSession is the local party's musig2 session. + LocalSession *MusigSession + + // RemoteSession is the remote party's musig2 session. + RemoteSession *MusigSession + + // signer is the signer that will be used to drive the session. + signer input.MuSig2Signer +} + +// NewMusigPairSession creates a new musig2 pair session. +func NewMusigPairSession(cfg *MusigSessionCfg) *MusigPairSession { + // Given the config passed in, we'll now create our two sessions: one + // for the local commit, and one for the remote commit. + // + // Both sessions will be created using only the verification nonce for + // the local+remote party. + localSession := NewPartialMusigSession( + cfg.LocalNonce, cfg.LocalKey, cfg.RemoteKey, + cfg.Signer, cfg.InputTxOut, localCommit, + ) + remoteSession := NewPartialMusigSession( + cfg.RemoteNonce, cfg.LocalKey, cfg.RemoteKey, + cfg.Signer, cfg.InputTxOut, remoteCommit, + ) + + return &MusigPairSession{ + LocalSession: localSession, + RemoteSession: remoteSession, + signer: cfg.Signer, + } +} From 36e5db3fed3a41ead5cf94e08aa69374b409a67b Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 1 Jun 2023 17:38:37 -0700 Subject: [PATCH 4/6] multimutex: remove HashMutex, make Mutex type a type param In this commit, we eliminate some code duplication by removing the old `HashMutex` struct as it just duplicates all the code with a different type (uint64 and hash). We then make the main Mutex struct take a type param, so the key can be parametrized when the struct is instantiated. --- blockcache/blockcache.go | 4 +- discovery/gossiper.go | 4 +- htlcswitch/payment_result.go | 4 +- multimutex/hash_mutex.go | 90 ------------------------------------ multimutex/multimutex.go | 77 ++++++++++++++---------------- routing/control_tower.go | 4 +- routing/router.go | 4 +- 7 files changed, 45 insertions(+), 142 deletions(-) delete mode 100644 multimutex/hash_mutex.go diff --git a/blockcache/blockcache.go b/blockcache/blockcache.go index 6c1f5f74c66..09d5d339554 100644 --- a/blockcache/blockcache.go +++ b/blockcache/blockcache.go @@ -14,7 +14,7 @@ import ( // BlockCache is an lru cache for blocks. type BlockCache struct { Cache *lru.Cache[wire.InvVect, *neutrino.CacheableBlock] - HashMutex *multimutex.HashMutex + HashMutex *multimutex.Mutex[lntypes.Hash] } // NewBlockCache creates a new BlockCache with the given maximum capacity. @@ -23,7 +23,7 @@ func NewBlockCache(capacity uint64) *BlockCache { Cache: lru.NewCache[wire.InvVect, *neutrino.CacheableBlock]( capacity, ), - HashMutex: multimutex.NewHashMutex(), + HashMutex: multimutex.NewMutex[lntypes.Hash](), } } diff --git a/discovery/gossiper.go b/discovery/gossiper.go index 71fc2ee8a92..e41e3c03e71 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -447,7 +447,7 @@ type AuthenticatedGossiper struct { // goroutine per channel ID. This is done to ensure that when // the gossiper is handling an announcement, the db state stays // consistent between when the DB is first read until it's written. - channelMtx *multimutex.Mutex + channelMtx *multimutex.Mutex[uint64] recentRejects *lru.Cache[rejectCacheKey, *cachedReject] @@ -493,7 +493,7 @@ func New(cfg Config, selfKeyDesc *keychain.KeyDescriptor) *AuthenticatedGossiper prematureChannelUpdates: lru.NewCache[uint64, *cachedNetworkMsg]( //nolint: lll maxPrematureUpdates, ), - channelMtx: multimutex.NewMutex(), + channelMtx: multimutex.NewMutex[uint64](), recentRejects: lru.NewCache[rejectCacheKey, *cachedReject]( maxRejectedUpdates, ), diff --git a/htlcswitch/payment_result.go b/htlcswitch/payment_result.go index 8d6cb5b3af9..cd982b8bb74 100644 --- a/htlcswitch/payment_result.go +++ b/htlcswitch/payment_result.go @@ -93,14 +93,14 @@ type networkResultStore struct { // paymentIDMtx is a multimutex used to make sure the database and // result subscribers map is consistent for each payment ID in case of // concurrent callers. - paymentIDMtx *multimutex.Mutex + paymentIDMtx *multimutex.Mutex[uint64] } func newNetworkResultStore(db kvdb.Backend) *networkResultStore { return &networkResultStore{ backend: db, results: make(map[uint64][]chan *networkResult), - paymentIDMtx: multimutex.NewMutex(), + paymentIDMtx: multimutex.NewMutex[uint64](), } } diff --git a/multimutex/hash_mutex.go b/multimutex/hash_mutex.go deleted file mode 100644 index 4a65394d1c6..00000000000 --- a/multimutex/hash_mutex.go +++ /dev/null @@ -1,90 +0,0 @@ -package multimutex - -import ( - "fmt" - "sync" - - "github.com/lightningnetwork/lnd/lntypes" -) - -// HashMutex is a struct that keeps track of a set of mutexes with a given hash. -// It can be used for making sure only one goroutine gets given the mutex per -// hash. -type HashMutex struct { - // mutexes is a map of hashes to a cntMutex. The cntMutex for - // a given hash will hold the mutex to be used by all - // callers requesting access for the hash, in addition to - // the count of callers. - mutexes map[lntypes.Hash]*cntMutex - - // mapMtx is used to give synchronize concurrent access - // to the mutexes map. - mapMtx sync.Mutex -} - -// NewHashMutex creates a new Mutex. -func NewHashMutex() *HashMutex { - return &HashMutex{ - mutexes: make(map[lntypes.Hash]*cntMutex), - } -} - -// Lock locks the mutex by the given hash. If the mutex is already -// locked by this hash, Lock blocks until the mutex is available. -func (c *HashMutex) Lock(hash lntypes.Hash) { - c.mapMtx.Lock() - mtx, ok := c.mutexes[hash] - if ok { - // If the mutex already existed in the map, we - // increment its counter, to indicate that there - // now is one more goroutine waiting for it. - mtx.cnt++ - } else { - // If it was not in the map, it means no other - // goroutine has locked the mutex for this hash, - // and we can create a new mutex with count 1 - // and add it to the map. - mtx = &cntMutex{ - cnt: 1, - } - c.mutexes[hash] = mtx - } - c.mapMtx.Unlock() - - // Acquire the mutex for this hash. - mtx.Lock() -} - -// Unlock unlocks the mutex by the given hash. It is a run-time -// error if the mutex is not locked by the hash on entry to Unlock. -func (c *HashMutex) Unlock(hash lntypes.Hash) { - // Since we are done with all the work for this - // update, we update the map to reflect that. - c.mapMtx.Lock() - - mtx, ok := c.mutexes[hash] - if !ok { - // The mutex not existing in the map means - // an unlock for an hash not currently locked - // was attempted. - panic(fmt.Sprintf("double unlock for hash %v", - hash)) - } - - // Decrement the counter. If the count goes to - // zero, it means this caller was the last one - // to wait for the mutex, and we can delete it - // from the map. We can do this safely since we - // are under the mapMtx, meaning that all other - // goroutines waiting for the mutex already - // have incremented it, or will create a new - // mutex when they get the mapMtx. - mtx.cnt-- - if mtx.cnt == 0 { - delete(c.mutexes, hash) - } - c.mapMtx.Unlock() - - // Unlock the mutex for this hash. - mtx.Unlock() -} diff --git a/multimutex/multimutex.go b/multimutex/multimutex.go index e37c88d510c..4180f3e53f7 100644 --- a/multimutex/multimutex.go +++ b/multimutex/multimutex.go @@ -5,51 +5,48 @@ import ( "sync" ) -// cntMutex is a struct that wraps a counter and a mutex, and is used -// to keep track of the number of goroutines waiting for access to the +// cntMutex is a struct that wraps a counter and a mutex, and is used to keep +// track of the number of goroutines waiting for access to the // mutex, such that we can forget about it when the counter is zero. type cntMutex struct { cnt int sync.Mutex } -// Mutex is a struct that keeps track of a set of mutexes with -// a given ID. It can be used for making sure only one goroutine -// gets given the mutex per ID. -type Mutex struct { - // mutexes is a map of IDs to a cntMutex. The cntMutex for - // a given ID will hold the mutex to be used by all - // callers requesting access for the ID, in addition to - // the count of callers. - mutexes map[uint64]*cntMutex +// Mutex is a struct that keeps track of a set of mutexes with a given ID. It +// can be used for making sure only one goroutine gets given the mutex per ID. +type Mutex[T comparable] struct { + // mutexes is a map of IDs to a cntMutex. The cntMutex for a given ID + // will hold the mutex to be used by all callers requesting access for + // the ID, in addition to the count of callers. + mutexes map[T]*cntMutex - // mapMtx is used to give synchronize concurrent access - // to the mutexes map. + // mapMtx is used to give synchronize concurrent access to the mutexes + // map. mapMtx sync.Mutex } // NewMutex creates a new Mutex. -func NewMutex() *Mutex { - return &Mutex{ - mutexes: make(map[uint64]*cntMutex), +func NewMutex[T comparable]() *Mutex[T] { + return &Mutex[T]{ + mutexes: make(map[T]*cntMutex), } } -// Lock locks the mutex by the given ID. If the mutex is already -// locked by this ID, Lock blocks until the mutex is available. -func (c *Mutex) Lock(id uint64) { +// Lock locks the mutex by the given ID. If the mutex is already locked by this +// ID, Lock blocks until the mutex is available. +func (c *Mutex[T]) Lock(id T) { c.mapMtx.Lock() mtx, ok := c.mutexes[id] if ok { - // If the mutex already existed in the map, we - // increment its counter, to indicate that there - // now is one more goroutine waiting for it. + // If the mutex already existed in the map, we increment its + // counter, to indicate that there now is one more goroutine + // waiting for it. mtx.cnt++ } else { - // If it was not in the map, it means no other - // goroutine has locked the mutex for this ID, - // and we can create a new mutex with count 1 - // and add it to the map. + // If it was not in the map, it means no other goroutine has + // locked the mutex for this ID, and we can create a new mutex + // with count 1 and add it to the map. mtx = &cntMutex{ cnt: 1, } @@ -61,30 +58,26 @@ func (c *Mutex) Lock(id uint64) { mtx.Lock() } -// Unlock unlocks the mutex by the given ID. It is a run-time -// error if the mutex is not locked by the ID on entry to Unlock. -func (c *Mutex) Unlock(id uint64) { - // Since we are done with all the work for this - // update, we update the map to reflect that. +// Unlock unlocks the mutex by the given ID. It is a run-time error if the +// mutex is not locked by the ID on entry to Unlock. +func (c *Mutex[T]) Unlock(id T) { + // Since we are done with all the work for this update, we update the + // map to reflect that. c.mapMtx.Lock() mtx, ok := c.mutexes[id] if !ok { - // The mutex not existing in the map means - // an unlock for an ID not currently locked - // was attempted. + // The mutex not existing in the map means an unlock for an ID + // not currently locked was attempted. panic(fmt.Sprintf("double unlock for id %v", id)) } - // Decrement the counter. If the count goes to - // zero, it means this caller was the last one - // to wait for the mutex, and we can delete it - // from the map. We can do this safely since we - // are under the mapMtx, meaning that all other - // goroutines waiting for the mutex already - // have incremented it, or will create a new - // mutex when they get the mapMtx. + // Decrement the counter. If the count goes to zero, it means this + // caller was the last one to wait for the mutex, and we can delete it + // from the map. We can do this safely since we are under the mapMtx, + // meaning that all other goroutines waiting for the mutex already have + // incremented it, or will create a new mutex when they get the mapMtx. mtx.cnt-- if mtx.cnt == 0 { delete(c.mutexes, id) diff --git a/routing/control_tower.go b/routing/control_tower.go index a0c5b1df772..d2cbc6bbf32 100644 --- a/routing/control_tower.go +++ b/routing/control_tower.go @@ -132,7 +132,7 @@ type controlTower struct { // paymentsMtx provides synchronization on the payment level to ensure // that no race conditions occur in between updating the database and // sending a notification. - paymentsMtx *multimutex.HashMutex + paymentsMtx *multimutex.Mutex[lntypes.Hash] } // NewControlTower creates a new instance of the controlTower. @@ -143,7 +143,7 @@ func NewControlTower(db *channeldb.PaymentControl) ControlTower { map[uint64]*controlTowerSubscriberImpl, ), subscribers: make(map[lntypes.Hash][]*controlTowerSubscriberImpl), - paymentsMtx: multimutex.NewHashMutex(), + paymentsMtx: multimutex.NewMutex[lntypes.Hash](), } } diff --git a/routing/router.go b/routing/router.go index eea5189d9b3..5ecce6646e8 100644 --- a/routing/router.go +++ b/routing/router.go @@ -437,7 +437,7 @@ type ChannelRouter struct { // channelEdgeMtx is a mutex we use to make sure we process only one // ChannelEdgePolicy at a time for a given channelID, to ensure // consistency between the various database accesses. - channelEdgeMtx *multimutex.Mutex + channelEdgeMtx *multimutex.Mutex[uint64] // statTicker is a resumable ticker that logs the router's progress as // it discovers channels or receives updates. @@ -475,7 +475,7 @@ func New(cfg Config) (*ChannelRouter, error) { networkUpdates: make(chan *routingMsg), topologyClients: &lnutils.SyncMap[uint64, *topologyClient]{}, ntfnClientUpdates: make(chan *topologyClientUpdate), - channelEdgeMtx: multimutex.NewMutex(), + channelEdgeMtx: multimutex.NewMutex[uint64](), selfNode: selfNode, statTicker: ticker.New(defaultStatInterval), stats: new(routerStats), From 0bc8f3f716542327e9b72e9f68ef4004eb036a0b Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 1 Jun 2023 17:52:41 -0700 Subject: [PATCH 5/6] input: use multmutex to increase concurrency for musig session manager By using the multimutex here, we'll no longer rely on a single mutex for the entire musig session set like we used to. Instead, we can use the session ID to key into a map of mutexes and use those directly. --- input/musig2_session_manager.go | 39 +++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/input/musig2_session_manager.go b/input/musig2_session_manager.go index 3572614fb85..3bf26004ad6 100644 --- a/input/musig2_session_manager.go +++ b/input/musig2_session_manager.go @@ -3,12 +3,12 @@ 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" + "github.com/lightningnetwork/lnd/multimutex" ) // MuSig2State is a struct that holds on to the internal signing session state @@ -34,10 +34,10 @@ type PrivKeyFetcher func(*keychain.KeyDescriptor) (*btcec.PrivateKey, error) // 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 + sessionMtx *multimutex.Mutex[MuSig2SessionID] + musig2Sessions map[MuSig2SessionID]*MuSig2State } @@ -45,7 +45,9 @@ type MusigSessionManager struct { // fetcher. func NewMusigSessionManager(keyFetcher PrivKeyFetcher) *MusigSessionManager { return &MusigSessionManager{ - keyFetcher: keyFetcher, + keyFetcher: keyFetcher, + musig2Sessions: make(map[MuSig2SessionID]*MuSig2State), + sessionMtx: multimutex.NewMutex[MuSig2SessionID](), } } @@ -70,7 +72,7 @@ func (m *MusigSessionManager) MuSig2CreateSession(bipVersion MuSig2Version, KeyLocator: keyLoc, }) if err != nil { - return nil, fmt.Errorf("error deriving private key: %v", err) + return nil, fmt.Errorf("error deriving private key: %w", err) } // Create a signing context and session with the given private key and @@ -98,7 +100,7 @@ func (m *MusigSessionManager) MuSig2CreateSession(bipVersion MuSig2Version, // Register the new session. combinedKey, err := musigContext.CombinedKey() if err != nil { - return nil, fmt.Errorf("error getting combined key: %v", err) + return nil, fmt.Errorf("error getting combined key: %w", err) } session := &MuSig2State{ MuSig2SessionInfo: MuSig2SessionInfo{ @@ -120,7 +122,7 @@ func (m *MusigSessionManager) MuSig2CreateSession(bipVersion MuSig2Version, if tweaks.HasTaprootTweak() { internalKey, err := musigContext.TaprootInternalKey() if err != nil { - return nil, fmt.Errorf("error getting internal key: %v", + return nil, fmt.Errorf("error getting internal key: %w", err) } session.TaprootInternalKey = internalKey @@ -129,9 +131,12 @@ func (m *MusigSessionManager) MuSig2CreateSession(bipVersion MuSig2Version, // 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() + // + // We'll use just all zeroes as the session ID for the mutex, as this + // is a "global" action. + m.sessionMtx.Lock(MuSig2SessionID{}) m.musig2Sessions[session.SessionID] = session - m.Unlock() + m.sessionMtx.Unlock(MuSig2SessionID{}) return &session.MuSig2SessionInfo, nil } @@ -149,8 +154,8 @@ func (m *MusigSessionManager) MuSig2Sign(sessionID MuSig2SessionID, // 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() + m.sessionMtx.Lock(sessionID) + defer m.sessionMtx.Unlock(sessionID) session, ok := m.musig2Sessions[sessionID] if !ok { @@ -190,8 +195,8 @@ func (m *MusigSessionManager) MuSig2CombineSig(sessionID MuSig2SessionID, // 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() + m.sessionMtx.Lock(sessionID) + defer m.sessionMtx.Unlock(sessionID) session, ok := m.musig2Sessions[sessionID] if !ok { @@ -237,8 +242,8 @@ 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() + m.sessionMtx.Lock(sessionID) + defer m.sessionMtx.Unlock(sessionID) _, ok := m.musig2Sessions[sessionID] if !ok { @@ -259,8 +264,8 @@ func (m *MusigSessionManager) MuSig2RegisterNonces(sessionID MuSig2SessionID, // 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() + m.sessionMtx.Lock(sessionID) + defer m.sessionMtx.Unlock(sessionID) session, ok := m.musig2Sessions[sessionID] if !ok { From 7c85f028256fe703a1fdf7f0f54714bf08a926be Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 2 Jun 2023 18:45:44 -0700 Subject: [PATCH 6/6] lnwallet: add initial unit test coverage for musig chan session --- lnwallet/musig_session_test.go | 273 +++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 lnwallet/musig_session_test.go diff --git a/lnwallet/musig_session_test.go b/lnwallet/musig_session_test.go new file mode 100644 index 00000000000..fc144e90eb3 --- /dev/null +++ b/lnwallet/musig_session_test.go @@ -0,0 +1,273 @@ +package lnwallet + +import ( + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/stretchr/testify/require" +) + +// nodeType is an enum that represents the two nodes in our test harness. +type nodeType uint8 + +const ( + // nodeAlice is the node that initiates the session. + nodeAlice nodeType = iota + + // nodeBob is the node that responds to the session. + nodeBob +) + +type muSessionHarness struct { + aliceCommit *wire.MsgTx + bobCommit *wire.MsgTx + + aliceSession *MusigPairSession + + bobSession *MusigPairSession + + t *testing.T +} + +func (h *muSessionHarness) selectSession(nodeName nodeType) *MusigPairSession { + var targetSession *MusigPairSession + switch nodeName { + case nodeAlice: + targetSession = h.aliceSession + case nodeBob: + targetSession = h.bobSession + } + + return targetSession +} + +func (h *muSessionHarness) refreshSession(nodeName nodeType, + nextNonce *musig2.Nonces, revoke bool) { + + var session *MusigPairSession + switch nodeName { + case nodeAlice: + session = h.aliceSession + case nodeBob: + session = h.bobSession + } + + var err error + + // If this isn't in response to a revoke, then we just signed, so we'll + // refresh our local session with the newly generated verification + // nonce. + if !revoke { + session.LocalSession, err = session.LocalSession.Refresh( + nextNonce, + ) + } else { + session.RemoteSession, err = session.RemoteSession.Refresh( + nextNonce, + ) + } + require.NoError(h.t, err) +} + +// SignCommitment signs a new remote commitment. This is equivalent to sending +// a CommitSig message on the normal LN protocol. +func (h *muSessionHarness) SignCommitment(nodeName nodeType) *MusigPartialSig { + targetSession := h.selectSession(nodeName) + + sig, err := targetSession.RemoteSession.SignCommit(h.bobCommit) + require.NoError(h.t, err) + + return sig +} + +// VerifyAndSignCommitment verifies a remote commitment, then signs a new +// commitment. This combines receiving a signature, then sending a revoke +// message. +func (h *muSessionHarness) VerifyAndSignCommitment(nodeName nodeType, + sig *MusigPartialSig) (*MusigPartialSig, *musig2.Nonces) { + + muSession := h.selectSession(nodeName) + + // Verify the commitment transaction from the remote party. The nonce + // returned will be sent along side the "revoke and ack" message in the + // actual p2p protocol. + nextVerificationNonce, err := muSession.LocalSession.VerifyCommitSig( + h.bobCommit, sig.ToWireSig(), + ) + require.NoError(h.t, err) + + // As we've just used our verification nonce to verify the remote sign, + // we'll refresh our local session with the new nonce. + h.refreshSession(nodeName, nextVerificationNonce, false) + + // Next, sign a new version of the commitment for the remote party. + // This uses a JIT nonce that'll be sent along side the signature, and + // consumes the verification nonce of the remote party. + remoteSig, err := muSession.RemoteSession.SignCommit(h.aliceCommit) + require.NoError(h.t, err) + + return remoteSig, nextVerificationNonce +} + +// VerifyCommitment verifies a remote commitment, then sends a nonce. This is +// equivalent to verifying a new incoming commitment, then sending a revoke +// message. +func (h *muSessionHarness) VerifyCommitment(nodeName nodeType, + sig *MusigPartialSig, nextNonce *musig2.Nonces) *musig2.Nonces { + + muSession := h.selectSession(nodeName) + + // We'll now verify the incoming signature, then refresh our local + // session as we've used up our prior verification nonce. + nextVerificationNonce, err := muSession.LocalSession.VerifyCommitSig( + h.aliceCommit, sig.ToWireSig(), + ) + require.NoError(h.t, err) + h.refreshSession(nodeName, nextVerificationNonce, false) + + // The packaged nonce is the remote party's new verification nonce, so + // we'll refresh their remote commitment: we just got the revocation + // and the sig in the same message. + h.refreshSession(nodeName, nextNonce, true) + + return nextVerificationNonce +} + +// ProcessVerificationNonce processes a verification nonce from the remote. +// This is equivalent to receiving the revoke from a remote party after you +// kicked off the commitment dance. +func (h *muSessionHarness) ProcessVerificationNonce(nodeName nodeType, + nextNonce *musig2.Nonces) { + + h.refreshSession(nodeName, nextNonce, true) +} + +func newMuSessionHarness(t *testing.T) *muSessionHarness { + aliceCommit := wire.NewMsgTx(2) + aliceCommit.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Index: 1, + }, + }) + + bobCommit := wire.NewMsgTx(2) + bobCommit.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Index: 2, + }, + }) + + alicePriv, alicePub := btcec.PrivKeyFromBytes(testWalletPrivKey) + aliceSigner := input.NewMockSigner([]*btcec.PrivateKey{alicePriv}, nil) + + aliceVerificationNonce, err := musig2.GenNonces( + musig2.WithPublicKey(alicePub), + ) + require.NoError(t, err) + + bobPriv, bobPub := btcec.PrivKeyFromBytes(bobsPrivKey) + bobSigner := input.NewMockSigner([]*btcec.PrivateKey{bobPriv}, nil) + + bobVerificationNonce, err := musig2.GenNonces( + musig2.WithPublicKey(bobPub), + ) + require.NoError(t, err) + + inputTxOut := &wire.TxOut{ + Value: 1000, + PkScript: testHdSeed[:], + } + + aliceSession := NewMusigPairSession(&MusigSessionCfg{ + LocalKey: keychain.KeyDescriptor{ + PubKey: alicePub, + }, + RemoteKey: keychain.KeyDescriptor{ + PubKey: bobPub, + }, + LocalNonce: *aliceVerificationNonce, + RemoteNonce: *bobVerificationNonce, + Signer: aliceSigner, + InputTxOut: inputTxOut, + }) + + bobSession := NewMusigPairSession(&MusigSessionCfg{ + LocalKey: keychain.KeyDescriptor{ + PubKey: bobPub, + }, + RemoteKey: keychain.KeyDescriptor{ + PubKey: alicePub, + }, + LocalNonce: *bobVerificationNonce, + RemoteNonce: *aliceVerificationNonce, + Signer: bobSigner, + InputTxOut: inputTxOut, + }) + + return &muSessionHarness{ + aliceCommit: aliceCommit, + aliceSession: aliceSession, + bobCommit: bobCommit, + bobSession: bobSession, + t: t, + } +} + +// TestMusigSession tests that we're able to send and receive signatures for +// the set of asymmetric musig sessions. This tests proper nonce rotation and +// signature verification. +func TestMusigSesssion(t *testing.T) { + t.Parallel() + + // First, we'll make a new musig session between Alice and Bob. This is + // 4 sessions total, as both sides maintain a session for their local + // commitment, and one for the remote commitment. + muSessions := newMuSessionHarness(t) + + t.Run("session_round_trips", func(t *testing.T) { //nolint:paralleltest + const numRounds = 10 + for i := 0; i < numRounds; i++ { + // We'll now simulate a full commitment dance. + // + // To start, Alice will sign a new commitment for Bob's + // remote commitment. + aliceSig := muSessions.SignCommitment(nodeAlice) + + // Bob will then verify Alice's signature, and sign a + // new commitment for Alice. + bobSig, bobNonce := muSessions.VerifyAndSignCommitment( + nodeBob, aliceSig, + ) + + // Next Alice will process Bob's signature, and then + // generate a new verification nonce to he can sign the + // next commitment. + aliceNonce := muSessions.VerifyCommitment( + nodeAlice, bobSig, bobNonce, + ) + + // To conclude the commitment dance, Bob will process + // Alice's new verification nonce. + muSessions.ProcessVerificationNonce(nodeBob, aliceNonce) + + // Modify the commitments after each round to simulate + // the LN protocol commitment randomness structure + // (sequence+locktime change each state, etc). + muSessions.aliceCommit.TxIn[0].PreviousOutPoint.Index++ + muSessions.bobCommit.TxIn[0].PreviousOutPoint.Index++ + } + }) + + t.Run("no_finalize_error", func(t *testing.T) { //nolint:paralleltest + // If a local party attempts to sign for their local commitment + // without finalizing first, they'll get this error. + _, err := muSessions.aliceSession.LocalSession.SignCommit( + muSessions.aliceCommit, + ) + require.ErrorIs(t, err, ErrSessionNotFinalized) + }) +}