From 640290f2c0f7921e14ebd887d2ba3b17cc6e1dcb Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Tue, 30 Apr 2024 04:03:31 +0200 Subject: [PATCH 01/11] common: add eip 7002 --- packages/common/src/eips.ts | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 240c09d83c..7513552f23 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -529,6 +529,63 @@ export const EIPs: EIPsDict = { }, }, }, + 7002: { + comment: 'Execution layer triggerable withdrawals (experimental)', + url: 'https://github.com/ethereum/EIPs/blob/3b5fcad6b35782f8aaeba7d4ac26004e8fbd720f/EIPS/eip-7002.md', + status: Status.Draft, + minimumHardfork: Hardfork.Paris, + requiredEIPs: [7685], + vm: { + withdrawalRequestType: { + v: BigInt(0x01), + d: 'The withdrawal request type for EIP-7685', + }, + excessWithdrawalsRequestStorageSlot: { + v: BigInt(0), + d: 'The storage slot of the excess withdrawals', + }, + withdrawalsRequestCountStorage: { + v: BigInt(1), + d: 'The storage slot of the withdrawal request count', + }, + withdrawalsRequestQueueHeadStorageSlot: { + v: BigInt(2), + d: 'The storage slot of the withdrawal request head of the queue', + }, + withdrawalsRequestTailHeadStorageSlot: { + v: BigInt(3), + d: 'The storage slot of the withdrawal request tail of the queue', + }, + withdrawalsRequestQueueStorageOffset: { + v: BigInt(4), + d: 'The storage slot of the withdrawal request queue offset', + }, + maxWithdrawalRequestsPerBlock: { + v: BigInt(16), + d: 'The max withdrawal requests per block', + }, + targetWithdrawalRequestsPerBlock: { + v: BigInt(2), + d: 'The target withdrawal requests per block', + }, + minWithdrawalRequestFee: { + v: BigInt(1), + d: 'The minimum withdrawal request fee (in wei)', + }, + withdrawalRequestFeeUpdateFraction: { + v: BigInt(17), + d: 'The withdrawal request fee update fraction (used in the fake exponential)', + }, + systemAddress: { + v: BigInt('0xfffffffffffffffffffffffffffffffffffffffe'), + d: 'The system address to perform operations on the withdrawal requests predeploy address', + }, + withdrawalRequestPredeployAddress: { + v: BigInt('0x00A3ca265EBcb825B45F985A16CEFB49958cE017'), + d: 'Address of the validator excess address', + }, + }, + }, 7516: { comment: 'BLOBBASEFEE opcode', url: 'https://eips.ethereum.org/EIPS/eip-7516', From 1a7145ada2e080367e57d68f55e4f61a411cc725 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Tue, 30 Apr 2024 07:06:26 +0200 Subject: [PATCH 02/11] vm: add 7002 request accumulation logic --- packages/vm/src/runBlock.ts | 135 +++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 3 deletions(-) diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 8bcba57b2c..21eaab4bf5 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -9,11 +9,16 @@ import { Address, BIGINT_0, BIGINT_1, + BIGINT_2, + BIGINT_3, BIGINT_8, + CLRequest, GWEI_TO_WEI, KECCAK256_RLP, + bigIntMin, bigIntToBytes, bigIntToHex, + bytesToBigInt, bytesToHex, concatBytes, equalsBytes, @@ -21,6 +26,7 @@ import { intToBytes, setLengthLeft, short, + unpadBytes, unprefixedHexToBytes, } from '@ethereumjs/util' import debugDefault from 'debug' @@ -40,7 +46,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 { CLRequestType, PrefixedHexString } from '@ethereumjs/util' const { debug: createDebugLogger } = debugDefault @@ -951,17 +957,140 @@ const DAOConfig = { DAORefundContract: 'bf4ed7b27f1d666546e30d74d50d173d20bca754', } +export class ValidatorWithdrawalRequest extends CLRequest implements CLRequestType { + constructor(type: number, bytes: Uint8Array) { + super(type, bytes) + } + + serialize() { + return concatBytes(Uint8Array.from([this.type]), this.bytes) + } +} + /** * This helper method generates a list of all CL requests that can be included in a pending block * @param _vm VM instance from which to derive CL requests * @returns an list of CL requests in ascending order by type */ -export const accumulateRequests = async (_vm: VM): Promise => { +export const accumulateRequests = async (vm: VM): Promise => { const requests: CLRequest[] = [] + const common = vm.common // TODO: Add in code to accumulate deposits (EIP-6110) - // TODO: Add in code to accumulate partial withdrawals (EIP-7002) + if (common.isActivatedEIP(7002)) { + // Partial withdrawals logic + const addressBigInt = common.param('vm', 'withdrawalRequestPredeployAddress') + const withdrawalsAddress = Address.fromString(bigIntToHex(addressBigInt)) + + const withdrawalsType = Number(common.param('vm', 'withdrawalRequestType')) + + const excessWithdrawalsSlot = setLengthLeft( + bigIntToBytes(common.param('vm', 'excessWithdrawalsRequestStorageSlot')), + 32 + ) + const withdrawalsCountSlot = setLengthLeft( + bigIntToBytes(common.param('vm', 'withdrawalsRequestCountStorage')), + 32 + ) + const queueHeadSlot = setLengthLeft( + bigIntToBytes(common.param('vm', 'withdrawalsRequestQueueHeadStorageSlot')), + 32 + ) + const queueTailSlot = setLengthLeft( + bigIntToBytes(common.param('vm', 'withdrawalsRequestTailHeadStorageSlot')), + 32 + ) + + const maxWithdrawalsPerBlock = common.param('vm', 'maxWithdrawalRequestsPerBlock') + const targetWithdrawalRequestsPerBlock = common.param('vm', 'targetWithdrawalRequestsPerBlock') + const offset = common.param('vm', 'withdrawalsRequestQueueStorageOffset') + + // Dequeue withdrawal requests + + const queueHeadIndex = bytesToBigInt( + await vm.stateManager.getContractStorage(withdrawalsAddress, queueHeadSlot) + ) + const queueTailIndex = bytesToBigInt( + await vm.stateManager.getContractStorage(withdrawalsAddress, queueTailSlot) + ) + const numInQueue = queueTailIndex - queueHeadIndex + const numDequeued = Number(bigIntMin(numInQueue, maxWithdrawalsPerBlock)) + + for (let i = 0; i < numDequeued; i++) { + const queueStorageSlot = offset + (queueHeadIndex + BigInt(i)) * BIGINT_3 + const sourceAddress = setLengthLeft( + await vm.stateManager.getContractStorage( + withdrawalsAddress, + setLengthLeft(bigIntToBytes(queueStorageSlot), 32) + ), + 32 + ).slice(12, 32) + + const slot1Data = setLengthLeft( + await vm.stateManager.getContractStorage( + withdrawalsAddress, + setLengthLeft(bigIntToBytes(queueStorageSlot + BIGINT_1), 32) + ), + 32 + ) + const slot2Data = setLengthLeft( + await vm.stateManager.getContractStorage( + withdrawalsAddress, + setLengthLeft(bigIntToBytes(queueStorageSlot + BIGINT_2), 32) + ), + 32 + ) + + const concatenatedData = concatBytes(slot1Data, slot2Data) + const validatorPubkey = concatenatedData.slice(0, 48) + + const amount = unpadBytes(concatenatedData.slice(48, 56)) + + const RLPdBytes = RLP.encode([sourceAddress, validatorPubkey, amount]) + const request = new ValidatorWithdrawalRequest(withdrawalsType, RLPdBytes) + requests.push(request) + } + + const newQueueHeadIndex = queueHeadIndex + BigInt(numDequeued) + + if (newQueueHeadIndex === queueTailIndex) { + await vm.stateManager.putContractStorage(withdrawalsAddress, queueHeadSlot, new Uint8Array()) + await vm.stateManager.putContractStorage(withdrawalsAddress, queueTailSlot, new Uint8Array()) + } else { + await vm.stateManager.putContractStorage( + withdrawalsAddress, + queueHeadSlot, + bigIntToBytes(newQueueHeadIndex) + ) + } + + // Update the excess withdrawal requests + const previousExcess = bytesToBigInt( + await vm.stateManager.getContractStorage(withdrawalsAddress, excessWithdrawalsSlot) + ) + const count = bytesToBigInt( + await vm.stateManager.getContractStorage(withdrawalsAddress, withdrawalsCountSlot) + ) + + let newExcess = BIGINT_0 + if (previousExcess + count > targetWithdrawalRequestsPerBlock) { + newExcess = previousExcess + count - targetWithdrawalRequestsPerBlock + } + + await vm.stateManager.putContractStorage( + withdrawalsAddress, + withdrawalsCountSlot, + bigIntToBytes(newExcess) + ) + + // Reset the withdrawals count + await vm.stateManager.putContractStorage( + withdrawalsAddress, + withdrawalsCountSlot, + new Uint8Array() + ) + } if (requests.length > 1) { for (let x = 1; x < requests.length; x++) { From a7f78eee9b42ae1d17b7916145caebed638423ab Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Tue, 30 Apr 2024 09:09:28 +0200 Subject: [PATCH 03/11] evm: support 7002 --- packages/evm/src/evm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index fcb61c1e09..fa5e0c3edc 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -210,7 +210,7 @@ export class EVM implements EVMInterface { // Supported EIPs const supportedEIPs = [ 1153, 1559, 2537, 2565, 2718, 2929, 2930, 2935, 3074, 3198, 3529, 3540, 3541, 3607, 3651, - 3670, 3855, 3860, 4399, 4895, 4788, 4844, 5133, 5656, 6780, 6800, 7516, 7685, + 3670, 3855, 3860, 4399, 4895, 4788, 4844, 5133, 5656, 6780, 6800, 7002, 7516, 7685, ] for (const eip of this.common.eips()) { From 2a7b755a231536be2b809dd0e62dc910d457dd8b Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Tue, 30 Apr 2024 09:10:42 +0200 Subject: [PATCH 04/11] vm: fix withdrawals address bug --- packages/vm/src/runBlock.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 21eaab4bf5..8409d48773 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -980,8 +980,11 @@ export const accumulateRequests = async (vm: VM): Promise => { if (common.isActivatedEIP(7002)) { // Partial withdrawals logic - const addressBigInt = common.param('vm', 'withdrawalRequestPredeployAddress') - const withdrawalsAddress = Address.fromString(bigIntToHex(addressBigInt)) + const addressBytes = setLengthLeft( + bigIntToBytes(common.param('vm', 'withdrawalRequestPredeployAddress')), + 20 + ) + const withdrawalsAddress = Address.fromString(bytesToHex(addressBytes)) const withdrawalsType = Number(common.param('vm', 'withdrawalRequestType')) From 8e67d220c82b012ac765eb481568eabacb0f5774 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Tue, 30 Apr 2024 09:10:57 +0200 Subject: [PATCH 05/11] vm: add eip7002 test (wip) [no ci] --- packages/vm/test/api/EIPs/eip-7002.spec.ts | 107 +++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 packages/vm/test/api/EIPs/eip-7002.spec.ts diff --git a/packages/vm/test/api/EIPs/eip-7002.spec.ts b/packages/vm/test/api/EIPs/eip-7002.spec.ts new file mode 100644 index 0000000000..0b94aacbc2 --- /dev/null +++ b/packages/vm/test/api/EIPs/eip-7002.spec.ts @@ -0,0 +1,107 @@ +import { Block } from '@ethereumjs/block' +import { Chain, Common, Hardfork } from '@ethereumjs/common' +import { LegacyTransaction } from '@ethereumjs/tx' +import { + Account, + Address, + bigIntToBytes, + bytesToHex, + concatBytes, + hexToBytes, + setLengthLeft, + zeros, +} from '@ethereumjs/util' +import { assert, describe, expect, it } from 'vitest' + +import { VM } from '../../../src/vm.js' +import { setupVM } from '../utils.js' + +const pkey = hexToBytes(`0x${'20'.repeat(32)}`) +const addr = Address.fromPrivateKey(pkey) + +const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Cancun, eips: [7685, 7002] }) + +const deploymentTxData = { + nonce: BigInt(0), + gasLimit: BigInt('0x3d090'), + gasPrice: BigInt('0xe8d4a51000'), + data: hexToBytes( + '0x61049d5f5561013280600f5f395ff33373fffffffffffffffffffffffffffffffffffffffe146090573615156028575f545f5260205ff35b366038141561012e5760115f54600182026001905f5b5f82111560595781019083028483029004916001019190603e565b90939004341061012e57600154600101600155600354806003026004013381556001015f3581556001016020359055600101600355005b6003546002548082038060101160a4575060105b5f5b81811460dd5780604c02838201600302600401805490600101805490600101549160601b83528260140152906034015260010160a6565b910180921460ed579060025560f8565b90505f6002555f6003555b5f548061049d141561010757505f5b60015460028282011161011c5750505f610122565b01600290035b5f555f600155604c025ff35b5f5ffd' + ), + v: BigInt('0x1b'), + r: BigInt('0x539'), + s: BigInt('0xaba653c9d105790c'), +} + +const deploymentTx = LegacyTransaction.fromTxData(deploymentTxData) +const sender = deploymentTx.getSenderAddress() +const upfrontCost = deploymentTx.getUpfrontCost() +const acc = new Account() +acc.balance = upfrontCost + +describe('EIP-7002 tests', () => { + it('should correctly create requests', async () => { + const vm = await setupVM({ common }) + const block = Block.fromBlockData( + { + header: { + number: 1, + parentBeaconBlockRoot: zeros(32), + }, + transactions: [deploymentTx], + }, + { common } + ) + await vm.stateManager.putAccount(sender, acc) + await vm.stateManager.putAccount(addr, acc) + + // Deploy withdrawals contract + await vm.runBlock({ + block, + skipHeaderValidation: true, + skipBlockValidation: true, + generate: true, + }) + + const validatorPubkey = hexToBytes(`0x${'20'.repeat(48)}`) + const amount = setLengthLeft(new Uint8Array(100), 8) + + const addressBytes = setLengthLeft( + bigIntToBytes(common.param('vm', 'withdrawalRequestPredeployAddress')), + 20 + ) + const withdrawalsAddress = Address.fromString(bytesToHex(addressBytes)) + + const tx = LegacyTransaction.fromTxData({ + gasPrice: BigInt(100), + data: concatBytes(validatorPubkey, amount), + value: BigInt(1), + to: withdrawalsAddress, + gasLimit: 100_000, + }).sign(pkey) + + // Call withdrawals contract with a withdrawals request + const block2 = Block.fromBlockData( + { + header: { + number: 2, + parentBeaconBlockRoot: zeros(32), + }, + transactions: [tx], + }, + { common } + ) + + vm.evm.events?.on('step', (e) => { + console.log(e.opcode.name, e.stack) + }) + + const res = await vm.runBlock({ + block: block2, + skipHeaderValidation: true, + skipBlockValidation: true, + generate: true, + }) + console.log(res.requests) + }) +}) From 1235c60bcb9474f075061d0ec8e5b5d2fac268df Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Wed, 1 May 2024 08:22:24 +0200 Subject: [PATCH 06/11] vm: update eip7002 to use system call [no ci] --- packages/vm/src/runBlock.ts | 158 +++++---------------- packages/vm/test/api/EIPs/eip-7002.spec.ts | 27 ++-- 2 files changed, 56 insertions(+), 129 deletions(-) diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 8409d48773..0a143480fd 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -9,16 +9,12 @@ import { Address, BIGINT_0, BIGINT_1, - BIGINT_2, - BIGINT_3, BIGINT_8, CLRequest, GWEI_TO_WEI, KECCAK256_RLP, - bigIntMin, bigIntToBytes, bigIntToHex, - bytesToBigInt, bytesToHex, concatBytes, equalsBytes, @@ -26,7 +22,6 @@ import { intToBytes, setLengthLeft, short, - unpadBytes, unprefixedHexToBytes, } from '@ethereumjs/util' import debugDefault from 'debug' @@ -979,120 +974,7 @@ export const accumulateRequests = async (vm: VM): Promise => { // TODO: Add in code to accumulate deposits (EIP-6110) if (common.isActivatedEIP(7002)) { - // Partial withdrawals logic - const addressBytes = setLengthLeft( - bigIntToBytes(common.param('vm', 'withdrawalRequestPredeployAddress')), - 20 - ) - const withdrawalsAddress = Address.fromString(bytesToHex(addressBytes)) - - const withdrawalsType = Number(common.param('vm', 'withdrawalRequestType')) - - const excessWithdrawalsSlot = setLengthLeft( - bigIntToBytes(common.param('vm', 'excessWithdrawalsRequestStorageSlot')), - 32 - ) - const withdrawalsCountSlot = setLengthLeft( - bigIntToBytes(common.param('vm', 'withdrawalsRequestCountStorage')), - 32 - ) - const queueHeadSlot = setLengthLeft( - bigIntToBytes(common.param('vm', 'withdrawalsRequestQueueHeadStorageSlot')), - 32 - ) - const queueTailSlot = setLengthLeft( - bigIntToBytes(common.param('vm', 'withdrawalsRequestTailHeadStorageSlot')), - 32 - ) - - const maxWithdrawalsPerBlock = common.param('vm', 'maxWithdrawalRequestsPerBlock') - const targetWithdrawalRequestsPerBlock = common.param('vm', 'targetWithdrawalRequestsPerBlock') - const offset = common.param('vm', 'withdrawalsRequestQueueStorageOffset') - - // Dequeue withdrawal requests - - const queueHeadIndex = bytesToBigInt( - await vm.stateManager.getContractStorage(withdrawalsAddress, queueHeadSlot) - ) - const queueTailIndex = bytesToBigInt( - await vm.stateManager.getContractStorage(withdrawalsAddress, queueTailSlot) - ) - const numInQueue = queueTailIndex - queueHeadIndex - const numDequeued = Number(bigIntMin(numInQueue, maxWithdrawalsPerBlock)) - - for (let i = 0; i < numDequeued; i++) { - const queueStorageSlot = offset + (queueHeadIndex + BigInt(i)) * BIGINT_3 - const sourceAddress = setLengthLeft( - await vm.stateManager.getContractStorage( - withdrawalsAddress, - setLengthLeft(bigIntToBytes(queueStorageSlot), 32) - ), - 32 - ).slice(12, 32) - - const slot1Data = setLengthLeft( - await vm.stateManager.getContractStorage( - withdrawalsAddress, - setLengthLeft(bigIntToBytes(queueStorageSlot + BIGINT_1), 32) - ), - 32 - ) - const slot2Data = setLengthLeft( - await vm.stateManager.getContractStorage( - withdrawalsAddress, - setLengthLeft(bigIntToBytes(queueStorageSlot + BIGINT_2), 32) - ), - 32 - ) - - const concatenatedData = concatBytes(slot1Data, slot2Data) - const validatorPubkey = concatenatedData.slice(0, 48) - - const amount = unpadBytes(concatenatedData.slice(48, 56)) - - const RLPdBytes = RLP.encode([sourceAddress, validatorPubkey, amount]) - const request = new ValidatorWithdrawalRequest(withdrawalsType, RLPdBytes) - requests.push(request) - } - - const newQueueHeadIndex = queueHeadIndex + BigInt(numDequeued) - - if (newQueueHeadIndex === queueTailIndex) { - await vm.stateManager.putContractStorage(withdrawalsAddress, queueHeadSlot, new Uint8Array()) - await vm.stateManager.putContractStorage(withdrawalsAddress, queueTailSlot, new Uint8Array()) - } else { - await vm.stateManager.putContractStorage( - withdrawalsAddress, - queueHeadSlot, - bigIntToBytes(newQueueHeadIndex) - ) - } - - // Update the excess withdrawal requests - const previousExcess = bytesToBigInt( - await vm.stateManager.getContractStorage(withdrawalsAddress, excessWithdrawalsSlot) - ) - const count = bytesToBigInt( - await vm.stateManager.getContractStorage(withdrawalsAddress, withdrawalsCountSlot) - ) - - let newExcess = BIGINT_0 - if (previousExcess + count > targetWithdrawalRequestsPerBlock) { - newExcess = previousExcess + count - targetWithdrawalRequestsPerBlock - } - - await vm.stateManager.putContractStorage( - withdrawalsAddress, - withdrawalsCountSlot, - bigIntToBytes(newExcess) - ) - - // Reset the withdrawals count - await vm.stateManager.putContractStorage( - withdrawalsAddress, - withdrawalsCountSlot, - new Uint8Array() - ) + await _accumulateEIP7002Requests(vm, requests) } if (requests.length > 1) { @@ -1103,3 +985,41 @@ export const accumulateRequests = async (vm: VM): Promise => { } return requests } + +const _accumulateEIP7002Requests = async (vm: VM, requests: CLRequest[]): Promise => { + console.log('Perform system call') + // TODO PERFORM LOGIC TO CHECK IF CONTRACT EXISTS + // Partial withdrawals logic + const addressBytes = setLengthLeft( + bigIntToBytes(vm.common.param('vm', 'withdrawalRequestPredeployAddress')), + 20 + ) + const withdrawalsAddress = Address.fromString(bytesToHex(addressBytes)) + + const systemAddressBytes = setLengthLeft( + bigIntToBytes(vm.common.param('vm', 'systemAddress')), + 20 + ) + const systemAddress = Address.fromString(bytesToHex(systemAddressBytes)) + + const results = await vm.evm.runCall({ + caller: systemAddress, + gasLimit: BigInt(1_000_000), + to: withdrawalsAddress, + }) + + 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) + const validatorPubkey = slicedBytes.slice(20, 20 + 48) + const amount = slicedBytes.slice(20 + 48, 20 + 48 + 8) + const rlpData = RLP.encode([sourceAddress, validatorPubkey, amount]) + const request = new ValidatorWithdrawalRequest(withdrawalRequestType, rlpData) + requests.push(request) + } + } +} diff --git a/packages/vm/test/api/EIPs/eip-7002.spec.ts b/packages/vm/test/api/EIPs/eip-7002.spec.ts index 0b94aacbc2..9d654a32cc 100644 --- a/packages/vm/test/api/EIPs/eip-7002.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7002.spec.ts @@ -11,9 +11,8 @@ import { setLengthLeft, zeros, } from '@ethereumjs/util' -import { assert, describe, expect, it } from 'vitest' +import { describe, it } from 'vitest' -import { VM } from '../../../src/vm.js' import { setupVM } from '../utils.js' const pkey = hexToBytes(`0x${'20'.repeat(32)}`) @@ -56,15 +55,18 @@ describe('EIP-7002 tests', () => { await vm.stateManager.putAccount(addr, acc) // Deploy withdrawals contract - await vm.runBlock({ + const results = await vm.runBlock({ block, skipHeaderValidation: true, skipBlockValidation: true, generate: true, }) + const root = results.stateRoot + const validatorPubkey = hexToBytes(`0x${'20'.repeat(48)}`) - const amount = setLengthLeft(new Uint8Array(100), 8) + const amount = BigInt(12345678) + const amountBytes = setLengthLeft(bigIntToBytes(amount), 8) const addressBytes = setLengthLeft( bigIntToBytes(common.param('vm', 'withdrawalRequestPredeployAddress')), @@ -74,10 +76,10 @@ describe('EIP-7002 tests', () => { const tx = LegacyTransaction.fromTxData({ gasPrice: BigInt(100), - data: concatBytes(validatorPubkey, amount), + data: concatBytes(validatorPubkey, amountBytes), value: BigInt(1), to: withdrawalsAddress, - gasLimit: 100_000, + gasLimit: 200_000, }).sign(pkey) // Call withdrawals contract with a withdrawals request @@ -92,16 +94,21 @@ describe('EIP-7002 tests', () => { { common } ) - vm.evm.events?.on('step', (e) => { - console.log(e.opcode.name, e.stack) + let generatedBlock: Block + vm.events.on('afterBlock', (e) => { + generatedBlock = e.block }) - const res = await vm.runBlock({ + await vm.runBlock({ block: block2, skipHeaderValidation: true, skipBlockValidation: true, generate: true, }) - console.log(res.requests) + + console.log(bytesToHex(generatedBlock!.header.requestsRoot!)) + console.log(generatedBlock!.requests) + + //await vm.runBlock({ block: generatedBlock!, skipHeaderValidation: true, root }) }) }) From ef00da08bea4e97d03ba4863473e6710d34a6c7c Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Wed, 1 May 2024 08:48:12 +0200 Subject: [PATCH 07/11] vm: ensure requests get added to the generated block --- packages/vm/src/runBlock.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 0a143480fd..9302adb7f7 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -226,6 +226,7 @@ export async function runBlock(this: VM, opts: RunBlockOpts): Promise => { } const _accumulateEIP7002Requests = async (vm: VM, requests: CLRequest[]): Promise => { - console.log('Perform system call') // TODO PERFORM LOGIC TO CHECK IF CONTRACT EXISTS // Partial withdrawals logic const addressBytes = setLengthLeft( From b58d6be19884aaec1efca7ab6c707a164a47ec45 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Wed, 1 May 2024 08:48:22 +0200 Subject: [PATCH 08/11] vm: expand 7002 tests [no ci] --- packages/vm/test/api/EIPs/eip-7002.spec.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/vm/test/api/EIPs/eip-7002.spec.ts b/packages/vm/test/api/EIPs/eip-7002.spec.ts index 9d654a32cc..a2c4cd8ad8 100644 --- a/packages/vm/test/api/EIPs/eip-7002.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7002.spec.ts @@ -1,5 +1,6 @@ import { Block } from '@ethereumjs/block' import { Chain, Common, Hardfork } from '@ethereumjs/common' +import { RLP } from '@ethereumjs/rlp' import { LegacyTransaction } from '@ethereumjs/tx' import { Account, @@ -7,11 +8,12 @@ import { bigIntToBytes, bytesToHex, concatBytes, + equalsBytes, hexToBytes, setLengthLeft, zeros, } from '@ethereumjs/util' -import { describe, it } from 'vitest' +import { assert, describe, it } from 'vitest' import { setupVM } from '../utils.js' @@ -106,8 +108,17 @@ describe('EIP-7002 tests', () => { generate: true, }) - console.log(bytesToHex(generatedBlock!.header.requestsRoot!)) - console.log(generatedBlock!.requests) + assert.ok(generatedBlock!.requests!.length === 1) + + const requestDecoded = RLP.decode(generatedBlock!.requests![0].bytes) + + const sourceAddressRequest = requestDecoded[0] as Uint8Array + const validatorPubkeyRequest = requestDecoded[1] as Uint8Array + const amountRequest = requestDecoded[2] as Uint8Array + + assert.ok(equalsBytes(sourceAddressRequest, tx.getSenderAddress().bytes)) + assert.ok(equalsBytes(validatorPubkey, validatorPubkeyRequest)) + assert.ok(equalsBytes(amountBytes, amountRequest)) //await vm.runBlock({ block: generatedBlock!, skipHeaderValidation: true, root }) }) From b48edf101ea702ebbd2e8e7436404932e3a2aee2 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Wed, 1 May 2024 09:07:59 +0200 Subject: [PATCH 09/11] vm: eip7002: ensure generated block runs --- packages/vm/test/api/EIPs/eip-7002.spec.ts | 42 ++++++++++++---------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/vm/test/api/EIPs/eip-7002.spec.ts b/packages/vm/test/api/EIPs/eip-7002.spec.ts index a2c4cd8ad8..91476a69e7 100644 --- a/packages/vm/test/api/EIPs/eip-7002.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7002.spec.ts @@ -40,6 +40,27 @@ const upfrontCost = deploymentTx.getUpfrontCost() const acc = new Account() acc.balance = upfrontCost +const validatorPubkey = hexToBytes(`0x${'20'.repeat(48)}`) +const amount = BigInt(12345678) +const amountBytes = setLengthLeft(bigIntToBytes(amount), 8) + +function generateTx(nonce: bigint) { + const addressBytes = setLengthLeft( + bigIntToBytes(common.param('vm', 'withdrawalRequestPredeployAddress')), + 20 + ) + const withdrawalsAddress = Address.fromString(bytesToHex(addressBytes)) + + return LegacyTransaction.fromTxData({ + nonce, + gasPrice: BigInt(100), + data: concatBytes(validatorPubkey, amountBytes), + value: BigInt(1), + to: withdrawalsAddress, + gasLimit: 200_000, + }).sign(pkey) +} + describe('EIP-7002 tests', () => { it('should correctly create requests', async () => { const vm = await setupVM({ common }) @@ -47,7 +68,6 @@ describe('EIP-7002 tests', () => { { header: { number: 1, - parentBeaconBlockRoot: zeros(32), }, transactions: [deploymentTx], }, @@ -66,23 +86,7 @@ describe('EIP-7002 tests', () => { const root = results.stateRoot - const validatorPubkey = hexToBytes(`0x${'20'.repeat(48)}`) - const amount = BigInt(12345678) - const amountBytes = setLengthLeft(bigIntToBytes(amount), 8) - - const addressBytes = setLengthLeft( - bigIntToBytes(common.param('vm', 'withdrawalRequestPredeployAddress')), - 20 - ) - const withdrawalsAddress = Address.fromString(bytesToHex(addressBytes)) - - const tx = LegacyTransaction.fromTxData({ - gasPrice: BigInt(100), - data: concatBytes(validatorPubkey, amountBytes), - value: BigInt(1), - to: withdrawalsAddress, - gasLimit: 200_000, - }).sign(pkey) + const tx = generateTx(BigInt(0)) // Call withdrawals contract with a withdrawals request const block2 = Block.fromBlockData( @@ -120,6 +124,6 @@ describe('EIP-7002 tests', () => { assert.ok(equalsBytes(validatorPubkey, validatorPubkeyRequest)) assert.ok(equalsBytes(amountBytes, amountRequest)) - //await vm.runBlock({ block: generatedBlock!, skipHeaderValidation: true, root }) + await vm.runBlock({ block: generatedBlock!, skipHeaderValidation: true, root }) }) }) From 29f64cb4a06ce16a05b7b0060ad820411e65b836 Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Wed, 1 May 2024 09:15:10 +0200 Subject: [PATCH 10/11] vm: eip7002 add error when contract is not deployed --- packages/vm/src/runBlock.ts | 8 ++++ packages/vm/test/api/EIPs/eip-7002.spec.ts | 53 ++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 9302adb7f7..61a87aa562 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -996,6 +996,14 @@ const _accumulateEIP7002Requests = async (vm: VM, requests: CLRequest[]): Promis ) const withdrawalsAddress = Address.fromString(bytesToHex(addressBytes)) + const code = await vm.stateManager.getContractCode(withdrawalsAddress) + + if (code.length === 0) { + throw new Error( + 'Attempt to accumulate EIP-7002 requests failed: the contract does not exist. Ensure the deployment tx has been run, or that the required contract code is stored' + ) + } + const systemAddressBytes = setLengthLeft( bigIntToBytes(vm.common.param('vm', 'systemAddress')), 20 diff --git a/packages/vm/test/api/EIPs/eip-7002.spec.ts b/packages/vm/test/api/EIPs/eip-7002.spec.ts index 91476a69e7..1ab74c1b89 100644 --- a/packages/vm/test/api/EIPs/eip-7002.spec.ts +++ b/packages/vm/test/api/EIPs/eip-7002.spec.ts @@ -22,6 +22,8 @@ const addr = Address.fromPrivateKey(pkey) const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Cancun, eips: [7685, 7002] }) +// Note: this deployment tx data is the deployment tx in order to setup the EIP-7002 contract +// It is taken from the EIP const deploymentTxData = { nonce: BigInt(0), gasLimit: BigInt('0x3d090'), @@ -112,6 +114,7 @@ describe('EIP-7002 tests', () => { generate: true, }) + // Ensure the request is generated assert.ok(generatedBlock!.requests!.length === 1) const requestDecoded = RLP.decode(generatedBlock!.requests![0].bytes) @@ -120,10 +123,60 @@ describe('EIP-7002 tests', () => { const validatorPubkeyRequest = requestDecoded[1] as Uint8Array const amountRequest = requestDecoded[2] as Uint8Array + // Ensure the requests are correct assert.ok(equalsBytes(sourceAddressRequest, tx.getSenderAddress().bytes)) assert.ok(equalsBytes(validatorPubkey, validatorPubkeyRequest)) assert.ok(equalsBytes(amountBytes, amountRequest)) await vm.runBlock({ block: generatedBlock!, skipHeaderValidation: true, root }) + + // Run block with 2 requests + + const tx2 = generateTx(BigInt(1)) + const tx3 = generateTx(BigInt(2)) + + const block3 = Block.fromBlockData( + { + header: { + number: 3, + parentBeaconBlockRoot: zeros(32), + }, + transactions: [tx2, tx3], + }, + { common } + ) + + await vm.runBlock({ + block: block3, + skipHeaderValidation: true, + skipBlockValidation: true, + generate: true, + }) + + // 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) + }) + + it('should throw when contract is not deployed', async () => { + const vm = await setupVM({ common }) + const block = Block.fromBlockData( + { + header: { + number: 1, + }, + }, + { common } + ) + try { + await vm.runBlock({ + block, + skipHeaderValidation: true, + skipBlockValidation: true, + generate: true, + }) + } catch (e: any) { + assert.ok(e.message.includes('Attempt to accumulate EIP-7002 requests failed')) + } }) }) From 6dbb5f2990bc1b96fcfe89aeb78c579b77ad67ec Mon Sep 17 00:00:00 2001 From: Jochem Brouwer Date: Thu, 2 May 2024 01:23:07 +0200 Subject: [PATCH 11/11] vm: cleanup commit --- packages/vm/src/runBlock.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 61a87aa562..12caa290bc 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -988,7 +988,6 @@ export const accumulateRequests = async (vm: VM): Promise => { } const _accumulateEIP7002Requests = async (vm: VM, requests: CLRequest[]): Promise => { - // TODO PERFORM LOGIC TO CHECK IF CONTRACT EXISTS // Partial withdrawals logic const addressBytes = setLengthLeft( bigIntToBytes(vm.common.param('vm', 'withdrawalRequestPredeployAddress')), @@ -1022,9 +1021,9 @@ const _accumulateEIP7002Requests = async (vm: VM, requests: CLRequest[]): Promis // 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) - const validatorPubkey = slicedBytes.slice(20, 20 + 48) - const amount = slicedBytes.slice(20 + 48, 20 + 48 + 8) + 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 ValidatorWithdrawalRequest(withdrawalRequestType, rlpData) requests.push(request)