What changed, and why it matters
This commit changes how LND tracks the newest channel ID in its network graph so the lookup can be filtered by a specific gossip protocol version. It is a structural refactoring to support multiple gossip versions, not a fix for an active security flaw. The change adds version checks that reject unsupported gossip versions, which is a defensive improvement, but the commit message and diff do not describe any vulnerability or security incident.
Treat as a normal code-quality and protocol-versioning change. Review related commits in the same release branch to confirm whether this is part of a larger security fix or feature work. No immediate security response is indicated by this commit alone.
Security signals we found
Adds explicit version validation in `KVStore.HighestChanID` and `SQLStore.HighestChanID`
Prevents queries against unsupported gossip versions
Refactors graph synchronization primitives to be version-aware
Evidence from the diff
The patch adds a lnwire.GossipVersion parameter to the HighestChanID method across the Store interface, KV store, SQL store, ChannelGraph, and VersionedGraph. The KV implementation now returns ErrVersionNotSupportedForKVDB for any version other than GossipVersion1, while the SQL implementation validates the version and queries HighestSCID with the requested version. Callers in discovery.NewChanSeries and server.go are updated to pass a versioned graph using GossipVersion1. The existing test is converted into a versioned test that runs against both gossip versions.
Changed components
graph/db/interfaces.gograph/db/kv_store.gograph/db/sql_store.gograph/db/graph.godiscovery/chan_series.goserver.gograph/db/graph_test.goInspect captured patch +49 / −39
diff --git a/discovery/chan_series.go b/discovery/chan_series.go
index 9ba9607..ed64453 100644
--- a/discovery/chan_series.go
+++ b/discovery/chan_series.go
@@ -73,12 +73,12 @@ type ChannelGraphTimeSeries interface {
// in-protocol channel range queries to quickly and efficiently synchronize our
// channel state with all peers.
type ChanSeries struct {
- graph *graphdb.ChannelGraph
+ graph *graphdb.VersionedGraph
}
// NewChanSeries constructs a new ChanSeries backed by a channeldb.ChannelGraph.
// The returned ChanSeries implements the ChannelGraphTimeSeries interface.
-func NewChanSeries(graph *graphdb.ChannelGraph) *ChanSeries {
+func NewChanSeries(graph *graphdb.VersionedGraph) *ChanSeries {
return &ChanSeries{
graph: graph,
}
diff --git a/graph/db/graph.go b/graph/db/graph.go
index 4579f16..f963286 100644
--- a/graph/db/graph.go
+++ b/graph/db/graph.go
@@ -688,8 +688,10 @@ func (c *ChannelGraph) AddEdgeProof(chanID lnwire.ShortChannelID,
}
// HighestChanID returns the "highest" known channel ID in the channel graph.
-func (c *ChannelGraph) HighestChanID(ctx context.Context) (uint64, error) {
- return c.db.HighestChanID(ctx)
+func (c *ChannelGraph) HighestChanID(ctx context.Context,
+ v lnwire.GossipVersion) (uint64, error) {
+
+ return c.db.HighestChanID(ctx, v)
}
// ChanUpdatesInHorizon returns all known channel edges with updates in the
@@ -950,6 +952,11 @@ func (c *VersionedGraph) DisabledChannelIDs() ([]uint64, error) {
return c.db.DisabledChannelIDs(c.v)
}
+// HighestChanID returns the "highest" known channel ID in the channel graph.
+func (c *VersionedGraph) HighestChanID(ctx context.Context) (uint64, error) {
+ return c.db.HighestChanID(ctx, c.v)
+}
+
// ChannelID attempts to lookup the 8-byte compact channel ID.
func (c *VersionedGraph) ChannelID(chanPoint *wire.OutPoint) (uint64, error) {
return c.db.ChannelID(c.v, chanPoint)
diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go
index d56c42d..6516489 100644
--- a/graph/db/graph_test.go
+++ b/graph/db/graph_test.go
@@ -198,6 +198,10 @@ var versionedTests = []versionedTest{
name: "graph cache for each node channel",
test: testGraphCacheForEachNodeChannel,
},
+ {
+ name: "highest chan id",
+ test: testHighestChanID,
+ },
}
// TestVersionedDBs runs various tests against both v1 and v2 versioned
@@ -2484,41 +2488,32 @@ func TestGraphPruning(t *testing.T) {
// TestHighestChanID tests that we're able to properly retrieve the highest
// known channel ID in the database.
-func TestHighestChanID(t *testing.T) {
+func testHighestChanID(t *testing.T, v lnwire.GossipVersion) {
t.Parallel()
ctx := t.Context()
- graph := MakeTestGraph(t)
+ graph := NewVersionedGraph(MakeTestGraph(t), v)
// If we don't yet have any channels in the database, then we should
// get a channel ID of zero if we ask for the highest channel ID.
bestID, err := graph.HighestChanID(ctx)
require.NoError(t, err, "unable to get highest ID")
if bestID != 0 {
- t.Fatalf("best ID w/ no chan should be zero, is instead: %v",
- bestID)
+ require.Equal(t, uint64(0), bestID)
}
// Next, we'll insert two channels into the database, with each channel
// connecting the same two nodes.
- node1 := createTestVertex(t, lnwire.GossipVersion1)
- node2 := createTestVertex(t, lnwire.GossipVersion1)
+ node1 := createTestVertex(t, v)
+ node2 := createTestVertex(t, v)
// The first channel with be at height 10, while the other will be at
// height 100.
- edge1, _ := createEdge(
- lnwire.GossipVersion1, 10, 0, 0, 0, node1, node2,
- )
- edge2, chanID2 := createEdge(
- lnwire.GossipVersion1, 100, 0, 0, 0, node1, node2,
- )
+ edge1, _ := createEdge(v, 10, 0, 0, 0, node1, node2)
+ edge2, chanID2 := createEdge(v, 100, 0, 0, 0, node1, node2)
- if err := graph.AddChannelEdge(ctx, edge1); err != nil {
- t.Fatalf("unable to create channel edge: %v", err)
- }
- if err := graph.AddChannelEdge(ctx, edge2); err != nil {
- t.Fatalf("unable to create channel edge: %v", err)
- }
+ require.NoError(t, graph.AddChannelEdge(ctx, edge1))
+ require.NoError(t, graph.AddChannelEdge(ctx, edge2))
// Now that the edges has been inserted, we'll query for the highest
// known channel ID in the database.
@@ -2526,25 +2521,17 @@ func TestHighestChanID(t *testing.T) {
require.NoError(t, err, "unable to get highest ID")
if bestID != chanID2.ToUint64() {
- t.Fatalf("expected %v got %v for best chan ID: ",
- chanID2.ToUint64(), bestID)
+ require.Equal(t, chanID2.ToUint64(), bestID)
}
// If we add another edge, then the current best chan ID should be
// updated as well.
- edge3, chanID3 := createEdge(
- lnwire.GossipVersion1, 1000, 0, 0, 0, node1, node2,
- )
- if err := graph.AddChannelEdge(ctx, edge3); err != nil {
- t.Fatalf("unable to create channel edge: %v", err)
- }
+ edge3, chanID3 := createEdge(v, 1000, 0, 0, 0, node1, node2)
+ require.NoError(t, graph.AddChannelEdge(ctx, edge3))
bestID, err = graph.HighestChanID(ctx)
require.NoError(t, err, "unable to get highest ID")
- if bestID != chanID3.ToUint64() {
- t.Fatalf("expected %v got %v for best chan ID: ",
- chanID3.ToUint64(), bestID)
- }
+ require.Equal(t, chanID3.ToUint64(), bestID)
}
// TestChanUpdatesInHorizon tests the we're able to properly retrieve all known
@@ -5210,6 +5197,7 @@ func BenchmarkForEachChannel(b *testing.B) {
// method works as expected, and is able to handle nil self edges.
func testGraphCacheForEachNodeChannel(t *testing.T,
v lnwire.GossipVersion) {
+
t.Parallel()
ctx := t.Context()
diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go
index 9613ff6..752473e 100644
--- a/graph/db/interfaces.go
+++ b/graph/db/interfaces.go
@@ -246,7 +246,8 @@ type Store interface { //nolint:interfacebloat
// graph. This represents the "newest" channel from the PoV of the
// chain. This method can be used by peers to quickly determine if
// they're graphs are in sync.
- HighestChanID(ctx context.Context) (uint64, error)
+ HighestChanID(ctx context.Context, v lnwire.GossipVersion) (
+ uint64, error)
// ChanUpdatesInHorizon returns all the known channel edges which have
// at least one edge that has an update timestamp within the specified
diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go
index d61713a..2d3fdf8 100644
--- a/graph/db/kv_store.go
+++ b/graph/db/kv_store.go
@@ -2056,7 +2056,13 @@ func getChanID(tx kvdb.RTx, chanPoint *wire.OutPoint) (uint64, error) {
// HighestChanID returns the "highest" known channel ID in the channel graph.
// This represents the "newest" channel from the PoV of the chain. This method
// can be used by peers to quickly determine if they're graphs are in sync.
-func (c *KVStore) HighestChanID(_ context.Context) (uint64, error) {
+func (c *KVStore) HighestChanID(_ context.Context,
+ v lnwire.GossipVersion) (uint64, error) {
+
+ if v != lnwire.GossipVersion1 {
+ return 0, ErrVersionNotSupportedForKVDB
+ }
+
var cid uint64
err := kvdb.View(c.db, func(tx kvdb.RTx) error {
diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go
index 4c64e2e..9bfb851 100644
--- a/graph/db/sql_store.go
+++ b/graph/db/sql_store.go
@@ -786,10 +786,16 @@ func (s *SQLStore) AddChannelEdge(ctx context.Context,
// can be used by peers to quickly determine if their graphs are in sync.
//
// NOTE: This is part of the Store interface.
-func (s *SQLStore) HighestChanID(ctx context.Context) (uint64, error) {
+func (s *SQLStore) HighestChanID(ctx context.Context,
+ v lnwire.GossipVersion) (uint64, error) {
+
var highestChanID uint64
err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
- chanID, err := db.HighestSCID(ctx, int16(lnwire.GossipVersion1))
+ if !isKnownGossipVersion(v) {
+ return fmt.Errorf("unsupported gossip version: %d", v)
+ }
+
+ chanID, err := db.HighestSCID(ctx, int16(v))
if errors.Is(err, sql.ErrNoRows) {
return nil
} else if err != nil {
diff --git a/server.go b/server.go
index a91d266..91b6624 100644
--- a/server.go
+++ b/server.go
@@ -1042,7 +1042,9 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
return nil, fmt.Errorf("can't create router: %w", err)
}
- chanSeries := discovery.NewChanSeries(s.graphDB)
+ chanSeries := discovery.NewChanSeries(
+ graphdb.NewVersionedGraph(s.graphDB, lnwire.GossipVersion1),
+ )
gossipMessageStore, err := discovery.NewMessageStore(dbs.ChanStateDB)
if err != nil {
return nil, err
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.