Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a generic lightning client which can sync from any node #4197

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
"@types/node": "^18.15.3",
"axios": "~1.4.0",
"bitcoinjs-lib": "~6.1.3",
"lightningdevkit": "0.0.116.0",
"lightningdevkit-node-net": "0.0.116.0",
"crypto-js": "~4.1.1",
"express": "~4.18.2",
"maxmind": "~4.3.11",
Expand Down
206 changes: 206 additions & 0 deletions backend/src/api/lightning/ldk/lightning-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
'use strict';

import { existsSync, statSync } from 'fs';
import { createConnection, Socket } from 'net';
import { homedir } from 'os';
import path from 'path';
import { createInterface, Interface } from 'readline';
import logger from '../../../logger';
import { AbstractLightningApi } from '../lightning-api-abstract-factory';
import { ILightningApi } from '../lightning-api.interface';
import { Common } from '../../common';

import * as ldk from "lightningdevkit";
import * as ldk_net from "lightningdevkit-node-net";
import * as fs from "fs";
import { strict as assert } from "assert";

function bytes_to_hex(inp: Uint8Array|Array<number>): string {
return Array.from(inp, b => ('0' + b.toString(16)).slice(-2)).join('');
}
function hex_to_bytes(inp: string): Uint8Array {
var res = new Uint8Array(inp.length / 2);
for (var i = 0; i < inp.length / 2; i++) {
res[i] = parseInt(inp.substr(i*2, 2), 16);
}
return res;
}

export default class GenericLightningClient implements AbstractLightningApi {
private constructor(
private network_graph: ldk.NetworkGraph,
private peer_manager: ldk.PeerManager,
private net_handler: ldk_net.NodeLDKNet,
private peer_pk: Uint8Array,
private peer_ip: string,
private peer_port: number
) {}

static async build(peerPubkey, peerIp, peerPort): Promise<GenericLightningClient> {
const wasm_file = fs.readFileSync("node_modules/lightningdevkit/liblightningjs.wasm");
await ldk.initializeWasmFromBinary(wasm_file);

//TODO: Create a random key, I guess, not that it matters really
const signer = ldk.KeysManager.constructor_new(new Uint8Array(32), 42n, 42);

// Construct a logger to handle log output from LDK, note that you can tweak
// the verbosity by chaning the level comparison.
const ldk_logger = ldk.Logger.new_impl({
log(record: ldk.Record): void {
if (record.get_level() != ldk.Level.LDKLevel_Gossip)
logger.debug("LDK: " + record.get_module_path() + ": " + record.get_args());
}
} as ldk.LoggerInterface);

// Construct the network graph and a callback it will use to verify lightning gossip data
const network_graph = ldk.NetworkGraph.constructor_new(ldk.Network.LDKNetwork_Bitcoin, ldk_logger);

const peer_manager;
const gossip_checker = ldk.UtxoLookup.new_impl({
get_utxo(_genesis_hash: Uint8Array, short_channel_id: bigint): ldk.UtxoResult {
// In order to verify lightning gossip data, LDK will call this method to request information
// about the UTXO at the given SCID.
const result_future = ldk.UtxoFuture.constructor_new();
const promise_future_copy = result_future.clone();
new Promise(function() {
try {
/*const txIds = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
const txIds = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
const transactions: TransactionExtended[] = [];
const startingIndex = Math.max(0, parseInt(req.params.index || '0', 10));

const endIndex = Math.min(startingIndex + 10, txIds.length);
for (let i = startingIndex; i < endIndex; i++) {
try {
const transaction = await transactionUtils.$getTransactionExtended(txIds[i], true, true);
transactions.push(transaction);
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i - startingIndex + 1) / (endIndex - startingIndex) * 100);
} catch (e) {
logger.debug('getBlockTransactions error: ' + (e instanceof Error ? e.message : e));
}
}
res.json(transactions);*/
// XXX
const utxo_value_satoshis = BigInt(4_000_000_000);
const utxo_script_pubkey = new Uint8Array(33);
const txout = ldk.TxOut.constructor_new(utxo_value_satoshis, utxo_script_pubkey);
const result = ldk.Result_TxOutUtxoLookupErrorZ.constructor_ok(txout);
promise_future_copy.resolve_without_forwarding(network_graph, result);
peer_manager.process_events();
} catch (e) {
logger.debug('Lightning transaction validation error: ' + (e instanceof Error ? e.message : e));
}
});
return ldk.UtxoResult.constructor_async(result_future);
}
} as ldk.UtxoLookupInterface);

// Now construct the gossip syncer.
const gossiper = ldk.P2PGossipSync.constructor_new(network_graph, ldk.Option_UtxoLookupZ.constructor_some(gossip_checker), ldk_logger);

// Construct the peer and socket handler
const ignoring_handler = ldk.IgnoringMessageHandler.constructor_new();
peer_manager = ldk.PeerManager.constructor_new(ldk.ErroringMessageHandler.constructor_new().as_ChannelMessageHandler(), gossiper.as_RoutingMessageHandler(), ignoring_handler.as_OnionMessageHandler(), ignoring_handler.as_CustomMessageHandler(), 4242, new Uint8Array(32), ldk_logger, signer.as_NodeSigner());
return new GenericLightningClient(
network_graph, peer_manager, new ldk_net.NodeLDKNet(peer_manager),
hex_to_bytes(peerPubkey), peerIp, peerPort
);
}

async $getNetworkGraph(): Promise<ILightningApi.NetworkGraph> {
if (this.peer_manager.get_peer_node_ids().length == 0) {
await this.net_handler.connect_peer(this.peer_ip, this.peer_port, this.peer_pk);
// XXX: Give us a bit of time to finish sync...
return { nodes: [], edges: [] };
}

const locked_graph = this.network_graph.read_only();
var nodes: ILightningApi.Node[] = [];
var edges: ILightningApi.Channel[] = [];

for (const scid of locked_graph.list_channels()) {
const chan_info = locked_graph.channel(scid);
const dir_a_update = chan_info.get_one_to_two();
const dir_b_update = chan_info.get_two_to_one();

var last_update = 0;
var node1_policy: null | ILightningApi.RoutingPolicy = null;
if (dir_a_update != null) {
last_update = Math.max(last_update, dir_a_update.get_last_update());
node1_policy = {
time_lock_delta: dir_a_update.get_cltv_expiry_delta(),
min_htlc: dir_a_update.get_htlc_minimum_msat() + "",
fee_base_msat: dir_a_update.get_fees().get_base_msat() + "",
fee_rate_milli_msat: dir_a_update.get_fees().get_proportional_millionths() + "",
disabled: !dir_a_update.get_enabled(),
max_htlc_msat: dir_a_update.get_htlc_maximum_msat() + "",
last_update: dir_a_update.get_last_update(),
};
}
var node2_policy: null | ILightningApi.RoutingPolicy = null;
if (dir_b_update != null) {
last_update = Math.max(last_update, dir_b_update.get_last_update());
node2_policy = {
time_lock_delta: dir_b_update.get_cltv_expiry_delta(),
min_htlc: dir_b_update.get_htlc_minimum_msat() + "",
fee_base_msat: dir_b_update.get_fees().get_base_msat() + "",
fee_rate_milli_msat: dir_b_update.get_fees().get_proportional_millionths() + "",
disabled: !dir_b_update.get_enabled(),
max_htlc_msat: dir_b_update.get_htlc_maximum_msat() + "",
last_update: dir_b_update.get_last_update(),
};
}
edges.push({
channel_id: Common.channelShortIdToIntegerId(scid + ""),
last_update, // XXX: this field makes no sense - channel announcements are never updated.
chan_point: "", // XXX:
capacity: (chan_info.get_capacity_sats() as ldk.Option_u64Z_Some).some + "",
node1_pub: bytes_to_hex(chan_info.get_node_one().as_slice()),
node2_pub: bytes_to_hex(chan_info.get_node_two().as_slice()),
node1_policy,
node2_policy,
});
}
for (const node_id of locked_graph.list_nodes()) {
const node_info = locked_graph.node(node_id);
var last_update = 0;
var alias = "";
var addresses: { network: string; addr: string; }[] = [];
var color = "000000";
var features = {};
const last_announcement = node_info.get_announcement_info();
if (last_announcement != null) {
last_update = last_announcement.get_last_update();
alias = bytes_to_hex(last_announcement.get_alias().get_a());
color = bytes_to_hex(last_announcement.get_rgb());
for (const address of last_announcement.addresses()) {
if (address instanceof ldk.NetAddress_IPv4) {
addresses.push({ network: "v4", addr: bytes_to_hex(address.addr) + ":" + address.port });
} else if (address instanceof ldk.NetAddress_IPv6) {
addresses.push({ network: "v4", addr: bytes_to_hex(address.addr) + ":" + address.port });
} else if (address instanceof ldk.NetAddress_OnionV3) {
const host_str = bytes_to_hex(address.ed25519_pubkey) +
bytes_to_hex([(address.checksum >> 8), (address.checksum & 0xff)]) +
bytes_to_hex([address.version & 0xff]);
// We should swap the hex string here for base32 for a proper ".onion"
addresses.push({ network: "onionv3", addr: host_str + ".onion:" + address.port });
} else if (address instanceof ldk.NetAddress_Hostname) {
addresses.push({ network: "hostname", addr: address.hostname + ":" + address.port });
}
}
// TODO: We should fill in features, but we don't currently have an API which is
// equivalent to the lnd one the returned object was built around.
}
nodes.push({
last_update,
pub_key: bytes_to_hex(node_id.as_slice()),
alias,
addresses,
color,
features,
});
}
locked_graph.free();
return { nodes, edges };
}
}
5 changes: 4 additions & 1 deletion backend/src/api/lightning/lightning-api-factory.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import config from '../../config';
import CLightningClient from './clightning/clightning-client';
import GenericLightningClient from './ldk/lightning-client';
import { AbstractLightningApi } from './lightning-api-abstract-factory';
import LndApi from './lnd/lnd-api';

function lightningApiFactory(): AbstractLightningApi {
async function lightningApiFactory(): Promise<AbstractLightningApi> {
switch (config.LIGHTNING.ENABLED === true && config.LIGHTNING.BACKEND) {
case 'cln':
return new CLightningClient(config.CLIGHTNING.SOCKET);
case 'ldk':
return await GenericLightningClient.build(config.LIGHTNING_NODE.PUBKEY, config.LIGHTNING_NODE.IP, config.LIGHTNING_NODE.PORT);
case 'lnd':
default:
return new LndApi();
Expand Down
12 changes: 12 additions & 0 deletions backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ interface IConfig {
CLIGHTNING: {
SOCKET: string;
};
LIGHTNING_NODE: {
PUBKEY: string;
IP: string;
PORT: number;
};
ELECTRUM: {
HOST: string;
PORT: number;
Expand Down Expand Up @@ -254,6 +259,11 @@ const defaults: IConfig = {
'CLIGHTNING': {
'SOCKET': '',
},
'LIGHTNING_NODE': {
'PUBKEY': '',
'IP': '',
'PORT': 0,
},
'SOCKS5PROXY': {
'ENABLED': false,
'USE_ONION': true,
Expand Down Expand Up @@ -305,6 +315,7 @@ class Config implements IConfig {
LIGHTNING: IConfig['LIGHTNING'];
LND: IConfig['LND'];
CLIGHTNING: IConfig['CLIGHTNING'];
LIGHTNING_NODE: IConfig['LIGHTNING_NODE'];
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
MAXMIND: IConfig['MAXMIND'];
Expand All @@ -326,6 +337,7 @@ class Config implements IConfig {
this.LIGHTNING = configs.LIGHTNING;
this.LND = configs.LND;
this.CLIGHTNING = configs.CLIGHTNING;
this.LIGHTNING_NODE = configs.LIGHTNING_NODE;
this.SOCKS5PROXY = configs.SOCKS5PROXY;
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
this.MAXMIND = configs.MAXMIND;
Expand Down
2 changes: 1 addition & 1 deletion backend/src/tasks/lightning/network-sync.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class NetworkSyncService {
try {
logger.debug(`Updating nodes and channels`, logger.tags.ln);

const networkGraph = await lightningApi.$getNetworkGraph();
const networkGraph = await (await lightningApi).$getNetworkGraph();
if (networkGraph.nodes.length === 0 || networkGraph.edges.length === 0) {
logger.info(`LN Network graph is empty, retrying in 10 seconds`, logger.tags.ln);
setTimeout(() => { this.$runTasks(); }, 10000);
Expand Down
2 changes: 1 addition & 1 deletion backend/src/tasks/lightning/stats-updater.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class LightningStatsUpdater {
try {
const date = new Date();
Common.setDateMidnight(date);
const networkGraph = await lightningApi.$getNetworkGraph();
const networkGraph = await(await lightningApi).$getNetworkGraph();
await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph);
logger.debug(`Updated latest network stats`, logger.tags.ln);
} catch (e) {
Expand Down
Loading