Skip to content

Commit 8c9c4b5

Browse files
authored
Merge pull request lightningnetwork#3184 from wpaulino/wtclient-subserver
multi: add watchtower client RPC subserver
2 parents 4cb3000 + 0690c8f commit 8c9c4b5

34 files changed

+3362
-243
lines changed

Diff for: cmd/lncli/main.go

+1
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ func main() {
306306
app.Commands = append(app.Commands, routerCommands()...)
307307
app.Commands = append(app.Commands, walletCommands()...)
308308
app.Commands = append(app.Commands, watchtowerCommands()...)
309+
app.Commands = append(app.Commands, wtclientCommands()...)
309310

310311
if err := app.Run(os.Args); err != nil {
311312
fatal(err)

Diff for: cmd/lncli/wtclient_active.go

+283
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
// +build wtclientrpc
2+
3+
package main
4+
5+
import (
6+
"context"
7+
"encoding/hex"
8+
"errors"
9+
"fmt"
10+
"strings"
11+
12+
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
13+
"github.com/urfave/cli"
14+
)
15+
16+
// wtclientCommands will return nil for non-wtclientrpc builds.
17+
func wtclientCommands() []cli.Command {
18+
return []cli.Command{
19+
{
20+
Name: "wtclient",
21+
Usage: "Interact with the watchtower client.",
22+
Category: "Watchtower",
23+
Subcommands: []cli.Command{
24+
addTowerCommand,
25+
removeTowerCommand,
26+
listTowersCommand,
27+
getTowerCommand,
28+
statsCommand,
29+
policyCommand,
30+
},
31+
},
32+
}
33+
}
34+
35+
// getWtclient initializes a connection to the watchtower client RPC in order to
36+
// interact with it.
37+
func getWtclient(ctx *cli.Context) (wtclientrpc.WatchtowerClientClient, func()) {
38+
conn := getClientConn(ctx, false)
39+
cleanUp := func() {
40+
conn.Close()
41+
}
42+
return wtclientrpc.NewWatchtowerClientClient(conn), cleanUp
43+
}
44+
45+
var addTowerCommand = cli.Command{
46+
Name: "add",
47+
Usage: "Register a watchtower to use for future sessions/backups.",
48+
Description: "If the watchtower has already been registered, then " +
49+
"this command serves as a way of updating the watchtower " +
50+
"with new addresses it is reachable over.",
51+
ArgsUsage: "pubkey@address",
52+
Action: actionDecorator(addTower),
53+
}
54+
55+
func addTower(ctx *cli.Context) error {
56+
// Display the command's help message if the number of arguments/flags
57+
// is not what we expect.
58+
if ctx.NArg() != 1 || ctx.NumFlags() > 0 {
59+
return cli.ShowCommandHelp(ctx, "add")
60+
}
61+
62+
parts := strings.Split(ctx.Args().First(), "@")
63+
if len(parts) != 2 {
64+
return errors.New("expected tower of format pubkey@address")
65+
}
66+
pubKey, err := hex.DecodeString(parts[0])
67+
if err != nil {
68+
return fmt.Errorf("invalid public key: %v", err)
69+
}
70+
address := parts[1]
71+
72+
client, cleanUp := getWtclient(ctx)
73+
defer cleanUp()
74+
75+
req := &wtclientrpc.AddTowerRequest{
76+
Pubkey: pubKey,
77+
Address: address,
78+
}
79+
resp, err := client.AddTower(context.Background(), req)
80+
if err != nil {
81+
return err
82+
}
83+
84+
printRespJSON(resp)
85+
return nil
86+
}
87+
88+
var removeTowerCommand = cli.Command{
89+
Name: "remove",
90+
Usage: "Remove a watchtower to prevent its use for future " +
91+
"sessions/backups.",
92+
Description: "An optional address can be provided to remove, " +
93+
"indicating that the watchtower is no longer reachable at " +
94+
"this address. If an address isn't provided, then the " +
95+
"watchtower will no longer be used for future sessions/backups.",
96+
ArgsUsage: "pubkey | pubkey@address",
97+
Action: actionDecorator(removeTower),
98+
}
99+
100+
func removeTower(ctx *cli.Context) error {
101+
// Display the command's help message if the number of arguments/flags
102+
// is not what we expect.
103+
if ctx.NArg() != 1 || ctx.NumFlags() > 0 {
104+
return cli.ShowCommandHelp(ctx, "remove")
105+
}
106+
107+
// The command can have only one argument, but it can be interpreted in
108+
// either of the following formats:
109+
//
110+
// pubkey or pubkey@address
111+
//
112+
// The hex-encoded public key of the watchtower is always required,
113+
// while the second is an optional address we'll remove from the
114+
// watchtower's database record.
115+
parts := strings.Split(ctx.Args().First(), "@")
116+
if len(parts) > 2 {
117+
return errors.New("expected tower of format pubkey@address")
118+
}
119+
pubKey, err := hex.DecodeString(parts[0])
120+
if err != nil {
121+
return fmt.Errorf("invalid public key: %v", err)
122+
}
123+
var address string
124+
if len(parts) == 2 {
125+
address = parts[1]
126+
}
127+
128+
client, cleanUp := getWtclient(ctx)
129+
defer cleanUp()
130+
131+
req := &wtclientrpc.RemoveTowerRequest{
132+
Pubkey: pubKey,
133+
Address: address,
134+
}
135+
resp, err := client.RemoveTower(context.Background(), req)
136+
if err != nil {
137+
return err
138+
}
139+
140+
printRespJSON(resp)
141+
return nil
142+
}
143+
144+
var listTowersCommand = cli.Command{
145+
Name: "towers",
146+
Usage: "Display information about all registered watchtowers.",
147+
Flags: []cli.Flag{
148+
cli.BoolFlag{
149+
Name: "include_sessions",
150+
Usage: "include sessions with the watchtower in the " +
151+
"response",
152+
},
153+
},
154+
Action: actionDecorator(listTowers),
155+
}
156+
157+
func listTowers(ctx *cli.Context) error {
158+
// Display the command's help message if the number of arguments/flags
159+
// is not what we expect.
160+
if ctx.NArg() > 0 || ctx.NumFlags() > 1 {
161+
return cli.ShowCommandHelp(ctx, "towers")
162+
}
163+
164+
client, cleanUp := getWtclient(ctx)
165+
defer cleanUp()
166+
167+
req := &wtclientrpc.ListTowersRequest{
168+
IncludeSessions: ctx.Bool("include_sessions"),
169+
}
170+
resp, err := client.ListTowers(context.Background(), req)
171+
if err != nil {
172+
return err
173+
}
174+
175+
var listTowersResp = struct {
176+
Towers []*Tower `json:"towers"`
177+
}{
178+
Towers: make([]*Tower, len(resp.Towers)),
179+
}
180+
for i, tower := range resp.Towers {
181+
listTowersResp.Towers[i] = NewTowerFromProto(tower)
182+
}
183+
184+
printJSON(listTowersResp)
185+
return nil
186+
}
187+
188+
var getTowerCommand = cli.Command{
189+
Name: "tower",
190+
Usage: "Display information about a specific registered watchtower.",
191+
ArgsUsage: "pubkey",
192+
Flags: []cli.Flag{
193+
cli.BoolFlag{
194+
Name: "include_sessions",
195+
Usage: "include sessions with the watchtower in the " +
196+
"response",
197+
},
198+
},
199+
Action: actionDecorator(getTower),
200+
}
201+
202+
func getTower(ctx *cli.Context) error {
203+
// Display the command's help message if the number of arguments/flags
204+
// is not what we expect.
205+
if ctx.NArg() != 1 || ctx.NumFlags() > 1 {
206+
return cli.ShowCommandHelp(ctx, "tower")
207+
}
208+
209+
// The command only has one argument, which we expect to be the
210+
// hex-encoded public key of the watchtower we'll display information
211+
// about.
212+
pubKey, err := hex.DecodeString(ctx.Args().Get(0))
213+
if err != nil {
214+
return fmt.Errorf("invalid public key: %v", err)
215+
}
216+
217+
client, cleanUp := getWtclient(ctx)
218+
defer cleanUp()
219+
220+
req := &wtclientrpc.GetTowerInfoRequest{
221+
Pubkey: pubKey,
222+
IncludeSessions: ctx.Bool("include_sessions"),
223+
}
224+
resp, err := client.GetTowerInfo(context.Background(), req)
225+
if err != nil {
226+
return err
227+
}
228+
229+
printJSON(NewTowerFromProto(resp))
230+
return nil
231+
}
232+
233+
var statsCommand = cli.Command{
234+
Name: "stats",
235+
Usage: "Display the session stats of the watchtower client.",
236+
Action: actionDecorator(stats),
237+
}
238+
239+
func stats(ctx *cli.Context) error {
240+
// Display the command's help message if the number of arguments/flags
241+
// is not what we expect.
242+
if ctx.NArg() > 0 || ctx.NumFlags() > 0 {
243+
return cli.ShowCommandHelp(ctx, "stats")
244+
}
245+
246+
client, cleanUp := getWtclient(ctx)
247+
defer cleanUp()
248+
249+
req := &wtclientrpc.StatsRequest{}
250+
resp, err := client.Stats(context.Background(), req)
251+
if err != nil {
252+
return err
253+
}
254+
255+
printRespJSON(resp)
256+
return nil
257+
}
258+
259+
var policyCommand = cli.Command{
260+
Name: "policy",
261+
Usage: "Display the active watchtower client policy configuration.",
262+
Action: actionDecorator(policy),
263+
}
264+
265+
func policy(ctx *cli.Context) error {
266+
// Display the command's help message if the number of arguments/flags
267+
// is not what we expect.
268+
if ctx.NArg() > 0 || ctx.NumFlags() > 0 {
269+
return cli.ShowCommandHelp(ctx, "policy")
270+
}
271+
272+
client, cleanUp := getWtclient(ctx)
273+
defer cleanUp()
274+
275+
req := &wtclientrpc.PolicyRequest{}
276+
resp, err := client.Policy(context.Background(), req)
277+
if err != nil {
278+
return err
279+
}
280+
281+
printRespJSON(resp)
282+
return nil
283+
}

Diff for: cmd/lncli/wtclient_default.go

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// +build !wtclientrpc
2+
3+
package main
4+
5+
import "github.com/urfave/cli"
6+
7+
// wtclientCommands will return nil for non-wtclientrpc builds.
8+
func wtclientCommands() []cli.Command {
9+
return nil
10+
}

Diff for: cmd/lncli/wtclient_types.go

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// +build wtclientrpc
2+
3+
package main
4+
5+
import (
6+
"encoding/hex"
7+
8+
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
9+
)
10+
11+
// TowerSession encompasses information about a tower session.
12+
type TowerSession struct {
13+
NumBackups uint32 `json:"num_backups"`
14+
NumPendingBackups uint32 `json:"num_pending_backups"`
15+
MaxBackups uint32 `json:"max_backups"`
16+
SweepSatPerByte uint32 `json:"sweep_sat_per_byte"`
17+
}
18+
19+
// NewTowerSessionsFromProto converts a set of tower sessions from their RPC
20+
// type to a CLI-friendly type.
21+
func NewTowerSessionsFromProto(sessions []*wtclientrpc.TowerSession) []*TowerSession {
22+
towerSessions := make([]*TowerSession, 0, len(sessions))
23+
for _, session := range sessions {
24+
towerSessions = append(towerSessions, &TowerSession{
25+
NumBackups: session.NumBackups,
26+
NumPendingBackups: session.NumPendingBackups,
27+
MaxBackups: session.MaxBackups,
28+
SweepSatPerByte: session.SweepSatPerByte,
29+
})
30+
}
31+
return towerSessions
32+
}
33+
34+
// Tower encompasses information about a registered watchtower.
35+
type Tower struct {
36+
PubKey string `json:"pubkey"`
37+
Addresses []string `json:"addresses"`
38+
ActiveSessionCandidate bool `json:"active_session_candidate"`
39+
NumSessions uint32 `json:"num_sessions"`
40+
Sessions []*TowerSession `json:"sessions"`
41+
}
42+
43+
// NewTowerFromProto converts a tower from its RPC type to a CLI-friendly type.
44+
func NewTowerFromProto(tower *wtclientrpc.Tower) *Tower {
45+
return &Tower{
46+
PubKey: hex.EncodeToString(tower.Pubkey),
47+
Addresses: tower.Addresses,
48+
ActiveSessionCandidate: tower.ActiveSessionCandidate,
49+
NumSessions: tower.NumSessions,
50+
Sessions: NewTowerSessionsFromProto(tower.Sessions),
51+
}
52+
}

Diff for: config.go

-12
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import (
3333
"github.com/lightningnetwork/lnd/lnwire"
3434
"github.com/lightningnetwork/lnd/routing"
3535
"github.com/lightningnetwork/lnd/tor"
36-
"github.com/lightningnetwork/lnd/watchtower"
3736
)
3837

3938
const (
@@ -1091,17 +1090,6 @@ func loadConfig() (*config, error) {
10911090
return nil, err
10921091
}
10931092

1094-
// If the user provided private watchtower addresses, parse them to
1095-
// obtain the LN addresses.
1096-
if cfg.WtClient.IsActive() {
1097-
err := cfg.WtClient.ParsePrivateTowers(
1098-
watchtower.DefaultPeerPort, cfg.net.ResolveTCPAddr,
1099-
)
1100-
if err != nil {
1101-
return nil, err
1102-
}
1103-
}
1104-
11051093
// Finally, ensure that the user's color is correctly formatted,
11061094
// otherwise the server will not be able to start after the unlocking
11071095
// the wallet.

0 commit comments

Comments
 (0)