From 55a8e66a39653f554057824a17a948017d8f51fb Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:12:39 -0400 Subject: [PATCH] Implement EIP 7685 (#3372) * Add request type to util * Add requests to block * Add requests root validation * Add ordering checks * Add note on requestsRoot non-determinism * Rework request structure to use interface and base class * Make requests optional * Update ordering test * Improve tests and remove unnecessary rlp encoding * Reorder requests order [no ci] * Add vm.runBlock * add tests with requests * Add buildblock changes and tests * lint * remove sorting function * Add order check for requests when generating trie * More fixes * remove generic * in flight fromValuesArray changes [no ci] * update min hardfork to cancun [no ci] * Throw on invalid requestsRoot [no ci] * add scaffolding for pending requests in pendingBlock * Update fromRPC constructors and toJSON methods * Add requests to JsonRpcBlock * update runBlock/buildBlock and tests * Remove obsolete references * fix hex typing * Check for 7685 before adding requests * address feedback * address feedback --- packages/block/src/block.ts | 100 +++++++++++- packages/block/src/from-rpc.ts | 9 +- packages/block/src/header-from-rpc.ts | 2 + packages/block/src/header.ts | 43 ++++- packages/block/src/helpers.ts | 4 +- packages/block/src/types.ts | 19 ++- packages/block/test/eip7685block.spec.ts | 154 ++++++++++++++++++ packages/block/test/header.spec.ts | 2 +- packages/blockchain/src/blockchain.ts | 12 ++ packages/blockchain/src/db/manager.ts | 16 +- .../blockchain/test/blockValidation.spec.ts | 50 +++++- packages/client/src/miner/pendingBlock.ts | 2 + packages/client/src/rpc/modules/eth.ts | 2 + packages/common/src/eips.ts | 9 + packages/evm/src/evm.ts | 2 +- packages/util/src/index.ts | 1 + packages/util/src/requests.ts | 28 ++++ packages/util/test/requests.spec.ts | 44 +++++ packages/vm/src/buildBlock.ts | 17 +- packages/vm/src/runBlock.ts | 149 +++++++++++------ packages/vm/src/types.ts | 17 +- packages/vm/test/api/EIPs/eip-7685.spec.ts | 103 ++++++++++++ 22 files changed, 717 insertions(+), 68 deletions(-) create mode 100644 packages/block/test/eip7685block.spec.ts create mode 100644 packages/util/src/requests.ts create mode 100644 packages/util/test/requests.spec.ts create mode 100644 packages/vm/test/api/EIPs/eip-7685.spec.ts diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 27c2340455..398712132d 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -4,6 +4,7 @@ import { Trie } from '@ethereumjs/trie' import { BlobEIP4844Transaction, Capability, TransactionFactory } from '@ethereumjs/tx' import { BIGINT_0, + CLRequest, KECCAK256_RLP, KECCAK256_RLP_ARRAY, Withdrawal, @@ -41,7 +42,13 @@ import type { TxOptions, TypedTransaction, } from '@ethereumjs/tx' -import type { EthersProvider, PrefixedHexString, WithdrawalBytes } from '@ethereumjs/util' +import type { + CLRequestType, + EthersProvider, + PrefixedHexString, + RequestBytes, + WithdrawalBytes, +} from '@ethereumjs/util' /** * An object that represents the block. @@ -51,6 +58,7 @@ export class Block { public readonly transactions: TypedTransaction[] = [] public readonly uncleHeaders: BlockHeader[] = [] public readonly withdrawals?: Withdrawal[] + public readonly requests?: CLRequestType[] public readonly common: Common protected keccakFunction: (msg: Uint8Array) => Uint8Array @@ -64,6 +72,7 @@ export class Block { protected cache: { txTrieRoot?: Uint8Array withdrawalsTrieRoot?: Uint8Array + requestsRoot?: Uint8Array } = {} /** @@ -92,6 +101,28 @@ export class Block { return trie.root() } + /** + * Returns the requests trie root for an array of CLRequests + * @param requests - an array of CLRequests + * @param emptyTrie optional empty trie used to generate the root + * @returns a 32 byte Uint8Array representing the requests trie root + */ + public static async genRequestsTrieRoot(requests: CLRequest[], emptyTrie?: Trie) { + // Requests should be sorted in monotonically ascending order based on type + // and whatever internal sorting logic is defined by each request type + if (requests.length > 1) { + for (let x = 1; x < requests.length; x++) { + if (requests[x].type < requests[x - 1].type) + throw new Error('requests are not sorted in ascending order') + } + } + const trie = emptyTrie ?? new Trie() + for (const [i, req] of requests.entries()) { + await trie.put(RLP.encode(i), req.serialize()) + } + return trie.root() + } + /** * Static constructor to create a block from a block data dictionary * @@ -105,6 +136,7 @@ export class Block { uncleHeaders: uhsData, withdrawals: withdrawalsData, executionWitness: executionWitnessData, + requests: clRequests, } = blockData const header = BlockHeader.fromHeaderData(headerData, opts) @@ -143,7 +175,15 @@ export class Block { // stub till that time const executionWitness = executionWitnessData - return new Block(header, transactions, uncleHeaders, withdrawals, opts, executionWitness) + return new Block( + header, + transactions, + uncleHeaders, + withdrawals, + opts, + clRequests, + executionWitness + ) } /** @@ -177,7 +217,8 @@ export class Block { // First try to load header so that we can use its common (in case of setHardfork being activated) // to correctly make checks on the hardforks - const [headerData, txsData, uhsData, withdrawalBytes, executionWitnessBytes] = values + const [headerData, txsData, uhsData, withdrawalBytes, requestBytes, executionWitnessBytes] = + values const header = BlockHeader.fromValuesArray(headerData, opts) if ( @@ -227,6 +268,12 @@ export class Block { })) ?.map(Withdrawal.fromWithdrawalData) + let requests + if (header.common.isActivatedEIP(7685)) { + requests = (requestBytes as RequestBytes[]).map( + (bytes) => new CLRequest(bytes[0], bytes.slice(1)) + ) + } // executionWitness are not part of the EL fetched blocks via eth_ bodies method // they are currently only available via the engine api constructed blocks let executionWitness @@ -242,7 +289,15 @@ export class Block { } } - return new Block(header, transactions, uncleHeaders, withdrawals, opts, executionWitness) + return new Block( + header, + transactions, + uncleHeaders, + withdrawals, + opts, + requests, + executionWitness + ) } /** @@ -334,6 +389,7 @@ export class Block { feeRecipient: coinbase, transactions, withdrawals: withdrawalsData, + requestsRoot, executionWitness, } = payload @@ -353,6 +409,7 @@ export class Block { } } + const reqRoot = requestsRoot === null ? undefined : requestsRoot const transactionsTrie = await Block.genTransactionsTrieRoot( txs, new Trie({ common: opts?.common }) @@ -369,6 +426,7 @@ export class Block { withdrawalsRoot, mixHash, coinbase, + requestsRoot: reqRoot, } // we are not setting setHardfork as common is already set to the correct hf @@ -417,6 +475,7 @@ export class Block { uncleHeaders: BlockHeader[] = [], withdrawals?: Withdrawal[], opts: BlockOptions = {}, + requests?: CLRequest[], executionWitness?: VerkleExecutionWitness | null ) { this.header = header ?? BlockHeader.fromHeaderData({}, opts) @@ -426,6 +485,7 @@ export class Block { this.transactions = transactions this.withdrawals = withdrawals ?? (this.common.isActivatedEIP(4895) ? [] : undefined) this.executionWitness = executionWitness + this.requests = requests ?? (this.common.isActivatedEIP(7685) ? [] : undefined) // null indicates an intentional absence of value or unavailability // undefined indicates that the executionWitness should be initialized with the default state if (this.common.isActivatedEIP(6800) && this.executionWitness === undefined) { @@ -474,6 +534,18 @@ export class Block { throw new Error(`Cannot have executionWitness field if EIP 6800 is not active `) } + if (!this.common.isActivatedEIP(7685) && requests !== undefined) { + throw new Error(`Cannot have requests field if EIP 7685 is not active`) + } + + // Requests should be sorted in monotonically ascending order based on type + // and whatever internal sorting logic is defined by each request type + if (requests !== undefined && requests.length > 1) { + for (let x = 1; x < requests.length; x++) { + if (requests[x].type < requests[x - 1].type) + throw new Error('requests are not sorted in ascending order') + } + } const freeze = opts?.freeze ?? true if (freeze) { Object.freeze(this) @@ -549,6 +621,25 @@ export class Block { return result } + async requestsTrieIsValid(): Promise { + if (!this.common.isActivatedEIP(7685)) { + throw new Error('EIP 7685 is not activated') + } + + let result + if (this.requests!.length === 0) { + result = equalsBytes(this.header.requestsRoot!, KECCAK256_RLP) + return result + } + + if (this.cache.requestsRoot === undefined) { + this.cache.requestsRoot = await Block.genRequestsTrieRoot(this.requests!) + } + + result = equalsBytes(this.cache.requestsRoot, this.header.requestsRoot!) + + return result + } /** * Validates transaction signatures and minimum gas requirements. * @returns {string[]} an array of error strings @@ -819,6 +910,7 @@ export class Block { transactions: this.transactions.map((tx) => tx.toJSON()), uncleHeaders: this.uncleHeaders.map((uh) => uh.toJSON()), ...withdrawalsAttr, + requests: this.requests?.map((req) => bytesToHex(req.serialize())), } } diff --git a/packages/block/src/from-rpc.ts b/packages/block/src/from-rpc.ts index 09ddc720ff..cf72da5f49 100644 --- a/packages/block/src/from-rpc.ts +++ b/packages/block/src/from-rpc.ts @@ -1,5 +1,5 @@ import { TransactionFactory } from '@ethereumjs/tx' -import { TypeOutput, setLengthLeft, toBytes, toType } from '@ethereumjs/util' +import { CLRequest, TypeOutput, hexToBytes, setLengthLeft, toBytes, toType } from '@ethereumjs/util' import { blockHeaderFromRpc } from './header-from-rpc.js' @@ -7,6 +7,7 @@ import { Block } from './index.js' import type { BlockOptions, JsonRpcBlock } from './index.js' import type { TypedTransaction } from '@ethereumjs/tx' +import type { PrefixedHexString } from '@ethereumjs/util' function normalizeTxParams(_txParams: any) { const txParams = Object.assign({}, _txParams) @@ -54,8 +55,12 @@ export function blockFromRpc( const uncleHeaders = uncles.map((uh) => blockHeaderFromRpc(uh, options)) + const requests = blockParams.requests?.map((req) => { + const bytes = hexToBytes(req as PrefixedHexString) + return new CLRequest(bytes[0], bytes.slice(1)) + }) return Block.fromBlockData( - { header, transactions, uncleHeaders, withdrawals: blockParams.withdrawals }, + { header, transactions, uncleHeaders, withdrawals: blockParams.withdrawals, requests }, options ) } diff --git a/packages/block/src/header-from-rpc.ts b/packages/block/src/header-from-rpc.ts index 97b92a6a61..a4ba8f3d45 100644 --- a/packages/block/src/header-from-rpc.ts +++ b/packages/block/src/header-from-rpc.ts @@ -31,6 +31,7 @@ export function blockHeaderFromRpc(blockParams: JsonRpcBlock, options?: BlockOpt blobGasUsed, excessBlobGas, parentBeaconBlockRoot, + requestsRoot, } = blockParams const blockHeader = BlockHeader.fromHeaderData( @@ -55,6 +56,7 @@ export function blockHeaderFromRpc(blockParams: JsonRpcBlock, options?: BlockOpt blobGasUsed, excessBlobGas, parentBeaconBlockRoot, + requestsRoot, }, options ) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index a2e5e41852..e99e7135fa 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -63,6 +63,7 @@ export class BlockHeader { public readonly blobGasUsed?: bigint public readonly excessBlobGas?: bigint public readonly parentBeaconBlockRoot?: Uint8Array + public readonly requestsRoot?: Uint8Array public readonly common: Common @@ -117,17 +118,24 @@ export class BlockHeader { */ public static fromValuesArray(values: BlockHeaderBytes, opts: BlockOptions = {}) { const headerData = valuesArrayToHeaderData(values) - const { number, baseFeePerGas, excessBlobGas, blobGasUsed, parentBeaconBlockRoot } = headerData + const { + number, + baseFeePerGas, + excessBlobGas, + blobGasUsed, + parentBeaconBlockRoot, + requestsRoot, + } = headerData const header = BlockHeader.fromHeaderData(headerData, opts) - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (header.common.isActivatedEIP(1559) && baseFeePerGas === undefined) { const eip1559ActivationBlock = bigIntToBytes(header.common.eipBlock(1559)!) - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (eip1559ActivationBlock && equalsBytes(eip1559ActivationBlock, number as Uint8Array)) { + if ( + eip1559ActivationBlock !== undefined && + equalsBytes(eip1559ActivationBlock, number as Uint8Array) + ) { throw new Error('invalid header. baseFeePerGas should be provided') } } - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (header.common.isActivatedEIP(4844)) { if (excessBlobGas === undefined) { throw new Error('invalid header. excessBlobGas should be provided') @@ -138,6 +146,10 @@ export class BlockHeader { if (header.common.isActivatedEIP(4788) && parentBeaconBlockRoot === undefined) { throw new Error('invalid header. parentBeaconBlockRoot should be provided') } + + if (header.common.isActivatedEIP(7685) && requestsRoot === undefined) { + throw new Error('invalid header. requestsRoot should be provided') + } return header } /** @@ -222,6 +234,7 @@ export class BlockHeader { blobGasUsed: this.common.isActivatedEIP(4844) ? BIGINT_0 : undefined, excessBlobGas: this.common.isActivatedEIP(4844) ? BIGINT_0 : undefined, parentBeaconBlockRoot: this.common.isActivatedEIP(4788) ? zeros(32) : undefined, + requestsRoot: this.common.isActivatedEIP(7685) ? KECCAK256_RLP : undefined, } const baseFeePerGas = @@ -235,6 +248,8 @@ export class BlockHeader { const parentBeaconBlockRoot = toType(headerData.parentBeaconBlockRoot, TypeOutput.Uint8Array) ?? hardforkDefaults.parentBeaconBlockRoot + const requestsRoot = + toType(headerData.requestsRoot, TypeOutput.Uint8Array) ?? hardforkDefaults.requestsRoot if (!this.common.isActivatedEIP(1559) && baseFeePerGas !== undefined) { throw new Error('A base fee for a block can only be set with EIP1559 being activated') @@ -262,6 +277,10 @@ export class BlockHeader { ) } + if (!this.common.isActivatedEIP(7685) && requestsRoot !== undefined) { + throw new Error('requestsRoot can only be provided with EIP 7685 activated') + } + this.parentHash = parentHash this.uncleHash = uncleHash this.coinbase = coinbase @@ -282,6 +301,7 @@ export class BlockHeader { this.blobGasUsed = blobGasUsed this.excessBlobGas = excessBlobGas this.parentBeaconBlockRoot = parentBeaconBlockRoot + this.requestsRoot = requestsRoot this._genericFormatValidation() this._validateDAOExtraData() @@ -407,6 +427,13 @@ export class BlockHeader { throw new Error(msg) } } + + if (this.common.isActivatedEIP(7685) === true) { + if (this.requestsRoot === undefined) { + const msg = this._errorMsg('EIP7685 block has no requestsRoot field') + throw new Error(msg) + } + } } /** @@ -693,6 +720,9 @@ export class BlockHeader { if (this.common.isActivatedEIP(4788)) { rawItems.push(this.parentBeaconBlockRoot!) } + if (this.common.isActivatedEIP(7685) === true) { + rawItems.push(this.requestsRoot!) + } return rawItems } @@ -960,6 +990,9 @@ export class BlockHeader { if (this.common.isActivatedEIP(4788)) { jsonDict.parentBeaconBlockRoot = bytesToHex(this.parentBeaconBlockRoot!) } + if (this.common.isActivatedEIP(7685)) { + jsonDict.requestsRoot = bytesToHex(this.requestsRoot!) + } return jsonDict } diff --git a/packages/block/src/helpers.ts b/packages/block/src/helpers.ts index d78a40e8ae..3668794321 100644 --- a/packages/block/src/helpers.ts +++ b/packages/block/src/helpers.ts @@ -44,9 +44,10 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData { blobGasUsed, excessBlobGas, parentBeaconBlockRoot, + requestsRoot, ] = values - if (values.length > 20) { + if (values.length > 21) { throw new Error( `invalid header. More values than expected were received. Max: 20, got: ${values.length}` ) @@ -78,6 +79,7 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData { blobGasUsed, excessBlobGas, parentBeaconBlockRoot, + requestsRoot, } } diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 26d36bc864..f5edac5cad 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -5,8 +5,10 @@ import type { AddressLike, BigIntLike, BytesLike, + CLRequest, JsonRpcWithdrawal, PrefixedHexString, + RequestBytes, WithdrawalBytes, WithdrawalData, } from '@ethereumjs/util' @@ -137,6 +139,7 @@ export interface HeaderData { blobGasUsed?: BigIntLike | string excessBlobGas?: BigIntLike | string parentBeaconBlockRoot?: BytesLike | string + requestsRoot?: BytesLike | string } /** @@ -150,6 +153,7 @@ export interface BlockData { transactions?: Array uncleHeaders?: Array withdrawals?: Array + requests?: Array /** * EIP-6800: Verkle Proof Data (experimental) */ @@ -157,16 +161,19 @@ export interface BlockData { } export type WithdrawalsBytes = WithdrawalBytes[] +export type RequestsBytes = RequestBytes[] export type ExecutionWitnessBytes = Uint8Array export type BlockBytes = | [BlockHeaderBytes, TransactionsBytes, UncleHeadersBytes] | [BlockHeaderBytes, TransactionsBytes, UncleHeadersBytes, WithdrawalsBytes] + | [BlockHeaderBytes, TransactionsBytes, UncleHeadersBytes, WithdrawalsBytes, RequestsBytes] | [ BlockHeaderBytes, TransactionsBytes, UncleHeadersBytes, WithdrawalsBytes, + RequestsBytes, ExecutionWitnessBytes ] @@ -174,7 +181,12 @@ export type BlockBytes = * BlockHeaderBuffer is a Buffer array, except for the Verkle PreState which is an array of prestate arrays. */ export type BlockHeaderBytes = Uint8Array[] -export type BlockBodyBytes = [TransactionsBytes, UncleHeadersBytes, WithdrawalsBytes?] +export type BlockBodyBytes = [ + TransactionsBytes, + UncleHeadersBytes, + WithdrawalsBytes?, + RequestBytes? +] /** * TransactionsBytes can be an array of serialized txs for Typed Transactions or an array of Uint8Array Arrays for legacy transactions. */ @@ -192,6 +204,7 @@ export interface JsonBlock { transactions?: JsonTx[] uncleHeaders?: JsonHeader[] withdrawals?: JsonRpcWithdrawal[] + requests?: PrefixedHexString[] | null executionWitness?: VerkleExecutionWitness | null } @@ -220,6 +233,7 @@ export interface JsonHeader { blobGasUsed?: PrefixedHexString | string excessBlobGas?: PrefixedHexString | string parentBeaconBlockRoot?: PrefixedHexString | string + requestsRoot?: PrefixedHexString | string } /* @@ -254,6 +268,8 @@ export interface JsonRpcBlock { excessBlobGas?: PrefixedHexString | string // If EIP-4844 is enabled for this block, returns the excess blob gas for the block parentBeaconBlockRoot?: PrefixedHexString | string // If EIP-4788 is enabled for this block, returns parent beacon block root executionWitness?: VerkleExecutionWitness | null // If Verkle is enabled for this block + requestsRoot?: PrefixedHexString | string // If EIP-7685 is enabled for this block, returns the requests root + requests?: Array // If EIP-7685 is enabled for this block, array of serialized CL requests } export type WithdrawalV1 = { @@ -286,4 +302,5 @@ export type ExecutionPayload = { parentBeaconBlockRoot?: PrefixedHexString | string // QUANTITY, 64 Bits // VerkleExecutionWitness is already a hex serialized object executionWitness?: VerkleExecutionWitness | null // QUANTITY, 64 Bits, null implies not available + requestsRoot?: PrefixedHexString | string | null // DATA, 32 bytes, null implies EIP 7685 not active yet } diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts new file mode 100644 index 0000000000..265d17de09 --- /dev/null +++ b/packages/block/test/eip7685block.spec.ts @@ -0,0 +1,154 @@ +import { Chain, Common, Hardfork } from '@ethereumjs/common' +import { CLRequest, KECCAK256_RLP, concatBytes, hexToBytes, randomBytes } from '@ethereumjs/util' +import { assert, describe, expect, it } from 'vitest' + +import { Block, BlockHeader } from '../src/index.js' + +import type { CLRequestType } from '@ethereumjs/util' + +class NumberRequest extends CLRequest implements CLRequestType { + constructor(type: number, bytes: Uint8Array) { + super(type, bytes) + } + + public static fromRequestData(bytes: Uint8Array): CLRequestType { + return new NumberRequest(0x1, bytes) + } + + serialize() { + return concatBytes(Uint8Array.from([this.type]), this.bytes) + } +} + +const common = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.Cancun, + eips: [7685, 4844, 4788], +}) +describe('7685 tests', () => { + it('should instantiate block with defaults', () => { + const block = Block.fromBlockData({}, { common }) + assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP) + const block2 = new Block(undefined, undefined, undefined, undefined, { common }) + assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP) + assert.equal(block2.requests?.length, 0) + }) + it('should instantiate a block with requests', async () => { + const request = new NumberRequest(0x1, randomBytes(32)) + const requestsRoot = await Block.genRequestsTrieRoot([request]) + const block = Block.fromBlockData( + { + requests: [request], + header: { requestsRoot }, + }, + { common } + ) + assert.equal(block.requests?.length, 1) + assert.deepEqual(block.header.requestsRoot, requestsRoot) + }) + it('RequestsRootIsValid should return false when requestsRoot is invalid', async () => { + const request = new NumberRequest(0x1, randomBytes(32)) + const block = Block.fromBlockData( + { + requests: [request], + header: { requestsRoot: randomBytes(32) }, + }, + { common } + ) + + assert.equal(await block.requestsTrieIsValid(), false) + }) + it('should validate requests order', async () => { + const request1 = new NumberRequest(0x1, hexToBytes('0x1234')) + const request2 = new NumberRequest(0x1, hexToBytes('0x2345')) + const request3 = new NumberRequest(0x2, hexToBytes('0x2345')) + const requests = [request1, request2, request3] + const requestsRoot = await Block.genRequestsTrieRoot(requests) + + // Construct block with requests in correct order + + const block = Block.fromBlockData( + { + requests, + header: { requestsRoot }, + }, + { common } + ) + + assert.ok(await block.requestsTrieIsValid()) + + // Throws when requests are not ordered correctly + await expect(async () => + Block.fromBlockData( + { + requests: [request1, request3, request2], + header: { requestsRoot }, + }, + { common } + ) + ).rejects.toThrow('ascending order') + }) +}) + +describe('fromValuesArray tests', () => { + it('should construct a block with empty requests root', () => { + const block = Block.fromValuesArray( + [BlockHeader.fromHeaderData({}, { common }).raw(), [], [], [], []], + { + common, + } + ) + assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP) + }) + it('should construct a block with a valid requests array', async () => { + const request1 = new NumberRequest(0x1, hexToBytes('0x1234')) + const request2 = new NumberRequest(0x1, hexToBytes('0x2345')) + const request3 = new NumberRequest(0x2, hexToBytes('0x2345')) + const requests = [request1, request2, request3] + const requestsRoot = await Block.genRequestsTrieRoot(requests) + const serializedRequests = [request1.serialize(), request2.serialize(), request3.serialize()] + + const block = Block.fromValuesArray( + [ + BlockHeader.fromHeaderData({ requestsRoot }, { common }).raw(), + [], + [], + [], + serializedRequests, + ], + { + common, + } + ) + assert.deepEqual(block.header.requestsRoot, requestsRoot) + assert.equal(block.requests?.length, 3) + }) +}) + +describe('fromRPC tests', () => { + it('should construct a block from a JSON object', async () => { + const request1 = new NumberRequest(0x1, hexToBytes('0x1234')) + const request2 = new NumberRequest(0x1, hexToBytes('0x2345')) + const request3 = new NumberRequest(0x2, hexToBytes('0x2345')) + const requests = [request1, request2, request3] + const requestsRoot = await Block.genRequestsTrieRoot(requests) + const serializedRequests = [request1.serialize(), request2.serialize(), request3.serialize()] + + const block = Block.fromValuesArray( + [ + BlockHeader.fromHeaderData({ requestsRoot }, { common }).raw(), + [], + [], + [], + serializedRequests, + ], + { + common, + } + ) + const jsonBlock = block.toJSON() + const rpcBlock: any = { ...jsonBlock.header, requests: jsonBlock.requests } + const blockFromJson = Block.fromRPC(rpcBlock, undefined, { common }) + assert.deepEqual(block.hash(), blockFromJson.hash()) + }) +}) diff --git a/packages/block/test/header.spec.ts b/packages/block/test/header.spec.ts index 26869a8494..6f4f08bfa7 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(21).fill(new Uint8Array(0)) + const headerArray = Array(22).fill(new Uint8Array(0)) // mock header data (if set to zeros(0) header throws) headerArray[0] = zeros(32) //parentHash diff --git a/packages/blockchain/src/blockchain.ts b/packages/blockchain/src/blockchain.ts index db36e87733..9c47f3fd7a 100644 --- a/packages/blockchain/src/blockchain.ts +++ b/packages/blockchain/src/blockchain.ts @@ -684,6 +684,12 @@ export class Blockchain implements BlockchainInterface { throw new Error(`expected blob gas: ${expectedExcessBlobGas}, got: ${header.excessBlobGas}`) } } + + if (header.common.isActivatedEIP(7685) === true) { + if (header.requestsRoot === undefined) { + throw new Error(`requestsRoot must be provided when EIP-7685 is active`) + } + } } /** @@ -699,6 +705,12 @@ export class Blockchain implements BlockchainInterface { // (one for each uncle header and then for validateBlobTxs). const parentBlock = await this.getBlock(block.header.parentHash) block.validateBlobTransactions(parentBlock.header) + if (block.common.isActivatedEIP(7685)) { + const valid = await block.requestsTrieIsValid() + if (!valid) { + throw new Error('invalid requestsRoot') + } + } } /** * The following rules are checked in this method: diff --git a/packages/blockchain/src/db/manager.ts b/packages/blockchain/src/db/manager.ts index 304f4676b4..32e149b8c4 100644 --- a/packages/blockchain/src/db/manager.ts +++ b/packages/blockchain/src/db/manager.ts @@ -124,7 +124,21 @@ export class DBManager { ) { throw new Error('withdrawals root shoot be equal to hash of null when no withdrawals') } - if (body.length <= 3) body.push([]) + if (body.length < 3) body.push([]) + } + // If requests root exists, validate that requests array exists or insert it + if (header.requestsRoot !== undefined) { + if ( + !equalsBytes(header.requestsRoot, KECCAK256_RLP) && + (body.length < 4 || body[3]?.length === 0) + ) { + throw new Error('requestsRoot should be equal to hash of null when no requests') + } + if (body.length < 4) { + for (let x = 0; x < 4 - body.length; x++) { + body.push([]) + } + } } } diff --git a/packages/blockchain/test/blockValidation.spec.ts b/packages/blockchain/test/blockValidation.spec.ts index b578d65be4..e0dcb3d63c 100644 --- a/packages/blockchain/test/blockValidation.spec.ts +++ b/packages/blockchain/test/blockValidation.spec.ts @@ -1,9 +1,9 @@ import { Block, BlockHeader } from '@ethereumjs/block' import { Chain, Common, Hardfork } from '@ethereumjs/common' import { RLP } from '@ethereumjs/rlp' -import { bytesToHex } from '@ethereumjs/util' +import { KECCAK256_RLP, bytesToHex, randomBytes } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak.js' -import { assert, describe, it } from 'vitest' +import { assert, describe, expect, it } from 'vitest' import { Blockchain } from '../src/index.js' @@ -380,3 +380,49 @@ describe('[Blockchain]: Block validation tests', () => { assert.equal(common.hardfork(), Hardfork.London, 'validation did not change common hardfork') }) }) +describe('EIP 7685: requests field validation tests', () => { + it('should throw when putting a block with an invalid requestsRoot', async () => { + const common = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.Cancun, + eips: [7685, 1559, 4895, 4844, 4788], + }) + const blockchain = await Blockchain.create({ + common, + validateConsensus: false, + }) + const block = Block.fromBlockData( + { + header: { + number: 1n, + requestsRoot: randomBytes(32), + withdrawalsRoot: KECCAK256_RLP, + parentHash: blockchain.genesisBlock.hash(), + timestamp: blockchain.genesisBlock.header.timestamp + 1n, + gasLimit: 5000, + }, + }, + { common } + ) + + await expect(async () => blockchain.putBlock(block)).rejects.toThrow('invalid requestsRoot') + + const blockWithRequest = Block.fromBlockData( + { + header: { + number: 1n, + requestsRoot: randomBytes(32), + withdrawalsRoot: KECCAK256_RLP, + parentHash: blockchain.genesisBlock.hash(), + timestamp: blockchain.genesisBlock.header.timestamp + 1n, + gasLimit: 5000, + }, + requests: [{ type: 0x1, bytes: randomBytes(12), serialize: () => randomBytes(32) }], + }, + { common } + ) + await expect(async () => blockchain.putBlock(blockWithRequest)).rejects.toThrow( + 'invalid requestsRoot' + ) + }) +}) diff --git a/packages/client/src/miner/pendingBlock.ts b/packages/client/src/miner/pendingBlock.ts index 0af8e05a80..6f548849d0 100644 --- a/packages/client/src/miner/pendingBlock.ts +++ b/packages/client/src/miner/pendingBlock.ts @@ -288,7 +288,9 @@ export class PendingBlock { ) const { skippedByAddErrors, blobTxs } = await this.addTransactions(builder, txs) + const block = await builder.build() + // Construct blobs bundle const blobs = block.common.isActivatedEIP(4844) ? this.constructBlobsBundle(payloadId, blobTxs) diff --git a/packages/client/src/rpc/modules/eth.ts b/packages/client/src/rpc/modules/eth.ts index 2c65e3c974..c45e3dd104 100644 --- a/packages/client/src/rpc/modules/eth.ts +++ b/packages/client/src/rpc/modules/eth.ts @@ -140,6 +140,8 @@ const jsonRpcBlock = async ( blobGasUsed: header.blobGasUsed, excessBlobGas: header.excessBlobGas, parentBeaconBlockRoot: header.parentBeaconBlockRoot, + requestsRoot: header.requestsRoot, + requests: block.requests?.map((req) => bytesToHex(req.serialize())), } } diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 0bf8863cd0..240c09d83c 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -542,4 +542,13 @@ export const EIPs: EIPsDict = { }, }, }, + 7685: { + comment: 'General purpose execution layer requests', + url: 'https://eips.ethereum.org/EIPS/eip-7685', + status: Status.Draft, + // TODO: Set correct minimum hardfork + minimumHardfork: Hardfork.Cancun, + requiredEIPs: [3675], + gasPrices: {}, + }, } diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index ac5664c581..fcb61c1e09 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -210,7 +210,7 @@ export class EVM implements EVMInterface { // Supported EIPs const supportedEIPs = [ 1153, 1559, 2537, 2565, 2718, 2929, 2930, 2935, 3074, 3198, 3529, 3540, 3541, 3607, 3651, - 3670, 3855, 3860, 4399, 4895, 4788, 4844, 5133, 5656, 6780, 6800, 7516, + 3670, 3855, 3860, 4399, 4895, 4788, 4844, 5133, 5656, 6780, 6800, 7516, 7685, ] for (const eip of this.common.eips()) { diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 972c87da5e..29799b8f59 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -65,3 +65,4 @@ export * from './kzg.js' export * from './lock.js' export * from './mapDB.js' export * from './provider.js' +export * from './requests.js' diff --git a/packages/util/src/requests.ts b/packages/util/src/requests.ts new file mode 100644 index 0000000000..5a27a1e99b --- /dev/null +++ b/packages/util/src/requests.ts @@ -0,0 +1,28 @@ +import { concatBytes } from 'ethereum-cryptography/utils' + +export type RequestBytes = Uint8Array + +export interface RequestData { + type: number + data: Uint8Array +} + +export interface CLRequestType { + readonly type: number + readonly bytes: Uint8Array + serialize(): Uint8Array +} + +export class CLRequest implements CLRequestType { + type: number + bytes: Uint8Array + constructor(type: number, bytes: Uint8Array) { + if (type === undefined) throw new Error('request type is required') + this.type = type + this.bytes = bytes + } + + serialize() { + return concatBytes(Uint8Array.from([this.type]), this.bytes) + } +} diff --git a/packages/util/test/requests.spec.ts b/packages/util/test/requests.spec.ts new file mode 100644 index 0000000000..12a6a3fb43 --- /dev/null +++ b/packages/util/test/requests.spec.ts @@ -0,0 +1,44 @@ +import { assert, describe, it } from 'vitest' + +import { + bigIntToBytes, + bytesToBigInt, + bytesToHex, + concatBytes, + hexToBytes, + randomBytes, +} from '../src/bytes.js' +import { CLRequest, type CLRequestType } from '../src/requests.js' + +class NumberRequest extends CLRequest implements CLRequestType { + constructor(type: number, bytes: Uint8Array) { + super(type, bytes) + } + + public static fromRequestData(bytes: Uint8Array): CLRequestType { + return new NumberRequest(0x1, bytes) + } + + serialize() { + return concatBytes(Uint8Array.from([this.type]), this.bytes) + } +} +describe('should create a request', () => { + it('should create a request', () => { + const requestType = 0x1 + const data = randomBytes(32) + const request = new NumberRequest(0x1, data) + const serialized = request.serialize() + assert.equal(serialized[0], requestType) + assert.deepEqual(serialized.slice(1), data) + }) + it('should create a request from RequestData', () => { + const request1 = NumberRequest.fromRequestData(hexToBytes('0x1234')) + assert.equal(request1.type, 0x1) + assert.equal(bytesToHex(request1.bytes), '0x1234') + + const request2 = NumberRequest.fromRequestData(bigIntToBytes(123n)) + assert.equal(request2.type, 0x1) + assert.equal(bytesToBigInt(request2.bytes), 123n) + }) +}) diff --git a/packages/vm/src/buildBlock.ts b/packages/vm/src/buildBlock.ts index f67539f391..ba9f06aade 100644 --- a/packages/vm/src/buildBlock.ts +++ b/packages/vm/src/buildBlock.ts @@ -21,6 +21,7 @@ import { Bloom } from './bloom/index.js' import { accumulateParentBeaconBlockRoot, accumulateParentBlockHash, + accumulateRequests, calculateMinerReward, encodeReceipt, rewardAccount, @@ -280,7 +281,7 @@ export class BlockBuilder { } /** - * This method returns the finalized block. + * This method constructs the finalized block, including withdrawals and any CLRequests. * It also: * - Assigns the reward for miner (PoW) * - Commits the checkpoint on the StateManager @@ -289,6 +290,9 @@ export class BlockBuilder { * which is validated along with the block number and difficulty by ethash. * For PoA, please pass `blockOption.cliqueSigner` into the buildBlock constructor, * as the signer will be awarded the txs amount spent on gas as they are added. + * + * Note: we add CLRequests here because they can be generated at any time during the + * lifecycle of a pending block so need to be provided only when the block is finalized. */ async build(sealOpts?: SealBlockOpts) { this.checkStatus() @@ -316,6 +320,14 @@ export class BlockBuilder { blobGasUsed = this.blobGasUsed } + let requests + let requestsRoot + if (this.vm.common.isActivatedEIP(7685)) { + requests = await accumulateRequests(this.vm) + requestsRoot = await Block.genRequestsTrieRoot(requests) + // Do other validations per request type + } + const headerData = { ...this.headerData, stateRoot, @@ -327,6 +339,7 @@ export class BlockBuilder { timestamp, // correct excessBlobGas should already be part of headerData used above blobGasUsed, + requestsRoot, } if (consensusType === ConsensusType.ProofOfWork) { @@ -338,7 +351,9 @@ export class BlockBuilder { header: headerData, transactions: this.transactions, withdrawals: this.withdrawals, + requests, } + const block = Block.fromBlockData(blockData, blockOpts) if (this.blockOpts.putBlockIntoBlockchain === true) { diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 2471c42f25..8bcba57b2c 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -40,7 +40,7 @@ import type { import type { VM } from './vm.js' import type { Common } from '@ethereumjs/common' import type { EVM, EVMInterface } from '@ethereumjs/evm' -import type { PrefixedHexString } from '@ethereumjs/util' +import type { CLRequest, PrefixedHexString } from '@ethereumjs/util' const { debug: createDebugLogger } = debugDefault @@ -192,6 +192,13 @@ export async function runBlock(this: VM, opts: RunBlockOpts): Promise => { + const requests: CLRequest[] = [] + + // TODO: Add in code to accumulate deposits (EIP-6110) + + // TODO: Add in code to accumulate partial withdrawals (EIP-7002) + + if (requests.length > 1) { + for (let x = 1; x < requests.length; x++) { + if (requests[x].type < requests[x - 1].type) + throw new Error('requests are not in ascending order') + } + } + return requests +} diff --git a/packages/vm/src/types.ts b/packages/vm/src/types.ts index a0471adaf8..79b859ee64 100644 --- a/packages/vm/src/types.ts +++ b/packages/vm/src/types.ts @@ -4,7 +4,13 @@ import type { BlockchainInterface } from '@ethereumjs/blockchain' import type { Common, EVMStateManagerInterface } from '@ethereumjs/common' import type { EVMInterface, EVMResult, Log } from '@ethereumjs/evm' import type { AccessList, TypedTransaction } from '@ethereumjs/tx' -import type { BigIntLike, GenesisState, PrefixedHexString, WithdrawalData } from '@ethereumjs/util' +import type { + BigIntLike, + CLRequest, + GenesisState, + PrefixedHexString, + WithdrawalData, +} from '@ethereumjs/util' export type TxReceipt = PreByzantiumTxReceipt | PostByzantiumTxReceipt | EIP4844BlobTxReceipt /** @@ -324,6 +330,15 @@ export interface RunBlockResult extends Omit { * The bloom filter of the LOGs (events) after executing the block */ logsBloom: Uint8Array + + /** + * The requestsRoot for any CL requests in the block + */ + requestsRoot?: Uint8Array + /** + * Any CL requests that were processed in the course of this block + */ + requests?: CLRequest[] } export interface AfterBlockEvent extends RunBlockResult { diff --git a/packages/vm/test/api/EIPs/eip-7685.spec.ts b/packages/vm/test/api/EIPs/eip-7685.spec.ts new file mode 100644 index 0000000000..ce097778be --- /dev/null +++ b/packages/vm/test/api/EIPs/eip-7685.spec.ts @@ -0,0 +1,103 @@ +import { Block } from '@ethereumjs/block' +import { Blockchain } from '@ethereumjs/blockchain' +import { Chain, Common, Hardfork } from '@ethereumjs/common' +import { CLRequest, KECCAK256_RLP, concatBytes, hexToBytes, randomBytes } from '@ethereumjs/util' +import { assert, describe, expect, it } from 'vitest' + +import { VM } from '../../../src/vm.js' +import { setupVM } from '../utils.js' + +import type { CLRequestType } from '@ethereumjs/util' + +const invalidRequestsRoot = hexToBytes( + '0xc98048d6605eb79ecc08d90b8817f44911ec474acd8d11688453d2c6ef743bc5' +) +class NumberRequest extends CLRequest implements CLRequestType { + constructor(type: number, bytes: Uint8Array) { + super(type, bytes) + } + + public static fromRequestData(bytes: Uint8Array): CLRequestType { + return new NumberRequest(0x1, bytes) + } + + serialize() { + return concatBytes(Uint8Array.from([this.type]), this.bytes) + } +} + +const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Cancun, eips: [7685] }) + +describe('EIP-7685 runBlock tests', () => { + it('should not error when a valid requestsRoot is provided', async () => { + const vm = await setupVM({ common }) + const emptyBlock = Block.fromBlockData({}, { common }) + const res = await vm.runBlock({ + block: emptyBlock, + generate: true, + }) + assert.equal(res.gasUsed, 0n) + }) + it('should error when an invalid requestsRoot is provided', async () => { + const vm = await setupVM({ common }) + + const emptyBlock = Block.fromBlockData( + { header: { requestsRoot: invalidRequestsRoot } }, + { common } + ) + await expect(async () => + vm.runBlock({ + block: emptyBlock, + }) + ).rejects.toThrow('invalid requestsRoot') + }) + it('should not throw invalid requestsRoot error when valid requests are provided', async () => { + const vm = await setupVM({ common }) + const request = new NumberRequest(0x1, randomBytes(32)) + const requestsRoot = await Block.genRequestsTrieRoot([request]) + const block = Block.fromBlockData( + { + requests: [request], + header: { requestsRoot }, + }, + { common } + ) + await expect(async () => vm.runBlock({ block })).rejects.toThrow('invalid block stateRoot') + }) + it('should error when requestsRoot does not match requests provided', async () => { + const vm = await setupVM({ common }) + const request = new NumberRequest(0x1, randomBytes(32)) + const block = Block.fromBlockData( + { + requests: [request], + header: { requestsRoot: invalidRequestsRoot }, + }, + { common } + ) + await expect(() => vm.runBlock({ block })).rejects.toThrow('invalid requestsRoot') + }) +}) + +describe('EIP 7685 buildBlock tests', () => { + it('should build a block without a request and a valid requestsRoot', async () => { + const common = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.Cancun, + eips: [7685, 1559, 4895, 4844, 4788], + }) + const genesisBlock = Block.fromBlockData( + { header: { gasLimit: 50000, baseFeePerGas: 100 } }, + { common } + ) + const blockchain = await Blockchain.create({ genesisBlock, common, validateConsensus: false }) + const vm = await VM.create({ common, blockchain }) + const blockBuilder = await vm.buildBlock({ + parentBlock: genesisBlock, + blockOpts: { calcDifficultyFromHeader: genesisBlock.header, freeze: false }, + }) + + const block = await blockBuilder.build() + + assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP) + }) +})