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

lightningnetwork / lnd / 12312390362

13 Dec 2024 08:44AM UTC coverage: 57.458% (+8.5%) from 48.92%
12312390362

Pull #9343

github

ellemouton
fn: rework the ContextGuard and add tests

In this commit, the ContextGuard struct is re-worked such that the
context that its new main WithCtx method provides is cancelled in sync
with a parent context being cancelled or with it's quit channel being
cancelled. Tests are added to assert the behaviour. In order for the
close of the quit channel to be consistent with the cancelling of the
derived context, the quit channel _must_ be contained internal to the
ContextGuard so that callers are only able to close the channel via the
exposed Quit method which will then take care to first cancel any
derived context that depend on the quit channel before returning.
Pull Request #9343: fn: expand the ContextGuard and add tests

101853 of 177264 relevant lines covered (57.46%)

24972.93 hits per line

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

54.02
/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 {
750✔
48
        return &MusigSessionManager{
750✔
49
                keyFetcher: keyFetcher,
750✔
50
                musig2Sessions: &lnutils.SyncMap[
750✔
51
                        MuSig2SessionID, *MuSig2State,
750✔
52
                ]{},
750✔
53
                sessionMtx: multimutex.NewMutex[MuSig2SessionID](),
750✔
54
        }
750✔
55
}
750✔
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) {
219✔
70

219✔
71
        // We need to derive the private key for signing. In the remote signing
219✔
72
        // setup, this whole RPC call will be forwarded to the signing
219✔
73
        // instance, which requires it to be stateful.
219✔
74
        privKey, err := m.keyFetcher(&keychain.KeyDescriptor{
219✔
75
                KeyLocator: keyLoc,
219✔
76
        })
219✔
77
        if err != nil {
219✔
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(
219✔
84
                bipVersion, privKey, allSignerPubKeys, tweaks, localNonces,
219✔
85
        )
219✔
86
        if err != nil {
219✔
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
219✔
93
        for _, otherSignerNonce := range otherSignerNonces {
438✔
94
                haveAllNonces, err = musigSession.RegisterPubNonce(
219✔
95
                        otherSignerNonce,
219✔
96
                )
219✔
97
                if err != nil {
219✔
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()
219✔
105
        if err != nil {
219✔
106
                return nil, fmt.Errorf("error getting combined key: %w", err)
×
107
        }
×
108
        session := &MuSig2State{
219✔
109
                MuSig2SessionInfo: MuSig2SessionInfo{
219✔
110
                        SessionID: NewMuSig2SessionID(
219✔
111
                                combinedKey, musigSession.PublicNonce(),
219✔
112
                        ),
219✔
113
                        Version:       bipVersion,
219✔
114
                        PublicNonce:   musigSession.PublicNonce(),
219✔
115
                        CombinedKey:   combinedKey,
219✔
116
                        TaprootTweak:  tweaks.HasTaprootTweak(),
219✔
117
                        HaveAllNonces: haveAllNonces,
219✔
118
                },
219✔
119
                context: musigContext,
219✔
120
                session: musigSession,
219✔
121
        }
219✔
122

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

219✔
142
        return &session.MuSig2SessionInfo, nil
219✔
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) {
119✔
154

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

119✔
161
        session, ok := m.musig2Sessions.Load(sessionID)
119✔
162
        if !ok {
119✔
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 {
119✔
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)
119✔
176
        if err != nil {
119✔
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 {
119✔
182
                m.musig2Sessions.Delete(sessionID)
×
183
        }
×
184

185
        return partialSig, nil
119✔
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) {
8✔
195

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

8✔
202
        session, ok := m.musig2Sessions.Load(sessionID)
8✔
203
        if !ok {
8✔
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 {
8✔
211
                return nil, true, fmt.Errorf("already have all partial" +
×
212
                        "signatures")
×
213
        }
×
214

215
        // Add all sigs we got so far.
216
        var (
8✔
217
                finalSig *schnorr.Signature
8✔
218
                err      error
8✔
219
        )
8✔
220
        for _, otherPartialSig := range partialSigs {
16✔
221
                session.HaveAllSigs, err = MuSig2CombineSig(
8✔
222
                        session.session, otherPartialSig,
8✔
223
                )
8✔
224
                if err != nil {
8✔
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 {
16✔
234
                finalSig = session.session.FinalSig()
8✔
235
                m.musig2Sessions.Delete(sessionID)
8✔
236
        }
8✔
237

238
        return finalSig, session.HaveAllSigs, nil
8✔
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