Skip to content

Commit

Permalink
VM: Adopted Backwards-compatible Dynamic Gas Cost Refactoring (#1553)
Browse files Browse the repository at this point in the history
* VM: report dynamic gas values in `fee` field of `step` event (#1364)
* vm: codes: add dynamicGas property
vm: codes: make codes more typesafe
* vm: create no-op dynamic gas handlers
* vm: first batch of dynamic gas up to 0x3f
* vm: add other opcodes to gas map
vm: change step event fee type from number to BN
vm: deduct dynamic gas
vm: fix stack peek
vm: do not add gasLimit to gas cost for CREATE
* vm: move errors to gas map
* vm: fix memory dynamic  gas bug
* vm: fix gas bugs caused by not considering base fee
* vm: fix message call gas related bugs, clone current gas left
* add typedoc for peek
use underscore for unused peek params (and fix eslint config)
format some comment newlines for readability
switch from require to import for exceptions
* simplify the 2929 state manager castings in runTx
* add changelog entry
* vm: add EIP1283 tests
* vm: split non-eip2929 and eip2929 gas costs
* vm: fix gas costs
* vm: add early-hardfork coverage
* vm: clarify pre-Constantinople SSTORE gas
vm: clarify EIP-150 comment
* run coverage for all state and blockchain tests, remove redundant istanbul run
* vm: fix CALLCODE gas
vm: explicitly clone gasLimit for maxCallGas
* vm: remove TODO in interpreter
* update defaultCost to BN, cast 2929 statemanager to simplify use syntax
* use underscore for unused variables, simplify types since they can be inferred
* vm: fix browser tests + fix rebase
* VM: moved dynamic fee to dedicated dynamicFee field in step event and retained fee behavior and type for backwards compatibility
* VM: aligned InterpreterStep and step event object property documentation, completed missing step event properties
* VM: test fix
* vm: fix hardhat e2e tests
* vm: fix MSTORE opcodes
* vm: added dynamicGas property to push0 (EIP-3855) opcode
* hardhat e2e: add temporary workaround for skipping tests with inconsistent memory field
* nit style: use underscore instead of comment out unused variable
Co-authored-by: Jochem Brouwer <jochembrouwer96@gmail.com>
Co-authored-by: Ryan Ghods <ryan@ryanio.com>
  • Loading branch information
holgerd77 committed Jan 12, 2022
1 parent 7f0a06a commit ee00047
Show file tree
Hide file tree
Showing 17 changed files with 1,098 additions and 540 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/vm-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,4 @@ jobs:

# Re-apply git stash to prepare for saving back to cache.
# Avoids exit code 1 by checking if there are changes to be stashed first
- run: STASH_LIST=`git stash list` && [ ! -z $STASH_LIST ] && git stash apply || echo "No files to stash-apply. Skipping…"
- run: STASH_LIST=`git stash list` && [ ! -z $STASH_LIST ] && git stash apply || echo "No files to stash-apply. Skipping…"
2 changes: 1 addition & 1 deletion .github/workflows/vm-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,4 @@ jobs:

# Re-apply git stash to prepare for saving back to cache.
# Avoids exit code 1 by checking if there are changes to be stashed first
- run: STASH_LIST=`git stash list` && [ ! -z $STASH_LIST ] && git stash apply || echo "No files to stash-apply. Skipping…"
- run: STASH_LIST=`git stash list` && [ ! -z $STASH_LIST ] && git stash apply || echo "No files to stash-apply. Skipping…"
1 change: 1 addition & 0 deletions packages/vm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Bug fix release to reverse StateManager interface breaking change. The method `m
**New Features**

- StateManager: Added `modifyAccountFields` method to simplify the `getAccount` -> modify fields -> `putAccount` pattern, PR [#1369](https://github.com/ethereumjs/ethereumjs-monorepo/pull/1369)
- Report dynamic gas values in `fee` field of `step` event, PR [#1364](https://github.com/ethereumjs/ethereumjs-monorepo/pull/1364)

**Bug Fixes**

Expand Down
2 changes: 1 addition & 1 deletion packages/vm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,4 @@
"bugs": {
"url": "https://github.com/ethereumjs/ethereumjs-monorepo/issues?q=is%3Aissue+label%3A%22package%3A+vm%22"
}
}
}
52 changes: 38 additions & 14 deletions packages/vm/src/evm/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Memory from './memory'
import Stack from './stack'
import EEI from './eei'
import { Opcode, handlers as opHandlers, OpHandler, AsyncOpHandler } from './opcodes'
import { dynamicGasHandlers } from './opcodes/gas'

export interface InterpreterOpts {
pc?: number
Expand All @@ -24,6 +25,7 @@ export interface RunState {
validJumpSubs: number[]
stateManager: StateManager
eei: EEI
messageGasLimit?: BN // Cache value from `gas.ts` to save gas limit for a message call
}

export interface InterpreterResult {
Expand All @@ -32,22 +34,23 @@ export interface InterpreterResult {
}

export interface InterpreterStep {
pc: number
opcode: {
name: string
fee: number
dynamicFee?: BN
isAsync: boolean
}
gasLeft: BN
gasRefund: BN
stateManager: StateManager
stack: BN[]
returnStack: BN[]
pc: number
depth: number
account: Account
address: Address
depth: number
memory: Buffer
memoryWordCount: BN
opcode: {
name: string
fee: number
isAsync: boolean
}
account: Account
codeAddress: Address
}

Expand Down Expand Up @@ -107,7 +110,6 @@ export default class Interpreter {
while (this._runState.programCounter < this._runState.code.length) {
const opCode = this._runState.code[this._runState.programCounter]
this._runState.opCode = opCode
await this._runStepHook()

try {
await this.runStep()
Expand Down Expand Up @@ -136,13 +138,28 @@ export default class Interpreter {
*/
async runStep(): Promise<void> {
const opInfo = this.lookupOpInfo(this._runState.opCode)

const gas = new BN(opInfo.fee)
// clone the gas limit; call opcodes can add stipend,
// which makes it seem like the gas left increases
const gasLimitClone = this._eei.getGasLeft()

if (opInfo.dynamicGas) {
const dynamicGasHandler = dynamicGasHandlers.get(this._runState.opCode)!
// This function updates the gas BN in-place using `i*` methods
// It needs the base fee, for correct gas limit calculation for the CALL opcodes
await dynamicGasHandler(this._runState, gas, this._vm._common)
}

await this._runStepHook(gas, gasLimitClone)

// Check for invalid opcode
if (opInfo.name === 'INVALID') {
throw new VmError(ERROR.INVALID_OPCODE)
}

// Reduce opcode's base fee
this._eei.useGas(new BN(opInfo.fee), `${opInfo.name} (base fee)`)
this._eei.useGas(gas, `${opInfo.name} fee`)
// Advance program counter
this._runState.programCounter++

Expand Down Expand Up @@ -170,15 +187,16 @@ export default class Interpreter {
return this._vm._opcodes.get(op) ?? this._vm._opcodes.get(0xfe)
}

async _runStepHook(): Promise<void> {
async _runStepHook(dynamicFee: BN, gasLeft: BN): Promise<void> {
const opcode = this.lookupOpInfo(this._runState.opCode)
const eventObj: InterpreterStep = {
pc: this._runState.programCounter,
gasLeft: this._eei.getGasLeft(),
gasLeft,
gasRefund: this._eei._evm._refund,
opcode: {
name: opcode.fullName,
fee: opcode.fee,
dynamicFee,
isAsync: opcode.isAsync,
},
stack: this._runState.stack._store,
Expand Down Expand Up @@ -222,15 +240,21 @@ export default class Interpreter {
* @event Event: step
* @type {Object}
* @property {Number} pc representing the program counter
* @property {String} opcode the next opcode to be ran
* @property {Object} opcode the next opcode to be ran
* @property {string} opcode.name
* @property {fee} opcode.number Base fee of the opcode
* @property {dynamicFee} opcode.dynamicFee Dynamic opcode fee
* @property {boolean} opcode.isAsync opcode is async
* @property {BN} gasLeft amount of gasLeft
* @property {BN} gasRefund gas refund
* @property {StateManager} stateManager a {@link StateManager} instance
* @property {Array} stack an `Array` of `Buffers` containing the stack
* @property {Array} returnStack the return stack
* @property {Account} account the Account which owns the code running
* @property {Address} address the address of the `account`
* @property {Number} depth the current number of calls deep the contract is
* @property {Buffer} memory the memory of the VM as a `buffer`
* @property {BN} memoryWordCount current size of memory in words
* @property {StateManager} stateManager a {@link StateManager} instance
* @property {Address} codeAddress the address of the code which is currently being ran (this differs from `address` in a `DELEGATECALL` and `CALLCODE` call)
*/
return this._vm._emit('step', eventObj)
Expand Down
21 changes: 4 additions & 17 deletions packages/vm/src/evm/opcodes/EIP1283.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,14 @@ export function updateSstoreGasEIP1283(
) {
if (currentStorage.equals(value)) {
// If current value equals new value (this is a no-op), 200 gas is deducted.
runState.eei.useGas(
new BN(common.param('gasPrices', 'netSstoreNoopGas')),
'EIP-1283 -> netSstoreNoopGas'
)
return
return new BN(common.param('gasPrices', 'netSstoreNoopGas'))
}
// If current value does not equal new value
if (originalStorage.equals(currentStorage)) {
// If original value equals current value (this storage slot has not been changed by the current execution context)
if (originalStorage.length === 0) {
// If original value is 0, 20000 gas is deducted.
return runState.eei.useGas(
new BN(common.param('gasPrices', 'netSstoreInitGas')),
'EIP-1283 -> netSstoreInitGas'
)
return new BN(common.param('gasPrices', 'netSstoreInitGas'))
}
if (value.length === 0) {
// If new value is 0, add 15000 gas to refund counter.
Expand All @@ -44,10 +37,7 @@ export function updateSstoreGasEIP1283(
)
}
// Otherwise, 5000 gas is deducted.
return runState.eei.useGas(
new BN(common.param('gasPrices', 'netSstoreCleanGas')),
'EIP-1283 -> netSstoreCleanGas'
)
return new BN(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 (originalStorage.length !== 0) {
Expand Down Expand Up @@ -82,8 +72,5 @@ export function updateSstoreGasEIP1283(
)
}
}
return runState.eei.useGas(
new BN(common.param('gasPrices', 'netSstoreDirtyGas')),
'EIP-1283 -> netSstoreDirtyGas'
)
return new BN(common.param('gasPrices', 'netSstoreDirtyGas'))
}
30 changes: 9 additions & 21 deletions packages/vm/src/evm/opcodes/EIP2200.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,13 @@ export function updateSstoreGasEIP2200(

// Noop
if (currentStorage.equals(value)) {
const sstoreNoopCost = common.param('gasPrices', 'sstoreNoopGasEIP2200')
return runState.eei.useGas(
new BN(adjustSstoreGasEIP2929(runState, key, sstoreNoopCost, 'noop', common)),
'EIP-2200 -> sstoreNoopGasEIP2200'
)
const sstoreNoopCost = new BN(common.param('gasPrices', 'sstoreNoopGasEIP2200'))
return adjustSstoreGasEIP2929(runState, key, sstoreNoopCost, 'noop', common)
}
if (originalStorage.equals(currentStorage)) {
// Create slot
if (originalStorage.length === 0) {
return runState.eei.useGas(
new BN(common.param('gasPrices', 'sstoreInitGasEIP2200')),
'EIP-2200 -> sstoreInitGasEIP2200'
)
return new BN(common.param('gasPrices', 'sstoreInitGasEIP2200'))
}
// Delete slot
if (value.length === 0) {
Expand All @@ -51,10 +45,7 @@ export function updateSstoreGasEIP2200(
)
}
// Write existing slot
return runState.eei.useGas(
new BN(common.param('gasPrices', 'sstoreCleanGasEIP2200')),
'EIP-2200 -> sstoreCleanGasEIP2200'
)
return new BN(common.param('gasPrices', 'sstoreCleanGasEIP2200'))
}
if (originalStorage.length > 0) {
if (currentStorage.length === 0) {
Expand All @@ -74,23 +65,20 @@ export function updateSstoreGasEIP2200(
if (originalStorage.equals(value)) {
if (originalStorage.length === 0) {
// Reset to original non-existent slot
const sstoreInitRefund = common.param('gasPrices', 'sstoreInitRefundEIP2200')
const sstoreInitRefund = new BN(common.param('gasPrices', 'sstoreInitRefundEIP2200'))
runState.eei.refundGas(
new BN(adjustSstoreGasEIP2929(runState, key, sstoreInitRefund, 'initRefund', common)),
adjustSstoreGasEIP2929(runState, key, sstoreInitRefund, 'initRefund', common),
'EIP-2200 -> initRefund'
)
} else {
// Reset to original existing slot
const sstoreCleanRefund = common.param('gasPrices', 'sstoreCleanRefundEIP2200')
const sstoreCleanRefund = new BN(common.param('gasPrices', 'sstoreCleanRefundEIP2200'))
runState.eei.refundGas(
new BN(adjustSstoreGasEIP2929(runState, key, sstoreCleanRefund, 'cleanRefund', common)),
adjustSstoreGasEIP2929(runState, key, sstoreCleanRefund, 'cleanRefund', common),
'EIP-2200 -> cleanRefund'
)
}
}
// Dirty update
return runState.eei.useGas(
new BN(common.param('gasPrices', 'sstoreDirtyGasEIP2200')),
'EIP-2200 -> sstoreDirtyGasEIP2200'
)
return new BN(common.param('gasPrices', 'sstoreDirtyGasEIP2200'))
}
63 changes: 27 additions & 36 deletions packages/vm/src/evm/opcodes/EIP2929.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,26 @@ export function accessAddressEIP2929(
common: Common,
chargeGas = true,
isSelfdestruct = false
) {
if (!common.isActivatedEIP(2929)) return
): BN {
if (!common.isActivatedEIP(2929)) return new BN(0)

const stateManager = runState.stateManager as EIP2929StateManager
const addressStr = address.buf

// Cold
if (!(<EIP2929StateManager>runState.stateManager).isWarmedAddress(addressStr)) {
// eslint-disable-next-line prettier/prettier
(<EIP2929StateManager>runState.stateManager).addWarmedAddress(addressStr)
if (!stateManager.isWarmedAddress(addressStr)) {
stateManager.addWarmedAddress(addressStr)

// CREATE, CREATE2 opcodes have the address warmed for free.
// selfdestruct beneficiary address reads are charged an *additional* cold access
if (chargeGas) {
runState.eei.useGas(
new BN(common.param('gasPrices', 'coldaccountaccess')),
'EIP-2929 -> coldaccountaccess'
)
return new BN(common.param('gasPrices', 'coldaccountaccess'))
}
// Warm: (selfdestruct beneficiary address reads are not charged when warm)
} else if (chargeGas && !isSelfdestruct) {
runState.eei.useGas(
new BN(common.param('gasPrices', 'warmstorageread')),
'EIP-2929 -> warmstorageread'
)
return new BN(common.param('gasPrices', 'warmstorageread'))
}
return new BN(0)
}

/**
Expand All @@ -59,59 +54,55 @@ export function accessStorageEIP2929(
key: Buffer,
isSstore: boolean,
common: Common
) {
if (!common.isActivatedEIP(2929)) return
): BN {
if (!common.isActivatedEIP(2929)) return new BN(0)

const stateManager = runState.stateManager as EIP2929StateManager
const address = runState.eei.getAddress().buf

const slotIsCold = !(<EIP2929StateManager>runState.stateManager).isWarmedStorage(address, key)
const slotIsCold = !stateManager.isWarmedStorage(address, key)

// Cold (SLOAD and SSTORE)
if (slotIsCold) {
// eslint-disable-next-line prettier/prettier
(<EIP2929StateManager>runState.stateManager).addWarmedStorage(address, key)
runState.eei.useGas(new BN(common.param('gasPrices', 'coldsload')), 'EIP-2929 -> coldsload')
stateManager.addWarmedStorage(address, key)
return new BN(common.param('gasPrices', 'coldsload'))
} else if (!isSstore) {
runState.eei.useGas(
new BN(common.param('gasPrices', 'warmstorageread')),
'EIP-2929 -> warmstorageread'
)
return new BN(common.param('gasPrices', 'warmstorageread'))
}
return new BN(0)
}

/**
* 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')
* @param {BN} defaultCost SSTORE_RESET_GAS / SLOAD
* @param {string} costName parameter name ('noop')
* @param {Common} common
* @return {number} adjusted cost
* @return {BN} adjusted cost
*/
export function adjustSstoreGasEIP2929(
runState: RunState,
key: Buffer,
defaultCost: number,
defaultCost: BN,
costName: string,
common: Common
): number {
): BN {
if (!common.isActivatedEIP(2929)) return defaultCost

const stateManager = runState.stateManager as EIP2929StateManager
const address = runState.eei.getAddress().buf
const warmRead = common.param('gasPrices', 'warmstorageread')
const coldSload = common.param('gasPrices', 'coldsload')
const warmRead = new BN(common.param('gasPrices', 'warmstorageread'))
const coldSload = new BN(common.param('gasPrices', 'coldsload'))

if ((<EIP2929StateManager>runState.stateManager).isWarmedStorage(address, key)) {
if (stateManager.isWarmedStorage(address, key)) {
switch (costName) {
case 'reset':
return defaultCost - coldSload
case 'noop':
return warmRead
case 'initRefund':
return common.param('gasPrices', 'sstoreInitGasEIP2200') - warmRead
return new BN(common.param('gasPrices', 'sstoreInitGasEIP2200')).sub(warmRead)
case 'cleanRefund':
return common.param('gasPrices', 'sstoreReset') - coldSload - warmRead
return new BN(common.param('gasPrices', 'sstoreReset')).sub(coldSload).sub(warmRead)
}
}

Expand Down
Loading

0 comments on commit ee00047

Please sign in to comment.