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.
This commit is contained in:
Elle Mouton committed 2025-06-24 21:04:36 +02:00
1 parent acae165f0a
commit f1da3812de
5 files changed
+90 -9

No files matched your search

+21 -9
View File
@@ -73,13 +73,11 @@ var (
) )
func createLightningNode(priv *btcec.PrivateKey) *models.LightningNode { func createLightningNode(priv *btcec.PrivateKey) *models.LightningNode {
updateTime := prand.Int63()
pub := priv.PubKey().SerializeCompressed() pub := priv.PubKey().SerializeCompressed()
n := &models.LightningNode{ n := &models.LightningNode{
HaveNodeAnnouncement: true, HaveNodeAnnouncement: true,
AuthSigBytes: testSig.Serialize(), AuthSigBytes: testSig.Serialize(),
LastUpdate: time.Unix(updateTime, 0), LastUpdate: time.Unix(nextUpdateTime(), 0),
Color: color.RGBA{1, 2, 3, 0}, Color: color.RGBA{1, 2, 3, 0},
Alias: "kek" + hex.EncodeToString(pub), Alias: "kek" + hex.EncodeToString(pub),
Features: testFeatures, 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 // TestNodeIsPublic ensures that we properly detect nodes that are seen as
// public within the network graph. // public within the network graph.
func TestNodeIsPublic(t *testing.T) { 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 // We'll need to create a separate database and channel graph for each
// participant to replicate real-world scenarios (private edges being in // participant to replicate real-world scenarios (private edges being in
// some graphs but not others, etc.). // some graphs but not others, etc.).
aliceGraph := MakeTestGraph(t) aliceGraph := MakeTestGraphNew(t)
aliceNode := createTestVertex(t) aliceNode := createTestVertex(t)
if err := aliceGraph.SetSourceNode(ctx, aliceNode); err != nil { if err := aliceGraph.SetSourceNode(ctx, aliceNode); err != nil {
t.Fatalf("unable to set source node: %v", err) t.Fatalf("unable to set source node: %v", err)
} }
bobGraph := MakeTestGraph(t) bobGraph := MakeTestGraphNew(t)
bobNode := createTestVertex(t) bobNode := createTestVertex(t)
if err := bobGraph.SetSourceNode(ctx, bobNode); err != nil { if err := bobGraph.SetSourceNode(ctx, bobNode); err != nil {
t.Fatalf("unable to set source node: %v", err) t.Fatalf("unable to set source node: %v", err)
} }
carolGraph := MakeTestGraph(t) carolGraph := MakeTestGraphNew(t)
carolNode := createTestVertex(t) carolNode := createTestVertex(t)
if err := carolGraph.SetSourceNode(ctx, carolNode); err != nil { if err := carolGraph.SetSourceNode(ctx, carolNode); err != nil {
t.Fatalf("unable to set source node: %v", err) t.Fatalf("unable to set source node: %v", err)
@@ -3481,13 +3493,13 @@ func TestNodeIsPublic(t *testing.T) {
graphs := []*ChannelGraph{aliceGraph, bobGraph, carolGraph} graphs := []*ChannelGraph{aliceGraph, bobGraph, carolGraph}
for _, graph := range graphs { for _, graph := range graphs {
for _, node := range nodes { for _, node := range nodes {
node.LastUpdate = time.Unix(nextUpdateTime(), 0)
err := graph.AddLightningNode(ctx, node) err := graph.AddLightningNode(ctx, node)
require.NoError(t, err) require.NoError(t, err)
} }
for _, edge := range edges { for _, edge := range edges {
if err := graph.AddChannelEdge(ctx, edge); err != nil { err := graph.AddChannelEdge(ctx, edge)
t.Fatalf("unable to add edge: %v", err) require.NoError(t, err)
}
} }
} }
+24
View File
@@ -62,6 +62,7 @@ type SQLQueries interface {
GetNodesByLastUpdateRange(ctx context.Context, arg sqlc.GetNodesByLastUpdateRangeParams) ([]sqlc.Node, error) GetNodesByLastUpdateRange(ctx context.Context, arg sqlc.GetNodesByLastUpdateRangeParams) ([]sqlc.Node, error)
ListNodesPaginated(ctx context.Context, arg sqlc.ListNodesPaginatedParams) ([]sqlc.Node, error) ListNodesPaginated(ctx context.Context, arg sqlc.ListNodesPaginatedParams) ([]sqlc.Node, error)
ListNodeIDsAndPubKeys(ctx context.Context, arg sqlc.ListNodeIDsAndPubKeysParams) ([]sqlc.ListNodeIDsAndPubKeysRow, 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) DeleteNodeByPubKey(ctx context.Context, arg sqlc.DeleteNodeByPubKeyParams) (sql.Result, error)
GetExtraNodeTypes(ctx context.Context, nodeID int64) ([]sqlc.NodeExtraType, 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 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 // 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 // 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 // skipped and the result will contain only those edges that exist at the time
+26
View File
@@ -1360,6 +1360,32 @@ func (q *Queries) InsertNodeFeature(ctx context.Context, arg InsertNodeFeaturePa
return err 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 const isZombieChannel = `-- name: IsZombieChannel :one
SELECT EXISTS ( SELECT EXISTS (
SELECT 1 SELECT 1
+1
View File
@@ -74,6 +74,7 @@ type Querier interface {
InsertMigratedInvoice(ctx context.Context, arg InsertMigratedInvoiceParams) (int64, error) InsertMigratedInvoice(ctx context.Context, arg InsertMigratedInvoiceParams) (int64, error)
InsertNodeAddress(ctx context.Context, arg InsertNodeAddressParams) error InsertNodeAddress(ctx context.Context, arg InsertNodeAddressParams) error
InsertNodeFeature(ctx context.Context, arg InsertNodeFeatureParams) error InsertNodeFeature(ctx context.Context, arg InsertNodeFeatureParams) error
IsPublicV1Node(ctx context.Context, pubKey []byte) (bool, error)
IsZombieChannel(ctx context.Context, arg IsZombieChannelParams) (bool, error) IsZombieChannel(ctx context.Context, arg IsZombieChannelParams) (bool, error)
ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNodeIDParams) ([]ListChannelsByNodeIDRow, error) ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNodeIDParams) ([]ListChannelsByNodeIDRow, error)
ListChannelsWithPoliciesPaginated(ctx context.Context, arg ListChannelsWithPoliciesPaginatedParams) ([]ListChannelsWithPoliciesPaginatedRow, error) ListChannelsWithPoliciesPaginated(ctx context.Context, arg ListChannelsWithPoliciesPaginatedParams) ([]ListChannelsWithPoliciesPaginatedRow, error)
+18
View File
@@ -47,6 +47,24 @@ WHERE version = $1 AND id > $2
ORDER BY id ORDER BY id
LIMIT $3; 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 -- name: DeleteNodeByPubKey :execresult
DELETE FROM nodes DELETE FROM nodes
WHERE pub_key = $1 WHERE pub_key = $1