Skip to content

Commit 85ef9f7

Browse files
committed
lncli: Add --route_hints flag to sendpayment, queryroutes (fixes lightningnetwork#6601)
Adds --route_hints flag to sendpayment for --keysend and --amp payments. CLI will error if neither is specified. Hints should be JSON encoded (see usage for example). Adds --route_hints flag to queryroutes (no restrictions). Adds integration tests for query routes over RPC, and manual keysend/amp over RPC to emulate the new feature. Testing revealed route hinting did not work for standard payment (w/ or w/o --pay_addr).
1 parent 7e50b84 commit 85ef9f7

File tree

4 files changed

+286
-0
lines changed

4 files changed

+286
-0
lines changed

cmd/commands/cmd_payments.go

+54
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"crypto/rand"
77
"encoding/hex"
8+
"encoding/json"
89
"errors"
910
"fmt"
1011
"io"
@@ -254,6 +255,15 @@ var SendPaymentCommand = cli.Command{
254255
Name: "keysend",
255256
Usage: "will generate a pre-image and encode it in the sphinx packet, a dest must be set [experimental]",
256257
},
258+
cli.StringFlag{
259+
Name: "route_hints",
260+
Usage: "route hints for reaching the destination of a" +
261+
" manual --keysend or --amp payment. eg: " +
262+
`[{"hop_hints":[{"node_id":"A","chan_id":1,` +
263+
`"fee_base_msat":2,` +
264+
`"fee_proportional_millionths":3,` +
265+
`"cltv_expiry_delta":4}]}]`,
266+
},
257267
),
258268
Action: SendPayment,
259269
}
@@ -473,6 +483,28 @@ func SendPayment(ctx *cli.Context) error {
473483

474484
req.PaymentAddr = payAddr
475485

486+
if ctx.IsSet("route_hints") {
487+
// Route hints can only be used with a manual keysend or amp
488+
// payment.
489+
if !ctx.Bool("keysend") && !ctx.Bool(ampFlag.Name) {
490+
return fmt.Errorf("--route_hints can only be used " +
491+
"with --keysend or --amp and should not be" +
492+
" used with --pay_req")
493+
}
494+
495+
// Parse the route hints JSON.
496+
routeHintsJSON := ctx.String("route_hints")
497+
var routeHints []*lnrpc.RouteHint
498+
499+
err := json.Unmarshal([]byte(routeHintsJSON), &routeHints)
500+
if err != nil {
501+
return fmt.Errorf("error unmarshaling route_hints "+
502+
"json: %w", err)
503+
}
504+
505+
req.RouteHints = routeHints
506+
}
507+
476508
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
477509
}
478510

@@ -1154,6 +1186,15 @@ var queryRoutesCommand = cli.Command{
11541186
blindedBaseFlag,
11551187
blindedPPMFlag,
11561188
blindedCLTVFlag,
1189+
cli.StringFlag{
1190+
Name: "route_hints",
1191+
Usage: "route hints for searching through private " +
1192+
"channels. eg: " +
1193+
`[{"hop_hints":[{"node_id":"A","chan_id":1,` +
1194+
`"fee_base_msat":2,` +
1195+
`"fee_proportional_millionths":3,` +
1196+
`"cltv_expiry_delta":4}]}]`,
1197+
},
11571198
},
11581199
Action: actionDecorator(queryRoutes),
11591200
}
@@ -1248,6 +1289,19 @@ func queryRoutes(ctx *cli.Context) error {
12481289
BlindedPaymentPaths: blindedRoutes,
12491290
}
12501291

1292+
if ctx.IsSet("route_hints") {
1293+
routeHintsJSON := ctx.String("route_hints")
1294+
var routeHints []*lnrpc.RouteHint
1295+
1296+
err := json.Unmarshal([]byte(routeHintsJSON), &routeHints)
1297+
if err != nil {
1298+
return fmt.Errorf("error unmarshaling route_hints "+
1299+
"json: %w", err)
1300+
}
1301+
1302+
req.RouteHints = routeHints
1303+
}
1304+
12511305
route, err := client.QueryRoutes(ctxc, req)
12521306
if err != nil {
12531307
return err

docs/release-notes/release-notes-0.20.0.md

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828

2929
## lncli Additions
3030

31+
* [`lncli sendpayment --keysend/--amp` and `lncli queryroutes` now support the
32+
`--route_hints` flag](https://github.com/lightningnetwork/lnd/pull/9721) to
33+
support routing through private channels.
34+
3135
# Improvements
3236
## Functional Updates
3337

itest/list_on_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,10 @@ var allTestCases = []*lntest.TestCase{
327327
Name: "query routes",
328328
TestFunc: testQueryRoutes,
329329
},
330+
{
331+
Name: "query routes routehints",
332+
TestFunc: testQueryRoutesRouteHints,
333+
},
330334
{
331335
Name: "route fee cutoff",
332336
TestFunc: testRouteFeeCutoff,
@@ -411,6 +415,14 @@ var allTestCases = []*lntest.TestCase{
411415
Name: "send payment keysend mpp fail",
412416
TestFunc: testSendPaymentKeysendMPPFail,
413417
},
418+
{
419+
Name: "send payment routehints keysend",
420+
TestFunc: testSendPaymentRouteHintsKeysend,
421+
},
422+
{
423+
Name: "send payment routehints amp",
424+
TestFunc: testSendPaymentRouteHintsAMP,
425+
},
414426
{
415427
Name: "forward interceptor dedup htlcs",
416428
TestFunc: testForwardInterceptorDedupHtlc,

itest/lnd_payment_test.go

+216
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/lightningnetwork/lnd/lntypes"
2323
"github.com/lightningnetwork/lnd/lnwire"
2424
"github.com/lightningnetwork/lnd/record"
25+
"github.com/lightningnetwork/lnd/routing"
2526
"github.com/stretchr/testify/require"
2627
)
2728

@@ -1396,3 +1397,218 @@ func testSendPaymentKeysendMPPFail(ht *lntest.HarnessTest) {
13961397
_, err = ht.ReceivePaymentUpdate(client)
13971398
require.Error(ht, err)
13981399
}
1400+
1401+
// testSendPaymentRouteHintsKeysend tests sending a keysend payment using
1402+
// manually provided route hints derived from a private channel.
1403+
func testSendPaymentRouteHintsKeysend(ht *lntest.HarnessTest) {
1404+
// Setup a three-node network: Alice -> Bob -> Carol.
1405+
// The Bob->Carol channel is private.
1406+
const chanAmt = btcutil.Amount(1_000_000)
1407+
alice, bob, carol, _, bobCarolCP, cleanup := setupThreeNodeNetwork(
1408+
ht, chanAmt, true,
1409+
)
1410+
defer cleanup()
1411+
1412+
// Manually create route hints for the private Bob -> Carol channel.
1413+
bobChan := ht.GetChannelByChanPoint(bob, bobCarolCP)
1414+
require.NotNil(ht, bobChan, "Bob should have channel with Carol")
1415+
hints := createRouteHintFromChannel(bob, bobChan)
1416+
1417+
// Prepare Keysend payment details.
1418+
preimage := ht.RandomPreimage()
1419+
payHash := preimage.Hash()
1420+
destBytes, err := hex.DecodeString(carol.PubKeyStr)
1421+
require.NoError(ht, err)
1422+
1423+
sendReq := &routerrpc.SendPaymentRequest{
1424+
Dest: destBytes,
1425+
Amt: 10_000,
1426+
PaymentHash: payHash[:],
1427+
FinalCltvDelta: int32(routing.MinCLTVDelta),
1428+
DestCustomRecords: map[uint64][]byte{
1429+
record.KeySendType: preimage[:],
1430+
},
1431+
// RouteHints omitted initially.
1432+
FeeLimitSat: int64(chanAmt),
1433+
TimeoutSeconds: 30,
1434+
}
1435+
1436+
// Attempt keysend payment without hints - should fail.
1437+
ht.AssertPaymentStatusFromStream(
1438+
alice.RPC.SendPayment(sendReq),
1439+
lnrpc.Payment_FAILED,
1440+
)
1441+
1442+
// Now, add the hints and try again - should succeed.
1443+
sendReq.RouteHints = hints
1444+
ht.AssertPaymentStatusFromStream(
1445+
alice.RPC.SendPayment(sendReq),
1446+
lnrpc.Payment_SUCCEEDED,
1447+
)
1448+
}
1449+
1450+
// testSendPaymentRouteHintsAMP tests sending an AMP payment using
1451+
// manually provided route hints derived from a private channel.
1452+
func testSendPaymentRouteHintsAMP(ht *lntest.HarnessTest) {
1453+
// Setup a three-node network: Alice -> Bob -> Carol.
1454+
// The Bob->Carol channel is private.
1455+
const chanAmt = btcutil.Amount(1_000_000)
1456+
alice, bob, carol, _, bobCarolCP, cleanup := setupThreeNodeNetwork(
1457+
ht, chanAmt, true,
1458+
)
1459+
defer cleanup()
1460+
1461+
// Manually create route hints for the private Bob -> Carol channel.
1462+
bobChan := ht.GetChannelByChanPoint(bob, bobCarolCP)
1463+
require.NotNil(ht, bobChan, "Bob should have channel with Carol")
1464+
hints := createRouteHintFromChannel(bob, bobChan)
1465+
1466+
// Carol creates an AMP invoice
1467+
const paymentAmtSat = 10_000
1468+
carolDestBytes, err := hex.DecodeString(carol.PubKeyStr)
1469+
require.NoError(ht, err)
1470+
1471+
invoiceTemplate := &lnrpc.Invoice{
1472+
Value: paymentAmtSat,
1473+
Private: true,
1474+
IsAmp: true,
1475+
}
1476+
addInvoiceResp := carol.RPC.AddInvoice(invoiceTemplate)
1477+
1478+
// We need to decode to get CltvExpiry.
1479+
decodedPayReq := alice.RPC.DecodePayReq(addInvoiceResp.PaymentRequest)
1480+
1481+
sendReq := &routerrpc.SendPaymentRequest{
1482+
Dest: carolDestBytes,
1483+
Amt: paymentAmtSat,
1484+
// PaymentHash is omitted for AMP
1485+
PaymentAddr: addInvoiceResp.PaymentAddr,
1486+
FinalCltvDelta: int32(decodedPayReq.CltvExpiry),
1487+
Amp: true,
1488+
// RouteHints omitted initially.
1489+
FeeLimitSat: int64(chanAmt),
1490+
TimeoutSeconds: 30,
1491+
}
1492+
1493+
// Attempt AMP payment without hints - should fail.
1494+
ht.AssertPaymentStatusFromStream(
1495+
alice.RPC.SendPayment(sendReq),
1496+
lnrpc.Payment_FAILED,
1497+
)
1498+
1499+
// Now, add the hints and try again - should succeed.
1500+
sendReq.RouteHints = hints
1501+
ht.AssertPaymentStatusFromStream(
1502+
alice.RPC.SendPayment(sendReq),
1503+
lnrpc.Payment_SUCCEEDED,
1504+
)
1505+
}
1506+
1507+
// testQueryRoutesRouteHints tests that QueryRoutes successfully
1508+
// finds a route through a private channel when provided with route hints.
1509+
func testQueryRoutesRouteHints(ht *lntest.HarnessTest) {
1510+
// Setup a three-node network: Alice -> Bob -> Carol.
1511+
// The Bob->Carol channel is private.
1512+
const chanAmt = btcutil.Amount(1_000_000)
1513+
alice, bob, carol, _, bobCarolCP, cleanup := setupThreeNodeNetwork(
1514+
ht, chanAmt, true,
1515+
)
1516+
defer cleanup()
1517+
1518+
// Manually create route hints for the private Bob -> Carol channel.
1519+
bobChan := ht.GetChannelByChanPoint(bob, bobCarolCP)
1520+
require.NotNil(ht, bobChan, "Bob should have channel with Carol")
1521+
hints := createRouteHintFromChannel(bob, bobChan)
1522+
1523+
queryReq := &lnrpc.QueryRoutesRequest{
1524+
PubKey: carol.PubKeyStr,
1525+
Amt: 10_000,
1526+
FinalCltvDelta: int32(routing.MinCLTVDelta),
1527+
// RouteHints omitted initially.
1528+
UseMissionControl: true, // Use MC for realistic pathfinding
1529+
}
1530+
1531+
// Query routes without hints - should fail (find no routes).
1532+
// Call the client directly to check the error without halting the test.
1533+
_, err := alice.RPC.LN.QueryRoutes(ht.Context(), queryReq)
1534+
require.Error(ht, err,
1535+
"QueryRoutes without hints should return an error")
1536+
1537+
// Now add the hints and query again - should succeed.
1538+
// Use the helper function here as we expect success.
1539+
queryReq.RouteHints = hints
1540+
routes := alice.RPC.QueryRoutes(queryReq)
1541+
1542+
// Assert that a route was found and it goes Alice -> Bob -> Carol.
1543+
require.NotEmpty(ht, routes.Routes,
1544+
"QueryRoutes with hints should find a route")
1545+
require.Len(ht, routes.Routes[0].Hops, 2, "Route should have 2 hops")
1546+
1547+
hop1 := routes.Routes[0].Hops[0]
1548+
hop2 := routes.Routes[0].Hops[1]
1549+
1550+
require.Equal(ht, bob.PubKeyStr, hop1.PubKey, "Hop 1 should be Bob")
1551+
require.Equal(ht, carol.PubKeyStr, hop2.PubKey, "Hop 2 should be Carol")
1552+
}
1553+
1554+
// createRouteHintFromChannel takes a source node and a channel object and
1555+
// constructs a RouteHint slice containing a single hop hint for that channel.
1556+
func createRouteHintFromChannel(sourceNode *node.HarnessNode,
1557+
channel *lnrpc.Channel) []*lnrpc.RouteHint {
1558+
1559+
return []*lnrpc.RouteHint{{HopHints: []*lnrpc.HopHint{
1560+
{
1561+
NodeId: sourceNode.PubKeyStr,
1562+
ChanId: channel.ChanId,
1563+
FeeBaseMsat: 1000,
1564+
FeeProportionalMillionths: 1,
1565+
CltvExpiryDelta: routing.MinCLTVDelta,
1566+
}}},
1567+
}
1568+
}
1569+
1570+
// setupThreeNodeNetwork sets up a standard three-node network topology:
1571+
// Alice -> Bob -> Carol. It creates the nodes, connects them, funds Alice and
1572+
// Bob, opens a channel between Alice and Bob, and optionally opens a private
1573+
// channel between Bob and Carol. It returns the nodes, channel points, and a
1574+
// cleanup function.
1575+
func setupThreeNodeNetwork(ht *lntest.HarnessTest, chanAmt btcutil.Amount,
1576+
bobCarolPrivate bool) (*node.HarnessNode, *node.HarnessNode,
1577+
*node.HarnessNode, *lnrpc.ChannelPoint, *lnrpc.ChannelPoint, func()) {
1578+
1579+
// Create nodes.
1580+
alice := ht.NewNode("Alice", nil)
1581+
bob := ht.NewNode("Bob", nil)
1582+
carol := ht.NewNode("Carol", nil)
1583+
1584+
// Connect nodes.
1585+
ht.ConnectNodes(alice, bob)
1586+
ht.ConnectNodes(bob, carol)
1587+
1588+
// Fund nodes.
1589+
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
1590+
ht.FundCoins(btcutil.SatoshiPerBitcoin, bob)
1591+
1592+
// Open Alice -> Bob channel (public).
1593+
aliceBobChanPoint := ht.OpenChannel(
1594+
alice, bob, lntest.OpenChannelParams{Amt: chanAmt},
1595+
)
1596+
1597+
// Open Bob -> Carol channel (potentially private).
1598+
bobCarolParams := lntest.OpenChannelParams{
1599+
Amt: chanAmt,
1600+
Private: bobCarolPrivate,
1601+
}
1602+
bobCarolChanPoint := ht.OpenChannel(bob, carol, bobCarolParams)
1603+
1604+
// Define cleanup function.
1605+
cleanup := func() {
1606+
ht.CloseChannel(alice, aliceBobChanPoint)
1607+
ht.CloseChannel(bob, bobCarolChanPoint)
1608+
ht.Shutdown(alice)
1609+
ht.Shutdown(bob)
1610+
ht.Shutdown(carol)
1611+
}
1612+
1613+
return alice, bob, carol, aliceBobChanPoint, bobCarolChanPoint, cleanup
1614+
}

0 commit comments

Comments
 (0)