graph/db+sqldb: implement ForEachSourceNodeChannel
In this commit, the ForEachSourceNodeChannel implementation of the SQLStore is added. Since this is the first method of the SQLStore that fetches channel and policy info, it also adds all the helpers that are required to do so. These will be re-used in upcoming commits as more "For"-type methods are added. With this implementation, we convert the `TestForEachSourceNodeChannel` such that it is run against SQL backends.
This commit is contained in:
6 files changed
+797
-1
No files matched your search
@@ -1183,7 +1183,7 @@ func TestAddEdgeProof(t *testing.T) {
|
||||
func TestForEachSourceNodeChannel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
graph := MakeTestGraph(t)
|
||||
graph := MakeTestGraphNew(t)
|
||||
|
||||
// Create a source node (A) and set it as such in the DB.
|
||||
nodeA := createTestVertex(t)
|
||||
|
||||
@@ -14,8 +14,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/batch"
|
||||
"github.com/lightningnetwork/lnd/fn/v2"
|
||||
"github.com/lightningnetwork/lnd/graph/db/models"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
@@ -77,7 +80,9 @@ type SQLQueries interface {
|
||||
CreateChannel(ctx context.Context, arg sqlc.CreateChannelParams) (int64, error)
|
||||
GetChannelBySCID(ctx context.Context, arg sqlc.GetChannelBySCIDParams) (sqlc.Channel, error)
|
||||
GetChannelAndNodesBySCID(ctx context.Context, arg sqlc.GetChannelAndNodesBySCIDParams) (sqlc.GetChannelAndNodesBySCIDRow, error)
|
||||
GetChannelFeaturesAndExtras(ctx context.Context, channelID int64) ([]sqlc.GetChannelFeaturesAndExtrasRow, error)
|
||||
HighestSCID(ctx context.Context, version int16) ([]byte, error)
|
||||
ListChannelsByNodeID(ctx context.Context, arg sqlc.ListChannelsByNodeIDParams) ([]sqlc.ListChannelsByNodeIDRow, error)
|
||||
|
||||
CreateChannelExtraType(ctx context.Context, arg sqlc.CreateChannelExtraTypeParams) error
|
||||
InsertChannelFeature(ctx context.Context, arg sqlc.InsertChannelFeatureParams) error
|
||||
@@ -88,6 +93,7 @@ type SQLQueries interface {
|
||||
UpsertEdgePolicy(ctx context.Context, arg sqlc.UpsertEdgePolicyParams) (int64, error)
|
||||
|
||||
InsertChanPolicyExtraType(ctx context.Context, arg sqlc.InsertChanPolicyExtraTypeParams) error
|
||||
GetChannelPolicyExtraTypes(ctx context.Context, arg sqlc.GetChannelPolicyExtraTypesParams) ([]sqlc.GetChannelPolicyExtraTypesRow, error)
|
||||
DeleteChannelPolicyExtraTypes(ctx context.Context, channelPolicyID int64) error
|
||||
}
|
||||
|
||||
@@ -651,6 +657,136 @@ func (s *SQLStore) updateEdgeCache(e *models.ChannelEdgePolicy,
|
||||
}
|
||||
}
|
||||
|
||||
// ForEachSourceNodeChannel iterates through all channels of the source node,
|
||||
// executing the passed callback on each. The call-back is provided with the
|
||||
// channel's outpoint, whether we have a policy for the channel and the channel
|
||||
// peer's node information.
|
||||
//
|
||||
// NOTE: part of the V1Store interface.
|
||||
func (s *SQLStore) ForEachSourceNodeChannel(cb func(chanPoint wire.OutPoint,
|
||||
havePolicy bool, otherNode *models.LightningNode) error) error {
|
||||
|
||||
var ctx = context.TODO()
|
||||
|
||||
return s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
|
||||
nodeID, nodePub, err := s.getSourceNode(ctx, db, ProtocolV1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch source node: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
return forEachNodeChannel(
|
||||
ctx, db, s.cfg.ChainHash, nodeID,
|
||||
func(info *models.ChannelEdgeInfo,
|
||||
outPolicy *models.ChannelEdgePolicy,
|
||||
_ *models.ChannelEdgePolicy) error {
|
||||
|
||||
// Fetch the other node.
|
||||
var (
|
||||
otherNodePub [33]byte
|
||||
node1 = info.NodeKey1Bytes
|
||||
node2 = info.NodeKey2Bytes
|
||||
)
|
||||
switch {
|
||||
case bytes.Equal(node1[:], nodePub[:]):
|
||||
otherNodePub = node2
|
||||
case bytes.Equal(node2[:], nodePub[:]):
|
||||
otherNodePub = node1
|
||||
default:
|
||||
return fmt.Errorf("node not " +
|
||||
"participating in this channel")
|
||||
}
|
||||
|
||||
_, otherNode, err := getNodeByPubKey(
|
||||
ctx, db, otherNodePub,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch "+
|
||||
"other node(%x): %w",
|
||||
otherNodePub, err)
|
||||
}
|
||||
|
||||
return cb(
|
||||
info.ChannelPoint, outPolicy != nil,
|
||||
otherNode,
|
||||
)
|
||||
},
|
||||
)
|
||||
}, sqldb.NoOpReset)
|
||||
}
|
||||
|
||||
// forEachNodeChannel iterates through all channels of a node, executing
|
||||
// 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
|
||||
// channel and node combo.
|
||||
func forEachNodeChannel(ctx context.Context, db SQLQueries,
|
||||
chain chainhash.Hash, id int64, cb func(*models.ChannelEdgeInfo,
|
||||
*models.ChannelEdgePolicy,
|
||||
*models.ChannelEdgePolicy) error) error {
|
||||
|
||||
// Get all the V1 channels for this node.Add commentMore actions
|
||||
rows, err := db.ListChannelsByNodeID(
|
||||
ctx, sqlc.ListChannelsByNodeIDParams{
|
||||
Version: int16(ProtocolV1),
|
||||
NodeID1: id,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch channels: %w", err)
|
||||
}
|
||||
|
||||
// Call the call-back for each channel and its known policies.
|
||||
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 := getAndBuildEdgeInfo(
|
||||
ctx, db, chain, row.Channel.ID, row.Channel, node1,
|
||||
node2,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to build channel info: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
dbPol1, dbPol2, err := extractChannelPolicies(row)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to extract channel "+
|
||||
"policies: %w", err)
|
||||
}
|
||||
|
||||
p1, p2, err := getAndBuildChanPolicies(
|
||||
ctx, db, dbPol1, dbPol2, edge.ChannelID, node1, node2,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to build channel "+
|
||||
"policies: %w", err)
|
||||
}
|
||||
|
||||
// Determine the outgoing and incoming policy for this
|
||||
// channel and node combo.
|
||||
p1ToNode := row.Channel.NodeID2
|
||||
p2ToNode := row.Channel.NodeID1
|
||||
outPolicy, inPolicy := p1, p2
|
||||
if (p1 != nil && p1ToNode == id) ||
|
||||
(p2 != nil && p2ToNode != id) {
|
||||
|
||||
outPolicy, inPolicy = p2, p1
|
||||
}
|
||||
|
||||
if err := cb(edge, outPolicy, inPolicy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateChanEdgePolicy upserts the channel policy info we have stored for
|
||||
// a channel we already know of.
|
||||
func updateChanEdgePolicy(ctx context.Context, tx SQLQueries,
|
||||
@@ -1515,3 +1651,311 @@ func upsertChanPolicyExtraSignedFields(ctx context.Context, db SQLQueries,
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAndBuildEdgeInfo builds a models.ChannelEdgeInfo instance from the
|
||||
// provided dbChanRow and also fetches any other required information
|
||||
// to construct the edge info.
|
||||
func getAndBuildEdgeInfo(ctx context.Context, db SQLQueries,
|
||||
chain chainhash.Hash, dbChanID int64, dbChan sqlc.Channel, node1,
|
||||
node2 route.Vertex) (*models.ChannelEdgeInfo, error) {
|
||||
|
||||
fv, extras, err := getChanFeaturesAndExtras(
|
||||
ctx, db, dbChanID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
op, err := wire.NewOutPointFromString(dbChan.Outpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var featureBuf bytes.Buffer
|
||||
if err := fv.Encode(&featureBuf); err != nil {
|
||||
return nil, fmt.Errorf("unable to encode features: %w", err)
|
||||
}
|
||||
|
||||
recs, err := lnwire.CustomRecords(extras).Serialize()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to serialize extra signed "+
|
||||
"fields: %w", err)
|
||||
}
|
||||
if recs == nil {
|
||||
recs = make([]byte, 0)
|
||||
}
|
||||
|
||||
var btcKey1, btcKey2 route.Vertex
|
||||
copy(btcKey1[:], dbChan.BitcoinKey1)
|
||||
copy(btcKey2[:], dbChan.BitcoinKey2)
|
||||
|
||||
channel := &models.ChannelEdgeInfo{
|
||||
ChainHash: chain,
|
||||
ChannelID: byteOrder.Uint64(dbChan.Scid),
|
||||
NodeKey1Bytes: node1,
|
||||
NodeKey2Bytes: node2,
|
||||
BitcoinKey1Bytes: btcKey1,
|
||||
BitcoinKey2Bytes: btcKey2,
|
||||
ChannelPoint: *op,
|
||||
Capacity: btcutil.Amount(dbChan.Capacity.Int64),
|
||||
Features: featureBuf.Bytes(),
|
||||
ExtraOpaqueData: recs,
|
||||
}
|
||||
|
||||
if dbChan.Bitcoin1Signature != nil {
|
||||
channel.AuthProof = &models.ChannelAuthProof{
|
||||
NodeSig1Bytes: dbChan.Node1Signature,
|
||||
NodeSig2Bytes: dbChan.Node2Signature,
|
||||
BitcoinSig1Bytes: dbChan.Bitcoin1Signature,
|
||||
BitcoinSig2Bytes: dbChan.Bitcoin2Signature,
|
||||
}
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
// buildNodeVertices is a helper that converts raw node public keys
|
||||
// into route.Vertex instances.
|
||||
func buildNodeVertices(node1Pub, node2Pub []byte) (route.Vertex,
|
||||
route.Vertex, error) {
|
||||
|
||||
node1Vertex, err := route.NewVertexFromBytes(node1Pub)
|
||||
if err != nil {
|
||||
return route.Vertex{}, route.Vertex{}, fmt.Errorf("unable to "+
|
||||
"create vertex from node1 pubkey: %w", err)
|
||||
}
|
||||
|
||||
node2Vertex, err := route.NewVertexFromBytes(node2Pub)
|
||||
if err != nil {
|
||||
return route.Vertex{}, route.Vertex{}, fmt.Errorf("unable to "+
|
||||
"create vertex from node2 pubkey: %w", err)
|
||||
}
|
||||
|
||||
return node1Vertex, node2Vertex, nil
|
||||
}
|
||||
|
||||
// getChanFeaturesAndExtras fetches the channel features and extra TLV types
|
||||
// for a channel with the given ID.
|
||||
func getChanFeaturesAndExtras(ctx context.Context, db SQLQueries,
|
||||
id int64) (*lnwire.FeatureVector, map[uint64][]byte, error) {
|
||||
|
||||
rows, err := db.GetChannelFeaturesAndExtras(ctx, id)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to fetch channel "+
|
||||
"features and extras: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
fv = lnwire.EmptyFeatureVector()
|
||||
extras = make(map[uint64][]byte)
|
||||
)
|
||||
for _, row := range rows {
|
||||
if row.IsFeature {
|
||||
fv.Set(lnwire.FeatureBit(row.FeatureBit))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
tlvType, ok := row.ExtraKey.(int64)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("unexpected type for "+
|
||||
"TLV type: %T", row.ExtraKey)
|
||||
}
|
||||
|
||||
valueBytes, ok := row.Value.([]byte)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("unexpected type for "+
|
||||
"Value: %T", row.Value)
|
||||
}
|
||||
|
||||
extras[uint64(tlvType)] = valueBytes
|
||||
}
|
||||
|
||||
return fv, extras, nil
|
||||
}
|
||||
|
||||
// getAndBuildChanPolicies uses the given sqlc.ChannelPolicy and also retrieves
|
||||
// all the extra info required to build the complete models.ChannelEdgePolicy
|
||||
// types. It returns two policies, which may be nil if the provided
|
||||
// sqlc.ChannelPolicy records are nil.
|
||||
func getAndBuildChanPolicies(ctx context.Context, db SQLQueries,
|
||||
dbPol1, dbPol2 *sqlc.ChannelPolicy, channelID uint64, node1,
|
||||
node2 route.Vertex) (*models.ChannelEdgePolicy,
|
||||
*models.ChannelEdgePolicy, error) {
|
||||
|
||||
if dbPol1 == nil && dbPol2 == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
policy1ID int64
|
||||
policy2ID int64
|
||||
)
|
||||
if dbPol1 != nil {
|
||||
policy1ID = dbPol1.ID
|
||||
}
|
||||
if dbPol2 != nil {
|
||||
policy2ID = dbPol2.ID
|
||||
}
|
||||
rows, err := db.GetChannelPolicyExtraTypes(
|
||||
ctx, sqlc.GetChannelPolicyExtraTypesParams{
|
||||
ID: policy1ID,
|
||||
ID_2: policy2ID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
dbPol1Extras = make(map[uint64][]byte)
|
||||
dbPol2Extras = make(map[uint64][]byte)
|
||||
)
|
||||
for _, row := range rows {
|
||||
switch row.PolicyID {
|
||||
case policy1ID:
|
||||
dbPol1Extras[uint64(row.Type)] = row.Value
|
||||
case policy2ID:
|
||||
dbPol2Extras[uint64(row.Type)] = row.Value
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unexpected policy ID %d "+
|
||||
"in row: %v", row.PolicyID, row)
|
||||
}
|
||||
}
|
||||
|
||||
var pol1, pol2 *models.ChannelEdgePolicy
|
||||
if dbPol1 != nil {
|
||||
pol1, err = buildChanPolicy(
|
||||
*dbPol1, channelID, dbPol1Extras, node2, true,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
if dbPol2 != nil {
|
||||
pol2, err = buildChanPolicy(
|
||||
*dbPol2, channelID, dbPol2Extras, node1, false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return pol1, pol2, nil
|
||||
}
|
||||
|
||||
// buildChanPolicy builds a models.ChannelEdgePolicy instance from the
|
||||
// provided sqlc.ChannelPolicy and other required information.
|
||||
func buildChanPolicy(dbPolicy sqlc.ChannelPolicy, channelID uint64,
|
||||
extras map[uint64][]byte, toNode route.Vertex,
|
||||
isNode1 bool) (*models.ChannelEdgePolicy, error) {
|
||||
|
||||
recs, err := lnwire.CustomRecords(extras).Serialize()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to serialize extra signed "+
|
||||
"fields: %w", err)
|
||||
}
|
||||
|
||||
var msgFlags lnwire.ChanUpdateMsgFlags
|
||||
if dbPolicy.MaxHtlcMsat.Valid {
|
||||
msgFlags |= lnwire.ChanUpdateRequiredMaxHtlc
|
||||
}
|
||||
|
||||
var chanFlags lnwire.ChanUpdateChanFlags
|
||||
if !isNode1 {
|
||||
chanFlags |= lnwire.ChanUpdateDirection
|
||||
}
|
||||
if dbPolicy.Disabled.Bool {
|
||||
chanFlags |= lnwire.ChanUpdateDisabled
|
||||
}
|
||||
|
||||
var inboundFee fn.Option[lnwire.Fee]
|
||||
if dbPolicy.InboundFeeRateMilliMsat.Valid ||
|
||||
dbPolicy.InboundBaseFeeMsat.Valid {
|
||||
|
||||
inboundFee = fn.Some(lnwire.Fee{
|
||||
BaseFee: int32(dbPolicy.InboundBaseFeeMsat.Int64),
|
||||
FeeRate: int32(dbPolicy.InboundFeeRateMilliMsat.Int64),
|
||||
})
|
||||
}
|
||||
|
||||
return &models.ChannelEdgePolicy{
|
||||
SigBytes: dbPolicy.Signature,
|
||||
ChannelID: channelID,
|
||||
LastUpdate: time.Unix(
|
||||
dbPolicy.LastUpdate.Int64, 0,
|
||||
),
|
||||
MessageFlags: msgFlags,
|
||||
ChannelFlags: chanFlags,
|
||||
TimeLockDelta: uint16(dbPolicy.Timelock),
|
||||
MinHTLC: lnwire.MilliSatoshi(
|
||||
dbPolicy.MinHtlcMsat,
|
||||
),
|
||||
MaxHTLC: lnwire.MilliSatoshi(
|
||||
dbPolicy.MaxHtlcMsat.Int64,
|
||||
),
|
||||
FeeBaseMSat: lnwire.MilliSatoshi(
|
||||
dbPolicy.BaseFeeMsat,
|
||||
),
|
||||
FeeProportionalMillionths: lnwire.MilliSatoshi(dbPolicy.FeePpm),
|
||||
ToNode: toNode,
|
||||
InboundFee: inboundFee,
|
||||
ExtraOpaqueData: recs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractChannelPolicies extracts the sqlc.ChannelPolicy records from the give
|
||||
// row which is expected to be a sqlc type that contains channel policy
|
||||
// information. It returns two policies, which may be nil if the policy
|
||||
// information is not present in the row.
|
||||
//
|
||||
//nolint:ll
|
||||
func extractChannelPolicies(row any) (*sqlc.ChannelPolicy, *sqlc.ChannelPolicy,
|
||||
error) {
|
||||
|
||||
var policy1, policy2 *sqlc.ChannelPolicy
|
||||
switch r := row.(type) {
|
||||
case sqlc.ListChannelsByNodeIDRow:
|
||||
if r.Policy1ID.Valid {
|
||||
policy1 = &sqlc.ChannelPolicy{
|
||||
ID: r.Policy1ID.Int64,
|
||||
Version: r.Policy1Version.Int16,
|
||||
ChannelID: r.Channel.ID,
|
||||
NodeID: r.Policy1NodeID.Int64,
|
||||
Timelock: r.Policy1Timelock.Int32,
|
||||
FeePpm: r.Policy1FeePpm.Int64,
|
||||
BaseFeeMsat: r.Policy1BaseFeeMsat.Int64,
|
||||
MinHtlcMsat: r.Policy1MinHtlcMsat.Int64,
|
||||
MaxHtlcMsat: r.Policy1MaxHtlcMsat,
|
||||
LastUpdate: r.Policy1LastUpdate,
|
||||
InboundBaseFeeMsat: r.Policy1InboundBaseFeeMsat,
|
||||
InboundFeeRateMilliMsat: r.Policy1InboundFeeRateMilliMsat,
|
||||
Disabled: r.Policy1Disabled,
|
||||
Signature: r.Policy1Signature,
|
||||
}
|
||||
}
|
||||
if r.Policy2ID.Valid {
|
||||
policy2 = &sqlc.ChannelPolicy{
|
||||
ID: r.Policy2ID.Int64,
|
||||
Version: r.Policy2Version.Int16,
|
||||
ChannelID: r.Channel.ID,
|
||||
NodeID: r.Policy2NodeID.Int64,
|
||||
Timelock: r.Policy2Timelock.Int32,
|
||||
FeePpm: r.Policy2FeePpm.Int64,
|
||||
BaseFeeMsat: r.Policy2BaseFeeMsat.Int64,
|
||||
MinHtlcMsat: r.Policy2MinHtlcMsat.Int64,
|
||||
MaxHtlcMsat: r.Policy2MaxHtlcMsat,
|
||||
LastUpdate: r.Policy2LastUpdate,
|
||||
InboundBaseFeeMsat: r.Policy2InboundBaseFeeMsat,
|
||||
InboundFeeRateMilliMsat: r.Policy2InboundFeeRateMilliMsat,
|
||||
Disabled: r.Policy2Disabled,
|
||||
Signature: r.Policy2Signature,
|
||||
}
|
||||
}
|
||||
|
||||
return policy1, policy2, nil
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unexpected row type in "+
|
||||
"extractChannelPolicies: %T", r)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user