Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate opFns to typescript, modify loop/interpreter structure #506

Merged
merged 5 commits into from
May 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 36 additions & 24 deletions lib/evm/eei.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import BN = require('bn.js')
import { toBuffer } from 'ethereumjs-util'
import Account from 'ethereumjs-account'
import Common from 'ethereumjs-common'
import PStateManager from '../state/promisified'
import { VmError, ERROR } from '../exceptions'
import Message from './message'
import { RunState, RunResult } from './loop'
import Interpreter from './interpreter'
const promisify = require('util.promisify')

Expand All @@ -23,21 +23,35 @@ export interface Env {
contract: Account
}

export interface RunResult {
logs: any // TODO: define type for Log (each log: [Buffer(address), [Buffer(topic0), ...]])
returnValue?: Buffer
gasRefund: BN
selfdestruct: {[k: string]: Buffer}
}

export default class EEI {
_env: Env
_runState: RunState
_result: RunResult
_state: PStateManager
_interpreter: Interpreter
_lastReturned: Buffer
_common: Common
_gasLeft: BN

constructor (env: Env, runState: RunState, result: RunResult, state: PStateManager, interpreter: Interpreter) {
constructor (env: Env, state: PStateManager, interpreter: Interpreter, common: Common, gasLeft: BN) {
this._env = env
this._runState = runState
this._result = result
this._state = state
this._interpreter = interpreter
this._lastReturned = Buffer.alloc(0)
this._common = common
this._gasLeft = gasLeft
this._result = {
logs: [],
returnValue: undefined,
gasRefund: new BN(0),
selfdestruct: {}
}
}

/**
Expand All @@ -46,9 +60,9 @@ export default class EEI {
* @throws if out of gas
*/
useGas (amount: BN): void {
this._runState.gasLeft.isub(amount)
if (this._runState.gasLeft.ltn(0)) {
this._runState.gasLeft = new BN(0)
this._gasLeft.isub(amount)
if (this._gasLeft.ltn(0)) {
this._gasLeft = new BN(0)
trap(ERROR.OUT_OF_GAS)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't need to be addressed in this PR but still have the impression, that gas handling should rather have it's own gas.ts module since this doesn't really belong to the environment but is rather some internal state component like Stack or Memory. This would be likely also similarly useful for VM users if they could hook into that class and get step-by-step gas usage info or can manipulate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also one indicator: gasLeft doesn't really look right in the constructor here, this is also used only in very isolated places and not throughout the whole class.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On-top note: but please resist the temptation of just doing it because I repeat the idea 2-3 times if you don't think it's a good idea. 😛

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest I'm not sure what you have in mind to put in the gas.ts module :) The useGas and refundGas methods (maybe like this)?

this is also used only in very isolated places and not throughout the whole class.

It's mostly used in opFns (and also used for instruction base fee in Loop). On purpose I avoided doing gas metering in EEI.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for digging this out from Py-EVM, yes I was actually thinking about something like this. We can do this later though if agreed upon, don't let this distract you here. I would assume this would be useful if e.g. extended with events which could be used to real-time display gas usage in IDEs e.g..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, I think this is doable. One possibility (for next versions) is to take inspiration from go-ethereum's gas table bring most gas calculation logic there (possibly even hardfork-specific gas costs).

Expand All @@ -67,18 +81,16 @@ export default class EEI {

/**
* Returns balance of the given account.
* @param {BN} address - Address of account
* @param address - Address of account
*/
async getExternalBalance (address: BN): Promise<BN> {
const addressBuf = addressToBuffer(address)

async getExternalBalance (address: Buffer): Promise<BN> {
// shortcut if current account
if (addressBuf.toString('hex') === this._env.address.toString('hex')) {
if (address.toString('hex') === this._env.address.toString('hex')) {
return new BN(this._env.contract.balance)
}

// otherwise load account then return balance
const account = await this._state.getAccount(addressBuf)
const account = await this._state.getAccount(address)
return new BN(account.balance)
}

Expand All @@ -103,10 +115,9 @@ export default class EEI {
/**
* Returns input data in current environment. This pertains to the input
* data passed with the message call instruction or transaction.
* @param {BN} pos - Offset of calldata
* @returns {Buffer}
*/
getCallData (pos: BN): Buffer {
getCallData (): Buffer {
return this._env.callData
}

Expand Down Expand Up @@ -276,7 +287,7 @@ export default class EEI {
* @returns {BN}
*/
getGasLeft (): BN {
return new BN(this._runState.gasLeft)
return this._gasLeft.clone()
}

/**
Expand All @@ -285,6 +296,7 @@ export default class EEI {
*/
finish (returnData: Buffer): void {
this._result.returnValue = returnData
trap(ERROR.STOP)
}

/**
Expand All @@ -310,11 +322,10 @@ export default class EEI {
async _selfDestruct (toAddress: Buffer): Promise<void> {
// only add to refund if this is the first selfdestruct for the address
if (!this._result.selfdestruct[this._env.address.toString('hex')]) {
this._result.gasRefund = this._result.gasRefund.addn(this._runState._common.param('gasPrices', 'selfdestructRefund'))
this._result.gasRefund = this._result.gasRefund.addn(this._common.param('gasPrices', 'selfdestructRefund'))
}

this._result.selfdestruct[this._env.address.toString('hex')] = toAddress
this._runState.stopped = true

// Add to beneficiary balance
const toAccount = await this._state.getAccount(toAddress)
Expand All @@ -326,6 +337,8 @@ export default class EEI {
const account = await this._state.getAccount(this._env.address)
account.balance = toBuffer(new BN(0))
await this._state.putAccount(this._env.address, account)

trap(ERROR.STOP)
}

/**
Expand Down Expand Up @@ -450,7 +463,7 @@ export default class EEI {
this._lastReturned = Buffer.alloc(0)

// Check if account has enough ether and max depth not exceeded
if (this._env.depth >= this._runState._common.param('vm', 'stackLimit') || (msg.delegatecall !== true && new BN(this._env.contract.balance).lt(msg.value))) {
if (this._env.depth >= this._common.param('vm', 'stackLimit') || (msg.delegatecall !== true && new BN(this._env.contract.balance).lt(msg.value))) {
return new BN(0)
}

Expand Down Expand Up @@ -505,7 +518,7 @@ export default class EEI {
this._lastReturned = Buffer.alloc(0)

// Check if account has enough ether and max depth not exceeded
if (this._env.depth >= this._runState._common.param('vm', 'stackLimit') || (msg.delegatecall !== true && new BN(this._env.contract.balance).lt(msg.value))) {
if (this._env.depth >= this._common.param('vm', 'stackLimit') || (msg.delegatecall !== true && new BN(this._env.contract.balance).lt(msg.value))) {
return new BN(0)
}

Expand Down Expand Up @@ -561,9 +574,8 @@ export default class EEI {
* Returns true if account is empty (according to EIP-161).
* @param address - Address of account
*/
async isAccountEmpty (address: BN): Promise<boolean> {
const addressBuf = addressToBuffer(address)
return this._state.accountIsEmpty(addressBuf)
async isAccountEmpty (address: Buffer): Promise<boolean> {
return this._state.accountIsEmpty(address)
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, some more signature tightening fof account functionality, good.

Expand Down
87 changes: 69 additions & 18 deletions lib/evm/interpreter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import BN = require('bn.js')
import { toBuffer, generateAddress, generateAddress2, KECCAK256_NULL, MAX_INTEGER } from 'ethereumjs-util'
import { toBuffer, generateAddress, generateAddress2, zeros, KECCAK256_NULL, MAX_INTEGER } from 'ethereumjs-util'
import Account from 'ethereumjs-account'
import { VmError, ERROR } from '../exceptions'
import { StorageReader } from '../state'
Expand All @@ -8,12 +8,28 @@ import { getPrecompile, PrecompileFunc, PrecompileResult } from './precompiles'
import { OOGResult } from './precompiles/types'
import TxContext from './txContext'
import Message from './message'
import { default as Loop, LoopResult } from './loop'
import EEI from './eei'
import { default as Loop, LoopResult, RunState, IsException, RunOpts } from './loop'
const Block = require('ethereumjs-block')

export interface InterpreterResult {
gasUsed: BN
createdAddress?: Buffer
vm: LoopResult
vm: ExecResult
}

export interface ExecResult {
runState?: RunState
exception: IsException
exceptionError?: VmError | ERROR
gas?: BN
gasUsed: BN
return: Buffer
// From RunResult
logs?: Buffer[]
returnValue?: Buffer
gasRefund?: BN
selfdestruct?: {[k: string]: Buffer}
}

export default class Interpreter {
Expand Down Expand Up @@ -87,7 +103,13 @@ export default class Interpreter {
}
}

const result = await this.runLoop(message)
let result
if (message.isCompiled) {
result = this.runPrecompile(message.code as PrecompileFunc, message.data, message.gasLimit)
} else {
result = await this.runLoop(message)
}

return {
gasUsed: result.gasUsed,
vm: result
Expand Down Expand Up @@ -168,24 +190,53 @@ export default class Interpreter {
}
}

async runLoop (message: Message): Promise<any> {
const opts = {
storageReader: this._storageReader,
block: this._block,
txContext: this._tx,
message
async runLoop (message: Message, loopOpts: RunOpts = {}): Promise<ExecResult> {
const env = {
blockchain: this._vm.blockchain, // Only used in BLOCKHASH
address: message.to || zeros(32),
caller: message.caller || zeros(32),
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 || zeros(32),
block: this._block || new Block(),
contract: await this._state.getAccount(message.to || zeros(32))
}
const eei = new EEI(env, this._state, this, this._vm._common, message.gasLimit.clone())
if (message.selfdestruct) {
eei._result.selfdestruct = message.selfdestruct
}

// Run code
let results
const loop = new Loop(this._vm, this)
if (message.isCompiled) {
results = this.runPrecompile(message.code as PrecompileFunc, message.data, message.gasLimit)
} else {
results = await loop.run(opts)
const loop = new Loop(this._vm, eei)
const opts = Object.assign({ storageReader: this._storageReader }, loopOpts)
const loopRes = await loop.run(message.code as Buffer, opts)

let result = eei._result
let gasUsed = message.gasLimit.sub(eei._gasLeft)
if (loopRes.exceptionError) {
if ((loopRes.exceptionError as VmError).error !== ERROR.REVERT) {
gasUsed = message.gasLimit
}

// remove any logs on error
result = Object.assign({}, result, {
logs: [],
gasRefund: null,
selfdestruct: null
})
}

return results
return Object.assign({}, result, {
runState: Object.assign({}, loopRes.runState, result, eei._env),
exception: loopRes.exception,
exceptionError: loopRes.exceptionError,
gas: eei._gasLeft,
gasUsed,
'return': result.returnValue ? result.returnValue : Buffer.alloc(0)
})
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't completely judge these changes in Interpreter but would give this a go since you're saying that this is only some intermediate step.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is mostly temporary. But I'd appreciate comments on how to change it in future (I'm not yet sure myself).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will give this some thought, maybe @alcuadrado can also join for discussion?

/**
Expand Down
Loading