Skip to content

Commit

Permalink
feat: implement execution layer exits eip 7002
Browse files Browse the repository at this point in the history
  • Loading branch information
g11tech committed Apr 11, 2024
1 parent 473a04f commit 3a68ce9
Show file tree
Hide file tree
Showing 15 changed files with 140 additions and 25 deletions.
16 changes: 8 additions & 8 deletions packages/beacon-node/src/execution/engine/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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]
Expand Down
5 changes: 5 additions & 0 deletions packages/beacon-node/src/execution/engine/payloadIdCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export type DepositReceiptV1 = {
index: QUANTITY;
};

export type ExecutionLayerExitV1 = {
sourceAddress: DATA;
validatorPubkey: DATA;
};

type FcuAttributes = {headBlockHash: DATA; finalizedBlockHash: DATA} & Omit<PayloadAttributesRpc, "withdrawals">;

export class PayloadIdCache {
Expand Down
48 changes: 35 additions & 13 deletions packages/beacon-node/src/execution/engine/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down Expand Up @@ -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 = {
Expand All @@ -147,6 +149,7 @@ export type ExecutionPayloadRpc = {
excessBlobGas?: QUANTITY; // DENEB
parentBeaconBlockRoot?: QUANTITY; // DENEB
depositReceipts?: DepositReceiptRpc[]; // ELECTRA
exits?: ExecutionLayerExitRpc[]; // ELECTRA
};

export type WithdrawalRpc = {
Expand All @@ -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[];

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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),
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/light-client/src/spec/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/params/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const {
KZG_COMMITMENT_INCLUSION_PROOF_DEPTH,

MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD,
MAX_EXECUTION_LAYER_EXITS,
} = activePreset;

////////////
Expand Down
1 change: 1 addition & 0 deletions packages/params/src/presets/mainnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,5 @@ export const mainnetPreset: BeaconPreset = {

// ELECTRA
MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 8192,
MAX_EXECUTION_LAYER_EXITS: 16,
};
1 change: 1 addition & 0 deletions packages/params/src/presets/minimal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,5 @@ export const minimalPreset: BeaconPreset = {

// ELECTRA
MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 4,
MAX_EXECUTION_LAYER_EXITS: 16,
};
2 changes: 2 additions & 0 deletions packages/params/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export type BeaconPreset = {

// ELECTRA
MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: number;
MAX_EXECUTION_LAYER_EXITS: number;
};

/**
Expand Down Expand Up @@ -173,6 +174,7 @@ export const beaconPresetTypes: BeaconPresetTypes = {

// ELECTRA
MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: "number",
MAX_EXECUTION_LAYER_EXITS: "number",
};

type BeaconPresetTypes = {
Expand Down
50 changes: 50 additions & 0 deletions packages/state-transition/src/block/processExecutionLayerExit.ts
Original file line number Diff line number Diff line change
@@ -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
);
}
8 changes: 8 additions & 0 deletions packages/state-transition/src/block/processOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -18,6 +19,7 @@ export {
processAttestations,
processDeposit,
processVoluntaryExit,
processExecutionLayerExit,
processBlsToExecutionChange,
processDepositReceipt,
};
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions packages/state-transition/src/util/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 15 additions & 2 deletions packages/types/src/electra/sszTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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(
{
Expand All @@ -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"}
);
Expand All @@ -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"}
);
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/electra/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import * as ssz from "./sszTypes.js";
export type DepositReceipt = ValueOf<typeof ssz.DepositReceipt>;
export type DepositReceipts = ValueOf<typeof ssz.DepositReceipts>;

export type ExecutionLayerExit = ValueOf<typeof ssz.ExecutionLayerExit>;
export type ExecutionLayerExits = ValueOf<typeof ssz.ExecutionLayerExits>;

export type ExecutionPayload = ValueOf<typeof ssz.ExecutionPayload>;
export type ExecutionPayloadHeader = ValueOf<typeof ssz.ExecutionPayloadHeader>;

Expand Down
1 change: 1 addition & 0 deletions packages/validator/src/util/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,5 +224,6 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record<keyof ConfigWit

// ELECTRA
MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: electraForkRelevant,
MAX_EXECUTION_LAYER_EXITS: electraForkRelevant,
};
}

0 comments on commit 3a68ce9

Please sign in to comment.