-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(swaps): recover crashed swap deals
This commit attempts to recover swap deals that were interrupted due to a system or `xud` crash. In the case where we are the maker and have attempted to send payment for the second leg of the swap, we attempt to query the swap client for the preimage of that payment in case it went through. We can then use that preimage to try to claim the payment from the first leg of the swap. In case the payment is knkown to have failed, we simply attempt to close any open invoices and mark the swap deal as having errored. If an outgoing payment is still in flight and we do not have the preimage for it, we add it to a set of "pending" swaps and check on it on a scheduled interval until we can determine whether it has failed or succeeded. A new `SwapRecovery` class is introduced to contain the logic for recovering interrupted swap deals and for tracking swaps that are still pending. Raiden currently does not expose an API call to push a preimage to claim an incoming payment or to reject an incoming payment, instead we print a warning to the log for now. The recovery attempts happen on `xud` startup by looking for any swap deals in the database that have an `Active` state. Closes #1079.
- Loading branch information
Showing
7 changed files
with
287 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import SwapClientManager from './SwapClientManager'; | ||
import { SwapDealInstance } from '../db/types'; | ||
import Logger from '../Logger'; | ||
import { SwapPhase, SwapState, SwapFailureReason, SwapRole, SwapClientType } from '../constants/enums'; | ||
import SwapClient, { PaymentState } from './SwapClient'; | ||
|
||
/** | ||
* A class that's responsible for recovering swap deals that were interrupted due to a system or xud crash, | ||
* ensuring that we do not lose funds on a partially completed swap. | ||
*/ | ||
class SwapRecovery { | ||
/** A set of swaps where we have a pending outgoing payment for swaps where we don't know the preimage. */ | ||
public pendingSwaps: Set<SwapDealInstance> = new Set(); | ||
private pendingSwapsTimer?: NodeJS.Timeout; | ||
|
||
constructor(private swapClientManager: SwapClientManager, private logger: Logger) { } | ||
|
||
public beginTimer = () => { | ||
if (!this.pendingSwapsTimer) { | ||
this.pendingSwapsTimer = setInterval(this.checkPendingSwaps, 300000); | ||
} | ||
} | ||
|
||
private checkPendingSwaps = () => { | ||
this.pendingSwaps.forEach(pendingSwap => this.recoverDeal(pendingSwap).catch(this.logger.error)); | ||
} | ||
|
||
public stopTimer = () => { | ||
if (this.pendingSwapsTimer) { | ||
clearInterval(this.pendingSwapsTimer); | ||
this.pendingSwapsTimer = undefined; | ||
} | ||
} | ||
|
||
private failDeal = async (deal: SwapDealInstance, receivingSwapClient?: SwapClient) => { | ||
if (receivingSwapClient) { | ||
try { | ||
await receivingSwapClient.removeInvoice(deal.rHash); | ||
} catch (err) {} | ||
} | ||
deal.state = SwapState.Error; | ||
deal.failureReason = SwapFailureReason.Crash; | ||
this.pendingSwaps.delete(deal); | ||
await deal.save(); | ||
} | ||
|
||
public recoverDeal = async (deal: SwapDealInstance) => { | ||
const makerSwapClient = this.swapClientManager.get(deal.makerCurrency); | ||
const takerSwapClient = this.swapClientManager.get(deal.takerCurrency); | ||
if (!makerSwapClient || !makerSwapClient.isConnected()) { | ||
this.logger.warn(`could not recover deal ${deal.rHash} because ${deal.makerCurrency} swap client is offline`); | ||
this.pendingSwaps.add(deal); | ||
return; | ||
} | ||
if (!takerSwapClient || !takerSwapClient.isConnected()) { | ||
this.logger.warn(`could not recover deal ${deal.rHash} because ${deal.takerCurrency} swap client is offline`); | ||
this.pendingSwaps.add(deal); | ||
return; | ||
} | ||
|
||
this.logger.info(`recovering swap deal ${deal.rHash}`); | ||
switch (deal.phase) { | ||
case SwapPhase.SwapAccepted: | ||
// we accepted the deal but stopped before sending payment | ||
// cancel the open invoice if we have one | ||
await this.failDeal(deal, makerSwapClient); | ||
break; | ||
case SwapPhase.SendingPayment: | ||
// we started sending payment but didn't claim our payment | ||
if (deal.role === SwapRole.Maker) { | ||
// we should check to see if our payment went through | ||
// if it did, we can claim payment with the preimage for our side of the swap | ||
const paymentStatus = await takerSwapClient.lookupPayment(deal.rHash); | ||
if (paymentStatus.state === PaymentState.Succeeded) { | ||
try { | ||
deal.rPreimage = paymentStatus.preimage!; | ||
if (makerSwapClient.type === SwapClientType.Raiden) { | ||
// tslint:disable-next-line: max-line-length | ||
this.logger.warn(`cannot claim payment on Raiden for swap ${deal.rHash} using preimage ${deal.rPreimage}, this should be investigated manually`); | ||
} else { | ||
await makerSwapClient.settleInvoice(deal.rHash, deal.rPreimage); | ||
this.logger.info(`recovered ${deal.makerCurrency} swap payment of ${deal.makerAmount} using preimage ${deal.rPreimage}`); | ||
} | ||
deal.state = SwapState.Recovered; | ||
this.pendingSwaps.delete(deal); | ||
await deal.save(); | ||
// TODO: update order and trade in database to indicate they were executed | ||
} catch (err) { | ||
// tslint:disable-next-line: max-line-length | ||
this.logger.error(`could not settle ${deal.makerCurrency} invoice for payment ${deal.rHash} and preimage ${deal.rPreimage}, this should be investigated manually`, err); | ||
await this.failDeal(deal); | ||
} | ||
} else if (paymentStatus.state === PaymentState.Failed) { | ||
// the payment failed, so cancel the open invoice if we have one | ||
await this.failDeal(deal, makerSwapClient); | ||
} else { | ||
// the payment is pending, we will need to follow up on this | ||
this.pendingSwaps.add(deal); | ||
} | ||
} else if (deal.role === SwapRole.Taker) { | ||
// we are not at risk of losing funds, but we should cancel any open invoices | ||
await this.failDeal(deal, takerSwapClient); | ||
} | ||
break; | ||
case SwapPhase.PaymentReceived: | ||
// we've claimed our payment | ||
// TODO: send a swap completed packet? it may be too late to do so | ||
deal.state = SwapState.Recovered; | ||
await deal.save(); | ||
break; | ||
default: | ||
break; | ||
} | ||
|
||
} | ||
} | ||
|
||
export default SwapRecovery; |
Oops, something went wrong.