Skip to content

Commit

Permalink
feat(swaps): new PreimageResolved swap phase
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sangaman committed Jun 30, 2020
1 parent f5423a8 commit d9d6bb5
Show file tree
Hide file tree
Showing 12 changed files with 469 additions and 259 deletions.
15 changes: 10 additions & 5 deletions lib/constants/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,26 @@ export enum SwapRole {
}

export enum SwapPhase {
/** The swap deal has been created locally. */
/** 0/5 The swap deal has been created locally. */
SwapCreated = 0,
/** We've made a request to a peer to accept this swap. */
/** 1/5 We've made a request to a peer to accept this swap. */
SwapRequested = 1,
/** The terms of the swap have been agreed to, and we will attempt to execute it. */
/** 2/5 The terms of the swap have been agreed to, and we will attempt to execute it. */
SwapAccepted = 2,
/**
* We have made a request to the swap client to send payment according to the agreed terms.
* 3/5 We have made a request to the swap client to send payment according to the agreed terms.
* The payment (and swap) could still fail due to no route with sufficient capacity, lack of
* cooperation from the receiver or any intermediary node along the route, or an unexpected
* error from the swap client.
*/
SendingPayment = 3,
/**
* We have received the agreed amount of the swap and released the preimage to the
* 4/5 We have completed our outgoing payment and retrieved the preimage which can be used to
* settle the incoming payment locked by the same hash.
*/
PreimageResolved = 5,
/**
* 5/5 We have received the agreed amount of the swap and released the preimage to the
* receiving swap client so it can accept payment.
*/
PaymentReceived = 4,
Expand Down
2 changes: 1 addition & 1 deletion lib/p2p/Pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ class Pool extends EventEmitter {

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

const peer = this.peers.get(nodePubKey);
if (peer) {
Expand Down
68 changes: 43 additions & 25 deletions lib/swaps/SwapRecovery.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SwapClientType, SwapFailureReason, SwapPhase, SwapRole, SwapState } from '../constants/enums';
import assert from 'assert';
import { SwapFailureReason, SwapPhase, SwapRole, SwapState } from '../constants/enums';
import { SwapDealInstance } from '../db/types';
import Logger from '../Logger';
import SwapClient, { PaymentState } from './SwapClient';
Expand All @@ -9,8 +10,6 @@ import SwapClientManager from './SwapClientManager';
* ensuring that we do not lose funds on a partially completed swap.
*/
class SwapRecovery {
/** A map of payment hashes to swaps where we have recovered the preimage but not used it to claim payment yet. */
public recoveredPreimageSwaps: Map<string, SwapDealInstance> = new Map();
/** A map of payment hashes to swaps where we have a pending outgoing payment but don't know the preimage. */
private pendingSwaps: Map<string, SwapDealInstance> = new Map();
private pendingSwapsTimer?: NodeJS.Timeout;
Expand Down Expand Up @@ -59,16 +58,51 @@ class SwapRecovery {
await deal.save();
}

/**
* Claims the incoming payment for a deal where the outgoing payment has
* already gone through and where we already know the preimage.
*/
private claimPayment = async (deal: SwapDealInstance) => {
assert(deal.rPreimage);

// the maker payment is always the one that is claimed second, after the payment to taker
const makerSwapClient = this.swapClientManager.get(deal.makerCurrency);
if (!makerSwapClient || !makerSwapClient.isConnected()) {
this.logger.warn(`could not claim payment for ${deal.rHash} because ${deal.makerCurrency} swap client is offline`);
return;
}

try {
await makerSwapClient.settleInvoice(deal.rHash, deal.rPreimage, deal.makerCurrency);
deal.state = SwapState.Recovered;
this.logger.info(`recovered ${deal.makerCurrency} swap payment of ${deal.makerAmount} using preimage ${deal.rPreimage}`);
this.pendingSwaps.delete(deal.rHash);
await deal.save();
} catch (err) {
this.logger.error(`could not settle ${deal.makerCurrency} invoice for payment ${deal.rHash}`, err);
// TODO: determine when we are permanently unable (due to htlc expiration or unknown invoice hash) to
// settle an invoice and fail the deal, rather than endlessly retrying settle invoice calls
}
// TODO: update order and trade in database to indicate they were executed
}

/**
* Checks the status of the outgoing payment for a swap where we have begun
* sending a payment and handles the resolution of the swap once a final
* status for the payment is determined.
*/
private checkPaymentStatus = async (deal: SwapDealInstance) => {
this.logger.debug(`checking outgoing payment status for swap ${deal.rHash}`);
// ensure that we are tracking this pending swap
this.pendingSwaps.set(deal.rHash, deal);

if (deal.rPreimage) {
// if we already have the preimage for this deal, we can attempt to claim our payment right away
await this.claimPayment(deal);
return;
}

this.logger.debug(`checking outgoing payment status for swap ${deal.rHash}`);

const takerSwapClient = this.swapClientManager.get(deal.takerCurrency);
if (!takerSwapClient || !takerSwapClient.isConnected()) {
this.logger.warn(`could not recover deal ${deal.rHash} because ${deal.takerCurrency} swap client is offline`);
Expand All @@ -81,32 +115,15 @@ class SwapRecovery {
const makerSwapClient = this.swapClientManager.get(deal.makerCurrency);
if (!makerSwapClient || !makerSwapClient.isConnected()) {
this.logger.warn(`could not recover deal ${deal.rHash} because ${deal.makerCurrency} swap client is offline`);
this.pendingSwaps.set(deal.rHash, deal);
return;
}

const paymentStatus = await takerSwapClient.lookupPayment(deal.rHash, deal.takerCurrency);
if (paymentStatus.state === PaymentState.Succeeded) {
try {
deal.rPreimage = paymentStatus.preimage!;
if (makerSwapClient.type === SwapClientType.Raiden) {
this.logger.info(`recovered preimage ${deal.rPreimage} for swap ${deal.rHash}, ` +
'waiting for raiden to request secret and claim payment.');
this.recoveredPreimageSwaps.set(deal.rHash, deal);
} else {
await makerSwapClient.settleInvoice(deal.rHash, deal.rPreimage, deal.makerCurrency);
deal.state = SwapState.Recovered;
this.logger.info(`recovered ${deal.makerCurrency} swap payment of ${deal.makerAmount} using preimage ${deal.rPreimage}`);
}

this.pendingSwaps.delete(deal.rHash);
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 must be resolved manually**`, err);
await this.failDeal(deal);
}
deal.rPreimage = paymentStatus.preimage!;
await deal.save(); // persist the preimage to the database once we retrieve it

await this.claimPayment(deal);
} else if (paymentStatus.state === PaymentState.Failed) {
// the payment failed, so cancel the open invoice if we have one
await this.failDeal(deal, makerSwapClient);
Expand Down Expand Up @@ -138,6 +155,7 @@ class SwapRecovery {
await this.failDeal(deal, makerSwapClient);
break;
case SwapPhase.SendingPayment:
case SwapPhase.PreimageResolved:
// we started sending payment but didn't claim our payment
await this.checkPaymentStatus(deal);
break;
Expand Down
Loading

0 comments on commit d9d6bb5

Please sign in to comment.