diff --git a/cmd/lncli/cmd_payments.go b/cmd/lncli/cmd_payments.go index 923f65b4c6..eceadc8e15 100644 --- a/cmd/lncli/cmd_payments.go +++ b/cmd/lncli/cmd_payments.go @@ -100,6 +100,45 @@ var ( Name: "time_pref", Usage: "(optional) expresses time preference (range -1 to 1)", } + + introductionNodeFlag = cli.StringFlag{ + Name: "introduction_point", + Usage: "the hex encoded, cleartext node ID of the node to " + + "use for queries to a blinded route", + } + + blindingPointFlag = cli.StringFlag{ + Name: "blinding_point", + Usage: "the hex encoded blinding point to use if querying a " + + "route to a blinded path, this value *must* be set " + + "for queries to a blinded path", + } + + blindedHopsFlag = cli.StringSliceFlag{ + Name: "blinded_hops", + Usage: "the blinded hops to include in the query, formatted " + + "as :. These " + + "hops must be provided *in order* starting with the " + + "introduction point and ending with the receiving node", + } + + blindedBaseFlag = cli.Uint64Flag{ + Name: "blinded_base_fee", + Usage: "the aggregate base fee for the blinded portion of " + + "the route, expressed in msat", + } + + blindedPPMFlag = cli.Uint64Flag{ + Name: "blinded_ppm_fee", + Usage: "the aggregate proportional fee for the blinded " + + "portion of the route, expressed in parts per million", + } + + blindedCLTVFlag = cli.Uint64Flag{ + Name: "blinded_cltv", + Usage: "the total cltv delay for the blinded portion of the " + + "route", + } ) // paymentFlags returns common flags for sendpayment and payinvoice. @@ -1050,6 +1089,12 @@ var queryRoutesCommand = cli.Command{ }, timePrefFlag, cltvLimitFlag, + introductionNodeFlag, + blindingPointFlag, + blindedHopsFlag, + blindedBaseFlag, + blindedPPMFlag, + blindedCLTVFlag, }, Action: actionDecorator(queryRoutes), } @@ -1070,9 +1115,15 @@ func queryRoutes(ctx *cli.Context) error { switch { case ctx.IsSet("dest"): dest = ctx.String("dest") + case args.Present(): dest = args.First() args = args.Tail() + + // If we have a blinded path set, we don't have to specify a + // destination. + case ctx.IsSet(introductionNodeFlag.Name): + default: return fmt.Errorf("dest argument missing") } @@ -1119,6 +1170,11 @@ func queryRoutes(ctx *cli.Context) error { } } + blindedRoute, err := parseBlindedPaymentParameters(ctx) + if err != nil { + return err + } + req := &lnrpc.QueryRoutesRequest{ PubKey: dest, Amt: amt, @@ -1129,6 +1185,7 @@ func queryRoutes(ctx *cli.Context) error { OutgoingChanId: ctx.Uint64("outgoing_chanid"), TimePref: ctx.Float64(timePrefFlag.Name), IgnoredPairs: ignoredPairs, + BlindedPath: blindedRoute, } route, err := client.QueryRoutes(ctxc, req) @@ -1140,6 +1197,92 @@ func queryRoutes(ctx *cli.Context) error { return nil } +func parseBlindedPaymentParameters(ctx *cli.Context) (*lnrpc.BlindedPayment, + error) { + + // If none of the blinded route params are set as we expect, then we + // don't need to return anything. + haveBlinded := ctx.IsSet(blindingPointFlag.Name) || + ctx.IsSet(blindedHopsFlag.Name) || + ctx.IsSet(introductionNodeFlag.Name) || + ctx.IsSet(blindedBaseFlag.Name) || + ctx.IsSet(blindedPPMFlag.Name) || + ctx.IsSet(blindedCLTVFlag.Name) + + if !haveBlinded { + return nil, nil + } + + // If any one of our blinding related flags is set, we expect the + // full set to be set and we'll error our accordingly. + introNode, err := route.NewVertexFromStr( + ctx.String(introductionNodeFlag.Name), + ) + if err != nil { + return nil, fmt.Errorf("decode introduction node: %w", err) + } + + blindingPoint, err := route.NewVertexFromStr(ctx.String( + blindingPointFlag.Name, + )) + if err != nil { + return nil, fmt.Errorf("decode blinding point: %w", err) + } + + blindedHops := ctx.StringSlice(blindedHopsFlag.Name) + + pmt := &lnrpc.BlindedPayment{ + Route: &lnrpc.BlindedRoute{ + IntroductionNode: introNode[:], + BlindingPoint: blindingPoint[:], + BlindedHops: make( + []*lnrpc.BlindedHop, len(blindedHops), + ), + }, + RelayParameters: &lnrpc.BlindedRelay{ + AggregateBaseFeeMsat: ctx.Uint64( + blindedBaseFlag.Name, + ), + AggregateProportionalFeePpm: ctx.Uint64( + blindedPPMFlag.Name, + ), + TotalCltvDelta: uint32(ctx.Uint64( + blindedCLTVFlag.Name, + )), + }, + RelayConstraints: &lnrpc.BlindedConstraints{ + CltvLimit: uint32(ctx.Uint(cltvLimitFlag.Name)), + }, + } + + for i, hop := range blindedHops { + parts := strings.Split(hop, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("blinded hops should be "+ + "expressed as "+ + "blinded_node_id:hex_encrypted_data, got: %v", + hop) + } + + hop, err := route.NewVertexFromStr(parts[0]) + if err != nil { + return nil, fmt.Errorf("hop: %v node: %w", i, err) + } + + data, err := hex.DecodeString(parts[1]) + if err != nil { + return nil, fmt.Errorf("hop: %v data: %w", i, err) + } + + pmt.Route.BlindedHops[i] = &lnrpc.BlindedHop{ + BlindedNode: hop[:], + EncryptedData: data, + } + } + + return pmt, nil +} + // retrieveFeeLimitLegacy retrieves the fee limit based on the different fee // limit flags passed. This function will eventually disappear in favor of // retrieveFeeLimit and the new payment rpc.