graph/db: use batch fetching for DeleteChannelEdges
What changed, and why it matters
This commit refactors how LND deletes channel edges in its graph database so that it fetches related channel data in batches rather than one by one. The change is a performance optimization and code cleanup. There is no direct evidence in the commit or supplied references that this fixes a security vulnerability.
Treat as routine maintenance/performance refactor. No security patch urgency indicated by the supplied materials. Standard review and regression testing are sufficient.
Security signals we found
Refactor only: no change to authorization, validation, or cryptographic logic visible in diff
No new input sanitization or bounds checks added
No vendor security disclosure or CVE references present in commit or supplied materials
Evidence from the diff
The patch modifies DeleteChannelEdges in graph/db/sql_store.go to collect all matching channel rows first, then call a new batchBuildChannelInfo helper that loads channel data in bulk via batchLoadChannelData and builds ChannelEdgeInfo objects in a single pass. Zombie marking logic is moved into a new handleZombieMarking helper. A new ChannelAndNodeIDs interface and methods are added in sqldb/sqlc/db_custom.go to abstract access to channel and node-pubkey fields. The observable behavior appears unchanged; the intent is to reduce query round-trips and avoid per-row getAndBuildEdgeInfo calls.
Changed components
graph/db/sql_store.go: DeleteChannelEdges, batchBuildChannelInfo, handleZombieMarkingsqldb/sqlc/db_custom.go: ChannelAndNodeIDs interface and GetChannelsBySCIDWithPoliciesRow methodsInspect captured patch +156 / −64
diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go
index d6d7c54..cd960b1 100644
--- a/graph/db/sql_store.go
+++ b/graph/db/sql_store.go
@@ -1615,11 +1615,12 @@ func (s *SQLStore) DeleteChannelEdges(strictZombiePruning, markZombie bool,
}
var (
- ctx = context.TODO()
- deleted []*models.ChannelEdgeInfo
+ ctx = context.TODO()
+ edges []*models.ChannelEdgeInfo
)
err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error {
- chanIDsToDelete := make([]int64, 0, len(chanIDs))
+ // First, collect all channel rows.
+ var channelRows []sqlc.GetChannelsBySCIDWithPoliciesRow
chanCallBack := func(ctx context.Context,
row sqlc.GetChannelsBySCIDWithPoliciesRow) error {
@@ -1628,65 +1629,7 @@ func (s *SQLStore) DeleteChannelEdges(strictZombiePruning, markZombie bool,
scid := byteOrder.Uint64(row.GraphChannel.Scid)
delete(chanLookup, scid)
- node1, node2, err := buildNodeVertices(
- row.GraphNode.PubKey, row.GraphNode_2.PubKey,
- )
- if err != nil {
- return err
- }
-
- info, err := getAndBuildEdgeInfo(
- ctx, db, s.cfg.ChainHash, row.GraphChannel,
- node1, node2,
- )
- if err != nil {
- return err
- }
-
- deleted = append(deleted, info)
- chanIDsToDelete = append(
- chanIDsToDelete, row.GraphChannel.ID,
- )
-
- if !markZombie {
- return nil
- }
-
- nodeKey1, nodeKey2 := info.NodeKey1Bytes,
- info.NodeKey2Bytes
- if strictZombiePruning {
- var e1UpdateTime, e2UpdateTime *time.Time
- if row.Policy1LastUpdate.Valid {
- e1Time := time.Unix(
- row.Policy1LastUpdate.Int64, 0,
- )
- e1UpdateTime = &e1Time
- }
- if row.Policy2LastUpdate.Valid {
- e2Time := time.Unix(
- row.Policy2LastUpdate.Int64, 0,
- )
- e2UpdateTime = &e2Time
- }
-
- nodeKey1, nodeKey2 = makeZombiePubkeys(
- info.NodeKey1Bytes, info.NodeKey2Bytes,
- e1UpdateTime, e2UpdateTime,
- )
- }
-
- err = db.UpsertZombieChannel(
- ctx, sqlc.UpsertZombieChannelParams{
- Version: int16(ProtocolV1),
- Scid: channelIDToBytes(scid),
- NodeKey1: nodeKey1[:],
- NodeKey2: nodeKey2[:],
- },
- )
- if err != nil {
- return fmt.Errorf("unable to mark channel as "+
- "zombie: %w", err)
- }
+ channelRows = append(channelRows, row)
return nil
}
@@ -1702,9 +1645,37 @@ func (s *SQLStore) DeleteChannelEdges(strictZombiePruning, markZombie bool,
return ErrEdgeNotFound
}
+ if len(channelRows) == 0 {
+ return nil
+ }
+
+ // Batch build all channel edges.
+ var chanIDsToDelete []int64
+ edges, chanIDsToDelete, err = batchBuildChannelInfo(
+ ctx, s.cfg, db, channelRows,
+ )
+ if err != nil {
+ return err
+ }
+
+ if markZombie {
+ for i, row := range channelRows {
+ scid := byteOrder.Uint64(row.GraphChannel.Scid)
+
+ err := handleZombieMarking(
+ ctx, db, row, edges[i],
+ strictZombiePruning, scid,
+ )
+ if err != nil {
+ return fmt.Errorf("unable to mark "+
+ "channel as zombie: %w", err)
+ }
+ }
+ }
+
return s.deleteChannels(ctx, db, chanIDsToDelete)
}, func() {
- deleted = nil
+ edges = nil
// Re-fill the lookup map.
for _, chanID := range chanIDs {
@@ -1721,7 +1692,7 @@ func (s *SQLStore) DeleteChannelEdges(strictZombiePruning, markZombie bool,
s.chanCache.remove(chanID)
}
- return deleted, nil
+ return edges, nil
}
// FetchChannelEdgesByID attempts to lookup the two directed edges for the
@@ -5406,3 +5377,90 @@ func batchBuildChannelEdges[T sqlc.ChannelAndNodes](ctx context.Context,
return edges, nil
}
+
+// batchBuildChannelInfo builds a slice of models.ChannelEdgeInfo
+// instances from the provided rows using batch loading for channel data.
+func batchBuildChannelInfo[T sqlc.ChannelAndNodeIDs](ctx context.Context,
+ cfg *SQLStoreConfig, db SQLQueries, rows []T) (
+ []*models.ChannelEdgeInfo, []int64, error) {
+
+ if len(rows) == 0 {
+ return nil, nil, nil
+ }
+
+ // Collect all the channel IDs needed for batch loading.
+ channelIDs := make([]int64, len(rows))
+ for i, row := range rows {
+ channelIDs[i] = row.Channel().ID
+ }
+
+ // Batch load the channel data.
+ channelBatchData, err := batchLoadChannelData(
+ ctx, cfg.QueryCfg, db, channelIDs, nil,
+ )
+ if err != nil {
+ return nil, nil, fmt.Errorf("unable to batch load channel "+
+ "data: %w", err)
+ }
+
+ // Build all channel edges using batch data.
+ edges := make([]*models.ChannelEdgeInfo, 0, len(rows))
+ for _, row := range rows {
+ node1, node2, err := buildNodeVertices(
+ row.Node1Pub(), row.Node2Pub(),
+ )
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Build channel info using batch data
+ info, err := buildEdgeInfoWithBatchData(
+ cfg.ChainHash, row.Channel(), node1, node2,
+ channelBatchData,
+ )
+ if err != nil {
+ return nil, nil, err
+ }
+
+ edges = append(edges, info)
+ }
+
+ return edges, channelIDs, nil
+}
+
+// handleZombieMarking is a helper function that handles the logic of
+// marking a channel as a zombie in the database. It takes into account whether
+// we are in strict zombie pruning mode, and adjusts the node public keys
+// accordingly based on the last update timestamps of the channel policies.
+func handleZombieMarking(ctx context.Context, db SQLQueries,
+ row sqlc.GetChannelsBySCIDWithPoliciesRow, info *models.ChannelEdgeInfo,
+ strictZombiePruning bool, scid uint64) error {
+
+ nodeKey1, nodeKey2 := info.NodeKey1Bytes, info.NodeKey2Bytes
+
+ if strictZombiePruning {
+ var e1UpdateTime, e2UpdateTime *time.Time
+ if row.Policy1LastUpdate.Valid {
+ e1Time := time.Unix(row.Policy1LastUpdate.Int64, 0)
+ e1UpdateTime = &e1Time
+ }
+ if row.Policy2LastUpdate.Valid {
+ e2Time := time.Unix(row.Policy2LastUpdate.Int64, 0)
+ e2UpdateTime = &e2Time
+ }
+
+ nodeKey1, nodeKey2 = makeZombiePubkeys(
+ info.NodeKey1Bytes, info.NodeKey2Bytes, e1UpdateTime,
+ e2UpdateTime,
+ )
+ }
+
+ return db.UpsertZombieChannel(
+ ctx, sqlc.UpsertZombieChannelParams{
+ Version: int16(ProtocolV1),
+ Scid: channelIDToBytes(scid),
+ NodeKey1: nodeKey1[:],
+ NodeKey2: nodeKey2[:],
+ },
+ )
+}
diff --git a/sqldb/sqlc/db_custom.go b/sqldb/sqlc/db_custom.go
index 64440a2..8230028 100644
--- a/sqldb/sqlc/db_custom.go
+++ b/sqldb/sqlc/db_custom.go
@@ -71,3 +71,37 @@ func (r GetChannelsByPolicyLastUpdateRangeRow) Node1() GraphNode {
func (r GetChannelsByPolicyLastUpdateRangeRow) Node2() GraphNode {
return r.GraphNode_2
}
+
+// ChannelAndNodeIDs is an interface that provides access to a channel and its
+// two node public keys.
+type ChannelAndNodeIDs interface {
+ // Channel returns the GraphChannel associated with this interface.
+ Channel() GraphChannel
+
+ // Node1Pub returns the public key of the first node as a byte slice.
+ Node1Pub() []byte
+
+ // Node2Pub returns the public key of the second node as a byte slice.
+ Node2Pub() []byte
+}
+
+// Channel returns the GraphChannel associated with this interface.
+//
+// NOTE: This method is part of the ChannelAndNodeIDs interface.
+func (r GetChannelsBySCIDWithPoliciesRow) Channel() GraphChannel {
+ return r.GraphChannel
+}
+
+// Node1Pub returns the public key of the first node as a byte slice.
+//
+// NOTE: This method is part of the ChannelAndNodeIDs interface.
+func (r GetChannelsBySCIDWithPoliciesRow) Node1Pub() []byte {
+ return r.GraphNode.PubKey
+}
+
+// Node2Pub returns the public key of the second node as a byte slice.
+//
+// NOTE: This method is part of the ChannelAndNodeIDs interface.
+func (r GetChannelsBySCIDWithPoliciesRow) Node2Pub() []byte {
+ return r.GraphNode_2.PubKey
+}
Why this scored 10/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.