diff --git a/lnrpc/invoicesrpc/addinvoice.go b/lnrpc/invoicesrpc/addinvoice.go index afa7e708..8c93502f 100644 --- a/lnrpc/invoicesrpc/addinvoice.go +++ b/lnrpc/invoicesrpc/addinvoice.go @@ -94,6 +94,10 @@ type AddInvoiceData struct { // HodlInvoice signals that this invoice shouldn't be settled // immediately upon receiving the payment. HodlInvoice bool + + // RouteHints are optional route hints that can each be individually used + // to assist in reaching the invoice's destination. + RouteHints [][]zpay32.HopHint } // AddInvoice attempts to add a new invoice to the invoice database. Any @@ -246,6 +250,27 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig, options = append(options, zpay32.CLTVExpiry(uint64(defaultDelta))) } + // We make sure that the given invoice routing hints number is within the + // valid range + if len(invoice.RouteHints) > 20 { + return nil, nil, fmt.Errorf("number of routing hints must not exceed " + + "maximum of 20") + } + + // We continue by populating the requested routing hints indexing their + // corresponding channels so we won't duplicate them. + forcedHints := make(map[uint64]struct{}) + for _, h := range invoice.RouteHints { + if len(h) == 0 { + return nil, nil, fmt.Errorf("number of hop hint within a route must " + + "be positive") + } + options = append(options, zpay32.RouteHint(h)) + + // Only this first hop is our direct channel. + forcedHints[h[0].ChannelID] = struct{}{} + } + // If we were requested to include routing hints in the invoice, then // we'll fetch all of our available private channels and create routing // hints for them. @@ -256,11 +281,21 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig, } if len(openChannels) > 0 { + // We filter the channels by excluding the ones that were specified by + // the caller and were already added. + var filteredChannels []*channeldb.OpenChannel + for _, c := range openChannels { + if _, ok := forcedHints[c.ShortChanID().ToUint64()]; ok { + continue + } + filteredChannels = append(filteredChannels, c) + } + // We'll restrict the number of individual route hints // to 20 to avoid creating overly large invoices. - const numMaxHophints = 20 + numMaxHophints := 20 - len(forcedHints) hopHints := selectHopHints( - amtMSat, cfg, openChannels, numMaxHophints, + amtMSat, cfg, filteredChannels, numMaxHophints, ) options = append(options, hopHints...) diff --git a/lnrpc/invoicesrpc/invoices_server.go b/lnrpc/invoicesrpc/invoices_server.go index 6ed36b0f..bbcdc461 100644 --- a/lnrpc/invoicesrpc/invoices_server.go +++ b/lnrpc/invoicesrpc/invoices_server.go @@ -289,6 +289,11 @@ func (s *Server) AddHoldInvoice(ctx context.Context, return nil, err } + // Convert the passed routing hints to the required format. + routeHints, err := CreateZpay32HopHints(invoice.RouteHints) + if err != nil { + return nil, err + } addInvoiceData := &AddInvoiceData{ Memo: invoice.Memo, Hash: &hash, @@ -300,6 +305,7 @@ func (s *Server) AddHoldInvoice(ctx context.Context, Private: invoice.Private, HodlInvoice: true, Preimage: nil, + RouteHints: routeHints, } _, dbInvoice, err := AddInvoice(ctx, addInvoiceCfg, addInvoiceData) diff --git a/lnrpc/invoicesrpc/utils.go b/lnrpc/invoicesrpc/utils.go index 9ae1818e..1d85ca9a 100644 --- a/lnrpc/invoicesrpc/utils.go +++ b/lnrpc/invoicesrpc/utils.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/lnrpc" @@ -204,3 +205,31 @@ func CreateRPCRouteHints(routeHints [][]zpay32.HopHint) []*lnrpc.RouteHint { return res } + +// CreateZpay32HopHints takes in the lnrpc form of route hints and converts them +// into an invoice decoded form. +func CreateZpay32HopHints(routeHints []*lnrpc.RouteHint) ([][]zpay32.HopHint, error) { + var res [][]zpay32.HopHint + for _, route := range routeHints { + hopHints := make([]zpay32.HopHint, 0, len(route.HopHints)) + for _, hop := range route.HopHints { + pubKeyBytes, err := hex.DecodeString(hop.NodeId) + if err != nil { + return nil, err + } + p, err := btcec.ParsePubKey(pubKeyBytes, btcec.S256()) + if err != nil { + return nil, err + } + hopHints = append(hopHints, zpay32.HopHint{ + NodeID: p, + ChannelID: hop.ChanId, + FeeBaseMSat: hop.FeeBaseMsat, + FeeProportionalMillionths: hop.FeeProportionalMillionths, + CLTVExpiryDelta: uint16(hop.CltvExpiryDelta), + }) + } + res = append(res, hopHints) + } + return res, nil +} diff --git a/lntest/itest/lnd_single_hop_invoice_test.go b/lntest/itest/lnd_single_hop_invoice_test.go index 83d914a1..9827f2cc 100644 --- a/lntest/itest/lnd_single_hop_invoice_test.go +++ b/lntest/itest/lnd_single_hop_invoice_test.go @@ -13,6 +13,7 @@ import ( "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" ) @@ -161,6 +162,68 @@ func testSingleHopInvoice(net *lntest.NetworkHarness, t *harnessTest) { t.Fatalf(err.Error()) } + // Now create an invoice and specify routing hints. + // We will test that the routing hints are encoded properly. + hintChannel := lnwire.ShortChannelID{BlockHeight: 10} + bobPubKey := hex.EncodeToString(net.Bob.PubKey[:]) + hints := []*lnrpc.RouteHint{ + { + HopHints: []*lnrpc.HopHint{ + { + NodeId: bobPubKey, + ChanId: hintChannel.ToUint64(), + FeeBaseMsat: 1, + FeeProportionalMillionths: 1000000, + CltvExpiryDelta: 20, + }, + }, + }, + } + + invoice = &lnrpc.Invoice{ + Memo: "hints", + Value: paymentAmt, + RouteHints: hints, + } + + ctxt, _ = context.WithTimeout(ctxt, defaultTimeout) + invoiceResp, err = net.Bob.AddInvoice(ctxt, invoice) + if err != nil { + t.Fatalf("unable to add invoice: %v", err) + } + payreq, err := net.Bob.DecodePayReq(ctxt, &lnrpc.PayReqString{PayReq: invoiceResp.PaymentRequest}) + if err != nil { + t.Fatalf("failed to decode payment request %v", err) + } + if len(payreq.RouteHints) != 1 { + t.Fatalf("expected one routing hint") + } + routingHint := payreq.RouteHints[0] + if len(routingHint.HopHints) != 1 { + t.Fatalf("expected one hop hint") + } + hopHint := routingHint.HopHints[0] + if hopHint.FeeProportionalMillionths != 1000000 { + t.Fatalf("wrong FeeProportionalMillionths %v", + hopHint.FeeProportionalMillionths) + } + if hopHint.NodeId != bobPubKey { + t.Fatalf("wrong NodeId %v", + hopHint.NodeId) + } + if hopHint.ChanId != hintChannel.ToUint64() { + t.Fatalf("wrong ChanId %v", + hopHint.ChanId) + } + if hopHint.FeeBaseMsat != 1 { + t.Fatalf("wrong FeeBaseMsat %v", + hopHint.FeeBaseMsat) + } + if hopHint.CltvExpiryDelta != 20 { + t.Fatalf("wrong CltvExpiryDelta %v", + hopHint.CltvExpiryDelta) + } + ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout) closeChannelAndAssert(ctxt, t, net, net.Alice, chanPoint, false) } diff --git a/rpcserver.go b/rpcserver.go index cde67806..d7b69988 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -4745,6 +4745,11 @@ func (r *rpcServer) AddInvoice(ctx context.Context, return nil, err } + // Convert the passed routing hints to the required format. + routeHints, err := invoicesrpc.CreateZpay32HopHints(invoice.RouteHints) + if err != nil { + return nil, err + } addInvoiceData := &invoicesrpc.AddInvoiceData{ Memo: invoice.Memo, Value: value, @@ -4753,6 +4758,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context, FallbackAddr: invoice.FallbackAddr, CltvExpiry: invoice.CltvExpiry, Private: invoice.Private, + RouteHints: routeHints, } if invoice.RPreimage != nil {