Skip to content
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

Automatically fix stuck swaps #416

Merged
merged 29 commits into from
Jul 13, 2018
Merged

Automatically fix stuck swaps #416

merged 29 commits into from
Jul 13, 2018

Conversation

lukechilds
Copy link
Member

@lukechilds lukechilds commented Jul 11, 2018

Resolves #163

This PR resolves the issue of users getting swaps that are stuck mid swap (e.g in swap 3/5 state).

If a swap fails due to connectivity issues or closing the app the swap would become "stuck". Although the funds were cryptographically safe and reclaimable via the atomic swap protocol, marketmaker wouldn't actually reclaim the funds automatically without custom intervention via the console or CLI.

We are now manually looping over old swaps every 15 mins that are in a stuck state and "kickstarting" them to get marketmaker to recheck the status.

There are quite a lot of changes required to the current code base to get this working. I will try and break down the changes here and explain why they are necessary.

Get updates on stuck swaps 1e7ceee 5eaedd9

We were previously listening for updates over the socket for a swap by adding a new event listener for each swap that listens out for messages containing it's UUID. This was not a great approach and resulted in a few crazy hacks to avoid memory leaks whilst also correctly handling quirky marketmaker behaviour where a swap would be reported as failed and the event listener was removed, only to later send us a completion message.

Now we only attach a single event listener and check all messages against a list of all known swap UUIDs. This means we can guarantee we will always catch updates to swaps, if they were started in a previous session and even if they are received in a strange order, e.g failed then complete. It's also much simpler code and avoids risks of memory leaks having a large amount of event listeners.

Force kickstart stuck swaps 2e89499 6514951

This is supposed to be handled automatically by marketmaker, but I've never personally seen it actually work. What we now do is loops over all swaps that are currently swapping and were started over 4 hours ago, then call marketmaker's kickstart method. This forces it to check the current state of the swap and if something has gone wrong or Bob hasn't responded it will attempt to reclaim the funds from the timelocked address. This is why we only check if the swap is over 4 hours old, because the timelocks last 4 hours. We do this instantly on startup and then check again every 15 minutes.

On the first startup check, we check all pending swaps, even if they weren't started over 4 hours ago, this is because there are some scenarios where a swap can be reverted sooner.

This means you don't need to keep HyperDEX (and marketmaker running). You can close the app, open it the next day, and it will instantly reclaim your stuck swap if possible.

Recalculate the state of a completed swap fc1caab 45b5b49

Due to the fact that HyperDEX (and marketmaker) might not be running for the entire swap, we can't rely on the WebSocket message history to provide an accurate overview of what happened during the swap. The completed message we get at the end of every normal swap and "kickstarted" stuck swaps contains all the information we need to rebuild the full history of what happened.

So we now have two ways of building a formatted swap object. The socket messages are used while a swap is in progress. Once a swap is completed, the final completed socket message is used to reconstruct the entire swap transaction history and state which we know contains all data. The resulting structure of the swap object follows the same pattern in both methods so all existing UI code works no matter how the swap data was calculated.

This means if a swap is completed it should never have missing tx data, it should always list the entire chain of TXs, even if it wasn't online the whole time. It also means we correctly handle weird states like when the swap doesn't go to plan and we reclaim our original payment or claim bobdeposit instead of bobpayment. It also abstracts out quite a few quirks in marketmaker responses.

Showing kickstarted swaps TXs in the swap modal 1a95811 0e55952

Kickstarted swaps don't always have the same transactions exposed as our current code expects which breaks the modal layout. I've modified the transactions array to not allow duplicate transactions (sometimes marketmaker sends multiple messages for the same TX which we were pushing into the transactions array). This means while the swap is in progress we can rely on just looping over the transaction array for the current data are less the 5 items fill them in with the placeholders.

Once a swap is completed however, there may not be 5 transactions due to the reversion and we don't want to show the placeholder boxes as those transactions are no longer expected to occur. However the reversion transactions are handled by the completed message and added to the transactions array, so just looping over that means they will show up, we just skip showing the placeholder boxes.


There's also a few other bits handling weird mm responses and adding extra data for stuck swaps, but they're fairly self explanatory.

To show how successfully resumed swaps look in the UI:

A swap that was stuck, but then resumed in time to proceed as normal

By either a reboot, losing network connectivity, accidentally closing the app etc.

This swap appears just like a normal swap that completed without issue:

screen shot 2018-07-12 at 3 21 59 pm

screen shot 2018-07-12 at 3 22 11 pm

Notice the final TX is alicespend, the expected TX for a successful swap

A swap that was stuck due to Bob not sending bobpayment

If Bob doesn't send bobpayment you need to wait four hours for the timelock on bobdeposit to expire. The we can claim bobdeposit with an aliceclaim TX. bobdepost is 12.5% more than the amount we were supposed to get. We get to keep this to punish Bob for being a dick. (In a normal trade Bob would reclaim his own bobdeposit after we spend bobpayment with alicespend).

You don't actually need to keep the app running for four hours though, you could go to bed and run the app in the morning and it will reclaim everything.

This swap appears just like a normal swap in the swap list. However in the modal it will show only four TXs in the TX chain and you'll notice that it shows a much higher percentage message than normal due to our 12.5% bonus:

screen shot 2018-07-12 at 3 24 10 pm

screen shot 2018-07-12 at 3 23 52 pm

Notice the final TX is aliceclaim and is higher than what we asked for

A swap that was stuck due to Bob not sending any payments

If Bob doesn't send any payments (bobdeposit or bobpayment) we can't really do much about it. We can reclaim our alicepayment funds but we lose our dexfee payment.

Although marketmaker reports the status as successfully completed (which kind of makes sense in terms of the atomic swap protocol), I'm mapping this to a failed swap in our logic because if the user doesn't get their desired funds, it's failed from their perspective.

So it will show in the swap list as reverted with the same styles as a failed/unmatched swap. The received value will be set as 0 and you will see the reclaim TX as the final TX in the chain. There is also a relatively vague message explaining what went wrong. (We really don't know what went wrong, it could have been many things):

screen shot 2018-07-12 at 3 24 21 pm

screen shot 2018-07-12 at 3 24 29 pm

Notice the final TX is alicereclaim which just claims back our original funds


As far as I'm aware, this correctly handles all stuck states and displays them correctly in the UI. You are guaranteed to either:

  • Get the amount you were supposed to
  • Get the amount you were supposed to + 12.5%
  • Reclaim your original payment (but lose your dexfee)

Even if the 4 hour timelock hasn't expired, some of them might still be claimable if we already know `bobpayment`
There are edge cases in marketmaker where the amounts aren't always set. sentflags appears to be more reliable.
The marketmaker supplied values `srcamount` and `destamount` are incorrect if the swap ends with any tx other than alicespend
marketmaker sends duplicate messages for some txs.
The reason for the long delay was that the kickstart command is quite
expensive and blocks marketmaker. However we only run this on swaps that
have been swapping for longer than four hours, so it shouldn't actually
match swaps fire the kickstart command very often.
Even if the atomic swap protocol completes successfully, the swap should be classed as failed if the user didin't end up with the funds they wanted.
@lukechilds lukechilds changed the title WIP Stuck swaps [WIP] Stuck swaps Jul 12, 2018
@lukechilds lukechilds requested a review from a team July 12, 2018 08:44
@lukechilds
Copy link
Member Author

Also, I'm aware the _formatSwap() function has grown a little bit out of control. It's now doing way more than we ever intended it to. It's handling multiple different ways of reading swap data and abstracting all of marketmaker's weirdness away to try and expose a single uniformly formatted swap object.

This function should probably be refactored into it's own file with more readable methods at some point.

@sindresorhus
Copy link
Contributor

This function should probably be refactored into it's own file with more readable methods at some point.

Can you add a TODO comment in the code so we don't forget?

});

const FIFTEEN_MINUTES = 1000 * 60 * 15;
fireEvery(async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

fireEvery({minutes: 15}, async () => {

swap.transactions.push({
stage: 'myfee',
coin: message.alice,
txid: undefined, // This is currently mising from marketmaker responses
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you link to a marketmaker issue?

Copy link
Member Author

Choose a reason for hiding this comment

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

There is no GH issue, but I mentioned it to James and he said he'd fixed it. Will test and create an issue if it isn't resolved.

// There is a bug in marketmaker where it doesn't always correctly report
// the values. If aliceclaim is 0 we fallback to bobdeposit as it should
// be very close (same amount minus our claim txfee)
// https://github.com/jl777/SuperNET/issues/920
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems to be fixed now? jl777/SuperNET#920 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, should be, yet to check that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Doesn't appear to be resolved jl777/SuperNET#920 (comment)

@sindresorhus sindresorhus changed the title [WIP] Stuck swaps Automatically fix stuck swaps Jul 13, 2018
@sindresorhus sindresorhus merged commit 1d4a0bc into master Jul 13, 2018
@sindresorhus sindresorhus deleted the stuck-swaps branch July 13, 2018 13:04
@sindresorhus
Copy link
Contributor

Oh yeah! Feels good 🙌

d8ee7104-273c-11e5-9676-43241f2fc082

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.

Support kickstarting swaps
2 participants