Skip to content

Commit

Permalink
Implement EIP 7002 (withdrawal requests) (#3385)
Browse files Browse the repository at this point in the history
* common: add eip 7002

* vm: add 7002 request accumulation logic

* evm: support 7002

* vm: fix withdrawals address bug

* vm: add eip7002 test (wip) [no ci]

* vm: update eip7002 to use system call [no ci]

* vm: ensure requests get added to the generated block

* vm: expand 7002 tests [no ci]

* vm: eip7002: ensure generated block runs

* vm: eip7002 add error when contract is not deployed

* vm: cleanup commit

---------

Co-authored-by: acolytec3 <17355484+acolytec3@users.noreply.github.com>
  • Loading branch information
jochem-brouwer and acolytec3 authored May 1, 2024
1 parent 2dd8a42 commit 36cd606
Show file tree
Hide file tree
Showing 4 changed files with 302 additions and 4 deletions.
57 changes: 57 additions & 0 deletions packages/common/src/eips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/evm/src/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
65 changes: 62 additions & 3 deletions packages/vm/src/runBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
BIGINT_0,
BIGINT_1,
BIGINT_8,
CLRequest,
GWEI_TO_WEI,
KECCAK256_RLP,
bigIntToBytes,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -225,6 +226,7 @@ export async function runBlock(this: VM, opts: RunBlockOpts): Promise<RunBlockRe
}
const blockData = {
...block,
requests,
header: { ...block.header, ...generatedFields },
}
block = Block.fromBlockData(blockData, { common: this.common })
Expand Down Expand Up @@ -951,17 +953,30 @@ 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<CLRequest[]> => {
export const accumulateRequests = async (vm: VM): Promise<CLRequest[]> => {
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++) {
Expand All @@ -971,3 +986,47 @@ export const accumulateRequests = async (_vm: VM): Promise<CLRequest[]> => {
}
return requests
}

const _accumulateEIP7002Requests = async (vm: VM, requests: CLRequest[]): Promise<void> => {
// 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)
}
}
}
182 changes: 182 additions & 0 deletions packages/vm/test/api/EIPs/eip-7002.spec.ts
Original file line number Diff line number Diff line change
@@ -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'))
}
})
})

0 comments on commit 36cd606

Please sign in to comment.