UNPKG

bitcore-node

Version:

A blockchain indexing node with extended capabilities using bitcore

364 lines 15.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EVMP2pWorker = void 0; const events_1 = require("events"); const os = __importStar(require("os")); const __1 = require("../../"); const logger_1 = require("../../../../logger"); const logger_2 = __importDefault(require("../../../../logger")); const state_1 = require("../../../../models/state"); const p2p_1 = require("../../../../services/p2p"); const utils_1 = require("../../../../utils"); const csp_1 = require("../api/csp"); const block_1 = require("../models/block"); const transaction_1 = require("../models/transaction"); const rpcs_1 = require("./rpcs"); const sync_1 = require("./sync"); class EVMP2pWorker extends p2p_1.BaseP2PWorker { constructor({ chain, network, chainConfig, blockModel = block_1.EVMBlockStorage, txModel = transaction_1.EVMTransactionStorage }) { super({ chain, network, chainConfig, blockModel }); this.chain = chain || 'ETH'; this.network = network; this.chainConfig = chainConfig; this.syncing = false; this.initialSyncComplete = false; this.blockModel = blockModel; this.txModel = txModel; this.provider = new csp_1.BaseEVMStateProvider(this.chain); this.events = new events_1.EventEmitter(); this.invCache = {}; this.invCacheLimits = { TX: 100000 }; this.disconnecting = false; this.multiThreadSync = new sync_1.MultiThreadSync({ chain, network }); } cacheInv(type, hash) { if (!this.invCache[type]) { this.invCache[type] = []; } if (this.invCache[type].length > this.invCacheLimits[type]) { this.invCache[type].shift(); } this.invCache[type].push(hash); } isCachedInv(type, hash) { if (!this.invCache[type]) { this.invCache[type] = []; } return this.invCache[type].includes(hash); } async setupListeners() { const { host, port } = this.chainConfig.provider || this.chainConfig.providers[0]; this.events.on('disconnected', async () => { logger_2.default.warn(`${(0, logger_1.timestamp)()} | Not connected to peer: ${host}:${port} | Chain: ${this.chain} | Network: ${this.network}`); }); this.events.on('connected', async () => { this.txSubscription = await this.web3.eth.subscribe('pendingTransactions'); this.txSubscription.subscribe(async (_err, txid) => { if (!this.isCachedInv('TX', txid)) { this.cacheInv('TX', txid); const tx = (await this.web3.eth.getTransaction(txid)); if (tx) { await this.processTransaction(tx); this.events.emit('transaction', tx); } } }); this.blockSubscription = await this.web3.eth.subscribe('newBlockHeaders'); this.blockSubscription.subscribe((_err, block) => { this.events.emit('block', block); if (!this.syncing) { this.sync(); } }); }); this.multiThreadSync.once('INITIALSYNCDONE', () => { this.initialSyncComplete = true; this.events.emit('SYNCDONE'); }); } async disconnect() { this.disconnecting = true; try { if (this.txSubscription) { this.txSubscription.unsubscribe(); } if (this.blockSubscription) { this.blockSubscription.unsubscribe(); } } catch (e) { console.error(e); } } async getWeb3() { return this.provider.getWeb3(this.network); } async getClient() { try { const nodeVersion = await this.web3.eth.getNodeInfo(); const client = nodeVersion.split('/')[0].toLowerCase(); if (client !== 'erigon' && client !== 'geth') { // assume it's a geth fork, or at least more like geth. // this is helpful when using a dev solution like ganache. return 'geth'; } return client; } catch (e) { console.error(e); return 'geth'; } } async handleReconnects() { this.disconnecting = false; let firstConnect = true; let connected = false; let disconnected = false; const { host, port } = this.chainConfig.provider || this.chainConfig.providers[0]; while (!this.disconnecting && !this.stopping) { try { if (!this.web3) { const { web3 } = await this.getWeb3(); this.web3 = web3; } try { if (!this.client || !this.rpc) { this.client = await this.getClient(); this.rpc = new rpcs_1.Rpcs[this.client](this.web3); } connected = await this.web3.eth.net.isListening(); } catch (e) { connected = false; } if (!connected) { this.web3 = undefined; this.client = undefined; this.events.emit('disconnected'); } else if (disconnected || firstConnect) { this.events.emit('connected'); } if (disconnected && connected && !firstConnect) { logger_2.default.warn(`${(0, logger_1.timestamp)()} | Reconnected to peer: ${host}:${port} | Chain: ${this.chain} | Network: ${this.network}`); } if (connected && firstConnect) { firstConnect = false; logger_2.default.info(`${(0, logger_1.timestamp)()} | Connected to peer: ${host}:${port} | Chain: ${this.chain} | Network: ${this.network}`); } disconnected = !connected; } catch (e) { } await (0, utils_1.wait)(2000); } } async connect() { this.handleReconnects(); return new Promise(resolve => this.events.once('connected', resolve)); } async getBlock(height) { return this.rpc.getBlock(height); } async processBlock(block, transactions) { await this.blockModel.addBlock({ chain: this.chain, network: this.network, forkHeight: this.chainConfig.forkHeight, parentChain: this.chainConfig.parentChain, initialSyncComplete: this.initialSyncComplete, block, transactions }); if (!this.syncing) { logger_2.default.info(`Added block ${block.hash}`, { chain: this.chain, network: this.network }); } } async processTransaction(tx) { const now = new Date(); const convertedTx = this.txModel.convertRawTx(this.chain, this.network, tx); this.txModel.batchImport({ chain: this.chain, network: this.network, txs: [convertedTx], height: -1, mempoolTime: now, blockTime: now, blockTimeNormalized: now, initialSyncComplete: true }); } useMultiThread() { if (this.chainConfig.threads == null) { // use multithread by default if there are >2 threads in the CPU return os.cpus().length > 2; } return this.chainConfig.threads > 0; } async sync() { if (this.syncing) { return false; } if (!this.initialSyncComplete && this.useMultiThread()) { return this.multiThreadSync.sync(); } const { chain, chainConfig, network } = this; const { parentChain, forkHeight = 0 } = chainConfig; this.syncing = true; const state = await state_1.StateStorage.collection.findOne({}); this.initialSyncComplete = state && state.initialSyncComplete && state.initialSyncComplete.includes(`${chain}:${network}`); let tip = await __1.ChainStateProvider.getLocalTip({ chain, network }); if (parentChain && (!tip || tip.height < forkHeight)) { let parentTip = await __1.ChainStateProvider.getLocalTip({ chain: parentChain, network }); while (!parentTip || parentTip.height < forkHeight) { logger_2.default.info(`Waiting until ${parentChain} syncs before ${chain} ${network}`); await new Promise(resolve => { setTimeout(resolve, 5000); }); parentTip = await __1.ChainStateProvider.getLocalTip({ chain: parentChain, network }); } } const startHeight = tip ? tip.height : chainConfig.syncStartHeight || 0; const startTime = Date.now(); try { let bestBlock = await this.web3.eth.getBlockNumber(); let lastLog = 0; let currentHeight = tip ? tip.height : chainConfig.syncStartHeight || 0; logger_2.default.info(`Syncing ${bestBlock - currentHeight} blocks for ${chain} ${network}`); while (currentHeight <= bestBlock) { const block = await this.getBlock(currentHeight); if (!block) { await (0, utils_1.wait)(1000); continue; } const { convertedBlock, convertedTxs } = await this.convertBlock(block); await this.processBlock(convertedBlock, convertedTxs); if (currentHeight === bestBlock) { bestBlock = await this.web3.eth.getBlockNumber(); } tip = await __1.ChainStateProvider.getLocalTip({ chain, network }); currentHeight = tip ? tip.height + 1 : 0; const oneSecond = 1000; const now = Date.now(); if (now - lastLog > oneSecond) { const blocksProcessed = currentHeight - startHeight; const elapsedMinutes = (now - startTime) / (60 * oneSecond); logger_2.default.info(`${(0, logger_1.timestamp)()} | Syncing... | Chain: ${chain} | Network: ${network} |${(blocksProcessed / elapsedMinutes) .toFixed(2) .padStart(8)} blocks/min | Height: ${currentHeight.toString().padStart(7)}`); lastLog = Date.now(); } } } catch (err) { logger_2.default.error(`Error syncing ${chain} ${network} -- %o`, err); await (0, utils_1.wait)(2000); this.syncing = false; return this.sync(); } logger_2.default.info(`${chain}:${network} up to date.`); this.syncing = false; state_1.StateStorage.collection.findOneAndUpdate({}, { $addToSet: { initialSyncComplete: `${chain}:${network}` } }, { upsert: true }); this.events.emit('SYNCDONE'); return true; } async syncDone() { return new Promise(resolve => this.events.once('SYNCDONE', resolve)); } getBlockReward(block) { // TODO: implement block reward block; return 0; } async convertBlock(block) { const blockTime = Number(block.timestamp) * 1000; const hash = block.hash; const height = block.number; const reward = this.getBlockReward(block); const convertedBlock = { chain: this.chain, network: this.network, height, hash, coinbase: Buffer.from(block.miner), merkleRoot: Buffer.from(block.transactionsRoot), time: new Date(blockTime), timeNormalized: new Date(blockTime), nonce: Buffer.from(block.extraData), previousBlockHash: block.parentHash, difficulty: block.difficulty, totalDifficulty: block.totalDifficulty, nextBlockHash: '', transactionCount: block.transactions.length, size: block.size, reward, logsBloom: Buffer.from(block.logsBloom), sha3Uncles: Buffer.from(block.sha3Uncles), receiptsRoot: Buffer.from(block.receiptsRoot), processed: false, gasLimit: block.gasLimit, gasUsed: block.gasUsed, stateRoot: Buffer.from(block.stateRoot) }; const convertedTxs = block.transactions.map(t => this.txModel.convertRawTx(this.chain, this.network, t, convertedBlock)); const traceTxs = await this.rpc.getTransactionsFromBlock(convertedBlock.height); this.rpc.reconcileTraces(convertedBlock, convertedTxs, traceTxs); this.txModel.addEffectsToTxs(convertedTxs); return { convertedBlock, convertedTxs }; } async stop() { this.stopping = true; this.multiThreadSync.stop(); logger_2.default.debug(`Stopping worker for chain ${this.chain} ${this.network}`); await this.disconnect(); } async start() { logger_2.default.debug(`Started worker for chain ${this.chain} ${this.network}`); this.setupListeners(); await this.connect(); this.sync(); } } exports.EVMP2pWorker = EVMP2pWorker; //# sourceMappingURL=p2p.js.map