graph/db: fix FetchChannelEdgesByID zombie fallback versioning
What changed, and why it matters
This commit fixes a bug in LND's Lightning graph database where a fallback path for deleted ('zombie') channels always created a version-1 channel object, even when the caller asked for version 2. The fix makes the fallback use the requested gossip version. The bug could cause version mismatches when reading zombie channel data, but it does not appear to be directly exploitable for theft or denial of service.
Apply the patch and run the versioned graph DB tests. Review any code paths that consume FetchChannelEdgesByID zombie results to ensure they handle the corrected version field consistently.
Security signals we found
Incorrect version metadata in reconstructed zombie channel edges
Potential version mismatch between stored/expected channel features
Versioned test added to prevent regression
Evidence from the diff
In graph/db/sql_store.go, SQLStore.FetchChannelEdgesByID has a zombie fallback that reconstructs a placeholder ChannelEdgeInfo when the underlying channel row is missing (zombie). Previously it unconditionally called models.NewV1Channel. The patch switches on the requested gossip version (gossipV1/gossipV2) and calls the matching NewV1Channel or NewV2Channel constructor. A new versioned test, testFetchZombieEdgeVersioning, asserts that the returned edge info carries the correct Version for both v1 and v2 after DeleteChannelEdges marks the edge as a zombie.
Changed components
graph/db/sql_store.go: SQLStore.FetchChannelEdgesByID zombie fallbackgraph/db/graph_test.go: versioned test suiteInspect captured patch +50 / −5
diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go
index 4652827..6521a84 100644
--- a/graph/db/graph_test.go
+++ b/graph/db/graph_test.go
@@ -239,6 +239,10 @@ var versionedTests = []versionedTest{
name: "filter known chan ids",
test: testFilterKnownChanIDs,
},
+ {
+ name: "fetch zombie edge versioning",
+ test: testFetchZombieEdgeVersioning,
+ },
}
// TestVersionedDBs runs various tests against both v1 and v2 versioned
@@ -5494,6 +5498,40 @@ func testGraphZombieIndex(t *testing.T, v lnwire.GossipVersion) {
assertNumZombies(t, graph, v, 1)
}
+// testFetchZombieEdgeVersioning verifies that when a zombie edge is fetched via
+// FetchChannelEdgesByID, the returned ChannelEdgeInfo carries the correct
+// gossip version.
+func testFetchZombieEdgeVersioning(t *testing.T, v lnwire.GossipVersion) {
+ t.Parallel()
+ ctx := t.Context()
+
+ graph := NewVersionedGraph(MakeTestGraph(t), v)
+
+ node1 := createTestVertex(t, v)
+ node2 := createTestVertex(t, v)
+
+ if bytes.Compare(node2.PubKeyBytes[:], node1.PubKeyBytes[:]) < 0 {
+ node1, node2 = node2, node1
+ }
+
+ edge, _, _ := createChannelEdge(node1, node2, v)
+ require.NoError(t, graph.AddChannelEdge(ctx, edge))
+
+ // Delete the edge and mark it as a zombie.
+ err := graph.DeleteChannelEdges(ctx, false, true, edge.ChannelID)
+ require.NoError(t, err)
+
+ // Fetch the zombie edge by ID. The returned edge info should carry
+ // the correct gossip version even though the channel data has been
+ // removed.
+ info, _, _, err := graph.FetchChannelEdgesByID(ctx, edge.ChannelID)
+ require.ErrorIs(t, err, ErrZombieEdge)
+ require.NotNil(t, info)
+ require.Equal(t, v, info.Version)
+ require.Equal(t, edge.NodeKey1Bytes, info.NodeKey1Bytes)
+ require.Equal(t, edge.NodeKey2Bytes, info.NodeKey2Bytes)
+}
+
// compareNodes is used to compare two Nodes.
func compareNodes(t *testing.T, a, b *models.Node) {
t.Helper()
diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go
index 65c0f9f..f4824d7 100644
--- a/graph/db/sql_store.go
+++ b/graph/db/sql_store.go
@@ -2544,14 +2544,21 @@ func (s *SQLStore) FetchChannelEdgesByID(ctx context.Context,
if err != nil {
return err
}
- zombieEdge, err := models.NewV1Channel(
- 0, chainhash.Hash{}, node1, node2,
- &models.ChannelV1Fields{},
- )
+ switch v {
+ case gossipV1:
+ edge, err = models.NewV1Channel(
+ 0, chainhash.Hash{}, node1,
+ node2, &models.ChannelV1Fields{},
+ )
+ case gossipV2:
+ edge, err = models.NewV2Channel(
+ 0, chainhash.Hash{}, node1,
+ node2, &models.ChannelV2Fields{},
+ )
+ }
if err != nil {
return err
}
- edge = zombieEdge
return ErrZombieEdge
} else if err != nil {
Why this scored 35/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.