-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
routing: fix route fee calculation and channel capacity check #1382
Conversation
routing/pathfind_test.go
Outdated
createHop(10000, 1000), | ||
}, | ||
expectedTotalAmount: 102010, | ||
} } |
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.
This test case fails on master. It returns a total amount of 102000
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.
Can you add to the commit message why this fails on master, and what you did to fix it?
routing/pathfind_test.go
Outdated
createHop(1000, 100), | ||
createHop(1000, 1000), | ||
}, | ||
expectFailure: true, |
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.
This test case fails on master. It does not generate a failure even though the first channel has insufficient capacity.
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.
Can you add to the commit message why this fails on master, and what you did to fix it?
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.
Another very welcome bugfix! 🙌
routing/pathfind.go
Outdated
fee := lnwire.MilliSatoshi(0) | ||
|
||
// If this isn't the last hop, to add enough funds to pay for | ||
// transit over the next link. | ||
if i != len(pathEdges)-1 { | ||
// We'll grab the edge policy and per-hop payload of | ||
// the prior hop so we can calculate fees properly. | ||
prevEdge := pathEdges[i+1] | ||
prevHop := route.Hops[i+1] |
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.
should really be named nextHop
?
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.
Yes. Renamed.
routing/pathfind_test.go
Outdated
createHop(1000, 100), | ||
createHop(1000, 1000), | ||
}, | ||
expectFailure: true, |
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.
Maybe we should make sure that the error we get is ErrInsufficientCapacity
? :)
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.
Good addition. Also because in another PR I will check for a different error code. I chose to use two fields: expectFailure and expectedErrorCode. Any suggestion to do this more go'ish is welcome.
routing/pathfind.go
Outdated
@@ -395,7 +384,7 @@ func newRoute(amtToSend, feeLimit lnwire.MilliSatoshi, sourceVertex Vertex, | |||
|
|||
// The total amount required for this route will be the value the | |||
// source extends to the first hop in the route. | |||
route.TotalAmount = runningAmt | |||
route.TotalAmount = route.Hops[0].AmtToForward + route.Hops[0].Fee |
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.
Comment should be updated to explain the calculation.
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.
Updated
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.
Nice. I would also add a note that the source node (the sender of the payment) is not part of the hops here.
routing/pathfind_test.go
Outdated
createHop(1000, 100), | ||
createHop(1000, 1000), | ||
}, | ||
expectFailure: true, |
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.
Can you add to the commit message why this fails on master, and what you did to fix it?
routing/pathfind_test.go
Outdated
createHop(10000, 1000), | ||
}, | ||
expectedTotalAmount: 102010, | ||
} } |
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.
Can you add to the commit message why this fails on master, and what you did to fix it?
@halseth Changes made. |
routing/pathfind.go
Outdated
// enough capacity to carry the required amount which | ||
// includes the fee dictated at each hop. Make the comparison | ||
// in msat to prevent rounding errors. | ||
if nextHop.AmtToForward + fee > lnwire.NewMSatFromSatoshis( |
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.
This is actually the outgoing channel though. Also at this point w/ the old code, the amtToForward
already factors in the fee added at the prior hop. The fee is the difference between the incoming and outgoing amount to a target node.
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.
I thiiink this is correct now.
routing/pathfind.go
Outdated
// | ||
// * amt_in - fee >= amt_to_forward | ||
amtToForward = runningAmt - fee | ||
nextHop := route.Hops[i+1] |
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.
This is actually the previous hop. We're traversing backwards right now along the route.
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.
It gets confusing with different definitions of prev and next. Is it next loop-wise or next in the payment route. It would be good to adhere to one definition. Discussed with @halseth and opted for "next = next in route".
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.
Maybe I should rename it to that. nextHopInRoute.
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.
Ah, yeah I was thinking "next = next in route". It is definitely confusing using prev
when we are traversing backwards. Looking at the comments, lastHop
is also being used, so I think sticking to "first = first in route, last = last in route, next = next in route" makes sense. Comments will need to be updated to reflect this though.
routing/pathfind.go
Outdated
@@ -75,7 +75,7 @@ type ChannelHop struct { | |||
// included within the Sphinx packet. | |||
type Hop struct { | |||
// Channel is the active payment channel edge that this hop will travel | |||
// along. | |||
// along. This is the _incoming_ channel to this hop. |
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.
This is the outgoing channel. The HTLC will flow on this path.
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.
As I understood it, a hop is an intermediate or final node (from rfc).
The result of findPath is a list of channels that the htlc should flow along. First element in this list contains the first channel. From source to first intermediate hop. So if the first element in the list is the first hop, than this Channel property must be the incoming channel to the first hop?
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.
As I understood it Hop
is analogous to an "edge" here, so saying it is incoming/outgoing doesn't make that much sense. However, the ChannelHop
contains a ChannelEdgePolicy
that houses the NodeID
of the node this edge leads to. So something about that can be added to the comment.
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.
Yes, it is in the language. According to the lightning rfc, a hop is a node. The Hop
type is really what it should be. But ChannelHop
might be better renamed to something else. ChannelEdge
then probably. Not too far off, it's a policy, a target and a bandwidth estimate.
routing/pathfind.go
Outdated
// The amount that this hop needs to forward is based | ||
// on how much the prior hop carried plus the fee | ||
// that needs to be paid to the prior hop | ||
amtToForward = nextHop.AmtToForward + nextHop.Fee |
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.
The fee has already been accumulated into what the next hop pays. The fee is the difference between hop_{i+1}.amount and hop_{i}.amount.
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.
I think what is confusing here is how the Hop.Fee
relates to the Hop.AmountToForward
. From the godoc on those fields, it looks like the end node of this Hop
(=edge) will receive AmountToFoward
, meaning the start node subtracted Fee
. But here we set the "last hop" fee to zero, which doesn't work with that definition. So I think we should make the documentation match the usage here (by either modifying docs or use).
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.
A hop is a node according to lightning-rfc. I think we should stick to that definition in the code?
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.
I think it makes sense to name hop=node
with the current comments :)
// following inequality most hold true: | ||
// | ||
// * amt_in - fee >= amt_to_forward | ||
amtToForward = runningAmt - fee |
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.
This is where the current implementation is incorrect in my understanding. The second to last hop will here always have amtToForward
equal to the last hop.
If this is correct, how did this ever work? 🤔
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.
That's correct, it should forward this exact amount to the last hop. Note that this is was goes into the hop payload for the node.
routing/pathfind_test.go
Outdated
createHop(10, 1000), | ||
createHop(10, 1000), | ||
}, | ||
expectedTotalAmount: 100002, |
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.
maybe add a per hop expected fees as well. Will also make it easier to see how the total is calculated :)
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.
Indeed, maybe better to assert the full result. But I'd rather postpone that until #1321 is merged. Then I can properly separate the newRoute impl+test from the findPath impl+test. Different responsibilties, separate files.
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.
I don't see why we would have to postpone that. Looks like that could be added to these testcases without any dependency on #1321.
813689f
to
13366b2
Compare
Integration test added. It fails on master with "unable to route payment to destination: FeeInsufficient(fee=1101000 mSAT, update=(lnwire.ChannelUpdate)" |
1e3d558
to
97bdf61
Compare
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.
This PR is starting to get into a rally good shape! The code makes more sense now, and it is IMO much easier to follow after your recent comment updates 👍
I also emailed Travis to unflag your account such that we can get the tests running.
lnd_test.go
Outdated
|
||
updateFeeReq := &lnrpc.PolicyUpdateRequest{ | ||
BaseFeeMsat: baseFee, | ||
FeeRate: float64(feeRate) / feeBase, |
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.
Is this a global feeBase
?
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.
That was the idea. But I see that I did not reference the global one defined in rpcserver
everywhere. And on a second thought, it does not look like such a good idea. Someone could accidentally change feeBase
in rpcserver, breaking the external interface, but not failing the test. Replaced all occurences in lnd_test
with a different value testFeeBase
.
lnd_test.go
Outdated
chanPoint, | ||
) | ||
|
||
time.Sleep(time.Millisecond * 50) |
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.
not needed?
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.
No, removed.
lnd_test.go
Outdated
@@ -2953,19 +3005,26 @@ func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) { | |||
// increasing of time is needed to embed the HTLC in commitment | |||
// transaction, in channel Carol->David->Alice->Bob, order is Bob, | |||
// Alice, David, Carol. | |||
const amountPaid = int64(5000) | |||
amountPaid := int64(5000) | |||
const expectedFeeAlice = 505 |
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.
These calculations should be explained.
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.
Comments added
lnd_test.go
Outdated
assertAmountPaid(t, ctxb, "Alice(local) => Bob(remote)", net.Bob, | ||
aliceFundPoint, int64(0), amountPaid) | ||
assertAmountPaid(t, ctxb, "Alice(local) => Bob(remote)", net.Alice, | ||
aliceFundPoint, amountPaid, int64(0)) | ||
|
||
amountPaid += expectedFeeAlice |
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.
Same, should be explained.
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.
Comments added and variables clarified.
lnd_test.go
Outdated
const exectedFees = 5 | ||
if feeReport.DayFeeSum != exectedFees { | ||
|
||
if feeReport.DayFeeSum != uint64(expectedFeeDave) { |
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.
Comments above should be updated.
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.
Fee amount updated
routing/pathfind_test.go
Outdated
|
||
return &ChannelHop { | ||
ChannelEdgePolicy: &channeldb.ChannelEdgePolicy { | ||
Node: &channeldb.LightningNode{}, |
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.
I think we should set the baseFee
here for clarity. Either as a parameter, or just BaseFee: 0
. I think it makes sense as a parameter, as that lets us also test hops with non-zero base fee.
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.
Added
routing/pathfind_test.go
Outdated
// A three hop payment where the first and second hop | ||
// will both charge 1 msat. The fee for the first hop | ||
// is actually slightly higher than 1, because the amount | ||
// t0 fwd also includes the fee for the second hop. This |
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.
"to forward"
routing/pathfind_test.go
Outdated
// will both charge 1 msat. The fee for the first hop | ||
// is actually slightly higher than 1, because the amount | ||
// t0 fwd also includes the fee for the second hop. This | ||
// gets rounded down to 1. |
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.
Nice test case! Also add a similar test with the feerate exactly high enough to not get rounded down?
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.
Added
routing/pathfind_test.go
Outdated
createHop(10, 1000), | ||
createHop(10, 1000), | ||
}, | ||
expectedTotalAmount: 100002, |
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.
I don't see why we would have to postpone that. Looks like that could be added to these testcases without any dependency on #1321.
routing/pathfind_test.go
Outdated
expectedCode := testCase.expectedErrorCode | ||
if err == nil || !IsError(err, expectedCode) { | ||
t.Fatalf("expected newRoute to fail " + | ||
"with error code %v", |
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.
add "instead got error %v"
@halseth Your comments have been addressed. |
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.
Excellent work! One worthy follow up I may add as an issue is to take advantage of the testing.Quick
package to allow us to generate a set of randomized "well formed" routes and assert certain properties against them based on path finding attempts. If we had this type of testing in place, we may have caught this bug much earlier on.
I left one minor comment w.r.t adding a comment within some fields on the newly added testing fixture just to elaborate on the meaning of a particular attribute. Other than that, LGTM ⚡️
Once the fixup commits have been rebased in, we can get this one merged, then proceed with the other related PR's.
routing/pathfind_test.go
Outdated
hops []*ChannelHop | ||
paymentAmount lnwire.MilliSatoshi | ||
expectedFees []lnwire.MilliSatoshi | ||
expectedTimeLocks []uint32 |
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.
What exactly is the expected time lock here? I'm having a hard time following this usage, but everything else is pristine. It may help to add a set of comments to all the fields in this struct.
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.
Comments added
This change makes the test more sensitive to bugs than a route with nodes that all enforce the same fee policy. In addition to that, the fee has been increased to a level at which potential problems with improper fee calculation become detectable (and not disappear in rounding).
This commit fixes the logic inside the newRoute function to address the following problems: - Fee calculation for a hop does not include the fee that needs to be paid to the next hop. - The incoming channel capacity "sanity" check does not include the fee to be paid to the current hop.
This comment extends the unit tests for NewRoute with checks on the total time lock for a route as well as the expected time lock values for every hop along the route.
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.
LGTM 🚀
This PR fixes two problems in the routing package:
It also updates the multi hop payment integration test to cover the fee miscalculation.