UNPKG

@neo-one/node-blockchain-esnext-esm

Version:

NEO•ONE NEO blockchain implementation.

641 lines (639 loc) 26.3 kB
import { common, crypto, ScriptBuilder, VMState } from '@neo-one/client-common-esnext-esm'; import { AggregationType, globalStats, MeasureUnit } from '@neo-one/client-switch-esnext-esm'; import { createChild, nodeLogger } from '@neo-one/logger-esnext-esm'; import { InvocationTransaction, LogAction, NotificationAction, NULL_ACTION, ScriptContainerType, TriggerType, utils, } from '@neo-one/node-core-esnext-esm'; import { Labels, utils as commonUtils } from '@neo-one/utils-esnext-esm'; import BN from 'bn.js'; import PriorityQueue from 'js-priority-queue'; import { BehaviorSubject, Subject } from 'rxjs'; import { toArray } from 'rxjs/operators'; import { CoinClaimedError, CoinUnspentError, GenesisBlockNotRegisteredError, InvalidClaimError, UnknownVerifyError, WitnessVerifyError, } from './errors'; import { getValidators } from './getValidators'; import { wrapExecuteScripts } from './wrapExecuteScripts'; import { WriteBatchBlockchain } from './WriteBatchBlockchain'; const logger = createChild(nodeLogger, { component: 'blockchain' }); const blockFailures = globalStats.createMeasureInt64('persist/failures', MeasureUnit.UNIT); const blockCurrent = globalStats.createMeasureInt64('persist/current', MeasureUnit.UNIT); const blockProgress = globalStats.createMeasureInt64('persist/progress', MeasureUnit.UNIT); const blockDurationMs = globalStats.createMeasureDouble('persist/duration', MeasureUnit.MS, 'time to persist block in milliseconds'); const blockLatencySec = globalStats.createMeasureDouble('persist/latency', MeasureUnit.SEC, "'The latency from block timestamp to persist'"); const NEO_BLOCKCHAIN_PERSIST_BLOCK_DURATION_MS = globalStats.createView('neo_blockchain_persist_block_duration_ms', blockDurationMs, AggregationType.DISTRIBUTION, [], 'distribution of the persist duration', [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000]); globalStats.registerView(NEO_BLOCKCHAIN_PERSIST_BLOCK_DURATION_MS); const NEO_BLOCKCHAIN_PERSIST_BLOCK_FAILURES_TOTAL = globalStats.createView('neo_blockchain_persist_block_failures_total', blockFailures, AggregationType.COUNT, [], 'total blockchain failures'); globalStats.registerView(NEO_BLOCKCHAIN_PERSIST_BLOCK_FAILURES_TOTAL); const NEO_BLOCKCHAIN_BLOCK_INDEX_GAUGE = globalStats.createView('neo_blockchain_block_index', blockCurrent, AggregationType.LAST_VALUE, [], 'the current block index'); globalStats.registerView(NEO_BLOCKCHAIN_BLOCK_INDEX_GAUGE); const NEO_BLOCKCHAIN_PERSISTING_BLOCK_INDEX_GAUGE = globalStats.createView('neo_blockchain_persisting_block_index', blockProgress, AggregationType.LAST_VALUE, [], 'The current in progress persist index'); globalStats.registerView(NEO_BLOCKCHAIN_PERSISTING_BLOCK_INDEX_GAUGE); const NEO_BLOCKCHAIN_PERSIST_BLOCK_LATENCY_SECONDS = globalStats.createView('neo_blockchain_persist_block_latency_seconds', blockLatencySec, AggregationType.DISTRIBUTION, [], 'The latency from block timestamp to persist', [1, 2, 5, 7.5, 10, 12.5, 15, 17.5, 20]); globalStats.registerView(NEO_BLOCKCHAIN_PERSIST_BLOCK_LATENCY_SECONDS); export class Blockchain { constructor(options) { this.mutablePersistingBlocks = false; this.mutableBlockQueue = new PriorityQueue({ comparator: (a, b) => a.block.index - b.block.index, }); this.mutableInQueue = new Set(); this.mutableRunning = false; this.mutableBlock$ = new Subject(); this.getValidators = async (transactions) => { logger.debug({ name: 'neo_blockchain_get_validators' }); return getValidators(this, transactions); }; this.calculateClaimAmount = async (claims) => { logger.debug({ name: 'neo_blockchain_calculate_claim_amount' }); const spentCoins = await Promise.all(claims.map(async (claim) => this.tryGetSpentCoin(claim))); const filteredSpentCoinsIn = spentCoins.filter(commonUtils.notNull); if (spentCoins.length !== filteredSpentCoinsIn.length) { throw new CoinUnspentError(spentCoins.length - filteredSpentCoinsIn.length); } const filteredSpentCoins = filteredSpentCoinsIn.filter((spentCoin) => { if (spentCoin.claimed) { throw new CoinClaimedError(common.uInt256ToString(spentCoin.output.asset), spentCoin.output.value.toString(10)); } if (!common.uInt256Equal(spentCoin.output.asset, this.settings.governingToken.hash)) { throw new InvalidClaimError(common.uInt256ToString(spentCoin.output.asset), common.uInt256ToString(this.settings.governingToken.hash)); } return true; }); return utils.calculateClaimAmount({ coins: filteredSpentCoins.map((coin) => ({ value: coin.output.value, startHeight: coin.startHeight, endHeight: coin.endHeight, })), decrementInterval: this.settings.decrementInterval, generationAmount: this.settings.generationAmount, getSystemFee: async (index) => { const header = await this.header.get({ hashOrIndex: index, }); const blockData = await this.blockData.get({ hash: header.hash, }); return blockData.systemFee; }, }); }; this.verifyScript = async ({ scriptContainer, hash, witness, }) => { let { verification } = witness; if (verification.length === 0) { const builder = new ScriptBuilder(); builder.emitAppCallVerification(hash); verification = builder.build(); } else if (!common.uInt160Equal(hash, crypto.toScriptHash(verification))) { throw new WitnessVerifyError(); } const blockchain = this.createWriteBlockchain(); const mutableActions = []; let globalActionIndex = new BN(0); const executeResult = await this.vm.executeScripts({ scripts: [{ code: witness.invocation }, { code: verification }], blockchain, scriptContainer, triggerType: TriggerType.Verification, action: NULL_ACTION, gas: utils.ONE_HUNDRED_MILLION, listeners: { onLog: ({ message, scriptHash }) => { mutableActions.push(new LogAction({ index: globalActionIndex, scriptHash, message, })); globalActionIndex = globalActionIndex.add(utils.ONE); }, onNotify: ({ args, scriptHash }) => { mutableActions.push(new NotificationAction({ index: globalActionIndex, scriptHash, args, })); globalActionIndex = globalActionIndex.add(utils.ONE); }, }, }); const result = { actions: mutableActions, hash, witness }; const { stack, state, errorMessage } = executeResult; if (state === VMState.Fault) { return { ...result, failureMessage: errorMessage === undefined ? 'Script execution ended in a FAULT state' : errorMessage, }; } if (stack.length !== 1) { return { ...result, failureMessage: `Verification did not return one result. This may be a bug in the ` + `smart contract compiler or the smart contract itself. If you are using the NEO•ONE compiler please file an issue. Found ${stack.length} results.`, }; } const top = stack[0]; if (!top.asBoolean()) { return { ...result, failureMessage: 'Verification did not succeed.' }; } return result; }; this.tryGetInvocationData = async (transaction) => { const data = await this.invocationData.tryGet({ hash: transaction.hash, }); if (data === undefined) { return undefined; } const [asset, contracts, actions] = await Promise.all([ data.assetHash === undefined ? Promise.resolve(undefined) : this.asset.get({ hash: data.assetHash }), Promise.all(data.contractHashes.map(async (contractHash) => this.contract.tryGet({ hash: contractHash }))), data.actionIndexStart.eq(data.actionIndexStop) ? Promise.resolve([]) : this.action .getAll$({ indexStart: data.actionIndexStart, indexStop: data.actionIndexStop.sub(utils.ONE), }) .pipe(toArray()) .toPromise(), ]); return { asset, contracts: contracts.filter(commonUtils.notNull), deletedContractHashes: data.deletedContractHashes, migratedContractHashes: data.migratedContractHashes, voteUpdates: data.voteUpdates, result: data.result, actions, storageChanges: data.storageChanges, }; }; this.tryGetTransactionData = async (transaction) => this.transactionData.tryGet({ hash: transaction.hash }); this.getUnclaimed = async (hash) => this.accountUnclaimed .getAll$({ hash }) .pipe(toArray()) .toPromise() .then((values) => values.map((value) => value.input)); this.getUnspent = async (hash) => { const unspent = await this.accountUnspent .getAll$({ hash }) .pipe(toArray()) .toPromise(); return unspent.map((value) => value.input); }; this.getAllValidators = async () => this.validator.all$.pipe(toArray()).toPromise(); this.isSpent = async (input) => { const transactionData = await this.transactionData.tryGet({ hash: input.hash, }); return (transactionData !== undefined && transactionData.endHeights[input.index] !== undefined); }; this.tryGetSpentCoin = async (input) => { const [transactionData, output] = await Promise.all([ this.transactionData.tryGet({ hash: input.hash }), this.output.get(input), ]); if (transactionData === undefined) { return undefined; } const endHeight = transactionData.endHeights[input.index]; if (endHeight === undefined) { return undefined; } const claimed = transactionData.claimed[input.index]; return { output, startHeight: transactionData.startHeight, endHeight, claimed: !!claimed, }; }; this.storage = options.storage; this.mutableCurrentBlock = options.currentBlock; this.mutablePreviousBlock = options.previousBlock; this.mutableCurrentHeader = options.currentHeader; this.vm = options.vm; this.settings$ = new BehaviorSubject(options.settings); globalStats.record([ { measure: blockProgress, value: this.currentBlockIndex, }, { measure: blockCurrent, value: this.currentBlockIndex, }, ]); const self = this; this.deserializeWireContext = { get messageMagic() { return self.settings.messageMagic; }, }; this.feeContext = { get getOutput() { return self.output.get; }, get governingToken() { return self.settings.governingToken; }, get utilityToken() { return self.settings.utilityToken; }, get fees() { return self.settings.fees; }, get registerValidatorFee() { return self.settings.registerValidatorFee; }, }; this.serializeJSONContext = { get addressVersion() { return self.settings.addressVersion; }, get feeContext() { return self.feeContext; }, get tryGetInvocationData() { return self.tryGetInvocationData; }, get tryGetTransactionData() { return self.tryGetTransactionData; }, get getUnclaimed() { return self.getUnclaimed; }, get getUnspent() { return self.getUnspent; }, }; this.start(); } static async create({ settings, storage, vm }) { const [currentBlock, currentHeader] = await Promise.all([ storage.block.tryGetLatest(), storage.header.tryGetLatest(), ]); let previousBlock; if (currentBlock !== undefined) { previousBlock = await storage.block.tryGet({ hashOrIndex: currentBlock.index - 1 }); } const blockchain = new Blockchain({ currentBlock, currentHeader, previousBlock, settings, storage, vm, }); if (currentHeader === undefined) { await blockchain.persistHeaders([settings.genesisBlock.header]); } if (currentBlock === undefined) { await blockchain.persistBlock({ block: settings.genesisBlock }); } return blockchain; } get settings() { return this.settings$.getValue(); } get currentBlock() { if (this.mutableCurrentBlock === undefined) { throw new GenesisBlockNotRegisteredError(); } return this.mutableCurrentBlock; } get previousBlock() { return this.mutablePreviousBlock; } get currentHeader() { if (this.mutableCurrentHeader === undefined) { throw new GenesisBlockNotRegisteredError(); } return this.mutableCurrentHeader; } get currentBlockIndex() { return this.mutableCurrentBlock === undefined ? -1 : this.currentBlock.index; } get block$() { return this.mutableBlock$; } get isPersistingBlock() { return this.mutablePersistingBlocks; } get account() { return this.storage.account; } get accountUnclaimed() { return this.storage.accountUnclaimed; } get accountUnspent() { return this.storage.accountUnspent; } get action() { return this.storage.action; } get asset() { return this.storage.asset; } get block() { return this.storage.block; } get blockData() { return this.storage.blockData; } get header() { return this.storage.header; } get transaction() { return this.storage.transaction; } get transactionData() { return this.storage.transactionData; } get output() { return this.storage.output; } get contract() { return this.storage.contract; } get storageItem() { return this.storage.storageItem; } get validator() { return this.storage.validator; } get invocationData() { return this.storage.invocationData; } get validatorsCount() { return this.storage.validatorsCount; } async stop() { if (!this.mutableRunning) { return; } if (this.mutablePersistingBlocks) { const doneRunningPromise = new Promise((resolve) => { this.mutableDoneRunningResolve = resolve; }); this.mutableRunning = false; await doneRunningPromise; this.mutableDoneRunningResolve = undefined; } else { this.mutableRunning = false; } logger.info({ name: 'neo_blockchain_stop' }, 'NEO blockchain stopped.'); } updateSettings(settings) { this.settings$.next(settings); } async persistBlock({ block, unsafe = false, }) { return new Promise((resolve, reject) => { if (this.mutableInQueue.has(block.hashHex)) { resolve(); return; } this.mutableInQueue.add(block.hashHex); this.mutableBlockQueue.queue({ block, resolve, reject, unsafe, }); this.persistBlocksAsync(); }); } async persistHeaders(_headers) { } async verifyBlock(block) { await block.verify({ genesisBlock: this.settings.genesisBlock, tryGetBlock: this.block.tryGet, tryGetHeader: this.header.tryGet, isSpent: this.isSpent, getAsset: this.asset.get, getOutput: this.output.get, tryGetAccount: this.account.tryGet, getValidators: this.getValidators, standbyValidators: this.settings.standbyValidators, getAllValidators: this.getAllValidators, calculateClaimAmount: async (claims) => this.calculateClaimAmount(claims), verifyScript: async (options) => this.verifyScript(options), currentHeight: this.mutableCurrentBlock === undefined ? 0 : this.mutableCurrentBlock.index, governingToken: this.settings.governingToken, utilityToken: this.settings.utilityToken, fees: this.settings.fees, registerValidatorFee: this.settings.registerValidatorFee, }); } async verifyConsensusPayload(payload) { await payload.verify({ getValidators: async () => this.getValidators([]), verifyScript: async (options) => this.verifyScript(options), currentIndex: this.mutableCurrentBlock === undefined ? 0 : this.mutableCurrentBlock.index, currentBlockHash: this.currentBlock.hash, }); } async verifyTransaction({ transaction, memPool, }) { try { const verifications = await transaction.verify({ calculateClaimAmount: this.calculateClaimAmount, isSpent: this.isSpent, getAsset: this.asset.get, getOutput: this.output.get, tryGetAccount: this.account.tryGet, standbyValidators: this.settings.standbyValidators, getAllValidators: this.getAllValidators, verifyScript: async (options) => this.verifyScript(options), governingToken: this.settings.governingToken, utilityToken: this.settings.utilityToken, fees: this.settings.fees, registerValidatorFee: this.settings.registerValidatorFee, currentHeight: this.currentBlockIndex, memPool, }); return { verifications }; } catch (error) { if (error.code === undefined || typeof error.code !== 'string' || !error.code.includes('VERIFY')) { throw new UnknownVerifyError(error.message); } throw error; } } async invokeScript(script) { const transaction = new InvocationTransaction({ script, gas: common.ONE_HUNDRED_FIXED8, }); return this.invokeTransaction(transaction); } async invokeTransaction(transaction) { const blockchain = this.createWriteBlockchain(); const mutableActions = []; let globalActionIndex = new BN(0); const result = await wrapExecuteScripts(async () => this.vm.executeScripts({ scripts: [{ code: transaction.script }], blockchain, scriptContainer: { type: ScriptContainerType.Transaction, value: transaction, }, listeners: { onLog: ({ message, scriptHash }) => { mutableActions.push(new LogAction({ index: globalActionIndex, scriptHash, message, })); globalActionIndex = globalActionIndex.add(utils.ONE); }, onNotify: ({ args, scriptHash }) => { mutableActions.push(new NotificationAction({ index: globalActionIndex, scriptHash, args, })); globalActionIndex = globalActionIndex.add(utils.ONE); }, }, triggerType: TriggerType.Application, action: NULL_ACTION, gas: transaction.gas, skipWitnessVerify: true, })); return { result, actions: mutableActions, }; } async reset() { await this.stop(); await this.storage.reset(); this.mutableCurrentHeader = undefined; this.mutableCurrentBlock = undefined; this.mutablePreviousBlock = undefined; this.start(); await this.persistHeaders([this.settings.genesisBlock.header]); await this.persistBlock({ block: this.settings.genesisBlock }); } async persistBlocksAsync() { if (this.mutablePersistingBlocks || !this.mutableRunning) { return; } this.mutablePersistingBlocks = true; let entry; try { entry = this.cleanBlockQueue(); while (this.mutableRunning && entry !== undefined && entry.block.index === this.currentBlockIndex + 1) { const startTime = Date.now(); const entryNonNull = entry; const logData = { [Labels.NEO_BLOCK_INDEX]: entry.block.index, name: 'neo_blockchain_persist_block_top_level', }; try { await this.persistBlockInternal(entryNonNull.block, entryNonNull.unsafe); logger.debug(logData); globalStats.record([ { measure: blockDurationMs, value: Date.now() - startTime, }, ]); } catch (err) { logger.error({ err, ...logData }); globalStats.record([ { measure: blockFailures, value: 1, }, ]); throw err; } entry.resolve(); this.mutableBlock$.next(entry.block); globalStats.record([ { measure: blockCurrent, value: entry.block.index, }, { measure: blockLatencySec, value: commonUtils.nowSeconds() - entry.block.timestamp, }, ]); entry = this.cleanBlockQueue(); } if (entry !== undefined) { this.mutableBlockQueue.queue(entry); } } catch (error) { if (entry !== undefined) { entry.reject(error); } } finally { this.mutablePersistingBlocks = false; if (this.mutableDoneRunningResolve !== undefined) { this.mutableDoneRunningResolve(); this.mutableDoneRunningResolve = undefined; } } } cleanBlockQueue() { let entry = this.dequeBlockQueue(); while (entry !== undefined && entry.block.index <= this.currentBlockIndex) { entry.resolve(); entry = this.dequeBlockQueue(); } return entry; } dequeBlockQueue() { if (this.mutableBlockQueue.length > 0) { return this.mutableBlockQueue.dequeue(); } return undefined; } start() { this.mutableBlock$ = new Subject(); this.mutablePersistingBlocks = false; this.mutableBlockQueue = new PriorityQueue({ comparator: (a, b) => a.block.index - b.block.index, }); this.mutableInQueue = new Set(); this.mutableDoneRunningResolve = undefined; this.mutableRunning = true; logger.info({ name: 'neo_blockchain_start' }, 'NEO blockchain started.'); } async persistBlockInternal(block, unsafe) { globalStats.record([ { measure: blockProgress, value: block.index, }, ]); if (!unsafe) { await this.verifyBlock(block); } const blockchain = this.createWriteBlockchain(); await blockchain.persistBlock(block); await this.storage.commit(blockchain.getChangeSet()); this.mutablePreviousBlock = this.mutableCurrentBlock; this.mutableCurrentBlock = block; this.mutableCurrentHeader = block.header; } createWriteBlockchain() { return new WriteBatchBlockchain({ settings: this.settings, currentBlock: this.mutableCurrentBlock, currentHeader: this.mutableCurrentHeader, storage: this.storage, vm: this.vm, getValidators: this.getValidators, }); } } //# sourceMappingURL=Blockchain.js.map