mempool/backend/src/tasks/lightning/stats-updater.service.ts

341 lines
11 KiB
TypeScript
Raw Normal View History

2022-07-06 13:20:37 +02:00
import DB from '../../database';
import logger from '../../logger';
import lightningApi from '../../api/lightning/lightning-api-factory';
import channelsApi from '../../api/explorer/channels.api';
2022-07-06 13:20:37 +02:00
import * as net from 'net';
2022-04-19 17:37:06 +04:00
class LightningStatsUpdater {
2022-07-09 17:45:34 +02:00
hardCodedStartTime = '2018-01-12';
2022-04-19 17:37:06 +04:00
2022-07-04 12:00:16 +02:00
public async $startService() {
logger.info('Starting Lightning Stats service');
let isInSync = false;
let error: any;
try {
error = null;
isInSync = await this.$lightningIsSynced();
} catch (e) {
error = e;
}
if (!isInSync) {
if (error) {
logger.warn('Was not able to fetch Lightning Node status: ' + (error instanceof Error ? error.message : error) + '. Retrying in 1 minute...');
} else {
logger.notice('The Lightning graph is not yet in sync. Retrying in 1 minute...');
}
setTimeout(() => this.$startService(), 60 * 1000);
return;
}
2022-04-19 17:37:06 +04:00
const now = new Date();
const nextHourInterval = new Date(now.getFullYear(), now.getMonth(), now.getDate(), Math.floor(now.getHours() / 1) + 1, 0, 0, 0);
const difference = nextHourInterval.getTime() - now.getTime();
setTimeout(() => {
2022-07-04 12:00:16 +02:00
setInterval(async () => {
await this.$runTasks();
2022-04-19 17:37:06 +04:00
}, 1000 * 60 * 60);
}, difference);
2022-05-01 03:01:27 +04:00
2022-07-04 12:00:16 +02:00
await this.$runTasks();
}
private async $lightningIsSynced(): Promise<boolean> {
const nodeInfo = await lightningApi.$getInfo();
return nodeInfo.is_synced_to_chain && nodeInfo.is_synced_to_graph;
}
2022-07-04 12:00:16 +02:00
private async $runTasks() {
2022-07-09 17:45:34 +02:00
await this.$populateHistoricalStatistics();
await this.$populateHistoricalNodeStatistics();
2022-07-04 12:00:16 +02:00
await this.$logLightningStatsDaily();
await this.$logNodeStatsDaily();
2022-04-19 17:37:06 +04:00
}
private async $logNodeStatsDaily() {
2022-04-29 03:57:27 +04:00
const currentDate = new Date().toISOString().split('T')[0];
try {
const [state]: any = await DB.query(`SELECT string FROM state WHERE name = 'last_node_stats'`);
// Only store once per day
2022-05-01 03:01:27 +04:00
if (state[0].string === currentDate) {
2022-04-29 03:57:27 +04:00
return;
}
2022-04-19 17:37:06 +04:00
logger.info(`Running daily node stats update...`);
2022-05-16 00:01:53 +04:00
const query = `SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left, c2.channels_capacity_right FROM nodes LEFT JOIN (SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left FROM channels WHERE channels.status < 2 GROUP BY node1_public_key) c1 ON c1.node1_public_key = nodes.public_key LEFT JOIN (SELECT node2_public_key, COUNT(id) AS channels_count_right, SUM(capacity) AS channels_capacity_right FROM channels WHERE channels.status < 2 GROUP BY node2_public_key) c2 ON c2.node2_public_key = nodes.public_key`;
2022-04-29 03:57:27 +04:00
const [nodes]: any = await DB.query(query);
// First run we won't have any nodes yet
if (nodes.length < 10) {
return;
}
2022-04-29 03:57:27 +04:00
for (const node of nodes) {
await DB.query(
`INSERT INTO node_stats(public_key, added, capacity, channels) VALUES (?, NOW(), ?, ?)`,
2022-05-05 23:19:24 +04:00
[node.public_key, (parseInt(node.channels_capacity_left || 0, 10)) + (parseInt(node.channels_capacity_right || 0, 10)),
node.channels_count_left + node.channels_count_right]);
2022-04-29 03:57:27 +04:00
}
await DB.query(`UPDATE state SET string = ? WHERE name = 'last_node_stats'`, [currentDate]);
logger.info('Daily node stats has updated.');
2022-04-29 03:57:27 +04:00
} catch (e) {
logger.err('$logNodeStatsDaily() error: ' + (e instanceof Error ? e.message : e));
}
}
2022-07-04 12:00:16 +02:00
// We only run this on first launch
2022-07-09 17:45:34 +02:00
private async $populateHistoricalStatistics() {
2022-04-19 17:37:06 +04:00
try {
const [rows]: any = await DB.query(`SELECT COUNT(*) FROM lightning_stats`);
2022-07-09 17:45:34 +02:00
// Only run if table is empty
2022-07-04 12:00:16 +02:00
if (rows[0]['COUNT(*)'] > 0) {
return;
}
logger.info(`Running historical stats population...`);
2022-07-04 12:00:16 +02:00
const [channels]: any = await DB.query(`SELECT capacity, created, closing_date FROM channels ORDER BY created ASC`);
2022-07-09 17:45:34 +02:00
let date: Date = new Date(this.hardCodedStartTime);
2022-07-04 12:00:16 +02:00
const currentDate = new Date();
while (date < currentDate) {
2022-07-09 17:45:34 +02:00
date.setUTCDate(date.getUTCDate() + 1);
2022-07-04 12:00:16 +02:00
let totalCapacity = 0;
let channelsCount = 0;
for (const channel of channels) {
if (new Date(channel.created) > date) {
break;
}
if (channel.closing_date !== null && new Date(channel.closing_date) < date) {
continue;
}
totalCapacity += channel.capacity;
channelsCount++;
}
const query = `INSERT INTO lightning_stats(
2022-07-04 12:00:16 +02:00
added,
channel_count,
node_count,
2022-07-06 13:20:37 +02:00
total_capacity,
tor_nodes,
clearnet_nodes,
unannounced_nodes
2022-07-04 12:00:16 +02:00
)
2022-07-06 13:20:37 +02:00
VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)`;
2022-07-04 12:00:16 +02:00
await DB.query(query, [
date.getTime() / 1000,
channelsCount,
0,
totalCapacity,
0,
0,
0
]);
2022-07-04 12:00:16 +02:00
// Add one day and continue
date.setDate(date.getDate() + 1);
}
2022-07-06 13:20:37 +02:00
const [nodes]: any = await DB.query(`SELECT first_seen, sockets FROM nodes ORDER BY first_seen ASC`);
2022-07-09 17:45:34 +02:00
date = new Date(this.hardCodedStartTime);
2022-07-04 12:00:16 +02:00
while (date < currentDate) {
2022-07-09 17:45:34 +02:00
date.setUTCDate(date.getUTCDate() + 1);
2022-07-04 12:00:16 +02:00
let nodeCount = 0;
2022-07-06 13:20:37 +02:00
let clearnetNodes = 0;
let torNodes = 0;
let unannouncedNodes = 0;
2022-07-04 12:00:16 +02:00
for (const node of nodes) {
if (new Date(node.first_seen) > date) {
break;
}
nodeCount++;
2022-07-06 13:20:37 +02:00
const sockets = node.sockets.split(',');
let isUnnanounced = true;
for (const socket of sockets) {
const hasOnion = socket.indexOf('.onion') !== -1;
if (hasOnion) {
torNodes++;
isUnnanounced = false;
}
const hasClearnet = [4, 6].includes(net.isIP(socket.split(':')[0]));
if (hasClearnet) {
clearnetNodes++;
isUnnanounced = false;
}
}
if (isUnnanounced) {
unannouncedNodes++;
}
2022-07-04 12:00:16 +02:00
}
2022-07-06 13:20:37 +02:00
const query = `UPDATE lightning_stats SET node_count = ?, tor_nodes = ?, clearnet_nodes = ?, unannounced_nodes = ? WHERE added = FROM_UNIXTIME(?)`;
2022-07-04 12:00:16 +02:00
await DB.query(query, [
nodeCount,
2022-07-06 13:20:37 +02:00
torNodes,
clearnetNodes,
unannouncedNodes,
2022-07-04 12:00:16 +02:00
date.getTime() / 1000,
]);
}
logger.info('Historical stats populated.');
2022-07-04 12:00:16 +02:00
} catch (e) {
logger.err('$populateHistoricalData() error: ' + (e instanceof Error ? e.message : e));
}
}
2022-07-09 17:45:34 +02:00
private async $populateHistoricalNodeStatistics() {
try {
const [rows]: any = await DB.query(`SELECT COUNT(*) FROM node_stats`);
// Only run if table is empty
if (rows[0]['COUNT(*)'] > 0) {
return;
}
logger.info(`Running historical node stats population...`);
const [nodes]: any = await DB.query(`SELECT public_key, first_seen, alias FROM nodes ORDER BY first_seen ASC`);
for (const node of nodes) {
const [channels]: any = await DB.query(`SELECT capacity, created, closing_date FROM channels WHERE node1_public_key = ? OR node2_public_key = ? ORDER BY created ASC`, [node.public_key, node.public_key]);
let date: Date = new Date(this.hardCodedStartTime);
const currentDate = new Date();
let lastTotalCapacity = 0;
let lastChannelsCount = 0;
while (date < currentDate) {
date.setUTCDate(date.getUTCDate() + 1);
let totalCapacity = 0;
let channelsCount = 0;
for (const channel of channels) {
if (new Date(channel.created) > date) {
break;
}
if (channel.closing_date !== null && new Date(channel.closing_date) < date) {
continue;
}
totalCapacity += channel.capacity;
channelsCount++;
}
if (lastTotalCapacity === totalCapacity && lastChannelsCount === channelsCount) {
continue;
}
lastTotalCapacity = totalCapacity;
lastChannelsCount = channelsCount;
const query = `INSERT INTO node_stats(
public_key,
added,
capacity,
channels
)
VALUES (?, FROM_UNIXTIME(?), ?, ?)`;
await DB.query(query, [
node.public_key,
date.getTime() / 1000,
totalCapacity,
channelsCount,
]);
}
logger.debug('Updated node_stats for: ' + node.alias);
}
logger.info('Historical stats populated.');
} catch (e) {
logger.err('$populateHistoricalNodeData() error: ' + (e instanceof Error ? e.message : e));
}
}
2022-07-04 12:00:16 +02:00
private async $logLightningStatsDaily() {
const currentDate = new Date().toISOString().split('T')[0];
try {
const [state]: any = await DB.query(`SELECT string FROM state WHERE name = 'last_node_stats'`);
// Only store once per day
if (state[0].string === currentDate) {
return;
}
logger.info(`Running lightning daily stats log...`);
2022-05-01 03:01:27 +04:00
const networkGraph = await lightningApi.$getNetworkGraph();
let total_capacity = 0;
for (const channel of networkGraph.channels) {
if (channel.capacity) {
total_capacity += channel.capacity;
}
}
2022-07-06 13:20:37 +02:00
let clearnetNodes = 0;
let torNodes = 0;
let unannouncedNodes = 0;
for (const node of networkGraph.nodes) {
let isUnnanounced = true;
for (const socket of node.sockets) {
const hasOnion = socket.indexOf('.onion') !== -1;
if (hasOnion) {
torNodes++;
isUnnanounced = false;
}
const hasClearnet = [4, 6].includes(net.isIP(socket.split(':')[0]));
if (hasClearnet) {
clearnetNodes++;
isUnnanounced = false;
}
}
if (isUnnanounced) {
unannouncedNodes++;
}
}
const channelStats = await channelsApi.$getChannelsStats();
const query = `INSERT INTO lightning_stats(
2022-04-19 17:37:06 +04:00
added,
channel_count,
node_count,
2022-07-06 13:20:37 +02:00
total_capacity,
tor_nodes,
clearnet_nodes,
unannounced_nodes,
avg_capacity,
avg_fee_rate,
avg_base_fee_mtokens,
med_capacity,
med_fee_rate,
med_base_fee_mtokens
2022-04-19 17:37:06 +04:00
)
2022-07-10 17:24:43 +02:00
VALUES (NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
2022-04-19 17:37:06 +04:00
await DB.query(query, [
2022-05-01 03:01:27 +04:00
networkGraph.channels.length,
networkGraph.nodes.length,
total_capacity,
2022-07-06 13:20:37 +02:00
torNodes,
clearnetNodes,
unannouncedNodes,
channelStats.avgCapacity,
channelStats.avgFeeRate,
channelStats.avgBaseFee,
channelStats.medianCapacity,
channelStats.medianFeeRate,
channelStats.medianBaseFee,
2022-04-19 17:37:06 +04:00
]);
logger.info(`Lightning daily stats done.`);
2022-04-19 17:37:06 +04:00
} catch (e) {
logger.err('$logLightningStatsDaily() error: ' + (e instanceof Error ? e.message : e));
2022-04-19 17:37:06 +04:00
}
}
}
export default new LightningStatsUpdater();