Skip to content

Commit 1d82bc8

Browse files
committed
graph/db: implement prefer-highest-version for node traversal
Update ForEachNodeDirectedChannel and FetchNodeFeatures on ChannelGraph to iterate across all gossip versions (highest first) instead of hardcoding v1. This ensures that channels announced via v2 are preferred over v1 and that v2 features are used when available.
1 parent 636b36c commit 1d82bc8

6 files changed

Lines changed: 640 additions & 329 deletions

File tree

graph/db/graph.go

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ type ChannelGraph struct {
6363
cancel fn.Option[context.CancelFunc]
6464
}
6565

66+
// preferHighestNodeDirectedChanneler is implemented by stores that can stream
67+
// cross-version node-directed channel traversals directly.
68+
type preferHighestNodeDirectedChanneler interface {
69+
ForEachNodeDirectedChannelPreferHighest(ctx context.Context,
70+
node route.Vertex, cb func(channel *DirectedChannel) error,
71+
reset func()) error
72+
}
73+
6674
// NewChannelGraph creates a new ChannelGraph instance with the given backend.
6775
func NewChannelGraph(v1Store Store,
6876
options ...ChanGraphOption) (*ChannelGraph, error) {
@@ -238,8 +246,9 @@ func (c *ChannelGraph) populateCache(ctx context.Context) error {
238246
for _, v := range []lnwire.GossipVersion{
239247
gossipV1, gossipV2,
240248
} {
241-
// TODO(elle): If we have both v1 and v2 entries for the same
242-
// node/channel, prefer v2 when merging.
249+
// We iterate v1 first, then v2. Since AddNodeFeatures and
250+
// AddChannel overwrite on key collision, v2 data naturally
251+
// takes precedence when both versions exist.
243252
err := c.db.ForEachNodeCacheable(ctx, v,
244253
func(node route.Vertex,
245254
features *lnwire.FeatureVector) error {
@@ -299,12 +308,59 @@ func (c *ChannelGraph) ForEachNodeDirectedChannel(ctx context.Context,
299308
return c.cache.graphCache.ForEachChannel(node, cb)
300309
}
301310

302-
// TODO(elle): once the no-cache path needs to support
303-
// pathfinding across gossip versions, this should iterate
304-
// across all versions rather than defaulting to v1.
305-
return c.db.ForEachNodeDirectedChannel(
306-
ctx, gossipV1, node, cb, reset,
307-
)
311+
if db, ok := c.db.(preferHighestNodeDirectedChanneler); ok {
312+
return db.ForEachNodeDirectedChannelPreferHighest(
313+
ctx, node, cb, reset,
314+
)
315+
}
316+
317+
// Iterate across all gossip versions (highest first) so that
318+
// channels announced via v2 are preferred over v1. We buffer
319+
// results and deliver them at the end so that ExecTx retries
320+
// within a single version don't corrupt the caller's state or
321+
// lose channels from already-committed versions.
322+
seen := make(map[uint64]struct{})
323+
var allChannels []*DirectedChannel
324+
325+
for _, v := range []lnwire.GossipVersion{gossipV2, gossipV1} {
326+
prevLen := len(allChannels)
327+
err := c.db.ForEachNodeDirectedChannel(
328+
ctx, v, node,
329+
func(channel *DirectedChannel) error {
330+
if _, ok := seen[channel.ChannelID]; ok {
331+
return nil
332+
}
333+
seen[channel.ChannelID] = struct{}{}
334+
335+
allChannels = append(allChannels, channel)
336+
337+
return nil
338+
},
339+
func() {
340+
// On ExecTx retry, undo this version's
341+
// additions while keeping channels from
342+
// earlier (already committed) versions.
343+
for _, ch := range allChannels[prevLen:] {
344+
delete(seen, ch.ChannelID)
345+
}
346+
allChannels = allChannels[:prevLen]
347+
},
348+
)
349+
if err != nil &&
350+
!errors.Is(err, ErrVersionNotSupportedForKVDB) {
351+
352+
return err
353+
}
354+
}
355+
356+
// Deliver the collected channels to the caller.
357+
for _, ch := range allChannels {
358+
if err := cb(ch); err != nil {
359+
return err
360+
}
361+
}
362+
363+
return nil
308364
}
309365

310366
// FetchNodeFeatures returns the features of the given node. If no features are
@@ -320,7 +376,22 @@ func (c *ChannelGraph) FetchNodeFeatures(ctx context.Context,
320376
return c.cache.graphCache.GetFeatures(node), nil
321377
}
322378

323-
return c.db.FetchNodeFeatures(ctx, lnwire.GossipVersion1, node)
379+
// Try v2 first, fall back to v1 if the v2 features are empty.
380+
for _, v := range []lnwire.GossipVersion{gossipV2, gossipV1} {
381+
features, err := c.db.FetchNodeFeatures(ctx, v, node)
382+
if errors.Is(err, ErrVersionNotSupportedForKVDB) {
383+
continue
384+
}
385+
if err != nil {
386+
return nil, err
387+
}
388+
389+
if !features.IsEmpty() {
390+
return features, nil
391+
}
392+
}
393+
394+
return lnwire.EmptyFeatureVector(), nil
324395
}
325396

326397
// GraphSession will provide the call-back with access to a NodeTraverser

0 commit comments

Comments
 (0)