UNPKG

@cityofzion/neo-js

Version:

Running NEO blockchain full node with Node.js and MongoDB.

343 lines 15.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = require("events"); const async_1 = require("async"); const node_log_it_1 = require("node-log-it"); const lodash_1 = require("lodash"); const block_helper_1 = require("../helpers/block-helper"); const MODULE_NAME = 'BlockAnalyzer'; const DEFAULT_OPTIONS = { minHeight: 1, maxHeight: undefined, startOnInit: true, toEvaluateTransactions: true, toEvaluateAssets: false, blockQueueConcurrency: 5, transactionQueueConcurrency: 10, enqueueEvaluateBlockIntervalMs: 5 * 1000, verifyBlocksIntervalMs: 30 * 1000, maxBlockQueueLength: 30 * 1000, maxTransactionQueueLength: 100 * 1000, standardEvaluateBlockPriority: 5, missingEvaluateBlockPriority: 3, legacyEvaluateBlockPriority: 3, standardEvaluateTransactionPriority: 5, missingEvaluateTransactionPriority: 5, legacyEvaluateTransactionPriority: 5, loggerOptions: {}, }; class BlockAnalyzer extends events_1.EventEmitter { constructor(storage, options = {}) { super(); this.BLOCK_META_API_LEVEL = 1; this.TRANSACTION_META_API_LEVEL = 1; this._isRunning = false; this.blockWritePointer = 0; this.isVerifyingBlocks = false; this.storage = storage; this.options = lodash_1.merge({}, DEFAULT_OPTIONS, options); this.validateOptionalParameters(); this.logger = new node_log_it_1.Logger(MODULE_NAME, this.options.loggerOptions); this.blockQueue = this.getPriorityQueue(this.options.blockQueueConcurrency); this.transactionQueue = this.getPriorityQueue(this.options.transactionQueueConcurrency); if (this.options.startOnInit) { this.start(); } this.logger.debug('constructor completes.'); } isRunning() { return this._isRunning; } start() { if (this._isRunning) { this.logger.info('BlockAnalyzer has already started.'); return; } if (!this.storage) { this.logger.info('Unable to start BlockAnalyzer when no storage are defined.'); return; } this.logger.info('Start BlockAnalyzer.'); this._isRunning = true; this.emit('start'); this.initEvaluateBlock(); this.initBlockVerification(); } stop() { if (!this._isRunning) { this.logger.info('BlockAnalyzer is not running at the moment.'); return; } this.logger.info('Stop BlockAnalyzer.'); this._isRunning = false; this.emit('stop'); clearInterval(this.enqueueEvaluateBlockIntervalId); clearInterval(this.blockVerificationIntervalId); } close() { this.stop(); } validateOptionalParameters() { } getPriorityQueue(concurrency) { return async_1.priorityQueue((task, callback) => { const method = task.method; const attrs = task.attrs; const meta = task.meta; this.logger.debug('New worker for queue. meta:', meta, 'attrs:', attrs); method(attrs) .then(() => { callback(); this.logger.debug('Worker queued method completed.'); this.emit('queue:worker:complete', { isSuccess: true, task }); }) .catch((err) => { this.logger.info('Worker queued method failed, but to continue... meta:', meta, 'Message:', err.message); callback(); this.emit('queue:worker:complete', { isSuccess: false, task }); }); }, concurrency); } initEvaluateBlock() { this.logger.debug('initEvaluateBlock triggered.'); this.setBlockWritePointer() .then(() => { this.enqueueEvaluateBlockIntervalId = setInterval(() => { this.doEnqueueEvaluateBlock(); }, this.options.enqueueEvaluateBlockIntervalMs); }) .catch((err) => { this.logger.warn('setBlockWritePointer() failed. Error:', err.message); }); } setBlockWritePointer() { return __awaiter(this, void 0, void 0, function* () { this.logger.debug('setBlockWritePointer triggered.'); try { const height = yield this.storage.getHighestBlockMetaHeight(); this.logger.debug('getBlockMetaCount success. height:', height); if (this.options.minHeight && height < this.options.minHeight) { this.logger.info(`storage height is smaller than designated minHeight. BlockWritePointer will be set to minHeight [${this.options.minHeight}] instead.`); this.blockWritePointer = this.options.minHeight; } else { this.blockWritePointer = height; } } catch (err) { this.logger.warn('storage.getBlockMetaCount() failed. Error:', err.message); this.logger.info('Assumed that there are no blocks.'); this.blockWritePointer = this.options.minHeight; } }); } initBlockVerification() { this.logger.debug('initBlockVerification triggered.'); this.blockVerificationIntervalId = setInterval(() => { this.doBlockVerification(); }, this.options.verifyBlocksIntervalMs); } doBlockVerification() { return __awaiter(this, void 0, void 0, function* () { this.logger.debug('doBlockVerification triggered.'); this.emit('blockVerification:init'); this.logger.info('blockQueue.length:', this.blockQueue.length()); this.logger.info('transactionQueue.length:', this.transactionQueue.length()); if (this.isVerifyingBlocks) { this.logger.info('doBlockVerification() is already running. Skip this turn.'); this.emit('blockVerification:complete', { isSkipped: true }); return; } this.isVerifyingBlocks = true; const startHeight = this.options.minHeight; const endHeight = this.options.maxHeight && this.blockWritePointer > this.options.maxHeight ? this.options.maxHeight : this.blockWritePointer; let blockMetasFullySynced = false; let transactionMetasFullySynced = false; try { blockMetasFullySynced = yield this.verifyBlockMetas(startHeight, endHeight); transactionMetasFullySynced = yield this.verifyTransactionMetas(startHeight, endHeight); } catch (err) { this.logger.info('Block verification failed. Message:', err.message); this.isVerifyingBlocks = false; this.emit('blockVerification:complete', { isSuccess: false }); return; } if (this.isReachedMaxHeight()) { if (blockMetasFullySynced && transactionMetasFullySynced) { this.logger.info('BlockAnalyzer is up to date.'); this.emit('upToDate'); } } this.isVerifyingBlocks = false; this.emit('blockVerification:complete', { isSuccess: true }); }); } verifyBlockMetas(startHeight, endHeight) { return __awaiter(this, void 0, void 0, function* () { this.logger.debug('verifyBlockMetas triggered.'); const blockMetaReport = yield this.storage.analyzeBlockMetas(startHeight, endHeight); this.logger.debug('Analyzing block metas complete!'); const all = this.getNumberArray(startHeight, endHeight); const availableBlocks = lodash_1.map(blockMetaReport, (item) => item.height); this.logger.info('Block metas available count:', availableBlocks.length); const missingBlocks = lodash_1.difference(all, availableBlocks); this.logger.info('Block metas missing count:', missingBlocks.length); this.emit('blockVerification:blockMetas:missing', { count: missingBlocks.length }); missingBlocks.forEach((height) => { this.enqueueEvaluateBlock(height, this.options.missingEvaluateBlockPriority); }); const legacyBlockObjs = lodash_1.filter(blockMetaReport, (item) => { return item.apiLevel < this.BLOCK_META_API_LEVEL; }); const legacyBlocks = lodash_1.map(legacyBlockObjs, (item) => item.height); this.logger.info('Legacy block metas count:', legacyBlockObjs.length); this.emit('blockVerification:blockMetas:legacy', { count: legacyBlocks.length }); legacyBlocks.forEach((height) => { this.storage.removeBlockMetaByHeight(height); this.enqueueEvaluateBlock(height, this.options.legacyEvaluateBlockPriority); }); const fullySynced = missingBlocks.length === 0 && legacyBlocks.length === 0; return fullySynced; }); } verifyTransactionMetas(startHeight, endHeight) { return __awaiter(this, void 0, void 0, function* () { this.logger.debug('verifyTransactionMetas triggered.'); const legacyCount = yield this.storage.countLegacyTransactionMeta(this.TRANSACTION_META_API_LEVEL); this.emit('blockVerification:transactionMetas:legacy', { metaCount: legacyCount }); if (legacyCount === 0) { return true; } yield this.storage.pruneLegacyTransactionMeta(this.TRANSACTION_META_API_LEVEL); return false; }); } doEnqueueEvaluateBlock() { this.logger.debug('doEnqueueEvaluateBlock triggered.'); if (this.isReachedMaxHeight()) { this.logger.info(`BlockWritePointer is greater or equal to designated maxHeight [${this.options.maxHeight}]. There will be no enqueue block beyond this point.`); return; } while (!this.isReachedMaxHeight() && !this.isReachedMaxQueueLength()) { this.increaseBlockWritePointer(); this.enqueueEvaluateBlock(this.blockWritePointer, this.options.standardEvaluateBlockPriority); } } isReachedMaxHeight() { return !!(this.options.maxHeight && this.blockWritePointer >= this.options.maxHeight); } isReachedMaxQueueLength() { return this.blockQueue.length() >= this.options.maxBlockQueueLength; } increaseBlockWritePointer() { this.logger.debug('increaseBlockWritePointer triggered.'); this.blockWritePointer += 1; } enqueueEvaluateBlock(height, priority) { this.logger.debug('enqueueEvaluateBlock triggered. height:', height, 'priority:', priority); if (height > this.blockWritePointer) { this.logger.debug('height > this.blockWritePointer, blockWritePointer is now:', height); this.blockWritePointer = height; } this.blockQueue.push({ method: this.evaluateBlock.bind(this), attrs: { height, }, meta: { methodName: 'evaluateBlock', }, }, priority); } evaluateBlock(attrs) { return __awaiter(this, void 0, void 0, function* () { this.logger.debug('evaluateBlock triggered. attrs:', attrs); const height = attrs.height; let previousBlock; if (height > 1) { previousBlock = yield this.storage.getBlock(height - 1); } const block = yield this.storage.getBlock(height); const blockMeta = { height, time: block.time, size: block.size, generationTime: block_helper_1.BlockHelper.getGenerationTime(block, previousBlock), transactionCount: block_helper_1.BlockHelper.getTransactionCount(block), apiLevel: this.BLOCK_META_API_LEVEL, }; if (this.options.toEvaluateTransactions) { this.enqueueEvaluateTransaction(block, this.options.standardEvaluateTransactionPriority); } yield this.storage.setBlockMeta(blockMeta); }); } enqueueEvaluateTransaction(block, priority) { this.logger.debug('enqueueEvaluateTransaction triggered.'); if (!block || !block.tx) { this.logger.info('Invalid block object. Skipping...'); return; } block.tx.forEach((transaction) => { this.transactionQueue.push({ method: this.evaluateTransaction.bind(this), attrs: { height: block.index, time: block.time, transaction, }, meta: { methodName: 'evaluateTransaction', }, }, priority); }); } enqueueEvaluateTransactionWithHeight(height, priority) { return __awaiter(this, void 0, void 0, function* () { this.logger.debug('enqueueEvaluateTransactionWithHeight triggered.'); const block = yield this.storage.getBlock(height); this.enqueueEvaluateTransaction(block, priority); }); } evaluateTransaction(attrs) { return __awaiter(this, void 0, void 0, function* () { this.logger.debug('evaluateTransaction triggered.'); const height = attrs.height; const time = attrs.time; const tx = attrs.transaction; const voutCount = lodash_1.isArray(tx.vout) ? tx.vout.length : undefined; const vinCount = lodash_1.isArray(tx.vin) ? tx.vin.length : undefined; const transactionMeta = { height, time, transactionId: tx.txid, type: tx.type, size: tx.size, networkFee: tx.net_fee, systemFee: tx.sys_fee, voutCount, vinCount, apiLevel: this.TRANSACTION_META_API_LEVEL, }; yield this.storage.setTransactionMeta(transactionMeta); }); } getNumberArray(start, end) { const all = []; for (let i = start; i <= end; i++) { all.push(i); } return all; } } exports.BlockAnalyzer = BlockAnalyzer; //# sourceMappingURL=block-analyzer.js.map