From 07ed61963531ede6ddd3e2c10e0d83bd61107fd2 Mon Sep 17 00:00:00 2001 From: Daniel McNally Date: Fri, 5 Jul 2019 16:15:19 -0400 Subject: [PATCH] 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 all other cases, we simply attempt to close any open invoices and mark the swap deal as having errored. Raiden currently does not expose an API call to query for the preimage of a completed payment. The recovery attempts happen once on `xud` startup by looking for any swap deals in the database that have an `Active` state. Closes #1079. --- lib/constants/enums.ts | 8 ++++ lib/lndclient/LndClient.ts | 17 +++++++ lib/raidenclient/RaidenClient.ts | 5 +++ lib/swaps/SwapClient.ts | 6 +++ lib/swaps/Swaps.ts | 77 ++++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+) diff --git a/lib/constants/enums.ts b/lib/constants/enums.ts index 79b632b7c..07a1e5302 100644 --- a/lib/constants/enums.ts +++ b/lib/constants/enums.ts @@ -81,6 +81,12 @@ export enum SwapState { Active = 0, Error = 1, Completed = 2, + /** + * A swap that was executed but wasn't formally completed. This may occur as a result of xud + * crashing late in the swap process, after htlcs for both legs of the swap are set up but + * before the swap is formally complete. + */ + Recovered = 3, } export enum ReputationEvent { @@ -125,6 +131,8 @@ export enum SwapFailureReason { DealTimedOut = 11, /** The swap failed due to an unrecognized error. */ UnknownError = 12, + /** The swap failed because of a system or xud crash while the swap was being executed. */ + Crash = 12, } export enum DisconnectionReason { diff --git a/lib/lndclient/LndClient.ts b/lib/lndclient/LndClient.ts index ba582eccc..a9db3aa9c 100644 --- a/lib/lndclient/LndClient.ts +++ b/lib/lndclient/LndClient.ts @@ -505,6 +505,23 @@ class LndClient extends SwapClient { } } + public lookupPayment = async (rHash: string) => { + const payments = await this.listPayments(); + for (const payment of payments.getPaymentsList()) { + if (payment.getPaymentHash() === rHash) { + const preimage = payment.getPaymentPreimage(); + if (preimage) { + return preimage; + } + } + } + return undefined; + } + + private listPayments = (): Promise => { + return this.unaryInvoiceCall('listPayments', new lndrpc.ListPaymentsRequest()); + } + private addHoldInvoice = (request: lndinvoices.AddHoldInvoiceRequest): Promise => { return this.unaryInvoiceCall('addHoldInvoice', request); } diff --git a/lib/raidenclient/RaidenClient.ts b/lib/raidenclient/RaidenClient.ts index 54450d18c..a3cbcd104 100644 --- a/lib/raidenclient/RaidenClient.ts +++ b/lib/raidenclient/RaidenClient.ts @@ -150,6 +150,11 @@ class RaidenClient extends SwapClient { // not implemented, raiden does not use invoices } + public lookupPayment = async () => { + // raiden does not currently expose an API to retrieve a preimage for a completed payment + return undefined; + } + public getRoutes = async (_amount: number, _destination: string) => { // stub placeholder, query routes not currently implemented in raiden return [{ diff --git a/lib/swaps/SwapClient.ts b/lib/swaps/SwapClient.ts index 503fc0221..69528774d 100644 --- a/lib/swaps/SwapClient.ts +++ b/lib/swaps/SwapClient.ts @@ -121,6 +121,12 @@ abstract class SwapClient extends EventEmitter { public abstract async removeInvoice(rHash: string): Promise; + /** + * Checks to see whether we've made a payment using a given rHash. + * @returns the preimage for the payment, or `undefined` if no payment was made + */ + public abstract async lookupPayment(rHash: string): Promise; + /** * Gets the block height of the chain backing this swap client. */ diff --git a/lib/swaps/Swaps.ts b/lib/swaps/Swaps.ts index f4e993a10..a6ad08249 100644 --- a/lib/swaps/Swaps.ts +++ b/lib/swaps/Swaps.ts @@ -127,9 +127,86 @@ class Swaps extends EventEmitter { // Load Swaps from database const result = await this.repository.getSwapDeals(); + const recoverPromises: Promise[] = []; result.forEach((deal: SwapDealInstance) => { this.usedHashes.add(deal.rHash); + + if (deal.state === SwapState.Active) { + recoverPromises.push(this.recoverDeal(deal)); + } }); + await Promise.all(recoverPromises); + } + + private 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 `); + return; + } + if (!takerSwapClient || !takerSwapClient.isConnected()) { + this.logger.warn(`could not recover deal ${deal.rHash} because ${deal.takerCurrency} swap client is offline `); + 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 + try { + await makerSwapClient.removeInvoice(deal.rHash); + } catch (err) {} + deal.state = SwapState.Error; + deal.failureReason = SwapFailureReason.Crash; + break; + case SwapPhase.SendingAmount: + // 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 preimage = await takerSwapClient.lookupPayment(deal.rHash); + if (preimage) { + try { + deal.rPreimage = preimage; + await makerSwapClient.settleInvoice(deal.rHash, preimage); + deal.state = SwapState.Recovered; + this.logger.info(`recovered ${deal.makerCurrency} swap payment of ${deal.makerAmount} using preimage ${deal.rPreimage}`); + // 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 ${preimage}, this should be investigated manually`, err); + deal.state = SwapState.Error; + deal.failureReason = SwapFailureReason.Crash; + } + } else { + // we didn't complete the payment, so cancel the open invoice if we have one + try { + await makerSwapClient.removeInvoice(deal.rHash); + } catch (err) {} + deal.state = SwapState.Error; + deal.failureReason = SwapFailureReason.Crash; + } + } else if (deal.role === SwapRole.Taker) { + // we are not at risk of losing funds, but we should cancel any open invoices + try { + await takerSwapClient.removeInvoice(deal.rHash); + } catch (err) {} + deal.state = SwapState.Error; + deal.failureReason = SwapFailureReason.Crash; + } + break; + case SwapPhase.AmountReceived: + // we've claimed our payment + // TODO: send a swap completed packet? it may be too late to do so + deal.state = SwapState.Recovered; + break; + default: + break; + } + + await deal.save(); } private bind() {