Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2/3] Graph RIP: multi: Graph Source Abstraction #9243

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
488fa3e
graph: remove unused ForEachNode method
ellemouton Nov 8, 2024
9d389ad
graph: let FetchNodeFeatures take an optional read tx
ellemouton Nov 8, 2024
755065b
graph: rename directory from graphsession to session
ellemouton Nov 13, 2024
6c008ff
lnd+graph: add GraphSource interface and implementation
ellemouton Nov 11, 2024
aa24804
graph: add ReadOnlyGraph interface to GraphSource interface
ellemouton Nov 11, 2024
9854bad
graph: add contexts to the ReadOnlyGraph interface
ellemouton Nov 11, 2024
6f3d45f
invoicesrpc: remove invoicerpc server's access to ChannelGraph pointer
ellemouton Nov 11, 2024
237151d
netann+lnd: add netann.ChannelGraph to the GraphSource interface
ellemouton Nov 11, 2024
bfe6262
graph+channeldb: add AddrSource interface to GraphSource
ellemouton Nov 12, 2024
28415f5
graph+lnd: add various calls to GraphSource
ellemouton Nov 12, 2024
0f33d41
discovery: pass contexts to NetworkPeerBootstrapper methods
ellemouton Nov 12, 2024
372883a
lnd+graph: add GraphBootstrapper to the GraphSource interface
ellemouton Nov 12, 2024
8007061
graph+lnd: add NetworkStats to GraphSource interface
ellemouton Nov 12, 2024
f36fbd0
graph+lnd: add BetweennessCentrality to GraphSource interface
ellemouton Nov 12, 2024
2192bf4
lnd+chanbackup: thread contexts through
ellemouton Nov 13, 2024
dcfffd6
invoicesrpc: remove a context.TODO
ellemouton Nov 13, 2024
75b1069
blindedpath: remove a context.TODO
ellemouton Nov 13, 2024
a28102e
netann: remove context.TODO
ellemouton Nov 11, 2024
c5cc6f1
routing: remove context.TODOs
ellemouton Nov 11, 2024
791ac91
remove context.TODOs from tests
ellemouton Nov 11, 2024
15c2161
docs: update release notes
ellemouton Nov 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions graph/db/models/stats.go
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively stats could jus be implemented locally using a GraphSource.

// 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
}
129 changes: 129 additions & 0 deletions graph/sources/chan_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions graph/sources/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 31 additions & 0 deletions graph/sources/log.go
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions log.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
132 changes: 16 additions & 116 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

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
})
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 := r.server.graphSource

// Graph diameter.
channelGraph := autopilot.ChannelGraphFromCachedDatabase(graph)
simpleGraph, err := autopilot.NewSimpleGraph(channelGraph)
stats, err := graph.NetworkStats(ctx)
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
}

Expand Down