From 180f61b9eb62213801b418f5b739fc57de319653 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 5 May 2025 14:58:03 +0200 Subject: [PATCH 1/3] lncli: change importmc category --- cmd/commands/cmd_import_mission_control.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/commands/cmd_import_mission_control.go b/cmd/commands/cmd_import_mission_control.go index ac15b2f1f..688e2fc89 100644 --- a/cmd/commands/cmd_import_mission_control.go +++ b/cmd/commands/cmd_import_mission_control.go @@ -15,7 +15,7 @@ const argsStr = "[source node] [dest node] [unix ts seconds] [amount in msat]" var importMissionControlCommand = cli.Command{ Name: "importmc", - Category: "Payments", + Category: "Mission Control", Usage: "Import a result to the internal mission control state.", ArgsUsage: fmt.Sprintf("importmc %v", argsStr), Action: actionDecorator(importMissionControl), From 8351018f8bf0c706dcb0c56474423a57d47b8621 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 5 May 2025 11:46:12 +0200 Subject: [PATCH 2/3] lncli: add loadmc command This command lets one import data exported by `lncli querymc > mc.json` via `lncli loadmc --mcdatapath mc.json`. --- cmd/commands/cmd_import_mission_control.go | 173 +++++++++++++++++++++ cmd/commands/routerrpc.go | 1 + 2 files changed, 174 insertions(+) diff --git a/cmd/commands/cmd_import_mission_control.go b/cmd/commands/cmd_import_mission_control.go index 688e2fc89..8bc69729b 100644 --- a/cmd/commands/cmd_import_mission_control.go +++ b/cmd/commands/cmd_import_mission_control.go @@ -4,8 +4,11 @@ import ( "context" "errors" "fmt" + "os" "strconv" + "time" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/routing/route" "github.com/urfave/cli" @@ -98,3 +101,173 @@ func importMissionControl(ctx *cli.Context) error { _, err = client.XImportMissionControl(rpcCtx, req) return err } + +var loadMissionControlCommand = cli.Command{ + Name: "loadmc", + Category: "Mission Control", + Usage: "Load mission control results to the internal mission " + + "control state from a file produced by querymc with the " + + "option to shift timestamps. Note that this data is not " + + "persisted across restarts.", + Action: actionDecorator(loadMissionControl), + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "mcdatapath", + Usage: "The path to the querymc output file (json).", + }, + cli.BoolFlag{ + Name: "discard", + Usage: "Discards current mission control data.", + }, + cli.StringFlag{ + Name: "timeoffset", + Usage: "Time offset to add to all timestamps. " + + "Format: 1m for a minute, 1h for an hour, 1d " + + "for one day. This can be used to let " + + "mission control data appear to be more " + + "recent, to trick pathfinding's in-built " + + "information decay mechanism. Additionally " + + "by setting 0m, this will report the most " + + "recent result timestamp, which can be used " + + "to find out how old this data is.", + }, + cli.BoolFlag{ + Name: "force", + Usage: "Whether to force overiding more recent " + + "results in the database with older results " + + "from the file.", + }, + }, +} + +// loadMissionControl loads mission control data into an LND instance. +func loadMissionControl(ctx *cli.Context) error { + rpcCtx := context.Background() + + mcDataPath := ctx.String("mcdatapath") + if mcDataPath == "" { + return fmt.Errorf("mcdatapath must be set") + } + + if _, err := os.Stat(mcDataPath); os.IsNotExist(err) { + return fmt.Errorf("%v does not exist", mcDataPath) + } + + conn := getClientConn(ctx, false) + defer conn.Close() + + client := routerrpc.NewRouterClient(conn) + + // Load and unmarshal the querymc output file. + mcRaw, err := os.ReadFile(mcDataPath) + if err != nil { + return fmt.Errorf("could not read querymc output file: %w", err) + } + + mc := &routerrpc.QueryMissionControlResponse{} + err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal(mcRaw, mc) + if err != nil { + return fmt.Errorf("could not unmarshal querymc output file: %w", + err) + } + + // We discard mission control data if requested. + if ctx.Bool("discard") { + if !promptForConfirmation("This will discard all current " + + "mission control data in the database (yes/no): ") { + + return nil + } + + _, err = client.ResetMissionControl( + rpcCtx, &routerrpc.ResetMissionControlRequest{}, + ) + if err != nil { + return err + } + } + + // Add a time offset to all timestamps if requested. + timeOffset := ctx.String("timeoffset") + if timeOffset != "" { + offset, err := time.ParseDuration(timeOffset) + if err != nil { + return fmt.Errorf("could not parse time offset: %w", + err) + } + + var maxTimestamp time.Time + + for _, pair := range mc.Pairs { + if pair.History.SuccessTime != 0 { + unix := time.Unix(pair.History.SuccessTime, 0) + unix = unix.Add(offset) + + if unix.After(maxTimestamp) { + maxTimestamp = unix + } + + pair.History.SuccessTime = unix.Unix() + } + + if pair.History.FailTime != 0 { + unix := time.Unix(pair.History.FailTime, 0) + unix = unix.Add(offset) + + if unix.After(maxTimestamp) { + maxTimestamp = unix + } + + pair.History.FailTime = unix.Unix() + } + } + + fmt.Printf("Adding time offset %v to all timestamps. "+ + "New max timestamp: %v\n", offset, maxTimestamp) + } + + sanitizeMCData(mc.Pairs) + + fmt.Printf("Mission control file contains %v pairs.\n", len(mc.Pairs)) + if !promptForConfirmation("Import mission control data (yes/no): ") { + return nil + } + + _, err = client.XImportMissionControl( + rpcCtx, + &routerrpc.XImportMissionControlRequest{ + Pairs: mc.Pairs, Force: ctx.Bool("force"), + }, + ) + if err != nil { + return fmt.Errorf("could not import mission control data: %w", + err) + } + + return nil +} + +// sanitizeMCData removes invalid data from the exported mission control data. +func sanitizeMCData(mc []*routerrpc.PairHistory) { + for _, pair := range mc { + // It is not allowed to import a zero-amount success to mission + // control if a timestamp is set. We unset it in this case. + if pair.History.SuccessTime != 0 && + pair.History.SuccessAmtMsat == 0 && + pair.History.SuccessAmtSat == 0 { + + pair.History.SuccessTime = 0 + } + + // If we only deal with a failure, we need to set the failure + // amount to a tiny value due to a limitation in the RPC. This + // will lead to a similar penalization in pathfinding. + if pair.History.SuccessTime == 0 && + pair.History.FailTime != 0 && + pair.History.FailAmtMsat == 0 && + pair.History.FailAmtSat == 0 { + + pair.History.FailAmtMsat = 1 + } + } +} diff --git a/cmd/commands/routerrpc.go b/cmd/commands/routerrpc.go index 82211affd..e7b877a08 100644 --- a/cmd/commands/routerrpc.go +++ b/cmd/commands/routerrpc.go @@ -7,6 +7,7 @@ func routerCommands() []cli.Command { return []cli.Command{ queryMissionControlCommand, importMissionControlCommand, + loadMissionControlCommand, queryProbCommand, resetMissionControlCommand, buildRouteCommand, From aaccc2bcaa85a06b1f3671af04a4710528eb2ca7 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Mon, 5 May 2025 11:53:59 +0200 Subject: [PATCH 3/3] docs: update release notes --- docs/release-notes/release-notes-0.19.0.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index fa5012c3f..5893fc11f 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -250,6 +250,9 @@ when running LND with an aux component injected (custom channels). to allow for restriction of access based on an IP range. Prior to this only specific IPs could be allowed or denied. +* A [command was created](https://github.com/lightningnetwork/lnd/pull/9781) to + load mission control data generated by `lncli querymc`. + # Improvements ## Functional Updates