routerrpc: add outgoing_chan_ids to EstimateRouteFee
What changed, and why it matters
This commit adds a new optional feature to LND's EstimateRouteFee API that lets callers specify which channels can be used as the first hop when estimating fees or probing payments. It is a feature enhancement, not a security fix or vulnerability. There is no indication of a security issue in the code changes.
No security action required. Review as a normal API feature addition.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change extends the routerrpc RouteFeeRequest protobuf message with a repeated uint64 outgoing_chan_ids field and threads that value into the existing routing restriction machinery (routing.RestrictParams.OutgoingChannelIDs and routerrpc.SendPaymentRequest.OutgoingChanIds). It applies to both graph-based fee estimation (probeDestination) and probe-based estimation using a payment request (probePaymentRequest). The implementation is a straightforward API extension with no validation bypass, privilege escalation, or unsafe handling of the new input.
Changed components
lnrpc/routerrpc/router_server.golnrpc/routerrpc/router.protolnrpc/routerrpc/router.pb.golnrpc/routerrpc/router.swagger.jsonInspect captured patch +51 / −15
diff --git a/lnrpc/routerrpc/router.pb.go b/lnrpc/routerrpc/router.pb.go
index d6d6bac..a65441b 100644
--- a/lnrpc/routerrpc/router.pb.go
+++ b/lnrpc/routerrpc/router.pb.go
@@ -862,9 +862,14 @@ type RouteFeeRequest struct {
// than the timeout if the HTLC becomes delayed or stuck. Canceling the context
// of this call will not cancel the payment loop, the duration is only
// controlled by the timeout parameter.
- Timeout uint32 `protobuf:"varint,4,opt,name=timeout,proto3" json:"timeout,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ Timeout uint32 `protobuf:"varint,4,opt,name=timeout,proto3" json:"timeout,omitempty"`
+ // The channel ids of the channels that are allowed for the first hop. If
+ // empty, any channel may be used. This field is applicable to both
+ // graph-based fee estimation (using dest + amt_sat) and probe-based
+ // estimation (using payment_request).
+ OutgoingChanIds []uint64 `protobuf:"varint,5,rep,packed,name=outgoing_chan_ids,json=outgoingChanIds,proto3" json:"outgoing_chan_ids,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
func (x *RouteFeeRequest) Reset() {
@@ -925,6 +930,13 @@ func (x *RouteFeeRequest) GetTimeout() uint32 {
return 0
}
+func (x *RouteFeeRequest) GetOutgoingChanIds() []uint64 {
+ if x != nil {
+ return x.OutgoingChanIds
+ }
+ return nil
+}
+
type RouteFeeResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// A lower bound of the estimated fee to the target destination within the
@@ -3833,12 +3845,13 @@ const file_routerrpc_router_proto_rawDesc = "" +
"\fpayment_hash\x18\x01 \x01(\fR\vpaymentHash\x12.\n" +
"\x13no_inflight_updates\x18\x02 \x01(\bR\x11noInflightUpdates\"F\n" +
"\x14TrackPaymentsRequest\x12.\n" +
- "\x13no_inflight_updates\x18\x01 \x01(\bR\x11noInflightUpdates\"\x81\x01\n" +
+ "\x13no_inflight_updates\x18\x01 \x01(\bR\x11noInflightUpdates\"\xad\x01\n" +
"\x0fRouteFeeRequest\x12\x12\n" +
"\x04dest\x18\x01 \x01(\fR\x04dest\x12\x17\n" +
"\aamt_sat\x18\x02 \x01(\x03R\x06amtSat\x12'\n" +
"\x0fpayment_request\x18\x03 \x01(\tR\x0epaymentRequest\x12\x18\n" +
- "\atimeout\x18\x04 \x01(\rR\atimeout\"\xa8\x01\n" +
+ "\atimeout\x18\x04 \x01(\rR\atimeout\x12*\n" +
+ "\x11outgoing_chan_ids\x18\x05 \x03(\x04R\x0foutgoingChanIds\"\xa8\x01\n" +
"\x10RouteFeeResponse\x12(\n" +
"\x10routing_fee_msat\x18\x01 \x01(\x03R\x0eroutingFeeMsat\x12&\n" +
"\x0ftime_lock_delay\x18\x02 \x01(\x03R\rtimeLockDelay\x12B\n" +
diff --git a/lnrpc/routerrpc/router.proto b/lnrpc/routerrpc/router.proto
index 5ddf922..12b9152 100644
--- a/lnrpc/routerrpc/router.proto
+++ b/lnrpc/routerrpc/router.proto
@@ -443,6 +443,14 @@ message RouteFeeRequest {
controlled by the timeout parameter.
*/
uint32 timeout = 4;
+
+ /*
+ The channel ids of the channels that are allowed for the first hop. If
+ empty, any channel may be used. This field is applicable to both
+ graph-based fee estimation (using dest + amt_sat) and probe-based
+ estimation (using payment_request).
+ */
+ repeated uint64 outgoing_chan_ids = 5;
}
message RouteFeeResponse {
diff --git a/lnrpc/routerrpc/router.swagger.json b/lnrpc/routerrpc/router.swagger.json
index e5b21ee..74dafab 100644
--- a/lnrpc/routerrpc/router.swagger.json
+++ b/lnrpc/routerrpc/router.swagger.json
@@ -1972,6 +1972,14 @@
"type": "integer",
"format": "int64",
"description": "A user preference of how long a probe payment should maximally be allowed to\ntake, denoted in seconds. The probing payment loop is aborted if this\ntimeout is reached. Note that the probing process itself can take longer\nthan the timeout if the HTLC becomes delayed or stuck. Canceling the context\nof this call will not cancel the payment loop, the duration is only\ncontrolled by the timeout parameter."
+ },
+ "outgoing_chan_ids": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "uint64"
+ },
+ "description": "The channel ids of the channels that are allowed for the first hop. If\nempty, any channel may be used. This field is applicable to both\ngraph-based fee estimation (using dest + amt_sat) and probe-based\nestimation (using payment_request)."
}
}
},
diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go
index e3ac207..bb00f28 100644
--- a/lnrpc/routerrpc/router_server.go
+++ b/lnrpc/routerrpc/router_server.go
@@ -448,12 +448,15 @@ func (s *Server) EstimateRouteFee(ctx context.Context,
return nil, errors.New("amount must be greater than 0")
default:
- return s.probeDestination(req.Dest, req.AmtSat)
+ return s.probeDestination(
+ req.Dest, req.AmtSat, req.OutgoingChanIds,
+ )
}
case isProbeInvoice:
return s.probePaymentRequest(
ctx, req.PaymentRequest, req.Timeout,
+ req.OutgoingChanIds,
)
}
@@ -462,8 +465,8 @@ func (s *Server) EstimateRouteFee(ctx context.Context,
// probeDestination estimates fees along a route to a destination based on the
// contents of the local graph.
-func (s *Server) probeDestination(dest []byte, amtSat int64) (*RouteFeeResponse,
- error) {
+func (s *Server) probeDestination(dest []byte, amtSat int64,
+ outgoingChanIDs []uint64) (*RouteFeeResponse, error) {
destNode, err := route.NewVertexFromBytes(dest)
if err != nil {
@@ -478,14 +481,16 @@ func (s *Server) probeDestination(dest []byte, amtSat int64) (*RouteFeeResponse,
// that target amount, we'll only request a single route. Set a
// restriction for the default CLTV limit, otherwise we can find a route
// that exceeds it and is useless to us.
- mc := s.cfg.RouterBackend.MissionControl
+ backend := s.cfg.RouterBackend
+ mc := backend.MissionControl
routeReq, err := routing.NewRouteRequest(
- s.cfg.RouterBackend.SelfNode, &destNode, amtMsat, 0,
+ backend.SelfNode, &destNode, amtMsat, 0,
&routing.RestrictParams{
- FeeLimit: routeFeeLimitSat,
- CltvLimit: s.cfg.RouterBackend.MaxTotalTimelock,
- ProbabilitySource: mc.GetProbability,
- }, nil, nil, nil, s.cfg.RouterBackend.DefaultFinalCltvDelta,
+ FeeLimit: routeFeeLimitSat,
+ CltvLimit: backend.MaxTotalTimelock,
+ ProbabilitySource: mc.GetProbability,
+ OutgoingChannelIDs: outgoingChanIDs,
+ }, nil, nil, nil, backend.DefaultFinalCltvDelta,
)
if err != nil {
return nil, err
@@ -522,7 +527,7 @@ func (s *Server) probeDestination(dest []byte, amtSat int64) (*RouteFeeResponse,
// identify LSPs, the probe payment might use a different node id as the
// final destination (the assumed LSP node id).
func (s *Server) probePaymentRequest(ctx context.Context, paymentRequest string,
- timeout uint32) (*RouteFeeResponse, error) {
+ timeout uint32, outgoingChanIDs []uint64) (*RouteFeeResponse, error) {
payReq, err := zpay32.Decode(
paymentRequest, s.cfg.RouterBackend.ActiveNetParams,
@@ -556,6 +561,7 @@ func (s *Server) probePaymentRequest(ctx context.Context, paymentRequest string,
FeeLimitSat: routeFeeLimitSat,
FinalCltvDelta: int32(payReq.MinFinalCLTVExpiry()),
DestFeatures: MarshalFeatures(payReq.Features),
+ OutgoingChanIds: outgoingChanIDs,
}
// If the payment addresses is specified, then we'll also populate that
@@ -625,6 +631,7 @@ func (s *Server) probePaymentRequest(ctx context.Context, paymentRequest string,
FeeLimitSat: probeRequest.FeeLimitSat,
FinalCltvDelta: int32(lspHint.CLTVExpiryDelta),
DestFeatures: probeRequest.DestFeatures,
+ OutgoingChanIds: probeRequest.OutgoingChanIds,
}
// Copy the payment address if present.
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.