Merge branch 'master' into fix-liquid-frontend-crash

This commit is contained in:
Felipe Knorr Kuhn 2024-02-03 09:17:53 -08:00 committed by GitHub
commit e478fb2279
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 87 additions and 43 deletions

6
backend/.gitignore vendored
View File

@ -7,6 +7,12 @@ mempool-config.json
pools.json pools.json
icons.json icons.json
# docker
Dockerfile
GeoIP
start.sh
wait-for-it.sh
# compiled output # compiled output
/dist /dist
/tmp /tmp

View File

@ -646,7 +646,7 @@ class BisqMarketsApi {
case 'year': case 'year':
return strtotime('midnight first day of january', ts); return strtotime('midnight first day of january', ts);
default: default:
throw new Error('Unsupported interval: ' + interval); throw new Error('Unsupported interval');
} }
} }

View File

@ -2,7 +2,7 @@ import config from '../config';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import logger from '../logger'; import logger from '../logger';
import memPool from './mempool'; import memPool from './mempool';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified } from '../mempool.interfaces'; import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces';
import { Common } from './common'; import { Common } from './common';
import diskCache from './disk-cache'; import diskCache from './disk-cache';
import transactionUtils from './transaction-utils'; import transactionUtils from './transaction-utils';
@ -451,7 +451,9 @@ class Blocks {
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx)); const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs); const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary if (cpfpSummary) {
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
}
} else { } else {
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
} }
@ -995,11 +997,11 @@ class Blocks {
return state; return state;
} }
private updateTimerProgress(state, msg) { private updateTimerProgress(state, msg): void {
state.progress = msg; state.progress = msg;
} }
private clearTimer(state) { private clearTimer(state): void {
if (state.timer) { if (state.timer) {
clearTimeout(state.timer); clearTimeout(state.timer);
} }
@ -1088,13 +1090,19 @@ class Blocks {
summary = { summary = {
id: hash, id: hash,
transactions: cpfpSummary.transactions.map(tx => { transactions: cpfpSummary.transactions.map(tx => {
let flags: number = 0;
try {
flags = tx.flags || Common.getTransactionFlags(tx);
} catch (e) {
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
}
return { return {
txid: tx.txid, txid: tx.txid,
fee: tx.fee || 0, fee: tx.fee || 0,
vsize: tx.vsize, vsize: tx.vsize,
value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)), value: Math.round(tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0)),
rate: tx.effectiveFeePerVsize, rate: tx.effectiveFeePerVsize,
flags: tx.flags || Common.getTransactionFlags(tx), flags: flags,
}; };
}), }),
}; };
@ -1284,7 +1292,7 @@ class Blocks {
return blocks; return blocks;
} }
public async $getBlockAuditSummary(hash: string): Promise<any> { public async $getBlockAuditSummary(hash: string): Promise<BlockAudit | null> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
return BlocksAuditsRepository.$getBlockAudit(hash); return BlocksAuditsRepository.$getBlockAudit(hash);
} else { } else {
@ -1304,7 +1312,7 @@ class Blocks {
return this.currentBlockHeight; return this.currentBlockHeight;
} }
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary> { public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> {
let transactions = txs; let transactions = txs;
if (!transactions) { if (!transactions) {
if (config.MEMPOOL.BACKEND === 'esplora') { if (config.MEMPOOL.BACKEND === 'esplora') {
@ -1319,14 +1327,19 @@ class Blocks {
} }
} }
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]); if (transactions?.length != null) {
const summary = Common.calculateCpfp(height, transactions as TransactionExtended[]);
await this.$saveCpfp(hash, height, summary); await this.$saveCpfp(hash, height, summary);
const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions); const effectiveFeeStats = Common.calcEffectiveFeeStatistics(summary.transactions);
await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats); await blocksRepository.$saveEffectiveFeeStats(hash, effectiveFeeStats);
return summary; return summary;
} else {
logger.err(`Cannot index CPFP for block ${height} - missing transaction data`);
return null;
}
} }
public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> { public async $saveCpfp(hash: string, height: number, cpfpSummary: CpfpSummary): Promise<void> {

View File

@ -6,6 +6,7 @@ import { NodeSocket } from '../repositories/NodesSocketsRepository';
import { isIP } from 'net'; import { isIP } from 'net';
import transactionUtils from './transaction-utils'; import transactionUtils from './transaction-utils';
import { isPoint } from '../utils/secp256k1'; import { isPoint } from '../utils/secp256k1';
import logger from '../logger';
export class Common { export class Common {
static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ? static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49' '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
@ -261,6 +262,9 @@ export class Common {
case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break; case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break;
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break; case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
case 'v1_p2tr': { case 'v1_p2tr': {
if (!vin.witness?.length) {
throw new Error('Taproot input missing witness data');
}
flags |= TransactionFlags.p2tr; flags |= TransactionFlags.p2tr;
// in taproot, if the last witness item begins with 0x50, it's an annex // in taproot, if the last witness item begins with 0x50, it's an annex
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50'); const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
@ -301,7 +305,7 @@ export class Common {
case 'p2pk': { case 'p2pk': {
flags |= TransactionFlags.p2pk; flags |= TransactionFlags.p2pk;
// detect fake pubkey (i.e. not a valid DER point on the secp256k1 curve) // detect fake pubkey (i.e. not a valid DER point on the secp256k1 curve)
hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey.slice(2, -2)); hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey?.slice(2, -2));
} break; } break;
case 'multisig': { case 'multisig': {
flags |= TransactionFlags.p2ms; flags |= TransactionFlags.p2ms;
@ -348,7 +352,12 @@ export class Common {
} }
static classifyTransaction(tx: TransactionExtended): TransactionClassified { static classifyTransaction(tx: TransactionExtended): TransactionClassified {
const flags = Common.getTransactionFlags(tx); let flags = 0;
try {
flags = Common.getTransactionFlags(tx);
} catch (e) {
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
}
tx.flags = flags; tx.flags = flags;
return { return {
...Common.stripTransaction(tx), ...Common.stripTransaction(tx),

View File

@ -142,7 +142,7 @@ class Mining {
public async $getPoolStat(slug: string): Promise<object> { public async $getPoolStat(slug: string): Promise<object> {
const pool = await PoolsRepository.$getPool(slug); const pool = await PoolsRepository.$getPool(slug);
if (!pool) { if (!pool) {
throw new Error('This mining pool does not exist ' + escape(slug)); throw new Error('This mining pool does not exist');
} }
const blockCount: number = await BlocksRepository.$blockCount(pool.id); const blockCount: number = await BlocksRepository.$blockCount(pool.id);

View File

@ -59,7 +59,7 @@ class BlocksAuditRepositories {
} }
} }
public async $getBlockAudit(hash: string): Promise<any> { public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
try { try {
const [rows]: any[] = await DB.query( const [rows]: any[] = await DB.query(
`SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp, `SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
@ -75,8 +75,8 @@ class BlocksAuditRepositories {
expected_weight as expectedWeight expected_weight as expectedWeight
FROM blocks_audits FROM blocks_audits
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
WHERE blocks_audits.hash = "${hash}" WHERE blocks_audits.hash = ?
`); `, [hash]);
if (rows.length) { if (rows.length) {
rows[0].missingTxs = JSON.parse(rows[0].missingTxs); rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
@ -101,8 +101,8 @@ class BlocksAuditRepositories {
const [rows]: any[] = await DB.query( const [rows]: any[] = await DB.query(
`SELECT hash, match_rate as matchRate, expected_fees as expectedFees, expected_weight as expectedWeight `SELECT hash, match_rate as matchRate, expected_fees as expectedFees, expected_weight as expectedWeight
FROM blocks_audits FROM blocks_audits
WHERE blocks_audits.hash = "${hash}" WHERE blocks_audits.hash = ?
`); `, [hash]);
return rows[0]; return rows[0];
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e));

View File

@ -5,7 +5,7 @@ import logger from '../logger';
import { Common } from '../api/common'; import { Common } from '../api/common';
import PoolsRepository from './PoolsRepository'; import PoolsRepository from './PoolsRepository';
import HashratesRepository from './HashratesRepository'; import HashratesRepository from './HashratesRepository';
import { escape } from 'mysql2'; import { RowDataPacket, escape } from 'mysql2';
import BlocksSummariesRepository from './BlocksSummariesRepository'; import BlocksSummariesRepository from './BlocksSummariesRepository';
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository'; import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
import bitcoinClient from '../api/bitcoin/bitcoin-client'; import bitcoinClient from '../api/bitcoin/bitcoin-client';
@ -478,7 +478,7 @@ class BlocksRepository {
public async $getBlocksByPool(slug: string, startHeight?: number): Promise<BlockExtended[]> { public async $getBlocksByPool(slug: string, startHeight?: number): Promise<BlockExtended[]> {
const pool = await PoolsRepository.$getPool(slug); const pool = await PoolsRepository.$getPool(slug);
if (!pool) { if (!pool) {
throw new Error('This mining pool does not exist ' + escape(slug)); throw new Error('This mining pool does not exist');
} }
const params: any[] = []; const params: any[] = [];
@ -802,10 +802,10 @@ class BlocksRepository {
/** /**
* Get a list of blocks that have been indexed * Get a list of blocks that have been indexed
*/ */
public async $getIndexedBlocks(): Promise<any[]> { public async $getIndexedBlocks(): Promise<{ height: number, hash: string }[]> {
try { try {
const [rows]: any = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`); const [rows] = await DB.query(`SELECT height, hash FROM blocks ORDER BY height DESC`) as RowDataPacket[][];
return rows; return rows as { height: number, hash: string }[];
} catch (e) { } catch (e) {
logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e));
throw e; throw e;
@ -815,7 +815,7 @@ class BlocksRepository {
/** /**
* Get a list of blocks that have not had CPFP data indexed * Get a list of blocks that have not had CPFP data indexed
*/ */
public async $getCPFPUnindexedBlocks(): Promise<any[]> { public async $getCPFPUnindexedBlocks(): Promise<number[]> {
try { try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo(); const blockchainInfo = await bitcoinClient.getBlockchainInfo();
const currentBlockHeight = blockchainInfo.blocks; const currentBlockHeight = blockchainInfo.blocks;
@ -825,13 +825,13 @@ class BlocksRepository {
} }
const minHeight = Math.max(0, currentBlockHeight - indexingBlockAmount + 1); const minHeight = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
const [rows]: any[] = await DB.query(` const [rows] = await DB.query(`
SELECT height SELECT height
FROM compact_cpfp_clusters FROM compact_cpfp_clusters
WHERE height <= ? AND height >= ? WHERE height <= ? AND height >= ?
GROUP BY height GROUP BY height
ORDER BY height DESC; ORDER BY height DESC;
`, [currentBlockHeight, minHeight]); `, [currentBlockHeight, minHeight]) as RowDataPacket[][];
const indexedHeights = {}; const indexedHeights = {};
rows.forEach((row) => { indexedHeights[row.height] = true; }); rows.forEach((row) => { indexedHeights[row.height] = true; });

View File

@ -1,3 +1,4 @@
import { RowDataPacket } from 'mysql2';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { BlockSummary, TransactionClassified } from '../mempool.interfaces'; import { BlockSummary, TransactionClassified } from '../mempool.interfaces';
@ -69,7 +70,7 @@ class BlocksSummariesRepository {
public async $getIndexedSummariesId(): Promise<string[]> { public async $getIndexedSummariesId(): Promise<string[]> {
try { try {
const [rows]: any[] = await DB.query(`SELECT id from blocks_summaries`); const [rows] = await DB.query(`SELECT id from blocks_summaries`) as RowDataPacket[][];
return rows.map(row => row.id); return rows.map(row => row.id);
} catch (e) { } catch (e) {
logger.err(`Cannot get block summaries id list. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot get block summaries id list. Reason: ` + (e instanceof Error ? e.message : e));

View File

@ -139,7 +139,7 @@ class HashratesRepository {
public async $getPoolWeeklyHashrate(slug: string): Promise<any[]> { public async $getPoolWeeklyHashrate(slug: string): Promise<any[]> {
const pool = await PoolsRepository.$getPool(slug); const pool = await PoolsRepository.$getPool(slug);
if (!pool) { if (!pool) {
throw new Error('This mining pool does not exist ' + escape(slug)); throw new Error('This mining pool does not exist');
} }
// Find hashrate boundaries // Find hashrate boundaries

View File

@ -31,6 +31,9 @@ const curveP = BigInt(`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
* @returns {boolean} true if the point is on the SECP256K1 curve * @returns {boolean} true if the point is on the SECP256K1 curve
*/ */
export function isPoint(pointHex: string): boolean { export function isPoint(pointHex: string): boolean {
if (!pointHex?.length) {
return false;
}
if ( if (
!( !(
// is uncompressed // is uncompressed

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 December 20, 2023.
Signed: jamesblacklock

View File

@ -55,7 +55,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:=""}
__ESPLORA_BATCH_QUERY_BASE_SIZE__=${ESPLORA_BATCH_QUERY_BASE_SIZE:=1000} __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}

7
frontend/.gitignore vendored
View File

@ -6,6 +6,13 @@
/out-tsc /out-tsc
server.run.js server.run.js
# docker
Dockerfile
entrypoint.sh
nginx-mempool.conf
nginx.conf
wait-for
# Only exists if Bazel was run # Only exists if Bazel was run
/bazel-out /bazel-out

View File

@ -45,28 +45,30 @@ export class AcceleratorDashboardComponent implements OnInit {
this.pendingAccelerations$ = interval(30000).pipe( this.pendingAccelerations$ = interval(30000).pipe(
startWith(true), startWith(true),
switchMap(() => { switchMap(() => {
return this.apiService.getAccelerations$(); return this.apiService.getAccelerations$().pipe(
}), catchError(() => {
catchError((e) => { return of([]);
return of([]); }),
);
}), }),
share(), share(),
); );
this.accelerations$ = this.stateService.chainTip$.pipe( this.accelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(), distinctUntilChanged(),
switchMap((chainTip) => { switchMap(() => {
return this.apiService.getAccelerationHistory$({ timeframe: '1m' }); return this.apiService.getAccelerationHistory$({ timeframe: '1m' }).pipe(
}), catchError(() => {
catchError((e) => { return of([]);
return of([]); }),
);
}), }),
share(), share(),
); );
this.minedAccelerations$ = this.accelerations$.pipe( this.minedAccelerations$ = this.accelerations$.pipe(
map(accelerations => { map(accelerations => {
return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status)) return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status));
}) })
); );

View File

@ -540,7 +540,7 @@
</ng-container> </ng-container>
</ng-template> </ng-template>
</div> </div>
<button *ngIf="cpfpInfo.bestDescendant || cpfpInfo.descendants?.length || cpfpInfo.ancestors?.length" type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button> <button *ngIf="cpfpInfo?.bestDescendant || cpfpInfo?.descendants?.length || cpfpInfo?.ancestors?.length" type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
</td> </td>
</tr> </tr>
</tbody> </tbody>