From 9e4b39eff3e9e6767af5483478941ada7dc0703d Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Thu, 22 Jun 2023 16:22:49 +0200 Subject: [PATCH 01/12] common: add 4788 --- packages/common/src/eips/4788.json | 18 ++++++++++++++++++ packages/common/src/eips/index.ts | 2 ++ 2 files changed, 20 insertions(+) create mode 100644 packages/common/src/eips/4788.json diff --git a/packages/common/src/eips/4788.json b/packages/common/src/eips/4788.json new file mode 100644 index 0000000000..79678eea0a --- /dev/null +++ b/packages/common/src/eips/4788.json @@ -0,0 +1,18 @@ +{ + "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": 2100, + "d": "Gas cost when calling the beaconroot stateful precompile" + } + }, + "vm": {}, + "pow": {} +} diff --git a/packages/common/src/eips/index.ts b/packages/common/src/eips/index.ts index e7ac399cb4..78d3745ab7 100644 --- a/packages/common/src/eips/index.ts +++ b/packages/common/src/eips/index.ts @@ -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' @@ -48,6 +49,7 @@ export const EIPs: { [key: number]: any } = { 3860: eip3860, 4345: eip4345, 4399: eip4399, + 4788: eip4788, 4844: eip4844, 4895: eip4895, 5133: eip5133, From d36ab9d6bed3ed936b4a2c196183326226e30886 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Thu, 22 Jun 2023 16:26:38 +0200 Subject: [PATCH 02/12] evm: add beaconroot --- packages/evm/src/evm.ts | 4 +- packages/evm/src/precompiles/0b-beaconroot.ts | 38 +++++++++++++++++++ packages/evm/src/precompiles/index.ts | 32 +++++++++++++++- packages/evm/src/precompiles/types.ts | 3 +- 4 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 packages/evm/src/precompiles/0b-beaconroot.ts diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index d3207a6112..79b4dd543f 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -84,6 +84,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 @@ -286,7 +287,7 @@ export class EVM implements EVMInterface { // 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, 6780, + 3670, 3855, 3860, 4399, 4788, 4895, 4844, 5133, 6780, ] for (const eip of this._common.eips()) { @@ -948,6 +949,7 @@ export class EVM implements EVMInterface { _common: this._common, _EVM: this, _debug: this.DEBUG ? debugPrecompiles : undefined, + stateManager: this.stateManager, } return code(opts) diff --git a/packages/evm/src/precompiles/0b-beaconroot.ts b/packages/evm/src/precompiles/0b-beaconroot.ts new file mode 100644 index 0000000000..6d697a6791 --- /dev/null +++ b/packages/evm/src/precompiles/0b-beaconroot.ts @@ -0,0 +1,38 @@ +import { Address, short } from '@ethereumjs/util' + +import { type ExecResult, OOGResult } from '../evm.js' + +import type { PrecompileInput } from './types.js' + +const address = Address.fromString('0x000000000000000000000000000000000000000b') + +export async function precompile0B_Beaconroot(opts: PrecompileInput): Promise { + 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) + } + + const returnData = await opts.stateManager.getContractStorage(address, opts.data) + + if (opts._debug !== undefined) { + opts._debug(`BEACONROOT (0x0B) return data=${short(returnData)}`) + } + + return { + executionGasUsed: gasUsed, + returnValue: returnData, + } +} diff --git a/packages/evm/src/precompiles/index.ts b/packages/evm/src/precompiles/index.ts index 7695dd9d89..a8e69bd383 100644 --- a/packages/evm/src/precompiles/index.ts +++ b/packages/evm/src/precompiles/index.ts @@ -12,6 +12,7 @@ import { precompile07 } from './07-ecmul.js' import { precompile08 } from './08-ecpairing.js' import { precompile09 } from './09-blake2f.js' import { precompile0a } from './0a-bls12-g1add.js' +import { precompile0B_Beaconroot } from './0b-beaconroot.js' import { precompile0b } from './0b-bls12-g1mul.js' import { precompile0c } from './0c-bls12-g1multiexp.js' import { precompile0d } from './0d-bls12-g2add.js' @@ -73,6 +74,30 @@ const precompiles: Precompiles = { '0000000000000000000000000000000000000011': precompile11, '0000000000000000000000000000000000000012': precompile12, '0000000000000000000000000000000000000014': precompile14, + '000000000000000000000000000000000000000b-beaconroot': precompile0B_Beaconroot, +} + +const addressMap: { [key: string]: string } = { + '0000000000000000000000000000000000000001': '0000000000000000000000000000000000000001', + '0000000000000000000000000000000000000002': '0000000000000000000000000000000000000002', + [ripemdPrecompileAddress]: ripemdPrecompileAddress, + '0000000000000000000000000000000000000004': '0000000000000000000000000000000000000004', + '0000000000000000000000000000000000000005': '0000000000000000000000000000000000000005', + '0000000000000000000000000000000000000006': '0000000000000000000000000000000000000006', + '0000000000000000000000000000000000000007': '0000000000000000000000000000000000000007', + '0000000000000000000000000000000000000008': '0000000000000000000000000000000000000008', + '0000000000000000000000000000000000000009': '0000000000000000000000000000000000000009', + '000000000000000000000000000000000000000a': '000000000000000000000000000000000000000a', + '000000000000000000000000000000000000000b': '000000000000000000000000000000000000000b', + '000000000000000000000000000000000000000c': '000000000000000000000000000000000000000c', + '000000000000000000000000000000000000000d': '000000000000000000000000000000000000000d', + '000000000000000000000000000000000000000e': '000000000000000000000000000000000000000e', + '000000000000000000000000000000000000000f': '000000000000000000000000000000000000000f', + '0000000000000000000000000000000000000010': '0000000000000000000000000000000000000010', + '0000000000000000000000000000000000000011': '0000000000000000000000000000000000000011', + '0000000000000000000000000000000000000012': '0000000000000000000000000000000000000012', + '0000000000000000000000000000000000000014': '0000000000000000000000000000000000000014', + '000000000000000000000000000000000000000b-beaconroot': '000000000000000000000000000000000000000b', } const precompileAvailability: PrecompileAvailability = { @@ -152,6 +177,10 @@ const precompileAvailability: PrecompileAvailability = { type: PrecompileAvailabilityCheck.EIP, param: 4844, }, + '000000000000000000000000000000000000000b-beaconroot': { + type: PrecompileAvailabilityCheck.EIP, + param: 4788, + }, } function getPrecompile(address: Address, common: Common): PrecompileFunc { @@ -194,7 +223,8 @@ function getActivePrecompiles( ) } } - for (const addressString in precompiles) { + for (const tag in precompiles) { + const addressString = addressMap[tag] if (precompileMap.has(addressString)) { continue } diff --git a/packages/evm/src/precompiles/types.ts b/packages/evm/src/precompiles/types.ts index 10da289f4b..2226f406f5 100644 --- a/packages/evm/src/precompiles/types.ts +++ b/packages/evm/src/precompiles/types.ts @@ -1,6 +1,6 @@ import type { ExecResult } from '../evm.js' import type { EVMInterface } from '../types.js' -import type { Common } from '@ethereumjs/common' +import type { Common, StateManagerInterface } from '@ethereumjs/common' import type { debug } from 'debug' export interface PrecompileFunc { @@ -13,4 +13,5 @@ export interface PrecompileInput { _common: Common _EVM: EVMInterface _debug?: debug.Debugger + stateManager: StateManagerInterface } From d3331435eed5b88120bfcb2aae0319f94f54e92b Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Thu, 22 Jun 2023 18:07:29 +0200 Subject: [PATCH 03/12] block/header: add 4788 --- packages/block/src/header.ts | 22 ++++++++++++++++++++++ packages/block/src/helpers.ts | 4 +++- packages/block/src/types.ts | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index f46d5d858b..bdda85c890 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -58,6 +58,7 @@ export class BlockHeader { public readonly withdrawalsRoot?: Uint8Array public readonly dataGasUsed?: bigint public readonly excessDataGas?: bigint + public readonly beaconRoot?: Uint8Array public readonly _common: Common @@ -201,6 +202,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 = @@ -211,6 +213,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') @@ -232,6 +236,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 @@ -251,6 +259,7 @@ export class BlockHeader { this.withdrawalsRoot = withdrawalsRoot this.dataGasUsed = dataGasUsed this.excessDataGas = excessDataGas + this.beaconRoot = this.beaconRoot this._genericFormatValidation() this._validateDAOExtraData() @@ -359,6 +368,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) + } + } } /** diff --git a/packages/block/src/helpers.ts b/packages/block/src/helpers.ts index 022bac6f26..286e98fb9d 100644 --- a/packages/block/src/helpers.ts +++ b/packages/block/src/helpers.ts @@ -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) { @@ -71,6 +72,7 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData { withdrawalsRoot, dataGasUsed, excessDataGas, + beaconRoot, } } diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 655299383a..8ecf836865 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -93,6 +93,7 @@ export interface HeaderData { withdrawalsRoot?: BytesLike dataGasUsed?: BigIntLike excessDataGas?: BigIntLike + beaconRoot?: BytesLike } /** From 6e3c65010bec9c63f35325c1356e1df83e867e7c Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Thu, 22 Jun 2023 18:12:03 +0200 Subject: [PATCH 04/12] vm: add 4788 --- packages/vm/src/runBlock.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 937bbf1941..9de6cb9f64 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -37,6 +37,8 @@ const debug = createDebugLogger('vm:block') const DAOAccountList = DAOConfig.DAOAccounts const DAORefundContract = DAOConfig.DAORefundContract +const beaconRootAddress = Address.fromString('0x000000000000000000000000000000000000000b') + /** * @ignore */ @@ -253,6 +255,15 @@ async function applyBlock(this: VM, block: Block, opts: RunBlockOpts) { await block.validateData() } } + if (this._common.isActivatedEIP(4788)) { + // Apply beacon root + const root = block.header.beaconRoot! + await this.stateManager.putContractStorage( + beaconRootAddress, + root, + bigIntToBytes(block.header.timestamp) + ) + } // Apply transactions if (this.DEBUG) { debug(`Apply transactions`) From 7c7f2b1ea0990ad3f200c6c769617ff0bbaed85b Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Sun, 25 Jun 2023 15:21:13 +0200 Subject: [PATCH 05/12] common/evm/vm: 4788 spec updates --- packages/common/src/eips/4788.json | 9 +++- packages/evm/src/precompiles/0b-beaconroot.ts | 42 ++++++++++++++++++- packages/vm/src/runBlock.ts | 14 ++++++- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/packages/common/src/eips/4788.json b/packages/common/src/eips/4788.json index 79678eea0a..b3bc50376c 100644 --- a/packages/common/src/eips/4788.json +++ b/packages/common/src/eips/4788.json @@ -9,10 +9,15 @@ "gasConfig": {}, "gasPrices": { "beaconrootCost": { - "v": 2100, + "v": 4200, "d": "Gas cost when calling the beaconroot stateful precompile" } }, - "vm": {}, + "vm": { + "historicalRootsLength": { + "v": 98304, + "d": "The modulo parameter of the beaconroot ring buffer in the beaconroot statefull precompile" + } + }, "pow": {} } diff --git a/packages/evm/src/precompiles/0b-beaconroot.ts b/packages/evm/src/precompiles/0b-beaconroot.ts index 6d697a6791..2ee9b9fe55 100644 --- a/packages/evm/src/precompiles/0b-beaconroot.ts +++ b/packages/evm/src/precompiles/0b-beaconroot.ts @@ -1,6 +1,14 @@ -import { Address, short } from '@ethereumjs/util' +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' @@ -25,7 +33,37 @@ export async function precompile0B_Beaconroot(opts: PrecompileInput): Promise Date: Sun, 25 Jun 2023 15:27:16 +0200 Subject: [PATCH 06/12] evm: fix build [no ci] --- packages/evm/src/precompiles/0b-beaconroot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/evm/src/precompiles/0b-beaconroot.ts b/packages/evm/src/precompiles/0b-beaconroot.ts index 2ee9b9fe55..f3e5aa45e6 100644 --- a/packages/evm/src/precompiles/0b-beaconroot.ts +++ b/packages/evm/src/precompiles/0b-beaconroot.ts @@ -14,7 +14,7 @@ import type { PrecompileInput } from './types.js' const address = Address.fromString('0x000000000000000000000000000000000000000b') -export async function precompile0B_Beaconroot(opts: PrecompileInput): Promise { +export async function precompile0b(opts: PrecompileInput): Promise { const data = opts.data const gasUsed = opts._common.param('gasPrices', 'beaconrootCost') From 01c35bb896dd847ca72fd805ba50254d9bc71339 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Sun, 25 Jun 2023 15:57:03 +0200 Subject: [PATCH 07/12] block: add 4788 tests --- packages/block/src/header.ts | 5 +- packages/block/src/types.ts | 1 + packages/block/test/eip4788block.spec.ts | 70 ++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 packages/block/test/eip4788block.spec.ts diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 844dad31ea..e90c8d826a 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -268,7 +268,7 @@ export class BlockHeader { this.withdrawalsRoot = withdrawalsRoot this.dataGasUsed = dataGasUsed this.excessDataGas = excessDataGas - this.beaconRoot = this.beaconRoot + this.beaconRoot = beaconRoot this._genericFormatValidation() this._validateDAOExtraData() @@ -916,6 +916,9 @@ export class BlockHeader { jsonDict.dataGasUsed = bigIntToHex(this.dataGasUsed!) jsonDict.excessDataGas = bigIntToHex(this.excessDataGas!) } + if (this._common.isActivatedEIP(4788) === true) { + jsonDict.beaconRoot = bytesToPrefixedHexString(this.beaconRoot!) + } return jsonDict } diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 8ecf836865..ec1bab3867 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -158,6 +158,7 @@ export interface JsonHeader { withdrawalsRoot?: string dataGasUsed?: string excessDataGas?: string + beaconRoot?: string } /* diff --git a/packages/block/test/eip4788block.spec.ts b/packages/block/test/eip4788block.spec.ts new file mode 100644 index 0000000000..896e0f2261 --- /dev/null +++ b/packages/block/test/eip4788block.spec.ts @@ -0,0 +1,70 @@ +import { Chain, Common, Hardfork } from '@ethereumjs/common' +import { KECCAK256_RLP, bytesToPrefixedHexString, 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, + bytesToPrefixedHexString(KECCAK256_RLP), + 'JSON output includes excessDataGas' + ) + }) +}) From de8ffd52a5ff753c60ad6910dab991d3275a4cfc Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Sun, 25 Jun 2023 17:41:14 +0200 Subject: [PATCH 08/12] vm: update 4788 + tests --- packages/vm/src/runBlock.ts | 12 ++ .../test/api/EIPs/eip-4788-beaconroot.spec.ts | 191 ++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 packages/vm/test/api/EIPs/eip-4788-beaconroot.spec.ts diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 5f7ba8fbfe..32606f6baa 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -263,6 +263,18 @@ async function applyBlock(this: VM, block: Block, opts: RunBlockOpts) { const historicalRootsLength = BigInt(this._common.param('vm', 'historicalRootsLength')) const timestampIndex = timestamp % historicalRootsLength const timestampExtended = timestampIndex + historicalRootsLength + + /** + * Note: (by Jochem) + * If we don't do this (put account if undefined / non-existant), block runner crashes because the beacon root address does not exist + * This is hence (for me) again a reason why it should /not/ throw if the address does not exist + * All ethereum accounts have empty storage by default + */ + + if ((await this.stateManager.getAccount(beaconRootAddress)) === undefined) { + await this.stateManager.putAccount(beaconRootAddress, new Account()) + } + await this.stateManager.putContractStorage( beaconRootAddress, setLengthLeft(bigIntToBytes(timestampIndex), 32), diff --git a/packages/vm/test/api/EIPs/eip-4788-beaconroot.spec.ts b/packages/vm/test/api/EIPs/eip-4788-beaconroot.spec.ts new file mode 100644 index 0000000000..225b6e3d36 --- /dev/null +++ b/packages/vm/test/api/EIPs/eip-4788-beaconroot.spec.ts @@ -0,0 +1,191 @@ +/** + * EIP 4788 beaconroot specs + * + * Test cases: + * - Beaconroot precompile call, timestamp matches + * - Beaconroot precompile call, timestamp does not match + * - Beaconroot precompile call, timestamp matches: + * - Input length > 32 bytes + * - Input length < 32 bytes (reverts) + */ + +import { Block, BlockHeader } from '@ethereumjs/block' +import { Chain, Common, Hardfork } from '@ethereumjs/common' +import { TransactionFactory } from '@ethereumjs/tx' +import { + Address, + bigIntToBytes, + bytesToBigInt, + hexStringToBytes, + setLengthLeft, + setLengthRight, + zeros, +} from '@ethereumjs/util' +import { assert, describe, it } from 'vitest' + +import { VM } from '../../../src' + +import type { TransactionType, TxData } from '@ethereumjs/tx' +import type { BigIntLike } from '@ethereumjs/util' + +const common = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.Cancun, + eips: [4788], +}) + +const pkey = hexStringToBytes('20'.repeat(32)) +const contractAddress = Address.fromString('0x' + 'c0de'.repeat(10)) + +function beaconrootBlock( + blockroot: bigint, + timestamp: BigIntLike, + transactions: Array +) { + const newTxData = [] + + for (const txData of transactions) { + const tx = TransactionFactory.fromTxData({ + gasPrice: 7, + gasLimit: 100000, + ...txData, + type: 0, + to: contractAddress, + }) + newTxData.push(tx.sign(pkey)) + } + + const root = setLengthLeft(bigIntToBytes(blockroot), 32) + const header = BlockHeader.fromHeaderData( + { + beaconRoot: root, + timestamp, + }, + { common, freeze: false } + ) + const block = Block.fromBlockData( + { + header, + transactions: newTxData, + }, + { + common, + freeze: false, + } + ) + return block +} + +/** + * This code: + * CALLDATACOPYs the calldata into memory + * CALLS with this calldata into 0x0B (beaconroot precompile) + * Stores the CALL-return field (either 0 or 1 depending if it reverts or not) at storage slot 0 + * Then it returns the data the precompile returns + */ + +const CODE = '0x365F5F375F5F365F5F600B5AF15F553D5F5F3E3D5FF3' + +/** + * Run a block inside a 4788 VM + * @param block Block to run + * @returns Two fields: block return status, and callStatus (field saved in the contract) + */ +async function runBlock(block: Block) { + const vm = await VM.create({ + common, + }) + + await vm.stateManager.putContractCode(contractAddress, hexStringToBytes(CODE)) + return { + vmResult: await vm.runBlock({ + block, + skipBalance: true, + skipBlockValidation: true, + generate: true, + }), + callStatus: await getCallStatus(vm), + } +} + +/** + * Get call status saved in the contract + */ +async function getCallStatus(vm: VM) { + const stat = await vm.stateManager.getContractStorage(contractAddress, zeros(32)) + return bytesToBigInt(stat) +} + +/** + * Run block test + * @param input + */ +async function runBlockTest(input: { + timestamp: bigint // Timestamp as input to our contract which calls into the precompile + timestampBlock: bigint // Timestamp of the block (this is saved in the precompile) + blockRoot: bigint // Blockroot of the block (also saved in the precompile) + extLeft?: number // Extend length left of the input (defaults to 32) + extRight?: number // Extend lenght right of the input (defaults to 32) - happens after extendLeft + expRet: bigint // Expected return value + expCallStatus: bigint // Expected call status (either 0 or 1) +}) { + const { timestamp, blockRoot, timestampBlock, expRet, expCallStatus } = input + + const data = setLengthRight( + setLengthLeft(bigIntToBytes(timestamp), input.extLeft ?? 32), + input.extRight ?? 32 + ) + const block = beaconrootBlock(blockRoot, timestampBlock, [ + { + data, + }, + ]) + + const ret = await runBlock(block) + const bigIntReturn = bytesToBigInt(ret.vmResult.results[0].execResult.returnValue) + assert.equal(bigIntReturn, expRet, 'blockRoot ok') + assert.equal(ret.callStatus, expCallStatus, 'call status ok') +} + +describe('should run beaconroot precompile correctly', async () => { + it('should run precompile with known timestamp', async () => { + await runBlockTest({ + timestamp: BigInt(12), + timestampBlock: BigInt(12), + blockRoot: BigInt(1), + expRet: BigInt(1), + expCallStatus: BigInt(1), + }) + }) + it('should run precompile with unknown timestamp', async () => { + await runBlockTest({ + timestamp: BigInt(12), + timestampBlock: BigInt(11), + blockRoot: BigInt(1), + expRet: BigInt(0), + expCallStatus: BigInt(1), + }) + }) + it('should run precompile with known timestamp, input length > 32 bytes', async () => { + await runBlockTest({ + timestamp: BigInt(12), + timestampBlock: BigInt(12), + blockRoot: BigInt(1), + extLeft: 32, + extRight: 320, + expRet: BigInt(1), + expCallStatus: BigInt(1), + }) + }) + it('should run precompile with known timestamp, input length < 32 bytes', async () => { + await runBlockTest({ + timestamp: BigInt(12), + timestampBlock: BigInt(12), + blockRoot: BigInt(1), + extLeft: 31, + extRight: 31, + expRet: BigInt(0), + expCallStatus: BigInt(0), + }) + }) +}) From 4a0c19a8d22d6f233be444d1fa7bf59172dfd52e Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 10 Jul 2023 16:11:43 +0200 Subject: [PATCH 09/12] Update packages/block/src/header.ts [no ci] Co-authored-by: g11tech --- packages/block/src/header.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 4fb33515da..5306714ec9 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -56,7 +56,7 @@ export class BlockHeader { public readonly withdrawalsRoot?: Uint8Array public readonly dataGasUsed?: bigint public readonly excessDataGas?: bigint - public readonly beaconRoot?: Uint8Array + public readonly parentBeaconBlockRoot?: Uint8Array public readonly common: Common From aba0117a4f3e4fd2b5560b16a03e7bebce5d4417 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 10 Jul 2023 16:16:24 +0200 Subject: [PATCH 10/12] block/vm: update beaconRoot property --- packages/block/src/header.ts | 27 +++++++++++-------- packages/block/src/helpers.ts | 4 +-- packages/block/src/types.ts | 4 +-- packages/block/test/eip4788block.spec.ts | 10 +++---- packages/vm/src/runBlock.ts | 16 ++++++----- .../test/api/EIPs/eip-4788-beaconroot.spec.ts | 2 +- 6 files changed, 35 insertions(+), 28 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 5306714ec9..a37ec906c7 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -209,7 +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, + parentBeaconBlockRoot: this.common.isActivatedEIP(4788) ? KECCAK256_RLP : undefined, } const baseFeePerGas = @@ -220,8 +220,9 @@ 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 + 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') @@ -243,8 +244,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') + 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 @@ -266,7 +269,7 @@ export class BlockHeader { this.withdrawalsRoot = withdrawalsRoot this.dataGasUsed = dataGasUsed this.excessDataGas = excessDataGas - this.beaconRoot = beaconRoot + this.parentBeaconBlockRoot = parentBeaconBlockRoot this._genericFormatValidation() this._validateDAOExtraData() @@ -377,13 +380,15 @@ export class BlockHeader { } if (this.common.isActivatedEIP(4788) === true) { - if (this.beaconRoot === undefined) { - const msg = this._errorMsg('EIP4788 block has no beaconRoot field') + if (this.parentBeaconBlockRoot === undefined) { + const msg = this._errorMsg('EIP4788 block has no parentBeaconBlockRoot field') throw new Error(msg) } - if (this.beaconRoot?.length !== 32) { + if (this.parentBeaconBlockRoot?.length !== 32) { const msg = this._errorMsg( - `beaconRoot must be 32 bytes, received ${this.beaconRoot!.length} bytes` + `parentBeaconBlockRoot must be 32 bytes, received ${ + this.parentBeaconBlockRoot!.length + } bytes` ) throw new Error(msg) } @@ -915,7 +920,7 @@ export class BlockHeader { jsonDict.excessDataGas = bigIntToHex(this.excessDataGas!) } if (this.common.isActivatedEIP(4788) === true) { - jsonDict.beaconRoot = bytesToHex(this.beaconRoot!) + jsonDict.parentBeaconBlockRoot = bytesToHex(this.parentBeaconBlockRoot!) } return jsonDict } diff --git a/packages/block/src/helpers.ts b/packages/block/src/helpers.ts index 286e98fb9d..1856846cce 100644 --- a/packages/block/src/helpers.ts +++ b/packages/block/src/helpers.ts @@ -42,7 +42,7 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData { withdrawalsRoot, dataGasUsed, excessDataGas, - beaconRoot, + parentBeaconBlockRoot, ] = values if (values.length > 20) { @@ -72,7 +72,7 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData { withdrawalsRoot, dataGasUsed, excessDataGas, - beaconRoot, + parentBeaconBlockRoot, } } diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 4ef02c7901..9e5628dcfe 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -94,7 +94,7 @@ export interface HeaderData { withdrawalsRoot?: BytesLike dataGasUsed?: BigIntLike excessDataGas?: BigIntLike - beaconRoot?: BytesLike + parentBeaconBlockRoot?: BytesLike } /** @@ -159,7 +159,7 @@ export interface JsonHeader { withdrawalsRoot?: string dataGasUsed?: string excessDataGas?: string - beaconRoot?: string + parentBeaconBlockRoot?: string } /* diff --git a/packages/block/test/eip4788block.spec.ts b/packages/block/test/eip4788block.spec.ts index 39fc607863..d49733b2dc 100644 --- a/packages/block/test/eip4788block.spec.ts +++ b/packages/block/test/eip4788block.spec.ts @@ -14,16 +14,16 @@ describe('EIP4788 header tests', () => { () => { BlockHeader.fromHeaderData( { - beaconRoot: zeros(32), + parentBeaconBlockRoot: zeros(32), }, { common: earlyCommon, } ) }, - 'A beaconRoot for a header can only be provided with EIP4788 being activated', + 'A parentBeaconBlockRoot for a header can only be provided with EIP4788 being activated', undefined, - 'should throw when setting beaconRoot with EIP4788 not being activated' + 'should throw when setting parentBeaconBlockRoot with EIP4788 not being activated' ) assert.throws( @@ -46,7 +46,7 @@ describe('EIP4788 header tests', () => { { excessDataGas: 0n, dataGasUsed: 0n, - beaconRoot: zeros(32), + parentBeaconBlockRoot: zeros(32), }, { common, @@ -62,7 +62,7 @@ describe('EIP4788 header tests', () => { { common, skipConsensusFormatValidation: true } ) assert.equal( - block.toJSON().header?.beaconRoot, + block.toJSON().header?.parentBeaconBlockRoot, bytesToHex(KECCAK256_RLP), 'JSON output includes excessDataGas' ) diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 018b5d1a4a..20dc841bac 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -40,7 +40,9 @@ const debug = createDebugLogger('vm:block') const DAOAccountList = DAOConfig.DAOAccounts const DAORefundContract = DAOConfig.DAORefundContract -const beaconRootAddress = Address.fromString('0x000000000000000000000000000000000000000b') +const parentBeaconBlockRootAddress = Address.fromString( + '0x000000000000000000000000000000000000000b' +) /** * @ignore @@ -259,8 +261,8 @@ async function applyBlock(this: VM, block: Block, opts: RunBlockOpts) { } } if (this.common.isActivatedEIP(4788)) { - // Save the beaconRoot to the beaconroot stateful precompile ring buffers - const root = block.header.beaconRoot! + // Save the parentBeaconBlockRoot to the beaconroot stateful precompile ring buffers + const root = block.header.parentBeaconBlockRoot! const timestamp = block.header.timestamp const historicalRootsLength = BigInt(this.common.param('vm', 'historicalRootsLength')) const timestampIndex = timestamp % historicalRootsLength @@ -273,17 +275,17 @@ async function applyBlock(this: VM, block: Block, opts: RunBlockOpts) { * All ethereum accounts have empty storage by default */ - if ((await this.stateManager.getAccount(beaconRootAddress)) === undefined) { - await this.stateManager.putAccount(beaconRootAddress, new Account()) + if ((await this.stateManager.getAccount(parentBeaconBlockRootAddress)) === undefined) { + await this.stateManager.putAccount(parentBeaconBlockRootAddress, new Account()) } await this.stateManager.putContractStorage( - beaconRootAddress, + parentBeaconBlockRootAddress, setLengthLeft(bigIntToBytes(timestampIndex), 32), bigIntToBytes(block.header.timestamp) ) await this.stateManager.putContractStorage( - beaconRootAddress, + parentBeaconBlockRootAddress, setLengthLeft(bigIntToBytes(timestampExtended), 32), root ) diff --git a/packages/vm/test/api/EIPs/eip-4788-beaconroot.spec.ts b/packages/vm/test/api/EIPs/eip-4788-beaconroot.spec.ts index 708eb21de4..2fbf495c66 100644 --- a/packages/vm/test/api/EIPs/eip-4788-beaconroot.spec.ts +++ b/packages/vm/test/api/EIPs/eip-4788-beaconroot.spec.ts @@ -58,7 +58,7 @@ function beaconrootBlock( const root = setLengthLeft(bigIntToBytes(blockroot), 32) const header = BlockHeader.fromHeaderData( { - beaconRoot: root, + parentBeaconBlockRoot: root, timestamp, }, { common, freeze: false } From ea1d15ffa019fd4ed82b887f3b7defc012838105 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Mon, 10 Jul 2023 17:40:13 +0200 Subject: [PATCH 11/12] block: fix test --- packages/block/test/header.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block/test/header.spec.ts b/packages/block/test/header.spec.ts index a98440b6dc..c3bcf5c86e 100644 --- a/packages/block/test/header.spec.ts +++ b/packages/block/test/header.spec.ts @@ -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 From 8917fcc6e0557e23898d08ac8f346f02875060fc Mon Sep 17 00:00:00 2001 From: harkamal Date: Mon, 10 Jul 2023 22:13:49 +0530 Subject: [PATCH 12/12] add validation in from values array --- packages/block/src/header.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index a37ec906c7..0033aca0c5 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -109,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) { @@ -127,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 } /**