Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 69 additions & 30 deletions routing/pathfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,31 @@ const (
fakeHopHintCapacity = btcutil.Amount(10 * btcutil.SatoshiPerBitcoin)
)

// pathFinder defines the interface of a path finding algorithm.
// IsRouteOrigin determines where routes can originate from. The backward
// Dijkstra terminates when it reaches any origin vertex. This is the
// source-end counterpart to AdditionalEdge, which extends the graph at the
Copy link
Copy Markdown
Collaborator

@ziggie1984 ziggie1984 May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this allow also chaining of the source vertex or is only one entry allowed ? Having Gateyway1<=GatewayPrevious ...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think we have chaining here. IsRouteOrigin is a flat set of termination points, not a chain of edges. I think the analogy made in the PR description to route hints is helpful but not total. Route hints on the receive side add actual edges the pathfinder traverses (each hop becomes an onion layer). IsRouteOrigin just widens the termination condition of path finding - no edges added or extra hops.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each origin must be a node you operate and can dispatch HTLCs from (via SendOnion). The use case is an external controller that pathfinds centrally across multiple lnd backends (like PS): the predicate returns true for each gateway backend, and the pathfinder terminates at whichever one provides the cheapest path.

You could technically put any node in the origin set and the pathfinder would produce a valid graph path. But the route is also a payment instruction: the source node must dispatch an HTLC to the first hop. If you don't operate that node, you can't execute payment via the route.

// destination end. Standard lnd uses a single source node. A
// multi-backend payment service can provide a multi-source implementation
// that terminates at any of its gateway nodes.
//
// NOTE: Only include vertices the caller can actually dispatch payments from.
// Circular self-payments (route-to-self) are only supported with the built-in
// single origin.
//
// Implementations should be O(1). findPath calls IsRouteOrigin once
// per heap pop and once per edge relaxation, so any per-call cost
// directly contributes to path-finding latency.
type IsRouteOrigin func(v route.Vertex) bool

// pathFinder defines the interface of a path finding algorithm. The first
// return value is the source vertex of the computed path. This is typically
// the node's own key, but it may be an arbitrary source or, for multi-origin
// callers, whichever origin provides the cheapest path.
type pathFinder = func(g *graphParams, r *RestrictParams,
cfg *PathFindingConfig, self, source, target route.Vertex,
amt lnwire.MilliSatoshi, timePref float64, finalHtlcExpiry int32) (
[]*unifiedEdge, float64, error)
cfg *PathFindingConfig, self route.Vertex, isOrigin IsRouteOrigin,
routeToSelf bool, target route.Vertex, amt lnwire.MilliSatoshi,
timePref float64, finalHtlcExpiry int32) (
route.Vertex, []*unifiedEdge, float64, error)

var (
// DefaultEstimator is the default estimator used for computing
Expand Down Expand Up @@ -601,9 +621,9 @@ func getOutgoingBalance(node route.Vertex, outgoingChans map[uint64]struct{},
// path and accurately check the amount to forward at every node against the
// available bandwidth.
func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
self, source, target route.Vertex, amt lnwire.MilliSatoshi,
timePref float64, finalHtlcExpiry int32) ([]*unifiedEdge, float64,
error) {
self route.Vertex, isOrigin IsRouteOrigin, routeToSelf bool,
target route.Vertex, amt lnwire.MilliSatoshi, timePref float64,
finalHtlcExpiry int32) (route.Vertex, []*unifiedEdge, float64, error) {

// Pathfinding can be a significant portion of the total payment
// latency, especially on low-powered devices. Log several metrics to
Expand All @@ -626,7 +646,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
context.TODO(), target,
)
if err != nil {
return nil, 0, err
return route.Vertex{}, nil, 0, err
}
}

Expand All @@ -635,14 +655,14 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
err := feature.ValidateRequired(features)
if err != nil {
log.Warnf("Pathfinding destination node features: %v", err)
return nil, 0, errUnknownRequiredFeature
return route.Vertex{}, nil, 0, errUnknownRequiredFeature
}

// Ensure that all transitive dependencies are set.
err = feature.ValidateDeps(features)
if err != nil {
log.Warnf("Pathfinding destination node features: %v", err)
return nil, 0, errMissingDependentFeature
return route.Vertex{}, nil, 0, errMissingDependentFeature
}

// Now that we know the feature vector is well-formed, we'll proceed in
Expand All @@ -652,7 +672,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
if r.PaymentAddr.IsSome() &&
!features.HasFeature(lnwire.PaymentAddrOptional) {

return nil, 0, errNoPaymentAddr
return route.Vertex{}, nil, 0, errNoPaymentAddr
}

// Set up outgoing channel map for quicker access.
Expand All @@ -665,13 +685,15 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
}

// If we are routing from ourselves, check that we have enough local
// balance available.
if source == self {
// balance available. This check is skipped when self is not in the
// origin set (e.g. multi-origin), since local balance information is
// not available for remote origin nodes.
if isOrigin(self) {
max, total, err := getOutgoingBalance(
self, outgoingChanMap, g.bandwidthHints, g.graph,
)
if err != nil {
return nil, 0, err
return route.Vertex{}, nil, 0, err
}

// If the total outgoing balance isn't sufficient, it will be
Expand All @@ -681,13 +703,18 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
"htlc of amount: %v, only have local "+
"balance: %v", amt, total)

return nil, 0, errInsufficientBalance
if routeToSelf {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the supplied optional origins could also include the source node and that's why we only abort if it is the legacy case only ?

return route.Vertex{}, nil, 0,
errInsufficientBalance
}
}

// If there is only not enough capacity on a single route, it
// may still be possible to complete the payment by splitting.
if max < amt {
return nil, 0, errNoPathFound
if routeToSelf {
return route.Vertex{}, nil, 0, errNoPathFound
}
}
}

Expand Down Expand Up @@ -729,7 +756,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// and depends on whether the destination is blinded or not.
lastHopPayloadSize, err := lastHopPayloadSize(r, finalHtlcExpiry, amt)
if err != nil {
return nil, 0, err
return route.Vertex{}, nil, 0, err
}

// We can't always assume that the end destination is publicly
Expand Down Expand Up @@ -763,8 +790,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,

// Validate time preference value.
if math.Abs(timePref) > 1 {
return nil, 0, fmt.Errorf("time preference %v out of range "+
"[-1, 1]", timePref)
return route.Vertex{}, nil, 0, fmt.Errorf("time preference %v "+
"out of range [-1, 1]", timePref)
}

// Scale to avoid the extremes -1 and 1 which run into infinity issues.
Expand Down Expand Up @@ -857,7 +884,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
outboundFee int64
)

if fromVertex != source {
if !isOrigin(fromVertex) {
outboundFee = int64(
edge.policy.ComputeFee(amountToSend),
)
Expand Down Expand Up @@ -956,7 +983,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// little inaccuracy here because we are over estimating by
// 1 hop.
var payloadSize uint64
if fromVertex != source {
if !isOrigin(fromVertex) {
// In case the unifiedEdge does not have a payload size
// function supplied we request a graceful shutdown
// because this should never happen.
Expand Down Expand Up @@ -1051,7 +1078,12 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
return fromFeatures, nil
}

routeToSelf := source == target
// Allow circular routes only for single-origin self-payments
// (e.g., rebalancing). This lets Dijkstra explore past the target
// on first visit rather than terminating immediately. For
// multi-origin, the target may happen to be in the origin set
// but we still want a direct route from another origin.
routeToSelf = routeToSelf && isOrigin(target)
for {
nodesVisited++

Expand All @@ -1066,7 +1098,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,

err := u.addGraphPolicies(g.graph)
if err != nil {
return nil, 0, err
return route.Vertex{}, nil, 0, err
}

// We add hop hints that were supplied externally.
Expand Down Expand Up @@ -1127,7 +1159,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// Get feature vector for fromNode.
fromFeatures, err := getGraphFeatures(fromNode)
if err != nil {
return nil, 0, err
return route.Vertex{}, nil, 0, err
}

// If there are no valid features, skip this node.
Expand All @@ -1148,14 +1180,21 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
// from the heap.
partialPath = heap.Pop(&nodeHeap).(*nodeWithDist)

// If we've reached our source (or we don't have any incoming
// edges), then we're done here and can exit the graph
// traversal early.
if partialPath.node == source {
// If we've reached a valid origin (or we don't have any
// incoming edges), then we're done here and can exit the
// graph traversal early.
if isOrigin(partialPath.node) {
break
}
}

// The path finding loop exits either when it reaches a valid origin or
// when the heap empties. In the latter case, no path exists.
source := partialPath.node
if !isOrigin(source) {
return route.Vertex{}, nil, 0, errNoPathFound
}

// Use the distance map to unravel the forward path from source to
// target.
var pathEdges []*unifiedEdge
Expand All @@ -1166,7 +1205,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
if !ok {
// If the node doesn't have a next hop it means we
// didn't find a path.
return nil, 0, errNoPathFound
return route.Vertex{}, nil, 0, errNoPathFound
}

// Add the next hop to the list of path edges.
Expand Down Expand Up @@ -1200,7 +1239,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
distance[source].probability, len(pathEdges),
distance[source].netAmountReceived-amt)

return pathEdges, distance[source].probability, nil
return source, pathEdges, distance[source].probability, nil
Comment thread
calvinrzachman marked this conversation as resolved.
}

// blindedPathRestrictions are a set of constraints to adhere to when
Expand Down
Loading