• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 13408822928

19 Feb 2025 08:59AM UTC coverage: 41.123% (-17.7%) from 58.794%
13408822928

Pull #9521

github

web-flow
Merge d2f397b3c into 0e8786348
Pull Request #9521: unit: remove GOACC, use Go 1.20 native coverage functionality

92496 of 224923 relevant lines covered (41.12%)

18825.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/input/musig2_session_manager.go
1
package input
2

3
import (
4
        "crypto/sha256"
5
        "fmt"
6

7
        "github.com/btcsuite/btcd/btcec/v2"
8
        "github.com/btcsuite/btcd/btcec/v2/schnorr"
9
        "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
10
        "github.com/lightningnetwork/lnd/keychain"
11
        "github.com/lightningnetwork/lnd/lnutils"
12
        "github.com/lightningnetwork/lnd/multimutex"
13
)
14

15
// MuSig2State is a struct that holds on to the internal signing session state
16
// of a MuSig2 session.
17
type MuSig2State struct {
18
        // MuSig2SessionInfo is the associated meta information of the signing
19
        // session.
20
        MuSig2SessionInfo
21

22
        // context is the signing context responsible for keeping track of the
23
        // public keys involved in the signing process.
24
        context MuSig2Context
25

26
        // session is the signing session responsible for keeping track of the
27
        // nonces and partial signatures involved in the signing process.
28
        session MuSig2Session
29
}
30

31
// PrivKeyFetcher is used to fetch a private key that matches a given key desc.
32
type PrivKeyFetcher func(*keychain.KeyDescriptor) (*btcec.PrivateKey, error)
33

34
// MusigSessionMusigSessionManager houses the state needed to manage concurrent
35
// musig sessions. Each session is identified by a unique session ID which is
36
// used by callers to interact with a given session.
37
type MusigSessionManager struct {
38
        keyFetcher PrivKeyFetcher
39

40
        sessionMtx *multimutex.Mutex[MuSig2SessionID]
41

42
        musig2Sessions *lnutils.SyncMap[MuSig2SessionID, *MuSig2State]
43
}
44

45
// NewMusigSessionManager creates a new musig manager given an abstract key
46
// fetcher.
47
func NewMusigSessionManager(keyFetcher PrivKeyFetcher) *MusigSessionManager {
×
48
        return &MusigSessionManager{
×
49
                keyFetcher: keyFetcher,
×
50
                musig2Sessions: &lnutils.SyncMap[
×
51
                        MuSig2SessionID, *MuSig2State,
×
52
                ]{},
×
53
                sessionMtx: multimutex.NewMutex[MuSig2SessionID](),
×
54
        }
×
55
}
×
56

57
// MuSig2CreateSession creates a new MuSig2 signing session using the local key
58
// identified by the key locator. The complete list of all public keys of all
59
// signing parties must be provided, including the public key of the local
60
// signing key. If nonces of other parties are already known, they can be
61
// submitted as well to reduce the number of method calls necessary later on.
62
//
63
// The set of sessionOpts are _optional_ and allow a caller to modify the
64
// generated sessions. As an example the local nonce might already be generated
65
// ahead of time.
66
func (m *MusigSessionManager) MuSig2CreateSession(bipVersion MuSig2Version,
67
        keyLoc keychain.KeyLocator, allSignerPubKeys []*btcec.PublicKey,
68
        tweaks *MuSig2Tweaks, otherSignerNonces [][musig2.PubNonceSize]byte,
69
        localNonces *musig2.Nonces) (*MuSig2SessionInfo, error) {
×
70

×
71
        // We need to derive the private key for signing. In the remote signing
×
72
        // setup, this whole RPC call will be forwarded to the signing
×
73
        // instance, which requires it to be stateful.
×
74
        privKey, err := m.keyFetcher(&keychain.KeyDescriptor{
×
75
                KeyLocator: keyLoc,
×
76
        })
×
77
        if err != nil {
×
78
                return nil, fmt.Errorf("error deriving private key: %w", err)
×
79
        }
×
80

81
        // Create a signing context and session with the given private key and
82
        // list of all known signer public keys.
83
        musigContext, musigSession, err := MuSig2CreateContext(
×
84
                bipVersion, privKey, allSignerPubKeys, tweaks, localNonces,
×
85
        )
×
86
        if err != nil {
×
87
                return nil, fmt.Errorf("error creating signing context: %w",
×
88
                        err)
×
89
        }
×
90

91
        // Add all nonces we might've learned so far.
92
        haveAllNonces := false
×
93
        for _, otherSignerNonce := range otherSignerNonces {
×
94
                haveAllNonces, err = musigSession.RegisterPubNonce(
×
95
                        otherSignerNonce,
×
96
                )
×
97
                if err != nil {
×
98
                        return nil, fmt.Errorf("error registering other "+
×
99
                                "signer public nonce: %v", err)
×
100
                }
×
101
        }
102

103
        // Register the new session.
104
        combinedKey, err := musigContext.CombinedKey()
×
105
        if err != nil {
×
106
                return nil, fmt.Errorf("error getting combined key: %w", err)
×
107
        }
×
108
        session := &MuSig2State{
×
109
                MuSig2SessionInfo: MuSig2SessionInfo{
×
110
                        SessionID: NewMuSig2SessionID(
×
111
                                combinedKey, musigSession.PublicNonce(),
×
112
                        ),
×
113
                        Version:       bipVersion,
×
114
                        PublicNonce:   musigSession.PublicNonce(),
×
115
                        CombinedKey:   combinedKey,
×
116
                        TaprootTweak:  tweaks.HasTaprootTweak(),
×
117
                        HaveAllNonces: haveAllNonces,
×
118
                },
×
119
                context: musigContext,
×
120
                session: musigSession,
×
121
        }
×
122

×
123
        // The internal key is only calculated if we are using a taproot tweak
×
124
        // and need to know it for a potential script spend.
×
125
        if tweaks.HasTaprootTweak() {
×
126
                internalKey, err := musigContext.TaprootInternalKey()
×
127
                if err != nil {
×
128
                        return nil, fmt.Errorf("error getting internal key: %w",
×
129
                                err)
×
130
                }
×
131
                session.TaprootInternalKey = internalKey
×
132
        }
133

134
        // Since we generate new nonces for every session, there is no way that
135
        // a session with the same ID already exists. So even if we call the API
136
        // twice with the same signers, we still get a new ID.
137
        //
138
        // We'll use just all zeroes as the session ID for the mutex, as this
139
        // is a "global" action.
140
        m.musig2Sessions.Store(session.SessionID, session)
×
141

×
142
        return &session.MuSig2SessionInfo, nil
×
143
}
144

145
// MuSig2Sign creates a partial signature using the local signing key
146
// that was specified when the session was created. This can only be
147
// called when all public nonces of all participants are known and have
148
// been registered with the session. If this node isn't responsible for
149
// combining all the partial signatures, then the cleanup parameter
150
// should be set, indicating that the session can be removed from memory
151
// once the signature was produced.
152
func (m *MusigSessionManager) MuSig2Sign(sessionID MuSig2SessionID,
153
        msg [sha256.Size]byte, cleanUp bool) (*musig2.PartialSignature, error) {
×
154

×
155
        // We hold the lock during the whole operation, we don't want any
×
156
        // interference with calls that might come through in parallel for the
×
157
        // same session.
×
158
        m.sessionMtx.Lock(sessionID)
×
159
        defer m.sessionMtx.Unlock(sessionID)
×
160

×
161
        session, ok := m.musig2Sessions.Load(sessionID)
×
162
        if !ok {
×
163
                return nil, fmt.Errorf("session with ID %x not found",
×
164
                        sessionID[:])
×
165
        }
×
166

167
        // We can only sign once we have all other signer's nonces.
168
        if !session.HaveAllNonces {
×
169
                return nil, fmt.Errorf("only have %d of %d required nonces",
×
170
                        session.session.NumRegisteredNonces(),
×
171
                        len(session.context.SigningKeys()))
×
172
        }
×
173

174
        // Create our own partial signature with the local signing key.
175
        partialSig, err := MuSig2Sign(session.session, msg, true)
×
176
        if err != nil {
×
177
                return nil, fmt.Errorf("error signing with local key: %w", err)
×
178
        }
×
179

180
        // Clean up our local state if requested.
181
        if cleanUp {
×
182
                m.musig2Sessions.Delete(sessionID)
×
183
        }
×
184

185
        return partialSig, nil
×
186
}
187

188
// MuSig2CombineSig combines the given partial signature(s) with the
189
// local one, if it already exists. Once a partial signature of all
190
// participants is registered, the final signature will be combined and
191
// returned.
192
func (m *MusigSessionManager) MuSig2CombineSig(sessionID MuSig2SessionID,
193
        partialSigs []*musig2.PartialSignature) (*schnorr.Signature, bool,
194
        error) {
×
195

×
196
        // We hold the lock during the whole operation, we don't want any
×
197
        // interference with calls that might come through in parallel for the
×
198
        // same session.
×
199
        m.sessionMtx.Lock(sessionID)
×
200
        defer m.sessionMtx.Unlock(sessionID)
×
201

×
202
        session, ok := m.musig2Sessions.Load(sessionID)
×
203
        if !ok {
×
204
                return nil, false, fmt.Errorf("session with ID %x not found",
×
205
                        sessionID[:])
×
206
        }
×
207

208
        // Make sure we don't exceed the number of expected partial signatures
209
        // as that would indicate something is wrong with the signing setup.
210
        if session.HaveAllSigs {
×
211
                return nil, true, fmt.Errorf("already have all partial" +
×
212
                        "signatures")
×
213
        }
×
214

215
        // Add all sigs we got so far.
216
        var (
×
217
                finalSig *schnorr.Signature
×
218
                err      error
×
219
        )
×
220
        for _, otherPartialSig := range partialSigs {
×
221
                session.HaveAllSigs, err = MuSig2CombineSig(
×
222
                        session.session, otherPartialSig,
×
223
                )
×
224
                if err != nil {
×
225
                        return nil, false, fmt.Errorf("error combining "+
×
226
                                "partial signature: %w", err)
×
227
                }
×
228
        }
229

230
        // If we have all partial signatures, we should be able to get the
231
        // complete signature now. We also remove this session from memory since
232
        // there is nothing more left to do.
233
        if session.HaveAllSigs {
×
234
                finalSig = session.session.FinalSig()
×
235
                m.musig2Sessions.Delete(sessionID)
×
236
        }
×
237

238
        return finalSig, session.HaveAllSigs, nil
×
239
}
240

241
// MuSig2Cleanup removes a session from memory to free up resources.
242
func (m *MusigSessionManager) MuSig2Cleanup(sessionID MuSig2SessionID) error {
×
243
        // We hold the lock during the whole operation, we don't want any
×
244
        // interference with calls that might come through in parallel for the
×
245
        // same session.
×
246
        m.sessionMtx.Lock(sessionID)
×
247
        defer m.sessionMtx.Unlock(sessionID)
×
248

×
249
        _, ok := m.musig2Sessions.Load(sessionID)
×
250
        if !ok {
×
251
                return fmt.Errorf("session with ID %x not found", sessionID[:])
×
252
        }
×
253

254
        m.musig2Sessions.Delete(sessionID)
×
255

×
256
        return nil
×
257
}
258

259
// MuSig2RegisterNonces registers one or more public nonces of other signing
260
// participants for a session identified by its ID. This method returns true
261
// once we have all nonces for all other signing participants.
262
func (m *MusigSessionManager) MuSig2RegisterNonces(sessionID MuSig2SessionID,
263
        otherSignerNonces [][musig2.PubNonceSize]byte) (bool, error) {
×
264

×
265
        // We hold the lock during the whole operation, we don't want any
×
266
        // interference with calls that might come through in parallel for the
×
267
        // same session.
×
268
        m.sessionMtx.Lock(sessionID)
×
269
        defer m.sessionMtx.Unlock(sessionID)
×
270

×
271
        session, ok := m.musig2Sessions.Load(sessionID)
×
272
        if !ok {
×
273
                return false, fmt.Errorf("session with ID %x not found",
×
274
                        sessionID[:])
×
275
        }
×
276

277
        // Make sure we don't exceed the number of expected nonces as that would
278
        // indicate something is wrong with the signing setup.
279
        if session.HaveAllNonces {
×
280
                return true, fmt.Errorf("already have all nonces")
×
281
        }
×
282

283
        numSigners := len(session.context.SigningKeys())
×
284
        remainingNonces := numSigners - session.session.NumRegisteredNonces()
×
285
        if len(otherSignerNonces) > remainingNonces {
×
286
                return false, fmt.Errorf("only %d other nonces remaining but "+
×
287
                        "trying to register %d more", remainingNonces,
×
288
                        len(otherSignerNonces))
×
289
        }
×
290

291
        // Add all nonces we've learned so far.
292
        var err error
×
293
        for _, otherSignerNonce := range otherSignerNonces {
×
294
                session.HaveAllNonces, err = session.session.RegisterPubNonce(
×
295
                        otherSignerNonce,
×
296
                )
×
297
                if err != nil {
×
298
                        return false, fmt.Errorf("error registering other "+
×
299
                                "signer public nonce: %v", err)
×
300
                }
×
301
        }
302

303
        return session.HaveAllNonces, nil
×
304
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc