diff --git a/config/cspell-ts.json b/config/cspell-ts.json index 454adfe0b5..f50bc6c720 100644 --- a/config/cspell-ts.json +++ b/config/cspell-ts.json @@ -12,6 +12,7 @@ } ], "words": [ + "EEST", "paulmillr", "t8ntool", "!Json", diff --git a/packages/block/CHANGELOG.md b/packages/block/CHANGELOG.md index 6424716c49..c24a5d79dc 100644 --- a/packages/block/CHANGELOG.md +++ b/packages/block/CHANGELOG.md @@ -26,6 +26,7 @@ import { type CLRequest, type CLRequestType, } from '@ethereumjs/util' +import { keccak256 } from 'ethereum-cryptography/keccak.js' const main = async () => { const common = new Common({ @@ -42,7 +43,7 @@ const main = async () => { } const request = DepositRequest.fromRequestData(depositRequestData) as CLRequest const requests = [request] - const requestsRoot = await Block.genRequestsTrieRoot(requests) + const requestsRoot = await Block.genRequestsRoot(requests, keccak256) const block = Block.fromBlockData( { @@ -77,6 +78,7 @@ import { type CLRequest, type CLRequestType, } from '@ethereumjs/util' +import { keccak256 } from 'ethereum-cryptography/keccak.js' const main = async () => { const common = new Common({ @@ -93,7 +95,7 @@ const main = async () => { withdrawalRequestData, ) as CLRequest const requests = [request] - const requestsRoot = await Block.genRequestsTrieRoot(requests) + const requestsRoot = await Block.genRequestsRoot(requests, keccak256) const block = Block.fromBlockData( { @@ -130,6 +132,7 @@ import { type CLRequest, type CLRequestType, } from '@ethereumjs/util' +import { keccak256 } from 'ethereum-cryptography/keccak.js' const main = async () => { const common = new Common({ @@ -146,7 +149,7 @@ const main = async () => { consolidationRequestData, ) as CLRequest const requests = [request] - const requestsRoot = await Block.genRequestsTrieRoot(requests) + const requestsRoot = await Block.genRequestsRoot(requests, keccak256) const block = Block.fromBlockData( { diff --git a/packages/block/README.md b/packages/block/README.md index b3558cfcd0..b303ca27fa 100644 --- a/packages/block/README.md +++ b/packages/block/README.md @@ -245,15 +245,16 @@ Starting with v5.3.0 this library supports requests to the consensus layer which ```ts // ./examples/6110Requests.ts -import { createBlock, genRequestsTrieRoot } from '@ethereumjs/block' +import { createBlock, genRequestsRoot } from '@ethereumjs/block' import { Common, Hardfork, Mainnet } from '@ethereumjs/common' import { type CLRequest, - type CLRequestType, - bytesToBigInt, - createDepositRequest, + CLRequestType, + bytesToHex, + createCLRequest, randomBytes, } from '@ethereumjs/util' +import { sha256 } from 'ethereum-cryptography/sha256.js' const main = async () => { const common = new Common({ @@ -264,26 +265,29 @@ const main = async () => { const depositRequestData = { pubkey: randomBytes(48), withdrawalCredentials: randomBytes(32), - amount: bytesToBigInt(randomBytes(8)), + amount: randomBytes(8), signature: randomBytes(96), - index: bytesToBigInt(randomBytes(8)), + index: randomBytes(8), } - const request = createDepositRequest(depositRequestData) as CLRequest + // flatten request bytes as per EIP-7685 + const depositRequestBytes = new Uint8Array( + Object.values(depositRequestData) + .map((arr) => Array.from(arr)) // Convert Uint8Arrays to regular arrays + .reduce((acc, curr) => acc.concat(curr), []), // Concatenate arrays + ) + const request = createCLRequest( + new Uint8Array([CLRequestType.Deposit, ...depositRequestBytes]), + ) as CLRequest const requests = [request] - const requestsRoot = await genRequestsTrieRoot(requests) + const requestsRoot = genRequestsRoot(requests, sha256) const block = createBlock( { - requests, - header: { requestsRoot }, + header: { requestsHash: requestsRoot }, }, { common }, ) - console.log( - `Instantiated block with ${ - block.requests?.length - } deposit request, requestTrieValid=${await block.requestsTrieIsValid()}`, - ) + console.log(`Instantiated block ${block}, requestsHash=${bytesToHex(block.header.requestsHash!)}`) } void main() @@ -298,7 +302,7 @@ Have a look at the EIP for some guidance on how to use and fill in the various d ```ts // ./examples/7002Requests.ts -import { createBlock, genRequestsTrieRoot } from '@ethereumjs/block' +import { createBlock, genRequestsRoot } from '@ethereumjs/block' import { Common, Hardfork, Mainnet } from '@ethereumjs/common' import { type CLRequest, @@ -307,6 +311,7 @@ import { createWithdrawalRequest, randomBytes, } from '@ethereumjs/util' +import { sha256 } from 'ethereum-cryptography/keccak.js' const main = async () => { const common = new Common({ @@ -321,7 +326,7 @@ const main = async () => { } const request = createWithdrawalRequest(withdrawalRequestData) as CLRequest const requests = [request] - const requestsRoot = await genRequestsTrieRoot(requests) + const requestsRoot = genRequestsRoot(requests, sha256) const block = createBlock( { @@ -349,7 +354,7 @@ Have a look at the EIP for some guidance on how to use and fill in the various w ```ts // ./examples/7251Requests.ts -import { createBlock, genRequestsTrieRoot } from '@ethereumjs/block' +import { createBlock, genRequestsRoot } from '@ethereumjs/block' import { Common, Hardfork, Mainnet } from '@ethereumjs/common' import { type CLRequest, @@ -357,6 +362,7 @@ import { createConsolidationRequest, randomBytes, } from '@ethereumjs/util' +import { sha256 } from 'ethereum-cryptography/keccak.js' const main = async () => { const common = new Common({ @@ -371,7 +377,7 @@ const main = async () => { } const request = createConsolidationRequest(consolidationRequestData) as CLRequest const requests = [request] - const requestsRoot = await genRequestsTrieRoot(requests) + const requestsRoot = genRequestsRoot(requests, sha256) const block = createBlock( { diff --git a/packages/block/examples/6110Requests.ts b/packages/block/examples/6110Requests.ts deleted file mode 100644 index 8ecd10b796..0000000000 --- a/packages/block/examples/6110Requests.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createBlock, genRequestsTrieRoot } from '@ethereumjs/block' -import { Common, Hardfork, Mainnet } from '@ethereumjs/common' -import { - type CLRequest, - type CLRequestType, - bytesToBigInt, - createDepositRequest, - randomBytes, -} from '@ethereumjs/util' - -const main = async () => { - const common = new Common({ - chain: Mainnet, - hardfork: Hardfork.Prague, - }) - - const depositRequestData = { - pubkey: randomBytes(48), - withdrawalCredentials: randomBytes(32), - amount: bytesToBigInt(randomBytes(8)), - signature: randomBytes(96), - index: bytesToBigInt(randomBytes(8)), - } - const request = createDepositRequest(depositRequestData) as CLRequest - const requests = [request] - const requestsRoot = await genRequestsTrieRoot(requests) - - const block = createBlock( - { - requests, - header: { requestsRoot }, - }, - { common }, - ) - console.log( - `Instantiated block with ${ - block.requests?.length - } deposit request, requestTrieValid=${await block.requestsTrieIsValid()}`, - ) -} - -void main() diff --git a/packages/block/examples/7002Requests.ts b/packages/block/examples/7002Requests.ts deleted file mode 100644 index 8a383ebc38..0000000000 --- a/packages/block/examples/7002Requests.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { createBlock, genRequestsTrieRoot } from '@ethereumjs/block' -import { Common, Hardfork, Mainnet } from '@ethereumjs/common' -import { - type CLRequest, - type CLRequestType, - bytesToBigInt, - createWithdrawalRequest, - randomBytes, -} from '@ethereumjs/util' - -const main = async () => { - const common = new Common({ - chain: Mainnet, - hardfork: Hardfork.Prague, - }) - - const withdrawalRequestData = { - sourceAddress: randomBytes(20), - validatorPubkey: randomBytes(48), - amount: bytesToBigInt(randomBytes(8)), - } - const request = createWithdrawalRequest(withdrawalRequestData) as CLRequest - const requests = [request] - const requestsRoot = await genRequestsTrieRoot(requests) - - const block = createBlock( - { - requests, - header: { requestsRoot }, - }, - { common }, - ) - console.log( - `Instantiated block with ${ - block.requests?.length - } withdrawal request, requestTrieValid=${await block.requestsTrieIsValid()}`, - ) -} - -void main() diff --git a/packages/block/examples/7251Requests.ts b/packages/block/examples/7251Requests.ts deleted file mode 100644 index a8e1f1fc9c..0000000000 --- a/packages/block/examples/7251Requests.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createBlock, genRequestsTrieRoot } from '@ethereumjs/block' -import { Common, Hardfork, Mainnet } from '@ethereumjs/common' -import { - type CLRequest, - type CLRequestType, - createConsolidationRequest, - randomBytes, -} from '@ethereumjs/util' - -const main = async () => { - const common = new Common({ - chain: Mainnet, - hardfork: Hardfork.Prague, - }) - - const consolidationRequestData = { - sourceAddress: randomBytes(20), - sourcePubkey: randomBytes(48), - targetPubkey: randomBytes(48), - } - const request = createConsolidationRequest(consolidationRequestData) as CLRequest - const requests = [request] - const requestsRoot = await genRequestsTrieRoot(requests) - - const block = createBlock( - { - requests, - header: { requestsRoot }, - }, - { common }, - ) - console.log( - `Instantiated block with ${ - block.requests?.length - } consolidation request, requestTrieValid=${await block.requestsTrieIsValid()}`, - ) -} - -void main() diff --git a/packages/block/src/block/block.ts b/packages/block/src/block/block.ts index 38b321759b..00467923c2 100644 --- a/packages/block/src/block/block.ts +++ b/packages/block/src/block/block.ts @@ -4,13 +4,13 @@ import { RLP } from '@ethereumjs/rlp' import { Blob4844Tx, Capability } from '@ethereumjs/tx' import { BIGINT_0, - CLRequestType, KECCAK256_RLP, KECCAK256_RLP_ARRAY, bytesToHex, equalsBytes, } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak.js' +import { sha256 } from 'ethereum-cryptography/sha256' /* eslint-disable */ // This is to allow for a proper and linked collection of constructors for the class header. @@ -19,7 +19,6 @@ import { keccak256 } from 'ethereum-cryptography/keccak.js' // See: https://github.com/microsoft/TypeScript/issues/47558 // (situation will eventually improve on Typescript and/or Eslint update) import { - genRequestsTrieRoot, genTransactionsTrieRoot, genWithdrawalsTrieRoot, BlockHeader, @@ -35,14 +34,7 @@ import { import type { BlockBytes, BlockOptions, ExecutionPayload, JSONBlock } from '../types.js' import type { Common } from '@ethereumjs/common' import type { FeeMarket1559Tx, LegacyTx, TypedTransaction } from '@ethereumjs/tx' -import type { - CLRequest, - ConsolidationRequest, - DepositRequest, - VerkleExecutionWitness, - Withdrawal, - WithdrawalRequest, -} from '@ethereumjs/util' +import type { VerkleExecutionWitness, Withdrawal } from '@ethereumjs/util' /** * Class representing a block in the Ethereum network. The {@link BlockHeader} has its own @@ -65,9 +57,9 @@ 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 + protected sha256Function: (msg: Uint8Array) => Uint8Array /** * EIP-6800: Verkle Proof Data (experimental) @@ -79,7 +71,6 @@ export class Block { protected cache: { txTrieRoot?: Uint8Array withdrawalsTrieRoot?: Uint8Array - requestsRoot?: Uint8Array } = {} /** @@ -94,17 +85,16 @@ export class Block { uncleHeaders: BlockHeader[] = [], withdrawals?: Withdrawal[], opts: BlockOptions = {}, - requests?: CLRequest[], executionWitness?: VerkleExecutionWitness | null, ) { this.header = header ?? new BlockHeader({}, opts) this.common = this.header.common this.keccakFunction = this.common.customCrypto.keccak256 ?? keccak256 + this.sha256Function = this.common.customCrypto.sha256 ?? sha256 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) { @@ -155,18 +145,6 @@ export class Block { throw new Error(`Cannot have executionWitness field if EIP 6800 is not active `) } - if (!this.common.isActivatedEIP(7685) && requests !== undefined) { - throw new Error(`Cannot have requests field if EIP 7685 is not active`) - } - - // Requests should be sorted in monotonically ascending order based on type - // and whatever internal sorting logic is defined by each request type - if (requests !== undefined && requests.length > 1) { - for (let x = 1; x < requests.length; x++) { - if (requests[x].type < requests[x - 1].type) - throw new Error('requests are not sorted in ascending order') - } - } const freeze = opts?.freeze ?? true if (freeze) { Object.freeze(this) @@ -189,11 +167,6 @@ export class Block { bytesArray.push(withdrawalsRaw) } - const requestsRaw = this.requests?.map((req) => req.serialize()) - if (requestsRaw) { - bytesArray.push(requestsRaw) - } - if (this.executionWitness !== undefined && this.executionWitness !== null) { const executionWitnessBytes = RLP.encode(JSON.stringify(this.executionWitness)) bytesArray.push(executionWitnessBytes as any) @@ -251,27 +224,6 @@ export class Block { return result } - async requestsTrieIsValid(requestsInput?: CLRequest[]): Promise { - if (!this.common.isActivatedEIP(7685)) { - throw new Error('EIP 7685 is not activated') - } - - const requests = requestsInput ?? this.requests! - - if (requests!.length === 0) { - return equalsBytes(this.header.requestsRoot!, KECCAK256_RLP) - } - - if (requestsInput === undefined) { - if (this.cache.requestsRoot === undefined) { - this.cache.requestsRoot = await genRequestsTrieRoot(this.requests!) - } - return equalsBytes(this.cache.requestsRoot, this.header.requestsRoot!) - } else { - const reportedRoot = await genRequestsTrieRoot(requests) - return equalsBytes(reportedRoot, this.header.requestsRoot!) - } - } /** * Validates transaction signatures and minimum gas requirements. * @returns {string[]} an array of error strings @@ -534,7 +486,6 @@ 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())), } } @@ -569,36 +520,8 @@ export class Block { transactions, ...withdrawalsArr, parentBeaconBlockRoot: header.parentBeaconBlockRoot, + requestsHash: header.requestsHash, executionWitness: this.executionWitness, - - // lets add the request fields first and then iterate over requests to fill them up - depositRequests: this.common.isActivatedEIP(6110) ? [] : undefined, - withdrawalRequests: this.common.isActivatedEIP(7002) ? [] : undefined, - consolidationRequests: this.common.isActivatedEIP(7251) ? [] : undefined, - } - - if (this.requests !== undefined) { - for (const request of this.requests) { - switch (request.type) { - case CLRequestType.Deposit: - executionPayload.depositRequests!.push((request as DepositRequest).toJSON()) - continue - - case CLRequestType.Withdrawal: - executionPayload.withdrawalRequests!.push((request as WithdrawalRequest).toJSON()) - continue - - case CLRequestType.Consolidation: - executionPayload.consolidationRequests!.push((request as ConsolidationRequest).toJSON()) - continue - } - } - } else if ( - executionPayload.depositRequests !== undefined || - executionPayload.withdrawalRequests !== undefined || - executionPayload.consolidationRequests !== undefined - ) { - throw Error(`Undefined requests for activated deposit or withdrawal requests`) } return executionPayload diff --git a/packages/block/src/block/constructors.ts b/packages/block/src/block/constructors.ts index 511e5835f1..86920243db 100644 --- a/packages/block/src/block/constructors.ts +++ b/packages/block/src/block/constructors.ts @@ -8,14 +8,10 @@ import { normalizeTxParams, } from '@ethereumjs/tx' import { - CLRequestFactory, bigIntToHex, bytesToHex, bytesToUtf8, - createConsolidationRequestFromJSON, - createDepositRequestFromJSON, createWithdrawal, - createWithdrawalRequestFromJSON, equalsBytes, fetchFromProvider, getProvider, @@ -25,7 +21,7 @@ import { } from '@ethereumjs/util' import { generateCliqueBlockExtraData } from '../consensus/clique.js' -import { genRequestsTrieRoot, genTransactionsTrieRoot, genWithdrawalsTrieRoot } from '../helpers.js' +import { genTransactionsTrieRoot, genWithdrawalsTrieRoot } from '../helpers.js' import { Block, createBlockHeader, @@ -43,18 +39,10 @@ import type { ExecutionWitnessBytes, HeaderData, JSONRPCBlock, - RequestsBytes, WithdrawalsBytes, } from '../types.js' import type { TypedTransaction } from '@ethereumjs/tx' -import type { - CLRequest, - CLRequestType, - EthersProvider, - PrefixedHexString, - RequestBytes, - WithdrawalBytes, -} from '@ethereumjs/util' +import type { EthersProvider, PrefixedHexString, WithdrawalBytes } from '@ethereumjs/util' /** * Static constructor to create a block from a block data dictionary @@ -69,7 +57,6 @@ export function createBlock(blockData: BlockData = {}, opts?: BlockOptions) { uncleHeaders: uhsData, withdrawals: withdrawalsData, executionWitness: executionWitnessData, - requests: clRequests, } = blockData const header = createBlockHeader(headerData, opts) @@ -108,15 +95,7 @@ export function createBlock(blockData: BlockData = {}, opts?: BlockOptions) { // stub till that time const executionWitness = executionWitnessData - return new Block( - header, - transactions, - uncleHeaders, - withdrawals, - opts, - clRequests, - executionWitness, - ) + return new Block(header, transactions, uncleHeaders, withdrawals, opts, executionWitness) } /** @@ -151,9 +130,6 @@ export function createBlockFromBytesArray(values: BlockBytes, opts?: BlockOption const withdrawalBytes = header.common.isActivatedEIP(4895) ? (valuesTail.splice(0, 1)[0] as WithdrawalsBytes) : undefined - const requestBytes = header.common.isActivatedEIP(7685) - ? (valuesTail.splice(0, 1)[0] as RequestsBytes) - : undefined // if witness bytes are not present that we should assume that witness has not been provided // in that scenario pass null as undefined is used for default witness assignment const executionWitnessBytes = header.common.isActivatedEIP(6800) @@ -169,16 +145,6 @@ export function createBlockFromBytesArray(values: BlockBytes, opts?: BlockOption ) } - if ( - header.common.isActivatedEIP(7685) && - (requestBytes === undefined || !Array.isArray(requestBytes)) - ) { - throw new Error( - 'Invalid serialized block input: EIP-7685 is active, and no requestBytes were provided as array', - ) - } - - // TODO: Decide if this check is necessary or not if (header.common.isActivatedEIP(6800) && executionWitnessBytes === undefined) { throw new Error( 'Invalid serialized block input: EIP-6800 is active, and execution witness is undefined', @@ -223,12 +189,6 @@ export function createBlockFromBytesArray(values: BlockBytes, opts?: BlockOption })) ?.map(createWithdrawal) - let requests - if (header.common.isActivatedEIP(7685)) { - requests = (requestBytes as RequestBytes[]).map((bytes) => - CLRequestFactory.fromSerializedRequest(bytes), - ) - } // 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 @@ -244,15 +204,7 @@ export function createBlockFromBytesArray(values: BlockBytes, opts?: BlockOption } } - return new Block( - header, - transactions, - uncleHeaders, - withdrawals, - opts, - requests, - executionWitness, - ) + return new Block(header, transactions, uncleHeaders, withdrawals, opts, executionWitness) } /** @@ -295,12 +247,8 @@ export function createBlockFromRPC( const uncleHeaders = uncles.map((uh) => createBlockHeaderFromRPC(uh, options)) - const requests = blockParams.requests?.map((req) => { - const bytes = hexToBytes(req as PrefixedHexString) - return CLRequestFactory.fromSerializedRequest(bytes) - }) return createBlock( - { header, transactions, uncleHeaders, withdrawals: blockParams.withdrawals, requests }, + { header, transactions, uncleHeaders, withdrawals: blockParams.withdrawals }, options, ) } @@ -383,9 +331,6 @@ export async function createBlockFromExecutionPayload( feeRecipient: coinbase, transactions, withdrawals: withdrawalsData, - depositRequests, - withdrawalRequests, - consolidationRequests, executionWitness, } = payload @@ -411,36 +356,6 @@ export async function createBlockFromExecutionPayload( ? await genWithdrawalsTrieRoot(withdrawals, new MerklePatriciaTrie({ common: opts?.common })) : undefined - const hasDepositRequests = depositRequests !== undefined && depositRequests !== null - const hasWithdrawalRequests = withdrawalRequests !== undefined && withdrawalRequests !== null - const hasConsolidationRequests = - consolidationRequests !== undefined && consolidationRequests !== null - - const requests = - hasDepositRequests || hasWithdrawalRequests || hasConsolidationRequests - ? ([] as CLRequest[]) - : undefined - - if (depositRequests !== undefined && depositRequests !== null) { - for (const dJSON of depositRequests) { - requests!.push(createDepositRequestFromJSON(dJSON)) - } - } - if (withdrawalRequests !== undefined && withdrawalRequests !== null) { - for (const wJSON of withdrawalRequests) { - requests!.push(createWithdrawalRequestFromJSON(wJSON)) - } - } - if (consolidationRequests !== undefined && consolidationRequests !== null) { - for (const cJSON of consolidationRequests) { - requests!.push(createConsolidationRequestFromJSON(cJSON)) - } - } - - const requestsRoot = requests - ? await genRequestsTrieRoot(requests, new MerklePatriciaTrie({ common: opts?.common })) - : undefined - const header: HeaderData = { ...payload, number, @@ -449,14 +364,10 @@ export async function createBlockFromExecutionPayload( withdrawalsRoot, mixHash, coinbase, - requestsRoot, } // we are not setting setHardfork as common is already set to the correct hf - const block = createBlock( - { header, transactions: txs, withdrawals, executionWitness, requests }, - opts, - ) + const block = createBlock({ header, transactions: txs, withdrawals, executionWitness }, opts) if ( block.common.isActivatedEIP(6800) && (executionWitness === undefined || executionWitness === null) diff --git a/packages/block/src/from-beacon-payload.ts b/packages/block/src/from-beacon-payload.ts index d6d93467f6..e83ea9f780 100644 --- a/packages/block/src/from-beacon-payload.ts +++ b/packages/block/src/from-beacon-payload.ts @@ -10,26 +10,6 @@ type BeaconWithdrawal = { amount: PrefixedHexString } -type BeaconDepositRequest = { - pubkey: PrefixedHexString - withdrawal_credentials: PrefixedHexString - amount: PrefixedHexString - signature: PrefixedHexString - index: PrefixedHexString -} - -type BeaconWithdrawalRequest = { - source_address: PrefixedHexString - validator_pubkey: PrefixedHexString - amount: PrefixedHexString -} - -type BeaconConsolidationRequest = { - source_address: PrefixedHexString - source_pubkey: PrefixedHexString - target_pubkey: PrefixedHexString -} - // Payload JSON that one gets using the beacon apis // curl localhost:5052/eth/v2/beacon/blocks/56610 | jq .data.message.body.execution_payload export type BeaconPayloadJSON = { @@ -51,11 +31,7 @@ export type BeaconPayloadJSON = { blob_gas_used?: NumericString excess_blob_gas?: NumericString parent_beacon_block_root?: PrefixedHexString - // requests data - deposit_requests?: BeaconDepositRequest[] - withdrawal_requests?: BeaconWithdrawalRequest[] - consolidation_requests?: BeaconConsolidationRequest[] - + requests_hash?: PrefixedHexString // the casing of VerkleExecutionWitness remains same camel case for now execution_witness?: VerkleExecutionWitness } @@ -156,32 +132,8 @@ export function executionPayloadFromBeaconPayload(payload: BeaconPayloadJSON): E if (payload.parent_beacon_block_root !== undefined && payload.parent_beacon_block_root !== null) { executionPayload.parentBeaconBlockRoot = payload.parent_beacon_block_root } - - // requests - if (payload.deposit_requests !== undefined && payload.deposit_requests !== null) { - executionPayload.depositRequests = payload.deposit_requests.map((beaconRequest) => ({ - pubkey: beaconRequest.pubkey, - withdrawalCredentials: beaconRequest.withdrawal_credentials, - amount: beaconRequest.amount, - signature: beaconRequest.signature, - index: beaconRequest.index, - })) - } - if (payload.withdrawal_requests !== undefined && payload.withdrawal_requests !== null) { - executionPayload.withdrawalRequests = payload.withdrawal_requests.map((beaconRequest) => ({ - sourceAddress: beaconRequest.source_address, - validatorPubkey: beaconRequest.validator_pubkey, - amount: beaconRequest.amount, - })) - } - if (payload.consolidation_requests !== undefined && payload.consolidation_requests !== null) { - executionPayload.consolidationRequests = payload.consolidation_requests.map( - (beaconRequest) => ({ - sourceAddress: beaconRequest.source_address, - sourcePubkey: beaconRequest.source_pubkey, - targetPubkey: beaconRequest.target_pubkey, - }), - ) + if (payload.requests_hash !== undefined && payload.requests_hash !== null) { + executionPayload.requestsHash = payload.requests_hash } if (payload.execution_witness !== undefined && payload.execution_witness !== null) { diff --git a/packages/block/src/header/constructors.ts b/packages/block/src/header/constructors.ts index 97222b970d..2cf6891e53 100644 --- a/packages/block/src/header/constructors.ts +++ b/packages/block/src/header/constructors.ts @@ -25,7 +25,7 @@ export function createBlockHeader(headerData: HeaderData = {}, opts: BlockOption */ export function createBlockHeaderFromBytesArray(values: BlockHeaderBytes, opts: BlockOptions = {}) { const headerData = valuesArrayToHeaderData(values) - const { number, baseFeePerGas, excessBlobGas, blobGasUsed, parentBeaconBlockRoot, requestsRoot } = + const { number, baseFeePerGas, excessBlobGas, blobGasUsed, parentBeaconBlockRoot, requestsHash } = headerData const header = createBlockHeader(headerData, opts) if (header.common.isActivatedEIP(1559) && baseFeePerGas === undefined) { @@ -48,8 +48,8 @@ export function createBlockHeaderFromBytesArray(values: BlockHeaderBytes, opts: 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') + if (header.common.isActivatedEIP(7685) && requestsHash === undefined) { + throw new Error('invalid header. requestsHash should be provided') } return header } @@ -118,7 +118,7 @@ export function createBlockHeaderFromRPC(blockParams: JSONRPCBlock, options?: Bl blobGasUsed, excessBlobGas, parentBeaconBlockRoot, - requestsRoot, + requestsHash, } = blockParams const blockHeader = new BlockHeader( @@ -143,7 +143,7 @@ export function createBlockHeaderFromRPC(blockParams: JSONRPCBlock, options?: Bl blobGasUsed, excessBlobGas, parentBeaconBlockRoot, - requestsRoot, + requestsHash, }, options, ) diff --git a/packages/block/src/header/header.ts b/packages/block/src/header/header.ts index 79675dafed..ccd7af2a6c 100644 --- a/packages/block/src/header/header.ts +++ b/packages/block/src/header/header.ts @@ -8,6 +8,7 @@ import { BIGINT_7, KECCAK256_RLP, KECCAK256_RLP_ARRAY, + SHA256_NULL, TypeOutput, bigIntToHex, bigIntToUnpaddedBytes, @@ -60,7 +61,7 @@ export class BlockHeader { public readonly blobGasUsed?: bigint public readonly excessBlobGas?: bigint public readonly parentBeaconBlockRoot?: Uint8Array - public readonly requestsRoot?: Uint8Array + public readonly requestsHash?: Uint8Array public readonly common: Common @@ -161,7 +162,9 @@ export class BlockHeader { blobGasUsed: this.common.isActivatedEIP(4844) ? BIGINT_0 : undefined, excessBlobGas: this.common.isActivatedEIP(4844) ? BIGINT_0 : undefined, parentBeaconBlockRoot: this.common.isActivatedEIP(4788) ? new Uint8Array(32) : undefined, - requestsRoot: this.common.isActivatedEIP(7685) ? KECCAK256_RLP : undefined, + // Note: as of devnet-4 we stub the null SHA256 hash, but for devnet5 this will actually + // be the correct hash for empty requests. + requestsHash: this.common.isActivatedEIP(7685) ? SHA256_NULL : undefined, } const baseFeePerGas = @@ -175,8 +178,8 @@ export class BlockHeader { const parentBeaconBlockRoot = toType(headerData.parentBeaconBlockRoot, TypeOutput.Uint8Array) ?? hardforkDefaults.parentBeaconBlockRoot - const requestsRoot = - toType(headerData.requestsRoot, TypeOutput.Uint8Array) ?? hardforkDefaults.requestsRoot + const requestsHash = + toType(headerData.requestsHash, TypeOutput.Uint8Array) ?? hardforkDefaults.requestsHash if (!this.common.isActivatedEIP(1559) && baseFeePerGas !== undefined) { throw new Error('A base fee for a block can only be set with EIP1559 being activated') @@ -204,8 +207,8 @@ export class BlockHeader { ) } - if (!this.common.isActivatedEIP(7685) && requestsRoot !== undefined) { - throw new Error('requestsRoot can only be provided with EIP 7685 activated') + if (!this.common.isActivatedEIP(7685) && requestsHash !== undefined) { + throw new Error('requestsHash can only be provided with EIP 7685 activated') } this.parentHash = parentHash @@ -228,7 +231,7 @@ export class BlockHeader { this.blobGasUsed = blobGasUsed this.excessBlobGas = excessBlobGas this.parentBeaconBlockRoot = parentBeaconBlockRoot - this.requestsRoot = requestsRoot + this.requestsHash = requestsHash this._genericFormatValidation() this._validateDAOExtraData() @@ -344,8 +347,8 @@ export class BlockHeader { } if (this.common.isActivatedEIP(7685)) { - if (this.requestsRoot === undefined) { - const msg = this._errorMsg('EIP7685 block has no requestsRoot field') + if (this.requestsHash === undefined) { + const msg = this._errorMsg('EIP7685 block has no requestsHash field') throw new Error(msg) } } @@ -626,7 +629,7 @@ export class BlockHeader { rawItems.push(this.parentBeaconBlockRoot!) } if (this.common.isActivatedEIP(7685)) { - rawItems.push(this.requestsRoot!) + rawItems.push(this.requestsHash!) } return rawItems @@ -768,7 +771,7 @@ export class BlockHeader { JSONDict.parentBeaconBlockRoot = bytesToHex(this.parentBeaconBlockRoot!) } if (this.common.isActivatedEIP(7685)) { - JSONDict.requestsRoot = bytesToHex(this.requestsRoot!) + JSONDict.requestsHash = bytesToHex(this.requestsHash!) } return JSONDict } diff --git a/packages/block/src/helpers.ts b/packages/block/src/helpers.ts index 9fc406788b..3f0b498796 100644 --- a/packages/block/src/helpers.ts +++ b/packages/block/src/helpers.ts @@ -2,6 +2,7 @@ import { MerklePatriciaTrie } from '@ethereumjs/mpt' import { RLP } from '@ethereumjs/rlp' import { Blob4844Tx } from '@ethereumjs/tx' import { BIGINT_0, BIGINT_1, TypeOutput, isHexString, toType } from '@ethereumjs/util' +import { concatBytes } from 'ethereum-cryptography/utils' import type { BlockHeaderBytes, HeaderData } from './types.js' import type { TypedTransaction } from '@ethereumjs/tx' @@ -46,7 +47,7 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData { blobGasUsed, excessBlobGas, parentBeaconBlockRoot, - requestsRoot, + requestsHash, ] = values if (values.length > 21) { @@ -81,7 +82,7 @@ export function valuesArrayToHeaderData(values: BlockHeaderBytes): HeaderData { blobGasUsed, excessBlobGas, parentBeaconBlockRoot, - requestsRoot, + requestsHash, } } @@ -154,9 +155,9 @@ export async function genTransactionsTrieRoot( * @param emptyTrie optional empty trie used to generate the root * @returns a 32 byte Uint8Array representing the requests trie root */ -export async function genRequestsTrieRoot( +export function genRequestsRoot( requests: CLRequest[], - emptyTrie?: MerklePatriciaTrie, + sha256Function: (msg: Uint8Array) => Uint8Array, ) { // Requests should be sorted in monotonically ascending order based on type // and whatever internal sorting logic is defined by each request type @@ -166,9 +167,14 @@ export async function genRequestsTrieRoot( throw new Error('requests are not sorted in ascending order') } } - const trie = emptyTrie ?? new MerklePatriciaTrie() - for (const [i, req] of requests.entries()) { - await trie.put(RLP.encode(i), req.serialize()) + + // def compute_requests_hash(list): + // return keccak256(rlp.encode([rlp.encode(req) for req in list])) + + let flatRequests = new Uint8Array() + for (const req of requests) { + flatRequests = concatBytes(flatRequests, sha256Function(req.bytes)) } - return trie.root() + + return sha256Function(flatRequests) } diff --git a/packages/block/src/index.ts b/packages/block/src/index.ts index 074d1a1c04..e3745e9e7c 100644 --- a/packages/block/src/index.ts +++ b/packages/block/src/index.ts @@ -4,7 +4,7 @@ export * from './consensus/index.js' export { type BeaconPayloadJSON, executionPayloadFromBeaconPayload } from './from-beacon-payload.js' export * from './header/index.js' export { - genRequestsTrieRoot, + genRequestsRoot, genTransactionsTrieRoot, genWithdrawalsTrieRoot, getDifficulty, diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 145ef4ef20..5efee2bedf 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -5,18 +5,12 @@ import type { AddressLike, BigIntLike, BytesLike, - CLRequest, - CLRequestType, - ConsolidationRequestV1, - DepositRequestV1, JSONRPCWithdrawal, NumericString, PrefixedHexString, - RequestBytes, VerkleExecutionWitness, WithdrawalBytes, WithdrawalData, - WithdrawalRequestV1, } from '@ethereumjs/util' /** @@ -114,7 +108,7 @@ export interface HeaderData { blobGasUsed?: BigIntLike excessBlobGas?: BigIntLike parentBeaconBlockRoot?: BytesLike - requestsRoot?: BytesLike + requestsHash?: BytesLike } /** @@ -128,7 +122,6 @@ export interface BlockData { transactions?: Array uncleHeaders?: Array withdrawals?: Array - requests?: Array> /** * EIP-6800: Verkle Proof Data (experimental) */ @@ -136,19 +129,16 @@ 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, ] @@ -156,12 +146,7 @@ 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?, - RequestsBytes?, -] +export type BlockBodyBytes = [TransactionsBytes, UncleHeadersBytes, WithdrawalsBytes?] /** * TransactionsBytes can be an array of serialized txs for Typed Transactions or an array of Uint8Array Arrays for legacy transactions. */ @@ -179,7 +164,6 @@ export interface JSONBlock { transactions?: JSONTx[] uncleHeaders?: JSONHeader[] withdrawals?: JSONRPCWithdrawal[] - requests?: PrefixedHexString[] | null executionWitness?: VerkleExecutionWitness | null } @@ -207,7 +191,7 @@ export interface JSONHeader { blobGasUsed?: PrefixedHexString excessBlobGas?: PrefixedHexString parentBeaconBlockRoot?: PrefixedHexString - requestsRoot?: PrefixedHexString + requestsHash?: PrefixedHexString } /* @@ -241,8 +225,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 - requests?: Array // If EIP-7685 is enabled for this block, array of serialized CL requests + requestsHash?: PrefixedHexString // If EIP-7685 is enabled for this block, returns the requests root } export type WithdrawalV1 = { @@ -272,9 +255,7 @@ export type ExecutionPayload = { blobGasUsed?: PrefixedHexString // QUANTITY, 64 Bits excessBlobGas?: PrefixedHexString // QUANTITY, 64 Bits parentBeaconBlockRoot?: PrefixedHexString // QUANTITY, 64 Bits + requestsHash?: PrefixedHexString // VerkleExecutionWitness is already a hex serialized object executionWitness?: VerkleExecutionWitness | null // QUANTITY, 64 Bits, null implies not available - depositRequests?: DepositRequestV1[] // Array of 6110 deposit requests - withdrawalRequests?: WithdrawalRequestV1[] // Array of 7002 withdrawal requests - consolidationRequests?: ConsolidationRequestV1[] // Array of 7251 consolidation requests } diff --git a/packages/block/test/eip4844block.spec.ts b/packages/block/test/eip4844block.spec.ts index d7632c90f3..9cd8288015 100644 --- a/packages/block/test/eip4844block.spec.ts +++ b/packages/block/test/eip4844block.spec.ts @@ -228,7 +228,6 @@ describe('transaction validation tests', () => { ) const blockJSON = blockWithValidTx.toJSON() blockJSON.header!.blobGasUsed = '0x0' - // @ts-expect-error const blockWithInvalidHeader = createBlock(blockJSON, { common }) assert.throws( () => blockWithInvalidHeader.validateBlobTransactions(parentHeader), diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts deleted file mode 100644 index 6b3f314f6c..0000000000 --- a/packages/block/test/eip7685block.spec.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Common, Hardfork, Mainnet } from '@ethereumjs/common' -import { - KECCAK256_RLP, - bytesToBigInt, - createDepositRequest, - createWithdrawalRequest, - randomBytes, -} from '@ethereumjs/util' -import { assert, describe, expect, it } from 'vitest' - -import { genRequestsTrieRoot } from '../src/helpers.js' -import { - Block, - createBlock, - createBlockFromBytesArray, - createBlockFromRPC, - createBlockHeader, -} from '../src/index.js' - -import type { JSONRPCBlock } from '../src/index.js' -import type { CLRequest, CLRequestType } from '@ethereumjs/util' - -function getRandomDepositRequest(): CLRequest { - const depositRequestData = { - pubkey: randomBytes(48), - withdrawalCredentials: randomBytes(32), - amount: bytesToBigInt(randomBytes(8)), - signature: randomBytes(96), - index: bytesToBigInt(randomBytes(8)), - } - return createDepositRequest(depositRequestData) as CLRequest -} - -function getRandomWithdrawalRequest(): CLRequest { - const withdrawalRequestData = { - sourceAddress: randomBytes(20), - validatorPubkey: randomBytes(48), - amount: bytesToBigInt(randomBytes(8)), - } - return createWithdrawalRequest(withdrawalRequestData) as CLRequest -} - -const common = new Common({ - chain: Mainnet, - hardfork: Hardfork.Cancun, - eips: [7685, 4844, 4788], -}) -describe('7685 tests', () => { - it('should instantiate block with defaults', () => { - const block = createBlock({}, { common }) - assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP) - const block2 = new Block(undefined, undefined, undefined, undefined, { common }) - assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP) - assert.equal(block2.requests?.length, 0) - }) - it('should instantiate a block with requests', async () => { - const request = getRandomDepositRequest() - const requestsRoot = await genRequestsTrieRoot([request]) - const block = createBlock( - { - requests: [request], - header: { requestsRoot }, - }, - { common }, - ) - assert.equal(block.requests?.length, 1) - assert.deepEqual(block.header.requestsRoot, requestsRoot) - }) - it('RequestsRootIsValid should return false when requestsRoot is invalid', async () => { - const request = getRandomDepositRequest() - const block = createBlock( - { - requests: [request], - header: { requestsRoot: randomBytes(32) }, - }, - { common }, - ) - - assert.equal(await block.requestsTrieIsValid(), false) - }) - it('should validate requests order', async () => { - const request1 = getRandomDepositRequest() - const request2 = getRandomDepositRequest() - const request3 = getRandomWithdrawalRequest() - const requests = [request1, request2, request3] - const requestsRoot = await genRequestsTrieRoot(requests) - - // Construct block with requests in correct order - - const block = createBlock( - { - requests, - header: { requestsRoot }, - }, - { common }, - ) - - assert.ok(await block.requestsTrieIsValid()) - - // Throws when requests are not ordered correctly - await expect(async () => - createBlock( - { - requests: [request1, request3, request2], - header: { requestsRoot }, - }, - { common }, - ), - ).rejects.toThrow('ascending order') - }) -}) - -describe('createWithdrawalFromBytesArray tests', () => { - it('should construct a block with empty requests root', () => { - const block = createBlockFromBytesArray( - [createBlockHeader({}, { common }).raw(), [], [], [], []], - { - common, - }, - ) - assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP) - }) - it('should construct a block with a valid requests array', async () => { - const request1 = getRandomDepositRequest() - const request2 = getRandomWithdrawalRequest() - const request3 = getRandomWithdrawalRequest() - const requests = [request1, request2, request3] - const requestsRoot = await genRequestsTrieRoot(requests) - const serializedRequests = [request1.serialize(), request2.serialize(), request3.serialize()] - - const block = createBlockFromBytesArray( - [createBlockHeader({ requestsRoot }, { common }).raw(), [], [], [], serializedRequests], - { - common, - }, - ) - assert.deepEqual(block.header.requestsRoot, requestsRoot) - assert.equal(block.requests?.length, 3) - }) -}) - -describe('fromRPC tests', () => { - it('should construct a block from a JSON object', async () => { - const request1 = getRandomDepositRequest() - const request2 = getRandomDepositRequest() - const request3 = getRandomWithdrawalRequest() - const requests = [request1, request2, request3] - const requestsRoot = await genRequestsTrieRoot(requests) - const serializedRequests = [request1.serialize(), request2.serialize(), request3.serialize()] - - const block = createBlockFromBytesArray( - [createBlockHeader({ requestsRoot }, { common }).raw(), [], [], [], serializedRequests], - { - common, - }, - ) - const JSONBlock = block.toJSON() - const RPCBlock = { ...JSONBlock.header, requests: JSONBlock.requests } - const createBlockFromJSON = createBlockFromRPC(RPCBlock as JSONRPCBlock, undefined, { common }) - assert.deepEqual(block.hash(), createBlockFromJSON.hash()) - }) -}) diff --git a/packages/blockchain/src/blockchain.ts b/packages/blockchain/src/blockchain.ts index df03607bd3..381c571042 100644 --- a/packages/blockchain/src/blockchain.ts +++ b/packages/blockchain/src/blockchain.ts @@ -26,6 +26,7 @@ import { } from './db/helpers.js' import { DBManager } from './db/manager.js' import { DBTarget } from './db/operation.js' +import { SHA256_EMPTY_RH } from './helpers.js' import type { BlockchainEvent, @@ -581,8 +582,8 @@ export class Blockchain implements BlockchainInterface { } if (header.common.isActivatedEIP(7685)) { - if (header.requestsRoot === undefined) { - throw new Error(`requestsRoot must be provided when EIP-7685 is active`) + if (header.requestsHash === undefined) { + throw new Error(`requestsHash must be provided when EIP-7685 is active`) } } } @@ -600,12 +601,6 @@ export class Blockchain implements BlockchainInterface { // (one for each uncle header and then for validateBlobTxs). const parentBlock = await this.getBlock(block.header.parentHash) block.validateBlobTransactions(parentBlock.header) - if (block.common.isActivatedEIP(7685)) { - const valid = await block.requestsTrieIsValid() - if (!valid) { - throw new Error('invalid requestsRoot') - } - } } /** * The following rules are checked in this method: @@ -1319,6 +1314,7 @@ export class Blockchain implements BlockchainInterface { number: 0, stateRoot, withdrawalsRoot: common.isActivatedEIP(4895) ? KECCAK256_RLP : undefined, + requestsHash: common.isActivatedEIP(7685) ? SHA256_EMPTY_RH : undefined, } if (common.consensusType() === 'poa') { if (common.genesis().extraData) { diff --git a/packages/blockchain/src/db/manager.ts b/packages/blockchain/src/db/manager.ts index 42f826c464..82338a1885 100644 --- a/packages/blockchain/src/db/manager.ts +++ b/packages/blockchain/src/db/manager.ts @@ -128,15 +128,6 @@ export class DBManager { body.push([]) } } - - // If requests root exists, validate that requests array exists or insert it - if (header.requestsRoot !== undefined) { - if (!equalsBytes(header.requestsRoot, KECCAK256_RLP)) { - throw new Error('requestsRoot should be equal to hash of null when no requests') - } else { - body.push([]) - } - } } const blockData = [header.raw(), ...body] as BlockBytes diff --git a/packages/blockchain/src/helpers.ts b/packages/blockchain/src/helpers.ts index b6529c26d2..d3c0272b03 100644 --- a/packages/blockchain/src/helpers.ts +++ b/packages/blockchain/src/helpers.ts @@ -1,8 +1,8 @@ import { ChainGenesis } from '@ethereumjs/common' import { genesisMPTStateRoot } from '@ethereumjs/mpt' +import { type GenesisState, hexToBytes } from '@ethereumjs/util' import type { Chain, Common } from '@ethereumjs/common' -import type { GenesisState } from '@ethereumjs/util' /** * Safe creation of a new Blockchain object awaiting the initialization function, @@ -40,3 +40,21 @@ export async function getGenesisStateRoot(chainId: Chain, common: Common): Promi const chainGenesis = ChainGenesis[chainId] return chainGenesis !== undefined ? chainGenesis.stateRoot : genGenesisStateRoot({}, common) } + +/* +The code below calculates the empty requests hash as of devnet-4 for EIP 7685 +Note: it is not possible to calculate this directly in the blockchain package, +this introduces the `ethereum-cryptography` dependency. + +// Used to calculate the empty requests hash +const z0 = sha256(new Uint8Array([0])) +const z1 = sha256(new Uint8Array([1])) +const z2 = sha256(new Uint8Array([2])) + +export const SHA256_EMPTY_RH = sha256(concatBytes(z0, z1, z2)) + +*/ + +export const SHA256_EMPTY_RH = hexToBytes( + '0x6036c41849da9c076ed79654d434017387a88fb833c2856b32e18218b3341c5f', +) diff --git a/packages/blockchain/test/blockValidation.spec.ts b/packages/blockchain/test/blockValidation.spec.ts index 0a9a57732e..8e6402d8b3 100644 --- a/packages/blockchain/test/blockValidation.spec.ts +++ b/packages/blockchain/test/blockValidation.spec.ts @@ -2,9 +2,9 @@ import { createBlock, createBlockHeader } from '@ethereumjs/block' import { Common, ConsensusAlgorithm, Hardfork, Mainnet } from '@ethereumjs/common' import { Ethash } from '@ethereumjs/ethash' import { RLP } from '@ethereumjs/rlp' -import { KECCAK256_RLP, bytesToHex, randomBytes } from '@ethereumjs/util' +import { bytesToHex } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak.js' -import { assert, describe, expect, it } from 'vitest' +import { assert, describe, it } from 'vitest' import { EthashConsensus, createBlockchain } from '../src/index.js' @@ -389,48 +389,3 @@ 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: Mainnet, - hardfork: Hardfork.Cancun, - eips: [7685, 1559, 4895, 4844, 4788], - }) - const blockchain = await createBlockchain({ - common, - }) - const block = createBlock( - { - header: { - number: 1n, - requestsRoot: randomBytes(32), - withdrawalsRoot: KECCAK256_RLP, - parentHash: blockchain.genesisBlock.hash(), - timestamp: blockchain.genesisBlock.header.timestamp + 1n, - gasLimit: 5000, - }, - }, - { common }, - ) - - await expect(async () => blockchain.putBlock(block)).rejects.toThrow('invalid requestsRoot') - - const blockWithRequest = createBlock( - { - 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) } as any], - }, - { common }, - ) - await expect(async () => blockchain.putBlock(blockWithRequest)).rejects.toThrow( - 'invalid requestsRoot', - ) - }) -}) diff --git a/packages/client/src/ext/qheap.ts b/packages/client/src/ext/qheap.ts index dc83e56f23..55d5d109ae 100644 --- a/packages/client/src/ext/qheap.ts +++ b/packages/client/src/ext/qheap.ts @@ -210,6 +210,7 @@ export class Heap { fail = i } } + // eslint-disable-next-line no-console if (fail) console.log('failed at', fail >>> 1, fail) return !fail } diff --git a/packages/client/src/miner/miner.ts b/packages/client/src/miner/miner.ts index 7e6e5124a2..f0a74141c5 100644 --- a/packages/client/src/miner/miner.ts +++ b/packages/client/src/miner/miner.ts @@ -332,7 +332,7 @@ export class Miner { } if (interrupt) return // Build block, sealing it - const block = await blockBuilder.build(this.nextSolution) + const { block } = await blockBuilder.build(this.nextSolution) if (this.config.saveReceipts) { await this.execution.receiptsManager?.saveReceipts(block, receipts) } diff --git a/packages/client/src/miner/pendingBlock.ts b/packages/client/src/miner/pendingBlock.ts index cc17cf8316..15ba431dc1 100644 --- a/packages/client/src/miner/pendingBlock.ts +++ b/packages/client/src/miner/pendingBlock.ts @@ -19,7 +19,7 @@ import type { Config } from '../config.js' import type { TxPool } from '../service/txpool.js' import type { Block, HeaderData } from '@ethereumjs/block' import type { TypedTransaction } from '@ethereumjs/tx' -import type { PrefixedHexString, WithdrawalData } from '@ethereumjs/util' +import type { CLRequest, CLRequestType, PrefixedHexString, WithdrawalData } from '@ethereumjs/util' import type { BlockBuilder, TxReceipt, VM } from '@ethereumjs/vm' interface PendingBlockOpts { @@ -239,7 +239,16 @@ export class PendingBlock { */ async build( payloadIdBytes: Uint8Array | string, - ): Promise { + ): Promise< + | void + | [ + block: Block, + receipts: TxReceipt[], + value: bigint, + blobs?: BlobsBundle, + requests?: CLRequest[], + ] + > { const payloadId = typeof payloadIdBytes !== 'string' ? bytesToHex(payloadIdBytes) : payloadIdBytes const builder = this.pendingPayloads.get(payloadId) @@ -283,7 +292,7 @@ export class PendingBlock { const { skippedByAddErrors, blobTxs } = await this.addTransactions(builder, txs) - const block = await builder.build() + const { block, requests } = await builder.build() // Construct blobs bundle const blobs = block.common.isActivatedEIP(4844) @@ -301,7 +310,7 @@ export class PendingBlock { )}`, ) - return [block, builder.transactionReceipts, builder.minerValue, blobs] + return [block, builder.transactionReceipts, builder.minerValue, blobs, requests] } private async addTransactions(builder: BlockBuilder, txs: TypedTransaction[]) { diff --git a/packages/client/src/rpc/modules/engine/engine.ts b/packages/client/src/rpc/modules/engine/engine.ts index 017adc98d0..31796bcad5 100644 --- a/packages/client/src/rpc/modules/engine/engine.ts +++ b/packages/client/src/rpc/modules/engine/engine.ts @@ -32,14 +32,12 @@ import { recursivelyFindParents, validExecutedChainBlock, validHash, - validate4844BlobVersionedHashes, validateHardforkRange, } from './util/index.js' import { executionPayloadV1FieldValidators, executionPayloadV2FieldValidators, executionPayloadV3FieldValidators, - executionPayloadV4FieldValidators, forkchoiceFieldValidators, payloadAttributesFieldValidatorsV1, payloadAttributesFieldValidatorsV2, @@ -59,7 +57,6 @@ import type { ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, - ExecutionPayloadV4, ForkchoiceResponseV1, ForkchoiceStateV1, PayloadAttributes, @@ -207,13 +204,14 @@ export class Engine { this.newPayloadV4 = cmMiddleware( middleware( callWithStackTrace(this.newPayloadV4.bind(this), this._rpcDebug), - 3, + 4, [ - [validators.object(executionPayloadV4FieldValidators)], + [validators.object(executionPayloadV3FieldValidators)], [validators.array(validators.bytes32)], [validators.bytes32], + [validators.array(validators.hex)], ], - ['executionPayload', 'blobVersionedHashes', 'parentBeaconBlockRoot'], + ['executionPayload', 'blobVersionedHashes', 'parentBeaconBlockRoot', 'executionRequests'], ), ([payload], response) => this.connectionManager.lastNewPayload({ payload, response }), ) @@ -344,9 +342,14 @@ export class Engine { * 3. validationError: String|null - validation error message */ private async newPayload( - params: [ExecutionPayload, (Bytes32[] | null)?, (Bytes32 | null)?], + params: [ + ExecutionPayload, + (Bytes32[] | null)?, + (Bytes32 | null)?, + (PrefixedHexString[] | null)?, + ], ): Promise { - const [payload, blobVersionedHashes, parentBeaconBlockRoot] = params + const [payload, blobVersionedHashes, parentBeaconBlockRoot, executionRequests] = params if (this.config.synchronized) { this.connectionManager.newPayloadLog() } @@ -364,10 +367,11 @@ export class Engine { */ // newpayloadv3 comes with parentBeaconBlockRoot out of the payload const { block: headBlock, error } = await assembleBlock( + payload, { - ...payload, - // ExecutionPayload only handles undefined parentBeaconBlockRoot: parentBeaconBlockRoot ?? undefined, + blobVersionedHashes: blobVersionedHashes ?? undefined, + executionRequests: executionRequests ?? undefined, }, this.chain, this.chainCache, @@ -388,41 +392,6 @@ export class Engine { return response } - /** - * Validate blob versioned hashes in the context of EIP-4844 blob transactions - */ - if (headBlock.common.isActivatedEIP(4844)) { - let validationError: string | null = null - if (blobVersionedHashes === undefined || blobVersionedHashes === null) { - validationError = `Error verifying blobVersionedHashes: received none` - } else { - validationError = validate4844BlobVersionedHashes(headBlock, blobVersionedHashes) - } - - // if there was a validation error return invalid - if (validationError !== null) { - this.config.logger.debug(validationError) - const latestValidHash = await validHash( - hexToBytes(parentHash as PrefixedHexString), - this.chain, - this.chainCache, - ) - const response = { status: Status.INVALID, latestValidHash, validationError } - // skip marking the block invalid as this is more of a data issue from CL - return response - } - } else if (blobVersionedHashes !== undefined && blobVersionedHashes !== null) { - const validationError = `Invalid blobVersionedHashes before EIP-4844 is activated` - const latestValidHash = await validHash( - hexToBytes(parentHash as PrefixedHexString), - this.chain, - this.chainCache, - ) - const response = { status: Status.INVALID, latestValidHash, validationError } - // skip marking the block invalid as this is more of a data issue from CL - return response - } - /** * Stats and hardfork updates */ @@ -852,7 +821,9 @@ export class Engine { return newPayloadRes } - async newPayloadV4(params: [ExecutionPayloadV4, Bytes32[], Bytes32]): Promise { + async newPayloadV4( + params: [ExecutionPayloadV3, Bytes32[], Bytes32, Bytes32[]], + ): Promise { const pragueTimestamp = this.chain.config.chainCommon.hardforkTimestamp(Hardfork.Prague) const ts = parseInt(params[0].timestamp) if (pragueTimestamp === null || ts < pragueTimestamp) { @@ -1334,7 +1305,7 @@ export class Engine { } // The third arg returned is the minerValue which we will use to // value the block - const [block, receipts, value, blobs] = built + const [block, receipts, value, blobs, requests] = built // do a blocking call even if execution might be busy for the moment and skip putting // it into chain till CL confirms with full data via new payload like versioned hashes @@ -1348,7 +1319,7 @@ export class Engine { /** * Creates the payload in ExecutionPayloadV1 format to be returned */ - const executionPayload = blockToExecutionPayload(block, value, blobs) + const executionPayload = blockToExecutionPayload(block, value, blobs, requests) let checkNotBeforeHf: Hardfork | null let checkNotAfterHf: Hardfork | null diff --git a/packages/client/src/rpc/modules/engine/types.ts b/packages/client/src/rpc/modules/engine/types.ts index 811633b26d..8563a7c59a 100644 --- a/packages/client/src/rpc/modules/engine/types.ts +++ b/packages/client/src/rpc/modules/engine/types.ts @@ -2,12 +2,7 @@ import { UNKNOWN_PAYLOAD } from '../../error-code.js' import type { Skeleton } from '../../../service/index.js' import type { Block, ExecutionPayload } from '@ethereumjs/block' -import type { - ConsolidationRequestV1, - DepositRequestV1, - PrefixedHexString, - WithdrawalRequestV1, -} from '@ethereumjs/util' +import type { PrefixedHexString } from '@ethereumjs/util' export enum Status { ACCEPTED = 'ACCEPTED', @@ -32,11 +27,6 @@ export type ExecutionPayloadV1 = ExecutionPayload export type ExecutionPayloadV2 = ExecutionPayloadV1 & { withdrawals: WithdrawalV1[] } // parentBeaconBlockRoot comes separate in new payloads and needs to be added to payload data export type ExecutionPayloadV3 = ExecutionPayloadV2 & { excessBlobGas: Uint64; blobGasUsed: Uint64 } -export type ExecutionPayloadV4 = ExecutionPayloadV3 & { - depositRequests: DepositRequestV1[] - withdrawalRequests: WithdrawalRequestV1[] - consolidationRequests: ConsolidationRequestV1[] -} export type ForkchoiceStateV1 = { headBlockHash: Bytes32 diff --git a/packages/client/src/rpc/modules/engine/util/getPayload.ts b/packages/client/src/rpc/modules/engine/util/getPayload.ts index cc7b674af8..fec0814f2d 100644 --- a/packages/client/src/rpc/modules/engine/util/getPayload.ts +++ b/packages/client/src/rpc/modules/engine/util/getPayload.ts @@ -1,22 +1,35 @@ -import { bigIntToHex } from '@ethereumjs/util' +import { bigIntToHex, bytesToHex } from '@ethereumjs/util' import type { BlobsBundle } from '../../../../miner/index.js' import type { BlobsBundleV1 } from '../types.js' import type { Block, ExecutionPayload } from '@ethereumjs/block' +import type { CLRequest, CLRequestType } from '@ethereumjs/util' /** * Formats a block to {@link ExecutionPayloadV1}. */ -export const blockToExecutionPayload = (block: Block, value: bigint, bundle?: BlobsBundle) => { +export const blockToExecutionPayload = ( + block: Block, + value: bigint, + bundle?: BlobsBundle, + requests?: CLRequest[], +) => { const executionPayload: ExecutionPayload = block.toExecutionPayload() // parentBeaconBlockRoot is not part of the CL payload if (executionPayload.parentBeaconBlockRoot !== undefined) { delete executionPayload.parentBeaconBlockRoot } + const blobsBundle: BlobsBundleV1 | undefined = bundle ? bundle : undefined // ethereumjs does not provide any transaction censoring detection (yet) to suggest // overriding builder/mev-boost blocks const shouldOverrideBuilder = false - return { executionPayload, blockValue: bigIntToHex(value), blobsBundle, shouldOverrideBuilder } + return { + executionPayload, + executionRequests: requests?.map((req) => bytesToHex(req.data)), + blockValue: bigIntToHex(value), + blobsBundle, + shouldOverrideBuilder, + } } diff --git a/packages/client/src/rpc/modules/engine/util/newPayload.ts b/packages/client/src/rpc/modules/engine/util/newPayload.ts index 594f24b85f..5155ddfc5a 100644 --- a/packages/client/src/rpc/modules/engine/util/newPayload.ts +++ b/packages/client/src/rpc/modules/engine/util/newPayload.ts @@ -1,6 +1,7 @@ -import { createBlockFromExecutionPayload } from '@ethereumjs/block' +import { createBlockFromExecutionPayload, genRequestsRoot } from '@ethereumjs/block' import { Blob4844Tx } from '@ethereumjs/tx' -import { hexToBytes } from '@ethereumjs/util' +import { CLRequest, CLRequestType, bytesToHex, hexToBytes } from '@ethereumjs/util' +import { sha256 } from 'ethereum-cryptography/sha256' import { short } from '../../../../util/index.js' import { Status } from '../types.js' @@ -10,44 +11,13 @@ import { validHash } from './generic.js' import type { Chain } from '../../../../blockchain/index.js' import type { ChainCache, PayloadStatusV1 } from '../types.js' import type { Block, ExecutionPayload } from '@ethereumjs/block' +import type { Common } from '@ethereumjs/common' import type { PrefixedHexString } from '@ethereumjs/util' -/** - * Returns a block from a payload. - * If errors, returns {@link PayloadStatusV1} - */ -export const assembleBlock = async ( - payload: ExecutionPayload, - chain: Chain, - chainCache: ChainCache, -): Promise<{ block?: Block; error?: PayloadStatusV1 }> => { - const { blockNumber, timestamp } = payload - const { config } = chain - const common = config.chainCommon.copy() - - common.setHardforkBy({ blockNumber, timestamp }) - - try { - const block = await createBlockFromExecutionPayload(payload, { common }) - // TODO: validateData is also called in applyBlock while runBlock, may be it can be optimized - // by removing/skipping block data validation from there - await block.validateData() - return { block } - } catch (error) { - const validationError = `Error assembling block from payload: ${error}` - config.logger.error(validationError) - const latestValidHash = await validHash( - hexToBytes(payload.parentHash as PrefixedHexString), - chain, - chainCache, - ) - const response = { - status: `${error}`.includes('Invalid blockHash') ? Status.INVALID_BLOCK_HASH : Status.INVALID, - latestValidHash, - validationError, - } - return { error: response } - } +type CLData = { + parentBeaconBlockRoot?: PrefixedHexString + blobVersionedHashes?: PrefixedHexString[] + executionRequests?: PrefixedHexString[] } export const validate4844BlobVersionedHashes = ( @@ -82,3 +52,111 @@ export const validate4844BlobVersionedHashes = ( } return validationError } + +export const validateAndGen7685RequestsHash = ( + common: Common, + executionRequests: PrefixedHexString[], +): PrefixedHexString => { + let validationError: string | null = null + + const requests: CLRequest[] = [] + let requestIndex = 0 + if (common.isActivatedEIP(6110)) { + requests.push(new CLRequest(CLRequestType.Deposit, hexToBytes(executionRequests[requestIndex]))) + requestIndex++ + } + if (common.isActivatedEIP(7002)) { + requests.push( + new CLRequest(CLRequestType.Withdrawal, hexToBytes(executionRequests[requestIndex])), + ) + requestIndex++ + } + if (common.isActivatedEIP(7251)) { + requests.push( + new CLRequest(CLRequestType.Consolidation, hexToBytes(executionRequests[requestIndex])), + ) + requestIndex++ + } + + if (requestIndex !== executionRequests.length) { + validationError = `Invalid executionRequests=${executionRequests.length} expected=${requestIndex}` + throw validationError + } + + const sha256Function = common.customCrypto.sha256 ?? sha256 + const requestsHash = genRequestsRoot(requests, sha256Function) + + return bytesToHex(requestsHash) +} + +/** + * Returns a block from a payload. + * If errors, returns {@link PayloadStatusV1} + */ +export const assembleBlock = async ( + payload: Omit, + clValidationData: CLData, + chain: Chain, + chainCache: ChainCache, +): Promise<{ block?: Block; error?: PayloadStatusV1 }> => { + const { blockNumber, timestamp } = payload + const { config } = chain + const common = config.chainCommon.copy() + common.setHardforkBy({ blockNumber, timestamp }) + + try { + // Validate CL data to see if it matches with the assembled block + const { blobVersionedHashes, executionRequests, parentBeaconBlockRoot } = clValidationData + + let requestsHash + if (executionRequests !== undefined) { + requestsHash = validateAndGen7685RequestsHash(common, executionRequests) + } else if (common.isActivatedEIP(7685)) { + throw `Invalid executionRequests=undefined for EIP-7685 activated block` + } + + const block = await createBlockFromExecutionPayload( + { ...payload, parentBeaconBlockRoot, requestsHash }, + { common }, + ) + // TODO: validateData is also called in applyBlock while runBlock, may be it can be optimized + // by removing/skipping block data validation from there + await block.validateData() + + /** + * Validate blob versioned hashes in the context of EIP-4844 blob transactions + */ + if (block.common.isActivatedEIP(4844)) { + let validationError: string | null = null + if (blobVersionedHashes === undefined) { + validationError = `Error verifying blobVersionedHashes: received none` + } else { + validationError = validate4844BlobVersionedHashes(block, blobVersionedHashes) + } + + // if there was a validation error return invalid + if (validationError !== null) { + throw validationError + } + } else if (blobVersionedHashes !== undefined) { + const validationError = `Invalid blobVersionedHashes before EIP-4844 is activated` + throw validationError + } + + return { block } + } catch (error) { + const validationError = `Error assembling block from payload: ${error}` + config.logger.error(validationError) + const latestValidHash = await validHash( + hexToBytes(payload.parentHash as PrefixedHexString), + chain, + chainCache, + ) + const response = { + status: `${error}`.includes('Invalid blockHash') ? Status.INVALID_BLOCK_HASH : Status.INVALID, + latestValidHash, + validationError, + } + return { error: response } + } +} diff --git a/packages/client/src/rpc/modules/engine/validators.ts b/packages/client/src/rpc/modules/engine/validators.ts index a90704cb5d..9aae69fe2a 100644 --- a/packages/client/src/rpc/modules/engine/validators.ts +++ b/packages/client/src/rpc/modules/engine/validators.ts @@ -26,13 +26,6 @@ export const executionPayloadV3FieldValidators = { excessBlobGas: validators.uint64, } -export const executionPayloadV4FieldValidators = { - ...executionPayloadV3FieldValidators, - depositRequests: validators.array(validators.depositRequest()), - withdrawalRequests: validators.array(validators.withdrawalRequest()), - consolidationRequests: validators.array(validators.consolidationRequest()), -} - export const forkchoiceFieldValidators = { headBlockHash: validators.blockHash, safeBlockHash: validators.blockHash, diff --git a/packages/client/src/rpc/modules/eth.ts b/packages/client/src/rpc/modules/eth.ts index 3fb2d72559..6c5dc49b8c 100644 --- a/packages/client/src/rpc/modules/eth.ts +++ b/packages/client/src/rpc/modules/eth.ts @@ -152,8 +152,7 @@ const toJSONRPCBlock = async ( blobGasUsed: header.blobGasUsed, excessBlobGas: header.excessBlobGas, parentBeaconBlockRoot: header.parentBeaconBlockRoot, - requestsRoot: header.requestsRoot, - requests: block.requests?.map((req) => bytesToHex(req.serialize())), + requestsHash: header.requestsHash, } } diff --git a/packages/client/src/sync/fetcher/blockfetcher.ts b/packages/client/src/sync/fetcher/blockfetcher.ts index 7aa6b1091b..892536438d 100644 --- a/packages/client/src/sync/fetcher/blockfetcher.ts +++ b/packages/client/src/sync/fetcher/blockfetcher.ts @@ -73,7 +73,7 @@ export class BlockFetcher extends BlockFetcherBase { `Requested blocks=${blocksRange} from ${peerInfo} (received: ${headers.length} headers / ${bodies.length} bodies)`, ) const blocks: Block[] = [] - for (const [i, [txsData, unclesData, withdrawalsData, requestsData]] of bodies.entries()) { + for (const [i, [txsData, unclesData, withdrawalsData]] of bodies.entries()) { const header = headers[i] if ( (!equalsBytes(header.transactionsTrie, KECCAK256_RLP) && txsData.length === 0) || @@ -92,9 +92,6 @@ export class BlockFetcher extends BlockFetcherBase { if (withdrawalsData !== undefined) { values.push(withdrawalsData) } - if (requestsData !== undefined) { - values.push(requestsData) - } // Supply the common from the corresponding block header already set on correct fork const block = createBlockFromBytesArray(values, { common: headers[i].common }) // Only validate the data integrity diff --git a/packages/client/test/rpc/debug/storageRangeAt.spec.ts b/packages/client/test/rpc/debug/storageRangeAt.spec.ts index 102c751e97..39ed821131 100644 --- a/packages/client/test/rpc/debug/storageRangeAt.spec.ts +++ b/packages/client/test/rpc/debug/storageRangeAt.spec.ts @@ -154,7 +154,7 @@ describe(method, () => { const thirdResult = await blockBuilder.addTransaction(thirdTx, { skipHardForkValidation: true }) - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() await chain.putBlocks([block], true) context.rpc = rpc diff --git a/packages/client/test/rpc/engine/newPayloadV3VersionedHashes.spec.ts b/packages/client/test/rpc/engine/newPayloadV3VersionedHashes.spec.ts index 0e324b19b7..1d337524a7 100644 --- a/packages/client/test/rpc/engine/newPayloadV3VersionedHashes.spec.ts +++ b/packages/client/test/rpc/engine/newPayloadV3VersionedHashes.spec.ts @@ -42,7 +42,7 @@ describe(`${method}: Cancun validations`, () => { assert.equal(res.result.status, 'INVALID') assert.equal( res.result.validationError, - 'Error verifying blobVersionedHashes: expected=0 received=2', + 'Error assembling block from payload: Error verifying blobVersionedHashes: expected=0 received=2', ) const txString = @@ -105,7 +105,7 @@ describe(`${method}: Cancun validations`, () => { assert.equal(res.result.status, 'INVALID') assert.equal( res.result.validationError, - 'Error verifying blobVersionedHashes: expected=2 received=1', + 'Error assembling block from payload: Error verifying blobVersionedHashes: expected=2 received=1', ) const blockDataExtraMisMatchingHashes1 = [ @@ -127,7 +127,7 @@ describe(`${method}: Cancun validations`, () => { assert.equal(res.result.status, 'INVALID') assert.equal( res.result.validationError, - 'Error verifying blobVersionedHashes: mismatch at index=1 expected=0x0131…52c5 received=0x3456…', + 'Error assembling block from payload: Error verifying blobVersionedHashes: mismatch at index=1 expected=0x0131…52c5 received=0x3456…', ) const blockDataMatchingVersionedHashes = [ diff --git a/packages/client/test/rpc/engine/newPayloadV4.spec.ts b/packages/client/test/rpc/engine/newPayloadV4.spec.ts index 204cff0d29..4538a2d95b 100644 --- a/packages/client/test/rpc/engine/newPayloadV4.spec.ts +++ b/packages/client/test/rpc/engine/newPayloadV4.spec.ts @@ -12,9 +12,9 @@ const [blockData] = beaconData const parentBeaconBlockRoot = '0x42942949c4ed512cd85c2cb54ca88591338cbb0564d3a2bea7961a639ef29d64' const validForkChoiceState = { - headBlockHash: '0x5040e6b0056398536751c187683a3ecde8aff8fd9ea1d3450d687d7032134caf', - safeBlockHash: '0x5040e6b0056398536751c187683a3ecde8aff8fd9ea1d3450d687d7032134caf', - finalizedBlockHash: '0x5040e6b0056398536751c187683a3ecde8aff8fd9ea1d3450d687d7032134caf', + headBlockHash: '0x6abe4c2777a6a1e994d83920cfb95229b071174b95c89343f54b926f733789f2', + safeBlockHash: '0x6abe4c2777a6a1e994d83920cfb95229b071174b95c89343f54b926f733789f2', + finalizedBlockHash: '0x6abe4c2777a6a1e994d83920cfb95229b071174b95c89343f54b926f733789f2', } const validPayloadAttributes = { timestamp: '0x64ba84fd', @@ -41,16 +41,16 @@ const electraGenesisContracts = { code: '0x3373fffffffffffffffffffffffffffffffffffffffe1460575767ffffffffffffffff5f3511605357600143035f3511604b575f35612000014311604b57611fff5f3516545f5260205ff35b5f5f5260205ff35b5f5ffd5b5f35611fff60014303165500', }, // consolidation requests contract - '0x00b42dbF2194e931E80326D950320f7d9Dbeac02': { + '0x00706203067988Ab3E2A2ab626EdCD6f28bDBbbb': { balance: '0', nonce: '1', - code: '0x3373fffffffffffffffffffffffffffffffffffffffe146098573615156028575f545f5260205ff35b36606014156101445760115f54600182026001905f5b5f82111560595781019083028483029004916001019190603e565b90939004341061014457600154600101600155600354806004026004013381556001015f35815560010160203581556001016040359055600101600355005b6003546002548082038060011160ac575060015b5f5b81811460f15780607402838201600402600401805490600101805490600101805490600101549260601b84529083601401528260340152906054015260010160ae565b9101809214610103579060025561010e565b90505f6002555f6003555b5f548061049d141561011d57505f5b6001546001828201116101325750505f610138565b01600190035b5f555f6001556074025ff35b5f5ffd', + code: '0x3373fffffffffffffffffffffffffffffffffffffffe1460cf573615156028575f545f5260205ff35b366060141561019a5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f821115608057810190830284830290049160010191906065565b90939004341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060011160e3575060015b5f5b8181146101295780607402838201600402600401805490600101805490600101805490600101549260601b84529083601401528260340152906054015260010160e5565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd', }, // withdrawals request contract - '0x00A3ca265EBcb825B45F985A16CEFB49958cE017': { + '0x05F27129610CB42103b665629CB5c8C00296AaAa': { balance: '0', nonce: '1', - code: '0x3373fffffffffffffffffffffffffffffffffffffffe146090573615156028575f545f5260205ff35b366038141561012e5760115f54600182026001905f5b5f82111560595781019083028483029004916001019190603e565b90939004341061012e57600154600101600155600354806003026004013381556001015f3581556001016020359055600101600355005b6003546002548082038060101160a4575060105b5f5b81811460dd5780604c02838201600302600401805490600101805490600101549160601b83528260140152906034015260010160a6565b910180921460ed579060025560f8565b90505f6002555f6003555b5f548061049d141561010757505f5b60015460028282011161011c5750505f610122565b01600290035b5f555f600155604c025ff35b5f5ffd', + code: '0x3373fffffffffffffffffffffffffffffffffffffffe1460c7573615156028575f545f5260205ff35b36603814156101f05760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f057600182026001905f5b5f821115608057810190830284830290049160010191906065565b9093900434106101f057600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160db575060105b5f5b81811461017f5780604c02838201600302600401805490600101805490600101549160601b83528260140152807fffffffffffffffffffffffffffffffff0000000000000000000000000000000016826034015260401c906044018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160dd565b9101809214610191579060025561019c565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101c957505f5b6001546002828201116101de5750505f6101e4565b01600290035b5f555f600155604c025ff35b5f5ffd', storage: { '0x0000000000000000000000000000000000000000000000000000000000000000': '0x000000000000000000000000000000000000000000000000000000000000049d', @@ -160,12 +160,9 @@ describe(`${method}: call with executionPayloadV4`, () => { withdrawals: [], blobGasUsed: '0x0', excessBlobGas: '0x0', - depositRequests: [], - withdrawalRequests: [], - consolidationRequests: [], - parentHash: '0x5040e6b0056398536751c187683a3ecde8aff8fd9ea1d3450d687d7032134caf', - stateRoot: '0xbde9840c609ffa39cae0a2c9e354ac673920fcc2a5e6faeef5b78817c7fba7dd', - blockHash: '0x6b3ee4bb75e316427142bb9b48629e3e87ed8eea9f6d42b6aae296a11ec920b3', + parentHash: '0x6abe4c2777a6a1e994d83920cfb95229b071174b95c89343f54b926f733789f2', + stateRoot: '0x7aa6e46df1f78988a3141b5e7da8abee78d1daca175f43fe8866b2d1bf8d8ef8', + blockHash: '0x9a5903d803e6e7c3631cd76cb7279f93d7facc995c53eaffadf4e225504b18eb', } const oldMethods = ['engine_newPayloadV1', 'engine_newPayloadV2', 'engine_newPayloadV3'] @@ -183,7 +180,7 @@ describe(`${method}: call with executionPayloadV4`, () => { assert.ok(res.error.message.includes(expectedError)) } - res = await rpc.request(method, [validBlock, [], parentBeaconBlockRoot]) + res = await rpc.request(method, [validBlock, [], parentBeaconBlockRoot, ['0x', '0x', '0x']]) assert.equal(res.result.status, 'VALID') res = await rpc.request('engine_forkchoiceUpdatedV3', validPayload) @@ -203,21 +200,18 @@ describe(`${method}: call with executionPayloadV4`, () => { await service.txPool.add(depositTx, true) res = await rpc.request('engine_getPayloadV4', [payloadId]) - const { executionPayload } = res.result + const { executionPayload, executionRequests } = res.result assert.ok( - executionPayload.depositRequests?.length === 1, - 'depositRequests should have 1 deposit request', - ) - assert.ok( - executionPayload.withdrawalRequests !== undefined, - 'depositRequests field should be received', - ) - assert.ok( - executionPayload.consolidationRequests !== undefined, - 'consolidationRequests field should be received', + executionRequests?.length === 3, + 'executionRequests should have 3 entries for each request type', ) - res = await rpc.request(method, [executionPayload, [], parentBeaconBlockRoot]) + res = await rpc.request(method, [ + executionPayload, + [], + parentBeaconBlockRoot, + executionRequests, + ]) assert.equal(res.result.status, 'VALID') const newBlockHashHex = executionPayload.blockHash diff --git a/packages/client/test/rpc/eth/blobBaseFee.spec.ts b/packages/client/test/rpc/eth/blobBaseFee.spec.ts index fe2bb51dcd..6d7b00afcb 100644 --- a/packages/client/test/rpc/eth/blobBaseFee.spec.ts +++ b/packages/client/test/rpc/eth/blobBaseFee.spec.ts @@ -84,7 +84,7 @@ const produceBlockWith4844Tx = async ( nonce++ } - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() await chain.putBlocks([block], true) await execution.run() } diff --git a/packages/client/test/rpc/eth/getFeeHistory.spec.ts b/packages/client/test/rpc/eth/getFeeHistory.spec.ts index 6d59877687..65598557a8 100644 --- a/packages/client/test/rpc/eth/getFeeHistory.spec.ts +++ b/packages/client/test/rpc/eth/getFeeHistory.spec.ts @@ -55,7 +55,7 @@ const produceFakeGasUsedBlock = async (execution: VMExecution, chain: Chain, gas }) blockBuilder.gasUsed = gasUsed - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() await chain.putBlocks([block], false) //await execution.run() } @@ -110,7 +110,7 @@ const produceBlockWithTx = async ( nonce++ } - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() await chain.putBlocks([block], false) await execution.run() } @@ -179,7 +179,7 @@ const produceBlockWith4844Tx = async ( nonce++ } - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() await chain.putBlocks([block], true) await execution.run() } diff --git a/packages/client/test/rpc/helpers.ts b/packages/client/test/rpc/helpers.ts index b30dfe94ce..cee1b6f2e3 100644 --- a/packages/client/test/rpc/helpers.ts +++ b/packages/client/test/rpc/helpers.ts @@ -306,7 +306,7 @@ export async function runBlockWithTxs( for (const tx of txs) { await blockBuilder.addTransaction(tx, { skipHardForkValidation: true }) } - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() // put block into chain and run execution await chain.putBlocks([block], fromEngine) diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 6176fb1b0e..7737ca4996 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -52,6 +52,7 @@ export interface GenesisBlockConfig { extraData: PrefixedHexString baseFeePerGas?: PrefixedHexString excessBlobGas?: PrefixedHexString + requestsHash?: PrefixedHexString } export interface HardforkTransitionConfig { diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 6fcbc92f44..fc8ce1c23f 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -38,6 +38,7 @@ function parseGethParams(json: any) { coinbase, baseFeePerGas, excessBlobGas, + requestsHash, extraData: unparsedExtraData, nonce: unparsedNonce, timestamp: unparsedTimestamp, @@ -50,6 +51,7 @@ function parseGethParams(json: any) { coinbase: PrefixedHexString baseFeePerGas: PrefixedHexString excessBlobGas: PrefixedHexString + requestsHash: PrefixedHexString extraData: string nonce: string timestamp: string @@ -105,6 +107,7 @@ function parseGethParams(json: any) { coinbase, baseFeePerGas, excessBlobGas, + requestsHash, }, hardfork: undefined as string | undefined, hardforks: [] as ConfigHardfork[], diff --git a/packages/util/src/constants.ts b/packages/util/src/constants.ts index b1587c9bc7..25eb3f1ee1 100644 --- a/packages/util/src/constants.ts +++ b/packages/util/src/constants.ts @@ -1,4 +1,5 @@ import { secp256k1 } from 'ethereum-cryptography/secp256k1.js' +import { sha256 } from 'ethereum-cryptography/sha256.js' import { hexToBytes } from './bytes.js' @@ -64,6 +65,8 @@ export const KECCAK256_RLP_S = '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cad */ export const KECCAK256_RLP = hexToBytes(KECCAK256_RLP_S) +export const SHA256_NULL = sha256(new Uint8Array()) + /** * RLP encoded empty string */ diff --git a/packages/util/src/request.ts b/packages/util/src/request.ts index 66e359e435..86ff768dd6 100644 --- a/packages/util/src/request.ts +++ b/packages/util/src/request.ts @@ -1,16 +1,5 @@ -import { RLP } from '@ethereumjs/rlp' import { concatBytes } from 'ethereum-cryptography/utils' -import { - bigIntToBytes, - bigIntToHex, - bytesToBigInt, - bytesToHex, - hexToBigInt, - hexToBytes, -} from './bytes.js' -import { BIGINT_0 } from './constants.js' - import type { PrefixedHexString } from './types.js' export type RequestBytes = Uint8Array @@ -21,268 +10,37 @@ export enum CLRequestType { Consolidation = 0x02, } -export type DepositRequestV1 = { - pubkey: PrefixedHexString // DATA 48 bytes - withdrawalCredentials: PrefixedHexString // DATA 32 bytes - amount: PrefixedHexString // QUANTITY 8 bytes in gwei - signature: PrefixedHexString // DATA 96 bytes - index: PrefixedHexString // QUANTITY 8 bytes -} - -export type WithdrawalRequestV1 = { - sourceAddress: PrefixedHexString // DATA 20 bytes - validatorPubkey: PrefixedHexString // DATA 48 bytes - amount: PrefixedHexString // QUANTITY 8 bytes in gwei -} - -export type ConsolidationRequestV1 = { - sourceAddress: PrefixedHexString // DATA 20 bytes - sourcePubkey: PrefixedHexString // DATA 48 bytes - targetPubkey: PrefixedHexString // DATA 48 bytes -} - export interface RequestJSON { - [CLRequestType.Deposit]: DepositRequestV1 - [CLRequestType.Withdrawal]: WithdrawalRequestV1 - [CLRequestType.Consolidation]: ConsolidationRequestV1 -} - -export type DepositRequestData = { - pubkey: Uint8Array - withdrawalCredentials: Uint8Array - amount: bigint - signature: Uint8Array - index: bigint -} - -export type WithdrawalRequestData = { - sourceAddress: Uint8Array - validatorPubkey: Uint8Array - amount: bigint -} - -export type ConsolidationRequestData = { - sourceAddress: Uint8Array - sourcePubkey: Uint8Array - targetPubkey: Uint8Array -} - -export interface RequestData { - [CLRequestType.Deposit]: DepositRequestData - [CLRequestType.Withdrawal]: WithdrawalRequestData - [CLRequestType.Consolidation]: ConsolidationRequestData -} - -export type TypedRequestData = RequestData[CLRequestType] - -export interface CLRequestInterface { - readonly type: T - serialize(): Uint8Array - toJSON(): RequestJSON[T] -} - -export abstract class CLRequest implements CLRequestInterface { - readonly type: T - abstract serialize(): Uint8Array - abstract toJSON(): RequestJSON[T] - constructor(type: T) { - this.type = type - } + type: PrefixedHexString + data: PrefixedHexString } -export class DepositRequest extends CLRequest { - constructor( - public readonly pubkey: Uint8Array, - public readonly withdrawalCredentials: Uint8Array, - public readonly amount: bigint, - public readonly signature: Uint8Array, - public readonly index: bigint, - ) { - super(CLRequestType.Deposit) - } - - serialize() { - const indexBytes = this.index === BIGINT_0 ? new Uint8Array() : bigIntToBytes(this.index) +export class CLRequest { + // for easy use + public readonly bytes: Uint8Array - const amountBytes = this.amount === BIGINT_0 ? new Uint8Array() : bigIntToBytes(this.amount) - - return concatBytes( - Uint8Array.from([this.type]), - RLP.encode([ - this.pubkey, - this.withdrawalCredentials, - amountBytes, - this.signature, - indexBytes, - ]), - ) + get type() { + return this.bytes[0] as T } - toJSON(): DepositRequestV1 { - return { - pubkey: bytesToHex(this.pubkey), - withdrawalCredentials: bytesToHex(this.withdrawalCredentials), - amount: bigIntToHex(this.amount), - signature: bytesToHex(this.signature), - index: bigIntToHex(this.index), - } + get data() { + return this.bytes.subarray(1) } -} - -export class WithdrawalRequest extends CLRequest { - constructor( - public readonly sourceAddress: Uint8Array, - public readonly validatorPubkey: Uint8Array, - public readonly amount: bigint, - ) { - super(CLRequestType.Withdrawal) - } - - serialize() { - const amountBytes = this.amount === BIGINT_0 ? new Uint8Array() : bigIntToBytes(this.amount) - return concatBytes( - Uint8Array.from([this.type]), - RLP.encode([this.sourceAddress, this.validatorPubkey, amountBytes]), - ) + constructor(requestType: T, requestData: Uint8Array) { + this.bytes = concatBytes(new Uint8Array([requestType]), requestData) } - - toJSON(): WithdrawalRequestV1 { - return { - sourceAddress: bytesToHex(this.sourceAddress), - validatorPubkey: bytesToHex(this.validatorPubkey), - amount: bigIntToHex(this.amount), - } - } -} - -export class ConsolidationRequest extends CLRequest { - constructor( - public readonly sourceAddress: Uint8Array, - public readonly sourcePubkey: Uint8Array, - public readonly targetPubkey: Uint8Array, - ) { - super(CLRequestType.Consolidation) - } - - serialize() { - return concatBytes( - Uint8Array.from([this.type]), - RLP.encode([this.sourceAddress, this.sourcePubkey, this.targetPubkey]), - ) - } - - toJSON(): ConsolidationRequestV1 { - return { - sourceAddress: bytesToHex(this.sourceAddress), - sourcePubkey: bytesToHex(this.sourcePubkey), - targetPubkey: bytesToHex(this.targetPubkey), - } - } -} - -export function createDepositRequest(depositData: DepositRequestData): DepositRequest { - const { pubkey, withdrawalCredentials, amount, signature, index } = depositData - return new DepositRequest(pubkey, withdrawalCredentials, amount, signature, index) -} - -export function createDepositRequestFromJSON(jsonData: DepositRequestV1): DepositRequest { - const { pubkey, withdrawalCredentials, amount, signature, index } = jsonData - return createDepositRequest({ - pubkey: hexToBytes(pubkey), - withdrawalCredentials: hexToBytes(withdrawalCredentials), - amount: hexToBigInt(amount), - signature: hexToBytes(signature), - index: hexToBigInt(index), - }) -} - -export function createDepositRequestFromRLP(bytes: Uint8Array): DepositRequest { - const [pubkey, withdrawalCredentials, amount, signature, index] = RLP.decode(bytes) as [ - Uint8Array, - Uint8Array, - Uint8Array, - Uint8Array, - Uint8Array, - ] - return createDepositRequest({ - pubkey, - withdrawalCredentials, - amount: bytesToBigInt(amount), - signature, - index: bytesToBigInt(index), - }) -} - -export function createWithdrawalRequest(withdrawalData: WithdrawalRequestData): WithdrawalRequest { - const { sourceAddress, validatorPubkey, amount } = withdrawalData - return new WithdrawalRequest(sourceAddress, validatorPubkey, amount) -} - -export function createWithdrawalRequestFromJSON(jsonData: WithdrawalRequestV1): WithdrawalRequest { - const { sourceAddress, validatorPubkey, amount } = jsonData - return createWithdrawalRequest({ - sourceAddress: hexToBytes(sourceAddress), - validatorPubkey: hexToBytes(validatorPubkey), - amount: hexToBigInt(amount), - }) -} - -export function createWithdrawalRequestFromRLP(bytes: Uint8Array): WithdrawalRequest { - const [sourceAddress, validatorPubkey, amount] = RLP.decode(bytes) as [ - Uint8Array, - Uint8Array, - Uint8Array, - ] - return createWithdrawalRequest({ - sourceAddress, - validatorPubkey, - amount: bytesToBigInt(amount), - }) -} - -export function createConsolidationRequest( - consolidationData: ConsolidationRequestData, -): ConsolidationRequest { - const { sourceAddress, sourcePubkey, targetPubkey } = consolidationData - return new ConsolidationRequest(sourceAddress, sourcePubkey, targetPubkey) -} - -export function createConsolidationRequestFromJSON( - jsonData: ConsolidationRequestV1, -): ConsolidationRequest { - const { sourceAddress, sourcePubkey, targetPubkey } = jsonData - return createConsolidationRequest({ - sourceAddress: hexToBytes(sourceAddress), - sourcePubkey: hexToBytes(sourcePubkey), - targetPubkey: hexToBytes(targetPubkey), - }) -} - -export function createConsolidationRequestFromRLP(bytes: Uint8Array): ConsolidationRequest { - const [sourceAddress, sourcePubkey, targetPubkey] = RLP.decode(bytes) as [ - Uint8Array, - Uint8Array, - Uint8Array, - ] - return createConsolidationRequest({ - sourceAddress, - sourcePubkey, - targetPubkey, - }) } -export class CLRequestFactory { - public static fromSerializedRequest(bytes: Uint8Array): CLRequest { - switch (bytes[0]) { - case CLRequestType.Deposit: - return createDepositRequestFromRLP(bytes.subarray(1)) - case CLRequestType.Withdrawal: - return createWithdrawalRequestFromRLP(bytes.subarray(1)) - case CLRequestType.Consolidation: - return createConsolidationRequestFromRLP(bytes.subarray(1)) - default: - throw Error(`Invalid request type=${bytes[0]}`) - } +export function createCLRequest(bytes: Uint8Array): CLRequest { + switch (bytes[0]) { + case CLRequestType.Deposit: + return new CLRequest(CLRequestType.Deposit, bytes.subarray(1)) + case CLRequestType.Withdrawal: + return new CLRequest(CLRequestType.Withdrawal, bytes.subarray(1)) + case CLRequestType.Consolidation: + return new CLRequest(CLRequestType.Consolidation, bytes.subarray(1)) + default: + throw Error(`Invalid request type=${bytes[0]}`) } } diff --git a/packages/util/test/requests.spec.ts b/packages/util/test/requests.spec.ts index 45832866cd..9253ec925e 100644 --- a/packages/util/test/requests.spec.ts +++ b/packages/util/test/requests.spec.ts @@ -1,49 +1,31 @@ import { assert, describe, it } from 'vitest' -import { bytesToBigInt, randomBytes } from '../src/bytes.js' -import { - CLRequestFactory, - CLRequestType, - createConsolidationRequest, - createDepositRequest, - createWithdrawalRequest, -} from '../src/request.js' +import { concatBytes, randomBytes } from '../src/bytes.js' +import { CLRequestType, createCLRequest } from '../src/request.js' -import type { - CLRequest, - ConsolidationRequest, - DepositRequest, - WithdrawalRequest, -} from '../src/request.js' +import type { CLRequest } from '../src/request.js' describe('Requests', () => { - const testCases: [ - string, - any, - CLRequestType, - (...args: any) => ConsolidationRequest | DepositRequest | WithdrawalRequest, - ][] = [ + const testCases: [string, { [key: string]: Uint8Array }, CLRequestType][] = [ [ 'DepositRequest', { pubkey: randomBytes(48), withdrawalCredentials: randomBytes(32), - amount: bytesToBigInt(randomBytes(8)), + amount: randomBytes(8), signature: randomBytes(96), - index: bytesToBigInt(randomBytes(8)), + index: randomBytes(8), }, CLRequestType.Deposit, - createDepositRequest, ], [ 'WithdrawalRequest', { sourceAddress: randomBytes(20), validatorPubkey: randomBytes(48), - amount: bytesToBigInt(randomBytes(8)), + amount: randomBytes(8), }, CLRequestType.Withdrawal, - createWithdrawalRequest, ], [ 'ConsolidationRequest', @@ -53,22 +35,22 @@ describe('Requests', () => { targetPubkey: randomBytes(48), }, CLRequestType.Consolidation, - createConsolidationRequest, ], ] - for (const [requestName, requestData, requestType, requestInstanceConstructor] of testCases) { + for (const [requestName, requestData, requestType] of testCases) { it(`${requestName}`, () => { - const requestObject = requestInstanceConstructor(requestData) as CLRequest - const requestJSON = requestObject.toJSON() - const serialized = requestObject.serialize() - assert.equal(serialized[0], requestType) + // flatten request bytes as per EIP-7685 + const depositRequestBytes = new Uint8Array( + Object.values(requestData) + .map((arr) => Array.from(arr)) // Convert Uint8Arrays to regular arrays + .reduce((acc, curr) => acc.concat(curr), []), // Concatenate arrays + ) + const requestObject = createCLRequest( + concatBytes(new Uint8Array([requestType]), depositRequestBytes), + ) as CLRequest - const deserialized = CLRequestFactory.fromSerializedRequest(serialized) - const deserializedJSON = deserialized.toJSON() - assert.deepEqual(deserializedJSON, requestJSON) - - const reserialized = deserialized.serialize() - assert.deepEqual(serialized, reserialized) + assert.equal(requestObject.type, requestType) + assert.deepEqual(requestObject.data, depositRequestBytes) }) } }) diff --git a/packages/vm/examples/buildBlock.ts b/packages/vm/examples/buildBlock.ts index 1aee92e9d2..7f2d07963e 100644 --- a/packages/vm/examples/buildBlock.ts +++ b/packages/vm/examples/buildBlock.ts @@ -35,7 +35,7 @@ const main = async () => { // Add more transactions - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() console.log(`Built a block with hash ${bytesToHex(block.hash())}`) } diff --git a/packages/vm/src/buildBlock.ts b/packages/vm/src/buildBlock.ts index b95fec859d..60535a28c6 100644 --- a/packages/vm/src/buildBlock.ts +++ b/packages/vm/src/buildBlock.ts @@ -1,7 +1,7 @@ import { createBlock, createSealedCliqueBlock, - genRequestsTrieRoot, + genRequestsRoot, genTransactionsTrieRoot, genWithdrawalsTrieRoot, } from '@ethereumjs/block' @@ -22,6 +22,7 @@ import { toBytes, toType, } from '@ethereumjs/util' +import { sha256 } from 'ethereum-cryptography/sha256' import { Bloom } from './bloom/index.js' import { accumulateRequests } from './requests.js' @@ -342,11 +343,11 @@ export class BlockBuilder { } let requests - let requestsRoot + let requestsHash if (this.vm.common.isActivatedEIP(7685)) { + const sha256Function = this.vm.common.customCrypto.sha256 ?? sha256 requests = await accumulateRequests(this.vm, this.transactionResults) - requestsRoot = await genRequestsTrieRoot(requests) - // Do other validations per request type + requestsHash = genRequestsRoot(requests, sha256Function) } // get stateRoot after all the accumulateRequests etc have been done @@ -362,7 +363,7 @@ export class BlockBuilder { timestamp, // correct excessBlobGas should already be part of headerData used above blobGasUsed, - requestsRoot, + requestsHash, } if (consensusType === ConsensusType.ProofOfWork) { @@ -374,7 +375,6 @@ export class BlockBuilder { header: headerData, transactions: this.transactions, withdrawals: this.withdrawals, - requests, } let block @@ -395,7 +395,7 @@ export class BlockBuilder { this.checkpointed = false } - return block + return { block, requests } } async initState() { diff --git a/packages/vm/src/params.ts b/packages/vm/src/params.ts index 1d3a576b2c..dfaa021cda 100644 --- a/packages/vm/src/params.ts +++ b/packages/vm/src/params.ts @@ -78,7 +78,8 @@ export const paramsVM: ParamsDict = { 7002: { // config systemAddress: '0xfffffffffffffffffffffffffffffffffffffffe', // The system address to perform operations on the withdrawal requests predeploy address - withdrawalRequestPredeployAddress: '0x00A3ca265EBcb825B45F985A16CEFB49958cE017', // Address of the validator excess address + // See: https://github.com/ethereum/EIPs/pull/8934/files + withdrawalRequestPredeployAddress: '0x09Fc772D0857550724b07B850a4323f39112aAaA', // Address of the validator excess address }, /** @@ -87,6 +88,7 @@ export const paramsVM: ParamsDict = { 7251: { // config systemAddress: '0xfffffffffffffffffffffffffffffffffffffffe', // The system address to perform operations on the consolidation requests predeploy address - consolidationRequestPredeployAddress: '0x00b42dbF2194e931E80326D950320f7d9Dbeac02', // Address of the consolidations contract + // See: https://github.com/ethereum/EIPs/pull/8934/files + consolidationRequestPredeployAddress: '0x01aBEa29659e5e97C95107F20bb753cD3e09bBBb', // Address of the consolidations contract }, } diff --git a/packages/vm/src/requests.ts b/packages/vm/src/requests.ts index 54230e18bc..136e6a43b0 100644 --- a/packages/vm/src/requests.ts +++ b/packages/vm/src/requests.ts @@ -1,21 +1,18 @@ import { Mainnet } from '@ethereumjs/common' import { + CLRequest, + CLRequestType, bigIntToAddressBytes, bigIntToBytes, - bytesToBigInt, bytesToHex, bytesToInt, + concatBytes, createAddressFromString, - createConsolidationRequest, - createDepositRequest, - createWithdrawalRequest, setLengthLeft, - unpadBytes, } from '@ethereumjs/util' import type { RunTxResult } from './types.js' import type { VM } from './vm.js' -import type { CLRequest, CLRequestType } from '@ethereumjs/util' /** * This helper method generates a list of all CL requests that can be included in a pending block @@ -35,30 +32,27 @@ export const accumulateRequests = async ( vm.common['_chainParams'].depositContractAddress ?? Mainnet.depositContractAddress if (depositContractAddress === undefined) throw new Error('deposit contract address required with EIP 6110') - await accumulateDeposits(depositContractAddress, txResults, requests) + const depositsRequest = accumulateDepositsRequest(depositContractAddress, txResults) + requests.push(depositsRequest) } if (common.isActivatedEIP(7002)) { - await accumulateEIP7002Requests(vm, requests) + const withdrawalsRequest = await accumulateWithdrawalsRequest(vm) + requests.push(withdrawalsRequest) } if (common.isActivatedEIP(7251)) { - await accumulateEIP7251Requests(vm, requests) + const consolidationsRequest = await accumulateConsolidationsRequest(vm) + requests.push(consolidationsRequest) } - 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') - } - } + // requests are already type byte ordered by construction return requests } -const accumulateEIP7002Requests = async ( +const accumulateWithdrawalsRequest = async ( vm: VM, - requests: CLRequest[], -): Promise => { +): Promise> => { // Partial withdrawals logic const addressBytes = setLengthLeft( bigIntToBytes(vm.common.param('withdrawalRequestPredeployAddress')), @@ -73,7 +67,7 @@ const accumulateEIP7002Requests = async ( const originalAccount = await vm.stateManager.getAccount(withdrawalsAddress) if (originalAccount === undefined) { - return + return new CLRequest(CLRequestType.Withdrawal, new Uint8Array()) } const results = await vm.evm.runCall({ @@ -89,22 +83,12 @@ const accumulateEIP7002Requests = async ( } const resultsBytes = results.execResult.returnValue - if (resultsBytes.length > 0) { - // Each request is 76 bytes - for (let startByte = 0; startByte < resultsBytes.length; startByte += 76) { - const slicedBytes = resultsBytes.slice(startByte, startByte + 76) - const sourceAddress = slicedBytes.slice(0, 20) // 20 Bytes - const validatorPubkey = slicedBytes.slice(20, 68) // 48 Bytes - const amount = bytesToBigInt(unpadBytes(slicedBytes.slice(68, 76))) // 8 Bytes / Uint64 - requests.push(createWithdrawalRequest({ sourceAddress, validatorPubkey, amount })) - } - } + return new CLRequest(CLRequestType.Withdrawal, resultsBytes) } -const accumulateEIP7251Requests = async ( +const accumulateConsolidationsRequest = async ( vm: VM, - requests: CLRequest[], -): Promise => { +): Promise> => { // Partial withdrawals logic const addressBytes = setLengthLeft( bigIntToBytes(vm.common.param('consolidationRequestPredeployAddress')), @@ -119,7 +103,7 @@ const accumulateEIP7251Requests = async ( const originalAccount = await vm.stateManager.getAccount(consolidationsAddress) if (originalAccount === undefined) { - return + return new CLRequest(CLRequestType.Consolidation, new Uint8Array(0)) } const results = await vm.evm.runCall({ @@ -135,91 +119,72 @@ const accumulateEIP7251Requests = async ( } const resultsBytes = results.execResult.returnValue - if (resultsBytes.length > 0) { - // Each request is 116 bytes - for (let startByte = 0; startByte < resultsBytes.length; startByte += 116) { - const slicedBytes = resultsBytes.slice(startByte, startByte + 116) - const sourceAddress = slicedBytes.slice(0, 20) // 20 Bytes - const sourcePubkey = slicedBytes.slice(20, 68) // 48 Bytes - const targetPubkey = slicedBytes.slice(68, 116) // 48 bytes - requests.push(createConsolidationRequest({ sourceAddress, sourcePubkey, targetPubkey })) - } - } + return new CLRequest(CLRequestType.Consolidation, resultsBytes) } -const accumulateDeposits = async ( +const accumulateDepositsRequest = ( depositContractAddress: string, txResults: RunTxResult[], - requests: CLRequest[], -) => { +): CLRequest => { + let resultsBytes = new Uint8Array(0) + const depositContractAddressLowerCase = depositContractAddress.toLowerCase() for (const [_, tx] of txResults.entries()) { for (let i = 0; i < tx.receipt.logs.length; i++) { const log = tx.receipt.logs[i] - if (bytesToHex(log[0]).toLowerCase() === depositContractAddress.toLowerCase()) { - // Extracts validator pubkey, withdrawal credential, deposit amount, signature, - // and validator index from Deposit Event log. - // The event fields are non-indexed so contained in one byte array (log[2]) so parsing is as follows: - // 1. Read the first 32 bytes to get the starting position of the first field. - // 2. Continue reading the byte array in 32 byte increments to get all the field starting positions - // 3. Read 32 bytes starting with the first field position to get the size of the first field - // 4. Read the bytes from first field position + 32 + the size of the first field to get the first field value - // 5. Repeat steps 3-4 for each field - const pubKeyIdx = bytesToInt(log[2].slice(0, 32)) - const pubKeySize = bytesToInt(log[2].slice(pubKeyIdx, pubKeyIdx + 32)) - const withdrawalCreditsIdx = bytesToInt(log[2].slice(32, 64)) - const withdrawalCreditsSize = bytesToInt( - log[2].slice(withdrawalCreditsIdx, withdrawalCreditsIdx + 32), - ) - const amountIdx = bytesToInt(log[2].slice(64, 96)) - const amountSize = bytesToInt(log[2].slice(amountIdx, amountIdx + 32)) - const sigIdx = bytesToInt(log[2].slice(96, 128)) - const sigSize = bytesToInt(log[2].slice(sigIdx, sigIdx + 32)) - const indexIdx = bytesToInt(log[2].slice(128, 160)) - const indexSize = bytesToInt(log[2].slice(indexIdx, indexIdx + 32)) - const pubkey = log[2].slice(pubKeyIdx + 32, pubKeyIdx + 32 + pubKeySize) - const withdrawalCredentials = log[2].slice( - withdrawalCreditsIdx + 32, - withdrawalCreditsIdx + 32 + withdrawalCreditsSize, - ) - const amountBytes = log[2].slice(amountIdx + 32, amountIdx + 32 + amountSize) - const amountBytesBigEndian = new Uint8Array([ - amountBytes[7], - amountBytes[6], - amountBytes[5], - amountBytes[4], - amountBytes[3], - amountBytes[2], - amountBytes[1], - amountBytes[0], - ]) - const amount = bytesToBigInt(amountBytesBigEndian) - - const signature = log[2].slice(sigIdx + 32, sigIdx + 32 + sigSize) - - const indexBytes = log[2].slice(indexIdx + 32, indexIdx + 32 + indexSize) - - // Convert the little-endian array to big-endian array - const indexBytesBigEndian = new Uint8Array([ - indexBytes[7], - indexBytes[6], - indexBytes[5], - indexBytes[4], - indexBytes[3], - indexBytes[2], - indexBytes[1], - indexBytes[0], - ]) - const index = bytesToBigInt(indexBytesBigEndian) - requests.push( - createDepositRequest({ - pubkey, - withdrawalCredentials, - amount, - signature, - index, - }), + if (bytesToHex(log[0]).toLowerCase() === depositContractAddressLowerCase) { + const { pubkey, withdrawalCredentials, amount, signature, index } = parseDepositLog(log[2]) + const depositRequestBytes = concatBytes( + pubkey, + withdrawalCredentials, + amount, + signature, + index, ) + + resultsBytes = concatBytes(resultsBytes, depositRequestBytes) } } } + + return new CLRequest(CLRequestType.Deposit, resultsBytes) +} + +function parseDepositLog(requestData: Uint8Array) { + // Extracts validator pubkey, withdrawal credential, deposit amount, signature, + // and validator index from Deposit Event log. + // The event fields are non-indexed so contained in one byte array (log[2]) so parsing is as follows: + // 1. Read the first 32 bytes to get the starting position of the first field. + // 2. Continue reading the byte array in 32 byte increments to get all the field starting positions + // 3. Read 32 bytes starting with the first field position to get the size of the first field + // 4. Read the bytes from first field position + 32 + the size of the first field to get the first field value + // 5. Repeat steps 3-4 for each field + const pubKeyIdx = bytesToInt(requestData.slice(0, 32)) + const pubKeySize = bytesToInt(requestData.slice(pubKeyIdx, pubKeyIdx + 32)) + const withdrawalCreditsIdx = bytesToInt(requestData.slice(32, 64)) + const withdrawalCreditsSize = bytesToInt( + requestData.slice(withdrawalCreditsIdx, withdrawalCreditsIdx + 32), + ) + const amountIdx = bytesToInt(requestData.slice(64, 96)) + const amountSize = bytesToInt(requestData.slice(amountIdx, amountIdx + 32)) + const sigIdx = bytesToInt(requestData.slice(96, 128)) + const sigSize = bytesToInt(requestData.slice(sigIdx, sigIdx + 32)) + const indexIdx = bytesToInt(requestData.slice(128, 160)) + const indexSize = bytesToInt(requestData.slice(indexIdx, indexIdx + 32)) + + const pubkey = requestData.slice(pubKeyIdx + 32, pubKeyIdx + 32 + pubKeySize) + const withdrawalCredentials = requestData.slice( + withdrawalCreditsIdx + 32, + withdrawalCreditsIdx + 32 + withdrawalCreditsSize, + ) + const amount = requestData.slice(amountIdx + 32, amountIdx + 32 + amountSize) + const signature = requestData.slice(sigIdx + 32, sigIdx + 32 + sigSize) + const index = requestData.slice(indexIdx + 32, indexIdx + 32 + indexSize) + + return { + pubkey, + withdrawalCredentials, + amount, + signature, + index, + } } diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 520960df2f..4ccb52434e 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -1,4 +1,4 @@ -import { createBlock, genRequestsTrieRoot } from '@ethereumjs/block' +import { createBlock, genRequestsRoot } from '@ethereumjs/block' import { ConsensusType, Hardfork } from '@ethereumjs/common' import { type EVM, type EVMInterface, VerkleAccessWitness } from '@ethereumjs/evm' import { MerklePatriciaTrie } from '@ethereumjs/mpt' @@ -27,6 +27,7 @@ import { unprefixedHexToBytes, } from '@ethereumjs/util' import debugDefault from 'debug' +import { sha256 } from 'ethereum-cryptography/sha256' import { Bloom } from './bloom/index.js' import { emitEVMProfile } from './emitEVMProfile.js' @@ -218,11 +219,12 @@ export async function runBlock(vm: VM, opts: RunBlockOpts): Promise[] | undefined if (block.common.isActivatedEIP(7685)) { + const sha256Function = vm.common.customCrypto.sha256 ?? sha256 requests = await accumulateRequests(vm, result.results) - requestsRoot = await genRequestsTrieRoot(requests) + requestsHash = genRequestsRoot(requests, sha256Function) } // Persist state @@ -237,40 +239,38 @@ export async function runBlock(vm: VM, opts: RunBlockOpts): Promise { logsBloom: Uint8Array /** - * The requestsRoot for any CL requests in the block + * The requestsHash for any CL requests in the block */ - requestsRoot?: Uint8Array + requestsHash?: Uint8Array /** * Any CL requests that were processed in the course of this block */ diff --git a/packages/vm/test/api/EIPs/eip-2935-historical-block-hashes.spec.ts b/packages/vm/test/api/EIPs/eip-2935-historical-block-hashes.spec.ts index 73aeffbef6..b5eae454e2 100644 --- a/packages/vm/test/api/EIPs/eip-2935-historical-block-hashes.spec.ts +++ b/packages/vm/test/api/EIPs/eip-2935-historical-block-hashes.spec.ts @@ -182,7 +182,7 @@ describe('EIP 2935: historical block hashes', () => { timestamp: 1, }) const genesis = (await vm.blockchain.getBlock(0)) as Block - const block = await ( + const { block } = await ( await buildBlock(vm, { parentBlock: genesis, blockOpts: { @@ -220,7 +220,7 @@ describe('EIP 2935: historical block hashes', () => { await vm.stateManager.putCode(historyAddress, contract2935Code) let lastBlock = (await vm.blockchain.getBlock(0)) as Block for (let i = 1; i <= blocksToBuild; i++) { - lastBlock = await ( + const buildResult = await ( await buildBlock(vm, { parentBlock: lastBlock, blockOpts: { @@ -232,6 +232,7 @@ describe('EIP 2935: historical block hashes', () => { }, }) ).build() + lastBlock = buildResult.block await vm.blockchain.putBlock(lastBlock) await runBlock(vm, { block: lastBlock, diff --git a/packages/vm/test/api/EIPs/eip-4844-blobs.spec.ts b/packages/vm/test/api/EIPs/eip-4844-blobs.spec.ts index 01189ed9f3..bd0e8cdf1c 100644 --- a/packages/vm/test/api/EIPs/eip-4844-blobs.spec.ts +++ b/packages/vm/test/api/EIPs/eip-4844-blobs.spec.ts @@ -82,7 +82,7 @@ describe('EIP4844 tests', () => { await blockBuilder.addTransaction(signedTx) - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() assert.equal(block.transactions.length, 1, 'blob transaction should be included') assert.equal( bytesToHex(block.transactions[0].hash()), diff --git a/packages/vm/test/api/EIPs/eip-4895-withdrawals.spec.ts b/packages/vm/test/api/EIPs/eip-4895-withdrawals.spec.ts index 6686a10350..86684b2f1f 100644 --- a/packages/vm/test/api/EIPs/eip-4895-withdrawals.spec.ts +++ b/packages/vm/test/api/EIPs/eip-4895-withdrawals.spec.ts @@ -226,7 +226,7 @@ describe('EIP4895 tests', () => { }, }) - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() assert.equal( bytesToHex(block.header.stateRoot), diff --git a/packages/vm/test/api/EIPs/eip-6110.spec.ts b/packages/vm/test/api/EIPs/eip-6110.spec.ts index 315bade06d..1ceb939bab 100644 --- a/packages/vm/test/api/EIPs/eip-6110.spec.ts +++ b/packages/vm/test/api/EIPs/eip-6110.spec.ts @@ -13,10 +13,10 @@ import { import { keccak256 } from 'ethereum-cryptography/keccak.js' import { assert, describe, it } from 'vitest' +import { CLRequestType } from '../../../../util/src/request.js' import { buildBlock, runBlock } from '../../../src/index.js' import { setupVM } from '../utils.js' -import type { DepositRequest } from '../../../../util/src/request.js' import type { PrefixedHexString } from '@ethereumjs/util' const depositContractByteCode = hexToBytes( @@ -65,8 +65,10 @@ describe('EIP-6110 runBlock tests', () => { ) const res = await runBlock(vm, { block, generate: true, skipBlockValidation: true }) assert.equal(res.requests?.length, 1) - const reqPubkey = (res.requests![0] as DepositRequest).pubkey - assert.equal(bytesToHex(reqPubkey), pubkey) + const depositRequest = res.requests![0] + assert.equal(depositRequest.type, CLRequestType.Deposit) + const parsedRequest = parseDepositRequest(depositRequest.data) + assert.equal(bytesToHex(parsedRequest.pubkey), pubkey) }) }) @@ -96,7 +98,26 @@ describe('EIP-7685 buildBlock tests', () => { await blockBuilder.addTransaction(depositTx) const res = await blockBuilder.build() assert.equal(res.requests?.length, 1) - const reqPubkey = (res.requests![0] as DepositRequest).pubkey - assert.equal(bytesToHex(reqPubkey), pubkey) + + const depositRequest = res.requests![0] + assert.equal(depositRequest.type, CLRequestType.Deposit) + const parsedRequest = parseDepositRequest(depositRequest.data) + assert.equal(bytesToHex(parsedRequest.pubkey), pubkey) }) }) + +function parseDepositRequest(requestData: Uint8Array) { + const pubkey = requestData.subarray(0, 48) + const withdrawalCredentials = requestData.subarray(48, 48 + 32) + const amount = requestData.subarray(48 + 32, 48 + 32 + 8) + const signature = requestData.subarray(48 + 32 + 8, 48 + 32 + 8 + 96) + const index = requestData.subarray(48 + 32 + 8 + 96, 48 + 32 + 8 + 96 + 8) + + return { + pubkey, + withdrawalCredentials, + amount, + signature, + index, + } +} diff --git a/packages/vm/test/api/EIPs/eip-7002.spec.ts b/packages/vm/test/api/EIPs/eip-7002.spec.ts index f92e8d661a..35d064686d 100644 --- a/packages/vm/test/api/EIPs/eip-7002.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7002.spec.ts @@ -1,6 +1,5 @@ import { createBlock } from '@ethereumjs/block' import { Common, Hardfork, Mainnet } from '@ethereumjs/common' -import { RLP } from '@ethereumjs/rlp' import { createLegacyTx } from '@ethereumjs/tx' import { Account, @@ -15,7 +14,7 @@ import { } from '@ethereumjs/util' import { assert, describe, it } from 'vitest' -import { bytesToBigInt } from '../../../../util/src/bytes.js' +import { CLRequestType } from '../../../../util/src/request.js' import { runBlock } from '../../../src/index.js' import { setupVM } from '../utils.js' @@ -33,11 +32,11 @@ const deploymentTxData = { gasLimit: BigInt('0x3d090'), gasPrice: BigInt('0xe8d4a51000'), data: hexToBytes( - '0x61049d5f5561013280600f5f395ff33373fffffffffffffffffffffffffffffffffffffffe146090573615156028575f545f5260205ff35b366038141561012e5760115f54600182026001905f5b5f82111560595781019083028483029004916001019190603e565b90939004341061012e57600154600101600155600354806003026004013381556001015f3581556001016020359055600101600355005b6003546002548082038060101160a4575060105b5f5b81811460dd5780604c02838201600302600401805490600101805490600101549160601b83528260140152906034015260010160a6565b910180921460ed579060025560f8565b90505f6002555f6003555b5f548061049d141561010757505f5b60015460028282011161011c5750505f610122565b01600290035b5f555f600155604c025ff35b5f5ffd', + '0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5f556101f480602d5f395ff33373fffffffffffffffffffffffffffffffffffffffe1460c7573615156028575f545f5260205ff35b36603814156101f05760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f057600182026001905f5b5f821115608057810190830284830290049160010191906065565b9093900434106101f057600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160db575060105b5f5b81811461017f5780604c02838201600302600401805490600101805490600101549160601b83528260140152807fffffffffffffffffffffffffffffffff0000000000000000000000000000000016826034015260401c906044018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160dd565b9101809214610191579060025561019c565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101c957505f5b6001546002828201116101de5750505f6101e4565b01600290035b5f555f600155604c025ff35b5f5ffd', ), v: BigInt('0x1b'), r: BigInt('0x539'), - s: BigInt('0xaba653c9d105790c'), + s: BigInt('0x10e740537d4d36b9'), } const deploymentTx = createLegacyTx(deploymentTxData) @@ -111,7 +110,7 @@ describe('EIP-7002 tests', () => { generatedBlock = e.block }) - await runBlock(vm, { + let runBlockResults = await runBlock(vm, { block: block2, skipHeaderValidation: true, skipBlockValidation: true, @@ -119,21 +118,30 @@ describe('EIP-7002 tests', () => { }) // Ensure the request is generated - assert.ok(generatedBlock!.requests!.length === 1) - - const requestDecoded = RLP.decode(generatedBlock!.requests![0].serialize().slice(1)) + assert.ok(runBlockResults.requests!.length === 1) + assert.equal( + generatedBlock!.transactions.length, + 1, + 'withdrawal transaction should be included', + ) - const sourceAddressRequest = requestDecoded[0] as Uint8Array - const validatorPubkeyRequest = requestDecoded[1] as Uint8Array - const amountRequest = requestDecoded[2] as Uint8Array + const withdrawalRequest = runBlockResults.requests![0] + assert.equal( + withdrawalRequest.type, + CLRequestType.Withdrawal, + 'make sure its withdrawal request', + ) + // amount is in le when contract pack it in requests + const expectedRequestData = concatBytes( + tx.getSenderAddress().bytes, + validatorPubkey, + amountBytes.reverse(), + ) // Ensure the requests are correct - assert.ok(equalsBytes(sourceAddressRequest, tx.getSenderAddress().bytes)) - assert.ok(equalsBytes(validatorPubkey, validatorPubkeyRequest)) - // the direct byte comparison fails because leading zeros have been stripped - // off the amountBytes because it was serialized in request from bigint - assert.equal(bytesToBigInt(amountBytes), bytesToBigInt(amountRequest)) + assert.ok(equalsBytes(expectedRequestData, withdrawalRequest.data)) + // generated block should be valid await runBlock(vm, { block: generatedBlock!, skipHeaderValidation: true, root }) // Run block with 2 requests @@ -152,7 +160,7 @@ describe('EIP-7002 tests', () => { { common }, ) - await runBlock(vm, { + runBlockResults = await runBlock(vm, { block: block3, skipHeaderValidation: true, skipBlockValidation: true, @@ -161,7 +169,12 @@ describe('EIP-7002 tests', () => { // Note: generatedBlock is now overridden with the new generated block (this is thus block number 3) // Ensure there are 2 requests - assert.ok(generatedBlock!.requests!.length === 2) + assert.ok(runBlockResults.requests!.length === 1) + assert.equal( + generatedBlock!.transactions.length, + 2, + 'withdrawal transactions should be included', + ) }) it('should throw when contract is not deployed', async () => { diff --git a/packages/vm/test/api/EIPs/eip-7685.spec.ts b/packages/vm/test/api/EIPs/eip-7685.spec.ts index 84747031ba..99b0f4c2d2 100644 --- a/packages/vm/test/api/EIPs/eip-7685.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7685.spec.ts @@ -1,13 +1,8 @@ -import { createBlock, genRequestsTrieRoot } from '@ethereumjs/block' +import { createBlock, genRequestsRoot } from '@ethereumjs/block' import { createBlockchain } from '@ethereumjs/blockchain' import { Common, Hardfork, Mainnet } from '@ethereumjs/common' -import { - KECCAK256_RLP, - bytesToBigInt, - createDepositRequest, - hexToBytes, - randomBytes, -} from '@ethereumjs/util' +import { createCLRequest, hexToBytes } from '@ethereumjs/util' +import { sha256 } from 'ethereum-cryptography/sha256' import { assert, describe, expect, it } from 'vitest' import { buildBlock, createVM, runBlock } from '../../../src/index.js' @@ -19,20 +14,16 @@ const invalidRequestsRoot = hexToBytes( '0xc98048d6605eb79ecc08d90b8817f44911ec474acd8d11688453d2c6ef743bc5', ) function getRandomDepositRequest(): CLRequest { - const depositRequestData = { - pubkey: randomBytes(48), - withdrawalCredentials: randomBytes(32), - amount: bytesToBigInt(randomBytes(8)), - signature: randomBytes(96), - index: bytesToBigInt(randomBytes(8)), - } - return createDepositRequest(depositRequestData) as CLRequest + const sampleDepositRequest = hexToBytes( + '0x00ac842878bb70009552a4cfcad801d6e659c50bd50d7d03306790cb455ce7363c5b6972f0159d170f625a99b2064dbefc010000000000000000000000818ccb1c4eda80270b04d6df822b1e72dd83c3030040597307000000a747f75c72d0cf0d2b52504c7385b516f0523e2f0842416399f42b4aee5c6384a5674f6426b1cc3d0827886fa9b909e616f5c9f61f986013ed2b9bf37071cbae951136265b549f44e3c8e26233c0433e9124b7fd0dc86e82f9fedfc0a179d7690000000000000000', + ) + return createCLRequest(sampleDepositRequest) } const common = new Common({ chain: Mainnet, hardfork: Hardfork.Cancun, eips: [7685] }) describe('EIP-7685 runBlock tests', () => { - it('should not error when a valid requestsRoot is provided', async () => { + it('should not error when a valid requestsHash is provided', async () => { const vm = await setupVM({ common }) const emptyBlock = createBlock({}, { common }) const res = await runBlock(vm, { @@ -41,45 +32,45 @@ describe('EIP-7685 runBlock tests', () => { }) assert.equal(res.gasUsed, 0n) }) - it('should error when an invalid requestsRoot is provided', async () => { + it('should error when an invalid requestsHash is provided', async () => { const vm = await setupVM({ common }) - const emptyBlock = createBlock({ header: { requestsRoot: invalidRequestsRoot } }, { common }) + const emptyBlock = createBlock({ header: { requestsHash: invalidRequestsRoot } }, { common }) await expect(async () => runBlock(vm, { block: emptyBlock, }), - ).rejects.toThrow('invalid requestsRoot') + ).rejects.toThrow('invalid requestsHash') }) - it('should not throw invalid requestsRoot error when valid requests are provided', async () => { + it('should not throw invalid requestsHash error when valid requests are provided', async () => { const vm = await setupVM({ common }) const request = getRandomDepositRequest() - const requestsRoot = await genRequestsTrieRoot([request]) + const requestsHash = genRequestsRoot([request], sha256) const block = createBlock( { - requests: [request], - header: { requestsRoot }, + header: { requestsHash }, }, { common }, ) - await expect(async () => runBlock(vm, { block })).rejects.toThrow(/invalid requestsRoot/) + await expect(async () => runBlock(vm, { block })).rejects.toThrow(/invalid requestsHash/) }) - it('should error when requestsRoot does not match requests provided', async () => { + + // TODO: no way to test this without running block, why check why does this test pass + // as it should not throw on some random request root + it('should error when requestsHash does not match requests provided', async () => { const vm = await setupVM({ common }) - const request = getRandomDepositRequest() const block = createBlock( { - requests: [request], - header: { requestsRoot: invalidRequestsRoot }, + header: { requestsHash: invalidRequestsRoot }, }, { common }, ) - await expect(() => runBlock(vm, { block })).rejects.toThrow('invalid requestsRoot') + await expect(() => runBlock(vm, { block })).rejects.toThrow('invalid requestsHash') }) }) describe('EIP 7685 buildBlock tests', () => { - it('should build a block without a request and a valid requestsRoot', async () => { + it('should build a block without a request and a valid requestsHash', async () => { const common = new Common({ chain: Mainnet, hardfork: Hardfork.Cancun, @@ -96,8 +87,11 @@ describe('EIP 7685 buildBlock tests', () => { blockOpts: { calcDifficultyFromHeader: genesisBlock.header, freeze: false }, }) - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() - assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP) + assert.deepEqual( + block.header.requestsHash, + hexToBytes('0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'), + ) }) }) diff --git a/packages/vm/test/api/buildBlock.spec.ts b/packages/vm/test/api/buildBlock.spec.ts index 5405a1a860..6bd2f8af2f 100644 --- a/packages/vm/test/api/buildBlock.spec.ts +++ b/packages/vm/test/api/buildBlock.spec.ts @@ -57,7 +57,7 @@ describe('BlockBuilder', () => { ).sign(privateKey) await blockBuilder.addTransaction(tx) - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() assert.equal( blockBuilder.transactionReceipts.length, 1, @@ -132,7 +132,7 @@ describe('BlockBuilder', () => { mixHash: new Uint8Array(32), nonce: new Uint8Array(8), } - const block = await blockBuilder.build(sealOpts) + const { block } = await blockBuilder.build(sealOpts) assert.deepEqual(block.header.mixHash, sealOpts.mixHash) assert.deepEqual(block.header.nonce, sealOpts.nonce) @@ -230,7 +230,7 @@ describe('BlockBuilder', () => { await blockBuilder.addTransaction(tx) - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() assert.ok(cliqueVerifySignature(block.header, [signer.address]), 'should verify signature') assert.deepEqual( @@ -306,7 +306,7 @@ describe('BlockBuilder', () => { blockOpts: { calcDifficultyFromHeader: genesisBlock.header, freeze: false }, }) - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() // block should successfully execute with VM.runBlock and have same outputs const result = await runBlock(vmCopy, { block }) @@ -374,7 +374,7 @@ describe('BlockBuilder', () => { assert.ok('should pass') } - const block = await blockBuilder.build() + const { block } = await blockBuilder.build() assert.equal( blockBuilder.transactionReceipts.length, 2, diff --git a/packages/vm/test/t8n/t8ntool.ts b/packages/vm/test/t8n/t8ntool.ts index d71c602439..068aacf97b 100644 --- a/packages/vm/test/t8n/t8ntool.ts +++ b/packages/vm/test/t8n/t8ntool.ts @@ -2,7 +2,7 @@ import { createBlock } from '@ethereumjs/block' import { EVMMockBlockchain, NobleBLS } from '@ethereumjs/evm' import { RLP } from '@ethereumjs/rlp' import { createTx } from '@ethereumjs/tx' -import { CLRequestType, bigIntToHex, bytesToHex, hexToBytes, toBytes } from '@ethereumjs/util' +import { bigIntToHex, bytesToHex, hexToBytes, toBytes } from '@ethereumjs/util' import { trustedSetup } from '@paulmillr/trusted-setups/fast.js' import { keccak256 } from 'ethereum-cryptography/keccak' import { readFileSync, writeFileSync } from 'fs' @@ -25,12 +25,7 @@ import type { Block } from '@ethereumjs/block' import type { Common } from '@ethereumjs/common' import type { Log } from '@ethereumjs/evm' import type { TypedTxData } from '@ethereumjs/tx' -import type { - ConsolidationRequestV1, - DepositRequestV1, - PrefixedHexString, - WithdrawalRequestV1, -} from '@ethereumjs/util' +import type { CLRequest, CLRequestType, PrefixedHexString } from '@ethereumjs/util' const kzg = new microEthKZG(trustedSetup) /** @@ -79,7 +74,9 @@ export class TransitionTool { private async run(args: T8NOptions) { await this.setup(args) - + // HACK: fix me! + this.inputEnv.parentUncleHash = + '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347' const block = makeBlockFromEnv(this.inputEnv, { common: this.common }) const parentBlockHeader = makeParentBlockHeader(this.inputEnv, { common: this.common }) const parentBlock = createBlock({ header: parentBlockHeader }, { common: this.common }) @@ -123,7 +120,7 @@ export class TransitionTool { const result = await builder.build() - const convertedOutput = this.getOutput(result) + const convertedOutput = this.getOutput(result.block, result.requests) const alloc = await this.stateTracker.dumpAlloc() this.writeOutput(args, convertedOutput, alloc) @@ -201,7 +198,7 @@ export class TransitionTool { }) } - private getOutput(block: Block): T8NOutput { + private getOutput(block: Block, requests?: CLRequest[]): T8NOutput { const output: T8NOutput = { stateRoot: bytesToHex(block.header.stateRoot), txRoot: bytesToHex(block.header.transactionsTrie), @@ -228,32 +225,13 @@ export class TransitionTool { output.currentExcessBlobGas = bigIntToHex(block.header.excessBlobGas) } - if (block.header.requestsRoot !== undefined) { - output.requestsRoot = bytesToHex(block.header.requestsRoot) + if (block.header.requestsHash !== undefined) { + output.requestsHash = bytesToHex(block.header.requestsHash) } - if (block.requests !== undefined) { - if (this.common.isActivatedEIP(6110)) { - output.depositRequests = [] - } - - if (this.common.isActivatedEIP(7002)) { - output.withdrawalRequests = [] - } - - if (this.common.isActivatedEIP(7251)) { - output.consolidationRequests = [] - } - - for (const request of block.requests) { - if (request.type === CLRequestType.Deposit) { - output.depositRequests!.push(request.toJSON()) - } else if (request.type === CLRequestType.Withdrawal) { - output.withdrawalRequests!.push(request.toJSON()) - } else if (request.type === CLRequestType.Consolidation) { - output.consolidationRequests!.push(request.toJSON()) - } - } + if (requests !== undefined) { + // NOTE: EEST currently wants the raw request bytes, **excluding** the type + output.requests = requests.map((request) => bytesToHex(request.bytes.slice(1))) } if (this.rejected.length > 0) { diff --git a/packages/vm/test/t8n/types.ts b/packages/vm/test/t8n/types.ts index 527be50049..f4891dca8f 100644 --- a/packages/vm/test/t8n/types.ts +++ b/packages/vm/test/t8n/types.ts @@ -1,9 +1,3 @@ -import type { - ConsolidationRequestV1, - DepositRequestV1, - WithdrawalRequestV1, -} from '@ethereumjs/util' - export type T8NOptions = { state: { fork: string @@ -73,10 +67,8 @@ export type T8NOutput = { withdrawalsRoot?: string blobGasUsed?: string currentExcessBlobGas?: string - requestsRoot?: string - depositRequests?: DepositRequestV1[] - withdrawalRequests?: WithdrawalRequestV1[] - consolidationRequests?: ConsolidationRequestV1[] + requestsHash?: string + requests?: string[] rejected?: T8NRejectedTx[] }