UNPKG

@polygon-hermez/vm

Version:
276 lines 11.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const debug_1 = require("debug"); const ethereumjs_util_1 = require("ethereumjs-util"); const exceptions_1 = require("../exceptions"); const memory_1 = __importDefault(require("./memory")); const stack_1 = __importDefault(require("./stack")); const opcodes_1 = require("./opcodes"); const gas_1 = require("./opcodes/gas"); const cloneDeep_1 = __importDefault(require("lodash/cloneDeep")); /** * Parses and executes EVM bytecode. */ class Interpreter { constructor(vm, eei) { this._evmStepAux = null; // Opcode debuggers (e.g. { 'push': [debug Object], 'sstore': [debug Object], ...}) this.opDebuggers = {}; this._vm = vm; this._state = vm.stateManager; this._eei = eei; this._runState = { programCounter: 0, opCode: 0xfe, memory: new memory_1.default(), memoryWordCount: new ethereumjs_util_1.BN(0), highestMemCost: new ethereumjs_util_1.BN(0), stack: new stack_1.default(), returnStack: new stack_1.default(1023), code: Buffer.alloc(0), validJumps: Uint8Array.from([]), stateManager: this._state, eei: this._eei, shouldDoJumpAnalysis: true, vcm: vm.vcm, }; this._evmStepAux = null; } async run(code, opts = {}) { var _a; this._runState.code = code; this._runState.programCounter = (_a = opts.pc) !== null && _a !== void 0 ? _a : this._runState.programCounter; let evmSteps = []; // Check that the programCounter is in range const pc = this._runState.programCounter; if (pc !== 0 && (pc < 0 || pc >= this._runState.code.length)) { throw new Error('Internal error: program counter not in range'); } let err; // Iterate through the given ops until something breaks or we hit STOP while (this._runState.programCounter < this._runState.code.length) { const opCode = this._runState.code[this._runState.programCounter]; if (this._runState.shouldDoJumpAnalysis && (opCode === 0x56 || opCode === 0x57 || opCode === 0x5e)) { // Only run the jump destination analysis if `code` actually contains a JUMP/JUMPI/JUMPSUB opcode this._runState.validJumps = this._getValidJumpDests(code); this._runState.shouldDoJumpAnalysis = false; } this._runState.opCode = opCode; try { const interpreterStep = await this.runStep(); //Store a copy of the object const callOpcodes = interpreterStep.callOpcodes; delete interpreterStep.callOpcodes; evmSteps.push((0, cloneDeep_1.default)(interpreterStep)); //If has extra steps from a call, add them to the array if (callOpcodes) { evmSteps = evmSteps.concat(callOpcodes); } } catch (e) { //Add evmStepAux to steps array if (this._evmStepAux) { // evmSteps.push(JSON.parse(JSON.stringify(this._evmStepAux))) evmSteps.push((0, cloneDeep_1.default)(this._evmStepAux)); this._evmStepAux = null; } // re-throw on non-VM errors if (!('errorType' in e && e.errorType === 'VmError')) { throw e; } // STOP is not an exception if (e.error !== exceptions_1.ERROR.STOP) { err = e; } break; } } return { runState: this._runState, exceptionError: err, evmSteps, }; } /** * Executes the opcode to which the program counter is pointing, * reducing its base gas cost, and increments the program counter. */ async runStep() { const opInfo = this.lookupOpInfo(this._runState.opCode); const gas = new ethereumjs_util_1.BN(opInfo.fee); // clone the gas limit; call opcodes can add stipend, // which makes it seem like the gas left increases const gasLimitClone = this._eei.getGasLeft(); if (opInfo.dynamicGas) { const dynamicGasHandler = gas_1.dynamicGasHandlers.get(this._runState.opCode); // This function updates the gas BN in-place using `i*` methods // It needs the base fee, for correct gas limit calculation for the CALL opcodes await dynamicGasHandler(this._runState, gas, this._vm._common); } const simpleInterpreterStep = await this._runStepHook(gas, gasLimitClone); // Add aux evm step for revert/invalid opcodes if (opInfo.name === 'STOP' || opInfo.name === 'INVALID' || opInfo.name === 'SELFDESTRUCT' || opInfo.name === 'REVERT') { // Copy step to aux to use it in case of reverted tx this._evmStepAux = simpleInterpreterStep; } // Check for invalid opcode if (opInfo.name === 'INVALID') { throw new exceptions_1.VmError(exceptions_1.ERROR.INVALID_OPCODE); } // Reduce opcode's base fee this._eei.useGas(gas, `${opInfo.name} fee`); // Advance program counter this._runState.programCounter++; // Execute opcode handler const opFn = this.getOpHandler(opInfo); let fnRes; if (opInfo.isAsync) { fnRes = await opFn.apply(null, [this._runState, this._vm._common]); } else { fnRes = opFn.apply(null, [this._runState, this._vm._common]); } // If is a CALL, append the call opcodes to the interpreter object if (['CALL', 'STATICCALL', 'DELEGATECALL', 'CALLCODE', 'CREATE', 'CREATE2'].includes(opInfo.name) && fnRes) { simpleInterpreterStep.callOpcodes = fnRes.evmSteps; } // Set counters consumption at interpreter step simpleInterpreterStep.counters = this._runState.vcm.currentCountersSnapshot; return simpleInterpreterStep; } /** * Get the handler function for an opcode. */ getOpHandler(opInfo) { return opcodes_1.handlers.get(opInfo.code); } /** * Get info for an opcode from VM's list of opcodes. */ lookupOpInfo(op) { var _a; // if not found, return 0xfe: INVALID return (_a = this._vm._opcodes.get(op)) !== null && _a !== void 0 ? _a : this._vm._opcodes.get(0xfe); } async _runStepHook(dynamicFee, gasLeft) { const opcode = this.lookupOpInfo(this._runState.opCode); const eventObj = { pc: this._runState.programCounter, gasLeft, gasRefund: this._eei._evm._refund, opcode: { name: opcode.fullName, fee: opcode.fee, dynamicFee, isAsync: opcode.isAsync, }, stack: this._runState.stack._store, returnStack: this._runState.returnStack._store, depth: this._eei._env.depth, address: this._eei._env.address, account: this._eei._env.contract, stateManager: this._runState.stateManager, memory: this._runState.memory._store, memoryWordCount: this._runState.memoryWordCount, codeAddress: this._eei._env.codeAddress, }; const simpleInterpreterStep = { pc: this._runState.programCounter, gasLeft, gasRefund: this._eei._evm._refund, opcode: { name: opcode.fullName, fee: opcode.fee, dynamicFee, isAsync: opcode.isAsync, }, stack: this._runState.stack._store, returnStack: this._runState.returnStack._store, depth: this._eei._env.depth, memory: this._runState.memory._store, memoryWordCount: this._runState.memoryWordCount, codeAddress: this._eei._env.codeAddress, }; if (this._vm.DEBUG) { // Create opTrace for debug functionality let hexStack = []; hexStack = eventObj.stack.map((item) => { return '0x' + new ethereumjs_util_1.BN(item).toString(16, 0); }); const name = eventObj.opcode.name; const opTrace = { pc: eventObj.pc, op: name, gas: '0x' + eventObj.gasLeft.toString('hex'), gasCost: '0x' + eventObj.opcode.fee.toString(16), stack: hexStack, depth: eventObj.depth, }; if (!(name in this.opDebuggers)) { this.opDebuggers[name] = (0, debug_1.debug)(`vm:ops:${name}`); } this.opDebuggers[name](JSON.stringify(opTrace)); } /** * The `step` event for trace output * * @event Event: step * @type {Object} * @property {Number} pc representing the program counter * @property {Object} opcode the next opcode to be ran * @property {string} opcode.name * @property {fee} opcode.number Base fee of the opcode * @property {dynamicFee} opcode.dynamicFee Dynamic opcode fee * @property {boolean} opcode.isAsync opcode is async * @property {BN} gasLeft amount of gasLeft * @property {BN} gasRefund gas refund * @property {StateManager} stateManager a {@link StateManager} instance * @property {Array} stack an `Array` of `Buffers` containing the stack * @property {Array} returnStack the return stack * @property {Account} account the Account which owns the code running * @property {Address} address the address of the `account` * @property {Number} depth the current number of calls deep the contract is * @property {Buffer} memory the memory of the VM as a `buffer` * @property {BN} memoryWordCount current size of memory in words * @property {Address} codeAddress the address of the code which is currently being ran (this differs from `address` in a `DELEGATECALL` and `CALLCODE` call) */ if (this._vm.listenerCount('step') > 0 || this._vm.DEBUG) { // Only run this stepHook function if there is an event listener (e.g. test runner) // or if the vm is running in debug mode (to display opcode debug logs) this._vm._emit('step', eventObj); } return simpleInterpreterStep; } // Returns all valid jump and jumpsub destinations. _getValidJumpDests(code) { const jumps = new Uint8Array(code.length).fill(0); for (let i = 0; i < code.length; i++) { const opcode = code[i]; // skip over PUSH0-32 since no jump destinations in the middle of a push block if (opcode <= 0x7f) { if (opcode >= 0x60) { i += opcode - 0x5f; } else if (opcode === 0x5b) { // Define a JUMPDEST as a 1 in the valid jumps array jumps[i] = 1; } else if (opcode === 0x5c) { // Define a BEGINSUB as a 2 in the valid jumps array jumps[i] = 2; } } } return jumps; } } exports.default = Interpreter; //# sourceMappingURL=interpreter.js.map