UNPKG

@polygon-hermez/vm

Version:
373 lines (340 loc) 12 kB
import { debug as createDebugLogger } from 'debug' import { Account, Address, BN } from 'ethereumjs-util' import { StateManager } from '../state/index' import { ERROR, VmError } from '../exceptions' import Memory from './memory' import Stack from './stack' import EEI from './eei' import { Opcode, handlers as opHandlers, OpHandler, AsyncOpHandler } from './opcodes' import { dynamicGasHandlers } from './opcodes/gas' import cloneDeep from 'lodash/cloneDeep' export interface InterpreterOpts { pc?: number } export interface RunState { programCounter: number opCode: number memory: Memory memoryWordCount: BN highestMemCost: BN stack: Stack returnStack: Stack code: Buffer shouldDoJumpAnalysis: boolean validJumps: Uint8Array // array of values where validJumps[index] has value 0 (default), 1 (jumpdest), 2 (beginsub) stateManager: StateManager eei: EEI messageGasLimit?: BN // Cache value from `gas.ts` to save gas limit for a message call vcm?: any } export interface InterpreterResult { runState?: RunState exceptionError?: VmError evmSteps?: SimpleInterpreterStep[] } export interface InterpreterStep { pc: number opcode: { name: string fee: number dynamicFee?: BN isAsync: boolean } gasLeft: BN gasRefund: BN stateManager: StateManager stack: BN[] returnStack: BN[] account: Account address: Address depth: number memory: Buffer memoryWordCount: BN codeAddress: Address } export interface SimpleInterpreterStep { pc: number opcode: { name: string fee: number dynamicFee?: BN isAsync: boolean } gasLeft: BN gasRefund: BN stack: BN[] returnStack: BN[] depth: number memory: Buffer memoryWordCount: BN codeAddress: Address callOpcodes?: SimpleInterpreterStep[] counters?: any } /** * Parses and executes EVM bytecode. */ export default class Interpreter { _vm: any _state: StateManager _runState: RunState _eei: EEI _evmStepAux: SimpleInterpreterStep | null = null // Opcode debuggers (e.g. { 'push': [debug Object], 'sstore': [debug Object], ...}) private opDebuggers: { [key: string]: (debug: string) => void } = {} constructor(vm: any, eei: EEI) { this._vm = vm this._state = vm.stateManager this._eei = eei this._runState = { programCounter: 0, opCode: 0xfe, // INVALID opcode memory: new Memory(), memoryWordCount: new BN(0), highestMemCost: new BN(0), stack: new Stack(), returnStack: new Stack(1023), // 1023 return stack height limit per EIP 2315 spec code: Buffer.alloc(0), validJumps: Uint8Array.from([]), stateManager: this._state, eei: this._eei, shouldDoJumpAnalysis: true, vcm: vm.vcm, } this._evmStepAux = null } async run(code: Buffer, opts: InterpreterOpts = {}): Promise<InterpreterResult> { this._runState.code = code this._runState.programCounter = opts.pc ?? this._runState.programCounter let evmSteps: any[] = [] // 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(cloneDeep(interpreterStep)) //If has extra steps from a call, add them to the array if (callOpcodes) { evmSteps = evmSteps.concat(callOpcodes) } } catch (e: any) { //Add evmStepAux to steps array if (this._evmStepAux) { // evmSteps.push(JSON.parse(JSON.stringify(this._evmStepAux))) evmSteps.push(cloneDeep(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 !== 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(): Promise<SimpleInterpreterStep> { const opInfo = this.lookupOpInfo(this._runState.opCode) const gas = new 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 = 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 VmError(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: any if (opInfo.isAsync) { fnRes = await (opFn as AsyncOpHandler).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: Opcode): OpHandler { return opHandlers.get(opInfo.code)! } /** * Get info for an opcode from VM's list of opcodes. */ lookupOpInfo(op: number): Opcode { // if not found, return 0xfe: INVALID return this._vm._opcodes.get(op) ?? this._vm._opcodes.get(0xfe) } async _runStepHook(dynamicFee: BN, gasLeft: BN): Promise<SimpleInterpreterStep> { const opcode = this.lookupOpInfo(this._runState.opCode) const eventObj: InterpreterStep = { 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, // Return underlying array for backwards-compatibility memoryWordCount: this._runState.memoryWordCount, codeAddress: this._eei._env.codeAddress, } const simpleInterpreterStep: 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, // Return underlying array for backwards-compatibility 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: any) => { return '0x' + new 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] = createDebugLogger(`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: Buffer) { 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 } }