diff --git a/lib/evm/eei.ts b/lib/evm/eei.ts index 54d7c9917b..04d9eb1890 100644 --- a/lib/evm/eei.ts +++ b/lib/evm/eei.ts @@ -7,11 +7,19 @@ const { VmError, ERROR } = require('../exceptions') export default class EEI { _env: any + _runState: any + _result: any _state: PStateManager + _interpreter: any + _lastReturned: Buffer - constructor (env: any) { + constructor (env: any, runState: any, result: any, state: PStateManager, interpreter: any) { this._env = env - this._state = new PStateManager(this._env.stateManager) + this._runState = runState + this._result = result + this._state = state + this._interpreter = interpreter + this._lastReturned = Buffer.alloc(0) } /** @@ -20,13 +28,17 @@ export default class EEI { * @throws if out of gas */ useGas (amount: BN): void { - this._env.gasLeft.isub(amount) - if (this._env.gasLeft.ltn(0)) { - this._env.gasLeft = new BN(0) + this._runState.gasLeft.isub(amount) + if (this._runState.gasLeft.ltn(0)) { + this._runState.gasLeft = new BN(0) trap(ERROR.OUT_OF_GAS) } } + refundGas (amount: BN): void { + this._result.gasRefund.iaddn(amount) + } + /** * Returns address of currently executing account. * @returns {Buffer} @@ -109,6 +121,10 @@ export default class EEI { return this._env.code } + isStatic (): boolean { + return this._env.isStatic + } + /** * Get size of an account’s code. * @param {BN} address - Address of account @@ -137,7 +153,7 @@ export default class EEI { * @returns {BN} */ getReturnDataSize (): BN { - return new BN(this._env.lastReturned.length) + return new BN(this._lastReturned.length) } /** @@ -147,7 +163,7 @@ export default class EEI { * @returns {Buffer} */ getReturnData (): Buffer { - return this._env.lastReturned + return this._lastReturned } /** @@ -242,7 +258,7 @@ export default class EEI { * @returns {BN} */ getGasLeft (): BN { - return new BN(this._env.gasLeft) + return new BN(this._runState.gasLeft) } /** @@ -250,7 +266,7 @@ export default class EEI { * @param {Buffer} returnData - Output data to return */ finish (returnData: Buffer): void { - this._env.returnValue = returnData + this._result.returnValue = returnData } /** @@ -259,7 +275,7 @@ export default class EEI { * @param {Buffer} returnData - Output data to return */ revert (returnData: Buffer): void { - this._env.returnValue = returnData + this._result.returnValue = returnData trap(ERROR.REVERT) } @@ -274,21 +290,13 @@ export default class EEI { } async _selfDestruct (toAddress: Buffer): Promise { - // TODO: Determine if gas consumption & refund should happen in EEI or opFn - if ((new BN(this._env.contract.balance)).gtn(0)) { - const empty = await this._state.accountIsEmpty(toAddress) - if (empty) { - this.useGas(new BN(this._env._common.param('gasPrices', 'callNewAccount'))) - } - } - // only add to refund if this is the first selfdestruct for the address - if (!this._env.selfdestruct[this._env.address.toString('hex')]) { - this._env.gasRefund = this._env.gasRefund.addn(this._env._common.param('gasPrices', 'selfdestructRefund')) + if (!this._result.selfdestruct[this._env.address.toString('hex')]) { + this._result.gasRefund = this._result.gasRefund.addn(this._runState._common.param('gasPrices', 'selfdestructRefund')) } - this._env.selfdestruct[this._env.address.toString('hex')] = toAddress - this._env.stopped = true + this._result.selfdestruct[this._env.address.toString('hex')] = toAddress + this._runState.stopped = true // Add to beneficiary balance const toAccount = await this._state.getAccount(toAddress) @@ -323,7 +331,7 @@ export default class EEI { // add data log.push(data) - this._env.logs.push(log) + this._result.logs.push(log) } /** @@ -340,7 +348,7 @@ export default class EEI { to: address, value: value, data: data, - isStatic: this._env.static, + isStatic: this._env.isStatic, depth: this._env.depth + 1 }) @@ -362,7 +370,7 @@ export default class EEI { codeAddress: address, value: value, data: data, - isStatic: this._env.static, + isStatic: this._env.isStatic, depth: this._env.depth + 1 }) @@ -408,7 +416,7 @@ export default class EEI { codeAddress: address, value: value, data: data, - isStatic: this._env.static, + isStatic: this._env.isStatic, delegatecall: true, depth: this._env.depth + 1 }) @@ -417,39 +425,38 @@ export default class EEI { } async _baseCall (msg: Message): Promise { - const selfdestruct = Object.assign({}, this._env.selfdestruct) + const selfdestruct = Object.assign({}, this._result.selfdestruct) msg.selfdestruct = selfdestruct // empty the return data buffer - this._env.lastReturned = Buffer.alloc(0) + this._lastReturned = Buffer.alloc(0) // Check if account has enough ether and max depth not exceeded - if (this._env.depth >= this._env._common.param('vm', 'stackLimit') || (msg.delegatecall !== true && new BN(this._env.contract.balance).lt(msg.value))) { + if (this._env.depth >= this._runState._common.param('vm', 'stackLimit') || (msg.delegatecall !== true && new BN(this._env.contract.balance).lt(msg.value))) { return new BN(0) } - const results = await this._env.interpreter.executeMessage(msg) + const results = await this._interpreter.executeMessage(msg) - // concat the runState.logs if (results.vm.logs) { - this._env.logs = this._env.logs.concat(results.vm.logs) + this._result.logs = this._result.logs.concat(results.vm.logs) } // add gasRefund if (results.vm.gasRefund) { - this._env.gasRefund = this._env.gasRefund.add(results.vm.gasRefund) + this._result.gasRefund = this._result.gasRefund.add(results.vm.gasRefund) } // this should always be safe - this._env.gasLeft.isub(results.gasUsed) + this.useGas(results.gasUsed) // Set return value if (results.vm.return && (!results.vm.exceptionError || results.vm.exceptionError.error === ERROR.REVERT)) { - this._env.lastReturned = results.vm.return + this._lastReturned = results.vm.return } if (!results.vm.exceptionError) { - Object.assign(this._env.selfdestruct, selfdestruct) + Object.assign(this._result.selfdestruct, selfdestruct) // update stateRoot on current contract const account = await this._state.getAccount(this._env.address) this._env.contract = account @@ -465,7 +472,7 @@ export default class EEI { * @param {Buffer} data */ async create (gasLimit: BN, value: BN, data: Buffer, salt: Buffer | null = null): Promise { - const selfdestruct = Object.assign({}, this._env.selfdestruct) + const selfdestruct = Object.assign({}, this._result.selfdestruct) const msg = new Message({ caller: this._env.address, gasLimit: gasLimit, @@ -477,38 +484,37 @@ export default class EEI { }) // empty the return data buffer - this._env.lastReturned = Buffer.alloc(0) + this._lastReturned = Buffer.alloc(0) // Check if account has enough ether and max depth not exceeded - if (this._env.depth >= this._env._common.param('vm', 'stackLimit') || (msg.delegatecall !== true && new BN(this._env.contract.balance).lt(msg.value))) { + if (this._env.depth >= this._runState._common.param('vm', 'stackLimit') || (msg.delegatecall !== true && new BN(this._env.contract.balance).lt(msg.value))) { return new BN(0) } this._env.contract.nonce = new BN(this._env.contract.nonce).addn(1) await this._state.putAccount(this._env.address, this._env.contract) - const results = await this._env.interpreter.executeMessage(msg) + const results = await this._interpreter.executeMessage(msg) - // concat the runState.logs if (results.vm.logs) { - this._env.logs = this._env.logs.concat(results.vm.logs) + this._result.logs = this._result.logs.concat(results.vm.logs) } // add gasRefund if (results.vm.gasRefund) { - this._env.gasRefund = this._env.gasRefund.add(results.vm.gasRefund) + this._result.gasRefund = this._result.gasRefund.add(results.vm.gasRefund) } // this should always be safe - this._env.gasLeft.isub(results.gasUsed) + this.useGas(results.gasUsed) // Set return buffer in case revert happened if (results.vm.exceptionError && results.vm.exceptionError.error === ERROR.REVERT) { - this._env.lastReturned = results.vm.return + this._lastReturned = results.vm.return } if (!results.vm.exceptionError) { - Object.assign(this._env.selfdestruct, selfdestruct) + Object.assign(this._result.selfdestruct, selfdestruct) // update stateRoot on current contract const account = await this._state.getAccount(this._env.address) this._env.contract = account @@ -537,6 +543,15 @@ export default class EEI { async create2 (gasLimit: BN, value: BN, data: Buffer, salt: Buffer): Promise { return this.create(gasLimit, value, data, salt) } + + /** + * Returns true if account is empty (according to EIP-161). + * @param address - Address of account + */ + async isAccountEmpty (address: BN): Promise { + const addressBuf = addressToBuffer(address) + return this._state.accountIsEmpty(addressBuf) + } } function trap (err: string) { @@ -545,5 +560,6 @@ function trap (err: string) { const MASK_160 = new BN(1).shln(160).subn(1) function addressToBuffer (address: BN) { + if (Buffer.isBuffer(address)) return address return address.and(MASK_160).toArrayLike(Buffer, 'be', 20) } diff --git a/lib/evm/interpreter.js b/lib/evm/interpreter.js index cc1d6cbcd4..e1c6f8dd8d 100644 --- a/lib/evm/interpreter.js +++ b/lib/evm/interpreter.js @@ -1,4 +1,3 @@ -const promisify = require('util.promisify') const BN = require('bn.js') const ethUtil = require('ethereumjs-util') const { ERROR } = require('../exceptions') @@ -48,8 +47,8 @@ module.exports = class Interpreter { } } - let { err, results } = await this.runLoop(message) - results = await this._parseRunResult(err, results, message, createdAddress) + let results = await this.runLoop(message) + results = await this._parseRunResult(results, message, createdAddress) // Save code if a new contract was created if (createdAddress && !results.runState.vmError && results.return && results.return.toString() !== '') { @@ -67,35 +66,20 @@ module.exports = class Interpreter { 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 + txContext: this._tx, + message } // Run code - let err, results + let 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 }) - }) - })) + results = this.runPrecompile(message.code, message.data, message.gasLimit) } else { - ({ err, results } = await loop.run(opts)) + results = await loop.run(opts) } - return { err, results } + return results } /** @@ -107,6 +91,20 @@ module.exports = class Interpreter { return getPrecompile(address.toString('hex')) } + runPrecompile (code, data, gasLimit) { + if (typeof code !== 'function') { + throw new Error('Invalid precompile') + } + + const opts = { + data, + gasLimit, + _common: this._vm._common + } + + return code(opts) + } + async _reduceSenderBalance (account, message) { if (!message.delegatecall) { const newBalance = new BN(account.balance).sub(message.value) @@ -165,7 +163,7 @@ module.exports = class Interpreter { // Setup new contract await this._state.clearContractStorage(address) - await promisify(this._vm.emit.bind(this._vm))('newContract', { + await this._vm._emit('newContract', { address: address, code: message.code }) @@ -175,7 +173,8 @@ module.exports = class Interpreter { return { toAccount: account, createdAddress } } - async _parseRunResult (err, results, message, createdAddress) { + async _parseRunResult (results, message, createdAddress) { + let err = results.exceptionError if (createdAddress) { // fee for size of the return value var totalGas = results.gasUsed diff --git a/lib/evm/loop.js b/lib/evm/loop.js index 87833109f9..d1bfa2c0cd 100644 --- a/lib/evm/loop.js +++ b/lib/evm/loop.js @@ -15,128 +15,137 @@ module.exports = class Loop { this._vm = vm // TODO: remove when not needed this._state = new PStateManager(vm.stateManager) this._interpreter = interpreter + this._runState = { + stopped: false, + programCounter: 0, + opCode: undefined, + memory: new Memory(), + memoryWordCount: new BN(0), + highestMemCost: new BN(0), + stack: new Stack(), + // TODO: Replace with EEI methods + _common: this._vm._common, + stateManager: this._state._wrapped, + storageReader: new StorageReader(this._state._wrapped) + } + this._result = { + logs: [], + returnValue: false, + gasRefund: new BN(0), + vmError: false, + selfdestruct: {} + } } async run (opts) { + if (opts.message.selfdestruct) { + this._result.selfdestruct = opts.message.selfdestruct + } + // Initialize internal vm state - const runState = await this.initRunState(opts) + await this.initRunState(opts) // Check that the programCounter is in range - const pc = runState.programCounter - if (pc !== 0 && (pc < 0 || pc >= runState.code.length)) { - return this.parseVmResults(runState, new VmError(ERROR.INVALID_OPCODE)) + 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.canContinueExecution(runState)) { - const opCode = runState.code[runState.programCounter] - runState.opCode = opCode - await this.runStepHook(runState) + while (this.canContinueExecution()) { + const opCode = this._runState.code[this._runState.programCounter] + this._runState.opCode = opCode + await this.runStepHook() try { - await this.runStep(runState) + await this.runStep() } catch (e) { err = e break } } - return this.parseVmResults(runState, err) + let gasUsed = opts.message.gasLimit.sub(this._runState.gasLeft) + if (err) { + if (err.error !== ERROR.REVERT) { + gasUsed = opts.message.gasLimit + } + + // remove any logs on error + this._result = Object.assign({}, this._result, { + logs: [], + vmError: true, + gasRefund: null, + selfdestruct: null + }) + } + + return Object.assign({}, this._result, { + runState: Object.assign({}, this._runState, this._result, this._runState.eei._env), + exception: err ? 0 : 1, + exceptionError: err, + gas: this._runState.gasLeft, + gasUsed, + 'return': this._result.returnValue ? this._result.returnValue : Buffer.alloc(0) + }) } - canContinueExecution (runState) { - const notAtEnd = runState.programCounter < runState.code.length - return !runState.stopped && notAtEnd && !runState.vmError && !runState.returnValue + canContinueExecution () { + const notAtEnd = this._runState.programCounter < this._runState.code.length + return !this._runState.stopped && notAtEnd && !this._result.vmError && !this._result.returnValue } async initRunState (opts) { - const runState = { - stateManager: this._state._wrapped, - storageReader: opts.storageReader || new StorageReader(this._state._wrapped), - returnValue: false, - stopped: false, - vmError: false, - programCounter: opts.pc | 0, - opCode: undefined, - opName: undefined, - gasLeft: new BN(opts.gasLimit), - gasLimit: new BN(opts.gasLimit), - gasPrice: opts.gasPrice, - memory: new Memory(), - memoryWordCount: new BN(0), - stack: new Stack(), - lastReturned: [], - logs: [], - validJumps: [], - gasRefund: new BN(0), - highestMemCost: new BN(0), - depth: opts.depth || 0, - // opts.suicides is kept for backward compatiblity with pre-EIP6 syntax - selfdestruct: opts.selfdestruct || opts.suicides || {}, - block: opts.block || new Block(), - callValue: opts.value || new BN(0), - address: opts.address || utils.zeros(32), - caller: opts.caller || utils.zeros(32), - origin: opts.origin || opts.caller || utils.zeros(32), - callData: opts.data || Buffer.from([0]), - code: opts.code, - static: opts.static || false, - // TODO: Replace with EEI methods + Object.assign(this._runState, { + code: opts.message.code, + programCounter: opts.pc | this._runState.programCounter, + gasLeft: new BN(opts.message.gasLimit), + validJumps: this._getValidJumpDests(opts.message.code), + storageReader: opts.storageReader || this._runState.storageReader + }) + + const env = { blockchain: this._vm.blockchain, // Only used in BLOCKHASH - interpreter: this._interpreter, // Used in CALLs and CREATEs - _common: this._vm._common - } - - // Do a pass over opcodes to detect valid jump destinations - runState.validJumps = this._getValidJumpDests(runState) - - // Ensure contract is loaded - if (!runState.contract) { - runState.contract = await this._state.getAccount(runState.address) + address: opts.message.to || utils.zeros(32), + caller: opts.message.caller || utils.zeros(32), + callData: opts.message.data || Buffer.from([0]), + callValue: opts.message.value || new BN(0), + code: opts.message.code, + isStatic: opts.message.isStatic || false, + depth: opts.message.depth || 0, + gasPrice: opts.txContext.gasPrice, + origin: opts.txContext.origin || opts.message.caller || utils.zeros(32), + block: opts.block || new Block(), + contract: await this._state.getAccount(opts.message.to || utils.zeros(32)) } - runState.eei = new EEI(runState) - - return runState + this._runState.eei = new EEI(env, this._runState, this._result, this._state, this._interpreter) } - async runStep (runState) { - const opInfo = lookupOpInfo(runState.opCode, false, this._vm.emitFreeLogs) - const opName = opInfo.name - - runState.opName = opName - + async runStep () { + const opInfo = lookupOpInfo(this._runState.opCode, false) // check for invalid opcode - if (runState.opName === 'INVALID') { + if (opInfo.name === 'INVALID') { throw new VmError(ERROR.INVALID_OPCODE) } // calculate gas - runState.gasLeft = runState.gasLeft.sub(new BN(opInfo.fee)) - if (runState.gasLeft.ltn(0)) { - runState.gasLeft = new BN(0) + this._runState.gasLeft = this._runState.gasLeft.sub(new BN(opInfo.fee)) + if (this._runState.gasLeft.ltn(0)) { + this._runState.gasLeft = new BN(0) throw new VmError(ERROR.OUT_OF_GAS) } // advance program counter - runState.programCounter++ - - // if opcode is log and emitFreeLogs is enabled, remove static context - let prevStatic = runState.static - if (this._vm.emitFreeLogs && runState.opName === 'LOG') { - runState.static = false - } + this._runState.programCounter++ - await this.handleOp(runState, opInfo) - - // restore previous static context - runState.static = prevStatic + await this.handleOp(opInfo) } - async handleOp (runState, opInfo) { + async handleOp (opInfo) { const opFn = this.getOpHandler(opInfo) - let args = [runState] + let args = [this._runState] // run the opcode if (opInfo.isAsync) { @@ -150,50 +159,18 @@ module.exports = class Loop { return opFns[opInfo.name] } - async parseVmResults (runState, err) { - // remove any logs on error - if (err) { - runState.logs = [] - runState.vmError = true - } - - const results = { - runState: runState, - selfdestruct: runState.selfdestruct, - gasRefund: runState.gasRefund, - exception: err ? 0 : 1, - exceptionError: err, - logs: runState.logs, - gas: runState.gasLeft, - 'return': runState.returnValue ? runState.returnValue : Buffer.alloc(0) - } - - if (results.exceptionError) { - delete results.gasRefund - delete results.selfdestruct - } - - if (err && err.error !== ERROR.REVERT) { - results.gasUsed = runState.gasLimit - } else { - results.gasUsed = runState.gasLimit.sub(runState.gasLeft) - } - - return { err, results } - } - - async runStepHook (runState) { + async runStepHook () { const eventObj = { - pc: runState.programCounter, - gasLeft: runState.gasLeft, - opcode: lookupOpInfo(runState.opCode, true, this._vm.emitFreeLogs), - stack: runState.stack._store, - depth: runState.depth, - address: runState.address, - account: runState.contract, - stateManager: runState.stateManager, - memory: runState.memory._store, // Return underlying array for backwards-compatibility - memoryWordCount: runState.memoryWordCount + pc: this._runState.programCounter, + gasLeft: this._runState.gasLeft, + opcode: lookupOpInfo(this._runState.opCode, true), + stack: this._runState.stack._store, + depth: this._runState.eei._env.depth, + address: this._runState.eei._env.address, + account: this._runState.eei._env.contract, + stateManager: this._runState.stateManager, + memory: this._runState.memory._store, // Return underlying array for backwards-compatibility + memoryWordCount: this._runState.memoryWordCount } /** * The `step` event for trace output @@ -215,15 +192,15 @@ module.exports = class Loop { } // Returns all valid jump destinations. - _getValidJumpDests (runState) { + _getValidJumpDests (code) { const jumps = [] - for (let i = 0; i < runState.code.length; i++) { - const curOpCode = lookupOpInfo(runState.code[i]).name + for (let i = 0; i < code.length; i++) { + const curOpCode = lookupOpInfo(code[i]).name // no destinations into the middle of PUSH if (curOpCode === 'PUSH') { - i += runState.code[i] - 0x5f + i += code[i] - 0x5f } if (curOpCode === 'JUMPDEST') { diff --git a/lib/evm/opFns.js b/lib/evm/opFns.js index 210bce29a5..205e7b33cb 100644 --- a/lib/evm/opFns.js +++ b/lib/evm/opFns.js @@ -4,7 +4,6 @@ const BN = utils.BN const exceptions = require('../exceptions.js') const ERROR = exceptions.ERROR const VmError = exceptions.VmError -const PStateManager = require('../state/promisified').default const MASK_160 = new BN(1).shln(160).subn(1) // Find Ceil(`this` / `num`) @@ -370,11 +369,9 @@ module.exports = { if (!runState._common.gteHardfork('constantinople')) { trap(ERROR.INVALID_OPCODE) } - const stateManager = new PStateManager(runState.stateManager) - address = addressToBuffer(address) - const account = await stateManager.getAccount(address) - if (account.isEmpty()) { + const empty = await runState.eei.isAccountEmpty(address) + if (empty) { runState.stack.push(new BN(0)) return } @@ -475,7 +472,7 @@ module.exports = { runState.stack.push(value) }, SSTORE: async function (runState) { - if (runState.static) { + if (runState.eei.isStatic()) { trap(ERROR.STATIC_STATE_CHANGE) } @@ -537,7 +534,7 @@ module.exports = { JUMPDEST: function (runState) {}, PUSH: function (runState) { const numToPush = runState.opCode - 0x5f - const loaded = new BN(runState.code.slice(runState.programCounter, runState.programCounter + numToPush).toString('hex'), 16) + const loaded = new BN(runState.eei.getCode().slice(runState.programCounter, runState.programCounter + numToPush).toString('hex'), 16) runState.programCounter += numToPush runState.stack.push(loaded) }, @@ -550,7 +547,7 @@ module.exports = { runState.stack.swap(stackPos) }, LOG: function (runState) { - if (runState.static) { + if (runState.eei.isStatic()) { trap(ERROR.STATIC_STATE_CHANGE) } @@ -578,7 +575,7 @@ module.exports = { // '0xf0' range - closures CREATE: async function (runState) { - if (runState.static) { + if (runState.eei.isStatic()) { trap(ERROR.STATIC_STATE_CHANGE) } @@ -601,7 +598,7 @@ module.exports = { trap(ERROR.INVALID_OPCODE) } - if (runState.static) { + if (runState.eei.isStatic()) { trap(ERROR.STATIC_STATE_CHANGE) } @@ -610,8 +607,8 @@ module.exports = { subMemUsage(runState, offset, length) // Deduct gas costs for hashing runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sha3Word')).imul(length.divCeil(new BN(32)))) - let gasLimit = new BN(runState.gasLeft) - gasLimit = maxCallGas(gasLimit, runState.gasLeft) + let gasLimit = new BN(runState.eei.getGasLeft()) + gasLimit = maxCallGas(gasLimit, runState.eei.getGasLeft()) let data = Buffer.alloc(0) if (!length.isZero()) { @@ -625,7 +622,7 @@ module.exports = { let [gasLimit, toAddress, value, inOffset, inLength, outOffset, outLength] = runState.stack.popN(7) toAddress = addressToBuffer(toAddress) - if (runState.static && !value.isZero()) { + if (runState.eei.isStatic() && !value.isZero()) { trap(ERROR.STATIC_STATE_CHANGE) } @@ -641,8 +638,7 @@ module.exports = { data = runState.memory.read(inOffset.toNumber(), inLength.toNumber()) } - const state = new PStateManager(runState.stateManager) - const empty = await state.accountIsEmpty(toAddress) + const empty = await runState.eei.isAccountEmpty(toAddress) if (empty) { if (!value.isZero()) { runState.eei.useGas(new BN(runState._common.param('gasPrices', 'callNewAccount'))) @@ -685,7 +681,7 @@ module.exports = { runState.stack.push(ret) }, DELEGATECALL: async function (runState) { - const value = runState.callValue + const value = runState.eei.getCallValue() let [gasLimit, toAddress, inOffset, inLength, outOffset, outLength] = runState.stack.popN(6) toAddress = addressToBuffer(toAddress) @@ -744,18 +740,26 @@ module.exports = { // '0x70', range - other SELFDESTRUCT: async function (runState) { let selfdestructToAddress = runState.stack.pop() - if (runState.static) { + if (runState.eei.isStatic()) { trap(ERROR.STATIC_STATE_CHANGE) } + const balance = await runState.eei.getExternalBalance(runState.eei.getAddress()) + if (balance.gtn(0)) { + const empty = await runState.eei.isAccountEmpty(selfdestructToAddress) + if (empty) { + runState.eei.useGas(new BN(runState._common.param('gasPrices', 'callNewAccount'))) + } + } + selfdestructToAddress = addressToBuffer(selfdestructToAddress) return runState.eei.selfDestruct(selfdestructToAddress) } } function describeLocation (runState) { - var hash = utils.keccak256(runState.code).toString('hex') - var address = runState.address.toString('hex') + var hash = utils.keccak256(runState.eei.getCode()).toString('hex') + var address = runState.eei.getAddress().toString('hex') var pc = runState.programCounter - 1 return hash + '/' + address + ':' + pc } @@ -860,7 +864,7 @@ function updateSstoreGas (runState, found, value) { } if (value.length === 0) { // If new value is 0, add 15000 gas to refund counter. - runState.gasRefund = runState.gasRefund.addn(runState._common.param('gasPrices', 'netSstoreClearRefund')) + runState.eei.refundGas(runState._common.param('gasPrices', 'netSstoreClearRefund')) } // Otherwise, 5000 gas is deducted. return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'netSstoreCleanGas'))) @@ -870,20 +874,21 @@ function updateSstoreGas (runState, found, value) { // If original value is not 0 if (current.length === 0) { // If current value is 0 (also means that new value is not 0), remove 15000 gas from refund counter. We can prove that refund counter will never go below 0. - runState.gasRefund = runState.gasRefund.subn(runState._common.param('gasPrices', 'netSstoreClearRefund')) + // TODO: Remove usage of private attr + runState.eei._result.gasRefund = runState.eei._result.gasRefund.subn(runState._common.param('gasPrices', 'netSstoreClearRefund')) } else if (value.length === 0) { // If new value is 0 (also means that current value is not 0), add 15000 gas to refund counter. - runState.gasRefund = runState.gasRefund.addn(runState._common.param('gasPrices', 'netSstoreClearRefund')) + runState.eei.refundGas(runState._common.param('gasPrices', 'netSstoreClearRefund')) } } if (original.equals(value)) { // If original value equals new value (this storage slot is reset) if (original.length === 0) { // If original value is 0, add 19800 gas to refund counter. - runState.gasRefund = runState.gasRefund.addn(runState._common.param('gasPrices', 'netSstoreResetClearRefund')) + runState.eei.refundGas(runState._common.param('gasPrices', 'netSstoreResetClearRefund')) } else { // Otherwise, add 4800 gas to refund counter. - runState.gasRefund = runState.gasRefund.addn(runState._common.param('gasPrices', 'netSstoreResetRefund')) + runState.eei.refundGas(runState._common.param('gasPrices', 'netSstoreResetRefund')) } } return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'netSstoreDirtyGas'))) @@ -892,7 +897,7 @@ function updateSstoreGas (runState, found, value) { runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreReset'))) } else if (value.length === 0 && found.length) { runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreReset'))) - runState.gasRefund.iaddn(runState._common.param('gasPrices', 'sstoreRefund')) + runState.eei.refundGas(runState._common.param('gasPrices', 'sstoreRefund')) } else if (value.length !== 0 && !found.length) { runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreSet'))) } else if (value.length !== 0 && found.length) { diff --git a/lib/evm/opcodes.ts b/lib/evm/opcodes.ts index 232c5a8b41..2d4c6cb605 100644 --- a/lib/evm/opcodes.ts +++ b/lib/evm/opcodes.ts @@ -170,9 +170,9 @@ const codes: any = { 0xff: ['SELFDESTRUCT', 5000, false, true] } -export function lookupOpInfo (op: number, full: boolean, freeLogs: boolean): OpInfo { - var code = codes[op] ? codes[op] : ['INVALID', 0, false, false] - var opcode = code[0] +export function lookupOpInfo (op: number, full: boolean): OpInfo { + const code = codes[op] ? codes[op] : ['INVALID', 0, false, false] + let opcode = code[0] if (full) { if (opcode === 'LOG') { @@ -192,13 +192,5 @@ export function lookupOpInfo (op: number, full: boolean, freeLogs: boolean): OpI } } - var fee = code[1] - - if (freeLogs) { - if (opcode === 'LOG') { - fee = 0 - } - } - - return { name: opcode, opcode: op, fee: fee, dynamic: code[2], isAsync: code[3] } + return { name: opcode, opcode: op, fee: code[1], dynamic: code[2], isAsync: code[3] } } diff --git a/lib/index.js b/lib/index.js index a45c114ab7..dc890fba63 100644 --- a/lib/index.js +++ b/lib/index.js @@ -20,7 +20,6 @@ const BN = ethUtil.BN * @param {String} opts.hardfork hardfork rules to be used [default: 'byzantium', supported: 'byzantium', 'constantinople', 'petersburg' (will throw on unsupported)] * @param {Boolean} opts.activatePrecompiles create entries in the state tree for the precompiled contracts * @param {Boolean} opts.allowUnlimitedContractSize allows unlimited contract sizes while debugging. By setting this to `true`, the check for contract size limit of 24KB (see [EIP-170](https://git.io/vxZkK)) is bypassed. (default: `false`; ONLY set to `true` during debugging) - * @param {Boolean} opts.emitFreeLogs Changes the behavior of the LOG opcode, the gas cost of the opcode becomes zero and calling it using STATICCALL won't throw. (default: `false`; ONLY set to `true` during debugging) */ module.exports = class VM extends AsyncEventEmitter { constructor (opts = {}) { @@ -52,9 +51,9 @@ module.exports = class VM extends AsyncEventEmitter { this.blockchain = opts.blockchain || new Blockchain({ common: this._common }) this.allowUnlimitedContractSize = opts.allowUnlimitedContractSize === undefined ? false : opts.allowUnlimitedContractSize - this.emitFreeLogs = opts.emitFreeLogs === undefined ? false : opts.emitFreeLogs this.runCode = require('./runCode.js').bind(this) + // @deprecated this.runJIT = require('./runJit.js').bind(this) this.runBlock = require('./runBlock.js').bind(this) this.runTx = require('./runTx.js').bind(this) diff --git a/lib/runCode.js b/lib/runCode.js index 16cdadc867..556d09cb9b 100644 --- a/lib/runCode.js +++ b/lib/runCode.js @@ -14,6 +14,7 @@ item length then you must use utils.pad(, 32) first. const Block = require('ethereumjs-block') const Loop = require('./evm/loop') const TxContext = require('./evm/txContext').default +const Message = require('./evm/message').default const Interpreter = require('./evm/interpreter') const { StorageReader } = require('./state') @@ -57,16 +58,33 @@ module.exports = function (opts, cb) { opts.storageReader = new StorageReader(this.stateManager) } + // Backwards compatibility + if (!opts.txContext) { + opts.txContext = new TxContext(opts.gasPrice, opts.origin || opts.caller) + } + if (!opts.message) { + opts.message = new Message({ + code: opts.code, + data: opts.data, + gasLimit: opts.gasLimit, + to: opts.address, + caller: opts.caller, + value: opts.value, + depth: opts.depth, + selfdestruct: opts.selfdestruct, + isStatic: opts.isStatic + }) + } + let interpreter = opts.interpreter if (!interpreter) { - const txContext = new TxContext(opts.gasPrice, opts.origin || opts.caller) - interpreter = new Interpreter(this, txContext, opts.block, opts.storageReader) + interpreter = new Interpreter(this, opts.txContext, opts.block, opts.storageReader) } opts.txRunState = interpreter.txRunState const loop = new Loop(this, interpreter) loop.run(opts) - .then((res) => cb(res.err, res.results)) + .then((res) => cb(res.exceptionError, res)) .catch((err) => cb(err, null)) } diff --git a/lib/runJit.js b/lib/runJit.js index 9515b7de85..23c9fc3f1a 100644 --- a/lib/runJit.js +++ b/lib/runJit.js @@ -1,3 +1,4 @@ +// @deprecated module.exports = function (opts, cb) { // for precompiled var results diff --git a/tests/api/freeLogs.js b/tests/api/freeLogs.js deleted file mode 100644 index e0bd004502..0000000000 --- a/tests/api/freeLogs.js +++ /dev/null @@ -1,64 +0,0 @@ -const tape = require('tape') -const VM = require('../../dist/index') - -/* -contract Contract1 { - event Event(); - function() external { - emit Event(); - } -} -*/ - -const code = Buffer.from('6080604052348015600f57600080fd5b506040517f57050ab73f6b9ebdd9f76b8d4997793f48cf956e965ee070551b9ca0bb71584e90600090a10000a165627a7a72305820f80265dc41ca5376abe548a02070b68b91119b77dc54c76563b9c19a758cf26f0029', 'hex') - -tape('VM with free logs', async (t) => { - t.test('should run code without charging for log opcode', async (st) => { - const vm = new VM({ emitFreeLogs: true }) - vm.runCode({ - code: code, - gasLimit: 1000 - }, function (err, val) { - st.notOk(err) - st.ok(val.runState.gasLeft >= 0x235, 'should expend less gas') - st.ok(val.logs.length === 1, 'should emit event') - st.end() - }) - }) - t.test('should charge normal gas if flag is not set', async (st) => { - const vm = new VM() - vm.runCode({ - code: code, - gasLimit: 1000 - }, function (err, val) { - st.notOk(err) - st.ok(val.runState.gasLeft < 0x235, 'should expend normal gas') - st.ok(val.logs.length === 1, 'should emit event') - st.end() - }) - }) - t.test('should emit event on static context', async (st) => { - const vm = new VM({ emitFreeLogs: true }) - vm.runCode({ - code: code, - gasLimit: 24000, - static: true - }, function (err, val) { - st.notOk(err) - st.ok(val.logs.length === 1, 'should emit event') - st.end() - }) - }) - t.test('should not emit event on static context if flag is not set', async (st) => { - const vm = new VM() - vm.runCode({ - code: code, - gasLimit: 24000, - static: true - }, function (err, val) { - st.ok(err) - st.ok(val.logs.length === 0, 'should emit zero events') - st.end() - }) - }) -}) diff --git a/tests/api/runCode.js b/tests/api/runCode.js index e626803c02..6efa895e6e 100644 --- a/tests/api/runCode.js +++ b/tests/api/runCode.js @@ -1,5 +1,6 @@ const tape = require('tape') const async = require('async') +const BN = require('bn.js') const VM = require('../../dist/index') const STOP = '00' @@ -9,8 +10,8 @@ const PUSH1 = '60' const testCases = [ { code: [STOP, JUMPDEST, PUSH1, '05', JUMP, JUMPDEST], pc: 1, resultPC: 6 }, - { code: [STOP, JUMPDEST, PUSH1, '05', JUMP, JUMPDEST], pc: -1, error: 'invalid opcode' }, - { code: [STOP], pc: 3, error: 'invalid opcode' }, + { code: [STOP, JUMPDEST, PUSH1, '05', JUMP, JUMPDEST], pc: -1, error: 'Internal error: program counter not in range' }, + { code: [STOP], pc: 3, error: 'Internal error: program counter not in range' }, { code: [STOP], resultPC: 1 } ] @@ -22,7 +23,7 @@ tape('VM.runcode: initial program counter', function (t) { const runCodeArgs = { code: Buffer.from(testData.code.join(''), 'hex'), pc: testData.pc, - gasLimit: 0xffff + gasLimit: new BN(0xffff) } let result @@ -45,7 +46,7 @@ tape('VM.runcode: initial program counter', function (t) { } ], function (err) { if (testData.error) { - err = err ? err.error : 'no error thrown' + err = err ? err.message : 'no error thrown' t.equals(err, testData.error, 'error message should match') err = false }