graph/db: add version parameter to IsPublicNode
What changed, and why it matters
This commit updates how LND decides whether a Lightning node is 'public' so that the decision respects the network's gossip protocol version. In the older v1 protocol, a node needed four signatures to be considered public; in the newer v2 protocol, one signature is enough. Previously the code always used the v1 rule, which could misclassify v2 nodes as private. The change is a correctness fix rather than an obvious remote-exploitable vulnerability, but misclassified publicity could affect routing, channel selection, or policy decisions.
Review the SQL implementations of IsPublicV1Node and IsPublicV2Node to confirm the v2 query correctly enforces the one-signature rule and that no edge cases allow private nodes to be treated as public. Ensure callers that need v2-aware behavior use VersionedGraph rather than ChannelGraph, since ChannelGraph still hardcodes GossipVersion1.
Security signals we found
Protocol-version-specific logic added to node publicity classification
Previously v2 gossip nodes could be evaluated under v1 four-signature rule
KVStore explicitly rejects non-v1 gossip versions for this query
SQLStore adds IsPublicV2Node query path
No explicit security framing in commit message or diff
Evidence from the diff
The patch makes IsPublicNode version-aware. It adds a lnwire.GossipVersion parameter to the Store interface and both KVStore and SQLStore implementations. The KVStore path only supports GossipVersion1 and returns ErrVersionNotSupportedForKVDB otherwise. The SQLStore path routes to either IsPublicV1Node or IsPublicV2Node. The ChannelGraph wrapper hardcodes GossipVersion1, while a new VersionedGraph wrapper uses its configured version. The Builder’s IsPublicNode now calls b.v1Graph.IsPublicNode. Tests are converted to run against both versions.
Changed components
graph/db/Store interfacegraph/db/KVStore.IsPublicNodegraph/db/SQLStore.IsPublicNodegraph/db/ChannelGraph.IsPublicNodegraph/db/VersionedGraph.IsPublicNodegraph/Builder.IsPublicNodeInspect captured patch +46 / −24
diff --git a/graph/builder.go b/graph/builder.go
index 96a7582..1f3309c 100644
--- a/graph/builder.go
+++ b/graph/builder.go
@@ -1330,7 +1330,7 @@ func (b *Builder) IsStaleNode(ctx context.Context, node route.Vertex,
//
// NOTE: This method is part of the ChannelGraphSource interface.
func (b *Builder) IsPublicNode(node route.Vertex) (bool, error) {
- return b.cfg.Graph.IsPublicNode(node)
+ return b.v1Graph.IsPublicNode(node)
}
// IsKnownEdge returns true if the graph source already knows of the passed
diff --git a/graph/db/graph.go b/graph/db/graph.go
index 302813f..6c4b0a1 100644
--- a/graph/db/graph.go
+++ b/graph/db/graph.go
@@ -637,7 +637,7 @@ func (c *ChannelGraph) HasV1Node(ctx context.Context,
// IsPublicNode determines whether the node is seen as public in the graph.
func (c *ChannelGraph) IsPublicNode(pubKey [33]byte) (bool, error) {
- return c.db.IsPublicNode(pubKey)
+ return c.db.IsPublicNode(lnwire.GossipVersion1, pubKey)
}
// ForEachChannel iterates through all channel edges stored within the graph.
@@ -861,6 +861,11 @@ func (c *VersionedGraph) DeleteChannelEdges(strictZombiePruning,
return err
}
+// IsPublicNode determines whether the node is seen as public in the graph.
+func (c *VersionedGraph) IsPublicNode(pubKey [33]byte) (bool, error) {
+ return c.db.IsPublicNode(c.v, pubKey)
+}
+
// MakeTestGraph creates a new instance of the ChannelGraph for testing
// purposes. The backing Store implementation depends on the version of
// NewTestDB included in the current build.
diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go
index ed6a5f5..2f106dd 100644
--- a/graph/db/graph_test.go
+++ b/graph/db/graph_test.go
@@ -146,6 +146,10 @@ var versionedTests = []versionedTest{
name: "partial node",
test: testPartialNode,
},
+ {
+ name: "node is public",
+ test: testNodeIsPublic,
+ },
}
// TestVersionedDBs runs various tests against both v1 and v2 versioned
@@ -4125,9 +4129,9 @@ func nextBlockHeight() uint32 {
return updateBlock
}
-// TestNodeIsPublic ensures that we properly detect nodes that are seen as
+// testNodeIsPublic ensures that we properly detect nodes that are seen as
// public within the network graph.
-func TestNodeIsPublic(t *testing.T) {
+func testNodeIsPublic(t *testing.T, v lnwire.GossipVersion) {
t.Parallel()
ctx := t.Context()
@@ -4139,33 +4143,29 @@ func TestNodeIsPublic(t *testing.T) {
// We'll need to create a separate database and channel graph for each
// participant to replicate real-world scenarios (private edges being in
// some graphs but not others, etc.).
- aliceGraph := MakeTestGraph(t)
- aliceNode := createTestVertex(t, lnwire.GossipVersion1)
+ aliceGraph := NewVersionedGraph(MakeTestGraph(t), v)
+ aliceNode := createTestVertex(t, v)
err := aliceGraph.SetSourceNode(ctx, aliceNode)
require.NoError(t, err, "unable to set source node")
- bobGraph := MakeTestGraph(t)
- bobNode := createTestVertex(t, lnwire.GossipVersion1)
+ bobGraph := NewVersionedGraph(MakeTestGraph(t), v)
+ bobNode := createTestVertex(t, v)
err = bobGraph.SetSourceNode(ctx, bobNode)
require.NoError(t, err, "unable to set source node")
- carolGraph := MakeTestGraph(t)
- carolNode := createTestVertex(t, lnwire.GossipVersion1)
+ carolGraph := NewVersionedGraph(MakeTestGraph(t), v)
+ carolNode := createTestVertex(t, v)
err = carolGraph.SetSourceNode(ctx, carolNode)
require.NoError(t, err, "unable to set source node")
- aliceBobEdge, _ := createEdge(
- lnwire.GossipVersion1, 10, 0, 0, 0, aliceNode, bobNode,
- )
- bobCarolEdge, _ := createEdge(
- lnwire.GossipVersion1, 10, 1, 0, 1, bobNode, carolNode,
- )
+ aliceBobEdge, _ := createEdge(v, 10, 0, 0, 0, aliceNode, bobNode)
+ bobCarolEdge, _ := createEdge(v, 10, 1, 0, 1, bobNode, carolNode)
// After creating all of our nodes and edges, we'll add them to each
// participant's graph.
nodes := []*models.Node{aliceNode, bobNode, carolNode}
edges := []*models.ChannelEdgeInfo{aliceBobEdge, bobCarolEdge}
- graphs := []*ChannelGraph{aliceGraph, bobGraph, carolGraph}
+ graphs := []*VersionedGraph{aliceGraph, bobGraph, carolGraph}
for _, graph := range graphs {
for _, node := range nodes {
node.LastUpdate = nextUpdateTime()
@@ -4181,7 +4181,7 @@ func TestNodeIsPublic(t *testing.T) {
// checkNodes is a helper closure that will be used to assert that the
// given nodes are seen as public/private within the given graphs.
checkNodes := func(nodes []*models.Node,
- graphs []*ChannelGraph, public bool) {
+ graphs []*VersionedGraph, public bool) {
t.Helper()
@@ -4212,7 +4212,7 @@ func TestNodeIsPublic(t *testing.T) {
}
checkNodes(
[]*models.Node{aliceNode},
- []*ChannelGraph{bobGraph, carolGraph},
+ []*VersionedGraph{bobGraph, carolGraph},
false,
)
@@ -4240,7 +4240,7 @@ func TestNodeIsPublic(t *testing.T) {
// node from both Alice's and Carol's perspective.
checkNodes(
[]*models.Node{bobNode},
- []*ChannelGraph{aliceGraph, carolGraph},
+ []*VersionedGraph{aliceGraph, carolGraph},
false,
)
}
diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go
index 24cfae9..5658fa5 100644
--- a/graph/db/interfaces.go
+++ b/graph/db/interfaces.go
@@ -146,7 +146,7 @@ type Store interface { //nolint:interfacebloat
// IsPublicNode is a helper method that determines whether the node with
// the given public key is seen as a public node in the graph from the
// graph's source node's point of view.
- IsPublicNode(pubKey [33]byte) (bool, error)
+ IsPublicNode(v lnwire.GossipVersion, pubKey [33]byte) (bool, error)
// GraphSession will provide the call-back with access to a
// NodeTraverser instance which can be used to perform queries against
diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go
index 38b6c97..b3442bd 100644
--- a/graph/db/kv_store.go
+++ b/graph/db/kv_store.go
@@ -4005,7 +4005,13 @@ func (c *KVStore) FetchChannelEdgesByID(chanID uint64) (
// IsPublicNode is a helper method that determines whether the node with the
// given public key is seen as a public node in the graph from the graph's
// source node's point of view.
-func (c *KVStore) IsPublicNode(pubKey [33]byte) (bool, error) {
+func (c *KVStore) IsPublicNode(v lnwire.GossipVersion, pubKey [33]byte) (bool,
+ error) {
+
+ if v != lnwire.GossipVersion1 {
+ return false, ErrVersionNotSupportedForKVDB
+ }
+
var nodeIsPublic bool
err := kvdb.View(c.db, func(tx kvdb.RTx) error {
nodes := tx.ReadBucket(nodeBucket)
diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go
index 18ab233..2f0b1c5 100644
--- a/graph/db/sql_store.go
+++ b/graph/db/sql_store.go
@@ -50,6 +50,7 @@ type SQLQueries interface {
ListNodesPaginated(ctx context.Context, arg sqlc.ListNodesPaginatedParams) ([]sqlc.GraphNode, error)
ListNodeIDsAndPubKeys(ctx context.Context, arg sqlc.ListNodeIDsAndPubKeysParams) ([]sqlc.ListNodeIDsAndPubKeysRow, error)
IsPublicV1Node(ctx context.Context, pubKey []byte) (bool, error)
+ IsPublicV2Node(ctx context.Context, pubKey []byte) (bool, error)
DeleteUnconnectedNodes(ctx context.Context) ([][]byte, error)
DeleteNodeByPubKey(ctx context.Context, arg sqlc.DeleteNodeByPubKeyParams) (sql.Result, error)
DeleteNode(ctx context.Context, id int64) error
@@ -2338,13 +2339,23 @@ func (s *SQLStore) ChannelID(chanPoint *wire.OutPoint) (uint64, error) {
// source node's point of view.
//
// NOTE: part of the Store interface.
-func (s *SQLStore) IsPublicNode(pubKey [33]byte) (bool, error) {
+func (s *SQLStore) IsPublicNode(v lnwire.GossipVersion, pubKey [33]byte) (bool,
+ error) {
+
ctx := context.TODO()
+ if !isKnownGossipVersion(v) {
+ return false, fmt.Errorf("unsupported gossip version: %d", v)
+ }
var isPublic bool
err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
var err error
- isPublic, err = db.IsPublicV1Node(ctx, pubKey[:])
+ switch v {
+ case lnwire.GossipVersion1:
+ isPublic, err = db.IsPublicV1Node(ctx, pubKey[:])
+ case lnwire.GossipVersion2:
+ isPublic, err = db.IsPublicV2Node(ctx, pubKey[:])
+ }
return err
}, sqldb.NoOpReset)
Why this scored 24/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.