UNPKG

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

Version:

NEO•ONE NEO blockchain implementation.

363 lines (323 loc) 12.6 kB
// tslint:disable no-object-mutation no-array-mutation no-loop-statement import { Account, BinaryReader, common, ECPoint, Output, StateDescriptor, StateTransaction, Transaction, TransactionType, UInt160, UInt256Hex, utils, Validator, ValidatorKey, ValidatorUpdate, } from '@neo-one/client-core-esnext-cjs'; import { ValidatorsCount, ValidatorsCountUpdate } from '@neo-one/node-core-esnext-cjs'; import { BN } from 'bn.js'; import _ from 'lodash'; import { Blockchain } from './Blockchain'; import { ValidatorCache } from './ValidatorCache'; const processOutput = async ( blockchain: Blockchain, cache: ValidatorCache, output: Output, negative: boolean, ): Promise<void> => { let { value } = output; if (negative) { value = value.neg(); } const [account] = await Promise.all([ cache.getAccount(output.address), cache.updateAccountBalance(output.address, output.asset, value), ]); if (common.uInt256Equal(output.asset, blockchain.settings.governingToken.hash) && account.votes.length > 0) { await Promise.all([ Promise.all(account.votes.map(async (publicKey) => cache.updateValidatorVotes(publicKey, value))), cache.updateValidatorsCountVotes(account.votes.length - 1, value), ]); } }; const processTransaction = async ( blockchain: Blockchain, cache: ValidatorCache, transaction: Transaction, ): Promise<void> => { let allOutputs = await Promise.all( transaction.inputs.map(async (input) => { const output = await blockchain.output.get(input); return { output, negative: true }; }), ); allOutputs = allOutputs.concat(transaction.outputs.map((output) => ({ output, negative: false }))); await Promise.all(allOutputs.map(async ({ output, negative }) => processOutput(blockchain, cache, output, negative))); const accountHashes = [...new Set(allOutputs.map(({ output }) => common.uInt160ToHex(output.address)))].map((hash) => common.hexToUInt160(hash), ); const touchedValidators = await Promise.all( accountHashes.map(async (hash) => { const account = await cache.getAccount(hash); return account.votes; }), ); const touchedValidatorsSet = [ ...new Set( touchedValidators.reduce<ReadonlyArray<string>>( (acc, votes) => acc.concat(votes.map((vote) => common.ecPointToHex(vote))), [], ), ), ].map((publicKey) => common.hexToECPoint(publicKey)); await Promise.all( touchedValidatorsSet.map(async (publicKey) => { const validator = await cache.getValidator(publicKey); if (!validator.registered && validator.votes.eq(utils.ZERO)) { await cache.deleteValidator(publicKey); } }), ); }; // tslint:disable readonly-keyword readonly-array export interface AccountChanges { [hash: string]: ReadonlyArray<ECPoint>; } export interface ValidatorVotesChanges { [hash: string]: BN; } export interface ValidatorRegisteredChanges { [hash: string]: boolean; } interface ValidatorChange { readonly registered?: boolean; readonly votes?: BN; } export interface ValidatorChanges { [hash: string]: ValidatorChange; } export type ValidatorsCountChanges = BN[]; // tslint:enable readonly-keyword readonly-array export const getDescriptorChanges = async ({ transactions, getAccount, governingTokenHash, }: { readonly transactions: ReadonlyArray<StateTransaction>; readonly getAccount: ((hash: UInt160) => Promise<Account>); readonly governingTokenHash: UInt256Hex; }): Promise<{ readonly accountChanges: AccountChanges; readonly validatorChanges: ValidatorChanges; readonly validatorsCountChanges: ValidatorsCountChanges; }> => { const accountChanges: AccountChanges = {}; const validatorVotesChanges: ValidatorVotesChanges = {}; const validatorRegisteredChanges: ValidatorRegisteredChanges = {}; const validatorsCountChanges: ValidatorsCountChanges = []; const allDescriptors = transactions.reduce<ReadonlyArray<StateDescriptor>>( (acc, transaction) => acc.concat(transaction.descriptors), [], ); const accountDescriptors = allDescriptors.filter((descriptor) => descriptor.type === 0x40); const groupedAccountDescriptors = Object.entries( _.groupBy(accountDescriptors, (descriptor) => common.uInt160ToHex(common.bufferToUInt160(descriptor.key))), ); await Promise.all( groupedAccountDescriptors.map(async ([hash, descriptors]) => { const account = await getAccount(common.hexToUInt160(hash)); const balance = account.getBalance(governingTokenHash); // tslint:disable-next-line no-loop-statement for (const vote of account.votes) { const voteHex = common.ecPointToHex(vote); validatorVotesChanges[voteHex] = ((validatorVotesChanges[voteHex] as BN | undefined) === undefined ? utils.ZERO : validatorVotesChanges[voteHex] ).sub(balance); } const descriptor = descriptors[descriptors.length - 1]; const reader = new BinaryReader(descriptor.value); const votes = reader.readArray(() => reader.readECPoint()); if (votes.length !== account.votes.length) { if (account.votes.length > 0) { validatorsCountChanges[account.votes.length - 1] = ((validatorsCountChanges[account.votes.length - 1] as | BN | undefined) === undefined ? utils.ZERO : validatorsCountChanges[account.votes.length - 1] ).sub(balance); } if (votes.length > 0) { validatorsCountChanges[votes.length - 1] = ((validatorsCountChanges[votes.length - 1] as BN | undefined) === undefined ? utils.ZERO : validatorsCountChanges[votes.length - 1] ).add(balance); } } accountChanges[hash] = votes; for (const vote of votes) { const voteHex = common.ecPointToHex(vote); validatorVotesChanges[voteHex] = ((validatorVotesChanges[voteHex] as BN | undefined) === undefined ? utils.ZERO : validatorVotesChanges[voteHex] ).add(balance); } }), ); const validatorDescriptors = allDescriptors.filter((descriptor) => descriptor.type === 0x48); for (const descriptor of validatorDescriptors) { const publicKey = common.bufferToECPoint(descriptor.key); validatorRegisteredChanges[common.ecPointToHex(publicKey)] = descriptor.value.some((byte) => byte !== 0); } const validatorChanges: ValidatorChanges = {}; for (const [publicKey, votes] of Object.entries(validatorVotesChanges)) { validatorChanges[publicKey] = { votes }; } for (const [publicKey, registered] of Object.entries(validatorRegisteredChanges)) { const current = (validatorChanges[publicKey] as ValidatorChange | undefined) === undefined ? {} : validatorChanges[publicKey]; validatorChanges[publicKey] = { registered, votes: current.votes, }; } return { accountChanges, validatorChanges, validatorsCountChanges, }; }; export const processStateTransaction = async ({ validatorChanges, validatorsCountChanges, tryGetValidatorsCount, addValidatorsCount, updateValidatorsCount, tryGetValidator, addValidator, deleteValidator, updateValidator, }: { readonly validatorChanges: ValidatorChanges; readonly validatorsCountChanges: ValidatorsCountChanges; readonly tryGetValidatorsCount: (() => Promise<ValidatorsCount | undefined>); readonly addValidatorsCount: ((validatorsCount: ValidatorsCount) => Promise<void>); readonly updateValidatorsCount: ((validatorsCount: ValidatorsCount, update: ValidatorsCountUpdate) => Promise<void>); readonly tryGetValidator: ((key: ValidatorKey) => Promise<Validator | undefined>); readonly addValidator: ((validator: Validator) => Promise<void>); readonly deleteValidator: ((key: ValidatorKey) => Promise<void>); readonly updateValidator: ((validator: Validator, update: ValidatorUpdate) => Promise<Validator>); }): Promise<void> => { const validatorsCount = await tryGetValidatorsCount(); const mutableValidatorsCountVotes = validatorsCount === undefined ? [] : [...validatorsCount.votes]; [...validatorsCountChanges.entries()].forEach(([index, value]) => { mutableValidatorsCountVotes[index] = value; }); await Promise.all([ Promise.all( Object.entries(validatorChanges).map(async ([publicKeyHex, { registered, votes }]) => { const publicKey = common.hexToECPoint(publicKeyHex); const validator = await tryGetValidator({ publicKey }); if (validator === undefined) { await addValidator( new Validator({ publicKey, registered, votes, }), ); } else if ( ((registered !== undefined && !registered) || (registered === undefined && !validator.registered)) && ((votes !== undefined && votes.eq(utils.ZERO)) || (votes === undefined && validator.votes.eq(utils.ZERO))) ) { await deleteValidator({ publicKey: validator.publicKey }); } else { await updateValidator(validator, { votes, registered }); } }), ), validatorsCount === undefined ? addValidatorsCount( new ValidatorsCount({ votes: mutableValidatorsCountVotes, }), ) : updateValidatorsCount(validatorsCount, { votes: mutableValidatorsCountVotes, }), ]); }; export const getValidators = async ( blockchain: Blockchain, transactions: ReadonlyArray<Transaction>, ): Promise<ReadonlyArray<ECPoint>> => { const cache = new ValidatorCache(blockchain); await Promise.all(transactions.map(async (transaction) => processTransaction(blockchain, cache, transaction))); const { validatorChanges, validatorsCountChanges } = await getDescriptorChanges({ transactions: transactions.filter( (transaction): transaction is StateTransaction => transaction.type === TransactionType.State && transaction instanceof StateTransaction, ), getAccount: async (hash) => cache.getAccount(hash), governingTokenHash: blockchain.settings.governingToken.hashHex, }); await processStateTransaction({ validatorChanges, validatorsCountChanges, tryGetValidatorsCount: async () => cache.getValidatorsCount(), addValidatorsCount: async (value) => cache.addValidatorsCount(value), updateValidatorsCount: async (update) => { await cache.updateValidatorsCount(update); }, tryGetValidator: async (key) => cache.getValidator(key.publicKey), addValidator: async (validator) => cache.addValidator(validator), deleteValidator: async (key) => cache.deleteValidator(key.publicKey), updateValidator: async (value, update) => cache.updateValidator(value.publicKey, update), }); const [validatorsCount, validators] = await Promise.all([cache.getValidatorsCount(), cache.getAllValidators()]); const numValidators = Math.max( utils.weightedAverage( utils .weightedFilter( validatorsCount.votes .map((votes, count) => ({ count, votes: votes === undefined ? utils.ZERO : votes })) .filter(({ votes }) => votes.gt(utils.ZERO)), 0.25, 0.75, ({ count }) => new BN(count), ) .map(([{ count }, weight]) => ({ value: count, weight })), ), blockchain.settings.standbyValidators.length, ); const standbyValidatorsSet = new Set( blockchain.settings.standbyValidators.map((publicKey) => common.ecPointToHex(publicKey)), ); const validatorsPublicKeySet = new Set( _.take( validators .filter( (validator) => (validator.registered && validator.votes.gt(utils.ZERO)) || standbyValidatorsSet.has(common.ecPointToHex(validator.publicKey)), ) .sort( (aValidator, bValidator) => aValidator.votes.eq(bValidator.votes) ? common.ecPointCompare(aValidator.publicKey, bValidator.publicKey) : -aValidator.votes.cmp(bValidator.votes), ) .map((validator) => common.ecPointToHex(validator.publicKey)), numValidators, ), ); const standbyValidatorsArray = [...standbyValidatorsSet]; for (let i = 0; i < standbyValidatorsArray.length && validatorsPublicKeySet.size < numValidators; i += 1) { validatorsPublicKeySet.add(standbyValidatorsArray[i]); } const validatorsPublicKeys = [...validatorsPublicKeySet].map((hex) => common.hexToECPoint(hex)); return validatorsPublicKeys.sort((aKey, bKey) => common.ecPointCompare(aKey, bKey)); };