UNPKG

vaporyjs-vm

Version:
879 lines (790 loc) 25.1 kB
const async = require('async') const fees = require('vapory-common') const utils = require('vaporyjs-util') const BN = utils.BN const constants = require('./constants.js') const logTable = require('./logTable.js') const ERROR = constants.ERROR const MAX_INT = 9007199254740991 // the opcode functions module.exports = { STOP: function (runState) { runState.stopped = true }, ADD: function (a, b, runState) { return Buffer.from( new BN(a) .add(new BN(b)) .mod(utils.TWO_POW256) .toArray()) }, MUL: function (a, b, runState) { return Buffer.from( new BN(a) .mul(new BN(b)) .mod(utils.TWO_POW256) .toArray() ) }, SUB: function (a, b, runState) { return utils.toUnsigned( new BN(a) .sub(new BN(b)) ) }, DIV: function (a, b, runState) { a = new BN(a) b = new BN(b) var r if (b.isZero()) { r = [0] } else { r = a.div(b).toArray() } return Buffer.from(r) }, SDIV: function (a, b, runState) { a = utils.fromSigned(a) b = utils.fromSigned(b) var r if (b.isZero()) { r = Buffer.from([0]) } else { r = utils.toUnsigned(a.div(b)) } return r }, MOD: function (a, b, runState) { a = new BN(a) b = new BN(b) var r if (b.isZero()) { r = [0] } else { r = a.mod(b).toArray() } return Buffer.from(r) }, SMOD: function (a, b, runState) { a = utils.fromSigned(a) b = utils.fromSigned(b) var r if (b.isZero()) { r = Buffer.from([0]) } else { r = a.abs().mod(b.abs()) if (a.isNeg()) { r = r.neg() } r = utils.toUnsigned(r) } return r }, ADDMOD: function (a, b, c, runState) { a = new BN(a).add(new BN(b)) c = new BN(c) var r if (c.isZero()) { r = [0] } else { r = a.mod(c).toArray() } return Buffer.from(r) }, MULMOD: function (a, b, c, runState) { a = new BN(a).mul(new BN(b)) c = new BN(c) var r if (c.isZero()) { r = [0] } else { r = a.mod(c).toArray() } return Buffer.from(r) }, EXP: function (base, exponent, runState) { base = new BN(base) exponent = new BN(exponent) var m = BN.red(utils.TWO_POW256) var result base = base.toRed(m) if (!exponent.isZero()) { var bytes = 1 + logTable(exponent) subGas(runState, new BN(bytes).muln(fees.expByteGas.v)) result = Buffer.from(base.redPow(exponent).toArray()) } else { result = Buffer.from([1]) } return result }, SIGNEXTEND: function (k, runState) { k = new BN(k) var extendOnes = false if (k.cmpn(31) <= 0) { k = k.toNumber() var val = Buffer.from(utils.setLengthLeft(runState.stack.pop(), 32)) if (val[31 - k] & 0x80) { extendOnes = true } // 31-k-1 since k-th byte shouldn't be modified for (var i = 30 - k; i >= 0; i--) { val[i] = extendOnes ? 0xff : 0 } return val } }, // 0x10 range - bit ops LT: function (a, b, runState) { return Buffer.from([ new BN(a) .cmp(new BN(b)) === -1 ]) }, GT: function (a, b, runState) { return Buffer.from([ new BN(a) .cmp(new BN(b)) === 1 ]) }, SLT: function (a, b, runState) { runState.stack.push( Buffer.from([ utils.fromSigned(a) .cmp(utils.fromSigned(b)) === -1 ]) ) }, SGT: function (a, b, runState) { return Buffer.from([ utils.fromSigned(a) .cmp(utils.fromSigned(b)) === 1 ]) }, EQ: function (a, b, runState) { a = utils.unpad(a) b = utils.unpad(b) return Buffer.from([a.toString('hex') === b.toString('hex')]) }, ISZERO: function (a, runState) { a = utils.bufferToInt(a) return Buffer.from([!a]) }, AND: function (a, b, runState) { return Buffer.from(( new BN(a) .and( new BN(b) ) ) .toArray()) }, OR: function (a, b, runState) { return Buffer.from(( new BN(a) .or( new BN(b) ) ) .toArray()) }, XOR: function (a, b, runState) { return Buffer.from(( new BN(a) .xor( new BN(b) ) ) .toArray()) }, NOT: function (a, runState) { return Buffer.from(new BN(a).inotn(256).toArray()) }, BYTE: function (pos, word, runState) { pos = utils.bufferToInt(pos) word = utils.setLengthLeft(word, 32) var byte if (pos < 32) { byte = utils.intToBuffer(word[pos]) } else { byte = Buffer.from([0]) } return byte }, // 0x20 range - crypto SHA3: function (offset, length, runState) { offset = utils.bufferToInt(offset) length = utils.bufferToInt(length) var data = memLoad(runState, offset, length) // copy fee subGas(runState, new BN(fees.sha3WordGas.v).imuln(Math.ceil(length / 32))) return utils.sha3(data) }, // 0x30 range - closure state ADDRESS: function (runState) { return runState.address }, BALANCE: function (address, runState, cb) { var stateManager = runState.stateManager // stack to address address = utils.setLengthLeft(address.slice(-20), 20) // shortcut if current account if (address.toString('hex') === runState.address.toString('hex')) { cb(null, runState.contract.balance) return } // otherwise load account then return balance stateManager.getAccountBalance(address, cb) }, ORIGIN: function (runState) { return runState.origin }, CALLER: function (runState) { return runState.caller }, CALLVALUE: function (runState) { return runState.callValue }, CALLDATALOAD: function (pos, runState) { pos = utils.bufferToInt(pos) var loaded = runState.callData.slice(pos, pos + 32) loaded = loaded.length ? loaded : Buffer.from([0]) return utils.setLengthRight(loaded, 32) }, CALLDATASIZE: function (runState) { if (runState.callData.length === 1 && runState.callData[0] === 0) { return Buffer.from([0]) } else { return utils.intToBuffer(runState.callData.length) } }, CALLDATACOPY: function (memOffset, dataOffsetBuf, dataLength, runState) { memOffset = utils.bufferToInt(memOffset) dataLength = utils.bufferToInt(dataLength) var dataOffset = utils.bufferToInt(dataOffsetBuf) memStore(runState, memOffset, runState.callData, dataOffset, dataLength) // sub the COPY fee subGas(runState, new BN(Number(fees.copyGas.v) * Math.ceil(dataLength / 32))) }, CODESIZE: function (runState) { return utils.intToBuffer(runState.code.length) }, CODECOPY: function (memOffset, codeOffset, length, runState) { memOffset = utils.bufferToInt(memOffset) codeOffset = utils.bufferToInt(codeOffset) length = utils.bufferToInt(length) memStore(runState, memOffset, runState.code, codeOffset, length) // sub the COPY fee subGas(runState, new BN(fees.copyGas.v * Math.ceil(length / 32))) }, EXTCODESIZE: function (address, runState, cb) { var stateManager = runState.stateManager address = address.slice(-20) stateManager.getContractCode(address, function (err, code) { cb(err, utils.intToBuffer(code.length)) }) }, EXTCODECOPY: function (address, memOffset, codeOffset, length, runState, cb) { var stateManager = runState.stateManager address = address.slice(-20) memOffset = utils.bufferToInt(memOffset) codeOffset = utils.bufferToInt(codeOffset) length = utils.bufferToInt(length) subMemUsage(runState, memOffset, length) // copy fee subGas(runState, new BN(fees.copyGas.v).imuln(Math.ceil(length / 32))) stateManager.getContractCode(address, function (err, code) { code = err ? Buffer.from([0]) : code memStore(runState, memOffset, code, codeOffset, length, false) cb(err) }) }, GASPRICE: function (runState) { runState.stack.push(runState.gasPrice) }, // '0x40' range - block operations BLOCKHASH: function (number, runState, cb) { var stateManager = runState.stateManager number = utils.bufferToInt(number) var diff = utils.bufferToInt(runState.block.header.number) - utils.bufferToInt(number) // block lookups must be within the past 256 blocks if (diff > 256 || diff <= 0) { cb(null, Buffer.from([0])) return } stateManager.getBlockHash(number, function (err, blockHash) { if (err) { // if we are at a low block height and request a blockhash before the genesis block cb(null, Buffer.from([0])) } else { cb(null, blockHash) } }) }, COINBASE: function (runState) { return runState.block.header.coinbase }, TIMESTAMP: function (runState) { return runState.block.header.timestamp }, NUMBER: function (runState) { return runState.block.header.number }, DIFFICULTY: function (runState) { return runState.block.header.difficulty }, GASLIMIT: function (runState) { return runState.block.header.gasLimit }, // 0x50 range - 'storage' and execution POP: function () {}, MLOAD: function (pos, runState) { pos = utils.bufferToInt(pos) var loaded = utils.unpad(memLoad(runState, pos, 32)) return loaded }, MSTORE: function (offset, word, runState) { offset = utils.bufferToInt(offset) word = utils.setLengthLeft(word, 32) memStore(runState, offset, word, 0, 32) }, MSTORE8: function (offset, byte, runState) { offset = utils.bufferToInt(offset) // grab the last byte byte = byte.slice(byte.length - 1) memStore(runState, offset, byte, 0, 1) }, SLOAD: function (key, runState, cb) { var stateManager = runState.stateManager key = utils.setLengthLeft(key, 32) stateManager.getContractStorage(runState.address, key, function (err, value) { if (err) return cb(err) value = value.length ? value : Buffer.from([0]) cb(null, value) }) }, SSTORE: function (key, val, runState, cb) { var stateManager = runState.stateManager var address = runState.address key = utils.setLengthLeft(key, 32) var value = utils.unpad(val) stateManager.getContractStorage(runState.address, key, function (err, found) { if (err) return cb(err) try { if (value.length === 0 && !found.length) { subGas(runState, new BN(fees.sstoreResetGas.v)) } else if (value.length === 0 && found.length) { subGas(runState, new BN(fees.sstoreResetGas.v)) runState.gasRefund.iadd(new BN(fees.sstoreRefundGas.v)) } else if (value.length !== 0 && !found.length) { subGas(runState, new BN(fees.sstoreSetGas.v)) } else if (value.length !== 0 && found.length) { subGas(runState, new BN(fees.sstoreResetGas.v)) } } catch (e) { cb(e.error) return } stateManager.putContractStorage(address, key, value, function (err) { if (err) return cb(err) runState.contract = stateManager.cache.get(address) cb() }) }) }, JUMP: function (dest, runState) { dest = utils.bufferToInt(dest) if (!jumpIsValid(runState, dest)) { trap(ERROR.INVALID_JUMP) } runState.programCounter = dest }, JUMPI: function (c, i, runState) { c = utils.bufferToInt(c) i = utils.bufferToInt(i) var dest = i ? c : runState.programCounter if (i && !jumpIsValid(runState, dest)) { trap(ERROR.INVALID_JUMP) } runState.programCounter = dest }, PC: function (runState) { return utils.intToBuffer(runState.programCounter - 1) }, MSIZE: function (runState) { return utils.intToBuffer(runState.memoryWordCount * 32) }, GAS: function (runState) { return Buffer.from(runState.gasLeft.toArray()) }, JUMPDEST: function (runState) {}, PUSH: function (runState) { var numToPush = runState.opCode - 0x5f var loaded = utils.unpad(runState.code.slice(runState.programCounter, runState.programCounter + numToPush)) runState.programCounter += numToPush return loaded }, DUP: function (runState) { const stackPos = runState.opCode - 0x7f if (stackPos > runState.stack.length) { trap(ERROR.STACK_UNDERFLOW) } // dupilcated stack items point to the same Buffer return runState.stack[runState.stack.length - stackPos] }, SWAP: function (runState) { var stackPos = runState.opCode - 0x8f // check the stack to make sure we have enough items on teh stack var swapIndex = runState.stack.length - stackPos - 1 if (swapIndex < 0) { trap(ERROR.STACK_UNDERFLOW) } // preform the swap var newTop = runState.stack[swapIndex] runState.stack[swapIndex] = runState.stack.pop() return newTop }, LOG: function (memOffset, memLength) { var args = Array.prototype.slice.call(arguments, 0) args.pop() // pop off callback var runState = args.pop() var topics = args.slice(2) topics = topics.map(function (a) { return utils.setLengthLeft(a, 32) }) memOffset = utils.bufferToInt(memOffset) memLength = utils.bufferToInt(memLength) const numOfTopics = runState.opCode - 0xa0 const mem = memLoad(runState, memOffset, memLength) subGas(runState, new BN(numOfTopics * fees.logTopicGas.v + memLength * fees.logDataGas.v)) // add address var log = [runState.address] log.push(topics) // add data log.push(mem) runState.logs.push(log) }, // '0xf0' range - closures CREATE: function (value, offset, length, runState, done) { value = new BN(value) offset = utils.bufferToInt(offset) length = utils.bufferToInt(length) // set up config var options = { value: value } var localOpts = { inOffset: offset, inLength: length } checkCallMemCost(runState, options, localOpts) makeCall(runState, options, localOpts, done) }, CALL: function (gasLimit, toAddress, value, inOffset, inLength, outOffset, outLength, runState, done) { var stateManager = runState.stateManager gasLimit = new BN(gasLimit) toAddress = utils.setLengthLeft(toAddress, 20) value = new BN(value) inOffset = utils.bufferToInt(inOffset) inLength = utils.bufferToInt(inLength) outOffset = utils.bufferToInt(outOffset) outLength = utils.bufferToInt(outLength) var data = memLoad(runState, inOffset, inLength) var options = { gasLimit: gasLimit, value: value, to: toAddress, data: data } var localOpts = { inOffset: inOffset, inLength: inLength, outOffset: outOffset, outLength: outLength } // add stipend if (!value.isZero()) { runState.gasLeft.iadd(new BN(fees.callStipend.v)) subGas(runState, new BN(fees.callValueTransferGas.v)) options.gasLimit.iadd(new BN(fees.callStipend.v)) } checkCallMemCost(runState, options, localOpts) stateManager.exists(toAddress, function (err, exists) { if (err) { done(err) return } if (!exists) { // can't wrap because we are in a callback runState.gasLeft.isub(new BN(fees.callNewAccountGas.v)) } makeCall(runState, options, localOpts, done) }) }, CALLCODE: function (gas, toAddress, value, inOffset, inLength, outOffset, outLength, runState, done) { var stateManager = runState.stateManager gas = new BN(gas) toAddress = utils.setLengthLeft(toAddress, 20) value = new BN(value) inOffset = utils.bufferToInt(inOffset) inLength = utils.bufferToInt(inLength) outOffset = utils.bufferToInt(outOffset) outLength = utils.bufferToInt(outLength) const options = { gasLimit: gas, value: value, to: runState.address } const localOpts = { inOffset: inOffset, inLength: inLength, outOffset: outOffset, outLength: outLength } // add stipend if (!value.isZero()) { runState.gasLeft.isub(new BN(fees.callValueTransferGas.v)).iadd(new BN(fees.callStipend.v)) options.gasLimit.iadd(new BN(fees.callStipend.v)) } checkCallMemCost(runState, options, localOpts) // load the code stateManager.getAccount(toAddress, function (err, account) { if (err) return done(err) if (utils.isPrecompiled(toAddress)) { options.compiled = true options.code = runState._precompiled[toAddress.toString('hex')] makeCall(runState, options, localOpts, done) } else { stateManager.getContractCode(toAddress, function (err, code, compiled) { if (err) return done(err) options.code = code options.compiled = compiled makeCall(runState, options, localOpts, done) }) } }) }, DELEGATECALL: function (gas, toAddress, inOffset, inLength, outOffset, outLength, runState, done) { var stateManager = runState.stateManager var value = runState.callValue gas = new BN(gas) toAddress = utils.setLengthLeft(toAddress, 20) inOffset = utils.bufferToInt(inOffset) inLength = utils.bufferToInt(inLength) outOffset = utils.bufferToInt(outOffset) outLength = utils.bufferToInt(outLength) const options = { gasLimit: gas, value: value, to: runState.address, caller: runState.caller } const localOpts = { inOffset: inOffset, inLength: inLength, outOffset: outOffset, outLength: outLength } checkCallMemCost(runState, options, localOpts) // load the code stateManager.getAccount(toAddress, function (err, account) { if (err) return done(err) if (utils.isPrecompiled(toAddress)) { options.compiled = true options.code = runState._precompiled[toAddress.toString('hex')] makeCall(runState, options, localOpts, done) } else { stateManager.getContractCode(toAddress, function (err, code, compiled) { if (err) return done(err) options.code = code options.compiled = compiled makeCall(runState, options, localOpts, done) }) } }) }, RETURN: function (offset, length, runState) { offset = utils.bufferToInt(offset) length = utils.bufferToInt(length) runState.returnValue = memLoad(runState, offset, length) }, // '0x70', range - other SUICIDE: function (suicideToAddress, runState, cb) { var stateManager = runState.stateManager var contract = runState.contract var contractAddress = runState.address suicideToAddress = utils.setLengthLeft(suicideToAddress, 20) // only add to refund if this is the first suicide for the address if (!runState.suicides[contractAddress.toString('hex')]) { runState.gasRefund = runState.gasRefund.add(new BN(fees.suicideRefundGas.v)) } runState.suicideTo = suicideToAddress runState.suicides[contractAddress.toString('hex')] = suicideToAddress runState.stopped = true stateManager.getAccount(suicideToAddress, function (err, toAccount) { // update balances if (err) { cb(err) return } var newBalance = Buffer.from(new BN(contract.balance).add(new BN(toAccount.balance)).toArray()) async.series([ stateManager.putAccountBalance.bind(stateManager, suicideToAddress, newBalance), stateManager.putAccountBalance.bind(stateManager, contractAddress, new BN(0)) ], cb) }) } } module.exports._DC = module.exports.DELEGATECALL function subGas (runState, amount) { runState.gasLeft.isub(amount) if (runState.gasLeft.cmpn(0) === -1) { trap(ERROR.OUT_OF_GAS) } } function trap (err) { function VmError (error) { this.error = error } throw new VmError(err) } /** * Subtracts the amount needed for memory usage from `runState.gasLeft` * @method subMemUsage * @param {Number} offset * @param {Number} length * @return {String} */ function subMemUsage (runState, offset, length) { // abort if no usage if (!length) return // hacky: if the dataOffset is larger than the largest safeInt then just // load 0's because if tx.data did have that amount of data then the fee // would be high than the maxGasLimit in the block if (offset > MAX_INT || length > MAX_INT) { trap(ERROR.OUT_OF_GAS) } var newMemoryWordCount = Math.ceil((offset + length) / 32) runState.memoryWordCount = Math.max(newMemoryWordCount, runState.memoryWordCount) var words = new BN(newMemoryWordCount) var fee = new BN(fees.memoryGas.v) var quadCoeff = new BN(fees.quadCoeffDiv.v) var cost = words.mul(fee).add(words.mul(words).div(quadCoeff)) if (cost.cmp(runState.highestMemCost) === 1) { subGas(runState, cost.sub(runState.highestMemCost)) runState.highestMemCost = cost } } /** * Loads bytes from memory and returns them as a buffer. If an error occurs * a string is instead returned. The function also subtracts the amount of * gas need for memory expansion. * @method memLoad * @param {Number} offset where to start reading from * @param {Number} length how far to read * @return {Buffer|String} */ function memLoad (runState, offset, length) { // check to see if we have enougth gas for the mem read subMemUsage(runState, offset, length) var loaded = runState.memory.slice(offset, offset + length) // fill the remaining lenth with zeros for (var i = loaded.length; i < length; i++) { loaded.push(0) } return Buffer.from(loaded) } /** * Stores bytes to memory. If an error occurs a string is instead returned. * The function also subtracts the amount of gas need for memory expansion. * @method memStore * @param {Number} offset where to start reading from * @param {Number} length how far to read * @return {Buffer|String} */ function memStore (runState, offset, val, valOffset, length, skipSubMem) { if (skipSubMem !== false) { subMemUsage(runState, offset, length) } for (var i = 0; i < length; i++) { runState.memory[offset + i] = val[valOffset + i] } } // checks if a jump is valid given a destination function jumpIsValid (runState, dest) { return runState.validJumps.indexOf(dest) !== -1 } // checks to see if we have enough gas left for the memory reads and writes // required by the CALLs function checkCallMemCost (runState, callOptions, localOpts) { // calculates the gase need for reading the input from memory callOptions.data = memLoad(runState, localOpts.inOffset, localOpts.inLength) // calculates the gas need for saving the output in memory if (localOpts.outLength) { subMemUsage(runState, localOpts.outOffset, localOpts.outLength) } if (!callOptions.gasLimit) { callOptions.gasLimit = runState.gasLeft } if (runState.gasLeft.cmp(callOptions.gasLimit) === -1) { trap(ERROR.OUT_OF_GAS) } } // sets up and calls runCall function makeCall (runState, callOptions, localOpts, cb) { callOptions.caller = callOptions.caller || runState.address callOptions.origin = runState.origin callOptions.gasPrice = runState.gasPrice callOptions.block = runState.block callOptions.populateCache = false callOptions.suicides = runState.suicides // increment the runState.depth callOptions.depth = runState.depth + 1 // check if account has enough vapor if (runState.depth >= fees.stackLimit.v || new BN(runState.contract.balance).cmp(callOptions.value) === -1) { runState.stack.push(Buffer.from([0])) cb() } else { // if creating a new contract then increament the nonce if (!callOptions.to) { runState.contract.nonce = new BN(runState.contract.nonce).addn(1) } runState.stateManager.cache.put(runState.address, runState.contract) runState._vm.runCall(callOptions, parseCallResults) } function parseCallResults (err, results) { // concat the runState.logs if (results.vm.logs) { runState.logs = runState.logs.concat(results.vm.logs) } // add gasRefund if (results.vm.gasRefund) { runState.gasRefund = runState.gasRefund.add(results.vm.gasRefund) } // this should always be safe runState.gasLeft.isub(results.gasUsed) if (!results.vm.exceptionError) { // save results to memory if (results.vm.return) { for (var i = 0; i < Math.min(localOpts.outLength, results.vm.return.length); i++) { runState.memory[localOpts.outOffset + i] = results.vm.return[i] } } // update stateRoot on current contract runState.stateManager.getAccount(runState.address, function (err, account) { runState.contract = account // push the created address to the stack if (results.createdAddress) { cb(err, results.createdAddress) } else { cb(err, Buffer.from([results.vm.exception])) } }) } else { // creation failed so don't increament the nonce if (results.vm.createdAddress) { runState.contract.nonce = new BN(runState.contract.nonce).subn(1) } cb(err, Buffer.from([results.vm.exception])) } } }