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

lightningnetwork / lnd / 14513053602

17 Apr 2025 09:56AM UTC coverage: 56.754% (-12.3%) from 69.035%
14513053602

Pull #9727

github

web-flow
Merge 5fb0f4317 into 24fdae7df
Pull Request #9727: Aux bandwidth manager: also pass HTLC blob to `ShouldHandleTraffic`

3 of 8 new or added lines in 2 files covered. (37.5%)

24357 existing lines in 290 files now uncovered.

107518 of 189445 relevant lines covered (56.75%)

22634.92 hits per line

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

55.1
/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) {
56
        a := &accessMan{
57
                cfg:        cfg,
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
1✔
65
        }
1✔
66

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

72
        return a, nil
73
}
1✔
74

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

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

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

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

5✔
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✔
UNCOV
98
        a.banScoreMtx.RLock()
×
UNCOV
99
        defer a.banScoreMtx.RUnlock()
×
UNCOV
100

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

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

5✔
115
                return access, ErrNoMoreRestrictedAccessSlots
5✔
116
        }
5✔
117

8✔
118
        return access, nil
5✔
119
}
2✔
120

2✔
121
// newPendingOpenChan is called after the pending-open channel has been
2✔
122
// committed to the database. This may transition a restricted-access peer to a
2✔
123
// temporary-access peer.
4✔
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

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

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

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

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

1✔
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✔
UNCOV
170
                a.numRestricted -= 1
×
UNCOV
171

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

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

×
UNCOV
181
        return nil
×
UNCOV
182
}
×
183

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

×
UNCOV
192
        peerMapKey := string(remotePub.SerializeCompressed())
×
UNCOV
193

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

×
UNCOV
200
        switch status.state {
×
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.
1✔
211
                        return ErrNoPendingPeerInfo
1✔
212
                }
1✔
213

1✔
214
                currentNumPending := peerCount.PendingOpenCount - 1
1✔
215
                if currentNumPending == 0 {
1✔
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 {
1✔
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

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

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

238
                        return nil
239
                }
1✔
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
1✔
247

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

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

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

×
265
        peerMapKey := string(remotePub.SerializeCompressed())
266

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

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

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

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

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

1✔
297
                return nil
1✔
298

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

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

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

×
UNCOV
323
        a.banScoreMtx.RLock()
×
UNCOV
324
        defer a.banScoreMtx.RUnlock()
×
UNCOV
325

×
UNCOV
326
        peerMapKey := string(remotePub.SerializeCompressed())
×
UNCOV
327

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

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

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

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

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

×
UNCOV
353
        peerMapKey := string(remotePub.SerializeCompressed())
×
UNCOV
354

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

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

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

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

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

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

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