Skip to content

Commit 856f14a

Browse files
committed
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 known 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. Any pending swaps are listed in the `GetInfo` response. 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. This commit includes a suite of test cases for the newly added functionality. Closes #1079.
1 parent b7861b8 commit 856f14a

File tree

17 files changed

+600
-45
lines changed

17 files changed

+600
-45
lines changed

docs/api.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/Xud.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,9 @@ class Xud extends EventEmitter {
222222
}
223223
if (this.grpcAPIProxy) {
224224
closePromises.push(this.grpcAPIProxy.close());
225-
await this.grpcAPIProxy.close();
225+
}
226+
if (this.swaps) {
227+
this.swaps.close();
226228
}
227229
await Promise.all(closePromises);
228230

lib/cli/commands/getinfo.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,6 @@ import Table, { VerticalTable } from 'cli-table3';
44
import colors from 'colors/safe';
55
import { GetInfoRequest, GetInfoResponse, LndInfo, RaidenInfo } from '../../proto/xudrpc_pb';
66

7-
type generalInfo = {
8-
version: string;
9-
numPeers: number;
10-
numPairs: number;
11-
nodePubKey: string;
12-
orders: {own: number, peer: number} | undefined
13-
};
14-
157
const displayChannels = (channels: any, asset: string) => {
168
const table = new Table() as VerticalTable;
179
Object.keys(channels).forEach((key: any) => {
@@ -74,7 +66,7 @@ const displayLndInfo = (asset: string, info: LndInfo.AsObject) => {
7466
}
7567
};
7668

77-
const displayGeneral = (info: generalInfo) => {
69+
const displayGeneral = (info: GetInfoResponse.AsObject) => {
7870
const table = new Table() as VerticalTable;
7971
table.push(
8072
{ [colors.blue('Version')]: info.version },
@@ -88,6 +80,11 @@ const displayGeneral = (info: generalInfo) => {
8880
{ [colors.blue('Peer orders')]: info.orders.peer },
8981
);
9082
}
83+
if (info.pendingSwapHashesList) {
84+
table.push(
85+
{ [colors.blue('Pending swaps')]: JSON.stringify(info.pendingSwapHashesList) },
86+
);
87+
}
9188
console.log(colors.underline(colors.bold('\nGeneral XUD Info')));
9289
console.log(table.toString(), '\n');
9390
};
@@ -105,13 +102,7 @@ const displayRaiden = (info: RaidenInfo.AsObject) => {
105102
};
106103

107104
const displayGetInfo = (response: GetInfoResponse.AsObject) => {
108-
displayGeneral({
109-
nodePubKey: response.nodePubKey,
110-
numPairs: response.numPairs,
111-
numPeers: response.numPeers,
112-
version: response.version,
113-
orders: response.orders,
114-
});
105+
displayGeneral(response);
115106
if (response.raiden) {
116107
displayRaiden(response.raiden);
117108
}

lib/constants/enums.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ export enum SwapState {
8181
Active = 0,
8282
Error = 1,
8383
Completed = 2,
84+
/**
85+
* A swap that was executed but wasn't formally completed. This may occur as a result of xud
86+
* crashing late in the swap process, after htlcs for both legs of the swap are set up but
87+
* before the swap is formally complete.
88+
*/
89+
Recovered = 3,
8490
}
8591

8692
export enum ReputationEvent {
@@ -126,7 +132,9 @@ export enum SwapFailureReason {
126132
/** The swap failed due to an unrecognized error. */
127133
UnknownError = 12,
128134
/** The swap failed due to an error or unexpected behavior on behalf of the remote peer. */
129-
RemoteError = 12,
135+
RemoteError = 13,
136+
/** The swap failed because of a system or xud crash while the swap was being executed. */
137+
Crash = 14,
130138
}
131139

132140
export enum DisconnectionReason {

lib/lndclient/LndClient.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import grpc, { ChannelCredentials, ClientReadableStream } from 'grpc';
22
import Logger from '../Logger';
3-
import SwapClient, { ClientStatus, SwapClientInfo } from '../swaps/SwapClient';
3+
import SwapClient, { ClientStatus, SwapClientInfo, PaymentState } from '../swaps/SwapClient';
44
import errors from './errors';
55
import { errors as swapErrors } from '../swaps/errors';
66
import { LightningClient, WalletUnlockerClient } from '../proto/lndrpc_grpc_pb';
@@ -703,11 +703,12 @@ class LndClient extends SwapClient {
703703
}
704704

705705
public settleInvoice = async (rHash: string, rPreimage: string) => {
706+
const settleInvoiceRequest = new lndinvoices.SettleInvoiceMsg();
707+
settleInvoiceRequest.setPreimage(hexToUint8Array(rPreimage));
708+
await this.settleInvoiceLnd(settleInvoiceRequest);
709+
706710
const invoiceSubscription = this.invoiceSubscriptions.get(rHash);
707711
if (invoiceSubscription) {
708-
const settleInvoiceRequest = new lndinvoices.SettleInvoiceMsg();
709-
settleInvoiceRequest.setPreimage(hexToUint8Array(rPreimage));
710-
await this.settleInvoiceLnd(settleInvoiceRequest);
711712
this.logger.debug(`settled invoice for ${rHash}`);
712713
invoiceSubscription.cancel();
713714
}
@@ -718,12 +719,54 @@ class LndClient extends SwapClient {
718719
if (invoiceSubscription) {
719720
const cancelInvoiceRequest = new lndinvoices.CancelInvoiceMsg();
720721
cancelInvoiceRequest.setPaymentHash(hexToUint8Array(rHash));
721-
await this.cancelInvoice(cancelInvoiceRequest);
722-
this.logger.debug(`canceled invoice for ${rHash}`);
722+
try {
723+
await this.cancelInvoice(cancelInvoiceRequest);
724+
this.logger.debug(`canceled invoice for ${rHash}`);
725+
} catch (err) {
726+
// handle errors due to attempting to remove an invoice that doesn't exist
727+
if (err.message === 'unable to locate invoice') {
728+
this.logger.debug(`attempted to cancel non-existent invoice for ${rHash}`);
729+
} else if (err.message === 'invoice already canceled') {
730+
this.logger.debug(`attempted to cancel already canceled invoice for ${rHash}`);
731+
} else {
732+
throw err;
733+
}
734+
}
723735
invoiceSubscription.cancel();
724736
}
725737
}
726738

739+
public lookupPayment = async (rHash: string) => {
740+
const payments = await this.listPayments(true);
741+
for (const payment of payments.getPaymentsList()) {
742+
if (payment.getPaymentHash() === rHash) {
743+
switch (payment.getStatus()) {
744+
case lndrpc.Payment.PaymentStatus.SUCCEEDED:
745+
const preimage = payment.getPaymentPreimage();
746+
return { preimage, state: PaymentState.Succeeded };
747+
case lndrpc.Payment.PaymentStatus.IN_FLIGHT:
748+
return { state: PaymentState.Pending };
749+
default:
750+
this.logger.warn(`unexpected payment state for payment with hash ${rHash}`);
751+
/* falls through */
752+
case lndrpc.Payment.PaymentStatus.FAILED:
753+
return { state: PaymentState.Failed };
754+
}
755+
}
756+
}
757+
758+
// if no payment is found, we assume that the payment was never attempted by lnd
759+
return { state: PaymentState.Failed };
760+
}
761+
762+
private listPayments = (includeIncomplete?: boolean): Promise<lndrpc.ListPaymentsResponse> => {
763+
const request = new lndrpc.ListPaymentsRequest();
764+
if (includeIncomplete) {
765+
request.setIncludeIncomplete(includeIncomplete);
766+
}
767+
return this.unaryCall<lndrpc.ListPaymentsRequest, lndrpc.ListPaymentsResponse>('listPayments', request);
768+
}
769+
727770
private addHoldInvoice = (request: lndinvoices.AddHoldInvoiceRequest): Promise<lndinvoices.AddHoldInvoiceResp> => {
728771
return this.unaryInvoiceCall<lndinvoices.AddHoldInvoiceRequest, lndinvoices.AddHoldInvoiceResp>('addHoldInvoice', request);
729772
}

lib/proto/xudrpc.swagger.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/proto/xudrpc_pb.d.ts

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/proto/xudrpc_pb.js

Lines changed: 43 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)