diff --git a/packages/block/src/block.ts b/packages/block/src/block.ts index 2670b096cd..194f8185cd 100644 --- a/packages/block/src/block.ts +++ b/packages/block/src/block.ts @@ -6,6 +6,7 @@ import { BIGINT_0, CLRequestFactory, CLRequestType, + ConsolidationRequest, DepositRequest, KECCAK256_RLP, KECCAK256_RLP_ARRAY, @@ -424,6 +425,7 @@ export class Block { withdrawals: withdrawalsData, depositRequests, withdrawalRequests, + consolidationRequests, executionWitness, } = payload @@ -454,8 +456,13 @@ export class Block { const hasDepositRequests = depositRequests !== undefined && depositRequests !== null const hasWithdrawalRequests = withdrawalRequests !== undefined && withdrawalRequests !== null + const hasConsolidationRequests = + consolidationRequests !== undefined && consolidationRequests !== null + const requests = - hasDepositRequests || hasWithdrawalRequests ? ([] as CLRequest[]) : undefined + hasDepositRequests || hasWithdrawalRequests || hasConsolidationRequests + ? ([] as CLRequest[]) + : undefined if (depositRequests !== undefined && depositRequests !== null) { for (const dJson of depositRequests) { @@ -467,6 +474,11 @@ export class Block { requests!.push(WithdrawalRequest.fromJSON(wJson)) } } + if (consolidationRequests !== undefined && consolidationRequests !== null) { + for (const cJson of consolidationRequests) { + requests!.push(ConsolidationRequest.fromJSON(cJson)) + } + } const requestsRoot = requests ? await Block.genRequestsTrieRoot(requests, new Trie({ common: opts?.common })) @@ -1006,6 +1018,7 @@ export class Block { // 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) { @@ -1018,11 +1031,16 @@ export class Block { 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.withdrawalRequests !== undefined || + executionPayload.consolidationRequests !== undefined ) { throw Error(`Undefined requests for activated deposit or withdrawal requests`) } diff --git a/packages/block/src/from-beacon-payload.ts b/packages/block/src/from-beacon-payload.ts index 822410ff8c..b72679d8a5 100644 --- a/packages/block/src/from-beacon-payload.ts +++ b/packages/block/src/from-beacon-payload.ts @@ -24,6 +24,12 @@ type BeaconWithdrawalRequest = { 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 = { @@ -48,6 +54,7 @@ export type BeaconPayloadJson = { // requests data deposit_requests?: BeaconDepositRequest[] withdrawal_requests?: BeaconWithdrawalRequest[] + consolidation_requests?: BeaconConsolidationRequest[] // the casing of VerkleExecutionWitness remains same camel case for now execution_witness?: VerkleExecutionWitness @@ -164,6 +171,13 @@ export function executionPayloadFromBeaconPayload(payload: BeaconPayloadJson): E amount: breq.amount, })) } + if (payload.consolidation_requests !== undefined && payload.consolidation_requests !== null) { + executionPayload.consolidationRequests = payload.consolidation_requests.map((breq) => ({ + sourceAddress: breq.source_address, + sourcePubkey: breq.source_pubkey, + targetPubkey: breq.target_pubkey, + })) + } if (payload.execution_witness !== undefined && payload.execution_witness !== null) { // the casing structure in payload could be camel case or snake depending upon the CL diff --git a/packages/block/src/types.ts b/packages/block/src/types.ts index 8c08048dc4..409705b60d 100644 --- a/packages/block/src/types.ts +++ b/packages/block/src/types.ts @@ -7,6 +7,7 @@ import type { BytesLike, CLRequest, CLRequestType, + ConsolidationRequestV1, DepositRequestV1, JsonRpcWithdrawal, PrefixedHexString, @@ -269,4 +270,5 @@ export type ExecutionPayload = { 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/eip7685block.spec.ts b/packages/block/test/eip7685block.spec.ts index 7c1c2f293a..1d1962aacb 100644 --- a/packages/block/test/eip7685block.spec.ts +++ b/packages/block/test/eip7685block.spec.ts @@ -26,7 +26,7 @@ function getRandomDepositRequest(): CLRequest { function getRandomWithdrawalRequest(): CLRequest { const withdrawalRequestData = { sourceAddress: randomBytes(20), - validatorPubkey: randomBytes(48), + validatorPublicKey: randomBytes(48), amount: bytesToBigInt(randomBytes(8)), } return WithdrawalRequest.fromRequestData(withdrawalRequestData) as CLRequest diff --git a/packages/client/src/rpc/modules/engine/types.ts b/packages/client/src/rpc/modules/engine/types.ts index fbfb7aaebd..c8be49cef5 100644 --- a/packages/client/src/rpc/modules/engine/types.ts +++ b/packages/client/src/rpc/modules/engine/types.ts @@ -2,7 +2,12 @@ import { UNKNOWN_PAYLOAD } from '../../error-code.js' import type { Skeleton } from '../../../service/index.js' import type { Block, ExecutionPayload } from '@ethereumjs/block' -import type { DepositRequestV1, PrefixedHexString, WithdrawalRequestV1 } from '@ethereumjs/util' +import type { + ConsolidationRequestV1, + DepositRequestV1, + PrefixedHexString, + WithdrawalRequestV1, +} from '@ethereumjs/util' export enum Status { ACCEPTED = 'ACCEPTED', @@ -31,6 +36,7 @@ export type ExecutionPayloadV3 = ExecutionPayloadV2 & { excessBlobGas: Uint64; b export type ExecutionPayloadV4 = ExecutionPayloadV3 & { depositRequests: DepositRequestV1[] withdrawalRequests: WithdrawalRequestV1[] + consolidationRequests: ConsolidationRequestV1[] } export type ForkchoiceStateV1 = { diff --git a/packages/client/src/rpc/modules/engine/validators.ts b/packages/client/src/rpc/modules/engine/validators.ts index ab63b512bd..a90704cb5d 100644 --- a/packages/client/src/rpc/modules/engine/validators.ts +++ b/packages/client/src/rpc/modules/engine/validators.ts @@ -30,6 +30,7 @@ export const executionPayloadV4FieldValidators = { ...executionPayloadV3FieldValidators, depositRequests: validators.array(validators.depositRequest()), withdrawalRequests: validators.array(validators.withdrawalRequest()), + consolidationRequests: validators.array(validators.consolidationRequest()), } export const forkchoiceFieldValidators = { diff --git a/packages/client/src/rpc/validation.ts b/packages/client/src/rpc/validation.ts index 84afefdb48..af2e2f30fe 100644 --- a/packages/client/src/rpc/validation.ts +++ b/packages/client/src/rpc/validation.ts @@ -440,10 +440,10 @@ export const validators = { } } - const wt = params[index] + const clReq = params[index] for (const field of requiredFields) { - if (wt[field] === undefined) { + if (clReq[field] === undefined) { return { code: INVALID_PARAMS, message: `invalid argument ${index}: required field ${field}`, @@ -458,25 +458,25 @@ export const validators = { } // validate pubkey - for (const field of [wt.pubkey]) { + for (const field of [clReq.pubkey]) { const v = validate(field, this.bytes48) if (v !== undefined) return v } // validate withdrawalCredentials - for (const field of [wt.withdrawalCredentials]) { + for (const field of [clReq.withdrawalCredentials]) { const v = validate(field, this.bytes32) if (v !== undefined) return v } // validate amount, index - for (const field of [wt.amount, wt.index]) { + for (const field of [clReq.amount, clReq.index]) { const v = validate(field, this.bytes8) if (v !== undefined) return v } // validate signature - for (const field of [wt.signature]) { + for (const field of [clReq.signature]) { const v = validate(field, this.bytes96) if (v !== undefined) return v } @@ -494,10 +494,10 @@ export const validators = { } } - const wt = params[index] + const clReq = params[index] for (const field of requiredFields) { - if (wt[field] === undefined) { + if (clReq[field] === undefined) { return { code: INVALID_PARAMS, message: `invalid argument ${index}: required field ${field}`, @@ -512,19 +512,19 @@ export const validators = { } // validate sourceAddress - for (const field of [wt.sourceAddress]) { + for (const field of [clReq.sourceAddress]) { const v = validate(field, this.address) if (v !== undefined) return v } // validate validatorPubkey - for (const field of [wt.validatorPubkey]) { + for (const field of [clReq.validatorPubkey]) { const v = validate(field, this.bytes48) if (v !== undefined) return v } // validate amount - for (const field of [wt.amount]) { + for (const field of [clReq.amount]) { const v = validate(field, this.bytes8) if (v !== undefined) return v } @@ -532,6 +532,54 @@ export const validators = { } }, + get consolidationRequest() { + return (requiredFields: string[] = ['sourceAddress', 'sourcePubkey', 'targetPubkey']) => { + return (params: any[], index: number) => { + if (typeof params[index] !== 'object') { + return { + code: INVALID_PARAMS, + message: `invalid argument ${index}: argument must be an object`, + } + } + + const clReq = params[index] + + for (const field of requiredFields) { + if (clReq[field] === undefined) { + return { + code: INVALID_PARAMS, + message: `invalid argument ${index}: required field ${field}`, + } + } + } + + const validate = (field: any, validator: Function) => { + if (field === undefined) return + const v = validator([field], 0) + if (v !== undefined) return v + } + + // validate sourceAddress + for (const field of [clReq.sourceAddress]) { + const v = validate(field, this.address) + if (v !== undefined) return v + } + + // validate validatorPubkey + for (const field of [clReq.sourcePubkey]) { + const v = validate(field, this.bytes48) + if (v !== undefined) return v + } + + // validate amount + for (const field of [clReq.targetPubkey]) { + const v = validate(field, this.bytes48) + if (v !== undefined) return v + } + } + } + }, + /** * object validator to check if type is object with * required keys and expected validation of values diff --git a/packages/client/test/rpc/engine/newPayloadV4.spec.ts b/packages/client/test/rpc/engine/newPayloadV4.spec.ts index 5528ef6950..a78e57bfe0 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] = blocks const parentBeaconBlockRoot = '0x42942949c4ed512cd85c2cb54ca88591338cbb0564d3a2bea7961a639ef29d64' const validForkChoiceState = { - headBlockHash: '0x3ff9144b3f0818580798b0a9ff5cedc1350ff62f46ec99b098344e2864be1e47', - safeBlockHash: '0x3ff9144b3f0818580798b0a9ff5cedc1350ff62f46ec99b098344e2864be1e47', - finalizedBlockHash: '0x3ff9144b3f0818580798b0a9ff5cedc1350ff62f46ec99b098344e2864be1e47', + headBlockHash: '0x5040e6b0056398536751c187683a3ecde8aff8fd9ea1d3450d687d7032134caf', + safeBlockHash: '0x5040e6b0056398536751c187683a3ecde8aff8fd9ea1d3450d687d7032134caf', + finalizedBlockHash: '0x5040e6b0056398536751c187683a3ecde8aff8fd9ea1d3450d687d7032134caf', } const validPayloadAttributes = { timestamp: '0x64ba84fd', @@ -63,9 +63,10 @@ describe(`${method}: call with executionPayloadV4`, () => { excessBlobGas: '0x0', depositRequests: [], withdrawalRequests: [], - parentHash: '0x3ff9144b3f0818580798b0a9ff5cedc1350ff62f46ec99b098344e2864be1e47', - stateRoot: '0xd207043769091b6cdc91621f12bf2800b0b4643aeff09118fca52543c7a8ff03', - blockHash: '0xf9b4285204630ca183fec0a9a282cb68021af1aa9f3ab5f10d6b9ea8a7a3d4b6', + consolidationRequests: [], + parentHash: '0x5040e6b0056398536751c187683a3ecde8aff8fd9ea1d3450d687d7032134caf', + stateRoot: '0xbde9840c609ffa39cae0a2c9e354ac673920fcc2a5e6faeef5b78817c7fba7dd', + blockHash: '0x6b3ee4bb75e316427142bb9b48629e3e87ed8eea9f6d42b6aae296a11ec920b3', } const oldMethods = ['engine_newPayloadV1', 'engine_newPayloadV2', 'engine_newPayloadV3'] @@ -112,6 +113,10 @@ describe(`${method}: call with executionPayloadV4`, () => { executionPayload.withdrawalRequests !== undefined, 'depositRequests field should be received' ) + assert.ok( + executionPayload.consolidationRequests !== undefined, + 'consolidationRequests field should be received' + ) res = await rpc.request(method, [executionPayload, [], parentBeaconBlockRoot]) assert.equal(res.result.status, 'VALID') @@ -133,11 +138,19 @@ describe(`${method}: call with executionPayloadV4`, () => { const electraGenesisContracts = { // sender corresponding to the priv key 0x9c9996335451aab4fc4eac58e31a8c300e095cdbcee532d53d09280e83360355 '0x610adc49ecd66cbf176a8247ebd59096c031bd9f': { balance: '0x6d6172697573766477000000' }, + // eip 2925 contract '0x0aae40965e6800cd9b1f4b05ff21581047e3f91e': { balance: '0', nonce: '1', code: '0x3373fffffffffffffffffffffffffffffffffffffffe1460575767ffffffffffffffff5f3511605357600143035f3511604b575f35612000014311604b57611fff5f3516545f5260205ff35b5f5f5260205ff35b5f5ffd5b5f35611fff60014303165500', }, + // consolidation requests contract + '0x00b42dbF2194e931E80326D950320f7d9Dbeac02': { + balance: '0', + nonce: '1', + code: '0x3373fffffffffffffffffffffffffffffffffffffffe146098573615156028575f545f5260205ff35b36606014156101445760115f54600182026001905f5b5f82111560595781019083028483029004916001019190603e565b90939004341061014457600154600101600155600354806004026004013381556001015f35815560010160203581556001016040359055600101600355005b6003546002548082038060011160ac575060015b5f5b81811460f15780607402838201600402600401805490600101805490600101805490600101549260601b84529083601401528260340152906054015260010160ae565b9101809214610103579060025561010e565b90505f6002555f6003555b5f548061049d141561011d57505f5b6001546001828201116101325750505f610138565b01600190035b5f555f6001556074025ff35b5f5ffd', + }, + // withdrawals request contract '0x00A3ca265EBcb825B45F985A16CEFB49958cE017': { balance: '0', nonce: '1', @@ -147,6 +160,7 @@ const electraGenesisContracts = { '0x000000000000000000000000000000000000000000000000000000000000049d', }, }, + // beacon deposit contract for deposit receipts '0x00000000219ab540356cBB839Cbe05303d7705Fa': { balance: '0', code: '0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100a4578063621fd130146101ba578063c5f2892f14610244575b600080fd5b34801561005057600080fd5b506100906004803603602081101561006757600080fd5b50357fffffffff000000000000000000000000000000000000000000000000000000001661026b565b604080519115158252519081900360200190f35b6101b8600480360360808110156100ba57600080fd5b8101906020810181356401000000008111156100d557600080fd5b8201836020820111156100e757600080fd5b8035906020019184600183028401116401000000008311171561010957600080fd5b91939092909160208101903564010000000081111561012757600080fd5b82018360208201111561013957600080fd5b8035906020019184600183028401116401000000008311171561015b57600080fd5b91939092909160208101903564010000000081111561017957600080fd5b82018360208201111561018b57600080fd5b803590602001918460018302840111640100000000831117156101ad57600080fd5b919350915035610304565b005b3480156101c657600080fd5b506101cf6110b5565b6040805160208082528351818301528351919283929083019185019080838360005b838110156102095781810151838201526020016101f1565b50505050905090810190601f1680156102365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561025057600080fd5b506102596110c7565b60408051918252519081900360200190f35b60007fffffffff0000000000000000000000000000000000000000000000000000000082167f01ffc9a70000000000000000000000000000000000000000000000000000000014806102fe57507fffffffff0000000000000000000000000000000000000000000000000000000082167f8564090700000000000000000000000000000000000000000000000000000000145b92915050565b6030861461035d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118056026913960400191505060405180910390fd5b602084146103b6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603681526020018061179c6036913960400191505060405180910390fd5b6060821461040f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806118786029913960400191505060405180910390fd5b670de0b6b3a7640000341015610470576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118526026913960400191505060405180910390fd5b633b9aca003406156104cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260338152602001806117d26033913960400191505060405180910390fd5b633b9aca00340467ffffffffffffffff811115610535576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602781526020018061182b6027913960400191505060405180910390fd5b6060610540826114ba565b90507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c589898989858a8a6105756020546114ba565b6040805160a0808252810189905290819060208201908201606083016080840160c085018e8e80828437600083820152601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690910187810386528c815260200190508c8c808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690920188810386528c5181528c51602091820193918e019250908190849084905b83811015610648578181015183820152602001610630565b50505050905090810190601f1680156106755780820380516001836020036101000a031916815260200191505b5086810383528881526020018989808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169092018881038452895181528951602091820193918b019250908190849084905b838110156106ef5781810151838201526020016106d7565b50505050905090810190601f16801561071c5780820380516001836020036101000a031916815260200191505b509d505050505050505050505050505060405180910390a1600060028a8a600060801b604051602001808484808284377fffffffffffffffffffffffffffffffff0000000000000000000000000000000090941691909301908152604080517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0818403018152601090920190819052815191955093508392506020850191508083835b602083106107fc57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016107bf565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610859573d6000803e3d6000fd5b5050506040513d602081101561086e57600080fd5b5051905060006002806108846040848a8c6116fe565b6040516020018083838082843780830192505050925050506040516020818303038152906040526040518082805190602001908083835b602083106108f857805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016108bb565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610955573d6000803e3d6000fd5b5050506040513d602081101561096a57600080fd5b5051600261097b896040818d6116fe565b60405160009060200180848480828437919091019283525050604080518083038152602092830191829052805190945090925082918401908083835b602083106109f457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016109b7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610a51573d6000803e3d6000fd5b5050506040513d6020811015610a6657600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610ada57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610a9d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610b37573d6000803e3d6000fd5b5050506040513d6020811015610b4c57600080fd5b50516040805160208101858152929350600092600292839287928f928f92018383808284378083019250505093505050506040516020818303038152906040526040518082805190602001908083835b60208310610bd957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610b9c565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610c36573d6000803e3d6000fd5b5050506040513d6020811015610c4b57600080fd5b50516040518651600291889160009188916020918201918291908601908083835b60208310610ca957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610c6c565b6001836020036101000a0380198251168184511680821785525050505050509050018367ffffffffffffffff191667ffffffffffffffff1916815260180182815260200193505050506040516020818303038152906040526040518082805190602001908083835b60208310610d4e57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610d11565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610dab573d6000803e3d6000fd5b5050506040513d6020811015610dc057600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610e3457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610df7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610e91573d6000803e3d6000fd5b5050506040513d6020811015610ea657600080fd5b50519050858114610f02576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260548152602001806117486054913960600191505060405180910390fd5b60205463ffffffff11610f60576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806117276021913960400191505060405180910390fd5b602080546001019081905560005b60208110156110a9578160011660011415610fa0578260008260208110610f9157fe5b0155506110ac95505050505050565b600260008260208110610faf57fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061102557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610fe8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015611082573d6000803e3d6000fd5b5050506040513d602081101561109757600080fd5b50519250600282049150600101610f6e565b50fe5b50505050505050565b60606110c26020546114ba565b905090565b6020546000908190815b60208110156112f05781600116600114156111e6576002600082602081106110f557fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061116b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161112e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156111c8573d6000803e3d6000fd5b5050506040513d60208110156111dd57600080fd5b505192506112e2565b600283602183602081106111f657fe5b015460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061126b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161122e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156112c8573d6000803e3d6000fd5b5050506040513d60208110156112dd57600080fd5b505192505b6002820491506001016110d1565b506002826112ff6020546114ba565b600060401b6040516020018084815260200183805190602001908083835b6020831061135a57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161131d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790527fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000095909516920191825250604080518083037ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8018152601890920190819052815191955093508392850191508083835b6020831061143f57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101611402565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa15801561149c573d6000803e3d6000fd5b5050506040513d60208110156114b157600080fd5b50519250505090565b60408051600880825281830190925260609160208201818036833701905050905060c082901b8060071a60f81b826000815181106114f457fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060061a60f81b8260018151811061153757fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060051a60f81b8260028151811061157a57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060041a60f81b826003815181106115bd57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060031a60f81b8260048151811061160057fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060021a60f81b8260058151811061164357fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060011a60f81b8260068151811061168657fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060001a60f81b826007815181106116c957fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535050919050565b6000808585111561170d578182fd5b83861115611719578182fd5b505082019391909203915056fe4465706f736974436f6e74726163743a206d65726b6c6520747265652066756c6c4465706f736974436f6e74726163743a207265636f6e7374727563746564204465706f7369744461746120646f6573206e6f74206d6174636820737570706c696564206465706f7369745f646174615f726f6f744465706f736974436f6e74726163743a20696e76616c6964207769746864726177616c5f63726564656e7469616c73206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c7565206e6f74206d756c7469706c65206f6620677765694465706f736974436f6e74726163743a20696e76616c6964207075626b6579206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f20686967684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f206c6f774465706f736974436f6e74726163743a20696e76616c6964207369676e6174757265206c656e677468a2646970667358221220dceca8706b29e917dacf25fceef95acac8d90d765ac926663ce4096195952b6164736f6c634300060b0033', diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 664d5fe5f8..5baf035267 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -601,6 +601,27 @@ export const EIPs: EIPsDict = { }, }, }, + 7251: { + comment: 'Execution layer triggered consolidations (experimental)', + url: 'https://eips.ethereum.org/EIPS/eip-7251', + status: Status.Draft, + minimumHardfork: Hardfork.Paris, + requiredEIPs: [7685], + vm: { + consolidationRequestType: { + v: BigInt(0x02), + d: 'The withdrawal request type for EIP-7685', + }, + systemAddress: { + v: BigInt('0xfffffffffffffffffffffffffffffffffffffffe'), + d: 'The system address to perform operations on the consolidation requests predeploy address', + }, + consolidationRequestPredeployAddress: { + v: BigInt('0x00b42dbF2194e931E80326D950320f7d9Dbeac02'), + d: 'Address of the consolidations contract', + }, + }, + }, 7516: { comment: 'BLOBBASEFEE opcode', url: 'https://eips.ethereum.org/EIPS/eip-7516', diff --git a/packages/common/src/hardforks.ts b/packages/common/src/hardforks.ts index 4b882d26ab..0a3c927c78 100644 --- a/packages/common/src/hardforks.ts +++ b/packages/common/src/hardforks.ts @@ -841,7 +841,7 @@ export const hardforks: HardforksDict = { 'Next feature hardfork after cancun, internally used for pectra testing/implementation (incomplete/experimental)', url: 'https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/prague.md', status: Status.Draft, - eips: [2537, 2935, 3074, 6110, 7002, 7685], + eips: [2537, 2935, 3074, 6110, 7002, 7251, 7685], }, osaka: { name: 'osaka', diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index d980bd0715..2759bff71e 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -210,8 +210,8 @@ 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, 6110, 6780, 6800, 7002, 7516, 7685, - 7709, + 3670, 3855, 3860, 4399, 4895, 4788, 4844, 5133, 5656, 6110, 6780, 6800, 7002, 7251, 7516, + 7685, 7709, ] for (const eip of this.common.eips()) { diff --git a/packages/util/src/requests.ts b/packages/util/src/requests.ts index 9df5121b66..576ae52856 100644 --- a/packages/util/src/requests.ts +++ b/packages/util/src/requests.ts @@ -18,6 +18,7 @@ export type RequestBytes = Uint8Array export enum CLRequestType { Deposit = 0x00, Withdrawal = 0x01, + Consolidation = 0x02, } export type DepositRequestV1 = { @@ -34,9 +35,16 @@ export type WithdrawalRequestV1 = { 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 = { @@ -53,9 +61,16 @@ export type WithdrawalRequestData = { 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] @@ -191,6 +206,58 @@ export class WithdrawalRequest extends CLRequest { } } +export class ConsolidationRequest extends CLRequest { + constructor( + public readonly sourceAddress: Uint8Array, + public readonly sourcePubkey: Uint8Array, + public readonly targetPubkey: Uint8Array + ) { + super(CLRequestType.Consolidation) + } + + public static fromRequestData(consolidationData: ConsolidationRequestData): ConsolidationRequest { + const { sourceAddress, sourcePubkey, targetPubkey } = consolidationData + return new ConsolidationRequest(sourceAddress, sourcePubkey, targetPubkey) + } + + public static fromJSON(jsonData: ConsolidationRequestV1): ConsolidationRequest { + const { sourceAddress, sourcePubkey, targetPubkey } = jsonData + return this.fromRequestData({ + sourceAddress: hexToBytes(sourceAddress), + sourcePubkey: hexToBytes(sourcePubkey), + targetPubkey: hexToBytes(targetPubkey), + }) + } + + 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), + } + } + + public static deserialize(bytes: Uint8Array): ConsolidationRequest { + const [sourceAddress, sourcePubkey, targetPubkey] = RLP.decode(bytes.slice(1)) as [ + Uint8Array, + Uint8Array, + Uint8Array + ] + return this.fromRequestData({ + sourceAddress, + sourcePubkey, + targetPubkey, + }) + } +} + export class CLRequestFactory { public static fromSerializedRequest(bytes: Uint8Array): CLRequest { switch (bytes[0]) { @@ -198,6 +265,8 @@ export class CLRequestFactory { return DepositRequest.deserialize(bytes) case CLRequestType.Withdrawal: return WithdrawalRequest.deserialize(bytes) + case CLRequestType.Consolidation: + return ConsolidationRequest.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 5e798f98e4..c2f51e7f47 100644 --- a/packages/util/test/requests.spec.ts +++ b/packages/util/test/requests.spec.ts @@ -4,6 +4,7 @@ import { bytesToBigInt, randomBytes } from '../src/bytes.js' import { CLRequestFactory, CLRequestType, + ConsolidationRequest, DepositRequest, WithdrawalRequest, } from '../src/requests.js' @@ -11,49 +12,55 @@ import { 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 testCases = [ + [ + 'DepositRequest', + { + pubkey: randomBytes(48), + withdrawalCredentials: randomBytes(32), + amount: bytesToBigInt(randomBytes(8)), + signature: randomBytes(96), + index: bytesToBigInt(randomBytes(8)), + }, + CLRequestType.Deposit, + DepositRequest, + ], + [ + 'WithdrawalRequest', + { + sourceAddress: randomBytes(20), + validatorPubkey: randomBytes(48), + amount: bytesToBigInt(randomBytes(8)), + }, + CLRequestType.Withdrawal, + WithdrawalRequest, + ], + [ + 'ConsolidationRequest', + { + sourceAddress: randomBytes(20), + sourcePubkey: randomBytes(48), + targetPubkey: randomBytes(48), + }, + CLRequestType.Consolidation, + ConsolidationRequest, + ], + ] + for (const [requestName, requestData, requestType, RequestInstanceType] of testCases) { + it(`${requestName}`, () => { + const requestObject = RequestInstanceType.fromRequestData( + requestData + ) as CLRequest + const requestJSON = requestObject.toJSON() + const serialized = requestObject.serialize() + assert.equal(serialized[0], requestType) - 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, requestJSON) - const deserialized = CLRequestFactory.fromSerializedRequest(serialized) - const deserializedJSON = deserialized.toJSON() - assert.deepEqual(deserializedJSON, depositJSON) - - const reserialized = deserialized.serialize() - assert.deepEqual(serialized, reserialized) - }) - - it('withdrawal request', () => { - const withdrawalRequestData = { - sourceAddress: randomBytes(20), - validatorPubkey: 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) - }) + const reserialized = deserialized.serialize() + assert.deepEqual(serialized, reserialized) + }) + } }) diff --git a/packages/vm/src/requests.ts b/packages/vm/src/requests.ts index 6d8d2f4f48..7141fa8c97 100644 --- a/packages/vm/src/requests.ts +++ b/packages/vm/src/requests.ts @@ -1,8 +1,10 @@ import { Common } from '@ethereumjs/common' import { Address, + ConsolidationRequest, DepositRequest, WithdrawalRequest, + bigIntToAddressBytes, bigIntToBytes, bytesToBigInt, bytesToHex, @@ -41,6 +43,10 @@ export const accumulateRequests = async ( await accumulateEIP7002Requests(vm, requests) } + if (common.isActivatedEIP(7251)) { + await accumulateEIP7251Requests(vm, requests) + } + if (requests.length > 1) { for (let x = 1; x < requests.length; x++) { if (requests[x].type < requests[x - 1].type) @@ -69,10 +75,7 @@ const accumulateEIP7002Requests = async ( ) } - const systemAddressBytes = setLengthLeft( - bigIntToBytes(vm.common.param('vm', 'systemAddress')), - 20 - ) + const systemAddressBytes = bigIntToAddressBytes(vm.common.param('vm', 'systemAddress')) const systemAddress = Address.fromString(bytesToHex(systemAddressBytes)) const addrIsEmpty = (await vm.stateManager.getAccount(systemAddress)) === undefined @@ -100,6 +103,55 @@ const accumulateEIP7002Requests = async ( } } +const accumulateEIP7251Requests = async ( + vm: VM, + requests: CLRequest[] +): Promise => { + // Partial withdrawals logic + const addressBytes = setLengthLeft( + bigIntToBytes(vm.common.param('vm', 'consolidationRequestPredeployAddress')), + 20 + ) + const consolidationsAddress = Address.fromString(bytesToHex(addressBytes)) + + const code = await vm.stateManager.getContractCode(consolidationsAddress) + + if (code.length === 0) { + throw new Error( + 'Attempt to accumulate EIP-7251 requests failed: the contract does not exist. Ensure the deployment tx has been run, or that the required contract code is stored' + ) + } + + const systemAddressBytes = bigIntToAddressBytes(vm.common.param('vm', 'systemAddress')) + const systemAddress = Address.fromString(bytesToHex(systemAddressBytes)) + + const addrIsEmpty = (await vm.stateManager.getAccount(systemAddress)) === undefined + + const results = await vm.evm.runCall({ + caller: systemAddress, + gasLimit: BigInt(1_000_000), + to: consolidationsAddress, + }) + + 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( + ConsolidationRequest.fromRequestData({ sourceAddress, sourcePubkey, targetPubkey }) + ) + } + } + + if (addrIsEmpty) { + await vm.stateManager.deleteAccount(systemAddress) + } +} + const accumulateDeposits = async ( depositContractAddress: string, txResults: RunTxResult[],