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', 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()) { diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 8bcba57b2c..12caa290bc 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -10,6 +10,7 @@ import { BIGINT_0, BIGINT_1, BIGINT_8, + CLRequest, GWEI_TO_WEI, KECCAK256_RLP, bigIntToBytes, @@ -40,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 { CLRequestType, PrefixedHexString } from '@ethereumjs/util' const { debug: createDebugLogger } = debugDefault @@ -225,6 +226,7 @@ export async function runBlock(this: VM, opts: RunBlockOpts): 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)) { + await _accumulateEIP7002Requests(vm, requests) + } if (requests.length > 1) { for (let x = 1; x < requests.length; x++) { @@ -971,3 +986,47 @@ export const accumulateRequests = async (_vm: VM): Promise => { } return requests } + +const _accumulateEIP7002Requests = async (vm: VM, requests: CLRequest[]): Promise => { + // Partial withdrawals logic + const addressBytes = setLengthLeft( + bigIntToBytes(vm.common.param('vm', 'withdrawalRequestPredeployAddress')), + 20 + ) + 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 + ) + 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) // 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) + } + } +} 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..1ab74c1b89 --- /dev/null +++ b/packages/vm/test/api/EIPs/eip-7002.spec.ts @@ -0,0 +1,182 @@ +import { Block } from '@ethereumjs/block' +import { Chain, Common, Hardfork } from '@ethereumjs/common' +import { RLP } from '@ethereumjs/rlp' +import { LegacyTransaction } from '@ethereumjs/tx' +import { + Account, + Address, + bigIntToBytes, + bytesToHex, + concatBytes, + equalsBytes, + hexToBytes, + setLengthLeft, + zeros, +} from '@ethereumjs/util' +import { assert, describe, it } from 'vitest' + +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] }) + +// 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'), + 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 + +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 }) + const block = Block.fromBlockData( + { + header: { + number: 1, + }, + transactions: [deploymentTx], + }, + { common } + ) + await vm.stateManager.putAccount(sender, acc) + await vm.stateManager.putAccount(addr, acc) + + // Deploy withdrawals contract + const results = await vm.runBlock({ + block, + skipHeaderValidation: true, + skipBlockValidation: true, + generate: true, + }) + + const root = results.stateRoot + + const tx = generateTx(BigInt(0)) + + // Call withdrawals contract with a withdrawals request + const block2 = Block.fromBlockData( + { + header: { + number: 2, + parentBeaconBlockRoot: zeros(32), + }, + transactions: [tx], + }, + { common } + ) + + let generatedBlock: Block + vm.events.on('afterBlock', (e) => { + generatedBlock = e.block + }) + + await vm.runBlock({ + block: block2, + skipHeaderValidation: true, + skipBlockValidation: true, + generate: true, + }) + + // Ensure the request is generated + 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 + + // 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')) + } + }) +})