Skip to content

Commit

Permalink
VM: report dynamic gas values in fee field of step event (#1364)
Browse files Browse the repository at this point in the history
* 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

Co-authored-by: Ryan Ghods <ryan@ryanio.com>
  • Loading branch information
2 people authored and holgerd77 committed Sep 1, 2021
1 parent 6d80656 commit 3cb693a
Show file tree
Hide file tree
Showing 16 changed files with 1,075 additions and 532 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/vm-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,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 @@ -228,4 +228,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 @@ -23,6 +23,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 @@ -101,4 +101,4 @@
"bugs": {
"url": "https://github.com/ethereumjs/ethereumjs-monorepo/issues?q=is%3Aissue+label%3A%22package%3A+vm%22"
}
}
}
28 changes: 22 additions & 6 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 @@ -44,7 +46,7 @@ export interface InterpreterStep {
memoryWordCount: BN
opcode: {
name: string
fee: number
fee: BN
isAsync: boolean
}
account: Account
Expand Down Expand Up @@ -107,7 +109,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 +137,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 +186,15 @@ export default class Interpreter {
return this._vm._opcodes.get(op) ?? this._vm._opcodes.get(0xfe)
}

async _runStepHook(): Promise<void> {
async _runStepHook(fee: 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,
fee,
isAsync: opcode.isAsync,
},
stack: this._runState.stack._store,
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 3cb693a

Please sign in to comment.