graph/db: implement ForEachNodeDirectedChannel and ForEachNodeCacheable

Here we add the `ForEachNodeDirectedChannel` and `ForEachNodeCacheable`
SQLStore implementations which then lets us run
`TestGraphTraversalCacheable` and `TestGraphCacheForEachNodeChannel`
against SQL backends.
This commit is contained in:
Elle Mouton committed 2025-06-18 16:55:00 +02:00
1 parent 8af32951c7
commit 0607982886
5 files changed
+324 -9

No files matched your search

+13 -8
View File
@@ -23,6 +23,7 @@ import (
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@@ -1366,7 +1367,7 @@ func TestGraphTraversal(t *testing.T) {
func TestGraphTraversalCacheable(t *testing.T) { func TestGraphTraversalCacheable(t *testing.T) {
t.Parallel() t.Parallel()
graph := MakeTestGraph(t) graph := MakeTestGraphNew(t)
// We'd like to test some of the graph traversal capabilities within // We'd like to test some of the graph traversal capabilities within
// the DB, so we'll create a series of fake nodes to insert into the // the DB, so we'll create a series of fake nodes to insert into the
@@ -4162,7 +4163,7 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) {
t.Parallel() t.Parallel()
ctx := context.Background() ctx := context.Background()
graph := MakeTestGraph(t) graph := MakeTestGraphNew(t)
// Unset the channel graph cache to simulate the user running with the // Unset the channel graph cache to simulate the user running with the
// option turned off. // option turned off.
@@ -4212,21 +4213,25 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) {
edge1.ExtraOpaqueData = []byte{ edge1.ExtraOpaqueData = []byte{
253, 217, 3, 8, 0, 0, 0, 10, 0, 0, 0, 20, 253, 217, 3, 8, 0, 0, 0, 10, 0, 0, 0, 20,
} }
inboundFee := lnwire.Fee{
BaseFee: 10,
FeeRate: 20,
}
edge1.InboundFee = fn.Some(inboundFee)
require.NoError(t, graph.UpdateEdgePolicy(edge1)) require.NoError(t, graph.UpdateEdgePolicy(edge1))
edge1 = copyEdgePolicy(edge1) // Avoid read/write race conditions. edge1 = copyEdgePolicy(edge1) // Avoid read/write race conditions.
directedChan := getSingleChannel() directedChan := getSingleChannel()
require.NotNil(t, directedChan) require.NotNil(t, directedChan)
expectedInbound := lnwire.Fee{ require.Equal(t, inboundFee, directedChan.InboundFee)
BaseFee: 10,
FeeRate: 20,
}
require.Equal(t, expectedInbound, directedChan.InboundFee)
// Set an invalid inbound fee and check that persistence fails. // Set an invalid inbound fee and check that persistence fails.
edge1.ExtraOpaqueData = []byte{ edge1.ExtraOpaqueData = []byte{
253, 217, 3, 8, 0, 253, 217, 3, 8, 0,
} }
// We need to update the timestamp so that we don't hit the DB conflict
// error when we try to update the edge policy.
edge1.LastUpdate = edge1.LastUpdate.Add(time.Second)
require.ErrorIs( require.ErrorIs(
t, graph.UpdateEdgePolicy(edge1), ErrParsingExtraTLVBytes, t, graph.UpdateEdgePolicy(edge1), ErrParsingExtraTLVBytes,
) )
@@ -4235,7 +4240,7 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) {
// the previous result when we query the channel again. // the previous result when we query the channel again.
directedChan = getSingleChannel() directedChan = getSingleChannel()
require.NotNil(t, directedChan) require.NotNil(t, directedChan)
require.Equal(t, expectedInbound, directedChan.InboundFee) require.Equal(t, inboundFee, directedChan.InboundFee)
} }
// TestGraphLoading asserts that the cache is properly reconstructed after a // TestGraphLoading asserts that the cache is properly reconstructed after a
+235 -1
View File
@@ -56,8 +56,10 @@ type SQLQueries interface {
*/ */
UpsertNode(ctx context.Context, arg sqlc.UpsertNodeParams) (int64, error) UpsertNode(ctx context.Context, arg sqlc.UpsertNodeParams) (int64, error)
GetNodeByPubKey(ctx context.Context, arg sqlc.GetNodeByPubKeyParams) (sqlc.Node, error) GetNodeByPubKey(ctx context.Context, arg sqlc.GetNodeByPubKeyParams) (sqlc.Node, error)
GetNodeIDByPubKey(ctx context.Context, arg sqlc.GetNodeIDByPubKeyParams) (int64, error)
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)
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)
@@ -844,6 +846,224 @@ func (s *sqlGraphNodeTx) FetchNode(nodePub route.Vertex) (NodeRTx, error) {
return newSQLGraphNodeTx(s.db, s.chain, id, node), nil return newSQLGraphNodeTx(s.db, s.chain, id, node), nil
} }
// ForEachNodeDirectedChannel iterates through all channels of a given node,
// executing the passed callback on the directed edge representing the channel
// and its incoming policy. If the callback returns an error, then the iteration
// is halted with the error propagated back up to the caller.
//
// Unknown policies are passed into the callback as nil values.
//
// NOTE: this is part of the graphdb.NodeTraverser interface.
func (s *SQLStore) ForEachNodeDirectedChannel(nodePub route.Vertex,
cb func(channel *DirectedChannel) error) error {
var ctx = context.TODO()
return s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
return forEachNodeDirectedChannel(ctx, db, nodePub, cb)
}, sqldb.NoOpReset)
}
// ForEachNodeCacheable iterates through all the stored vertices/nodes in the
// graph, executing the passed callback with each node encountered. If the
// callback returns an error, then the transaction is aborted and the iteration
// stops early.
//
// NOTE: This is a part of the V1Store interface.
func (s *SQLStore) ForEachNodeCacheable(cb func(route.Vertex,
*lnwire.FeatureVector) error) error {
ctx := context.TODO()
err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
return forEachNodeCacheable(ctx, db, func(nodeID int64,
nodePub route.Vertex) error {
features, err := getNodeFeatures(ctx, db, nodeID)
if err != nil {
return fmt.Errorf("unable to fetch node "+
"features: %w", err)
}
return cb(nodePub, features)
})
}, sqldb.NoOpReset)
if err != nil {
return fmt.Errorf("unable to fetch nodes: %w", err)
}
return nil
}
// forEachNodeDirectedChannel iterates through all channels of a given
// node, executing the passed callback on the directed edge representing the
// channel and its incoming policy. If the node is not found, no error is
// returned.
func forEachNodeDirectedChannel(ctx context.Context, db SQLQueries,
nodePub route.Vertex, cb func(channel *DirectedChannel) error) error {
toNodeCallback := func() route.Vertex {
return nodePub
}
dbID, err := db.GetNodeIDByPubKey(
ctx, sqlc.GetNodeIDByPubKeyParams{
Version: int16(ProtocolV1),
PubKey: nodePub[:],
},
)
if errors.Is(err, sql.ErrNoRows) {
return nil
} else if err != nil {
return fmt.Errorf("unable to fetch node: %w", err)
}
rows, err := db.ListChannelsByNodeID(
ctx, sqlc.ListChannelsByNodeIDParams{
Version: int16(ProtocolV1),
NodeID1: dbID,
},
)
if err != nil {
return fmt.Errorf("unable to fetch channels: %w", err)
}
// Exit early if there are no channels for this node so we don't
// do the unnecessary feature fetching.
if len(rows) == 0 {
return nil
}
features, err := getNodeFeatures(ctx, db, dbID)
if err != nil {
return fmt.Errorf("unable to fetch node features: %w", err)
}
for _, row := range rows {
node1, node2, err := buildNodeVertices(
row.Node1Pubkey, row.Node2Pubkey,
)
if err != nil {
return fmt.Errorf("unable to build node vertices: %w",
err)
}
edge, err := buildCacheableChannelInfo(
row.Channel, node1, node2,
)
if err != nil {
return err
}
dbPol1, dbPol2, err := extractChannelPolicies(row)
if err != nil {
return err
}
var p1, p2 *models.CachedEdgePolicy
if dbPol1 != nil {
policy1, err := buildChanPolicy(
*dbPol1, edge.ChannelID, nil, node2, true,
)
if err != nil {
return err
}
p1 = models.NewCachedPolicy(policy1)
}
if dbPol2 != nil {
policy2, err := buildChanPolicy(
*dbPol2, edge.ChannelID, nil, node1, false,
)
if err != nil {
return err
}
p2 = models.NewCachedPolicy(policy2)
}
// Determine the outgoing and incoming policy for this
// channel and node combo.
outPolicy, inPolicy := p1, p2
if p1 != nil && node2 == nodePub {
outPolicy, inPolicy = p2, p1
} else if p2 != nil && node1 != nodePub {
outPolicy, inPolicy = p2, p1
}
var cachedInPolicy *models.CachedEdgePolicy
if inPolicy != nil {
cachedInPolicy = inPolicy
cachedInPolicy.ToNodePubKey = toNodeCallback
cachedInPolicy.ToNodeFeatures = features
}
directedChannel := &DirectedChannel{
ChannelID: edge.ChannelID,
IsNode1: nodePub == edge.NodeKey1Bytes,
OtherNode: edge.NodeKey2Bytes,
Capacity: edge.Capacity,
OutPolicySet: outPolicy != nil,
InPolicy: cachedInPolicy,
}
if outPolicy != nil {
outPolicy.InboundFee.WhenSome(func(fee lnwire.Fee) {
directedChannel.InboundFee = fee
})
}
if nodePub == edge.NodeKey2Bytes {
directedChannel.OtherNode = edge.NodeKey1Bytes
}
if err := cb(directedChannel); err != nil {
return err
}
}
return nil
}
// forEachNodeCacheable fetches all V1 node IDs and pub keys from the database,
// and executes the provided callback for each node.
func forEachNodeCacheable(ctx context.Context, db SQLQueries,
cb func(nodeID int64, nodePub route.Vertex) error) error {
var lastID int64
for {
nodes, err := db.ListNodeIDsAndPubKeys(
ctx, sqlc.ListNodeIDsAndPubKeysParams{
Version: int16(ProtocolV1),
ID: lastID,
Limit: pageSize,
},
)
if err != nil {
return fmt.Errorf("unable to fetch nodes: %w", err)
}
if len(nodes) == 0 {
break
}
for _, node := range nodes {
var pub route.Vertex
copy(pub[:], node.PubKey)
if err := cb(node.ID, pub); err != nil {
return fmt.Errorf("forEachNodeCacheable "+
"callback failed for node(id=%d): %w",
node.ID, err)
}
lastID = node.ID
}
}
return nil
}
// forEachNodeChannel iterates through all channels of a node, executing // forEachNodeChannel iterates through all channels of a node, executing
// the passed callback on each. The call-back is provided with the channel's // the passed callback on each. The call-back is provided with the channel's
// edge information, the outgoing policy and the incoming policy for the // edge information, the outgoing policy and the incoming policy for the
@@ -1033,6 +1253,20 @@ func getNodeByPubKey(ctx context.Context, db SQLQueries,
return dbNode.ID, node, nil return dbNode.ID, node, nil
} }
// buildCacheableChannelInfo builds a models.CachedEdgeInfo instance from the
// provided database channel row and the public keys of the two nodes
// involved in the channel.
func buildCacheableChannelInfo(dbChan sqlc.Channel, node1Pub,
node2Pub route.Vertex) (*models.CachedEdgeInfo, error) {
return &models.CachedEdgeInfo{
ChannelID: byteOrder.Uint64(dbChan.Scid),
NodeKey1Bytes: node1Pub,
NodeKey2Bytes: node2Pub,
Capacity: btcutil.Amount(dbChan.Capacity.Int64),
}, nil
}
// buildNode constructs a LightningNode instance from the given database node // buildNode constructs a LightningNode instance from the given database node
// record. The node's features, addresses and extra signed fields are also // record. The node's features, addresses and extra signed fields are also
// fetched from the database and set on the node. // fetched from the database and set on the node.
@@ -1589,7 +1823,7 @@ func marshalExtraOpaqueData(data []byte) (map[uint64][]byte, error) {
// pass it into the P2P decoding variant. // pass it into the P2P decoding variant.
parsedTypes, err := tlvStream.DecodeWithParsedTypesP2P(r) parsedTypes, err := tlvStream.DecodeWithParsedTypesP2P(r)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("%w: %w", ErrParsingExtraTLVBytes, err)
} }
if len(parsedTypes) == 0 { if len(parsedTypes) == 0 {
return nil, nil return nil, nil
+61
View File
@@ -533,6 +533,25 @@ func (q *Queries) GetNodeFeaturesByPubKey(ctx context.Context, arg GetNodeFeatur
return items, nil return items, nil
} }
const getNodeIDByPubKey = `-- name: GetNodeIDByPubKey :one
SELECT id
FROM nodes
WHERE pub_key = $1
AND version = $2
`
type GetNodeIDByPubKeyParams struct {
PubKey []byte
Version int16
}
func (q *Queries) GetNodeIDByPubKey(ctx context.Context, arg GetNodeIDByPubKeyParams) (int64, error) {
row := q.db.QueryRowContext(ctx, getNodeIDByPubKey, arg.PubKey, arg.Version)
var id int64
err := row.Scan(&id)
return id, err
}
const getNodesByLastUpdateRange = `-- name: GetNodesByLastUpdateRange :many const getNodesByLastUpdateRange = `-- name: GetNodesByLastUpdateRange :many
SELECT id, version, pub_key, alias, last_update, color, signature SELECT id, version, pub_key, alias, last_update, color, signature
FROM nodes FROM nodes
@@ -879,6 +898,48 @@ func (q *Queries) ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNo
return items, nil return items, nil
} }
const listNodeIDsAndPubKeys = `-- name: ListNodeIDsAndPubKeys :many
SELECT id, pub_key
FROM nodes
WHERE version = $1 AND id > $2
ORDER BY id
LIMIT $3
`
type ListNodeIDsAndPubKeysParams struct {
Version int16
ID int64
Limit int32
}
type ListNodeIDsAndPubKeysRow struct {
ID int64
PubKey []byte
}
func (q *Queries) ListNodeIDsAndPubKeys(ctx context.Context, arg ListNodeIDsAndPubKeysParams) ([]ListNodeIDsAndPubKeysRow, error) {
rows, err := q.db.QueryContext(ctx, listNodeIDsAndPubKeys, arg.Version, arg.ID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListNodeIDsAndPubKeysRow
for rows.Next() {
var i ListNodeIDsAndPubKeysRow
if err := rows.Scan(&i.ID, &i.PubKey); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listNodesPaginated = `-- name: ListNodesPaginated :many const listNodesPaginated = `-- name: ListNodesPaginated :many
SELECT id, version, pub_key, alias, last_update, color, signature SELECT id, version, pub_key, alias, last_update, color, signature
FROM nodes FROM nodes
+2
View File
@@ -48,6 +48,7 @@ type Querier interface {
GetNodeByPubKey(ctx context.Context, arg GetNodeByPubKeyParams) (Node, error) GetNodeByPubKey(ctx context.Context, arg GetNodeByPubKeyParams) (Node, error)
GetNodeFeatures(ctx context.Context, nodeID int64) ([]NodeFeature, error) GetNodeFeatures(ctx context.Context, nodeID int64) ([]NodeFeature, error)
GetNodeFeaturesByPubKey(ctx context.Context, arg GetNodeFeaturesByPubKeyParams) ([]int32, error) GetNodeFeaturesByPubKey(ctx context.Context, arg GetNodeFeaturesByPubKeyParams) ([]int32, error)
GetNodeIDByPubKey(ctx context.Context, arg GetNodeIDByPubKeyParams) (int64, error)
GetNodesByLastUpdateRange(ctx context.Context, arg GetNodesByLastUpdateRangeParams) ([]Node, error) GetNodesByLastUpdateRange(ctx context.Context, arg GetNodesByLastUpdateRangeParams) ([]Node, error)
GetSourceNodesByVersion(ctx context.Context, version int16) ([]GetSourceNodesByVersionRow, error) GetSourceNodesByVersion(ctx context.Context, version int16) ([]GetSourceNodesByVersionRow, error)
HighestSCID(ctx context.Context, version int16) ([]byte, error) HighestSCID(ctx context.Context, version int16) ([]byte, error)
@@ -64,6 +65,7 @@ type Querier interface {
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
ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNodeIDParams) ([]ListChannelsByNodeIDRow, error) ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNodeIDParams) ([]ListChannelsByNodeIDRow, error)
ListNodeIDsAndPubKeys(ctx context.Context, arg ListNodeIDsAndPubKeysParams) ([]ListNodeIDsAndPubKeysRow, error)
ListNodesPaginated(ctx context.Context, arg ListNodesPaginatedParams) ([]Node, error) ListNodesPaginated(ctx context.Context, arg ListNodesPaginatedParams) ([]Node, error)
NextInvoiceSettleIndex(ctx context.Context) (int64, error) NextInvoiceSettleIndex(ctx context.Context) (int64, error)
OnAMPSubInvoiceCanceled(ctx context.Context, arg OnAMPSubInvoiceCanceledParams) error OnAMPSubInvoiceCanceled(ctx context.Context, arg OnAMPSubInvoiceCanceledParams) error
+13
View File
@@ -27,6 +27,12 @@ FROM nodes
WHERE pub_key = $1 WHERE pub_key = $1
AND version = $2; AND version = $2;
-- name: GetNodeIDByPubKey :one
SELECT id
FROM nodes
WHERE pub_key = $1
AND version = $2;
-- name: ListNodesPaginated :many -- name: ListNodesPaginated :many
SELECT * SELECT *
FROM nodes FROM nodes
@@ -34,6 +40,13 @@ WHERE version = $1 AND id > $2
ORDER BY id ORDER BY id
LIMIT $3; LIMIT $3;
-- name: ListNodeIDsAndPubKeys :many
SELECT id, pub_key
FROM nodes
WHERE version = $1 AND id > $2
ORDER BY id
LIMIT $3;
-- name: DeleteNodeByPubKey :execresult -- name: DeleteNodeByPubKey :execresult
DELETE FROM nodes DELETE FROM nodes
WHERE pub_key = $1 WHERE pub_key = $1