@neo-one/node-blockchain-esnext-cjs
Version:
NEO•ONE NEO blockchain implementation.
1,120 lines (1,037 loc) • 42.1 kB
text/typescript
// tslint:disable no-array-mutation no-object-mutation
import {
Account,
AccountKey,
AccountUpdate,
Action,
ActionKey,
ActionsKey,
Asset,
AssetKey,
AssetUpdate,
Block,
ClaimTransaction,
common,
Contract,
ContractKey,
ContractTransaction,
ECPoint,
EnrollmentTransaction,
Header,
Input,
InvocationData,
InvocationDataKey,
InvocationResultSuccess,
InvocationTransaction,
IssueTransaction,
LogAction,
MinerTransaction,
NotificationAction,
Output,
PublishTransaction,
RegisterTransaction,
ScriptContainerType,
StateTransaction,
StorageItem,
StorageItemKey,
StorageItemsKey,
StorageItemUpdate,
Transaction,
TransactionData,
TransactionDataKey,
TransactionDataUpdate,
TransactionKey,
TransactionType,
UInt160,
UInt256,
utils,
Validator,
ValidatorKey,
ValidatorUpdate,
} from '@neo-one/client-core-esnext-cjs';
import { Monitor } from '@neo-one/monitor-esnext-cjs';
import {
AccountUnclaimed,
AccountUnclaimedKey,
AccountUnclaimedsKey,
AccountUnspent,
AccountUnspentKey,
AccountUnspentsKey,
BlockchainStorage,
BlockData,
BlockDataKey,
ChangeSet,
TriggerType,
ValidatorsCount,
ValidatorsCountUpdate,
VM,
WriteBlockchain,
} from '@neo-one/node-core-esnext-cjs';
import { labels, utils as commonUtils } from '@neo-one/utils-esnext-cjs';
import { BN } from 'bn.js';
import _ from 'lodash';
import { GenesisBlockNotRegisteredError } from './errors';
import { AccountChanges, getDescriptorChanges, ValidatorChanges, ValidatorsCountChanges } from './getValidators';
import {
BlockLikeStorageCache,
OutputStorageCache,
ReadAddDeleteStorageCache,
ReadAddStorageCache,
ReadAddUpdateMetadataStorageCache,
ReadAddUpdateStorageCache,
ReadAllAddUpdateDeleteStorageCache,
ReadGetAllAddDeleteStorageCache,
ReadGetAllAddStorageCache,
ReadGetAllAddUpdateDeleteStorageCache,
} from './StorageCache';
import { wrapExecuteScripts } from './wrapExecuteScripts';
interface WriteBatchBlockchainOptions {
readonly settings: WriteBlockchain['settings'];
readonly currentBlock: WriteBlockchain['currentBlock'] | undefined;
readonly currentHeader: WriteBlockchain['currentHeader'] | undefined;
readonly storage: BlockchainStorage;
readonly vm: VM;
readonly getValidators: WriteBlockchain['getValidators'];
}
interface Caches {
readonly account: ReadAllAddUpdateDeleteStorageCache<AccountKey, Account, AccountUpdate>;
readonly accountUnspent: ReadGetAllAddDeleteStorageCache<AccountUnspentKey, AccountUnspentsKey, AccountUnspent>;
readonly accountUnclaimed: ReadGetAllAddDeleteStorageCache<
AccountUnclaimedKey,
AccountUnclaimedsKey,
AccountUnclaimed
>;
readonly action: ReadGetAllAddStorageCache<ActionKey, ActionsKey, Action>;
readonly asset: ReadAddUpdateStorageCache<AssetKey, Asset, AssetUpdate>;
readonly block: BlockLikeStorageCache<Block>;
readonly blockData: ReadAddStorageCache<BlockDataKey, BlockData>;
readonly header: BlockLikeStorageCache<Header>;
readonly transaction: ReadAddStorageCache<TransactionKey, Transaction>;
readonly transactionData: ReadAddUpdateStorageCache<TransactionDataKey, TransactionData, TransactionDataUpdate>;
readonly output: OutputStorageCache;
readonly contract: ReadAddDeleteStorageCache<ContractKey, Contract>;
readonly storageItem: ReadGetAllAddUpdateDeleteStorageCache<
StorageItemKey,
StorageItemsKey,
StorageItem,
StorageItemUpdate
>;
readonly validator: ReadAllAddUpdateDeleteStorageCache<ValidatorKey, Validator, ValidatorUpdate>;
readonly invocationData: ReadAddStorageCache<InvocationDataKey, InvocationData>;
readonly validatorsCount: ReadAddUpdateMetadataStorageCache<ValidatorsCount, ValidatorsCountUpdate>;
}
interface InputClaim {
readonly type: 'claim' | 'input';
readonly hash: UInt256;
readonly input: Input;
}
interface OutputWithInput {
readonly output: Output;
readonly input: Input;
}
export class WriteBatchBlockchain {
public readonly settings: WriteBlockchain['settings'];
public readonly account: ReadAllAddUpdateDeleteStorageCache<AccountKey, Account, AccountUpdate>;
public readonly accountUnspent: ReadGetAllAddDeleteStorageCache<
AccountUnspentKey,
AccountUnspentsKey,
AccountUnspent
>;
public readonly accountUnclaimed: ReadGetAllAddDeleteStorageCache<
AccountUnclaimedKey,
AccountUnclaimedsKey,
AccountUnclaimed
>;
public readonly action: ReadGetAllAddStorageCache<ActionKey, ActionsKey, Action>;
public readonly asset: ReadAddUpdateStorageCache<AssetKey, Asset, AssetUpdate>;
public readonly block: BlockLikeStorageCache<Block>;
public readonly blockData: ReadAddStorageCache<BlockDataKey, BlockData>;
public readonly header: BlockLikeStorageCache<Header>;
public readonly transaction: ReadAddStorageCache<TransactionKey, Transaction>;
public readonly transactionData: ReadAddUpdateStorageCache<
TransactionDataKey,
TransactionData,
TransactionDataUpdate
>;
public readonly output: OutputStorageCache;
public readonly contract: ReadAddDeleteStorageCache<ContractKey, Contract>;
public readonly storageItem: ReadGetAllAddUpdateDeleteStorageCache<
StorageItemKey,
StorageItemsKey,
StorageItem,
StorageItemUpdate
>;
public readonly validator: ReadAllAddUpdateDeleteStorageCache<ValidatorKey, Validator, ValidatorUpdate>;
public readonly invocationData: ReadAddStorageCache<InvocationDataKey, InvocationData>;
public readonly validatorsCount: ReadAddUpdateMetadataStorageCache<ValidatorsCount, ValidatorsCountUpdate>;
public readonly getValidators: WriteBlockchain['getValidators'];
private readonly currentBlockInternal: WriteBlockchain['currentBlock'] | undefined;
private readonly currentHeaderInternal: WriteBlockchain['currentHeader'] | undefined;
private mutableStorage: BlockchainStorage;
private readonly vm: VM;
private readonly caches: Caches;
public constructor(options: WriteBatchBlockchainOptions) {
this.settings = options.settings;
this.currentBlockInternal = options.currentBlock;
this.currentHeaderInternal = options.currentHeader;
this.mutableStorage = options.storage;
this.vm = options.vm;
this.getValidators = options.getValidators;
const output = new OutputStorageCache(() => this.storage.output);
this.caches = {
account: new ReadAllAddUpdateDeleteStorageCache({
name: 'account',
readAllStorage: () => this.storage.account,
update: (value, update) => value.update(update),
getKeyFromValue: (value) => ({ hash: value.hash }),
getKeyString: (key) => common.uInt160ToString(key.hash),
createAddChange: (value) => ({ type: 'account', value }),
createDeleteChange: (key) => ({ type: 'account', key }),
}),
accountUnspent: new ReadGetAllAddDeleteStorageCache({
name: 'accountUnspent',
readGetAllStorage: () => this.storage.accountUnspent,
getKeyFromValue: (value) => ({ hash: value.hash, input: value.input }),
getKeyString: (key) =>
`${common.uInt160ToString(key.hash)}:${common.uInt256ToString(key.input.hash)}:${key.input.index}`,
matchesPartialKey: (value, key) => common.uInt160Equal(value.hash, key.hash),
createAddChange: (value) => ({ type: 'accountUnspent', value }),
createDeleteChange: (key) => ({ type: 'accountUnspent', key }),
}),
accountUnclaimed: new ReadGetAllAddDeleteStorageCache({
name: 'accountUnclaimed',
readGetAllStorage: () => this.storage.accountUnclaimed,
getKeyFromValue: (value) => ({ hash: value.hash, input: value.input }),
getKeyString: (key) =>
`${common.uInt160ToString(key.hash)}:${common.uInt256ToString(key.input.hash)}:${key.input.index}`,
matchesPartialKey: (value, key) => common.uInt160Equal(value.hash, key.hash),
createAddChange: (value) => ({ type: 'accountUnclaimed', value }),
createDeleteChange: (key) => ({ type: 'accountUnclaimed', key }),
}),
action: new ReadGetAllAddStorageCache({
name: 'action',
readGetAllStorage: () => this.storage.action,
getKeyFromValue: (value) => ({
index: value.index,
}),
getKeyString: (key) => key.index.toString(10),
matchesPartialKey: (value, key) =>
(key.indexStart === undefined || value.index.gte(key.indexStart)) &&
(key.indexStop === undefined || value.index.lte(key.indexStop)),
createAddChange: (value) => ({ type: 'action', value }),
}),
asset: new ReadAddUpdateStorageCache({
name: 'asset',
readStorage: () => this.storage.asset,
update: (value, update) => value.update(update),
getKeyFromValue: (value) => ({ hash: value.hash }),
getKeyString: (key) => common.uInt256ToString(key.hash),
createAddChange: (value) => ({ type: 'asset', value }),
}),
block: new BlockLikeStorageCache({
name: 'block',
readStorage: () => ({
get: this.storage.block.get,
tryGet: this.storage.block.tryGet,
}),
createAddChange: (value) => ({ type: 'block', value }),
}),
blockData: new ReadAddStorageCache({
name: 'blockData',
readStorage: () => this.storage.blockData,
getKeyFromValue: (value) => ({ hash: value.hash }),
getKeyString: (key) => common.uInt256ToString(key.hash),
createAddChange: (value) => ({ type: 'blockData', value }),
}),
header: new BlockLikeStorageCache({
name: 'header',
readStorage: () => ({
get: this.storage.header.get,
tryGet: this.storage.header.tryGet,
}),
createAddChange: (value) => ({ type: 'header', value }),
}),
transaction: new ReadAddStorageCache({
name: 'transaction',
readStorage: () => this.storage.transaction,
getKeyFromValue: (value) => ({ hash: value.hash }),
getKeyString: (key) => common.uInt256ToString(key.hash),
createAddChange: (value) => ({ type: 'transaction', value }),
onAdd: async (value) => {
await Promise.all(
value.outputs.map(async (out, index) => output.add({ hash: value.hash, index, output: out })),
);
},
}),
transactionData: new ReadAddUpdateStorageCache({
name: 'transactionData',
readStorage: () => this.storage.transactionData,
update: (value, update) => value.update(update),
getKeyFromValue: (value) => ({ hash: value.hash }),
getKeyString: (key) => common.uInt256ToString(key.hash),
createAddChange: (value) => ({ type: 'transactionData', value }),
}),
output,
contract: new ReadAddDeleteStorageCache({
name: 'contract',
readStorage: () => this.storage.contract,
getKeyFromValue: (value) => ({ hash: value.hash }),
getKeyString: (key) => common.uInt160ToString(key.hash),
createAddChange: (value) => ({ type: 'contract', value }),
createDeleteChange: (key) => ({ type: 'contract', key }),
}),
storageItem: new ReadGetAllAddUpdateDeleteStorageCache({
name: 'storageItem',
readGetAllStorage: () => this.storage.storageItem,
update: (value, update) => value.update(update),
getKeyFromValue: (value) => ({
hash: value.hash,
key: value.key,
}),
getKeyString: (key) => `${common.uInt160ToString(key.hash)}:${key.key.toString('hex')}`,
matchesPartialKey: (value, key) =>
(key.hash === undefined || common.uInt160Equal(value.hash, key.hash)) &&
(key.prefix === undefined || key.prefix.every((byte, idx) => value.key[idx] === byte)),
createAddChange: (value) => ({ type: 'storageItem', value }),
createDeleteChange: (key) => ({ type: 'storageItem', key }),
}),
validator: new ReadAllAddUpdateDeleteStorageCache({
name: 'validator',
readAllStorage: () => this.storage.validator,
getKeyFromValue: (value) => ({ publicKey: value.publicKey }),
getKeyString: (key) => common.ecPointToString(key.publicKey),
createAddChange: (value) => ({ type: 'validator', value }),
update: (value, update) => value.update(update),
createDeleteChange: (key) => ({ type: 'validator', key }),
}),
invocationData: new ReadAddStorageCache({
name: 'invocationData',
readStorage: () => this.storage.invocationData,
getKeyFromValue: (value) => ({ hash: value.hash }),
getKeyString: (key) => common.uInt256ToString(key.hash),
createAddChange: (value) => ({ type: 'invocationData', value }),
}),
validatorsCount: new ReadAddUpdateMetadataStorageCache({
name: 'validatorsCount',
readStorage: () => this.storage.validatorsCount,
createAddChange: (value) => ({ type: 'validatorsCount', value }),
update: (value, update) => value.update(update),
}),
};
this.account = this.caches.account;
this.accountUnspent = this.caches.accountUnspent;
this.accountUnclaimed = this.caches.accountUnclaimed;
this.action = this.caches.action;
this.asset = this.caches.asset;
this.block = this.caches.block;
this.blockData = this.caches.blockData;
this.header = this.caches.header;
this.transaction = this.caches.transaction;
this.transactionData = this.caches.transactionData;
this.output = this.caches.output;
this.contract = this.caches.contract;
this.storageItem = this.caches.storageItem;
this.validator = this.caches.validator;
this.invocationData = this.caches.invocationData;
this.validatorsCount = this.caches.validatorsCount;
}
public get storage(): BlockchainStorage {
return this.mutableStorage;
}
public setStorage(storage: BlockchainStorage): void {
this.mutableStorage = storage;
}
public get currentBlock(): Block {
if (this.currentBlockInternal === undefined) {
throw new GenesisBlockNotRegisteredError();
}
return this.currentBlockInternal;
}
public get currentBlockIndex(): number {
return this.currentBlockInternal === undefined ? 0 : this.currentBlockInternal.index;
}
public get currentHeader(): Header {
if (this.currentHeaderInternal === undefined) {
throw new GenesisBlockNotRegisteredError();
}
return this.currentHeaderInternal;
}
public getChangeSet(): ChangeSet {
return Object.values(this.caches).reduce<ChangeSet>((acc, cache) => acc.concat(cache.getChangeSet()), []);
}
public async persistBlock(monitorIn: Monitor, block: Block): Promise<void> {
const monitor = monitorIn.at('write_blockchain').withData({
[labels.NEO_BLOCK_INDEX]: block.index,
});
const [maybePrevBlockData, outputContractsList] = await monitor.captureSpan(
async () =>
Promise.all([
block.index === 0 ? Promise.resolve(undefined) : this.blockData.get({ hash: block.previousHash }),
Promise.all(
[
...new Set(
block.transactions.reduce<string[]>(
(acc, transaction) =>
acc.concat(transaction.outputs.map((output) => common.uInt160ToString(output.address))),
[],
),
),
].map(async (hash) => this.contract.tryGet({ hash: common.stringToUInt160(hash) })),
),
this.block.add(block),
this.header.add(block.header),
]),
{
name: 'neo_write_blockchain_stage_0',
},
);
const prevBlockData =
maybePrevBlockData === undefined
? {
lastGlobalTransactionIndex: utils.NEGATIVE_ONE,
lastGlobalActionIndex: utils.NEGATIVE_ONE,
systemFee: utils.ZERO,
}
: {
lastGlobalTransactionIndex: maybePrevBlockData.lastGlobalTransactionIndex,
lastGlobalActionIndex: maybePrevBlockData.lastGlobalActionIndex,
systemFee: maybePrevBlockData.systemFee,
};
const outputContracts: { [key: string]: Contract | undefined } = {};
outputContractsList.filter(commonUtils.notNull).forEach((outputContract) => {
outputContracts[outputContract.hashHex] = outputContract;
});
const [utxo, rest] = _.partition(
block.transactions.map<[number, Transaction]>((transaction, idx) => [idx, transaction]),
// tslint:disable-next-line no-unused
([idx, transaction]) =>
((transaction.type === TransactionType.Claim && transaction instanceof ClaimTransaction) ||
(transaction.type === TransactionType.Contract && transaction instanceof ContractTransaction) ||
(transaction.type === TransactionType.Miner && transaction instanceof MinerTransaction)) &&
!transaction.outputs.some((output) => outputContracts[common.uInt160ToString(output.address)] !== undefined),
);
const [globalActionIndex] = await monitor.captureSpan(
async (span) =>
Promise.all([
rest.length > 0
? this.persistTransactions(
span,
block,
// tslint:disable-next-line no-any
rest as any,
prevBlockData.lastGlobalTransactionIndex,
prevBlockData.lastGlobalActionIndex,
)
: Promise.resolve(prevBlockData.lastGlobalActionIndex),
utxo.length > 0
? // tslint:disable-next-line no-any
this.persistUTXOTransactions(span, block, utxo as any, prevBlockData.lastGlobalTransactionIndex)
: Promise.resolve(),
]),
{
name: 'neo_write_blockchain_stage_1',
},
);
await monitor.captureSpan(
async () =>
this.blockData.add(
new BlockData({
hash: block.hash,
lastGlobalTransactionIndex: prevBlockData.lastGlobalTransactionIndex.add(new BN(block.transactions.length)),
lastGlobalActionIndex: globalActionIndex,
systemFee: prevBlockData.systemFee.add(
block.getSystemFee({
getOutput: this.output.get,
governingToken: this.settings.governingToken,
utilityToken: this.settings.utilityToken,
fees: this.settings.fees,
registerValidatorFee: this.settings.registerValidatorFee,
}),
),
}),
),
{ name: 'neo_write_blockchain_stage_2' },
);
}
private async persistUTXOTransactions(
monitor: Monitor,
block: Block,
transactions: ReadonlyArray<[number, (ContractTransaction | ClaimTransaction | MinerTransaction)]>,
lastGlobalTransactionIndex: BN,
): Promise<void> {
await monitor.captureSpan(
async (span) => {
const inputs = [];
const claims = [];
const outputWithInputs = [];
// tslint:disable-next-line no-unused no-loop-statement no-dead-store
for (const idxAndTransaction of transactions) {
const transaction = idxAndTransaction[1];
inputs.push(...transaction.inputs);
if (transaction.type === TransactionType.Claim && transaction instanceof ClaimTransaction) {
claims.push(...transaction.claims);
}
outputWithInputs.push(...this.getOutputWithInput(transaction));
}
await Promise.all([
Promise.all(
// tslint:disable-next-line no-unused
transactions.map(async ([idx, transaction]) => this.transaction.add(transaction, true)),
),
Promise.all(
transactions.map(async ([idx, transaction]) =>
this.transactionData.add(
new TransactionData({
hash: transaction.hash,
startHeight: block.index,
blockHash: block.hash,
index: idx,
globalIndex: lastGlobalTransactionIndex.add(new BN(idx + 1)),
}),
true,
),
),
),
this.updateAccounts(span, inputs, claims, outputWithInputs),
this.updateCoins(span, inputs, claims, block),
]);
},
{
name: 'neo_write_blockchain_persist_utxo_transactions',
},
);
}
private async persistTransactions(
monitor: Monitor,
block: Block,
transactions: ReadonlyArray<[number, Transaction]>,
lastGlobalTransactionIndex: BN,
lastGlobalActionIndex: BN,
): Promise<BN> {
return monitor.captureSpan(
async (span) => {
let globalActionIndex = lastGlobalActionIndex.add(utils.ONE);
// tslint:disable-next-line no-loop-statement
for (const [idx, transaction] of transactions) {
globalActionIndex = await this.persistTransaction(
span,
block,
transaction,
idx,
lastGlobalTransactionIndex,
globalActionIndex,
);
}
return globalActionIndex.sub(utils.ONE);
},
{
name: 'neo_write_blockchain_persist_transactions',
},
);
}
private async persistTransaction(
monitor: Monitor,
block: Block,
transactionIn: Transaction,
transactionIndex: number,
lastGlobalTransactionIndex: BN,
globalActionIndexIn: BN,
): Promise<BN> {
let globalActionIndex = globalActionIndexIn;
await monitor
.withLabels({ [labels.NEO_TRANSACTION_TYPE]: transactionIn.type })
.withData({ [labels.NEO_TRANSACTION_HASH]: transactionIn.hashHex })
.captureSpan(
async (span) => {
const transaction = transactionIn;
const claims =
transaction.type === TransactionType.Claim && transaction instanceof ClaimTransaction
? transaction.claims
: [];
let accountChanges = {};
let validatorChanges = {};
let validatorsCountChanges: ValidatorsCountChanges = [];
if (transaction.type === TransactionType.State && transaction instanceof StateTransaction) {
({ accountChanges, validatorChanges, validatorsCountChanges } = await getDescriptorChanges({
transactions: [transaction],
getAccount: async (hash) =>
this.account
.tryGet({ hash })
.then((account) => (account === undefined ? new Account({ hash }) : account)),
governingTokenHash: this.settings.governingToken.hashHex,
}));
}
await Promise.all([
this.transaction.add(transaction, true),
this.transactionData.add(
new TransactionData({
hash: transaction.hash,
blockHash: block.hash,
startHeight: block.index,
index: transactionIndex,
globalIndex: lastGlobalTransactionIndex.add(new BN(transactionIndex + 1)),
}),
true,
),
this.updateAccounts(span, transaction.inputs, claims, this.getOutputWithInput(transaction), accountChanges),
this.updateCoins(span, transaction.inputs, claims, block),
this.processStateTransaction(span, validatorChanges, validatorsCountChanges),
]);
if (transaction.type === TransactionType.Register && transaction instanceof RegisterTransaction) {
await this.asset.add(
new Asset({
hash: transaction.hash,
type: transaction.asset.type,
name: transaction.asset.name,
amount: transaction.asset.amount,
precision: transaction.asset.precision,
owner: transaction.asset.owner,
admin: transaction.asset.admin,
issuer: transaction.asset.admin,
expiration: this.currentBlockIndex + 2 * 2000000,
isFrozen: false,
}),
);
} else if (transaction.type === TransactionType.Issue && transaction instanceof IssueTransaction) {
const results = await Promise.all(
Object.entries(
transaction.getTransactionResults({
getOutput: this.output.get,
}),
),
);
await Promise.all(
results.map(async ([assetHex, value]) => {
const hash = common.stringToUInt256(assetHex);
const asset = await this.asset.get({ hash });
await this.asset.update(asset, {
available: asset.available.add(value.neg()),
});
}),
);
} else if (transaction.type === TransactionType.Enrollment && transaction instanceof EnrollmentTransaction) {
await this.validator.add(
new Validator({
publicKey: transaction.publicKey,
}),
);
} else if (transaction.type === TransactionType.Publish && transaction instanceof PublishTransaction) {
const contract = await this.contract.tryGet({
hash: transaction.contract.hash,
});
if (contract === undefined) {
await this.contract.add(transaction.contract);
}
} else if (transaction.type === TransactionType.Invocation && transaction instanceof InvocationTransaction) {
const temporaryBlockchain = new WriteBatchBlockchain({
settings: this.settings,
currentBlock: this.currentBlockInternal,
currentHeader: this.currentHeader,
// tslint:disable-next-line no-any
storage: this as any,
vm: this.vm,
getValidators: this.getValidators,
});
const migratedContractHashes: Array<[UInt160, UInt160]> = [];
const voteUpdates: Array<[UInt160, ReadonlyArray<ECPoint>]> = [];
const actions: Array<NotificationAction | LogAction> = [];
const result = await wrapExecuteScripts(async () =>
this.vm.executeScripts({
monitor: span,
scripts: [{ code: transaction.script }],
blockchain: temporaryBlockchain,
scriptContainer: {
type: ScriptContainerType.Transaction,
value: transaction,
},
triggerType: TriggerType.Application,
action: {
blockIndex: block.index,
blockHash: block.hash,
transactionIndex,
transactionHash: transaction.hash,
},
gas: transaction.gas,
listeners: {
onLog: ({ message, scriptHash }) => {
actions.push(
new LogAction({
index: globalActionIndex,
scriptHash,
message,
}),
);
globalActionIndex = globalActionIndex.add(utils.ONE);
},
onNotify: ({ args, scriptHash }) => {
actions.push(
new NotificationAction({
index: globalActionIndex,
scriptHash,
args,
}),
);
globalActionIndex = globalActionIndex.add(utils.ONE);
},
onMigrateContract: ({ from, to }) => {
migratedContractHashes.push([from, to]);
},
onSetVotes: ({ address, votes }) => {
voteUpdates.push([address, votes]);
},
},
persistingBlock: block,
}),
);
const addActionsPromise = Promise.all(actions.map(async (action) => this.action.add(action)));
if (result instanceof InvocationResultSuccess) {
const assetChangeSet = temporaryBlockchain.asset.getChangeSet();
const assetHash = assetChangeSet
.map(
(change) =>
change.type === 'add' && change.change.type === 'asset' ? change.change.value.hash : undefined,
)
.find((value) => value !== undefined);
const contractsChangeSet = temporaryBlockchain.contract.getChangeSet();
const contractHashes = contractsChangeSet
.map(
(change) =>
change.type === 'add' && change.change.type === 'contract' ? change.change.value.hash : undefined,
)
.filter(commonUtils.notNull);
const deletedContractHashes = contractsChangeSet
.map(
(change) =>
change.type === 'delete' && change.change.type === 'contract' ? change.change.key.hash : undefined,
)
.filter(commonUtils.notNull);
await Promise.all([
Promise.all(
temporaryBlockchain.getChangeSet().map(async (change) => {
if (change.type === 'add') {
// tslint:disable-next-line no-any
await (this.caches[change.change.type] as any).add(change.change.value as any, true);
} else if (change.type === 'delete') {
// tslint:disable-next-line no-any
await (this.caches[change.change.type] as any).delete(change.change.key as any);
}
}),
),
this.invocationData.add(
new InvocationData({
hash: transaction.hash,
assetHash,
contractHashes,
deletedContractHashes,
migratedContractHashes,
voteUpdates,
blockIndex: block.index,
transactionIndex,
actionIndexStart: globalActionIndexIn,
actionIndexStop: globalActionIndex,
result,
}),
),
addActionsPromise,
]);
} else {
await Promise.all([
this.invocationData.add(
new InvocationData({
hash: transaction.hash,
assetHash: undefined,
contractHashes: [],
deletedContractHashes: [],
migratedContractHashes: [],
voteUpdates: [],
blockIndex: block.index,
transactionIndex,
actionIndexStart: globalActionIndexIn,
actionIndexStop: globalActionIndex,
result,
}),
),
addActionsPromise,
]);
}
}
},
{
name: 'neo_write_blockchain_persist_single_transaction',
},
);
return globalActionIndex;
}
private async processStateTransaction(
monitor: Monitor,
validatorChanges: ValidatorChanges,
validatorsCountChanges: ValidatorsCountChanges,
): Promise<void> {
await monitor.captureSpan(
async () => {
const validatorsCount = await this.validatorsCount.tryGet();
const validatorsCountVotes = validatorsCount === undefined ? [] : [...validatorsCount.votes];
// tslint:disable-next-line no-loop-statement
for (const [index, value] of validatorsCountChanges.entries()) {
validatorsCountVotes[index] = value;
}
await Promise.all([
Promise.all(
Object.entries(validatorChanges).map(async ([publicKeyHex, { registered, votes }]) => {
const publicKey = common.hexToECPoint(publicKeyHex);
const validator = await this.validator.tryGet({ publicKey });
if (validator === undefined) {
await this.validator.add(
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 this.validator.delete({ publicKey: validator.publicKey });
} else {
await this.validator.update(validator, { votes, registered });
}
}),
),
validatorsCount === undefined
? this.validatorsCount.add(
new ValidatorsCount({
votes: validatorsCountVotes,
}),
)
: (async () => {
await this.validatorsCount.update(validatorsCount, {
votes: validatorsCountVotes,
});
})(),
]);
},
{
name: 'neo_write_blockchain_process_state_transaction',
},
);
}
private async updateAccounts(
monitor: Monitor,
inputs: ReadonlyArray<Input>,
claims: ReadonlyArray<Input>,
outputs: ReadonlyArray<OutputWithInput>,
accountChanges: AccountChanges = {},
): Promise<void> {
const [inputOutputs, claimOutputs] = await monitor.captureSpan(
async () => Promise.all([this.getInputOutputs(inputs), this.getInputOutputs(claims)]),
{
name: 'neo_write_blockchain_update_accounts_get_input_outputs',
},
);
await monitor.captureSpan(
async () => {
const addressValues = Object.entries(
_.groupBy(
inputOutputs
.map<[UInt160, UInt256, BN]>(({ output }) => [output.address, output.asset, output.value.neg()])
.concat(
outputs.map<[UInt160, UInt256, BN]>(({ output }) => [output.address, output.asset, output.value]),
),
([address]) => common.uInt160ToHex(address),
),
);
const addressSpent = this.groupByAddress(inputOutputs);
const addressClaimed = _.mapValues(this.groupByAddress(claimOutputs), (values) =>
values.map(({ input }) => input),
);
const addressOutputs = _.groupBy(outputs, (output) => common.uInt160ToHex(output.output.address));
await Promise.all(
addressValues.map(async ([address, values]) => {
const spent = addressSpent[address] as ReadonlyArray<OutputWithInput> | undefined;
const claimed = addressClaimed[address] as ReadonlyArray<Input> | undefined;
const outs = addressOutputs[address] as ReadonlyArray<OutputWithInput> | undefined;
const changes = accountChanges[address] as ReadonlyArray<ECPoint> | undefined;
await this.updateAccount(
common.hexToUInt160(address),
// tslint:disable-next-line no-unused
values.map<[UInt256, BN]>(([_address, asset, value]) => [asset, value]),
spent === undefined ? [] : spent,
claimed === undefined ? [] : claimed,
outs === undefined ? [] : outs,
changes === undefined ? [] : changes,
);
}),
);
},
{
name: 'neo_write_blockchain_update_accounts_process',
},
);
}
private getOutputWithInput(transaction: Transaction): ReadonlyArray<OutputWithInput> {
return transaction.outputs.map((output, index) => ({
output,
input: new Input({ hash: transaction.hash, index }),
}));
}
private async getInputOutputs(
inputs: ReadonlyArray<Input>,
): Promise<
ReadonlyArray<{
readonly input: Input;
readonly output: Output;
}>
> {
return Promise.all(
inputs.map(async (input) => {
const output = await this.output.get(input);
return { input, output };
}),
);
}
private groupByAddress(
inputOutputs: ReadonlyArray<OutputWithInput>,
): { readonly [key: string]: ReadonlyArray<OutputWithInput> } {
return _.groupBy(inputOutputs, ({ output }) => common.uInt160ToHex(output.address));
}
private async updateAccount(
address: UInt160,
values: ReadonlyArray<[UInt256, BN]>,
spent: ReadonlyArray<OutputWithInput>,
claimed: ReadonlyArray<Input>,
outputs: ReadonlyArray<OutputWithInput>,
votes: ReadonlyArray<ECPoint>,
): Promise<void> {
const account = await this.account.tryGet({ hash: address });
const balances = values.reduce<{ [asset: string]: BN }>((acc, [asset, value]) => {
const key = common.uInt256ToHex(asset);
if ((acc[key] as BN | undefined) === undefined) {
acc[key] = utils.ZERO;
}
acc[key] = acc[key].add(value);
return acc;
}, account === undefined ? {} : { ...account.balances });
const promises = [];
promises.push(
...spent.map(async ({ input }) =>
this.accountUnspent.delete({
hash: address,
input,
}),
),
);
promises.push(
...outputs.map(async ({ input }) => this.accountUnspent.add(new AccountUnspent({ hash: address, input }))),
);
promises.push(
...claimed.map(async (input) =>
this.accountUnclaimed.delete({
hash: address,
input,
}),
),
);
promises.push(
...spent
.filter(({ output }) => common.uInt256Equal(output.asset, this.settings.governingToken.hash))
.map(async ({ input }) => this.accountUnclaimed.add(new AccountUnclaimed({ hash: address, input }))),
);
if (account === undefined) {
promises.push(
this.account.add(
new Account({
hash: address,
balances,
votes,
}),
),
);
} else {
promises.push(
this.account.update(account, { balances, votes }).then(async (newAccount) => {
if (newAccount.isDeletable()) {
await this.account.delete({ hash: address });
}
}),
);
}
await Promise.all(promises);
}
private async updateCoins(
monitor: Monitor,
inputs: ReadonlyArray<Input>,
claims: ReadonlyArray<Input>,
block: Block,
): Promise<void> {
await monitor.captureSpan(
async () => {
const inputClaims = inputs
.map<InputClaim>((input) => ({ type: 'input', input, hash: input.hash }))
.concat(claims.map<InputClaim>((input) => ({ type: 'claim', input, hash: input.hash })));
const hashInputClaims = Object.entries(_.groupBy(inputClaims, ({ hash }) => common.uInt256ToHex(hash)));
await Promise.all(
hashInputClaims.map(async ([hash, values]) => this.updateCoin(common.hexToUInt256(hash), values, block)),
);
},
{
name: 'neo_write_blockchain_update_coins',
},
);
}
private async updateCoin(hash: UInt256, inputClaims: ReadonlyArray<InputClaim>, block: Block): Promise<void> {
const spentCoins = await this.transactionData.get({ hash });
const endHeights = { ...spentCoins.endHeights };
const claimed = { ...spentCoins.claimed };
// tslint:disable-next-line no-loop-statement
for (const inputClaim of inputClaims) {
if (inputClaim.type === 'input') {
endHeights[inputClaim.input.index] = block.index;
} else {
claimed[inputClaim.input.index] = true;
}
}
await this.transactionData.update(spentCoins, {
endHeights,
claimed,
});
}
}
/*
Possibly broken on TestNet:
if (
block.index !== 31331 && // Just seems like a bad script - unknown op
block.index !== 62024 && // Invalid order for Account arguments
block.index !== 131854 && // Calls contract without storage
block.index !== 163432 && // Calls contract without storage
block.index !== 163446 && // Calls contract without storage
block.index !== 163457 && // Calls contract without storage
block.index !== 163470 && // Calls contract without storage
block.index !== 163484 && // Calls contract without storage
block.index !== 163491 && // Calls contract without storage
block.index !== 163512 && // Calls contract without storage
block.index !== 460363 && // PICKITEM on non-array.
block.index !== 460376 && // PICKITEM on non-array.
block.index !== 460393 && // PICKITEM on non-array.
block.index !== 460410 && // PICKITEM on non-array.
block.index !== 561159 && // Bug in contract code - no inputs for transaction
block.index !== 568381 && // Bug in contract code - no inputs for transaction
block.index !== 572375 && // Bug in contract code - no inputs for transaction
block.index !== 608107 && // Unknown OP 0xDB (219)
block.index !== 608111 && // Unknown OP 0xDB (219)
block.index !== 608135 && // Unknown OP 0x70 (112)
block.index !== 609278 && // Unknown OP 0x70 (112)
block.index !== 609402 && // Unknown OP 0x70 (112)
block.index !== 609408 && // Unknown OP 0x70 (112)
block.index !== 609504 &&
block.index !== 609513 && // Unknown op: 0x70 (112)
block.index !== 637192 && // Seems like a bad argument to CheckWitness
!error.message.includes('Unknown op: 112') &&
!error.message.includes('Script execution threw an Error')
) {
console.log(block.index);
console.error(error);
throw error;
}
*/