@polygon-hermez/vm
Version:
An Ethereum VM implementation
373 lines (340 loc) • 12 kB
text/typescript
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
}
}