Skip to content

Conversation

@hieblmi
Copy link
Collaborator

@hieblmi hieblmi commented Nov 13, 2023

This PR introduces a AbandonSwap Api that currently supports only loop-in swaps. Any pending swap's status can be overridden with a new database state called StateFailAbandoned. 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.

@hieblmi hieblmi force-pushed the abandon-swaps branch 2 times, most recently from ce8e097 to a96802c Compare November 13, 2023 16:10
@hieblmi hieblmi changed the title Abandon pending Loop-in swaps Abandon API for pending Loop-in swaps Nov 13, 2023
Copy link
Member

@bhandras bhandras left a 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.

@hieblmi hieblmi force-pushed the abandon-swaps branch 4 times, most recently from 3de6e81 to 9e0ea5e Compare November 14, 2023 18:17
@hieblmi hieblmi requested a review from bhandras November 14, 2023 18:21
@hieblmi hieblmi force-pushed the abandon-swaps branch 3 times, most recently from c8d5d0b to 303631e Compare November 15, 2023 09:14
@hieblmi hieblmi force-pushed the abandon-swaps branch 2 times, most recently from ca863da to 0f5fa01 Compare November 20, 2023 19:12
@lightninglabs-deploy
Copy link

@bhandras: review reminder

Copy link
Member

@GeorgeTsagk GeorgeTsagk left a 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 {
}
Copy link
Member

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?

Copy link
Member

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;
Copy link
Member

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 {
Copy link
Member

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
Copy link
Member

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:

  1. 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 Bool which means one dedicated channel for each swap). When user tries to AbandonSwap we will first find the correct channel by accessing the map, and then try to write to that channel.

  2. The more hacky approach is for each loop-in to re-write the value of lntypes.Hash into 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)

Copy link
Collaborator Author

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.

@hieblmi hieblmi force-pushed the abandon-swaps branch 3 times, most recently from 322c807 to b255724 Compare November 22, 2023 09:31
@hieblmi hieblmi requested a review from GeorgeTsagk November 22, 2023 09:31
Copy link
Member

@GeorgeTsagk GeorgeTsagk left a 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

}

// AbandonSwap requests the server to abandon a swap with the given hash.
func (s *swapClientServer) AbandonSwap(_ context.Context,
Copy link
Member

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{}{}
Copy link
Member

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

Suggested change
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)

  1. 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?
  2. 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

Copy link
Member

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

Copy link
Collaborator Author

@hieblmi hieblmi Nov 22, 2023

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.

Copy link
Collaborator Author

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.

@hieblmi hieblmi requested a review from GeorgeTsagk November 22, 2023 17:32
Copy link
Member

@GeorgeTsagk GeorgeTsagk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work, LGTM 🤌

Copy link
Member

@bhandras bhandras left a 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.

ABANDON_IN_PROGRESS = 0;

/*
ABANDON_FAILURE indicates that the swap was not abandoned due to an unknown
Copy link
Member

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.

Copy link
Member

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.

FAILURE_REASON_MISSING_FLAG = 1;

/*
ABANDON_FAILURE indicates that the swap was not abandoned because it is
Copy link
Member

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.

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" +
Copy link
Member

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.

"!!! 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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing period.

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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing period.

/*
The result of the abandon operation.
*/
AbandonResult result = 1;
Copy link
Member

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.

Copy link
Member

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
Copy link
Member

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.

Copy link
Member

@bhandras bhandras left a 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.

/*
The result of the abandon operation.
*/
AbandonResult result = 1;
Copy link
Member

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.

ABANDON_IN_PROGRESS = 0;

/*
ABANDON_FAILURE indicates that the swap was not abandoned due to an unknown
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still applies.

@hieblmi hieblmi force-pushed the abandon-swaps branch 2 times, most recently from a4f2c12 to 06f3f35 Compare November 24, 2023 10:46
@hieblmi hieblmi requested review from bhandras and sputn1ck November 24, 2023 11:10
Copy link
Member

@bhandras bhandras left a 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(
Copy link
Member

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 {
Copy link
Member

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.

swaps map[lntypes.Hash]loop.SwapInfo
subscribers map[int]chan<- interface{}
statusChan chan loop.SwapInfo
abandonChans map[lntypes.Hash]chan struct{}
Copy link
Member

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(
Copy link
Member

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 {

Copy link
Member

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 "+
Copy link
Member

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 {

Copy link
Member

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 "+
Copy link
Member

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()?

Copy link
Collaborator Author

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 {
Copy link
Member

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.

Copy link
Collaborator Author

@hieblmi hieblmi Nov 27, 2023

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.
Copy link
Member

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.

Copy link
Member

@sputn1ck sputn1ck left a 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitty nit: missing godoc

@hieblmi hieblmi requested a review from bhandras November 27, 2023 10:26
@hieblmi hieblmi merged commit 9c1b745 into lightninglabs:master Nov 27, 2023
@hieblmi hieblmi deleted the abandon-swaps branch August 22, 2025 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants