graph/db: treat empty channel signatures as missing

This commit improves handling of missing channel signatures in the
database:

- Return nil from auth proof accessors instead of empty slices so that
  missing signatures are stored as NULL in SQL.
- Update public channel checks to require signature length > 0, which
  properly handles existing empty bytea values in the database.
- Add regression test covering empty v1 and v2 channel signatures to
  prevent future issues.
This commit is contained in:
Elle Mouton committed 2026-02-13 07:21:23 +02:00
1 parent a39f94eb4a
commit 6328c4d897
5 files changed
+85 -27

No files matched your search

+57
View File
@@ -154,6 +154,10 @@ var versionedTests = []versionedTest{
name: "node is public",
test: testNodeIsPublic,
},
{
name: "node is public empty channel signature",
test: testIsPublicNodeEmptyChannelSignature,
},
}
// TestVersionedDBs runs various tests against both v1 and v2 versioned
@@ -4265,6 +4269,59 @@ func testNodeIsPublic(t *testing.T, v lnwire.GossipVersion) {
)
}
// testIsPublicNodeEmptyChannelSignature ensures empty channel signatures don't
// mark nodes as public.
func testIsPublicNodeEmptyChannelSignature(t *testing.T,
v lnwire.GossipVersion) {
t.Parallel()
ctx := t.Context()
testGraph := MakeTestGraph(t)
graph := NewVersionedGraph(testGraph, v)
// Set a source node as it's required for IsPublicNode.
sourceNode := createTestVertex(t, v)
err := graph.SetSourceNode(ctx, sourceNode)
require.NoError(t, err)
node1 := createTestVertex(t, v)
node1.LastUpdate = nextUpdateTime()
err = graph.AddNode(ctx, node1)
require.NoError(t, err)
// Create an edge between source node and node1, with
// empty signatures. This tests that empty signatures
// don't mark nodes as public.
edgeInfo, _ := createEdge(
v, 10, 0, 0, 0, sourceNode, node1,
true,
)
switch v {
case lnwire.GossipVersion1:
edgeInfo.AuthProof =
models.NewV1ChannelAuthProof(
[]byte{}, []byte{},
[]byte{}, []byte{},
)
case lnwire.GossipVersion2:
edgeInfo.AuthProof =
models.NewV2ChannelAuthProof([]byte{})
}
err = graph.AddChannelEdge(ctx, edgeInfo)
require.NoError(t, err)
// node1 should NOT be considered public because the
// channel announcement has empty signatures.
isPublic, err := graph.IsPublicNode(node1.PubKeyBytes)
require.NoError(t, err)
require.False(t, isPublic)
}
// BenchmarkIsPublicNode measures the performance of IsPublicNode when checking
// a large number of nodes.
func BenchmarkIsPublicNode(b *testing.B) {
+5 -2
View File
@@ -3377,8 +3377,11 @@ func (c *KVStore) isPublic(tx kvdb.RTx, nodePub route.Vertex,
}
// Since the edge _does_ extend to the source node, we'll also
// need to ensure that this is a public edge.
if info.AuthProof != nil {
// need to ensure that this is a public edge with valid
// signatures (not empty).
if info.AuthProof != nil && !info.AuthProof.IsEmpty() &&
len(info.AuthProof.BitcoinSig1()) > 0 {
nodeIsPublic = true
return errDone
}
+12 -14
View File
@@ -89,31 +89,29 @@ func NewV2ChannelAuthProof(signature []byte) *ChannelAuthProof {
}
}
// NodeSig1 returns the first node signature bytes, or an empty slice if not
// present.
// NodeSig1 returns the first node signature bytes, or nil if not present.
func (c *ChannelAuthProof) NodeSig1() []byte {
return c.NodeSig1Bytes.UnwrapOr([]byte{})
return c.NodeSig1Bytes.UnwrapOr(nil)
}
// NodeSig2 returns the second node signature bytes, or an empty slice if not
// present.
// NodeSig2 returns the second node signature bytes, or nil if not present.
func (c *ChannelAuthProof) NodeSig2() []byte {
return c.NodeSig2Bytes.UnwrapOr([]byte{})
return c.NodeSig2Bytes.UnwrapOr(nil)
}
// BitcoinSig1 returns the first bitcoin signature bytes, or an empty slice if
// not present.
// BitcoinSig1 returns the first bitcoin signature bytes, or nil if not
// present.
func (c *ChannelAuthProof) BitcoinSig1() []byte {
return c.BitcoinSig1Bytes.UnwrapOr([]byte{})
return c.BitcoinSig1Bytes.UnwrapOr(nil)
}
// BitcoinSig2 returns the second bitcoin signature bytes, or an empty slice if
// not present.
// BitcoinSig2 returns the second bitcoin signature bytes, or nil if not
// present.
func (c *ChannelAuthProof) BitcoinSig2() []byte {
return c.BitcoinSig2Bytes.UnwrapOr([]byte{})
return c.BitcoinSig2Bytes.UnwrapOr(nil)
}
// Sig returns the v2 signature bytes, or an empty slice if not present.
// Sig returns the v2 signature bytes, or nil if not present.
func (c *ChannelAuthProof) Sig() []byte {
return c.Signature.UnwrapOr([]byte{})
return c.Signature.UnwrapOr(nil)
}
+5 -5
View File
@@ -2085,7 +2085,7 @@ WHERE last_update >= $1
SELECT 1
FROM graph_channels c
WHERE c.version = 1
AND c.bitcoin_1_signature IS NOT NULL
AND COALESCE(length(c.bitcoin_1_signature), 0) > 0
AND (c.node_id_1 = graph_nodes.id OR c.node_id_2 = graph_nodes.id)
)
)
@@ -2211,7 +2211,7 @@ func (q *Queries) GetPruneTip(ctx context.Context) (GraphPruneLog, error) {
const getPublicV1ChannelsBySCID = `-- name: GetPublicV1ChannelsBySCID :many
SELECT id, version, scid, node_id_1, node_id_2, outpoint, capacity, bitcoin_key_1, bitcoin_key_2, node_1_signature, node_2_signature, bitcoin_1_signature, bitcoin_2_signature, signature, funding_pk_script, merkle_root_hash
FROM graph_channels
WHERE node_1_signature IS NOT NULL
WHERE COALESCE(length(node_1_signature), 0) > 0
AND scid >= $1
AND scid < $2
`
@@ -2732,14 +2732,14 @@ SELECT EXISTS (
-- one of the signatures since we only ever set them
-- together.
WHERE c.version = 1
AND c.bitcoin_1_signature IS NOT NULL
AND COALESCE(length(c.bitcoin_1_signature), 0) > 0
AND n.pub_key = $1
UNION ALL
SELECT 1
FROM graph_channels c
JOIN graph_nodes n ON n.id = c.node_id_2
WHERE c.version = 1
AND c.bitcoin_1_signature IS NOT NULL
AND COALESCE(length(c.bitcoin_1_signature), 0) > 0
AND n.pub_key = $1
)
`
@@ -2760,7 +2760,7 @@ SELECT EXISTS (
-- here that determine if a node is public is specific
-- to the V2 gossip protocol.
WHERE c.version = 2
AND c.signature IS NOT NULL
AND COALESCE(length(c.signature), 0) > 0
AND n.pub_key = $1
UNION ALL
+6 -6
View File
@@ -101,14 +101,14 @@ SELECT EXISTS (
-- one of the signatures since we only ever set them
-- together.
WHERE c.version = 1
AND c.bitcoin_1_signature IS NOT NULL
AND COALESCE(length(c.bitcoin_1_signature), 0) > 0
AND n.pub_key = $1
UNION ALL
SELECT 1
FROM graph_channels c
JOIN graph_nodes n ON n.id = c.node_id_2
WHERE c.version = 1
AND c.bitcoin_1_signature IS NOT NULL
AND COALESCE(length(c.bitcoin_1_signature), 0) > 0
AND n.pub_key = $1
);
@@ -121,7 +121,7 @@ SELECT EXISTS (
-- here that determine if a node is public is specific
-- to the V2 gossip protocol.
WHERE c.version = 2
AND c.signature IS NOT NULL
AND COALESCE(length(c.signature), 0) > 0
AND n.pub_key = $1
UNION ALL
@@ -251,7 +251,7 @@ WHERE last_update >= @start_time
SELECT 1
FROM graph_channels c
WHERE c.version = 1
AND c.bitcoin_1_signature IS NOT NULL
AND COALESCE(length(c.bitcoin_1_signature), 0) > 0
AND (c.node_id_1 = graph_nodes.id OR c.node_id_2 = graph_nodes.id)
)
)
@@ -731,7 +731,7 @@ WHERE c.version = $1
-- name: GetPublicV1ChannelsBySCID :many
SELECT *
FROM graph_channels
WHERE node_1_signature IS NOT NULL
WHERE COALESCE(length(node_1_signature), 0) > 0
AND scid >= @start_scid
AND scid < @end_scid;
@@ -1217,4 +1217,4 @@ ON CONFLICT (channel_id, node_id, version)
message_flags = EXCLUDED.message_flags,
channel_flags = EXCLUDED.channel_flags,
signature = EXCLUDED.signature
RETURNING id;
RETURNING id;