-
Notifications
You must be signed in to change notification settings - Fork 2.3k
routing: add RouteOrigin interface for multi-source pathfinding #10764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
calvinrzachman
wants to merge
3
commits into
lightningnetwork:elle-base-branch-payment-service
Choose a base branch
from
calvinrzachman:route-origin-interface
base: elle-base-branch-payment-service
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| // 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 | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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. | ||
|
|
@@ -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 | ||
|
|
@@ -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 { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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. | ||
|
|
@@ -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), | ||
| ) | ||
|
|
@@ -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. | ||
|
|
@@ -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++ | ||
|
|
||
|
|
@@ -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. | ||
|
|
@@ -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. | ||
|
|
@@ -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 | ||
|
|
@@ -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. | ||
|
|
@@ -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 | ||
|
calvinrzachman marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // blindedPathRestrictions are a set of constraints to adhere to when | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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 ...
There was a problem hiding this comment.
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.
IsRouteOriginis 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).IsRouteOriginjust widens the termination condition of path finding - no edges added or extra hops.There was a problem hiding this comment.
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.