Skip to content

Commit

Permalink
Add a generic lightning client which can sync from any node
Browse files Browse the repository at this point in the history
  • Loading branch information
TheBlueMatt committed Aug 21, 2023
1 parent ead32a4 commit 5cc1c2b
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 3 deletions.
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

0 comments on commit 5cc1c2b

Please sign in to comment.