Skip to content

Commit c216bdc

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 or if --pay_req is specified, however it appears --pay_req codepath is unreachable, left for robustness. 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 cb481df commit c216bdc

File tree

3 files changed

+281
-0
lines changed

3 files changed

+281
-0
lines changed

cmd/commands/cmd_payments.go

+57
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,31 @@ 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. It should probably not be allowed with a payment
489+
// request.
490+
if ctx.IsSet("pay_req") ||
491+
(!ctx.Bool("keysend") && !ctx.Bool(ampFlag.Name)) {
492+
493+
return fmt.Errorf("--route_hints can only be used " +
494+
"with --keysend or --amp and should not be" +
495+
" used with --pay_req")
496+
}
497+
498+
// Parse the route hints JSON.
499+
routeHintsJSON := ctx.String("route_hints")
500+
var routeHints []*lnrpc.RouteHint
501+
502+
err := json.Unmarshal([]byte(routeHintsJSON), &routeHints)
503+
if err != nil {
504+
return fmt.Errorf("error unmarshaling route_hints "+
505+
"json: %w", err)
506+
}
507+
508+
req.RouteHints = routeHints
509+
}
510+
476511
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
477512
}
478513

@@ -1154,6 +1189,15 @@ var queryRoutesCommand = cli.Command{
11541189
blindedBaseFlag,
11551190
blindedPPMFlag,
11561191
blindedCLTVFlag,
1192+
cli.StringFlag{
1193+
Name: "route_hints",
1194+
Usage: "route hints for searching through private " +
1195+
"channels. eg: " +
1196+
`[{"hop_hints":[{"node_id":"A","chan_id":1,` +
1197+
`"fee_base_msat":2,` +
1198+
`"fee_proportional_millionths":3,` +
1199+
`"cltv_expiry_delta":4}]}]`,
1200+
},
11571201
},
11581202
Action: actionDecorator(queryRoutes),
11591203
}
@@ -1248,6 +1292,19 @@ func queryRoutes(ctx *cli.Context) error {
12481292
BlindedPaymentPaths: blindedRoutes,
12491293
}
12501294

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

itest/list_on_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,10 @@ var allTestCases = []*lntest.TestCase{
326326
Name: "query routes",
327327
TestFunc: testQueryRoutes,
328328
},
329+
{
330+
Name: "query routes routehints",
331+
TestFunc: testQueryRoutesRouteHints,
332+
},
329333
{
330334
Name: "route fee cutoff",
331335
TestFunc: testRouteFeeCutoff,
@@ -410,6 +414,14 @@ var allTestCases = []*lntest.TestCase{
410414
Name: "send payment keysend mpp fail",
411415
TestFunc: testSendPaymentKeysendMPPFail,
412416
},
417+
{
418+
Name: "send payment routehints keysend",
419+
TestFunc: testSendPaymentRouteHintsKeysend,
420+
},
421+
{
422+
Name: "send payment routehints amp",
423+
TestFunc: testSendPaymentRouteHintsAMP,
424+
},
413425
{
414426
Name: "forward interceptor dedup htlcs",
415427
TestFunc: testForwardInterceptorDedupHtlc,

itest/lnd_payment_test.go

+212
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,214 @@ 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+
alice := ht.NewNode("Alice", nil)
1406+
defer ht.Shutdown(alice)
1407+
bob := ht.NewNode("Bob", nil)
1408+
defer ht.Shutdown(bob)
1409+
// Ensure Carol accepts keysend payments.
1410+
carol := ht.NewNode("Carol", []string{"--accept-keysend"})
1411+
defer ht.Shutdown(carol)
1412+
1413+
ht.ConnectNodes(alice, bob)
1414+
ht.ConnectNodes(bob, carol)
1415+
1416+
// Fund Alice and Bob.
1417+
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
1418+
ht.FundCoins(btcutil.SatoshiPerBitcoin, bob)
1419+
1420+
// Open channels: Alice -> Bob (public), Bob -> Carol (private).
1421+
const chanAmt = btcutil.Amount(1_000_000)
1422+
aliceBobChanPoint := ht.OpenChannel(
1423+
alice, bob, lntest.OpenChannelParams{Amt: chanAmt},
1424+
)
1425+
defer ht.CloseChannel(alice, aliceBobChanPoint)
1426+
bobCarolChanPoint := ht.OpenChannel(bob, carol,
1427+
lntest.OpenChannelParams{Amt: chanAmt, Private: true},
1428+
)
1429+
defer ht.CloseChannel(bob, bobCarolChanPoint)
1430+
1431+
// Manually create route hints for the private Bob -> Carol channel.
1432+
bobChan := ht.GetChannelByChanPoint(bob, bobCarolChanPoint)
1433+
require.NotNil(ht, bobChan, "Bob should have channel with Carol")
1434+
1435+
hints := []*lnrpc.RouteHint{{HopHints: []*lnrpc.HopHint{
1436+
{
1437+
NodeId: bob.PubKeyStr,
1438+
ChanId: bobChan.ChanId,
1439+
FeeBaseMsat: 1000,
1440+
FeeProportionalMillionths: 1,
1441+
CltvExpiryDelta: routing.MinCLTVDelta,
1442+
}}},
1443+
}
1444+
1445+
// Prepare Keysend payment details.
1446+
preimage := ht.RandomPreimage()
1447+
payHash := preimage.Hash()
1448+
destBytes, err := hex.DecodeString(carol.PubKeyStr)
1449+
require.NoError(ht, err)
1450+
1451+
sendReq := &routerrpc.SendPaymentRequest{
1452+
Dest: destBytes,
1453+
Amt: 10_000,
1454+
PaymentHash: payHash[:],
1455+
FinalCltvDelta: int32(routing.MinCLTVDelta),
1456+
DestCustomRecords: map[uint64][]byte{
1457+
record.KeySendType: preimage[:],
1458+
},
1459+
RouteHints: hints,
1460+
FeeLimitSat: int64(chanAmt),
1461+
TimeoutSeconds: 60,
1462+
}
1463+
1464+
// Send keysend payment and assert success.
1465+
ht.AssertPaymentStatusFromStream(
1466+
alice.RPC.SendPayment(sendReq),
1467+
lnrpc.Payment_SUCCEEDED,
1468+
)
1469+
}
1470+
1471+
// testSendPaymentRouteHintsAMP tests sending an AMP payment using
1472+
// manually provided route hints derived from a private channel.
1473+
func testSendPaymentRouteHintsAMP(ht *lntest.HarnessTest) {
1474+
// Setup a three-node network: Alice -> Bob -> Carol.
1475+
alice := ht.NewNode("Alice", nil)
1476+
defer ht.Shutdown(alice)
1477+
bob := ht.NewNode("Bob", nil)
1478+
defer ht.Shutdown(bob)
1479+
carol := ht.NewNode("Carol", nil)
1480+
defer ht.Shutdown(carol)
1481+
1482+
ht.ConnectNodes(alice, bob)
1483+
ht.ConnectNodes(bob, carol)
1484+
1485+
// Fund Alice and Bob.
1486+
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
1487+
ht.FundCoins(btcutil.SatoshiPerBitcoin, bob)
1488+
1489+
// Open channels: Alice -> Bob (public), Bob -> Carol (private).
1490+
const chanAmt = btcutil.Amount(1_000_000)
1491+
aliceBobChanPoint := ht.OpenChannel(
1492+
alice, bob, lntest.OpenChannelParams{Amt: chanAmt},
1493+
)
1494+
defer ht.CloseChannel(alice, aliceBobChanPoint)
1495+
bobCarolChanPoint := ht.OpenChannel(bob, carol,
1496+
lntest.OpenChannelParams{Amt: chanAmt, Private: true},
1497+
)
1498+
defer ht.CloseChannel(bob, bobCarolChanPoint)
1499+
1500+
// Manually create route hints for the private Bob -> Carol channel.
1501+
bobChan := ht.GetChannelByChanPoint(bob, bobCarolChanPoint)
1502+
require.NotNil(ht, bobChan, "Bob should have channel with Carol")
1503+
1504+
hints := []*lnrpc.RouteHint{{HopHints: []*lnrpc.HopHint{
1505+
{
1506+
NodeId: bob.PubKeyStr,
1507+
ChanId: bobChan.ChanId,
1508+
FeeBaseMsat: 1000,
1509+
FeeProportionalMillionths: 1,
1510+
CltvExpiryDelta: routing.MinCLTVDelta,
1511+
}}},
1512+
}
1513+
1514+
// Carol creates an AMP invoice
1515+
const paymentAmtSat = 10_000
1516+
carolDestBytes, err := hex.DecodeString(carol.PubKeyStr)
1517+
require.NoError(ht, err)
1518+
1519+
invoiceTemplate := &lnrpc.Invoice{
1520+
Value: paymentAmtSat,
1521+
Private: true,
1522+
IsAmp: true,
1523+
}
1524+
addInvoiceResp := carol.RPC.AddInvoice(invoiceTemplate)
1525+
1526+
// We need to decode to get CltvExpiry.
1527+
decodedPayReq := alice.RPC.DecodePayReq(addInvoiceResp.PaymentRequest)
1528+
1529+
sendReq := &routerrpc.SendPaymentRequest{
1530+
Dest: carolDestBytes,
1531+
Amt: paymentAmtSat,
1532+
// PaymentHash is omitted for AMP
1533+
PaymentAddr: addInvoiceResp.PaymentAddr,
1534+
FinalCltvDelta: int32(decodedPayReq.CltvExpiry),
1535+
Amp: true,
1536+
RouteHints: hints,
1537+
FeeLimitSat: int64(chanAmt),
1538+
TimeoutSeconds: 60,
1539+
}
1540+
1541+
// Send AMP payment and assert success
1542+
ht.AssertPaymentStatusFromStream(
1543+
alice.RPC.SendPayment(sendReq),
1544+
lnrpc.Payment_SUCCEEDED,
1545+
)
1546+
}
1547+
1548+
// testQueryRoutesRouteHints tests that QueryRoutes successfully
1549+
// finds a route through a private channel when provided with route hints.
1550+
func testQueryRoutesRouteHints(ht *lntest.HarnessTest) {
1551+
// Setup a three-node network: Alice -> Bob -> Carol.
1552+
alice := ht.NewNode("Alice", nil)
1553+
defer ht.Shutdown(alice)
1554+
bob := ht.NewNode("Bob", nil)
1555+
defer ht.Shutdown(bob)
1556+
carol := ht.NewNode("Carol", nil)
1557+
defer ht.Shutdown(carol)
1558+
1559+
ht.ConnectNodes(alice, bob)
1560+
ht.ConnectNodes(bob, carol)
1561+
1562+
// Fund Alice and Bob.
1563+
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
1564+
ht.FundCoins(btcutil.SatoshiPerBitcoin, bob)
1565+
1566+
// Open channels: Alice -> Bob (public), Bob -> Carol (private).
1567+
const chanAmt = btcutil.Amount(1_000_000)
1568+
aliceBobChanPoint := ht.OpenChannel(
1569+
alice, bob, lntest.OpenChannelParams{Amt: chanAmt},
1570+
)
1571+
defer ht.CloseChannel(alice, aliceBobChanPoint)
1572+
bobCarolChanPoint := ht.OpenChannel(bob, carol,
1573+
lntest.OpenChannelParams{Amt: chanAmt, Private: true},
1574+
)
1575+
defer ht.CloseChannel(bob, bobCarolChanPoint)
1576+
1577+
// Manually create route hints for the private Bob -> Carol channel.
1578+
bobChan := ht.GetChannelByChanPoint(bob, bobCarolChanPoint)
1579+
require.NotNil(ht, bobChan, "Bob should have channel with Carol")
1580+
1581+
hints := []*lnrpc.RouteHint{{HopHints: []*lnrpc.HopHint{
1582+
{
1583+
NodeId: bob.PubKeyStr,
1584+
ChanId: bobChan.ChanId,
1585+
FeeBaseMsat: 1000,
1586+
FeeProportionalMillionths: 1,
1587+
CltvExpiryDelta: routing.MinCLTVDelta,
1588+
}}},
1589+
}
1590+
1591+
queryReq := &lnrpc.QueryRoutesRequest{
1592+
PubKey: carol.PubKeyStr,
1593+
Amt: 10_000,
1594+
FinalCltvDelta: int32(routing.MinCLTVDelta),
1595+
RouteHints: hints,
1596+
UseMissionControl: true, // Use MC for realistic pathfinding
1597+
}
1598+
1599+
routes := alice.RPC.QueryRoutes(queryReq)
1600+
1601+
// Assert that a route was found and it goes Alice -> Bob -> Carol.
1602+
require.NotEmpty(ht, routes.Routes, "QueryRoutes should find a route")
1603+
require.Len(ht, routes.Routes[0].Hops, 2, "Route should have 2 hops")
1604+
1605+
hop1 := routes.Routes[0].Hops[0]
1606+
hop2 := routes.Routes[0].Hops[1]
1607+
1608+
require.Equal(ht, bob.PubKeyStr, hop1.PubKey, "Hop 1 should be Bob")
1609+
require.Equal(ht, carol.PubKeyStr, hop2.PubKey, "Hop 2 should be Carol")
1610+
}

0 commit comments

Comments
 (0)