Skip to content

Commit 5122a7d

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 8ae30f9 commit 5122a7d

6 files changed

Lines changed: 754 additions & 40 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

graph/db/graph_test.go

Lines changed: 134 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6234,49 +6234,56 @@ func TestPreferHighestAndGetVersions(t *testing.T) {
62346234
return
62356235
}
62366236

6237-
// Add v2 node announcements for the same pubkeys so we can create a
6238-
// versioned duplicate of the channel in the SQL store.
62396237
node1V2 := createNode(t, lnwire.GossipVersion2, node1Priv)
62406238
node2V2 := createNode(t, lnwire.GossipVersion2, node2Priv)
6241-
62426239
require.NoError(t, graph.AddNode(ctx, node1V2))
62436240
require.NoError(t, graph.AddNode(ctx, node2V2))
62446241

6245-
// Add a v2 copy of the same channel so the prefer-highest queries can
6246-
// choose between both versions for the same SCID/outpoint.
6247-
edgeInfoV2, _ := createEdge(
6248-
lnwire.GossipVersion2, 100, 1, 0, 1, node1V2, node2V2,
6242+
// Add a duplicate v1/v2 channel and verify prefer-highest chooses
6243+
// the v2 edge while GetVersions reports both versions.
6244+
dupV1, dupSCID := createEdge(
6245+
lnwire.GossipVersion1, 101, 1, 0, 2, node1V1, node2V1,
6246+
)
6247+
dupV2, _ := createEdge(
6248+
lnwire.GossipVersion2, 101, 1, 0, 2, node1V2, node2V2,
62496249
)
6250-
require.NoError(t, graph.AddChannelEdge(ctx, edgeInfoV2))
6250+
require.NoError(t, graph.AddChannelEdge(ctx, dupV1))
6251+
require.NoError(t, graph.AddChannelEdge(ctx, dupV2))
6252+
6253+
dupChanID := dupSCID.ToUint64()
6254+
dupOutpoint := dupV1.ChannelPoint
62516255

6252-
versions, err = store.GetVersionsBySCID(ctx, chanID)
6256+
info, _, _, err = store.FetchChannelEdgesByIDPreferHighest(
6257+
ctx, dupChanID,
6258+
)
6259+
require.NoError(t, err)
6260+
require.Equal(t, dupChanID, info.ChannelID)
6261+
require.Equal(t, lnwire.GossipVersion2, info.Version)
6262+
6263+
info, _, _, err = store.FetchChannelEdgesByOutpointPreferHighest(
6264+
ctx, &dupOutpoint,
6265+
)
6266+
require.NoError(t, err)
6267+
require.Equal(t, dupChanID, info.ChannelID)
6268+
require.Equal(t, lnwire.GossipVersion2, info.Version)
6269+
6270+
versions, err = store.GetVersionsBySCID(ctx, dupChanID)
62536271
require.NoError(t, err)
62546272
require.Equal(t, []lnwire.GossipVersion{
62556273
lnwire.GossipVersion1,
62566274
lnwire.GossipVersion2,
62576275
}, versions)
62586276

6259-
versions, err = store.GetVersionsByOutpoint(ctx, &op)
6277+
versions, err = store.GetVersionsByOutpoint(ctx, &dupOutpoint)
62606278
require.NoError(t, err)
62616279
require.Equal(t, []lnwire.GossipVersion{
62626280
lnwire.GossipVersion1,
62636281
lnwire.GossipVersion2,
62646282
}, versions)
6265-
6266-
// With no policies present for either version, prefer-highest should
6267-
// fall back to the higher gossip version.
6268-
info, _, _, err = store.FetchChannelEdgesByIDPreferHighest(ctx, chanID)
6269-
require.NoError(t, err)
6270-
require.Equal(t, lnwire.GossipVersion2, info.Version)
6271-
6272-
info, _, _, err = store.FetchChannelEdgesByOutpointPreferHighest(
6273-
ctx, &op,
6274-
)
6275-
require.NoError(t, err)
6276-
require.Equal(t, lnwire.GossipVersion2, info.Version)
62776283
}
6278-
// TestPreferHighestNodeTraversal verifies the no-cache traversal behavior of
6279-
// ChannelGraph's ForEachNodeDirectedChannel and FetchNodeFeatures.
6284+
// TestPreferHighestNodeTraversal verifies that ChannelGraph's
6285+
// ForEachNodeDirectedChannel and FetchNodeFeatures correctly prefer v2 over v1
6286+
// when the graph cache is disabled (exercising the no-cache code paths).
62806287
func TestPreferHighestNodeTraversal(t *testing.T) {
62816288
t.Parallel()
62826289

@@ -6314,33 +6321,35 @@ func TestPreferHighestNodeTraversal(t *testing.T) {
63146321

63156322
features, err = graph.FetchNodeFeatures(ctx, nodeV2.PubKeyBytes)
63166323
require.NoError(t, err)
6317-
require.Empty(t, features.Features(),
6318-
"no-cache traversal still uses the v1 feature lookup path")
6324+
require.False(t, features.IsEmpty(),
6325+
"v2-only node should have features")
63196326

63206327
// Create a node with both v1 and v2 announcements.
63216328
privBoth, err := btcec.NewPrivateKey()
63226329
require.NoError(t, err)
63236330

63246331
nodeBothV1 := createNode(t, lnwire.GossipVersion1, privBoth)
6325-
nodeBothV1.Features = lnwire.NewFeatureVector(
6332+
v1Features := lnwire.NewFeatureVector(
63266333
lnwire.NewRawFeatureVector(lnwire.GossipQueriesRequired),
63276334
lnwire.Features,
63286335
)
6336+
nodeBothV1.Features = v1Features
63296337
require.NoError(t, graph.AddNode(ctx, nodeBothV1))
63306338

63316339
nodeBothV2 := createNode(t, lnwire.GossipVersion2, privBoth)
6332-
nodeBothV2.Features = lnwire.NewFeatureVector(
6333-
lnwire.NewRawFeatureVector(lnwire.DataLossProtectRequired),
6340+
v2Features := lnwire.NewFeatureVector(
6341+
lnwire.NewRawFeatureVector(lnwire.TLVOnionPayloadRequired),
63346342
lnwire.Features,
63356343
)
6344+
nodeBothV2.Features = v2Features
63366345
require.NoError(t, graph.AddNode(ctx, nodeBothV2))
63376346

63386347
features, err = graph.FetchNodeFeatures(
63396348
ctx, nodeBothV1.PubKeyBytes,
63406349
)
63416350
require.NoError(t, err)
6342-
require.Equal(t, nodeBothV1.Features, features,
6343-
"no-cache feature lookup should return the v1 features")
6351+
require.Equal(t, v2Features, features)
6352+
require.NotEqual(t, v1Features, features)
63446353

63456354
// --- ForEachNodeDirectedChannel ---
63466355

@@ -6569,3 +6578,97 @@ func TestPreferHighestForEachChannel(t *testing.T) {
65696578
require.Nil(t, gotShellPref.p1)
65706579
require.Nil(t, gotShellPref.p2)
65716580
}
6581+
6582+
// TestPreferHighestNodeDirectedChannelTraversal verifies that the no-cache
6583+
// ChannelGraph.ForEachNodeDirectedChannel path streams one directed channel per
6584+
// SCID while preferring the v2 advertisement when both versions exist.
6585+
func TestPreferHighestNodeDirectedChannelTraversal(t *testing.T) {
6586+
t.Parallel()
6587+
6588+
if !isSQLDB {
6589+
t.Skip("prefer-highest requires SQL backend")
6590+
}
6591+
6592+
ctx := t.Context()
6593+
graph := MakeTestGraph(t, WithUseGraphCache(false))
6594+
6595+
localPriv, err := btcec.NewPrivateKey()
6596+
require.NoError(t, err)
6597+
peerBothPriv, err := btcec.NewPrivateKey()
6598+
require.NoError(t, err)
6599+
peerV1OnlyPriv, err := btcec.NewPrivateKey()
6600+
require.NoError(t, err)
6601+
6602+
localV1 := createNode(t, lnwire.GossipVersion1, localPriv)
6603+
localV2 := createNode(t, lnwire.GossipVersion2, localPriv)
6604+
peerBothV1 := createNode(t, lnwire.GossipVersion1, peerBothPriv)
6605+
peerBothV2 := createNode(t, lnwire.GossipVersion2, peerBothPriv)
6606+
peerV1Only := createNode(t, lnwire.GossipVersion1, peerV1OnlyPriv)
6607+
6608+
require.NoError(t, graph.AddNode(ctx, localV1))
6609+
require.NoError(t, graph.AddNode(ctx, localV2))
6610+
require.NoError(t, graph.AddNode(ctx, peerBothV1))
6611+
require.NoError(t, graph.AddNode(ctx, peerBothV2))
6612+
require.NoError(t, graph.AddNode(ctx, peerV1Only))
6613+
6614+
dupV1, _ := createEdge(
6615+
lnwire.GossipVersion1, 400, 0, 0, 10, localV1, peerBothV1,
6616+
)
6617+
dupV2, _ := createEdge(
6618+
lnwire.GossipVersion2, 400, 0, 0, 10, localV2, peerBothV2,
6619+
)
6620+
v1Only, _ := createEdge(
6621+
lnwire.GossipVersion1, 401, 0, 0, 11, localV1, peerV1Only,
6622+
)
6623+
6624+
require.NoError(t, graph.AddChannelEdge(ctx, dupV1))
6625+
require.NoError(t, graph.AddChannelEdge(ctx, dupV2))
6626+
require.NoError(t, graph.AddChannelEdge(ctx, v1Only))
6627+
6628+
addPolicies := func(edgeInfo *models.ChannelEdgeInfo,
6629+
version lnwire.GossipVersion,
6630+
fee lnwire.MilliSatoshi) {
6631+
6632+
policy1 := newEdgePolicy(version, edgeInfo.ChannelID, 1000, true)
6633+
policy1.ToNode = edgeInfo.NodeKey2Bytes
6634+
policy1.SigBytes = testSig.Serialize()
6635+
policy1.FeeBaseMSat = fee
6636+
6637+
policy2 := newEdgePolicy(version, edgeInfo.ChannelID, 1001, false)
6638+
policy2.ToNode = edgeInfo.NodeKey1Bytes
6639+
policy2.SigBytes = testSig.Serialize()
6640+
policy2.FeeBaseMSat = fee
6641+
6642+
require.NoError(t, graph.UpdateEdgePolicy(ctx, policy1))
6643+
require.NoError(t, graph.UpdateEdgePolicy(ctx, policy2))
6644+
}
6645+
6646+
addPolicies(dupV1, lnwire.GossipVersion1, 1111)
6647+
addPolicies(dupV2, lnwire.GossipVersion2, 2222)
6648+
addPolicies(v1Only, lnwire.GossipVersion1, 3333)
6649+
6650+
channelsByID := make(map[uint64]*DirectedChannel)
6651+
err = graph.ForEachNodeDirectedChannel(
6652+
ctx, localV1.PubKeyBytes,
6653+
func(channel *DirectedChannel) error {
6654+
channelsByID[channel.ChannelID] = channel.DeepCopy()
6655+
return nil
6656+
}, func() {
6657+
clear(channelsByID)
6658+
},
6659+
)
6660+
require.NoError(t, err)
6661+
require.Len(t, channelsByID, 2)
6662+
6663+
gotDup := channelsByID[dupV1.ChannelID]
6664+
require.NotNil(t, gotDup)
6665+
require.NotNil(t, gotDup.InPolicy)
6666+
require.Equal(t, lnwire.MilliSatoshi(2222), gotDup.InPolicy.FeeBaseMSat)
6667+
6668+
gotV1Only := channelsByID[v1Only.ChannelID]
6669+
require.NotNil(t, gotV1Only)
6670+
require.NotNil(t, gotV1Only.InPolicy)
6671+
require.Equal(
6672+
t, lnwire.MilliSatoshi(3333), gotV1Only.InPolicy.FeeBaseMSat,
6673+
)
6674+
}

0 commit comments

Comments
 (0)