Skip to content

Commit 485e482

Browse files
committed
lncli: Add --route_hints flag to sendpayment, queryroutes (fixes lightningnetwork#6601)
Adds --route_hints flag to sendpayment for --keysend payments. 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 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 b068d79 commit 485e482

File tree

4 files changed

+223
-0
lines changed

4 files changed

+223
-0
lines changed

cmd/commands/cmd_payments.go

+52
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 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,26 @@ func SendPayment(ctx *cli.Context) error {
473483

474484
req.PaymentAddr = payAddr
475485

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

@@ -1154,6 +1184,15 @@ var queryRoutesCommand = cli.Command{
11541184
blindedBaseFlag,
11551185
blindedPPMFlag,
11561186
blindedCLTVFlag,
1187+
cli.StringFlag{
1188+
Name: "route_hints",
1189+
Usage: "route hints for searching through private " +
1190+
"channels. eg: " +
1191+
`[{"hop_hints":[{"node_id":"A","chan_id":1,` +
1192+
`"fee_base_msat":2,` +
1193+
`"fee_proportional_millionths":3,` +
1194+
`"cltv_expiry_delta":4}]}]`,
1195+
},
11571196
},
11581197
Action: actionDecorator(queryRoutes),
11591198
}
@@ -1248,6 +1287,19 @@ func queryRoutes(ctx *cli.Context) error {
12481287
BlindedPaymentPaths: blindedRoutes,
12491288
}
12501289

1290+
if ctx.IsSet("route_hints") {
1291+
routeHintsJSON := ctx.String("route_hints")
1292+
var routeHints []*lnrpc.RouteHint
1293+
1294+
err := json.Unmarshal([]byte(routeHintsJSON), &routeHints)
1295+
if err != nil {
1296+
return fmt.Errorf("error unmarshaling route_hints "+
1297+
"json: %w", err)
1298+
}
1299+
1300+
req.RouteHints = routeHints
1301+
}
1302+
12511303
route, err := client.QueryRoutes(ctxc, req)
12521304
if err != nil {
12531305
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` 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

+8
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,10 @@ 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+
},
414422
{
415423
Name: "forward interceptor dedup htlcs",
416424
TestFunc: testForwardInterceptorDedupHtlc,

itest/lnd_payment_test.go

+159
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,161 @@ 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+
// testQueryRoutesRouteHints tests that QueryRoutes successfully
1451+
// finds a route through a private channel when provided with route hints.
1452+
func testQueryRoutesRouteHints(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+
queryReq := &lnrpc.QueryRoutesRequest{
1467+
PubKey: carol.PubKeyStr,
1468+
Amt: 10_000,
1469+
FinalCltvDelta: int32(routing.MinCLTVDelta),
1470+
// RouteHints omitted initially.
1471+
UseMissionControl: true, // Use MC for realistic pathfinding
1472+
}
1473+
1474+
// Query routes without hints - should fail (find no routes).
1475+
// Call the client directly to check the error without halting the test.
1476+
_, err := alice.RPC.LN.QueryRoutes(ht.Context(), queryReq)
1477+
require.Error(ht, err,
1478+
"QueryRoutes without hints should return an error")
1479+
1480+
// Now add the hints and query again - should succeed.
1481+
// Use the helper function here as we expect success.
1482+
queryReq.RouteHints = hints
1483+
routes := alice.RPC.QueryRoutes(queryReq)
1484+
1485+
// Assert that a route was found and it goes Alice -> Bob -> Carol.
1486+
require.NotEmpty(ht, routes.Routes,
1487+
"QueryRoutes with hints should find a route")
1488+
require.Len(ht, routes.Routes[0].Hops, 2, "Route should have 2 hops")
1489+
1490+
hop1 := routes.Routes[0].Hops[0]
1491+
hop2 := routes.Routes[0].Hops[1]
1492+
1493+
require.Equal(ht, bob.PubKeyStr, hop1.PubKey, "Hop 1 should be Bob")
1494+
require.Equal(ht, carol.PubKeyStr, hop2.PubKey, "Hop 2 should be Carol")
1495+
}
1496+
1497+
// createRouteHintFromChannel takes a source node and a channel object and
1498+
// constructs a RouteHint slice containing a single hop hint for that channel.
1499+
func createRouteHintFromChannel(sourceNode *node.HarnessNode,
1500+
channel *lnrpc.Channel) []*lnrpc.RouteHint {
1501+
1502+
return []*lnrpc.RouteHint{{HopHints: []*lnrpc.HopHint{
1503+
{
1504+
NodeId: sourceNode.PubKeyStr,
1505+
ChanId: channel.ChanId,
1506+
FeeBaseMsat: 1000,
1507+
FeeProportionalMillionths: 1,
1508+
CltvExpiryDelta: routing.MinCLTVDelta,
1509+
}}},
1510+
}
1511+
}
1512+
1513+
// setupThreeNodeNetwork sets up a standard three-node network topology:
1514+
// Alice -> Bob -> Carol. It creates the nodes, connects them, funds Alice and
1515+
// Bob, opens a channel between Alice and Bob, and optionally opens a private
1516+
// channel between Bob and Carol. It returns the nodes, channel points, and a
1517+
// cleanup function.
1518+
func setupThreeNodeNetwork(ht *lntest.HarnessTest, chanAmt btcutil.Amount,
1519+
bobCarolPrivate bool) (*node.HarnessNode, *node.HarnessNode,
1520+
*node.HarnessNode, *lnrpc.ChannelPoint, *lnrpc.ChannelPoint, func()) {
1521+
1522+
// Create nodes.
1523+
alice := ht.NewNode("Alice", nil)
1524+
bob := ht.NewNode("Bob", nil)
1525+
carol := ht.NewNode("Carol", nil)
1526+
1527+
// Connect nodes.
1528+
ht.ConnectNodes(alice, bob)
1529+
ht.ConnectNodes(bob, carol)
1530+
1531+
// Fund nodes.
1532+
ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
1533+
ht.FundCoins(btcutil.SatoshiPerBitcoin, bob)
1534+
1535+
// Open Alice -> Bob channel (public).
1536+
aliceBobChanPoint := ht.OpenChannel(
1537+
alice, bob, lntest.OpenChannelParams{Amt: chanAmt},
1538+
)
1539+
1540+
// Open Bob -> Carol channel (potentially private).
1541+
bobCarolParams := lntest.OpenChannelParams{
1542+
Amt: chanAmt,
1543+
Private: bobCarolPrivate,
1544+
}
1545+
bobCarolChanPoint := ht.OpenChannel(bob, carol, bobCarolParams)
1546+
1547+
// Define cleanup function.
1548+
cleanup := func() {
1549+
ht.CloseChannel(alice, aliceBobChanPoint)
1550+
ht.CloseChannel(bob, bobCarolChanPoint)
1551+
ht.Shutdown(alice)
1552+
ht.Shutdown(bob)
1553+
ht.Shutdown(carol)
1554+
}
1555+
1556+
return alice, bob, carol, aliceBobChanPoint, bobCarolChanPoint, cleanup
1557+
}

0 commit comments

Comments
 (0)