Skip to content
Merged
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
175 changes: 174 additions & 1 deletion cmd/commands/cmd_import_mission_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -15,7 +18,7 @@ const argsStr = "[source node] [dest node] [unix ts seconds] [amount in msat]"

var importMissionControlCommand = cli.Command{
Name: "importmc",
Category: "Payments",
Category: "Mission Control",
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Usage: "Import a result to the internal mission control state.",
ArgsUsage: fmt.Sprintf("importmc %v", argsStr),
Action: actionDecorator(importMissionControl),
Expand Down Expand Up @@ -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 " +
Copy link
Collaborator

Choose a reason for hiding this comment

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

Micro-Nit: use backticks for the querymc cmd.

"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. " +
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we explain what this is useful for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right, added an explanation.

"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.",
Comment on lines +129 to +132
Copy link
Collaborator

Choose a reason for hiding this comment

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

Q: What do you mean by report here? The import mc data rpc does not return any information or ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ah, I was referring to the stdout text with the new max timestamp

},
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. "+
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: Adding => Added ?

"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{
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's also the Force flag, not sure if that's useful to expose?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, good to have API feature completeness

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
}
Comment on lines +253 to +260
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When testing I've seen entries like that

        {
            "node_from":  "X",
            "node_to":  "Y",
            "history":  {
                "fail_time":  "1746388491",
                "fail_amt_sat":  "0",
                "fail_amt_msat":  "0",
                "success_time":  "1746386085",
                "success_amt_sat":  "0",
                "success_amt_msat":  "0"
            }
        },

which is inconsistent with what we allow to import, which is why I've added those clean-ups. See here:

// Return an error if it does have a timestamp without an amount, and
// it's not expected to be a failure.
case !isFailure && timeSet && !amountSet:
return 0, time.Time{}, errors.New("non-zero timestamp " +
"requires non-zero amount for success pairs")

Copy link
Collaborator

Choose a reason for hiding this comment

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

Definitely something we need to think about and potentially removing this validation check on the server side.


// 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
}
Comment on lines +262 to +271
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is an other check we would violate with the above input.

if successAmt == 0 && failAmt == 0 {
return nil, fmt.Errorf("%v: either success or failure result "+
"required", pairPrefix)
}

}
}
1 change: 1 addition & 0 deletions cmd/commands/routerrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ func routerCommands() []cli.Command {
return []cli.Command{
queryMissionControlCommand,
importMissionControlCommand,
loadMissionControlCommand,
queryProbCommand,
resetMissionControlCommand,
buildRouteCommand,
Expand Down
3 changes: 3 additions & 0 deletions docs/release-notes/release-notes-0.19.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading