Open feelancer21 opened 1 month ago
I am looking forward to your assessment or if I am missing something. The shift seems to be too simple to be true. ;)
Yes, unfortunately I think that doesn't solve it, during implementation/review of https://github.com/lightningnetwork/lnd/pull/6934 this approach was tried but not documented. Here's an example graph that would lead to non-optimal outcomes with the suggested approach.
Take the following graph with outbound and inbound fees as indicated on the edges:
flowchart LR
S -->|out: 0, in: -4| A
A -->|out: 9, in: -11| B
A -->|out: 0, in: -6| C
B -->|out: 8, in: 0| T
C -->|out: 10, in: 0| T
The top route (SABT
) has fees or weight of 5=8-8+9-4
, the lower, optimal, route (SACT
) has weight of 4=10-6+0+0
, keeping in mind that the inbound discount is capped by the next hop's outbound fee, so the total node fee is not allowed to become negative.
I'll show the current approach, the suggested approach and the line-graph approach as examples.
This is the currently implemented approach: taking the weight as the edge's inbound fee plus its outbound fee, capped from below by zero. Dijkstra's algorithm would work like this:
explore T
accumulated weight = 0 (prev accWeight) + 0 (inFee) + 8 (outFee) = 8
accWeight = 0 + 0 + 10 = 10
Node: accWeight (prevOutFee)
)
B: 8 (8)
C: 10 (10)
explore B
accumulated weight = 8 - 8 (capped inFee) + 9 = 9
A 9 (9)
C 10 (10)
explore A
accumulated weight = 9 + 0 + 0 (capped channel fee) = 9
S 9 (0)
: the current algo ends here with a non-optimal routeC 10 (10)
accumulated weight = 0 (prev accWeight) + 0 (inFee) + 0 (prev outFee) = 0
accWeight = 0 + 0 + 0 = 0
B: 0 (8)
C: 0 (10)
The accumulated weights are 0 for both, so the order by which nodes are explored is non-deterministic.
0 (prev accWeight) - 8 (capped inFee) + 8 (prev outFee) = 0
A 0 (9)
C 0 (10)
Both nodes in the queue have the same accumulated weight.
1.1. explore A first
S: 0 - 4 + 9 = 5
queue:
C 0 (10)
S 5 (0)
explore C
0 - 6 + 10 = 4
A 4 (0)
S 5 (0)
explore A
4 + 0 + 0 = 4
: new best: optimal route1.2. explore C first
A: 0 - 6 + 10 = 4
-> worse than before, keep A 0 (9)
queue:
A 0 (9)
explore A
0 - 4 + 9 = 5
-> algo stops here, S found, non-optimal0 - 6 + 10 = 4
B 0 (8)
A 4 (0)
0 - 8 + 8 = 0
: new bestA 0 (9)
-> ends up at S with non-optimal route as beforeSo in some cases optimal routes are found, sometimes not. Perhaps one could apply the next outbound fee as a tie-breaker and maybe there are other edge cases, but I think the next approach is the exact one.
Transforming the problem to a line graph (https://en.wikipedia.org/wiki/Line_graph) solves the problem for this example graph. The new pivots of exploration are the middle points between two nodes, which helps to keep track of weights without overriding information (a solution found by Joost Jager).
0 (prev accWeight) + 0 (inFee) + 0 (prev outFee) = 0
0 + 0 + 0 = 0
NodeTuple accWeight (prev outFee)
(B,T): 0 (8)
(C,T): 0 (10)
Again, we have same weights, any tuple could get picked.
0 - 8 (capped inFee) + 8 (prev outFee) = 0
(A,B) 0 (9)
(C,T) 0 (10)
Both nodes in the queue both have again the same weight.
1.1. explore (A,B) first
S: 0 - 4 + 9 = 5
queue:
(C,T) 0 (10)
: (S,A) is not first yet, keep exploring(S,A) 5 (0)
explore (C,T)
0 + 10 - 6 = 4
(A,C) 4 (0)
(S,A) 5 (0)
explore (A,C)
4 + 0 + 0 = 4
: override last best(S,A) 4 (0)
: optimal route found1.2. explore (C,T) first
A: 0 - 6 + 10 = 4
(A,B) 0 (9)
explore (A,B)
0 - 4 + 9 = 5
-queue:
explore (A,C)
4 + 0 + 0 = 4
: new best
-queue:
0 -6 + 10 = 4
(B,T) 0 (8)
(A,C) 4 (0)
explore (B,T)
0 - 8 + 8 = 0
(A,B) 0 (9)
(A,C) 4 (0)
explore (A,B)
0 - 4 + 9 = 5
(A,C) 4 (0)
(S,A) 5 (0)
explore (A,C)
4 + 0 + 0 = 4
, new best(S,A) 4 (0)
: optimal routeSo the suggestion to use the inbound fee plus the previous outbound fee makes the algo correct (at least for this toy graph) if the heap/queue is used with a node pair tuple, which is the case with https://github.com/lightningnetwork/lnd/commit/54c498870c13b25a2c6c96a0c473a711b7db6457. I'm going to explore this change and check it's execution time behavior. The exit condition can be changed by adding a fake hop (0,S)
we are really looking for, I think.
First of all, thank you very much for the detailed answer and the very interesting example. I already thought that the dependencies between inbound and outbound go much deeper, but haven't seen it yet.
To summarize it for me: 1.1. vs. 1.2. is about the decision of A 0 (9)
vs A 4 (0)
. However, the optimal solution depends on the inbound discount on S. If it had been -6 or lower, A 0 (9)
would have been the right choice.
I suppose you can also construct cases where it is better to choose a higher distance with a higher outbound fee, which is later improved by discounts. So the classic reason why Dijsktra does not find the optimal solution with negative weights
explore A
- S:
accumulated weight = 9 - 4 + 0 = 5
queue:
S 5 (0)
: the current algo ends here with a non-optimal routeC 10 (10)
Not sure if I have really understood the current approach. I had expected accumulated weight = 9 + max(- 4 + 0; 0) = 9
because of the checkt of the signedFee
Edit: If I'm not wrong about the previous one and the conversion to the line graph is more extensive than you thought, then I would like to suggest that at least the senders can benefit better from the inbound discounts of their direct peers.
You could set tempWeight = totalFee + sumTimeLockPenalty
directly in the case fromVertex == source
. Here, sumTimeLockPenalty
is the sum of the timeLockPenalty
, which would have to be kept separately in nodeWithDist
. Then the distance would be correct again, at least in the last iteration, and the chance of finding the optimum path would increase.
Not sure if I have really understood the current approach. I had expected accumulated weight = 9 + max(- 4 + 0; 0) = 9 because of the checkt of the signedFee
Ah yes, thank you for the correction, updated (it doesn't change the outcome)!
If I'm not wrong about the previous one and the conversion to the line graph is more extensive than you thought, then I would like to suggest that at least the senders can benefit better from the inbound discounts of their direct peers.
Changing to that approach could make sense, as it is closer to the optimal solution :+1:, but I think it's worth to explore the behavior of the line-graph approach first.
Is your feature request related to a problem? Please describe.
I am picking up a topic that I already mentioned in #8940. In the course of #6934, a split for the total channel fees was implemented. The current cutoff results in not always finding the optimal route as mentioned here. https://github.com/lightningnetwork/lnd/pull/6934#pullrequestreview-1836184718
Here is another example from the perspective of a sender A, who can send a payment to D either through B or C.
A-[out fee 0 msat | in fee -100 msat]-B-[out 150 msat | in fee 0]-D
andA-[out fee 0 msat | in fee 0 msat]-C-[out 100 msat | in fee 0]-D
Since the
out_fee
in the outgoing channels of a source is always counted as 0, the incoming fee on this channel is completely absorbed by the current cutoff, and both source channels are considered with a weight of 0 in the pathfinding (ignoring the timelock penalty). The optimal solution is determined based on the second channel, and the result is the more expensive second route (about 50ppm). This is even though the success probability of the second route could be lower. However, if afee_limit
of 75ppm is set, the first route will be found at least after the fix of #8940.Describe the solution you'd like
In my opinion, this behavior can be addressed if we do not base the calculation on the total channel fee, but measure the distance from the
fromVertex
to thetarget
without considering the outbound fee of thefromVertex
. Instead calculating thefee
with https://github.com/lightningnetwork/lnd/blob/b7c59b36a74975c4e710a02ea42959053735402e/routing/pathfind.go#L805-L809it should be calculated with
fee := toNodeDist.outboundFee + inboundFee
, which cannot become negative due to the construction of theinboundFee
. The outbound fee of the source would be disregarded, which is unproblematic since it is currently counted as 0. Moreover, the individualfee
components would add up to thetotalFee
again after merging #8941.In the end, the proposal is nothing more than shifting the measurement point of the distance from the point between the involved edges to the middle of the outgoing edge of the
fromVertex
. Intuitively, I would even argue that we find the optimal solution in this way without transforming the graph into a line graph.I am looking forward to your assessment or if I am missing something. The shift seems to be too simple to be true. ;)