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..2ad7f8944d 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()) + this._runState.accessedAddresses.add(this._eei.getAddress().toString()) + + for (let address of Object.keys(precompiles)) { + this._runState.accessedAddresses.add(`0x${address}`) + } + } } diff --git a/packages/vm/lib/evm/opcodes/EIP1283.ts b/packages/vm/lib/evm/opcodes/EIP1283.ts new file mode 100644 index 0000000000..085972208e --- /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..385516273a --- /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..25d9f0608c --- /dev/null +++ b/packages/vm/lib/evm/opcodes/EIP2929.ts @@ -0,0 +1,102 @@ +import BN = require('bn.js') +import { Address } from 'ethereumjs-util' +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: Address, baseFee?: number) { + if (!runState._common.eips().includes(2929)) return + + const addressStr = address.toString() + + // 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() + 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() + 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 34cd338c21..f0cfe6bd1f 100644 --- a/packages/vm/lib/evm/opcodes/functions.ts +++ b/packages/vm/lib/evm/opcodes/functions.ts @@ -18,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' @@ -408,6 +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, runState._common.param('gasPrices', 'balance')) const balance = await runState.eei.getExternalBalance(address) runState.stack.push(balance) }, @@ -506,8 +509,10 @@ export const handlers: Map = new Map([ [ 0x3b, async function (runState: RunState) { - const address = runState.stack.pop() - const size = await runState.eei.getExternalCodeSize(address) + 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(addressBN) runState.stack.push(size) }, ], @@ -515,16 +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() @@ -539,6 +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, runState._common.param('gasPrices', 'extcodehash')) const empty = await runState.eei.isAccountEmpty(address) if (empty) { runState.stack.push(new BN(0)) @@ -710,6 +718,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 +745,9 @@ 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) + updateSstoreGasEIP1283(runState, found, setLengthLeftStorage(value), keyBuf) + updateSstoreGasEIP2200(runState, found, setLengthLeftStorage(value), keyBuf) await runState.eei.storageStore(keyBuf, value) }, ], @@ -924,6 +935,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 +951,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 +988,7 @@ export const handlers: Map = new Map([ } subMemUsage(runState, inOffset, inLength) subMemUsage(runState, outOffset, outLength) + accessAddressEIP2929(runState, toAddress, runState._common.param('gasPrices', 'call')) if (!value.isZero()) { runState.eei.useGas(new BN(runState._common.param('gasPrices', 'callValueTransfer'))) @@ -1025,6 +1040,8 @@ export const handlers: Map = new Map([ subMemUsage(runState, inOffset, inLength) subMemUsage(runState, outOffset, outLength) + accessAddressEIP2929(runState, toAddress, runState._common.param('gasPrices', 'callcode')) + if (!value.isZero()) { runState.eei.useGas(new BN(runState._common.param('gasPrices', 'callValueTransfer'))) } @@ -1060,6 +1077,8 @@ export const handlers: Map = new Map([ subMemUsage(runState, inOffset, inLength) subMemUsage(runState, outOffset, outLength) + 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) if (gasLimit.gt(runState.eei.getGasLeft())) { @@ -1087,6 +1106,7 @@ export const handlers: Map = new Map([ subMemUsage(runState, inOffset, inLength) subMemUsage(runState, outOffset, outLength) + 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) @@ -1161,6 +1181,7 @@ export const handlers: Map = new Map([ runState.eei.useGas(new BN(runState._common.param('gasPrices', 'callNewAccount'))) } + accessAddressEIP2929(runState, selfdestructToAddress, 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 aed3d7d7f0..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) @@ -35,7 +36,8 @@ export function trap(err: string) { * @param {BN} address * @return {Buffer} */ -export 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) } @@ -218,138 +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) { - 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)) { - return runState.eei.useGas( - new BN(runState._common.param('gasPrices', 'sstoreNoopGasEIP2200')) - ) - } - 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 - runState.eei.refundGas( - new BN(runState._common.param('gasPrices', 'sstoreInitRefundEIP2200')) - ) - } else { - // Reset to original existing slot - runState.eei.refundGas( - new BN(runState._common.param('gasPrices', 'sstoreCleanRefundEIP2200')) - ) - } - } - // Dirty update - return runState.eei.useGas(new BN(runState._common.param('gasPrices', 'sstoreDirtyGasEIP2200'))) - } else { - if (value.length === 0 && !found.length) { - 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.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'))) - } - } -} - /** * Writes data returned by eei.call* methods to memory * diff --git a/packages/vm/lib/index.ts b/packages/vm/lib/index.ts index d6f937a392..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:* * @@ -134,7 +135,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..ec694f374c --- /dev/null +++ b/packages/vm/tests/api/EIPs/eip-2929.spec.ts @@ -0,0 +1,205 @@ +import tape from 'tape' +import { Address, 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 = 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) { + 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, `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) { + 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++ + }) + + 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() + }) +})