Store & expose node extension TLV data in backend
This commit is contained in:
parent
d36b239dbe
commit
373e02a5b0
@ -4,7 +4,7 @@ import logger from '../logger';
|
|||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 42;
|
private static currentVersion = 43;
|
||||||
private queryTimeout = 120000;
|
private queryTimeout = 120000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -356,6 +356,10 @@ class DatabaseMigration {
|
|||||||
if (databaseSchemaVersion < 42 && isBitcoin === true) {
|
if (databaseSchemaVersion < 42 && isBitcoin === true) {
|
||||||
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
|
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 43 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -791,6 +795,19 @@ class DatabaseMigration {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getCreateLNNodeRecordsTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS nodes_records (
|
||||||
|
public_key varchar(66) NOT NULL,
|
||||||
|
type int(10) unsigned NOT NULL,
|
||||||
|
payload blob NOT NULL,
|
||||||
|
UNIQUE KEY public_key_type (public_key, type),
|
||||||
|
INDEX (public_key),
|
||||||
|
FOREIGN KEY (public_key)
|
||||||
|
REFERENCES nodes (public_key)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
public async $truncateIndexedData(tables: string[]) {
|
public async $truncateIndexedData(tables: string[]) {
|
||||||
const allowedTables = ['blocks', 'hashrates', 'prices'];
|
const allowedTables = ['blocks', 'hashrates', 'prices'];
|
||||||
|
|
||||||
|
@ -105,6 +105,18 @@ class NodesApi {
|
|||||||
node.closed_channel_count = rows[0].closed_channel_count;
|
node.closed_channel_count = rows[0].closed_channel_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom records
|
||||||
|
query = `
|
||||||
|
SELECT type, payload
|
||||||
|
FROM nodes_records
|
||||||
|
WHERE public_key = ?
|
||||||
|
`;
|
||||||
|
[rows] = await DB.query(query, [public_key]);
|
||||||
|
node.custom_records = {};
|
||||||
|
for (const record of rows) {
|
||||||
|
node.custom_records[record.type] = Buffer.from(record.payload, 'binary').toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot get node information for ${public_key}. Reason: ${(e instanceof Error ? e.message : e)}`);
|
logger.err(`Cannot get node information for ${public_key}. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
|
@ -7,6 +7,15 @@ import { Common } from '../../common';
|
|||||||
* Convert a clightning "listnode" entry to a lnd node entry
|
* Convert a clightning "listnode" entry to a lnd node entry
|
||||||
*/
|
*/
|
||||||
export function convertNode(clNode: any): ILightningApi.Node {
|
export function convertNode(clNode: any): ILightningApi.Node {
|
||||||
|
let custom_records: { [type: number]: string } | undefined = undefined;
|
||||||
|
if (clNode.option_will_fund) {
|
||||||
|
try {
|
||||||
|
custom_records = { '1': Buffer.from(clNode.option_will_fund.compact_lease || '', 'hex').toString('base64') };
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot decode option_will_fund compact_lease for ${clNode.nodeid}). Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
custom_records = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
alias: clNode.alias ?? '',
|
alias: clNode.alias ?? '',
|
||||||
color: `#${clNode.color ?? ''}`,
|
color: `#${clNode.color ?? ''}`,
|
||||||
@ -23,6 +32,7 @@ export function convertNode(clNode: any): ILightningApi.Node {
|
|||||||
};
|
};
|
||||||
}) ?? [],
|
}) ?? [],
|
||||||
last_update: clNode?.last_timestamp ?? 0,
|
last_update: clNode?.last_timestamp ?? 0,
|
||||||
|
custom_records
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ export namespace ILightningApi {
|
|||||||
}[];
|
}[];
|
||||||
color: string;
|
color: string;
|
||||||
features: { [key: number]: Feature };
|
features: { [key: number]: Feature };
|
||||||
|
custom_records?: { [type: number]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Info {
|
export interface Info {
|
||||||
|
67
backend/src/repositories/NodeRecordsRepository.ts
Normal file
67
backend/src/repositories/NodeRecordsRepository.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { ResultSetHeader, RowDataPacket } from 'mysql2';
|
||||||
|
import DB from '../database';
|
||||||
|
import logger from '../logger';
|
||||||
|
|
||||||
|
export interface NodeRecord {
|
||||||
|
publicKey: string; // node public key
|
||||||
|
type: number; // TLV extension record type
|
||||||
|
payload: string; // base64 record payload
|
||||||
|
}
|
||||||
|
|
||||||
|
class NodesRecordsRepository {
|
||||||
|
public async $saveRecord(record: NodeRecord): Promise<void> {
|
||||||
|
try {
|
||||||
|
const payloadBytes = Buffer.from(record.payload, 'base64');
|
||||||
|
await DB.query(`
|
||||||
|
INSERT INTO nodes_records(public_key, type, payload)
|
||||||
|
VALUE (?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
payload = ?
|
||||||
|
`, [record.publicKey, record.type, payloadBytes, payloadBytes]);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this
|
||||||
|
logger.err(`Cannot save node record (${[record.publicKey, record.type, record.payload]}) into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
// We don't throw, not a critical issue if we miss some nodes records
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getRecordTypes(publicKey: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT type FROM nodes_records
|
||||||
|
WHERE public_key = ?
|
||||||
|
`;
|
||||||
|
const [rows] = await DB.query<RowDataPacket[][]>(query, [publicKey]);
|
||||||
|
return rows.map(row => row['type']);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot retrieve custom records for ${publicKey} from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $deleteUnusedRecords(publicKey: string, recordTypes: number[]): Promise<number> {
|
||||||
|
try {
|
||||||
|
let query;
|
||||||
|
if (recordTypes.length) {
|
||||||
|
query = `
|
||||||
|
DELETE FROM nodes_records
|
||||||
|
WHERE public_key = ?
|
||||||
|
AND type NOT IN (${recordTypes.map(type => `${type}`).join(',')})
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
query = `
|
||||||
|
DELETE FROM nodes_records
|
||||||
|
WHERE public_key = ?
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
const [result] = await DB.query<ResultSetHeader>(query, [publicKey]);
|
||||||
|
return result.affectedRows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot delete unused custom records for ${publicKey} from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new NodesRecordsRepository();
|
@ -13,6 +13,7 @@ import fundingTxFetcher from './sync-tasks/funding-tx-fetcher';
|
|||||||
import NodesSocketsRepository from '../../repositories/NodesSocketsRepository';
|
import NodesSocketsRepository from '../../repositories/NodesSocketsRepository';
|
||||||
import { Common } from '../../api/common';
|
import { Common } from '../../api/common';
|
||||||
import blocks from '../../api/blocks';
|
import blocks from '../../api/blocks';
|
||||||
|
import NodeRecordsRepository from '../../repositories/NodeRecordsRepository';
|
||||||
|
|
||||||
class NetworkSyncService {
|
class NetworkSyncService {
|
||||||
loggerTimer = 0;
|
loggerTimer = 0;
|
||||||
@ -63,6 +64,7 @@ class NetworkSyncService {
|
|||||||
let progress = 0;
|
let progress = 0;
|
||||||
|
|
||||||
let deletedSockets = 0;
|
let deletedSockets = 0;
|
||||||
|
let deletedRecords = 0;
|
||||||
const graphNodesPubkeys: string[] = [];
|
const graphNodesPubkeys: string[] = [];
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key);
|
const latestUpdated = await channelsApi.$getLatestChannelUpdateForNode(node.pub_key);
|
||||||
@ -84,8 +86,23 @@ class NetworkSyncService {
|
|||||||
addresses.push(socket.addr);
|
addresses.push(socket.addr);
|
||||||
}
|
}
|
||||||
deletedSockets += await NodesSocketsRepository.$deleteUnusedSockets(node.pub_key, addresses);
|
deletedSockets += await NodesSocketsRepository.$deleteUnusedSockets(node.pub_key, addresses);
|
||||||
|
|
||||||
|
const oldRecordTypes = await NodeRecordsRepository.$getRecordTypes(node.pub_key);
|
||||||
|
const customRecordTypes: number[] = [];
|
||||||
|
for (const [type, payload] of Object.entries(node.custom_records || {})) {
|
||||||
|
const numericalType = parseInt(type);
|
||||||
|
await NodeRecordsRepository.$saveRecord({
|
||||||
|
publicKey: node.pub_key,
|
||||||
|
type: numericalType,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
customRecordTypes.push(numericalType);
|
||||||
|
}
|
||||||
|
if (oldRecordTypes.reduce((changed, type) => changed || customRecordTypes.indexOf(type) === -1, false)) {
|
||||||
|
deletedRecords += await NodeRecordsRepository.$deleteUnusedRecords(node.pub_key, customRecordTypes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logger.info(`${progress} nodes updated. ${deletedSockets} sockets deleted`);
|
logger.info(`${progress} nodes updated. ${deletedSockets} sockets deleted. ${deletedRecords} custom records deleted.`);
|
||||||
|
|
||||||
// If a channel if not present in the graph, mark it as inactive
|
// If a channel if not present in the graph, mark it as inactive
|
||||||
await nodesApi.$setNodesInactive(graphNodesPubkeys);
|
await nodesApi.$setNodesInactive(graphNodesPubkeys);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user