-
Notifications
You must be signed in to change notification settings - Fork 791
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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') | ||
|
||
|
@@ -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: {} | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -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) | ||
} | ||
} | ||
|
@@ -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) | ||
} | ||
|
||
|
@@ -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 | ||
} | ||
|
||
|
@@ -276,7 +287,7 @@ export default class EEI { | |
* @returns {BN} | ||
*/ | ||
getGasLeft (): BN { | ||
return new BN(this._runState.gasLeft) | ||
return this._gasLeft.clone() | ||
} | ||
|
||
/** | ||
|
@@ -285,6 +296,7 @@ export default class EEI { | |
*/ | ||
finish (returnData: Buffer): void { | ||
this._result.returnValue = returnData | ||
trap(ERROR.STOP) | ||
} | ||
|
||
/** | ||
|
@@ -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) | ||
|
@@ -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) | ||
} | ||
|
||
/** | ||
|
@@ -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) | ||
} | ||
|
||
|
@@ -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) | ||
} | ||
|
||
|
@@ -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) | ||
} | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, some more signature tightening fof account functionality, good. |
||
|
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' | ||
|
@@ -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 { | ||
|
@@ -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 | ||
|
@@ -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) | ||
}) | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't completely judge these changes in There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will give this some thought, maybe @alcuadrado can also join for discussion? |
||
/** | ||
|
There was a problem hiding this comment.
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 likeStack
orMemory
. 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.There was a problem hiding this comment.
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.There was a problem hiding this comment.
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. 😛
There was a problem hiding this comment.
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 :) TheuseGas
andrefundGas
methods (maybe like this)?It's mostly used in
opFns
(and also used for instruction base fee inLoop
). On purpose I avoided doing gas metering inEEI
.There was a problem hiding this comment.
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..
There was a problem hiding this comment.
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).