What changed, and why it matters
This commit changes how LND tracks whether a Lightning channel is 'alive' versus a 'zombie' by adding a gossip-version parameter throughout the code. It is a plumbing/refactoring change to prepare for multiple gossip versions. It does not by itself fix a known exploit, but it tightens version handling: the older KV database backend now explicitly rejects non-version-1 gossip, and the newer SQL backend now uses the supplied version when deleting zombie records and clearing its cache. The most concrete security-relevant effect is preventing version confusion between the database and cache layers, which could otherwise leave stale or mismatched channel state.
Treat as a hardening/refactoring commit rather than an urgent security patch. Review related follow-up commits to confirm that FilterKnownChanIDs is also properly versioned and that all future gossip-version call sites supply the correct version. Continue normal testing, especially around zombie channel resurrection and cache consistency across backends.
Security signals we found
Adds explicit version validation in KVStore MarkEdgeLive (rejects non-v1)
Adds explicit version validation in SQLStore MarkEdgeLive (rejects unknown versions)
SQL zombie deletion and cache invalidation now use the passed gossip version instead of a hard-coded GossipVersion1
Graph cache repopulation now uses the same gossip version as the store call
No direct vulnerability description, CVE, or security advisory present in commit or references
Evidence from the diff
The commit threads lnwire.GossipVersion through MarkEdgeLive across Store, ChannelGraph, ChannelGraphSource/Builder, and the discovery gossiper. KVStore now returns ErrVersionNotSupportedForKVDB for any version other than GossipVersion1. SQLStore validates the version with isKnownGossipVersion, passes it to DeleteZombieChannel, and invalidates rejectCache/chanCache for that exact version. ChannelGraph.MarkEdgeLive also uses the same version when repopulating the graph cache via FetchChanInfos. FilterKnownChanIDs hard-codes GossipVersion1 for now (noted as a temporary site). Call sites in the gossiper and tests pass GossipVersion1 explicitly. This is preparatory work for future gossip versions and reduces the risk of version-mismatched zombie/live state and cache inconsistency.
Changed components
graph/db/kv_store.gograph/db/sql_store.gograph/db/graph.gograph/db/interfaces.gograph/builder.gograph/interfaces.godiscovery/gossiper.godiscovery/gossiper_test.gograph/db/graph_test.goInspect captured patch +58 / −34
diff --git a/discovery/gossiper.go b/discovery/gossiper.go
index 42fe9ac..fd0f371 100644
--- a/discovery/gossiper.go
+++ b/discovery/gossiper.go
@@ -2340,7 +2340,7 @@ func (d *AuthenticatedGossiper) processZombieUpdate(_ context.Context,
// With the signature valid, we'll proceed to mark the
// edge as live and wait for the channel announcement to
// come through again.
- err = d.cfg.Graph.MarkEdgeLive(scid)
+ err = d.cfg.Graph.MarkEdgeLive(lnwire.GossipVersion1, scid)
switch {
case errors.Is(err, graphdb.ErrZombieEdgeNotFound):
log.Errorf("edge with chan_id=%v was not found in the "+
diff --git a/discovery/gossiper_test.go b/discovery/gossiper_test.go
index 0ee33ec..198f14d 100644
--- a/discovery/gossiper_test.go
+++ b/discovery/gossiper_test.go
@@ -413,7 +413,9 @@ func (r *mockGraphSource) IsStaleEdgePolicy(chanID lnwire.ShortChannelID,
// MarkEdgeLive clears an edge from our zombie index, deeming it as live.
//
// NOTE: This method is part of the ChannelGraphSource interface.
-func (r *mockGraphSource) MarkEdgeLive(chanID lnwire.ShortChannelID) error {
+func (r *mockGraphSource) MarkEdgeLive(_ lnwire.GossipVersion,
+ chanID lnwire.ShortChannelID) error {
+
r.mu.Lock()
defer r.mu.Unlock()
delete(r.zombies, chanID.ToUint64())
@@ -2440,7 +2442,8 @@ func TestRejectZombieEdge(t *testing.T) {
// If we then mark the edge as live, the edge's zombie status should be
// overridden and the announcements should be processed.
- if err := tCtx.router.MarkEdgeLive(chanID); err != nil {
+ err = tCtx.router.MarkEdgeLive(lnwire.GossipVersion1, chanID)
+ if err != nil {
t.Fatalf("unable mark channel %v as zombie: %v", chanID, err)
}
@@ -4811,7 +4814,7 @@ func TestChanAnnBanningNonChanPeer(t *testing.T) {
// as a zombie if any error occurs in the chanvalidate.Validate call.
// For the sake of the rest of the test, however, we mark it as live
// here.
- _ = tCtx.router.MarkEdgeLive(ca.ShortChannelID)
+ _ = tCtx.router.MarkEdgeLive(lnwire.GossipVersion1, ca.ShortChannelID)
select {
case err = <-tCtx.gossiper.ProcessRemoteAnnouncement(
diff --git a/graph/builder.go b/graph/builder.go
index 1dc5aff..3040709 100644
--- a/graph/builder.go
+++ b/graph/builder.go
@@ -1450,11 +1450,14 @@ func (b *Builder) IsStaleEdgePolicy(chanID lnwire.ShortChannelID,
return false
}
-// MarkEdgeLive clears an edge from our zombie index, deeming it as live.
+// MarkEdgeLive clears an edge from our zombie index for the given gossip
+// version, deeming it as live.
//
// NOTE: This method is part of the ChannelGraphSource interface.
-func (b *Builder) MarkEdgeLive(chanID lnwire.ShortChannelID) error {
+func (b *Builder) MarkEdgeLive(v lnwire.GossipVersion,
+ chanID lnwire.ShortChannelID) error {
+
return b.cfg.Graph.MarkEdgeLive(
- context.TODO(), chanID.ToUint64(),
+ context.TODO(), v, chanID.ToUint64(),
)
}
diff --git a/graph/db/graph.go b/graph/db/graph.go
index d60ca24..b98a806 100644
--- a/graph/db/graph.go
+++ b/graph/db/graph.go
@@ -345,11 +345,13 @@ func (c *ChannelGraph) AddChannelEdge(ctx context.Context,
return nil
}
-// MarkEdgeLive clears an edge from our zombie index, deeming it as live.
-// If the cache is enabled, the edge will be added back to the graph cache if
-// we still have a record of this channel in the DB.
-func (c *ChannelGraph) MarkEdgeLive(ctx context.Context, chanID uint64) error {
- err := c.db.MarkEdgeLive(ctx, chanID)
+// MarkEdgeLive clears an edge from our zombie index for the given gossip
+// version, deeming it as live. If the cache is enabled, the edge will be added
+// back to the graph cache if we still have a record of this channel in the DB.
+func (c *ChannelGraph) MarkEdgeLive(ctx context.Context,
+ v lnwire.GossipVersion, chanID uint64) error {
+
+ err := c.db.MarkEdgeLive(ctx, v, chanID)
if err != nil {
return err
}
@@ -357,9 +359,7 @@ func (c *ChannelGraph) MarkEdgeLive(ctx context.Context, chanID uint64) error {
if c.graphCache != nil {
// We need to add the channel back into our graph cache,
// otherwise we won't use it for path finding.
- infos, err := c.db.FetchChanInfos(
- ctx, lnwire.GossipVersion1, []uint64{chanID},
- )
+ infos, err := c.db.FetchChanInfos(ctx, v, []uint64{chanID})
if err != nil {
return err
}
@@ -554,7 +554,8 @@ func (c *ChannelGraph) FilterKnownChanIDs(ctx context.Context,
// alive, and we let it be added to the set of IDs to query our
// peer for.
err := c.db.MarkEdgeLive(
- ctx, info.ShortChannelID.ToUint64(),
+ ctx, lnwire.GossipVersion1,
+ info.ShortChannelID.ToUint64(),
)
// Since there is a chance that the edge could have been marked
// as "live" between the FilterKnownChanIDs call and the
diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go
index ebe958b..f49eb3a 100644
--- a/graph/db/graph_test.go
+++ b/graph/db/graph_test.go
@@ -4610,12 +4610,16 @@ func TestGraphZombieIndex(t *testing.T) {
// Similarly, if we mark the same edge as live, we should no longer see
// it within the index.
- require.NoError(t, graph.MarkEdgeLive(ctx, edge.ChannelID))
+ require.NoError(
+ t, graph.MarkEdgeLive(ctx, lnwire.GossipVersion1, edge.ChannelID),
+ )
// Attempting to mark the edge as live again now that it is no longer
// in the zombie index should fail.
require.ErrorIs(
- t, graph.MarkEdgeLive(ctx, edge.ChannelID),
+ t, graph.MarkEdgeLive(
+ ctx, lnwire.GossipVersion1, edge.ChannelID,
+ ),
ErrZombieEdgeNotFound,
)
diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go
index f507162..ae07bbe 100644
--- a/graph/db/interfaces.go
+++ b/graph/db/interfaces.go
@@ -332,9 +332,10 @@ type Store interface { //nolint:interfacebloat
MarkEdgeZombie(ctx context.Context, chanID uint64,
pubKey1, pubKey2 [33]byte) error
- // MarkEdgeLive clears an edge from our zombie index, deeming it as
- // live.
- MarkEdgeLive(ctx context.Context, chanID uint64) error
+ // MarkEdgeLive clears an edge from our zombie index for the given
+ // gossip version, deeming it as live.
+ MarkEdgeLive(ctx context.Context, v lnwire.GossipVersion,
+ chanID uint64) error
// IsZombieEdge returns whether the edge is considered zombie. If it is
// a zombie, then the two node public keys corresponding to this edge
diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go
index c202d76..5e911aa 100644
--- a/graph/db/kv_store.go
+++ b/graph/db/kv_store.go
@@ -4291,8 +4291,15 @@ func markEdgeZombie(zombieIndex kvdb.RwBucket, chanID uint64, pubKey1,
return zombieIndex.Put(k[:], v[:])
}
-// MarkEdgeLive clears an edge from our zombie index, deeming it as live.
-func (c *KVStore) MarkEdgeLive(_ context.Context, chanID uint64) error {
+// MarkEdgeLive clears an edge from our zombie index for the given gossip
+// version, deeming it as live.
+func (c *KVStore) MarkEdgeLive(_ context.Context, v lnwire.GossipVersion,
+ chanID uint64) error {
+
+ if v != lnwire.GossipVersion1 {
+ return ErrVersionNotSupportedForKVDB
+ }
+
c.cacheMu.Lock()
defer c.cacheMu.Unlock()
diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go
index fea367a..7536721 100644
--- a/graph/db/sql_store.go
+++ b/graph/db/sql_store.go
@@ -1812,22 +1812,27 @@ func (s *SQLStore) MarkEdgeZombie(ctx context.Context, chanID uint64,
return nil
}
-// MarkEdgeLive clears an edge from our zombie index, deeming it as live.
+// MarkEdgeLive clears an edge from our zombie index for the given gossip
+// version, deeming it as live.
//
// NOTE: part of the Store interface.
-func (s *SQLStore) MarkEdgeLive(ctx context.Context, chanID uint64) error {
+func (s *SQLStore) MarkEdgeLive(ctx context.Context,
+ v lnwire.GossipVersion, chanID uint64) error {
+
s.cacheMu.Lock()
defer s.cacheMu.Unlock()
- var (
- chanIDB = channelIDToBytes(chanID)
- )
+ if !isKnownGossipVersion(v) {
+ return fmt.Errorf("unsupported gossip version: %d", v)
+ }
+
+ chanIDB := channelIDToBytes(chanID)
err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error {
res, err := db.DeleteZombieChannel(
ctx, sqlc.DeleteZombieChannelParams{
Scid: chanIDB,
- Version: int16(lnwire.GossipVersion1),
+ Version: int16(v),
},
)
if err != nil {
@@ -1854,8 +1859,8 @@ func (s *SQLStore) MarkEdgeLive(ctx context.Context, chanID uint64) error {
"(channel_id=%d): %w", chanID, err)
}
- s.rejectCache.remove(lnwire.GossipVersion1, chanID)
- s.chanCache.remove(lnwire.GossipVersion1, chanID)
+ s.rejectCache.remove(v, chanID)
+ s.chanCache.remove(v, chanID)
return err
}
diff --git a/graph/interfaces.go b/graph/interfaces.go
index 75f4755..bc05d04 100644
--- a/graph/interfaces.go
+++ b/graph/interfaces.go
@@ -60,9 +60,9 @@ type ChannelGraphSource interface {
IsStaleEdgePolicy(chanID lnwire.ShortChannelID, timestamp time.Time,
flags lnwire.ChanUpdateChanFlags) bool
- // MarkEdgeLive clears an edge from our zombie index, deeming it as
- // live.
- MarkEdgeLive(chanID lnwire.ShortChannelID) error
+ // MarkEdgeLive clears an edge from our zombie index for the given
+ // gossip version, deeming it as live.
+ MarkEdgeLive(v lnwire.GossipVersion, chanID lnwire.ShortChannelID) error
// ForAllOutgoingChannels is used to iterate over all channels
// emanating from the "source" node which is the center of the
Why this scored 29/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.