-
Notifications
You must be signed in to change notification settings - Fork 123
Abandon API for pending Loop-in swaps #661
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
Conversation
ce8e097 to
a96802c
Compare
bhandras
left a 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.
Did a quick pass, looking good so far! I think we'll need some test coverage on the client side too. Essentially we want to make sure that calling the API in an on-going swap will indeed abandon it.
3de6e81 to
9e0ea5e
Compare
c8d5d0b to
303631e
Compare
ca863da to
0f5fa01
Compare
|
@bhandras: review reminder |
0f5fa01 to
7be9351
Compare
GeorgeTsagk
left a 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.
Very clean PR 🙌
I identified one issue with the usage of abandonChannel. Let me know if you agree and whether one of my suggestions make sense
| } | ||
|
|
||
| message AbandonSwapResponse { | ||
| } |
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.
why not include some basic info here?
Can't the "abandon" operation result in an error?
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 we usually have a pattern where if there is only one success state with no information we return an empty response (is in some lnd apis). If there is an error, we return an error
| risking loss of funds by abandoning a swap. This could happen if an | ||
| abandoned swap would wait on a timeout sweep by the client. | ||
| */ | ||
| bool i_know_what_i_am_doing = 2; |
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 choice 👍
| TypeOut | ||
| ) | ||
|
|
||
| func (t Type) IsOut() bool { |
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.
nit: could fixup this commit? bit tiny
client.go
Outdated
| return errors.New("no request provided") | ||
| } | ||
|
|
||
| s.abandonChan <- req.SwapHash |
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 you need to change the core structure here. A single channel won't notify all swaps that are waiting to read from it.
Here's an example:
We have 3 loop-ins active, in1 in2 and in3
All of them are waiting to read from s.abandonChan in case the user requests for one of them to be abandoned
User wants to abandon in2 so they call AbandonSwap method and that method writes to s.abandonChan as shown in the line above.
A random swap will read that value once, and then the channel is going to be clear. Let's say that in the above example in1 is reading the value, it checks that in1 != in2 and continues with a noop. The intended swap never received the value over the channel because it was consumed only once by in1.
Here's 2 ways you can proceed with this:
-
Enhance the "abandonChan" into a structure that is able to hold multiple channels for each subscriber (i.e you could have a
map[lntypes.Hash]chan Boolwhich means one dedicated channel for each swap). When user tries toAbandonSwapwe will first find the correct channel by accessing the map, and then try to write to that channel. -
The more hacky approach is for each loop-in to re-write the value of
lntypes.Hashinto the channel when they check it and realise it doesn't match theirs. In that case the value would be written only once on the channel from this speciifc line above, but then it would ping-pong between all active loop-ins until it would eventually reach the intended target. (hacky and dirty, but only 1 line of 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.
Thank you for the thorough review and the nice catch. I prefer option 1.) to fix this.
322c807 to
b255724
Compare
GeorgeTsagk
left a 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.
Looks good! just 1 last comment about deleting abandon chans
loopd/swapclient_server.go
Outdated
| } | ||
|
|
||
| // AbandonSwap requests the server to abandon a swap with the given hash. | ||
| func (s *swapClientServer) AbandonSwap(_ context.Context, |
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.
Here we are not actually making use of the request context, but the current implementation of AbandonSwap is writing to a channel that is not buffered. Why not pass down the request context to the methods?
client.go
Outdated
| return errors.New("no request provided") | ||
| } | ||
|
|
||
| s.abandonChans[req.SwapHash] <- 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.
According to my previous comment we could make use of ctx here and do smth like this
| s.abandonChans[req.SwapHash] <- struct{}{} | |
| select { | |
| case s.abandonChans[req.SwapHash] <- struct{}{}: | |
| case ctx.Done(): | |
| return ctx.Err() | |
| } |
in order to avoid blocking indefinitely.
This also raises a new topic: How are channels erased from memory?
And I think an important parameter here is whether the channel is buffered or not (i.e whether you block while writing to it or not)
- Buffered channel: That means you immediately return from the above line thus there is also no need for using
ctx
Note: How/When could we delete entries from the map in this case? - Unbuffered channel: That means we block in the above line and when the execution resumes either
i) ctx is done
ii) swap actually read the signal
and since the channel is only used once we could safely then delete the channel from the map
Seems to me like sticking with 2 (unbuffered channels) is more simple, so I would suggest adding this line on top of the function
// When this function returns there is no use of the swap's abandon chan.
defer delete(s.abandonChans, req.SwapHash)given that the upper block of code with the select statement and ctx is used too
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.
Or if there is another place in the code where client receives signals from swaps when they reach a final state we could delete the map entry there, as it is no longer of 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.
Great points @GeorgeTsagk, thank you!
I think the defer delete however should only be applied if the swap was successfully abandoned, which could still fail even if it was sent on abandon channel. So one might try later again to abandon?
Let me check out the places where we delete from the map.
Thanks again.
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 deletion introduces another problem, namely concurrent access to the channel map. I solved this by introducing a Mutx to the executor that is locked when a loop-in is created and locked again when a swap finishes.
Would be interested in alternative solutions.
b255724 to
0f4cbb7
Compare
0f4cbb7 to
4ffd3fd
Compare
GeorgeTsagk
left a 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.
Great work, LGTM 🤌
bhandras
left a 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.
Looks really good now! Just a question on my side whether we should allow abandoning a loopin swap if the client already published the htlc (because they won't sweep it back then and it'll just hang in limbo) and some smallish comments.
looprpc/client.proto
Outdated
| ABANDON_IN_PROGRESS = 0; | ||
|
|
||
| /* | ||
| ABANDON_FAILURE indicates that the swap was not abandoned due to an unknown |
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.
nit: doc seems to be out of sync.
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.
See other comment, if we go with a simple error then there's no need to fix these comments.
looprpc/client.proto
Outdated
| FAILURE_REASON_MISSING_FLAG = 1; | ||
|
|
||
| /* | ||
| ABANDON_FAILURE indicates that the swap was not abandoned because it is |
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.
nit: seems out of sync.
cmd/loop/swaps.go
Outdated
| Usage: "abandon a swap with a given swap hash", | ||
| Description: "This command overrides the database and abandons a " + | ||
| "swap with a given swap hash.\n\n" + | ||
| "!!! This command might potentially lead to loss of funds if" + |
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.
nit: add space at the end.
cmd/loop/swaps.go
Outdated
| "!!! This command might potentially lead to loss of funds if" + | ||
| "it is applied to swaps that are still waiting for pending " + | ||
| "user funds. Before executing this command make sure that " + | ||
| "no funds are locked by the swap", |
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.
nit: missing period.
cmd/loop/swaps.go
Outdated
| Name: "i_know_what_i_am_doing", | ||
| Usage: "Specify this flag if you made sure that you " + | ||
| "read and understood the following " + | ||
| "consequence of applying this command", |
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.
nit: missing period.
looprpc/client.proto
Outdated
| /* | ||
| The result of the abandon operation. | ||
| */ | ||
| AbandonResult result = 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.
to me the result struct here does not really make sense. The enums are only ever used in conjunction with an error (besides the ABANDON_IN_PROGRESS enum), this means that on the loop-cli side we will always just print the written out error instead of the enum. IMO the swap response can just be empty if successful and throw an error otherwise.
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 tend to agree with this.
| }, | ||
| ) | ||
| if err != nil { | ||
| return err |
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.
see earlier comment, but we will always error out, except on empty response.
4ffd3fd to
84ead39
Compare
bhandras
left a 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.
LGTM, pending decision about the status enum vs just returning an error.
looprpc/client.proto
Outdated
| /* | ||
| The result of the abandon operation. | ||
| */ | ||
| AbandonResult result = 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.
I tend to agree with this.
looprpc/client.proto
Outdated
| ABANDON_IN_PROGRESS = 0; | ||
|
|
||
| /* | ||
| ABANDON_FAILURE indicates that the swap was not abandoned due to an unknown |
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.
See other comment, if we go with a simple error then there's no need to fix these comments.
loopin.go
Outdated
| // If the client requested the swap to be abandoned, we override | ||
| // the status in the database. | ||
| case <-s.abandonChan: | ||
| err = s.setStateAbandoned(ctx) |
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.
nit: can extract this block as a method and then reuse in waitForSwapToComplete too.
loopin.go
Outdated
| s.log.Infof("Abandoning swap %v", s.hash) | ||
| s.setState(loopdb.StateFailAbandoned) | ||
|
|
||
| err := s.persistAndAnnounceState(ctx) |
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.
Still applies.
84ead39 to
4036ff7
Compare
swaprpc: abandon swap api
a4f2c12 to
06f3f35
Compare
bhandras
left a 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.
LGTM, pending the last few comments 🤓 Great work! 🎉
executor.go
Outdated
| // since the swap finalized. | ||
| if swap, ok := newSwap.(*loopInSwap); ok { | ||
| s.Lock() | ||
| delete( |
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.
nit: it seems there's no need for the line breaks as the delete still fits in 80 cols.
client.go
Outdated
| // cancelling the context. | ||
| func (s *Client) Run(ctx context.Context, statusChan chan<- SwapInfo) error { | ||
| func (s *Client) Run(ctx context.Context, statusChan chan<- SwapInfo, | ||
| abandonChans map[lntypes.Hash]chan struct{}) error { |
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.
Instead of passing in this map, it could just be instantiated in NewClient.
loopd/swapclient_server.go
Outdated
| swaps map[lntypes.Hash]loop.SwapInfo | ||
| subscribers map[int]chan<- interface{} | ||
| statusChan chan loop.SwapInfo | ||
| abandonChans map[lntypes.Hash]chan 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.
Seems like this is not used?
loopin.go
Outdated
|
|
||
| return err | ||
| } | ||
| err = s.lnd.Invoices.CancelInvoice( |
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.
nit: no need for line break
loopin.go
Outdated
| ctx, s.hash, | ||
| ) | ||
| if err != nil { | ||
|
|
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.
nit: extra nl
loopin.go
Outdated
| ) | ||
| if err != nil { | ||
|
|
||
| return fmt.Errorf("swap hash "+ |
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: no need for so many line breaks.
loopin.go
Outdated
| func (s *loopInSwap) abandonSwapCancelInvoice(ctx context.Context) error { | ||
| err := s.setStateAbandoned(ctx) | ||
| if err != nil { | ||
|
|
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.
nit: extra nl
loopin.go
Outdated
| return err | ||
| } | ||
|
|
||
| return fmt.Errorf("swap hash "+ |
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.
How about returning this simply from abandonSwapCancelInvoice()?
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.
We rely on the error here, it might be that abandonSwapCancelInvoice() is not returning an error. Also the function might fail due db issues which we want to catch here.
| } | ||
|
|
||
| // setStateAbandoned stores the abandoned state and announces it. | ||
| func (s *loopInSwap) setStateAbandoned(ctx context.Context) error { |
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.
Perhaps this function could return a bool and and error indicating whether canceling the invoice is necessary. Alternatively, since this function is only called from one place you can just inline it there.
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 right, setStateAbandoned could just handle state and invoice cancellation. Less code 👍
loopin.go
Outdated
| return nil | ||
| } | ||
|
|
||
| // abandonSwapCancelInvoice abandons the swap and cancels the swap 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.
nit: either move in previous commit or don't move here to make the diff smaller and consistent with the commit intent.
sputn1ck
left a 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.
Great work! tACK!
LGTM pending @bhandras comments
| TypeOut | ||
| ) | ||
|
|
||
| func (t Type) IsOut() bool { |
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.
nitty nit: missing godoc
06f3f35 to
ce6f826
Compare
This PR introduces a
AbandonSwapApi that currently supports only loop-in swaps. Any pending swap's status can be overridden with a new database state calledStateFailAbandoned. This command relies on the users judgement to only abandon swaps that don't lead to loss of funds, e.g. if a swap is abandoned that had it's time out path not swept yet.