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/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/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/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..3bf26004ad6 --- /dev/null +++ b/input/musig2_session_manager.go @@ -0,0 +1,303 @@ +package input + +import ( + "crypto/sha256" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/multimutex" +) + +// 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 { + keyFetcher PrivKeyFetcher + + sessionMtx *multimutex.Mutex[MuSig2SessionID] + + musig2Sessions map[MuSig2SessionID]*MuSig2State +} + +// NewMusigSessionManager creates a new musig manager given an abstract key +// fetcher. +func NewMusigSessionManager(keyFetcher PrivKeyFetcher) *MusigSessionManager { + return &MusigSessionManager{ + keyFetcher: keyFetcher, + musig2Sessions: make(map[MuSig2SessionID]*MuSig2State), + sessionMtx: multimutex.NewMutex[MuSig2SessionID](), + } +} + +// 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: %w", 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: %w", 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: %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. + // + // 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.sessionMtx.Unlock(MuSig2SessionID{}) + + 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.sessionMtx.Lock(sessionID) + defer m.sessionMtx.Unlock(sessionID) + + 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.sessionMtx.Lock(sessionID) + defer m.sessionMtx.Unlock(sessionID) + + 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.sessionMtx.Lock(sessionID) + defer m.sessionMtx.Unlock(sessionID) + + _, 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.sessionMtx.Lock(sessionID) + defer m.sessionMtx.Unlock(sessionID) + + 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/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/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, + } +} 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) + }) +} 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/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/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/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/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/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), 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 } 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 }