-
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
channeldb+invoices: add spontaneous AMP receiving + sending via SendToRoute #5108
channeldb+invoices: add spontaneous AMP receiving + sending via SendToRoute #5108
Conversation
bc85984
to
a1443d7
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.
Completed an initial high level pass......my body is ready 😈
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func testSendToRouteAMP(net *lntest.NetworkHarness, t *harnessTest) { |
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 LIVES!!!! 🤘
@@ -266,6 +266,10 @@ var allTestCases = []*testCase{ | |||
name: "sendtoroute multi path payment", | |||
test: testSendToRouteMultiPath, | |||
}, | |||
{ | |||
name: "sendtoroute amp", |
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.
Yooohooo!
@@ -206,15 +229,91 @@ func updateMpp(ctx *invoiceUpdateCtx, | |||
return &update, ctx.acceptRes(resultAccepted), nil | |||
} | |||
|
|||
var ( |
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.
If we add a check for an invalid AMP payload, before the the check for the total amount, then it would be possible to send a corrupt share to cancel all HTLCs w/ that set ID. This lets the sender bail out if they know things won't work for w/e reason. It would also be useful for more iterative probing.
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 isn't really possible to send an invalid AMP payload, and it's only possible to know if the shares are invalid after receiving all of them. if the receiver tries to reconstruct early they will always be invalid. given that the amount is the termination condition, i don't think there's any getting around that.
the current iteration does fail back all HTLCs if the reconstruction fails tho, to support the probing use case.
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.
Hmm, yeah I get what you mean. Perhaps this can be done by extending the ValidateParsedPayloadTypes
checks: if the sender sends a payload w/o an amount, but with a valid set ID of already held HTLCs, they call get cancelled back?
given that the amount is the termination condition, i don't think there's any getting around that.
Another idea: we reserve a special value of the childIndex
that can be used to signal intent to bail the payment early.
The motivation here is to support a fast-fail mode w/o having to wait for the normal MPP timeout. This can be useful in contexts like attempting to Loop In wherein the user doesn't really know how much they can Loop In, and even if we fail w/ probing the full amount, if can get some additional feedback, then we can offer a: "Loop In as much as you can" mode.
Stepping back a bit, perhaps this can just be implemented by moving one layer higher: a special custom record that just says "ok, can you guys cancel things back now". Applications would be able to use the to-be-designed API for handling hodl AMP payments to handle this, or we'd grow to do it automatically.
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 the PR to include immediate cancellation when seed reconstruction fails as well as some tests.
Stepping back a bit, perhaps this can just be implemented by moving one layer higher: a special custom record that just says "ok, can you guys cancel things back now". Applications would be able to use the to-be-designed API for handling hodl AMP payments to handle this, or we'd grow to do it automatically.
This idea sounds pretty workable! Seems like a pretty contained change as well, also wouldn't need reserved values, special cases, etc in the base protocol
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.
So the receiver gets the cancel signal and cancels all htlcs back, but does the sender learn from that which htlcs were actually held by the receiver vs the ones that were still in-flight or didn't even reach the receiver? I suppose the sender can inspect the failure message to see what the origin of the failure is. Only when an htlc arrives at the receiver after the set has already been cancelled, and is then cancelled back, what does this look like to the sender? Will it be clear that that final htlc wasn't part of the set and reached the receiver "independently", possibly reusing liquidity that was just released?
1deef44
to
e2c684e
Compare
c27b375
to
0dd4856
Compare
0dd4856
to
4b98eec
Compare
4b98eec
to
2ec35b6
Compare
invoices/invoiceregistry.go
Outdated
// processAMP just-in-time inserts an invoice if this htlc is a keysend | ||
// htlc. | ||
func (i *InvoiceRegistry) processAMP(ctx invoiceUpdateCtx) error { | ||
// Only allow keysend for non-mpp payments. |
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 is wrong?
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.
Yeah the comment is dated. IIUC, this now allows a sender to send an invalid payload that causes a failure to occur for the HTLC with the invalid payload. Still getting through the commits to check if this'll end up cancelling back the entire HTLC set w/ the same setID or not....
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 just a sanity check since we can't identify an HTLC set without (pay_addr, set_id), this does not cancel back the entire set
payAddr := ctx.mpp.PaymentAddr() | ||
|
||
// Create placeholder invoice. | ||
invoice := &channeldb.Invoice{ |
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 it is nice to keep around AMP payments that were attempted but didn't complete?
invoices/invoiceregistry.go
Outdated
}, | ||
} | ||
|
||
if i.cfg.KeysendHoldTime != 0 { |
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
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.
IIUC, as is, if this flag is set then we won't immeidatley settle things after all teh shards arrive. However this is only useful if the caller is able to manually settle the payment using the invoice RPC APIs. The existing settle invoice API is based on the caller knowing the pre-image (sub-swaps or they made it themselves) while in this case, such an action isn't possible as the code will attempt to look up the invoice via it's payment hash as a ref: https://github.com/lightningnetwork/lnd/blob/master/invoices/invoiceregistry.go#L1030
So I think we can remove this clause all together since it can't properly be used w/ the existing API. This was originally added for hodl keysend, but in that case there's a single pre-image that's returned via the subscription API once the invoice gets "just in time" inserted into the database.
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.
sounds good, we can add it back one we have the ability to settle cancel the set via RPC
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.
Latest set of changes are looking really good! I have a final pass planned to focus on the added uni test coverage.
invoices/invoiceregistry.go
Outdated
}, | ||
} | ||
|
||
if i.cfg.KeysendHoldTime != 0 { |
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.
IIUC, as is, if this flag is set then we won't immeidatley settle things after all teh shards arrive. However this is only useful if the caller is able to manually settle the payment using the invoice RPC APIs. The existing settle invoice API is based on the caller knowing the pre-image (sub-swaps or they made it themselves) while in this case, such an action isn't possible as the code will attempt to look up the invoice via it's payment hash as a ref: https://github.com/lightningnetwork/lnd/blob/master/invoices/invoiceregistry.go#L1030
So I think we can remove this clause all together since it can't properly be used w/ the existing API. This was originally added for hodl keysend, but in that case there's a single pre-image that's returned via the subscription API once the invoice gets "just in time" inserted into the database.
invoices/invoiceregistry.go
Outdated
// processAMP just-in-time inserts an invoice if this htlc is a keysend | ||
// htlc. | ||
func (i *InvoiceRegistry) processAMP(ctx invoiceUpdateCtx) error { | ||
// Only allow keysend for non-mpp payments. |
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.
Yeah the comment is dated. IIUC, this now allows a sender to send an invalid payload that causes a failure to occur for the HTLC with the invalid payload. Still getting through the commits to check if this'll end up cancelling back the entire HTLC set w/ the same setID or not....
2ec35b6
to
29786c5
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.
LGTM! I've also been building on top of this branch, and it's been working great 👍
rawFeatures := lnwire.NewRawFeatureVector( | ||
lnwire.TLVOnionPayloadOptional, | ||
lnwire.PaymentAddrOptional, | ||
lnwire.AMPRequired, |
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.
If AMP is required, can't we also make paymentaddr
and TLV
required? (not that it really matters)
func (s FailResolutionResult) IsSetFailure() bool { | ||
switch s { | ||
case | ||
ResultAmpReconstruction, |
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.
return ResultAmp.. || ResultHtlc.. || ...
?
payAddr := ctx.mpp.PaymentAddr() | ||
|
||
// Create placeholder invoice. | ||
invoice := &channeldb.Invoice{ |
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 what extent is this an attack vector? With keysend there always a single shard, so every invoice created will at least have a corresponding settled htlc with real money attached to it. With AMP, someone can endlessly send htlcs with different payment addresses. All of them are cancelled back but leave behind invoices in the database.
@@ -206,15 +229,91 @@ func updateMpp(ctx *invoiceUpdateCtx, | |||
return &update, ctx.acceptRes(resultAccepted), nil | |||
} | |||
|
|||
var ( |
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.
So the receiver gets the cancel signal and cancels all htlcs back, but does the sender learn from that which htlcs were actually held by the receiver vs the ones that were still in-flight or didn't even reach the receiver? I suppose the sender can inspect the failure message to see what the origin of the failure is. Only when an htlc arrives at the receiver after the set has already been cancelled, and is then cancelled back, what does this look like to the sender? Will it be clear that that final htlc wasn't part of the set and reached the receiver "independently", possibly reusing liquidity that was just released?
|
||
// Using the child descriptors, reconstruct the root seed and derive the | ||
// child hash/preimage pairs for each of the HTLCs. | ||
children := amp.ReconstructChildren(childDescs...) |
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 had one thing come to my mind about constructing the shares. Maybe it is trivial because I am not completely understandig the tree concept yet.
Suppose the sender sends three htlcs. The share tree then looks like this:
R
/\
/ \
A / \
/ \
B C
C arrives, but A and B fail. Then the sender decides to replace A and B by a single htlc. They didn't do this in the first place, because the amount for that single htlc forces them to take an expensive route. How is the share for this new htlc constructed? I guess it is just A^B?
If the single replacement htlc fails then too and another thing needs to be tried, is the previous tree 'restored'?
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 the new share is constructed as A^B
, then the two HTLCs would be (A^B)^C = R
.
If the single replacement htlc fails then too and another thing needs to be tried, is the previous tree 'restored'?
No there is not need to restore the tree, the algo can continue splitting using A^B
as the parent node and derive two child shares.
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, looking at @halseth's branch clarified it. I think I've been seeing more of a tree than it really is.
Yes, the source of the failure will tell the sender that all HTLCs were held by the receiver. If the sender intentionally fails reconstruction, they should get
The set is never cancelled unless the receiver has HTLCs satisfying the full amount. If any of the individual HTLCs are canceled early, that would happen with |
29786c5
to
7a87a4c
Compare
Yes, so mean the sender-requested early cancellation of the set that was discussed ("ok, can you guys cancel things back now"). For example:
Doesn't it appear to the sender as if there is liquidity for A, B and C, while in reality it could be that there isn't enough for all three at the same time? |
It may have changed in this diff, but last I checked the early cancel isn't yet implemented, though it was something we realized we could. The main gate to reconstruction is still having the full amount present across the set of HTLCS: https://github.com/cfromknecht/lnd/blob/sendtoroute-amp/invoices/update.go#L216. edit: Though I think rn it's possible to still trigger the behavior by sending an mpp payload that has the wrong total amount that mismatches the others. Re your question: I guess it depends on how the sender interprets the failures and if it's path finding is aware of multiple HTLCs sharing the same path or not. Or maybe I don't understand the scenario. |
Just needs a rebase! |
// to create our AMP invoice. | ||
payAddr := ctx.mpp.PaymentAddr() | ||
|
||
// Create placeholder invoice. |
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.
After looking at lnd flame graphs for some time, I now realize that first creating and then updating the invoice is not ideal for maximum throughput. Everything database shows up prominently on the graph. Yes, for multiple shards it matters less, because multiple updates are needed anyway (although - isn't it possible to keep the invoice states fully in-memory until the set is complete? the htlcs are replayed anyway after a restart). But I can imagine that the majority of the cases will be successful single shard AMP payments. Especially when AMP turns out to be the platform of choice for implementing money streaming.
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.
although - isn't it possible to keep the invoice states fully in-memory until the set is complete? the htlcs are replayed anyway after a restart
I guess we do it somewhat for record keeping purposes as well, since by updating it incrementally we can track the arrival, settle, and thus the "hodl" time for a given invoice across all the HTLCs. At a glance though, if we're willing to sacrifice that information, or collect it in an alternative manner (that may potentially be more lossy), I think the same potential optimization can be applied to MPP given that we'll also update the invoice incrementally (the state) as new HTLCs start to arrive.
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 was also considered here: https://github.com/lightningnetwork/lnd/pull/5108/files/7a87a4c6627b6727facdfe1eb52aaeb5353f650a#r604347123
Worthy of a follow up if we don't think it's useful to know the history of prior failed attempts of a spontaneous payment.
This is to not conflict with trampoline BOLT.
Currently we use the AMP record type, this allows us to change it easily as we add new known records. We also bump this to 0x0c instead of 0x0a.
Currently we support queries by payHash or payHash+payAddr. For handling of AMP HTLCs, we only need to support querying by payAddr.
Also refactor existing unit tests to use them.
Adds a set of test cases that exercise the spontaneous AMP payment flow with valid and invalid reconstructions, as well as with single and multiple HTLCs. This also asserts that spontaneous AMP is gated behind the existing AcceptKeysend flag.
This will be reused by the amp itest.
7a87a4c
to
730b718
Compare
This PR adds the ability to receive spontaneous AMP payments, and send them by crafting the appropriate request using
SendToRoute
. This PR is opened as a Draft since not all feature bits/TLV record types are decided.PLEASE DO NOT RUN THIS ON A PRODUCTION NODE
Summary of changes:
InvoiceRef
support for querying bypay_addr
onlyamp
packageSendToRoute
TODO: