Skip to content

Commit

Permalink
feat(swaps): timeout stalled swaps
Browse files Browse the repository at this point in the history
This adds timeouts to all swap deals to fail them after a certain
amount of time. Current time limits are set to 10 seconds for a swap to
be accepted and 30 seconds for a swap to be completed once accepted.
Previously, swaps that stalled out due to an unresponsive peer or swap
client would last indefinitely and the corresponding order would remain
on hold.

Closes #653.
  • Loading branch information
sangaman committed Jan 16, 2019
1 parent c86a113 commit a463056
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 4 deletions.
39 changes: 36 additions & 3 deletions lib/swaps/Swaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ interface Swaps {
class Swaps extends EventEmitter {
/** A map between payment hashes and swap deals. */
private deals = new Map<string, SwapDeal>();
/** A map between payment hashes and timeouts for swaps. */
private timeouts = new Map<string, number>();
private usedHashes = new Set<string>();
private repository: SwapRepository;
/** The number of satoshis in a bitcoin. */
private static readonly SATOSHIS_PER_COIN = 100000000;
/** The maximum time in milliseconds we will wait for a swap to be accepted before failing it. */
private static readonly SWAP_ACCEPT_TIMEOUT = 10000;
/** The maximum time in milliseconds we will wait for a swap to be completed before failing it. */
private static readonly SWAP_COMPLETE_TIMEOUT = 30000;

constructor(private logger: Logger, private models: Models, private pool: Pool, private lndBtcClient: LndClient, private lndLtcClient: LndClient) {
super();
Expand Down Expand Up @@ -293,9 +299,11 @@ class Swaps extends EventEmitter {
}
const preimage = await randomBytes(32);

const rHash = createHash('sha256').update(preimage).digest('hex');

const swapRequestBody: packets.SwapRequestPacketBody = {
takerCltvDelta,
rHash: createHash('sha256').update(preimage).digest('hex'),
rHash,
orderId: maker.id,
pairId: maker.pairId,
proposedQuantity: taker.quantity,
Expand All @@ -318,6 +326,8 @@ class Swaps extends EventEmitter {
createTime: Date.now(),
};

this.timeouts.set(rHash, setTimeout(this.handleSwapTimeout, Swaps.SWAP_ACCEPT_TIMEOUT, rHash));

this.addDeal(deal);

// Verify LND setup. Make sure we are connected to BTC and LTC and that
Expand All @@ -344,7 +354,8 @@ class Swaps extends EventEmitter {
// TODO: consider the time gap between taking the routes and using them.
// TODO: multi route support (currently only 1)

if (this.usedHashes.has(requestPacket.body!.rHash)) {
const rHash = requestPacket.body!.rHash;
if (this.usedHashes.has(rHash)) {
this.sendErrorToPeer(peer, requestPacket.body!.rHash, SwapFailureReason.PaymentHashReuse, undefined, requestPacket.header.id);
return false;
}
Expand Down Expand Up @@ -376,6 +387,8 @@ class Swaps extends EventEmitter {
createTime: Date.now(),
};

this.timeouts.set(rHash, setTimeout(this.handleSwapTimeout, Swaps.SWAP_COMPLETE_TIMEOUT, rHash));

// add the deal. Going forward we can "record" errors related to this deal.
this.addDeal(deal);

Expand Down Expand Up @@ -454,10 +467,19 @@ class Swaps extends EventEmitter {
const { quantity, rHash, makerCltvDelta } = responsePacket.body!;
const deal = this.getDeal(rHash);
if (!deal) {
this.logger.error(`received swap accepted for unrecognized deal payment hash ${rHash}`);
this.logger.warn(`received swap accepted for unrecognized deal: ${rHash}`);
// TODO: penalize peer
return;
}
if (deal.phase !== SwapPhase.SwapRequested) {
this.logger.warn(`received swap accepted for deal that is not in SwapRequested phase: ${rHash}`);
// TODO: penalize peer
return;
}

clearTimeout(this.timeouts.get(rHash));
this.timeouts.set(rHash, setTimeout(this.handleSwapTimeout, Swaps.SWAP_COMPLETE_TIMEOUT, rHash));

// update deal with taker's makerCltvDelta
deal.makerCltvDelta = makerCltvDelta;

Expand Down Expand Up @@ -636,6 +658,12 @@ class Swaps extends EventEmitter {

}

private handleSwapTimeout = (rHash: string) => {
const deal = this.getDeal(rHash)!;
this.timeouts.delete(rHash);
this.failDeal(deal, SwapFailureReason.Timeout);
}

private failDeal = (deal: SwapDeal, failureReason: SwapFailureReason, errorMessage?: string): void => {
// If we are already in error state and got another error report we
// aggregate all error reasons by concatenation
Expand All @@ -650,6 +678,8 @@ class Swaps extends EventEmitter {
deal.state = SwapState.Error;
deal.failureReason = failureReason;
deal.errorMessage = errorMessage;
clearTimeout(this.timeouts.get(deal.rHash));
this.timeouts.delete(deal.rHash);
this.emit('swap.failed', deal);
}

Expand Down Expand Up @@ -708,6 +738,9 @@ class Swaps extends EventEmitter {
role: deal.role,
};
this.emit('swap.paid', swapResult);

clearTimeout(this.timeouts.get(deal.rHash));
this.timeouts.delete(deal.rHash);
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/swaps/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export type SwapDeal = {
makerToTakerRoutes?: Route[];
createTime: number;
executeTime?: number;
completeTime?: number
completeTime?: number;
};

/** The result of a successful swap. */
Expand Down
1 change: 1 addition & 0 deletions lib/types/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export enum SwapFailureReason {
InvalidResolveRequest = 8,
/** The swap request attempts to reuse a payment hash. */
PaymentHashReuse = 9,
Timeout = 10,
}

export enum DisconnectionReason {
Expand Down

0 comments on commit a463056

Please sign in to comment.