/*
* Copyright (C) 2018 Matus Zamborsky & The ontology Authors
* This file is part of The ontology library.
*
* The ontology is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The ontology is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with The ontology. If not, see .
*/
import { Address } from '../common/address';
import { TracedError } from '../common/error';
import { Uint256 } from '../common/uint256';
import { LedgerStore } from '../core/ledgerStore';
import { isDeployCode } from '../core/payload/deployCode';
import { ST_CONTRACT } from '../core/state/dataEntryPrefix';
import { StateItem, StateStore } from '../core/state/stateStore';
import { Transaction } from '../core/transaction';
import { PublicKey } from '../crypto/publicKey';
import { Signature } from '../crypto/signature';
import { LogCallback, LogEventInfo, NotificationCallback, NotifyEventInfo } from '../event/notifyEvents';
import { MAX_BYTEARRAY_SIZE } from '../vm/consts';
import { ExecutionContext } from '../vm/executionContext';
import { evaluationStackCount, peekStackItem, popByteArray, pushData } from '../vm/func/common';
import { ExecutionEngine, FAULT } from '../vm/interfaces/engine';
import * as O from '../vm/opCode';
import { isArrayType } from '../vm/types/array';
import { StackItem } from '../vm/types/stackItem';
import { isStructType } from '../vm/types/struct';
import { MAX_STACK_SIZE, OPCODE_GAS } from './consts';
import { ContextRef, InvokeOptions, VmService } from './context';
import * as errors from './errors';
import { gasPrice } from './gasCost';
import { ServiceMap } from './serviceMap';
const BYTE_ZERO_20: Buffer = new Buffer([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
interface NeoVmServiceOptions {
store: LedgerStore;
stateStore: StateStore;
contextRef: ContextRef;
code: Buffer;
tx: Transaction;
time: number;
// height: number; - unused
randomHash: Uint256;
engine: ExecutionEngine;
notificationCallback?: NotificationCallback;
logCallback?: LogCallback;
}
export class NeoVmService implements VmService {
private store: LedgerStore;
private stateStore: StateStore;
private contextRef: ContextRef;
private notifications: NotifyEventInfo[];
private logs: LogEventInfo[];
private code: Buffer;
private tx: Transaction;
private time: number;
// private height: number; - unused
private randomHash: Uint256;
private engine: ExecutionEngine;
private notificationCallback?: NotificationCallback;
private logCallback?: LogCallback;
constructor(options: NeoVmServiceOptions) {
this.store = options.store;
this.stateStore = options.stateStore;
this.contextRef = options.contextRef;
this.code = options.code;
this.tx = options.tx;
this.time = options.time;
// this.height = options.height;
this.randomHash = options.randomHash;
this.engine = options.engine;
this.notifications = [];
this.logs = [];
this.notificationCallback = options.notificationCallback;
this.logCallback = options.logCallback;
}
getTx() {
return this.tx;
}
getTime() {
return this.time;
}
getEngine() {
return this.engine;
}
getContextRef() {
return this.contextRef;
}
getStateStore() {
return this.stateStore;
}
getStore() {
return this.store;
}
getNotifications() {
return this.notifications;
}
getLogs() {
return this.logs;
}
getRandomHash() {
return this.randomHash;
}
// Invoke a smart contract
async invoke({ inspect = () => Promise.resolve(true) }: InvokeOptions): Promise {
if (this.code.length === 0) {
throw errors.ERR_EXECUTE_CODE;
}
const contractAddress = Address.parseFromVmCode(this.code);
this.contextRef.pushContext({ contractAddress, code: this.code });
this.engine.pushContext(new ExecutionContext(this.code));
while (true) {
// check the execution step count
if (!this.contextRef.checkExecStep()) {
throw errors.VM_EXEC_STEP_EXCEED;
}
if (this.engine.getContexts().length === 0) {
break;
}
if (this.engine.getContext().getInstructionPointer() >= this.engine.getContext().getCode().length) {
break;
}
const instructionPointer = this.engine.getContext().getInstructionPointer();
this.engine.executeCode();
if (this.engine.getContext().getInstructionPointer() < this.engine.getContext().getCode().length) {
if (!this.checkStackSize()) {
throw errors.ERR_CHECK_STACK_SIZE;
}
}
const opCode = this.engine.getOpCode();
let opName = '';
if (opCode >= O.PUSHBYTES1 && opCode <= O.PUSHBYTES75) {
opName = `PUSHBYTES${opCode}`;
if (!this.contextRef.checkUseGas(OPCODE_GAS)) {
throw errors.ERR_GAS_INSUFFICIENT;
}
} else {
this.engine.validateOp();
opName = this.engine.getOpExec().name;
const price = gasPrice(this.engine, opName);
if (!this.contextRef.checkUseGas(price)) {
throw errors.ERR_GAS_INSUFFICIENT;
}
}
const evaluationStack = this.getEngine().getEvaluationStack();
const altStack = this.getEngine().getAltStack();
const inspectionResult = await inspect({
opCode,
opName,
contractAddress,
instructionPointer,
evaluationStack,
altStack,
contexts: this.getEngine().getContexts()
});
if (!inspectionResult) {
return;
}
switch (this.engine.getOpCode()) {
case O.VERIFY:
if (evaluationStackCount(this.engine) < 3) {
throw new TracedError('[VERIFY] Too few input parameters ');
}
const pubKey = popByteArray(this.engine);
const key = PublicKey.deserialize(pubKey);
const sig = popByteArray(this.engine);
const data = popByteArray(this.engine);
const signature = Signature.deserialize(sig);
if (!key.verify(data, signature)) {
pushData(this.engine, false);
} else {
pushData(this.engine, true);
}
break;
case O.SYSCALL:
this.systemCall();
break;
case O.APPCALL:
let address = this.engine
.getContext()
.getReader()
.readBytes(20);
if (address.compare(BYTE_ZERO_20) === 0) {
if (evaluationStackCount(this.engine) < 1) {
throw new TracedError(`[Appcall] Too few input parameters:${evaluationStackCount(this.engine)}`);
}
try {
address = popByteArray(this.engine);
} catch (e) {
throw new TracedError(`[Appcall] pop contract address error.`, e);
}
if (address.length !== 20) {
throw new TracedError(`[Appcall] pop contract address len != 20:${address}`);
}
}
const code = this.getContract(address);
const service = this.contextRef.newExecuteEngine(code);
this.engine.getEvaluationStack().copyTo(service.getEngine().getEvaluationStack());
const result = await service.invoke({ inspect });
if (result !== undefined) {
pushData(this.engine, result);
}
break;
default:
const err = this.engine.stepInto();
if (err !== undefined) {
throw new TracedError(`[NeoVmService] vm execute error!`, err);
}
if (this.engine.getState() === FAULT) {
throw errors.VM_EXEC_FAULT;
}
}
}
this.contextRef.popContext();
this.contextRef.pushNotifications(this.notifications);
this.contextRef.pushLogs(this.logs);
if (this.engine.getEvaluationStack().count() !== 0) {
return this.engine.getEvaluationStack().peek(0);
}
}
/**
* SystemCall provide register service for smart contract to interaction with blockchain
*/
systemCall() {
const serviceName = this.engine
.getContext()
.getReader()
.readVarString(MAX_BYTEARRAY_SIZE);
const service = ServiceMap.get(serviceName);
if (service === undefined) {
throw new TracedError(`[SystemCall] service not support: ${serviceName}`);
}
const price = gasPrice(this.engine, serviceName);
if (!this.contextRef.checkUseGas(price)) {
throw errors.ERR_GAS_INSUFFICIENT;
}
if (service.validator !== undefined) {
try {
service.validator(this.engine);
} catch (e) {
throw new TracedError(`[SystemCall] service validator error.`, e);
}
}
try {
service.execute(this, this.engine);
} catch (e) {
throw new TracedError(`[SystemCall] service execute error.`, e);
}
}
/**
* DUMMY: this method will call other SC.
* Need to devise a way how to use it in test VM
* @param address
*/
getContract(address: Buffer): Buffer {
let item: StateItem | undefined;
try {
item = this.stateStore.get(ST_CONTRACT, address);
} catch (e) {
throw new TracedError('[getContract] Get contract context error!', e);
}
// log.Debugf("invoke contract address:%x", scommon.ToArrayReverse(address))
if (item === undefined) {
throw errors.CONTRACT_NOT_EXIST;
}
const contract = item.value;
if (!isDeployCode(contract)) {
throw errors.DEPLOYCODE_TYPE_ERROR;
}
return contract.getCode();
}
checkStackSize(): boolean {
let size = 0;
const opCode = this.engine.getOpCode();
if (opCode < O.PUSH16) {
size = 1;
} else {
switch (opCode) {
case O.DEPTH:
case O.DUP:
case O.OVER:
case O.TUCK:
size = 1;
break;
case O.UNPACK:
if (this.engine.getEvaluationStack().count() === 0) {
return false;
}
const item = peekStackItem(this.engine);
if (isArrayType(item)) {
size = item.count();
} else if (isStructType(item)) {
size = item.count();
}
}
}
size += this.engine.getEvaluationStack().count() + this.engine.getAltStack().count();
if (size > MAX_STACK_SIZE) {
return false;
}
return true;
}
addNotification(event: NotifyEventInfo) {
this.notifications.push(event);
if (this.notificationCallback !== undefined) {
this.notificationCallback(event);
}
}
addLog(event: LogEventInfo) {
this.logs.push(event);
if (this.logCallback !== undefined) {
this.logCallback(event);
}
}
getNotificationCallback() {
return this.notificationCallback;
}
}