Skip to content

Commit

Permalink
feat: implement execution layer exits eip 7002 (#6651)
Browse files Browse the repository at this point in the history
* feat: implement execution layer exits eip 7002

* lint and tsc fix

* apply feedback

* improve comment
  • Loading branch information
g11tech authored and philknows committed Sep 3, 2024
1 parent 9aae8ac commit d181a5c
Show file tree
Hide file tree
Showing 22 changed files with 152 additions and 26 deletions.
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 @@ -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 = {
Expand All @@ -154,6 +156,7 @@ export type ExecutionPayloadRpc = {
excessBlobGas?: QUANTITY; // DENEB
parentBeaconBlockRoot?: QUANTITY; // DENEB
depositReceipts?: DepositReceiptRpc[]; // ELECTRA
exits?: ExecutionLayerExitRpc[]; // ELECTRA
};

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

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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),
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/beacon-node/test/sim/electra-interop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions packages/beacon-node/test/unit/chain/shufflingCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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();
});
Expand All @@ -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);
});
Expand Down
4 changes: 4 additions & 0 deletions packages/beacon-node/test/unit/executionEngine/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ describe("ExecutionEngine / http", () => {
},
],
depositReceipts: null, // depositReceipts is null pre-electra
exits: null,
},
null, // null returned for missing blocks
{
Expand All @@ -203,6 +204,7 @@ describe("ExecutionEngine / http", () => {
],
withdrawals: null, // withdrawals is null pre-capella
depositReceipts: null, // depositReceipts is null pre-electra
exits: null,
},
],
};
Expand Down Expand Up @@ -251,6 +253,7 @@ describe("ExecutionEngine / http", () => {
},
],
depositReceipts: null, // depositReceipts is null pre-electra
exits: null,
},
null, // null returned for missing blocks
{
Expand All @@ -260,6 +263,7 @@ describe("ExecutionEngine / http", () => {
],
withdrawals: null, // withdrawals is null pre-capella
depositReceipts: null, // depositReceipts is null pre-electra
exits: null,
},
],
};
Expand Down
4 changes: 2 additions & 2 deletions packages/beacon-node/test/utils/validationData/attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
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 @@ -117,6 +117,8 @@ export function upgradeLightClientHeader(
case ForkName.electra:
(upgradedHeader as LightClientHeader<ForkName.electra>).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 @@ -154,7 +156,10 @@ export function isValidLightClientHeader(config: ChainForkConfig, header: LightC
}

if (epoch < config.ELECTRA_FORK_EPOCH) {
if ((header as LightClientHeader<ForkName.electra>).execution.depositReceiptsRoot !== undefined) {
if (
(header as LightClientHeader<ForkName.electra>).execution.depositReceiptsRoot !== undefined ||
(header as LightClientHeader<ForkName.electra>).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
56 changes: 56 additions & 0 deletions packages/state-transition/src/block/processExecutionLayerExit.ts
Original file line number Diff line number Diff line change
@@ -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<typeof ssz.phase0.Validator> | 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;
}
}
Loading

0 comments on commit d181a5c

Please sign in to comment.