mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-23 14:40:30 +01:00
Merge pull request #3865 from bhandras/betweenness_centrality
calculate betweenness centrality of nodes
This commit is contained in:
commit
03ff5961c6
11 changed files with 1766 additions and 783 deletions
265
autopilot/betweenness_centrality.go
Normal file
265
autopilot/betweenness_centrality.go
Normal file
|
@ -0,0 +1,265 @@
|
|||
package autopilot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// stack is a simple int stack to help with readability of Brandes'
|
||||
// betweenness centrality implementation below.
|
||||
type stack struct {
|
||||
stack []int
|
||||
}
|
||||
|
||||
func (s *stack) push(v int) {
|
||||
s.stack = append(s.stack, v)
|
||||
}
|
||||
|
||||
func (s *stack) top() int {
|
||||
return s.stack[len(s.stack)-1]
|
||||
}
|
||||
|
||||
func (s *stack) pop() {
|
||||
s.stack = s.stack[:len(s.stack)-1]
|
||||
}
|
||||
|
||||
func (s *stack) empty() bool {
|
||||
return len(s.stack) == 0
|
||||
}
|
||||
|
||||
// queue is a simple int queue to help with readability of Brandes'
|
||||
// betweenness centrality implementation below.
|
||||
type queue struct {
|
||||
queue []int
|
||||
}
|
||||
|
||||
func (q *queue) push(v int) {
|
||||
q.queue = append(q.queue, v)
|
||||
}
|
||||
|
||||
func (q *queue) front() int {
|
||||
return q.queue[0]
|
||||
}
|
||||
|
||||
func (q *queue) pop() {
|
||||
q.queue = q.queue[1:]
|
||||
}
|
||||
|
||||
func (q *queue) empty() bool {
|
||||
return len(q.queue) == 0
|
||||
}
|
||||
|
||||
// BetweennessCentrality is a NodeMetric that calculates node betweenness
|
||||
// centrality using Brandes' algorithm. Betweenness centrality for each node
|
||||
// is the number of shortest paths passing trough that node, not counting
|
||||
// shortest paths starting or ending at that node. This is a useful metric
|
||||
// to measure control of individual nodes over the whole network.
|
||||
type BetweennessCentrality struct {
|
||||
// workers number of goroutines are used to parallelize
|
||||
// centrality calculation.
|
||||
workers int
|
||||
|
||||
// centrality stores original (not normalized) centrality values for
|
||||
// each node in the graph.
|
||||
centrality map[NodeID]float64
|
||||
|
||||
// min is the minimum centrality in the graph.
|
||||
min float64
|
||||
|
||||
// max is the maximum centrality in the graph.
|
||||
max float64
|
||||
}
|
||||
|
||||
// NewBetweennessCentralityMetric creates a new BetweennessCentrality instance.
|
||||
// Users can specify the number of workers to use for calculating centrality.
|
||||
func NewBetweennessCentralityMetric(workers int) (*BetweennessCentrality, error) {
|
||||
// There should be at least one worker.
|
||||
if workers < 1 {
|
||||
return nil, fmt.Errorf("workers must be positive")
|
||||
}
|
||||
return &BetweennessCentrality{
|
||||
workers: workers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Name returns the name of the metric.
|
||||
func (bc *BetweennessCentrality) Name() string {
|
||||
return "betweeness_centrality"
|
||||
}
|
||||
|
||||
// betweennessCentrality is the core of Brandes' algorithm.
|
||||
// We first calculate the shortest paths from the start node s to all other
|
||||
// nodes with BFS, then update the betweenness centrality values by using
|
||||
// Brandes' dependency trick.
|
||||
// For detailed explanation please read:
|
||||
// https://www.cl.cam.ac.uk/teaching/1617/MLRD/handbook/brandes.html
|
||||
func betweennessCentrality(g *SimpleGraph, s int, centrality []float64) {
|
||||
// pred[w] is the list of nodes that immediately precede w on a
|
||||
// shortest path from s to t for each node t.
|
||||
pred := make([][]int, len(g.Nodes))
|
||||
|
||||
// sigma[t] is the number of shortest paths between nodes s and t for
|
||||
// each node t.
|
||||
sigma := make([]int, len(g.Nodes))
|
||||
sigma[s] = 1
|
||||
|
||||
// dist[t] holds the distance between s and t for each node t. We initialize
|
||||
// this to -1 (meaning infinity) for each t != s.
|
||||
dist := make([]int, len(g.Nodes))
|
||||
for i := range dist {
|
||||
dist[i] = -1
|
||||
}
|
||||
|
||||
dist[s] = 0
|
||||
|
||||
var (
|
||||
st stack
|
||||
q queue
|
||||
)
|
||||
q.push(s)
|
||||
|
||||
// BFS to calculate the shortest paths (sigma and pred)
|
||||
// from s to t for each node t.
|
||||
for !q.empty() {
|
||||
v := q.front()
|
||||
q.pop()
|
||||
st.push(v)
|
||||
|
||||
for _, w := range g.Adj[v] {
|
||||
// If distance from s to w is infinity (-1)
|
||||
// then set it and enqueue w.
|
||||
if dist[w] < 0 {
|
||||
dist[w] = dist[v] + 1
|
||||
q.push(w)
|
||||
}
|
||||
|
||||
// If w is on a shortest path the update
|
||||
// sigma and add v to w's predecessor list.
|
||||
if dist[w] == dist[v]+1 {
|
||||
sigma[w] += sigma[v]
|
||||
pred[w] = append(pred[w], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delta[v] is the ratio of the shortest paths between s and t that go
|
||||
// through v and the total number of shortest paths between s and t.
|
||||
// If we have delta then the betweenness centrality is simply the sum
|
||||
// of delta[w] for each w != s.
|
||||
delta := make([]float64, len(g.Nodes))
|
||||
|
||||
for !st.empty() {
|
||||
w := st.top()
|
||||
st.pop()
|
||||
|
||||
// pred[w] is the list of nodes that immediately precede w on a
|
||||
// shortest path from s.
|
||||
for _, v := range pred[w] {
|
||||
// Update delta using Brandes' equation.
|
||||
delta[v] += (float64(sigma[v]) / float64(sigma[w])) * (1.0 + delta[w])
|
||||
}
|
||||
|
||||
if w != s {
|
||||
// As noted above centrality is simply the sum
|
||||
// of delta[w] for each w != s.
|
||||
centrality[w] += delta[w]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh recaculates and stores centrality values.
|
||||
func (bc *BetweennessCentrality) Refresh(graph ChannelGraph) error {
|
||||
cache, err := NewSimpleGraph(graph)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
work := make(chan int)
|
||||
partials := make(chan []float64, bc.workers)
|
||||
|
||||
// Each worker will compute a partial result. This
|
||||
// partial result is a sum of centrality updates on
|
||||
// roughly N / workers nodes.
|
||||
worker := func() {
|
||||
defer wg.Done()
|
||||
partial := make([]float64, len(cache.Nodes))
|
||||
|
||||
// Consume the next node, update centrality
|
||||
// parital to avoid unnecessary synchronizaton.
|
||||
for node := range work {
|
||||
betweennessCentrality(cache, node, partial)
|
||||
}
|
||||
partials <- partial
|
||||
}
|
||||
|
||||
// Now start the N workers.
|
||||
wg.Add(bc.workers)
|
||||
for i := 0; i < bc.workers; i++ {
|
||||
go worker()
|
||||
}
|
||||
|
||||
// Distribute work amongst workers Should be
|
||||
// fair when graph is sufficiently large.
|
||||
for node := range cache.Nodes {
|
||||
work <- node
|
||||
}
|
||||
|
||||
close(work)
|
||||
wg.Wait()
|
||||
close(partials)
|
||||
|
||||
// Collect and sum partials for final result.
|
||||
centrality := make([]float64, len(cache.Nodes))
|
||||
for partial := range partials {
|
||||
for i := 0; i < len(partial); i++ {
|
||||
centrality[i] += partial[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Get min/max to be able to normalize
|
||||
// centrality values between 0 and 1.
|
||||
bc.min = 0
|
||||
bc.max = 0
|
||||
if len(centrality) > 0 {
|
||||
for _, v := range centrality {
|
||||
if v < bc.min {
|
||||
bc.min = v
|
||||
} else if v > bc.max {
|
||||
bc.max = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Divide by two as this is an undirected graph.
|
||||
bc.min /= 2.0
|
||||
bc.max /= 2.0
|
||||
|
||||
bc.centrality = make(map[NodeID]float64)
|
||||
for u, value := range centrality {
|
||||
// Divide by two as this is an undirected graph.
|
||||
bc.centrality[cache.Nodes[u]] = value / 2.0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMetric returns the current centrality values for each node indexed
|
||||
// by node id.
|
||||
func (bc *BetweennessCentrality) GetMetric(normalize bool) map[NodeID]float64 {
|
||||
// Normalization factor.
|
||||
var z float64
|
||||
if (bc.max - bc.min) > 0 {
|
||||
z = 1.0 / (bc.max - bc.min)
|
||||
}
|
||||
|
||||
centrality := make(map[NodeID]float64)
|
||||
for k, v := range bc.centrality {
|
||||
if normalize {
|
||||
v = (v - bc.min) * z
|
||||
}
|
||||
centrality[k] = v
|
||||
}
|
||||
|
||||
return centrality
|
||||
}
|
187
autopilot/betweenness_centrality_test.go
Normal file
187
autopilot/betweenness_centrality_test.go
Normal file
|
@ -0,0 +1,187 @@
|
|||
package autopilot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcutil"
|
||||
)
|
||||
|
||||
func TestBetweennessCentralityMetricConstruction(t *testing.T) {
|
||||
failing := []int{-1, 0}
|
||||
ok := []int{1, 10}
|
||||
|
||||
for _, workers := range failing {
|
||||
m, err := NewBetweennessCentralityMetric(workers)
|
||||
if m != nil || err == nil {
|
||||
t.Fatalf("construction must fail with <= 0 workers")
|
||||
}
|
||||
}
|
||||
|
||||
for _, workers := range ok {
|
||||
m, err := NewBetweennessCentralityMetric(workers)
|
||||
if m == nil || err != nil {
|
||||
t.Fatalf("construction must succeed with >= 1 workers")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that empty graph results in empty centrality result.
|
||||
func TestBetweennessCentralityEmptyGraph(t *testing.T) {
|
||||
centralityMetric, err := NewBetweennessCentralityMetric(1)
|
||||
if err != nil {
|
||||
t.Fatalf("construction must succeed with positive number of workers")
|
||||
}
|
||||
|
||||
for _, chanGraph := range chanGraphs {
|
||||
graph, cleanup, err := chanGraph.genFunc()
|
||||
success := t.Run(chanGraph.name, func(t1 *testing.T) {
|
||||
if err != nil {
|
||||
t1.Fatalf("unable to create graph: %v", err)
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
if err := centralityMetric.Refresh(graph); err != nil {
|
||||
t.Fatalf("unexpected failure during metric refresh: %v", err)
|
||||
}
|
||||
|
||||
centrality := centralityMetric.GetMetric(false)
|
||||
if len(centrality) > 0 {
|
||||
t.Fatalf("expected empty metric, got: %v", len(centrality))
|
||||
}
|
||||
|
||||
centrality = centralityMetric.GetMetric(true)
|
||||
if len(centrality) > 0 {
|
||||
t.Fatalf("expected empty metric, got: %v", len(centrality))
|
||||
}
|
||||
|
||||
})
|
||||
if !success {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testGraphDesc is a helper type to describe a test graph.
|
||||
type testGraphDesc struct {
|
||||
nodes int
|
||||
edges map[int][]int
|
||||
}
|
||||
|
||||
// buildTestGraph builds a test graph from a passed graph desriptor.
|
||||
func buildTestGraph(t *testing.T,
|
||||
graph testGraph, desc testGraphDesc) map[int]*btcec.PublicKey {
|
||||
|
||||
nodes := make(map[int]*btcec.PublicKey)
|
||||
|
||||
for i := 0; i < desc.nodes; i++ {
|
||||
key, err := graph.addRandNode()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create random node")
|
||||
}
|
||||
|
||||
nodes[i] = key
|
||||
}
|
||||
|
||||
const chanCapacity = btcutil.SatoshiPerBitcoin
|
||||
for u, neighbors := range desc.edges {
|
||||
for _, v := range neighbors {
|
||||
_, _, err := graph.addRandChannel(nodes[u], nodes[v], chanCapacity)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error while adding random channel: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// Test betweenness centrality calculating using an example graph.
|
||||
func TestBetweennessCentralityWithNonEmptyGraph(t *testing.T) {
|
||||
graphDesc := testGraphDesc{
|
||||
nodes: 9,
|
||||
edges: map[int][]int{
|
||||
0: {1, 2, 3},
|
||||
1: {2},
|
||||
2: {3},
|
||||
3: {4, 5},
|
||||
4: {5, 6, 7},
|
||||
5: {6, 7},
|
||||
6: {7, 8},
|
||||
},
|
||||
}
|
||||
|
||||
workers := []int{1, 3, 9, 100}
|
||||
|
||||
results := []struct {
|
||||
normalize bool
|
||||
centrality []float64
|
||||
}{
|
||||
{
|
||||
normalize: true,
|
||||
centrality: []float64{
|
||||
0.2, 0.0, 0.2, 1.0, 0.4, 0.4, 7.0 / 15.0, 0.0, 0.0,
|
||||
},
|
||||
},
|
||||
{
|
||||
normalize: false,
|
||||
centrality: []float64{
|
||||
3.0, 0.0, 3.0, 15.0, 6.0, 6.0, 7.0, 0.0, 0.0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, numWorkers := range workers {
|
||||
for _, chanGraph := range chanGraphs {
|
||||
numWorkers := numWorkers
|
||||
graph, cleanup, err := chanGraph.genFunc()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create graph: %v", err)
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
testName := fmt.Sprintf("%v %d workers", chanGraph.name, numWorkers)
|
||||
success := t.Run(testName, func(t1 *testing.T) {
|
||||
centralityMetric, err := NewBetweennessCentralityMetric(numWorkers)
|
||||
if err != nil {
|
||||
t.Fatalf("construction must succeed with positive number of workers")
|
||||
}
|
||||
|
||||
graphNodes := buildTestGraph(t1, graph, graphDesc)
|
||||
if err := centralityMetric.Refresh(graph); err != nil {
|
||||
t1.Fatalf("error while calculating betweeness centrality")
|
||||
}
|
||||
for _, expected := range results {
|
||||
expected := expected
|
||||
centrality := centralityMetric.GetMetric(expected.normalize)
|
||||
|
||||
if len(centrality) != graphDesc.nodes {
|
||||
t.Fatalf("expected %v values, got: %v",
|
||||
graphDesc.nodes, len(centrality))
|
||||
}
|
||||
|
||||
for node, nodeCentrality := range expected.centrality {
|
||||
nodeID := NewNodeID(graphNodes[node])
|
||||
calculatedCentrality, ok := centrality[nodeID]
|
||||
if !ok {
|
||||
t1.Fatalf("no result for node: %x (%v)", nodeID, node)
|
||||
}
|
||||
|
||||
if nodeCentrality != calculatedCentrality {
|
||||
t1.Errorf("centrality for node: %v should be %v, got: %v",
|
||||
node, nodeCentrality, calculatedCentrality)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if !success {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -147,6 +147,23 @@ type AttachmentHeuristic interface {
|
|||
map[NodeID]*NodeScore, error)
|
||||
}
|
||||
|
||||
// NodeMetric is a common interface for all graph metrics that are not
|
||||
// directly used as autopilot node scores but may be used in compositional
|
||||
// heuristics or statistical information exposed to users.
|
||||
type NodeMetric interface {
|
||||
// Name returns the unique name of this metric.
|
||||
Name() string
|
||||
|
||||
// Refresh refreshes the metric values based on the current graph.
|
||||
Refresh(graph ChannelGraph) error
|
||||
|
||||
// GetMetric returns the latest value of this metric. Values in the
|
||||
// map are per node and can be in arbitrary domain. If normalize is
|
||||
// set to true, then the returned values are normalized to either
|
||||
// [0, 1] or [-1, 1] depending on the metric.
|
||||
GetMetric(normalize bool) map[NodeID]float64
|
||||
}
|
||||
|
||||
// ScoreSettable is an interface that indicates that the scores returned by the
|
||||
// heuristic can be mutated by an external caller. The ExternalScoreAttachment
|
||||
// currently implements this interface, and so should any heuristic that is
|
||||
|
|
66
autopilot/simple_graph.go
Normal file
66
autopilot/simple_graph.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package autopilot
|
||||
|
||||
// SimpleGraph stores a simplifed adj graph of a channel graph to speed
|
||||
// up graph processing by eliminating all unnecessary hashing and map access.
|
||||
type SimpleGraph struct {
|
||||
// Nodes is a map from node index to NodeID.
|
||||
Nodes []NodeID
|
||||
|
||||
// Adj stores nodes and neighbors in an adjacency list.
|
||||
Adj [][]int
|
||||
}
|
||||
|
||||
// NewSimpleGraph creates a simplified graph from the current channel graph.
|
||||
// Returns an error if the channel graph iteration fails due to underlying
|
||||
// failure.
|
||||
func NewSimpleGraph(g ChannelGraph) (*SimpleGraph, error) {
|
||||
nodes := make(map[NodeID]int)
|
||||
adj := make(map[int][]int)
|
||||
nextIndex := 0
|
||||
|
||||
// getNodeIndex returns the integer index of the passed node.
|
||||
// The returned index is then used to create a simplifed adjacency list
|
||||
// where each node is identified by its index instead of its pubkey, and
|
||||
// also to create a mapping from node index to node pubkey.
|
||||
getNodeIndex := func(node Node) int {
|
||||
key := NodeID(node.PubKey())
|
||||
nodeIndex, ok := nodes[key]
|
||||
|
||||
if !ok {
|
||||
nodes[key] = nextIndex
|
||||
nodeIndex = nextIndex
|
||||
nextIndex++
|
||||
}
|
||||
|
||||
return nodeIndex
|
||||
}
|
||||
|
||||
// Iterate over each node and each channel and update the adj and the node
|
||||
// index.
|
||||
err := g.ForEachNode(func(node Node) error {
|
||||
u := getNodeIndex(node)
|
||||
|
||||
return node.ForEachChannel(func(edge ChannelEdge) error {
|
||||
v := getNodeIndex(edge.Peer)
|
||||
|
||||
adj[u] = append(adj[u], v)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
graph := &SimpleGraph{
|
||||
Nodes: make([]NodeID, len(nodes)),
|
||||
Adj: make([][]int, len(nodes)),
|
||||
}
|
||||
|
||||
// Fill the adj and the node index to node pubkey mapping.
|
||||
for nodeID, nodeIndex := range nodes {
|
||||
graph.Adj[nodeIndex] = adj[nodeIndex]
|
||||
graph.Nodes[nodeIndex] = nodeID
|
||||
}
|
||||
|
||||
return graph, nil
|
||||
}
|
|
@ -2942,7 +2942,7 @@ func listInvoices(ctx *cli.Context) error {
|
|||
|
||||
var describeGraphCommand = cli.Command{
|
||||
Name: "describegraph",
|
||||
Category: "Peers",
|
||||
Category: "Graph",
|
||||
Description: "Prints a human readable version of the known channel " +
|
||||
"graph from the PoV of the node",
|
||||
Usage: "Describe the network graph.",
|
||||
|
@ -2974,6 +2974,31 @@ func describeGraph(ctx *cli.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
var getNodeMetricsCommand = cli.Command{
|
||||
Name: "getnodemetrics",
|
||||
Category: "Graph",
|
||||
Description: "Prints out node metrics calculated from the current graph",
|
||||
Usage: "Get node metrics.",
|
||||
Action: actionDecorator(getNodeMetrics),
|
||||
}
|
||||
|
||||
func getNodeMetrics(ctx *cli.Context) error {
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &lnrpc.NodeMetricsRequest{
|
||||
Types: []lnrpc.NodeMetricType{lnrpc.NodeMetricType_BETWEENNESS_CENTRALITY},
|
||||
}
|
||||
|
||||
nodeMetrics, err := client.GetNodeMetrics(context.Background(), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(nodeMetrics)
|
||||
return nil
|
||||
}
|
||||
|
||||
var listPaymentsCommand = cli.Command{
|
||||
Name: "listpayments",
|
||||
Category: "Payments",
|
||||
|
@ -3006,7 +3031,7 @@ func listPayments(ctx *cli.Context) error {
|
|||
|
||||
var getChanInfoCommand = cli.Command{
|
||||
Name: "getchaninfo",
|
||||
Category: "Channels",
|
||||
Category: "Graph",
|
||||
Usage: "Get the state of a channel.",
|
||||
Description: "Prints out the latest authenticated state for a " +
|
||||
"particular channel",
|
||||
|
@ -3057,7 +3082,7 @@ func getChanInfo(ctx *cli.Context) error {
|
|||
|
||||
var getNodeInfoCommand = cli.Command{
|
||||
Name: "getnodeinfo",
|
||||
Category: "Peers",
|
||||
Category: "Graph",
|
||||
Usage: "Get information on a specific node.",
|
||||
Description: "Prints out the latest authenticated node state for an " +
|
||||
"advertised node",
|
||||
|
|
|
@ -282,6 +282,7 @@ func main() {
|
|||
closedChannelsCommand,
|
||||
listPaymentsCommand,
|
||||
describeGraphCommand,
|
||||
getNodeMetricsCommand,
|
||||
getChanInfoCommand,
|
||||
getNodeInfoCommand,
|
||||
queryRoutesCommand,
|
||||
|
|
1768
lnrpc/rpc.pb.go
1768
lnrpc/rpc.pb.go
File diff suppressed because it is too large
Load diff
|
@ -653,6 +653,23 @@ func request_Lightning_DescribeGraph_0(ctx context.Context, marshaler runtime.Ma
|
|||
|
||||
}
|
||||
|
||||
var (
|
||||
filter_Lightning_GetNodeMetrics_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
)
|
||||
|
||||
func request_Lightning_GetNodeMetrics_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq NodeMetricsRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_Lightning_GetNodeMetrics_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.GetNodeMetrics(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_Lightning_GetChanInfo_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq ChanInfoRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
@ -1678,6 +1695,26 @@ func RegisterLightningHandlerClient(ctx context.Context, mux *runtime.ServeMux,
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Lightning_GetNodeMetrics_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Lightning_GetNodeMetrics_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Lightning_GetNodeMetrics_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Lightning_GetChanInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
|
@ -1980,6 +2017,8 @@ var (
|
|||
|
||||
pattern_Lightning_DescribeGraph_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "graph"}, ""))
|
||||
|
||||
pattern_Lightning_GetNodeMetrics_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "graph", "nodemetrics"}, ""))
|
||||
|
||||
pattern_Lightning_GetChanInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "graph", "edge", "chan_id"}, ""))
|
||||
|
||||
pattern_Lightning_GetNodeInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "graph", "node", "pub_key"}, ""))
|
||||
|
@ -2064,6 +2103,8 @@ var (
|
|||
|
||||
forward_Lightning_DescribeGraph_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Lightning_GetNodeMetrics_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Lightning_GetChanInfo_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Lightning_GetNodeInfo_0 = runtime.ForwardResponseMessage
|
||||
|
|
|
@ -641,6 +641,17 @@ service Lightning {
|
|||
};
|
||||
}
|
||||
|
||||
/** lncli: `getnodemetrics`
|
||||
GetNodeMetrics returns node metrics calculated from the graph. Currently
|
||||
the only supported metric is betweenness centrality of individual nodes.
|
||||
*/
|
||||
rpc GetNodeMetrics (NodeMetricsRequest) returns (NodeMetricsResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/v1/graph/nodemetrics"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/** lncli: `getchaninfo`
|
||||
GetChanInfo returns the latest authenticated network announcement for the
|
||||
given channel identified by its channel ID: an 8-byte integer which
|
||||
|
@ -2516,6 +2527,34 @@ message ChannelGraph {
|
|||
repeated ChannelEdge edges = 2;
|
||||
}
|
||||
|
||||
enum NodeMetricType {
|
||||
BETWEENNESS_CENTRALITY = 0;
|
||||
}
|
||||
|
||||
message NodeMetricsRequest {
|
||||
/// The requesteded node metrics.
|
||||
repeated NodeMetricType types = 1;
|
||||
}
|
||||
|
||||
message NodeMetricsResponse {
|
||||
/**
|
||||
Betweenness centrality is the sum of the ratio of shortest paths that pass
|
||||
through the node for each pair of nodes in the graph (not counting paths
|
||||
starting or ending at this node).
|
||||
Map of node pubkey to betweenness centrality of the node. Normalized
|
||||
values are in the [0,1] closed interval.
|
||||
*/
|
||||
map<string, FloatValue> betweenness_centrality = 1;
|
||||
}
|
||||
|
||||
message FloatValue {
|
||||
/// Arbitrary float value.
|
||||
double value = 1;
|
||||
|
||||
/// The value normalized to [0,1] or [-1,1].
|
||||
double normalized_value = 2;
|
||||
}
|
||||
|
||||
message ChanInfoRequest {
|
||||
/**
|
||||
The unique channel ID for the channel. The first 3 bytes are the block
|
||||
|
|
|
@ -718,6 +718,39 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/v1/graph/nodemetrics": {
|
||||
"get": {
|
||||
"summary": "* lncli: `getnodemetrics`\nGetNodeMetrics returns node metrics calculated from the graph. Currently\nthe only supported metric is betweenness centrality of individual nodes.",
|
||||
"operationId": "GetNodeMetrics",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/lnrpcNodeMetricsResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "types",
|
||||
"description": "/ The requesteded node metrics.",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"BETWEENNESS_CENTRALITY"
|
||||
]
|
||||
},
|
||||
"collectionFormat": "multi"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Lightning"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/graph/routes/{pub_key}/{amt}": {
|
||||
"get": {
|
||||
"summary": "* lncli: `queryroutes`\nQueryRoutes attempts to query the daemon's Channel Router for a possible\nroute to a target destination capable of carrying a specific amount of\nsatoshis. The returned route contains the full details required to craft and\nsend an HTLC, also including the necessary information that should be\npresent within the Sphinx packet encapsulated within the HTLC.",
|
||||
|
@ -2684,6 +2717,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"lnrpcFloatValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "/ Arbitrary float value."
|
||||
},
|
||||
"normalized_value": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "/ The value normalized to [0,1] or [-1,1]."
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcForwardingEvent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -3574,6 +3622,25 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"lnrpcNodeMetricType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"BETWEENNESS_CENTRALITY"
|
||||
],
|
||||
"default": "BETWEENNESS_CENTRALITY"
|
||||
},
|
||||
"lnrpcNodeMetricsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"betweenness_centrality": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/lnrpcFloatValue"
|
||||
},
|
||||
"description": "*\nBetweenness centrality is the sum of the ratio of shortest paths that pass\nthrough the node for each pair of nodes in the graph (not counting paths\nstarting or ending at this node).\nMap of node pubkey to betweenness centrality of the node. Normalized\nvalues are in the [0,1] closed interval."
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcNodePair": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
67
rpcserver.go
67
rpcserver.go
|
@ -10,6 +10,7 @@ import (
|
|||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -354,6 +355,10 @@ func mainRPCServerPermissions() map[string][]bakery.Op {
|
|||
Entity: "info",
|
||||
Action: "read",
|
||||
}},
|
||||
"/lnrpc.Lightning/GetNodeMetrics": {{
|
||||
Entity: "info",
|
||||
Action: "read",
|
||||
}},
|
||||
"/lnrpc.Lightning/GetChanInfo": {{
|
||||
Entity: "info",
|
||||
Action: "read",
|
||||
|
@ -4561,14 +4566,16 @@ func (r *rpcServer) DescribeGraph(ctx context.Context,
|
|||
nodeAddrs = append(nodeAddrs, nodeAddr)
|
||||
}
|
||||
|
||||
resp.Nodes = append(resp.Nodes, &lnrpc.LightningNode{
|
||||
lnNode := &lnrpc.LightningNode{
|
||||
LastUpdate: uint32(node.LastUpdate.Unix()),
|
||||
PubKey: hex.EncodeToString(node.PubKeyBytes[:]),
|
||||
Addresses: nodeAddrs,
|
||||
Alias: node.Alias,
|
||||
Color: routing.EncodeHexColor(node.Color),
|
||||
Features: invoicesrpc.CreateRPCFeatures(node.Features),
|
||||
})
|
||||
}
|
||||
|
||||
resp.Nodes = append(resp.Nodes, lnNode)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
@ -4657,6 +4664,62 @@ func marshalDbEdge(edgeInfo *channeldb.ChannelEdgeInfo,
|
|||
return edge
|
||||
}
|
||||
|
||||
// GetNodeMetrics returns all available node metrics calculated from the
|
||||
// current channel graph.
|
||||
func (r *rpcServer) GetNodeMetrics(ctx context.Context,
|
||||
req *lnrpc.NodeMetricsRequest) (*lnrpc.NodeMetricsResponse, error) {
|
||||
|
||||
// Get requested metric types.
|
||||
getCentrality := false
|
||||
for _, t := range req.Types {
|
||||
if t == lnrpc.NodeMetricType_BETWEENNESS_CENTRALITY {
|
||||
getCentrality = true
|
||||
}
|
||||
}
|
||||
|
||||
// Only centrality can be requested for now.
|
||||
if !getCentrality {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
resp := &lnrpc.NodeMetricsResponse{
|
||||
BetweennessCentrality: make(map[string]*lnrpc.FloatValue),
|
||||
}
|
||||
|
||||
// Obtain the pointer to the global singleton channel graph, this will
|
||||
// provide a consistent view of the graph due to bolt db's
|
||||
// transactional model.
|
||||
graph := r.server.chanDB.ChannelGraph()
|
||||
|
||||
// Calculate betweenness centrality if requested. Note that depending on the
|
||||
// graph size, this may take up to a few minutes.
|
||||
channelGraph := autopilot.ChannelGraphFromDatabase(graph)
|
||||
centralityMetric, err := autopilot.NewBetweennessCentralityMetric(
|
||||
runtime.NumCPU(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := centralityMetric.Refresh(channelGraph); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Fill normalized and non normalized centrality.
|
||||
centrality := centralityMetric.GetMetric(true)
|
||||
for nodeID, val := range centrality {
|
||||
resp.BetweennessCentrality[hex.EncodeToString(nodeID[:])] = &lnrpc.FloatValue{
|
||||
NormalizedValue: val,
|
||||
}
|
||||
}
|
||||
|
||||
centrality = centralityMetric.GetMetric(false)
|
||||
for nodeID, val := range centrality {
|
||||
resp.BetweennessCentrality[hex.EncodeToString(nodeID[:])].Value = val
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetChanInfo returns the latest authenticated network announcement for the
|
||||
// given channel identified by its channel ID: an 8-byte integer which uniquely
|
||||
// identifies the location of transaction's funding output within the block
|
||||
|
|
Loading…
Add table
Reference in a new issue