Skip to content

Commit d9d6bb5

Browse files
committed
feat(swaps): new PreimageResolved swap phase
This creates a new phase for SwapDeals called `PreimageResolved` that represents the part after the maker has completed its payment to taker and resolved the preimage, but before it has settled its incoming payment using the preimage. Swaps that have reached this phase are considered atomic - our outgoing payment has been settle and we've acquired the ability to settle our incoming payment - even if they're not complete. Therefore, the maker ends the swap timeout and gives the call to settle the incoming payment as long as it needs to complete while keeping the hold on the order to prevent it from being filled a second time. Should a swap fail the `settleInvoice` call, it enters swap SwapRecovery where settling the payment will be attempted one more time but only after ensuring we are connected to the SwapClient responsible for that payment. Previously, deals that hit an error upon the `settleInvoice` would not fail the deal and send it to recovery until the timeout was reached. We persist the preimage to the database immediately upon reaching the `PreimageResolved` phase and then use that preimage in SwapRecovery if it is available, rather than querying the swap client for the outgoing payment to lookup the preimage again. We modify the timeout behavior for the maker in the swap. The maker will no longer "fail" a deal once it has begun sending its payment to the taker due to a swap timing out. Instead it will only notify the taker that its time limit has been reached, and a cooperative taker will cancel its invoice and call off the swap. The maker also tracks the elapsed time for a swap, and if its payment to taker succeeds after the time out period it penalizes the taker for accepting payment and settling its invoice too late. Takers that delay before they settle payment can abuse the free options problem. On the swap recovery side, we no longer fail a recovered deal if a settle invoice call fails. Instead, we continuosly attempt settle invoice calls until one succeeds. Longer term we can permanently stop settle invoice attempts if we can recognize the sorts of errors that indicate permanent failure such as htlc expiration or an unrecognized invoice hash. Resolves #1654. Resolves #1659.
1 parent f5423a8 commit d9d6bb5

File tree

12 files changed

+469
-259
lines changed

12 files changed

+469
-259
lines changed

Diff for: lib/constants/enums.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,26 @@ export enum SwapRole {
6464
}
6565

6666
export enum SwapPhase {
67-
/** The swap deal has been created locally. */
67+
/** 0/5 The swap deal has been created locally. */
6868
SwapCreated = 0,
69-
/** We've made a request to a peer to accept this swap. */
69+
/** 1/5 We've made a request to a peer to accept this swap. */
7070
SwapRequested = 1,
71-
/** The terms of the swap have been agreed to, and we will attempt to execute it. */
71+
/** 2/5 The terms of the swap have been agreed to, and we will attempt to execute it. */
7272
SwapAccepted = 2,
7373
/**
74-
* We have made a request to the swap client to send payment according to the agreed terms.
74+
* 3/5 We have made a request to the swap client to send payment according to the agreed terms.
7575
* The payment (and swap) could still fail due to no route with sufficient capacity, lack of
7676
* cooperation from the receiver or any intermediary node along the route, or an unexpected
7777
* error from the swap client.
7878
*/
7979
SendingPayment = 3,
8080
/**
81-
* We have received the agreed amount of the swap and released the preimage to the
81+
* 4/5 We have completed our outgoing payment and retrieved the preimage which can be used to
82+
* settle the incoming payment locked by the same hash.
83+
*/
84+
PreimageResolved = 5,
85+
/**
86+
* 5/5 We have received the agreed amount of the swap and released the preimage to the
8287
* receiving swap client so it can accept payment.
8388
*/
8489
PaymentReceived = 4,

Diff for: lib/p2p/Pool.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ class Pool extends EventEmitter {
292292

293293
private bindNodeList = () => {
294294
this.nodes.on('node.ban', (nodePubKey: string, events: ReputationEventInstance[]) => {
295-
this.logger.warn(`node ${nodePubKey} was banned`);
295+
this.logger.info(`node ${nodePubKey} was banned`);
296296

297297
const peer = this.peers.get(nodePubKey);
298298
if (peer) {

Diff for: lib/swaps/SwapRecovery.ts

+43-25
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { SwapClientType, SwapFailureReason, SwapPhase, SwapRole, SwapState } from '../constants/enums';
1+
import assert from 'assert';
2+
import { SwapFailureReason, SwapPhase, SwapRole, SwapState } from '../constants/enums';
23
import { SwapDealInstance } from '../db/types';
34
import Logger from '../Logger';
45
import SwapClient, { PaymentState } from './SwapClient';
@@ -9,8 +10,6 @@ import SwapClientManager from './SwapClientManager';
910
* ensuring that we do not lose funds on a partially completed swap.
1011
*/
1112
class SwapRecovery {
12-
/** A map of payment hashes to swaps where we have recovered the preimage but not used it to claim payment yet. */
13-
public recoveredPreimageSwaps: Map<string, SwapDealInstance> = new Map();
1413
/** A map of payment hashes to swaps where we have a pending outgoing payment but don't know the preimage. */
1514
private pendingSwaps: Map<string, SwapDealInstance> = new Map();
1615
private pendingSwapsTimer?: NodeJS.Timeout;
@@ -59,16 +58,51 @@ class SwapRecovery {
5958
await deal.save();
6059
}
6160

61+
/**
62+
* Claims the incoming payment for a deal where the outgoing payment has
63+
* already gone through and where we already know the preimage.
64+
*/
65+
private claimPayment = async (deal: SwapDealInstance) => {
66+
assert(deal.rPreimage);
67+
68+
// the maker payment is always the one that is claimed second, after the payment to taker
69+
const makerSwapClient = this.swapClientManager.get(deal.makerCurrency);
70+
if (!makerSwapClient || !makerSwapClient.isConnected()) {
71+
this.logger.warn(`could not claim payment for ${deal.rHash} because ${deal.makerCurrency} swap client is offline`);
72+
return;
73+
}
74+
75+
try {
76+
await makerSwapClient.settleInvoice(deal.rHash, deal.rPreimage, deal.makerCurrency);
77+
deal.state = SwapState.Recovered;
78+
this.logger.info(`recovered ${deal.makerCurrency} swap payment of ${deal.makerAmount} using preimage ${deal.rPreimage}`);
79+
this.pendingSwaps.delete(deal.rHash);
80+
await deal.save();
81+
} catch (err) {
82+
this.logger.error(`could not settle ${deal.makerCurrency} invoice for payment ${deal.rHash}`, err);
83+
// TODO: determine when we are permanently unable (due to htlc expiration or unknown invoice hash) to
84+
// settle an invoice and fail the deal, rather than endlessly retrying settle invoice calls
85+
}
86+
// TODO: update order and trade in database to indicate they were executed
87+
}
88+
6289
/**
6390
* Checks the status of the outgoing payment for a swap where we have begun
6491
* sending a payment and handles the resolution of the swap once a final
6592
* status for the payment is determined.
6693
*/
6794
private checkPaymentStatus = async (deal: SwapDealInstance) => {
68-
this.logger.debug(`checking outgoing payment status for swap ${deal.rHash}`);
6995
// ensure that we are tracking this pending swap
7096
this.pendingSwaps.set(deal.rHash, deal);
7197

98+
if (deal.rPreimage) {
99+
// if we already have the preimage for this deal, we can attempt to claim our payment right away
100+
await this.claimPayment(deal);
101+
return;
102+
}
103+
104+
this.logger.debug(`checking outgoing payment status for swap ${deal.rHash}`);
105+
72106
const takerSwapClient = this.swapClientManager.get(deal.takerCurrency);
73107
if (!takerSwapClient || !takerSwapClient.isConnected()) {
74108
this.logger.warn(`could not recover deal ${deal.rHash} because ${deal.takerCurrency} swap client is offline`);
@@ -81,32 +115,15 @@ class SwapRecovery {
81115
const makerSwapClient = this.swapClientManager.get(deal.makerCurrency);
82116
if (!makerSwapClient || !makerSwapClient.isConnected()) {
83117
this.logger.warn(`could not recover deal ${deal.rHash} because ${deal.makerCurrency} swap client is offline`);
84-
this.pendingSwaps.set(deal.rHash, deal);
85118
return;
86119
}
87120

88121
const paymentStatus = await takerSwapClient.lookupPayment(deal.rHash, deal.takerCurrency);
89122
if (paymentStatus.state === PaymentState.Succeeded) {
90-
try {
91-
deal.rPreimage = paymentStatus.preimage!;
92-
if (makerSwapClient.type === SwapClientType.Raiden) {
93-
this.logger.info(`recovered preimage ${deal.rPreimage} for swap ${deal.rHash}, ` +
94-
'waiting for raiden to request secret and claim payment.');
95-
this.recoveredPreimageSwaps.set(deal.rHash, deal);
96-
} else {
97-
await makerSwapClient.settleInvoice(deal.rHash, deal.rPreimage, deal.makerCurrency);
98-
deal.state = SwapState.Recovered;
99-
this.logger.info(`recovered ${deal.makerCurrency} swap payment of ${deal.makerAmount} using preimage ${deal.rPreimage}`);
100-
}
101-
102-
this.pendingSwaps.delete(deal.rHash);
103-
await deal.save();
104-
// TODO: update order and trade in database to indicate they were executed
105-
} catch (err) {
106-
// tslint:disable-next-line: max-line-length
107-
this.logger.error(`could not settle ${deal.makerCurrency} invoice for payment ${deal.rHash} and preimage ${deal.rPreimage}, **this must be resolved manually**`, err);
108-
await this.failDeal(deal);
109-
}
123+
deal.rPreimage = paymentStatus.preimage!;
124+
await deal.save(); // persist the preimage to the database once we retrieve it
125+
126+
await this.claimPayment(deal);
110127
} else if (paymentStatus.state === PaymentState.Failed) {
111128
// the payment failed, so cancel the open invoice if we have one
112129
await this.failDeal(deal, makerSwapClient);
@@ -138,6 +155,7 @@ class SwapRecovery {
138155
await this.failDeal(deal, makerSwapClient);
139156
break;
140157
case SwapPhase.SendingPayment:
158+
case SwapPhase.PreimageResolved:
141159
// we started sending payment but didn't claim our payment
142160
await this.checkPaymentStatus(deal);
143161
break;

0 commit comments

Comments
 (0)