Merge pull request #10057 from Abdulkbk/outchanids-to-queryroutes

routerrpc: accept mutliple outgoing chan ids in queryroutes
This commit is contained in:
Oliver Gugger authored and GitHub committed 2025-07-15 15:02:23 -06:00
commit d7100ebe52
7 files changed
+1874 -1757

No files matched your search

+17 -3
View File
@@ -1158,10 +1158,11 @@ var queryRoutesCommand = cli.Command{
Name: "use_mc", Name: "use_mc",
Usage: "use mission control probabilities", Usage: "use mission control probabilities",
}, },
cli.Uint64Flag{ cli.StringSliceFlag{
Name: "outgoing_chan_id", Name: "outgoing_chan_id",
Usage: "(optional) the channel id of the channel " + Usage: "(optional) the channel id of the channel " +
"that must be taken to the first hop", "to use as the first hop. This flag can be " +
"specified multiple times in the same command.",
}, },
cli.StringSliceFlag{ cli.StringSliceFlag{
Name: "ignore_pair", Name: "ignore_pair",
@@ -1275,12 +1276,25 @@ func queryRoutes(ctx *cli.Context) error {
FinalCltvDelta: int32(ctx.Int("final_cltv_delta")), FinalCltvDelta: int32(ctx.Int("final_cltv_delta")),
UseMissionControl: ctx.Bool("use_mc"), UseMissionControl: ctx.Bool("use_mc"),
CltvLimit: uint32(ctx.Uint64(cltvLimitFlag.Name)), CltvLimit: uint32(ctx.Uint64(cltvLimitFlag.Name)),
OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
TimePref: ctx.Float64(timePrefFlag.Name), TimePref: ctx.Float64(timePrefFlag.Name),
IgnoredPairs: ignoredPairs, IgnoredPairs: ignoredPairs,
BlindedPaymentPaths: blindedRoutes, BlindedPaymentPaths: blindedRoutes,
} }
outgoingChanIds := ctx.StringSlice("outgoing_chan_id")
if len(outgoingChanIds) != 0 {
req.OutgoingChanIds = make([]uint64, len(outgoingChanIds))
for i, chanID := range outgoingChanIds {
id, err := strconv.ParseUint(chanID, 10, 64)
if err != nil {
return fmt.Errorf("invalid outgoing_chan_id "+
"argument: %w", err)
}
req.OutgoingChanIds[i] = id
}
}
if ctx.IsSet("route_hints") { if ctx.IsSet("route_hints") {
if len(blindedRoutes) > 0 { if len(blindedRoutes) > 0 {
return fmt.Errorf("--route_hints should not be used " + return fmt.Errorf("--route_hints should not be used " +
@@ -122,6 +122,10 @@ circuit. The indices are only available for forwarding events saved after v0.20.
to the database. to the database.
## lncli Updates ## lncli Updates
* Previously, users could only specify one `outgoing_chan_id` when calling the
`lncli queryroutes` or the QueryRoutes RPC. With this change, multiple
`outgoing_chan_id` can be passed during the call.
## Code Health ## Code Health
@@ -155,6 +159,10 @@ reader of a payment request.
| [`routerrpc.SendPaymentV2`](https://lightning.engineering/api-docs/api/lnd/router/send-payment-v2/index.html) | ✅ | `POST` | `/v2/router/send` | | [`routerrpc.SendPaymentV2`](https://lightning.engineering/api-docs/api/lnd/router/send-payment-v2/index.html) | ✅ | `POST` | `/v2/router/send` |
| [`routerrpc.TrackPaymentV2`](https://lightning.engineering/api-docs/api/lnd/router/track-payment-v2/) | ✅ | `GET` | `/v2/router/track/{payment_hash}` | | [`routerrpc.TrackPaymentV2`](https://lightning.engineering/api-docs/api/lnd/router/track-payment-v2/) | ✅ | `GET` | `/v2/router/track/{payment_hash}` |
* We are deprecating `OutgoingChanId` in favour of `OutgoingChanIds` in the
`QueryRoutes` RPC. This [transition](https://github.com/lightningnetwork/lnd/pull/10057) allows us to specify more than one outgoing channel
the pathfinder should use when finding a route.
# Technical and Architectural Updates # Technical and Architectural Updates
## BOLT Spec Updates ## BOLT Spec Updates
+1753 -1737
View File
File diff suppressed because it is too large. Load diff
+9 -3
View File
@@ -3158,10 +3158,10 @@ message QueryRoutesRequest {
map<uint64, bytes> dest_custom_records = 13; map<uint64, bytes> dest_custom_records = 13;
/* /*
The channel id of the channel that must be taken to the first hop. If zero, Deprecated, use outgoing_chan_ids. The channel id of the channel that must
any channel may be used. be taken to the first hop. If zero, any channel may be used.
*/ */
uint64 outgoing_chan_id = 14 [jstype = JS_STRING]; uint64 outgoing_chan_id = 14 [jstype = JS_STRING, deprecated = true];
/* /*
The pubkey of the last hop of the route. If empty, any hop may be used. The pubkey of the last hop of the route. If empty, any hop may be used.
@@ -3196,6 +3196,12 @@ message QueryRoutesRequest {
only, to 1 to optimize for reliability only or a value inbetween for a mix. only, to 1 to optimize for reliability only or a value inbetween for a mix.
*/ */
double time_pref = 18; double time_pref = 18;
/*
The channel ids of the channels allowed for the first hop. If empty, any
channel may be used.
*/
repeated uint64 outgoing_chan_ids = 20;
} }
message NodePair { message NodePair {
+22 -2
View File
@@ -1459,7 +1459,7 @@
}, },
{ {
"name": "outgoing_chan_id", "name": "outgoing_chan_id",
"description": "The channel id of the channel that must be taken to the first hop. If zero,\nany channel may be used.", "description": "Deprecated, use outgoing_chan_ids. The channel id of the channel that must\nbe taken to the first hop. If zero, any channel may be used.",
"in": "query", "in": "query",
"required": false, "required": false,
"type": "string", "type": "string",
@@ -1520,6 +1520,18 @@
"required": false, "required": false,
"type": "number", "type": "number",
"format": "double" "format": "double"
},
{
"name": "outgoing_chan_ids",
"description": "The channel ids of the channels allowed for the first hop. If empty, any\nchannel may be used.",
"in": "query",
"required": false,
"type": "array",
"items": {
"type": "string",
"format": "uint64"
},
"collectionFormat": "multi"
} }
], ],
"tags": [ "tags": [
@@ -1629,7 +1641,7 @@
"outgoing_chan_id": { "outgoing_chan_id": {
"type": "string", "type": "string",
"format": "uint64", "format": "uint64",
"description": "The channel id of the channel that must be taken to the first hop. If zero,\nany channel may be used." "description": "Deprecated, use outgoing_chan_ids. The channel id of the channel that must\nbe taken to the first hop. If zero, any channel may be used."
}, },
"last_hop_pubkey": { "last_hop_pubkey": {
"type": "string", "type": "string",
@@ -1663,6 +1675,14 @@
"type": "number", "type": "number",
"format": "double", "format": "double",
"description": "The time preference for this payment. Set to -1 to optimize for fees\nonly, to 1 to optimize for reliability only or a value inbetween for a mix." "description": "The time preference for this payment. Set to -1 to optimize for fees\nonly, to 1 to optimize for reliability only or a value inbetween for a mix."
},
"outgoing_chan_ids": {
"type": "array",
"items": {
"type": "string",
"format": "uint64"
},
"description": "The channel ids of the channels allowed for the first hop. If empty, any\nchannel may be used."
} }
} }
} }
+12 -2
View File
@@ -413,8 +413,18 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
BlindedPaymentPathSet: blindedPathSet, BlindedPaymentPathSet: blindedPathSet,
} }
// Pass along an outgoing channel restriction if specified. // We set the outgoing channel restrictions if the user provides a
if in.OutgoingChanId != 0 { // list of channel ids. We also handle the case where the user
// provides the deprecated `OutgoingChanId` field.
switch {
case len(in.OutgoingChanIds) > 0 && in.OutgoingChanId != 0:
return nil, errors.New("outgoing_chan_id and " +
"outgoing_chan_ids cannot both be set")
case len(in.OutgoingChanIds) > 0:
restrictions.OutgoingChannelIDs = in.OutgoingChanIds
case in.OutgoingChanId != 0:
restrictions.OutgoingChannelIDs = []uint64{in.OutgoingChanId} restrictions.OutgoingChannelIDs = []uint64{in.OutgoingChanId}
} }
+53 -10
View File
@@ -34,25 +34,39 @@ var (
node2 = route.Vertex{11} node2 = route.Vertex{11}
) )
var (
singleChanID = "singleChanID"
multiChanID = "multiChanID"
bothChanIds = "bothChanIds"
)
// TestQueryRoutes asserts that query routes rpc parameters are properly parsed // TestQueryRoutes asserts that query routes rpc parameters are properly parsed
// and passed onto path finding. // and passed onto path finding.
func TestQueryRoutes(t *testing.T) { func TestQueryRoutes(t *testing.T) {
t.Run("no mission control", func(t *testing.T) { t.Run("no mission control", func(t *testing.T) {
testQueryRoutes(t, false, false, true) testQueryRoutes(t, false, false, true, singleChanID)
}) })
t.Run("no mission control and msat", func(t *testing.T) { t.Run("no mission control and msat", func(t *testing.T) {
testQueryRoutes(t, false, true, true) testQueryRoutes(t, false, true, true, singleChanID)
}) })
t.Run("with mission control", func(t *testing.T) { t.Run("with mission control", func(t *testing.T) {
testQueryRoutes(t, true, false, true) testQueryRoutes(t, true, false, true, singleChanID)
}) })
t.Run("no mission control bad cltv limit", func(t *testing.T) { t.Run("no mission control bad cltv limit", func(t *testing.T) {
testQueryRoutes(t, false, false, false) testQueryRoutes(t, false, false, false, singleChanID)
})
t.Run("both outgoing chan id and chan ids", func(t *testing.T) {
testQueryRoutes(t, true, false, true, bothChanIds)
})
t.Run("multiple outgoing chan ids", func(t *testing.T) {
testQueryRoutes(t, false, true, true, multiChanID)
}) })
} }
func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool, func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool,
setTimelock bool) { setTimelock bool, outgoingChanConfig string) {
ignoreNodeBytes, err := hex.DecodeString(ignoreNodeKey) ignoreNodeBytes, err := hex.DecodeString(ignoreNodeKey)
if err != nil { if err != nil {
@@ -68,8 +82,9 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool,
} }
var ( var (
lastHop = route.Vertex{64} lastHop = route.Vertex{64}
outgoingChan = uint64(383322) outgoingChan = uint64(383322)
outgoingChanIds = []uint64{383322, 383323}
) )
hintNode, err := route.NewVertexFromStr(hintNodeKey) hintNode, err := route.NewVertexFromStr(hintNodeKey)
@@ -102,7 +117,6 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool,
}}, }},
UseMissionControl: useMissionControl, UseMissionControl: useMissionControl,
LastHopPubkey: lastHop[:], LastHopPubkey: lastHop[:],
OutgoingChanId: outgoingChan,
DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_MPP_OPT}, DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_MPP_OPT},
RouteHints: rpcRouteHints, RouteHints: rpcRouteHints,
} }
@@ -124,6 +138,18 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool,
} }
} }
switch outgoingChanConfig {
case singleChanID:
request.OutgoingChanId = outgoingChan
case multiChanID:
request.OutgoingChanIds = outgoingChanIds
case bothChanIds:
request.OutgoingChanId = outgoingChan
request.OutgoingChanIds = outgoingChanIds
}
findRoute := func(req *routing.RouteRequest) (*route.Route, float64, findRoute := func(req *routing.RouteRequest) (*route.Route, float64,
error) { error) {
@@ -165,8 +191,18 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool,
t.Fatal("unexpected last hop") t.Fatal("unexpected last hop")
} }
if restrictions.OutgoingChannelIDs[0] != outgoingChan { switch outgoingChanConfig {
t.Fatal("unexpected outgoing channel id") case singleChanID:
require.Equal(
t, restrictions.OutgoingChannelIDs,
[]uint64{outgoingChan},
)
case multiChanID:
require.Equal(
t, restrictions.OutgoingChannelIDs,
outgoingChanIds,
)
} }
if !restrictions.DestFeatures.HasFeature(lnwire.MPPOptional) { if !restrictions.DestFeatures.HasFeature(lnwire.MPPOptional) {
@@ -230,6 +266,13 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool,
resp, err := backend.QueryRoutes(context.Background(), request) resp, err := backend.QueryRoutes(context.Background(), request)
// If we're using both OutgoingChanId and OutgoingChanIds, we should get
// an error.
if outgoingChanConfig == bothChanIds {
require.Error(t, err)
return
}
// If no MaxTotalTimelock was set for the QueryRoutes request, make // If no MaxTotalTimelock was set for the QueryRoutes request, make
// sure an error was returned. // sure an error was returned.
if !setTimelock { if !setTimelock {