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

Enhance lncli listchannels command with the chan_id and short_chan_id (human readable format) #9390

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
105 changes: 105 additions & 0 deletions cmd/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/jessevdk/go-flags"
"github.com/lightningnetwork/lnd"
NishantBansal2003 marked this conversation as resolved.
Show resolved Hide resolved
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/signal"
Expand Down Expand Up @@ -50,6 +51,14 @@ var (
customDataPattern = regexp.MustCompile(
`"custom_channel_data":\s*"([0-9a-f]+)"`,
)

chanIDPattern = regexp.MustCompile(
`"chan_id":\s*"(\d+)"`,
)

channelPointPattern = regexp.MustCompile(
`"channel_point":\s*"([0-9a-fA-F]+:[0-9]+)"`,
)
)

// replaceCustomData replaces the custom channel data hex string with the
Expand Down Expand Up @@ -86,6 +95,95 @@ func replaceCustomData(jsonBytes []byte) []byte {
return buf.Bytes()
}

// replaceAndAppendScid replaces the chan_id with scid and appends the human
// readable string representation of scid.
func replaceAndAppendScid(jsonBytes []byte) []byte {
// If there's nothing to replace, return the original JSON.
if !chanIDPattern.Match(jsonBytes) {
return jsonBytes
}

replacedBytes := chanIDPattern.ReplaceAllFunc(
jsonBytes, func(match []byte) []byte {
// Extract the captured scid group from the match.
chanID := chanIDPattern.FindStringSubmatch(
string(match),
)[1]

scid, err := strconv.ParseUint(chanID, 10, 64)
if err != nil {
return match
}

// Format a new JSON field for the scid (chan_id),
// including both its numeric representation and its
// string representation (scid_str).
scidStr := lnwire.NewShortChanIDFromInt(scid).
AltString()
updatedField := fmt.Sprintf(
`"scid": "%d", "scid_str": "%s"`, scid, scidStr,
)

// Replace the entire match with the new structure.
return []byte(updatedField)
},
)

var buf bytes.Buffer
err := json.Indent(&buf, replacedBytes, "", " ")
if err != nil {
// If we can't indent the JSON, it likely means the replacement
// data wasn't correct, so we return the original JSON.
return jsonBytes
}

return buf.Bytes()
}

// appendChanID appends the chan_id which is computed using the outpoint
// of the funding transaction (the txid, and output index).
func appendChanID(jsonBytes []byte) []byte {
// If there's nothing to replace, return the original JSON.
if !channelPointPattern.Match(jsonBytes) {
return jsonBytes
}

replacedBytes := channelPointPattern.ReplaceAllFunc(
jsonBytes, func(match []byte) []byte {
chanPoint := channelPointPattern.FindStringSubmatch(
string(match),
)[1]

chanOutpoint, err := wire.NewOutPointFromString(
chanPoint)
if err != nil {
return match
}

// Format a new JSON field computed from the
// channel_point (chan_id).
chanID := lnwire.NewChanIDFromOutPoint(*chanOutpoint)
updatedField := fmt.Sprintf(
`"channel_point": "%s", "chan_id": "%s"`,
chanPoint, chanID.String(),
)

// Replace the entire match with the new structure.
return []byte(updatedField)
},
)

var buf bytes.Buffer
err := json.Indent(&buf, replacedBytes, "", " ")
if err != nil {
// If we can't indent the JSON, it likely means the replacement
// data wasn't correct, so we return the original JSON.
return jsonBytes
}

return buf.Bytes()
}

func getContext() context.Context {
shutdownInterceptor, err := signal.Intercept()
if err != nil {
Expand Down Expand Up @@ -120,8 +218,15 @@ func printRespJSON(resp proto.Message) {
return
}

// Replace custom_channel_data in the JSON.
jsonBytesReplaced := replaceCustomData(jsonBytes)

// Replace chan_id with scid, and append scid_str and scid fields.
jsonBytesReplaced = replaceAndAppendScid(jsonBytesReplaced)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we please add some unit tests for these two new functions? See commands_test.go.


// Append the chan_id field to the JSON.
jsonBytesReplaced = appendChanID(jsonBytesReplaced)

fmt.Printf("%s\n", jsonBytesReplaced)
}

Expand Down
143 changes: 139 additions & 4 deletions cmd/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,9 @@ func TestReplaceCustomData(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
data string
replaceData string
expected string
name string
data string
expected string
}{
{
name: "no replacement necessary",
Expand Down Expand Up @@ -191,3 +190,139 @@ func TestReplaceCustomData(t *testing.T) {
})
}
}

// TestReplaceAndAppendScid tests whether chan_id is replaced with scid and
// scid_str in the JSON console output.
func TestReplaceAndAppendScid(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
data string
expected string
}{
{
name: "no replacement necessary",
data: "foo",
expected: "foo",
},
{
name: "valid json with replacement",
data: "{\"foo\":\"bar\",\"chan_id\":\"" +
"829031767408640" + "\"}",
expected: `{
"foo": "bar",
"scid": "829031767408640",
"scid_str": "754x1x0"
}`,
},
{
name: "valid json with replacement and space",
data: "{\"foo\":\"bar\",\"chan_id\": \"" +
"829031767408640" + "\"}",
expected: `{
"foo": "bar",
"scid": "829031767408640",
"scid_str": "754x1x0"
}`,
},
{
name: "doesn't match pattern, returned identical",
data: "this ain't even json, and no chan_id " +
"either",
expected: "this ain't even json, and no chan_id " +
"either",
},
{
name: "invalid json",
data: "this ain't json, " +
"\"chan_id\":\"18446744073709551616\"",
expected: "this ain't json, " +
"\"chan_id\":\"18446744073709551616\"",
},
{
name: "valid json, invalid uint, just formatted",
data: "{\"chan_id\":\"18446744073709551616\"}",
expected: `{
"chan_id": "18446744073709551616"
}`,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := replaceAndAppendScid([]byte(tc.data))
require.Equal(t, tc.expected, string(result))
})
}
}

// TestAppendChanID tests whether chan_id (BOLT02) is appended
// to the JSON console output.
func TestAppendChanID(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
data string
expected string
}{
{
name: "no amendment necessary",
data: "foo",
expected: "foo",
},
{
name: "valid json with amendment",
data: "{\"foo\":\"bar\",\"channel_point\":\"" +
"6ab312e3b744e1b80a33a6541697df88766515c31c" +
"08e839bf11dc9fcc036a19:0" + "\"}",
expected: `{
"foo": "bar",
"channel_point": "6ab312e3b744e1b80a33a6541697df88766515c31c` +
`08e839bf11dc9fcc036a19:0",
"chan_id": "196a03cc9fdc11bf39e8081cc315657688df971654a` +
`6330ab8e144b7e312b36a"
}`,
},
{
name: "valid json with amendment and space",
data: "{\"foo\":\"bar\",\"channel_point\": \"" +
"6ab312e3b744e1b80a33a6541697df88766515c31c" +
"08e839bf11dc9fcc036a19:0" + "\"}",
expected: `{
"foo": "bar",
"channel_point": "6ab312e3b744e1b80a33a6541697df88766515c31c` +
`08e839bf11dc9fcc036a19:0",
"chan_id": "196a03cc9fdc11bf39e8081cc315657688df971654a` +
`6330ab8e144b7e312b36a"
}`,
},
{
name: "doesn't match pattern, returned identical",
data: "this ain't even json, and no channel_point " +
"either",
expected: "this ain't even json, and no channel_point" +
" either",
},
{
name: "invalid json",
data: "this ain't json, " +
"\"channel_point\":\"f:0\"",
expected: "this ain't json, " +
"\"channel_point\":\"f:0\"",
},
{
name: "valid json with invalid outpoint, formatted",
data: "{\"channel_point\":\"f:0\"}",
expected: "{\n \"channel_point\": \"f:0\"\n}",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := appendChanID([]byte(tc.data))
require.Equal(t, tc.expected, string(result))
})
}
}
7 changes: 7 additions & 0 deletions docs/release-notes/release-notes-0.19.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@
now update the channel policy if the edge was not found in the graph
database if the `create_missing_edge` flag is set.

* [Enhance](https://github.com/lightningnetwork/lnd/pull/9390) the
`lncli listchannels` output by adding the human readable short
channel id and the channel id defined in BOLT02. Moreover change
the misnomer of `chan_id` which was describing the short channel
id to `scid` to represent what it really is.

# Improvements
## Functional Updates

Expand Down Expand Up @@ -274,6 +280,7 @@ The underlying functionality between those two options remain the same.
* hieblmi
* Jesse de Wit
* Keagan McClelland
* Nishant Bansal
* Oliver Gugger
* Pins
* Viktor Tigerström
Expand Down
6 changes: 6 additions & 0 deletions lnwire/short_channel_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ func (c ShortChannelID) String() string {
return fmt.Sprintf("%d:%d:%d", c.BlockHeight, c.TxIndex, c.TxPosition)
}

// String generates a human-readable representation of the channel ID
// with 'x' as a separator.
func (c ShortChannelID) AltString() string {
return fmt.Sprintf("%dx%dx%d", c.BlockHeight, c.TxIndex, c.TxPosition)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we should change the behavior of this method, as we don't really know where it's used and what impact this has. So perhaps just add a new one, called AltString() for "alternative"?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Or we just leave this as is and replace the colon characters with the x in the CLI where we use this.

Copy link
Author

Choose a reason for hiding this comment

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

Since using 'x' as a separator is also mentioned in the BOLTs documentation, I believe adding a new method AltString() would be better for future use.

}

// Record returns a TLV record that can be used to encode/decode a
// ShortChannelID to/from a TLV stream.
func (c *ShortChannelID) Record() tlv.Record {
Expand Down