Skip to content

EIP-2929: Gas cost increases for state access opcodes #889

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Oct 21, 2020
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just seeing here that this implementation has also these common.eips().includes() calls. We should really replace this soon by a method in common also taking the HFs into account. This will otherwise fall onto our feet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(so "soon" means: in the next 1-2 weeks)

Copy link
Member

@jochem-brouwer jochem-brouwer Feb 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to introduce this in the EIP2718 PR, but won't do it as it will get too bloated otherwise. Can you open an issue for this so we don't "forget" it? 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will just prioritize this as the first task I'll do on Monday, so we won't forget then. 😄


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