Merge pull request #2319 from mempool/nymkappa/feature/import-json-topology
Import json topology
This commit is contained in:
commit
4cf4efd3f2
@ -207,6 +207,10 @@ export class Common {
|
|||||||
|
|
||||||
/** Decodes a channel id returned by lnd as uint64 to a short channel id */
|
/** Decodes a channel id returned by lnd as uint64 to a short channel id */
|
||||||
static channelIntegerIdToShortId(id: string): string {
|
static channelIntegerIdToShortId(id: string): string {
|
||||||
|
if (id.indexOf('/') !== -1) {
|
||||||
|
id = id.slice(0, -2);
|
||||||
|
}
|
||||||
|
|
||||||
if (id.indexOf('x') !== -1) { // Already a short id
|
if (id.indexOf('x') !== -1) { // Already a short id
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
@ -71,9 +71,7 @@ class FundingTxFetcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $fetchChannelOpenTx(channelId: string): Promise<{timestamp: number, txid: string, value: number}> {
|
public async $fetchChannelOpenTx(channelId: string): Promise<{timestamp: number, txid: string, value: number}> {
|
||||||
if (channelId.indexOf('x') === -1) {
|
channelId = Common.channelIntegerIdToShortId(channelId);
|
||||||
channelId = Common.channelIntegerIdToShortId(channelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.fundingTxCache[channelId]) {
|
if (this.fundingTxCache[channelId]) {
|
||||||
return this.fundingTxCache[channelId];
|
return this.fundingTxCache[channelId];
|
||||||
|
@ -8,30 +8,6 @@ import { isIP } from 'net';
|
|||||||
|
|
||||||
const fsPromises = promises;
|
const fsPromises = promises;
|
||||||
|
|
||||||
interface Node {
|
|
||||||
id: string;
|
|
||||||
timestamp: number;
|
|
||||||
features: string;
|
|
||||||
rgb_color: string;
|
|
||||||
alias: string;
|
|
||||||
addresses: unknown[];
|
|
||||||
out_degree: number;
|
|
||||||
in_degree: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Channel {
|
|
||||||
channel_id: string;
|
|
||||||
node1_pub: string;
|
|
||||||
node2_pub: string;
|
|
||||||
timestamp: number;
|
|
||||||
features: string;
|
|
||||||
fee_base_msat: number;
|
|
||||||
fee_rate_milli_msat: number;
|
|
||||||
htlc_minimim_msat: number;
|
|
||||||
cltv_expiry_delta: number;
|
|
||||||
htlc_maximum_msat: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class LightningStatsImporter {
|
class LightningStatsImporter {
|
||||||
topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER;
|
topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER;
|
||||||
|
|
||||||
@ -59,11 +35,11 @@ class LightningStatsImporter {
|
|||||||
let isUnnanounced = true;
|
let isUnnanounced = true;
|
||||||
|
|
||||||
for (const socket of (node.addresses ?? [])) {
|
for (const socket of (node.addresses ?? [])) {
|
||||||
if (!socket.network?.length || !socket.addr?.length) {
|
if (!socket.network?.length && !socket.addr?.length) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
hasOnion = hasOnion || ['torv2', 'torv3'].includes(socket.network) || socket.addr.indexOf('onion') !== -1;
|
hasOnion = hasOnion || ['torv2', 'torv3'].includes(socket.network) || socket.addr.indexOf('onion') !== -1 || socket.addr.indexOf('torv2') !== -1 || socket.addr.indexOf('torv3') !== -1;
|
||||||
hasClearnet = hasClearnet || ['ipv4', 'ipv6'].includes(socket.network) || [4, 6].includes(isIP(socket.addr.split(':')[0]));
|
hasClearnet = hasClearnet || ['ipv4', 'ipv6'].includes(socket.network) || [4, 6].includes(isIP(socket.addr.split(':')[0])) || socket.addr.indexOf('ipv4') !== -1 || socket.addr.indexOf('ipv6') !== -1;;
|
||||||
}
|
}
|
||||||
if (hasOnion && hasClearnet) {
|
if (hasOnion && hasClearnet) {
|
||||||
clearnetTorNodes++;
|
clearnetTorNodes++;
|
||||||
@ -262,83 +238,152 @@ class LightningStatsImporter {
|
|||||||
* Import topology files LN historical data into the database
|
* Import topology files LN historical data into the database
|
||||||
*/
|
*/
|
||||||
async $importHistoricalLightningStats(): Promise<void> {
|
async $importHistoricalLightningStats(): Promise<void> {
|
||||||
const fileList = await fsPromises.readdir(this.topologiesFolder);
|
try {
|
||||||
// Insert history from the most recent to the oldest
|
let fileList: string[] = [];
|
||||||
// This also put the .json cached files first
|
|
||||||
fileList.sort().reverse();
|
|
||||||
|
|
||||||
const [rows]: any[] = await DB.query(`
|
|
||||||
SELECT UNIX_TIMESTAMP(added) AS added, node_count
|
|
||||||
FROM lightning_stats
|
|
||||||
ORDER BY added DESC
|
|
||||||
`);
|
|
||||||
const existingStatsTimestamps = {};
|
|
||||||
for (const row of rows) {
|
|
||||||
existingStatsTimestamps[row.added] = row;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For logging purpose
|
|
||||||
let processed = 10;
|
|
||||||
let totalProcessed = 0;
|
|
||||||
let logStarted = false;
|
|
||||||
|
|
||||||
for (const filename of fileList) {
|
|
||||||
processed++;
|
|
||||||
|
|
||||||
const timestamp = parseInt(filename.split('_')[1], 10);
|
|
||||||
|
|
||||||
// Stats exist already, don't calculate/insert them
|
|
||||||
if (existingStatsTimestamps[timestamp] !== undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filename.indexOf('.topology') === -1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Reading ${this.topologiesFolder}/${filename}`);
|
|
||||||
let fileContent = '';
|
|
||||||
try {
|
try {
|
||||||
fileContent = await fsPromises.readFile(`${this.topologiesFolder}/${filename}`, 'utf8');
|
fileList = await fsPromises.readdir(this.topologiesFolder);
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
if (e.errno == -1) { // EISDIR - Ignore directorie
|
logger.err(`Unable to open topology folder at ${this.topologiesFolder}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
// Insert history from the most recent to the oldest
|
||||||
|
// This also put the .json cached files first
|
||||||
|
fileList.sort().reverse();
|
||||||
|
|
||||||
|
const [rows]: any[] = await DB.query(`
|
||||||
|
SELECT UNIX_TIMESTAMP(added) AS added, node_count
|
||||||
|
FROM lightning_stats
|
||||||
|
ORDER BY added DESC
|
||||||
|
`);
|
||||||
|
const existingStatsTimestamps = {};
|
||||||
|
for (const row of rows) {
|
||||||
|
existingStatsTimestamps[row.added] = row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For logging purpose
|
||||||
|
let processed = 10;
|
||||||
|
let totalProcessed = 0;
|
||||||
|
let logStarted = false;
|
||||||
|
|
||||||
|
for (const filename of fileList) {
|
||||||
|
processed++;
|
||||||
|
|
||||||
|
const timestamp = parseInt(filename.split('_')[1], 10);
|
||||||
|
|
||||||
|
// Stats exist already, don't calculate/insert them
|
||||||
|
if (existingStatsTimestamps[timestamp] !== undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filename.indexOf('topology_') === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Reading ${this.topologiesFolder}/${filename}`);
|
||||||
|
let fileContent = '';
|
||||||
|
try {
|
||||||
|
fileContent = await fsPromises.readFile(`${this.topologiesFolder}/${filename}`, 'utf8');
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.errno == -1) { // EISDIR - Ignore directorie
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
logger.err(`Unable to open ${this.topologiesFolder}/${filename}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let graph;
|
||||||
|
try {
|
||||||
|
graph = JSON.parse(fileContent);
|
||||||
|
graph = await this.cleanupTopology(graph);
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!logStarted) {
|
||||||
|
logger.info(`Founds a topology file that we did not import. Importing historical lightning stats now.`);
|
||||||
|
logStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`;
|
||||||
|
logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.edges.length} channels`);
|
||||||
|
|
||||||
|
totalProcessed++;
|
||||||
|
|
||||||
|
if (processed > 10) {
|
||||||
|
logger.info(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`);
|
||||||
|
processed = 0;
|
||||||
|
} else {
|
||||||
|
logger.debug(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`);
|
||||||
|
}
|
||||||
|
await fundingTxFetcher.$fetchChannelsFundingTxs(graph.edges.map(channel => channel.channel_id.slice(0, -2)));
|
||||||
|
const stat = await this.computeNetworkStats(timestamp, graph);
|
||||||
|
|
||||||
|
existingStatsTimestamps[timestamp] = stat;
|
||||||
}
|
}
|
||||||
|
|
||||||
let graph;
|
if (totalProcessed > 0) {
|
||||||
try {
|
logger.info(`Lightning network stats historical import completed`);
|
||||||
graph = JSON.parse(fileContent);
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content`);
|
logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanupTopology(graph) {
|
||||||
|
const newGraph = {
|
||||||
|
nodes: <ILightningApi.Node[]>[],
|
||||||
|
edges: <ILightningApi.Channel[]>[],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const node of graph.nodes) {
|
||||||
|
const addressesParts = (node.addresses ?? '').split(',');
|
||||||
|
const addresses: any[] = [];
|
||||||
|
for (const address of addressesParts) {
|
||||||
|
addresses.push({
|
||||||
|
network: '',
|
||||||
|
addr: address
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newGraph.nodes.push({
|
||||||
|
last_update: node.timestamp ?? 0,
|
||||||
|
pub_key: node.id ?? null,
|
||||||
|
alias: node.alias ?? null,
|
||||||
|
addresses: addresses,
|
||||||
|
color: node.rgb_color ?? null,
|
||||||
|
features: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const adjacency of graph.adjacency) {
|
||||||
|
if (adjacency.length === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
if (!logStarted) {
|
|
||||||
logger.info(`Founds a topology file that we did not import. Importing historical lightning stats now.`);
|
|
||||||
logStarted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`;
|
|
||||||
logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.edges.length} channels`);
|
|
||||||
|
|
||||||
totalProcessed++;
|
|
||||||
|
|
||||||
if (processed > 10) {
|
|
||||||
logger.info(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`);
|
|
||||||
processed = 0;
|
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`);
|
for (const edge of adjacency) {
|
||||||
|
newGraph.edges.push({
|
||||||
|
channel_id: edge.scid,
|
||||||
|
chan_point: '',
|
||||||
|
last_update: edge.timestamp,
|
||||||
|
node1_pub: edge.source ?? null,
|
||||||
|
node2_pub: edge.destination ?? null,
|
||||||
|
capacity: '0', // Will be fetch later
|
||||||
|
node1_policy: {
|
||||||
|
time_lock_delta: edge.cltv_expiry_delta,
|
||||||
|
min_htlc: edge.htlc_minimim_msat,
|
||||||
|
fee_base_msat: edge.fee_base_msat,
|
||||||
|
fee_rate_milli_msat: edge.fee_proportional_millionths,
|
||||||
|
max_htlc_msat: edge.htlc_maximum_msat,
|
||||||
|
last_update: edge.timestamp,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
node2_policy: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await fundingTxFetcher.$fetchChannelsFundingTxs(graph.edges.map(channel => channel.channel_id.slice(0, -2)));
|
|
||||||
const stat = await this.computeNetworkStats(timestamp, graph);
|
|
||||||
|
|
||||||
existingStatsTimestamps[timestamp] = stat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalProcessed > 0) {
|
return newGraph;
|
||||||
logger.info(`Lightning network stats historical import completed`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user