diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 0c5c983080..896e94faf2 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -4,10 +4,13 @@ import { Trie } from '@ethereumjs/trie' import { BlobEIP4844Transaction, Capability, TransactionFactory } from '@ethereumjs/tx' import { BIGINT_0, - CLRequest, + CLRequestFactory, + CLRequestType, + DepositRequest, KECCAK256_RLP, KECCAK256_RLP_ARRAY, Withdrawal, + WithdrawalRequest, bigIntToHex, bytesToHex, bytesToUtf8, @@ -46,7 +49,7 @@ import type { TypedTransaction, } from '@ethereumjs/tx' import type { - CLRequestType, + CLRequest, EthersProvider, PrefixedHexString, RequestBytes, @@ -61,7 +64,7 @@ export class Block { public readonly transactions: TypedTransaction[] = [] public readonly uncleHeaders: BlockHeader[] = [] public readonly withdrawals?: Withdrawal[] - public readonly requests?: CLRequestType[] + public readonly requests?: CLRequest[] public readonly common: Common protected keccakFunction: (msg: Uint8Array) => Uint8Array @@ -110,7 +113,7 @@ export class Block { * @param emptyTrie optional empty trie used to generate the root * @returns a 32 byte Uint8Array representing the requests trie root */ - public static async genRequestsTrieRoot(requests: CLRequest[], emptyTrie?: Trie) { + public static async genRequestsTrieRoot(requests: CLRequest[], emptyTrie?: Trie) { // Requests should be sorted in monotonically ascending order based on type // and whatever internal sorting logic is defined by each request type if (requests.length > 1) { @@ -300,8 +303,8 @@ export class Block { let requests if (header.common.isActivatedEIP(7685)) { - requests = (requestBytes as RequestBytes[]).map( - (bytes) => new CLRequest(bytes[0], bytes.slice(1)) + requests = (requestBytes as RequestBytes[]).map((bytes) => + CLRequestFactory.fromSerializedRequest(bytes) ) } // executionWitness are not part of the EL fetched blocks via eth_ bodies method @@ -420,6 +423,8 @@ export class Block { transactions, withdrawals: withdrawalsData, requestsRoot, + depositRequests, + withdrawalRequests, executionWitness, } = payload @@ -459,9 +464,25 @@ export class Block { requestsRoot: reqRoot, } + const hasDepositRequests = depositRequests !== undefined && depositRequests !== null + const hasWithdrawalRequests = withdrawalRequests !== undefined && withdrawalRequests !== null + const requests = + hasDepositRequests || hasWithdrawalRequests ? ([] as CLRequest[]) : undefined + + if (depositRequests !== undefined && depositRequests !== null) { + for (const dJson of depositRequests) { + requests!.push(DepositRequest.fromJSON(dJson)) + } + } + if (withdrawalRequests !== undefined && withdrawalRequests !== null) { + for (const wJson of withdrawalRequests) { + requests!.push(WithdrawalRequest.fromJSON(wJson)) + } + } + // we are not setting setHardfork as common is already set to the correct hf const block = Block.fromBlockData( - { header, transactions: txs, withdrawals, executionWitness }, + { header, transactions: txs, withdrawals, executionWitness, requests }, opts ) if ( @@ -505,7 +526,7 @@ export class Block { uncleHeaders: BlockHeader[] = [], withdrawals?: Withdrawal[], opts: BlockOptions = {}, - requests?: CLRequest[], + requests?: CLRequest[], executionWitness?: VerkleExecutionWitness | null ) { this.header = header ?? BlockHeader.fromHeaderData({}, opts) @@ -657,7 +678,7 @@ export class Block { return result } - async requestsTrieIsValid(requestsInput?: CLRequest[]): Promise { + async requestsTrieIsValid(requestsInput?: CLRequest[]): Promise { if (!this.common.isActivatedEIP(7685)) { throw new Error('EIP 7685 is not activated') } @@ -978,6 +999,29 @@ export class Block { ...withdrawalsArr, parentBeaconBlockRoot: header.parentBeaconBlockRoot, 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, + } + + 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 + } + } + } else if ( + executionPayload.depositRequests !== undefined || + executionPayload.withdrawalRequests !== undefined + ) { + throw Error(`Undefined requests for activated deposit or withdrawal requests`) } return executionPayload diff --git a/packages/block/src/from-beacon-payload.ts b/packages/block/src/from-beacon-payload.ts index 7c0d10eede..5de5da9f44 100644 --- a/packages/block/src/from-beacon-payload.ts +++ b/packages/block/src/from-beacon-payload.ts @@ -10,6 +10,20 @@ type BeaconWithdrawal = { amount: PrefixedHexString } +type BeaconDepositRequest = { + pubkey: PrefixedHexString + withdrawal_credentials: PrefixedHexString + amount: PrefixedHexString + signature: PrefixedHexString + index: PrefixedHexString +} + +type BeaconWithdrawalRequest = { + source_address: PrefixedHexString + validator_public_key: PrefixedHexString + amount: 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 = { @@ -31,6 +45,10 @@ export type BeaconPayloadJson = { blob_gas_used?: PrefixedHexString excess_blob_gas?: PrefixedHexString parent_beacon_block_root?: PrefixedHexString + // requests data + deposit_requests?: BeaconDepositRequest[] + withdrawal_requests?: BeaconWithdrawalRequest[] + // the casing of VerkleExecutionWitness remains same camel case for now execution_witness?: VerkleExecutionWitness } @@ -128,6 +146,25 @@ 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((breq) => ({ + pubkey: breq.pubkey, + withdrawalCredentials: breq.withdrawal_credentials, + amount: breq.amount, + signature: breq.signature, + index: breq.index, + })) + } + if (payload.withdrawal_requests !== undefined && payload.withdrawal_requests !== null) { + executionPayload.withdrawalRequests = payload.withdrawal_requests.map((breq) => ({ + sourceAddress: breq.source_address, + validatorPublicKey: breq.validator_public_key, + amount: breq.amount, + })) + } + if (payload.execution_witness !== undefined && payload.execution_witness !== null) { // the casing structure in payload could be camel case or snake depending upon the CL executionPayload.executionWitness = diff --git a/packages/block/src/from-rpc.ts b/packages/block/src/from-rpc.ts index cf72da5f49..c8646ebc28 100644 --- a/packages/block/src/from-rpc.ts +++ b/packages/block/src/from-rpc.ts @@ -1,5 +1,12 @@ import { TransactionFactory } from '@ethereumjs/tx' -import { CLRequest, TypeOutput, hexToBytes, setLengthLeft, toBytes, toType } from '@ethereumjs/util' +import { + CLRequestFactory, + TypeOutput, + hexToBytes, + setLengthLeft, + toBytes, + toType, +} from '@ethereumjs/util' import { blockHeaderFromRpc } from './header-from-rpc.js' @@ -57,7 +64,7 @@ export function blockFromRpc( const requests = blockParams.requests?.map((req) => { const bytes = hexToBytes(req as PrefixedHexString) - return new CLRequest(bytes[0], bytes.slice(1)) + return CLRequestFactory.fromSerializedRequest(bytes) }) return Block.fromBlockData( { header, transactions, uncleHeaders, withdrawals: blockParams.withdrawals, requests }, diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index f5edac5cad..1a21c27ded 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -6,11 +6,14 @@ import type { BigIntLike, BytesLike, CLRequest, + CLRequestType, + DepositRequestV1, JsonRpcWithdrawal, PrefixedHexString, RequestBytes, WithdrawalBytes, WithdrawalData, + WithdrawalRequestV1, } from '@ethereumjs/util' /** @@ -153,7 +156,7 @@ export interface BlockData { transactions?: Array uncleHeaders?: Array withdrawals?: Array - requests?: Array + requests?: Array> /** * EIP-6800: Verkle Proof Data (experimental) */ @@ -303,4 +306,6 @@ export type ExecutionPayload = { // VerkleExecutionWitness is already a hex serialized object executionWitness?: VerkleExecutionWitness | null // QUANTITY, 64 Bits, null implies not available requestsRoot?: PrefixedHexString | string | null // DATA, 32 bytes, null implies EIP 7685 not active yet + depositRequests?: DepositRequestV1[] // Array of 6110 deposit requests + withdrawalRequests?: WithdrawalRequestV1[] // Array of 7002 withdrawal requests } diff --git a/packages/block/test/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index 265d17de09..1d1962aacb 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -1,23 +1,35 @@ import { Chain, Common, Hardfork } from '@ethereumjs/common' -import { CLRequest, KECCAK256_RLP, concatBytes, hexToBytes, randomBytes } from '@ethereumjs/util' +import { + DepositRequest, + KECCAK256_RLP, + WithdrawalRequest, + bytesToBigInt, + randomBytes, +} from '@ethereumjs/util' import { assert, describe, expect, it } from 'vitest' import { Block, BlockHeader } from '../src/index.js' -import type { CLRequestType } from '@ethereumjs/util' +import type { CLRequest, CLRequestType } from '@ethereumjs/util' -class NumberRequest extends CLRequest implements CLRequestType { - constructor(type: number, bytes: Uint8Array) { - super(type, bytes) - } - - public static fromRequestData(bytes: Uint8Array): CLRequestType { - return new NumberRequest(0x1, bytes) +function getRandomDepositRequest(): CLRequest { + const depositRequestData = { + pubkey: randomBytes(48), + withdrawalCredentials: randomBytes(32), + amount: bytesToBigInt(randomBytes(8)), + signature: randomBytes(96), + index: bytesToBigInt(randomBytes(8)), } + return DepositRequest.fromRequestData(depositRequestData) as CLRequest +} - serialize() { - return concatBytes(Uint8Array.from([this.type]), this.bytes) +function getRandomWithdrawalRequest(): CLRequest { + const withdrawalRequestData = { + sourceAddress: randomBytes(20), + validatorPublicKey: randomBytes(48), + amount: bytesToBigInt(randomBytes(8)), } + return WithdrawalRequest.fromRequestData(withdrawalRequestData) as CLRequest } const common = new Common({ @@ -34,7 +46,7 @@ describe('7685 tests', () => { assert.equal(block2.requests?.length, 0) }) it('should instantiate a block with requests', async () => { - const request = new NumberRequest(0x1, randomBytes(32)) + const request = getRandomDepositRequest() const requestsRoot = await Block.genRequestsTrieRoot([request]) const block = Block.fromBlockData( { @@ -47,7 +59,7 @@ describe('7685 tests', () => { assert.deepEqual(block.header.requestsRoot, requestsRoot) }) it('RequestsRootIsValid should return false when requestsRoot is invalid', async () => { - const request = new NumberRequest(0x1, randomBytes(32)) + const request = getRandomDepositRequest() const block = Block.fromBlockData( { requests: [request], @@ -59,9 +71,9 @@ describe('7685 tests', () => { assert.equal(await block.requestsTrieIsValid(), false) }) it('should validate requests order', async () => { - const request1 = new NumberRequest(0x1, hexToBytes('0x1234')) - const request2 = new NumberRequest(0x1, hexToBytes('0x2345')) - const request3 = new NumberRequest(0x2, hexToBytes('0x2345')) + const request1 = getRandomDepositRequest() + const request2 = getRandomDepositRequest() + const request3 = getRandomWithdrawalRequest() const requests = [request1, request2, request3] const requestsRoot = await Block.genRequestsTrieRoot(requests) @@ -101,9 +113,9 @@ describe('fromValuesArray tests', () => { assert.deepEqual(block.header.requestsRoot, KECCAK256_RLP) }) it('should construct a block with a valid requests array', async () => { - const request1 = new NumberRequest(0x1, hexToBytes('0x1234')) - const request2 = new NumberRequest(0x1, hexToBytes('0x2345')) - const request3 = new NumberRequest(0x2, hexToBytes('0x2345')) + const request1 = getRandomDepositRequest() + const request2 = getRandomWithdrawalRequest() + const request3 = getRandomWithdrawalRequest() const requests = [request1, request2, request3] const requestsRoot = await Block.genRequestsTrieRoot(requests) const serializedRequests = [request1.serialize(), request2.serialize(), request3.serialize()] @@ -127,9 +139,9 @@ describe('fromValuesArray tests', () => { describe('fromRPC tests', () => { it('should construct a block from a JSON object', async () => { - const request1 = new NumberRequest(0x1, hexToBytes('0x1234')) - const request2 = new NumberRequest(0x1, hexToBytes('0x2345')) - const request3 = new NumberRequest(0x2, hexToBytes('0x2345')) + const request1 = getRandomDepositRequest() + const request2 = getRandomDepositRequest() + const request3 = getRandomWithdrawalRequest() const requests = [request1, request2, request3] const requestsRoot = await Block.genRequestsTrieRoot(requests) const serializedRequests = [request1.serialize(), request2.serialize(), request3.serialize()] diff --git a/packages/util/src/bytes.ts b/packages/util/src/bytes.ts index 28ddbda90e..2fefe5a30b 100644 --- a/packages/util/src/bytes.ts +++ b/packages/util/src/bytes.ts @@ -539,3 +539,7 @@ export function bigInt64ToBytes(value: bigint, littleEndian: boolean = false): U // eslint-disable-next-line no-restricted-imports export { bytesToUtf8, equalsBytes, utf8ToBytes } from 'ethereum-cryptography/utils.js' + +export function hexToBigInt(input: PrefixedHexString): bigint { + return bytesToBigInt(hexToBytes(input)) +} diff --git a/packages/util/src/requests.ts b/packages/util/src/requests.ts index 5a27a1e99b..cc651113bd 100644 --- a/packages/util/src/requests.ts +++ b/packages/util/src/requests.ts @@ -1,28 +1,205 @@ +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 +export enum CLRequestType { + Deposit = 0x00, + Withdrawal = 0x01, +} + +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 + validatorPublicKey: PrefixedHexString // DATA 48 bytes + amount: PrefixedHexString // QUANTITY 8 bytes in gwei +} + +export interface RequestJSON { + [CLRequestType.Deposit]: DepositRequestV1 + [CLRequestType.Withdrawal]: WithdrawalRequestV1 +} + +export type DepositRequestData = { + pubkey: Uint8Array + withdrawalCredentials: Uint8Array + amount: bigint + signature: Uint8Array + index: bigint +} + +export type WithdrawalRequestData = { + sourceAddress: Uint8Array + validatorPublicKey: Uint8Array + amount: bigint +} + export interface RequestData { - type: number - data: Uint8Array + [CLRequestType.Deposit]: DepositRequestData + [CLRequestType.Withdrawal]: WithdrawalRequestData } -export interface CLRequestType { - readonly type: number - readonly bytes: Uint8Array +export type TypedRequestData = RequestData[CLRequestType] + +export interface CLRequestInterface { + readonly type: T serialize(): Uint8Array + toJSON(): RequestJSON[T] } -export class CLRequest implements CLRequestType { - type: number - bytes: Uint8Array - constructor(type: number, bytes: Uint8Array) { - if (type === undefined) throw new Error('request type is required') +export abstract class CLRequest implements CLRequestInterface { + readonly type: T + abstract serialize(): Uint8Array + abstract toJSON(): RequestJSON[T] + constructor(type: T) { this.type = type - this.bytes = bytes + } +} + +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) + } + + public static fromRequestData(depositData: DepositRequestData): DepositRequest { + const { pubkey, withdrawalCredentials, amount, signature, index } = depositData + return new DepositRequest(pubkey, withdrawalCredentials, amount, signature, index) + } + + public static fromJSON(jsonData: DepositRequestV1): DepositRequest { + const { pubkey, withdrawalCredentials, amount, signature, index } = jsonData + return this.fromRequestData({ + pubkey: hexToBytes(pubkey), + withdrawalCredentials: hexToBytes(withdrawalCredentials), + amount: hexToBigInt(amount), + signature: hexToBytes(signature), + index: hexToBigInt(index), + }) + } + + serialize() { + const indexBytes = this.index === BIGINT_0 ? new Uint8Array() : bigIntToBytes(this.index) + + 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]) + ) + } + + toJSON(): DepositRequestV1 { + return { + pubkey: bytesToHex(this.pubkey), + withdrawalCredentials: bytesToHex(this.withdrawalCredentials), + amount: bigIntToHex(this.amount), + signature: bytesToHex(this.signature), + index: bigIntToHex(this.index), + } + } + + public static deserialize(bytes: Uint8Array): DepositRequest { + const [pubkey, withdrawalCredentials, amount, signature, index] = RLP.decode( + bytes.slice(1) + ) as [Uint8Array, Uint8Array, Uint8Array, Uint8Array, Uint8Array] + return this.fromRequestData({ + pubkey, + withdrawalCredentials, + amount: bytesToBigInt(amount), + signature, + index: bytesToBigInt(index), + }) + } +} + +export class WithdrawalRequest extends CLRequest { + constructor( + public readonly sourceAddress: Uint8Array, + public readonly validatorPublicKey: Uint8Array, + public readonly amount: bigint + ) { + super(CLRequestType.Withdrawal) + } + + public static fromRequestData(withdrawalData: WithdrawalRequestData): WithdrawalRequest { + const { sourceAddress, validatorPublicKey, amount } = withdrawalData + return new WithdrawalRequest(sourceAddress, validatorPublicKey, amount) + } + + public static fromJSON(jsonData: WithdrawalRequestV1): WithdrawalRequest { + const { sourceAddress, validatorPublicKey, amount } = jsonData + return this.fromRequestData({ + sourceAddress: hexToBytes(sourceAddress), + validatorPublicKey: hexToBytes(validatorPublicKey), + amount: hexToBigInt(amount), + }) } serialize() { - return concatBytes(Uint8Array.from([this.type]), this.bytes) + const amountBytes = this.amount === BIGINT_0 ? new Uint8Array() : bigIntToBytes(this.amount) + + return concatBytes( + Uint8Array.from([this.type]), + RLP.encode([this.sourceAddress, this.validatorPublicKey, amountBytes]) + ) + } + + toJSON(): WithdrawalRequestV1 { + return { + sourceAddress: bytesToHex(this.sourceAddress), + validatorPublicKey: bytesToHex(this.validatorPublicKey), + amount: bigIntToHex(this.amount), + } + } + + public static deserialize(bytes: Uint8Array): WithdrawalRequest { + const [sourceAddress, validatorPublicKey, amount] = RLP.decode(bytes.slice(1)) as [ + Uint8Array, + Uint8Array, + Uint8Array + ] + return this.fromRequestData({ + sourceAddress, + validatorPublicKey, + amount: bytesToBigInt(amount), + }) + } +} + +export class CLRequestFactory { + public static fromSerializedRequest(bytes: Uint8Array): CLRequest { + switch (bytes[0]) { + case CLRequestType.Deposit: + return DepositRequest.deserialize(bytes) + case CLRequestType.Withdrawal: + return WithdrawalRequest.deserialize(bytes) + 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 12a6a3fb43..297fb30f41 100644 --- a/packages/util/test/requests.spec.ts +++ b/packages/util/test/requests.spec.ts @@ -1,44 +1,59 @@ import { assert, describe, it } from 'vitest' +import { bytesToBigInt, randomBytes } from '../src/bytes.js' import { - bigIntToBytes, - bytesToBigInt, - bytesToHex, - concatBytes, - hexToBytes, - randomBytes, -} from '../src/bytes.js' -import { CLRequest, type CLRequestType } from '../src/requests.js' - -class NumberRequest extends CLRequest implements CLRequestType { - constructor(type: number, bytes: Uint8Array) { - super(type, bytes) - } - - public static fromRequestData(bytes: Uint8Array): CLRequestType { - return new NumberRequest(0x1, bytes) - } - - serialize() { - return concatBytes(Uint8Array.from([this.type]), this.bytes) - } -} -describe('should create a request', () => { - it('should create a request', () => { - const requestType = 0x1 - const data = randomBytes(32) - const request = new NumberRequest(0x1, data) - const serialized = request.serialize() - assert.equal(serialized[0], requestType) - assert.deepEqual(serialized.slice(1), data) + CLRequestFactory, + CLRequestType, + DepositRequest, + WithdrawalRequest, +} from '../src/requests.js' + +import type { CLRequest } from '../src/requests.js' + +describe('Requests', () => { + it('deposit request', () => { + const depositRequestData = { + pubkey: randomBytes(48), + withdrawalCredentials: randomBytes(32), + amount: bytesToBigInt(randomBytes(8)), + signature: randomBytes(96), + index: bytesToBigInt(randomBytes(8)), + } + + const depositObject = DepositRequest.fromRequestData( + depositRequestData + ) as CLRequest + const depositJSON = depositObject.toJSON() + const serialized = depositObject.serialize() + assert.equal(serialized[0], CLRequestType.Deposit) + + const deserialized = CLRequestFactory.fromSerializedRequest(serialized) + const deserializedJSON = deserialized.toJSON() + assert.deepEqual(deserializedJSON, depositJSON) + + const reserialized = deserialized.serialize() + assert.deepEqual(serialized, reserialized) }) - it('should create a request from RequestData', () => { - const request1 = NumberRequest.fromRequestData(hexToBytes('0x1234')) - assert.equal(request1.type, 0x1) - assert.equal(bytesToHex(request1.bytes), '0x1234') - - const request2 = NumberRequest.fromRequestData(bigIntToBytes(123n)) - assert.equal(request2.type, 0x1) - assert.equal(bytesToBigInt(request2.bytes), 123n) + + it('withdrawal request', () => { + const withdrawalRequestData = { + sourceAddress: randomBytes(20), + validatorPublicKey: randomBytes(48), + amount: bytesToBigInt(randomBytes(8)), + } + + const withdrawalObject = WithdrawalRequest.fromRequestData( + withdrawalRequestData + ) as CLRequest + const withdrawalJSON = withdrawalObject.toJSON() + const serialized = withdrawalObject.serialize() + assert.equal(serialized[0], CLRequestType.Withdrawal) + + const deserialized = CLRequestFactory.fromSerializedRequest(serialized) + const deserializedJSON = deserialized.toJSON() + assert.deepEqual(deserializedJSON, withdrawalJSON) + + const reserialized = deserialized.serialize() + assert.deepEqual(serialized, reserialized) }) }) diff --git a/packages/vm/src/requests.ts b/packages/vm/src/requests.ts index 66c4fd90be..6a0c9c1327 100644 --- a/packages/vm/src/requests.ts +++ b/packages/vm/src/requests.ts @@ -1,9 +1,8 @@ import { Common } from '@ethereumjs/common' -import { RLP } from '@ethereumjs/rlp' import { Address, - BIGINT_0, - CLRequest, + DepositRequest, + WithdrawalRequest, bigIntToBytes, bytesToBigInt, bytesToHex, @@ -13,6 +12,7 @@ import { import type { RunTxResult } from './types' 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 @@ -23,8 +23,8 @@ import type { VM } from './vm.js' export const accumulateRequests = async ( vm: VM, txResults: RunTxResult[] -): Promise => { - const requests: CLRequest[] = [] +): Promise[]> => { + const requests: CLRequest[] = [] const common = vm.common if (common.isActivatedEIP(6110)) { @@ -49,7 +49,10 @@ export const accumulateRequests = async ( return requests } -const accumulateEIP7002Requests = async (vm: VM, requests: CLRequest[]): Promise => { +const accumulateEIP7002Requests = async ( + vm: VM, + requests: CLRequest[] +): Promise => { // Partial withdrawals logic const addressBytes = setLengthLeft( bigIntToBytes(vm.common.param('vm', 'withdrawalRequestPredeployAddress')), @@ -79,16 +82,15 @@ const accumulateEIP7002Requests = async (vm: VM, requests: CLRequest[]): Promise const resultsBytes = results.execResult.returnValue if (resultsBytes.length > 0) { - const withdrawalRequestType = Number(vm.common.param('vm', 'withdrawalRequestType')) // 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 = slicedBytes.slice(68, 76) // 8 Bytes / Uint64 - const rlpData = RLP.encode([sourceAddress, validatorPubkey, amount]) - const request = new CLRequest(withdrawalRequestType, rlpData) - requests.push(request) + const validatorPublicKey = slicedBytes.slice(20, 68) // 48 Bytes + const amount = bytesToBigInt(slicedBytes.slice(68, 76)) // 8 Bytes / Uint64 + requests.push( + WithdrawalRequest.fromRequestData({ sourceAddress, validatorPublicKey, amount }) + ) } } } @@ -96,7 +98,7 @@ const accumulateEIP7002Requests = async (vm: VM, requests: CLRequest[]): Promise const accumulateDeposits = async ( depositContractAddress: string, txResults: RunTxResult[], - requests: CLRequest[] + requests: CLRequest[] ) => { for (const [_, tx] of txResults.entries()) { for (let i = 0; i < tx.receipt.logs.length; i++) { @@ -123,7 +125,7 @@ const accumulateDeposits = async ( 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 withdrawalCreds = log[2].slice( + const withdrawalCredentials = log[2].slice( withdrawalCredsIdx + 32, withdrawalCredsIdx + 32 + withdrawalCredsSize ) @@ -138,14 +140,7 @@ const accumulateDeposits = async ( amountBytes[1], amountBytes[0], ]) - const amountBigInt = bytesToBigInt(amountBytesBigEndian) - - let amount: Uint8Array - if (amountBigInt === BIGINT_0) { - amount = new Uint8Array() - } else { - amount = bigIntToBytes(amountBigInt) - } + const amount = bytesToBigInt(amountBytesBigEndian) const signature = log[2].slice(sigIdx + 32, sigIdx + 32 + sigSize) @@ -162,18 +157,15 @@ const accumulateDeposits = async ( indexBytes[1], indexBytes[0], ]) - const indexBigInt = bytesToBigInt(indexBytesBigEndian) - - let index: Uint8Array - - if (indexBigInt === BIGINT_0) { - index = new Uint8Array() - } else { - index = bigIntToBytes(indexBigInt) - } - + const index = bytesToBigInt(indexBytesBigEndian) requests.push( - new CLRequest(0x0, RLP.encode([pubkey, withdrawalCreds, amount, signature, index])) + DepositRequest.fromRequestData({ + pubkey, + withdrawalCredentials, + amount, + signature, + index, + }) ) } } diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 14a04d8d49..29a2e85f39 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -41,7 +41,7 @@ import type { import type { VM } from './vm.js' import type { Common } from '@ethereumjs/common' import type { EVM, EVMInterface } from '@ethereumjs/evm' -import type { CLRequest, PrefixedHexString } from '@ethereumjs/util' +import type { CLRequest, CLRequestType, PrefixedHexString } from '@ethereumjs/util' const { debug: createDebugLogger } = debugDefault @@ -194,7 +194,7 @@ export async function runBlock(this: VM, opts: RunBlockOpts): Promise[] | undefined if (block.common.isActivatedEIP(7685)) { requests = await accumulateRequests(this, result.results) requestsRoot = await Block.genRequestsTrieRoot(requests) diff --git a/packages/vm/src/types.ts b/packages/vm/src/types.ts index 79b859ee64..cbd041cf1e 100644 --- a/packages/vm/src/types.ts +++ b/packages/vm/src/types.ts @@ -7,6 +7,7 @@ import type { AccessList, TypedTransaction } from '@ethereumjs/tx' import type { BigIntLike, CLRequest, + CLRequestType, GenesisState, PrefixedHexString, WithdrawalData, @@ -338,7 +339,7 @@ export interface RunBlockResult extends Omit { /** * Any CL requests that were processed in the course of this block */ - requests?: CLRequest[] + requests?: CLRequest[] } export interface AfterBlockEvent extends RunBlockResult { diff --git a/packages/vm/test/api/EIPs/eip-6110.spec.ts b/packages/vm/test/api/EIPs/eip-6110.spec.ts index 9711e1ee49..f2c94aba67 100644 --- a/packages/vm/test/api/EIPs/eip-6110.spec.ts +++ b/packages/vm/test/api/EIPs/eip-6110.spec.ts @@ -1,6 +1,5 @@ import { Block } from '@ethereumjs/block' import { Chain, Common, Hardfork } from '@ethereumjs/common' -import { RLP } from '@ethereumjs/rlp' import { TransactionFactory } from '@ethereumjs/tx' import { Account, Address, bytesToHex, hexToBytes, randomBytes } from '@ethereumjs/util' import { keccak256 } from 'ethereum-cryptography/keccak.js' @@ -8,6 +7,7 @@ import { assert, describe, it } from 'vitest' import { setupVM } from '../utils.js' +import type { DepositRequest } from '../../../../util/src/requests.js' import type { PrefixedHexString } from '@ethereumjs/util' const depositContractByteCode = hexToBytes( @@ -59,7 +59,8 @@ describe('EIP-6110 runBlock tests', () => { ) const res = await vm.runBlock({ block, generate: true, skipBlockValidation: true }) assert.equal(res.requests?.length, 1) - assert.equal(bytesToHex((RLP.decode(res.requests![0].bytes) as Uint8Array[])[0]), pubkey) + const reqPubkey = (res.requests![0] as DepositRequest).pubkey + assert.equal(bytesToHex(reqPubkey), pubkey) }) }) @@ -92,6 +93,7 @@ describe('EIP-7685 buildBlock tests', () => { await blockBuilder.addTransaction(depositTx) const res = await blockBuilder.build() assert.equal(res.requests?.length, 1) - assert.equal(bytesToHex((RLP.decode(res.requests![0].bytes) as Uint8Array[])[0]), pubkey) + const reqPubkey = (res.requests![0] as DepositRequest).pubkey + assert.equal(bytesToHex(reqPubkey), pubkey) }) }) diff --git a/packages/vm/test/api/EIPs/eip-7002.spec.ts b/packages/vm/test/api/EIPs/eip-7002.spec.ts index 1ab74c1b89..0a52aa610d 100644 --- a/packages/vm/test/api/EIPs/eip-7002.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7002.spec.ts @@ -15,6 +15,7 @@ import { } from '@ethereumjs/util' import { assert, describe, it } from 'vitest' +import { bytesToBigInt } from '../../../../util/src/bytes.js' import { setupVM } from '../utils.js' const pkey = hexToBytes(`0x${'20'.repeat(32)}`) @@ -117,7 +118,7 @@ describe('EIP-7002 tests', () => { // Ensure the request is generated assert.ok(generatedBlock!.requests!.length === 1) - const requestDecoded = RLP.decode(generatedBlock!.requests![0].bytes) + const requestDecoded = RLP.decode(generatedBlock!.requests![0].serialize().slice(1)) const sourceAddressRequest = requestDecoded[0] as Uint8Array const validatorPubkeyRequest = requestDecoded[1] as Uint8Array @@ -126,7 +127,9 @@ describe('EIP-7002 tests', () => { // Ensure the requests are correct assert.ok(equalsBytes(sourceAddressRequest, tx.getSenderAddress().bytes)) assert.ok(equalsBytes(validatorPubkey, validatorPubkeyRequest)) - assert.ok(equalsBytes(amountBytes, amountRequest)) + // the direct byte comparision fails because leading zeros have been stripped + // off the amountBytes because it was serialized in request from bigint + assert.equal(bytesToBigInt(amountBytes), bytesToBigInt(amountRequest)) await vm.runBlock({ block: generatedBlock!, skipHeaderValidation: true, root }) diff --git a/packages/vm/test/api/EIPs/eip-7685.spec.ts b/packages/vm/test/api/EIPs/eip-7685.spec.ts index 873d8f612a..ebbbed760e 100644 --- a/packages/vm/test/api/EIPs/eip-7685.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7685.spec.ts @@ -1,29 +1,32 @@ import { Block } from '@ethereumjs/block' import { Blockchain } from '@ethereumjs/blockchain' import { Chain, Common, Hardfork } from '@ethereumjs/common' -import { CLRequest, KECCAK256_RLP, concatBytes, hexToBytes, randomBytes } from '@ethereumjs/util' +import { + DepositRequest, + KECCAK256_RLP, + bytesToBigInt, + hexToBytes, + randomBytes, +} from '@ethereumjs/util' import { assert, describe, expect, it } from 'vitest' import { VM } from '../../../src/vm.js' import { setupVM } from '../utils.js' -import type { CLRequestType } from '@ethereumjs/util' +import type { CLRequest, CLRequestType } from '@ethereumjs/util' const invalidRequestsRoot = hexToBytes( '0xc98048d6605eb79ecc08d90b8817f44911ec474acd8d11688453d2c6ef743bc5' ) -class NumberRequest extends CLRequest implements CLRequestType { - constructor(type: number, bytes: Uint8Array) { - super(type, bytes) - } - - public static fromRequestData(bytes: Uint8Array): CLRequestType { - return new NumberRequest(0x1, bytes) - } - - serialize() { - return concatBytes(Uint8Array.from([this.type]), this.bytes) +function getRandomDepositRequest(): CLRequest { + const depositRequestData = { + pubkey: randomBytes(48), + withdrawalCredentials: randomBytes(32), + amount: bytesToBigInt(randomBytes(8)), + signature: randomBytes(96), + index: bytesToBigInt(randomBytes(8)), } + return DepositRequest.fromRequestData(depositRequestData) as CLRequest } const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Cancun, eips: [7685] }) @@ -53,7 +56,7 @@ describe('EIP-7685 runBlock tests', () => { }) it('should not throw invalid requestsRoot error when valid requests are provided', async () => { const vm = await setupVM({ common }) - const request = new NumberRequest(0x1, randomBytes(32)) + const request = getRandomDepositRequest() const requestsRoot = await Block.genRequestsTrieRoot([request]) const block = Block.fromBlockData( { @@ -66,7 +69,7 @@ describe('EIP-7685 runBlock tests', () => { }) it('should error when requestsRoot does not match requests provided', async () => { const vm = await setupVM({ common }) - const request = new NumberRequest(0x1, randomBytes(32)) + const request = getRandomDepositRequest() const block = Block.fromBlockData( { requests: [request],