Skip to content

Commit

Permalink
Implement EIP4788: Beacon block root in EVM (#2810)
Browse files Browse the repository at this point in the history
* common: add 4788

* evm: add beaconroot

* block/header: add 4788

* vm: add 4788

* common/evm/vm: 4788 spec updates

* evm: fix build [no ci]

* block: add 4788 tests

* vm: update 4788 + tests

* Update packages/block/src/header.ts [no ci]

Co-authored-by: g11tech <gajinder@g11.in>

* block/vm: update beaconRoot property

* block: fix test

* add validation in from values array

---------

Co-authored-by: g11tech <gajinder@g11.in>
  • Loading branch information
jochem-brouwer and g11tech committed Jul 10, 2023
1 parent 0747b4c commit b3cb348
Show file tree
Hide file tree
Showing 13 changed files with 452 additions and 7 deletions.
35 changes: 34 additions & 1 deletion 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 parentBeaconBlockRoot?: Uint8Array

public readonly common: Common

Expand Down Expand Up @@ -108,7 +109,7 @@ export class BlockHeader {
*/
public static fromValuesArray(values: BlockHeaderBytes, opts: BlockOptions = {}) {
const headerData = valuesArrayToHeaderData(values)
const { number, baseFeePerGas, excessDataGas, dataGasUsed } = headerData
const { number, baseFeePerGas, excessDataGas, dataGasUsed, parentBeaconBlockRoot } = headerData
const header = BlockHeader.fromHeaderData(headerData, opts)
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (header.common.isActivatedEIP(1559) && baseFeePerGas === undefined) {
Expand All @@ -126,6 +127,9 @@ export class BlockHeader {
throw new Error('invalid header. dataGasUsed should be provided')
}
}
if (header.common.isActivatedEIP(4788) && parentBeaconBlockRoot === undefined) {
throw new Error('invalid header. parentBeaconBlockRoot should be provided')
}
return header
}
/**
Expand Down Expand Up @@ -208,6 +212,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,
parentBeaconBlockRoot: this.common.isActivatedEIP(4788) ? KECCAK256_RLP : undefined,
}

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

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 +247,12 @@ export class BlockHeader {
}
}

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

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

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

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

/**
Expand Down Expand Up @@ -892,6 +922,9 @@ export class BlockHeader {
jsonDict.dataGasUsed = bigIntToHex(this.dataGasUsed!)
jsonDict.excessDataGas = bigIntToHex(this.excessDataGas!)
}
if (this.common.isActivatedEIP(4788) === true) {
jsonDict.parentBeaconBlockRoot = bytesToHex(this.parentBeaconBlockRoot!)
}
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,
parentBeaconBlockRoot,
] = 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,
parentBeaconBlockRoot,
}
}

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
parentBeaconBlockRoot?: BytesLike
}

/**
Expand Down Expand Up @@ -158,6 +159,7 @@ export interface JsonHeader {
withdrawalsRoot?: string
dataGasUsed?: string
excessDataGas?: string
parentBeaconBlockRoot?: 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(
{
parentBeaconBlockRoot: zeros(32),
},
{
common: earlyCommon,
}
)
},
'A parentBeaconBlockRoot for a header can only be provided with EIP4788 being activated',
undefined,
'should throw when setting parentBeaconBlockRoot 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,
parentBeaconBlockRoot: 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?.parentBeaconBlockRoot,
bytesToHex(KECCAK256_RLP),
'JSON output includes excessDataGas'
)
})
})
2 changes: 1 addition & 1 deletion packages/block/test/header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ describe('[Block]: Header functions', () => {
})

it('Initialization -> fromValuesArray() -> error cases', () => {
const headerArray = Array(20).fill(new Uint8Array(0))
const headerArray = Array(21).fill(new Uint8Array(0))

// mock header data (if set to zeros(0) header throws)
headerArray[0] = zeros(32) //parentHash
Expand Down
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",
"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 @@ -19,6 +19,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 @@ -47,6 +48,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 @@ -72,6 +72,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 @@ -243,7 +244,7 @@ export class EVM {
// Supported EIPs
const supportedEIPs = [
1153, 1559, 2315, 2565, 2718, 2929, 2930, 3074, 3198, 3529, 3540, 3541, 3607, 3651, 3670,
3855, 3860, 4399, 4895, 4844, 5133, 5656, 6780,
3855, 3860, 4399, 4895, 4788, 4844, 5133, 5656, 6780,
]

for (const eip of this.common.eips()) {
Expand Down Expand Up @@ -877,6 +878,7 @@ export class EVM {
common: this.common,
_EVM: this,
_debug: this.DEBUG ? debugPrecompiles : undefined,
stateManager: this.stateManager,
}

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(
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 type { PrecompileFunc, PrecompileInput } from './types.js'
import type { Common } from '@ethereumjs/common'
Expand Down Expand Up @@ -127,7 +128,14 @@ const precompileEntries: PrecompileEntry[] = [
},
precompile: precompile0a,
},
// 0x00..0b: beacon block root, see PR 2810
{
address: '000000000000000000000000000000000000000b',
check: {
type: PrecompileAvailabilityCheck.EIP,
param: 4788,
},
precompile: precompile0b,
},
]

const precompiles: Precompiles = {
Expand All @@ -141,7 +149,7 @@ const precompiles: Precompiles = {
'0000000000000000000000000000000000000008': precompile08,
'0000000000000000000000000000000000000009': precompile09,
'000000000000000000000000000000000000000a': precompile0a,
// 0b: beacon block root see PR 2810
'000000000000000000000000000000000000000b': precompile0b,
}

type DeletePrecompile = {
Expand Down
Loading

0 comments on commit b3cb348

Please sign in to comment.