Skip to content

Commit

Permalink
Implement EIP 2935 (ethereumjs#3268)
Browse files Browse the repository at this point in the history
* common/evm/vm: draft impl eip 2935 [no ci]

* evm: excempt blockhash addr from eip158 [no ci]

* evm: exempt historyStorageAddress eip-158

* common: add 2935 to prague

* evm: ensure 0 is pushed if hash of block number or higher is requested (no state inspection)

* common: add eipTimestamp method

* vm: update 2935 logic

* vm: add tests for 2935 (skeleton, no contents) [no ci]

* common: add `customHardforks` option

* delete double test

* common: remove 2935 from prague

* common: custom chain test fix

* evm: fix 2935

* evm/vm: fix eip2935

* vm: add more eip 2935 tests

* common: clarify docs

* vm: remove `bind` in accumulateParentBlockHash

* common: eip2935 update url to point to correct commit

* common: extra sanity checks on test

* vm: add docs for accumulateParentBlockHash

* vm/common: address review
  • Loading branch information
jochem-brouwer authored Mar 4, 2024
1 parent f502494 commit 2fea5cb
Show file tree
Hide file tree
Showing 12 changed files with 397 additions and 19 deletions.
27 changes: 25 additions & 2 deletions packages/common/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,8 @@ export class Common {
// Assign hardfork changes in the sequence of the applied hardforks
this.HARDFORK_CHANGES = this.hardforks().map((hf) => [
hf.name as HardforkSpecKeys,
HARDFORK_SPECS[hf.name as HardforkSpecKeys],
HARDFORK_SPECS[hf.name] ??
(this._chainParams.customHardforks && this._chainParams.customHardforks[hf.name]),
])
this._hardfork = this.DEFAULT_HARDFORK
if (opts.hardfork !== undefined) {
Expand Down Expand Up @@ -779,6 +780,24 @@ export class Common {
return null
}

/**
* Returns the scheduled timestamp of the EIP (if scheduled and scheduled by timestamp)
* @param eip EIP number
* @returns Scheduled timestamp. If this EIP is unscheduled, or the EIP is scheduled by block number or ttd, then it returns `null`.
*/
eipTimestamp(eip: number): bigint | null {
for (const hfChanges of this.HARDFORK_CHANGES) {
const hf = hfChanges[1]
if ('eips' in hf) {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if ((hf['eips'] as any).includes(eip)) {
return this.hardforkTimestamp(hfChanges[0])
}
}
}
return null
}

/**
* Returns the hardfork change total difficulty (Merge HF) for hardfork provided or set
* @param hardfork Hardfork name, optional if HF set
Expand Down Expand Up @@ -946,7 +965,11 @@ export class Common {
* @returns {Array} Array with arrays of hardforks
*/
hardforks(): HardforkTransitionConfig[] {
return this._chainParams.hardforks
const hfs = this._chainParams.hardforks
if (this._chainParams.customHardforks !== undefined) {
this._chainParams.customHardforks
}
return hfs
}

/**
Expand Down
18 changes: 18 additions & 0 deletions packages/common/src/eips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type EIPsDict = {
}

enum Status {
Stagnant = 'stagnant',
Draft = 'draft',
Review = 'review',
Final = 'final',
Expand Down Expand Up @@ -190,6 +191,23 @@ export const EIPs: EIPsDict = {
},
},
},
2935: {
comment: 'Save historical block hashes in state (Verkle related usage, UNSTABLE)',
url: 'https://github.com/ethereum/EIPs/pull/8166/commits/941d3beb084d638be258b8fded6171cf0705a5db',
status: Status.Draft,
minimumHardfork: Hardfork.Chainstart,
requiredEIPs: [],
vm: {
historyStorageAddress: {
v: BigInt('0xfffffffffffffffffffffffffffffffffffffffe'),
d: 'The address where the historical blockhashes are stored',
},
minHistoryServeWindow: {
v: BigInt(256),
d: 'The minimum amount of blocks to be served by the historical blockhash contract',
},
},
},
3074: {
comment: 'AUTH and AUTHCALL opcodes',
url: 'https://eips.ethereum.org/EIPS/eip-3074',
Expand Down
8 changes: 2 additions & 6 deletions packages/common/src/hardforks.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import type { HardforkConfig } from './types.js'
import type { HardforksDict } from './types.js'

type HardforksDict = {
[key: string]: HardforkConfig
}

enum Status {
export enum Status {
Draft = 'draft',
Review = 'review',
Final = 'final',
Expand Down
5 changes: 5 additions & 0 deletions packages/common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface ChainConfig {
url?: string
genesis: GenesisBlockConfig
hardforks: HardforkTransitionConfig[]
customHardforks?: HardforksDict
bootstrapNodes: BootstrapNodeConfig[]
dnsNetworks?: string[]
consensus: ConsensusConfig
Expand Down Expand Up @@ -194,3 +195,7 @@ export type HardforkConfig = {
eips?: number[]
consensus?: ConsensusConfig
} & EIPOrHFConfig

export type HardforksDict = {
[key: string]: HardforkConfig
}
53 changes: 53 additions & 0 deletions packages/common/test/customChains.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { assert, describe, it } from 'vitest'

import { Status } from '../src/hardforks.js'
import { Chain, Common, ConsensusType, CustomChain, Hardfork } from '../src/index.js'

import * as testnet from './data/testnet.json'
Expand Down Expand Up @@ -151,6 +152,58 @@ describe('[Common]: Custom chains', () => {
'customChains, should allow to switch custom chain'
)
})

it('customHardforks parameter: initialization and transition tests', () => {
const c = Common.custom({
customHardforks: {
testEIP2935Hardfork: {
name: 'testEIP2935Hardfork',
comment: 'Hardfork to test EIP 2935',
url: '',
status: Status.Final,
eips: [2935],
},
},
hardforks: [
{
name: 'chainstart',
block: 0,
},
{
name: 'berlin',
block: null,
timestamp: 999,
},
{
// Note: this custom hardfork name MUST be in customHardforks as field
// If this is not the case, Common will throw with a random error
// Should we throw early with a descriptive error? TODO
name: 'testEIP2935Hardfork',
block: null,
timestamp: 1000,
},
],
})
// Note: default HF of Common is currently Shanghai
// Did not pass any "hardfork" param
assert.equal(c.hardfork(), Hardfork.Shanghai)
c.setHardforkBy({
blockNumber: 0,
})
assert.equal(c.hardfork(), Hardfork.Chainstart)
c.setHardforkBy({
blockNumber: 1,
timestamp: 999,
})
assert.equal(c.hardfork(), Hardfork.Berlin)
assert.notOk(c.isActivatedEIP(2935))
c.setHardforkBy({
blockNumber: 1,
timestamp: 1000,
})
assert.equal(c.hardfork(), 'testEIP2935Hardfork')
assert.ok(c.isActivatedEIP(2935))
})
})

describe('custom chain setup with hardforks with undefined/null block numbers', () => {
Expand Down
13 changes: 13 additions & 0 deletions packages/common/test/eips.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,17 @@ describe('[Common/EIPs]: Initialization / Chain params', () => {
msg = 'should return null for unscheduled eip'
assert.equal(c.eipBlock(0), null, msg)
})

it('eipTimestamp', () => {
const c = new Common({ chain: Chain.Mainnet })

let msg = 'should return null for unscheduled eip by timestamp'
assert.ok(c.eipTimestamp(1559) === null, msg)

msg = 'should return null for unscheduled eip'
assert.equal(c.eipTimestamp(0), null, msg)

msg = 'should return correct value'
assert.equal(c.eipTimestamp(3651), BigInt(1681338455), msg)
})
})
4 changes: 2 additions & 2 deletions packages/evm/src/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ export class EVM implements EVMInterface {

// Supported EIPs
const supportedEIPs = [
1153, 1559, 2315, 2565, 2718, 2929, 2930, 3074, 3198, 3529, 3540, 3541, 3607, 3651, 3670,
3855, 3860, 4399, 4895, 4788, 4844, 5133, 5656, 6780, 6800, 7516,
1153, 1559, 2315, 2565, 2718, 2929, 2930, 2935, 3074, 3198, 3529, 3540, 3541, 3607, 3651,
3670, 3855, 3860, 4399, 4895, 4788, 4844, 5133, 5656, 6780, 6800, 7516,
]

for (const eip of this.common.eips()) {
Expand Down
8 changes: 8 additions & 0 deletions packages/evm/src/journal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Hardfork } from '@ethereumjs/common'
import {
Address,
RIPEMD160_ADDRESS_STRING,
bigIntToHex,
bytesToHex,
bytesToUnprefixedHex,
stripHexPrefix,
Expand Down Expand Up @@ -198,6 +199,13 @@ export class Journal {
const address = new Address(toBytes('0x' + addressHex))
const account = await this.stateManager.getAccount(address)
if (account === undefined || account.isEmpty()) {
if (this.common.isActivatedEIP(2935)) {
// The history storage address is exempt of state clearing by EIP-158 if the EIP is activated
const addr = bigIntToHex(this.common.param('vm', 'historyStorageAddress')).slice(2)
if (addressHex === addr) {
continue
}
}
await this.deleteAccount(address)
if (this.DEBUG) {
this._debug(`Cleanup touched account address=${address} (>= SpuriousDragon)`)
Expand Down
35 changes: 26 additions & 9 deletions packages/evm/src/opcodes/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
SECP256K1_ORDER_DIV_2,
TWO_POW256,
bigIntToBytes,
bigIntToHex,
bytesToBigInt,
bytesToHex,
concatBytes,
Expand Down Expand Up @@ -593,19 +594,35 @@ export const handlers: Map<number, OpHandler> = new Map([
// 0x40: BLOCKHASH
[
0x40,
async function (runState) {
async function (runState, common) {
const number = runState.stack.pop()

const diff = runState.interpreter.getBlockNumber() - number
// block lookups must be within the past 256 blocks
if (diff > BIGINT_256 || diff <= BIGINT_0) {
runState.stack.push(BIGINT_0)
return
}
if (common.isActivatedEIP(2935)) {
if (number >= runState.interpreter.getBlockNumber()) {
runState.stack.push(BIGINT_0)
return
}

const block = await runState.blockchain.getBlock(Number(number))
const historyAddress = Address.fromString(
bigIntToHex(common.param('vm', 'historyStorageAddress'))
)
const key = setLengthLeft(bigIntToBytes(number), 32)

runState.stack.push(bytesToBigInt(block.hash()))
const storage = await runState.stateManager.getContractStorage(historyAddress, key)

runState.stack.push(bytesToBigInt(storage))
} else {
const diff = runState.interpreter.getBlockNumber() - number
// block lookups must be within the past 256 blocks
if (diff > BIGINT_256 || diff <= BIGINT_0) {
runState.stack.push(BIGINT_0)
return
}

const block = await runState.blockchain.getBlock(Number(number))

runState.stack.push(bytesToBigInt(block.hash()))
}
},
],
// 0x41: COINBASE
Expand Down
14 changes: 14 additions & 0 deletions packages/vm/src/buildBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { Bloom } from './bloom/index.js'
import {
accumulateParentBeaconBlockRoot,
accumulateParentBlockHash,
calculateMinerReward,
encodeReceipt,
rewardAccount,
Expand Down Expand Up @@ -368,6 +369,19 @@ export class BlockBuilder {

await accumulateParentBeaconBlockRoot.bind(this.vm)(parentBeaconBlockRootBuf, timestampBigInt)
}
if (this.vm.common.isActivatedEIP(2935)) {
if (!this.checkpointed) {
await this.vm.evm.journal.checkpoint()
this.checkpointed = true
}

const { parentHash, number } = this.headerData
// timestamp should already be set in constructor
const numberBigInt = toType(number ?? 0, TypeOutput.BigInt)
const parentHashSanitized = toType(parentHash, TypeOutput.Uint8Array) ?? zeros(32)

await accumulateParentBlockHash.bind(this.vm)(numberBigInt, parentHashSanitized)
}
}
}

Expand Down
Loading

0 comments on commit 2fea5cb

Please sign in to comment.