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

lightningnetwork / lnd / 14193549836

01 Apr 2025 10:40AM UTC coverage: 69.046% (+0.007%) from 69.039%
14193549836

Pull #9665

github

web-flow
Merge e8825f209 into b01f4e514
Pull Request #9665: kvdb: bump etcd libs to v3.5.12

133439 of 193262 relevant lines covered (69.05%)

22119.45 hits per line

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

77.14
/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) {
4✔
56
        a := &accessMan{
4✔
57
                cfg:        cfg,
4✔
58
                peerCounts: make(map[string]channeldb.ChanCount),
4✔
59
                peerScores: make(map[string]peerSlotStatus),
4✔
60
        }
4✔
61

4✔
62
        counts, err := a.cfg.initAccessPerms()
4✔
63
        if err != nil {
4✔
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)
4✔
71

4✔
72
        return a, nil
4✔
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) {
8✔
79

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

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

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

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

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

8✔
101
        if count, found := a.peerCounts[peerMapKey]; found {
14✔
102
                if count.HasOpenOrClosedChan {
11✔
103
                        access = peerStatusProtected
5✔
104
                } else if count.PendingOpenCount != 0 {
13✔
105
                        access = peerStatusTemporary
4✔
106
                }
4✔
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 &&
8✔
113
                access == peerStatusRestricted {
8✔
114

×
115
                return access, ErrNoMoreRestrictedAccessSlots
×
116
        }
×
117

118
        return access, nil
8✔
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 {
4✔
125
        a.banScoreMtx.Lock()
4✔
126
        defer a.banScoreMtx.Unlock()
4✔
127

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

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

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

143
        case peerStatusTemporary:
3✔
144
                // If this peer's access status is temporary, we'll need to
3✔
145
                // update the peerCounts map. The peer's access status will
3✔
146
                // stay temporary.
3✔
147
                peerCount, found := a.peerCounts[peerMapKey]
3✔
148
                if !found {
3✔
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
3✔
155
                a.peerCounts[peerMapKey] = peerCount
3✔
156

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

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

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

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

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

181
        return nil
4✔
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 {
4✔
189
        a.banScoreMtx.Lock()
4✔
190
        defer a.banScoreMtx.Unlock()
4✔
191

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

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

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

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

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

4✔
219
                        // If this is the only pending-open channel for this
4✔
220
                        // peer and it's getting removed, attempt to demote
4✔
221
                        // this peer to a restricted peer.
4✔
222
                        if a.numRestricted == a.cfg.maxRestrictedSlots {
5✔
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{
3✔
232
                                state: peerStatusRestricted,
3✔
233
                        }
3✔
234

3✔
235
                        // Update numRestricted.
3✔
236
                        a.numRestricted++
3✔
237

3✔
238
                        return nil
3✔
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 {
4✔
262
        a.banScoreMtx.Lock()
4✔
263
        defer a.banScoreMtx.Unlock()
4✔
264

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

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

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

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

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

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

4✔
297
                return nil
4✔
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) {
8✔
322

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

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

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

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

340
        // Else, the peer is either protected or temporary.
341
        return true, nil
6✔
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) {
8✔
348

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

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

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

8✔
357
        // Increment numRestricted.
8✔
358
        if access == peerStatusRestricted {
13✔
359
                a.numRestricted++
5✔
360
        }
5✔
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) {
3✔
366
        a.banScoreMtx.Lock()
3✔
367
        defer a.banScoreMtx.Unlock()
3✔
368

3✔
369
        peerMapKey := string(remotePub.SerializeCompressed())
3✔
370

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

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

382
        delete(a.peerScores, peerMapKey)
3✔
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