From bcd3c5249443b85e7f2685f1f913d8528a46f338 Mon Sep 17 00:00:00 2001 From: g11tech Date: Mon, 22 Apr 2024 19:18:35 +0530 Subject: [PATCH] feat: implement execution layer exits eip 7002 (#6651) * feat: implement execution layer exits eip 7002 * lint and tsc fix * apply feedback * improve comment --- .../src/execution/engine/payloadIdCache.ts | 5 ++ .../beacon-node/src/execution/engine/types.ts | 48 +++++++++++----- .../opPools/aggregatedAttestationPool.test.ts | 2 +- .../test/perf/chain/opPools/opPool.test.ts | 2 +- .../produceBlock/produceBlockBody.test.ts | 2 +- .../test/sim/electra-interop.test.ts | 1 + .../opPools/aggregatedAttestationPool.test.ts | 4 +- .../test/unit/chain/shufflingCache.test.ts | 6 +- .../test/unit/executionEngine/http.test.ts | 4 ++ .../test/utils/validationData/attestation.ts | 4 +- 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 | 56 +++++++++++++++++++ .../src/block/processOperations.ts | 8 +++ .../src/slot/upgradeStateToElectra.ts | 2 +- .../state-transition/src/util/execution.ts | 3 + packages/types/src/electra/sszTypes.ts | 15 ++++- packages/types/src/electra/types.ts | 3 + packages/validator/src/util/params.ts | 1 + 22 files changed, 152 insertions(+), 26 deletions(-) create mode 100644 packages/state-transition/src/block/processExecutionLayerExit.ts 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 999c77f86b9c..f12f98e1a0f7 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 */ @@ -126,12 +126,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 = { @@ -154,6 +156,7 @@ export type ExecutionPayloadRpc = { excessBlobGas?: QUANTITY; // DENEB parentBeaconBlockRoot?: QUANTITY; // DENEB depositReceipts?: DepositReceiptRpc[]; // ELECTRA + exits?: ExecutionLayerExitRpc[]; // ELECTRA }; export type WithdrawalRpc = { @@ -163,13 +166,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[]; @@ -235,8 +233,9 @@ export function serializeExecutionPayload(fork: ForkName, data: ExecutionPayload // 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; @@ -325,14 +324,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}; @@ -411,7 +417,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), @@ -421,12 +427,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; } @@ -437,6 +458,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/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts b/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts index 7a882b03af0e..ee14eb27b51d 100644 --- a/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts @@ -32,7 +32,7 @@ describe(`getAttestationsForBlock vc=${vc}`, () => { before(function () { this.timeout(5 * 60 * 1000); // Generating the states for the first time is very slow - originalState = generatePerfTestCachedStateAltair({goBackOneSlot: true, vc}) as unknown as CachedBeaconStateAltair; + originalState = generatePerfTestCachedStateAltair({goBackOneSlot: true, vc}) as CachedBeaconStateAltair; const {blockHeader, checkpoint} = computeAnchorCheckpoint(originalState.config, originalState); // TODO figure out why getBlockRootAtSlot(originalState, justifiedSlot) is not the same to justifiedCheckpoint.root diff --git a/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts b/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts index 7998a204f09d..2632c593e78c 100644 --- a/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts +++ b/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts @@ -24,7 +24,7 @@ describe("opPool", () => { before(function () { this.timeout(2 * 60 * 1000); // Generating the states for the first time is very slow - originalState = generatePerfTestCachedStateAltair({goBackOneSlot: true}) as unknown as CachedBeaconStateAltair; + originalState = generatePerfTestCachedStateAltair({goBackOneSlot: true}) as CachedBeaconStateAltair; }); itBench({ diff --git a/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts index 2386f3538205..f90f148f3e01 100644 --- a/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts +++ b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts @@ -25,7 +25,7 @@ describe("produceBlockBody", () => { before(async () => { db = new BeaconDb(config, await LevelDbController.create({name: ".tmpdb"}, {logger})); - state = stateOg.clone() as unknown as CachedBeaconStateAltair; + state = stateOg.clone() as CachedBeaconStateAltair; chain = new BeaconChain( { proposerBoost: true, diff --git a/packages/beacon-node/test/sim/electra-interop.test.ts b/packages/beacon-node/test/sim/electra-interop.test.ts index 428bba3c63ad..ab3f1ae6b2c1 100644 --- a/packages/beacon-node/test/sim/electra-interop.test.ts +++ b/packages/beacon-node/test/sim/electra-interop.test.ts @@ -173,6 +173,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { blockHash: dataToBytes(newPayloadBlockHash, 32), receiptsRoot: dataToBytes("0x79ee3424eb720a3ad4b1c5a372bb8160580cbe4d893778660f34213c685627a9", 32), blobGasUsed: 0n, + exits: [], }; const parentBeaconBlockRoot = dataToBytes("0x0000000000000000000000000000000000000000000000000000000000000000", 32); const payloadResult = await executionEngine.notifyNewPayload( diff --git a/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts b/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts index 41c205cdd0e9..800984fa84bc 100644 --- a/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts @@ -61,9 +61,9 @@ describe("AggregatedAttestationPool", function () { epochParticipation[committee[i]] = 0b000; } } - (originalState as unknown as CachedBeaconStateAltair).previousEpochParticipation = + (originalState as CachedBeaconStateAltair).previousEpochParticipation = ssz.altair.EpochParticipation.toViewDU(epochParticipation); - (originalState as unknown as CachedBeaconStateAltair).currentEpochParticipation = + (originalState as CachedBeaconStateAltair).currentEpochParticipation = ssz.altair.EpochParticipation.toViewDU(epochParticipation); originalState.commit(); let altairState: CachedBeaconStateAllForks; diff --git a/packages/beacon-node/test/unit/chain/shufflingCache.test.ts b/packages/beacon-node/test/unit/chain/shufflingCache.test.ts index e6a7e8706bbe..035746438563 100644 --- a/packages/beacon-node/test/unit/chain/shufflingCache.test.ts +++ b/packages/beacon-node/test/unit/chain/shufflingCache.test.ts @@ -14,7 +14,7 @@ describe("ShufflingCache", function () { beforeEach(() => { shufflingCache = new ShufflingCache(null, {maxShufflingCacheEpochs: 1}); - shufflingCache.processState(state as unknown as CachedBeaconStateAllForks, currentEpoch); + shufflingCache.processState(state as CachedBeaconStateAllForks, currentEpoch); }); it("should get shuffling from cache", async function () { @@ -29,7 +29,7 @@ describe("ShufflingCache", function () { shufflingCache.insertPromise(currentEpoch, "0x00"); expect(await shufflingCache.get(currentEpoch, decisionRoot)).toEqual(state.epochCtx.currentShuffling); // insert shufflings at other epochs does prune the cache - shufflingCache.processState(state as unknown as CachedBeaconStateAllForks, currentEpoch + 1); + shufflingCache.processState(state as CachedBeaconStateAllForks, currentEpoch + 1); // the current shuffling is not available anymore expect(await shufflingCache.get(currentEpoch, decisionRoot)).toBeNull(); }); @@ -39,7 +39,7 @@ describe("ShufflingCache", function () { shufflingCache.insertPromise(currentEpoch + 1, nextDecisionRoot); const shufflingRequest0 = shufflingCache.get(currentEpoch + 1, nextDecisionRoot); const shufflingRequest1 = shufflingCache.get(currentEpoch + 1, nextDecisionRoot); - shufflingCache.processState(state as unknown as CachedBeaconStateAllForks, currentEpoch + 1); + shufflingCache.processState(state as CachedBeaconStateAllForks, currentEpoch + 1); expect(await shufflingRequest0).toEqual(state.epochCtx.nextShuffling); expect(await shufflingRequest1).toEqual(state.epochCtx.nextShuffling); }); diff --git a/packages/beacon-node/test/unit/executionEngine/http.test.ts b/packages/beacon-node/test/unit/executionEngine/http.test.ts index b0e20ad1f2b5..d58eefa10368 100644 --- a/packages/beacon-node/test/unit/executionEngine/http.test.ts +++ b/packages/beacon-node/test/unit/executionEngine/http.test.ts @@ -194,6 +194,7 @@ describe("ExecutionEngine / http", () => { }, ], depositReceipts: null, // depositReceipts is null pre-electra + exits: null, }, null, // null returned for missing blocks { @@ -203,6 +204,7 @@ describe("ExecutionEngine / http", () => { ], withdrawals: null, // withdrawals is null pre-capella depositReceipts: null, // depositReceipts is null pre-electra + exits: null, }, ], }; @@ -251,6 +253,7 @@ describe("ExecutionEngine / http", () => { }, ], depositReceipts: null, // depositReceipts is null pre-electra + exits: null, }, null, // null returned for missing blocks { @@ -260,6 +263,7 @@ describe("ExecutionEngine / http", () => { ], withdrawals: null, // withdrawals is null pre-capella depositReceipts: null, // depositReceipts is null pre-electra + exits: null, }, ], }; diff --git a/packages/beacon-node/test/utils/validationData/attestation.ts b/packages/beacon-node/test/utils/validationData/attestation.ts index 335ec81870c6..82bdca901889 100644 --- a/packages/beacon-node/test/utils/validationData/attestation.ts +++ b/packages/beacon-node/test/utils/validationData/attestation.ts @@ -83,8 +83,8 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { }; const shufflingCache = new ShufflingCache(); - shufflingCache.processState(state as unknown as CachedBeaconStateAllForks, state.epochCtx.currentShuffling.epoch); - shufflingCache.processState(state as unknown as CachedBeaconStateAllForks, state.epochCtx.nextShuffling.epoch); + shufflingCache.processState(state as CachedBeaconStateAllForks, state.epochCtx.currentShuffling.epoch); + shufflingCache.processState(state as CachedBeaconStateAllForks, state.epochCtx.nextShuffling.epoch); const dependentRoot = getShufflingDecisionBlock(state, state.epochCtx.currentShuffling.epoch); const forkChoice = { diff --git a/packages/light-client/src/spec/utils.ts b/packages/light-client/src/spec/utils.ts index 152c55ac6d15..8e6b3456a600 100644 --- a/packages/light-client/src/spec/utils.ts +++ b/packages/light-client/src/spec/utils.ts @@ -117,6 +117,8 @@ export function upgradeLightClientHeader( case ForkName.electra: (upgradedHeader as 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; @@ -154,7 +156,10 @@ export function isValidLightClientHeader(config: ChainForkConfig, header: LightC } if (epoch < config.ELECTRA_FORK_EPOCH) { - if ((header as LightClientHeader).execution.depositReceiptsRoot !== undefined) { + if ( + (header as LightClientHeader).execution.depositReceiptsRoot !== undefined || + (header as 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..5068d9af8667 --- /dev/null +++ b/packages/state-transition/src/block/processExecutionLayerExit.ts @@ -0,0 +1,56 @@ +import {CompositeViewDU} from "@chainsafe/ssz"; +import {electra, ssz} 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"; + +/** + * Process execution layer exit messages and initiate exit incase they belong to a valid active validator + * otherwise silent ignore. + */ +export function processExecutionLayerExit(state: CachedBeaconStateElectra, exit: electra.ExecutionLayerExit): void { + const validator = isValidExecutionLayerExit(state, exit); + if (validator === null) { + return; + } + + initiateValidatorExit(state, validator); +} + +export function isValidExecutionLayerExit( + state: CachedBeaconStateElectra, + exit: electra.ExecutionLayerExit +): CompositeViewDU | null { + const {config, epochCtx} = state; + const validatorIndex = epochCtx.getValidatorIndex(exit.validatorPubkey); + const validator = validatorIndex !== undefined ? state.validators.getReadonly(validatorIndex) : undefined; + if (validator === undefined) { + return null; + } + + const {withdrawalCredentials} = validator; + if (withdrawalCredentials[0] !== ETH1_ADDRESS_WITHDRAWAL_PREFIX) { + return null; + } + + const executionAddress = withdrawalCredentials.subarray(12, 32); + if (Buffer.compare(executionAddress, exit.sourceAddress) !== 0) { + return null; + } + + const currentEpoch = epochCtx.epoch; + if ( + // 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 + ) { + return validator; + } else { + return null; + } +} diff --git a/packages/state-transition/src/block/processOperations.ts b/packages/state-transition/src/block/processOperations.ts index 7b8b39efe743..c4879da4aa71 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 d9a88e5475b5..2975b84bf747 100644 --- a/packages/state-transition/src/util/execution.ts +++ b/packages/state-transition/src/util/execution.ts @@ -174,6 +174,9 @@ export function executionPayloadToPayloadHeader(fork: ForkSeq, payload: Executio 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..7b5ed51fb786 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( { @@ -28,10 +30,20 @@ export const DepositReceipt = new ContainerType( export const DepositReceipts = new ListCompositeType(DepositReceipt, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD); +export const ExecutionLayerExit = new ContainerType( + { + sourceAddress: ExecutionAddress, + validatorPubkey: BLSPubkey, + }, + {typeName: "ExecutionLayerExit", jsonCase: "eth2"} +); +export const ExecutionLayerExits = new ListCompositeType(ExecutionLayerExit, MAX_EXECUTION_LAYER_EXITS); + 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