Skip to content

Commit

Permalink
feat: CLN integration in backend
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Aug 20, 2023
1 parent 79a0999 commit 8a81829
Show file tree
Hide file tree
Showing 96 changed files with 91,298 additions and 1,257 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ jobs:
- name: Python lint
run: npm run python:lint

- name: Start hold invoice plugin
run: docker exec -it regtest lightning-cli plugin start /root/hold.sh

- name: Unit tests
run: npm run test:unit

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,6 @@ contracts/

# Bitcoin and Litecoin Core RPC cookies
docker/regtest/data/core/cookies/

# CLN data directory
docker/regtest/data/cln/regtest
2 changes: 1 addition & 1 deletion docker/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class Image:
],
),
"regtest": Image(
tags=["4.0.1"],
tags=["4.0.2"],
arguments=[
UBUNTU_VERSION,
BITCOIN_BUILD_ARG,
Expand Down
2 changes: 2 additions & 0 deletions docker/regtest/data/cln/config
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ fee-base=0
fee-per-satoshi=1
large-channels

grpc-port=9291

dev-fast-gossip
2 changes: 2 additions & 0 deletions docker/regtest/scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ elements-cli rescanblockchain > /dev/null
startCln
startLnds

mkdir /root/.lightning/regtest/certs

echo "Opening BTC channels"

openChannel bitcoin-cli \
Expand Down
9 changes: 7 additions & 2 deletions docker/regtest/startRegtest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
docker run \
-d \
--name regtest \
--volume "${PWD}"/docker/regtest/data/core/cookies:/cookies \
--volume "${PWD}"/docker/regtest/data/core/cookies:/cookies/ \
--volume "${PWD}"/docker/regtest/data/cln/regtest:/root/.lightning/regtest/certs \
--volume "${PWD}"/tools:/tools \
-p 10735:10735 \
-p 9736:9735 \
-p 9291:9291 \
-p 9292:9292 \
-p 18443:18443 \
-p 18444:18444 \
Expand Down Expand Up @@ -34,4 +36,7 @@ docker run \
-p 31000:31000 \
-p 31001:31001 \
-p 31002:31002 \
boltz/regtest:4.0.1
boltz/regtest:4.0.2

docker exec regtest bash -c "cp /root/.lightning/regtest/*.pem /root/.lightning/regtest/certs"
docker exec regtest chmod -R 777 /root/.lightning/regtest/certs
63 changes: 43 additions & 20 deletions lib/Boltz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import VersionCheck from './VersionCheck';
import GrpcServer from './grpc/GrpcServer';
import ChainTip from './db/models/ChainTip';
import GrpcService from './grpc/GrpcService';
import ClnClient from './lightning/ClnClient';
import LndClient from './lightning/LndClient';
import ChainClient from './chain/ChainClient';
import Config, { ConfigType } from './Config';
import { CurrencyType } from './consts/Enums';
import { formatError, getVersion } from './Utils';
import ElementsClient from './chain/ElementsClient';
import BackupScheduler from './backup/BackupScheduler';
import { LightningClient } from './lightning/LightningClient';
import EthereumManager from './wallet/ethereum/EthereumManager';
import WalletManager, { Currency } from './wallet/WalletManager';
import ChainTipRepository from './db/repositories/ChainTipRepository';
Expand Down Expand Up @@ -147,15 +149,26 @@ class Boltz {
// Query the chain tips now to avoid them being updated after the chain clients are initialized
const chainTips = await ChainTipRepository.getChainTips();

for (const [, currency] of this.currencies) {
if (currency.chainClient) {
await this.connectChainClient(currency.chainClient);
await Promise.all(
Array.from(this.currencies.values()).flatMap((currency) => {
const prms: Promise<void>[] = [];

if (currency.lndClient) {
await this.connectLnd(currency.lndClient);
if (currency.chainClient) {
prms.push(this.connectChainClient(currency.chainClient));
}
}
}

prms.concat(
[currency.lndClient, currency.clnClient]
.filter(
(client): client is ClnClient | LndClient =>
client !== undefined,
)
.map((client) => this.connectLightningClient(client)),
);

return prms;
}),
);

await this.walletManager.init(this.config.currencies);
await this.service.init(this.config.pairs);
Expand Down Expand Up @@ -248,18 +261,27 @@ class Boltz {
}
};

private connectLnd = async (client: LndClient) => {
const service = `${client.symbol} LND`;
private connectLightningClient = async (client: LightningClient) => {
const service = `${client.symbol} ${client.serviceName()}`;

try {
await client.connect();

const info = await client.getInfo();
VersionCheck.checkLightningVersion(
client.serviceName(),
client.symbol,
info.version,
);

VersionCheck.checkLndVersion(client.symbol, info.version);

// The featuresMap is just annoying to see on startup
info.featuresMap = undefined as any;
if (client instanceof ClnClient) {
const holdInfo = await client.getHoldInfo();
VersionCheck.checkLightningVersion(
ClnClient.serviceNameHold,
client.symbol,
holdInfo.version,
);
}

this.logStatus(service, info);
} catch (error) {
Expand All @@ -278,18 +300,19 @@ class Boltz {
currency.symbol,
);

let lndClient: LndClient | undefined;

if (currency.lnd) {
lndClient = new LndClient(this.logger, currency.symbol, currency.lnd);
}

result.set(currency.symbol, {
lndClient,
chainClient,
symbol: currency.symbol,
type: CurrencyType.BitcoinLike,
network: Networks[currency.network],
lndClient:
currency.lnd !== undefined
? new LndClient(this.logger, currency.symbol, currency.lnd)
: undefined,
clnClient:
currency.cln !== undefined
? new ClnClient(this.logger, currency.symbol, currency.cln)
: undefined,
limits: {
...currency,
},
Expand Down
2 changes: 2 additions & 0 deletions lib/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { LndConfig } from './lightning/LndClient';
import { WebdavConfig } from './backup/providers/Webdav';
import { GoogleCloudConfig } from './backup/providers/GoogleCloud';
import { deepMerge, getServiceDataDir, resolveHome } from './Utils';
import { ClnConfig } from './lightning/ClnClient';

type ChainConfig = {
host: string;
Expand Down Expand Up @@ -50,6 +51,7 @@ type CurrencyConfig = BaseCurrencyConfig & {
preferredWallet: 'lnd' | 'core' | undefined;

lnd?: LndConfig;
cln?: ClnConfig;
routingOffsetExceptions?: RoutingOffsetException[];

// Expiry for invoices of this currency in seconds
Expand Down
151 changes: 123 additions & 28 deletions lib/VersionCheck.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,155 @@
import ClnClient from './lightning/ClnClient';
import LndClient from './lightning/LndClient';
import ChainClient from './chain/ChainClient';

type Version = string | number;

class VersionCheck {
private static chainClientVersionLimits = {
minimal: 180100,
maximal: 250000,
type VersionLimits = {
minimal: Version;
maximal: Version;
};

class VersionSplit {
private readonly parts: number[];

constructor(version: string) {
const split = version.split('.');
this.parts = split.map(Number);
}

public eq = (cmp: VersionSplit): boolean => {
return (
this.parts.length === cmp.parts.length &&
this.parts.every((elem, i) => elem === cmp.parts[i])
);
};

public gt = (cmp: VersionSplit): boolean => {
for (let i = 0; i < Math.max(this.parts.length, cmp.parts.length); i++) {
const v1 = VersionSplit.getVersionPart(this.parts, i);
const v2 = VersionSplit.getVersionPart(cmp.parts, i);

if (v1 > v2) {
return true;
} else if (v2 > v1) {
return false;
}
}

return false;
};

private static getVersionPart = (arr: number[], index: number): number => {
return arr.length > index ? arr[index] : 0;
};
}

class Comperator {
public static versionInBounds = (
version: Version,
limits: VersionLimits,
): boolean => {
if (typeof version === 'number') {
return (
version <= (limits.maximal as number) &&
version >= (limits.minimal as number)
);
}

const [versionSplit, minSplit, maxSplit] = [
version,
limits.minimal,
limits.maximal,
].map((ver) => new VersionSplit(ver as string));
return (
!minSplit.gt(versionSplit) &&
(maxSplit.gt(versionSplit) || maxSplit.eq(versionSplit))
);
};
}

private static lndVersionLimits = {
minimal: '0.15.0',
maximal: '0.16.4',
class VersionCheck {
private static versionLimits = {
[ChainClient.serviceName]: {
minimal: 180100,
maximal: 250000,
},
[ClnClient.serviceName]: {
minimal: '23.05',
maximal: '23.05.2',
},
[ClnClient.serviceNameHold]: {
minimal: '0.0.1',
maximal: '0.0.1',
},
[LndClient.serviceName]: {
minimal: '0.15.0',
maximal: '0.16.4',
},
};

public static checkChainClientVersion = (
symbol: string,
version: number,
): void => {
const { maximal, minimal } = VersionCheck.chainClientVersionLimits;

if (version > maximal || version < minimal) {
if (
!Comperator.versionInBounds(
version,
VersionCheck.versionLimits[ChainClient.serviceName],
)
) {
throw VersionCheck.unsupportedVersionError(
`${symbol} Core`,
version,
maximal,
minimal,
VersionCheck.versionLimits[ChainClient.serviceName],
);
}
};

public static checkLndVersion = (symbol: string, version: string): void => {
const parseStringVersion = (version: string) => {
return Number(version.split('-')[0].replace('.', ''));
};
public static checkLightningVersion = (
serviceName: string,
symbol: string,
version: string,
): void => {
let limits: VersionLimits;
let sanitizedVersion = version;

const { maximal, minimal } = VersionCheck.lndVersionLimits;
const versionNumber = parseStringVersion(version);
switch (serviceName) {
case LndClient.serviceName:
sanitizedVersion = version.split('-')[0];
limits = VersionCheck.versionLimits[LndClient.serviceName];
break;

if (
versionNumber > parseStringVersion(maximal) ||
versionNumber < parseStringVersion(minimal)
) {
case ClnClient.serviceName:
if (version.startsWith('v')) {
sanitizedVersion = version.slice(1);
}
limits = VersionCheck.versionLimits[ClnClient.serviceName];
break;

case ClnClient.serviceNameHold:
limits = VersionCheck.versionLimits[ClnClient.serviceNameHold];
break;

default:
throw `unsupported lightning client ${serviceName}`;
}

if (!Comperator.versionInBounds(sanitizedVersion, limits)) {
throw VersionCheck.unsupportedVersionError(
`${symbol} LND`,
`${symbol} ${serviceName}`,
version,
maximal,
minimal,
limits,
);
}
};

private static unsupportedVersionError = (
service: string,
actual: Version,
maximal: Version,
minimal: Version,
limits: VersionLimits,
) => {
return `unsupported ${service} version: ${actual}; min version ${minimal}; max version ${maximal}`;
return `unsupported ${service} version: ${actual}; min version ${limits.minimal}; max version ${limits.maximal}`;
};
}

Expand Down
Loading

0 comments on commit 8a81829

Please sign in to comment.