vaporyjs-vm
Version:
an vapory VM implementation
259 lines (225 loc) • 7.04 kB
JavaScript
/*
This is the core of the Vapory Virtual Machine (EVM or just VM).
NOTES:
stack items are lazly dupilicated.
So you must never directly change a buffer from the stack,
instead you should `copy` it first
not all stack items are 32 bytes, so if the operation realies on the stack
item length then you must use utils.pad(<item>, 32) first.
*/
const async = require('async')
const utils = require('vaporyjs-util')
const Block = require('vaporyjs-block')
const lookupOpInfo = require('./opcodes.js')
const opFns = require('./opFns.js')
const constants = require('./constants.js')
const setImmediate = require('timers').setImmediate
const BN = utils.BN
const ERROR = constants.ERROR
/**
* Runs EVM code
* @param opts
* @param opts.account {Account} the account that the exucuting code belongs to
* @param opts.address {Buffer} the address of the account that is exucuting this code
* @param opts.block {Block} the block that the transaction is part of
* @param opts.caller {Buffer} the address that ran this code
* @param opts.code {Buffer} the code to be run
* @param opts.data {Buffer} the input data
* @param opts.gasLimit {Buffer}
* @param opts.origin {Buffer} the address where the call originated from
* @param opts.value {Buffer} the amount the being transfered
* @param cb {Function}
*/
module.exports = function (opts, cb) {
var self = this
var stateManager = self.stateManager
var block = opts.block || new Block()
// VM internal state
var runState = {
stateManager: stateManager,
returnValue: false,
stopped: false,
vmError: false,
suicideTo: undefined,
programCounter: 0,
opCode: undefined,
opName: undefined,
gasLeft: new BN(opts.gasLimit),
gasLimit: new BN(opts.gasLimit),
gasPrice: opts.gasPrice,
memory: [],
memoryWordCount: 0,
stack: [],
logs: [],
validJumps: [],
gasRefund: new BN(0),
highestMemCost: new BN(0),
depth: opts.depth || 0,
suicides: opts.suicides || {},
block: block,
callValue: opts.value || new BN(0),
address: opts.address || utils.zeros(32),
caller: opts.caller || utils.zeros(32),
origin: opts.origin || opts.caller || utils.zeros(32),
callData: opts.data || Buffer.from([0]),
code: opts.code,
populateCache: opts.populateCache === undefined ? true : opts.populateCache,
enableHomestead: this.opts.enableHomestead === undefined ? block.isHomestead() : this.opts.enableHomestead // this == vm
}
// temporary - to be factored out
runState._precompiled = self._precompiled
runState._vm = self
if (!runState.enableHomestead) {
delete opFns.DELEGATECALL
} else {
opFns.DELEGATECALL = opFns._DC
}
// prepare to run vm
preprocessValidJumps(runState)
// load contract then start vm run
loadContract(runVm)
// iterate through the given ops until something breaks or we hit STOP
function runVm () {
async.whilst(vmIsActive, iterateVm, parseVmResults)
}
// ensure contract is loaded; only used if runCode is called directly
function loadContract (cb) {
stateManager.getAccount(runState.address, function (err, account) {
if (err) return cb(err)
runState.contract = account
cb()
})
}
function vmIsActive () {
var notAtEnd = runState.programCounter < runState.code.length
return !runState.stopped && notAtEnd && !runState.vmError && !runState.returnValue
}
function iterateVm (done) {
if (runState.stack.length > 1024) {
return done(ERROR.INVALID_OPCODE)
}
var opCode = runState.code[runState.programCounter]
var opInfo = lookupOpInfo(opCode)
var opName = opInfo.name
var opFn = opFns[opName]
runState.opName = opName
runState.opCode = opCode
// check for invalid opcode
if (opName === 'INVALID') {
return done(ERROR.INVALID_OPCODE)
}
// check for stack underflows
if (runState.stack.length < opInfo.in) {
return done(ERROR.STACK_UNDERFLOW)
}
async.series([
runStepHook,
runOp
], function (err) {
setImmediate(done.bind(null, err))
})
function runStepHook (cb) {
var eventObj = {
pc: runState.programCounter,
gasLeft: runState.gasLeft,
opcode: lookupOpInfo(opCode, true),
stack: runState.stack,
depth: runState.depth,
address: runState.address,
account: runState.contract,
cache: runState.stateManager.cache,
memory: runState.memory
}
self.emit('step', eventObj, cb)
}
function runOp (cb) {
// calculate gas
var fee = new BN(opInfo.fee)
// TODO: move to a shared funtion; subGas in opFuns
runState.gasLeft = runState.gasLeft.sub(fee)
if (runState.gasLeft.cmpn(0) === -1) {
runState.vmError = ERROR.OUT_OF_GAS
cb()
return
}
// advance program counter
runState.programCounter++
var argsNum = opInfo.in
// pop the stack
var args = argsNum ? runState.stack.splice(-opInfo.in) : []
args.reverse()
args.push(runState)
// create a callback for async opFunc
args.push(function (err, val) {
// save result to the stack
if (val) {
runState.stack.push(val)
}
cb(err)
})
try {
// run the opcode
var result = opFn.apply(null, args)
} catch (e) {
runState.vmError = e.error
cb()
return
}
// save result to the stack
if (result) {
runState.stack.push(result)
}
// call the callback if opFn was sync
if (opFn.length - argsNum !== 2) {
cb()
}
}
}
function parseVmResults (err) {
err = runState.vmError || err
// remove any logs on error
if (err) {
runState.logs = []
stateManager.revertContracts()
}
var results = {
suicides: runState.suicides,
suicideTo: runState.suicideTo,
gasRefund: runState.gasRefund,
exception: err ? 0 : 1,
exceptionError: err,
logs: runState.logs,
gas: runState.gasLeft,
'return': runState.returnValue ? runState.returnValue : Buffer.from([])
}
if (results.exceptionError) {
delete results.gasRefund
}
if (err) {
results.gasUsed = runState.gasLimit
} else {
results.gasUsed = runState.gasLimit.sub(runState.gasLeft)
}
if (runState.populateCache) {
self.stateManager.cache.flush(function () {
self.stateManager.cache.clear()
cb(err, results)
})
} else {
cb(err, results)
}
}
}
// find all the valid jumps and puts them in the `validJumps` array
function preprocessValidJumps (runState) {
for (var i = 0; i < runState.code.length; i++) {
var curOpCode = lookupOpInfo(runState.code[i]).name
// no destinations into the middle of PUSH
if (curOpCode === 'PUSH') {
i += runState.code[i] - 0x5f
}
if (curOpCode === 'JUMPDEST') {
runState.validJumps.push(i)
}
}
}