sqldb: use version-specific staleness checks in UpsertChannelPolicy
What changed, and why it matters
This change fixes how LND stores routing policy updates received over the Lightning network gossip protocol. Different gossip versions use different freshness indicators: v1 uses a timestamp, while v2 uses a block height. Previously the code always compared timestamps, which could cause newer v2 policies to be ignored if their timestamp was older, or stale v2 policies to overwrite newer ones. The patch makes the database update conditional on the correct freshness field for each gossip version.
Treat as a correctness/security hardening fix and include in the next maintenance release. Review related graph policy code paths to ensure no other queries still rely solely on last_update for v2 policies. No immediate emergency response is indicated absent evidence of active exploitation.
Security signals we found
Incorrect freshness comparison could allow stale routing policies to overwrite newer ones
Version-specific conditional logic added to SQL upsert
v2 gossip policies now use block_height instead of last_update for staleness
NULL handling added for both version branches to initialize empty rows
Evidence from the diff
The UpsertChannelPolicy query previously used a single staleness guard: WHERE EXCLUDED.last_update > graph_channel_policies.last_update. That is correct for gossip v1 policies, which carry a last_update timestamp, but incorrect for v2 policies, which are ordered by block_height. The patch splits the WHERE clause: for version=1 it requires a greater last_update (or NULL existing value); for version=2 it requires a greater-or-equal block_height (or NULL existing value). The >= for v2 is intentional because multiple policies may be announced in the same block. This prevents stale v2 policies from replacing fresher ones and ensures v2 updates from the same block are accepted.
Changed components
sqldb/sqlc/graph.sql.gosqldb/sqlc/queries/graph.sqlUpsertChannelPolicy SQL queryLightning channel graph policy storageInspect captured patch +24 / −2
diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go
index 1d569b1..aa2a046 100644
--- a/sqldb/sqlc/graph.sql.go
+++ b/sqldb/sqlc/graph.sql.go
@@ -3804,7 +3804,18 @@ ON CONFLICT (channel_id, node_id, version)
signature = EXCLUDED.signature,
block_height = EXCLUDED.block_height,
disable_flags = EXCLUDED.disable_flags
-WHERE EXCLUDED.last_update > graph_channel_policies.last_update
+WHERE (
+ EXCLUDED.version = 1 AND (
+ graph_channel_policies.last_update IS NULL
+ OR EXCLUDED.last_update > graph_channel_policies.last_update
+ )
+)
+OR (
+ EXCLUDED.version = 2 AND (
+ graph_channel_policies.block_height IS NULL
+ OR EXCLUDED.block_height >= graph_channel_policies.block_height
+ )
+)
RETURNING id
`
diff --git a/sqldb/sqlc/queries/graph.sql b/sqldb/sqlc/queries/graph.sql
index e5c18c3..a4a42a1 100644
--- a/sqldb/sqlc/queries/graph.sql
+++ b/sqldb/sqlc/queries/graph.sql
@@ -954,7 +954,18 @@ ON CONFLICT (channel_id, node_id, version)
signature = EXCLUDED.signature,
block_height = EXCLUDED.block_height,
disable_flags = EXCLUDED.disable_flags
-WHERE EXCLUDED.last_update > graph_channel_policies.last_update
+WHERE (
+ EXCLUDED.version = 1 AND (
+ graph_channel_policies.last_update IS NULL
+ OR EXCLUDED.last_update > graph_channel_policies.last_update
+ )
+)
+OR (
+ EXCLUDED.version = 2 AND (
+ graph_channel_policies.block_height IS NULL
+ OR EXCLUDED.block_height >= graph_channel_policies.block_height
+ )
+)
RETURNING id;
-- name: GetChannelPolicyByChannelAndNode :one
Why this scored 46/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.