graph/db: make some ChannelEdgeInfo fields optional
What changed, and why it matters
This commit changes how Lightning Network channel announcement data is stored so that two public-key fields (BitcoinKey1Bytes and BitcoinKey2Bytes) are now optional rather than always required. This is preparation for a newer channel-announcement format (V2) where those keys may not be present. The change itself is a data-model refactor, not a fix for an active bug or vulnerability. It adds explicit error handling when those keys are unexpectedly missing in places that still require them, which is a defensive improvement.
Treat as a routine refactor. Reviewers should verify that all V1 code paths requiring bitcoin keys now correctly error when keys are None, and that V2 paths do not accidentally rely on these keys being present. Pay attention to UnwrapOr fallback sites (e.g., genMultiSigP2WSH) to ensure they are never reached with absent keys in production.
Security signals we found
Data model change from required to optional public-key fields
Addition of explicit error handling (UnwrapOrErr) on required serialization/announcement paths
Potential for zero-value public keys to be used in multi-sig script generation when keys are absent (UnwrapOr route.Vertex{})
No mention of CVE, bug bounty, or security advisory in commit message
Evidence from the diff
The patch wraps ChannelEdgeInfo.BitcoinKey1Bytes and BitcoinKey2Bytes in fn.Option[route.Vertex] because V2 gossip channel announcements do not require these bitcoin keys. NewV1Channel continues to populate them with fn.Some(…). Serialization paths that still require the keys (KV store putChanEdgeInfo/deserializeChanEdgeInfo and ToChannelAnnouncement) now explicitly unwrap with UnwrapOrErr and return errors if absent. Non-critical consumers (graph_test.go, ChannelView, SQL store) use UnwrapOr or WhenSome to tolerate absent keys. The SQL store now leaves BitcoinKey1/BitcoinKey2 as default zero-length values when the option is None. No security bug is fixed; the change is architectural to support V2 channels.
Changed components
graph/db/models/channel_edge_info.gograph/db/kv_store.gograph/db/sql_store.gograph/db/graph_test.goInspect captured patch +73 / −27
diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go
index c9e703f..5c9c030 100644
--- a/graph/db/graph_test.go
+++ b/graph/db/graph_test.go
@@ -838,10 +838,14 @@ func assertEdgeInfoEqual(t *testing.T, e1 *models.ChannelEdgeInfo,
if !bytes.Equal(e1.NodeKey2Bytes[:], e2.NodeKey2Bytes[:]) {
t.Fatalf("nodekey2 doesn't match")
}
- if !bytes.Equal(e1.BitcoinKey1Bytes[:], e2.BitcoinKey1Bytes[:]) {
+ btcKey1E1 := e1.BitcoinKey1Bytes.UnwrapOr(route.Vertex{})
+ btcKey1E2 := e2.BitcoinKey1Bytes.UnwrapOr(route.Vertex{})
+ if !bytes.Equal(btcKey1E1[:], btcKey1E2[:]) {
t.Fatalf("bitcoinkey1 doesn't match")
}
- if !bytes.Equal(e1.BitcoinKey2Bytes[:], e2.BitcoinKey2Bytes[:]) {
+ btcKey2E1 := e1.BitcoinKey2Bytes.UnwrapOr(route.Vertex{})
+ btcKey2E2 := e2.BitcoinKey2Bytes.UnwrapOr(route.Vertex{})
+ if !bytes.Equal(btcKey2E1[:], btcKey2E2[:]) {
t.Fatalf("bitcoinkey2 doesn't match")
}
@@ -2040,9 +2044,10 @@ func TestGraphPruning(t *testing.T) {
t.Fatalf("unable to add node: %v", err)
}
+ btcKey1 := edgeInfo.BitcoinKey1Bytes.UnwrapOr(route.Vertex{})
+ btcKey2 := edgeInfo.BitcoinKey2Bytes.UnwrapOr(route.Vertex{})
pkScript, err := genMultiSigP2WSH(
- edgeInfo.BitcoinKey1Bytes[:],
- edgeInfo.BitcoinKey2Bytes[:],
+ btcKey1[:], btcKey2[:],
)
if err != nil {
t.Fatalf("unable to gen multi-sig p2wsh: %v", err)
diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go
index 14185f1..f5a1ec2 100644
--- a/graph/db/kv_store.go
+++ b/graph/db/kv_store.go
@@ -4112,9 +4112,14 @@ func (c *KVStore) ChannelView() ([]EdgePoint, error) {
return err
}
+ btcKey1 := edgeInfo.BitcoinKey1Bytes.UnwrapOr(
+ route.Vertex{},
+ )
+ btcKey2 := edgeInfo.BitcoinKey2Bytes.UnwrapOr(
+ route.Vertex{},
+ )
pkScript, err := genMultiSigP2WSH(
- edgeInfo.BitcoinKey1Bytes[:],
- edgeInfo.BitcoinKey2Bytes[:],
+ btcKey1[:], btcKey2[:],
)
if err != nil {
return err
@@ -4728,10 +4733,24 @@ func putChanEdgeInfo(edgeIndex kvdb.RwBucket,
if _, err := b.Write(edgeInfo.NodeKey2Bytes[:]); err != nil {
return err
}
- if _, err := b.Write(edgeInfo.BitcoinKey1Bytes[:]); err != nil {
+
+ btc1Key, err := edgeInfo.BitcoinKey1Bytes.UnwrapOrErr(
+ fmt.Errorf("edge missing bitcoin key 1"),
+ )
+ if err != nil {
+ return err
+ }
+ btc2Key, err := edgeInfo.BitcoinKey2Bytes.UnwrapOrErr(
+ fmt.Errorf("edge missing bitcoin key 2"),
+ )
+ if err != nil {
return err
}
- if _, err := b.Write(edgeInfo.BitcoinKey2Bytes[:]); err != nil {
+
+ if _, err := b.Write(btc1Key[:]); err != nil {
+ return err
+ }
+ if _, err := b.Write(btc2Key[:]); err != nil {
return err
}
@@ -4769,7 +4788,7 @@ func putChanEdgeInfo(edgeIndex kvdb.RwBucket,
if err := WriteOutpoint(&b, &edgeInfo.ChannelPoint); err != nil {
return err
}
- err := binary.Write(&b, byteOrder, uint64(edgeInfo.Capacity))
+ err = binary.Write(&b, byteOrder, uint64(edgeInfo.Capacity))
if err != nil {
return err
}
@@ -4891,12 +4910,17 @@ func deserializeChanEdgeInfo(r io.Reader) (*models.ChannelEdgeInfo, error) {
if _, err := io.ReadFull(r, edgeInfo.NodeKey2Bytes[:]); err != nil {
return nil, err
}
- if _, err := io.ReadFull(r, edgeInfo.BitcoinKey1Bytes[:]); err != nil {
+
+ var btcKey1, btcKey2 route.Vertex
+ if _, err := io.ReadFull(r, btcKey1[:]); err != nil {
return nil, err
}
- if _, err := io.ReadFull(r, edgeInfo.BitcoinKey2Bytes[:]); err != nil {
+ edgeInfo.BitcoinKey1Bytes = fn.Some(btcKey1)
+
+ if _, err := io.ReadFull(r, btcKey2[:]); err != nil {
return nil, err
}
+ edgeInfo.BitcoinKey2Bytes = fn.Some(btcKey2)
featureBytes, err := wire.ReadVarBytes(r, 0, 900, "features")
if err != nil {
diff --git a/graph/db/models/channel_edge_info.go b/graph/db/models/channel_edge_info.go
index 9f94770..aea69e9 100644
--- a/graph/db/models/channel_edge_info.go
+++ b/graph/db/models/channel_edge_info.go
@@ -39,10 +39,10 @@ type ChannelEdgeInfo struct {
NodeKey2Bytes route.Vertex
// BitcoinKey1Bytes is the raw public key of the first node.
- BitcoinKey1Bytes route.Vertex
+ BitcoinKey1Bytes fn.Option[route.Vertex]
// BitcoinKey2Bytes is the raw public key of the second node.
- BitcoinKey2Bytes route.Vertex
+ BitcoinKey2Bytes fn.Option[route.Vertex]
// Features is the list of protocol features supported by this channel
// edge.
@@ -147,8 +147,8 @@ func NewV1Channel(chanID uint64, chainHash chainhash.Hash, node1,
Version: lnwire.GossipVersion1,
NodeKey1Bytes: node1,
NodeKey2Bytes: node2,
- BitcoinKey1Bytes: v1Fields.BitcoinKey1Bytes,
- BitcoinKey2Bytes: v1Fields.BitcoinKey2Bytes,
+ BitcoinKey1Bytes: fn.Some(v1Fields.BitcoinKey1Bytes),
+ BitcoinKey2Bytes: fn.Some(v1Fields.BitcoinKey2Bytes),
ChannelID: chanID,
ChainHash: chainHash,
Features: lnwire.EmptyFeatureVector(),
@@ -219,19 +219,32 @@ func (c *ChannelEdgeInfo) ToChannelAnnouncement() (
"without auth proof")
}
+ btc1, err := c.BitcoinKey1Bytes.UnwrapOrErr(
+ fmt.Errorf("bitcoin key 1 missing for v1 channel announcement"),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ btc2, err := c.BitcoinKey2Bytes.UnwrapOrErr(
+ fmt.Errorf("bitcoin key 2 missing for v1 channel announcement"),
+ )
+ if err != nil {
+ return nil, err
+ }
+
chanID := lnwire.NewShortChanIDFromInt(c.ChannelID)
chanAnn := &lnwire.ChannelAnnouncement1{
ShortChannelID: chanID,
NodeID1: c.NodeKey1Bytes,
NodeID2: c.NodeKey2Bytes,
ChainHash: c.ChainHash,
- BitcoinKey1: c.BitcoinKey1Bytes,
- BitcoinKey2: c.BitcoinKey2Bytes,
+ BitcoinKey1: btc1,
+ BitcoinKey2: btc2,
Features: c.Features.RawFeatureVector,
ExtraOpaqueData: c.ExtraOpaqueData,
}
- var err error
chanAnn.NodeSig1, err = lnwire.NewSigFromECDSARawSignature(
c.AuthProof.NodeSig1(),
)
diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go
index 0ef5855..281975e 100644
--- a/graph/db/sql_store.go
+++ b/graph/db/sql_store.go
@@ -4288,15 +4288,19 @@ func insertChannel(ctx context.Context, db SQLQueries,
}
createParams := sqlc.CreateChannelParams{
- Version: int16(v),
- Scid: channelIDToBytes(edge.ChannelID),
- NodeID1: node1DBID,
- NodeID2: node2DBID,
- Outpoint: edge.ChannelPoint.String(),
- Capacity: capacity,
- BitcoinKey1: edge.BitcoinKey1Bytes[:],
- BitcoinKey2: edge.BitcoinKey2Bytes[:],
- }
+ Version: int16(v),
+ Scid: channelIDToBytes(edge.ChannelID),
+ NodeID1: node1DBID,
+ NodeID2: node2DBID,
+ Outpoint: edge.ChannelPoint.String(),
+ Capacity: capacity,
+ }
+ edge.BitcoinKey1Bytes.WhenSome(func(vertex route.Vertex) {
+ createParams.BitcoinKey1 = vertex[:]
+ })
+ edge.BitcoinKey2Bytes.WhenSome(func(vertex route.Vertex) {
+ createParams.BitcoinKey2 = vertex[:]
+ })
if edge.AuthProof != nil {
proof := edge.AuthProof
Why this scored 20/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.