Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement execution layer exits eip 7002 #6651

Merged
merged 4 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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(
{
proposerBoostEnabled: 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 @@ -62,9 +62,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 @@ -189,6 +189,7 @@ describe("ExecutionEngine / http", () => {
},
],
depositReceipts: null, // depositReceipts is null pre-electra
exits: null,
},
null, // null returned for missing blocks
{
Expand All @@ -198,6 +199,7 @@ describe("ExecutionEngine / http", () => {
],
withdrawals: null, // withdrawals is null pre-capella
depositReceipts: null, // depositReceipts is null pre-electra
exits: null,
},
],
};
Expand Down Expand Up @@ -246,6 +248,7 @@ describe("ExecutionEngine / http", () => {
},
],
depositReceipts: null, // depositReceipts is null pre-electra
exits: null,
},
null, // null returned for missing blocks
{
Expand All @@ -255,6 +258,7 @@ describe("ExecutionEngine / http", () => {
],
withdrawals: null, // withdrawals is null pre-capella
depositReceipts: null, // depositReceipts is null pre-electra
exits: null,
},
],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,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 @@ -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
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 {
g11tech marked this conversation as resolved.
Show resolved Hide resolved
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
Loading