graph/db: add version parameter to IsZombieEdge
What changed, and why it matters
This commit extends the channel graph database so that zombie-edge lookups know which gossip protocol version (v1 or v2) they are dealing with. Previously the code only checked v1 zombie records; now it can check the matching version. It is a correctness/refactoring change rather than a fix for an active vulnerability, but it removes a version-confusion bug class that could cause routing or channel-state mistakes in a multi-version Lightning network.
No immediate action required. Treat as routine correctness improvement. If deploying, verify that any callers of IsZombieEdge outside this repo supply a valid GossipVersion, since the signature changed.
Security signals we found
version-aware query prevents cross-version zombie state confusion
hard-coded GossipVersion1 replaced with parameterized version
unsupported gossip versions now return explicit errors instead of silently using v1 data
test coverage expanded to both v1 and v2 channel edges
Evidence from the diff
The Store interface’s IsZombieEdge method now takes a lnwire.GossipVersion parameter. The KVStore implementation rejects non-v1 versions (KVDB only supports v1), while the SQLStore implementation queries the zombie table using the supplied version instead of hard-coding GossipVersion1. A VersionedGraph wrapper is added so callers can ask about zombies for a specific version, and the legacy ChannelGraph continues to default to GossipVersion1. Tests are converted to exercise both v1 and v2 through a shared createEdge helper.
Changed components
graph/db/graph.gograph/db/interfaces.gograph/db/kv_store.gograph/db/sql_store.gograph/db/graph_test.goInspect captured patch +103 / −65
diff --git a/graph/db/graph.go b/graph/db/graph.go
index 0f4dda6..41396ac 100644
--- a/graph/db/graph.go
+++ b/graph/db/graph.go
@@ -735,7 +735,7 @@ func (c *ChannelGraph) ChannelView() ([]EdgePoint, error) {
func (c *ChannelGraph) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte,
error) {
- return c.db.IsZombieEdge(chanID)
+ return c.db.IsZombieEdge(lnwire.GossipVersion1, chanID)
}
// NumZombies returns the current number of zombie channels in the graph.
@@ -806,6 +806,13 @@ func (c *VersionedGraph) FetchChannelEdgesByOutpoint(op *wire.OutPoint) (
return c.db.FetchChannelEdgesByOutpoint(c.v, op)
}
+// IsZombieEdge returns whether the edge is considered zombie for this version.
+func (c *VersionedGraph) IsZombieEdge(chanID uint64) (bool, [33]byte,
+ [33]byte, error) {
+
+ return c.db.IsZombieEdge(c.v, chanID)
+}
+
// AddrsForNode returns all known addresses for the target node public key.
func (c *VersionedGraph) AddrsForNode(ctx context.Context,
nodePub *btcec.PublicKey) (bool, []net.Addr, error) {
diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go
index 2f106dd..a6dcb10 100644
--- a/graph/db/graph_test.go
+++ b/graph/db/graph_test.go
@@ -142,6 +142,10 @@ var versionedTests = []versionedTest{
name: "add edge proof",
test: testAddEdgeProof,
},
+ {
+ name: "edge insertion deletion",
+ test: testEdgeInsertionDeletion,
+ },
{
name: "partial node",
test: testPartialNode,
@@ -549,88 +553,104 @@ func TestSetSourceNodeSameTimestamp(t *testing.T) {
require.Equal(t, testNode.LastUpdate, updatedNode.LastUpdate)
}
-// TestEdgeInsertionDeletion tests the basic CRUD operations for channel edges.
-func TestEdgeInsertionDeletion(t *testing.T) {
+// testEdgeInsertionDeletion tests the basic CRUD operations for channel edges.
+func testEdgeInsertionDeletion(t *testing.T, v lnwire.GossipVersion) {
t.Parallel()
ctx := t.Context()
- graph := MakeTestGraph(t)
+ graph := NewVersionedGraph(MakeTestGraph(t), v)
// We'd like to test the insertion/deletion of edges, so we create two
// vertexes to connect.
- node1 := createTestVertex(t, lnwire.GossipVersion1)
- node2 := createTestVertex(t, lnwire.GossipVersion1)
-
- // In addition to the fake vertexes we create some fake channel
- // identifiers.
- chanID := uint64(prand.Int63())
- outpoint := wire.OutPoint{
- Hash: rev,
- Index: 9,
- }
-
- // Add the new edge to the database, this should proceed without any
- // errors.
- node1Pub, err := node1.PubKey()
- require.NoError(t, err, "unable to generate node key")
- node2Pub, err := node2.PubKey()
- require.NoError(t, err, "unable to generate node key")
-
- node1Vertex, err := route.NewVertexFromBytes(
- node1Pub.SerializeCompressed(),
- )
- require.NoError(t, err)
- node2Vertex, err := route.NewVertexFromBytes(
- node2Pub.SerializeCompressed(),
- )
- require.NoError(t, err)
-
- btcKey1, err := route.NewVertexFromBytes(
- node1Pub.SerializeCompressed(),
- )
- require.NoError(t, err)
- btcKey2, err := route.NewVertexFromBytes(
- node2Pub.SerializeCompressed(),
- )
- require.NoError(t, err)
+ node1 := createTestVertex(t, v)
+ node2 := createTestVertex(t, v)
- proof := models.NewV1ChannelAuthProof(
- testSig.Serialize(),
- testSig.Serialize(),
- testSig.Serialize(),
- testSig.Serialize(),
+ // Create a fake channel and add it to the graph.
+ const (
+ blockHeight = 1234
+ txIndex = 1
+ txPosition = 0
+ outPointIndex = 9
)
- edgeInfo, err := models.NewV1Channel(
- chanID, *chaincfg.MainNetParams.GenesisHash, node1Vertex,
- node2Vertex, &models.ChannelV1Fields{
- BitcoinKey1Bytes: btcKey1,
- BitcoinKey2Bytes: btcKey2,
- },
- models.WithChanProof(proof),
- models.WithChannelPoint(outpoint),
- models.WithCapacity(9000),
+ edgeInfo, shortChanID := createEdge(
+ v, blockHeight, txIndex, txPosition, outPointIndex, node1,
+ node2,
)
- require.NoError(t, err)
+ chanID := shortChanID.ToUint64()
+ outpoint := wire.OutPoint{
+ Hash: rev,
+ Index: outPointIndex,
+ }
require.NoError(t, graph.AddChannelEdge(ctx, edgeInfo))
- assertEdgeWithNoPoliciesInCache(t, graph, edgeInfo)
+ assertEdgeWithNoPoliciesInCache(t, graph.ChannelGraph, edgeInfo)
// Show that trying to insert the same channel again will return the
// expected error.
- err = graph.AddChannelEdge(ctx, edgeInfo)
+ err := graph.AddChannelEdge(ctx, edgeInfo)
require.ErrorIs(t, err, ErrEdgeAlreadyExist)
- // Ensure that both policies are returned as unknown (nil).
- _, e1, e2, err := graph.FetchChannelEdgesByID(chanID)
+ // Ensure that both policies are returned as unknown (nil) and that
+ // the edge info round-trips correctly.
+ dbEdge, e1, e2, err := graph.FetchChannelEdgesByID(chanID)
require.NoError(t, err)
require.Nil(t, e1)
require.Nil(t, e2)
+ // Verify core fields match.
+ require.Equal(t, edgeInfo.ChannelID, dbEdge.ChannelID)
+ require.Equal(t, edgeInfo.Version, dbEdge.Version)
+ require.Equal(t, edgeInfo.NodeKey1Bytes, dbEdge.NodeKey1Bytes)
+ require.Equal(t, edgeInfo.NodeKey2Bytes, dbEdge.NodeKey2Bytes)
+ require.Equal(t, edgeInfo.ChainHash, dbEdge.ChainHash)
+ require.Equal(t, edgeInfo.ChannelPoint, dbEdge.ChannelPoint)
+ require.Equal(t, edgeInfo.Capacity, dbEdge.Capacity)
+
+ // Verify auth proof round-trips.
+ require.NotNil(t, dbEdge.AuthProof)
+ require.Equal(t, edgeInfo.AuthProof.Version, dbEdge.AuthProof.Version)
+
+ // Verify version-specific fields.
+ switch v {
+ case lnwire.GossipVersion1:
+ require.Equal(t,
+ edgeInfo.BitcoinKey1Bytes, dbEdge.BitcoinKey1Bytes,
+ )
+ require.Equal(t,
+ edgeInfo.BitcoinKey2Bytes, dbEdge.BitcoinKey2Bytes,
+ )
+ require.Equal(t,
+ edgeInfo.ExtraOpaqueData, dbEdge.ExtraOpaqueData,
+ )
+
+ case lnwire.GossipVersion2:
+ require.Equal(t,
+ edgeInfo.BitcoinKey1Bytes, dbEdge.BitcoinKey1Bytes,
+ )
+ require.Equal(t,
+ edgeInfo.BitcoinKey2Bytes, dbEdge.BitcoinKey2Bytes,
+ )
+ require.Equal(t,
+ edgeInfo.MerkleRootHash, dbEdge.MerkleRootHash,
+ )
+ require.Equal(t,
+ edgeInfo.FundingScript, dbEdge.FundingScript,
+ )
+ require.Equal(t,
+ edgeInfo.ExtraSignedFields, dbEdge.ExtraSignedFields,
+ )
+ }
+
+ // Also verify fetching by outpoint returns the same data.
+ dbEdge2, _, _, err := graph.FetchChannelEdgesByOutpoint(&outpoint)
+ require.NoError(t, err)
+ require.Equal(t, dbEdge.ChannelID, dbEdge2.ChannelID)
+
// Next, attempt to delete the edge from the database, again this
// should proceed without any issues.
require.NoError(t, graph.DeleteChannelEdges(false, true, chanID))
- assertNoEdge(t, graph, chanID)
+ assertNoEdge(t, graph.ChannelGraph, chanID)
// Ensure that any query attempts to lookup the delete channel edge are
// properly deleted.
diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go
index c684813..24a48e4 100644
--- a/graph/db/interfaces.go
+++ b/graph/db/interfaces.go
@@ -314,7 +314,8 @@ type Store interface { //nolint:interfacebloat
// IsZombieEdge returns whether the edge is considered zombie. If it is
// a zombie, then the two node public keys corresponding to this edge
// are also returned.
- IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte, error)
+ IsZombieEdge(v lnwire.GossipVersion, chanID uint64) (bool, [33]byte,
+ [33]byte, error)
// NumZombies returns the current number of zombie channels in the
// graph.
diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go
index 0887ff0..550ed32 100644
--- a/graph/db/kv_store.go
+++ b/graph/db/kv_store.go
@@ -4257,14 +4257,19 @@ func (c *KVStore) markEdgeLiveUnsafe(tx kvdb.RwTx, 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 are also
// returned.
-func (c *KVStore) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte,
- error) {
+func (c *KVStore) IsZombieEdge(v lnwire.GossipVersion,
+ chanID uint64) (bool, [33]byte, [33]byte, error) {
var (
isZombie bool
pubKey1, pubKey2 [33]byte
)
+ if v != lnwire.GossipVersion1 {
+ return false, [33]byte{}, [33]byte{},
+ ErrVersionNotSupportedForKVDB
+ }
+
err := kvdb.View(c.db, func(tx kvdb.RTx) error {
edges := tx.ReadBucket(edgeBucket)
if edges == nil {
diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go
index 7cebede..c551103 100644
--- a/graph/db/sql_store.go
+++ b/graph/db/sql_store.go
@@ -1824,8 +1824,8 @@ func (s *SQLStore) MarkEdgeLive(chanID uint64) error {
// returned.
//
// NOTE: part of the Store interface.
-func (s *SQLStore) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte,
- error) {
+func (s *SQLStore) IsZombieEdge(v lnwire.GossipVersion,
+ chanID uint64) (bool, [33]byte, [33]byte, error) {
var (
ctx = context.TODO()
@@ -1834,11 +1834,16 @@ func (s *SQLStore) IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte,
chanIDB = channelIDToBytes(chanID)
)
+ if !isKnownGossipVersion(v) {
+ return false, [33]byte{}, [33]byte{},
+ fmt.Errorf("unsupported gossip version: %d", v)
+ }
+
err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
zombie, err := db.GetZombieChannel(
ctx, sqlc.GetZombieChannelParams{
Scid: chanIDB,
- Version: int16(lnwire.GossipVersion1),
+ Version: int16(v),
},
)
if errors.Is(err, sql.ErrNoRows) {
Why this scored 19/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.