@neo-one/node-blockchain-esnext-esm
Version:
NEO•ONE NEO blockchain implementation.
641 lines (639 loc) • 26.3 kB
JavaScript
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