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

lightningnetwork / lnd / 14471480810

15 Apr 2025 02:05PM UTC coverage: 58.611% (-10.5%) from 69.088%
14471480810

Pull #9702

github

web-flow
Merge 811aac3b1 into 014706cc3
Pull Request #9702: multi: make payment address mandatory

2 of 4 new or added lines in 1 file covered. (50.0%)

28451 existing lines in 450 files now uncovered.

97194 of 165828 relevant lines covered (58.61%)

1.82 hits per line

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

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

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

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

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

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

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

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

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

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

×
115
                return access, ErrNoMoreRestrictedAccessSlots
×
116
        }
×
117

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

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

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

137
        switch status.state {
3✔
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:
3✔
158
                // If the peer's access status is restricted, then we can
3✔
159
                // transition it to a temporary-access peer. We'll need to
3✔
160
                // update numRestricted and also peerScores. We'll also need to
3✔
161
                // update peerCounts.
3✔
162
                peerCount := channeldb.ChanCount{
3✔
163
                        HasOpenOrClosedChan: false,
3✔
164
                        PendingOpenCount:    1,
3✔
165
                }
3✔
166

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

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

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

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

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

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

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

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

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

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

3✔
219
                        // If this is the only pending-open channel for this
3✔
220
                        // peer and it's getting removed, attempt to demote
3✔
221
                        // this peer to a restricted peer.
3✔
222
                        if a.numRestricted == a.cfg.maxRestrictedSlots {
3✔
UNCOV
223
                                // There are no available restricted slots, so
×
UNCOV
224
                                // we need to disconnect this peer. We leave
×
UNCOV
225
                                // this up to the caller.
×
UNCOV
226
                                return ErrNoMoreRestrictedAccessSlots
×
UNCOV
227
                        }
×
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 {
3✔
262
        a.banScoreMtx.Lock()
3✔
263
        defer a.banScoreMtx.Unlock()
3✔
264

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

3✔
267
        // Fetch the peer's access status from peerScores.
3✔
268
        status, found := a.peerScores[peerMapKey]
3✔
269
        if !found {
6✔
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 {
3✔
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:
3✔
281
                // If the peer's state is temporary, we'll upgrade the peer to
3✔
282
                // a protected peer.
3✔
283
                peerCount, found := a.peerCounts[peerMapKey]
3✔
284
                if !found {
3✔
285
                        // Error if we did not find any info in peerCounts.
×
286
                        return ErrNoPendingPeerInfo
×
287
                }
×
288

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

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

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

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

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

3✔
328
        if _, found := a.peerCounts[peerMapKey]; !found {
6✔
329
                // Check numRestricted to see if there is an available slot. In
3✔
330
                // the future, it's possible to add better heuristics.
3✔
331
                if a.numRestricted < a.cfg.maxRestrictedSlots {
6✔
332
                        // There is an available slot.
3✔
333
                        return true, nil
3✔
334
                }
3✔
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
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) {
3✔
348

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

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

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

3✔
357
        // Increment numRestricted.
3✔
358
        if access == peerStatusRestricted {
6✔
359
                a.numRestricted++
3✔
360
        }
3✔
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