Merge pull request #3933 from mempool/nymkappa/feature-bits
Show raw and decoded lightning node features
This commit is contained in:
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 63;
|
||||
private static currentVersion = 64;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -543,6 +543,11 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"');
|
||||
await this.updateToSchemaVersion(63);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 64 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
|
||||
await this.updateToSchemaVersion(64);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@ import DB from '../../database';
|
||||
import { ResultSetHeader } from 'mysql2';
|
||||
import { ILightningApi } from '../lightning/lightning-api.interface';
|
||||
import { ITopNodesPerCapacity, ITopNodesPerChannels } from '../../mempool.interfaces';
|
||||
import { bin2hex } from '../../utils/format';
|
||||
|
||||
class NodesApi {
|
||||
public async $getWorldNodes(): Promise<any> {
|
||||
@@ -56,7 +57,8 @@ class NodesApi {
|
||||
UNIX_TIMESTAMP(updated_at) AS updated_at, color, sockets as sockets,
|
||||
as_number, city_id, country_id, subdivision_id, longitude, latitude,
|
||||
geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city,
|
||||
geo_names_country.names as country, geo_names_subdivision.names as subdivision
|
||||
geo_names_country.names as country, geo_names_subdivision.names as subdivision,
|
||||
features
|
||||
FROM nodes
|
||||
LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number
|
||||
LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id
|
||||
@@ -76,6 +78,23 @@ class NodesApi {
|
||||
node.city = JSON.parse(node.city);
|
||||
node.country = JSON.parse(node.country);
|
||||
|
||||
// Features
|
||||
node.features = JSON.parse(node.features);
|
||||
node.featuresBits = null;
|
||||
if (node.features) {
|
||||
let maxBit = 0;
|
||||
for (const feature of node.features) {
|
||||
maxBit = Math.max(maxBit, feature.bit);
|
||||
}
|
||||
maxBit = Math.ceil(maxBit / 4) * 4 - 1;
|
||||
|
||||
node.featuresBits = new Array(maxBit + 1).fill(0);
|
||||
for (const feature of node.features) {
|
||||
node.featuresBits[feature.bit] = 1;
|
||||
}
|
||||
node.featuresBits = bin2hex(node.featuresBits.reverse().join(''));
|
||||
}
|
||||
|
||||
// Active channels and capacity
|
||||
const activeChannelsStats: any = await this.$getActiveChannelsStats(public_key);
|
||||
node.active_channel_count = activeChannelsStats.active_channel_count ?? 0;
|
||||
@@ -656,10 +675,19 @@ class NodesApi {
|
||||
alias_search,
|
||||
color,
|
||||
sockets,
|
||||
status
|
||||
status,
|
||||
features
|
||||
)
|
||||
VALUES (?, NOW(), FROM_UNIXTIME(?), ?, ?, ?, ?, 1)
|
||||
ON DUPLICATE KEY UPDATE updated_at = FROM_UNIXTIME(?), alias = ?, alias_search = ?, color = ?, sockets = ?, status = 1`;
|
||||
VALUES (?, NOW(), FROM_UNIXTIME(?), ?, ?, ?, ?, 1, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
updated_at = FROM_UNIXTIME(?),
|
||||
alias = ?,
|
||||
alias_search = ?,
|
||||
color = ?,
|
||||
sockets = ?,
|
||||
status = 1,
|
||||
features = ?
|
||||
`;
|
||||
|
||||
await DB.query(query, [
|
||||
node.pub_key,
|
||||
@@ -668,11 +696,13 @@ class NodesApi {
|
||||
this.aliasToSearchText(node.alias),
|
||||
node.color,
|
||||
sockets,
|
||||
JSON.stringify(node.features),
|
||||
node.last_update,
|
||||
node.alias,
|
||||
this.aliasToSearchText(node.alias),
|
||||
node.color,
|
||||
sockets,
|
||||
JSON.stringify(node.features),
|
||||
]);
|
||||
} catch (e) {
|
||||
logger.err('$saveNode() error: ' + (e instanceof Error ? e.message : e));
|
||||
|
||||
@@ -2,8 +2,91 @@ import { ILightningApi } from '../lightning-api.interface';
|
||||
import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher';
|
||||
import logger from '../../../logger';
|
||||
import { Common } from '../../common';
|
||||
import { hex2bin } from '../../../utils/format';
|
||||
import config from '../../../config';
|
||||
|
||||
// https://github.com/lightningnetwork/lnd/blob/master/lnwire/features.go
|
||||
export enum FeatureBits {
|
||||
DataLossProtectRequired = 0,
|
||||
DataLossProtectOptional = 1,
|
||||
InitialRoutingSync = 3,
|
||||
UpfrontShutdownScriptRequired = 4,
|
||||
UpfrontShutdownScriptOptional = 5,
|
||||
GossipQueriesRequired = 6,
|
||||
GossipQueriesOptional = 7,
|
||||
TLVOnionPayloadRequired = 8,
|
||||
TLVOnionPayloadOptional = 9,
|
||||
StaticRemoteKeyRequired = 12,
|
||||
StaticRemoteKeyOptional = 13,
|
||||
PaymentAddrRequired = 14,
|
||||
PaymentAddrOptional = 15,
|
||||
MPPRequired = 16,
|
||||
MPPOptional = 17,
|
||||
WumboChannelsRequired = 18,
|
||||
WumboChannelsOptional = 19,
|
||||
AnchorsRequired = 20,
|
||||
AnchorsOptional = 21,
|
||||
AnchorsZeroFeeHtlcTxRequired = 22,
|
||||
AnchorsZeroFeeHtlcTxOptional = 23,
|
||||
ShutdownAnySegwitRequired = 26,
|
||||
ShutdownAnySegwitOptional = 27,
|
||||
AMPRequired = 30,
|
||||
AMPOptional = 31,
|
||||
ExplicitChannelTypeRequired = 44,
|
||||
ExplicitChannelTypeOptional = 45,
|
||||
ScidAliasRequired = 46,
|
||||
ScidAliasOptional = 47,
|
||||
PaymentMetadataRequired = 48,
|
||||
PaymentMetadataOptional = 49,
|
||||
ZeroConfRequired = 50,
|
||||
ZeroConfOptional = 51,
|
||||
KeysendRequired = 54,
|
||||
KeysendOptional = 55,
|
||||
ScriptEnforcedLeaseRequired = 2022,
|
||||
ScriptEnforcedLeaseOptional = 2023,
|
||||
MaxBolt11Feature = 5114,
|
||||
};
|
||||
|
||||
export const FeaturesMap = new Map<FeatureBits, string>([
|
||||
[FeatureBits.DataLossProtectRequired, 'data-loss-protect'],
|
||||
[FeatureBits.DataLossProtectOptional, 'data-loss-protect'],
|
||||
[FeatureBits.InitialRoutingSync, 'initial-routing-sync'],
|
||||
[FeatureBits.UpfrontShutdownScriptRequired, 'upfront-shutdown-script'],
|
||||
[FeatureBits.UpfrontShutdownScriptOptional, 'upfront-shutdown-script'],
|
||||
[FeatureBits.GossipQueriesRequired, 'gossip-queries'],
|
||||
[FeatureBits.GossipQueriesOptional, 'gossip-queries'],
|
||||
[FeatureBits.TLVOnionPayloadRequired, 'tlv-onion'],
|
||||
[FeatureBits.TLVOnionPayloadOptional, 'tlv-onion'],
|
||||
[FeatureBits.StaticRemoteKeyOptional, 'static-remote-key'],
|
||||
[FeatureBits.StaticRemoteKeyRequired, 'static-remote-key'],
|
||||
[FeatureBits.PaymentAddrOptional, 'payment-addr'],
|
||||
[FeatureBits.PaymentAddrRequired, 'payment-addr'],
|
||||
[FeatureBits.MPPOptional, 'multi-path-payments'],
|
||||
[FeatureBits.MPPRequired, 'multi-path-payments'],
|
||||
[FeatureBits.AnchorsRequired, 'anchor-commitments'],
|
||||
[FeatureBits.AnchorsOptional, 'anchor-commitments'],
|
||||
[FeatureBits.AnchorsZeroFeeHtlcTxRequired, 'anchors-zero-fee-htlc-tx'],
|
||||
[FeatureBits.AnchorsZeroFeeHtlcTxOptional, 'anchors-zero-fee-htlc-tx'],
|
||||
[FeatureBits.WumboChannelsRequired, 'wumbo-channels'],
|
||||
[FeatureBits.WumboChannelsOptional, 'wumbo-channels'],
|
||||
[FeatureBits.AMPRequired, 'amp'],
|
||||
[FeatureBits.AMPOptional, 'amp'],
|
||||
[FeatureBits.PaymentMetadataOptional, 'payment-metadata'],
|
||||
[FeatureBits.PaymentMetadataRequired, 'payment-metadata'],
|
||||
[FeatureBits.ExplicitChannelTypeOptional, 'explicit-commitment-type'],
|
||||
[FeatureBits.ExplicitChannelTypeRequired, 'explicit-commitment-type'],
|
||||
[FeatureBits.KeysendOptional, 'keysend'],
|
||||
[FeatureBits.KeysendRequired, 'keysend'],
|
||||
[FeatureBits.ScriptEnforcedLeaseRequired, 'script-enforced-lease'],
|
||||
[FeatureBits.ScriptEnforcedLeaseOptional, 'script-enforced-lease'],
|
||||
[FeatureBits.ScidAliasRequired, 'scid-alias'],
|
||||
[FeatureBits.ScidAliasOptional, 'scid-alias'],
|
||||
[FeatureBits.ZeroConfRequired, 'zero-conf'],
|
||||
[FeatureBits.ZeroConfOptional, 'zero-conf'],
|
||||
[FeatureBits.ShutdownAnySegwitRequired, 'shutdown-any-segwit'],
|
||||
[FeatureBits.ShutdownAnySegwitOptional, 'shutdown-any-segwit'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Convert a clightning "listnode" entry to a lnd node entry
|
||||
*/
|
||||
@@ -17,10 +100,36 @@ export function convertNode(clNode: any): ILightningApi.Node {
|
||||
custom_records = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const nodeFeatures: ILightningApi.Feature[] = [];
|
||||
const nodeFeaturesBinary = hex2bin(clNode.features).split('').reverse().join('');
|
||||
|
||||
for (let i = 0; i < nodeFeaturesBinary.length; i++) {
|
||||
if (nodeFeaturesBinary[i] === '0') {
|
||||
continue;
|
||||
}
|
||||
const feature = FeaturesMap.get(i);
|
||||
if (!feature) {
|
||||
nodeFeatures.push({
|
||||
bit: i,
|
||||
name: 'unknown',
|
||||
is_required: i % 2 === 0,
|
||||
is_known: false
|
||||
});
|
||||
} else {
|
||||
nodeFeatures.push({
|
||||
bit: i,
|
||||
name: feature,
|
||||
is_required: i % 2 === 0,
|
||||
is_known: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
alias: clNode.alias ?? '',
|
||||
color: `#${clNode.color ?? ''}`,
|
||||
features: [], // TODO parse and return clNode.feature
|
||||
features: nodeFeatures,
|
||||
pub_key: clNode.nodeid,
|
||||
addresses: clNode.addresses?.map((addr) => {
|
||||
let address = addr.address;
|
||||
|
||||
@@ -79,6 +79,7 @@ export namespace ILightningApi {
|
||||
}
|
||||
|
||||
export interface Feature {
|
||||
bit: number;
|
||||
name: string;
|
||||
is_required: boolean;
|
||||
is_known: boolean;
|
||||
|
||||
@@ -41,8 +41,23 @@ class LndApi implements AbstractLightningApi {
|
||||
}
|
||||
|
||||
async $getNetworkGraph(): Promise<ILightningApi.NetworkGraph> {
|
||||
return axios.get<ILightningApi.NetworkGraph>(config.LND.REST_API_URL + '/v1/graph', this.axiosConfig)
|
||||
const graph = await axios.get<ILightningApi.NetworkGraph>(config.LND.REST_API_URL + '/v1/graph', this.axiosConfig)
|
||||
.then((response) => response.data);
|
||||
|
||||
for (const node of graph.nodes) {
|
||||
const nodeFeatures: ILightningApi.Feature[] = [];
|
||||
for (const bit in node.features) {
|
||||
nodeFeatures.push({
|
||||
bit: parseInt(bit, 10),
|
||||
name: node.features[bit].name,
|
||||
is_required: node.features[bit].is_required,
|
||||
is_known: node.features[bit].is_known,
|
||||
});
|
||||
}
|
||||
node.features = nodeFeatures;
|
||||
}
|
||||
|
||||
return graph;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import logger from '../../logger';
|
||||
import channelsApi from '../../api/explorer/channels.api';
|
||||
import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory';
|
||||
import config from '../../config';
|
||||
import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface';
|
||||
import { ILightningApi } from '../../api/lightning/lightning-api.interface';
|
||||
import { $lookupNodeLocation } from './sync-tasks/node-locations';
|
||||
import lightningApi from '../../api/lightning/lightning-api-factory';
|
||||
|
||||
@@ -26,4 +26,70 @@ export function formatBytes(bytes: number, toUnit: string, skipUnit = false): st
|
||||
}
|
||||
|
||||
return `${bytes.toFixed(2)}${skipUnit ? '' : ' ' + byteUnits[unitIndex]}`;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/64235212
|
||||
export function hex2bin(hex: string): string {
|
||||
if (!hex) {
|
||||
return '';
|
||||
}
|
||||
|
||||
hex = hex.replace('0x', '').toLowerCase();
|
||||
let out = '';
|
||||
|
||||
for (const c of hex) {
|
||||
switch (c) {
|
||||
case '0': out += '0000'; break;
|
||||
case '1': out += '0001'; break;
|
||||
case '2': out += '0010'; break;
|
||||
case '3': out += '0011'; break;
|
||||
case '4': out += '0100'; break;
|
||||
case '5': out += '0101'; break;
|
||||
case '6': out += '0110'; break;
|
||||
case '7': out += '0111'; break;
|
||||
case '8': out += '1000'; break;
|
||||
case '9': out += '1001'; break;
|
||||
case 'a': out += '1010'; break;
|
||||
case 'b': out += '1011'; break;
|
||||
case 'c': out += '1100'; break;
|
||||
case 'd': out += '1101'; break;
|
||||
case 'e': out += '1110'; break;
|
||||
case 'f': out += '1111'; break;
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function bin2hex(bin: string): string {
|
||||
if (!bin) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let out = '';
|
||||
|
||||
for (let i = 0; i < bin.length; i += 4) {
|
||||
const c = bin.substring(i, i + 4);
|
||||
switch (c) {
|
||||
case '0000': out += '0'; break;
|
||||
case '0001': out += '1'; break;
|
||||
case '0010': out += '2'; break;
|
||||
case '0011': out += '3'; break;
|
||||
case '0100': out += '4'; break;
|
||||
case '0101': out += '5'; break;
|
||||
case '0110': out += '6'; break;
|
||||
case '0111': out += '7'; break;
|
||||
case '1000': out += '8'; break;
|
||||
case '1001': out += '9'; break;
|
||||
case '1010': out += 'a'; break;
|
||||
case '1011': out += 'b'; break;
|
||||
case '1100': out += 'c'; break;
|
||||
case '1101': out += 'd'; break;
|
||||
case '1110': out += 'e'; break;
|
||||
case '1111': out += 'f'; break;
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user