diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 1452b6fc8..483fd3f3e 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -170,10 +170,7 @@ class Blocks { * Index all blocks metadata for the mining dashboard */ public async $generateBlockDatabase() { - if (this.blockIndexingStarted === true || - !Common.indexingEnabled() || - memPool.hasPriority() - ) { + if (this.blockIndexingStarted) { return; } diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index b44585580..154068164 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -6,7 +6,7 @@ import logger from '../logger'; const sleep = (ms: number) => new Promise(res => setTimeout(res, ms)); class DatabaseMigration { - private static currentVersion = 6; + private static currentVersion = 7; private queryTimeout = 120000; private statisticsAddedIndexed = false; @@ -116,6 +116,12 @@ class DatabaseMigration { await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""'); await this.$executeQuery(connection, 'ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL'); } + + if (databaseSchemaVersion < 7 && isBitcoin === true) { + await this.$executeQuery(connection, 'DROP table IF EXISTS hashrates;'); + await this.$executeQuery(connection, this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates')); + } + connection.release(); } catch (e) { connection.release(); @@ -398,6 +404,17 @@ class DatabaseMigration { FOREIGN KEY (pool_id) REFERENCES pools (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; } + + private getCreateDailyStatsTableQuery(): string { + return `CREATE TABLE IF NOT EXISTS hashrates ( + hashrate_timestamp timestamp NOT NULL, + avg_hashrate double unsigned DEFAULT '0', + pool_id smallint unsigned NULL, + PRIMARY KEY (hashrate_timestamp), + INDEX (pool_id), + FOREIGN KEY (pool_id) REFERENCES pools (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; + } } export default new DatabaseMigration(); diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index beca52893..fc13c2f5e 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -1,9 +1,13 @@ import { PoolInfo, PoolStats } from '../mempool.interfaces'; import BlocksRepository, { EmptyBlocks } from '../repositories/BlocksRepository'; import PoolsRepository from '../repositories/PoolsRepository'; +import HashratesRepository from '../repositories/HashratesRepository'; import bitcoinClient from './bitcoin/bitcoin-client'; +import logger from '../logger'; class Mining { + hashrateIndexingStarted = false; + constructor() { } @@ -45,7 +49,7 @@ class Mining { poolsStatistics['blockCount'] = blockCount; const blockHeightTip = await bitcoinClient.getBlockCount(); - const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(120, blockHeightTip); + const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(144, blockHeightTip); poolsStatistics['lastEstimatedHashrate'] = lastBlockHashrate; return poolsStatistics; @@ -82,6 +86,52 @@ class Mining { oldestIndexedBlockTimestamp: oldestBlock.getTime(), } } + + /** + * + */ + public async $generateNetworkHashrateHistory() : Promise { + if (this.hashrateIndexingStarted) { + return; + } + this.hashrateIndexingStarted = true; + + const totalIndexed = await BlocksRepository.$blockCount(null, null); + const indexedTimestamp = await HashratesRepository.$getAllTimestamp(); + + const genesisTimestamp = 1231006505; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f + const lastMidnight = new Date(); + lastMidnight.setUTCHours(0); lastMidnight.setUTCMinutes(0); lastMidnight.setUTCSeconds(0); lastMidnight.setUTCMilliseconds(0); + let toTimestamp = Math.round(lastMidnight.getTime() / 1000); + + while (toTimestamp > genesisTimestamp) { + const fromTimestamp = toTimestamp - 86400; + if (indexedTimestamp.includes(fromTimestamp)) { + toTimestamp -= 86400; + continue; + } + + const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp( + null, fromTimestamp, toTimestamp + ); + let lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, blockStats.lastBlockHeight); + + if (toTimestamp % 864000 === 0) { + const progress = Math.round((totalIndexed - blockStats.lastBlockHeight) / totalIndexed * 100); + const formattedDate = new Date(fromTimestamp * 1000).toUTCString(); + logger.debug(`Counting blocks and hashrate for ${formattedDate}. Progress: ${progress}%`); + } + + await HashratesRepository.$saveDailyStat({ + hashrateTimestamp: fromTimestamp, + avgHashrate: lastBlockHashrate, + poolId: null, + }); + + toTimestamp -= 86400; + } + } + } export default new Mining(); diff --git a/backend/src/index.ts b/backend/src/index.ts index 23c70f59d..d09196a45 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -26,6 +26,7 @@ import poolsParser from './api/pools-parser'; import syncAssets from './sync-assets'; import icons from './api/liquid/icons'; import { Common } from './api/common'; +import mining from './api/mining'; class Server { private wss: WebSocket.Server | undefined; @@ -138,7 +139,7 @@ class Server { } await blocks.$updateBlocks(); await memPool.$updateMempool(); - blocks.$generateBlockDatabase(); + this.runIndexingWhenReady(); setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS); this.currentBackendRetryInterval = 5; @@ -157,6 +158,14 @@ class Server { } } + async runIndexingWhenReady() { + if (!Common.indexingEnabled() || mempool.hasPriority()) { + return; + } + await blocks.$generateBlockDatabase(); + await mining.$generateNetworkHashrateHistory(); + } + setUpWebsocketHandling() { if (this.wss) { websocketHandler.setWebsocketServer(this.wss); diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index ac0ea25bc..937320a3a 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -149,6 +149,40 @@ class BlocksRepository { return rows[0].blockCount; } + /** + * Get blocks count between two dates + * @param poolId + * @param from - The oldest timestamp + * @param to - The newest timestamp + * @returns + */ + public async $blockCountBetweenTimestamp(poolId: number | null, from: number, to: number): Promise { + const params: any[] = []; + let query = `SELECT + count(height) as blockCount, + max(height) as lastBlockHeight + FROM blocks`; + + if (poolId) { + query += ` WHERE pool_id = ?`; + params.push(poolId); + } + + if (poolId) { + query += ` AND`; + } else { + query += ` WHERE`; + } + query += ` UNIX_TIMESTAMP(blockTimestamp) BETWEEN '${from}' AND '${to}'`; + + // logger.debug(query); + const connection = await DB.pool.getConnection(); + const [rows] = await connection.query(query, params); + connection.release(); + + return rows[0]; + } + /** * Get the oldest indexed block */ diff --git a/backend/src/repositories/HashratesRepository.ts b/backend/src/repositories/HashratesRepository.ts new file mode 100644 index 000000000..f55700812 --- /dev/null +++ b/backend/src/repositories/HashratesRepository.ts @@ -0,0 +1,42 @@ +import { DB } from '../database'; +import logger from '../logger'; + +class HashratesRepository { + /** + * Save indexed block data in the database + */ + public async $saveDailyStat(dailyStat: any) { + const connection = await DB.pool.getConnection(); + + try { + const query = `INSERT INTO + hashrates(hashrate_timestamp, avg_hashrate, pool_id) + VALUE (FROM_UNIXTIME(?), ?, ?)`; + + const params: any[] = [ + dailyStat.hashrateTimestamp, dailyStat.avgHashrate, + dailyStat.poolId + ]; + + // logger.debug(query); + await connection.query(query, params); + } catch (e: any) { + logger.err('$saveHashrateInDatabase() error' + (e instanceof Error ? e.message : e)); + } + + connection.release(); + } + + /** + * Returns an array of all timestamp we've already indexed + */ + public async $getAllTimestamp(): Promise { + const connection = await DB.pool.getConnection(); + const [rows]: any[] = await connection.query(`SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp from hashrates`); + connection.release(); + + return rows.map(val => val.timestamp); + } +} + +export default new HashratesRepository(); diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 4a9cb1f8f..df5e7cb62 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -22,7 +22,6 @@ import elementsParser from './api/liquid/elements-parser'; import icons from './api/liquid/icons'; import miningStats from './api/mining'; import axios from 'axios'; -import PoolsRepository from './repositories/PoolsRepository'; import mining from './api/mining'; import BlocksRepository from './repositories/BlocksRepository';