Merge branch 'nymkappa/mega-branch' into nymkappa/new-enterprise-launch

This commit is contained in:
nymkappa 2023-11-15 18:18:01 +09:00
commit e4b56bac88
No known key found for this signature in database
GPG key ID: E155910B16E8BD04
26 changed files with 211 additions and 76 deletions

View file

@ -52,6 +52,7 @@
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000", "REST_API_URL": "http://127.0.0.1:3000",
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet", "UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
"BATCH_QUERY_BASE_SIZE": 1000,
"RETRY_UNIX_SOCKET_AFTER": 30000, "RETRY_UNIX_SOCKET_AFTER": 30000,
"REQUEST_TIMEOUT": 10000, "REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000, "FALLBACK_TIMEOUT": 5000,
@ -132,6 +133,11 @@
"BISQ_URL": "https://bisq.markets/api", "BISQ_URL": "https://bisq.markets/api",
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api" "BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
}, },
"REDIS": {
"ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
"BATCH_QUERY_BASE_SIZE": 5000
},
"REPLICATION": { "REPLICATION": {
"ENABLED": false, "ENABLED": false,
"AUDIT": false, "AUDIT": false,

View file

@ -53,6 +53,7 @@
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__", "REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__", "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"BATCH_QUERY_BASE_SIZE": 1000,
"RETRY_UNIX_SOCKET_AFTER": 888, "RETRY_UNIX_SOCKET_AFTER": 888,
"REQUEST_TIMEOUT": 10000, "REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000, "FALLBACK_TIMEOUT": 5000,
@ -140,6 +141,7 @@
}, },
"REDIS": { "REDIS": {
"ENABLED": false, "ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock" "UNIX_SOCKET_PATH": "/tmp/redis.sock",
"BATCH_QUERY_BASE_SIZE": 5000
} }
} }

View file

@ -55,6 +55,7 @@ describe('Mempool Backend Config', () => {
expect(config.ESPLORA).toStrictEqual({ expect(config.ESPLORA).toStrictEqual({
REST_API_URL: 'http://127.0.0.1:3000', REST_API_URL: 'http://127.0.0.1:3000',
UNIX_SOCKET_PATH: null, UNIX_SOCKET_PATH: null,
BATCH_QUERY_BASE_SIZE: 1000,
RETRY_UNIX_SOCKET_AFTER: 30000, RETRY_UNIX_SOCKET_AFTER: 30000,
REQUEST_TIMEOUT: 10000, REQUEST_TIMEOUT: 10000,
FALLBACK_TIMEOUT: 5000, FALLBACK_TIMEOUT: 5000,
@ -144,7 +145,8 @@ describe('Mempool Backend Config', () => {
expect(config.REDIS).toStrictEqual({ expect(config.REDIS).toStrictEqual({
ENABLED: false, ENABLED: false,
UNIX_SOCKET_PATH: '' UNIX_SOCKET_PATH: '',
BATCH_QUERY_BASE_SIZE: 5000,
}); });
}); });
}); });

View file

@ -5,7 +5,7 @@ export interface AbstractBitcoinApi {
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>; $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
$getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>; $getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>; $getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getAllMempoolTransactions(lastTxid: string); $getAllMempoolTransactions(lastTxid?: string, max_txs?: number);
$getTransactionHex(txId: string): Promise<string>; $getTransactionHex(txId: string): Promise<string>;
$getBlockHeightTip(): Promise<number>; $getBlockHeightTip(): Promise<number>;
$getBlockHashTip(): Promise<string>; $getBlockHashTip(): Promise<string>;

View file

@ -77,7 +77,7 @@ class BitcoinApi implements AbstractBitcoinApi {
throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.'); throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.');
} }
$getAllMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> { $getAllMempoolTransactions(lastTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.'); throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.');
} }

View file

@ -8,8 +8,9 @@ import logger from '../../logger';
interface FailoverHost { interface FailoverHost {
host: string, host: string,
rtts: number[], rtts: number[],
rtt: number rtt: number,
failures: number, failures: number,
latestHeight?: number,
socket?: boolean, socket?: boolean,
outOfSync?: boolean, outOfSync?: boolean,
unreachable?: boolean, unreachable?: boolean,
@ -92,6 +93,7 @@ class FailoverRouter {
host.rtts.unshift(rtt); host.rtts.unshift(rtt);
host.rtts.slice(0, 5); host.rtts.slice(0, 5);
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length; host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
host.latestHeight = height;
if (height == null || isNaN(height) || (maxHeight - height > 2)) { if (height == null || isNaN(height) || (maxHeight - height > 2)) {
host.outOfSync = true; host.outOfSync = true;
} else { } else {
@ -99,22 +101,23 @@ class FailoverRouter {
} }
host.unreachable = false; host.unreachable = false;
} else { } else {
host.outOfSync = true;
host.unreachable = true; host.unreachable = true;
} }
} }
this.sortHosts(); this.sortHosts();
logger.debug(`Tomahawk ranking: ${this.hosts.map(host => '\navg rtt ' + Math.round(host.rtt).toString().padStart(5, ' ') + ' | reachable? ' + (!host.unreachable || false).toString().padStart(5, ' ') + ' | in sync? ' + (!host.outOfSync || false).toString().padStart(5, ' ') + ` | ${host.host}`).join('')}`); logger.debug(`Tomahawk ranking:\n${this.hosts.map((host, index) => this.formatRanking(index, host, this.activeHost, maxHeight)).join('\n')}`);
// switch if the current host is out of sync or significantly slower than the next best alternative // switch if the current host is out of sync or significantly slower than the next best alternative
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) { if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) {
if (this.activeHost.unreachable) { if (this.activeHost.unreachable) {
logger.warn(`Unable to reach ${this.activeHost.host}, failing over to next best alternative`); logger.warn(`🚨🚨🚨 Unable to reach ${this.activeHost.host}, failing over to next best alternative 🚨🚨🚨`);
} else if (this.activeHost.outOfSync) { } else if (this.activeHost.outOfSync) {
logger.warn(`${this.activeHost.host} has fallen behind, failing over to next best alternative`); logger.warn(`🚨🚨🚨 ${this.activeHost.host} has fallen behind, failing over to next best alternative 🚨🚨🚨`);
} else { } else {
logger.debug(`${this.activeHost.host} is no longer the best esplora host`); logger.debug(`🛠️ ${this.activeHost.host} is no longer the best esplora host 🛠️`);
} }
this.electHost(); this.electHost();
} }
@ -122,6 +125,11 @@ class FailoverRouter {
this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval); this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval);
} }
private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string {
const heightStatus = host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅');
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : ' - '} ${host.unreachable ? '🔥' : '✅'} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
}
// sort hosts by connection quality, and update default fallback // sort hosts by connection quality, and update default fallback
private sortHosts(): void { private sortHosts(): void {
// sort by connection quality // sort by connection quality
@ -156,7 +164,7 @@ class FailoverRouter {
private addFailure(host: FailoverHost): FailoverHost { private addFailure(host: FailoverHost): FailoverHost {
host.failures++; host.failures++;
if (host.failures > 5 && this.multihost) { if (host.failures > 5 && this.multihost) {
logger.warn(`Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative`); logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`);
this.electHost(); this.electHost();
return this.activeHost; return this.activeHost;
} else { } else {
@ -225,8 +233,8 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/mempool/txs', txids, 'json'); return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/mempool/txs', txids, 'json');
} }
async $getAllMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> { async $getAllMempoolTransactions(lastSeenTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : '')); return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''), 'json', max_txs ? { max_txs } : null);
} }
$getTransactionHex(txId: string): Promise<string> { $getTransactionHex(txId: string): Promise<string> {

View file

@ -761,8 +761,13 @@ class Blocks {
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`); this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
if (!fastForwarded) { if (!fastForwarded) {
const lastestPriceId = await PricesRepository.$getLatestPriceId(); let lastestPriceId;
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`); try {
lastestPriceId = await PricesRepository.$getLatestPriceId();
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
} catch (e) {
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e));
}
if (priceUpdater.historyInserted === true && lastestPriceId !== null) { if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
await blocksRepository.$saveBlockPrices([{ await blocksRepository.$saveBlockPrices([{
height: blockExtended.height, height: blockExtended.height,
@ -771,9 +776,7 @@ class Blocks {
this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`); this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`);
} else { } else {
logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining); logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
setTimeout(() => { indexer.scheduleSingleTask('blocksPrices', 10000);
indexer.runSingleTask('blocksPrices');
}, 10000);
} }
// Save blocks summary for visualization if it's enabled // Save blocks summary for visualization if it's enabled

View file

@ -44,9 +44,13 @@ export enum FeatureBits {
KeysendOptional = 55, KeysendOptional = 55,
ScriptEnforcedLeaseRequired = 2022, ScriptEnforcedLeaseRequired = 2022,
ScriptEnforcedLeaseOptional = 2023, ScriptEnforcedLeaseOptional = 2023,
SimpleTaprootChannelsRequiredFinal = 80,
SimpleTaprootChannelsOptionalFinal = 81,
SimpleTaprootChannelsRequiredStaging = 180,
SimpleTaprootChannelsOptionalStaging = 181,
MaxBolt11Feature = 5114, MaxBolt11Feature = 5114,
}; };
export const FeaturesMap = new Map<FeatureBits, string>([ export const FeaturesMap = new Map<FeatureBits, string>([
[FeatureBits.DataLossProtectRequired, 'data-loss-protect'], [FeatureBits.DataLossProtectRequired, 'data-loss-protect'],
[FeatureBits.DataLossProtectOptional, 'data-loss-protect'], [FeatureBits.DataLossProtectOptional, 'data-loss-protect'],
@ -85,6 +89,10 @@ export const FeaturesMap = new Map<FeatureBits, string>([
[FeatureBits.ZeroConfOptional, 'zero-conf'], [FeatureBits.ZeroConfOptional, 'zero-conf'],
[FeatureBits.ShutdownAnySegwitRequired, 'shutdown-any-segwit'], [FeatureBits.ShutdownAnySegwitRequired, 'shutdown-any-segwit'],
[FeatureBits.ShutdownAnySegwitOptional, 'shutdown-any-segwit'], [FeatureBits.ShutdownAnySegwitOptional, 'shutdown-any-segwit'],
[FeatureBits.SimpleTaprootChannelsRequiredFinal, 'taproot-channels'],
[FeatureBits.SimpleTaprootChannelsOptionalFinal, 'taproot-channels'],
[FeatureBits.SimpleTaprootChannelsRequiredStaging, 'taproot-channels-staging'],
[FeatureBits.SimpleTaprootChannelsOptionalStaging, 'taproot-channels-staging'],
]); ]);
/** /**

View file

@ -126,7 +126,7 @@ class Mempool {
loadingIndicators.setProgress('mempool', count / expectedCount * 100); loadingIndicators.setProgress('mempool', count / expectedCount * 100);
while (!done) { while (!done) {
try { try {
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid); const result = await bitcoinApi.$getAllMempoolTransactions(last_txid, config.ESPLORA.BATCH_QUERY_BASE_SIZE);
if (result) { if (result) {
for (const tx of result) { for (const tx of result) {
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx); const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
@ -235,7 +235,7 @@ class Mempool {
if (!loaded) { if (!loaded) {
const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]); const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]);
const sliceLength = 10000; const sliceLength = config.ESPLORA.BATCH_QUERY_BASE_SIZE;
for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) { for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) {
const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength); const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength);
const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false); const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false);

View file

@ -480,7 +480,7 @@ class RbfCache {
}; };
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const sliceLength = 250; const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 40);
for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) { for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) {
const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength); const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength);
try { try {

View file

@ -122,8 +122,9 @@ class RedisCache {
async $removeTransactions(transactions: string[]) { async $removeTransactions(transactions: string[]) {
try { try {
await this.$ensureConnected(); await this.$ensureConnected();
for (let i = 0; i < Math.ceil(transactions.length / 10000); i++) { const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE;
const slice = transactions.slice(i * 10000, (i + 1) * 10000); for (let i = 0; i < Math.ceil(transactions.length / sliceLength); i++) {
const slice = transactions.slice(i * sliceLength, (i + 1) * sliceLength);
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`)); await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`); logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
} }

View file

@ -43,6 +43,7 @@ interface IConfig {
ESPLORA: { ESPLORA: {
REST_API_URL: string; REST_API_URL: string;
UNIX_SOCKET_PATH: string | void | null; UNIX_SOCKET_PATH: string | void | null;
BATCH_QUERY_BASE_SIZE: number;
RETRY_UNIX_SOCKET_AFTER: number; RETRY_UNIX_SOCKET_AFTER: number;
REQUEST_TIMEOUT: number; REQUEST_TIMEOUT: number;
FALLBACK_TIMEOUT: number; FALLBACK_TIMEOUT: number;
@ -151,6 +152,7 @@ interface IConfig {
REDIS: { REDIS: {
ENABLED: boolean; ENABLED: boolean;
UNIX_SOCKET_PATH: string; UNIX_SOCKET_PATH: string;
BATCH_QUERY_BASE_SIZE: number;
}, },
} }
@ -195,6 +197,7 @@ const defaults: IConfig = {
'ESPLORA': { 'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000', 'REST_API_URL': 'http://127.0.0.1:3000',
'UNIX_SOCKET_PATH': null, 'UNIX_SOCKET_PATH': null,
'BATCH_QUERY_BASE_SIZE': 1000,
'RETRY_UNIX_SOCKET_AFTER': 30000, 'RETRY_UNIX_SOCKET_AFTER': 30000,
'REQUEST_TIMEOUT': 10000, 'REQUEST_TIMEOUT': 10000,
'FALLBACK_TIMEOUT': 5000, 'FALLBACK_TIMEOUT': 5000,
@ -303,6 +306,7 @@ const defaults: IConfig = {
'REDIS': { 'REDIS': {
'ENABLED': false, 'ENABLED': false,
'UNIX_SOCKET_PATH': '', 'UNIX_SOCKET_PATH': '',
'BATCH_QUERY_BASE_SIZE': 5000,
}, },
}; };

View file

@ -2,6 +2,7 @@ import * as fs from 'fs';
import path from 'path'; import path from 'path';
import config from './config'; import config from './config';
import { createPool, Pool, PoolConnection } from 'mysql2/promise'; import { createPool, Pool, PoolConnection } from 'mysql2/promise';
import { LogLevel } from './logger';
import logger from './logger'; import logger from './logger';
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql'; import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
@ -33,7 +34,7 @@ import { execSync } from 'child_process';
} }
public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket | public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(query, params?, connection?: PoolConnection): Promise<[T, FieldPacket[]]> OkPacket[] | ResultSetHeader>(query, params?, errorLogLevel: LogLevel | 'silent' = 'debug', connection?: PoolConnection): Promise<[T, FieldPacket[]]>
{ {
this.checkDBFlag(); this.checkDBFlag();
let hardTimeout; let hardTimeout;
@ -55,19 +56,38 @@ import { execSync } from 'child_process';
}).then(result => { }).then(result => {
resolve(result); resolve(result);
}).catch(error => { }).catch(error => {
if (errorLogLevel !== 'silent') {
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
}
reject(error); reject(error);
}).finally(() => { }).finally(() => {
clearTimeout(timer); clearTimeout(timer);
}); });
}); });
} else { } else {
const pool = await this.getPool(); try {
return pool.query(query, params); const pool = await this.getPool();
return pool.query(query, params);
} catch (e) {
if (errorLogLevel !== 'silent') {
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
}
throw e;
}
}
}
private async $rollbackAtomic(connection: PoolConnection): Promise<void> {
try {
await connection.rollback();
await connection.release();
} catch (e) {
logger.warn('Failed to rollback incomplete db transaction: ' + (e instanceof Error ? e.message : e));
} }
} }
public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket | public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(queries: { query, params }[]): Promise<[T, FieldPacket[]][]> OkPacket[] | ResultSetHeader>(queries: { query, params }[], errorLogLevel: LogLevel | 'silent' = 'debug'): Promise<[T, FieldPacket[]][]>
{ {
const pool = await this.getPool(); const pool = await this.getPool();
const connection = await pool.getConnection(); const connection = await pool.getConnection();
@ -76,7 +96,7 @@ import { execSync } from 'child_process';
const results: [T, FieldPacket[]][] = []; const results: [T, FieldPacket[]][] = [];
for (const query of queries) { for (const query of queries) {
const result = await this.query(query.query, query.params, connection) as [T, FieldPacket[]]; const result = await this.query(query.query, query.params, errorLogLevel, connection) as [T, FieldPacket[]];
results.push(result); results.push(result);
} }
@ -84,9 +104,8 @@ import { execSync } from 'child_process';
return results; return results;
} catch (e) { } catch (e) {
logger.err('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e)); logger.warn('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
connection.rollback(); this.$rollbackAtomic(connection);
connection.release();
throw e; throw e;
} finally { } finally {
connection.release(); connection.release();

View file

@ -92,9 +92,15 @@ class Server {
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`); logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
// Register cleanup listeners for exit events // Register cleanup listeners for exit events
['exit', 'SIGHUP', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'unhandledRejection'].forEach(event => { ['exit', 'SIGHUP', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'].forEach(event => {
process.on(event, () => { this.onExit(event); }); process.on(event, () => { this.onExit(event); });
}); });
process.on('uncaughtException', (error) => {
this.onUnhandledException('uncaughtException', error);
});
process.on('unhandledRejection', (reason, promise) => {
this.onUnhandledException('unhandledRejection', reason);
});
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
bitcoinApi.startHealthChecks(); bitcoinApi.startHealthChecks();
@ -200,7 +206,7 @@ class Server {
} }
const newMempool = await bitcoinApi.$getRawMempool(); const newMempool = await bitcoinApi.$getRawMempool();
const numHandledBlocks = await blocks.$updateBlocks(); const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerRunning ? 10 : 1); const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
if (numHandledBlocks === 0) { if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, pollRate); await memPool.$updateMempool(newMempool, pollRate);
} }
@ -314,14 +320,18 @@ class Server {
} }
} }
onExit(exitEvent): void { onExit(exitEvent, code = 0): void {
logger.debug(`onExit for signal: ${exitEvent}`);
if (config.DATABASE.ENABLED) { if (config.DATABASE.ENABLED) {
DB.releasePidLock(); DB.releasePidLock();
} }
process.exit(0); process.exit(code);
}
onUnhandledException(type, error): void {
console.error(`${type}:`, error);
this.onExit(type, 1);
} }
} }
((): Server => new Server())(); ((): Server => new Server())();

View file

@ -15,11 +15,18 @@ export interface CoreIndex {
best_block_height: number; best_block_height: number;
} }
type TaskName = 'blocksPrices' | 'coinStatsIndex';
class Indexer { class Indexer {
runIndexer = true; private runIndexer = true;
indexerRunning = false; private indexerRunning = false;
tasksRunning: string[] = []; private tasksRunning: { [key in TaskName]?: boolean; } = {};
coreIndexes: CoreIndex[] = []; private tasksScheduled: { [key in TaskName]?: NodeJS.Timeout; } = {};
private coreIndexes: CoreIndex[] = [];
public indexerIsRunning(): boolean {
return this.indexerRunning;
}
/** /**
* Check which core index is available for indexing * Check which core index is available for indexing
@ -69,33 +76,69 @@ class Indexer {
} }
} }
public async runSingleTask(task: 'blocksPrices' | 'coinStatsIndex'): Promise<void> { /**
if (!Common.indexingEnabled()) { * schedules a single task to run in `timeout` ms
return; * only one task of each type may be scheduled
} *
* @param {TaskName} task - the type of task
if (task === 'blocksPrices' && !this.tasksRunning.includes(task) && !['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { * @param {number} timeout - delay in ms
this.tasksRunning.push(task); * @param {boolean} replace - `true` replaces any already scheduled task (works like a debounce), `false` ignores subsequent requests (works like a throttle)
const lastestPriceId = await PricesRepository.$getLatestPriceId(); */
if (priceUpdater.historyInserted === false || lastestPriceId === null) { public scheduleSingleTask(task: TaskName, timeout: number = 10000, replace = false): void {
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining); if (this.tasksScheduled[task]) {
setTimeout(() => { if (!replace) { //throttle
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task); return;
this.runSingleTask('blocksPrices'); } else { // debounce
}, 10000); clearTimeout(this.tasksScheduled[task]);
} else {
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
await mining.$indexBlockPrices();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
} }
} }
this.tasksScheduled[task] = setTimeout(async () => {
try {
await this.runSingleTask(task);
} catch (e) {
logger.err(`Unexpected error in scheduled task ${task}: ` + (e instanceof Error ? e.message : e));
} finally {
clearTimeout(this.tasksScheduled[task]);
}
}, timeout);
}
if (task === 'coinStatsIndex' && !this.tasksRunning.includes(task)) { /**
this.tasksRunning.push(task); * Runs a single task immediately
logger.debug(`Indexing coinStatsIndex now`); *
await mining.$indexCoinStatsIndex(); * (use `scheduleSingleTask` instead to queue a task to run after some timeout)
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task); */
public async runSingleTask(task: TaskName): Promise<void> {
if (!Common.indexingEnabled() || this.tasksRunning[task]) {
return;
} }
this.tasksRunning[task] = true;
switch (task) {
case 'blocksPrices': {
if (!['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
let lastestPriceId;
try {
lastestPriceId = await PricesRepository.$getLatestPriceId();
} catch (e) {
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e));
} if (priceUpdater.historyInserted === false || lastestPriceId === null) {
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
this.scheduleSingleTask(task, 10000);
} else {
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
await mining.$indexBlockPrices();
}
}
} break;
case 'coinStatsIndex': {
logger.debug(`Indexing coinStatsIndex now`);
await mining.$indexCoinStatsIndex();
} break;
}
this.tasksRunning[task] = false;
} }
public async $run(): Promise<void> { public async $run(): Promise<void> {

View file

@ -157,4 +157,6 @@ class Logger {
} }
} }
export type LogLevel = 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
export default new Logger(); export default new Logger();

View file

@ -14,7 +14,7 @@ class NodesSocketsRepository {
await DB.query(` await DB.query(`
INSERT INTO nodes_sockets(public_key, socket, type) INSERT INTO nodes_sockets(public_key, socket, type)
VALUE (?, ?, ?) VALUE (?, ?, ?)
`, [socket.publicKey, socket.addr, socket.network]); `, [socket.publicKey, socket.addr, socket.network], 'silent');
} catch (e: any) { } catch (e: any) {
if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this
logger.err(`Cannot save node socket (${[socket.publicKey, socket.addr, socket.network]}) into db. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot save node socket (${[socket.publicKey, socket.addr, socket.network]}) into db. Reason: ` + (e instanceof Error ? e.message : e));

View file

@ -79,7 +79,7 @@ class ForensicsService {
} }
let progress = 0; let progress = 0;
const sliceLength = 1000; const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 10);
// process batches of 1000 channels // process batches of 1000 channels
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) { for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength); const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);

View file

@ -290,7 +290,7 @@ class NetworkSyncService {
const allChannels = await channelsApi.$getChannelsByStatus([0, 1]); const allChannels = await channelsApi.$getChannelsByStatus([0, 1]);
const sliceLength = 5000; const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 2);
// process batches of 5000 channels // process batches of 5000 channels
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) { for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength); const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);

3
contributors/starius.txt Normal file
View file

@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of Oct 13, 2023.
Signed starius

View file

@ -53,6 +53,7 @@
"ESPLORA": { "ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__", "REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__", "UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"BATCH_QUERY_BASE_SIZE": __ESPLORA_BATCH_QUERY_BASE_SIZE__,
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__, "RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__, "REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__, "FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
@ -146,6 +147,7 @@
}, },
"REDIS": { "REDIS": {
"ENABLED": __REDIS_ENABLED__, "ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__" "UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
"BATCH_QUERY_BASE_SIZE": __REDIS_BATCH_QUERY_BASE_SIZE__
} }
} }

View file

@ -54,6 +54,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
# ESPLORA # ESPLORA
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000} __ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"} __ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
__ESPLORA_BATCH_QUERY_BASE_SIZE__=${ESPLORA_BATCH_QUERY_BASE_SIZE:=1000}
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000} __ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000} __ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000} __ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
@ -148,6 +149,7 @@ __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# REDIS # REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false} __REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true} __REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
mkdir -p "${__MEMPOOL_CACHE_DIR__}" mkdir -p "${__MEMPOOL_CACHE_DIR__}"
@ -201,6 +203,7 @@ sed -i "s!__ELECTRUM_TLS_ENABLED__!${__ELECTRUM_TLS_ENABLED__}!g" mempool-config
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__ESPLORA_BATCH_QUERY_BASE_SIZE__!${__ESPLORA_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json
sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
@ -288,5 +291,6 @@ sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS_
# REDIS # REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
node /backend/package/index.js node /backend/package/index.js

View file

@ -1,4 +1,5 @@
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null" [style.justify-content]="textAlign" [class.inline]="inline"> <span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null" [style.justify-content]="textAlign" [class.inline]="inline">
<div class="hidden">{{ text }}</div>
<ng-container *ngIf="link"> <ng-container *ngIf="link">
<a [routerLink]="link" class="truncate-link"> <a [routerLink]="link" class="truncate-link">
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container> <ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
@ -11,6 +12,7 @@
</span> </span>
<ng-template #ltrTruncated> <ng-template #ltrTruncated>
<span class="first">{{text.slice(0,-lastChars)}}</span><span class="last-four">{{text.slice(-lastChars)}}</span> <span class="first">{{text.slice(0,-lastChars)}}</span><span class="last-four">{{text.slice(-lastChars)}}</span>
</ng-template> </ng-template>

View file

@ -27,4 +27,17 @@
&.inline { &.inline {
display: inline-flex; display: inline-flex;
} }
} }
.hidden {
color: transparent;
position: absolute;
max-width: 300px;
overflow: hidden;
}
@media (max-width: 567px) {
.hidden {
max-width: 150px;
}
}

View file

@ -394,7 +394,7 @@ FREEBSD_PKG=()
FREEBSD_PKG+=(zsh sudo git git-lfs screen curl wget calc neovim) FREEBSD_PKG+=(zsh sudo git git-lfs screen curl wget calc neovim)
FREEBSD_PKG+=(openssh-portable py39-pip rust llvm10 jq base64 libzmq4) FREEBSD_PKG+=(openssh-portable py39-pip rust llvm10 jq base64 libzmq4)
FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf) FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf)
FREEBSD_PKG+=(nginx rsync py39-certbot-nginx mariadb105-server keybase) FREEBSD_PKG+=(nginx rsync py39-certbot-nginx mariadb1011-server keybase)
FREEBSD_PKG+=(geoipupdate) FREEBSD_PKG+=(geoipupdate)
FREEBSD_UNFURL_PKG=() FREEBSD_UNFURL_PKG=()

View file

@ -1,16 +1,19 @@
#!/usr/bin/env zsh #!/usr/bin/env zsh
# kill "while true" loops
killall sh
# kill actual node backends # kill actual node backends
killall node killall node 2>/dev/null
# kill "while true" loops
killall sh 2>/dev/null
# kill unfurler chrome instances # kill unfurler chrome instances
killall chrome killall chrome 2>/dev/null
# kill xorg # kill xorg
killall xinit killall xinit 2>/dev/null
# kill dbus
killall dbus-daemon 2>/dev/null
# kill nginx cache warmer scripts # kill nginx cache warmer scripts
for pid in `ps uaxww|grep warmer|grep zsh|awk '{print $2}'`;do for pid in `ps uaxww|grep warmer|grep zsh|awk '{print $2}'`;do