Skip to content
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

Implement EIP 2935 #3268

Merged
merged 24 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4cbb9c4
common/evm/vm: draft impl eip 2935 [no ci]
jochem-brouwer Feb 3, 2024
2c6b70e
evm: excempt blockhash addr from eip158 [no ci]
jochem-brouwer Feb 9, 2024
c2d8aa2
evm: exempt historyStorageAddress eip-158
jochem-brouwer Feb 9, 2024
4e31fbe
common: add 2935 to prague
jochem-brouwer Feb 10, 2024
c172e04
evm: ensure 0 is pushed if hash of block number or higher is requeste…
jochem-brouwer Feb 10, 2024
2a8a64f
common: add eipTimestamp method
jochem-brouwer Feb 10, 2024
29f2a81
vm: update 2935 logic
jochem-brouwer Feb 10, 2024
44c0fbb
vm: add tests for 2935 (skeleton, no contents) [no ci]
jochem-brouwer Feb 10, 2024
4427858
Merge branch 'master' into eip2935
jochem-brouwer Feb 28, 2024
afabbec
common: add `customHardforks` option
jochem-brouwer Feb 28, 2024
cf673e0
delete double test
jochem-brouwer Feb 28, 2024
07b5707
common: remove 2935 from prague
jochem-brouwer Feb 28, 2024
eee0e7f
common: custom chain test fix
jochem-brouwer Feb 28, 2024
da2cde6
evm: fix 2935
jochem-brouwer Feb 28, 2024
08cf2ca
evm/vm: fix eip2935
jochem-brouwer Feb 28, 2024
63950f0
vm: add more eip 2935 tests
jochem-brouwer Feb 28, 2024
2066f08
common: clarify docs
jochem-brouwer Feb 29, 2024
fec979b
vm: remove `bind` in accumulateParentBlockHash
jochem-brouwer Feb 29, 2024
516a7be
common: eip2935 update url to point to correct commit
jochem-brouwer Feb 29, 2024
9c79a10
common: extra sanity checks on test
jochem-brouwer Feb 29, 2024
4ddd63a
vm: add docs for accumulateParentBlockHash
jochem-brouwer Feb 29, 2024
f3fca59
Merge branch 'master' into eip2935
jochem-brouwer Feb 29, 2024
f4d96c8
Merge branch 'master' into eip2935
jochem-brouwer Mar 4, 2024
f9ce54c
vm/common: address review
jochem-brouwer Mar 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
Copy link
Member

Choose a reason for hiding this comment

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

Can we add some addition like ...state (Verkle related usage, likely not stable yet) or so?

(let me know if the comment does not make sense "as is")

Copy link
Member Author

Choose a reason for hiding this comment

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

It does, I normally just copy the EIP title but you are right this definitely should get an unstable warning.

Copy link
Member

Choose a reason for hiding this comment

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

👍

Take whatever text you like, just that people know this is not "one of the normal EIPs they can integrate" (and DRAFT status is not that much of a warning, even EIPs close to hardfork often still in DRAFT 😂 ).

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', () => {
jochem-brouwer marked this conversation as resolved.
Show resolved Hide resolved
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 {
Address,
RIPEMD160_ADDRESS_STRING,
bigIntToHex,
bytesToHex,
bytesToUnprefixedHex,
stripHexPrefix,
Expand Down Expand Up @@ -198,6 +199,13 @@
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
}
}

Check warning on line 208 in packages/evm/src/journal.ts

View check run for this annotation

Codecov / codecov/patch

packages/evm/src/journal.ts#L202-L208

Added lines #L202 - L208 were not covered by tests
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 @@
SECP256K1_ORDER_DIV_2,
TWO_POW256,
bigIntToBytes,
bigIntToHex,
bytesToBigInt,
bytesToHex,
concatBytes,
Expand Down Expand Up @@ -593,19 +594,35 @@
// 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
}

Check warning on line 604 in packages/evm/src/opcodes/functions.ts

View check run for this annotation

Codecov / codecov/patch

packages/evm/src/opcodes/functions.ts#L600-L604

Added lines #L600 - L604 were not covered by tests

const block = await runState.blockchain.getBlock(Number(number))
const historyAddress = Address.fromString(
jochem-brouwer marked this conversation as resolved.
Show resolved Hide resolved
bigIntToHex(common.param('vm', 'historyStorageAddress'))
)
const key = setLengthLeft(bigIntToBytes(number), 32)

Check warning on line 609 in packages/evm/src/opcodes/functions.ts

View check run for this annotation

Codecov / codecov/patch

packages/evm/src/opcodes/functions.ts#L606-L609

Added lines #L606 - L609 were not covered by tests

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()))
}

Check warning on line 625 in packages/evm/src/opcodes/functions.ts

View check run for this annotation

Codecov / codecov/patch

packages/evm/src/opcodes/functions.ts#L611-L625

Added lines #L611 - L625 were not covered by tests
},
],
// 0x41: COINBASE
Expand Down
54 changes: 54 additions & 0 deletions packages/vm/src/runBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
Account,
Address,
BIGINT_0,
BIGINT_1,
BIGINT_8,
GWEI_TO_WEI,
KECCAK256_RLP,
bigIntToBytes,
bigIntToHex,
bytesToHex,
concatBytes,
equalsBytes,
Expand Down Expand Up @@ -350,6 +352,13 @@
block.header.timestamp
)
}
if (this.common.isActivatedEIP(2935)) {
if (this.DEBUG) {
debug(`accumulate parentBlockHash `)
}

await accumulateParentBlockHash.bind(this)(block)
}

if (enableProfiler) {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -407,6 +416,51 @@
return blockResults
}

/**
* This method runs the logic of EIP 2935 (save blockhashes to state)
* It will put the `parentHash` of the block to the storage slot of `block.number - 1` of the history storage contract.
* This contract is used to retrieve BLOCKHASHes in EVM if EIP 2935 is activated.
* In case that the previous block of `block` is pre-EIP-2935 (so we are on the EIP 2935 fork block), additionally
* also add the currently available past blockhashes which are available by BLOCKHASH (so, the past 256 block hashes)
* @param this The VM to run on
* @param block The current block to save the parent block hash of
*/
export async function accumulateParentBlockHash(this: VM, block: Block) {
jochem-brouwer marked this conversation as resolved.
Show resolved Hide resolved
const historyAddress = Address.fromString(
bigIntToHex(this.common.param('vm', 'historyStorageAddress'))
)

if ((await this.stateManager.getAccount(historyAddress)) === undefined) {
await this.evm.journal.putAccount(historyAddress, new Account())
}

async function putBlockHash(vm: VM, hash: Uint8Array, number: bigint) {
const key = setLengthLeft(bigIntToBytes(number), 32)
await vm.stateManager.putContractStorage(historyAddress, key, hash)
}
await putBlockHash(this, block.header.parentHash, block.header.number - BIGINT_1)

// Check if we are on the fork block
const forkTime = this.common.eipTimestamp(2935)
if (forkTime === null) {
throw new Error('EIP 2935 should be activated by timestamp')
}

Check warning on line 447 in packages/vm/src/runBlock.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/runBlock.ts#L446-L447

Added lines #L446 - L447 were not covered by tests
Copy link
Member

Choose a reason for hiding this comment

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

I don't think that we need this extra eipTimestamp() method. Isn't this the normal isActivatedEIP() call??

Also: such a check should likely be at the beginning of the method (?).

Copy link
Member Author

Choose a reason for hiding this comment

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

I have added this because I need to detect we are on the first fork block. The reason is that on the first fork block, we have to store all the MIN_HISTORY_SERVE_WINDOW hashes. I don't know if there is another way to detect if we are on the fork block? I think we need to fork timestamp and then the exact condition if (parentBlock.header.timestamp < forkTime) { (this only happens once and is on the fork block)

Copy link
Member

Choose a reason for hiding this comment

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

Ah, you are right, sorry, please dismiss! 🙂

Should then the whole method get a check at the beginning if 2935 is activated and throw otherwise?

Copy link
Member Author

Choose a reason for hiding this comment

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

I am comparing it with accumulateParentBeaconBlockRoot, this also does not have a check for common.isActivatedEIP(4788), but for consistency I can add (but then I will add it to both methods)

Copy link
Member

Choose a reason for hiding this comment

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

Yes, maybe do (for both).

const parentBlock = await this.blockchain.getBlock(block.header.parentHash)

if (parentBlock.header.timestamp < forkTime) {
let ancestor = parentBlock
const range = this.common.param('vm', 'minHistoryServeWindow')
for (let i = 0; i < Number(range) - 1; i++) {
Copy link
Member

Choose a reason for hiding this comment

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

I would cautiously think that this < usage together with - 1 might produce an "off by one" error?

Since the code from the EIP states for i in range(MIN_HISTORY_SERVE_WINDOW - 1)?

(maybe confirm with edge case tests?)

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this is correct (but this is definitely off-by-one error prone! 😄 )

From EIP: for i in range(MIN_HISTORY_SERVE_WINDOW - 1): this is python code. This loops the for loop MIN_HISTORY_SERVE_WINDOW - 1 time. Note that MIN_HISTORY_SERVE_WINDOW is 256 so we loop this 255 times. In the current TS code we loop i < 256 - 1 = 255 times also. Note that for each loop, we store the ancestor block. Also note that previously we have already stored the parent block hash of the current block. So, in total we have stored 256 blocks and therefore the history contract has access to 256 slots.

Copy link
Member

Choose a reason for hiding this comment

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

👍

if (ancestor.header.number === BIGINT_0) {
break
}

ancestor = await this.blockchain.getBlock(ancestor.header.parentHash)
await putBlockHash(this, ancestor.hash(), ancestor.header.number)
}
}
}

export async function accumulateParentBeaconBlockRoot(
this: VM,
root: Uint8Array,
Expand Down
Loading
Loading