@polygon-hermez/vm
Version:
An Ethereum VM implementation
637 lines (585 loc) • 18.5 kB
text/typescript
import { debug as createDebugLogger } from 'debug'
import {
Account,
Address,
BN,
generateAddress,
generateAddress2,
KECCAK256_NULL,
MAX_INTEGER,
} from 'ethereumjs-util'
import { Block } from '@ethereumjs/block'
import { ERROR, VmError } from '../exceptions'
import { StateManager } from '../state/index'
import { getPrecompile, PrecompileFunc } from './precompiles'
import TxContext from './txContext'
import Message from './message'
import EEI from './eei'
// eslint-disable-next-line
import { short } from './opcodes/util'
import { Log } from './types'
import {
default as Interpreter,
InterpreterOpts,
RunState,
SimpleInterpreterStep,
} from './interpreter'
const debug = createDebugLogger('vm:evm')
const debugGas = createDebugLogger('vm:evm:gas')
/**
* Result of executing a message via the {@link EVM}.
*/
export interface EVMResult {
/**
* Amount of gas used by the transaction
*/
gasUsed: BN
/**
* Address of created account durint transaction, if any
*/
createdAddress?: Address
/**
* Contains the results from running the code, if any, as described in {@link runCode}
*/
execResult: ExecResult
/**
* Array of evm steps to process the tx bytecode
*/
evmSteps?: SimpleInterpreterStep[]
}
/**
* Result of executing a call via the {@link EVM}.
*/
export interface ExecResult {
runState?: RunState
/**
* Description of the exception, if any occured
*/
exceptionError?: VmError
/**
* Amount of gas left
*/
gas?: BN
/**
* Amount of gas the code used to run
*/
gasUsed: BN
/**
* Return value from the contract
*/
returnValue: Buffer
/**
* Array of logs that the contract emitted
*/
logs?: Log[]
/**
* A map from the accounts that have self-destructed to the addresses to send their funds to
*/
selfdestruct?: { [k: string]: Buffer }
/**
* Total amount of gas to be refunded from all nested calls.
*/
gasRefund?: BN
/**
* Array of evm steps to process the tx bytecode
*/
evmSteps?: SimpleInterpreterStep[]
}
export interface NewContractEvent {
address: Address
// The deployment code
code: Buffer
}
export function OOGResult(gasLimit: BN): ExecResult {
return {
returnValue: Buffer.alloc(0),
gasUsed: gasLimit,
exceptionError: new VmError(ERROR.OUT_OF_GAS),
}
}
// CodeDeposit OOG Result
export function COOGResult(gasUsedCreateCode: BN): ExecResult {
return {
returnValue: Buffer.alloc(0),
gasUsed: gasUsedCreateCode,
exceptionError: new VmError(ERROR.CODESTORE_OUT_OF_GAS),
}
}
export function INVALID_BYTECODE_RESULT(gasLimit: BN): ExecResult {
return {
returnValue: Buffer.alloc(0),
gasUsed: gasLimit,
exceptionError: new VmError(ERROR.INVALID_BYTECODE_RESULT),
}
}
export function VmErrorResult(error: VmError, gasUsed: BN): ExecResult {
return {
returnValue: Buffer.alloc(0),
gasUsed: gasUsed,
exceptionError: error,
}
}
/**
* EVM is responsible for executing an EVM message fully
* (including any nested calls and creates), processing the results
* and storing them to state (or discarding changes in case of exceptions).
* @ignore
*/
export default class EVM {
_vm: any
_state: StateManager
_tx: TxContext
_block: Block
/**
* Amount of gas to refund from deleting storage values
*/
_refund: BN
constructor(vm: any, txContext: TxContext, block: Block) {
this._vm = vm
this._state = this._vm.stateManager
this._tx = txContext
this._block = block
this._refund = new BN(0)
}
/**
* Executes an EVM message, determining whether it's a call or create
* based on the `to` address. It checkpoints the state and reverts changes
* if an exception happens during the message execution.
*/
async executeMessage(message: Message): Promise<EVMResult> {
await this._vm._emit('beforeMessage', message)
if (!message.to && this._vm._common.isActivatedEIP(2929)) {
message.code = message.data
;(<any>this._state).addWarmedAddress((await this._generateAddress(message)).buf)
}
const oldRefund = this._refund.clone()
await this._state.checkpoint()
if (this._vm.DEBUG) {
debug('-'.repeat(100))
debug(`message checkpoint`)
}
let result
if (this._vm.DEBUG) {
debug(
`New message caller=${message.caller} gasLimit=${message.gasLimit} to=${
message.to ? message.to.toString() : ''
} value=${message.value} delegatecall=${message.delegatecall ? 'yes' : 'no'}`
)
}
if (message.to) {
if (this._vm.DEBUG) {
debug(`Message CALL execution (to: ${message.to})`)
}
result = await this._executeCall(message)
} else {
if (this._vm.DEBUG) {
debug(`Message CREATE execution (to undefined)`)
}
result = await this._executeCreate(message)
}
if (this._vm.DEBUG) {
debug(
`Received message results gasUsed=${result.gasUsed} execResult: [ gasUsed=${
result.gasUsed
} exceptionError=${
result.execResult.exceptionError ? result.execResult.exceptionError.toString() : ''
} returnValue=${short(result.execResult.returnValue)} gasRefund=${
result.execResult.gasRefund
} ]`
)
}
const err = result.execResult.exceptionError
// This clause captures any error which happened during execution
// If that is the case, then set the _refund tracker to the old refund value
if (err) {
// TODO: Move `gasRefund` to a tx-level result object
// instead of `ExecResult`.
this._refund = oldRefund
result.execResult.selfdestruct = {}
}
result.execResult.gasRefund = this._refund.clone()
if (err) {
if (this._vm._common.gteHardfork('homestead') || err.error != ERROR.CODESTORE_OUT_OF_GAS) {
result.execResult.logs = []
await this._state.revert()
if (this._vm.DEBUG) {
debug(`message checkpoint reverted`)
}
} else {
// we are in chainstart and the error was the code deposit error
// we do like nothing happened.
await this._state.commit()
if (this._vm.DEBUG) {
debug(`message checkpoint committed`)
}
}
} else {
await this._state.commit()
if (this._vm.DEBUG) {
debug(`message checkpoint committed`)
}
}
await this._vm._emit('afterMessage', result)
return result
}
async _executeCall(message: Message): Promise<EVMResult> {
const account = await this._state.getAccount(message.caller)
// Reduce tx value from sender
if (!message.delegatecall) {
await this._reduceSenderBalance(account, message)
}
// Load `to` account
const toAccount = await this._state.getAccount(message.to)
// Add tx value to the `to` account
let errorMessage
if (!message.delegatecall) {
try {
await this._addToBalance(toAccount, message)
} catch (e: any) {
errorMessage = e
}
}
// Load code
await this._loadCode(message)
let exit = false
if (!message.code || message.code.length === 0) {
exit = true
if (this._vm.DEBUG) {
debug(`Exit early on no code`)
}
}
if (errorMessage) {
exit = true
if (this._vm.DEBUG) {
debug(`Exit early on value tranfer overflowed`)
}
}
if (exit) {
return {
gasUsed: new BN(0),
execResult: {
gasUsed: new BN(0),
exceptionError: errorMessage, // Only defined if addToBalance failed
returnValue: Buffer.alloc(0),
},
}
}
let result: ExecResult
if (message.isCompiled) {
if (this._vm.DEBUG) {
debug(`Run precompile`)
}
const unsupportedPrecompileds = [
'0x0000000000000000000000000000000000000003', // RIPEMD-160
'0x0000000000000000000000000000000000000009', // blake
];
if (unsupportedPrecompileds.includes(message.to.toString())) {
result = {
returnValue: Buffer.alloc(0),
gasUsed: new BN(0),
exceptionError: new VmError(ERROR.REVERT),
}
} else {
result = await this.runPrecompile(
message.code as PrecompileFunc,
message.data,
message.outLength,
message.gasLimit
)
}
} else {
if (this._vm.DEBUG) {
debug(`Start bytecode processing...`)
}
result = await this.runInterpreter(message)
}
return {
evmSteps: result.evmSteps,
gasUsed: result.gasUsed,
execResult: result,
}
}
async _executeCreate(message: Message): Promise<EVMResult> {
const account = await this._state.getAccount(message.caller)
// Reduce tx value from sender
await this._reduceSenderBalance(account, message)
message.code = message.data
message.data = Buffer.alloc(0)
message.to = await this._generateAddress(message)
message.isDeploy = true
if (this._vm.DEBUG) {
debug(`Generated CREATE contract address ${message.to}`)
}
let toAccount = await this._state.getAccount(message.to)
// Check for collision
if ((toAccount.nonce && toAccount.nonce.gtn(0)) || !toAccount.codeHash.equals(KECCAK256_NULL)) {
if (this._vm.DEBUG) {
debug(`Returning on address collision`)
}
return {
gasUsed: message.gasLimit,
createdAddress: message.to,
execResult: {
returnValue: Buffer.alloc(0),
exceptionError: new VmError(ERROR.CREATE_COLLISION),
gasUsed: message.gasLimit,
},
}
}
await this._state.clearContractStorage(message.to)
const newContractEvent: NewContractEvent = {
address: message.to,
code: message.code,
}
await this._vm._emit('newContract', newContractEvent)
toAccount = await this._state.getAccount(message.to)
// EIP-161 on account creation and CREATE execution
if (this._vm._common.gteHardfork('spuriousDragon')) {
toAccount.nonce.iaddn(1)
}
// Add tx value to the `to` account
let errorMessage
try {
await this._addToBalance(toAccount, message)
} catch (e: any) {
errorMessage = e
}
let exit = false
if (!message.code || message.code.length === 0) {
exit = true
if (this._vm.DEBUG) {
debug(`Exit early on no code`)
}
}
if (errorMessage) {
exit = true
if (this._vm.DEBUG) {
debug(`Exit early on value tranfer overflowed`)
}
}
if (exit) {
return {
gasUsed: new BN(0),
createdAddress: message.to,
execResult: {
gasUsed: new BN(0),
exceptionError: errorMessage, // only defined if addToBalance failed
returnValue: Buffer.alloc(0),
},
}
}
if (this._vm.DEBUG) {
debug(`Start bytecode processing...`)
}
let result = await this.runInterpreter(message)
// fee for size of the return value
let totalGas = result.gasUsed
let returnFee = new BN(0)
if (!result.exceptionError) {
returnFee = new BN(result.returnValue.length).imuln(
this._vm._common.param('gasPrices', 'createData')
)
totalGas = totalGas.add(returnFee)
if (this._vm.DEBUG) {
debugGas(`Add return value size fee (${returnFee} to gas used (-> ${totalGas}))`)
}
}
// Check for SpuriousDragon EIP-170 code size limit
let allowedCodeSize = true
if (
this._vm._common.gteHardfork('spuriousDragon') &&
result.returnValue.length > this._vm._common.param('vm', 'maxCodeSize')
) {
allowedCodeSize = false
}
// If enough gas and allowed code size
let CodestoreOOG = false
if (
totalGas.lte(message.gasLimit) &&
(this._vm._allowUnlimitedContractSize || allowedCodeSize)
) {
if (
this._vm._common.isActivatedEIP(3541) &&
result.returnValue.slice(0, 1).equals(Buffer.from('EF', 'hex'))
) {
result = { ...result, ...INVALID_BYTECODE_RESULT(message.gasLimit) }
} else {
result.gasUsed = totalGas
}
} else {
if (this._vm._common.gteHardfork('homestead')) {
if (this._vm.DEBUG) {
debug(`Not enough gas or code size not allowed (>= Homestead)`)
}
result = { ...result, ...OOGResult(message.gasLimit) }
} else {
// we are in Frontier
if (this._vm.DEBUG) {
debug(`Not enough gas or code size not allowed (Frontier)`)
}
if (totalGas.sub(returnFee).lte(message.gasLimit)) {
// we cannot pay the code deposit fee (but the deposit code actually did run)
result = { ...result, ...COOGResult(totalGas.sub(returnFee)) }
CodestoreOOG = true
} else {
result = { ...result, ...OOGResult(message.gasLimit) }
}
}
}
// Save code if a new contract was created
if (!result.exceptionError && result.returnValue && result.returnValue.toString() !== '') {
await this._state.putContractCode(message.to, result.returnValue)
if (this._vm.DEBUG) {
debug(`Code saved on new contract creation`)
}
} else if (CodestoreOOG) {
// This only happens at Frontier. But, let's do a sanity check;
if (!this._vm._common.gteHardfork('homestead')) {
// Pre-Homestead behavior; put an empty contract.
// This contract would be considered "DEAD" in later hard forks.
// It is thus an unecessary default item, which we have to save to dik
// It does change the state root, but it only wastes storage.
//await this._state.putContractCode(message.to, result.returnValue)
const account = await this._state.getAccount(message.to)
await this._state.putAccount(message.to, account)
}
}
return {
evmSteps: result.evmSteps,
gasUsed: result.gasUsed,
createdAddress: message.to,
execResult: result,
}
}
/**
* Starts the actual bytecode processing for a CALL or CREATE, providing
* it with the {@link EEI}.
*/
async runInterpreter(message: Message, opts: InterpreterOpts = {}): Promise<ExecResult> {
const env = {
blockchain: this._vm.blockchain, // Only used in BLOCKHASH
address: message.to || Address.zero(),
caller: message.caller || Address.zero(),
callData: message.data || Buffer.from([0]),
callValue: message.value || new BN(0),
code: message.code as Buffer,
isStatic: message.isStatic || false,
depth: message.depth || 0,
gasPrice: this._tx.gasPrice,
origin: this._tx.origin || message.caller || Address.zero(),
block: this._block || new Block(),
contract: await this._state.getAccount(message.to || Address.zero()),
codeAddress: message.codeAddress,
isCreate: message.isCreate,
isDeploy: message.isDeploy,
}
const eei = new EEI(env, this._state, this, this._vm._common, message.gasLimit.clone())
if (message.selfdestruct) {
eei._result.selfdestruct = message.selfdestruct
}
const interpreter = new Interpreter(this._vm, eei)
const interpreterRes = await interpreter.run(message.code as Buffer, opts)
let result = eei._result
let gasUsed = message.gasLimit.sub(eei._gasLeft)
if (interpreterRes.exceptionError) {
if (interpreterRes.exceptionError.error !== ERROR.REVERT) {
gasUsed = message.gasLimit
}
// Clear the result on error
result = {
...result,
logs: [],
selfdestruct: {},
}
}
return {
...result,
runState: {
...interpreterRes.runState!,
...result,
...eei._env,
},
evmSteps: interpreterRes.evmSteps,
exceptionError: interpreterRes.exceptionError,
gas: eei._gasLeft,
gasUsed,
returnValue: result.returnValue ? result.returnValue : Buffer.alloc(0),
}
}
/**
* Returns code for precompile at the given address, or undefined
* if no such precompile exists.
*/
getPrecompile(address: Address): PrecompileFunc {
return getPrecompile(address, this._vm._common)
}
/**
* Executes a precompiled contract with given data and gas limit.
*/
runPrecompile(
code: PrecompileFunc,
data: Buffer,
outLength: BN,
gasLimit: BN
): Promise<ExecResult> | ExecResult {
if (typeof code !== 'function') {
throw new Error('Invalid precompile')
}
const opts = {
data,
gasLimit,
outLength,
_common: this._vm._common,
_VM: this._vm,
}
return code(opts)
}
async _loadCode(message: Message): Promise<void> {
if (!message.code) {
const precompile = this.getPrecompile(message.codeAddress)
if (precompile) {
message.code = precompile
message.isCompiled = true
} else {
message.code = await this._state.getContractCode(message.codeAddress)
message.isCompiled = false
}
}
}
async _generateAddress(message: Message): Promise<Address> {
let addr
if (message.salt) {
addr = generateAddress2(message.caller.buf, message.salt, message.code as Buffer)
} else {
const acc = await this._state.getAccount(message.caller)
const newNonce = acc.nonce.subn(1)
addr = generateAddress(message.caller.buf, newNonce.toArrayLike(Buffer))
}
return new Address(addr)
}
async _reduceSenderBalance(account: Account, message: Message): Promise<void> {
account.balance.isub(message.value)
const result = this._state.putAccount(message.caller, account)
if (this._vm.DEBUG) {
debug(`Reduced sender (${message.caller}) balance (-> ${account.balance})`)
}
return result
}
async _addToBalance(toAccount: Account, message: Message): Promise<void> {
const newBalance = toAccount.balance.add(message.value)
if (newBalance.gt(MAX_INTEGER)) {
throw new VmError(ERROR.VALUE_OVERFLOW)
}
toAccount.balance = newBalance
// putAccount as the nonce may have changed for contract creation
const result = this._state.putAccount(message.to, toAccount)
if (this._vm.DEBUG) {
debug(`Added toAccount (${message.to}) balance (-> ${toAccount.balance})`)
}
return result
}
async _touchAccount(address: Address): Promise<void> {
const account = await this._state.getAccount(address)
return this._state.putAccount(address, account)
}
}