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 EIP4788: Beacon block root in EVM #2810

Merged
merged 15 commits into from
Jul 10, 2023
Merged
25 changes: 25 additions & 0 deletions packages/block/src/header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class BlockHeader {
public readonly withdrawalsRoot?: Uint8Array
public readonly dataGasUsed?: bigint
public readonly excessDataGas?: bigint
public readonly beaconRoot?: Uint8Array
jochem-brouwer marked this conversation as resolved.
Show resolved Hide resolved

public readonly common: Common

Expand Down Expand Up @@ -208,6 +209,7 @@ export class BlockHeader {
withdrawalsRoot: this.common.isActivatedEIP(4895) ? KECCAK256_RLP : undefined,
dataGasUsed: this.common.isActivatedEIP(4844) ? BigInt(0) : undefined,
excessDataGas: this.common.isActivatedEIP(4844) ? BigInt(0) : undefined,
beaconRoot: this.common.isActivatedEIP(4788) ? KECCAK256_RLP : undefined,
}

const baseFeePerGas =
Expand All @@ -218,6 +220,8 @@ export class BlockHeader {
toType(headerData.dataGasUsed, TypeOutput.BigInt) ?? hardforkDefaults.dataGasUsed
const excessDataGas =
toType(headerData.excessDataGas, TypeOutput.BigInt) ?? hardforkDefaults.excessDataGas
const beaconRoot =
toType(headerData.beaconRoot, TypeOutput.Uint8Array) ?? hardforkDefaults.beaconRoot

if (!this.common.isActivatedEIP(1559) && baseFeePerGas !== undefined) {
throw new Error('A base fee for a block can only be set with EIP1559 being activated')
Expand All @@ -239,6 +243,10 @@ export class BlockHeader {
}
}

if (!this.common.isActivatedEIP(4788) && beaconRoot !== undefined) {
throw new Error('A beaconRoot for a header can only be provided with EIP4788 being activated')
}

this.parentHash = parentHash
this.uncleHash = uncleHash
this.coinbase = coinbase
Expand All @@ -258,6 +266,7 @@ export class BlockHeader {
this.withdrawalsRoot = withdrawalsRoot
this.dataGasUsed = dataGasUsed
this.excessDataGas = excessDataGas
this.beaconRoot = beaconRoot
this._genericFormatValidation()
this._validateDAOExtraData()

Expand Down Expand Up @@ -366,6 +375,19 @@ export class BlockHeader {
throw new Error(msg)
}
}

if (this.common.isActivatedEIP(4788) === true) {
if (this.beaconRoot === undefined) {
const msg = this._errorMsg('EIP4788 block has no beaconRoot field')
throw new Error(msg)
}
if (this.beaconRoot?.length !== 32) {
const msg = this._errorMsg(
`beaconRoot must be 32 bytes, received ${this.beaconRoot!.length} bytes`
)
throw new Error(msg)
}
}
}

/**
Expand Down Expand Up @@ -892,6 +914,9 @@ export class BlockHeader {
jsonDict.dataGasUsed = bigIntToHex(this.dataGasUsed!)
jsonDict.excessDataGas = bigIntToHex(this.excessDataGas!)
}
if (this.common.isActivatedEIP(4788) === true) {
jsonDict.beaconRoot = bytesToHex(this.beaconRoot!)
}
return jsonDict
}

Expand Down
4 changes: 3 additions & 1 deletion packages/block/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData {
withdrawalsRoot,
dataGasUsed,
excessDataGas,
beaconRoot,
] = values

if (values.length > 19) {
if (values.length > 20) {
throw new Error('invalid header. More values than expected were received')
}
if (values.length < 15) {
Expand All @@ -71,6 +72,7 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData {
withdrawalsRoot,
dataGasUsed,
excessDataGas,
beaconRoot,
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/block/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface HeaderData {
withdrawalsRoot?: BytesLike
dataGasUsed?: BigIntLike
excessDataGas?: BigIntLike
beaconRoot?: BytesLike
}

/**
Expand Down Expand Up @@ -158,6 +159,7 @@ export interface JsonHeader {
withdrawalsRoot?: string
dataGasUsed?: string
excessDataGas?: string
beaconRoot?: string
}

/*
Expand Down
70 changes: 70 additions & 0 deletions packages/block/test/eip4788block.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Chain, Common, Hardfork } from '@ethereumjs/common'
import { KECCAK256_RLP, bytesToHex, zeros } from '@ethereumjs/util'
import { assert, describe, it } from 'vitest'

import { BlockHeader } from '../src/header.js'
import { Block } from '../src/index.js'

describe('EIP4788 header tests', () => {
it('should work', () => {
const earlyCommon = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Istanbul })
const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Cancun, eips: [4788] })

assert.throws(
() => {
BlockHeader.fromHeaderData(
{
beaconRoot: zeros(32),
},
{
common: earlyCommon,
}
)
},
'A beaconRoot for a header can only be provided with EIP4788 being activated',
undefined,
'should throw when setting beaconRoot with EIP4788 not being activated'
)

assert.throws(
() => {
BlockHeader.fromHeaderData(
{
dataGasUsed: 1n,
},
{
common: earlyCommon,
}
)
},
'data gas used can only be provided with EIP4844 activated',
undefined,
'should throw when setting dataGasUsed with EIP4844 not being activated'
)
assert.doesNotThrow(() => {
BlockHeader.fromHeaderData(
{
excessDataGas: 0n,
dataGasUsed: 0n,
beaconRoot: zeros(32),
},
{
common,
skipConsensusFormatValidation: true,
}
)
}, 'correctly instantiates an EIP4788 block header')

const block = Block.fromBlockData(
{
header: BlockHeader.fromHeaderData({}, { common }),
},
{ common, skipConsensusFormatValidation: true }
)
assert.equal(
block.toJSON().header?.beaconRoot,
bytesToHex(KECCAK256_RLP),
'JSON output includes excessDataGas'
)
})
})
23 changes: 23 additions & 0 deletions packages/common/src/eips/4788.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "EIP-4788",
"number": 4788,
"comment": "Beacon block root in the EVM",
"url": "https://eips.ethereum.org/EIPS/eip-4788",
"status": "Draft",
"minimumHardfork": "cancun",
Copy link
Member

Choose a reason for hiding this comment

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

I know it's somewhat counter intuitive and I wonder if we ever "get this in" reliably 😋, but the minimum HF here is not the inclusion HF but St least one lower (do here Shanghai would be a good choice), otherwise this can't be used in a Shanghai+EIP way.

So this is to read: the state of the network where all preconditions are fulfilled so that this EIP can be activated.

Copy link
Member

Choose a reason for hiding this comment

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

So just to re-iterate on this and make this very concrete: this is the situation which breaks otherwise (and currently does) if the minimum HF is set to Cancun here:

➜  vm git:(master) ts-node
> import { Chain, Common, Hardfork } from '@ethereumjs/common'
undefined
> let c = new Common({ chain: Chain.Mainnet, eips: [ 4788 ] })
/ethereumjs-monorepo/packages/common/dist/cjs/common.js:361
                throw new Error(`${eip} cannot be activated on hardfork ${this.hardfork()}, minimumHardfork: ${minHF}`);
                ^

Uncaught Error: 4788 cannot be activated on hardfork shanghai, minimumHardfork: false
    at Common.setEIPs (/ethereumjs-monorepo/packages/common/src/common.ts:455:15)
    at new Common (/ethereumjs-monorepo/packages/common/src/common.ts:241:12)
    at /ethereumjs-monorepo/packages/vm/<repl>.ts:2:9
    at Script.runInThisContext (node:vm:129:12)
    at runInContext (/node/v18.14.2/lib/node_modules/ts-node/src/repl.ts:673:19)
    at Object.execCommand (/node/v18.14.2/lib/node_modules/ts-node/src/repl.ts:639:28)
    at /node/v18.14.2/lib/node_modules/ts-node/src/repl.ts:661:47
    at Array.reduce (<anonymous>)
    at appendCompileAndEvalInput (/node/v18.14.2/lib/node_modules/ts-node/src/repl.ts:661:23)
    at evalCodeInternal (/node/v18.14.2/lib/node_modules/ts-node/src/repl.ts:222:12)
>

Copy link
Member

Choose a reason for hiding this comment

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

Not sure, since this is such a re-occuring place for stumbling over it might be really worth to rename to something like the following? 🤔

minimumHardforkForIsolatedEIP

Even if the name is a bit bulky.

Copy link
Member

Choose a reason for hiding this comment

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

Will take over this discussion to the chat for a quick exchange.

Copy link
Member Author

Choose a reason for hiding this comment

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

For EL it should be Merge (instead of Shanghai), I agree here though we should review whatever the lowest hardfork should be (I should have set this to Merge)

"requiredEIPs": [],
"gasConfig": {},
"gasPrices": {
"beaconrootCost": {
"v": 4200,
"d": "Gas cost when calling the beaconroot stateful precompile"
}
},
"vm": {
"historicalRootsLength": {
"v": 98304,
"d": "The modulo parameter of the beaconroot ring buffer in the beaconroot statefull precompile"
}
},
"pow": {}
}
2 changes: 2 additions & 0 deletions packages/common/src/eips/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as eip3855 from './3855.json'
import * as eip3860 from './3860.json'
import * as eip4345 from './4345.json'
import * as eip4399 from './4399.json'
import * as eip4788 from './4788.json'
import * as eip4844 from './4844.json'
import * as eip4895 from './4895.json'
import * as eip5133 from './5133.json'
Expand Down Expand Up @@ -49,6 +50,7 @@ export const EIPs: { [key: number]: any } = {
3860: eip3860,
4345: eip4345,
4399: eip4399,
4788: eip4788,
4844: eip4844,
4895: eip4895,
5133: eip5133,
Expand Down
4 changes: 3 additions & 1 deletion packages/evm/src/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface EVMOpts {
* - [EIP-3855](https://eips.ethereum.org/EIPS/eip-3855) - PUSH0 instruction
* - [EIP-3860](https://eips.ethereum.org/EIPS/eip-3860) - Limit and meter initcode
* - [EIP-4399](https://eips.ethereum.org/EIPS/eip-4399) - Supplant DIFFICULTY opcode with PREVRANDAO (Merge)
* - [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788) - Beacon block root in the EVM (`experimental`)
* - [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) - Shard Blob Transactions (`experimental`)
* - [EIP-4895](https://eips.ethereum.org/EIPS/eip-4895) - Beacon chain push withdrawals as operations
* - [EIP-5133](https://eips.ethereum.org/EIPS/eip-5133) - Delaying Difficulty Bomb to mid-September 2022
Expand Down Expand Up @@ -273,7 +274,7 @@ export class EVM {
// Supported EIPs
const supportedEIPs = [
1153, 1559, 2315, 2537, 2565, 2718, 2929, 2930, 3074, 3198, 3529, 3540, 3541, 3607, 3651,
3670, 3855, 3860, 4399, 4895, 4844, 5133, 5656, 6780,
3670, 3855, 3860, 4399, 4788, 4895, 4844, 5133, 5656, 6780,
]

for (const eip of this.common.eips()) {
Expand Down Expand Up @@ -935,6 +936,7 @@ export class EVM {
common: this.common,
_EVM: this,
_debug: this.DEBUG ? debugPrecompiles : undefined,
stateManager: this.stateManager,
Copy link
Contributor

Choose a reason for hiding this comment

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

super 👍

Copy link
Member

Choose a reason for hiding this comment

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

Out of interest because I'm totally not getting the whole picture: what's the "super" part here? 😋 What does this do/change?

Copy link
Member

Choose a reason for hiding this comment

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

Update: just only getting this now, is this whole thing a precompile and not an opcode??

}

return code(opts)
Expand Down
76 changes: 76 additions & 0 deletions packages/evm/src/precompiles/0b-beaconroot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
Address,
bigIntToBytes,
bytesToBigInt,
setLengthLeft,
short,
zeros,
} from '@ethereumjs/util'

import { type ExecResult, OOGResult } from '../evm.js'
import { ERROR, EvmError } from '../exceptions.js'

import type { PrecompileInput } from './types.js'

const address = Address.fromString('0x000000000000000000000000000000000000000b')

export async function precompile0b(opts: PrecompileInput): Promise<ExecResult> {
const data = opts.data

const gasUsed = opts.common.param('gasPrices', 'beaconrootCost')
if (opts._debug !== undefined) {
opts._debug(
`Run BEACONROOT (0x0B) precompile data=${short(opts.data)} length=${
opts.data.length
} gasLimit=${opts.gasLimit} gasUsed=${gasUsed}`
)
}

if (opts.gasLimit < gasUsed) {
if (opts._debug !== undefined) {
opts._debug(`BEACONROOT (0x0B) failed: OOG`)
}
return OOGResult(opts.gasLimit)
}

if (data.length < 32) {
return {
returnValue: new Uint8Array(0),
executionGasUsed: gasUsed,
exceptionError: new EvmError(ERROR.INVALID_INPUT_LENGTH),
}
}

const timestampInput = bytesToBigInt(data.slice(0, 32))
const historicalRootsLength = BigInt(opts.common.param('vm', 'historicalRootsLength'))

const timestampIndex = timestampInput % historicalRootsLength
const recordedTimestamp = await opts.stateManager.getContractStorage(
address,
setLengthLeft(bigIntToBytes(timestampIndex), 32)
)

if (bytesToBigInt(recordedTimestamp) !== timestampInput) {
return {
executionGasUsed: gasUsed,
returnValue: zeros(32),
}
}
const timestampExtended = timestampIndex + historicalRootsLength
const returnData = setLengthLeft(
Copy link
Contributor

Choose a reason for hiding this comment

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

does the output of getContractStorage needs setLengthLeft?

Copy link
Member Author

Choose a reason for hiding this comment

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

Note that this writes to CALLs out memory, so we need to write 32 bytes. This is not explicit in getContractStorage;

    const value = await trie.get(key)
    if (!this._storageCacheSettings.deactivate) {
      this._storageCache?.put(address, key, value ?? hexToBytes('0x80'))
    }
    const decoded = RLP.decode(value ?? new Uint8Array(0)) as Uint8Array
    return decoded
    ```

Copy link
Contributor

Choose a reason for hiding this comment

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

ahh makes sense, thank you!

await opts.stateManager.getContractStorage(
address,
setLengthLeft(bigIntToBytes(timestampExtended), 32)
),
32
)

if (opts._debug !== undefined) {
opts._debug(`BEACONROOT (0x0B) return data=${short(returnData)}`)
}

return {
executionGasUsed: gasUsed,
returnValue: returnData,
}
}
12 changes: 10 additions & 2 deletions packages/evm/src/precompiles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { precompile07 } from './07-ecmul.js'
import { precompile08 } from './08-ecpairing.js'
import { precompile09 } from './09-blake2f.js'
import { precompile0a } from './0a-kzg-point-evaluation.js'
import { precompile0b } from './0b-beaconroot.js'
import { precompile0c } from './0c-bls12-g1add.js'
import { precompile0d } from './0d-bls12-g1mul.js'
import { precompile0e } from './0e-bls12-g1multiexp.js'
Expand Down Expand Up @@ -136,7 +137,14 @@ const precompileEntries: PrecompileEntry[] = [
},
precompile: precompile0a,
},
// 0x00..0b: beacon block root, see PR 2810
{
address: '000000000000000000000000000000000000000b',
check: {
type: PrecompileAvailabilityCheck.EIP,
param: 4788,
},
precompile: precompile0b,
},
{
address: '000000000000000000000000000000000000000c',
check: {
Expand Down Expand Up @@ -222,7 +230,7 @@ const precompiles: Precompiles = {
'0000000000000000000000000000000000000008': precompile08,
'0000000000000000000000000000000000000009': precompile09,
'000000000000000000000000000000000000000a': precompile0a,
// 0b: beacon block root see PR 2810
'000000000000000000000000000000000000000b': precompile0b,
'000000000000000000000000000000000000000c': precompile0c,
'000000000000000000000000000000000000000d': precompile0d,
'000000000000000000000000000000000000000e': precompile0e,
Expand Down
3 changes: 2 additions & 1 deletion packages/evm/src/precompiles/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { EVM, ExecResult } from '../evm.js'
import type { Common } from '@ethereumjs/common'
import type { Common, StateManagerInterface } from '@ethereumjs/common'
import type { debug } from 'debug'

export interface PrecompileFunc {
Expand All @@ -12,4 +12,5 @@ export interface PrecompileInput {
common: Common
_EVM: EVM
_debug?: debug.Debugger
stateManager: StateManagerInterface
}
Loading