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

lightningnetwork / lnd / 15635919293

13 Jun 2025 01:37PM UTC coverage: 56.351% (-2.0%) from 58.333%
15635919293

Pull #9903

github

web-flow
Merge 174181006 into 35102e7c3
Pull Request #9903: docs: add sphinx replay description

108065 of 191770 relevant lines covered (56.35%)

22781.11 hits per line

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

59.09
/accessman.go
1
package lnd
2

3
import (
4
        "context"
5
        "fmt"
6
        "maps"
7
        "sync"
8

9
        "github.com/btcsuite/btcd/btcec/v2"
10
        "github.com/btcsuite/btclog/v2"
11
        "github.com/lightningnetwork/lnd/channeldb"
12
        "github.com/lightningnetwork/lnd/lnutils"
13
)
14

15
// accessMan is responsible for managing the server's access permissions.
16
type accessMan struct {
17
        cfg *accessManConfig
18

19
        // banScoreMtx is used for the server's ban tracking. If the server
20
        // mutex is also going to be locked, ensure that this is locked after
21
        // the server mutex.
22
        banScoreMtx sync.RWMutex
23

24
        // peerCounts is a mapping from remote public key to {bool, uint64}
25
        // where the bool indicates that we have an open/closed channel with
26
        // the peer and where the uint64 indicates the number of pending-open
27
        // channels we currently have with them. This mapping will be used to
28
        // determine access permissions for the peer. The map key is the
29
        // string-version of the serialized public key.
30
        //
31
        // NOTE: This MUST be accessed with the banScoreMtx held.
32
        peerCounts map[string]channeldb.ChanCount
33

34
        // peerScores stores each connected peer's access status. The map key
35
        // is the string-version of the serialized public key.
36
        //
37
        // NOTE: This MUST be accessed with the banScoreMtx held.
38
        peerScores map[string]peerSlotStatus
39

40
        // numRestricted tracks the number of peers with restricted access in
41
        // peerScores. This MUST be accessed with the banScoreMtx held.
42
        numRestricted int64
43
}
44

45
type accessManConfig struct {
46
        // initAccessPerms checks the channeldb for initial access permissions
47
        // and then populates the peerCounts and peerScores maps.
48
        initAccessPerms func() (map[string]channeldb.ChanCount, error)
49

50
        // shouldDisconnect determines whether we should disconnect a peer or
51
        // not.
52
        shouldDisconnect func(*btcec.PublicKey) (bool, error)
53

54
        // maxRestrictedSlots is the number of restricted slots we'll allocate.
55
        maxRestrictedSlots int64
56
}
57

58
func newAccessMan(cfg *accessManConfig) (*accessMan, error) {
10✔
59
        a := &accessMan{
10✔
60
                cfg:        cfg,
10✔
61
                peerCounts: make(map[string]channeldb.ChanCount),
10✔
62
                peerScores: make(map[string]peerSlotStatus),
10✔
63
        }
10✔
64

10✔
65
        counts, err := a.cfg.initAccessPerms()
10✔
66
        if err != nil {
10✔
67
                return nil, err
×
68
        }
×
69

70
        // We'll populate the server's peerCounts map with the counts fetched
71
        // via initAccessPerms. Also note that we haven't yet connected to the
72
        // peers.
73
        maps.Copy(a.peerCounts, counts)
10✔
74

10✔
75
        acsmLog.Info("Access Manager initialized")
10✔
76

10✔
77
        return a, nil
10✔
78
}
79

80
// assignPeerPerms assigns a new peer its permissions. This does not track the
81
// access in the maps. This is intentional.
82
func (a *accessMan) assignPeerPerms(remotePub *btcec.PublicKey) (
83
        peerAccessStatus, error) {
14✔
84

14✔
85
        ctx := btclog.WithCtx(
14✔
86
                context.TODO(), lnutils.LogPubKey("peer", remotePub),
14✔
87
        )
14✔
88

14✔
89
        peerMapKey := string(remotePub.SerializeCompressed())
14✔
90

14✔
91
        acsmLog.DebugS(ctx, "Assigning permissions")
14✔
92

14✔
93
        // Default is restricted unless the below filters say otherwise.
14✔
94
        access := peerStatusRestricted
14✔
95

14✔
96
        // Lock banScoreMtx for reading so that we can update the banning maps
14✔
97
        // below.
14✔
98
        a.banScoreMtx.RLock()
14✔
99
        if count, found := a.peerCounts[peerMapKey]; found {
26✔
100
                if count.HasOpenOrClosedChan {
18✔
101
                        acsmLog.DebugS(ctx, "Peer has open/closed channel, "+
6✔
102
                                "assigning protected access")
6✔
103

6✔
104
                        access = peerStatusProtected
6✔
105
                } else if count.PendingOpenCount != 0 {
15✔
106
                        acsmLog.DebugS(ctx, "Peer has pending channel(s), "+
3✔
107
                                "assigning temporary access")
3✔
108

3✔
109
                        access = peerStatusTemporary
3✔
110
                }
3✔
111
        }
112
        a.banScoreMtx.RUnlock()
14✔
113

14✔
114
        // Exit early if the peer status is no longer restricted.
14✔
115
        if access != peerStatusRestricted {
23✔
116
                return access, nil
9✔
117
        }
9✔
118

119
        // Check whether this peer is banned.
120
        shouldDisconnect, err := a.cfg.shouldDisconnect(remotePub)
5✔
121
        if err != nil {
5✔
122
                acsmLog.ErrorS(ctx, "Error checking disconnect status", err)
×
123

×
124
                // Access is restricted here.
×
125
                return access, err
×
126
        }
×
127

128
        if shouldDisconnect {
6✔
129
                acsmLog.WarnS(ctx, "Peer is banned, assigning restricted access",
1✔
130
                        ErrGossiperBan)
1✔
131

1✔
132
                // Access is restricted here.
1✔
133
                return access, ErrGossiperBan
1✔
134
        }
1✔
135

136
        // If we've reached this point and access hasn't changed from
137
        // restricted, then we need to check if we even have a slot for this
138
        // peer.
139
        acsmLog.DebugS(ctx, "Peer has no channels, assigning restricted access")
4✔
140

4✔
141
        a.banScoreMtx.RLock()
4✔
142
        defer a.banScoreMtx.RUnlock()
4✔
143

4✔
144
        if a.numRestricted >= a.cfg.maxRestrictedSlots {
5✔
145
                acsmLog.WarnS(ctx, "No more restricted slots available, "+
1✔
146
                        "denying peer", ErrNoMoreRestrictedAccessSlots,
1✔
147
                        "num_restricted", a.numRestricted, "max_restricted",
1✔
148
                        a.cfg.maxRestrictedSlots)
1✔
149

1✔
150
                return access, ErrNoMoreRestrictedAccessSlots
1✔
151
        }
1✔
152

153
        return access, nil
3✔
154
}
155

156
// newPendingOpenChan is called after the pending-open channel has been
157
// committed to the database. This may transition a restricted-access peer to a
158
// temporary-access peer.
159
func (a *accessMan) newPendingOpenChan(remotePub *btcec.PublicKey) error {
1✔
160
        a.banScoreMtx.Lock()
1✔
161
        defer a.banScoreMtx.Unlock()
1✔
162

1✔
163
        ctx := btclog.WithCtx(
1✔
164
                context.TODO(), lnutils.LogPubKey("peer", remotePub),
1✔
165
        )
1✔
166

1✔
167
        acsmLog.DebugS(ctx, "Processing new pending open channel")
1✔
168

1✔
169
        peerMapKey := string(remotePub.SerializeCompressed())
1✔
170

1✔
171
        // Fetch the peer's access status from peerScores.
1✔
172
        status, found := a.peerScores[peerMapKey]
1✔
173
        if !found {
1✔
174
                acsmLog.ErrorS(ctx, "Peer score not found", ErrNoPeerScore)
×
175

×
176
                // If we didn't find the peer, we'll return an error.
×
177
                return ErrNoPeerScore
×
178
        }
×
179

180
        switch status.state {
1✔
181
        case peerStatusProtected:
×
182
                acsmLog.DebugS(ctx, "Peer already protected, no change")
×
183

×
184
                // If this peer's access status is protected, we don't need to
×
185
                // do anything.
×
186
                return nil
×
187

188
        case peerStatusTemporary:
×
189
                // If this peer's access status is temporary, we'll need to
×
190
                // update the peerCounts map. The peer's access status will stay
×
191
                // temporary.
×
192
                peerCount, found := a.peerCounts[peerMapKey]
×
193
                if !found {
×
194
                        // Error if we did not find any info in peerCounts.
×
195
                        acsmLog.ErrorS(ctx, "Pending peer info not found",
×
196
                                ErrNoPendingPeerInfo)
×
197

×
198
                        return ErrNoPendingPeerInfo
×
199
                }
×
200

201
                // Increment the pending channel amount.
202
                peerCount.PendingOpenCount += 1
×
203
                a.peerCounts[peerMapKey] = peerCount
×
204

×
205
                acsmLog.DebugS(ctx, "Peer is temporary, incremented "+
×
206
                        "pending count",
×
207
                        "pending_count", peerCount.PendingOpenCount)
×
208

209
        case peerStatusRestricted:
1✔
210
                // If the peer's access status is restricted, then we can
1✔
211
                // transition it to a temporary-access peer. We'll need to
1✔
212
                // update numRestricted and also peerScores. We'll also need to
1✔
213
                // update peerCounts.
1✔
214
                peerCount := channeldb.ChanCount{
1✔
215
                        HasOpenOrClosedChan: false,
1✔
216
                        PendingOpenCount:    1,
1✔
217
                }
1✔
218

1✔
219
                a.peerCounts[peerMapKey] = peerCount
1✔
220

1✔
221
                // A restricted-access slot has opened up.
1✔
222
                oldRestricted := a.numRestricted
1✔
223
                a.numRestricted -= 1
1✔
224

1✔
225
                a.peerScores[peerMapKey] = peerSlotStatus{
1✔
226
                        state: peerStatusTemporary,
1✔
227
                }
1✔
228

1✔
229
                acsmLog.InfoS(ctx, "Peer transitioned restricted -> "+
1✔
230
                        "temporary (pending open)",
1✔
231
                        "old_restricted", oldRestricted,
1✔
232
                        "new_restricted", a.numRestricted)
1✔
233

234
        default:
×
235
                // This should not be possible.
×
236
                err := fmt.Errorf("invalid peer access status %v for %x",
×
237
                        status.state, peerMapKey)
×
238
                acsmLog.ErrorS(ctx, "Invalid peer access status", err)
×
239

×
240
                return err
×
241
        }
242

243
        return nil
1✔
244
}
245

246
// newPendingCloseChan is called when a pending-open channel prematurely closes
247
// before the funding transaction has confirmed. This potentially demotes a
248
// temporary-access peer to a restricted-access peer. If no restricted-access
249
// slots are available, the peer will be disconnected.
250
func (a *accessMan) newPendingCloseChan(remotePub *btcec.PublicKey) error {
1✔
251
        a.banScoreMtx.Lock()
1✔
252
        defer a.banScoreMtx.Unlock()
1✔
253

1✔
254
        ctx := btclog.WithCtx(
1✔
255
                context.TODO(), lnutils.LogPubKey("peer", remotePub),
1✔
256
        )
1✔
257

1✔
258
        acsmLog.DebugS(ctx, "Processing pending channel close")
1✔
259

1✔
260
        peerMapKey := string(remotePub.SerializeCompressed())
1✔
261

1✔
262
        // Fetch the peer's access status from peerScores.
1✔
263
        status, found := a.peerScores[peerMapKey]
1✔
264
        if !found {
1✔
265
                acsmLog.ErrorS(ctx, "Peer score not found", ErrNoPeerScore)
×
266

×
267
                return ErrNoPeerScore
×
268
        }
×
269

270
        switch status.state {
1✔
271
        case peerStatusProtected:
×
272
                // If this peer is protected, we don't do anything.
×
273
                acsmLog.DebugS(ctx, "Peer is protected, no change")
×
274

×
275
                return nil
×
276

277
        case peerStatusTemporary:
1✔
278
                // If this peer is temporary, we need to check if it will
1✔
279
                // revert to a restricted-access peer.
1✔
280
                peerCount, found := a.peerCounts[peerMapKey]
1✔
281
                if !found {
1✔
282
                        acsmLog.ErrorS(ctx, "Pending peer info not found",
×
283
                                ErrNoPendingPeerInfo)
×
284

×
285
                        // Error if we did not find any info in peerCounts.
×
286
                        return ErrNoPendingPeerInfo
×
287
                }
×
288

289
                currentNumPending := peerCount.PendingOpenCount - 1
1✔
290

1✔
291
                acsmLog.DebugS(ctx, "Peer is temporary, decrementing "+
1✔
292
                        "pending count",
1✔
293
                        "pending_count", currentNumPending)
1✔
294

1✔
295
                if currentNumPending == 0 {
2✔
296
                        // Remove the entry from peerCounts.
1✔
297
                        delete(a.peerCounts, peerMapKey)
1✔
298

1✔
299
                        // If this is the only pending-open channel for this
1✔
300
                        // peer and it's getting removed, attempt to demote
1✔
301
                        // this peer to a restricted peer.
1✔
302
                        if a.numRestricted == a.cfg.maxRestrictedSlots {
2✔
303
                                // There are no available restricted slots, so
1✔
304
                                // we need to disconnect this peer. We leave
1✔
305
                                // this up to the caller.
1✔
306
                                acsmLog.WarnS(ctx, "Peer last pending "+
1✔
307
                                        "channel closed: ",
1✔
308
                                        ErrNoMoreRestrictedAccessSlots,
1✔
309
                                        "num_restricted", a.numRestricted,
1✔
310
                                        "max_restricted", a.cfg.maxRestrictedSlots)
1✔
311

1✔
312
                                return ErrNoMoreRestrictedAccessSlots
1✔
313
                        }
1✔
314

315
                        // Otherwise, there is an available restricted-access
316
                        // slot, so we can demote this peer.
317
                        a.peerScores[peerMapKey] = peerSlotStatus{
×
318
                                state: peerStatusRestricted,
×
319
                        }
×
320

×
321
                        // Update numRestricted.
×
322
                        oldRestricted := a.numRestricted
×
323
                        a.numRestricted++
×
324

×
325
                        acsmLog.InfoS(ctx, "Peer transitioned "+
×
326
                                "temporary -> restricted "+
×
327
                                "(last pending closed)",
×
328
                                "old_restricted", oldRestricted,
×
329
                                "new_restricted", a.numRestricted)
×
330

×
331
                        return nil
×
332
                }
333

334
                // Else, we don't need to demote this peer since it has other
335
                // pending-open channels with us.
336
                peerCount.PendingOpenCount = currentNumPending
×
337
                a.peerCounts[peerMapKey] = peerCount
×
338

×
339
                acsmLog.DebugS(ctx, "Peer still has other pending channels",
×
340
                        "pending_count", currentNumPending)
×
341

×
342
                return nil
×
343

344
        case peerStatusRestricted:
×
345
                // This should not be possible. This indicates an error.
×
346
                err := fmt.Errorf("invalid peer access state transition: "+
×
347
                        "pending close for restricted peer %x", peerMapKey)
×
348
                acsmLog.ErrorS(ctx, "Invalid peer access state transition", err)
×
349

×
350
                return err
×
351

352
        default:
×
353
                // This should not be possible.
×
354
                err := fmt.Errorf("invalid peer access status %v for %x",
×
355
                        status.state, peerMapKey)
×
356
                acsmLog.ErrorS(ctx, "Invalid peer access status", err)
×
357

×
358
                return err
×
359
        }
360
}
361

362
// newOpenChan is called when a pending-open channel becomes an open channel
363
// (i.e. the funding transaction has confirmed). If the remote peer is a
364
// temporary-access peer, it will be promoted to a protected-access peer.
365
func (a *accessMan) newOpenChan(remotePub *btcec.PublicKey) error {
1✔
366
        a.banScoreMtx.Lock()
1✔
367
        defer a.banScoreMtx.Unlock()
1✔
368

1✔
369
        ctx := btclog.WithCtx(
1✔
370
                context.TODO(), lnutils.LogPubKey("peer", remotePub),
1✔
371
        )
1✔
372

1✔
373
        acsmLog.DebugS(ctx, "Processing new open channel")
1✔
374

1✔
375
        peerMapKey := string(remotePub.SerializeCompressed())
1✔
376

1✔
377
        // Fetch the peer's access status from peerScores.
1✔
378
        status, found := a.peerScores[peerMapKey]
1✔
379
        if !found {
1✔
380
                // If we didn't find the peer, we'll return an error.
×
381
                acsmLog.ErrorS(ctx, "Peer score not found", ErrNoPeerScore)
×
382

×
383
                return ErrNoPeerScore
×
384
        }
×
385

386
        switch status.state {
1✔
387
        case peerStatusProtected:
×
388
                acsmLog.DebugS(ctx, "Peer already protected, no change")
×
389

×
390
                // If the peer's state is already protected, we don't need to do
×
391
                // anything more.
×
392
                return nil
×
393

394
        case peerStatusTemporary:
1✔
395
                // If the peer's state is temporary, we'll upgrade the peer to
1✔
396
                // a protected peer.
1✔
397
                peerCount, found := a.peerCounts[peerMapKey]
1✔
398
                if !found {
1✔
399
                        // Error if we did not find any info in peerCounts.
×
400
                        acsmLog.ErrorS(ctx, "Pending peer info not found",
×
401
                                ErrNoPendingPeerInfo)
×
402

×
403
                        return ErrNoPendingPeerInfo
×
404
                }
×
405

406
                peerCount.HasOpenOrClosedChan = true
1✔
407
                a.peerCounts[peerMapKey] = peerCount
1✔
408

1✔
409
                newStatus := peerSlotStatus{
1✔
410
                        state: peerStatusProtected,
1✔
411
                }
1✔
412
                a.peerScores[peerMapKey] = newStatus
1✔
413

1✔
414
                acsmLog.InfoS(ctx, "Peer transitioned temporary -> "+
1✔
415
                        "protected (channel opened)")
1✔
416

1✔
417
                return nil
1✔
418

419
        case peerStatusRestricted:
×
420
                // This should not be possible. For the server to receive a
×
421
                // state-transition event via NewOpenChan, the server must have
×
422
                // previously granted this peer "temporary" access. This
×
423
                // temporary access would not have been revoked or downgraded
×
424
                // without `CloseChannel` being called with the pending
×
425
                // argument set to true. This means that an open-channel state
×
426
                // transition would be impossible. Therefore, we can return an
×
427
                // error.
×
428
                err := fmt.Errorf("invalid peer access status: new open "+
×
429
                        "channel for restricted peer %x", peerMapKey)
×
430

×
431
                acsmLog.ErrorS(ctx, "Invalid peer access status", err)
×
432

×
433
                return err
×
434

435
        default:
×
436
                // This should not be possible.
×
437
                err := fmt.Errorf("invalid peer access status %v for %x",
×
438
                        status.state, peerMapKey)
×
439

×
440
                acsmLog.ErrorS(ctx, "Invalid peer access status", err)
×
441

×
442
                return err
×
443
        }
444
}
445

446
// checkIncomingConnBanScore checks whether, given the remote's public hex-
447
// encoded key, we should not accept this incoming connection or immediately
448
// disconnect. This does not assign to the server's peerScores maps. This is
449
// just an inbound filter that the brontide listeners use.
450
func (a *accessMan) checkIncomingConnBanScore(remotePub *btcec.PublicKey) (
451
        bool, error) {
5✔
452

5✔
453
        ctx := btclog.WithCtx(
5✔
454
                context.TODO(), lnutils.LogPubKey("peer", remotePub),
5✔
455
        )
5✔
456

5✔
457
        peerMapKey := string(remotePub.SerializeCompressed())
5✔
458

5✔
459
        acsmLog.TraceS(ctx, "Checking incoming connection ban score")
5✔
460

5✔
461
        a.banScoreMtx.RLock()
5✔
462
        defer a.banScoreMtx.RUnlock()
5✔
463

5✔
464
        if _, found := a.peerCounts[peerMapKey]; !found {
7✔
465
                acsmLog.DebugS(ctx, "Peer not found in counts, "+
2✔
466
                        "checking restricted slots")
2✔
467

2✔
468
                // Check numRestricted to see if there is an available slot. In
2✔
469
                // the future, it's possible to add better heuristics.
2✔
470
                if a.numRestricted < a.cfg.maxRestrictedSlots {
4✔
471
                        // There is an available slot.
2✔
472
                        acsmLog.DebugS(ctx, "Restricted slot available, "+
2✔
473
                                "accepting",
2✔
474
                                "num_restricted", a.numRestricted,
2✔
475
                                "max_restricted", a.cfg.maxRestrictedSlots)
2✔
476

2✔
477
                        return true, nil
2✔
478
                }
2✔
479

480
                // If there are no slots left, then we reject this connection.
481
                acsmLog.WarnS(ctx, "No restricted slots available, "+
×
482
                        "rejecting",
×
483
                        ErrNoMoreRestrictedAccessSlots,
×
484
                        "num_restricted", a.numRestricted,
×
485
                        "max_restricted", a.cfg.maxRestrictedSlots)
×
486

×
487
                return false, ErrNoMoreRestrictedAccessSlots
×
488
        }
489

490
        // Else, the peer is either protected or temporary.
491
        acsmLog.DebugS(ctx, "Peer found (protected/temporary), accepting")
3✔
492

3✔
493
        return true, nil
3✔
494
}
495

496
// addPeerAccess tracks a peer's access in the maps. This should be called when
497
// the peer has fully connected.
498
func (a *accessMan) addPeerAccess(remotePub *btcec.PublicKey,
499
        access peerAccessStatus) {
5✔
500

5✔
501
        ctx := btclog.WithCtx(
5✔
502
                context.TODO(), lnutils.LogPubKey("peer", remotePub),
5✔
503
        )
5✔
504

5✔
505
        acsmLog.DebugS(ctx, "Adding peer access", "access", access)
5✔
506

5✔
507
        // Add the remote public key to peerScores.
5✔
508
        a.banScoreMtx.Lock()
5✔
509
        defer a.banScoreMtx.Unlock()
5✔
510

5✔
511
        peerMapKey := string(remotePub.SerializeCompressed())
5✔
512

5✔
513
        a.peerScores[peerMapKey] = peerSlotStatus{state: access}
5✔
514

5✔
515
        // Increment numRestricted.
5✔
516
        if access == peerStatusRestricted {
7✔
517
                oldRestricted := a.numRestricted
2✔
518
                a.numRestricted++
2✔
519

2✔
520
                acsmLog.DebugS(ctx, "Incremented restricted slots",
2✔
521
                        "old_restricted", oldRestricted,
2✔
522
                        "new_restricted", a.numRestricted)
2✔
523
        }
2✔
524
}
525

526
// removePeerAccess removes the peer's access from the maps. This should be
527
// called when the peer has been disconnected.
528
func (a *accessMan) removePeerAccess(remotePub *btcec.PublicKey) {
×
529
        a.banScoreMtx.Lock()
×
530
        defer a.banScoreMtx.Unlock()
×
531

×
532
        ctx := btclog.WithCtx(
×
533
                context.TODO(), lnutils.LogPubKey("peer", remotePub),
×
534
        )
×
535

×
536
        acsmLog.DebugS(ctx, "Removing peer access")
×
537

×
538
        peerMapKey := string(remotePub.SerializeCompressed())
×
539

×
540
        status, found := a.peerScores[peerMapKey]
×
541
        if !found {
×
542
                acsmLog.InfoS(ctx, "Peer score not found during removal")
×
543
                return
×
544
        }
×
545

546
        if status.state == peerStatusRestricted {
×
547
                // If the status is restricted, then we decrement from
×
548
                // numRestrictedSlots.
×
549
                oldRestricted := a.numRestricted
×
550
                a.numRestricted--
×
551

×
552
                acsmLog.DebugS(ctx, "Decremented restricted slots",
×
553
                        "old_restricted", oldRestricted,
×
554
                        "new_restricted", a.numRestricted)
×
555
        }
×
556

557
        acsmLog.TraceS(ctx, "Deleting peer from peerScores")
×
558

×
559
        delete(a.peerScores, peerMapKey)
×
560
}
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