From f1da3812dedfe5621d071f8a4dd3ae616cb6852e Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 11 Jun 2025 17:29:47 +0200 Subject: [PATCH] graph/db+sqldb: impl IsPublicNode Which lets us run `TestNodeIsPublic` against our SQL DB backends. Note that we need to tweak the tests a little bit so that `AddLightningNode` for the same node is always called with a newer LastUpdate time else it will fail the SQL constraint that only allows the upsert if the update is newer than the persisted one. --- graph/db/graph_test.go | 30 +++++++++++++++++++++--------- graph/db/sql_store.go | 24 ++++++++++++++++++++++++ sqldb/sqlc/graph.sql.go | 26 ++++++++++++++++++++++++++ sqldb/sqlc/querier.go | 1 + sqldb/sqlc/queries/graph.sql | 18 ++++++++++++++++++ 5 files changed, 90 insertions(+), 9 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index b8526bb56..a340acb1d 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -73,13 +73,11 @@ var ( ) func createLightningNode(priv *btcec.PrivateKey) *models.LightningNode { - updateTime := prand.Int63() - pub := priv.PubKey().SerializeCompressed() n := &models.LightningNode{ HaveNodeAnnouncement: true, AuthSigBytes: testSig.Serialize(), - LastUpdate: time.Unix(updateTime, 0), + LastUpdate: time.Unix(nextUpdateTime(), 0), Color: color.RGBA{1, 2, 3, 0}, Alias: "kek" + hex.EncodeToString(pub), Features: testFeatures, @@ -3439,6 +3437,20 @@ func TestNodePruningUpdateIndexDeletion(t *testing.T) { } } +var ( + updateTime = prand.Int63() + updateTimeMu sync.Mutex +) + +func nextUpdateTime() int64 { + updateTimeMu.Lock() + defer updateTimeMu.Unlock() + + updateTime++ + + return updateTime +} + // TestNodeIsPublic ensures that we properly detect nodes that are seen as // public within the network graph. func TestNodeIsPublic(t *testing.T) { @@ -3453,19 +3465,19 @@ func TestNodeIsPublic(t *testing.T) { // We'll need to create a separate database and channel graph for each // participant to replicate real-world scenarios (private edges being in // some graphs but not others, etc.). - aliceGraph := MakeTestGraph(t) + aliceGraph := MakeTestGraphNew(t) aliceNode := createTestVertex(t) if err := aliceGraph.SetSourceNode(ctx, aliceNode); err != nil { t.Fatalf("unable to set source node: %v", err) } - bobGraph := MakeTestGraph(t) + bobGraph := MakeTestGraphNew(t) bobNode := createTestVertex(t) if err := bobGraph.SetSourceNode(ctx, bobNode); err != nil { t.Fatalf("unable to set source node: %v", err) } - carolGraph := MakeTestGraph(t) + carolGraph := MakeTestGraphNew(t) carolNode := createTestVertex(t) if err := carolGraph.SetSourceNode(ctx, carolNode); err != nil { t.Fatalf("unable to set source node: %v", err) @@ -3481,13 +3493,13 @@ func TestNodeIsPublic(t *testing.T) { graphs := []*ChannelGraph{aliceGraph, bobGraph, carolGraph} for _, graph := range graphs { for _, node := range nodes { + node.LastUpdate = time.Unix(nextUpdateTime(), 0) err := graph.AddLightningNode(ctx, node) require.NoError(t, err) } for _, edge := range edges { - if err := graph.AddChannelEdge(ctx, edge); err != nil { - t.Fatalf("unable to add edge: %v", err) - } + err := graph.AddChannelEdge(ctx, edge) + require.NoError(t, err) } } diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 4f7233d9b..cfaaf3b3d 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -62,6 +62,7 @@ type SQLQueries interface { GetNodesByLastUpdateRange(ctx context.Context, arg sqlc.GetNodesByLastUpdateRangeParams) ([]sqlc.Node, error) ListNodesPaginated(ctx context.Context, arg sqlc.ListNodesPaginatedParams) ([]sqlc.Node, error) ListNodeIDsAndPubKeys(ctx context.Context, arg sqlc.ListNodeIDsAndPubKeysParams) ([]sqlc.ListNodeIDsAndPubKeysRow, error) + IsPublicV1Node(ctx context.Context, pubKey []byte) (bool, error) DeleteNodeByPubKey(ctx context.Context, arg sqlc.DeleteNodeByPubKeyParams) (sql.Result, error) GetExtraNodeTypes(ctx context.Context, nodeID int64) ([]sqlc.NodeExtraType, error) @@ -1990,6 +1991,29 @@ func (s *SQLStore) ChannelID(chanPoint *wire.OutPoint) (uint64, error) { return channelID, nil } +// IsPublicNode is a helper method that determines whether the node with the +// given public key is seen as a public node in the graph from the graph's +// source node's point of view. +// +// NOTE: part of the V1Store interface. +func (s *SQLStore) IsPublicNode(pubKey [33]byte) (bool, error) { + ctx := context.TODO() + + var isPublic bool + err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error { + var err error + isPublic, err = db.IsPublicV1Node(ctx, pubKey[:]) + + return err + }, sqldb.NoOpReset) + if err != nil { + return false, fmt.Errorf("unable to check if node is "+ + "public: %w", err) + } + + return isPublic, nil +} + // FetchChanInfos returns the set of channel edges that correspond to the passed // channel ID's. If an edge is the query is unknown to the database, it will // skipped and the result will contain only those edges that exist at the time diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go index 77771052e..58b02425d 100644 --- a/sqldb/sqlc/graph.sql.go +++ b/sqldb/sqlc/graph.sql.go @@ -1360,6 +1360,32 @@ func (q *Queries) InsertNodeFeature(ctx context.Context, arg InsertNodeFeaturePa return err } +const isPublicV1Node = `-- name: IsPublicV1Node :one +SELECT EXISTS ( + SELECT 1 + FROM channels c + JOIN nodes n ON n.id = c.node_id_1 OR n.id = c.node_id_2 + -- NOTE: we hard-code the version here since the clauses + -- here that determine if a node is public is specific + -- to the V1 gossip protocol. In V1, a node is public + -- if it has a public channel and a public channel is one + -- where we have the set of signatures of the channel + -- announcement. It is enough to just check that we have + -- one of the signatures since we only ever set them + -- together. + WHERE c.version = 1 + AND c.bitcoin_1_signature IS NOT NULL + AND n.pub_key = $1 +) +` + +func (q *Queries) IsPublicV1Node(ctx context.Context, pubKey []byte) (bool, error) { + row := q.db.QueryRowContext(ctx, isPublicV1Node, pubKey) + var exists bool + err := row.Scan(&exists) + return exists, err +} + const isZombieChannel = `-- name: IsZombieChannel :one SELECT EXISTS ( SELECT 1 diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index da417d5cd..8c0064bf9 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -74,6 +74,7 @@ type Querier interface { InsertMigratedInvoice(ctx context.Context, arg InsertMigratedInvoiceParams) (int64, error) InsertNodeAddress(ctx context.Context, arg InsertNodeAddressParams) error InsertNodeFeature(ctx context.Context, arg InsertNodeFeatureParams) error + IsPublicV1Node(ctx context.Context, pubKey []byte) (bool, error) IsZombieChannel(ctx context.Context, arg IsZombieChannelParams) (bool, error) ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNodeIDParams) ([]ListChannelsByNodeIDRow, error) ListChannelsWithPoliciesPaginated(ctx context.Context, arg ListChannelsWithPoliciesPaginatedParams) ([]ListChannelsWithPoliciesPaginatedRow, error) diff --git a/sqldb/sqlc/queries/graph.sql b/sqldb/sqlc/queries/graph.sql index af11fb515..332d5f958 100644 --- a/sqldb/sqlc/queries/graph.sql +++ b/sqldb/sqlc/queries/graph.sql @@ -47,6 +47,24 @@ WHERE version = $1 AND id > $2 ORDER BY id LIMIT $3; +-- name: IsPublicV1Node :one +SELECT EXISTS ( + SELECT 1 + FROM channels c + JOIN nodes n ON n.id = c.node_id_1 OR n.id = c.node_id_2 + -- NOTE: we hard-code the version here since the clauses + -- here that determine if a node is public is specific + -- to the V1 gossip protocol. In V1, a node is public + -- if it has a public channel and a public channel is one + -- where we have the set of signatures of the channel + -- announcement. It is enough to just check that we have + -- one of the signatures since we only ever set them + -- together. + WHERE c.version = 1 + AND c.bitcoin_1_signature IS NOT NULL + AND n.pub_key = $1 +); + -- name: DeleteNodeByPubKey :execresult DELETE FROM nodes WHERE pub_key = $1