routerrpc: dont query for the channel capacity
What changed, and why it matters
This change removes a database lookup for channel capacity when returning a payment route through the RPC interface. Instead of fetching the real channel capacity from the graph database, it now reports the amount flowing through each hop as a stand-in capacity. This is a performance and deprecation cleanup, not a security fix. The included test corrects a subtle bug where every hop was being assigned the route's total amount rather than the amount actually entering that hop.
Treat as a routine performance/cleanup commit. Review whether downstream consumers of the ChanCapacity RPC field rely on exact capacity values, since the field now returns a lower-bound approximation and is scheduled for removal in the next release. No immediate security action is required.
Security signals we found
No security framing in commit title or message
Change removes graph DB query and replaces real capacity with a lower-bound approximation
New regression test fixes per-hop capacity assignment logic
No mention of vulnerabilities, CVEs, researchers, or incident response
Evidence from the diff
In routerrpc/router_backend.go’s MarshallRoute, the code previously called FetchChannelCapacity for each hop and fell back to incomingAmt.ToSatoshis() when the channel was closed/private or unknown. The patch removes the graph lookup entirely and always uses incomingAmt.ToSatoshis(). A new regression test confirms incomingAmt is updated per hop so that hop[1].ChanCapacity equals hop[0].AmtToForward, not route.TotalAmount. The commit message frames this as a performance optimization and a step toward removing a deprecated RPC field.
Changed components
lnrpc/routerrpc/router_backend.golnrpc/routerrpc/router_backend_test.goRPC route marshalling (MarshallRoute)Deprecated ChanCapacity field in Route RPC responseInspect captured patch +58 / −10
diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go
index 3085f58..62e98d5 100644
--- a/lnrpc/routerrpc/router_backend.go
+++ b/lnrpc/routerrpc/router_backend.go
@@ -655,16 +655,11 @@ func (r *RouterBackend) MarshallRoute(route *route.Route) (*lnrpc.Route, error)
for i, hop := range route.Hops {
fee := route.HopFee(i)
- // Channel capacity is not a defining property of a route. For
- // backwards RPC compatibility, we retrieve it here from the
- // graph.
- chanCapacity, err := r.FetchChannelCapacity(hop.ChannelID)
- if err != nil {
- // If capacity cannot be retrieved, this may be a
- // not-yet-received or private channel. Then report
- // amount that is sent through the channel as capacity.
- chanCapacity = incomingAmt.ToSatoshis()
- }
+ // Avoid per-hop graph lookups by using the incoming amount as a
+ // lower bound for the capacity. This is not the actual channel
+ // capacity, but it is a reasonable approximation that avoids
+ // slow graph lookups and works for closed/private channels too.
+ chanCapacity := incomingAmt.ToSatoshis()
// Extract the MPP fields if present on this hop.
var mpp *lnrpc.MPPRecord
@@ -713,6 +708,7 @@ func (r *RouterBackend) MarshallRoute(route *route.Route) (*lnrpc.Route, error)
blinding := hop.BlindingPoint.SerializeCompressed()
resp.Hops[i].BlindingPoint = blinding
}
+
incomingAmt = hop.AmtToForward
}
diff --git a/lnrpc/routerrpc/router_backend_test.go b/lnrpc/routerrpc/router_backend_test.go
index 373b929..e572f80 100644
--- a/lnrpc/routerrpc/router_backend_test.go
+++ b/lnrpc/routerrpc/router_backend_test.go
@@ -937,3 +937,55 @@ func TestExtractIntentFromSendRequest(t *testing.T) {
})
}
}
+
+// TestMarshallRouteChanCapacity verifies that MarshallRoute correctly sets the
+// ChanCapacity for each hop based on the incoming amount at that hop, not
+// the total route amount. This is a regression test to ensure the
+// incomingAmt is updated per hop.
+func TestMarshallRouteChanCapacity(t *testing.T) {
+ t.Parallel()
+
+ // Build a two-hop route: source -> hop1 -> hop2 -> dest.
+ //
+ // TotalAmount (incoming to hop1) = 1000 msat
+ // hop1.AmtToForward (incoming to hop2) = 900 msat (after fee)
+ const (
+ totalAmtMsat = lnwire.MilliSatoshi(1000)
+ hop1Forward = lnwire.MilliSatoshi(900)
+ hop2Forward = lnwire.MilliSatoshi(900)
+ )
+
+ hops := []*route.Hop{
+ {
+ ChannelID: 1,
+ AmtToForward: hop1Forward,
+ PubKeyBytes: node1,
+ },
+ {
+ ChannelID: 2,
+ AmtToForward: hop2Forward,
+ PubKeyBytes: node2,
+ },
+ }
+
+ r, err := route.NewRouteFromHops(totalAmtMsat, 100, sourceKey, hops)
+ require.NoError(t, err)
+
+ backend := &RouterBackend{}
+ rpcRoute, err := backend.MarshallRoute(r)
+ require.NoError(t, err)
+ require.Len(t, rpcRoute.Hops, 2)
+
+ // The first hop's capacity should reflect the total incoming amount
+ // (route.TotalAmount), converted to satoshis.
+ require.EqualValues(
+ t, totalAmtMsat.ToSatoshis(), rpcRoute.Hops[0].ChanCapacity,
+ )
+
+ // The second hop's capacity should reflect hop1's forwarded amount, not
+ // the total route amount. Before the fix, both hops incorrectly used
+ // the total route amount.
+ require.EqualValues(
+ t, hop1Forward.ToSatoshis(), rpcRoute.Hops[1].ChanCapacity,
+ )
+}
Why this scored 19/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.