Skip to content

Commit

Permalink
feat(swaps): recover crashed swap deals
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sangaman committed Jul 5, 2019
1 parent 7430641 commit b39c4c3
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 0 deletions.
8 changes: 8 additions & 0 deletions lib/constants/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions lib/lndclient/LndClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<lndrpc.ListPaymentsResponse> => {
return this.unaryInvoiceCall<lndrpc.ListPaymentsRequest, lndrpc.ListPaymentsResponse>('listPayments', new lndrpc.ListPaymentsRequest());
}

private addHoldInvoice = (request: lndinvoices.AddHoldInvoiceRequest): Promise<lndinvoices.AddHoldInvoiceResp> => {
return this.unaryInvoiceCall<lndinvoices.AddHoldInvoiceRequest, lndinvoices.AddHoldInvoiceResp>('addHoldInvoice', request);
}
Expand Down
6 changes: 6 additions & 0 deletions lib/raidenclient/RaidenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ class RaidenClient extends SwapClient {
// not implemented, raiden does not use invoices
}

public lookupPayment = async (rHash: string) => {
// raiden does not currently expose an API to retrieve a preimage for a completed payment
this.logger.warn(`raiden API can not provide the preimage for payment ${rHash}, manual intervention may be required`).
return undefined;
}

public getRoutes = async (_amount: number, _destination: string) => {
// stub placeholder, query routes not currently implemented in raiden
return [{
Expand Down
6 changes: 6 additions & 0 deletions lib/swaps/SwapClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ abstract class SwapClient extends EventEmitter {

public abstract async removeInvoice(rHash: string): Promise<void>;

/**
* 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<string | undefined>;

/**
* Gets the block height of the chain backing this swap client.
*/
Expand Down
77 changes: 77 additions & 0 deletions lib/swaps/Swaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,86 @@ class Swaps extends EventEmitter {

// Load Swaps from database
const result = await this.repository.getSwapDeals();
const recoverPromises: Promise<void>[] = [];
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() {
Expand Down

0 comments on commit b39c4c3

Please sign in to comment.