Skip to content

Commit

Permalink
Merge pull request #889 from ethereumjs/eip-2929
Browse files Browse the repository at this point in the history
EIP-2929: Gas cost increases for state access opcodes
  • Loading branch information
holgerd77 authored Oct 21, 2020
2 parents e759b8f + c186e33 commit 1afe8db
Show file tree
Hide file tree
Showing 11 changed files with 537 additions and 141 deletions.
24 changes: 24 additions & 0 deletions packages/common/src/eips/2929.json
Original file line number Diff line number Diff line change
@@ -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": {}
}
1 change: 1 addition & 0 deletions packages/common/src/eips/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { eipsType } from './../types'
export const EIPs: eipsType = {
2315: require('./2315.json'),
2537: require('./2537.json'),
2929: require('./2929.json'),
}
2 changes: 1 addition & 1 deletion packages/common/tests/eips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
20 changes: 20 additions & 0 deletions packages/vm/lib/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 } from './opcodes'
import { precompiles } from './precompiles'

export interface InterpreterOpts {
pc?: number
Expand All @@ -25,6 +26,8 @@ export interface RunState {
_common: Common
stateManager: StateManager
eei: EEI
accessedAddresses: Set<string>
accessedStorage: Map<string, Set<string>>
}

export interface InterpreterResult {
Expand Down Expand Up @@ -84,6 +87,8 @@ export default class Interpreter {
_common: this._vm._common,
stateManager: this._state,
eei: this._eei,
accessedAddresses: new Set(),
accessedStorage: new Map(),
}
}

Expand All @@ -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
Expand Down Expand Up @@ -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}`)
}
}
}
59 changes: 59 additions & 0 deletions packages/vm/lib/evm/opcodes/EIP1283.ts
Original file line number Diff line number Diff line change
@@ -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')))
}
}
93 changes: 93 additions & 0 deletions packages/vm/lib/evm/opcodes/EIP2200.ts
Original file line number Diff line number Diff line change
@@ -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')))
}
}
}
102 changes: 102 additions & 0 deletions packages/vm/lib/evm/opcodes/EIP2929.ts
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 1afe8db

Please sign in to comment.