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

lightningnetwork / lnd / 22762840404

06 Mar 2026 12:09PM UTC coverage: 62.282% (+0.08%) from 62.205%
22762840404

Pull #9265

github

web-flow
Merge 5d47fe8cd into 6ea479189
Pull Request #9265: multi: remote graph source

928 of 1267 new or added lines in 16 files covered. (73.24%)

67 existing lines in 18 files now uncovered.

140567 of 225693 relevant lines covered (62.28%)

19432.01 hits per line

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

91.63
/graph/db/graph_cache.go
1
package graphdb
2

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

7
        "github.com/btcsuite/btcd/btcutil"
8
        "github.com/lightningnetwork/lnd/graph/db/models"
9
        "github.com/lightningnetwork/lnd/lnwire"
10
        "github.com/lightningnetwork/lnd/routing/route"
11
)
12

13
// DirectedChannel is a type that stores the channel information as seen from
14
// one side of the channel.
15
type DirectedChannel struct {
16
        // ChannelID is the unique identifier of this channel.
17
        ChannelID uint64
18

19
        // IsNode1 indicates if this is the node with the smaller public key.
20
        IsNode1 bool
21

22
        // OtherNode is the public key of the node on the other end of this
23
        // channel.
24
        OtherNode route.Vertex
25

26
        // Capacity is the announced capacity of this channel in satoshis.
27
        Capacity btcutil.Amount
28

29
        // OutPolicySet is a boolean that indicates whether the node has an
30
        // outgoing policy set. For pathfinding only the existence of the policy
31
        // is important to know, not the actual content.
32
        OutPolicySet bool
33

34
        // InPolicy is the incoming policy *from* the other node to this node.
35
        // In path finding, we're walking backward from the destination to the
36
        // source, so we're always interested in the edge that arrives to us
37
        // from the other node.
38
        InPolicy *models.CachedEdgePolicy
39

40
        // Inbound fees of this node.
41
        InboundFee lnwire.Fee
42
}
43

44
// DeepCopy creates a deep copy of the channel, including the incoming policy.
45
func (c *DirectedChannel) DeepCopy() *DirectedChannel {
1,600✔
46
        channelCopy := *c
1,600✔
47

1,600✔
48
        if channelCopy.InPolicy != nil {
3,172✔
49
                inPolicyCopy := *channelCopy.InPolicy
1,572✔
50
                channelCopy.InPolicy = &inPolicyCopy
1,572✔
51

1,572✔
52
                // The fields for the ToNode can be overwritten by the path
1,572✔
53
                // finding algorithm, which is why we need a deep copy in the
1,572✔
54
                // first place. So we always start out with nil values, just to
1,572✔
55
                // be sure they don't contain any old data.
1,572✔
56
                channelCopy.InPolicy.ToNodePubKey = nil
1,572✔
57
                channelCopy.InPolicy.ToNodeFeatures = nil
1,572✔
58
        }
1,572✔
59

60
        return &channelCopy
1,600✔
61
}
62

63
// GraphCache is a type that holds a minimal set of information of the public
64
// channel graph that can be used for pathfinding.
65
type GraphCache struct {
66
        nodeChannels map[route.Vertex]map[uint64]*DirectedChannel
67
        nodeFeatures map[route.Vertex]*lnwire.FeatureVector
68

69
        mtx sync.RWMutex
70
}
71

72
// NewGraphCache creates a new graphCache.
73
func NewGraphCache(preAllocNumNodes int) *GraphCache {
158✔
74
        return &GraphCache{
158✔
75
                nodeChannels: make(
158✔
76
                        map[route.Vertex]map[uint64]*DirectedChannel,
158✔
77
                        // A channel connects two nodes, so we can look it up
158✔
78
                        // from both sides, meaning we get double the number of
158✔
79
                        // entries.
158✔
80
                        preAllocNumNodes*2,
158✔
81
                ),
158✔
82
                nodeFeatures: make(
158✔
83
                        map[route.Vertex]*lnwire.FeatureVector,
158✔
84
                        preAllocNumNodes,
158✔
85
                ),
158✔
86
        }
158✔
87
}
158✔
88

89
// Stats returns statistics about the current cache size.
90
func (c *GraphCache) Stats() string {
390✔
91
        c.mtx.RLock()
390✔
92
        defer c.mtx.RUnlock()
390✔
93

390✔
94
        numChannels := 0
390✔
95
        for node := range c.nodeChannels {
775✔
96
                numChannels += len(c.nodeChannels[node])
385✔
97
        }
385✔
98
        return fmt.Sprintf("num_node_features=%d, num_nodes=%d, "+
390✔
99
                "num_channels=%d", len(c.nodeFeatures), len(c.nodeChannels),
390✔
100
                numChannels)
390✔
101
}
102

103
// AddNodeFeatures adds a graph node and its features to the cache.
104
func (c *GraphCache) AddNodeFeatures(node route.Vertex,
105
        features *lnwire.FeatureVector) {
923✔
106

923✔
107
        c.mtx.Lock()
923✔
108
        defer c.mtx.Unlock()
923✔
109

923✔
110
        c.nodeFeatures[node] = features
923✔
111
}
923✔
112

113
// AddChannel adds a non-directed channel, meaning that the order of policy 1
114
// and policy 2 does not matter, the directionality is extracted from the info
115
// and policy flags automatically. The policy will be set as the outgoing policy
116
// on one node and the incoming policy on the peer's side.
117
func (c *GraphCache) AddChannel(info *models.CachedEdgeInfo,
118
        policy1, policy2 *models.CachedEdgePolicy) {
1,802✔
119

1,802✔
120
        if info == nil {
1,802✔
121
                return
×
122
        }
×
123

124
        // Create the edge entry for both nodes. We always add the channel
125
        // structure to the cache, even if both policies are currently disabled,
126
        // so that later policy updates can find and update the channel entry.
127
        c.mtx.Lock()
1,802✔
128
        c.maybeAddEdge(info.NodeKey1Bytes, &DirectedChannel{
1,802✔
129
                ChannelID: info.ChannelID,
1,802✔
130
                IsNode1:   true,
1,802✔
131
                OtherNode: info.NodeKey2Bytes,
1,802✔
132
                Capacity:  info.Capacity,
1,802✔
133
        })
1,802✔
134
        c.maybeAddEdge(info.NodeKey2Bytes, &DirectedChannel{
1,802✔
135
                ChannelID: info.ChannelID,
1,802✔
136
                IsNode1:   false,
1,802✔
137
                OtherNode: info.NodeKey1Bytes,
1,802✔
138
                Capacity:  info.Capacity,
1,802✔
139
        })
1,802✔
140
        c.mtx.Unlock()
1,802✔
141

1,802✔
142
        // Skip adding policies if both are disabled, as the channel is
1,802✔
143
        // currently unusable for routing. However, we still add the channel
1,802✔
144
        // structure above so that policy updates can later enable it.
1,802✔
145
        if policy1 != nil && policy1.IsDisabled &&
1,802✔
146
                policy2 != nil && policy2.IsDisabled {
1,806✔
147

4✔
148
                log.Debugf("Skipping policies for channel %v: both "+
4✔
149
                        "policies are disabled (channel structure still "+
4✔
150
                        "cached for future updates)", info.ChannelID)
4✔
151

4✔
152
                return
4✔
153
        }
4✔
154

155
        // The policy's node is always the to_node. So if policy 1 has to_node
156
        // of node 2 then we have the policy 1 as seen from node 1.
157
        if policy1 != nil {
2,202✔
158
                fromNode, toNode := info.NodeKey1Bytes, info.NodeKey2Bytes
401✔
159
                if !policy1.IsNode1 {
402✔
160
                        fromNode, toNode = toNode, fromNode
1✔
161
                }
1✔
162
                c.UpdatePolicy(policy1, fromNode, toNode)
401✔
163
        }
164
        if policy2 != nil {
2,202✔
165
                fromNode, toNode := info.NodeKey2Bytes, info.NodeKey1Bytes
401✔
166
                if policy2.IsNode1 {
402✔
167
                        fromNode, toNode = toNode, fromNode
1✔
168
                }
1✔
169
                c.UpdatePolicy(policy2, fromNode, toNode)
401✔
170
        }
171
}
172

173
// maybeAddEdge adds the edge to the node's channel list only if it does not
174
// already exist. This prevents overwriting policies that were set by previous
175
// updates (e.g., from topology subscription updates where channel structure
176
// and policy updates arrive separately).
177
func (c *GraphCache) maybeAddEdge(node route.Vertex, edge *DirectedChannel) {
3,601✔
178
        if len(c.nodeChannels[node]) == 0 {
4,471✔
179
                c.nodeChannels[node] = make(map[uint64]*DirectedChannel)
870✔
180
        }
870✔
181

182
        if c.nodeChannels[node][edge.ChannelID] != nil {
3,604✔
183
                return
3✔
184
        }
3✔
185

186
        c.nodeChannels[node][edge.ChannelID] = edge
3,601✔
187
}
188

189
// UpdatePolicy updates a single policy on both the from and to node. The order
190
// of the from and to node is not strictly important. But we assume that a
191
// channel edge was added beforehand so that the directed channel struct already
192
// exists in the cache.
193
func (c *GraphCache) UpdatePolicy(policy *models.CachedEdgePolicy, fromNode,
194
        toNode route.Vertex) {
3,285✔
195

3,285✔
196
        c.mtx.Lock()
3,285✔
197
        defer c.mtx.Unlock()
3,285✔
198

3,285✔
199
        updatePolicy := func(nodeKey route.Vertex) {
9,852✔
200
                if len(c.nodeChannels[nodeKey]) == 0 {
6,567✔
201
                        log.Debugf("Node=%v not found in graph cache", nodeKey)
×
202

×
203
                        return
×
204
                }
×
205

206
                channel, ok := c.nodeChannels[nodeKey][policy.ChannelID]
6,567✔
207
                if !ok {
6,567✔
208
                        log.Debugf("Channel=%v not found in graph cache",
×
209
                                policy.ChannelID)
×
210

×
211
                        return
×
212
                }
×
213

214
                // Edge 1 is defined as the policy for the direction of node1 to
215
                // node2.
216
                switch {
6,567✔
217
                // This is node 1, and it is edge 1, so this is the outgoing
218
                // policy for node 1.
219
                case channel.IsNode1 && policy.IsNode1:
1,648✔
220
                        channel.OutPolicySet = true
1,648✔
221
                        policy.InboundFee.WhenSome(func(fee lnwire.Fee) {
1,813✔
222
                                channel.InboundFee = fee
165✔
223
                        })
165✔
224

225
                // This is node 2, and it is edge 2, so this is the outgoing
226
                // policy for node 2.
227
                case !channel.IsNode1 && !policy.IsNode1:
1,640✔
228
                        channel.OutPolicySet = true
1,640✔
229
                        policy.InboundFee.WhenSome(func(fee lnwire.Fee) {
1,804✔
230
                                channel.InboundFee = fee
164✔
231
                        })
164✔
232

233
                // The other two cases left mean it's the inbound policy for the
234
                // node.
235
                default:
3,285✔
236
                        channel.InPolicy = policy
3,285✔
237
                }
238
        }
239

240
        updatePolicy(fromNode)
3,285✔
241
        updatePolicy(toNode)
3,285✔
242
}
243

244
// RemoveNode completely removes a node and all its channels (including the
245
// peer's side).
246
func (c *GraphCache) RemoveNode(node route.Vertex) {
65✔
247
        c.mtx.Lock()
65✔
248
        defer c.mtx.Unlock()
65✔
249

65✔
250
        delete(c.nodeFeatures, node)
65✔
251

65✔
252
        // First remove all channels from the other nodes' lists.
65✔
253
        for _, channel := range c.nodeChannels[node] {
69✔
254
                c.removeChannelIfFound(channel.OtherNode, channel.ChannelID)
4✔
255
        }
4✔
256

257
        // Then remove our whole node completely.
258
        delete(c.nodeChannels, node)
65✔
259
}
260

261
// RemoveChannel removes a single channel between two nodes.
262
func (c *GraphCache) RemoveChannel(node1, node2 route.Vertex, chanID uint64) {
272✔
263
        c.mtx.Lock()
272✔
264
        defer c.mtx.Unlock()
272✔
265

272✔
266
        // Remove that one channel from both sides.
272✔
267
        c.removeChannelIfFound(node1, chanID)
272✔
268
        c.removeChannelIfFound(node2, chanID)
272✔
269
}
272✔
270

271
// RemoveChannelByID removes a channel by its ID. This is less efficient than
272
// RemoveChannel since it needs to scan all nodes to find the channel, but is
273
// useful when the channel's endpoint nodes are not known.
NEW
274
func (c *GraphCache) RemoveChannelByID(chanID uint64) {
×
NEW
275
        c.mtx.Lock()
×
NEW
276
        defer c.mtx.Unlock()
×
NEW
277

×
NEW
278
        for node := range c.nodeChannels {
×
NEW
279
                c.removeChannelIfFound(node, chanID)
×
NEW
280
        }
×
281
}
282

283
// removeChannelIfFound removes a single channel from one side.
284
func (c *GraphCache) removeChannelIfFound(node route.Vertex, chanID uint64) {
542✔
285
        if len(c.nodeChannels[node]) == 0 {
724✔
286
                return
182✔
287
        }
182✔
288

289
        delete(c.nodeChannels[node], chanID)
360✔
290
}
291

292
// getChannels returns a copy of the passed node's channels or nil if there
293
// isn't any.
294
func (c *GraphCache) getChannels(node route.Vertex) []*DirectedChannel {
551✔
295
        c.mtx.RLock()
551✔
296
        defer c.mtx.RUnlock()
551✔
297

551✔
298
        channels, ok := c.nodeChannels[node]
551✔
299
        if !ok {
561✔
300
                return nil
10✔
301
        }
10✔
302

303
        features, ok := c.nodeFeatures[node]
544✔
304
        if !ok {
702✔
305
                // If the features were set to nil explicitly, that's fine here.
158✔
306
                // The router will overwrite the features of the destination
158✔
307
                // node with those found in the invoice if necessary. But if we
158✔
308
                // didn't yet get a node announcement we want to mimic the
158✔
309
                // behavior of the old DB based code that would always set an
158✔
310
                // empty feature vector instead of leaving it nil.
158✔
311
                features = lnwire.EmptyFeatureVector()
158✔
312
        }
158✔
313

314
        toNodeCallback := func() route.Vertex {
995✔
315
                return node
451✔
316
        }
451✔
317

318
        i := 0
544✔
319
        channelsCopy := make([]*DirectedChannel, len(channels))
544✔
320
        for _, channel := range channels {
2,140✔
321
                // We need to copy the channel and policy to avoid it being
1,596✔
322
                // updated in the cache if the path finding algorithm sets
1,596✔
323
                // fields on it (currently only the ToNodeFeatures of the
1,596✔
324
                // policy).
1,596✔
325
                channelCopy := channel.DeepCopy()
1,596✔
326
                if channelCopy.InPolicy != nil {
3,165✔
327
                        channelCopy.InPolicy.ToNodePubKey = toNodeCallback
1,569✔
328
                        channelCopy.InPolicy.ToNodeFeatures = features
1,569✔
329
                }
1,569✔
330

331
                channelsCopy[i] = channelCopy
1,596✔
332
                i++
1,596✔
333
        }
334

335
        return channelsCopy
544✔
336
}
337

338
// ForEachChannel invokes the given callback for each channel of the given node.
339
func (c *GraphCache) ForEachChannel(node route.Vertex,
340
        cb func(channel *DirectedChannel) error) error {
551✔
341

551✔
342
        // Obtain a copy of the node's channels. We need do this in order to
551✔
343
        // avoid deadlocks caused by interaction with the graph cache, channel
551✔
344
        // state and the graph database from multiple goroutines. This snapshot
551✔
345
        // is only used for path finding where being stale is acceptable since
551✔
346
        // the real world graph and our representation may always become
551✔
347
        // slightly out of sync for a short time and the actual channel state
551✔
348
        // is stored separately.
551✔
349
        channels := c.getChannels(node)
551✔
350
        for _, channel := range channels {
2,125✔
351
                if err := cb(channel); err != nil {
1,588✔
352
                        return err
14✔
353
                }
14✔
354
        }
355

356
        return nil
540✔
357
}
358

359
// ForEachNode iterates over the adjacency list of the graph, executing the
360
// call back for each node and the set of channels that emanate from the given
361
// node.
362
//
363
// NOTE: This method should be considered _read only_, the channels or nodes
364
// passed in MUST NOT be modified.
365
func (c *GraphCache) ForEachNode(cb func(node route.Vertex,
366
        channels map[uint64]*DirectedChannel) error) error {
2✔
367

2✔
368
        c.mtx.RLock()
2✔
369
        defer c.mtx.RUnlock()
2✔
370

2✔
371
        for node, channels := range c.nodeChannels {
6✔
372
                // We don't make a copy here since this is a read-only RPC
4✔
373
                // call. We also don't need the node features either for this
4✔
374
                // call.
4✔
375
                if err := cb(node, channels); err != nil {
4✔
376
                        return err
×
377
                }
×
378
        }
379

380
        return nil
2✔
381
}
382

383
// GetFeatures returns the features of the node with the given ID. If no
384
// features are known for the node, an empty feature vector is returned.
385
func (c *GraphCache) GetFeatures(node route.Vertex) *lnwire.FeatureVector {
473✔
386
        c.mtx.RLock()
473✔
387
        defer c.mtx.RUnlock()
473✔
388

473✔
389
        features, ok := c.nodeFeatures[node]
473✔
390
        if !ok || features == nil {
556✔
391
                // The router expects the features to never be nil, so we return
83✔
392
                // an empty feature set instead.
83✔
393
                return lnwire.EmptyFeatureVector()
83✔
394
        }
83✔
395

396
        return features
393✔
397
}
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

© 2026 Coveralls, Inc