Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
110 changes: 80 additions & 30 deletions routing/pathfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,45 @@ const (
fakeHopHintCapacity = btcutil.Amount(10 * btcutil.SatoshiPerBitcoin)
)

// pathFinder defines the interface of a path finding algorithm.
// RouteOrigin 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
// destination end. Standard lnd uses singleOrigin (one 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
// singleOrigin.
type RouteOrigin interface {
// IsOrigin reports whether the given vertex is a valid route starting
// point.
//
// NOTE: Implementations should be O(1). findPath calls IsOrigin once
// per heap pop and once per edge relaxation, so any per-call cost
// directly contributes to path-finding latency.
IsOrigin(v route.Vertex) bool
}

// singleOrigin is the default RouteOrigin: a single source vertex.
type singleOrigin struct {
source route.Vertex
}

// IsOrigin reports whether v is the source vertex.
func (s *singleOrigin) IsOrigin(v route.Vertex) bool {
return v == s.source
}

// 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, origin RouteOrigin,
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 +635,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, origin RouteOrigin, 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 +660,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 +669,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 +686,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 +699,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 origin.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 +717,13 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
"htlc of amount: %v, only have local "+
"balance: %v", amt, total)

return nil, 0, errInsufficientBalance
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
return route.Vertex{}, nil, 0, errNoPathFound
}
}

Expand Down Expand Up @@ -729,7 +765,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 +799,9 @@ 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 +894,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
outboundFee int64
)

if fromVertex != source {
if !origin.IsOrigin(fromVertex) {
outboundFee = int64(
edge.policy.ComputeFee(amountToSend),
)
Expand Down Expand Up @@ -956,7 +993,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 !origin.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 +1088,13 @@ 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.
_, isSingle := origin.(*singleOrigin)
routeToSelf := isSingle && origin.IsOrigin(target)
for {
nodesVisited++

Expand All @@ -1066,7 +1109,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 +1170,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 +1191,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 origin.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 !origin.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 +1216,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 +1250,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 on lines 1250 to +1253
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

There is a potential nil pointer dereference here if source is not present in the distance map. While the unraveling loop at line 1213 checks for presence in distance, it returns errNoPathFound if the node is missing. However, if source == target and routeToSelf is false (which can happen with a multiOrigin implementation where the target is in the origin set), the target node is never added to the distance map. In this specific case, the unraveling loop will return errNoPathFound at line 1219, so the code won't reach line 1250. Nevertheless, relying on this side effect for safety is fragile. Consider adding an explicit check or ensuring source is always in distance if it's a valid origin.

}

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