import DB from '../database'; import logger from '../logger'; import config from '../config'; import PoolsRepository from '../repositories/PoolsRepository'; import { PoolTag } from '../mempool.interfaces'; import diskCache from './disk-cache'; import mining from './mining/mining'; import transactionUtils from './transaction-utils'; import BlocksRepository from '../repositories/BlocksRepository'; class PoolsParser { miningPools: any[] = []; unknownPool: any = { 'id': 0, 'name': 'Unknown', 'link': 'https://learnmeabitcoin.com/technical/coinbase-transaction', 'regexes': '[]', 'addresses': '[]', 'slug': 'unknown' }; private uniqueLogs: string[] = []; private uniqueLog(loggerFunction: any, msg: string): void { if (this.uniqueLogs.includes(msg)) { return; } this.uniqueLogs.push(msg); loggerFunction(msg); } public setMiningPools(pools): void { for (const pool of pools) { pool.regexes = pool.tags; pool.slug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase(); delete(pool.tags); } this.miningPools = pools; } /** * Populate our db with updated mining pool definition * @param pools */ public async migratePoolsJson(): Promise { // We also need to wipe the backend cache to make sure we don't serve blocks with // the wrong mining pool (usually happen with unknown blocks) diskCache.setIgnoreBlocksCache(); await this.$insertUnknownPool(); let reindexUnknown = false; for (const pool of this.miningPools) { if (!pool.id) { logger.info(`Mining pool ${pool.name} has no unique 'id' defined. Skipping.`); continue; } // One of the two fields 'addresses' or 'regexes' must be a non-empty array if (!pool.addresses && !pool.regexes) { logger.err(`Mining pool ${pool.name} must have at least one of the fields 'addresses' or 'regexes'. Skipping.`); continue; } pool.addresses = pool.addresses || []; pool.regexes = pool.regexes || []; if (pool.addresses.length === 0 && pool.regexes.length === 0) { logger.err(`Mining pool ${pool.name} has no 'addresses' nor 'regexes' defined. Skipping.`); continue; } if (pool.addresses.length === 0) { logger.warn(`Mining pool ${pool.name} has no 'addresses' defined.`); } if (pool.regexes.length === 0) { logger.warn(`Mining pool ${pool.name} has no 'regexes' defined.`); } const poolDB = await PoolsRepository.$getPoolByUniqueId(pool.id, false); if (!poolDB) { // New mining pool const slug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase(); logger.debug(`Inserting new mining pool ${pool.name}`); await PoolsRepository.$insertNewMiningPool(pool, slug); reindexUnknown = true; } else { if (poolDB.name !== pool.name) { // Pool has been renamed const newSlug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase(); logger.warn(`Renaming ${poolDB.name} mining pool to ${pool.name}. Slug has been updated. Maybe you want to make a redirection from 'https://mempool.space/mining/pool/${poolDB.slug}' to 'https://mempool.space/mining/pool/${newSlug}`); await PoolsRepository.$renameMiningPool(poolDB.id, newSlug, pool.name); } if (poolDB.link !== pool.link) { // Pool link has changed logger.debug(`Updating link for ${pool.name} mining pool`); await PoolsRepository.$updateMiningPoolLink(poolDB.id, pool.link); } if (JSON.stringify(pool.addresses) !== poolDB.addresses || JSON.stringify(pool.regexes) !== poolDB.regexes) { // Pool addresses changed or coinbase tags changed logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool.`); await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes); reindexUnknown = true; await this.$reindexBlocksForPool(poolDB.id); } } } if (reindexUnknown) { logger.notice(`Updating addresses and/or coinbase tags for unknown mining pool.`); let unknownPool; if (config.DATABASE.ENABLED === true) { unknownPool = await PoolsRepository.$getUnknownPool(); } else { unknownPool = this.unknownPool; } await this.$reindexBlocksForPool(unknownPool.id); } } public matchBlockMiner(scriptsig: string, addresses: string[], pools: PoolTag[]): PoolTag | undefined { const asciiScriptSig = transactionUtils.hex2ascii(scriptsig); for (let i = 0; i < pools.length; ++i) { if (addresses.length) { const poolAddresses: string[] = typeof pools[i].addresses === 'string' ? JSON.parse(pools[i].addresses) : pools[i].addresses; for (let y = 0; y < poolAddresses.length; y++) { if (addresses.indexOf(poolAddresses[y]) !== -1) { return pools[i]; } } } const regexes: string[] = typeof pools[i].regexes === 'string' ? JSON.parse(pools[i].regexes) : pools[i].regexes; for (let y = 0; y < regexes.length; ++y) { const regex = new RegExp(regexes[y], 'i'); const match = asciiScriptSig.match(regex); if (match !== null) { return pools[i]; } } } } /** * Manually add the 'unknown pool' */ public async $insertUnknownPool(): Promise { if (!config.DATABASE.ENABLED) { return; } try { const [rows]: any[] = await DB.query({ sql: 'SELECT name from pools where name="Unknown"', timeout: 120000 }); if (rows.length === 0) { await DB.query({ sql: `INSERT INTO pools(name, link, regexes, addresses, slug, unique_id) VALUES("${this.unknownPool.name}", "${this.unknownPool.link}", "[]", "[]", "${this.unknownPool.slug}", 0); `}); } else { await DB.query(`UPDATE pools SET name='${this.unknownPool.name}', link='${this.unknownPool.link}', regexes='[]', addresses='[]', slug='${this.unknownPool.slug}', unique_id=0 WHERE slug='${this.unknownPool.slug}' `); } } catch (e) { logger.err(`Unable to insert or update "Unknown" mining pool. Reason: ${e instanceof Error ? e.message : e}`); } } /** * re-index pool assignment for blocks previously associated with pool * * @param pool local id of existing pool to reindex */ private async $reindexBlocksForPool(poolId: number): Promise { let firstKnownBlockPool = 130635; // https://mempool.space/block/0000000000000a067d94ff753eec72830f1205ad3a4c216a08a80c832e551a52 if (config.MEMPOOL.NETWORK === 'testnet') { firstKnownBlockPool = 21106; // https://mempool.space/testnet/block/0000000070b701a5b6a1b965f6a38e0472e70b2bb31b973e4638dec400877581 } else if (config.MEMPOOL.NETWORK === 'signet') { firstKnownBlockPool = 0; } const [blocks]: any[] = await DB.query(` SELECT height, hash, coinbase_raw, coinbase_addresses FROM blocks WHERE pool_id = ? AND height >= ? ORDER BY height DESC `, [poolId, firstKnownBlockPool]); let pools: PoolTag[] = []; if (config.DATABASE.ENABLED === true) { pools = await PoolsRepository.$getPools(); } else { pools = this.miningPools; } let changed = 0; for (const block of blocks) { const addresses = JSON.parse(block.coinbase_addresses) || []; const newPool = this.matchBlockMiner(block.coinbase_raw, addresses, pools); if (newPool && newPool.id !== poolId) { changed++; await BlocksRepository.$savePool(block.hash, newPool.id); } } logger.info(`${changed} blocks assigned to a new pool`, logger.tags.mining); // Re-index hashrates and difficulty adjustments later mining.reindexHashrateRequested = true; } } export default new PoolsParser();