Skip to content

Commit

Permalink
feat(swaps): dynamic lock buffer
Browse files Browse the repository at this point in the history
This modifies the logic around calculating the `makerCltvDelta` value
for swaps which specifies the minimum expected lock duration for the
final hop of the first leg. We use the poisson quantile function to
determine a very high probability that the first leg final lock won't
expire before the second leg payment.

This also removes the recently added `lockBuffer` config option in
favor of a `cltvDelta` for lnd that specifies the lock delta that is
used for the final hop of the second leg and gets added to the lock
buffer to determine the final lock delta for the first leg.

Closes #1164.
  • Loading branch information
sangaman committed Sep 14, 2019
1 parent abf0eaa commit cf83ab0
Show file tree
Hide file tree
Showing 17 changed files with 1,021 additions and 85 deletions.
9 changes: 5 additions & 4 deletions bin/xud
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ const { argv } = require('yargs')
type: 'boolean',
default: undefined,
},
lockbuffer: {
describe: 'Lock time in hours to add to cross-chain hop for swaps',
type: 'number',
},
loglevel: {
describe: 'Verbosity of the logger',
type: 'string',
Expand Down Expand Up @@ -93,6 +89,10 @@ const { argv } = require('yargs')
describe: 'Path to the SSL certificate for lnd',
type: 'string',
},
'lnd.[currency].cltvdelta': {
describe: 'CLTV delta for the final timelock',
type: 'number',
},
'lnd.[currency].disable': {
describe: 'Disable lnd integration',
type: 'boolean',
Expand Down Expand Up @@ -191,6 +191,7 @@ const { argv } = require('yargs')
currencies.forEach((currency) => {
parseBoolean(arg[currency], 'disable');
parseBoolean(arg[currency], 'nomacaroons');
parseNumber(arg[currency], 'cltvdelta');
parseNumber(arg[currency], 'port');
});

Expand Down
5 changes: 2 additions & 3 deletions lib/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ class Config {
public logpath: string;
public logdateformat: string;
public network: XuNetwork;
/** The lock time in hours that we add to the cross-chain "hop" for swaps. */
public lockbuffer: number;
public rpc: { disable: boolean, host: string, port: number };
public http: { host: string, port: number };
public lnd: { [currency: string]: LndClientConfig | undefined } = {};
Expand Down Expand Up @@ -77,7 +75,6 @@ class Config {
this.logdateformat = 'DD/MM/YYYY HH:mm:ss.SSS';
this.network = this.getDefaultNetwork();
this.dbpath = this.getDefaultDbPath();
this.lockbuffer = 24;

this.p2p = {
listen: true,
Expand Down Expand Up @@ -114,6 +111,7 @@ class Config {
host: 'localhost',
port: 10009,
nomacaroons: false,
cltvdelta: 40,
};
this.lnd.LTC = {
disable: false,
Expand All @@ -123,6 +121,7 @@ class Config {
host: 'localhost',
port: 10010,
nomacaroons: false,
cltvdelta: 576,
};
this.raiden = {
disable: false,
Expand Down
29 changes: 12 additions & 17 deletions lib/lndclient/LndClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const MAXFEE = 0.03;
/** A class representing a client to interact with lnd. */
class LndClient extends SwapClient {
public readonly type = SwapClientType.Lnd;
public readonly lockBuffer: number;
public readonly finalLock: number;
public config: LndClientConfig;
public currency: string;
Expand Down Expand Up @@ -59,15 +58,13 @@ class LndClient extends SwapClient {
* Creates an lnd client.
*/
constructor(
{ config, logger, currency, lockBufferHours }:
{ config: LndClientConfig, logger: Logger, currency: string, lockBufferHours: number },
{ config, logger, currency }:
{ config: LndClientConfig, logger: Logger, currency: string },
) {
super(logger);
this.config = config;
this.currency = currency;
this.lockBuffer = Math.round(lockBufferHours * 60 / LndClient.MINUTES_PER_BLOCK_BY_CURRENCY[currency]);
// we set the expected final lock to 400 minutes which is the default for bitcoin on lnd 0.7.1
this.finalLock = Math.round(400 / LndClient.MINUTES_PER_BLOCK_BY_CURRENCY[currency]);
this.finalLock = config.cltvdelta;
}

private static waitForClientReady = (client: grpc.Client) => {
Expand All @@ -91,8 +88,6 @@ class LndClient extends SwapClient {
* @param awaitingCreate whether xud is waiting for its node key to be created
*/
public init = async (awaitingCreate = false) => {
assert(this.lockBuffer > 0, `lnd-${this.currency}: lock buffer must be a positive number`);

const { disable, certpath, macaroonpath, nomacaroons, host, port } = this.config;
if (disable) {
await this.setStatus(ClientStatus.Disabled);
Expand Down Expand Up @@ -413,7 +408,7 @@ class LndClient extends SwapClient {
// In case of sanity swaps we don't know the
// takerCltvDelta or the makerCltvDelta. Using our
// client's default.
finalCltvDelta: this.lockBuffer,
finalCltvDelta: this.finalLock,
});
const preimage = await this.executeSendRequest(request);
return preimage;
Expand Down Expand Up @@ -622,10 +617,10 @@ class LndClient extends SwapClient {
return this.unaryCall<lndrpc.ListChannelsRequest, lndrpc.ListChannelsResponse>('listChannels', new lndrpc.ListChannelsRequest());
}

public getRoute = async (units: number, destination: string, _currency: string, finalCltvDelta = this.lockBuffer) => {
public getRoute = async (units: number, destination: string, _currency: string, finalLock = this.finalLock) => {
const request = new lndrpc.QueryRoutesRequest();
request.setAmt(units);
request.setFinalCltvDelta(finalCltvDelta);
request.setFinalCltvDelta(finalLock);
request.setPubKey(destination);
const fee = new lndrpc.FeeLimit();
fee.setFixed(Math.floor(MAXFEE * request.getAmt()));
Expand All @@ -641,15 +636,15 @@ class LndClient extends SwapClient {
!err.message.includes('unable to find a path to destination') &&
!err.message.includes('target not found')
)) {
this.logger.error(`error calling queryRoutes to ${destination}, amount ${units}, finalCltvDelta ${finalCltvDelta}`, err);
this.logger.error(`error calling queryRoutes to ${destination}, amount ${units}, finalCltvDelta ${finalLock}`, err);
throw err;
}
}

if (route) {
this.logger.debug(`found a route to ${destination} for ${units} units with finalCltvDelta ${finalCltvDelta}: ${route}`);
this.logger.debug(`found a route to ${destination} for ${units} units with finalCltvDelta ${finalLock}: ${route}`);
} else {
this.logger.debug(`could not find a route to ${destination} for ${units} units with finalCltvDelta ${finalCltvDelta}: ${route}`);
this.logger.debug(`could not find a route to ${destination} for ${units} units with finalCltvDelta ${finalLock}: ${route}`);
}
return route;
}
Expand Down Expand Up @@ -692,13 +687,13 @@ class LndClient extends SwapClient {
return unlockWalletResponse.toObject();
}

public addInvoice = async (rHash: string, units: number, cltvExpiry: number) => {
public addInvoice = async (rHash: string, units: number, expiry = this.finalLock) => {
const addHoldInvoiceRequest = new lndinvoices.AddHoldInvoiceRequest();
addHoldInvoiceRequest.setHash(hexToUint8Array(rHash));
addHoldInvoiceRequest.setValue(units);
addHoldInvoiceRequest.setCltvExpiry(cltvExpiry);
addHoldInvoiceRequest.setCltvExpiry(expiry);
await this.addHoldInvoice(addHoldInvoiceRequest);
this.logger.debug(`added invoice of ${units} for ${rHash} with cltvExpiry ${cltvExpiry}`);
this.logger.debug(`added invoice of ${units} for ${rHash} with cltvExpiry ${expiry}`);
this.subscribeSingleInvoice(rHash);
}

Expand Down
1 change: 1 addition & 0 deletions lib/lndclient/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type LndClientConfig = {
host: string;
port: number;
nomacaroons: boolean;
cltvdelta: number;
};

/** General information about the state of this lnd client. */
Expand Down
6 changes: 2 additions & 4 deletions lib/raidenclient/RaidenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ async function parseResponseBody<T>(res: http.IncomingMessage): Promise<T> {
*/
class RaidenClient extends SwapClient {
public readonly type = SwapClientType.Raiden;
public readonly lockBuffer: number;
public readonly finalLock = 100;
public address?: string;
/** A map of currency symbols to token addresses. */
Expand All @@ -61,13 +60,12 @@ class RaidenClient extends SwapClient {
* Creates a raiden client.
*/
constructor(
{ config, logger, unitConverter, directChannelChecks = false, lockBufferHours }:
{ config: RaidenClientConfig, logger: Logger, unitConverter: UnitConverter, directChannelChecks: boolean, lockBufferHours: number },
{ config, logger, unitConverter, directChannelChecks = false }:
{ config: RaidenClientConfig, logger: Logger, unitConverter: UnitConverter, directChannelChecks: boolean },
) {
super(logger);
const { disable, host, port } = config;

this.lockBuffer = Math.round(lockBufferHours * 60 / this.minutesPerBlock);
this.port = port;
this.host = host;
this.disable = disable;
Expand Down
12 changes: 2 additions & 10 deletions lib/swaps/SwapClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,7 @@ interface SwapClient {
*/
abstract class SwapClient extends EventEmitter {
/**
* The number of blocks to use for determining the minimum delay for an incoming payment in excess
* of the total time delay of the contingent outgoing payment. This buffer ensures that the lock
* for incoming payments does not expire before the contingent outgoing payment lock.
*/
public abstract readonly lockBuffer: number;
/**
* The number of blocks of lock time to expect on the final incoming hop of a swap. This affects
* only the second leg of a swap where knowledge of the preimage is not contingent on making a
* separate payment.
* The number of blocks of lock time to expect on the final hop of an incoming swap payment.
*/
public abstract readonly finalLock: number;
public abstract readonly type: SwapClientType;
Expand Down Expand Up @@ -161,7 +153,7 @@ abstract class SwapClient extends EventEmitter {
/**
* @param units the amount of the invoice denominated in the smallest units supported by its currency
*/
public abstract async addInvoice(rHash: string, units: number, cltvExpiry: number): Promise<void>;
public abstract async addInvoice(rHash: string, units: number, expiry?: number): Promise<void>;

public abstract async settleInvoice(rHash: string, rPreimage: string): Promise<void>;

Expand Down
2 changes: 0 additions & 2 deletions lib/swaps/SwapClientManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ class SwapClientManager extends EventEmitter {
this.raidenClient = new RaidenClient({
unitConverter,
config: config.raiden,
lockBufferHours: config.lockbuffer,
logger: loggers.raiden,
directChannelChecks: config.debug.raidenDirectChannelChecks,
});
Expand All @@ -71,7 +70,6 @@ class SwapClientManager extends EventEmitter {
const lndClient = new LndClient({
currency,
config: lndConfig,
lockBufferHours: this.config.lockbuffer,
logger: this.loggers.lnd.createSubLogger(currency),
});
this.swapClients.set(currency, lndClient);
Expand Down
51 changes: 34 additions & 17 deletions lib/swaps/Swaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { PacketType } from '../p2p/packets';
import SwapClientManager from './SwapClientManager';
import { errors, errorCodes } from './errors';
import SwapRecovery from './SwapRecovery';
import poissonQuantile from 'distributions-poisson-quantile';

export type OrderToAccept = Pick<SwapDeal, 'quantity' | 'price' | 'localId' | 'isBuy'> & {
quantity: number;
Expand Down Expand Up @@ -77,6 +78,26 @@ class Swaps extends EventEmitter {
return proposedQuantity > 0 && rHash.length === 64;
}

/**
* Calculates the minimum expected lock delta for the final hop of the first leg to ensure a
* very high probability that it won't expire before the second leg payment. We use a Poisson
* distribution to model the possible block times of two independent chains, first calculating
* a probabilistic upper bound for the lock time in minuntes of the second leg then a
* probabilistic lower bound for the number of blocks for the lock time extended to the final
* hop of the first leg.
* @param secondLegLockDuration The lock duration (aka time lock or cltv delta) of the second
* leg (maker to taker) denominated in blocks of that chain.
* @returns A number of blocks for the chain of the first leg that is highly likely to take
* more time in minutes than the provided second leg lock duration.
*/
private static calculateLockBuffer = (secondLegLockDuration: number, secondLegMinutesPerBlock: number, firstLegMinutesPerBlock: number) => {
/** A probabilistic upper bound for the time it will take for the second leg route time lock to expire. */
const secondLegLockMinutes = poissonQuantile(.9999, { lambda: secondLegLockDuration }) * secondLegMinutesPerBlock;
const firstLegLockBuffer = poissonQuantile(.9999, { lambda: secondLegLockMinutes / firstLegMinutesPerBlock });

return firstLegLockBuffer;
}

/**
* Calculates the currencies and amounts of subunits/satoshis each side of a swap should receive.
* @param quantity The quantity being swapped
Expand Down Expand Up @@ -161,7 +182,7 @@ class Swaps extends EventEmitter {
this.sanitySwaps.set(rHash, sanitySwap);
const swapClient = this.swapClientManager.get(currency)!;
try {
await swapClient.addInvoice(rHash, 1, swapClient.lockBuffer);
await swapClient.addInvoice(rHash, 1);
} catch (err) {
this.logger.error('could not add invoice for sanity swap', err);
return;
Expand Down Expand Up @@ -356,7 +377,7 @@ class Swaps extends EventEmitter {

try {
await Promise.all([
swapClient.addInvoice(rHash, 1, swapClient.lockBuffer),
swapClient.addInvoice(rHash, 1),
peer.sendPacket(sanitySwapInitPacket),
peer.wait(sanitySwapInitPacket.header.id, PacketType.SanitySwapAck, Swaps.SANITY_SWAP_INIT_TIMEOUT),
]);
Expand Down Expand Up @@ -575,27 +596,23 @@ class Swaps extends EventEmitter {
if (height) {
this.logger.debug(`got ${takerCurrency} block height of ${height}`);

const routeAbsoluteTimeLock = makerToTakerRoute.getTotalTimeLock();
const routeLockDuration = routeAbsoluteTimeLock - height;
const routeTotalTimeLock = makerToTakerRoute.getTotalTimeLock();
const routeLockDuration = routeTotalTimeLock - height;
const routeLockHours = Math.round(routeLockDuration * takerSwapClient.minutesPerBlock / 60);
this.logger.debug(`found route to taker with total lock duration of ${routeLockDuration} ${takerCurrency} blocks (~${routeLockHours}h)`);
deal.takerMaxTimeLock = routeLockDuration;

const makerClientLockBuffer = this.swapClientManager.get(makerCurrency)!.lockBuffer;
const makerClientLockBufferHours = Math.round(makerClientLockBuffer * makerSwapClient.minutesPerBlock / 60);
this.logger.debug(`maker client lock buffer: ${makerClientLockBuffer} ${makerCurrency} blocks (~${makerClientLockBufferHours}h)`);

/** The ratio of the average time for blocks on the taker (2nd leg) currency per blocks on the maker (1st leg) currency. */
const blockTimeFactor = takerSwapClient.minutesPerBlock / makerSwapClient.minutesPerBlock;
this.logger.debug(`block time factor of ${makerCurrency} to ${takerCurrency}: ${blockTimeFactor}`);
// Here we calculate the minimum lock delta we will expect as maker on the final hop to us on
// the first leg of the swap. This should ensure a very high probability that the final hop
// of the payment to us won't expire before our payment to the taker with time leftover to
// satisfy our finalLock/cltvDelta requirement for the incoming payment swap client.
const lockBuffer = Swaps.calculateLockBuffer(routeLockDuration, takerSwapClient.minutesPerBlock, makerSwapClient.minutesPerBlock);
const lockBufferHours = Math.round(lockBuffer * makerSwapClient.minutesPerBlock / 60);
this.logger.debug(`calculated lock buffer for first leg: ${lockBuffer} ${makerCurrency} blocks (~${lockBufferHours}h)`);

// Here we calculate the minimum lock duration the maker will expect on the final hop to him
// on the first leg of the swap. This is equal to the maker's configurable lock buffer plus
// the total lock duration of the taker route times a factor to convert taker blocks to maker
// blocks. This is to ensure that the 1st leg payment HTLC doesn't expire before the 2nd leg.
deal.makerCltvDelta = makerClientLockBuffer + Math.ceil(routeLockDuration * blockTimeFactor);
deal.makerCltvDelta = lockBuffer + makerSwapClient.finalLock;
const makerCltvDeltaHours = Math.round(deal.makerCltvDelta * makerSwapClient.minutesPerBlock / 60);
this.logger.debug(`calculated lock delta for final hop to maker: ${deal.makerCltvDelta} ${makerCurrency} blocks (~${makerCltvDeltaHours}h)`);
this.logger.debug(`lock delta for final hop to maker: ${deal.makerCltvDelta} ${makerCurrency} blocks (~${makerCltvDeltaHours}h)`);
}

if (!deal.makerCltvDelta) {
Expand Down
Loading

0 comments on commit cf83ab0

Please sign in to comment.