graph+lnd: add NetworkStats to GraphSource interface

so that the external graph source can be used to query network
information rather than depending on the local graph DB.
This commit is contained in:
Elle Mouton
2024-11-12 09:03:52 +02:00
parent 372883ab81
commit 80070618a7
6 changed files with 219 additions and 116 deletions

37
graph/db/models/stats.go Normal file
View File

@@ -0,0 +1,37 @@
package models
import "github.com/btcsuite/btcd/btcutil"
// NetworkStats represents various statistics about the state of the Lightning
// network graph.
type NetworkStats struct {
// Diameter is the diameter of the graph, which is the length of the
// longest shortest path between any two nodes in the graph.
Diameter uint32
// MaxChanOut is the maximum number of outgoing channels from a single
// node.
MaxChanOut uint32
// NumNodes is the total number of nodes in the graph.
NumNodes uint32
// NumChannels is the total number of channels in the graph.
NumChannels uint32
// TotalNetworkCapacity is the total capacity of all channels in the
// graph.
TotalNetworkCapacity btcutil.Amount
// MinChanSize is the smallest channel size in the graph.
MinChanSize btcutil.Amount
// MaxChanSize is the largest channel size in the graph.
MaxChanSize btcutil.Amount
// MedianChanSize is the median channel size in the graph.
MedianChanSize btcutil.Amount
// NumZombies is the number of zombie channels in the graph.
NumZombies uint64
}

View File

@@ -3,10 +3,12 @@ package sources
import (
"context"
"fmt"
"math"
"net"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/autopilot"
"github.com/lightningnetwork/lnd/discovery"
@@ -231,6 +233,133 @@ func (s *DBSource) GraphBootstrapper(_ context.Context) (
return discovery.NewGraphBootstrapper(chanGraph)
}
// NetworkStats returns statistics concerning the current state of the known
// channel graph within the network.
//
// NOTE: this is part of the GraphSource interface.
func (s *DBSource) NetworkStats(_ context.Context) (*models.NetworkStats,
error) {
var (
numNodes uint32
numChannels uint32
maxChanOut uint32
totalNetworkCapacity btcutil.Amount
minChannelSize btcutil.Amount = math.MaxInt64
maxChannelSize btcutil.Amount
medianChanSize btcutil.Amount
)
// We'll use this map to de-duplicate channels during our traversal.
// This is needed since channels are directional, so there will be two
// edges for each channel within the graph.
seenChans := make(map[uint64]struct{})
// We also keep a list of all encountered capacities, in order to
// calculate the median channel size.
var allChans []btcutil.Amount
// We'll run through all the known nodes in the within our view of the
// network, tallying up the total number of nodes, and also gathering
// each node so we can measure the graph diameter and degree stats
// below.
err := s.db.ForEachNodeCached(func(node route.Vertex,
edges map[uint64]*graphdb.DirectedChannel) error {
// Increment the total number of nodes with each iteration.
numNodes++
// For each channel we'll compute the out degree of each node,
// and also update our running tallies of the min/max channel
// capacity, as well as the total channel capacity. We pass
// through the DB transaction from the outer view so we can
// re-use it within this inner view.
var outDegree uint32
for _, edge := range edges {
// Bump up the out degree for this node for each
// channel encountered.
outDegree++
// If we've already seen this channel, then we'll
// return early to ensure that we don't double-count
// stats.
if _, ok := seenChans[edge.ChannelID]; ok {
return nil
}
// Compare the capacity of this channel against the
// running min/max to see if we should update the
// extrema.
chanCapacity := edge.Capacity
if chanCapacity < minChannelSize {
minChannelSize = chanCapacity
}
if chanCapacity > maxChannelSize {
maxChannelSize = chanCapacity
}
// Accumulate the total capacity of this channel to the
// network wide-capacity.
totalNetworkCapacity += chanCapacity
numChannels++
seenChans[edge.ChannelID] = struct{}{}
allChans = append(allChans, edge.Capacity)
}
// Finally, if the out degree of this node is greater than what
// we've seen so far, update the maxChanOut variable.
if outDegree > maxChanOut {
maxChanOut = outDegree
}
return nil
})
if err != nil {
return nil, err
}
// Find the median.
medianChanSize = autopilot.Median(allChans)
// If we don't have any channels, then reset the minChannelSize to zero
// to avoid outputting NaN in encoded JSON.
if numChannels == 0 {
minChannelSize = 0
}
// Graph diameter.
channelGraph := autopilot.ChannelGraphFromCachedDatabase(s.db)
simpleGraph, err := autopilot.NewSimpleGraph(channelGraph)
if err != nil {
return nil, err
}
start := time.Now()
diameter := simpleGraph.DiameterRadialCutoff()
log.Infof("Elapsed time for diameter (%d) calculation: %v", diameter,
time.Since(start))
// Query the graph for the current number of zombie channels.
numZombies, err := s.db.NumZombies()
if err != nil {
return nil, err
}
return &models.NetworkStats{
Diameter: diameter,
MaxChanOut: maxChanOut,
NumNodes: numNodes,
NumChannels: numChannels,
TotalNetworkCapacity: totalNetworkCapacity,
MinChanSize: minChannelSize,
MaxChanSize: maxChannelSize,
MedianChanSize: medianChanSize,
NumZombies: numZombies,
}, nil
}
// kvdbRTx is an implementation of graphdb.RTx backed by a KVDB database read
// transaction.
type kvdbRTx struct {

View File

@@ -74,4 +74,8 @@ type GraphSource interface {
// used to discover new peers to connect to.
GraphBootstrapper(ctx context.Context) (
discovery.NetworkPeerBootstrapper, error)
// NetworkStats returns statistics concerning the current state of the
// known channel graph within the network.
NetworkStats(ctx context.Context) (*models.NetworkStats, error)
}

31
graph/sources/log.go Normal file
View File

@@ -0,0 +1,31 @@
package sources
import (
"github.com/btcsuite/btclog/v2"
"github.com/lightningnetwork/lnd/build"
)
// log is a logger that is initialized with no output filters. This means the
// package will not perform any logging by default until the caller requests
// it.
var log btclog.Logger
const Subsystem = "GRSR"
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger(Subsystem, nil))
}
// DisableLog disables all library log output. Logging output is disabled by
// default until UseLogger is called.
func DisableLog() {
UseLogger(btclog.Disabled)
}
// UseLogger uses a specified Logger to output package logging info. This
// should be used in preference to SetLogWriter if the caller is also using
// btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}

2
log.go
View File

@@ -22,6 +22,7 @@ import (
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/graph"
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/graph/sources"
"github.com/lightningnetwork/lnd/healthcheck"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/invoices"
@@ -196,6 +197,7 @@ func SetupLoggers(root *build.SubLoggerManager, interceptor signal.Interceptor)
root, blindedpath.Subsystem, interceptor, blindedpath.UseLogger,
)
AddV1SubLogger(root, graphdb.Subsystem, interceptor, graphdb.UseLogger)
AddSubLogger(root, sources.Subsystem, interceptor, sources.UseLogger)
}
// AddSubLogger is a helper method to conveniently create and register the

View File

@@ -6903,134 +6903,34 @@ func (r *rpcServer) QueryRoutes(ctx context.Context,
func (r *rpcServer) GetNetworkInfo(ctx context.Context,
_ *lnrpc.NetworkInfoRequest) (*lnrpc.NetworkInfo, error) {
graph := r.server.graphDB
graph := r.server.graphSource
var (
numNodes uint32
numChannels uint32
maxChanOut uint32
totalNetworkCapacity btcutil.Amount
minChannelSize btcutil.Amount = math.MaxInt64
maxChannelSize btcutil.Amount
medianChanSize btcutil.Amount
)
// We'll use this map to de-duplicate channels during our traversal.
// This is needed since channels are directional, so there will be two
// edges for each channel within the graph.
seenChans := make(map[uint64]struct{})
// We also keep a list of all encountered capacities, in order to
// calculate the median channel size.
var allChans []btcutil.Amount
// We'll run through all the known nodes in the within our view of the
// network, tallying up the total number of nodes, and also gathering
// each node so we can measure the graph diameter and degree stats
// below.
err := graph.ForEachNodeCached(func(node route.Vertex,
edges map[uint64]*graphdb.DirectedChannel) error {
// Increment the total number of nodes with each iteration.
numNodes++
// For each channel we'll compute the out degree of each node,
// and also update our running tallies of the min/max channel
// capacity, as well as the total channel capacity. We pass
// through the db transaction from the outer view so we can
// re-use it within this inner view.
var outDegree uint32
for _, edge := range edges {
// Bump up the out degree for this node for each
// channel encountered.
outDegree++
// If we've already seen this channel, then we'll
// return early to ensure that we don't double-count
// stats.
if _, ok := seenChans[edge.ChannelID]; ok {
return nil
}
// Compare the capacity of this channel against the
// running min/max to see if we should update the
// extrema.
chanCapacity := edge.Capacity
if chanCapacity < minChannelSize {
minChannelSize = chanCapacity
}
if chanCapacity > maxChannelSize {
maxChannelSize = chanCapacity
}
// Accumulate the total capacity of this channel to the
// network wide-capacity.
totalNetworkCapacity += chanCapacity
numChannels++
seenChans[edge.ChannelID] = struct{}{}
allChans = append(allChans, edge.Capacity)
}
// Finally, if the out degree of this node is greater than what
// we've seen so far, update the maxChanOut variable.
if outDegree > maxChanOut {
maxChanOut = outDegree
}
return nil
})
stats, err := graph.NetworkStats(ctx)
if err != nil {
return nil, err
}
// Query the graph for the current number of zombie channels.
numZombies, err := graph.NumZombies()
if err != nil {
return nil, err
}
// Find the median.
medianChanSize = autopilot.Median(allChans)
// If we don't have any channels, then reset the minChannelSize to zero
// to avoid outputting NaN in encoded JSON.
if numChannels == 0 {
minChannelSize = 0
}
// Graph diameter.
channelGraph := autopilot.ChannelGraphFromCachedDatabase(graph)
simpleGraph, err := autopilot.NewSimpleGraph(channelGraph)
if err != nil {
return nil, err
}
start := time.Now()
diameter := simpleGraph.DiameterRadialCutoff()
rpcsLog.Infof("elapsed time for diameter (%d) calculation: %v", diameter,
time.Since(start))
// TODO(roasbeef): also add oldest channel?
netInfo := &lnrpc.NetworkInfo{
GraphDiameter: diameter,
MaxOutDegree: maxChanOut,
AvgOutDegree: float64(2*numChannels) / float64(numNodes),
NumNodes: numNodes,
NumChannels: numChannels,
TotalNetworkCapacity: int64(totalNetworkCapacity),
AvgChannelSize: float64(totalNetworkCapacity) / float64(numChannels),
MinChannelSize: int64(minChannelSize),
MaxChannelSize: int64(maxChannelSize),
MedianChannelSizeSat: int64(medianChanSize),
NumZombieChans: numZombies,
GraphDiameter: stats.Diameter,
MaxOutDegree: stats.MaxChanOut,
AvgOutDegree: float64(2*stats.NumChannels) /
float64(stats.NumNodes),
NumNodes: stats.NumNodes,
NumChannels: stats.NumChannels,
TotalNetworkCapacity: int64(stats.TotalNetworkCapacity),
AvgChannelSize: float64(stats.TotalNetworkCapacity) /
float64(stats.NumChannels),
MinChannelSize: int64(stats.MinChanSize),
MaxChannelSize: int64(stats.MaxChanSize),
MedianChannelSizeSat: int64(stats.MedianChanSize),
NumZombieChans: stats.NumZombies,
}
// Similarly, if we don't have any channels, then we'll also set the
// average channel size to zero in order to avoid weird JSON encoding
// outputs.
if numChannels == 0 {
if stats.NumChannels == 0 {
netInfo.AvgChannelSize = 0
}