Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

routing: pre-payment probe [PoC] #2537

Closed
wants to merge 16 commits into from
Closed
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
8 changes: 8 additions & 0 deletions channeldb/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -2728,6 +2728,14 @@ func (c *ChannelEdgePolicy) Signature() (*btcec.Signature, error) {
return sig, nil
}

// ComputeFee computes the fee to forward an HTLC of `amt` milli-satoshis over
// the passed active payment channel. This value is currently computed as
// specified in BOLT07, but will likely change in the near future.
func (c *ChannelEdgePolicy) ComputeFee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {

return c.FeeBaseMSat + (amt*c.FeeProportionalMillionths)/1000000
}

// FetchChannelEdgesByOutpoint attempts to lookup the two directed edges for
// the channel identified by the funding outpoint. If the channel can't be
// found, then ErrEdgeNotFound is returned. A struct which houses the general
Expand Down
169 changes: 163 additions & 6 deletions cmd/lncli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"sync"
"syscall"

"github.com/lightningnetwork/lnd/lntypes"

"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/golang/protobuf/jsonpb"
Expand Down Expand Up @@ -2113,6 +2115,75 @@ func retrieveFeeLimit(ctx *cli.Context) (*lnrpc.FeeLimit, error) {
return nil, nil
}

func confirmPayReqProbed(payReq *lnrpc.PayReq, amt int64, route *lnrpc.Route,
currentHeight int32, aliases map[string]string) error {
// If the amount was not included in the invoice, then we let
// the payee specify the amount of satoshis they wish to send.
reqAmt := payReq.GetNumSatoshis()
if reqAmt == 0 {
if amt == 0 {
return fmt.Errorf("amount must be specified when " +
"paying a zero amount invoice")
}
reqAmt = amt
}

var hops strings.Builder
for _, hop := range route.Hops {
hops.WriteString("-> ")
alias, ok := aliases[hop.PubKey]
if !ok {
alias = hop.PubKey[:6]
}
hops.WriteString(alias)
if hop.FeeMsat != 0 {
hops.WriteString(
fmt.Sprintf(
" (fee %v) ",
msatToText(hop.FeeMsat),
),
)
}
}

delta := int32(route.TotalTimeLock) - currentHeight

fmt.Printf("Description: %v\n", payReq.GetDescription())
fmt.Printf("Amount (in satoshis): %v\n", reqAmt)
fmt.Printf("Destination: %v\n", payReq.GetDestination())
fmt.Printf("Route:\n")
fmt.Printf(" Fee: %v\n", msatToText(route.TotalFeesMsat))
fmt.Printf(" Time lock: %v (%v blocks, ~%v)\n",
route.TotalTimeLock, delta, durationToText(delta))
fmt.Printf(" Hops: %v\n", hops.String())

fmt.Println()
confirm := promptForConfirmation("Confirm payment (yes/no): ")
if !confirm {
return fmt.Errorf("payment not confirmed")
}

return nil
}

func msatToText(msat int64) string {
if msat < 1000 {
return "< 1"
}
return strconv.FormatInt(msat/1000, 10)
}

func durationToText(blocks int32) string {
switch {
case blocks <= 9:
return fmt.Sprintf("%v mins", blocks*10)
case blocks <= 48*6:
return fmt.Sprintf("%v hours", blocks/6)
default:
return fmt.Sprintf("%v days", blocks/(6*24))
}
}

func confirmPayReq(ctx *cli.Context, client lnrpc.LightningClient, payReq string) error {
ctxb := context.Background()

Expand Down Expand Up @@ -2335,6 +2406,14 @@ var payInvoiceCommand = cli.Command{
Name: "force, f",
Usage: "will skip payment request confirmation",
},
cli.BoolFlag{
Name: "probe",
Usage: "use pre-payment probing",
},
cli.BoolFlag{
Name: "estimatefee",
Usage: "return a realistic fee estimate, but do not make the payment",
},
},
Action: actionDecorator(payInvoice),
}
Expand All @@ -2354,18 +2433,49 @@ func payInvoice(ctx *cli.Context) error {
return fmt.Errorf("pay_req argument missing")
}

feeLimit, err := retrieveFeeLimit(ctx)
decodeResp, err := client.DecodePayReq(context.Background(),
&lnrpc.PayReqString{
PayReq: payReq,
},
)
if err != nil {
return err
}

if !ctx.Bool("force") {
err = confirmPayReq(ctx, client, payReq)
if err != nil {
return err
hash, err := lntypes.MakeHashFromStr(decodeResp.PaymentHash)
if err != nil {
return err
}

info, err := client.GetInfo(context.Background(), &lnrpc.GetInfoRequest{})
if err != nil {
return err
}

graph, err := client.DescribeGraph(context.Background(),
&lnrpc.ChannelGraphRequest{})
if err != nil {
return err
}

aliases := make(map[string]string)
for _, node := range graph.Nodes {
alias := node.Alias
if alias != "" {
alias = alias + "-"
}
alias = alias + node.PubKey[:6]
aliases[node.PubKey] = alias
}

feeLimit, err := retrieveFeeLimit(ctx)
if err != nil {
return err
}

probe := ctx.Bool("probe")
estimateFee := ctx.Bool("estimatefee")

req := &lnrpc.SendRequest{
PaymentRequest: payReq,
Amt: ctx.Int64("amt"),
Expand All @@ -2374,7 +2484,54 @@ func payInvoice(ctx *cli.Context) error {
CltvLimit: uint32(ctx.Int(cltvLimitFlag.Name)),
}

return sendPaymentRequest(client, req)
switch {
case !probe && !estimateFee:
req.Mode = lnrpc.PaymentMode_PAY_DIRECT

if !ctx.Bool("force") {
err = confirmPayReq(ctx, client, payReq)
if err != nil {
return err
}
}

return sendPaymentRequest(client, req)

case probe && !estimateFee:
req.Mode = lnrpc.PaymentMode_PROBE_ONLY

resp, err := client.SendPaymentSync(context.Background(), req)
if err != nil {
return err
}

if resp.PaymentError != "" {
return errors.New(resp.PaymentError)
}

err = confirmPayReqProbed(
decodeResp, ctx.Int64("amt"), resp.PaymentRoute,
int32(info.BlockHeight), aliases,
)
if err != nil {
return err
}

req := &lnrpc.SendToRouteRequest{
PaymentHash: hash[:],
Routes: []*lnrpc.Route{resp.PaymentRoute},
}

return sendToRouteRequest(ctx, req)

case !probe && estimateFee:
req.Mode = lnrpc.PaymentMode_PROBE_ONLY

return sendPaymentRequest(client, req)
default:
return errors.New("cannot use probe and estimatefee flags at the same time")
}

}

var sendToRouteCommand = cli.Command{
Expand Down
12 changes: 9 additions & 3 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1078,9 +1078,15 @@ func parseAndSetDebugLevels(debugLevel string) error {
// issues and update the log levels accordingly.
for _, logLevelPair := range strings.Split(debugLevel, ",") {
if !strings.Contains(logLevelPair, "=") {
str := "The specified debug level contains an invalid " +
"subsystem/level pair [%v]"
return fmt.Errorf(str, logLevelPair)
if !validLogLevel(logLevelPair) {
str := "The specified debug level [%v] is invalid"
return fmt.Errorf(str, logLevelPair)
}

// Change the logging level for all subsystems.
setLogLevels(logLevelPair)

continue
}

// Extract the specified subsystem and log level.
Expand Down
7 changes: 0 additions & 7 deletions lnrpc/routerrpc/config_active.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
package routerrpc

import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/routing"
)
Expand All @@ -23,12 +22,6 @@ type Config struct {
// server will find the macaroon named DefaultRouterMacFilename.
NetworkDir string

// ActiveNetParams are the network parameters of the primary network
// that the route is operating on. This is necessary so we can ensure
// that we receive payment requests that send to destinations on our
// network.
ActiveNetParams *chaincfg.Params

// MacService is the main macaroon service that we'll use to handle
// authentication for the Router rpc server.
MacService *macaroons.Service
Expand Down
Loading