From 3a68ce9f7003ace11a2e9da9883ded370546248c Mon Sep 17 00:00:00 2001 From: harkamal Date: Mon, 8 Apr 2024 19:31:13 +0530 Subject: [PATCH] feat: implement execution layer exits eip 7002 --- .../beacon-node/src/execution/engine/http.ts | 16 +++--- .../src/execution/engine/payloadIdCache.ts | 5 ++ .../beacon-node/src/execution/engine/types.ts | 48 +++++++++++++----- packages/light-client/src/spec/utils.ts | 7 ++- packages/params/src/index.ts | 1 + packages/params/src/presets/mainnet.ts | 1 + packages/params/src/presets/minimal.ts | 1 + packages/params/src/types.ts | 2 + .../src/block/processExecutionLayerExit.ts | 50 +++++++++++++++++++ .../src/block/processOperations.ts | 8 +++ .../src/slot/upgradeStateToElectra.ts | 2 +- .../state-transition/src/util/execution.ts | 3 ++ packages/types/src/electra/sszTypes.ts | 17 ++++++- packages/types/src/electra/types.ts | 3 ++ packages/validator/src/util/params.ts | 1 + 15 files changed, 140 insertions(+), 25 deletions(-) create mode 100644 packages/state-transition/src/block/processExecutionLayerExit.ts diff --git a/packages/beacon-node/src/execution/engine/http.ts b/packages/beacon-node/src/execution/engine/http.ts index 274e2164c7be..140663aef6ac 100644 --- a/packages/beacon-node/src/execution/engine/http.ts +++ b/packages/beacon-node/src/execution/engine/http.ts @@ -179,10 +179,10 @@ export class ExecutionEngineHttp implements IExecutionEngine { ForkSeq[fork] >= ForkSeq.electra ? "engine_newPayloadV6110" : ForkSeq[fork] >= ForkSeq.deneb - ? "engine_newPayloadV3" - : ForkSeq[fork] >= ForkSeq.capella - ? "engine_newPayloadV2" - : "engine_newPayloadV1"; + ? "engine_newPayloadV3" + : ForkSeq[fork] >= ForkSeq.capella + ? "engine_newPayloadV2" + : "engine_newPayloadV1"; const serializedExecutionPayload = serializeExecutionPayload(fork, executionPayload); @@ -375,10 +375,10 @@ export class ExecutionEngineHttp implements IExecutionEngine { ForkSeq[fork] >= ForkSeq.electra ? "engine_getPayloadV6110" : ForkSeq[fork] >= ForkSeq.deneb - ? "engine_getPayloadV3" - : ForkSeq[fork] >= ForkSeq.capella - ? "engine_getPayloadV2" - : "engine_getPayloadV1"; + ? "engine_getPayloadV3" + : ForkSeq[fork] >= ForkSeq.capella + ? "engine_getPayloadV2" + : "engine_getPayloadV1"; const payloadResponse = await this.rpc.fetchWithRetries< EngineApiRpcReturnTypes[typeof method], EngineApiRpcParamTypes[typeof method] diff --git a/packages/beacon-node/src/execution/engine/payloadIdCache.ts b/packages/beacon-node/src/execution/engine/payloadIdCache.ts index e5baa9fba92d..960b061f12da 100644 --- a/packages/beacon-node/src/execution/engine/payloadIdCache.ts +++ b/packages/beacon-node/src/execution/engine/payloadIdCache.ts @@ -26,6 +26,11 @@ export type DepositReceiptV1 = { index: QUANTITY; }; +export type ExecutionLayerExitV1 = { + sourceAddress: DATA; + validatorPubkey: DATA; +}; + type FcuAttributes = {headBlockHash: DATA; finalizedBlockHash: DATA} & Omit; export class PayloadIdCache { diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index ca64b20afaed..c3da8e37826e 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -17,7 +17,7 @@ import { quantityToBigint, } from "../../eth1/provider/utils.js"; import {ExecutionPayloadStatus, BlobsBundle, PayloadAttributes, VersionedHashes} from "./interface.js"; -import {WithdrawalV1, DepositReceiptV1} from "./payloadIdCache.js"; +import {WithdrawalV1, DepositReceiptV1, ExecutionLayerExitV1} from "./payloadIdCache.js"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -119,12 +119,14 @@ export type ExecutionPayloadBodyRpc = { transactions: DATA[]; withdrawals: WithdrawalV1[] | null | undefined; depositReceipts: DepositReceiptV1[] | null | undefined; + exits: ExecutionLayerExitV1[] | null | undefined; }; export type ExecutionPayloadBody = { transactions: bellatrix.Transaction[]; withdrawals: capella.Withdrawals | null; depositReceipts: electra.DepositReceipts | null; + exits: electra.ExecutionLayerExits | null; }; export type ExecutionPayloadRpc = { @@ -147,6 +149,7 @@ export type ExecutionPayloadRpc = { excessBlobGas?: QUANTITY; // DENEB parentBeaconBlockRoot?: QUANTITY; // DENEB depositReceipts?: DepositReceiptRpc[]; // ELECTRA + exits?: ExecutionLayerExitRpc[]; // ELECTRA }; export type WithdrawalRpc = { @@ -156,13 +159,8 @@ export type WithdrawalRpc = { amount: QUANTITY; }; -export type DepositReceiptRpc = { - pubkey: DATA; - withdrawalCredentials: DATA; - amount: QUANTITY; - signature: DATA; - index: QUANTITY; -}; +export type DepositReceiptRpc = DepositReceiptV1; +export type ExecutionLayerExitRpc = ExecutionLayerExitV1; export type VersionedHashesRpc = DATA[]; @@ -217,8 +215,9 @@ export function serializeExecutionPayload(fork: ForkName, data: allForks.Executi // ELECTRA adds depositReceipts to the ExecutionPayload if (ForkSeq[fork] >= ForkSeq.electra) { - const {depositReceipts} = data as electra.ExecutionPayload; + const {depositReceipts, exits} = data as electra.ExecutionPayload; payload.depositReceipts = depositReceipts.map(serializeDepositReceipt); + payload.exits = exits.map(serializeExecutionLayerExit); } return payload; @@ -307,14 +306,21 @@ export function parseExecutionPayload( } if (ForkSeq[fork] >= ForkSeq.electra) { - const {depositReceipts} = data; + const {depositReceipts, exits} = data; // Geth can also reply with null if (depositReceipts == null) { throw Error( `depositReceipts missing for ${fork} >= electra executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}` ); } - (executionPayload as electra.ExecutionPayload).depositReceipts = depositReceipts.map(deserializeDepositReceipts); + (executionPayload as electra.ExecutionPayload).depositReceipts = depositReceipts.map(deserializeDepositReceipt); + + if (exits == null) { + throw Error( + `exits missing for ${fork} >= electra executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}` + ); + } + (executionPayload as electra.ExecutionPayload).exits = exits.map(deserializeExecutionLayerExit); } return {executionPayload, executionPayloadValue, blobsBundle, shouldOverrideBuilder}; @@ -393,7 +399,7 @@ export function serializeDepositReceipt(depositReceipt: electra.DepositReceipt): }; } -export function deserializeDepositReceipts(serialized: DepositReceiptRpc): electra.DepositReceipt { +export function deserializeDepositReceipt(serialized: DepositReceiptRpc): electra.DepositReceipt { return { pubkey: dataToBytes(serialized.pubkey, 48), withdrawalCredentials: dataToBytes(serialized.withdrawalCredentials, 32), @@ -403,12 +409,27 @@ export function deserializeDepositReceipts(serialized: DepositReceiptRpc): elect } as electra.DepositReceipt; } +export function serializeExecutionLayerExit(exit: electra.ExecutionLayerExit): ExecutionLayerExitRpc { + return { + sourceAddress: bytesToData(exit.sourceAddress), + validatorPubkey: bytesToData(exit.validatorPubkey), + }; +} + +export function deserializeExecutionLayerExit(exit: ExecutionLayerExitRpc): electra.ExecutionLayerExit { + return { + sourceAddress: dataToBytes(exit.sourceAddress, 20), + validatorPubkey: dataToBytes(exit.validatorPubkey, 48), + }; +} + export function deserializeExecutionPayloadBody(data: ExecutionPayloadBodyRpc | null): ExecutionPayloadBody | null { return data ? { transactions: data.transactions.map((tran) => dataToBytes(tran, null)), withdrawals: data.withdrawals ? data.withdrawals.map(deserializeWithdrawal) : null, - depositReceipts: data.depositReceipts ? data.depositReceipts.map(deserializeDepositReceipts) : null, + depositReceipts: data.depositReceipts ? data.depositReceipts.map(deserializeDepositReceipt) : null, + exits: data.exits ? data.exits.map(deserializeExecutionLayerExit) : null, } : null; } @@ -419,6 +440,7 @@ export function serializeExecutionPayloadBody(data: ExecutionPayloadBody | null) transactions: data.transactions.map((tran) => bytesToData(tran)), withdrawals: data.withdrawals ? data.withdrawals.map(serializeWithdrawal) : null, depositReceipts: data.depositReceipts ? data.depositReceipts.map(serializeDepositReceipt) : null, + exits: data.exits ? data.exits.map(serializeExecutionLayerExit) : null, } : null; } diff --git a/packages/light-client/src/spec/utils.ts b/packages/light-client/src/spec/utils.ts index 8502cb6f5656..c8aa2795a957 100644 --- a/packages/light-client/src/spec/utils.ts +++ b/packages/light-client/src/spec/utils.ts @@ -108,6 +108,8 @@ export function upgradeLightClientHeader( case ForkName.electra: (upgradedHeader as electra.LightClientHeader).execution.depositReceiptsRoot = ssz.electra.LightClientHeader.fields.execution.fields.depositReceiptsRoot.defaultValue(); + (upgradedHeader as electra.LightClientHeader).execution.exitsRoot = + ssz.electra.LightClientHeader.fields.execution.fields.exitsRoot.defaultValue(); // Break if no further upgrades is required else fall through if (ForkSeq[targetFork] <= ForkSeq.electra) break; @@ -145,7 +147,10 @@ export function isValidLightClientHeader(config: ChainForkConfig, header: allFor } if (epoch < config.ELECTRA_FORK_EPOCH) { - if ((header as electra.LightClientHeader).execution.depositReceiptsRoot !== undefined) { + if ( + (header as electra.LightClientHeader).execution.depositReceiptsRoot !== undefined || + (header as electra.LightClientHeader).execution.exitsRoot !== undefined + ) { return false; } } diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index 482e591123b6..d903a404dfb5 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -95,6 +95,7 @@ export const { KZG_COMMITMENT_INCLUSION_PROOF_DEPTH, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD, + MAX_EXECUTION_LAYER_EXITS, } = activePreset; //////////// diff --git a/packages/params/src/presets/mainnet.ts b/packages/params/src/presets/mainnet.ts index 86f5c39c539e..802a6691c311 100644 --- a/packages/params/src/presets/mainnet.ts +++ b/packages/params/src/presets/mainnet.ts @@ -121,4 +121,5 @@ export const mainnetPreset: BeaconPreset = { // ELECTRA MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 8192, + MAX_EXECUTION_LAYER_EXITS: 16, }; diff --git a/packages/params/src/presets/minimal.ts b/packages/params/src/presets/minimal.ts index 6239a8c1a3eb..d10b420ed97c 100644 --- a/packages/params/src/presets/minimal.ts +++ b/packages/params/src/presets/minimal.ts @@ -122,4 +122,5 @@ export const minimalPreset: BeaconPreset = { // ELECTRA MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 4, + MAX_EXECUTION_LAYER_EXITS: 16, }; diff --git a/packages/params/src/types.ts b/packages/params/src/types.ts index 57ee8230a77d..7856f1be72ba 100644 --- a/packages/params/src/types.ts +++ b/packages/params/src/types.ts @@ -85,6 +85,7 @@ export type BeaconPreset = { // ELECTRA MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: number; + MAX_EXECUTION_LAYER_EXITS: number; }; /** @@ -173,6 +174,7 @@ export const beaconPresetTypes: BeaconPresetTypes = { // ELECTRA MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: "number", + MAX_EXECUTION_LAYER_EXITS: "number", }; type BeaconPresetTypes = { diff --git a/packages/state-transition/src/block/processExecutionLayerExit.ts b/packages/state-transition/src/block/processExecutionLayerExit.ts new file mode 100644 index 000000000000..43a072d770f9 --- /dev/null +++ b/packages/state-transition/src/block/processExecutionLayerExit.ts @@ -0,0 +1,50 @@ +import {byteArrayEquals} from "@chainsafe/ssz"; +import {electra} from "@lodestar/types"; +import {ETH1_ADDRESS_WITHDRAWAL_PREFIX, FAR_FUTURE_EPOCH} from "@lodestar/params"; + +import {isActiveValidator} from "../util/index.js"; +import {CachedBeaconStateElectra} from "../types.js"; +import {initiateValidatorExit} from "./index.js"; + +export function processExecutionLayerExit(state: CachedBeaconStateElectra, exit: electra.ExecutionLayerExit): void { + if (!isValidExecutionLayerExit(state, exit)) { + return; + } + + const {epochCtx} = state; + const validatorIndex = epochCtx.getValidatorIndex(exit.validatorPubkey); + const validator = validatorIndex !== undefined ? state.validators.get(validatorIndex) : undefined; + if (validator === undefined) { + throw Error("Internal error validator=undefined for a valid execution layer exit"); + } + initiateValidatorExit(state, validator); +} + +export function isValidExecutionLayerExit(state: CachedBeaconStateElectra, exit: electra.ExecutionLayerExit): boolean { + const {config, epochCtx} = state; + const validatorIndex = epochCtx.getValidatorIndex(exit.validatorPubkey); + const validator = validatorIndex !== undefined ? state.validators.getReadonly(validatorIndex) : undefined; + if (validator === undefined) { + return false; + } + + const {withdrawalCredentials} = validator; + if (withdrawalCredentials[0] !== ETH1_ADDRESS_WITHDRAWAL_PREFIX) { + return false; + } + + const executionAddress = withdrawalCredentials.slice(12, 32); + if (!byteArrayEquals(executionAddress, exit.sourceAddress)) { + return false; + } + + const currentEpoch = epochCtx.epoch; + return ( + // verify the validator is active + isActiveValidator(validator, currentEpoch) && + // verify exit has not been initiated + validator.exitEpoch === FAR_FUTURE_EPOCH && + // verify the validator had been active long enough + currentEpoch >= validator.activationEpoch + config.SHARD_COMMITTEE_PERIOD + ); +} diff --git a/packages/state-transition/src/block/processOperations.ts b/packages/state-transition/src/block/processOperations.ts index 968182fdf8c5..0e8c66942faf 100644 --- a/packages/state-transition/src/block/processOperations.ts +++ b/packages/state-transition/src/block/processOperations.ts @@ -8,6 +8,7 @@ import {processProposerSlashing} from "./processProposerSlashing.js"; import {processAttesterSlashing} from "./processAttesterSlashing.js"; import {processDeposit} from "./processDeposit.js"; import {processVoluntaryExit} from "./processVoluntaryExit.js"; +import {processExecutionLayerExit} from "./processExecutionLayerExit.js"; import {processBlsToExecutionChange} from "./processBlsToExecutionChange.js"; import {processDepositReceipt} from "./processDepositReceipt.js"; import {ProcessBlockOpts} from "./types.js"; @@ -18,6 +19,7 @@ export { processAttestations, processDeposit, processVoluntaryExit, + processExecutionLayerExit, processBlsToExecutionChange, processDepositReceipt, }; @@ -48,9 +50,15 @@ export function processOperations( for (const deposit of body.deposits) { processDeposit(fork, state, deposit); } + for (const voluntaryExit of body.voluntaryExits) { processVoluntaryExit(state, voluntaryExit, opts.verifySignatures); } + if (fork >= ForkSeq.electra) { + for (const elExit of (body as electra.BeaconBlockBody).executionPayload.exits) { + processExecutionLayerExit(state as CachedBeaconStateElectra, elExit); + } + } if (fork >= ForkSeq.capella) { for (const blsToExecutionChange of (body as capella.BeaconBlockBody).blsToExecutionChanges) { diff --git a/packages/state-transition/src/slot/upgradeStateToElectra.ts b/packages/state-transition/src/slot/upgradeStateToElectra.ts index 19cac8811f77..369ab19c447b 100644 --- a/packages/state-transition/src/slot/upgradeStateToElectra.ts +++ b/packages/state-transition/src/slot/upgradeStateToElectra.ts @@ -20,7 +20,7 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache epoch: stateDeneb.epochCtx.epoch, }); - // latestExecutionPayloadHeader's depositReceiptsRoot set to zeros by default + // latestExecutionPayloadHeader's depositReceiptsRoot and exitsRoot set to zeros by default // default value of depositReceiptsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX stateElectra.depositReceiptsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; diff --git a/packages/state-transition/src/util/execution.ts b/packages/state-transition/src/util/execution.ts index b7923fd9f807..37295f1e3802 100644 --- a/packages/state-transition/src/util/execution.ts +++ b/packages/state-transition/src/util/execution.ts @@ -173,6 +173,9 @@ export function executionPayloadToPayloadHeader( if (fork >= ForkSeq.electra) { (bellatrixPayloadFields as electra.ExecutionPayloadHeader).depositReceiptsRoot = ssz.electra.DepositReceipts.hashTreeRoot((payload as electra.ExecutionPayload).depositReceipts); + (bellatrixPayloadFields as electra.ExecutionPayloadHeader).exitsRoot = ssz.electra.ExecutionLayerExits.hashTreeRoot( + (payload as electra.ExecutionPayload).exits + ); } return bellatrixPayloadFields; diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index b347b20df9c7..7cc7e02b6583 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -5,6 +5,7 @@ import { EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD, + MAX_EXECUTION_LAYER_EXITS, } from "@lodestar/params"; import {ssz as primitiveSsz} from "../primitive/index.js"; import {ssz as phase0Ssz} from "../phase0/index.js"; @@ -13,7 +14,8 @@ import {ssz as bellatrixSsz} from "../bellatrix/index.js"; import {ssz as capellaSsz} from "../capella/index.js"; import {ssz as denebSsz} from "../deneb/index.js"; -const {UintNum64, Slot, Root, BLSSignature, UintBn256, Bytes32, BLSPubkey, DepositIndex, UintBn64} = primitiveSsz; +const {UintNum64, Slot, Root, BLSSignature, UintBn256, Bytes32, BLSPubkey, DepositIndex, UintBn64, ExecutionAddress} = + primitiveSsz; export const DepositReceipt = new ContainerType( { @@ -26,12 +28,22 @@ export const DepositReceipt = new ContainerType( {typeName: "DepositReceipt", jsonCase: "eth2"} ); -export const DepositReceipts = new ListCompositeType(DepositReceipt, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD); +export const DepositReceipts = new ListCompositeType(DepositReceipt, MAX_EXECUTION_LAYER_EXITS); + +export const ExecutionLayerExit = new ContainerType( + { + sourceAddress: ExecutionAddress, + validatorPubkey: BLSPubkey, + }, + {typeName: "ExecutionLayerExit", jsonCase: "eth2"} +); +export const ExecutionLayerExits = new ListCompositeType(ExecutionLayerExit, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD); export const ExecutionPayload = new ContainerType( { ...denebSsz.ExecutionPayload.fields, depositReceipts: DepositReceipts, // New in ELECTRA + exits: ExecutionLayerExits, // New in ELECTRA }, {typeName: "ExecutionPayload", jsonCase: "eth2"} ); @@ -40,6 +52,7 @@ export const ExecutionPayloadHeader = new ContainerType( { ...denebSsz.ExecutionPayloadHeader.fields, depositReceiptsRoot: Root, // New in ELECTRA + exitsRoot: Root, // New in ELECTRA }, {typeName: "ExecutionPayloadHeader", jsonCase: "eth2"} ); diff --git a/packages/types/src/electra/types.ts b/packages/types/src/electra/types.ts index 3286c10a0334..1b9b42217b8c 100644 --- a/packages/types/src/electra/types.ts +++ b/packages/types/src/electra/types.ts @@ -4,6 +4,9 @@ import * as ssz from "./sszTypes.js"; export type DepositReceipt = ValueOf; export type DepositReceipts = ValueOf; +export type ExecutionLayerExit = ValueOf; +export type ExecutionLayerExits = ValueOf; + export type ExecutionPayload = ValueOf; export type ExecutionPayloadHeader = ValueOf; diff --git a/packages/validator/src/util/params.ts b/packages/validator/src/util/params.ts index 298707c07303..ca1b36883a90 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -224,5 +224,6 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record