Skip to content

Commit

Permalink
Class-based promisified rewrite of runCall and runCode (#483)
Browse files Browse the repository at this point in the history
* Bump version to 4

* Upgrade babel to 7, use transform-runtime plugin

* evm: add Interpreter class (alternative to runCall)

* evm: add Loop, a promisified version of runCode

* evm: execute opFn calls through interpreter

* evm/loop: mv pc range check out of hot loop

* evm: use Interpreter and Loop in runCall and runCode

* evm: use promisified state in interpreter and loop

* evm/loop: break run into initRunState, runStep, handleOp, getOpHandler

* evm/loop: minor fix

* test: drop test cases assuming runCall

* evm: use util.promisify package

* evm: Mv some logic from executeMessage to private methods

* Fix linting errors
  • Loading branch information
s1na authored Mar 28, 2019
1 parent c5b25c6 commit 4678325
Show file tree
Hide file tree
Showing 13 changed files with 575 additions and 580 deletions.
206 changes: 206 additions & 0 deletions lib/evm/interpreter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
const promisify = require('util.promisify')
const BN = require('bn.js')
const ethUtil = require('ethereumjs-util')
const { ERROR } = require('../exceptions')
const Loop = require('./loop')
const { StorageReader } = require('../state')
const PStateManager = require('../state/promisified')

module.exports = class Interpreter {
constructor (vm, txContext, block, storageReader) {
this._vm = vm
this._state = new PStateManager(this._vm.stateManager)
this._storageReader = storageReader || new StorageReader(this._state._wrapped)
this._tx = txContext
this._block = block
}

async executeMessage (message) {
await this._state.checkpoint()

const account = await this._state.getAccount(message.caller)
// Reduce tx value from sender
await this._reduceSenderBalance(account, message)

// Load `to` account
let toAccount
let createdAddress
if (message.to) {
toAccount = await this._state.getAccount(message.to)
} else {
({ toAccount, createdAddress } = await this._createContract(account, message, message.salt))
}

// Add tx value to the `to` account
await this._addToBalance(toAccount, message)

// Load code
await this._loadCode(message)
if (!message.code || message.code.length === 0) {
await this._state.commit()
return {
gasUsed: new BN(0),
createdAddress: createdAddress,
vm: {
exception: 1
}
}
}

let { err, results } = await this.runLoop(message)
results = await this._parseRunResult(err, results, message, createdAddress)

// Save code if a new contract was created
if (createdAddress && !results.runState.vmError && results.return && results.return.toString() !== '') {
await this._state.putContractCode(createdAddress, results.return)
}

return {
gasUsed: results.gasUsed,
createdAddress: createdAddress,
vm: results
}
}

async runLoop (message) {
const opts = {
storageReader: this._storageReader,
block: this._block,
gasPrice: this._tx.gasPrice,
origin: this._tx.origin,
code: message.code,
data: message.data,
gasLimit: message.gasLimit,
address: message.to,
caller: message.caller,
value: message.value,
depth: message.depth,
selfdestruct: message.selfdestruct,
static: message.isStatic
}

// Run code
let err, results
const loop = new Loop(this._vm, this)
if (message.isCompiled) {
({ err, results } = await new Promise((resolve, reject) => {
this._vm.runJIT(opts, (err, results) => {
// TODO: Standardize errors so they're all VmError
// if (err && err.errorType !== 'VmError' && err !== ERROR.OUT_OF_GAS) return reject(err)
return resolve({ err, results })
})
}))
} else {
({ err, results } = await loop.run(opts))
}

return { err, results }
}

async _reduceSenderBalance (account, message) {
if (!message.delegatecall) {
const newBalance = new BN(account.balance).sub(message.value)
account.balance = newBalance
await this._state.putAccount(ethUtil.toBuffer(message.caller), account)
}
}

async _addToBalance (toAccount, message) {
if (!message.delegatecall) {
const newBalance = new BN(toAccount.balance).add(message.value)
if (newBalance.gt(ethUtil.MAX_INTEGER)) {
throw new Error('Value overflow')
}
toAccount.balance = newBalance
// putAccount as the nonce may have changed for contract creation
this._state.putAccount(ethUtil.toBuffer(message.to), toAccount)
}
}

async _loadCode (message) {
if (!message.code) {
if (this._vm._precompiled[message.to.toString('hex')]) {
message.code = this._vm._precompiled[message.to.toString('hex')]
message.isCompiled = true
} else {
message.code = await this._state.getContractCode(message.to)
message.isCompiled = false
}
}
}

async _createContract (from, message, salt) {
// Generate a new contract if no `to`
// _loadToAccount
message.code = message.data
message.data = undefined
const newNonce = new BN(from.nonce).subn(1)

let createdAddress
if (salt) {
createdAddress = message.to = ethUtil.generateAddress2(message.caller, salt, message.code)
} else {
createdAddress = message.to = ethUtil.generateAddress(message.caller, newNonce.toArray())
}

// Check account state
let address = createdAddress
let account = await this._state.getAccount(address)
if ((account.nonce && new BN(account.nonce) > 0) || account.codeHash.compare(ethUtil.KECCAK256_NULL) !== 0) {
message.code = Buffer.from('fe', 'hex') // Invalid init code
return { toAccount: account, createdAddress }
}

// Setup new contract
await this._state.clearContractStorage(address)

await promisify(this._vm.emit.bind(this._vm))('newContract', {
address: address,
code: message.code
})

account = await this._state.getAccount(address)
account.nonce = new BN(account.nonce).addn(1).toArrayLike(Buffer)
return { toAccount: account, createdAddress }
}

async _parseRunResult (err, results, message, createdAddress) {
if (createdAddress) {
// fee for size of the return value
var totalGas = results.gasUsed
if (!results.runState.vmError) {
var returnFee = new BN(results.return.length * this._vm._common.param('gasPrices', 'createData'))
totalGas = totalGas.add(returnFee)
}
// if not enough gas
if (totalGas.lte(message.gasLimit) && (this._vm.allowUnlimitedContractSize || results.return.length <= 24576)) {
results.gasUsed = totalGas
} else {
results.return = Buffer.alloc(0)
// since Homestead
results.exception = 0
err = results.exceptionError = ERROR.OUT_OF_GAS
results.gasUsed = message.gasLimit
}
}

if (err) {
results.logs = []
await this._state.revert()
if (message.isCompiled) {
// Empty precompiled contracts need to be deleted even in case of OOG
// because the bug in both Geth and Parity led to deleting RIPEMD precompiled in this case
// see https://github.com/ethereum/go-ethereum/pull/3341/files#diff-2433aa143ee4772026454b8abd76b9dd
// We mark the account as touched here, so that is can be removed among other touched empty accounts (after tx finalization)
if (err === ERROR.OUT_OF_GAS || err.error === ERROR.OUT_OF_GAS) {
let acc = await this._state.getAccount(message.to)
await this._state.putAccount(message.to, acc)
}
}
} else {
await this._state.commit()
}

return results
}
}
Loading

0 comments on commit 4678325

Please sign in to comment.