Skip to content

Commit 7b6c1cf

Browse files
authored
Merge pull request #9781 from bitromortac/2505-loadmc
lncli: add loadmc command
2 parents 334a7d1 + aaccc2b commit 7b6c1cf

File tree

3 files changed

+178
-1
lines changed

3 files changed

+178
-1
lines changed

cmd/commands/cmd_import_mission_control.go

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"os"
78
"strconv"
9+
"time"
810

11+
"github.com/lightningnetwork/lnd/lnrpc"
912
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1013
"github.com/lightningnetwork/lnd/routing/route"
1114
"github.com/urfave/cli"
@@ -15,7 +18,7 @@ const argsStr = "[source node] [dest node] [unix ts seconds] [amount in msat]"
1518

1619
var importMissionControlCommand = cli.Command{
1720
Name: "importmc",
18-
Category: "Payments",
21+
Category: "Mission Control",
1922
Usage: "Import a result to the internal mission control state.",
2023
ArgsUsage: fmt.Sprintf("importmc %v", argsStr),
2124
Action: actionDecorator(importMissionControl),
@@ -98,3 +101,173 @@ func importMissionControl(ctx *cli.Context) error {
98101
_, err = client.XImportMissionControl(rpcCtx, req)
99102
return err
100103
}
104+
105+
var loadMissionControlCommand = cli.Command{
106+
Name: "loadmc",
107+
Category: "Mission Control",
108+
Usage: "Load mission control results to the internal mission " +
109+
"control state from a file produced by querymc with the " +
110+
"option to shift timestamps. Note that this data is not " +
111+
"persisted across restarts.",
112+
Action: actionDecorator(loadMissionControl),
113+
Flags: []cli.Flag{
114+
cli.StringFlag{
115+
Name: "mcdatapath",
116+
Usage: "The path to the querymc output file (json).",
117+
},
118+
cli.BoolFlag{
119+
Name: "discard",
120+
Usage: "Discards current mission control data.",
121+
},
122+
cli.StringFlag{
123+
Name: "timeoffset",
124+
Usage: "Time offset to add to all timestamps. " +
125+
"Format: 1m for a minute, 1h for an hour, 1d " +
126+
"for one day. This can be used to let " +
127+
"mission control data appear to be more " +
128+
"recent, to trick pathfinding's in-built " +
129+
"information decay mechanism. Additionally " +
130+
"by setting 0m, this will report the most " +
131+
"recent result timestamp, which can be used " +
132+
"to find out how old this data is.",
133+
},
134+
cli.BoolFlag{
135+
Name: "force",
136+
Usage: "Whether to force overiding more recent " +
137+
"results in the database with older results " +
138+
"from the file.",
139+
},
140+
},
141+
}
142+
143+
// loadMissionControl loads mission control data into an LND instance.
144+
func loadMissionControl(ctx *cli.Context) error {
145+
rpcCtx := context.Background()
146+
147+
mcDataPath := ctx.String("mcdatapath")
148+
if mcDataPath == "" {
149+
return fmt.Errorf("mcdatapath must be set")
150+
}
151+
152+
if _, err := os.Stat(mcDataPath); os.IsNotExist(err) {
153+
return fmt.Errorf("%v does not exist", mcDataPath)
154+
}
155+
156+
conn := getClientConn(ctx, false)
157+
defer conn.Close()
158+
159+
client := routerrpc.NewRouterClient(conn)
160+
161+
// Load and unmarshal the querymc output file.
162+
mcRaw, err := os.ReadFile(mcDataPath)
163+
if err != nil {
164+
return fmt.Errorf("could not read querymc output file: %w", err)
165+
}
166+
167+
mc := &routerrpc.QueryMissionControlResponse{}
168+
err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal(mcRaw, mc)
169+
if err != nil {
170+
return fmt.Errorf("could not unmarshal querymc output file: %w",
171+
err)
172+
}
173+
174+
// We discard mission control data if requested.
175+
if ctx.Bool("discard") {
176+
if !promptForConfirmation("This will discard all current " +
177+
"mission control data in the database (yes/no): ") {
178+
179+
return nil
180+
}
181+
182+
_, err = client.ResetMissionControl(
183+
rpcCtx, &routerrpc.ResetMissionControlRequest{},
184+
)
185+
if err != nil {
186+
return err
187+
}
188+
}
189+
190+
// Add a time offset to all timestamps if requested.
191+
timeOffset := ctx.String("timeoffset")
192+
if timeOffset != "" {
193+
offset, err := time.ParseDuration(timeOffset)
194+
if err != nil {
195+
return fmt.Errorf("could not parse time offset: %w",
196+
err)
197+
}
198+
199+
var maxTimestamp time.Time
200+
201+
for _, pair := range mc.Pairs {
202+
if pair.History.SuccessTime != 0 {
203+
unix := time.Unix(pair.History.SuccessTime, 0)
204+
unix = unix.Add(offset)
205+
206+
if unix.After(maxTimestamp) {
207+
maxTimestamp = unix
208+
}
209+
210+
pair.History.SuccessTime = unix.Unix()
211+
}
212+
213+
if pair.History.FailTime != 0 {
214+
unix := time.Unix(pair.History.FailTime, 0)
215+
unix = unix.Add(offset)
216+
217+
if unix.After(maxTimestamp) {
218+
maxTimestamp = unix
219+
}
220+
221+
pair.History.FailTime = unix.Unix()
222+
}
223+
}
224+
225+
fmt.Printf("Adding time offset %v to all timestamps. "+
226+
"New max timestamp: %v\n", offset, maxTimestamp)
227+
}
228+
229+
sanitizeMCData(mc.Pairs)
230+
231+
fmt.Printf("Mission control file contains %v pairs.\n", len(mc.Pairs))
232+
if !promptForConfirmation("Import mission control data (yes/no): ") {
233+
return nil
234+
}
235+
236+
_, err = client.XImportMissionControl(
237+
rpcCtx,
238+
&routerrpc.XImportMissionControlRequest{
239+
Pairs: mc.Pairs, Force: ctx.Bool("force"),
240+
},
241+
)
242+
if err != nil {
243+
return fmt.Errorf("could not import mission control data: %w",
244+
err)
245+
}
246+
247+
return nil
248+
}
249+
250+
// sanitizeMCData removes invalid data from the exported mission control data.
251+
func sanitizeMCData(mc []*routerrpc.PairHistory) {
252+
for _, pair := range mc {
253+
// It is not allowed to import a zero-amount success to mission
254+
// control if a timestamp is set. We unset it in this case.
255+
if pair.History.SuccessTime != 0 &&
256+
pair.History.SuccessAmtMsat == 0 &&
257+
pair.History.SuccessAmtSat == 0 {
258+
259+
pair.History.SuccessTime = 0
260+
}
261+
262+
// If we only deal with a failure, we need to set the failure
263+
// amount to a tiny value due to a limitation in the RPC. This
264+
// will lead to a similar penalization in pathfinding.
265+
if pair.History.SuccessTime == 0 &&
266+
pair.History.FailTime != 0 &&
267+
pair.History.FailAmtMsat == 0 &&
268+
pair.History.FailAmtSat == 0 {
269+
270+
pair.History.FailAmtMsat = 1
271+
}
272+
}
273+
}

cmd/commands/routerrpc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ func routerCommands() []cli.Command {
77
return []cli.Command{
88
queryMissionControlCommand,
99
importMissionControlCommand,
10+
loadMissionControlCommand,
1011
queryProbCommand,
1112
resetMissionControlCommand,
1213
buildRouteCommand,

docs/release-notes/release-notes-0.19.0.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,9 @@ when running LND with an aux component injected (custom channels).
250250
to allow for restriction of access based on an IP range. Prior to this only
251251
specific IPs could be allowed or denied.
252252

253+
* A [command was created](https://github.com/lightningnetwork/lnd/pull/9781) to
254+
load mission control data generated by `lncli querymc`.
255+
253256
# Improvements
254257
## Functional Updates
255258

0 commit comments

Comments
 (0)