From 3735c1a3a89e0f75c1636d033fc9a59ca481fb62 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:26:52 -0400 Subject: [PATCH 01/30] Add request type to util --- packages/util/src/requests.ts | 29 +++++++++++++++++++++++++++++ packages/util/test/requests.spec.ts | 24 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 packages/util/src/requests.ts create mode 100644 packages/util/test/requests.spec.ts diff --git a/packages/util/src/requests.ts b/packages/util/src/requests.ts new file mode 100644 index 0000000000..473f862c69 --- /dev/null +++ b/packages/util/src/requests.ts @@ -0,0 +1,29 @@ +import { concatBytes } from 'ethereum-cryptography/utils' + +import { toBytes } from './bytes.js' + +import type { BytesLike } from './types.js' + +export type RequestBytes = Uint8Array + +export interface RequestData { + type: number + data: BytesLike +} +export class CLRequest { + type: number + data: Uint8Array + constructor(type: number, data?: Uint8Array) { + if (type === undefined) throw new Error('request type is required') + this.type = type + this.data = data ?? new Uint8Array() + } + + public static fromRequestsData = (requestData: RequestData) => { + return new CLRequest(requestData.type, toBytes(requestData.data)) + } + + serialize = () => { + return concatBytes(Uint8Array.from([this.type]), this.data) + } +} diff --git a/packages/util/test/requests.spec.ts b/packages/util/test/requests.spec.ts new file mode 100644 index 0000000000..dba66d9ddd --- /dev/null +++ b/packages/util/test/requests.spec.ts @@ -0,0 +1,24 @@ +import { assert, describe, it } from 'vitest' + +import { bytesToBigInt, bytesToHex, randomBytes } from '../src/bytes.js' +import { CLRequest } from '../src/requests.js' + +describe('should create a request', () => { + it('should create a request', () => { + const requestType = 0x1 + const data = randomBytes(32) + const request = new CLRequest(requestType, 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 = CLRequest.fromRequestsData({ type: 0x1, data: '0x1234' }) + assert.equal(request1.type, 0x1) + assert.equal(bytesToHex(request1.data), '0x1234') + + const request2 = CLRequest.fromRequestsData({ type: 0x2, data: 123n }) + assert.equal(request2.type, 0x2) + assert.equal(bytesToBigInt(request2.data), 123n) + }) +}) From 237e52793f05128ce976731336eadaec8c37e422 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Thu, 25 Apr 2024 12:32:34 -0400 Subject: [PATCH 02/30] Add requests to block --- packages/block/src/block.ts | 37 ++++++++++++++++++++++-- packages/block/src/header.ts | 25 +++++++++++++--- packages/block/src/types.ts | 7 +++++ packages/block/test/eip7685block.spec.ts | 31 ++++++++++++++++++++ packages/common/src/eips.ts | 9 ++++++ packages/util/src/index.ts | 1 + 6 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 packages/block/test/eip7685block.spec.ts diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index fe1a4df2b1..9cbbe319bd 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, @@ -51,6 +52,7 @@ export class Block { public readonly transactions: TypedTransaction[] = [] public readonly uncleHeaders: BlockHeader[] = [] public readonly withdrawals?: Withdrawal[] + public readonly requests?: CLRequest[] public readonly common: Common protected keccakFunction: (msg: Uint8Array) => Uint8Array @@ -92,6 +94,20 @@ 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) { + const trie = emptyTrie ?? new Trie() + for (const [i, req] of requests.entries()) { + await trie.put(RLP.encode(i), RLP.encode(req.serialize())) + } + return trie.root() + } + /** * Static constructor to create a block from a block data dictionary * @@ -105,6 +121,7 @@ export class Block { uncleHeaders: uhsData, withdrawals: withdrawalsData, executionWitness: executionWitnessData, + requests: requestsData, } = blockData const header = BlockHeader.fromHeaderData(headerData, opts) @@ -143,7 +160,17 @@ export class Block { // stub till that time const executionWitness = executionWitnessData - return new Block(header, transactions, uncleHeaders, withdrawals, opts, executionWitness) + const requests = requestsData?.map(CLRequest.fromRequestsData) + + return new Block( + header, + transactions, + uncleHeaders, + withdrawals, + opts, + executionWitness, + requests + ) } /** @@ -414,7 +441,8 @@ export class Block { uncleHeaders: BlockHeader[] = [], withdrawals?: Withdrawal[], opts: BlockOptions = {}, - executionWitness?: VerkleExecutionWitness | null + executionWitness?: VerkleExecutionWitness | null, + requests: CLRequest[] = [] ) { this.header = header ?? BlockHeader.fromHeaderData({}, opts) this.common = this.header.common @@ -423,6 +451,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) { @@ -471,6 +500,10 @@ 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`) + } + const freeze = opts?.freeze ?? true if (freeze) { Object.freeze(this) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index 9d25f2a9da..e4a72b8434 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -21,6 +21,7 @@ import { ecsign, equalsBytes, hexToBytes, + toBytes, toType, zeros, } from '@ethereumjs/util' @@ -63,6 +64,7 @@ export class BlockHeader { public readonly blobGasUsed?: bigint public readonly excessBlobGas?: bigint public readonly parentBeaconBlockRoot?: Uint8Array + public readonly requestsRoot?: Uint8Array public readonly common: Common @@ -119,15 +121,15 @@ export class BlockHeader { const headerData = valuesArrayToHeaderData(values) const { number, baseFeePerGas, excessBlobGas, blobGasUsed, parentBeaconBlockRoot } = 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') @@ -222,6 +224,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) ? zeros(32) : undefined, } const baseFeePerGas = @@ -235,6 +238,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 +267,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 +291,7 @@ export class BlockHeader { this.blobGasUsed = blobGasUsed this.excessBlobGas = excessBlobGas this.parentBeaconBlockRoot = parentBeaconBlockRoot + this.requestsRoot = requestsRoot this._genericFormatValidation() this._validateDAOExtraData() @@ -407,6 +417,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) + } + } } /** diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index b2a50b104e..f9ad69a756 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -7,6 +7,8 @@ import type { BytesLike, JsonRpcWithdrawal, PrefixedHexString, + RequestBytes, + RequestData, WithdrawalBytes, WithdrawalData, } from '@ethereumjs/util' @@ -136,6 +138,7 @@ export interface HeaderData { blobGasUsed?: BigIntLike excessBlobGas?: BigIntLike parentBeaconBlockRoot?: BytesLike + requestsRoot?: BytesLike } /** @@ -149,6 +152,7 @@ export interface BlockData { transactions?: Array uncleHeaders?: Array withdrawals?: Array + requests?: Array /** * EIP-6800: Verkle Proof Data (experimental) */ @@ -218,6 +222,7 @@ export interface JsonHeader { blobGasUsed?: PrefixedHexString excessBlobGas?: PrefixedHexString parentBeaconBlockRoot?: PrefixedHexString + requestsRoot?: PrefixedHexString } /* @@ -251,6 +256,7 @@ export interface JsonRpcBlock { excessBlobGas?: PrefixedHexString // If EIP-4844 is enabled for this block, returns the excess blob gas for the block parentBeaconBlockRoot?: PrefixedHexString // 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 // If EIP-7685 is enabled for this block, returns the requests root } export type WithdrawalV1 = { @@ -282,4 +288,5 @@ export type ExecutionPayload = { parentBeaconBlockRoot?: PrefixedHexString // QUANTITY, 64 Bits // VerkleExecutionWitness is already a hex serialized object executionWitness?: VerkleExecutionWitness | null // QUANTITY, 64 Bits, null implies not available + requests?: RequestBytes[] } diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts new file mode 100644 index 0000000000..f6838ac55c --- /dev/null +++ b/packages/block/test/eip7685block.spec.ts @@ -0,0 +1,31 @@ +import { Chain, Common, Hardfork } from '@ethereumjs/common' +import { CLRequest, randomBytes, zeros } from '@ethereumjs/util' +import { assert, describe, it } from 'vitest' + +import { Block, BlockData } from '../src/index.js' + +import type { RequestData } from '@ethereumjs/util' + +const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Shanghai, eips: [7685] }) +describe('7685 tests', () => { + it('should instantiate block with defaults', () => { + const block = Block.fromBlockData({}, { common }) + assert.deepEqual(block.header.requestsRoot, zeros(32)) + const block2 = new Block(undefined, undefined, undefined, undefined, { common }) + assert.deepEqual(block.header.requestsRoot, zeros(32)) + assert.equal(block2.requests?.length, 0) + }) + it('should instantiate a block with requests', async () => { + const request: RequestData = { type: 0x1, data: randomBytes(32) } + const requestsRoot = await Block.genRequestsTrieRoot([CLRequest.fromRequestsData(request)]) + const block = Block.fromBlockData( + { + requests: [request], + header: { requestsRoot }, + }, + { common } + ) + assert.equal(block.requests?.length, 1) + assert.deepEqual(block.header.requestsRoot, requestsRoot) + }) +}) diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 0bf8863cd0..9a0a94eaf3 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.Shanghai, + requiredEIPs: [], + gasPrices: {}, + }, } 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' From 352cd9357f7f12ac1aab165d0892a029e00355f7 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Thu, 25 Apr 2024 12:40:23 -0400 Subject: [PATCH 03/30] Add requests root validation --- packages/block/src/block.ts | 21 +++++++++++++++++++++ packages/block/src/header.ts | 2 +- packages/block/test/eip7685block.spec.ts | 20 ++++++++++++++++---- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 9cbbe319bd..1114922568 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -18,6 +18,7 @@ import { intToHex, isHexPrefixed, } from '@ethereumjs/util' +import { equal } from 'assert' import { keccak256 } from 'ethereum-cryptography/keccak.js' import { executionPayloadFromBeaconPayload } from './from-beacon-payload.js' @@ -66,6 +67,7 @@ export class Block { protected cache: { txTrieRoot?: Uint8Array withdrawalsTrieRoot?: Uint8Array + requestsRoot?: Uint8Array } = {} /** @@ -579,6 +581,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 diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index e4a72b8434..bd6f48fe8e 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -224,7 +224,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) ? zeros(32) : undefined, + requestsRoot: this.common.isActivatedEIP(7685) ? KECCAK256_RLP : undefined, } const baseFeePerGas = diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index f6838ac55c..79c17ac9f7 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -1,8 +1,8 @@ import { Chain, Common, Hardfork } from '@ethereumjs/common' -import { CLRequest, randomBytes, zeros } from '@ethereumjs/util' +import { CLRequest, KECCAK256_RLP, randomBytes } from '@ethereumjs/util' import { assert, describe, it } from 'vitest' -import { Block, BlockData } from '../src/index.js' +import { Block } from '../src/index.js' import type { RequestData } from '@ethereumjs/util' @@ -10,9 +10,9 @@ const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Shanghai, e describe('7685 tests', () => { it('should instantiate block with defaults', () => { const block = Block.fromBlockData({}, { common }) - assert.deepEqual(block.header.requestsRoot, zeros(32)) + assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP) const block2 = new Block(undefined, undefined, undefined, undefined, { common }) - assert.deepEqual(block.header.requestsRoot, zeros(32)) + assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP) assert.equal(block2.requests?.length, 0) }) it('should instantiate a block with requests', async () => { @@ -28,4 +28,16 @@ describe('7685 tests', () => { assert.equal(block.requests?.length, 1) assert.deepEqual(block.header.requestsRoot, requestsRoot) }) + it('RequestsRootIsValid should return false when requestsRoot is invalid', async () => { + const request: RequestData = { type: 0x1, data: randomBytes(32) } + const block = Block.fromBlockData( + { + requests: [request], + header: { requestsRoot: randomBytes(32) }, + }, + { common } + ) + + assert.equal(await block.requestsTrieIsValid(), false) + }) }) From 975ab382e75f926735f05f36189ed6c556614885 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Thu, 25 Apr 2024 12:52:55 -0400 Subject: [PATCH 04/30] Add ordering checks --- packages/block/src/block.ts | 5 +++-- packages/block/src/header.ts | 1 - packages/block/test/eip7685block.spec.ts | 17 ++++++++++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 1114922568..9451fa579f 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -18,7 +18,6 @@ import { intToHex, isHexPrefixed, } from '@ethereumjs/util' -import { equal } from 'assert' import { keccak256 } from 'ethereum-cryptography/keccak.js' import { executionPayloadFromBeaconPayload } from './from-beacon-payload.js' @@ -162,7 +161,9 @@ export class Block { // stub till that time const executionWitness = executionWitnessData - const requests = requestsData?.map(CLRequest.fromRequestsData) + // Requests are sorted in ascending order based on type + // TODO: Decide if we should require requests to be sorted correctly or just do it automatically + const requests = requestsData?.map(CLRequest.fromRequestsData).sort((a, b) => a.type - b.type) return new Block( header, diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index bd6f48fe8e..ab9efaf996 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -21,7 +21,6 @@ import { ecsign, equalsBytes, hexToBytes, - toBytes, toType, zeros, } from '@ethereumjs/util' diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index 79c17ac9f7..659bb48168 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -1,5 +1,5 @@ import { Chain, Common, Hardfork } from '@ethereumjs/common' -import { CLRequest, KECCAK256_RLP, randomBytes } from '@ethereumjs/util' +import { CLRequest, KECCAK256_RLP, bytesToHex, equalsBytes, randomBytes } from '@ethereumjs/util' import { assert, describe, it } from 'vitest' import { Block } from '../src/index.js' @@ -40,4 +40,19 @@ describe('7685 tests', () => { assert.equal(await block.requestsTrieIsValid(), false) }) + it('should produce order requests correctly', async () => { + const request1: RequestData = { type: 0x1, data: '0x1234' } + const request2: RequestData = { type: 0x2, data: '0x2345' } + const requests = [CLRequest.fromRequestsData(request1), CLRequest.fromRequestsData(request2)] + const requestsRoot = await Block.genRequestsTrieRoot(requests) + const block = Block.fromBlockData( + { + requests: [request2, request1], + header: { requestsRoot }, + }, + { common } + ) + assert.equal(block.requests![0].type, 0x1) + assert.equal(bytesToHex(block.requests![1].data), '0x2345') + }) }) From 0f06222712d63c9a1535bf6f888912aba030d3a4 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Thu, 25 Apr 2024 12:54:22 -0400 Subject: [PATCH 05/30] Add note on requestsRoot non-determinism --- packages/block/src/block.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 9451fa579f..0877422643 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -162,6 +162,10 @@ export class Block { const executionWitness = executionWitnessData // Requests are sorted in ascending order based on type + // NOTE: This is a huge issue right now because there's no specific ordering within types so + // the requestsRoot corresponding to the requests is nondeterministic when you have multiple requests + // of the same type since the EIP explicitly does not specify the "intra-type" order + // TODO: Decide if we should require requests to be sorted correctly or just do it automatically const requests = requestsData?.map(CLRequest.fromRequestsData).sort((a, b) => a.type - b.type) From 74369ba4df501aef1dc2bf3f79b08f4cc812935c Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:29:56 -0400 Subject: [PATCH 06/30] Rework request structure to use interface and base class --- packages/block/src/block.ts | 19 +++++------ packages/block/src/types.ts | 4 +-- packages/block/test/eip7685block.spec.ts | 43 +++++++++++++++++++----- packages/util/src/requests.ts | 31 ++++++++--------- packages/util/test/requests.spec.ts | 41 +++++++++++++++++----- 5 files changed, 94 insertions(+), 44 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 0877422643..ecf5d34b32 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -4,7 +4,6 @@ import { Trie } from '@ethereumjs/trie' import { BlobEIP4844Transaction, Capability, TransactionFactory } from '@ethereumjs/tx' import { BIGINT_0, - CLRequest, KECCAK256_RLP, KECCAK256_RLP_ARRAY, Withdrawal, @@ -42,7 +41,7 @@ import type { TxOptions, TypedTransaction, } from '@ethereumjs/tx' -import type { EthersProvider, WithdrawalBytes } from '@ethereumjs/util' +import type { CLRequest, CLRequestType, EthersProvider, WithdrawalBytes } from '@ethereumjs/util' /** * An object that represents the block. @@ -52,7 +51,7 @@ export class Block { public readonly transactions: TypedTransaction[] = [] public readonly uncleHeaders: BlockHeader[] = [] public readonly withdrawals?: Withdrawal[] - public readonly requests?: CLRequest[] + public readonly requests?: CLRequestType[] public readonly common: Common protected keccakFunction: (msg: Uint8Array) => Uint8Array @@ -122,7 +121,7 @@ export class Block { uncleHeaders: uhsData, withdrawals: withdrawalsData, executionWitness: executionWitnessData, - requests: requestsData, + requests: clRequests, } = blockData const header = BlockHeader.fromHeaderData(headerData, opts) @@ -161,13 +160,13 @@ export class Block { // stub till that time const executionWitness = executionWitnessData - // Requests are sorted in ascending order based on type - // NOTE: This is a huge issue right now because there's no specific ordering within types so - // the requestsRoot corresponding to the requests is nondeterministic when you have multiple requests - // of the same type since the EIP explicitly does not specify the "intra-type" order + // Requests are sorted in ascending order based on type and then the internal + // ordering logic defined by the request type - // TODO: Decide if we should require requests to be sorted correctly or just do it automatically - const requests = requestsData?.map(CLRequest.fromRequestsData).sort((a, b) => a.type - b.type) + const requests = clRequests?.sort((a, b) => { + if (a.type !== b.type) return a.type - b.type + return a.greaterThan(b) === true ? 1 : -1 + }) return new Block( header, diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index f9ad69a756..21f114e373 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -5,10 +5,10 @@ import type { AddressLike, BigIntLike, BytesLike, + CLRequest, JsonRpcWithdrawal, PrefixedHexString, RequestBytes, - RequestData, WithdrawalBytes, WithdrawalData, } from '@ethereumjs/util' @@ -152,7 +152,7 @@ export interface BlockData { transactions?: Array uncleHeaders?: Array withdrawals?: Array - requests?: Array + requests?: Array /** * EIP-6800: Verkle Proof Data (experimental) */ diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index 659bb48168..d3b795c315 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -1,10 +1,35 @@ import { Chain, Common, Hardfork } from '@ethereumjs/common' -import { CLRequest, KECCAK256_RLP, bytesToHex, equalsBytes, randomBytes } from '@ethereumjs/util' +import { + CLRequest, + KECCAK256_RLP, + bytesToBigInt, + bytesToHex, + concatBytes, + hexToBytes, + randomBytes, +} from '@ethereumjs/util' import { assert, describe, it } from 'vitest' import { Block } from '../src/index.js' -import type { RequestData } from '@ethereumjs/util' +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) + } + public greaterThan(a: NumberRequest): boolean { + return bytesToBigInt(a.bytes) < bytesToBigInt(this.bytes) + } + + serialize() { + return concatBytes(Uint8Array.from([this.type]), this.bytes) + } +} const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Shanghai, eips: [7685] }) describe('7685 tests', () => { @@ -16,8 +41,8 @@ describe('7685 tests', () => { assert.equal(block2.requests?.length, 0) }) it('should instantiate a block with requests', async () => { - const request: RequestData = { type: 0x1, data: randomBytes(32) } - const requestsRoot = await Block.genRequestsTrieRoot([CLRequest.fromRequestsData(request)]) + const request = new NumberRequest(0x1, randomBytes(32)) + const requestsRoot = await Block.genRequestsTrieRoot([request]) const block = Block.fromBlockData( { requests: [request], @@ -29,7 +54,7 @@ describe('7685 tests', () => { assert.deepEqual(block.header.requestsRoot, requestsRoot) }) it('RequestsRootIsValid should return false when requestsRoot is invalid', async () => { - const request: RequestData = { type: 0x1, data: randomBytes(32) } + const request = new NumberRequest(0x1, randomBytes(32)) const block = Block.fromBlockData( { requests: [request], @@ -41,9 +66,9 @@ describe('7685 tests', () => { assert.equal(await block.requestsTrieIsValid(), false) }) it('should produce order requests correctly', async () => { - const request1: RequestData = { type: 0x1, data: '0x1234' } - const request2: RequestData = { type: 0x2, data: '0x2345' } - const requests = [CLRequest.fromRequestsData(request1), CLRequest.fromRequestsData(request2)] + const request1 = new NumberRequest(0x1, hexToBytes('0x1234')) + const request2 = new NumberRequest(0x1, hexToBytes('0x2345')) + const requests = [request1, request2] const requestsRoot = await Block.genRequestsTrieRoot(requests) const block = Block.fromBlockData( { @@ -53,6 +78,6 @@ describe('7685 tests', () => { { common } ) assert.equal(block.requests![0].type, 0x1) - assert.equal(bytesToHex(block.requests![1].data), '0x2345') + assert.equal(bytesToHex(block.requests![1].bytes), '0x2345') }) }) diff --git a/packages/util/src/requests.ts b/packages/util/src/requests.ts index 473f862c69..4389523e50 100644 --- a/packages/util/src/requests.ts +++ b/packages/util/src/requests.ts @@ -1,29 +1,30 @@ import { concatBytes } from 'ethereum-cryptography/utils' -import { toBytes } from './bytes.js' - -import type { BytesLike } from './types.js' - export type RequestBytes = Uint8Array export interface RequestData { type: number - data: BytesLike + data: Uint8Array +} + +export interface CLRequestType { + readonly type: number + readonly bytes: Uint8Array + greaterThan(a: T): boolean + serialize(): Uint8Array } -export class CLRequest { + +export abstract class CLRequest implements CLRequestType { type: number - data: Uint8Array - constructor(type: number, data?: Uint8Array) { + bytes: Uint8Array = new Uint8Array() + constructor(type: number, bytes: Uint8Array) { if (type === undefined) throw new Error('request type is required') this.type = type - this.data = data ?? new Uint8Array() - } - - public static fromRequestsData = (requestData: RequestData) => { - return new CLRequest(requestData.type, toBytes(requestData.data)) + this.bytes = bytes } + public abstract greaterThan(a: CLRequestType): boolean - serialize = () => { - return concatBytes(Uint8Array.from([this.type]), this.data) + 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 index dba66d9ddd..93aa2e5249 100644 --- a/packages/util/test/requests.spec.ts +++ b/packages/util/test/requests.spec.ts @@ -1,24 +1,49 @@ import { assert, describe, it } from 'vitest' -import { bytesToBigInt, bytesToHex, randomBytes } from '../src/bytes.js' -import { CLRequest } from '../src/requests.js' +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) + } + public greaterThan(a: NumberRequest): boolean { + return bytesToBigInt(a.bytes) < bytesToBigInt(this.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 CLRequest(requestType, data) + 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 = CLRequest.fromRequestsData({ type: 0x1, data: '0x1234' }) + const request1 = NumberRequest.fromRequestData(hexToBytes('0x1234')) assert.equal(request1.type, 0x1) - assert.equal(bytesToHex(request1.data), '0x1234') + assert.equal(bytesToHex(request1.bytes), '0x1234') + + const request2 = NumberRequest.fromRequestData(bigIntToBytes(123n)) + assert.equal(request2.type, 0x1) + assert.equal(bytesToBigInt(request2.bytes), 123n) - const request2 = CLRequest.fromRequestsData({ type: 0x2, data: 123n }) - assert.equal(request2.type, 0x2) - assert.equal(bytesToBigInt(request2.data), 123n) + assert.ok(request1.greaterThan(request2)) }) }) From c1b725ea958f6414ace233baa0caa5c2d4e3867c Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:42:43 -0400 Subject: [PATCH 07/30] Make requests optional --- packages/block/src/block.ts | 2 +- packages/block/test/eip7685block.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index ecf5d34b32..03bf1fef0e 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -448,7 +448,7 @@ export class Block { withdrawals?: Withdrawal[], opts: BlockOptions = {}, executionWitness?: VerkleExecutionWitness | null, - requests: CLRequest[] = [] + requests?: CLRequest[] ) { this.header = header ?? BlockHeader.fromHeaderData({}, opts) this.common = this.header.common diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index d3b795c315..b579463cd9 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -65,7 +65,7 @@ describe('7685 tests', () => { assert.equal(await block.requestsTrieIsValid(), false) }) - it('should produce order requests correctly', async () => { + it('should order requests correctly in block', async () => { const request1 = new NumberRequest(0x1, hexToBytes('0x1234')) const request2 = new NumberRequest(0x1, hexToBytes('0x2345')) const requests = [request1, request2] From 387af7cc7754fd3dacf62d6161ba98d906547f9d Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:51:49 -0400 Subject: [PATCH 08/30] Update ordering test --- packages/block/test/eip7685block.spec.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index b579463cd9..39b89f5491 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -68,16 +68,26 @@ describe('7685 tests', () => { it('should order requests correctly in block', 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] const requestsRoot = await Block.genRequestsTrieRoot(requests) const block = Block.fromBlockData( { - requests: [request2, request1], + requests: [request2, request1, request3], + header: { requestsRoot }, + }, + { common } + ) + const block2 = Block.fromBlockData( + { + requests: [request1, request3, request2], header: { requestsRoot }, }, { common } ) assert.equal(block.requests![0].type, 0x1) assert.equal(bytesToHex(block.requests![1].bytes), '0x2345') + assert.equal(block.requests![2].type, 0x2) + assert.equal(block.requests![2].type, block2.requests![2].type) }) }) From 43ed67db61801fb73eae9c89f8323c98500eafda Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:57:37 -0400 Subject: [PATCH 09/30] Improve tests and remove unnecessary rlp encoding --- packages/block/src/block.ts | 10 +++++++--- packages/block/test/eip7685block.spec.ts | 16 +++++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 03bf1fef0e..0bdcf216c9 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -96,14 +96,18 @@ export class Block { /** * Returns the requests trie root for an array of CLRequests - * @param requests 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) { + const sortedRequests = requests?.sort((a, b) => { + if (a.type !== b.type) return a.type - b.type + return a.greaterThan(b) === true ? 1 : -1 + }) const trie = emptyTrie ?? new Trie() - for (const [i, req] of requests.entries()) { - await trie.put(RLP.encode(i), RLP.encode(req.serialize())) + for (const [i, req] of sortedRequests.entries()) { + await trie.put(RLP.encode(i), req.serialize()) } return trie.root() } diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index 39b89f5491..f25f1fb1f6 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -65,12 +65,15 @@ describe('7685 tests', () => { assert.equal(await block.requestsTrieIsValid(), false) }) - it('should order requests correctly in block', async () => { + it('should order requests correctly in block and produce correct requestsRoot', 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] + const requests = [request1, request2, request3] const requestsRoot = await Block.genRequestsTrieRoot(requests) + + // Construct 2 blocks with differently ordered requests and verify requestsRoot is valid for both + const block = Block.fromBlockData( { requests: [request2, request1, request3], @@ -78,6 +81,9 @@ describe('7685 tests', () => { }, { common } ) + + assert.ok(await block.requestsTrieIsValid()) + const block2 = Block.fromBlockData( { requests: [request1, request3, request2], @@ -85,7 +91,11 @@ describe('7685 tests', () => { }, { common } ) - assert.equal(block.requests![0].type, 0x1) + + assert.ok(await block2.requestsTrieIsValid()) + + // Verifies that requests are in same sort order + assert.deepEqual(block.requests!, block2.requests!) assert.equal(bytesToHex(block.requests![1].bytes), '0x2345') assert.equal(block.requests![2].type, 0x2) assert.equal(block.requests![2].type, block2.requests![2].type) From 643e96e0ce9349588c7ccd42b0d21bdf59c9b1c2 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Thu, 25 Apr 2024 21:55:46 -0400 Subject: [PATCH 10/30] Reorder requests order [no ci] --- packages/block/src/block.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 0bdcf216c9..19a9e20c9e 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -451,8 +451,8 @@ export class Block { uncleHeaders: BlockHeader[] = [], withdrawals?: Withdrawal[], opts: BlockOptions = {}, - executionWitness?: VerkleExecutionWitness | null, - requests?: CLRequest[] + requests?: CLRequest[], + executionWitness?: VerkleExecutionWitness | null ) { this.header = header ?? BlockHeader.fromHeaderData({}, opts) this.common = this.header.common From 70ba18f76981c1034036501d07415c2ee85103b5 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Fri, 26 Apr 2024 13:05:15 -0400 Subject: [PATCH 11/30] Add vm.runBlock --- packages/block/src/block.ts | 4 +- packages/evm/src/evm.ts | 2 +- packages/vm/src/runBlock.ts | 6 +++ packages/vm/test/api/EIPs/eip-7685.spec.ts | 52 ++++++++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) 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 19a9e20c9e..25a7ee5216 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -178,8 +178,8 @@ export class Block { uncleHeaders, withdrawals, opts, - executionWitness, - requests + requests, + executionWitness ) } diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index 5f1e85d65c..787a1e8727 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -209,7 +209,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/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 67154f0d79..4b6b3167af 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -285,6 +285,12 @@ export async function runBlock(this: VM, opts: RunBlockOpts): Promise { + constructor(type: number, bytes: Uint8Array) { + super(type, bytes) + } + + public static fromRequestData(bytes: Uint8Array): CLRequestType { + return new NumberRequest(0x1, bytes) + } + public greaterThan(a: NumberRequest): boolean { + return bytesToBigInt(a.bytes) < bytesToBigInt(this.bytes) + } + + serialize() { + return concatBytes(Uint8Array.from([this.type]), this.bytes) + } +} + +const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Shanghai, eips: [7685] }) + +describe('EIP-7685 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: randomBytes(32) } }, + { common } + ) + await expect(async () => + vm.runBlock({ + block: emptyBlock, + generate: true, + }) + ).rejects.toThrow('invalid requestsRoot') + }) +}) From a5264d9b156b0ecb89682589d4a7292af9a62122 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Fri, 26 Apr 2024 13:19:01 -0400 Subject: [PATCH 12/30] add tests with requests --- packages/vm/test/api/EIPs/eip-7685.spec.ts | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/vm/test/api/EIPs/eip-7685.spec.ts b/packages/vm/test/api/EIPs/eip-7685.spec.ts index 3412c22b2f..30fca75592 100644 --- a/packages/vm/test/api/EIPs/eip-7685.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7685.spec.ts @@ -49,4 +49,32 @@ describe('EIP-7685 tests', () => { }) ).rejects.toThrow('invalid requestsRoot') }) + it('should not 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 } + ) + const res = await vm.runBlock({ block, generate: true }) + assert.equal(res.gasUsed, 0n) + }) + 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: randomBytes(32) }, + }, + { common } + ) + await expect(() => vm.runBlock({ block, generate: true })).rejects.toThrow( + 'invalid requestsRoot' + ) + }) }) From 0bf6387048bb865b054a5e95e6eec620259aaf8b Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Fri, 26 Apr 2024 13:48:00 -0400 Subject: [PATCH 13/30] Add buildblock changes and tests --- packages/vm/src/buildBlock.ts | 12 +++- packages/vm/src/types.ts | 13 ++++- packages/vm/test/api/EIPs/eip-7685.spec.ts | 64 +++++++++++++++++++++- 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/packages/vm/src/buildBlock.ts b/packages/vm/src/buildBlock.ts index 1e57eb8a62..598e4a9950 100644 --- a/packages/vm/src/buildBlock.ts +++ b/packages/vm/src/buildBlock.ts @@ -280,7 +280,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 +289,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 +319,11 @@ export class BlockBuilder { blobGasUsed = this.blobGasUsed } + let requestsRoot = undefined + if (this.vm.common.isActivatedEIP(7685)) { + requestsRoot = await Block.genRequestsTrieRoot(sealOpts?.requests ?? []) + } + const headerData = { ...this.headerData, stateRoot, @@ -327,6 +335,7 @@ export class BlockBuilder { timestamp, // correct excessBlobGas should already be part of headerData used above blobGasUsed, + requestsRoot, } if (consensusType === ConsensusType.ProofOfWork) { @@ -338,6 +347,7 @@ export class BlockBuilder { header: headerData, transactions: this.transactions, withdrawals: this.withdrawals, + requests: sealOpts?.requests, } const block = Block.fromBlockData(blockData, blockOpts) diff --git a/packages/vm/src/types.ts b/packages/vm/src/types.ts index a0471adaf8..3b23cb7600 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 /** @@ -208,6 +214,11 @@ export interface SealBlockOpts { * Overrides the value passed in the constructor. */ mixHash?: Uint8Array + + /** + * An array of CLRequests + */ + requests?: CLRequest[] } /** diff --git a/packages/vm/test/api/EIPs/eip-7685.spec.ts b/packages/vm/test/api/EIPs/eip-7685.spec.ts index 30fca75592..a67327de0d 100644 --- a/packages/vm/test/api/EIPs/eip-7685.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7685.spec.ts @@ -1,9 +1,19 @@ import { Block } from '@ethereumjs/block' +import { Blockchain } from '@ethereumjs/blockchain' import { Chain, Common, Hardfork } from '@ethereumjs/common' -import { CLRequest, bytesToBigInt, concatBytes, randomBytes } from '@ethereumjs/util' +import { + Address, + CLRequest, + KECCAK256_RLP, + bytesToBigInt, + concatBytes, + hexToBytes, + randomBytes, +} from '@ethereumjs/util' import { assert, describe, expect, it } from 'vitest' -import { setupVM } from '../utils.js' +import { VM } from '../../../src/vm.js' +import { setBalance, setupVM } from '../utils.js' import type { CLRequestType } from '@ethereumjs/util' @@ -26,7 +36,7 @@ class NumberRequest extends CLRequest implements CLRequestType { const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Shanghai, eips: [7685] }) -describe('EIP-7685 tests', () => { +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 }) @@ -78,3 +88,51 @@ describe('EIP-7685 tests', () => { ) }) }) + +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.Shanghai, + eips: [7685, 1559, 4895], + }) + 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) + }) + + it('should build a block with a request and a valid requestsRoot', async () => { + const request = new NumberRequest(0x1, randomBytes(32)) + const common = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.Shanghai, + eips: [7685, 1559, 4895], + }) + 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({ requests: [request] }) + + assert.deepEqual(block.requests!.length, 1) + assert.deepEqual(block.header.requestsRoot, await Block.genRequestsTrieRoot([request])) + }) +}) From b118e5df41c69df39864bc018aa3781a0c7399dc Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Fri, 26 Apr 2024 13:54:39 -0400 Subject: [PATCH 14/30] lint --- packages/vm/test/api/EIPs/eip-7685.spec.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/vm/test/api/EIPs/eip-7685.spec.ts b/packages/vm/test/api/EIPs/eip-7685.spec.ts index a67327de0d..252133bf2a 100644 --- a/packages/vm/test/api/EIPs/eip-7685.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7685.spec.ts @@ -1,19 +1,11 @@ import { Block } from '@ethereumjs/block' import { Blockchain } from '@ethereumjs/blockchain' import { Chain, Common, Hardfork } from '@ethereumjs/common' -import { - Address, - CLRequest, - KECCAK256_RLP, - bytesToBigInt, - concatBytes, - hexToBytes, - randomBytes, -} from '@ethereumjs/util' +import { CLRequest, KECCAK256_RLP, bytesToBigInt, concatBytes, randomBytes } from '@ethereumjs/util' import { assert, describe, expect, it } from 'vitest' import { VM } from '../../../src/vm.js' -import { setBalance, setupVM } from '../utils.js' +import { setupVM } from '../utils.js' import type { CLRequestType } from '@ethereumjs/util' From 040f779d28e615bf654bb4dd65c2ee4b3b107a90 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:33:55 -0400 Subject: [PATCH 15/30] remove sorting function --- packages/block/src/block.ts | 19 ++++++------- packages/block/test/eip7685block.spec.ts | 34 +++++++++++------------- packages/util/src/requests.ts | 2 -- packages/util/test/requests.spec.ts | 5 ---- 4 files changed, 25 insertions(+), 35 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 25a7ee5216..bb4bcd8783 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -160,25 +160,26 @@ export class Block { } const withdrawals = withdrawalsData?.map(Withdrawal.fromWithdrawalData) + + // Requests should be sorted in monotonically ascending order based on type + // and whatever internal sorting logic is defined by each request type + if (clRequests !== undefined && clRequests.length > 1) { + for (let x = 1; x < clRequests.length; x++) { + if (clRequests[x].type < clRequests[x - 1].type) + throw new Error('requests are not sorted in ascending order') + } + } // The witness data is planned to come in rlp serialized bytes so leave this // stub till that time const executionWitness = executionWitnessData - // Requests are sorted in ascending order based on type and then the internal - // ordering logic defined by the request type - - const requests = clRequests?.sort((a, b) => { - if (a.type !== b.type) return a.type - b.type - return a.greaterThan(b) === true ? 1 : -1 - }) - return new Block( header, transactions, uncleHeaders, withdrawals, opts, - requests, + clRequests, executionWitness ) } diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index f25f1fb1f6..a4ba0dad8f 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -8,7 +8,8 @@ import { hexToBytes, randomBytes, } from '@ethereumjs/util' -import { assert, describe, it } from 'vitest' +import { rejects } from 'assert' +import { assert, describe, expect, it } from 'vitest' import { Block } from '../src/index.js' @@ -65,18 +66,18 @@ describe('7685 tests', () => { assert.equal(await block.requestsTrieIsValid(), false) }) - it('should order requests correctly in block and produce correct requestsRoot', async () => { + 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 2 blocks with differently ordered requests and verify requestsRoot is valid for both + // Construct block with requests in correct order const block = Block.fromBlockData( { - requests: [request2, request1, request3], + requests, header: { requestsRoot }, }, { common } @@ -84,20 +85,15 @@ describe('7685 tests', () => { assert.ok(await block.requestsTrieIsValid()) - const block2 = Block.fromBlockData( - { - requests: [request1, request3, request2], - header: { requestsRoot }, - }, - { common } - ) - - assert.ok(await block2.requestsTrieIsValid()) - - // Verifies that requests are in same sort order - assert.deepEqual(block.requests!, block2.requests!) - assert.equal(bytesToHex(block.requests![1].bytes), '0x2345') - assert.equal(block.requests![2].type, 0x2) - assert.equal(block.requests![2].type, block2.requests![2].type) + // Throws when requests are not ordered correctly + await expect(async () => + Block.fromBlockData( + { + requests: [request1, request3, request2], + header: { requestsRoot }, + }, + { common } + ) + ).rejects.toThrow('ascending order') }) }) diff --git a/packages/util/src/requests.ts b/packages/util/src/requests.ts index 4389523e50..b3c19e2476 100644 --- a/packages/util/src/requests.ts +++ b/packages/util/src/requests.ts @@ -10,7 +10,6 @@ export interface RequestData { export interface CLRequestType { readonly type: number readonly bytes: Uint8Array - greaterThan(a: T): boolean serialize(): Uint8Array } @@ -22,7 +21,6 @@ export abstract class CLRequest implements CLRequestType { this.type = type this.bytes = bytes } - public abstract greaterThan(a: CLRequestType): boolean 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 index 93aa2e5249..12a6a3fb43 100644 --- a/packages/util/test/requests.spec.ts +++ b/packages/util/test/requests.spec.ts @@ -18,9 +18,6 @@ class NumberRequest extends CLRequest implements CLRequestType { public static fromRequestData(bytes: Uint8Array): CLRequestType { return new NumberRequest(0x1, bytes) } - public greaterThan(a: NumberRequest): boolean { - return bytesToBigInt(a.bytes) < bytesToBigInt(this.bytes) - } serialize() { return concatBytes(Uint8Array.from([this.type]), this.bytes) @@ -43,7 +40,5 @@ describe('should create a request', () => { const request2 = NumberRequest.fromRequestData(bigIntToBytes(123n)) assert.equal(request2.type, 0x1) assert.equal(bytesToBigInt(request2.bytes), 123n) - - assert.ok(request1.greaterThan(request2)) }) }) From fc191bb56213fa7f94cdd7e5f89bceb0e8a5e7ea Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:37:17 -0400 Subject: [PATCH 16/30] Add order check for requests when generating trie --- packages/block/src/block.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index bb4bcd8783..69c2a8c786 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -101,12 +101,16 @@ export class Block { * @returns a 32 byte Uint8Array representing the requests trie root */ public static async genRequestsTrieRoot(requests: CLRequest[], emptyTrie?: Trie) { - const sortedRequests = requests?.sort((a, b) => { - if (a.type !== b.type) return a.type - b.type - return a.greaterThan(b) === true ? 1 : -1 - }) + // 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 sortedRequests.entries()) { + for (const [i, req] of requests.entries()) { await trie.put(RLP.encode(i), req.serialize()) } return trie.root() From b203722aff0e18f67a09e29fdd55e0e2f27a3cb3 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:54:26 -0400 Subject: [PATCH 17/30] More fixes --- packages/block/src/block.ts | 22 +++++++++++++++++++--- packages/block/src/types.ts | 15 ++++++++++++--- packages/block/test/eip7685block.spec.ts | 2 -- packages/blockchain/src/blockchain.ts | 7 +++++++ packages/blockchain/src/db/manager.ts | 12 +++++++++++- packages/util/src/requests.ts | 4 ++-- 6 files changed, 51 insertions(+), 11 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 69c2a8c786..a0b321981d 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,7 @@ import type { TxOptions, TypedTransaction, } from '@ethereumjs/tx' -import type { CLRequest, CLRequestType, EthersProvider, WithdrawalBytes } from '@ethereumjs/util' +import type { CLRequestType, EthersProvider, RequestBytes, WithdrawalBytes } from '@ethereumjs/util' /** * An object that represents the block. @@ -219,7 +220,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 ( @@ -269,6 +271,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 @@ -284,7 +292,15 @@ export class Block { } } - return new Block(header, transactions, uncleHeaders, withdrawals, opts, executionWitness) + return new Block( + header, + transactions, + uncleHeaders, + withdrawals, + opts, + requests, + executionWitness + ) } /** diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 21f114e373..733fe40b3e 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -160,16 +160,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 ] @@ -177,7 +180,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. */ @@ -195,6 +203,8 @@ export interface JsonBlock { transactions?: JsonTx[] uncleHeaders?: JsonHeader[] withdrawals?: JsonRpcWithdrawal[] + // TODO: Define JsonRequest (if possible) + requests?: any | null executionWitness?: VerkleExecutionWitness | null } @@ -255,8 +265,8 @@ export interface JsonRpcBlock { blobGasUsed?: PrefixedHexString // If EIP-4844 is enabled for this block, returns the blob gas used for the block excessBlobGas?: PrefixedHexString // If EIP-4844 is enabled for this block, returns the excess blob gas for the block parentBeaconBlockRoot?: PrefixedHexString // 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 // If EIP-7685 is enabled for this block, returns the requests root + executionWitness?: VerkleExecutionWitness | null // If Verkle is enabled for this block } export type WithdrawalV1 = { @@ -288,5 +298,4 @@ export type ExecutionPayload = { parentBeaconBlockRoot?: PrefixedHexString // QUANTITY, 64 Bits // VerkleExecutionWitness is already a hex serialized object executionWitness?: VerkleExecutionWitness | null // QUANTITY, 64 Bits, null implies not available - requests?: RequestBytes[] } diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index a4ba0dad8f..b67d596f95 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -3,12 +3,10 @@ import { CLRequest, KECCAK256_RLP, bytesToBigInt, - bytesToHex, concatBytes, hexToBytes, randomBytes, } from '@ethereumjs/util' -import { rejects } from 'assert' import { assert, describe, expect, it } from 'vitest' import { Block } from '../src/index.js' diff --git a/packages/blockchain/src/blockchain.ts b/packages/blockchain/src/blockchain.ts index acf4c64d76..90da1d2d22 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,7 @@ 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) + await block.requestsTrieIsValid() } /** * 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..24d0c3c747 100644 --- a/packages/blockchain/src/db/manager.ts +++ b/packages/blockchain/src/db/manager.ts @@ -124,7 +124,17 @@ 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 + 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) body.push([]) } } diff --git a/packages/util/src/requests.ts b/packages/util/src/requests.ts index b3c19e2476..f7db45ab8b 100644 --- a/packages/util/src/requests.ts +++ b/packages/util/src/requests.ts @@ -7,13 +7,13 @@ export interface RequestData { data: Uint8Array } -export interface CLRequestType { +export interface CLRequestType { readonly type: number readonly bytes: Uint8Array serialize(): Uint8Array } -export abstract class CLRequest implements CLRequestType { +export class CLRequest implements CLRequestType { type: number bytes: Uint8Array = new Uint8Array() constructor(type: number, bytes: Uint8Array) { From 6d7e6760505c3042fdcc8c52fd6c2a82d4dc3c3e Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:55:12 -0400 Subject: [PATCH 18/30] remove generic --- packages/block/src/block.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index a0b321981d..cc899fb10a 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -52,7 +52,7 @@ export class Block { public readonly transactions: TypedTransaction[] = [] public readonly uncleHeaders: BlockHeader[] = [] public readonly withdrawals?: Withdrawal[] - public readonly requests?: CLRequestType[] + public readonly requests?: CLRequestType[] public readonly common: Common protected keccakFunction: (msg: Uint8Array) => Uint8Array From de4964c9b04a9805e3baf7035d9d4bf32893d28d Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Fri, 26 Apr 2024 21:02:36 -0400 Subject: [PATCH 19/30] in flight fromValuesArray changes [no ci] --- packages/block/src/header.ts | 16 +++++- packages/block/src/helpers.ts | 2 + packages/block/test/eip7685block.spec.ts | 55 ++++++++++++++----- packages/blockchain/src/blockchain.ts | 2 +- packages/blockchain/src/db/manager.ts | 13 +++-- .../blockchain/test/blockValidation.spec.ts | 32 ++++++++++- 6 files changed, 99 insertions(+), 21 deletions(-) diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index ab9efaf996..d5693a8b3c 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -118,7 +118,14 @@ 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) if (header.common.isActivatedEIP(1559) && baseFeePerGas === undefined) { const eip1559ActivationBlock = bigIntToBytes(header.common.eipBlock(1559)!) @@ -139,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 } /** @@ -709,6 +720,9 @@ export class BlockHeader { if (this.common.isActivatedEIP(4788) === true) { rawItems.push(this.parentBeaconBlockRoot!) } + if (this.common.isActivatedEIP(7685) === true) { + rawItems.push(this.requestsRoot!) + } return rawItems } diff --git a/packages/block/src/helpers.ts b/packages/block/src/helpers.ts index d78a40e8ae..c2c714ec99 100644 --- a/packages/block/src/helpers.ts +++ b/packages/block/src/helpers.ts @@ -44,6 +44,7 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData { blobGasUsed, excessBlobGas, parentBeaconBlockRoot, + requestsRoot, ] = values if (values.length > 20) { @@ -78,6 +79,7 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData { blobGasUsed, excessBlobGas, parentBeaconBlockRoot, + requestsRoot, } } diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index b67d596f95..91901123bd 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -1,29 +1,20 @@ import { Chain, Common, Hardfork } from '@ethereumjs/common' -import { - CLRequest, - KECCAK256_RLP, - bytesToBigInt, - concatBytes, - hexToBytes, - randomBytes, -} from '@ethereumjs/util' +import { CLRequest, KECCAK256_RLP, concatBytes, hexToBytes, randomBytes } from '@ethereumjs/util' +import { bytesToHex } from 'ethereum-cryptography/utils.js' import { assert, describe, expect, it } from 'vitest' -import { Block } from '../src/index.js' +import { Block, BlockHeader } from '../src/index.js' import type { CLRequestType } from '@ethereumjs/util' -class NumberRequest extends CLRequest implements CLRequestType { +class NumberRequest extends CLRequest implements CLRequestType { constructor(type: number, bytes: Uint8Array) { super(type, bytes) } - public static fromRequestData(bytes: Uint8Array): CLRequestType { + public static fromRequestData(bytes: Uint8Array): CLRequestType { return new NumberRequest(0x1, bytes) } - public greaterThan(a: NumberRequest): boolean { - return bytesToBigInt(a.bytes) < bytesToBigInt(this.bytes) - } serialize() { return concatBytes(Uint8Array.from([this.type]), this.bytes) @@ -95,3 +86,39 @@ describe('7685 tests', () => { ).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.equal(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) + console.log(block.requests) + }) +}) diff --git a/packages/blockchain/src/blockchain.ts b/packages/blockchain/src/blockchain.ts index 90da1d2d22..d2eddeb49a 100644 --- a/packages/blockchain/src/blockchain.ts +++ b/packages/blockchain/src/blockchain.ts @@ -705,7 +705,7 @@ 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) - await block.requestsTrieIsValid() + if (block.common.isActivatedEIP(7685)) await block.requestsTrieIsValid() } /** * 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 24d0c3c747..347c8c7414 100644 --- a/packages/blockchain/src/db/manager.ts +++ b/packages/blockchain/src/db/manager.ts @@ -126,15 +126,19 @@ export class DBManager { } if (body.length < 3) body.push([]) } - // If requests root exists, validate that requests + // 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 + !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) body.push([]) + if (body.length < 4) { + for (let x = 0; x < 4 - body.length; x++) { + body.push([]) + } + } } } @@ -145,6 +149,7 @@ export class DBManager { } else { opts.setHardfork = await this.getTotalDifficulty(header.parentHash, number - BIGINT_1) } + console.log(blockData) return Block.fromValuesArray(blockData, opts) } diff --git a/packages/blockchain/test/blockValidation.spec.ts b/packages/blockchain/test/blockValidation.spec.ts index b578d65be4..b63ea36325 100644 --- a/packages/blockchain/test/blockValidation.spec.ts +++ b/packages/blockchain/test/blockValidation.spec.ts @@ -1,7 +1,7 @@ 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' @@ -380,3 +380,33 @@ 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.Shanghai, + eips: [7685, 4844], + }) + const blockchain = await Blockchain.create({ + common, + validateConsensus: false, + validateBlocks: false, + }) + const block = Block.fromBlockData( + { header: { number: 1n, requestsRoot: randomBytes(32), withdrawalsRoot: KECCAK256_RLP } }, + { common } + ) + await blockchain.putBlock(block) + // await expect(async () => blockchain.putBlock(block)).rejects.toThrow('hash of null') + + const blockWithRequest = Block.fromBlockData( + { + header: { number: 1n, requestsRoot: randomBytes(32) }, + requests: [{ type: 0x1, bytes: randomBytes(12), serialize: () => randomBytes(32) }], + }, + { common } + ) + // await expect(async () => blockchain.putBlock(blockWithRequest)).rejects.toThrow('hash of null') + await blockchain.putBlock(blockWithRequest) + }) +}) From 39aa2e046cec61681be8611d85ca089cd5bffe2c Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Fri, 26 Apr 2024 21:20:39 -0400 Subject: [PATCH 20/30] update min hardfork to cancun [no ci] --- packages/block/src/helpers.ts | 2 +- packages/block/test/eip7685block.spec.ts | 12 +++++++----- packages/blockchain/test/blockValidation.spec.ts | 6 +++--- packages/common/src/eips.ts | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/block/src/helpers.ts b/packages/block/src/helpers.ts index c2c714ec99..3668794321 100644 --- a/packages/block/src/helpers.ts +++ b/packages/block/src/helpers.ts @@ -47,7 +47,7 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData { 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}` ) diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index 91901123bd..b89aa94a10 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -1,6 +1,5 @@ import { Chain, Common, Hardfork } from '@ethereumjs/common' import { CLRequest, KECCAK256_RLP, concatBytes, hexToBytes, randomBytes } from '@ethereumjs/util' -import { bytesToHex } from 'ethereum-cryptography/utils.js' import { assert, describe, expect, it } from 'vitest' import { Block, BlockHeader } from '../src/index.js' @@ -21,7 +20,11 @@ class NumberRequest extends CLRequest implements CLRequestType { } } -const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Shanghai, eips: [7685] }) +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 }) @@ -95,7 +98,7 @@ describe('fromValuesArray tests', () => { common, } ) - assert.equal(block.header.requestsRoot, KECCAK256_RLP) + 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')) @@ -117,8 +120,7 @@ describe('fromValuesArray tests', () => { common, } ) - // assert.deepEqual(block.header.requestsRoot, requestsRoot) + assert.deepEqual(block.header.requestsRoot, requestsRoot) assert.equal(block.requests?.length, 3) - console.log(block.requests) }) }) diff --git a/packages/blockchain/test/blockValidation.spec.ts b/packages/blockchain/test/blockValidation.spec.ts index b63ea36325..a935d24d67 100644 --- a/packages/blockchain/test/blockValidation.spec.ts +++ b/packages/blockchain/test/blockValidation.spec.ts @@ -381,11 +381,11 @@ describe('[Blockchain]: Block validation tests', () => { }) }) describe('EIP 7685: requests field validation tests', () => { - it('should throw when putting a block with an invalid requestsRoot', async () => { + it.only('should throw when putting a block with an invalid requestsRoot', async () => { const common = new Common({ chain: Chain.Mainnet, - hardfork: Hardfork.Shanghai, - eips: [7685, 4844], + hardfork: Hardfork.Cancun, + eips: [7685, 1559, 4895, 4844, 4788], }) const blockchain = await Blockchain.create({ common, diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 9a0a94eaf3..25b97001b9 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -547,7 +547,7 @@ export const EIPs: EIPsDict = { url: 'https://eips.ethereum.org/EIPS/eip-7685', status: Status.Draft, // TODO: Set correct minimum hardfork - minimumHardfork: Hardfork.Shanghai, + minimumHardfork: Hardfork.Cancun, requiredEIPs: [], gasPrices: {}, }, From 27817086e3b30f141d41c3da63e0413667763bd4 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Fri, 26 Apr 2024 21:37:25 -0400 Subject: [PATCH 21/30] Throw on invalid requestsRoot [no ci] --- packages/blockchain/src/blockchain.ts | 7 +++- packages/blockchain/src/db/manager.ts | 1 - .../blockchain/test/blockValidation.spec.ts | 34 ++++++++++++++----- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/packages/blockchain/src/blockchain.ts b/packages/blockchain/src/blockchain.ts index d2eddeb49a..b1fb3fb246 100644 --- a/packages/blockchain/src/blockchain.ts +++ b/packages/blockchain/src/blockchain.ts @@ -705,7 +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)) await block.requestsTrieIsValid() + 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 347c8c7414..32e149b8c4 100644 --- a/packages/blockchain/src/db/manager.ts +++ b/packages/blockchain/src/db/manager.ts @@ -149,7 +149,6 @@ export class DBManager { } else { opts.setHardfork = await this.getTotalDifficulty(header.parentHash, number - BIGINT_1) } - console.log(blockData) return Block.fromValuesArray(blockData, opts) } diff --git a/packages/blockchain/test/blockValidation.spec.ts b/packages/blockchain/test/blockValidation.spec.ts index a935d24d67..e0dcb3d63c 100644 --- a/packages/blockchain/test/blockValidation.spec.ts +++ b/packages/blockchain/test/blockValidation.spec.ts @@ -3,7 +3,7 @@ import { Chain, Common, Hardfork } from '@ethereumjs/common' import { RLP } from '@ethereumjs/rlp' 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' @@ -381,7 +381,7 @@ describe('[Blockchain]: Block validation tests', () => { }) }) describe('EIP 7685: requests field validation tests', () => { - it.only('should throw when putting a block with an invalid requestsRoot', async () => { + it('should throw when putting a block with an invalid requestsRoot', async () => { const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Cancun, @@ -390,23 +390,39 @@ describe('EIP 7685: requests field validation tests', () => { const blockchain = await Blockchain.create({ common, validateConsensus: false, - validateBlocks: false, }) const block = Block.fromBlockData( - { header: { number: 1n, requestsRoot: randomBytes(32), withdrawalsRoot: KECCAK256_RLP } }, + { + header: { + number: 1n, + requestsRoot: randomBytes(32), + withdrawalsRoot: KECCAK256_RLP, + parentHash: blockchain.genesisBlock.hash(), + timestamp: blockchain.genesisBlock.header.timestamp + 1n, + gasLimit: 5000, + }, + }, { common } ) - await blockchain.putBlock(block) - // await expect(async () => blockchain.putBlock(block)).rejects.toThrow('hash of null') + + await expect(async () => blockchain.putBlock(block)).rejects.toThrow('invalid requestsRoot') const blockWithRequest = Block.fromBlockData( { - header: { number: 1n, requestsRoot: randomBytes(32) }, + 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('hash of null') - await blockchain.putBlock(blockWithRequest) + await expect(async () => blockchain.putBlock(blockWithRequest)).rejects.toThrow( + 'invalid requestsRoot' + ) }) }) From 52823c21d3413a90933d3087b81adc7c0f4fda24 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Sat, 27 Apr 2024 07:30:05 -0400 Subject: [PATCH 22/30] add scaffolding for pending requests in pendingBlock --- packages/client/src/miner/pendingBlock.ts | 9 ++++++++- packages/vm/src/buildBlock.ts | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/client/src/miner/pendingBlock.ts b/packages/client/src/miner/pendingBlock.ts index 0af8e05a80..55aa3cefb3 100644 --- a/packages/client/src/miner/pendingBlock.ts +++ b/packages/client/src/miner/pendingBlock.ts @@ -288,7 +288,14 @@ export class PendingBlock { ) const { skippedByAddErrors, blobTxs } = await this.addTransactions(builder, txs) - const block = await builder.build() + + // Add pending CL requests + let requests + if (vm.common.isActivatedEIP(7685)) { + // Add pending CL requests by type + } + const block = await builder.build({ requests }) + // Construct blobs bundle const blobs = block.common.isActivatedEIP(4844) ? this.constructBlobsBundle(payloadId, blobTxs) diff --git a/packages/vm/src/buildBlock.ts b/packages/vm/src/buildBlock.ts index 598e4a9950..c3746fc776 100644 --- a/packages/vm/src/buildBlock.ts +++ b/packages/vm/src/buildBlock.ts @@ -322,6 +322,7 @@ export class BlockBuilder { let requestsRoot = undefined if (this.vm.common.isActivatedEIP(7685)) { requestsRoot = await Block.genRequestsTrieRoot(sealOpts?.requests ?? []) + // Do other validations per request type } const headerData = { From a18cd5a57cb69e1202cb757326996f8b016681ee Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Sat, 27 Apr 2024 07:56:07 -0400 Subject: [PATCH 23/30] Update fromRPC constructors and toJSON methods --- packages/block/src/block.ts | 1 + packages/block/src/from-rpc.ts | 8 +++++-- packages/block/src/header-from-rpc.ts | 2 ++ packages/block/src/header.ts | 3 +++ packages/block/src/types.ts | 7 +++--- packages/block/test/eip7685block.spec.ts | 28 ++++++++++++++++++++++++ 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 3ef4190be8..6c98dd0542 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -899,6 +899,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..5f2b8e0594 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' @@ -54,8 +54,12 @@ export function blockFromRpc( const uncleHeaders = uncles.map((uh) => blockHeaderFromRpc(uh, options)) + const requests = blockParams.requests?.map((req) => { + const bytes = hexToBytes(req) + 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 44c58ee662..e99e7135fa 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -990,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/types.ts b/packages/block/src/types.ts index 733fe40b3e..8c4ff19c71 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -203,8 +203,7 @@ export interface JsonBlock { transactions?: JsonTx[] uncleHeaders?: JsonHeader[] withdrawals?: JsonRpcWithdrawal[] - // TODO: Define JsonRequest (if possible) - requests?: any | null + requests?: PrefixedHexString[] | null executionWitness?: VerkleExecutionWitness | null } @@ -265,8 +264,9 @@ export interface JsonRpcBlock { blobGasUsed?: PrefixedHexString // If EIP-4844 is enabled for this block, returns the blob gas used for the block excessBlobGas?: PrefixedHexString // If EIP-4844 is enabled for this block, returns the excess blob gas for the block parentBeaconBlockRoot?: PrefixedHexString // If EIP-4788 is enabled for this block, returns parent beacon block root - requestsRoot?: PrefixedHexString // If EIP-7685 is enabled for this block, returns the requests root executionWitness?: VerkleExecutionWitness | null // If Verkle is enabled for this block + requestsRoot?: PrefixedHexString // 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 = { @@ -298,4 +298,5 @@ export type ExecutionPayload = { parentBeaconBlockRoot?: PrefixedHexString // QUANTITY, 64 Bits // VerkleExecutionWitness is already a hex serialized object executionWitness?: VerkleExecutionWitness | null // QUANTITY, 64 Bits, null implies not available + // TODO: Determine if we need the requestsRoot here } diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index b89aa94a10..265d17de09 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -124,3 +124,31 @@ describe('fromValuesArray tests', () => { 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()) + }) +}) From 96ecc0acf7f6569009f588a9e5a8eb5109a44966 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Sat, 27 Apr 2024 08:18:23 -0400 Subject: [PATCH 24/30] Add requests to JsonRpcBlock --- packages/client/src/rpc/modules/eth.ts | 2 ++ 1 file changed, 2 insertions(+) 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())), } } From 71be096b7b79b81ef23fd7ace38821ff56ed5cd6 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Mon, 29 Apr 2024 12:20:43 -0400 Subject: [PATCH 25/30] update runBlock/buildBlock and tests --- packages/vm/src/buildBlock.ts | 7 +- packages/vm/src/runBlock.ts | 155 ++++++++++++++------- packages/vm/src/types.ts | 14 +- packages/vm/test/api/EIPs/eip-7685.spec.ts | 51 ++----- 4 files changed, 125 insertions(+), 102 deletions(-) diff --git a/packages/vm/src/buildBlock.ts b/packages/vm/src/buildBlock.ts index 968e89ba61..577a1ef7b3 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, @@ -319,9 +320,11 @@ export class BlockBuilder { blobGasUsed = this.blobGasUsed } + const requests = await accumulateRequests(this.vm) + let requestsRoot = undefined if (this.vm.common.isActivatedEIP(7685)) { - requestsRoot = await Block.genRequestsTrieRoot(sealOpts?.requests ?? []) + requestsRoot = await Block.genRequestsTrieRoot(requests) // Do other validations per request type } @@ -348,7 +351,7 @@ export class BlockBuilder { header: headerData, transactions: this.transactions, withdrawals: this.withdrawals, - requests: sealOpts?.requests, + requests, } const block = Block.fromBlockData(blockData, blockOpts) diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 45bee54388..3836c07eb9 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 3b23cb7600..79b859ee64 100644 --- a/packages/vm/src/types.ts +++ b/packages/vm/src/types.ts @@ -214,11 +214,6 @@ export interface SealBlockOpts { * Overrides the value passed in the constructor. */ mixHash?: Uint8Array - - /** - * An array of CLRequests - */ - requests?: CLRequest[] } /** @@ -335,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 index 252133bf2a..066e049e05 100644 --- a/packages/vm/test/api/EIPs/eip-7685.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7685.spec.ts @@ -1,7 +1,7 @@ import { Block } from '@ethereumjs/block' import { Blockchain } from '@ethereumjs/blockchain' import { Chain, Common, Hardfork } from '@ethereumjs/common' -import { CLRequest, KECCAK256_RLP, bytesToBigInt, concatBytes, randomBytes } from '@ethereumjs/util' +import { CLRequest, KECCAK256_RLP, concatBytes, randomBytes } from '@ethereumjs/util' import { assert, describe, expect, it } from 'vitest' import { VM } from '../../../src/vm.js' @@ -9,24 +9,21 @@ import { setupVM } from '../utils.js' import type { CLRequestType } from '@ethereumjs/util' -class NumberRequest extends CLRequest implements CLRequestType { +class NumberRequest extends CLRequest implements CLRequestType { constructor(type: number, bytes: Uint8Array) { super(type, bytes) } - public static fromRequestData(bytes: Uint8Array): CLRequestType { + public static fromRequestData(bytes: Uint8Array): CLRequestType { return new NumberRequest(0x1, bytes) } - public greaterThan(a: NumberRequest): boolean { - return bytesToBigInt(a.bytes) < bytesToBigInt(this.bytes) - } serialize() { return concatBytes(Uint8Array.from([this.type]), this.bytes) } } -const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Shanghai, eips: [7685] }) +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 () => { @@ -47,11 +44,10 @@ describe('EIP-7685 runBlock tests', () => { await expect(async () => vm.runBlock({ block: emptyBlock, - generate: true, }) ).rejects.toThrow('invalid requestsRoot') }) - it('should not error when valid requests are provided', async () => { + 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]) @@ -62,10 +58,9 @@ describe('EIP-7685 runBlock tests', () => { }, { common } ) - const res = await vm.runBlock({ block, generate: true }) - assert.equal(res.gasUsed, 0n) + await expect(async () => vm.runBlock({ block })).rejects.toThrow('invalid block stateRoot') }) - it('should error when requestsRoot does not match requests provided', async () => { + 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( @@ -75,9 +70,7 @@ describe('EIP-7685 runBlock tests', () => { }, { common } ) - await expect(() => vm.runBlock({ block, generate: true })).rejects.toThrow( - 'invalid requestsRoot' - ) + await expect(() => vm.runBlock({ block })).rejects.toThrow('invalid requestsRoot') }) }) @@ -85,8 +78,8 @@ 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.Shanghai, - eips: [7685, 1559, 4895], + hardfork: Hardfork.Cancun, + eips: [7685, 1559, 4895, 4844, 4788], }) const genesisBlock = Block.fromBlockData( { header: { gasLimit: 50000, baseFeePerGas: 100 } }, @@ -103,28 +96,4 @@ describe('EIP 7685 buildBlock tests', () => { assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP) }) - - it('should build a block with a request and a valid requestsRoot', async () => { - const request = new NumberRequest(0x1, randomBytes(32)) - const common = new Common({ - chain: Chain.Mainnet, - hardfork: Hardfork.Shanghai, - eips: [7685, 1559, 4895], - }) - 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({ requests: [request] }) - - assert.deepEqual(block.requests!.length, 1) - assert.deepEqual(block.header.requestsRoot, await Block.genRequestsTrieRoot([request])) - }) }) From 1aa3bb376c59ba8b315dea769593022ca534ca7c Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Mon, 29 Apr 2024 12:50:35 -0400 Subject: [PATCH 26/30] Remove obsolete references --- packages/client/src/miner/pendingBlock.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/client/src/miner/pendingBlock.ts b/packages/client/src/miner/pendingBlock.ts index 55aa3cefb3..6f548849d0 100644 --- a/packages/client/src/miner/pendingBlock.ts +++ b/packages/client/src/miner/pendingBlock.ts @@ -289,12 +289,7 @@ export class PendingBlock { const { skippedByAddErrors, blobTxs } = await this.addTransactions(builder, txs) - // Add pending CL requests - let requests - if (vm.common.isActivatedEIP(7685)) { - // Add pending CL requests by type - } - const block = await builder.build({ requests }) + const block = await builder.build() // Construct blobs bundle const blobs = block.common.isActivatedEIP(4844) From cd235456af0812f144efb9141a500b2aa08bd3a5 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Mon, 29 Apr 2024 13:03:59 -0400 Subject: [PATCH 27/30] fix hex typing --- packages/block/src/from-rpc.ts | 3 ++- packages/block/src/types.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/block/src/from-rpc.ts b/packages/block/src/from-rpc.ts index 5f2b8e0594..cf72da5f49 100644 --- a/packages/block/src/from-rpc.ts +++ b/packages/block/src/from-rpc.ts @@ -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) @@ -55,7 +56,7 @@ export function blockFromRpc( const uncleHeaders = uncles.map((uh) => blockHeaderFromRpc(uh, options)) const requests = blockParams.requests?.map((req) => { - const bytes = hexToBytes(req) + const bytes = hexToBytes(req as PrefixedHexString) return new CLRequest(bytes[0], bytes.slice(1)) }) return Block.fromBlockData( diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 49b145c42d..acb703a274 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -268,8 +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 // 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 + 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 = { From 6f0712667ee9f38a7df53abf3230abe517fddc07 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:18:34 -0400 Subject: [PATCH 28/30] Check for 7685 before adding requests --- packages/block/test/header.spec.ts | 2 +- packages/vm/src/buildBlock.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) 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/vm/src/buildBlock.ts b/packages/vm/src/buildBlock.ts index 577a1ef7b3..fd1418f055 100644 --- a/packages/vm/src/buildBlock.ts +++ b/packages/vm/src/buildBlock.ts @@ -320,10 +320,10 @@ export class BlockBuilder { blobGasUsed = this.blobGasUsed } - const requests = await accumulateRequests(this.vm) - - let requestsRoot = undefined + let requests + let requestsRoot if (this.vm.common.isActivatedEIP(7685)) { + const requests = await accumulateRequests(this.vm) requestsRoot = await Block.genRequestsTrieRoot(requests) // Do other validations per request type } @@ -353,6 +353,7 @@ export class BlockBuilder { withdrawals: this.withdrawals, requests, } + console.log(blockData) const block = Block.fromBlockData(blockData, blockOpts) if (this.blockOpts.putBlockIntoBlockchain === true) { From 9a8258f0886b6278429866fb31734c00a6efe391 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Mon, 29 Apr 2024 21:35:16 -0400 Subject: [PATCH 29/30] address feedback --- packages/block/src/block.ts | 17 ++++++++--------- packages/vm/src/buildBlock.ts | 4 ++-- packages/vm/src/runBlock.ts | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index ebe672ccb6..eb1d243f09 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -171,15 +171,6 @@ export class Block { } const withdrawals = withdrawalsData?.map(Withdrawal.fromWithdrawalData) - - // Requests should be sorted in monotonically ascending order based on type - // and whatever internal sorting logic is defined by each request type - if (clRequests !== undefined && clRequests.length > 1) { - for (let x = 1; x < clRequests.length; x++) { - if (clRequests[x].type < clRequests[x - 1].type) - throw new Error('requests are not sorted in ascending order') - } - } // The witness data is planned to come in rlp serialized bytes so leave this // stub till that time const executionWitness = executionWitnessData @@ -544,6 +535,14 @@ export class Block { 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) diff --git a/packages/vm/src/buildBlock.ts b/packages/vm/src/buildBlock.ts index fd1418f055..ba9f06aade 100644 --- a/packages/vm/src/buildBlock.ts +++ b/packages/vm/src/buildBlock.ts @@ -323,7 +323,7 @@ export class BlockBuilder { let requests let requestsRoot if (this.vm.common.isActivatedEIP(7685)) { - const requests = await accumulateRequests(this.vm) + requests = await accumulateRequests(this.vm) requestsRoot = await Block.genRequestsTrieRoot(requests) // Do other validations per request type } @@ -353,7 +353,7 @@ export class BlockBuilder { withdrawals: this.withdrawals, requests, } - console.log(blockData) + 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 3836c07eb9..8bcba57b2c 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -195,7 +195,7 @@ export async function runBlock(this: VM, opts: RunBlockOpts): Promise Date: Tue, 30 Apr 2024 11:37:24 -0400 Subject: [PATCH 30/30] address feedback --- packages/block/src/block.ts | 3 +++ packages/block/src/types.ts | 2 +- packages/common/src/eips.ts | 2 +- packages/util/src/requests.ts | 2 +- packages/vm/test/api/EIPs/eip-7685.spec.ts | 10 +++++++--- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index eb1d243f09..398712132d 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -389,6 +389,7 @@ export class Block { feeRecipient: coinbase, transactions, withdrawals: withdrawalsData, + requestsRoot, executionWitness, } = payload @@ -408,6 +409,7 @@ export class Block { } } + const reqRoot = requestsRoot === null ? undefined : requestsRoot const transactionsTrie = await Block.genTransactionsTrieRoot( txs, new Trie({ common: opts?.common }) @@ -424,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 diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index acb703a274..f5edac5cad 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -302,5 +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 - // TODO: Determine if we need the requestsRoot here + requestsRoot?: PrefixedHexString | string | null // DATA, 32 bytes, null implies EIP 7685 not active yet } diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 25b97001b9..240c09d83c 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -548,7 +548,7 @@ export const EIPs: EIPsDict = { status: Status.Draft, // TODO: Set correct minimum hardfork minimumHardfork: Hardfork.Cancun, - requiredEIPs: [], + requiredEIPs: [3675], gasPrices: {}, }, } diff --git a/packages/util/src/requests.ts b/packages/util/src/requests.ts index f7db45ab8b..5a27a1e99b 100644 --- a/packages/util/src/requests.ts +++ b/packages/util/src/requests.ts @@ -15,7 +15,7 @@ export interface CLRequestType { export class CLRequest implements CLRequestType { type: number - bytes: Uint8Array = new Uint8Array() + bytes: Uint8Array constructor(type: number, bytes: Uint8Array) { if (type === undefined) throw new Error('request type is required') this.type = type diff --git a/packages/vm/test/api/EIPs/eip-7685.spec.ts b/packages/vm/test/api/EIPs/eip-7685.spec.ts index 066e049e05..ce097778be 100644 --- a/packages/vm/test/api/EIPs/eip-7685.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7685.spec.ts @@ -1,7 +1,7 @@ import { Block } from '@ethereumjs/block' import { Blockchain } from '@ethereumjs/blockchain' import { Chain, Common, Hardfork } from '@ethereumjs/common' -import { CLRequest, KECCAK256_RLP, concatBytes, randomBytes } from '@ethereumjs/util' +import { CLRequest, KECCAK256_RLP, concatBytes, hexToBytes, randomBytes } from '@ethereumjs/util' import { assert, describe, expect, it } from 'vitest' import { VM } from '../../../src/vm.js' @@ -9,6 +9,9 @@ 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) @@ -37,8 +40,9 @@ describe('EIP-7685 runBlock tests', () => { }) it('should error when an invalid requestsRoot is provided', async () => { const vm = await setupVM({ common }) + const emptyBlock = Block.fromBlockData( - { header: { requestsRoot: randomBytes(32) } }, + { header: { requestsRoot: invalidRequestsRoot } }, { common } ) await expect(async () => @@ -66,7 +70,7 @@ describe('EIP-7685 runBlock tests', () => { const block = Block.fromBlockData( { requests: [request], - header: { requestsRoot: randomBytes(32) }, + header: { requestsRoot: invalidRequestsRoot }, }, { common } )