Skip to content

Commit

Permalink
fix: Lightning payment reliability (#358)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 authored Jun 28, 2023
1 parent 6152458 commit 7f9628a
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 43 deletions.
2 changes: 2 additions & 0 deletions lib/db/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import ChannelCreation from './models/ChannelCreation';
import PendingEthereumTransaction from './models/PendingEthereumTransaction';

class Database {
public static readonly memoryDatabase = ':memory:';

public static sequelize: Sequelize.Sequelize;

private migration: Migration;
Expand Down
35 changes: 23 additions & 12 deletions lib/lightning/LndClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,15 @@ interface LndClient {
class LndClient extends BaseClient implements LndClient {
public static readonly serviceName = 'LND';

public static readonly paymentMaxParts = 5;

private static readonly grpcOptions = {
// 200 MB which is the same value lncli uses: https://github.com/lightningnetwork/lnd/commit/7470f696aebc51b4ab354324e6536f54446538e1
'grpc.max_receive_message_length': 1024 * 1024 * 200,
};

private static readonly minPaymentFee = 21;
public static readonly paymentMaxParts = 5;
private static readonly paymentMinFee = 121;
private static readonly paymentTimeout = 300;
private static readonly paymentTimePreference = 0.9;

private readonly uri!: string;
private readonly maxPaymentFeeRatio!: number;
Expand Down Expand Up @@ -108,7 +108,7 @@ class LndClient extends BaseClient implements LndClient {
const { host, port, certpath, macaroonpath, maxPaymentFeeRatio } = config;

this.maxPaymentFeeRatio =
maxPaymentFeeRatio > 0 ? maxPaymentFeeRatio : 0.03;
maxPaymentFeeRatio > 0 ? maxPaymentFeeRatio : 0.01;

if (fs.existsSync(certpath)) {
this.uri = `${host}:${port}`;
Expand Down Expand Up @@ -291,6 +291,13 @@ class LndClient extends BaseClient implements LndClient {
return this.unaryCall(this.invoices, methodName, params, true);
};

private unaryRouterCall = <T, U>(
methodName: keyof RouterClient,
params: T,
): Promise<U> => {
return this.unaryCall(this.router, methodName, params, true);
};

private unaryLightningCall = <T, U>(
methodName: keyof LightningClient,
params: T,
Expand Down Expand Up @@ -437,6 +444,7 @@ class LndClient extends BaseClient implements LndClient {

request.setMaxParts(LndClient.paymentMaxParts);
request.setTimeoutSeconds(LndClient.paymentTimeout);
request.setTimePref(LndClient.paymentTimePreference);
request.setFeeLimitSat(this.calculatePaymentFee(invoice));

request.setPaymentRequest(invoice);
Expand Down Expand Up @@ -476,9 +484,6 @@ class LndClient extends BaseClient implements LndClient {
});
};

/**
*
*/
public static formatPaymentFailureReason = (
reason: lndrpc.PaymentFailureReason,
): string => {
Expand All @@ -496,6 +501,13 @@ class LndClient extends BaseClient implements LndClient {
}
};

public resetMissionControl = () => {
return this.unaryRouterCall<
routerrpc.ResetMissionControlRequest,
routerrpc.ResetMissionControlResponse.AsObject
>('resetMissionControl', new routerrpc.ResetMissionControlRequest());
};

/**
* Cancel a hold invoice
*/
Expand Down Expand Up @@ -579,7 +591,8 @@ class LndClient extends BaseClient implements LndClient {
};

/**
* Returns the latest advertised, aggregated, and authenticated channel information for the specified node identified by its public key
* Returns the latest advertised, aggregated, and authenticated channel information
* for the specified node identified by its public key
*/
public getNodeInfo = (
publicKey: string,
Expand Down Expand Up @@ -922,10 +935,8 @@ class LndClient extends BaseClient implements LndClient {

private calculatePaymentFee = (invoice: string): number => {
const invoiceAmt = bolt11.decode(invoice).satoshis || 0;

return Math.max(
Math.ceil(invoiceAmt * this.maxPaymentFeeRatio),
LndClient.minPaymentFee,
return Math.ceil(
Math.max(invoiceAmt * this.maxPaymentFeeRatio, LndClient.paymentMinFee),
);
};
}
Expand Down
14 changes: 9 additions & 5 deletions lib/swap/PaymentHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import { Payment, PaymentFailureReason } from '../proto/lnd/rpc_pb';
import { ChannelCreationStatus, SwapUpdateEvent } from '../consts/Enums';
import {
formatError,
getChainCurrency,
splitPairId,
getHexBuffer,
getChainCurrency,
getLightningCurrency,
splitPairId,
} from '../Utils';

class PaymentHandler {
Expand Down Expand Up @@ -177,8 +177,13 @@ class PaymentHandler {
),
);

// If the invoice could not be paid but the Swap has a Channel Creation attached to it, a channel will be opened
} else if (
return undefined;
}

await lightningCurrency.lndClient!.resetMissionControl();

// If the invoice could not be paid but the Swap has a Channel Creation attached to it, a channel will be opened
if (
typeof error === 'number' &&
channelCreation &&
channelCreation.status !== ChannelCreationStatus.Created
Expand All @@ -187,7 +192,6 @@ class PaymentHandler {
case PaymentFailureReason.FAILURE_REASON_TIMEOUT:
case PaymentFailureReason.FAILURE_REASON_NO_ROUTE:
case PaymentFailureReason.FAILURE_REASON_INSUFFICIENT_BALANCE:
// TODO: !formattedError.startsWith('unable to route payment to destination: UnknownNextPeer')
await this.channelNursery.openChannel(
lightningCurrency,
swap,
Expand Down
2 changes: 1 addition & 1 deletion test/integration/Nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ export const bitcoinLndClient = new LndClient(Logger.disabledLogger, 'BTC', {
port: 10009,
certpath: `${lndDataPath}/certificates/tls.cert`,
macaroonpath: `${lndDataPath}/macaroons/admin.macaroon`,
maxPaymentFeeRatio: 0.03,
maxPaymentFeeRatio: 0.01,
});
51 changes: 26 additions & 25 deletions test/integration/lightning/LndClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import * as grpc from '@grpc/grpc-js';
import { readFileSync } from 'fs';
import { getPort } from '../../Utils';
import Logger from '../../../lib/Logger';
import { bitcoinLndClient, lndDataPath } from '../Nodes';
import Database from '../../../lib/db/Database';
import LndClient from '../../../lib/lightning/LndClient';
import { bitcoinClient, bitcoinLndClient, lndDataPath } from '../Nodes';
import {
LightningClient,
LightningService,
Expand All @@ -15,37 +16,37 @@ import {
} from '../../../lib/proto/lnd/rpc_pb';

describe('LndClient', () => {
const db = new Database(Logger.disabledLogger, Database.memoryDatabase);

beforeAll(async () => {
await bitcoinLndClient.connect();
await db.init();
await bitcoinClient.connect();
await Promise.all([bitcoinLndClient.connect(), bitcoinClient.generate(1)]);
});

afterAll(async () => {
await db.close();

bitcoinClient.disconnect();
bitcoinLndClient.disconnect();
});

test('should calculate payment fees', async () => {
const calculatePaymentFee = bitcoinLndClient['calculatePaymentFee'];

const bigInvoiceAmount = 8754398;
const maxPaymentFeeRatio = 0.03;
let invoice = await bitcoinLndClient.addInvoice(bigInvoiceAmount);

// Should use the payment fee ratio for big payments
expect(calculatePaymentFee(invoice.paymentRequest)).toEqual(
Math.ceil(bigInvoiceAmount * maxPaymentFeeRatio),
);

// Should use the minimal payment fee for small payments
invoice = await bitcoinLndClient.addInvoice(1);
expect(calculatePaymentFee(invoice.paymentRequest)).toEqual(
LndClient['minPaymentFee'],
);

invoice = await bitcoinLndClient.addInvoice(0);
expect(calculatePaymentFee(invoice.paymentRequest)).toEqual(
LndClient['minPaymentFee'],
);
});
test.each`
fee | amount
${10000} | ${1000000}
${87544} | ${8754398}
${LndClient['paymentMinFee']} | ${0}
${LndClient['paymentMinFee']} | ${1}
`(
'should calculate payment fee $fee for invoice amount $amount',
async ({ fee, amount }) => {
expect(
bitcoinLndClient['calculatePaymentFee'](
(await bitcoinLndClient.addInvoice(amount)).paymentRequest,
),
).toEqual(fee);
},
);

test('should handle messages longer than the default gRPC limit', async () => {
// 4 MB is the default gRPC limit
Expand Down

0 comments on commit 7f9628a

Please sign in to comment.