Skip to content

Commit

Permalink
Merge pull request #34 from BitfuryLightning/light_138_showroutingtab…
Browse files Browse the repository at this point in the history
…le_final

Enhance showroutingtable RPC command, fix routing bugs
  • Loading branch information
Roasbeef authored Sep 7, 2016
2 parents d764493 + 2bcff18 commit a28c011
Show file tree
Hide file tree
Showing 18 changed files with 573 additions and 191 deletions.
294 changes: 289 additions & 5 deletions cmd/lncli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/lightningnetwork/lnd/lnrpc"
"github.com/roasbeef/btcd/wire"
"github.com/urfave/cli"
"golang.org/x/net/context"
"github.com/BitfuryLightning/tools/rt"
"github.com/BitfuryLightning/tools/prefix_tree"
"github.com/BitfuryLightning/tools/rt/graph"

"github.com/BitfuryLightning/tools/rt/visualizer"
)

// TODO(roasbeef): cli logic for supporting both positional and unix style
Expand Down Expand Up @@ -525,19 +532,296 @@ func sendPaymentCommand(ctx *cli.Context) error {
var ShowRoutingTableCommand = cli.Command{
Name: "showroutingtable",
Description: "shows routing table for a node",
Action: showRoutingTable,
Usage: "showroutingtable text|image",
Subcommands: []cli.Command{
{
Name: "text",
Usage: "[--table|--human]",
Description: "Show routing table in textual format. By default in JSON",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "table",
Usage: "Print channels in routing table in table format.",
},
cli.BoolFlag{
Name: "human",
Usage: "Print channels in routing table in table format. Output lightning_id partially - only a few first symbols which uniquelly identifies it.",
},
},
Action: showRoutingTableAsText,
},
{
Name: "image",
Usage: "[--type <IMAGE_TYPE>] [--dest OUTPUT_FILE] [--open]",
Description: "Create image with graphical representation of routing table",
Flags: []cli.Flag{
cli.StringFlag{
Name: "type",
Usage: "Type of image file. Use one of: http://www.graphviz.org/content/output-formats. Usage of this option supresses textual output",
},
cli.StringFlag{
Name: "dest",
Usage: "Specifies where to save the generated file. If don't specified use os.TempDir Usage of this option supresses textual output",
},
cli.BoolFlag{
Name: "open",
Usage: "Open generated file automatically. Uses command line \"open\" command",
},
},
Action: showRoutingTableAsImage,
},
},
}

func getRoutingTable(ctxb context.Context, client lnrpc.LightningClient) (*rt.RoutingTable, error) {
req := &lnrpc.ShowRoutingTableRequest{}
resp, err := client.ShowRoutingTable(ctxb, req)
if err != nil {
return nil, err
}

r := rt.NewRoutingTable()
for _, channel := range resp.Channels {
r.AddChannel(
graph.NewID(channel.Id1),
graph.NewID(channel.Id2),
graph.NewEdgeID(channel.Outpoint),
&rt.ChannelInfo{channel.Capacity, channel.Weight},
)
}
return r, nil
}

func showRoutingTable(ctx *cli.Context) error {
func showRoutingTableAsText(ctx *cli.Context) error {
ctxb := context.Background()
client := getClient(ctx)

req := &lnrpc.ShowRoutingTableRequest{}
resp, err := client.ShowRoutingTable(ctxb, req)
r, err := getRoutingTable(ctxb, client)
if err != nil{
return err
}

if ctx.Bool("table") && ctx.Bool("human"){
return fmt.Errorf("--table and --human cannot be used at the same time")
}

if ctx.Bool("table") {
printRTAsTable(r, false)
} else if ctx.Bool("human") {
printRTAsTable(r, true)
} else {
printRTAsJSON(r)
}
return nil
}

func showRoutingTableAsImage(ctx *cli.Context) error {
ctxb := context.Background()
client := getClient(ctx)

r, err := getRoutingTable(ctxb, client)
if err != nil{
return err
}

reqGetInfo := &lnrpc.GetInfoRequest{}
respGetInfo, err := client.GetInfo(ctxb, reqGetInfo)
if err != nil {
return err
}
selfLightningId, err := hex.DecodeString(respGetInfo.LightningId)
if err != nil {
return err
}

printRespJson(resp)
imgType := ctx.String("type")
imgDest := ctx.String("dest")
if imgType == "" && imgDest == "" {
return fmt.Errorf("One or both of --type or --dest should be specified")
}

tempFile, err := ioutil.TempFile("", "")
if err != nil {
return err
}
var imageFile *os.File
// if the type is not specified explicitly parse the filename
if imgType == "" {
imgType = filepath.Ext(imgDest)[1:]
}
// if the filename is not specified explicitly use tempfile
if imgDest == "" {
imageFile, err = TempFileWithSuffix("", "rt_", "."+ imgType)
if err != nil {
return err
}
} else {
imageFile, err = os.Create(imgDest)
if err != nil {
return err
}
}
if _, ok := visualizer.SupportedFormatsAsMap()[imgType]; !ok {
fmt.Printf("Format: '%v' not recognized. Use one of: %v\n", imgType, visualizer.SupportedFormats())
return nil
}
// generate description graph by dot language
err = writeToTempFile(r, tempFile, selfLightningId)
if err != nil {
return err
}
err = writeToImageFile(tempFile, imageFile)
if err != nil {
return err
}
if ctx.Bool("open") {
if err := visualizer.Open(imageFile); err != nil {
return err
}
}
return nil
}

func writeToTempFile(r *rt.RoutingTable, file *os.File, self []byte) error {
slc := []graph.ID{graph.NewID(string(self))}
viz := visualizer.New(r.G, slc, nil, nil)
viz.ApplyToNode = func(s string) string { return hex.EncodeToString([]byte(s)) }
viz.ApplyToEdge = func(info interface{}) string {
if info, ok := info.(*rt.ChannelInfo); ok {
return fmt.Sprintf(`"%v"`, info.Capacity())
}
return "nil"
}
// need to call method if plan to use shortcut, autocomplete, etc
viz.BuildPrefixTree()
viz.EnableShortcut(true)
dot := viz.Draw()
_, err := file.Write([]byte(dot))
if err != nil {
return err
}
err = file.Sync()
if err != nil {
return err
}
return nil
}

func writeToImageFile(TempFile, ImageFile *os.File) error {
err := visualizer.Run("neato", TempFile, ImageFile)
if err != nil {
return err
}
err = TempFile.Close()
if err != nil {
return err
}
err = os.Remove(TempFile.Name())
if err != nil {
return err
}
err = ImageFile.Close()
if err != nil {
return err
}
return nil
}

// get around a bug in the standard library, add suffix param
func TempFileWithSuffix(dir, prefix, suffix string) (*os.File, error) {
f, err := ioutil.TempFile(dir, prefix)
if err != nil {
return nil, err
}
defer os.Remove(f.Name())
f, err = os.Create(f.Name()+suffix)
return f, err
}

// Prints routing table in human readable table format
func printRTAsTable(r *rt.RoutingTable, humanForm bool) {
// Minimum length of data part to which name can be shortened
var minLen int
var tmpl string
var lightningIdTree, edgeIdTree prefix_tree.PrefixTree
if humanForm {
tmpl = "%-10v %-10v %-10v %-10v %-10v\n"
minLen = 6
} else {
tmpl = "%-64v %-64v %-66v %-10v %-10v\n"
minLen = 100
}
fmt.Printf(tmpl, "ID1", "ID2", "Outpoint", "Capacity", "Weight")
channels := r.AllChannels()
if humanForm {
// Generate prefix tree for shortcuts
lightningIdTree = prefix_tree.NewPrefixTree()
for _, node := range r.Nodes() {
lightningIdTree.Add(hex.EncodeToString([]byte(node.String())))
}
edgeIdTree = prefix_tree.NewPrefixTree()
for _, channel := range channels {
edgeIdTree.Add(channel.EdgeID.String())
}
}
for _, channel := range channels {
var source, target, edgeId string
sourceHex := hex.EncodeToString([]byte(channel.Id1.String()))
targetHex := hex.EncodeToString([]byte(channel.Id2.String()))
edgeIdRaw := channel.EdgeID.String()
if humanForm {
source = getShortcut(lightningIdTree, sourceHex, minLen)
target = getShortcut(lightningIdTree, targetHex, minLen)
edgeId = getShortcut(edgeIdTree, edgeIdRaw, minLen)
} else {
source = sourceHex
target = targetHex
edgeId = edgeIdRaw
}
fmt.Printf(tmpl, source, target, edgeId, channel.Info.Cpt, channel.Info.Wgt)
}
}

func getShortcut(tree prefix_tree.PrefixTree, s string, minLen int) string {
s1, err := tree.Shortcut(s)
if err != nil || s == s1 {
return s
}
if len(s1) < minLen && minLen < len(s) {
s1 = s[:minLen]
}
shortcut := fmt.Sprintf("%v...", s1)
if len(shortcut) >= len(s) {
shortcut = s
}
return shortcut
}

func printRTAsJSON(r *rt.RoutingTable) {
type ChannelDesc struct {
ID1 string `json:"lightning_id1"`
ID2 string `json:"lightning_id2"`
EdgeId string `json:"outpoint"`
Capacity int64 `json:"capacity"`
Weight float64 `json:"weight"`
}
var channels struct {
Channels []ChannelDesc `json:"channels"`
}
channelsRaw := r.AllChannels()
channels.Channels = make([]ChannelDesc, 0, len(channelsRaw))
for _, channelRaw := range channelsRaw {
sourceHex := hex.EncodeToString([]byte(channelRaw.Id1.String()))
targetHex := hex.EncodeToString([]byte(channelRaw.Id2.String()))
channels.Channels = append(channels.Channels,
ChannelDesc{
ID1: sourceHex,
ID2: targetHex,
EdgeId: channelRaw.EdgeID.String(),
Weight: channelRaw.Info.Weight(),
Capacity: channelRaw.Info.Capacity(),
},
)
}
printRespJson(channels)
}
12 changes: 5 additions & 7 deletions fundingmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,11 +584,10 @@ func (f *fundingManager) handleFundingSignComplete(fmsg *fundingSignCompleteMsg)
// so this new channel can be utilized during path
// finding.
chanInfo := openChan.StateSnapshot()
capacity := float64(chanInfo.Capacity)
fmsg.peer.server.routingMgr.AddChannel(
graph.NewID(fmsg.peer.server.lightningID),
capacity := int64(chanInfo.Capacity)
fmsg.peer.server.routingMgr.OpenChannel(
graph.NewID(chanInfo.RemoteID),
graph.NewEdgeID(fundingPoint.Hash.String()),
graph.NewEdgeID(fundingPoint.String()),
&rt.ChannelInfo{
Cpt: capacity,
},
Expand Down Expand Up @@ -655,10 +654,9 @@ func (f *fundingManager) handleFundingOpen(fmsg *fundingOpenMsg) {
resCtx.reservation.FundingOutpoint, fmsg.peer.id)

// Notify the L3 routing manager of the newly active channel link.
capacity := float64(resCtx.reservation.OurContribution().FundingAmount +
capacity := int64(resCtx.reservation.OurContribution().FundingAmount +
resCtx.reservation.TheirContribution().FundingAmount)
fmsg.peer.server.routingMgr.AddChannel(
graph.NewID(fmsg.peer.server.lightningID),
fmsg.peer.server.routingMgr.OpenChannel(
graph.NewID([32]byte(fmsg.peer.lightningID)),
graph.NewEdgeID(resCtx.reservation.FundingOutpoint().String()),
&rt.ChannelInfo{
Expand Down
Loading

0 comments on commit a28c011

Please sign in to comment.