diff --git a/graph/db/models/stats.go b/graph/db/models/stats.go new file mode 100644 index 000000000..833770bed --- /dev/null +++ b/graph/db/models/stats.go @@ -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 +} diff --git a/graph/sources/chan_graph.go b/graph/sources/chan_graph.go index f5a880323..5944b0221 100644 --- a/graph/sources/chan_graph.go +++ b/graph/sources/chan_graph.go @@ -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 { diff --git a/graph/sources/interfaces.go b/graph/sources/interfaces.go index 44fe79fce..2d483c2ea 100644 --- a/graph/sources/interfaces.go +++ b/graph/sources/interfaces.go @@ -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) } diff --git a/graph/sources/log.go b/graph/sources/log.go new file mode 100644 index 000000000..b05d38928 --- /dev/null +++ b/graph/sources/log.go @@ -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 +} diff --git a/log.go b/log.go index 795fb4d72..720343913 100644 --- a/log.go +++ b/log.go @@ -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 diff --git a/rpcserver.go b/rpcserver.go index b702a053f..4c56546e1 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -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 }