Skip to content

Commit d93d104

Browse files
committed
graph/db+sqldb: implement AddChannelEdge on SQLStore
In this commit, the `AddChannelEdge` method of the SQLStore is implemented. Like the KVStore implementation, it makes use of the available channel `batch.Scheduler` and also updates the reject and channel caches. This then lets us convert the following 2 unit tests to run against the SQL backends: - TestPartialNode - TestAddChannelEdgeShellNodes
1 parent c5f159f commit d93d104

File tree

5 files changed

+383
-2
lines changed

5 files changed

+383
-2
lines changed

graph/db/graph_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ func TestNodeInsertionAndDeletion(t *testing.T) {
261261
func TestPartialNode(t *testing.T) {
262262
t.Parallel()
263263

264-
graph := MakeTestGraph(t)
264+
graph := MakeTestGraphNew(t)
265265

266266
// To insert a partial node, we need to add a channel edge that has
267267
// node keys for nodes we are not yet aware
@@ -3332,7 +3332,7 @@ func TestPruneGraphNodes(t *testing.T) {
33323332
func TestAddChannelEdgeShellNodes(t *testing.T) {
33333333
t.Parallel()
33343334

3335-
graph := MakeTestGraph(t)
3335+
graph := MakeTestGraphNew(t)
33363336

33373337
// To start, we'll create two nodes, and only add one of them to the
33383338
// channel graph.

graph/db/sql_store.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ type SQLQueries interface {
6969
*/
7070
AddSourceNode(ctx context.Context, nodeID int64) error
7171
GetSourceNodesByVersion(ctx context.Context, version int16) ([]sqlc.GetSourceNodesByVersionRow, error)
72+
73+
/*
74+
Channel queries.
75+
*/
76+
CreateChannel(ctx context.Context, arg sqlc.CreateChannelParams) (int64, error)
77+
GetChannelBySCID(ctx context.Context, arg sqlc.GetChannelBySCIDParams) (sqlc.Channel, error)
78+
79+
CreateChannelExtraType(ctx context.Context, arg sqlc.CreateChannelExtraTypeParams) error
80+
InsertChannelFeature(ctx context.Context, arg sqlc.InsertChannelFeatureParams) error
7281
}
7382

7483
// BatchedSQLQueries is a version of SQLQueries that's capable of batched
@@ -455,6 +464,54 @@ func (s *SQLStore) NodeUpdatesInHorizon(startTime,
455464
return nodes, nil
456465
}
457466

467+
// AddChannelEdge adds a new (undirected, blank) edge to the graph database. An
468+
// undirected edge from the two target nodes are created. The information stored
469+
// denotes the static attributes of the channel, such as the channelID, the keys
470+
// involved in creation of the channel, and the set of features that the channel
471+
// supports. The chanPoint and chanID are used to uniquely identify the edge
472+
// globally within the database.
473+
//
474+
// NOTE: part of the V1Store interface.
475+
func (s *SQLStore) AddChannelEdge(edge *models.ChannelEdgeInfo,
476+
opts ...batch.SchedulerOption) error {
477+
478+
ctx := context.TODO()
479+
480+
var alreadyExists bool
481+
r := &batch.Request[SQLQueries]{
482+
Opts: batch.NewSchedulerOptions(opts...),
483+
Reset: func() {
484+
alreadyExists = false
485+
},
486+
Do: func(tx SQLQueries) error {
487+
err := insertChannel(ctx, tx, edge)
488+
489+
// Silence ErrEdgeAlreadyExist so that the batch can
490+
// succeed, but propagate the error via local state.
491+
if errors.Is(err, ErrEdgeAlreadyExist) {
492+
alreadyExists = true
493+
return nil
494+
}
495+
496+
return err
497+
},
498+
OnCommit: func(err error) error {
499+
switch {
500+
case err != nil:
501+
return err
502+
case alreadyExists:
503+
return ErrEdgeAlreadyExist
504+
default:
505+
s.rejectCache.remove(edge.ChannelID)
506+
s.chanCache.remove(edge.ChannelID)
507+
return nil
508+
}
509+
},
510+
}
511+
512+
return s.chanScheduler.Execute(ctx, r)
513+
}
514+
458515
// getNodeByPubKey attempts to look up a target node by its public key.
459516
func getNodeByPubKey(ctx context.Context, db SQLQueries,
460517
pubKey route.Vertex) (int64, *models.LightningNode, error) {
@@ -1022,3 +1079,151 @@ func marshalExtraOpaqueData(data []byte) (map[uint64][]byte, error) {
10221079

10231080
return records, nil
10241081
}
1082+
1083+
// insertChannel inserts a new channel record into the database.
1084+
func insertChannel(ctx context.Context, db SQLQueries,
1085+
edge *models.ChannelEdgeInfo) error {
1086+
1087+
var chanIDB [8]byte
1088+
byteOrder.PutUint64(chanIDB[:], edge.ChannelID)
1089+
1090+
// Make sure that the channel doesn't already exist. We do this
1091+
// explicitly instead of relying on catching a unique constraint error
1092+
// because relying on SQL to throw that error would abort the entire
1093+
// batch of transactions.
1094+
_, err := db.GetChannelBySCID(
1095+
ctx, sqlc.GetChannelBySCIDParams{
1096+
Scid: chanIDB[:],
1097+
Version: int16(ProtocolV1),
1098+
},
1099+
)
1100+
if err == nil {
1101+
return ErrEdgeAlreadyExist
1102+
} else if !errors.Is(err, sql.ErrNoRows) {
1103+
return fmt.Errorf("unable to fetch channel: %w", err)
1104+
}
1105+
1106+
// Make sure that at least a "shell" entry for each node is present in
1107+
// the nodes table.
1108+
node1DBID, err := maybeCreateShellNode(ctx, db, edge.NodeKey1Bytes)
1109+
if err != nil {
1110+
return fmt.Errorf("unable to create shell node: %w", err)
1111+
}
1112+
1113+
node2DBID, err := maybeCreateShellNode(ctx, db, edge.NodeKey2Bytes)
1114+
if err != nil {
1115+
return fmt.Errorf("unable to create shell node: %w", err)
1116+
}
1117+
1118+
var capacity sql.NullInt64
1119+
if edge.Capacity != 0 {
1120+
capacity = sqldb.SQLInt64(int64(edge.Capacity))
1121+
}
1122+
1123+
createParams := sqlc.CreateChannelParams{
1124+
Version: int16(ProtocolV1),
1125+
Scid: chanIDB[:],
1126+
NodeID1: node1DBID,
1127+
NodeID2: node2DBID,
1128+
Outpoint: edge.ChannelPoint.String(),
1129+
Capacity: capacity,
1130+
BitcoinKey1: edge.BitcoinKey1Bytes[:],
1131+
BitcoinKey2: edge.BitcoinKey2Bytes[:],
1132+
}
1133+
1134+
if edge.AuthProof != nil {
1135+
proof := edge.AuthProof
1136+
1137+
createParams.Node1Signature = proof.NodeSig1Bytes
1138+
createParams.Node2Signature = proof.NodeSig2Bytes
1139+
createParams.Bitcoin1Signature = proof.BitcoinSig1Bytes
1140+
createParams.Bitcoin2Signature = proof.BitcoinSig2Bytes
1141+
}
1142+
1143+
// Insert the new channel record.
1144+
dbChanID, err := db.CreateChannel(ctx, createParams)
1145+
if err != nil {
1146+
return err
1147+
}
1148+
1149+
// Insert any channel features.
1150+
if len(edge.Features) != 0 {
1151+
chanFeatures := lnwire.NewRawFeatureVector()
1152+
err := chanFeatures.Decode(bytes.NewReader(edge.Features))
1153+
if err != nil {
1154+
return err
1155+
}
1156+
1157+
fv := lnwire.NewFeatureVector(chanFeatures, lnwire.Features)
1158+
for feature := range fv.Features() {
1159+
err = db.InsertChannelFeature(
1160+
ctx, sqlc.InsertChannelFeatureParams{
1161+
ChannelID: dbChanID,
1162+
FeatureBit: int32(feature),
1163+
},
1164+
)
1165+
if err != nil {
1166+
return fmt.Errorf("unable to insert "+
1167+
"channel(%d) feature(%v): %w", dbChanID,
1168+
feature, err)
1169+
}
1170+
}
1171+
}
1172+
1173+
// Finally, insert any extra TLV fields in the channel announcement.
1174+
extra, err := marshalExtraOpaqueData(edge.ExtraOpaqueData)
1175+
if err != nil {
1176+
return fmt.Errorf("unable to marshal extra opaque data: %w",
1177+
err)
1178+
}
1179+
1180+
for tlvType, value := range extra {
1181+
err := db.CreateChannelExtraType(
1182+
ctx, sqlc.CreateChannelExtraTypeParams{
1183+
ChannelID: dbChanID,
1184+
Type: int64(tlvType),
1185+
Value: value,
1186+
},
1187+
)
1188+
if err != nil {
1189+
return fmt.Errorf("unable to upsert channel(%d) extra "+
1190+
"signed field(%v): %w", edge.ChannelID,
1191+
tlvType, err)
1192+
}
1193+
}
1194+
1195+
return nil
1196+
}
1197+
1198+
// maybeCreateShellNode checks if a shell node entry exists for the
1199+
// given public key. If it does not exist, then a new shell node entry is
1200+
// created. The ID of the node is returned. A shell node only has a protocol
1201+
// version and public key persisted.
1202+
func maybeCreateShellNode(ctx context.Context, db SQLQueries,
1203+
pubKey route.Vertex) (int64, error) {
1204+
1205+
dbNode, err := db.GetNodeByPubKey(
1206+
ctx, sqlc.GetNodeByPubKeyParams{
1207+
PubKey: pubKey[:],
1208+
Version: int16(ProtocolV1),
1209+
},
1210+
)
1211+
// The node exists. Return the ID.
1212+
if err == nil {
1213+
return dbNode.ID, nil
1214+
} else if !errors.Is(err, sql.ErrNoRows) {
1215+
return 0, err
1216+
}
1217+
1218+
// Otherwise, the node does not exist, so we create a shell entry for
1219+
// it.
1220+
id, err := db.UpsertNode(ctx, sqlc.UpsertNodeParams{
1221+
Version: int16(ProtocolV1),
1222+
PubKey: pubKey[:],
1223+
})
1224+
if err != nil {
1225+
return 0, fmt.Errorf("unable to create shell node: %w", err)
1226+
}
1227+
1228+
return id, nil
1229+
}

sqldb/sqlc/graph.sql.go

Lines changed: 129 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)