From cca37a6685b26413806a147cddc118d515122fc3 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 22 Sep 2020 18:52:27 -0700 Subject: [PATCH 1/9] Initial draft --- packages/common/src/eips/2929.json | 24 +++ packages/common/src/eips/index.ts | 1 + packages/common/tests/eips.ts | 2 +- packages/vm/lib/evm/interpreter.ts | 20 ++ packages/vm/lib/evm/opcodes/functions.ts | 25 ++- packages/vm/lib/evm/opcodes/util.ts | 107 +++++++++- packages/vm/lib/index.ts | 2 +- packages/vm/tests/api/EIPs/eip-2929.spec.ts | 204 ++++++++++++++++++++ 8 files changed, 376 insertions(+), 9 deletions(-) create mode 100644 packages/common/src/eips/2929.json create mode 100644 packages/vm/tests/api/EIPs/eip-2929.spec.ts diff --git a/packages/common/src/eips/2929.json b/packages/common/src/eips/2929.json new file mode 100644 index 0000000000..9be41b5408 --- /dev/null +++ b/packages/common/src/eips/2929.json @@ -0,0 +1,24 @@ +{ + "name": "EIP-2929", + "comment": "Gas cost increases for state access opcodes", + "url": "https://eips.ethereum.org/EIPS/eip-2929", + "status": "Draft", + "minimumHardfork": "chainstart", + "gasConfig": {}, + "gasPrices": { + "coldsload": { + "v": 2100, + "d": "Gas cost of the first read of storage from a given location (per transaction)" + }, + "coldaccountaccess": { + "v": 2600, + "d": "Gas cost of the first read of a given address (per transaction)" + }, + "warmstorageread": { + "v": 100, + "d": "Gas cost of reading storage locations which have already loaded 'cold'" + } + }, + "vm": {}, + "pow": {} +} diff --git a/packages/common/src/eips/index.ts b/packages/common/src/eips/index.ts index 29a6a99455..18c1762626 100644 --- a/packages/common/src/eips/index.ts +++ b/packages/common/src/eips/index.ts @@ -3,4 +3,5 @@ import { eipsType } from './../types' export const EIPs: eipsType = { 2315: require('./2315.json'), 2537: require('./2537.json'), + 2929: require('./2929.json'), } diff --git a/packages/common/tests/eips.ts b/packages/common/tests/eips.ts index 3c13900750..0faafe0fd5 100644 --- a/packages/common/tests/eips.ts +++ b/packages/common/tests/eips.ts @@ -3,7 +3,7 @@ import Common from '../src/' tape('[Common]: Initialization / Chain params', function (t: tape.Test) { t.test('Correct initialization', function (st: tape.Test) { - const eips = [2537] + const eips = [2537, 2929] const c = new Common({ chain: 'mainnet', eips }) st.equal(c.eips(), eips, 'should initialize with supported EIP') st.end() diff --git a/packages/vm/lib/evm/interpreter.ts b/packages/vm/lib/evm/interpreter.ts index 27fe2afc73..b731ab2707 100644 --- a/packages/vm/lib/evm/interpreter.ts +++ b/packages/vm/lib/evm/interpreter.ts @@ -6,6 +6,7 @@ import Memory from './memory' import Stack from './stack' import EEI from './eei' import { Opcode, handlers as opHandlers, OpHandler } from './opcodes' +import { precompiles } from './precompiles' export interface InterpreterOpts { pc?: number @@ -25,6 +26,8 @@ export interface RunState { _common: Common stateManager: StateManager eei: EEI + accessedAddresses: Set + accessedStorage: Map> } export interface InterpreterResult { @@ -84,6 +87,8 @@ export default class Interpreter { _common: this._vm._common, stateManager: this._state, eei: this._eei, + accessedAddresses: new Set(), + accessedStorage: new Map(), } } @@ -94,6 +99,8 @@ export default class Interpreter { const valid = this._getValidJumpDests(code) this._runState.validJumps = valid.jumps this._runState.validJumpSubs = valid.jumpSubs + this._initAccessedAddresses() + this._runState.accessedStorage.clear() // Check that the programCounter is in range const pc = this._runState.programCounter @@ -230,4 +237,17 @@ export default class Interpreter { return { jumps, jumpSubs } } + + // Populates accessedAddresses with 'pre-warmed' addresses. Includes + // tx.origin, `this` (e.g the address of the code being executed), and + // all the precompiles. (EIP 2929) + _initAccessedAddresses() { + this._runState.accessedAddresses.clear() + this._runState.accessedAddresses.add(this._eei._env.origin.toString('hex')) + this._runState.accessedAddresses.add(this._eei.getAddress().toString('hex')) + + for (let address of Object.keys(precompiles)) { + this._runState.accessedAddresses.add(address) + } + } } diff --git a/packages/vm/lib/evm/opcodes/functions.ts b/packages/vm/lib/evm/opcodes/functions.ts index 34cd338c21..dbc5ee25fa 100644 --- a/packages/vm/lib/evm/opcodes/functions.ts +++ b/packages/vm/lib/evm/opcodes/functions.ts @@ -408,6 +408,7 @@ export const handlers: Map = new Map([ async function (runState: RunState) { const addressBN = runState.stack.pop() const address = new Address(addressToBuffer(addressBN)) + accessAddressEIP2929(runState, address.buf, runState._common.param('gasPrices', 'balance')) const balance = await runState.eei.getExternalBalance(address) runState.stack.push(balance) }, @@ -507,6 +508,7 @@ export const handlers: Map = new Map([ 0x3b, async function (runState: RunState) { const address = runState.stack.pop() + accessAddressEIP2929(runState, address, runState._common.param('gasPrices', 'extcodesize')) const size = await runState.eei.getExternalCodeSize(address) runState.stack.push(size) }, @@ -519,6 +521,7 @@ export const handlers: Map = new Map([ // FIXME: for some reason this must come before subGas subMemUsage(runState, memOffset, length) + accessAddressEIP2929(runState, address, runState._common.param('gasPrices', 'extcodecopy')) // copy fee runState.eei.useGas( new BN(runState._common.param('gasPrices', 'copy')).imul(divCeil(length, new BN(32))) @@ -539,6 +542,7 @@ export const handlers: Map = new Map([ async function (runState: RunState) { const addressBN = runState.stack.pop() const address = new Address(addressToBuffer(addressBN)) + accessAddressEIP2929(runState, address.buf, runState._common.param('gasPrices', 'extcodehash')) const empty = await runState.eei.isAccountEmpty(address) if (empty) { runState.stack.push(new BN(0)) @@ -710,6 +714,7 @@ export const handlers: Map = new Map([ const key = runState.stack.pop() const keyBuf = key.toArrayLike(Buffer, 'be', 32) + accessStorageEIP2929(runState, keyBuf, false) const value = await runState.eei.storageLoad(keyBuf) const valueBN = value.length ? new BN(value) : new BN(0) runState.stack.push(valueBN) @@ -736,7 +741,8 @@ export const handlers: Map = new Map([ // TODO: Replace getContractStorage with EEI method const found = await getContractStorage(runState, runState.eei.getAddress(), keyBuf) - updateSstoreGas(runState, found, setLengthLeftStorage(value)) + accessStorageEIP2929(runState, keyBuf, true) + updateSstoreGas(runState, found, setLengthLeftStorage(value), keyBuf) await runState.eei.storageStore(keyBuf, value) }, ], @@ -924,6 +930,7 @@ export const handlers: Map = new Map([ data = runState.memory.read(offset.toNumber(), length.toNumber()) } + accessAddressEIP2929(runState, runState.eei.getAddress()) const ret = await runState.eei.create(gasLimit, value, data) runState.stack.push(ret) }, @@ -939,6 +946,8 @@ export const handlers: Map = new Map([ const [value, offset, length, salt] = runState.stack.popN(4) subMemUsage(runState, offset, length) + accessAddressEIP2929(runState, runState.eei.getAddress()) + // Deduct gas costs for hashing runState.eei.useGas( new BN(runState._common.param('gasPrices', 'sha3Word')).imul(divCeil(length, new BN(32))) @@ -974,6 +983,7 @@ export const handlers: Map = new Map([ } subMemUsage(runState, inOffset, inLength) subMemUsage(runState, outOffset, outLength) + accessAddressEIP2929(runState, toAddressBuf, runState._common.param('gasPrices', 'call')) if (!value.isZero()) { runState.eei.useGas(new BN(runState._common.param('gasPrices', 'callValueTransfer'))) @@ -1025,6 +1035,8 @@ export const handlers: Map = new Map([ subMemUsage(runState, inOffset, inLength) subMemUsage(runState, outOffset, outLength) + accessAddressEIP2929(runState, toAddressBuf, runState._common.param('gasPrices', 'callcode')) + if (!value.isZero()) { runState.eei.useGas(new BN(runState._common.param('gasPrices', 'callValueTransfer'))) } @@ -1060,6 +1072,12 @@ export const handlers: Map = new Map([ subMemUsage(runState, inOffset, inLength) subMemUsage(runState, outOffset, outLength) + accessAddressEIP2929( + runState, + toAddressBuf, + runState._common.param('gasPrices', 'delegatecall'), + ) + gasLimit = maxCallGas(gasLimit, runState.eei.getGasLeft(), runState) // note that TangerineWhistle or later this cannot happen (it could have ran out of gas prior to getting here though) if (gasLimit.gt(runState.eei.getGasLeft())) { @@ -1087,6 +1105,11 @@ export const handlers: Map = new Map([ subMemUsage(runState, inOffset, inLength) subMemUsage(runState, outOffset, outLength) + accessAddressEIP2929( + runState, + toAddressBuf, + runState._common.param('gasPrices', 'staticcall'), + ) gasLimit = maxCallGas(gasLimit, runState.eei.getGasLeft(), runState) // we set TangerineWhistle or later to true here, as STATICCALL was available from Byzantium (which is after TangerineWhistle) let data = Buffer.alloc(0) diff --git a/packages/vm/lib/evm/opcodes/util.ts b/packages/vm/lib/evm/opcodes/util.ts index aed3d7d7f0..8e0f3462fb 100644 --- a/packages/vm/lib/evm/opcodes/util.ts +++ b/packages/vm/lib/evm/opcodes/util.ts @@ -35,10 +35,103 @@ export function trap(err: string) { * @param {BN} address * @return {Buffer} */ -export function addressToBuffer(address: BN): Buffer { +function addressToBuffer(address: BN | Buffer) { + if (Buffer.isBuffer(address)) return address return address.and(MASK_160).toArrayLike(Buffer, 'be', 20) } +/** + * Adds address to accessedAddresses set if not already included. + * Adjusts cost incurred for executing opcode based on whether address read + * is warm/cold. (EIP 2929) + * @param {RunState} runState + * @param {BN} address + */ +function accessAddressEIP2929(runState: RunState, address: BN | Buffer, baseFee?: number) { + if (!runState._common.eips().includes(2929)) return + + const addressStr = addressToBuffer(address).toString('hex') + + // Cold + if (!runState.accessedAddresses.has(addressStr)) { + runState.accessedAddresses.add(addressStr) + + // CREATE, CREATE2 opcodes have the address warmed for free. + if (baseFee) { + runState.eei.useGas( + new BN(runState._common.param('gasPrices', 'coldaccountaccess') - baseFee), + ) + } + // Warm + } else if (baseFee) { + runState.eei.useGas(new BN(runState._common.param('gasPrices', 'warmstorageread') - baseFee)) + } +} + +/** + * Adds (address, key) to accessedStorage tuple set if not already included. + * Adjusts cost incurred for executing opcode based on whether storage read + * is warm/cold. (EIP 2929) + * @param {RunState} runState + * @param {Buffer} key (to storage slot) + */ +function accessStorageEIP2929(runState: RunState, key: Buffer, isSstore: boolean) { + if (!runState._common.eips().includes(2929)) return + + const keyStr = key.toString('hex') + const baseFee = !isSstore ? runState._common.param('gasPrices', 'sload') : 0 + const address = runState.eei.getAddress().toString('hex') + const keysAtAddress = runState.accessedStorage.get(address) + + // Cold (SLOAD and SSTORE) + if (!keysAtAddress) { + runState.accessedStorage.set(address, new Set()) + // @ts-ignore Set Object is possibly 'undefined' + runState.accessedStorage.get(address).add(keyStr) + runState.eei.useGas(new BN(runState._common.param('gasPrices', 'coldsload') - baseFee)) + } else if (keysAtAddress && !keysAtAddress.has(keyStr)) { + keysAtAddress.add(keyStr) + runState.eei.useGas(new BN(runState._common.param('gasPrices', 'coldsload') - baseFee)) + // Warm (SLOAD only) + } else if (!isSstore) { + runState.eei.useGas(new BN(runState._common.param('gasPrices', 'warmstorageread') - baseFee)) + } +} + +/** + * Adjusts cost of SSTORE_RESET_GAS or SLOAD (aka sstorenoop) (EIP-2200) downward when storage + * location is already warm + * @param {RunState} runState + * @param {Buffer} key storage slot + * @param {number} defaultCost SSTORE_RESET_GAS / SLOAD + * @param {string} costName parameter name ('reset' or 'noop') + * @return {number} adjusted cost + */ +function adjustSstoreGasEIP2929( + runState: RunState, + key: Buffer, + defaultCost: number, + costName: string, +): number { + if (!runState._common.eips().includes(2929)) return defaultCost + + const keyStr = key.toString('hex') + const address = runState.eei.getAddress().toString('hex') + + // @ts-ignore Set Object is possibly 'undefined' + if (runState.accessedStorage.has(address) && runState.accessedStorage.get(address).has(keyStr)) { + if (costName === 'reset') { + return defaultCost - runState._common.param('gasPrices', 'coldsload') + } + + if (costName === 'noop') { + return runState._common.param('gasPrices', 'warmstorageread') + } + } + + return defaultCost +} + /** * Error message helper - generates location string * @@ -225,7 +318,7 @@ export function subMemUsage(runState: RunState, offset: BN, length: BN) { * @param {any} found * @param {Buffer} value */ -export function updateSstoreGas(runState: RunState, found: any, value: Buffer) { +function updateSstoreGas(runState: RunState, found: any, value: Buffer, key: Buffer) { if (runState._common.hardfork() === 'constantinople') { const original = found.original const current = found.current @@ -285,8 +378,9 @@ export function updateSstoreGas(runState: RunState, found: any, value: Buffer) { // Noop if (current.equals(value)) { + const sstoreNoopCost = runState._common.param('gasPrices', 'sstoreNoopGasEIP2200') return runState.eei.useGas( - new BN(runState._common.param('gasPrices', 'sstoreNoopGasEIP2200')) + new BN(adjustSstoreGasEIP2929(runState, key, sstoreNoopCost, 'noop')), ) } if (original.equals(current)) { @@ -336,16 +430,17 @@ export function updateSstoreGas(runState: RunState, found: any, value: Buffer) { // Dirty update return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreDirtyGasEIP2200'))) } else { + const sstoreResetCost = runState._common.param('gasPrices', 'sstoreReset') if (value.length === 0 && !found.length) { - runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreReset'))) + runState.eei.useGas(new BN(adjustSstoreGasEIP2929(runState, key, sstoreResetCost, 'reset'))) } else if (value.length === 0 && found.length) { - runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreReset'))) + runState.eei.useGas(new BN(adjustSstoreGasEIP2929(runState, key, sstoreResetCost, 'reset'))) runState.eei.refundGas(new BN(runState._common.param('gasPrices', 'sstoreRefund'))) } else if (value.length !== 0 && !found.length) { runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreSet'))) /* eslint-disable-next-line sonarjs/no-duplicated-branches */ } else if (value.length !== 0 && found.length) { - runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreReset'))) + runState.eei.useGas(new BN(adjustSstoreGasEIP2929(runState, key, sstoreResetCost, 'reset'))) } } } diff --git a/packages/vm/lib/index.ts b/packages/vm/lib/index.ts index d6f937a392..31895743bb 100644 --- a/packages/vm/lib/index.ts +++ b/packages/vm/lib/index.ts @@ -134,7 +134,7 @@ export default class VM extends AsyncEventEmitter { if (opts.common) { //EIPs - const supportedEIPs = [2537] + const supportedEIPs = [2537, 2929] for (const eip of opts.common.eips()) { if (!supportedEIPs.includes(eip)) { throw new Error(`${eip} is not supported by the VM`) diff --git a/packages/vm/tests/api/EIPs/eip-2929.spec.ts b/packages/vm/tests/api/EIPs/eip-2929.spec.ts new file mode 100644 index 0000000000..2994fc6fb8 --- /dev/null +++ b/packages/vm/tests/api/EIPs/eip-2929.spec.ts @@ -0,0 +1,204 @@ +import * as tape from 'tape' +import { BN } from 'ethereumjs-util' +import VM from '../../../lib' +import Common from '@ethereumjs/common' +import { inspect } from 'util' + +// Test cases source: https://gist.github.com/holiman/174548cad102096858583c6fbbb0649a +tape('EIP 2929: gas cost tests', (t) => { + const initialGas = new BN(0xffffffffff) + const address = Buffer.from('000000000000000000000000636F6E7472616374', 'hex') + const common = new Common({ chain: 'mainnet', hardfork: 'berlin', eips: [2929] }) + + const runTest = async function (test: any, st: tape.Test) { + let i = 0 + let currentGas = initialGas + const vm = new VM({ common }) + + vm.on('step', function (step: any) { + const gasUsed = currentGas.sub(step.gasLeft) + currentGas = step.gasLeft + + if (test.steps.length) { + st.equal(step.opcode.name, test.steps[i].expectedOpcode) + + // Validates the gas consumption of the (i - 1)th opcode + // b/c the step event fires before gas is debited. + // The first opcode of every test should be +/- irrelevant + // (ex: PUSH) and the last opcode is always STOP + if (i > 0) { + st.equal(true, gasUsed.eq(new BN(test.steps[i - 1].expectedGasUsed))) + } + } + i++ + }) + + const result = await vm.runCode({ + code: Buffer.from(test.code, 'hex'), + gasLimit: initialGas, + address: address, + origin: address, + }) + + const totalGasUsed = initialGas.sub(currentGas) + st.equal(true, totalGasUsed.eq(new BN(test.totalGasUsed))) + return result + } + + // Checks EXT(codehash,codesize,balance) of precompiles, which should be 100, + // and later checks the same operations twice against some non-precompiles. Those are + // cheaper second time they are accessed. Lastly, it checks the BALANCE of origin and this. + t.test('should charge for warm address loads correctly', async (st) => { + const test = { + code: + '60013f5060023b506003315060f13f5060f23b5060f3315060f23f5060f33b5060f1315032315030315000', + totalGasUsed: 8653, + steps: [ + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'EXTCODEHASH', expectedGasUsed: 100 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'EXTCODESIZE', expectedGasUsed: 100 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'BALANCE', expectedGasUsed: 100 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'EXTCODEHASH', expectedGasUsed: 2600 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'EXTCODESIZE', expectedGasUsed: 2600 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'BALANCE', expectedGasUsed: 2600 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'EXTCODEHASH', expectedGasUsed: 100 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'EXTCODESIZE', expectedGasUsed: 100 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'BALANCE', expectedGasUsed: 100 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'ORIGIN', expectedGasUsed: 2 }, + { expectedOpcode: 'BALANCE', expectedGasUsed: 100 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'ADDRESS', expectedGasUsed: 2 }, + { expectedOpcode: 'BALANCE', expectedGasUsed: 100 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'STOP', expectedGasUsed: 0 }, + ], + } + + const result = await runTest(test, st) + st.equal(undefined, result.exceptionError) + st.end() + }) + + // Checks `extcodecopy( 0xff,0,0,0,0)` twice, (should be expensive first time), + // and then does `extcodecopy( this,0,0,0,0)`. + t.test('should charge for extcodecopy correctly', async (st) => { + const test = { + code: '60006000600060ff3c60006000600060ff3c600060006000303c00', + totalGasUsed: 2835, + steps: [ + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'EXTCODECOPY', expectedGasUsed: 2600 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'EXTCODECOPY', expectedGasUsed: 100 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'ADDRESS', expectedGasUsed: 2 }, + { expectedOpcode: 'EXTCODECOPY', expectedGasUsed: 100 }, + { expectedOpcode: 'STOP', expectedGasUsed: 0 }, + ], + } + + const result = await runTest(test, st) + st.equal(undefined, result.exceptionError) + st.end() + }) + + // Checks `sload( 0x1)` followed by `sstore(loc: 0x01, val:0x11)`, + // then 'naked' sstore:`sstore(loc: 0x02, val:0x11)` twice, and `sload(0x2)`, `sload(0x1)`. + t.test('should charge for sload and sstore correctly )', async (st) => { + const test = { + code: '6001545060116001556011600255601160025560025460015400', + totalGasUsed: 44529, + steps: [ + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'SLOAD', expectedGasUsed: 2100 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'SSTORE', expectedGasUsed: 20000 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'SSTORE', expectedGasUsed: 22100 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'SSTORE', expectedGasUsed: 100 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'SLOAD', expectedGasUsed: 100 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'SLOAD', expectedGasUsed: 100 }, + { expectedOpcode: 'STOP', expectedGasUsed: 0 }, + ], + } + + const result = await runTest(test, st) + st.equal(undefined, result.exceptionError) + st.end() + }) + + // Calls the `identity`-precompile (cheap), then calls an account (expensive) + // and `staticcall`s the sameaccount (cheap) + t.test('should charge for pre-compiles and staticcalls correctly', async (st) => { + const test = { + code: '60008080808060046000f15060008080808060ff6000f15060008080808060ff6000fa5000', + totalGasUsed: 2869, + steps: [ + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3 }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3 }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3 }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'CALL', expectedGasUsed: 100 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3 }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3 }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3 }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'CALL', expectedGasUsed: 2600 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3 }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3 }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3 }, + { expectedOpcode: 'DUP1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'PUSH1', expectedGasUsed: 3 }, + { expectedOpcode: 'STATICCALL', expectedGasUsed: 100 }, + { expectedOpcode: 'POP', expectedGasUsed: 2 }, + { expectedOpcode: 'STOP', expectedGasUsed: 0 }, + ], + } + + const result = await runTest(test, st) + st.equal(undefined, result.exceptionError) + st.end() + }) +}) From 582d62517afc4d09e04060b92657811332c70762 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Sun, 27 Sep 2020 21:03:57 -0700 Subject: [PATCH 2/9] Adjust EIP-2200 refunds --- packages/vm/lib/evm/opcodes/util.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/vm/lib/evm/opcodes/util.ts b/packages/vm/lib/evm/opcodes/util.ts index 8e0f3462fb..d47ea566fc 100644 --- a/packages/vm/lib/evm/opcodes/util.ts +++ b/packages/vm/lib/evm/opcodes/util.ts @@ -117,15 +117,20 @@ function adjustSstoreGasEIP2929( const keyStr = key.toString('hex') const address = runState.eei.getAddress().toString('hex') + const warmRead = runState._common.param('gasPrices', 'warmstorageread') + const coldSload = runState._common.param('gasPrices', 'coldsload') // @ts-ignore Set Object is possibly 'undefined' if (runState.accessedStorage.has(address) && runState.accessedStorage.get(address).has(keyStr)) { - if (costName === 'reset') { - return defaultCost - runState._common.param('gasPrices', 'coldsload') - } - - if (costName === 'noop') { - return runState._common.param('gasPrices', 'warmstorageread') + switch (costName) { + case 'reset': + return defaultCost - coldSload + case 'noop': + return warmRead + case 'initRefund': + return runState._common.param('gasPrices', 'sstoreInitGasEIP2200') - warmRead + case 'cleanRefund': + return runState._common.param('gasPrices', 'sstoreReset') - coldSload - warmRead } } @@ -417,13 +422,15 @@ function updateSstoreGas(runState: RunState, found: any, value: Buffer, key: Buf if (original.equals(value)) { if (original.length === 0) { // Reset to original non-existent slot + const sstoreInitRefund = runState._common.param('gasPrices', 'sstoreInitRefundEIP2200') runState.eei.refundGas( - new BN(runState._common.param('gasPrices', 'sstoreInitRefundEIP2200')) + new BN(adjustSstoreGasEIP2929(runState, key, sstoreInitRefund, 'initRefund')), ) } else { // Reset to original existing slot + const sstoreCleanRefund = runState._common.param('gasPrices', 'sstoreCleanRefundEIP2200') runState.eei.refundGas( - new BN(runState._common.param('gasPrices', 'sstoreCleanRefundEIP2200')) + new BN(adjustSstoreGasEIP2929(runState, key, sstoreCleanRefund, 'cleanRefund')), ) } } From 52c7f962a4fd2195ec0181688819450743bc4eed Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 28 Sep 2020 09:21:19 -0700 Subject: [PATCH 3/9] Add charges for selfdestruct ETH recipient address reads --- packages/vm/lib/evm/opcodes/functions.ts | 1 + packages/vm/lib/evm/opcodes/util.ts | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/vm/lib/evm/opcodes/functions.ts b/packages/vm/lib/evm/opcodes/functions.ts index dbc5ee25fa..705af98228 100644 --- a/packages/vm/lib/evm/opcodes/functions.ts +++ b/packages/vm/lib/evm/opcodes/functions.ts @@ -1184,6 +1184,7 @@ export const handlers: Map = new Map([ runState.eei.useGas(new BN(runState._common.param('gasPrices', 'callNewAccount'))) } + accessAddressEIP2929(runState, selfdestructToAddress.buf, 0) return runState.eei.selfDestruct(selfdestructToAddress) }, ], diff --git a/packages/vm/lib/evm/opcodes/util.ts b/packages/vm/lib/evm/opcodes/util.ts index d47ea566fc..23121ea42a 100644 --- a/packages/vm/lib/evm/opcodes/util.ts +++ b/packages/vm/lib/evm/opcodes/util.ts @@ -57,13 +57,14 @@ function accessAddressEIP2929(runState: RunState, address: BN | Buffer, baseFee? runState.accessedAddresses.add(addressStr) // CREATE, CREATE2 opcodes have the address warmed for free. - if (baseFee) { + // selfdestruct beneficiary address reads are charged an *additional* cold access + if (baseFee !== undefined) { runState.eei.useGas( new BN(runState._common.param('gasPrices', 'coldaccountaccess') - baseFee), ) } - // Warm - } else if (baseFee) { + // Warm: (selfdestruct beneficiary address reads are not charged when warm) + } else if (baseFee !== undefined && baseFee > 0) { runState.eei.useGas(new BN(runState._common.param('gasPrices', 'warmstorageread') - baseFee)) } } From c93eef7feea835e8e6befbc809ea7fdb84c64b27 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 28 Sep 2020 10:49:36 -0700 Subject: [PATCH 4/9] Fix errors from file reorg rebase --- packages/vm/lib/evm/opcodes/functions.ts | 3 +++ packages/vm/lib/evm/opcodes/util.ts | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/vm/lib/evm/opcodes/functions.ts b/packages/vm/lib/evm/opcodes/functions.ts index 705af98228..94ad20dbf0 100644 --- a/packages/vm/lib/evm/opcodes/functions.ts +++ b/packages/vm/lib/evm/opcodes/functions.ts @@ -8,6 +8,9 @@ import { KECCAK256_NULL, } from 'ethereumjs-util' import { + accessAddressEIP2929, + accessStorageEIP2929, + adjustSstoreGasEIP2929, addressToBuffer, describeLocation, divCeil, diff --git a/packages/vm/lib/evm/opcodes/util.ts b/packages/vm/lib/evm/opcodes/util.ts index 23121ea42a..86fa8c5a6c 100644 --- a/packages/vm/lib/evm/opcodes/util.ts +++ b/packages/vm/lib/evm/opcodes/util.ts @@ -35,7 +35,7 @@ export function trap(err: string) { * @param {BN} address * @return {Buffer} */ -function addressToBuffer(address: BN | Buffer) { +export function addressToBuffer(address: BN | Buffer) { if (Buffer.isBuffer(address)) return address return address.and(MASK_160).toArrayLike(Buffer, 'be', 20) } @@ -47,7 +47,7 @@ function addressToBuffer(address: BN | Buffer) { * @param {RunState} runState * @param {BN} address */ -function accessAddressEIP2929(runState: RunState, address: BN | Buffer, baseFee?: number) { +export function accessAddressEIP2929(runState: RunState, address: BN | Buffer, baseFee?: number) { if (!runState._common.eips().includes(2929)) return const addressStr = addressToBuffer(address).toString('hex') @@ -76,7 +76,7 @@ function accessAddressEIP2929(runState: RunState, address: BN | Buffer, baseFee? * @param {RunState} runState * @param {Buffer} key (to storage slot) */ -function accessStorageEIP2929(runState: RunState, key: Buffer, isSstore: boolean) { +export function accessStorageEIP2929(runState: RunState, key: Buffer, isSstore: boolean) { if (!runState._common.eips().includes(2929)) return const keyStr = key.toString('hex') @@ -108,7 +108,7 @@ function accessStorageEIP2929(runState: RunState, key: Buffer, isSstore: boolean * @param {string} costName parameter name ('reset' or 'noop') * @return {number} adjusted cost */ -function adjustSstoreGasEIP2929( +export function adjustSstoreGasEIP2929( runState: RunState, key: Buffer, defaultCost: number, @@ -324,7 +324,7 @@ export function subMemUsage(runState: RunState, offset: BN, length: BN) { * @param {any} found * @param {Buffer} value */ -function updateSstoreGas(runState: RunState, found: any, value: Buffer, key: Buffer) { +export function updateSstoreGas(runState: RunState, found: any, value: Buffer, key: Buffer) { if (runState._common.hardfork() === 'constantinople') { const original = found.original const current = found.current From 2e06aaa65a6bc46afec578a10198fe170c879ee4 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 28 Sep 2020 11:34:03 -0700 Subject: [PATCH 5/9] Move dynamic gas pricing logic into own EIP files --- packages/vm/lib/evm/opcodes/EIP1283.ts | 59 ++++++ packages/vm/lib/evm/opcodes/EIP2200.ts | 93 +++++++++ packages/vm/lib/evm/opcodes/EIP2929.ts | 101 ++++++++++ packages/vm/lib/evm/opcodes/functions.ts | 10 +- packages/vm/lib/evm/opcodes/util.ts | 235 +---------------------- 5 files changed, 259 insertions(+), 239 deletions(-) create mode 100644 packages/vm/lib/evm/opcodes/EIP1283.ts create mode 100644 packages/vm/lib/evm/opcodes/EIP2200.ts create mode 100644 packages/vm/lib/evm/opcodes/EIP2929.ts diff --git a/packages/vm/lib/evm/opcodes/EIP1283.ts b/packages/vm/lib/evm/opcodes/EIP1283.ts new file mode 100644 index 0000000000..54812c4c4b --- /dev/null +++ b/packages/vm/lib/evm/opcodes/EIP1283.ts @@ -0,0 +1,59 @@ +import BN = require('bn.js') +import { RunState } from './../interpreter' + +/** + * Adjusts gas usage and refunds of SStore ops per EIP-1283 (Constantinople) + * + * @param {RunState} runState + * @param {any} found + * @param {Buffer} value + */ +export function updateSstoreGasEIP1283(runState: RunState, found: any, value: Buffer, key: Buffer) { + if (runState._common.hardfork() === 'constantinople') { + const original = found.original + const current = found.current + if (current.equals(value)) { + // If current value equals new value (this is a no-op), 200 gas is deducted. + runState.eei.useGas(new BN(runState._common.param('gasPrices', 'netSstoreNoopGas'))) + return + } + // If current value does not equal new value + if (original.equals(current)) { + // If original value equals current value (this storage slot has not been changed by the current execution context) + if (original.length === 0) { + // If original value is 0, 20000 gas is deducted. + return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'netSstoreInitGas'))) + } + if (value.length === 0) { + // If new value is 0, add 15000 gas to refund counter. + runState.eei.refundGas(new BN(runState._common.param('gasPrices', 'netSstoreClearRefund'))) + } + // Otherwise, 5000 gas is deducted. + return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'netSstoreCleanGas'))) + } + // If original value does not equal current value (this storage slot is dirty), 200 gas is deducted. Apply both of the following clauses. + if (original.length !== 0) { + // 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.eei.subRefund(new BN(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.eei.refundGas(new BN(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.eei.refundGas( + new BN(runState._common.param('gasPrices', 'netSstoreResetClearRefund')), + ) + } else { + // Otherwise, add 4800 gas to refund counter. + runState.eei.refundGas(new BN(runState._common.param('gasPrices', 'netSstoreResetRefund'))) + } + } + return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'netSstoreDirtyGas'))) + } +} diff --git a/packages/vm/lib/evm/opcodes/EIP2200.ts b/packages/vm/lib/evm/opcodes/EIP2200.ts new file mode 100644 index 0000000000..6b2873b750 --- /dev/null +++ b/packages/vm/lib/evm/opcodes/EIP2200.ts @@ -0,0 +1,93 @@ +import BN = require('bn.js') +import { RunState } from './../interpreter' +import { ERROR } from '../../exceptions' +import { adjustSstoreGasEIP2929 } from './EIP2929' +import { trap } from './util' + +/** + * Adjusts gas usage and refunds of SStore ops per EIP-2200 (Istanbul) + * + * @param {RunState} runState + * @param {any} found + * @param {Buffer} value + */ +export function updateSstoreGasEIP2200(runState: RunState, found: any, value: Buffer, key: Buffer) { + if (runState._common.gteHardfork('istanbul')) { + const original = found.original + const current = found.current + // Fail if not enough gas is left + if ( + runState.eei.getGasLeft().lten(runState._common.param('gasPrices', 'sstoreSentryGasEIP2200')) + ) { + trap(ERROR.OUT_OF_GAS) + } + + // Noop + if (current.equals(value)) { + const sstoreNoopCost = runState._common.param('gasPrices', 'sstoreNoopGasEIP2200') + return runState.eei.useGas( + new BN(adjustSstoreGasEIP2929(runState, key, sstoreNoopCost, 'noop')), + ) + } + if (original.equals(current)) { + // Create slot + if (original.length === 0) { + return runState.eei.useGas( + new BN(runState._common.param('gasPrices', 'sstoreInitGasEIP2200')), + ) + } + // Delete slot + if (value.length === 0) { + runState.eei.refundGas( + new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')), + ) + } + // Write existing slot + return runState.eei.useGas( + new BN(runState._common.param('gasPrices', 'sstoreCleanGasEIP2200')), + ) + } + if (original.length > 0) { + if (current.length === 0) { + // Recreate slot + runState.eei.subRefund( + new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')), + ) + } else if (value.length === 0) { + // Delete slot + runState.eei.refundGas( + new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')), + ) + } + } + if (original.equals(value)) { + if (original.length === 0) { + // Reset to original non-existent slot + const sstoreInitRefund = runState._common.param('gasPrices', 'sstoreInitRefundEIP2200') + runState.eei.refundGas( + new BN(adjustSstoreGasEIP2929(runState, key, sstoreInitRefund, 'initRefund')), + ) + } else { + // Reset to original existing slot + const sstoreCleanRefund = runState._common.param('gasPrices', 'sstoreCleanRefundEIP2200') + runState.eei.refundGas( + new BN(adjustSstoreGasEIP2929(runState, key, sstoreCleanRefund, 'cleanRefund')), + ) + } + } + // Dirty update + return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreDirtyGasEIP2200'))) + } else { + const sstoreResetCost = runState._common.param('gasPrices', 'sstoreReset') + if (value.length === 0 && !found.length) { + runState.eei.useGas(new BN(adjustSstoreGasEIP2929(runState, key, sstoreResetCost, 'reset'))) + } else if (value.length === 0 && found.length) { + runState.eei.useGas(new BN(adjustSstoreGasEIP2929(runState, key, sstoreResetCost, 'reset'))) + runState.eei.refundGas(new BN(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) { + runState.eei.useGas(new BN(adjustSstoreGasEIP2929(runState, key, sstoreResetCost, 'reset'))) + } + } +} diff --git a/packages/vm/lib/evm/opcodes/EIP2929.ts b/packages/vm/lib/evm/opcodes/EIP2929.ts new file mode 100644 index 0000000000..5640683b7e --- /dev/null +++ b/packages/vm/lib/evm/opcodes/EIP2929.ts @@ -0,0 +1,101 @@ +import BN = require('bn.js') +import { RunState } from './../interpreter' +import { addressToBuffer } from './util' + +/** + * Adds address to accessedAddresses set if not already included. + * Adjusts cost incurred for executing opcode based on whether address read + * is warm/cold. (EIP 2929) + * @param {RunState} runState + * @param {BN} address + */ +export function accessAddressEIP2929(runState: RunState, address: BN | Buffer, baseFee?: number) { + if (!runState._common.eips().includes(2929)) return + + const addressStr = addressToBuffer(address).toString('hex') + + // Cold + if (!runState.accessedAddresses.has(addressStr)) { + runState.accessedAddresses.add(addressStr) + + // CREATE, CREATE2 opcodes have the address warmed for free. + // selfdestruct beneficiary address reads are charged an *additional* cold access + if (baseFee !== undefined) { + runState.eei.useGas( + new BN(runState._common.param('gasPrices', 'coldaccountaccess') - baseFee), + ) + } + // Warm: (selfdestruct beneficiary address reads are not charged when warm) + } else if (baseFee !== undefined && baseFee > 0) { + runState.eei.useGas(new BN(runState._common.param('gasPrices', 'warmstorageread') - baseFee)) + } +} + +/** + * Adds (address, key) to accessedStorage tuple set if not already included. + * Adjusts cost incurred for executing opcode based on whether storage read + * is warm/cold. (EIP 2929) + * @param {RunState} runState + * @param {Buffer} key (to storage slot) + */ +export function accessStorageEIP2929(runState: RunState, key: Buffer, isSstore: boolean) { + if (!runState._common.eips().includes(2929)) return + + const keyStr = key.toString('hex') + const baseFee = !isSstore ? runState._common.param('gasPrices', 'sload') : 0 + const address = runState.eei.getAddress().toString('hex') + const keysAtAddress = runState.accessedStorage.get(address) + + // Cold (SLOAD and SSTORE) + if (!keysAtAddress) { + runState.accessedStorage.set(address, new Set()) + // @ts-ignore Set Object is possibly 'undefined' + runState.accessedStorage.get(address).add(keyStr) + runState.eei.useGas(new BN(runState._common.param('gasPrices', 'coldsload') - baseFee)) + } else if (keysAtAddress && !keysAtAddress.has(keyStr)) { + keysAtAddress.add(keyStr) + runState.eei.useGas(new BN(runState._common.param('gasPrices', 'coldsload') - baseFee)) + // Warm (SLOAD only) + } else if (!isSstore) { + runState.eei.useGas(new BN(runState._common.param('gasPrices', 'warmstorageread') - baseFee)) + } +} + +/** + * Adjusts cost of SSTORE_RESET_GAS or SLOAD (aka sstorenoop) (EIP-2200) downward when storage + * location is already warm + * @param {RunState} runState + * @param {Buffer} key storage slot + * @param {number} defaultCost SSTORE_RESET_GAS / SLOAD + * @param {string} costName parameter name ('reset' or 'noop') + * @return {number} adjusted cost + */ +export function adjustSstoreGasEIP2929( + runState: RunState, + key: Buffer, + defaultCost: number, + costName: string, +): number { + if (!runState._common.eips().includes(2929)) return defaultCost + + const keyStr = key.toString('hex') + const address = runState.eei.getAddress().toString('hex') + const warmRead = runState._common.param('gasPrices', 'warmstorageread') + const coldSload = runState._common.param('gasPrices', 'coldsload') + + // @ts-ignore Set Object is possibly 'undefined' + if (runState.accessedStorage.has(address) && runState.accessedStorage.get(address).has(keyStr)) { + switch (costName) { + case 'reset': + return defaultCost - coldSload + case 'noop': + return warmRead + case 'initRefund': + return runState._common.param('gasPrices', 'sstoreInitGasEIP2200') - warmRead + case 'cleanRefund': + return runState._common.param('gasPrices', 'sstoreReset') - coldSload - warmRead + } + } + + return defaultCost +} diff --git a/packages/vm/lib/evm/opcodes/functions.ts b/packages/vm/lib/evm/opcodes/functions.ts index 94ad20dbf0..3168aab9cb 100644 --- a/packages/vm/lib/evm/opcodes/functions.ts +++ b/packages/vm/lib/evm/opcodes/functions.ts @@ -8,9 +8,6 @@ import { KECCAK256_NULL, } from 'ethereumjs-util' import { - accessAddressEIP2929, - accessStorageEIP2929, - adjustSstoreGasEIP2929, addressToBuffer, describeLocation, divCeil, @@ -21,10 +18,12 @@ import { maxCallGas, setLengthLeftStorage, subMemUsage, - updateSstoreGas, trap, writeCallOutput, } from './util' +import { updateSstoreGasEIP1283 } from './EIP1283' +import { updateSstoreGasEIP2200 } from './EIP2200' +import { accessAddressEIP2929, accessStorageEIP2929 } from './EIP2929' import { ERROR } from '../../exceptions' import { RunState } from './../interpreter' @@ -745,7 +744,8 @@ export const handlers: Map = new Map([ // TODO: Replace getContractStorage with EEI method const found = await getContractStorage(runState, runState.eei.getAddress(), keyBuf) accessStorageEIP2929(runState, keyBuf, true) - updateSstoreGas(runState, found, setLengthLeftStorage(value), keyBuf) + updateSstoreGasEIP1283(runState, found, setLengthLeftStorage(value), keyBuf) + updateSstoreGasEIP2200(runState, found, setLengthLeftStorage(value), keyBuf) await runState.eei.storageStore(keyBuf, value) }, ], diff --git a/packages/vm/lib/evm/opcodes/util.ts b/packages/vm/lib/evm/opcodes/util.ts index 86fa8c5a6c..7715c6fdca 100644 --- a/packages/vm/lib/evm/opcodes/util.ts +++ b/packages/vm/lib/evm/opcodes/util.ts @@ -1,6 +1,7 @@ import { Address, BN, keccak256, setLengthRight, setLengthLeft } from 'ethereumjs-util' import { ERROR, VmError } from './../../exceptions' import { RunState } from './../interpreter' +import { adjustSstoreGasEIP2929 } from './EIP2929' const MASK_160 = new BN(1).shln(160).subn(1) @@ -40,104 +41,6 @@ export function addressToBuffer(address: BN | Buffer) { return address.and(MASK_160).toArrayLike(Buffer, 'be', 20) } -/** - * Adds address to accessedAddresses set if not already included. - * Adjusts cost incurred for executing opcode based on whether address read - * is warm/cold. (EIP 2929) - * @param {RunState} runState - * @param {BN} address - */ -export function accessAddressEIP2929(runState: RunState, address: BN | Buffer, baseFee?: number) { - if (!runState._common.eips().includes(2929)) return - - const addressStr = addressToBuffer(address).toString('hex') - - // Cold - if (!runState.accessedAddresses.has(addressStr)) { - runState.accessedAddresses.add(addressStr) - - // CREATE, CREATE2 opcodes have the address warmed for free. - // selfdestruct beneficiary address reads are charged an *additional* cold access - if (baseFee !== undefined) { - runState.eei.useGas( - new BN(runState._common.param('gasPrices', 'coldaccountaccess') - baseFee), - ) - } - // Warm: (selfdestruct beneficiary address reads are not charged when warm) - } else if (baseFee !== undefined && baseFee > 0) { - runState.eei.useGas(new BN(runState._common.param('gasPrices', 'warmstorageread') - baseFee)) - } -} - -/** - * Adds (address, key) to accessedStorage tuple set if not already included. - * Adjusts cost incurred for executing opcode based on whether storage read - * is warm/cold. (EIP 2929) - * @param {RunState} runState - * @param {Buffer} key (to storage slot) - */ -export function accessStorageEIP2929(runState: RunState, key: Buffer, isSstore: boolean) { - if (!runState._common.eips().includes(2929)) return - - const keyStr = key.toString('hex') - const baseFee = !isSstore ? runState._common.param('gasPrices', 'sload') : 0 - const address = runState.eei.getAddress().toString('hex') - const keysAtAddress = runState.accessedStorage.get(address) - - // Cold (SLOAD and SSTORE) - if (!keysAtAddress) { - runState.accessedStorage.set(address, new Set()) - // @ts-ignore Set Object is possibly 'undefined' - runState.accessedStorage.get(address).add(keyStr) - runState.eei.useGas(new BN(runState._common.param('gasPrices', 'coldsload') - baseFee)) - } else if (keysAtAddress && !keysAtAddress.has(keyStr)) { - keysAtAddress.add(keyStr) - runState.eei.useGas(new BN(runState._common.param('gasPrices', 'coldsload') - baseFee)) - // Warm (SLOAD only) - } else if (!isSstore) { - runState.eei.useGas(new BN(runState._common.param('gasPrices', 'warmstorageread') - baseFee)) - } -} - -/** - * Adjusts cost of SSTORE_RESET_GAS or SLOAD (aka sstorenoop) (EIP-2200) downward when storage - * location is already warm - * @param {RunState} runState - * @param {Buffer} key storage slot - * @param {number} defaultCost SSTORE_RESET_GAS / SLOAD - * @param {string} costName parameter name ('reset' or 'noop') - * @return {number} adjusted cost - */ -export function adjustSstoreGasEIP2929( - runState: RunState, - key: Buffer, - defaultCost: number, - costName: string, -): number { - if (!runState._common.eips().includes(2929)) return defaultCost - - const keyStr = key.toString('hex') - const address = runState.eei.getAddress().toString('hex') - const warmRead = runState._common.param('gasPrices', 'warmstorageread') - const coldSload = runState._common.param('gasPrices', 'coldsload') - - // @ts-ignore Set Object is possibly 'undefined' - if (runState.accessedStorage.has(address) && runState.accessedStorage.get(address).has(keyStr)) { - switch (costName) { - case 'reset': - return defaultCost - coldSload - case 'noop': - return warmRead - case 'initRefund': - return runState._common.param('gasPrices', 'sstoreInitGasEIP2200') - warmRead - case 'cleanRefund': - return runState._common.param('gasPrices', 'sstoreReset') - coldSload - warmRead - } - } - - return defaultCost -} - /** * Error message helper - generates location string * @@ -317,142 +220,6 @@ export function subMemUsage(runState: RunState, offset: BN, length: BN) { runState.memoryWordCount = newMemoryWordCount } -/** - * Adjusts gas usage and refunds of SStore ops per EIP-2200 - * - * @param {RunState} runState - * @param {any} found - * @param {Buffer} value - */ -export function updateSstoreGas(runState: RunState, found: any, value: Buffer, key: Buffer) { - if (runState._common.hardfork() === 'constantinople') { - const original = found.original - const current = found.current - if (current.equals(value)) { - // If current value equals new value (this is a no-op), 200 gas is deducted. - runState.eei.useGas(new BN(runState._common.param('gasPrices', 'netSstoreNoopGas'))) - return - } - // If current value does not equal new value - if (original.equals(current)) { - // If original value equals current value (this storage slot has not been changed by the current execution context) - if (original.length === 0) { - // If original value is 0, 20000 gas is deducted. - return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'netSstoreInitGas'))) - } - if (value.length === 0) { - // If new value is 0, add 15000 gas to refund counter. - runState.eei.refundGas(new BN(runState._common.param('gasPrices', 'netSstoreClearRefund'))) - } - // Otherwise, 5000 gas is deducted. - return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'netSstoreCleanGas'))) - } - // If original value does not equal current value (this storage slot is dirty), 200 gas is deducted. Apply both of the following clauses. - if (original.length !== 0) { - // 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.eei.subRefund(new BN(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.eei.refundGas(new BN(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.eei.refundGas( - new BN(runState._common.param('gasPrices', 'netSstoreResetClearRefund')) - ) - } else { - // Otherwise, add 4800 gas to refund counter. - runState.eei.refundGas(new BN(runState._common.param('gasPrices', 'netSstoreResetRefund'))) - } - } - return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'netSstoreDirtyGas'))) - } else if (runState._common.gteHardfork('istanbul')) { - // EIP-2200 - const original = found.original - const current = found.current - // Fail if not enough gas is left - if ( - runState.eei.getGasLeft().lten(runState._common.param('gasPrices', 'sstoreSentryGasEIP2200')) - ) { - trap(ERROR.OUT_OF_GAS) - } - - // Noop - if (current.equals(value)) { - const sstoreNoopCost = runState._common.param('gasPrices', 'sstoreNoopGasEIP2200') - return runState.eei.useGas( - new BN(adjustSstoreGasEIP2929(runState, key, sstoreNoopCost, 'noop')), - ) - } - if (original.equals(current)) { - // Create slot - if (original.length === 0) { - return runState.eei.useGas( - new BN(runState._common.param('gasPrices', 'sstoreInitGasEIP2200')) - ) - } - // Delete slot - if (value.length === 0) { - runState.eei.refundGas( - new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')) - ) - } - // Write existing slot - return runState.eei.useGas( - new BN(runState._common.param('gasPrices', 'sstoreCleanGasEIP2200')) - ) - } - if (original.length > 0) { - if (current.length === 0) { - // Recreate slot - runState.eei.subRefund( - new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')) - ) - } else if (value.length === 0) { - // Delete slot - runState.eei.refundGas( - new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')) - ) - } - } - if (original.equals(value)) { - if (original.length === 0) { - // Reset to original non-existent slot - const sstoreInitRefund = runState._common.param('gasPrices', 'sstoreInitRefundEIP2200') - runState.eei.refundGas( - new BN(adjustSstoreGasEIP2929(runState, key, sstoreInitRefund, 'initRefund')), - ) - } else { - // Reset to original existing slot - const sstoreCleanRefund = runState._common.param('gasPrices', 'sstoreCleanRefundEIP2200') - runState.eei.refundGas( - new BN(adjustSstoreGasEIP2929(runState, key, sstoreCleanRefund, 'cleanRefund')), - ) - } - } - // Dirty update - return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreDirtyGasEIP2200'))) - } else { - const sstoreResetCost = runState._common.param('gasPrices', 'sstoreReset') - if (value.length === 0 && !found.length) { - runState.eei.useGas(new BN(adjustSstoreGasEIP2929(runState, key, sstoreResetCost, 'reset'))) - } else if (value.length === 0 && found.length) { - runState.eei.useGas(new BN(adjustSstoreGasEIP2929(runState, key, sstoreResetCost, 'reset'))) - runState.eei.refundGas(new BN(runState._common.param('gasPrices', 'sstoreRefund'))) - } else if (value.length !== 0 && !found.length) { - runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreSet'))) - /* eslint-disable-next-line sonarjs/no-duplicated-branches */ - } else if (value.length !== 0 && found.length) { - runState.eei.useGas(new BN(adjustSstoreGasEIP2929(runState, key, sstoreResetCost, 'reset'))) - } - } -} - /** * Writes data returned by eei.call* methods to memory * From 0ef96864f065c627ceebe2494eca81c44db82b9d Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 20 Oct 2020 09:03:20 -0700 Subject: [PATCH 6/9] Fix tape import (for config 2.0 rebase) --- packages/vm/tests/api/EIPs/eip-2929.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vm/tests/api/EIPs/eip-2929.spec.ts b/packages/vm/tests/api/EIPs/eip-2929.spec.ts index 2994fc6fb8..99601bf43c 100644 --- a/packages/vm/tests/api/EIPs/eip-2929.spec.ts +++ b/packages/vm/tests/api/EIPs/eip-2929.spec.ts @@ -1,4 +1,4 @@ -import * as tape from 'tape' +import tape from 'tape' import { BN } from 'ethereumjs-util' import VM from '../../../lib' import Common from '@ethereumjs/common' From f82db4a7622df7b048a9194c88f87813d4a31fe8 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 20 Oct 2020 09:10:26 -0700 Subject: [PATCH 7/9] Update supported EIPs comment in lib/index --- packages/vm/lib/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vm/lib/index.ts b/packages/vm/lib/index.ts index 31895743bb..68105335c8 100644 --- a/packages/vm/lib/index.ts +++ b/packages/vm/lib/index.ts @@ -40,6 +40,7 @@ export interface VMOpts { * ### Supported EIPs * * - [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537) (`experimental`) - BLS12-381 precompiles + * - [EIP-2929](https://eips.ethereum.org/EIPS/eip-2929) (`experimental`) - Gas cost increases for state access opcodes * * *Annotations:* * From 20e845dd5bb18e89680508b2f222d3db74e02706 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 20 Oct 2020 10:13:36 -0700 Subject: [PATCH 8/9] Lint fixes (config 2.0) --- packages/vm/lib/evm/opcodes/EIP1283.ts | 2 +- packages/vm/lib/evm/opcodes/EIP2200.ts | 16 ++++++++-------- packages/vm/lib/evm/opcodes/EIP2929.ts | 4 ++-- packages/vm/lib/evm/opcodes/functions.ts | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/vm/lib/evm/opcodes/EIP1283.ts b/packages/vm/lib/evm/opcodes/EIP1283.ts index 54812c4c4b..085972208e 100644 --- a/packages/vm/lib/evm/opcodes/EIP1283.ts +++ b/packages/vm/lib/evm/opcodes/EIP1283.ts @@ -47,7 +47,7 @@ export function updateSstoreGasEIP1283(runState: RunState, found: any, value: Bu if (original.length === 0) { // If original value is 0, add 19800 gas to refund counter. runState.eei.refundGas( - new BN(runState._common.param('gasPrices', 'netSstoreResetClearRefund')), + new BN(runState._common.param('gasPrices', 'netSstoreResetClearRefund')) ) } else { // Otherwise, add 4800 gas to refund counter. diff --git a/packages/vm/lib/evm/opcodes/EIP2200.ts b/packages/vm/lib/evm/opcodes/EIP2200.ts index 6b2873b750..385516273a 100644 --- a/packages/vm/lib/evm/opcodes/EIP2200.ts +++ b/packages/vm/lib/evm/opcodes/EIP2200.ts @@ -26,37 +26,37 @@ export function updateSstoreGasEIP2200(runState: RunState, found: any, value: Bu if (current.equals(value)) { const sstoreNoopCost = runState._common.param('gasPrices', 'sstoreNoopGasEIP2200') return runState.eei.useGas( - new BN(adjustSstoreGasEIP2929(runState, key, sstoreNoopCost, 'noop')), + new BN(adjustSstoreGasEIP2929(runState, key, sstoreNoopCost, 'noop')) ) } if (original.equals(current)) { // Create slot if (original.length === 0) { return runState.eei.useGas( - new BN(runState._common.param('gasPrices', 'sstoreInitGasEIP2200')), + new BN(runState._common.param('gasPrices', 'sstoreInitGasEIP2200')) ) } // Delete slot if (value.length === 0) { runState.eei.refundGas( - new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')), + new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')) ) } // Write existing slot return runState.eei.useGas( - new BN(runState._common.param('gasPrices', 'sstoreCleanGasEIP2200')), + new BN(runState._common.param('gasPrices', 'sstoreCleanGasEIP2200')) ) } if (original.length > 0) { if (current.length === 0) { // Recreate slot runState.eei.subRefund( - new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')), + new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')) ) } else if (value.length === 0) { // Delete slot runState.eei.refundGas( - new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')), + new BN(runState._common.param('gasPrices', 'sstoreClearRefundEIP2200')) ) } } @@ -65,13 +65,13 @@ export function updateSstoreGasEIP2200(runState: RunState, found: any, value: Bu // Reset to original non-existent slot const sstoreInitRefund = runState._common.param('gasPrices', 'sstoreInitRefundEIP2200') runState.eei.refundGas( - new BN(adjustSstoreGasEIP2929(runState, key, sstoreInitRefund, 'initRefund')), + new BN(adjustSstoreGasEIP2929(runState, key, sstoreInitRefund, 'initRefund')) ) } else { // Reset to original existing slot const sstoreCleanRefund = runState._common.param('gasPrices', 'sstoreCleanRefundEIP2200') runState.eei.refundGas( - new BN(adjustSstoreGasEIP2929(runState, key, sstoreCleanRefund, 'cleanRefund')), + new BN(adjustSstoreGasEIP2929(runState, key, sstoreCleanRefund, 'cleanRefund')) ) } } diff --git a/packages/vm/lib/evm/opcodes/EIP2929.ts b/packages/vm/lib/evm/opcodes/EIP2929.ts index 5640683b7e..5a30526298 100644 --- a/packages/vm/lib/evm/opcodes/EIP2929.ts +++ b/packages/vm/lib/evm/opcodes/EIP2929.ts @@ -22,7 +22,7 @@ export function accessAddressEIP2929(runState: RunState, address: BN | Buffer, b // selfdestruct beneficiary address reads are charged an *additional* cold access if (baseFee !== undefined) { runState.eei.useGas( - new BN(runState._common.param('gasPrices', 'coldaccountaccess') - baseFee), + new BN(runState._common.param('gasPrices', 'coldaccountaccess') - baseFee) ) } // Warm: (selfdestruct beneficiary address reads are not charged when warm) @@ -74,7 +74,7 @@ export function adjustSstoreGasEIP2929( runState: RunState, key: Buffer, defaultCost: number, - costName: string, + costName: string ): number { if (!runState._common.eips().includes(2929)) return defaultCost diff --git a/packages/vm/lib/evm/opcodes/functions.ts b/packages/vm/lib/evm/opcodes/functions.ts index 3168aab9cb..f728be9f56 100644 --- a/packages/vm/lib/evm/opcodes/functions.ts +++ b/packages/vm/lib/evm/opcodes/functions.ts @@ -1078,7 +1078,7 @@ export const handlers: Map = new Map([ accessAddressEIP2929( runState, toAddressBuf, - runState._common.param('gasPrices', 'delegatecall'), + runState._common.param('gasPrices', 'delegatecall') ) gasLimit = maxCallGas(gasLimit, runState.eei.getGasLeft(), runState) @@ -1111,7 +1111,7 @@ export const handlers: Map = new Map([ accessAddressEIP2929( runState, toAddressBuf, - runState._common.param('gasPrices', 'staticcall'), + runState._common.param('gasPrices', 'staticcall') ) gasLimit = maxCallGas(gasLimit, runState.eei.getGasLeft(), runState) // we set TangerineWhistle or later to true here, as STATICCALL was available from Byzantium (which is after TangerineWhistle) From c186e33daf587e9504f90acc82c40a48832be8c0 Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Wed, 21 Oct 2020 11:55:42 +0200 Subject: [PATCH 9/9] vm -> EIP2929: updated accessAddressEIP2929 to use Address type, test fixes --- packages/vm/lib/evm/interpreter.ts | 6 ++-- packages/vm/lib/evm/opcodes/EIP2929.ts | 9 +++--- packages/vm/lib/evm/opcodes/functions.ts | 32 +++++++++------------ packages/vm/tests/api/EIPs/eip-2929.spec.ts | 9 +++--- 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/packages/vm/lib/evm/interpreter.ts b/packages/vm/lib/evm/interpreter.ts index b731ab2707..2ad7f8944d 100644 --- a/packages/vm/lib/evm/interpreter.ts +++ b/packages/vm/lib/evm/interpreter.ts @@ -243,11 +243,11 @@ export default class Interpreter { // all the precompiles. (EIP 2929) _initAccessedAddresses() { this._runState.accessedAddresses.clear() - this._runState.accessedAddresses.add(this._eei._env.origin.toString('hex')) - this._runState.accessedAddresses.add(this._eei.getAddress().toString('hex')) + this._runState.accessedAddresses.add(this._eei._env.origin.toString()) + this._runState.accessedAddresses.add(this._eei.getAddress().toString()) for (let address of Object.keys(precompiles)) { - this._runState.accessedAddresses.add(address) + this._runState.accessedAddresses.add(`0x${address}`) } } } diff --git a/packages/vm/lib/evm/opcodes/EIP2929.ts b/packages/vm/lib/evm/opcodes/EIP2929.ts index 5a30526298..25d9f0608c 100644 --- a/packages/vm/lib/evm/opcodes/EIP2929.ts +++ b/packages/vm/lib/evm/opcodes/EIP2929.ts @@ -1,4 +1,5 @@ import BN = require('bn.js') +import { Address } from 'ethereumjs-util' import { RunState } from './../interpreter' import { addressToBuffer } from './util' @@ -9,10 +10,10 @@ import { addressToBuffer } from './util' * @param {RunState} runState * @param {BN} address */ -export function accessAddressEIP2929(runState: RunState, address: BN | Buffer, baseFee?: number) { +export function accessAddressEIP2929(runState: RunState, address: Address, baseFee?: number) { if (!runState._common.eips().includes(2929)) return - const addressStr = addressToBuffer(address).toString('hex') + const addressStr = address.toString() // Cold if (!runState.accessedAddresses.has(addressStr)) { @@ -43,7 +44,7 @@ export function accessStorageEIP2929(runState: RunState, key: Buffer, isSstore: const keyStr = key.toString('hex') const baseFee = !isSstore ? runState._common.param('gasPrices', 'sload') : 0 - const address = runState.eei.getAddress().toString('hex') + const address = runState.eei.getAddress().toString() const keysAtAddress = runState.accessedStorage.get(address) // Cold (SLOAD and SSTORE) @@ -79,7 +80,7 @@ export function adjustSstoreGasEIP2929( if (!runState._common.eips().includes(2929)) return defaultCost const keyStr = key.toString('hex') - const address = runState.eei.getAddress().toString('hex') + const address = runState.eei.getAddress().toString() const warmRead = runState._common.param('gasPrices', 'warmstorageread') const coldSload = runState._common.param('gasPrices', 'coldsload') diff --git a/packages/vm/lib/evm/opcodes/functions.ts b/packages/vm/lib/evm/opcodes/functions.ts index f728be9f56..f0cfe6bd1f 100644 --- a/packages/vm/lib/evm/opcodes/functions.ts +++ b/packages/vm/lib/evm/opcodes/functions.ts @@ -410,7 +410,7 @@ export const handlers: Map = new Map([ async function (runState: RunState) { const addressBN = runState.stack.pop() const address = new Address(addressToBuffer(addressBN)) - accessAddressEIP2929(runState, address.buf, runState._common.param('gasPrices', 'balance')) + accessAddressEIP2929(runState, address, runState._common.param('gasPrices', 'balance')) const balance = await runState.eei.getExternalBalance(address) runState.stack.push(balance) }, @@ -509,9 +509,10 @@ export const handlers: Map = new Map([ [ 0x3b, async function (runState: RunState) { - const address = runState.stack.pop() + const addressBN = runState.stack.pop() + const address = new Address(addressToBuffer(addressBN)) accessAddressEIP2929(runState, address, runState._common.param('gasPrices', 'extcodesize')) - const size = await runState.eei.getExternalCodeSize(address) + const size = await runState.eei.getExternalCodeSize(addressBN) runState.stack.push(size) }, ], @@ -519,17 +520,18 @@ export const handlers: Map = new Map([ [ 0x3c, async function (runState: RunState) { - const [address, memOffset, codeOffset, length] = runState.stack.popN(4) + const [addressBN, memOffset, codeOffset, length] = runState.stack.popN(4) // FIXME: for some reason this must come before subGas subMemUsage(runState, memOffset, length) + const address = new Address(addressToBuffer(addressBN)) accessAddressEIP2929(runState, address, runState._common.param('gasPrices', 'extcodecopy')) // copy fee runState.eei.useGas( new BN(runState._common.param('gasPrices', 'copy')).imul(divCeil(length, new BN(32))) ) - const code = await runState.eei.getExternalCode(address) + const code = await runState.eei.getExternalCode(addressBN) const data = getDataSlice(code, codeOffset, length) const memOffsetNum = memOffset.toNumber() @@ -544,7 +546,7 @@ export const handlers: Map = new Map([ async function (runState: RunState) { const addressBN = runState.stack.pop() const address = new Address(addressToBuffer(addressBN)) - accessAddressEIP2929(runState, address.buf, runState._common.param('gasPrices', 'extcodehash')) + accessAddressEIP2929(runState, address, runState._common.param('gasPrices', 'extcodehash')) const empty = await runState.eei.isAccountEmpty(address) if (empty) { runState.stack.push(new BN(0)) @@ -986,7 +988,7 @@ export const handlers: Map = new Map([ } subMemUsage(runState, inOffset, inLength) subMemUsage(runState, outOffset, outLength) - accessAddressEIP2929(runState, toAddressBuf, runState._common.param('gasPrices', 'call')) + accessAddressEIP2929(runState, toAddress, runState._common.param('gasPrices', 'call')) if (!value.isZero()) { runState.eei.useGas(new BN(runState._common.param('gasPrices', 'callValueTransfer'))) @@ -1038,7 +1040,7 @@ export const handlers: Map = new Map([ subMemUsage(runState, inOffset, inLength) subMemUsage(runState, outOffset, outLength) - accessAddressEIP2929(runState, toAddressBuf, runState._common.param('gasPrices', 'callcode')) + accessAddressEIP2929(runState, toAddress, runState._common.param('gasPrices', 'callcode')) if (!value.isZero()) { runState.eei.useGas(new BN(runState._common.param('gasPrices', 'callValueTransfer'))) @@ -1075,11 +1077,7 @@ export const handlers: Map = new Map([ subMemUsage(runState, inOffset, inLength) subMemUsage(runState, outOffset, outLength) - accessAddressEIP2929( - runState, - toAddressBuf, - runState._common.param('gasPrices', 'delegatecall') - ) + accessAddressEIP2929(runState, toAddress, runState._common.param('gasPrices', 'delegatecall')) gasLimit = maxCallGas(gasLimit, runState.eei.getGasLeft(), runState) // note that TangerineWhistle or later this cannot happen (it could have ran out of gas prior to getting here though) @@ -1108,11 +1106,7 @@ export const handlers: Map = new Map([ subMemUsage(runState, inOffset, inLength) subMemUsage(runState, outOffset, outLength) - accessAddressEIP2929( - runState, - toAddressBuf, - runState._common.param('gasPrices', 'staticcall') - ) + accessAddressEIP2929(runState, toAddress, runState._common.param('gasPrices', 'staticcall')) gasLimit = maxCallGas(gasLimit, runState.eei.getGasLeft(), runState) // we set TangerineWhistle or later to true here, as STATICCALL was available from Byzantium (which is after TangerineWhistle) let data = Buffer.alloc(0) @@ -1187,7 +1181,7 @@ export const handlers: Map = new Map([ runState.eei.useGas(new BN(runState._common.param('gasPrices', 'callNewAccount'))) } - accessAddressEIP2929(runState, selfdestructToAddress.buf, 0) + accessAddressEIP2929(runState, selfdestructToAddress, 0) return runState.eei.selfDestruct(selfdestructToAddress) }, ], diff --git a/packages/vm/tests/api/EIPs/eip-2929.spec.ts b/packages/vm/tests/api/EIPs/eip-2929.spec.ts index 99601bf43c..ec694f374c 100644 --- a/packages/vm/tests/api/EIPs/eip-2929.spec.ts +++ b/packages/vm/tests/api/EIPs/eip-2929.spec.ts @@ -1,5 +1,5 @@ import tape from 'tape' -import { BN } from 'ethereumjs-util' +import { Address, BN } from 'ethereumjs-util' import VM from '../../../lib' import Common from '@ethereumjs/common' import { inspect } from 'util' @@ -7,7 +7,7 @@ import { inspect } from 'util' // Test cases source: https://gist.github.com/holiman/174548cad102096858583c6fbbb0649a tape('EIP 2929: gas cost tests', (t) => { const initialGas = new BN(0xffffffffff) - const address = Buffer.from('000000000000000000000000636F6E7472616374', 'hex') + const address = new Address(Buffer.from('000000000000000000000000636F6E7472616374', 'hex')) const common = new Common({ chain: 'mainnet', hardfork: 'berlin', eips: [2929] }) const runTest = async function (test: any, st: tape.Test) { @@ -20,14 +20,15 @@ tape('EIP 2929: gas cost tests', (t) => { currentGas = step.gasLeft if (test.steps.length) { - st.equal(step.opcode.name, test.steps[i].expectedOpcode) + st.equal(step.opcode.name, test.steps[i].expectedOpcode, `Expected Opcode: ${test.steps[i].expectedOpcode}`) // Validates the gas consumption of the (i - 1)th opcode // b/c the step event fires before gas is debited. // The first opcode of every test should be +/- irrelevant // (ex: PUSH) and the last opcode is always STOP if (i > 0) { - st.equal(true, gasUsed.eq(new BN(test.steps[i - 1].expectedGasUsed))) + const expectedGasUsed = new BN(test.steps[i - 1].expectedGasUsed) + st.equal(true, gasUsed.eq(expectedGasUsed), `Opcode: ${test.steps[i - 1].expectedOpcode}, Gase Used: ${gasUsed}, Expected: ${expectedGasUsed}`) } } i++