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

lightningnetwork / lnd / 14358372723

09 Apr 2025 01:26PM UTC coverage: 56.696% (-12.3%) from 69.037%
14358372723

Pull #9696

github

web-flow
Merge e2837e400 into 867d27d68
Pull Request #9696: Add `development_guidelines.md` for both human and machine

107055 of 188823 relevant lines covered (56.7%)

22721.56 hits per line

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

60.0
/accessman.go
1
package lnd
2

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

8
        "github.com/btcsuite/btcd/btcec/v2"
9
        "github.com/lightningnetwork/lnd/channeldb"
10
)
11

12
// accessMan is responsible for managing the server's access permissions.
13
type accessMan struct {
14
        cfg *accessManConfig
15

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

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

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

37
        // numRestricted tracks the number of peers with restricted access in
38
        // peerScores. This MUST be accessed with the banScoreMtx held.
39
        numRestricted int64
40
}
41

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

47
        // shouldDisconnect determines whether we should disconnect a peer or
48
        // not.
49
        shouldDisconnect func(*btcec.PublicKey) (bool, error)
50

51
        // maxRestrictedSlots is the number of restricted slots we'll allocate.
52
        maxRestrictedSlots int64
53
}
54

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

1✔
62
        counts, err := a.cfg.initAccessPerms()
1✔
63
        if err != nil {
1✔
64
                return nil, err
×
65
        }
×
66

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

1✔
72
        return a, nil
1✔
73
}
74

75
// assignPeerPerms assigns a new peer its permissions. This does not track the
76
// access in the maps. This is intentional.
77
func (a *accessMan) assignPeerPerms(remotePub *btcec.PublicKey) (
78
        peerAccessStatus, error) {
5✔
79

5✔
80
        // Default is restricted unless the below filters say otherwise.
5✔
81
        access := peerStatusRestricted
5✔
82

5✔
83
        shouldDisconnect, err := a.cfg.shouldDisconnect(remotePub)
5✔
84
        if err != nil {
5✔
85
                // Access is restricted here.
×
86
                return access, err
×
87
        }
×
88

89
        if shouldDisconnect {
5✔
90
                // Access is restricted here.
×
91
                return access, ErrGossiperBan
×
92
        }
×
93

94
        peerMapKey := string(remotePub.SerializeCompressed())
5✔
95

5✔
96
        // Lock banScoreMtx for reading so that we can update the banning maps
5✔
97
        // below.
5✔
98
        a.banScoreMtx.RLock()
5✔
99
        defer a.banScoreMtx.RUnlock()
5✔
100

5✔
101
        if count, found := a.peerCounts[peerMapKey]; found {
8✔
102
                if count.HasOpenOrClosedChan {
5✔
103
                        access = peerStatusProtected
2✔
104
                } else if count.PendingOpenCount != 0 {
4✔
105
                        access = peerStatusTemporary
1✔
106
                }
1✔
107
        }
108

109
        // If we've reached this point and access hasn't changed from
110
        // restricted, then we need to check if we even have a slot for this
111
        // peer.
112
        if a.numRestricted >= a.cfg.maxRestrictedSlots &&
5✔
113
                access == peerStatusRestricted {
5✔
114

×
115
                return access, ErrNoMoreRestrictedAccessSlots
×
116
        }
×
117

118
        return access, nil
5✔
119
}
120

121
// newPendingOpenChan is called after the pending-open channel has been
122
// committed to the database. This may transition a restricted-access peer to a
123
// temporary-access peer.
124
func (a *accessMan) newPendingOpenChan(remotePub *btcec.PublicKey) error {
1✔
125
        a.banScoreMtx.Lock()
1✔
126
        defer a.banScoreMtx.Unlock()
1✔
127

1✔
128
        peerMapKey := string(remotePub.SerializeCompressed())
1✔
129

1✔
130
        // Fetch the peer's access status from peerScores.
1✔
131
        status, found := a.peerScores[peerMapKey]
1✔
132
        if !found {
1✔
133
                // If we didn't find the peer, we'll return an error.
×
134
                return ErrNoPeerScore
×
135
        }
×
136

137
        switch status.state {
1✔
138
        case peerStatusProtected:
×
139
                // If this peer's access status is protected, we don't need to
×
140
                // do anything.
×
141
                return nil
×
142

143
        case peerStatusTemporary:
×
144
                // If this peer's access status is temporary, we'll need to
×
145
                // update the peerCounts map. The peer's access status will
×
146
                // stay temporary.
×
147
                peerCount, found := a.peerCounts[peerMapKey]
×
148
                if !found {
×
149
                        // Error if we did not find any info in peerCounts.
×
150
                        return ErrNoPendingPeerInfo
×
151
                }
×
152

153
                // Increment the pending channel amount.
154
                peerCount.PendingOpenCount += 1
×
155
                a.peerCounts[peerMapKey] = peerCount
×
156

157
        case peerStatusRestricted:
1✔
158
                // If the peer's access status is restricted, then we can
1✔
159
                // transition it to a temporary-access peer. We'll need to
1✔
160
                // update numRestricted and also peerScores. We'll also need to
1✔
161
                // update peerCounts.
1✔
162
                peerCount := channeldb.ChanCount{
1✔
163
                        HasOpenOrClosedChan: false,
1✔
164
                        PendingOpenCount:    1,
1✔
165
                }
1✔
166

1✔
167
                a.peerCounts[peerMapKey] = peerCount
1✔
168

1✔
169
                // A restricted-access slot has opened up.
1✔
170
                a.numRestricted -= 1
1✔
171

1✔
172
                a.peerScores[peerMapKey] = peerSlotStatus{
1✔
173
                        state: peerStatusTemporary,
1✔
174
                }
1✔
175

176
        default:
×
177
                // This should not be possible.
×
178
                return fmt.Errorf("invalid peer access status")
×
179
        }
180

181
        return nil
1✔
182
}
183

184
// newPendingCloseChan is called when a pending-open channel prematurely closes
185
// before the funding transaction has confirmed. This potentially demotes a
186
// temporary-access peer to a restricted-access peer. If no restricted-access
187
// slots are available, the peer will be disconnected.
188
func (a *accessMan) newPendingCloseChan(remotePub *btcec.PublicKey) error {
1✔
189
        a.banScoreMtx.Lock()
1✔
190
        defer a.banScoreMtx.Unlock()
1✔
191

1✔
192
        peerMapKey := string(remotePub.SerializeCompressed())
1✔
193

1✔
194
        // Fetch the peer's access status from peerScores.
1✔
195
        status, found := a.peerScores[peerMapKey]
1✔
196
        if !found {
1✔
197
                return ErrNoPeerScore
×
198
        }
×
199

200
        switch status.state {
1✔
201
        case peerStatusProtected:
×
202
                // If this peer is protected, we don't do anything.
×
203
                return nil
×
204

205
        case peerStatusTemporary:
1✔
206
                // If this peer is temporary, we need to check if it will
1✔
207
                // revert to a restricted-access peer.
1✔
208
                peerCount, found := a.peerCounts[peerMapKey]
1✔
209
                if !found {
1✔
210
                        // Error if we did not find any info in peerCounts.
×
211
                        return ErrNoPendingPeerInfo
×
212
                }
×
213

214
                currentNumPending := peerCount.PendingOpenCount - 1
1✔
215
                if currentNumPending == 0 {
2✔
216
                        // Remove the entry from peerCounts.
1✔
217
                        delete(a.peerCounts, peerMapKey)
1✔
218

1✔
219
                        // If this is the only pending-open channel for this
1✔
220
                        // peer and it's getting removed, attempt to demote
1✔
221
                        // this peer to a restricted peer.
1✔
222
                        if a.numRestricted == a.cfg.maxRestrictedSlots {
2✔
223
                                // There are no available restricted slots, so
1✔
224
                                // we need to disconnect this peer. We leave
1✔
225
                                // this up to the caller.
1✔
226
                                return ErrNoMoreRestrictedAccessSlots
1✔
227
                        }
1✔
228

229
                        // Otherwise, there is an available restricted-access
230
                        // slot, so we can demote this peer.
231
                        a.peerScores[peerMapKey] = peerSlotStatus{
×
232
                                state: peerStatusRestricted,
×
233
                        }
×
234

×
235
                        // Update numRestricted.
×
236
                        a.numRestricted++
×
237

×
238
                        return nil
×
239
                }
240

241
                // Else, we don't need to demote this peer since it has other
242
                // pending-open channels with us.
243
                peerCount.PendingOpenCount = currentNumPending
×
244
                a.peerCounts[peerMapKey] = peerCount
×
245

×
246
                return nil
×
247

248
        case peerStatusRestricted:
×
249
                // This should not be possible. This indicates an error.
×
250
                return fmt.Errorf("invalid peer access state transition")
×
251

252
        default:
×
253
                // This should not be possible.
×
254
                return fmt.Errorf("invalid peer access status")
×
255
        }
256
}
257

258
// newOpenChan is called when a pending-open channel becomes an open channel
259
// (i.e. the funding transaction has confirmed). If the remote peer is a
260
// temporary-access peer, it will be promoted to a protected-access peer.
261
func (a *accessMan) newOpenChan(remotePub *btcec.PublicKey) error {
1✔
262
        a.banScoreMtx.Lock()
1✔
263
        defer a.banScoreMtx.Unlock()
1✔
264

1✔
265
        peerMapKey := string(remotePub.SerializeCompressed())
1✔
266

1✔
267
        // Fetch the peer's access status from peerScores.
1✔
268
        status, found := a.peerScores[peerMapKey]
1✔
269
        if !found {
1✔
270
                // If we didn't find the peer, we'll return an error.
×
271
                return ErrNoPeerScore
×
272
        }
×
273

274
        switch status.state {
1✔
275
        case peerStatusProtected:
×
276
                // If the peer's state is already protected, we don't need to
×
277
                // do anything more.
×
278
                return nil
×
279

280
        case peerStatusTemporary:
1✔
281
                // If the peer's state is temporary, we'll upgrade the peer to
1✔
282
                // a protected peer.
1✔
283
                peerCount, found := a.peerCounts[peerMapKey]
1✔
284
                if !found {
1✔
285
                        // Error if we did not find any info in peerCounts.
×
286
                        return ErrNoPendingPeerInfo
×
287
                }
×
288

289
                peerCount.HasOpenOrClosedChan = true
1✔
290
                a.peerCounts[peerMapKey] = peerCount
1✔
291

1✔
292
                newStatus := peerSlotStatus{
1✔
293
                        state: peerStatusProtected,
1✔
294
                }
1✔
295
                a.peerScores[peerMapKey] = newStatus
1✔
296

1✔
297
                return nil
1✔
298

299
        case peerStatusRestricted:
×
300
                // This should not be possible. For the server to receive a
×
301
                // state-transition event via NewOpenChan, the server must have
×
302
                // previously granted this peer "temporary" access. This
×
303
                // temporary access would not have been revoked or downgraded
×
304
                // without `CloseChannel` being called with the pending
×
305
                // argument set to true. This means that an open-channel state
×
306
                // transition would be impossible. Therefore, we can return an
×
307
                // error.
×
308
                return fmt.Errorf("invalid peer access status")
×
309

310
        default:
×
311
                // This should not be possible.
×
312
                return fmt.Errorf("invalid peer access status")
×
313
        }
314
}
315

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

5✔
323
        a.banScoreMtx.RLock()
5✔
324
        defer a.banScoreMtx.RUnlock()
5✔
325

5✔
326
        peerMapKey := string(remotePub.SerializeCompressed())
5✔
327

5✔
328
        if _, found := a.peerCounts[peerMapKey]; !found {
7✔
329
                // Check numRestricted to see if there is an available slot. In
2✔
330
                // the future, it's possible to add better heuristics.
2✔
331
                if a.numRestricted < a.cfg.maxRestrictedSlots {
4✔
332
                        // There is an available slot.
2✔
333
                        return true, nil
2✔
334
                }
2✔
335

336
                // If there are no slots left, then we reject this connection.
337
                return false, ErrNoMoreRestrictedAccessSlots
×
338
        }
339

340
        // Else, the peer is either protected or temporary.
341
        return true, nil
3✔
342
}
343

344
// addPeerAccess tracks a peer's access in the maps. This should be called when
345
// the peer has fully connected.
346
func (a *accessMan) addPeerAccess(remotePub *btcec.PublicKey,
347
        access peerAccessStatus) {
5✔
348

5✔
349
        // Add the remote public key to peerScores.
5✔
350
        a.banScoreMtx.Lock()
5✔
351
        defer a.banScoreMtx.Unlock()
5✔
352

5✔
353
        peerMapKey := string(remotePub.SerializeCompressed())
5✔
354

5✔
355
        a.peerScores[peerMapKey] = peerSlotStatus{state: access}
5✔
356

5✔
357
        // Increment numRestricted.
5✔
358
        if access == peerStatusRestricted {
7✔
359
                a.numRestricted++
2✔
360
        }
2✔
361
}
362

363
// removePeerAccess removes the peer's access from the maps. This should be
364
// called when the peer has been disconnected.
365
func (a *accessMan) removePeerAccess(remotePub *btcec.PublicKey) {
×
366
        a.banScoreMtx.Lock()
×
367
        defer a.banScoreMtx.Unlock()
×
368

×
369
        peerMapKey := string(remotePub.SerializeCompressed())
×
370

×
371
        status, found := a.peerScores[peerMapKey]
×
372
        if !found {
×
373
                return
×
374
        }
×
375

376
        if status.state == peerStatusRestricted {
×
377
                // If the status is restricted, then we decrement from
×
378
                // numRestrictedSlots.
×
379
                a.numRestricted--
×
380
        }
×
381

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