From 3ec4eeb5e24ae9a94b2110b7d5c0777a76a6faec Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 23 Jan 2017 20:32:17 -0800 Subject: [PATCH] cmd/lncli: add ability to render graph to 'describegraph' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds an ability to render the channel graph as returned by the ‘displaygraph’ command. The rendering of the graph itself is carried about the by the ‘dot’ command which eventually calls out to graphviz. Currently the graph is always saved to the same file in the local directory, but in a later commit the location of the file will be made configurable. Finally, the attributes sent to the ‘dot’ command used to render the graph are still a bit in flux. The parameters will likely be tuned once the channel graph on testnet grows a bit more. --- cmd/lncli/commands.go | 117 +++++++++++++++++++++++++++++++++++++++++- glide.lock | 13 +++-- glide.yaml | 3 +- 3 files changed, 127 insertions(+), 6 deletions(-) diff --git a/cmd/lncli/commands.go b/cmd/lncli/commands.go index 15d86c49..5d7fcd0e 100644 --- a/cmd/lncli/commands.go +++ b/cmd/lncli/commands.go @@ -6,11 +6,16 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "os" + "os/exec" + "strconv" "strings" + "github.com/awalterschulze/gographviz" "github.com/lightningnetwork/lnd/lnrpc" "github.com/roasbeef/btcd/chaincfg/chainhash" + "github.com/roasbeef/btcutil" "github.com/urfave/cli" "golang.org/x/net/context" ) @@ -762,7 +767,13 @@ var DescribeGraphCommand = cli.Command{ Name: "describegraph", Description: "prints a human readable version of the known channel " + "graph from the PoV of the node", - Usage: "describegraph", + Usage: "describegraph", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "draw", + Usage: "If true, then an image of graph will be generated and displayed. The generated image is stored within the current directory with a file name of 'graph.png'", + }, + }, Action: describeGraph, } @@ -776,10 +787,114 @@ func describeGraph(ctx *cli.Context) error { return err } + // If the draw flag is on, then we'll use the 'dot' command to create a + // visualization of the graph itself. + if ctx.Bool("draw") { + return drawChannelGraph(graph) + } + printRespJson(graph) return nil } +func drawChannelGraph(graph *lnrpc.ChannelGraph) error { + // First we'll create a temporary file that we'll write the compiled + // string that describes our graph in the dot format to. + tempDotFile, err := ioutil.TempFile("", "") + if err != nil { + return err + } + defer os.Remove(tempDotFile.Name()) + + // Next, we'll create (or re-create) the file that the final graph + // image will be written to. + imageFile, err := os.Create("graph.png") + if err != nil { + return err + } + + // With our temporary files set up, we'll initialize the graphviz + // object that we'll use to draw our graph. + graphName := "LightningNetwork" + graphCanvas := gographviz.NewGraph() + graphCanvas.SetName(graphName) + graphCanvas.SetDir(true) + + // For each node within the graph, we'll add a new vertex to the graph. + for _, node := range graph.Nodes { + // Rather than using the entire hex-encoded string, we'll only + // use the first 10 characters. We also add a prefix of "Z" as + // graphviz is unable to parse the compressed pubkey as a + // non-integer. + // + // TODO(roasbeef): should be able to get around this? + nodeID := "Z" + node.PubKey[:10] + + graphCanvas.AddNode(graphName, nodeID, gographviz.Attrs{}) + } + + // Similarly, for each edge we'll add an edge between the corresponding + // nodes added to the graph above. + for _, edge := range graph.Edges { + // Once again, we add a 'Z' prefix so we're compliant with the + // dot grammar. + src := "Z" + edge.Node1Pub[:10] + dest := "Z" + edge.Node2Pub[:10] + + // The weight for our edge will be the total capacity of the + // channel, in BTC. + amt := btcutil.Amount(edge.Capacity).ToBTC() + edgeWeight := strconv.FormatFloat(amt, 'f', -1, 64) + + // The label for each edge will simply be a truncated version + // of it's channel ID. + edgeLabel := strconv.FormatUint(edge.ChannelId, 10)[:10] + + // We'll only draw an edge from a source to a destination if + // the source is currently advertising a route in that + // direction. + if edge.Node1Policy.TimeLockDelta != 0 { + graphCanvas.AddEdge(src, dest, true, gographviz.Attrs{ + "weight": edgeWeight, + "label": edgeLabel, + }) + } + if edge.Node2Policy.TimeLockDelta != 0 { + graphCanvas.AddEdge(dest, src, true, gographviz.Attrs{ + "weight": edgeWeight, + "label": edgeLabel, + }) + } + } + + // With the declarative generation of the graph complete, we now write + // the dot-string description of the graph + graphDotString := graphCanvas.String() + if _, err := tempDotFile.WriteString(graphDotString); err != nil { + return err + } + if err := tempDotFile.Sync(); err != nil { + return err + } + + // Once our dot file has been written to disk, we can use the dot + // command itself to generate the drawn rendering of the graph + // described. + drawCmd := exec.Command("dot", "-T"+"png", "-o"+imageFile.Name(), + tempDotFile.Name()) + if err := drawCmd.Run(); err != nil { + return err + } + + // Finally, we'll open the drawn graph to display to the user. + openCmd := exec.Command("open", imageFile.Name()) + if err := openCmd.Run(); err != nil { + return err + } + + return nil +} + var ListPaymentsCommand = cli.Command{ Name: "listpayments", Usage: "listpayments", diff --git a/glide.lock b/glide.lock index 6e434e63..401f899d 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 63c8e025e68860690ecd8c0f5d6a2ce0c20aae601f23add3cb41d702f07c2980 -updated: 2017-01-16T17:29:52.63308745-08:00 +hash: a292fd48b31862c25e68915190bd53af1c2a5138151e45a38ffa7f192b8456ac +updated: 2017-01-23T20:26:07.380822159-08:00 imports: - name: github.com/aead/chacha20 version: 7e1038a97ad08a9a16cb88ed7a6778b366ba4d99 @@ -7,6 +7,13 @@ imports: - chacha - name: github.com/aead/poly1305 version: 7ab7663051fa9fa57de391f966a859e781d48b0a +- name: github.com/awalterschulze/gographviz + version: 761fd5fbb34e4c2c138c280395b65b48e4ff5a53 + subpackages: + - ast + - parser + - scanner + - token - name: github.com/boltdb/bolt version: 583e8937c61f1af6513608ccc75c97b6abdf4ff9 - name: github.com/btcsuite/bolt @@ -37,8 +44,6 @@ imports: version: 31079b6807923eb23992c421b114992b95131b55 - name: github.com/codahale/chacha20 version: ec07b4f69a3f70b1dd2a8ad77230deb1ba5d6953 -- name: github.com/codahale/chacha20poly1305 - version: f8a5c48301822c3d7dd26d78e68ea2968db0ab20 - name: github.com/davecgh/go-spew version: 346938d642f2ec3594ed81d874461961cd0faa76 subpackages: diff --git a/glide.yaml b/glide.yaml index 04cd6919..34a010f1 100644 --- a/glide.yaml +++ b/glide.yaml @@ -7,7 +7,6 @@ import: - package: github.com/btcsuite/go-flags - package: github.com/btcsuite/seelog version: ^2.1.0 -- package: github.com/codahale/chacha20poly1305 - package: github.com/davecgh/go-spew subpackages: - spew @@ -61,3 +60,5 @@ import: - package: github.com/aead/chacha20 - package: github.com/go-errors/errors - package: github.com/tv42/zbase32 +- package: github.com/awalterschulze/gographviz + version: ^1.0.0